From bdd1f8e24f57e6362fea810d0b762972226cb0c7 Mon Sep 17 00:00:00 2001 From: blais Date: Sat, 27 Jul 2024 22:34:10 -0400 Subject: [PATCH] Deployed ea2252b with MkDocs version: 1.2.4 --- 404.html | 12 +- ...rison_of_beancount_and_ledger_hledger.html | 12 +- ...r_an_improvement_on_inventory_booking.html | 12 +- api_reference/beancount.core.html | 12140 ++++++---- api_reference/beancount.ingest.html | 7567 ------ api_reference/beancount.loader.html | 956 +- api_reference/beancount.ops.html | 4873 ++-- api_reference/beancount.parser.html | 8759 +++---- api_reference/beancount.plugins.html | 7809 ++---- api_reference/beancount.prices.html | 4117 ---- api_reference/beancount.query.html | 20171 ---------------- api_reference/beancount.reports.html | 10386 -------- api_reference/beancount.scripts.html | 9013 +++---- api_reference/beancount.tools.html | 549 +- api_reference/beancount.utils.html | 7654 +++--- api_reference/beancount.web.html | 5209 ---- api_reference/index.html | 17 +- balance_assertions_in_beancount.html | 12 +- beancount_cheat_sheet.html | 12 +- beancount_design_doc.html | 16 +- ...3922268f9fe3d624c80b311804a81e4990ae07.png | Bin 12355 -> 0 bytes ...bdc0339d6207eab9d578bc1fc954db96ed97d.png} | Bin 20169 -> 20170 bytes ...3adf6a5ea98d821f74996315976389bb1683dd.png | Bin 0 -> 12359 bytes beancount_history_and_credits.html | 12 +- beancount_language_syntax.html | 12 +- beancount_options_reference.html | 12 +- beancount_query_language.html | 12 +- beancount_scripting_plugins.html | 16 +- beancount_v3.html | 12 +- beancount_v3_dependencies.html | 12 +- beangulp.html | 12 +- calculating_portolio_returns.html | 18 +- ...4d457bc907c750277ca40745ed1686f7d9017a.png | Bin 0 -> 36742 bytes ...7ab6a50f230c54b1821653b48792a8a1e3a451.png | Bin 36733 -> 0 bytes command_line_accounting_cookbook.html | 12 +- command_line_accounting_in_context.html | 12 +- exporting_your_portfolio.html | 12 +- external_contributions.html | 13 +- fetching_prices_in_beancount.html | 12 +- fund_accounting_with_beancount.html | 12 +- getting_started_with_beancount.html | 12 +- health_care_expenses.html | 12 +- how_inventories_work.html | 12 +- how_we_share_expenses.html | 14 +- ...9a53a2976ca14d122483a7f69e419afe2609c0.png | Bin 0 -> 41785 bytes ...ea06bbfc314c84ad8172b5b42f72fdb5ee8d67.png | Bin 41782 -> 0 bytes importing_external_data.html | 12 +- index.html | 14 +- installing_beancount.html | 263 +- installing_beancount_v3.html | 12 +- ledgerhub_design_doc.html | 12 +- objects.inv | Bin 12918 -> 6723 bytes precision_tolerances.html | 12 +- rounding_precision_in_beancount.html | 12 +- running_beancount_and_generating_reports.html | 12 +- search.html | 12 +- search/search_index.json | 2 +- settlement_dates_in_beancount.html | 12 +- sharing_expenses_with_beancount.html | 20 +- ...2c6d11d6e252da29b6947bcd9d65f56d08370e.png | Bin 0 -> 8597 bytes ...45addecaf77f45c6cc5867529bfaed232e45f1.png | Bin 8644 -> 0 bytes ...e501649abf9acccf147ac47836b82a2d07b8f9.png | Bin 0 -> 8645 bytes ...97688e752bd90be8d7257afa678cc1fd1d8084.png | Bin 8596 -> 0 bytes ...5d3349c7af8500347aa0e5af656e943abe2591.png | Bin 7644 -> 0 bytes ...f0e5ed8b0be4c152e147c3f41ee833bb11cc5c.png | Bin 0 -> 11592 bytes ...97ff1fb4ea70efa1164dca215811e00b4a06d0.png | Bin 11583 -> 0 bytes ...418073c42793ed5a2bdbc69f415ad2d0a1ff08.png | Bin 0 -> 7646 bytes sitemap.xml | 121 +- sitemap.xml.gz | Bin 795 -> 769 bytes stock_vesting_in_beancount.html | 12 +- the_double_entry_counting_method.html | 12 +- tracking_medical_claims.html | 47 +- trading_with_beancount.html | 12 +- tutorial_example.html | 12 +- 74 files changed, 23811 insertions(+), 76337 deletions(-) delete mode 100644 api_reference/beancount.ingest.html delete mode 100644 api_reference/beancount.prices.html delete mode 100644 api_reference/beancount.query.html delete mode 100644 api_reference/beancount.reports.html delete mode 100644 api_reference/beancount.web.html delete mode 100644 beancount_design_doc/media/033922268f9fe3d624c80b311804a81e4990ae07.png rename beancount_design_doc/media/{4117596158f0642dae7becf17be7a89f169bf4a0.png => 6bbbdc0339d6207eab9d578bc1fc954db96ed97d.png} (76%) create mode 100644 beancount_design_doc/media/973adf6a5ea98d821f74996315976389bb1683dd.png create mode 100644 calculating_portolio_returns/media/4f4d457bc907c750277ca40745ed1686f7d9017a.png delete mode 100644 calculating_portolio_returns/media/5a7ab6a50f230c54b1821653b48792a8a1e3a451.png create mode 100644 how_we_share_expenses/media/859a53a2976ca14d122483a7f69e419afe2609c0.png delete mode 100644 how_we_share_expenses/media/beea06bbfc314c84ad8172b5b42f72fdb5ee8d67.png create mode 100644 sharing_expenses_with_beancount/media/132c6d11d6e252da29b6947bcd9d65f56d08370e.png delete mode 100644 sharing_expenses_with_beancount/media/1c45addecaf77f45c6cc5867529bfaed232e45f1.png create mode 100644 sharing_expenses_with_beancount/media/51e501649abf9acccf147ac47836b82a2d07b8f9.png delete mode 100644 sharing_expenses_with_beancount/media/6497688e752bd90be8d7257afa678cc1fd1d8084.png delete mode 100644 sharing_expenses_with_beancount/media/905d3349c7af8500347aa0e5af656e943abe2591.png create mode 100644 sharing_expenses_with_beancount/media/98f0e5ed8b0be4c152e147c3f41ee833bb11cc5c.png delete mode 100644 sharing_expenses_with_beancount/media/e897ff1fb4ea70efa1164dca215811e00b4a06d0.png create mode 100644 sharing_expenses_with_beancount/media/e9418073c42793ed5a2bdbc69f415ad2d0a1ff08.png diff --git a/404.html b/404.html index b52e4349..e8b109db 100644 --- a/404.html +++ b/404.html @@ -98,6 +98,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -145,8 +147,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -155,20 +155,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/a_comparison_of_beancount_and_ledger_hledger.html b/a_comparison_of_beancount_and_ledger_hledger.html index 9421a7fc..e17d7767 100644 --- a/a_comparison_of_beancount_and_ledger_hledger.html +++ b/a_comparison_of_beancount_and_ledger_hledger.html @@ -159,6 +159,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -206,8 +208,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -216,20 +216,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/a_proposal_for_an_improvement_on_inventory_booking.html b/a_proposal_for_an_improvement_on_inventory_booking.html index b9ef66d4..5463491b 100644 --- a/a_proposal_for_an_improvement_on_inventory_booking.html +++ b/a_proposal_for_an_improvement_on_inventory_booking.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -250,8 +252,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -260,20 +260,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/api_reference/beancount.core.html b/api_reference/beancount.core.html index 33035e44..17260b06 100644 --- a/api_reference/beancount.core.html +++ b/api_reference/beancount.core.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -216,6 +218,8 @@
  • is_income_statement_account()
  • +
  • is_inverted_account() +
  • is_root_account()
  • @@ -448,6 +452,34 @@ +
  • dtypes + +
  • create_simple_posting()
  • create_simple_posting_with_cost() @@ -464,8 +496,6 @@
  • iter_entry_dates()
  • -
  • new_directive() -
  • new_metadata()
  • posting_has_conversion() @@ -495,6 +525,8 @@
  • set_commas()
  • update() +
  • +
  • update_from()
  • @@ -517,6 +549,8 @@
  • mode()
  • update() +
  • +
  • update_from()
  • @@ -574,7 +608,7 @@
  • get_all_tags()
  • -
  • get_commodity_map() +
  • get_commodity_directives()
  • get_dict_accounts()
  • @@ -622,8 +656,6 @@
  • inventory
  • - -
  • beancount.ingest
  • beancount.loader
  • @@ -854,20 +896,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • @@ -966,6 +1000,7 @@

    +
    @@ -1020,7 +1055,7 @@

    -beancount.core.account.AccountTransformer.parse(self, transformed_name) +beancount.core.account.AccountTransformer.parse(self, transformed_name)

    @@ -1031,12 +1066,14 @@

    Source code in beancount/core/account.py -
    def parse(self, transformed_name):
    -    "Convert the transform account name to an account name."
    -    return (transformed_name
    -            if self.rsep is None
    -            else transformed_name.replace(self.rsep, sep))
    -
    +
    def parse(self, transformed_name: str) -> Account:
    +    "Convert the transform account name to an account name."
    +    return (
    +        transformed_name
    +        if self.rsep is None
    +        else transformed_name.replace(self.rsep, sep)
    +    )
    +

    @@ -1049,7 +1086,7 @@

    -beancount.core.account.AccountTransformer.render(self, account_name) +beancount.core.account.AccountTransformer.render(self, account_name)

    @@ -1060,12 +1097,10 @@

    Source code in beancount/core/account.py -
    def render(self, account_name):
    -    "Convert the account name to a transformed account name."
    -    return (account_name
    -            if self.rsep is None
    -            else account_name.replace(sep, self.rsep))
    -
    +
    def render(self, account_name: Account) -> str:
    +    "Convert the account name to a transformed account name."
    +    return account_name if self.rsep is None else account_name.replace(sep, self.rsep)
    +
    @@ -1089,7 +1124,7 @@

    -beancount.core.account.commonprefix(accounts) +beancount.core.account.commonprefix(accounts)

    @@ -1108,7 +1143,7 @@

    Parameters: @@ -1124,7 +1159,7 @@

    Returns: @@ -1132,22 +1167,21 @@

    Source code in beancount/core/account.py -
    def commonprefix(accounts):
    -    """Return the common prefix of a list of account names.
    -
    -    Args:
    -      accounts: A sequence of account name strings.
    -    Returns:
    -      A string, the common parent account. If none, returns an empty string.
    -    """
    -    accounts_lists = [account_.split(sep)
    -                      for account_ in accounts]
    -    # Note: the os.path.commonprefix() function just happens to work here.
    -    # Inspect its code, and even the special case of no common prefix
    -    # works well with str.join() below.
    -    common_list = path.commonprefix(accounts_lists)
    -    return sep.join(common_list)
    -
    +
    def commonprefix(accounts: Iterable[Account]) -> Account:
    +    """Return the common prefix of a list of account names.
    +
    +    Args:
    +      accounts: A sequence of account name strings.
    +    Returns:
    +      A string, the common parent account. If none, returns an empty string.
    +    """
    +    accounts_lists = [account_.split(sep) for account_ in accounts]
    +    # Note: the os.path.commonprefix() function just happens to work here.
    +    # Inspect its code, and even the special case of no common prefix
    +    # works well with str.join() below.
    +    common_list = path.commonprefix(accounts_lists)
    +    return sep.join(common_list)
    +
    @@ -1160,7 +1194,7 @@

    -beancount.core.account.has_component(account_name, component) +beancount.core.account.has_component(account_name, component)

    @@ -1179,8 +1213,8 @@

    Parameters: @@ -1206,19 +1240,19 @@

    Source code in beancount/core/account.py -
    def has_component(account_name, component):
    -    """Return true if one of the account contains a given component.
    -
    -    Args:
    -      account_name: A string, an account name.
    -      component: A string, a component of an account name. For instance,
    -        ``Food`` in ``Expenses:Food:Restaurant``. All components are considered.
    -    Returns:
    -      Boolean: true if the component is in the account. Note that a component
    -      name must be whole, that is ``NY`` is not in ``Expenses:Taxes:StateNY``.
    -    """
    -    return bool(re.search('(^|:){}(:|$)'.format(component), account_name))
    -
    +
    def has_component(account_name: Account, component: str) -> bool:
    +    """Return true if one of the account contains a given component.
    +
    +    Args:
    +      account_name: A string, an account name.
    +      component: A string, a component of an account name. For instance,
    +        ``Food`` in ``Expenses:Food:Restaurant``. All components are considered.
    +    Returns:
    +      Boolean: true if the component is in the account. Note that a component
    +      name must be whole, that is ``NY`` is not in ``Expenses:Taxes:StateNY``.
    +    """
    +    return bool(re.search("(^|:){}(:|$)".format(component), account_name))
    +
    @@ -1231,7 +1265,7 @@

    -beancount.core.account.is_valid(string) +beancount.core.account.is_valid(string)

    @@ -1251,7 +1285,7 @@

    Parameters: @@ -1267,7 +1301,7 @@

    Returns: @@ -1275,18 +1309,17 @@

    Source code in beancount/core/account.py -
    def is_valid(string):
    -    """Return true if the given string is a valid account name.
    -    This does not check for the root account types, just the general syntax.
    -
    -    Args:
    -      string: A string, to be checked for account name pattern.
    -    Returns:
    -      A boolean, true if the string has the form of an account's name.
    -    """
    -    return (isinstance(string, str) and
    -            bool(re.match('{}$'.format(ACCOUNT_RE), string)))
    -
    +
    def is_valid(string: Account) -> bool:
    +    """Return true if the given string is a valid account name.
    +    This does not check for the root account types, just the general syntax.
    +
    +    Args:
    +      string: A string, to be checked for account name pattern.
    +    Returns:
    +      A boolean, true if the string has the form of an account's name.
    +    """
    +    return isinstance(string, str) and bool(regex.match("{}$".format(ACCOUNT_RE), string))
    +
    @@ -1299,7 +1332,7 @@

    -beancount.core.account.join(*components) +beancount.core.account.join(*components)

    @@ -1318,7 +1351,7 @@

    Parameters: @@ -1334,7 +1367,7 @@

    Returns: @@ -1342,16 +1375,16 @@

    Source code in beancount/core/account.py -
    def join(*components):
    -    """Join the names with the account separator.
    -
    -    Args:
    -      *components: Strings, the components of an account name.
    -    Returns:
    -      A string, joined in a single account name.
    -    """
    -    return sep.join(components)
    -
    +
    def join(*components: Tuple[str]) -> Account:
    +    """Join the names with the account separator.
    +
    +    Args:
    +      *components: Strings, the components of an account name.
    +    Returns:
    +      A string, joined in a single account name.
    +    """
    +    return sep.join(components)
    +
    @@ -1364,7 +1397,7 @@

    -beancount.core.account.leaf(account_name) +beancount.core.account.leaf(account_name)

    @@ -1383,7 +1416,7 @@

    Parameters: @@ -1399,7 +1432,7 @@

    Returns: @@ -1407,17 +1440,17 @@

    Source code in beancount/core/account.py -
    def leaf(account_name):
    -    """Get the name of the leaf of this account.
    -
    -    Args:
    -      account_name: A string, the name of the account whose leaf name to return.
    -    Returns:
    -      A string, the name of the leaf of the account.
    -    """
    -    assert isinstance(account_name, str)
    -    return account_name.split(sep)[-1] if account_name else None
    -
    +
    def leaf(account_name: Account) -> Account:
    +    """Get the name of the leaf of this account.
    +
    +    Args:
    +      account_name: A string, the name of the account whose leaf name to return.
    +    Returns:
    +      A string, the name of the leaf of the account.
    +    """
    +    assert isinstance(account_name, str)
    +    return account_name.split(sep)[-1] if account_name else None
    +
    @@ -1430,7 +1463,7 @@

    -beancount.core.account.parent(account_name) +beancount.core.account.parent(account_name)

    @@ -1449,7 +1482,7 @@

    Parameters: @@ -1465,7 +1498,7 @@

    Returns: @@ -1473,21 +1506,21 @@

    Source code in beancount/core/account.py -
    def parent(account_name):
    -    """Return the name of the parent account of the given account.
    -
    -    Args:
    -      account_name: A string, the name of the account whose parent to return.
    -    Returns:
    -      A string, the name of the parent account of this account.
    -    """
    -    assert isinstance(account_name, str), account_name
    -    if not account_name:
    -        return None
    -    components = account_name.split(sep)
    -    components.pop(-1)
    -    return sep.join(components)
    -
    +
    def parent(account_name: Account) -> Account:
    +    """Return the name of the parent account of the given account.
    +
    +    Args:
    +      account_name: A string, the name of the account whose parent to return.
    +    Returns:
    +      A string, the name of the parent account of this account.
    +    """
    +    assert isinstance(account_name, str), account_name
    +    if not account_name:
    +        return None
    +    components = account_name.split(sep)
    +    components.pop(-1)
    +    return sep.join(components)
    +
    @@ -1500,7 +1533,7 @@

    -beancount.core.account.parent_matcher(account_name) +beancount.core.account.parent_matcher(account_name)

    @@ -1519,7 +1552,7 @@

    Parameters: @@ -1535,7 +1568,7 @@

    Returns: @@ -1544,17 +1577,17 @@

    Source code in beancount/core/account.py -
    def parent_matcher(account_name):
    -    """Build a predicate that returns whether an account is under the given one.
    -
    -    Args:
    -      account_name: The name of the parent account we want to check for.
    -    Returns:
    -      A callable, which, when called, will return true if the given account is a
    -      child of ``account_name``.
    -    """
    -    return re.compile(r'{}($|{})'.format(re.escape(account_name), sep)).match
    -
    +
    def parent_matcher(account_name: Account) -> Callable[[str], Any]:
    +    """Build a predicate that returns whether an account is under the given one.
    +
    +    Args:
    +      account_name: The name of the parent account we want to check for.
    +    Returns:
    +      A callable, which, when called, will return true if the given account is a
    +      child of ``account_name``.
    +    """
    +    return re.compile(r"{}($|{})".format(re.escape(account_name), sep)).match
    +
    @@ -1567,7 +1600,7 @@

    -beancount.core.account.parents(account_name) +beancount.core.account.parents(account_name)

    @@ -1586,7 +1619,7 @@

    Parameters: @@ -1602,7 +1635,7 @@

    Returns: @@ -1610,18 +1643,18 @@

    Source code in beancount/core/account.py -
    def parents(account_name):
    -    """A generator of the names of the parents of this account, including this account.
    -
    -    Args:
    -      account_name: The name of the account we want to start iterating from.
    -    Returns:
    -      A generator of account name strings.
    -    """
    -    while account_name:
    -        yield account_name
    -        account_name = parent(account_name)
    -
    +
    def parents(account_name: Account) -> Iterator[Account]:
    +    """A generator of the names of the parents of this account, including this account.
    +
    +    Args:
    +      account_name: The name of the account we want to start iterating from.
    +    Returns:
    +      A generator of account name strings.
    +    """
    +    while account_name:
    +        yield account_name
    +        account_name = parent(account_name)
    +
    @@ -1634,7 +1667,7 @@

    -beancount.core.account.root(num_components, account_name) +beancount.core.account.root(num_components, account_name)

    @@ -1653,8 +1686,8 @@

    Parameters: @@ -1670,7 +1703,7 @@

    Returns: @@ -1678,17 +1711,17 @@

    Source code in beancount/core/account.py -
    def root(num_components, account_name):
    -    """Return the first few components of an account's name.
    -
    -    Args:
    -      num_components: An integer, the number of components to return.
    -      account_name: A string, an account name.
    -    Returns:
    -      A string, the account root up to 'num_components' components.
    -    """
    -    return join(*(split(account_name)[:num_components]))
    -
    +
    def root(num_components: int, account_name: Account) -> str:
    +    """Return the first few components of an account's name.
    +
    +    Args:
    +      num_components: An integer, the number of components to return.
    +      account_name: A string, an account name.
    +    Returns:
    +      A string, the account root up to 'num_components' components.
    +    """
    +    return join(*(split(account_name)[:num_components]))
    +
    @@ -1701,7 +1734,7 @@

    -beancount.core.account.sans_root(account_name) +beancount.core.account.sans_root(account_name)

    @@ -1721,7 +1754,7 @@

    Parameters: @@ -1737,7 +1770,7 @@

    Returns: @@ -1745,20 +1778,20 @@

    Source code in beancount/core/account.py -
    def sans_root(account_name):
    -    """Get the name of the account without the root.
    -
    -    For example, an input of 'Assets:BofA:Checking' will produce 'BofA:Checking'.
    -
    -    Args:
    -      account_name: A string, the name of the account whose leaf name to return.
    -    Returns:
    -      A string, the name of the non-root portion of this account name.
    -    """
    -    assert isinstance(account_name, str)
    -    components = account_name.split(sep)[1:]
    -    return join(*components) if account_name else None
    -
    +
    def sans_root(account_name: Account) -> Account:
    +    """Get the name of the account without the root.
    +
    +    For example, an input of 'Assets:BofA:Checking' will produce 'BofA:Checking'.
    +
    +    Args:
    +      account_name: A string, the name of the account whose leaf name to return.
    +    Returns:
    +      A string, the name of the non-root portion of this account name.
    +    """
    +    assert isinstance(account_name, str)
    +    components = account_name.split(sep)[1:]
    +    return join(*components) if account_name else None
    +
    @@ -1771,7 +1804,7 @@

    -beancount.core.account.split(account_name) +beancount.core.account.split(account_name)

    @@ -1790,7 +1823,7 @@

    Parameters: @@ -1806,7 +1839,7 @@

    Returns: @@ -1814,16 +1847,16 @@

    Source code in beancount/core/account.py -
    def split(account_name):
    -    """Split an account's name into its components.
    -
    -    Args:
    -      account_name: A string, an account name.
    -    Returns:
    -      A list of strings, the components of the account name (without the separators).
    -    """
    -    return account_name.split(sep)
    -
    +
    def split(account_name: Account) -> List[str]:
    +    """Split an account's name into its components.
    +
    +    Args:
    +      account_name: A string, an account name.
    +    Returns:
    +      A list of strings, the components of the account name (without the separators).
    +    """
    +    return account_name.split(sep)
    +
    @@ -1836,7 +1869,7 @@

    -beancount.core.account.walk(root_directory) +beancount.core.account.walk(root_directory)

    @@ -1857,7 +1890,7 @@

    Parameters: @@ -1867,25 +1900,30 @@

    Source code in beancount/core/account.py -
    def walk(root_directory):
    -    """A version of os.walk() which yields directories that are valid account names.
    -
    -    This only yields directories that are accounts... it skips the other ones.
    -    For convenience, it also yields you the account's name.
    -
    -    Args:
    -      root_directory: A string, the name of the root of the hierarchy to be walked.
    -    Yields:
    -      Tuples of (root, account-name, dirs, files), similar to os.walk().
    -    """
    -    for root, dirs, files in os.walk(root_directory):
    -        dirs.sort()
    -        files.sort()
    -        relroot = root[len(root_directory)+1:]
    -        account_name = relroot.replace(os.sep, sep)
    -        if is_valid(account_name):
    -            yield (root, account_name, dirs, files)
    -
    +
    def walk(root_directory: Account) -> Iterator[Tuple[str, Account, List[str], List[str]]]:
    +    """A version of os.walk() which yields directories that are valid account names.
    +
    +    This only yields directories that are accounts... it skips the other ones.
    +    For convenience, it also yields you the account's name.
    +
    +    Args:
    +      root_directory: A string, the name of the root of the hierarchy to be walked.
    +    Yields:
    +      Tuples of (root, account-name, dirs, files), similar to os.walk().
    +    """
    +    for root, dirs, files in os.walk(root_directory):
    +        dirs.sort()
    +        files.sort()
    +        relroot = root[len(root_directory) + 1 :]
    +        account_name = relroot.replace(os.sep, sep)
    +        # The regex module does not handle Unicode characters in decomposed
    +        # form. Python uses the normal form for representing string. However,
    +        # some filesystems use the canonical decomposition form.
    +        # See https://docs.python.org/3/library/unicodedata.html#unicodedata.normalize
    +        account_name = unicodedata.normalize("NFKC", account_name)
    +        if is_valid(account_name):
    +            yield (root, account_name, dirs, files)
    +
    @@ -1972,7 +2010,7 @@

    -beancount.core.account_types.AccountTypes.__getnewargs__(self) +beancount.core.account_types.AccountTypes.__getnewargs__(self) special @@ -1986,10 +2024,10 @@

    Source code in beancount/core/account_types.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -2002,7 +2040,7 @@

    -beancount.core.account_types.AccountTypes.__new__(_cls, assets, liabilities, equity, income, expenses) +beancount.core.account_types.AccountTypes.__new__(_cls, assets, liabilities, equity, income, expenses) special @@ -2026,7 +2064,7 @@

    -beancount.core.account_types.AccountTypes.__repr__(self) +beancount.core.account_types.AccountTypes.__repr__(self) special @@ -2040,10 +2078,10 @@

    Source code in beancount/core/account_types.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -2067,7 +2105,7 @@

    -beancount.core.account_types.get_account_sign(account_name, account_types=None) +beancount.core.account_types.get_account_sign(account_name, account_types=None)

    @@ -2086,8 +2124,8 @@

    Parameters:
      -
    • account_name – A string, the name of the account whose sign is to return.

    • -
    • account_types – An optional instance of the current account_types.

    • +
    • account_name (str) – A string, the name of the account whose sign is to return.

    • +
    • account_types (AccountTypes) – An optional instance of the current account_types.

    @@ -2103,7 +2141,7 @@

    Returns:
      -
    • +1 or -1, depending on the account's type.

    • +
    • int – +1 or -1, depending on the account's type.

    @@ -2111,24 +2149,21 @@

    Source code in beancount/core/account_types.py -
    def get_account_sign(account_name, account_types=None):
    -    """Return the sign of the normal balance of a particular account.
    -
    -    Args:
    -      account_name: A string, the name of the account whose sign is to return.
    -      account_types: An optional instance of the current account_types.
    -    Returns:
    -      +1 or -1, depending on the account's type.
    -    """
    -    if account_types is None:
    -        account_types = DEFAULT_ACCOUNT_TYPES
    -    assert isinstance(account_name, str), "Account is not a string: {}".format(account_name)
    -    account_type = get_account_type(account_name)
    -    return (+1
    -            if account_type in (account_types.assets,
    -                                account_types.expenses)
    -            else -1)
    -
    +
    def get_account_sign(account_name: Account, account_types: AccountTypes = None) -> int:
    +    """Return the sign of the normal balance of a particular account.
    +
    +    Args:
    +      account_name: A string, the name of the account whose sign is to return.
    +      account_types: An optional instance of the current account_types.
    +    Returns:
    +      +1 or -1, depending on the account's type.
    +    """
    +    if account_types is None:
    +        account_types = DEFAULT_ACCOUNT_TYPES
    +    assert isinstance(account_name, str), "Account is not a string: {}".format(account_name)
    +    account_type = get_account_type(account_name)
    +    return +1 if account_type in (account_types.assets, account_types.expenses) else -1
    +
    @@ -2141,7 +2176,7 @@

    -beancount.core.account_types.get_account_sort_key(account_types, account_name) +beancount.core.account_types.get_account_sort_key(account_types, account_name)

    @@ -2160,7 +2195,7 @@

    Parameters:
      -
    • account_types – An instance of AccountTypes, a tuple of account type names.

    • +
    • account_types (AccountTypes) – An instance of AccountTypes, a tuple of account type names.

    @@ -2176,9 +2211,7 @@

    Returns:
      -
    • A function object to use as the optional 'key' argument to the sort -function. It accepts a single argument, the account name to sort and -produces a sortable key.

    • +
    • Tuple[str, str] – An object to use as the 'key' argument to the sort function.

    @@ -2186,18 +2219,18 @@

    Source code in beancount/core/account_types.py -
    def get_account_sort_key(account_types, account_name):
    -    """Return a tuple that can be used to order/sort account names.
    -
    -    Args:
    -      account_types: An instance of AccountTypes, a tuple of account type names.
    -    Returns:
    -      A function object to use as the optional 'key' argument to the sort
    -      function. It accepts a single argument, the account name to sort and
    -      produces a sortable key.
    -    """
    -    return (account_types.index(get_account_type(account_name)), account_name)
    -
    +
    def get_account_sort_key(
    +    account_types: AccountTypes, account_name: Account
    +) -> Tuple[str, Account]:
    +    """Return a tuple that can be used to order/sort account names.
    +
    +    Args:
    +      account_types: An instance of AccountTypes, a tuple of account type names.
    +    Returns:
    +      An object to use as the 'key' argument to the sort function.
    +    """
    +    return (account_types.index(get_account_type(account_name)), account_name)
    +
    @@ -2210,7 +2243,7 @@

    -beancount.core.account_types.get_account_type(account_name) +beancount.core.account_types.get_account_type(account_name)

    @@ -2231,7 +2264,7 @@

    Parameters:
      -
    • account_name – A string, the name of the account whose type is to return.

    • +
    • account_name (str) – A string, the name of the account whose type is to return.

    @@ -2255,21 +2288,21 @@

    Source code in beancount/core/account_types.py -
    def get_account_type(account_name):
    -    """Return the type of this account's name.
    +          
    def get_account_type(account_name: Account):
    +    """Return the type of this account's name.
     
    -    Warning: No check is made on the validity of the account type. This merely
    -    returns the root account of the corresponding account name.
    +    Warning: No check is made on the validity of the account type. This merely
    +    returns the root account of the corresponding account name.
     
    -    Args:
    -      account_name: A string, the name of the account whose type is to return.
    -    Returns:
    -      A string, the type of the account in 'account_name'.
    +    Args:
    +      account_name: A string, the name of the account whose type is to return.
    +    Returns:
    +      A string, the type of the account in 'account_name'.
     
    -    """
    -    assert isinstance(account_name, str), "Account is not a string: {}".format(account_name)
    -    return account.split(account_name)[0]
    -
    + """ + assert isinstance(account_name, str), "Account is not a string: {}".format(account_name) + return account.split(account_name)[0] +
    @@ -2282,7 +2315,7 @@

    -beancount.core.account_types.is_account_type(account_type, account_name) +beancount.core.account_types.is_account_type(account_type, account_name)

    @@ -2303,8 +2336,8 @@

    Parameters:
      -
    • account_type – A string, the prefix type of the account.

    • -
    • account_name – A string, the name of the account whose type is to return.

    • +
    • account_type (str) – A string, the prefix type of the account.

    • +
    • account_name (str) – A string, the name of the account whose type is to return.

    @@ -2320,7 +2353,7 @@

    Returns:
      -
    • A boolean, true if the account is of the given type.

    • +
    • bool – A boolean, true if the account is of the given type.

    @@ -2328,20 +2361,20 @@

    Source code in beancount/core/account_types.py -
    def is_account_type(account_type, account_name):
    -    """Return the type of this account's name.
    -
    -    Warning: No check is made on the validity of the account type. This merely
    -    returns the root account of the corresponding account name.
    -
    -    Args:
    -      account_type: A string, the prefix type of the account.
    -      account_name: A string, the name of the account whose type is to return.
    -    Returns:
    -      A boolean, true if the account is of the given type.
    -    """
    -    return bool(re.match('^{}{}'.format(account_type, account.sep), account_name))
    -
    +
    def is_account_type(account_type: str, account_name: Account) -> bool:
    +    """Return the type of this account's name.
    +
    +    Warning: No check is made on the validity of the account type. This merely
    +    returns the root account of the corresponding account name.
    +
    +    Args:
    +      account_type: A string, the prefix type of the account.
    +      account_name: A string, the name of the account whose type is to return.
    +    Returns:
    +      A boolean, true if the account is of the given type.
    +    """
    +    return bool(re.match("^{}{}".format(account_type, account.sep), account_name))
    +
    @@ -2354,7 +2387,7 @@

    -beancount.core.account_types.is_balance_sheet_account(account_name, account_types) +beancount.core.account_types.is_balance_sheet_account(account_name, account_types)

    @@ -2374,8 +2407,8 @@

    Parameters:
      -
    • account_name – A string, an account name.

    • -
    • account_types – An instance of AccountTypes.

    • +
    • account_name (str) – A string, an account name.

    • +
    • account_types (AccountTypes) – An instance of AccountTypes.

    @@ -2391,7 +2424,7 @@

    Returns:
      -
    • A boolean, true if the account is a balance sheet account.

    • +
    • bool – A boolean, true if the account is a balance sheet account.

    @@ -2399,24 +2432,27 @@

    Source code in beancount/core/account_types.py -
    def is_balance_sheet_account(account_name, account_types):
    -    """Return true if the given account is a balance sheet account.
    -    Assets, liabilities and equity accounts are balance sheet accounts.
    -
    -    Args:
    -      account_name: A string, an account name.
    -      account_types: An instance of AccountTypes.
    -    Returns:
    -      A boolean, true if the account is a balance sheet account.
    -    """
    -    assert isinstance(account_name, str), "Account is not a string: {}".format(account_name)
    -    assert isinstance(account_types, AccountTypes), (
    -        "Account types has invalid type: {}".format(account_types))
    -    account_type = get_account_type(account_name)
    -    return account_type in (account_types.assets,
    -                            account_types.liabilities,
    -                            account_types.equity)
    -
    +
    def is_balance_sheet_account(account_name: Account, account_types: AccountTypes) -> bool:
    +    """Return true if the given account is a balance sheet account.
    +    Assets, liabilities and equity accounts are balance sheet accounts.
    +
    +    Args:
    +      account_name: A string, an account name.
    +      account_types: An instance of AccountTypes.
    +    Returns:
    +      A boolean, true if the account is a balance sheet account.
    +    """
    +    assert isinstance(account_name, str), "Account is not a string: {}".format(account_name)
    +    assert isinstance(
    +        account_types, AccountTypes
    +    ), "Account types has invalid type: {}".format(account_types)
    +    account_type = get_account_type(account_name)
    +    return account_type in (
    +        account_types.assets,
    +        account_types.liabilities,
    +        account_types.equity,
    +    )
    +
    @@ -2429,7 +2465,7 @@

    -beancount.core.account_types.is_equity_account(account_name, account_types) +beancount.core.account_types.is_equity_account(account_name, account_types)

    @@ -2448,8 +2484,8 @@

    Parameters:
      -
    • account_name – A string, an account name.

    • -
    • account_types – An instance of AccountTypes.

    • +
    • account_name (str) – A string, an account name.

    • +
    • account_types (AccountTypes) – An instance of AccountTypes.

    @@ -2465,7 +2501,7 @@

    Returns:
      -
    • A boolean, true if the account is an equity account.

    • +
    • bool – A boolean, true if the account is an equity account.

    @@ -2473,21 +2509,22 @@

    Source code in beancount/core/account_types.py -
    def is_equity_account(account_name, account_types):
    -    """Return true if the given account is an equity account.
    -
    -    Args:
    -      account_name: A string, an account name.
    -      account_types: An instance of AccountTypes.
    -    Returns:
    -      A boolean, true if the account is an equity account.
    -    """
    -    assert isinstance(account_name, str), "Account is not a string: {}".format(account_name)
    -    assert isinstance(account_types, AccountTypes), (
    -        "Account types has invalid type: {}".format(account_types))
    -    account_type = get_account_type(account_name)
    -    return account_type == account_types.equity
    -
    +
    def is_equity_account(account_name: Account, account_types: AccountTypes) -> bool:
    +    """Return true if the given account is an equity account.
    +
    +    Args:
    +      account_name: A string, an account name.
    +      account_types: An instance of AccountTypes.
    +    Returns:
    +      A boolean, true if the account is an equity account.
    +    """
    +    assert isinstance(account_name, str), "Account is not a string: {}".format(account_name)
    +    assert isinstance(
    +        account_types, AccountTypes
    +    ), "Account types has invalid type: {}".format(account_types)
    +    account_type = get_account_type(account_name)
    +    return account_type == account_types.equity
    +
    @@ -2500,7 +2537,7 @@

    -beancount.core.account_types.is_income_statement_account(account_name, account_types) +beancount.core.account_types.is_income_statement_account(account_name, account_types)

    @@ -2520,8 +2557,8 @@

    Parameters:
      -
    • account_name – A string, an account name.

    • -
    • account_types – An instance of AccountTypes.

    • +
    • account_name (str) – A string, an account name.

    • +
    • account_types (AccountTypes) – An instance of AccountTypes.

    @@ -2537,7 +2574,7 @@

    Returns:
      -
    • A boolean, true if the account is an income statement account.

    • +
    • bool – A boolean, true if the account is an income statement account.

    @@ -2545,23 +2582,104 @@

    Source code in beancount/core/account_types.py -
    def is_income_statement_account(account_name, account_types):
    -    """Return true if the given account is an income statement account.
    -    Income and expense accounts are income statement accounts.
    -
    -    Args:
    -      account_name: A string, an account name.
    -      account_types: An instance of AccountTypes.
    -    Returns:
    -      A boolean, true if the account is an income statement account.
    -    """
    -    assert isinstance(account_name, str), "Account is not a string: {}".format(account_name)
    -    assert isinstance(account_types, AccountTypes), (
    -        "Account types has invalid type: {}".format(account_types))
    -    account_type = get_account_type(account_name)
    -    return account_type in (account_types.income,
    -                            account_types.expenses)
    -
    +
    def is_income_statement_account(account_name: Account, account_types: AccountTypes) -> bool:
    +    """Return true if the given account is an income statement account.
    +    Income and expense accounts are income statement accounts.
    +
    +    Args:
    +      account_name: A string, an account name.
    +      account_types: An instance of AccountTypes.
    +    Returns:
    +      A boolean, true if the account is an income statement account.
    +    """
    +    assert isinstance(account_name, str), "Account is not a string: {}".format(account_name)
    +    assert isinstance(
    +        account_types, AccountTypes
    +    ), "Account types has invalid type: {}".format(account_types)
    +    account_type = get_account_type(account_name)
    +    return account_type in (account_types.income, account_types.expenses)
    +
    + + + + + + + +
    + + + +

    +beancount.core.account_types.is_inverted_account(account_name, account_types) + + +

    + +
    + +

    Return true if the given account has inverted signs.

    +

    An inverted sign is the inverse as you'd expect in an external report, i.e., +with all positive signs expected.

    + + + + + + + + + + + + +
    Parameters: +
      +
    • account_name (str) – A string, an account name.

    • +
    • account_types (AccountTypes) – An instance of AccountTypes.

    • +
    +
    + + + + + + + + + + + +
    Returns: +
      +
    • bool – A boolean, true if the account has an inverted sign.

    • +
    +
    +
    + Source code in beancount/core/account_types.py +
    def is_inverted_account(account_name: Account, account_types: AccountTypes) -> bool:
    +    """Return true if the given account has inverted signs.
    +
    +    An inverted sign is the inverse as you'd expect in an external report, i.e.,
    +    with all positive signs expected.
    +
    +    Args:
    +      account_name: A string, an account name.
    +      account_types: An instance of AccountTypes.
    +    Returns:
    +      A boolean, true if the account has an inverted sign.
    +    """
    +    assert isinstance(account_name, str), "Account is not a string: {}".format(account_name)
    +    assert isinstance(
    +        account_types, AccountTypes
    +    ), "Account types has invalid type: {}".format(account_types)
    +    account_type = get_account_type(account_name)
    +    return account_type in (
    +        account_types.liabilities,
    +        account_types.income,
    +        account_types.equity,
    +    )
    +
    @@ -2574,15 +2692,15 @@

    -beancount.core.account_types.is_root_account(account_name, account_types=None) +beancount.core.account_types.is_root_account(account_name)

    -

    Return true if the account name is a root account. -This function does not verify whether the account root is a valid +

    Return true if the account name is a root account.

    +

    This function does not verify whether the account root is a valid one, just that it is a root account or not.

    @@ -2595,11 +2713,7 @@

    @@ -2615,7 +2729,7 @@

    @@ -2623,29 +2737,20 @@

    Parameters:
      -
    • account_name – A string, the name of the account to check for.

    • -
    • account_types – An optional instance of the current account_types; -if provided, we check against these values. If not provided, we -merely check that name pattern is that of an account component with -no separator.

    • +
    • account_name (str) – A string, the name of the account to check for.

    Returns:
      -
    • A boolean, true if the account is root account.

    • +
    • bool – A boolean, true if the account is root account.

    Source code in beancount/core/account_types.py -
    def is_root_account(account_name, account_types=None):
    -    """Return true if the account name is a root account.
    -    This function does not verify whether the account root is a valid
    -    one, just that it is a root account or not.
    -
    -    Args:
    -      account_name: A string, the name of the account to check for.
    -      account_types: An optional instance of the current account_types;
    -        if provided, we check against these values. If not provided, we
    -        merely check that name pattern is that of an account component with
    -        no separator.
    -    Returns:
    -      A boolean, true if the account is root account.
    -    """
    -    assert isinstance(account_name, str), "Account is not a string: {}".format(account_name)
    -    if account_types is not None:
    -        assert isinstance(account_types, AccountTypes), (
    -            "Account types has invalid type: {}".format(account_types))
    -        return account_name in account_types
    -    else:
    -        return (account_name and
    -                bool(re.match(r'([A-Z][A-Za-z0-9\-]+)$', account_name)))
    -
    +
    def is_root_account(account_name: Account) -> bool:
    +    """Return true if the account name is a root account.
    +
    +    This function does not verify whether the account root is a valid
    +    one, just that it is a root account or not.
    +
    +    Args:
    +      account_name: A string, the name of the account to check for.
    +    Returns:
    +      A boolean, true if the account is root account.
    +    """
    +    assert isinstance(account_name, str), "Account is not a string: {}".format(account_name)
    +    return account_name and bool(re.match(r"([A-Z][A-Za-z0-9\-]+)$", account_name))
    +
    @@ -2695,7 +2800,6 @@

    -
    @@ -2736,7 +2840,7 @@

    -beancount.core.amount.Amount.__bool__(self) +beancount.core.amount.Amount.__bool__(self) special @@ -2766,13 +2870,13 @@

    Source code in beancount/core/amount.py -
    def __bool__(self):
    -    """Boolean predicate returns true if the number is non-zero.
    -    Returns:
    -      A boolean, true if non-zero number.
    -    """
    -    return self.number != ZERO
    -
    +
    def __bool__(self):
    +    """Boolean predicate returns true if the number is non-zero.
    +    Returns:
    +      A boolean, true if non-zero number.
    +    """
    +    return self.number != ZERO
    +

    @@ -2785,7 +2889,7 @@

    -beancount.core.amount.Amount.__eq__(self, other) +beancount.core.amount.Amount.__eq__(self, other) special @@ -2815,15 +2919,15 @@

    Source code in beancount/core/amount.py -
    def __eq__(self, other):
    -    """Equality predicate. Returns true if both number and currency are equal.
    -    Returns:
    -      A boolean.
    -    """
    -    if other is None:
    -        return False
    -    return (self.number, self.currency) == (other.number, other.currency)
    -
    +
    def __eq__(self, other):
    +    """Equality predicate. Returns true if both number and currency are equal.
    +    Returns:
    +      A boolean.
    +    """
    +    if other is None:
    +        return False
    +    return (self.number, self.currency) == (other.number, other.currency)
    +

    @@ -2836,7 +2940,7 @@

    -beancount.core.amount.Amount.__hash__(self) +beancount.core.amount.Amount.__hash__(self) special @@ -2866,13 +2970,13 @@

    Source code in beancount/core/amount.py -
    def __hash__(self):
    -    """A hashing function for amounts. The hash includes the currency.
    -    Returns:
    -      An integer, the hash for this amount.
    -    """
    -    return hash((self.number, self.currency))
    -
    +
    def __hash__(self):
    +    """A hashing function for amounts. The hash includes the currency.
    +    Returns:
    +      An integer, the hash for this amount.
    +    """
    +    return hash((self.number, self.currency))
    +
    @@ -2885,7 +2989,7 @@

    -beancount.core.amount.Amount.__lt__(self, other) +beancount.core.amount.Amount.__lt__(self, other) special @@ -2931,15 +3035,15 @@

    Source code in beancount/core/amount.py -
    def __lt__(self, other):
    -    """Ordering comparison. This is used in the sorting key of positions.
    -    Args:
    -      other: An instance of Amount.
    -    Returns:
    -      True if this is less than the other Amount.
    -    """
    -    return sortkey(self) < sortkey(other)
    -
    +
    def __lt__(self, other):
    +    """Ordering comparison. This is used in the sorting key of positions.
    +    Args:
    +      other: An instance of Amount.
    +    Returns:
    +      True if this is less than the other Amount.
    +    """
    +    return sortkey(self) < sortkey(other)
    +
    @@ -2952,7 +3056,7 @@

    -beancount.core.amount.Amount.__neg__(self) +beancount.core.amount.Amount.__neg__(self) special @@ -2982,13 +3086,13 @@

    Source code in beancount/core/amount.py -
    def __neg__(self):
    -    """Return the negative of this amount.
    -    Returns:
    -      A new instance of Amount, with the negative number of units.
    -    """
    -    return Amount(-self.number, self.currency)
    -
    +
    def __neg__(self):
    +    """Return the negative of this amount.
    +    Returns:
    +      A new instance of Amount, with the negative number of units.
    +    """
    +    return Amount(-self.number, self.currency)
    +
    @@ -3001,7 +3105,7 @@

    -beancount.core.amount.Amount.__new__(cls, number, currency) +beancount.core.amount.Amount.__new__(cls, number, currency) special @@ -3024,7 +3128,7 @@

    Parameters:
      -
    • number – A string or Decimal instance. Will get converted automatically.

    • +
    • number – A Decimal instance.

    • currency – A string, the currency symbol to use.

    @@ -3033,17 +3137,17 @@

    Source code in beancount/core/amount.py -
    def __new__(cls, number, currency):
    -    """Constructor from a number and currency.
    -
    -    Args:
    -      number: A string or Decimal instance. Will get converted automatically.
    -      currency: A string, the currency symbol to use.
    -    """
    -    assert isinstance(number, Amount.valid_types_number), repr(number)
    -    assert isinstance(currency, Amount.valid_types_currency), repr(currency)
    -    return _Amount.__new__(cls, number, currency)
    -
    +
    def __new__(cls, number, currency):
    +    """Constructor from a number and currency.
    +
    +    Args:
    +      number: A Decimal instance.
    +      currency: A string, the currency symbol to use.
    +    """
    +    assert isinstance(number, Amount.valid_types_number), repr(number)
    +    assert isinstance(currency, Amount.valid_types_currency), repr(currency)
    +    return _Amount.__new__(cls, number, currency)
    +
    @@ -3056,7 +3160,7 @@

    -beancount.core.amount.Amount.__repr__(self) +beancount.core.amount.Amount.__repr__(self) special @@ -3086,14 +3190,14 @@

    Source code in beancount/core/amount.py -
    def __str__(self):
    -    """Convert an Amount instance to a printable string with the defaults.
    -
    -    Returns:
    -      A formatted string of the quantized amount and symbol.
    -    """
    -    return self.to_string()
    -
    +
    def __str__(self):
    +    """Convert an Amount instance to a printable string with the defaults.
    +
    +    Returns:
    +      A formatted string of the quantized amount and symbol.
    +    """
    +    return self.to_string()
    +
    @@ -3106,7 +3210,7 @@

    -beancount.core.amount.Amount.__str__(self) +beancount.core.amount.Amount.__str__(self) special @@ -3136,14 +3240,14 @@

    Source code in beancount/core/amount.py -
    def __str__(self):
    -    """Convert an Amount instance to a printable string with the defaults.
    -
    -    Returns:
    -      A formatted string of the quantized amount and symbol.
    -    """
    -    return self.to_string()
    -
    +
    def __str__(self):
    +    """Convert an Amount instance to a printable string with the defaults.
    +
    +    Returns:
    +      A formatted string of the quantized amount and symbol.
    +    """
    +    return self.to_string()
    +
    @@ -3156,7 +3260,7 @@

    -beancount.core.amount.Amount.from_string(string) +beancount.core.amount.Amount.from_string(string) staticmethod @@ -3203,24 +3307,25 @@

    Source code in beancount/core/amount.py -
    @staticmethod
    -def from_string(string):
    -    """Create an amount from a string.
    -
    -    This is a miniature parser used for building tests.
    -
    -    Args:
    -      string: A string of <number> <currency>.
    -    Returns:
    -      A new instance of Amount.
    -    """
    -    match = re.match(r'\s*([-+]?[0-9.]+)\s+({currency})'.format(currency=CURRENCY_RE),
    -                     string)
    -    if not match:
    -        raise ValueError("Invalid string for amount: '{}'".format(string))
    -    number, currency = match.group(1, 2)
    -    return Amount(D(number), currency)
    -
    +
    @staticmethod
    +def from_string(string):
    +    """Create an amount from a string.
    +
    +    This is a miniature parser used for building tests.
    +
    +    Args:
    +      string: A string of <number> <currency>.
    +    Returns:
    +      A new instance of Amount.
    +    """
    +    match = re.match(
    +        r"\s*([-+]?[0-9.]+)\s+({currency})".format(currency=CURRENCY_RE), string
    +    )
    +    if not match:
    +        raise ValueError("Invalid string for amount: '{}'".format(string))
    +    number, currency = match.group(1, 2)
    +    return Amount(D(number), currency)
    +
    @@ -3233,7 +3338,7 @@

    -beancount.core.amount.Amount.to_string(self, dformat=<beancount.core.display_context.DisplayFormatter object at 0x78e868ae5e50>) +beancount.core.amount.Amount.to_string(self, dformat=<beancount.core.display_context.DisplayFormatter object at 0x78fcde6b7290>)

    @@ -3276,19 +3381,22 @@

    Source code in beancount/core/amount.py -
    def to_string(self, dformat=DEFAULT_FORMATTER):
    -    """Convert an Amount instance to a printable string.
    -
    -    Args:
    -      dformat: An instance of DisplayFormatter.
    -    Returns:
    -      A formatted string of the quantized amount and symbol.
    -    """
    -    number_fmt = (dformat.format(self.number, self.currency)
    -                  if isinstance(self.number, Decimal)
    -                  else str(self.number))
    -    return "{} {}".format(number_fmt, self.currency)
    -
    +
    def to_string(self, dformat=DEFAULT_FORMATTER):
    +    """Convert an Amount instance to a printable string.
    +
    +    Args:
    +      dformat: An instance of DisplayFormatter.
    +    Returns:
    +      A formatted string of the quantized amount and symbol.
    +    """
    +    if isinstance(self.number, Decimal):
    +        number_fmt = dformat.format(self.number, self.currency)
    +    elif self.number is MISSING:
    +        number_fmt = ""
    +    else:
    +        number_fmt = str(self.number)
    +    return "{} {}".format(number_fmt, self.currency)
    +
    @@ -3312,7 +3420,7 @@

    -beancount.core.amount.A(string) +beancount.core.amount.A(string)

    @@ -3356,24 +3464,25 @@

    Source code in beancount/core/amount.py -
    @staticmethod
    -def from_string(string):
    -    """Create an amount from a string.
    -
    -    This is a miniature parser used for building tests.
    -
    -    Args:
    -      string: A string of <number> <currency>.
    -    Returns:
    -      A new instance of Amount.
    -    """
    -    match = re.match(r'\s*([-+]?[0-9.]+)\s+({currency})'.format(currency=CURRENCY_RE),
    -                     string)
    -    if not match:
    -        raise ValueError("Invalid string for amount: '{}'".format(string))
    -    number, currency = match.group(1, 2)
    -    return Amount(D(number), currency)
    -
    +
    @staticmethod
    +def from_string(string):
    +    """Create an amount from a string.
    +
    +    This is a miniature parser used for building tests.
    +
    +    Args:
    +      string: A string of <number> <currency>.
    +    Returns:
    +      A new instance of Amount.
    +    """
    +    match = re.match(
    +        r"\s*([-+]?[0-9.]+)\s+({currency})".format(currency=CURRENCY_RE), string
    +    )
    +    if not match:
    +        raise ValueError("Invalid string for amount: '{}'".format(string))
    +    number, currency = match.group(1, 2)
    +    return Amount(D(number), currency)
    +
    @@ -3386,7 +3495,7 @@

    -beancount.core.amount.abs(amount) +beancount.core.amount.abs(amount)

    @@ -3429,18 +3538,16 @@

    Source code in beancount/core/amount.py -
    def abs(amount):
    -    """Return the absolute value of the given amount.
    -
    -    Args:
    -      amount: An instance of Amount.
    -    Returns:
    -      An instance of Amount.
    -    """
    -    return (amount
    -            if amount.number >= ZERO
    -            else Amount(-amount.number, amount.currency))
    -
    +
    def abs(amount):
    +    """Return the absolute value of the given amount.
    +
    +    Args:
    +      amount: An instance of Amount.
    +    Returns:
    +      An instance of Amount.
    +    """
    +    return amount if amount.number >= ZERO else Amount(-amount.number, amount.currency)
    +
    @@ -3453,7 +3560,7 @@

    -beancount.core.amount.add(amount1, amount2) +beancount.core.amount.add(amount1, amount2)

    @@ -3498,26 +3605,28 @@

    Source code in beancount/core/amount.py -
    def add(amount1, amount2):
    -    """Add the given amounts with the same currency.
    -
    -    Args:
    -      amount1: An instance of Amount.
    -      amount2: An instance of Amount.
    -    Returns:
    -      An instance of Amount, with the sum the two amount's numbers, in the same
    -      currency.
    -    """
    -    assert isinstance(amount1.number, Decimal), (
    -        "Amount1's number is not a Decimal instance: {}".format(amount1.number))
    -    assert isinstance(amount2.number, Decimal), (
    -        "Amount2's number is not a Decimal instance: {}".format(amount2.number))
    -    if amount1.currency != amount2.currency:
    -        raise ValueError(
    -            "Unmatching currencies for operation on {} and {}".format(
    -                amount1, amount2))
    -    return Amount(amount1.number + amount2.number, amount1.currency)
    -
    +
    def add(amount1, amount2):
    +    """Add the given amounts with the same currency.
    +
    +    Args:
    +      amount1: An instance of Amount.
    +      amount2: An instance of Amount.
    +    Returns:
    +      An instance of Amount, with the sum the two amount's numbers, in the same
    +      currency.
    +    """
    +    assert isinstance(
    +        amount1.number, Decimal
    +    ), "Amount1's number is not a Decimal instance: {}".format(amount1.number)
    +    assert isinstance(
    +        amount2.number, Decimal
    +    ), "Amount2's number is not a Decimal instance: {}".format(amount2.number)
    +    if amount1.currency != amount2.currency:
    +        raise ValueError(
    +            "Unmatching currencies for operation on {} and {}".format(amount1, amount2)
    +        )
    +    return Amount(amount1.number + amount2.number, amount1.currency)
    +
    @@ -3530,7 +3639,7 @@

    -beancount.core.amount.div(amount, number) +beancount.core.amount.div(amount, number)

    @@ -3574,21 +3683,23 @@

    Source code in beancount/core/amount.py -
    def div(amount, number):
    -    """Divide the given amount by a number.
    -
    -    Args:
    -      amount: An instance of Amount.
    -      number: A decimal number.
    -    Returns:
    -      An Amount, with the same currency, but with amount units divided by 'number'.
    -    """
    -    assert isinstance(amount.number, Decimal), (
    -        "Amount's number is not a Decimal instance: {}".format(amount.number))
    -    assert isinstance(number, Decimal), (
    -        "Number is not a Decimal instance: {}".format(number))
    -    return Amount(amount.number / number, amount.currency)
    -
    +
    def div(amount, number):
    +    """Divide the given amount by a number.
    +
    +    Args:
    +      amount: An instance of Amount.
    +      number: A decimal number.
    +    Returns:
    +      An Amount, with the same currency, but with amount units divided by 'number'.
    +    """
    +    assert isinstance(
    +        amount.number, Decimal
    +    ), "Amount's number is not a Decimal instance: {}".format(amount.number)
    +    assert isinstance(number, Decimal), "Number is not a Decimal instance: {}".format(
    +        number
    +    )
    +    return Amount(amount.number / number, amount.currency)
    +
    @@ -3601,7 +3712,7 @@

    -beancount.core.amount.from_string(string) +beancount.core.amount.from_string(string)

    @@ -3645,24 +3756,25 @@

    Source code in beancount/core/amount.py -
    @staticmethod
    -def from_string(string):
    -    """Create an amount from a string.
    -
    -    This is a miniature parser used for building tests.
    -
    -    Args:
    -      string: A string of <number> <currency>.
    -    Returns:
    -      A new instance of Amount.
    -    """
    -    match = re.match(r'\s*([-+]?[0-9.]+)\s+({currency})'.format(currency=CURRENCY_RE),
    -                     string)
    -    if not match:
    -        raise ValueError("Invalid string for amount: '{}'".format(string))
    -    number, currency = match.group(1, 2)
    -    return Amount(D(number), currency)
    -
    +
    @staticmethod
    +def from_string(string):
    +    """Create an amount from a string.
    +
    +    This is a miniature parser used for building tests.
    +
    +    Args:
    +      string: A string of <number> <currency>.
    +    Returns:
    +      A new instance of Amount.
    +    """
    +    match = re.match(
    +        r"\s*([-+]?[0-9.]+)\s+({currency})".format(currency=CURRENCY_RE), string
    +    )
    +    if not match:
    +        raise ValueError("Invalid string for amount: '{}'".format(string))
    +    number, currency = match.group(1, 2)
    +    return Amount(D(number), currency)
    +
    @@ -3675,7 +3787,7 @@

    -beancount.core.amount.mul(amount, number) +beancount.core.amount.mul(amount, number)

    @@ -3719,21 +3831,23 @@

    Source code in beancount/core/amount.py -
    def mul(amount, number):
    -    """Multiply the given amount by a number.
    -
    -    Args:
    -      amount: An instance of Amount.
    -      number: A decimal number.
    -    Returns:
    -      An Amount, with the same currency, but with 'number' times units.
    -    """
    -    assert isinstance(amount.number, Decimal), (
    -        "Amount's number is not a Decimal instance: {}".format(amount.number))
    -    assert isinstance(number, Decimal), (
    -        "Number is not a Decimal instance: {}".format(number))
    -    return Amount(amount.number * number, amount.currency)
    -
    +
    def mul(amount, number):
    +    """Multiply the given amount by a number.
    +
    +    Args:
    +      amount: An instance of Amount.
    +      number: A decimal number.
    +    Returns:
    +      An Amount, with the same currency, but with 'number' times units.
    +    """
    +    assert isinstance(
    +        amount.number, Decimal
    +    ), "Amount's number is not a Decimal instance: {}".format(amount.number)
    +    assert isinstance(number, Decimal), "Number is not a Decimal instance: {}".format(
    +        number
    +    )
    +    return Amount(amount.number * number, amount.currency)
    +
    @@ -3746,7 +3860,7 @@

    -beancount.core.amount.sortkey(amount) +beancount.core.amount.sortkey(amount)

    @@ -3789,16 +3903,16 @@

    Source code in beancount/core/amount.py -
    def sortkey(amount):
    -    """A comparison function that sorts by currency first.
    -
    -    Args:
    -      amount: An instance of Amount.
    -    Returns:
    -      A sort key, composed of the currency first and then the number.
    -    """
    -    return (amount.currency, amount.number)
    -
    +
    def sortkey(amount):
    +    """A comparison function that sorts by currency first.
    +
    +    Args:
    +      amount: An instance of Amount.
    +    Returns:
    +      A sort key, composed of the currency first and then the number.
    +    """
    +    return (amount.currency, amount.number)
    +
    @@ -3811,7 +3925,7 @@

    -beancount.core.amount.sub(amount1, amount2) +beancount.core.amount.sub(amount1, amount2)

    @@ -3856,26 +3970,28 @@

    Source code in beancount/core/amount.py -
    def sub(amount1, amount2):
    -    """Subtract the given amounts with the same currency.
    -
    -    Args:
    -      amount1: An instance of Amount.
    -      amount2: An instance of Amount.
    -    Returns:
    -      An instance of Amount, with the difference between the two amount's
    -      numbers, in the same currency.
    -    """
    -    assert isinstance(amount1.number, Decimal), (
    -        "Amount1's number is not a Decimal instance: {}".format(amount1.number))
    -    assert isinstance(amount2.number, Decimal), (
    -        "Amount2's number is not a Decimal instance: {}".format(amount2.number))
    -    if amount1.currency != amount2.currency:
    -        raise ValueError(
    -            "Unmatching currencies for operation on {} and {}".format(
    -                amount1, amount2))
    -    return Amount(amount1.number - amount2.number, amount1.currency)
    -
    +
    def sub(amount1, amount2):
    +    """Subtract the given amounts with the same currency.
    +
    +    Args:
    +      amount1: An instance of Amount.
    +      amount2: An instance of Amount.
    +    Returns:
    +      An instance of Amount, with the difference between the two amount's
    +      numbers, in the same currency.
    +    """
    +    assert isinstance(
    +        amount1.number, Decimal
    +    ), "Amount1's number is not a Decimal instance: {}".format(amount1.number)
    +    assert isinstance(
    +        amount2.number, Decimal
    +    ), "Amount2's number is not a Decimal instance: {}".format(amount2.number)
    +    if amount1.currency != amount2.currency:
    +        raise ValueError(
    +            "Unmatching currencies for operation on {} and {}".format(amount1, amount2)
    +        )
    +    return Amount(amount1.number - amount2.number, amount1.currency)
    +
    @@ -3957,7 +4073,7 @@

    -beancount.core.compare.CompareError.__getnewargs__(self) +beancount.core.compare.CompareError.__getnewargs__(self) special @@ -3971,10 +4087,10 @@

    Source code in beancount/core/compare.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -3987,7 +4103,7 @@

    -beancount.core.compare.CompareError.__new__(_cls, source, message, entry) +beancount.core.compare.CompareError.__new__(_cls, source, message, entry) special @@ -4011,7 +4127,7 @@

    -beancount.core.compare.CompareError.__repr__(self) +beancount.core.compare.CompareError.__repr__(self) special @@ -4025,10 +4141,10 @@

    Source code in beancount/core/compare.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -4052,7 +4168,7 @@

    -beancount.core.compare.compare_entries(entries1, entries2) +beancount.core.compare.compare_entries(entries1, entries2)

    @@ -4117,38 +4233,38 @@

    Source code in beancount/core/compare.py -
    def compare_entries(entries1, entries2):
    -    """Compare two lists of entries. This is used for testing.
    -
    -    The entries are compared with disregard for their file location.
    -
    -    Args:
    -      entries1: A list of directives of any type.
    -      entries2: Another list of directives of any type.
    -    Returns:
    -      A tuple of (success, not_found1, not_found2), where the fields are:
    -        success: A boolean, true if all the values are equal.
    -        missing1: A list of directives from 'entries1' not found in
    -          'entries2'.
    -        missing2: A list of directives from 'entries2' not found in
    -          'entries1'.
    -    Raises:
    -      ValueError: If a duplicate entry is found.
    -    """
    -    hashes1, errors1 = hash_entries(entries1, exclude_meta=True)
    -    hashes2, errors2 = hash_entries(entries2, exclude_meta=True)
    -    keys1 = set(hashes1.keys())
    -    keys2 = set(hashes2.keys())
    -
    -    if errors1 or errors2:
    -        error = (errors1 + errors2)[0]
    -        raise ValueError(str(error))
    -
    -    same = keys1 == keys2
    -    missing1 = data.sorted([hashes1[key] for key in keys1 - keys2])
    -    missing2 = data.sorted([hashes2[key] for key in keys2 - keys1])
    -    return (same, missing1, missing2)
    -
    +
    def compare_entries(entries1, entries2):
    +    """Compare two lists of entries. This is used for testing.
    +
    +    The entries are compared with disregard for their file location.
    +
    +    Args:
    +      entries1: A list of directives of any type.
    +      entries2: Another list of directives of any type.
    +    Returns:
    +      A tuple of (success, not_found1, not_found2), where the fields are:
    +        success: A boolean, true if all the values are equal.
    +        missing1: A list of directives from 'entries1' not found in
    +          'entries2'.
    +        missing2: A list of directives from 'entries2' not found in
    +          'entries1'.
    +    Raises:
    +      ValueError: If a duplicate entry is found.
    +    """
    +    hashes1, errors1 = hash_entries(entries1, exclude_meta=True)
    +    hashes2, errors2 = hash_entries(entries2, exclude_meta=True)
    +    keys1 = set(hashes1.keys())
    +    keys2 = set(hashes2.keys())
    +
    +    if errors1 or errors2:
    +        error = (errors1 + errors2)[0]
    +        raise ValueError(str(error))
    +
    +    same = keys1 == keys2
    +    missing1 = data.sorted([hashes1[key] for key in keys1 - keys2])
    +    missing2 = data.sorted([hashes2[key] for key in keys2 - keys1])
    +    return (same, missing1, missing2)
    +
    @@ -4161,7 +4277,7 @@

    -beancount.core.compare.excludes_entries(subset_entries, entries) +beancount.core.compare.excludes_entries(subset_entries, entries)

    @@ -4221,31 +4337,31 @@

    Source code in beancount/core/compare.py -
    def excludes_entries(subset_entries, entries):
    -    """Check that a list of entries does not appear in another list.
    -
    -    Args:
    -      subset_entries: The set of entries to look for in 'entries'.
    -      entries: The larger list of entries that should not include 'subset_entries'.
    -    Returns:
    -      A boolean and a list of entries that are not supposed to appear.
    -    Raises:
    -      ValueError: If a duplicate entry is found.
    -    """
    -    subset_hashes, subset_errors = hash_entries(subset_entries, exclude_meta=True)
    -    subset_keys = set(subset_hashes.keys())
    -    hashes, errors = hash_entries(entries, exclude_meta=True)
    -    keys = set(hashes.keys())
    -
    -    if subset_errors or errors:
    -        error = (subset_errors + errors)[0]
    -        raise ValueError(str(error))
    -
    -    intersection = keys.intersection(subset_keys)
    -    excludes = not bool(intersection)
    -    extra = data.sorted([subset_hashes[key] for key in intersection])
    -    return (excludes, extra)
    -
    +
    def excludes_entries(subset_entries, entries):
    +    """Check that a list of entries does not appear in another list.
    +
    +    Args:
    +      subset_entries: The set of entries to look for in 'entries'.
    +      entries: The larger list of entries that should not include 'subset_entries'.
    +    Returns:
    +      A boolean and a list of entries that are not supposed to appear.
    +    Raises:
    +      ValueError: If a duplicate entry is found.
    +    """
    +    subset_hashes, subset_errors = hash_entries(subset_entries, exclude_meta=True)
    +    subset_keys = set(subset_hashes.keys())
    +    hashes, errors = hash_entries(entries, exclude_meta=True)
    +    keys = set(hashes.keys())
    +
    +    if subset_errors or errors:
    +        error = (subset_errors + errors)[0]
    +        raise ValueError(str(error))
    +
    +    intersection = keys.intersection(subset_keys)
    +    excludes = not bool(intersection)
    +    extra = data.sorted([subset_hashes[key] for key in intersection])
    +    return (excludes, extra)
    +
    @@ -4258,7 +4374,7 @@

    -beancount.core.compare.hash_entries(entries, exclude_meta=False) +beancount.core.compare.hash_entries(entries, exclude_meta=False)

    @@ -4308,48 +4424,54 @@

    Source code in beancount/core/compare.py -
    def hash_entries(entries, exclude_meta=False):
    -    """Compute unique hashes of each of the entries and return a map of them.
    -
    -    This is used for comparisons between sets of entries.
    -
    -    Args:
    -      entries: A list of directives.
    -      exclude_meta: If set, exclude the metadata from the hash. Use this for
    -        unit tests comparing entries coming from different sources as the
    -        filename and lineno will be distinct. However, when you're using the
    -        hashes to uniquely identify transactions, you want to include the
    -        filenames and line numbers (the default).
    -    Returns:
    -      A dict of hash-value to entry (for all entries) and a list of errors.
    -      Errors are created when duplicate entries are found.
    -    """
    -    entry_hash_dict = {}
    -    errors = []
    -    num_legal_duplicates = 0
    -    for entry in entries:
    -        hash_ = hash_entry(entry, exclude_meta)
    -
    -        if hash_ in entry_hash_dict:
    -            if isinstance(entry, Price):
    -                # Note: Allow duplicate Price entries, they should be common
    -                # because of the nature of stock markets (if they're closed, the
    -                # data source is likely to return an entry for the previously
    -                # available date, which may already have been fetched).
    -                num_legal_duplicates += 1
    -            else:
    -                other_entry = entry_hash_dict[hash_]
    -                errors.append(
    -                    CompareError(entry.meta,
    -                                 "Duplicate entry: {} == {}".format(entry, other_entry),
    -                                 entry))
    -        entry_hash_dict[hash_] = entry
    -
    -    if not errors:
    -        assert len(entry_hash_dict) + num_legal_duplicates == len(entries), (
    -            len(entry_hash_dict), len(entries), num_legal_duplicates)
    -    return entry_hash_dict, errors
    -
    +
    def hash_entries(entries, exclude_meta=False):
    +    """Compute unique hashes of each of the entries and return a map of them.
    +
    +    This is used for comparisons between sets of entries.
    +
    +    Args:
    +      entries: A list of directives.
    +      exclude_meta: If set, exclude the metadata from the hash. Use this for
    +        unit tests comparing entries coming from different sources as the
    +        filename and lineno will be distinct. However, when you're using the
    +        hashes to uniquely identify transactions, you want to include the
    +        filenames and line numbers (the default).
    +    Returns:
    +      A dict of hash-value to entry (for all entries) and a list of errors.
    +      Errors are created when duplicate entries are found.
    +    """
    +    entry_hash_dict = {}
    +    errors = []
    +    num_legal_duplicates = 0
    +    for entry in entries:
    +        hash_ = hash_entry(entry, exclude_meta)
    +
    +        if hash_ in entry_hash_dict:
    +            if isinstance(entry, Price):
    +                # Note: Allow duplicate Price entries, they should be common
    +                # because of the nature of stock markets (if they're closed, the
    +                # data source is likely to return an entry for the previously
    +                # available date, which may already have been fetched).
    +                num_legal_duplicates += 1
    +            else:
    +                other_entry = entry_hash_dict[hash_]
    +                errors.append(
    +                    CompareError(
    +                        entry.meta,
    +                        "Duplicate entry: {} == {}".format(entry, other_entry),
    +                        entry,
    +                    )
    +                )
    +        entry_hash_dict[hash_] = entry
    +
    +    if not errors:
    +        assert len(entry_hash_dict) + num_legal_duplicates == len(entries), (
    +            len(entry_hash_dict),
    +            len(entries),
    +            num_legal_duplicates,
    +        )
    +    return entry_hash_dict, errors
    +
    @@ -4362,7 +4484,7 @@

    -beancount.core.compare.hash_entry(entry, exclude_meta=False) +beancount.core.compare.hash_entry(entry, exclude_meta=False)

    @@ -4410,23 +4532,24 @@

    Source code in beancount/core/compare.py -
    def hash_entry(entry, exclude_meta=False):
    -    """Compute the stable hash of a single entry.
    -
    -    Args:
    -      entry: A directive instance.
    -      exclude_meta: If set, exclude the metadata from the hash. Use this for
    -        unit tests comparing entries coming from different sources as the
    -        filename and lineno will be distinct. However, when you're using the
    -        hashes to uniquely identify transactions, you want to include the
    -        filenames and line numbers (the default).
    -    Returns:
    -      A stable hexadecimal hash of this entry.
    -
    -    """
    -    return stable_hash_namedtuple(entry,
    -                                  IGNORED_FIELD_NAMES if exclude_meta else frozenset())
    -
    +
    def hash_entry(entry, exclude_meta=False):
    +    """Compute the stable hash of a single entry.
    +
    +    Args:
    +      entry: A directive instance.
    +      exclude_meta: If set, exclude the metadata from the hash. Use this for
    +        unit tests comparing entries coming from different sources as the
    +        filename and lineno will be distinct. However, when you're using the
    +        hashes to uniquely identify transactions, you want to include the
    +        filenames and line numbers (the default).
    +    Returns:
    +      A stable hexadecimal hash of this entry.
    +
    +    """
    +    return stable_hash_namedtuple(
    +        entry, IGNORED_FIELD_NAMES if exclude_meta else frozenset()
    +    )
    +
    @@ -4439,7 +4562,7 @@

    -beancount.core.compare.includes_entries(subset_entries, entries) +beancount.core.compare.includes_entries(subset_entries, entries)

    @@ -4499,30 +4622,30 @@

    Source code in beancount/core/compare.py -
    def includes_entries(subset_entries, entries):
    -    """Check if a list of entries is included in another list.
    -
    -    Args:
    -      subset_entries: The set of entries to look for in 'entries'.
    -      entries: The larger list of entries that could include 'subset_entries'.
    -    Returns:
    -      A boolean and a list of missing entries.
    -    Raises:
    -      ValueError: If a duplicate entry is found.
    -    """
    -    subset_hashes, subset_errors = hash_entries(subset_entries, exclude_meta=True)
    -    subset_keys = set(subset_hashes.keys())
    -    hashes, errors = hash_entries(entries, exclude_meta=True)
    -    keys = set(hashes.keys())
    -
    -    if subset_errors or errors:
    -        error = (subset_errors + errors)[0]
    -        raise ValueError(str(error))
    -
    -    includes = subset_keys.issubset(keys)
    -    missing = data.sorted([subset_hashes[key] for key in subset_keys - keys])
    -    return (includes, missing)
    -
    +
    def includes_entries(subset_entries, entries):
    +    """Check if a list of entries is included in another list.
    +
    +    Args:
    +      subset_entries: The set of entries to look for in 'entries'.
    +      entries: The larger list of entries that could include 'subset_entries'.
    +    Returns:
    +      A boolean and a list of missing entries.
    +    Raises:
    +      ValueError: If a duplicate entry is found.
    +    """
    +    subset_hashes, subset_errors = hash_entries(subset_entries, exclude_meta=True)
    +    subset_keys = set(subset_hashes.keys())
    +    hashes, errors = hash_entries(entries, exclude_meta=True)
    +    keys = set(hashes.keys())
    +
    +    if subset_errors or errors:
    +        error = (subset_errors + errors)[0]
    +        raise ValueError(str(error))
    +
    +    includes = subset_keys.issubset(keys)
    +    missing = data.sorted([subset_hashes[key] for key in subset_keys - keys])
    +    return (includes, missing)
    +
    @@ -4535,7 +4658,7 @@

    -beancount.core.compare.stable_hash_namedtuple(objtuple, ignore=frozenset()) +beancount.core.compare.stable_hash_namedtuple(objtuple, ignore=frozenset())

    @@ -4568,40 +4691,40 @@

    Source code in beancount/core/compare.py -
    def stable_hash_namedtuple(objtuple, ignore=frozenset()):
    -    """Hash the given namedtuple and its child fields.
    -
    -    This iterates over all the members of objtuple, skipping the attributes from
    -    the 'ignore' set, and computes a unique hash string code. If the elements
    -    are lists or sets, sorts them for stability.
    -
    -    Args:
    -      objtuple: A tuple object or other.
    -      ignore: A set of strings, attribute names to be skipped in
    -        computing a stable hash. For instance, circular references to objects
    -        or irrelevant data.
    -
    -    """
    -    # Note: this routine is slow and would stand to be implemented in C.
    -    hashobj = hashlib.md5()
    -    for attr_name, attr_value in zip(objtuple._fields, objtuple):
    -        if attr_name in ignore:
    -            continue
    -        if isinstance(attr_value, (list, set, frozenset)):
    -            subhashes = set()
    -            for element in attr_value:
    -                if isinstance(element, tuple):
    -                    subhashes.add(stable_hash_namedtuple(element, ignore))
    -                else:
    -                    md5 = hashlib.md5()
    -                    md5.update(str(element).encode())
    -                    subhashes.add(md5.hexdigest())
    -            for subhash in sorted(subhashes):
    -                hashobj.update(subhash.encode())
    -        else:
    -            hashobj.update(str(attr_value).encode())
    -    return hashobj.hexdigest()
    -
    +
    def stable_hash_namedtuple(objtuple, ignore=frozenset()):
    +    """Hash the given namedtuple and its child fields.
    +
    +    This iterates over all the members of objtuple, skipping the attributes from
    +    the 'ignore' set, and computes a unique hash string code. If the elements
    +    are lists or sets, sorts them for stability.
    +
    +    Args:
    +      objtuple: A tuple object or other.
    +      ignore: A set of strings, attribute names to be skipped in
    +        computing a stable hash. For instance, circular references to objects
    +        or irrelevant data.
    +
    +    """
    +    # Note: this routine is slow and would stand to be implemented in C.
    +    hashobj = hashlib.md5()
    +    for attr_name, attr_value in zip(objtuple._fields, objtuple):
    +        if attr_name in ignore:
    +            continue
    +        if isinstance(attr_value, (list, set, frozenset)):
    +            subhashes = []
    +            for element in attr_value:
    +                if isinstance(element, tuple):
    +                    subhashes.append(stable_hash_namedtuple(element, ignore))
    +                else:
    +                    md5 = hashlib.md5()
    +                    md5.update(str(element).encode())
    +                    subhashes.append(md5.hexdigest())
    +            for subhash in sorted(subhashes):
    +                hashobj.update(subhash.encode())
    +        else:
    +            hashobj.update(str(attr_value).encode())
    +    return hashobj.hexdigest()
    +
    @@ -4669,7 +4792,7 @@

    -beancount.core.convert.convert_amount(amt, target_currency, price_map, date=None, via=None) +beancount.core.convert.convert_amount(amt, target_currency, price_map, date=None, via=None)

    @@ -4720,50 +4843,50 @@

    Source code in beancount/core/convert.py -
    def convert_amount(amt, target_currency, price_map, date=None, via=None):
    -    """Return the market value of an Amount in a particular currency.
    -
    -    In addition, if a conversion rate isn't available, you can provide a list of
    -    currencies to attempt to synthesize a rate for via implied rates.
    -
    -    Args:
    -      amt: An instance of Amount.
    -      target_currency: The target currency to convert to.
    -      price_map: A dict of prices, as built by prices.build_price_map().
    -      date: A datetime.date instance to evaluate the value at, or None.
    -      via: A list of currencies to attempt to synthesize an implied rate if the
    -        direct conversion fails.
    -    Returns:
    -      An Amount, either with a successful value currency conversion, or if we
    -      could not convert the value, the amount itself, unmodified.
    -
    -    """
    -    # First, attempt to convert directly. This should be the most
    -    # straightforward conversion.
    -    base_quote = (amt.currency, target_currency)
    -    _, rate = prices.get_price(price_map, base_quote, date)
    -    if rate is not None:
    -        # On success, just make the conversion directly.
    -        return Amount(amt.number * rate, target_currency)
    -    elif via:
    -        assert isinstance(via, (tuple, list))
    -
    -        # A price is unavailable, attempt to convert via cost/price currency
    -        # hop, if the value currency isn't the target currency.
    -        for implied_currency in via:
    -            if implied_currency == target_currency:
    -                continue
    -            base_quote1 = (amt.currency, implied_currency)
    -            _, rate1 = prices.get_price(price_map, base_quote1, date)
    -            if rate1 is not None:
    -                base_quote2 = (implied_currency, target_currency)
    -                _, rate2 = prices.get_price(price_map, base_quote2, date)
    -                if rate2 is not None:
    -                    return Amount(amt.number * rate1 * rate2, target_currency)
    -
    -    # We failed to infer a conversion rate; return the amt.
    -    return amt
    -
    +
    def convert_amount(amt, target_currency, price_map, date=None, via=None):
    +    """Return the market value of an Amount in a particular currency.
    +
    +    In addition, if a conversion rate isn't available, you can provide a list of
    +    currencies to attempt to synthesize a rate for via implied rates.
    +
    +    Args:
    +      amt: An instance of Amount.
    +      target_currency: The target currency to convert to.
    +      price_map: A dict of prices, as built by prices.build_price_map().
    +      date: A datetime.date instance to evaluate the value at, or None.
    +      via: A list of currencies to attempt to synthesize an implied rate if the
    +        direct conversion fails.
    +    Returns:
    +      An Amount, either with a successful value currency conversion, or if we
    +      could not convert the value, the amount itself, unmodified.
    +
    +    """
    +    # First, attempt to convert directly. This should be the most
    +    # straightforward conversion.
    +    base_quote = (amt.currency, target_currency)
    +    _, rate = prices.get_price(price_map, base_quote, date)
    +    if rate is not None:
    +        # On success, just make the conversion directly.
    +        return Amount(amt.number * rate, target_currency)
    +    elif via:
    +        assert isinstance(via, (tuple, list))
    +
    +        # A price is unavailable, attempt to convert via cost/price currency
    +        # hop, if the value currency isn't the target currency.
    +        for implied_currency in via:
    +            if implied_currency == target_currency:
    +                continue
    +            base_quote1 = (amt.currency, implied_currency)
    +            _, rate1 = prices.get_price(price_map, base_quote1, date)
    +            if rate1 is not None:
    +                base_quote2 = (implied_currency, target_currency)
    +                _, rate2 = prices.get_price(price_map, base_quote2, date)
    +                if rate2 is not None:
    +                    return Amount(amt.number * rate1 * rate2, target_currency)
    +
    +    # We failed to infer a conversion rate; return the amt.
    +    return amt
    +
    @@ -4776,7 +4899,7 @@

    -beancount.core.convert.convert_position(pos, target_currency, price_map, date=None) +beancount.core.convert.convert_position(pos, target_currency, price_map, date=None)

    @@ -4827,31 +4950,33 @@

    Source code in beancount/core/convert.py -
    def convert_position(pos, target_currency, price_map, date=None):
    -    """Return the market value of a Position or Posting in a particular currency.
    -
    -    In addition, if the rate from the position's currency to target_currency
    -    isn't available, an attempt is made to convert from its cost currency, if
    -    one is available.
    -
    -    Args:
    -      pos: An instance of Position or Posting, equivalently.
    -      target_currency: The target currency to convert to.
    -      price_map: A dict of prices, as built by prices.build_price_map().
    -      date: A datetime.date instance to evaluate the value at, or None.
    -    Returns:
    -      An Amount, either with a successful value currency conversion, or if we
    -      could not convert the value, just the units, unmodified. (See get_value()
    -      above for details.)
    -    """
    -    cost = pos.cost
    -    value_currency = (
    -        (isinstance(cost, Cost) and cost.currency) or
    -        (hasattr(pos, 'price') and pos.price and pos.price.currency) or
    -        None)
    -    return convert_amount(pos.units, target_currency, price_map,
    -                          date=date, via=(value_currency,))
    -
    +
    def convert_position(pos, target_currency, price_map, date=None):
    +    """Return the market value of a Position or Posting in a particular currency.
    +
    +    In addition, if the rate from the position's currency to target_currency
    +    isn't available, an attempt is made to convert from its cost currency, if
    +    one is available.
    +
    +    Args:
    +      pos: An instance of Position or Posting, equivalently.
    +      target_currency: The target currency to convert to.
    +      price_map: A dict of prices, as built by prices.build_price_map().
    +      date: A datetime.date instance to evaluate the value at, or None.
    +    Returns:
    +      An Amount, either with a successful value currency conversion, or if we
    +      could not convert the value, just the units, unmodified. (See get_value()
    +      above for details.)
    +    """
    +    cost = pos.cost
    +    value_currency = (
    +        (isinstance(cost, Cost) and cost.currency)
    +        or (hasattr(pos, "price") and pos.price and pos.price.currency)
    +        or None
    +    )
    +    return convert_amount(
    +        pos.units, target_currency, price_map, date=date, via=(value_currency,)
    +    )
    +
    @@ -4864,7 +4989,7 @@

    -beancount.core.convert.get_cost(pos) +beancount.core.convert.get_cost(pos)

    @@ -4907,20 +5032,22 @@

    Source code in beancount/core/convert.py -
    def get_cost(pos):
    -    """Return the total cost of a Position or Posting.
    -
    -    Args:
    -      pos: An instance of Position or Posting, equivalently.
    -    Returns:
    -      An Amount.
    -    """
    -    assert isinstance(pos, Position) or type(pos).__name__ == 'Posting'
    -    cost = pos.cost
    -    return (Amount(cost.number * pos.units.number, cost.currency)
    -            if (isinstance(cost, Cost) and isinstance(cost.number, Decimal))
    -            else pos.units)
    -
    +
    def get_cost(pos):
    +    """Return the total cost of a Position or Posting.
    +
    +    Args:
    +      pos: An instance of Position or Posting, equivalently.
    +    Returns:
    +      An Amount.
    +    """
    +    assert isinstance(pos, Position) or type(pos).__name__ == "Posting"
    +    cost = pos.cost
    +    return (
    +        Amount(cost.number * pos.units.number, cost.currency)
    +        if (isinstance(cost, Cost) and isinstance(cost.number, Decimal))
    +        else pos.units
    +    )
    +
    @@ -4933,7 +5060,7 @@

    -beancount.core.convert.get_units(pos) +beancount.core.convert.get_units(pos)

    @@ -4976,17 +5103,17 @@

    Source code in beancount/core/convert.py -
    def get_units(pos):
    -    """Return the units of a Position or Posting.
    -
    -    Args:
    -      pos: An instance of Position or Posting, equivalently.
    -    Returns:
    -      An Amount.
    -    """
    -    assert isinstance(pos, Position) or type(pos).__name__ == 'Posting'
    -    return pos.units
    -
    +
    def get_units(pos):
    +    """Return the units of a Position or Posting.
    +
    +    Args:
    +      pos: An instance of Position or Posting, equivalently.
    +    Returns:
    +      An Amount.
    +    """
    +    assert isinstance(pos, Position) or type(pos).__name__ == "Posting"
    +    return pos.units
    +
    @@ -4999,7 +5126,7 @@

    -beancount.core.convert.get_value(pos, price_map, date=None) +beancount.core.convert.get_value(pos, price_map, date=None, output_date_prices=None)

    @@ -5010,7 +5137,9 @@

    Note that if the position is not held at cost, this does not convert anything, even if a price is available in the 'price_map'. We don't specify a target currency here. If you're attempting to make such a conversion, see -convert_*() functions below.

    +convert_*() functions below. However, is the object is a posting and it +has a price, we will use that price to infer the target currency and those +will be converted.

    @@ -5025,6 +5154,9 @@

  • pos – An instance of Position or Posting, equivalently.

  • price_map – A dict of prices, as built by prices.build_price_map().

  • date – A datetime.date instance to evaluate the value at, or None.

  • +
  • output_date_prices – An optional output list of (date, price). If this list +is provided, it will be appended to (mutated) to output the price +pulled in making the conversions.

  • @@ -5053,46 +5185,55 @@

    Source code in beancount/core/convert.py -
    def get_value(pos, price_map, date=None):
    -    """Return the market value of a Position or Posting.
    -
    -    Note that if the position is not held at cost, this does not convert
    -    anything, even if a price is available in the 'price_map'. We don't specify
    -    a target currency here. If you're attempting to make such a conversion, see
    -    ``convert_*()`` functions below.
    -
    -    Args:
    -      pos: An instance of Position or Posting, equivalently.
    -      price_map: A dict of prices, as built by prices.build_price_map().
    -      date: A datetime.date instance to evaluate the value at, or None.
    -    Returns:
    -      An Amount, either with a successful value currency conversion, or if we
    -      could not convert the value, just the units, unmodified. This is designed
    -      so that you could reduce an inventory with this and not lose any
    -      information silently in case of failure to convert (possibly due to an
    -      empty price map). Compare the returned currency to that of the input
    -      position if you need to check for success.
    -    """
    -    assert isinstance(pos, Position) or type(pos).__name__ == 'Posting'
    -    units = pos.units
    -    cost = pos.cost
    -
    -    # Try to infer what the cost/price currency should be.
    -    value_currency = (
    -        (isinstance(cost, Cost) and cost.currency) or
    -        (hasattr(pos, 'price') and pos.price and pos.price.currency) or
    -        None)
    -
    -    if isinstance(value_currency, str):
    -        # We have a value currency; hit the price database.
    -        base_quote = (units.currency, value_currency)
    -        _, price_number = prices.get_price(price_map, base_quote, date)
    -        if price_number is not None:
    -            return Amount(units.number * price_number, value_currency)
    -
    -    # We failed to infer a conversion rate; return the units.
    -    return units
    -
    +
    def get_value(pos, price_map, date=None, output_date_prices=None):
    +    """Return the market value of a Position or Posting.
    +
    +    Note that if the position is not held at cost, this does not convert
    +    anything, even if a price is available in the 'price_map'. We don't specify
    +    a target currency here. If you're attempting to make such a conversion, see
    +    ``convert_*()`` functions below. However, is the object is a posting and it
    +    has a price, we will use that price to infer the target currency and those
    +    will be converted.
    +
    +    Args:
    +      pos: An instance of Position or Posting, equivalently.
    +      price_map: A dict of prices, as built by prices.build_price_map().
    +      date: A datetime.date instance to evaluate the value at, or None.
    +      output_date_prices: An optional output list of (date, price). If this list
    +        is provided, it will be appended to (mutated) to output the price
    +        pulled in making the conversions.
    +    Returns:
    +      An Amount, either with a successful value currency conversion, or if we
    +      could not convert the value, just the units, unmodified. This is designed
    +      so that you could reduce an inventory with this and not lose any
    +      information silently in case of failure to convert (possibly due to an
    +      empty price map). Compare the returned currency to that of the input
    +      position if you need to check for success.
    +
    +    """
    +    assert isinstance(pos, Position) or type(pos).__name__ == "Posting"
    +    units = pos.units
    +    cost = pos.cost
    +
    +    # Try to infer what the cost/price currency should be.
    +    value_currency = (
    +        (isinstance(cost, Cost) and cost.currency)
    +        or (hasattr(pos, "price") and pos.price and pos.price.currency)
    +        or None
    +    )
    +
    +    if isinstance(value_currency, str):
    +        # We have a value currency; hit the price database.
    +        base_quote = (units.currency, value_currency)
    +        price_date, price_number = prices.get_price(price_map, base_quote, date)
    +        if output_date_prices is not None:
    +            output_date_prices.append((price_date, price_number))
    +        if price_number is not None:
    +            return Amount(units.number * price_number, value_currency)
    +
    +    # We failed to infer a conversion rate; return the units.
    +    return units
    +
    @@ -5105,7 +5246,7 @@

    -beancount.core.convert.get_weight(pos) +beancount.core.convert.get_weight(pos)

    @@ -5159,51 +5300,51 @@

    Source code in beancount/core/convert.py -
    def get_weight(pos):
    -    """Return the weight of a Position or Posting.
    -
    -    This is the amount that will need to be balanced from a posting of a
    -    transaction.
    -
    -    This is a *key* element of the semantics of transactions in this software. A
    -    balance amount is the amount used to check the balance of a transaction.
    -    Here are all relevant examples, with the amounts used to balance the
    -    postings:
    -
    -        Assets:Account  5234.50 USD                             ->  5234.50 USD
    -        Assets:Account  3877.41 EUR @ 1.35 USD                  ->  5234.50 USD
    -        Assets:Account       10 HOOL {523.45 USD}               ->  5234.50 USD
    -        Assets:Account       10 HOOL {523.45 USD} @ 545.60 CAD  ->  5234.50 USD
    -
    -    Args:
    -      pos: An instance of Position or Posting, equivalently.
    -    Returns:
    -      An Amount.
    -    """
    -    assert isinstance(pos, Position) or type(pos).__name__ == 'Posting'
    -    units = pos.units
    -    cost = pos.cost
    -
    -    # It the object has a cost, use that as the weight, to balance.
    -    if isinstance(cost, Cost) and isinstance(cost.number, Decimal):
    -        weight = Amount(cost.number * pos.units.number, cost.currency)
    -    else:
    -        # Otherwise use the postings.
    -        weight = units
    -
    -        # Unless there is a price available; use that if present.
    -        if not isinstance(pos, Position):
    -            price = pos.price
    -            if price is not None:
    -                # Note: Here we could assert that price.currency == units.currency.
    -                if price.number is MISSING or units.number is MISSING:
    -                    converted_number = MISSING
    -                else:
    -                    converted_number = price.number * units.number
    -                weight = Amount(converted_number, price.currency)
    -
    -    return weight
    -
    +
    def get_weight(pos):
    +    """Return the weight of a Position or Posting.
    +
    +    This is the amount that will need to be balanced from a posting of a
    +    transaction.
    +
    +    This is a *key* element of the semantics of transactions in this software. A
    +    balance amount is the amount used to check the balance of a transaction.
    +    Here are all relevant examples, with the amounts used to balance the
    +    postings:
    +
    +        Assets:Account  5234.50 USD                             ->  5234.50 USD
    +        Assets:Account  3877.41 EUR @ 1.35 USD                  ->  5234.50 USD
    +        Assets:Account       10 HOOL {523.45 USD}               ->  5234.50 USD
    +        Assets:Account       10 HOOL {523.45 USD} @ 545.60 CAD  ->  5234.50 USD
    +
    +    Args:
    +      pos: An instance of Position or Posting, equivalently.
    +    Returns:
    +      An Amount.
    +    """
    +    assert isinstance(pos, Position) or type(pos).__name__ == "Posting"
    +    units = pos.units
    +    cost = pos.cost
    +
    +    # It the object has a cost, use that as the weight, to balance.
    +    if isinstance(cost, Cost) and isinstance(cost.number, Decimal):
    +        weight = Amount(cost.number * pos.units.number, cost.currency)
    +    else:
    +        # Otherwise use the postings.
    +        weight = units
    +
    +        # Unless there is a price available; use that if present.
    +        if not isinstance(pos, Position):
    +            price = pos.price
    +            if price is not None:
    +                # Note: Here we could assert that price.currency == units.currency.
    +                if price.number is MISSING or units.number is MISSING:
    +                    converted_number = MISSING
    +                else:
    +                    converted_number = price.number * units.number
    +                weight = Amount(converted_number, price.currency)
    +
    +    return weight
    +
    @@ -5258,6 +5399,8 @@

    + +
    @@ -5274,8 +5417,58 @@

    -

    Balance(meta, date, account, amount, tolerance, diff_amount)

    +

    A "check the balance of this account" directive. This directive asserts that +the declared account should have a known number of units of a particular +currency at the beginning of its date. This is essentially an assertion, and +corresponds to the final "Statement Balance" line of a real-world statement. +These assertions act as checkpoints to help ensure that you have entered your +transactions correctly.

    +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    accountstr

    A string, the account whose balance to check at the given date.

    amountAmount

    An Amount, the number of units of the given currency you're +expecting 'account' to have at this date.

    diff_amountOptional[beancount.core.amount.Amount]

    None if the balance check succeeds. This value is set to +an Amount instance if the balance fails, the amount of the difference.

    toleranceOptional[decimal.Decimal]

    A Decimal object, the amount of tolerance to use in the +verification.

    @@ -5289,12 +5482,18 @@

    + + + + + +

    -beancount.core.data.Balance.__getnewargs__(self) +beancount.core.data.Balance.__getnewargs__(self) special @@ -5308,10 +5507,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -5324,7 +5523,7 @@

    -beancount.core.data.Balance.__new__(_cls, meta, date, account, amount, tolerance, diff_amount) +beancount.core.data.Balance.__new__(_cls, meta, date, account, amount, tolerance, diff_amount) special @@ -5348,7 +5547,7 @@

    -beancount.core.data.Balance.__repr__(self) +beancount.core.data.Balance.__repr__(self) special @@ -5362,10 +5561,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -5399,8 +5598,35 @@

    -

    Close(meta, date, account)

    +

    A "close account" directive.

    +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    accountstr

    A string, the name of the account that is being closed.

    @@ -5414,12 +5640,15 @@

    + + +

    -beancount.core.data.Close.__getnewargs__(self) +beancount.core.data.Close.__getnewargs__(self) special @@ -5433,10 +5662,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -5449,7 +5678,7 @@

    -beancount.core.data.Close.__new__(_cls, meta, date, account) +beancount.core.data.Close.__new__(_cls, meta, date, account) special @@ -5473,7 +5702,7 @@

    -beancount.core.data.Close.__repr__(self) +beancount.core.data.Close.__repr__(self) special @@ -5487,10 +5716,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -5523,8 +5752,42 @@

    -

    Commodity(meta, date, currency)

    +

    An optional commodity declaration directive. Commodities generally do not need +to be declared, but they may, and this is mainly created as intended to be +used to attach meta-data on a commodity name. Whenever a plugin needs +per-commodity meta-data, you would define such a commodity directive. Another +use is to define a commodity that isn't otherwise (yet) used anywhere in an +input file. (At the moment the date is meaningless but is specified for +coherence with all the other directives; if you can think of a good use case, +let us know).

    +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    currencystr

    A string, the commodity under consideration.

    @@ -5538,12 +5801,15 @@

    + + +

    -beancount.core.data.Commodity.__getnewargs__(self) +beancount.core.data.Commodity.__getnewargs__(self) special @@ -5557,10 +5823,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -5573,7 +5839,7 @@

    -beancount.core.data.Commodity.__new__(_cls, meta, date, currency) +beancount.core.data.Commodity.__new__(_cls, meta, date, currency) special @@ -5597,7 +5863,7 @@

    -beancount.core.data.Commodity.__repr__(self) +beancount.core.data.Commodity.__repr__(self) special @@ -5611,10 +5877,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -5647,8 +5913,43 @@

    -

    Custom(meta, date, type, values)

    +

    A custom directive. This directive can be used to implement new experimental +dated features in the Beancount file. This is meant as an intermediate measure +to be used when you would need to implement a new directive in a plugin. These +directives will be parsed liberally... any list of tokens are supported. All +that is required is some unique name for them that acts as a "type". These +directives are included in the stream and a plugin should be able to gather +them.

    +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    typestr

    A string that represents the type of the directive.

    valuesList

    A list of values of various simple types supported by the grammar. +(Note that this list is not enforced to be consistent for all directives +of the same type by the parser.)

    @@ -5662,12 +5963,16 @@

    + + + +

    -beancount.core.data.Custom.__getnewargs__(self) +beancount.core.data.Custom.__getnewargs__(self) special @@ -5681,10 +5986,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -5697,7 +6002,7 @@

    -beancount.core.data.Custom.__new__(_cls, meta, date, type, values) +beancount.core.data.Custom.__new__(_cls, meta, date, type, values) special @@ -5721,7 +6026,7 @@

    -beancount.core.data.Custom.__repr__(self) +beancount.core.data.Custom.__repr__(self) special @@ -5735,10 +6040,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -5771,14 +6076,71 @@

    -

    Document(meta, date, account, filename, tags, links)

    - - - - -
    - - +

    A document file declaration directive. This directive is used to attach a +statement to an account, at a particular date. A typical usage would be to +render PDF files or scans of your bank statements into the account's journal. +While you can explicitly create those directives in the input syntax, it is +much more convenient to provide Beancount with a root directory to search for +filenames in a hierarchy mirroring the chart of accounts, filenames which +should match the following dated format: "YYYY-MM-DD.*". See options for +detail. Beancount will automatically create these documents directives based +on the file hierarchy, and you can get them by parsing the list of entries.

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    accountstr

    A string, the account which the statement or document is associated +with.

    filenamestr

    The absolute filename of the document file.

    tagsOptional[Set]

    A set of tag strings (without the '#'), or None, if an empty set.

    linksOptional[Set]

    A set of link strings (without the '^'), or None, if an empty set.

    + + + +
    + + + + + + + + @@ -5791,7 +6153,7 @@

    -beancount.core.data.Document.__getnewargs__(self) +beancount.core.data.Document.__getnewargs__(self) special @@ -5805,10 +6167,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -5821,7 +6183,7 @@

    -beancount.core.data.Document.__new__(_cls, meta, date, account, filename, tags, links) +beancount.core.data.Document.__new__(_cls, meta, date, account, filename, tags, links) special @@ -5845,7 +6207,7 @@

    -beancount.core.data.Document.__repr__(self) +beancount.core.data.Document.__repr__(self) special @@ -5859,10 +6221,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -5895,8 +6257,60 @@

    -

    Event(meta, date, type, description)

    +

    An "event value change" directive. These directives are used as string +variables that have different values over time. You can use these to track an +address, your location, your current employer, anything you like. The kind of +reporting that is made of these generic events is based on days and a +timeline. For instance, if you need to track the number of days you spend in +each country or state, create a "location" event and whenever you travel, add +an event directive to indicate its new value. You should be able to write +simple scripts against those in order to compute if you were present somewhere +for a particular number of days. Here's an illustrative example usage, in +order to maintain your health insurance coverage in Canada, you need to be +present in the country for 183 days or more, excluding trips of less than 30 +days. There is a similar test to be done in the US by aliens to figure out if +they need to be considered as residents for tax purposes (the so-called +"substantial presence test"). By integrating these directives into your +bookkeeping, you can easily have a little program that computes the tests for +you. This is, of course, entirely optional and somewhat auxiliary to the main +purpose of double-entry bookkeeping, but correlates strongly with the +transactions you insert in it, and so it's a really convenient thing to have +in the same input file.

    +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    "type"

    A short string, typically a single lowercase word, that defines a +unique variable whose value changes over time. For example, 'location'.

    descriptionstr

    A free-form string, the value of the variable as of the date +of the transaction.

    @@ -5910,12 +6324,16 @@

    + + + +

    -beancount.core.data.Event.__getnewargs__(self) +beancount.core.data.Event.__getnewargs__(self) special @@ -5929,10 +6347,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -5945,7 +6363,7 @@

    -beancount.core.data.Event.__new__(_cls, meta, date, type, description) +beancount.core.data.Event.__new__(_cls, meta, date, type, description) special @@ -5969,7 +6387,7 @@

    -beancount.core.data.Event.__repr__(self) +beancount.core.data.Event.__repr__(self) special @@ -5983,10 +6401,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -6019,8 +6437,47 @@

    -

    Note(meta, date, account, comment)

    +

    A note directive, a general note that is attached to an account. These are +used to attach text at a particular date in a specific account. The notes can +be anything; a typical use would be to jot down an answer from a phone call to +the institution represented by the account. It should show up in an account's +journal. If you don't want this rendered, use the comment syntax in the input +file, which does not get parsed and stored.

    +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    accountstr

    A string, the account which the note is to be attached to. This is +never None, notes always have an account they correspond to.

    commentstr

    A free-form string, the text of the note. This can be long if you +want it to.

    @@ -6034,12 +6491,18 @@

    + + + + + +

    -beancount.core.data.Note.__getnewargs__(self) +beancount.core.data.Note.__getnewargs__(self) special @@ -6053,10 +6516,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -6069,7 +6532,7 @@

    -beancount.core.data.Note.__new__(_cls, meta, date, account, comment) +beancount.core.data.Note.__new__(_cls, meta, date, account, comment, tags, links) special @@ -6080,7 +6543,7 @@

    -

    Create new instance of Note(meta, date, account, comment)

    +

    Create new instance of Note(meta, date, account, comment, tags, links)

    @@ -6093,7 +6556,7 @@

    -beancount.core.data.Note.__repr__(self) +beancount.core.data.Note.__repr__(self) special @@ -6107,10 +6570,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -6143,8 +6606,51 @@

    -

    Open(meta, date, account, currencies, booking)

    +

    An "open account" directive.

    +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    accountstr

    A string, the name of the account that is being opened.

    currenciesList[str]

    A list of strings, currencies that are allowed in this account. +May be None, in which case it means that there are no restrictions on which +currencies may be stored in this account.

    bookingOptional[beancount.core.data.Booking]

    A Booking enum, the booking method to use to disambiguate +postings to this account (when zero or more than one postings match the +specification), or None if not specified. In practice, this attribute will +be should be left unspecified (None) in the vast majority of cases. See +Booking below for a selection of valid methods.

    @@ -6158,12 +6664,17 @@

    + + + + +

    -beancount.core.data.Open.__getnewargs__(self) +beancount.core.data.Open.__getnewargs__(self) special @@ -6177,10 +6688,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -6193,7 +6704,7 @@

    -beancount.core.data.Open.__new__(_cls, meta, date, account, currencies, booking) +beancount.core.data.Open.__new__(_cls, meta, date, account, currencies, booking) special @@ -6217,7 +6728,7 @@

    -beancount.core.data.Open.__repr__(self) +beancount.core.data.Open.__repr__(self) special @@ -6231,10 +6742,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -6267,8 +6778,45 @@

    -

    Pad(meta, date, account, source_account)

    +

    A "pad this account with this other account" directive. This directive +automatically inserts transactions that will make the next chronological +balance directive succeeds. It can be used to fill in missing date ranges of +transactions, as a convenience. You don't have to use this, it's sugar coating +in case you need it, while you're entering past history into your Ledger.

    +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    accountstr

    A string, the name of the account which needs to be filled.

    source_accountstr

    A string, the name of the account which is used to debit from +in order to fill 'account'.

    @@ -6282,12 +6830,16 @@

    + + + +

    -beancount.core.data.Pad.__getnewargs__(self) +beancount.core.data.Pad.__getnewargs__(self) special @@ -6301,10 +6853,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -6317,7 +6869,7 @@

    -beancount.core.data.Pad.__new__(_cls, meta, date, account, source_account) +beancount.core.data.Pad.__new__(_cls, meta, date, account, source_account) special @@ -6341,7 +6893,7 @@

    -beancount.core.data.Pad.__repr__(self) +beancount.core.data.Pad.__repr__(self) special @@ -6355,10 +6907,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -6387,16 +6939,2130 @@

    -

    +

    + +
    + +

    Postings are contained in Transaction entries. These represent the individual +legs of a transaction. Note: a posting may only appear within a single entry +(multiple transactions may not share a Posting instance), and that's what the +entry field should be set to.

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    accountstr

    A string, the account that is modified by this posting.

    unitsOptional[beancount.core.amount.Amount]

    An Amount, the units of the position, or None if it is to be +inferred from the other postings in the transaction.

    costUnion[beancount.core.position.Cost, beancount.core.position.CostSpec]

    A Cost or CostSpec instances, the units of the position.

    priceOptional[beancount.core.amount.Amount]

    An Amount, the price at which the position took place, or +None, where not relevant. Providing a price member to a posting +automatically adds a price in the prices database at the date of the +transaction.

    flagOptional[str]

    An optional flag, a one-character string or None, which is to be +associated with the posting. Most postings don't have a flag, but it can +be convenient to mark a particular posting as problematic or pending to +be reconciled for a future import of its account.

    metaOptional[Dict[str, Any]]

    A dict of strings to values, the metadata that was attached +specifically to that posting, or None, if not provided. In practice, most +of the instances will be unlikely to have metadata.

    + + + +
    + + + + + + + + + + + + + + + +
    + + + +

    +beancount.core.data.Posting.__getnewargs__(self) + + + special + + +

    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/core/data.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    + + + +

    +beancount.core.data.Posting.__new__(_cls, account, units, cost, price, flag, meta) + + + special + staticmethod + + +

    + +
    + +

    Create new instance of Posting(account, units, cost, price, flag, meta)

    + +
    + +
    + + + +
    + + + +

    +beancount.core.data.Posting.__repr__(self) + + + special + + +

    + +
    + +

    Return a nicely formatted representation string

    + +
    + Source code in beancount/core/data.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    + +
    + + + + + +
    + +
    + +
    + + + +
    + + + +

    + +beancount.core.data.Price (tuple) + + + + +

    + +
    + +

    A price declaration directive. This establishes the price of a currency in +terms of another currency as of the directive's date. A history of the prices +for each currency pairs is built and can be queried within the bookkeeping +system. Note that because Beancount does not store any data at time-of-day +resolution, it makes no sense to have multiple price directives at the same +date. (Beancount will not attempt to solve this problem; this is beyond the +general scope of double-entry bookkeeping and if you need to build a day +trading system, you should probably use something else).

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    currency: A string, the currency that is being priced, e.g. HOOL. + amount: An instance of Amount, the number of units and currency that + 'currency' is worth, for instance 1200.12 USD.

    + + + + +
    + + + + + + + + + + + + + +
    + + + +

    +beancount.core.data.Price.__getnewargs__(self) + + + special + + +

    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/core/data.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    + + + +

    +beancount.core.data.Price.__new__(_cls, meta, date, currency, amount) + + + special + staticmethod + + +

    + +
    + +

    Create new instance of Price(meta, date, currency, amount)

    + +
    + +
    + + + +
    + + + +

    +beancount.core.data.Price.__repr__(self) + + + special + + +

    + +
    + +

    Return a nicely formatted representation string

    + +
    + Source code in beancount/core/data.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    + +
    + + + + + +
    + +
    + +
    + + + +
    + + + +

    + +beancount.core.data.Query (tuple) + + + + +

    + +
    + +

    A named query declaration. This directive is used to create pre-canned queries +that can then be automatically run or made available to the shell, or perhaps be +rendered as part of a web interface. The purpose of this routine is to define +useful queries for the context of the particular given Beancount input file.

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    The date at which this query should be run. All directives following +this date will be ignored automatically. This is essentially equivalent to +the CLOSE modifier in the shell syntax.

    namestr

    A string, the unique identifier for the query.

    query_stringstr

    The SQL query string to be run or made available.

    + + + +
    + + + + + + + + + + + + + +
    + + + +

    +beancount.core.data.Query.__getnewargs__(self) + + + special + + +

    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/core/data.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    + + + +

    +beancount.core.data.Query.__new__(_cls, meta, date, name, query_string) + + + special + staticmethod + + +

    + +
    + +

    Create new instance of Query(meta, date, name, query_string)

    + +
    + +
    + + + +
    + + + +

    +beancount.core.data.Query.__repr__(self) + + + special + + +

    + +
    + +

    Return a nicely formatted representation string

    + +
    + Source code in beancount/core/data.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    + +
    + + + + + +
    + +
    + +
    + + + +
    + + + +

    + +beancount.core.data.Transaction (tuple) + + + + +

    + +
    + +

    A transaction! This is the main type of object that we manipulate, and the +entire reason this whole project exists in the first place, because +representing these types of structures with a spreadsheet is difficult.

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    flagstr

    A single-character string or None. This user-specified string +represents some custom/user-defined state of the transaction. You can use +this for various purposes. Otherwise common, pre-defined flags are defined +under beancount.core.flags, to flags transactions that are automatically +generated.

    payeeOptional[str]

    A free-form string that identifies the payee, or None, if absent.

    narrationstr

    A free-form string that provides a description for the transaction. +All transactions have at least a narration string, this is never None.

    tagsFrozenSet

    A set of tag strings (without the '#'), or EMPTY_SET.

    linksFrozenSet

    A set of link strings (without the '^'), or EMPTY_SET.

    postingsList[beancount.core.data.Posting]

    A list of Posting instances, the legs of this transaction. See the +doc under Posting above.

    + + + +
    + + + + + + + + + + + + + + + + + +
    + + + +

    +beancount.core.data.Transaction.__getnewargs__(self) + + + special + + +

    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/core/data.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    + + + +

    +beancount.core.data.Transaction.__new__(_cls, meta, date, flag, payee, narration, tags, links, postings) + + + special + staticmethod + + +

    + +
    + +

    Create new instance of Transaction(meta, date, flag, payee, narration, tags, links, postings)

    + +
    + +
    + + + +
    + + + +

    +beancount.core.data.Transaction.__repr__(self) + + + special + + +

    + +
    + +

    Return a nicely formatted representation string

    + +
    + Source code in beancount/core/data.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    + +
    + + + + + +
    + +
    + +
    + + + +
    + + + +

    + +beancount.core.data.TxnPosting (tuple) + + + + +

    + +
    + +

    A pair of a Posting and its parent Transaction. This is inserted as +temporaries in lists of postings-of-entries, which is the product of a +realization.

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    txnTransaction

    The parent Transaction instance.

    postingPosting

    The Posting instance.

    + + + +
    + + + + + + + + + + + +
    + + + +

    +beancount.core.data.TxnPosting.__getnewargs__(self) + + + special + + +

    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/core/data.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    + + + +

    +beancount.core.data.TxnPosting.__new__(_cls, txn, posting) + + + special + staticmethod + + +

    + +
    + +

    Create new instance of TxnPosting(txn, posting)

    + +
    + +
    + + + +
    + + + +

    +beancount.core.data.TxnPosting.__repr__(self) + + + special + + +

    + +
    + +

    Return a nicely formatted representation string

    + +
    + Source code in beancount/core/data.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    + +
    + + + + + +
    + +
    + +
    + + + +
    + + + +

    + +beancount.core.data.dtypes + + + +

    + +
    + +

    Types of directives.

    + + + + +
    + + + + + + + +
    + + + +

    + +beancount.core.data.dtypes.Balance (tuple) + + + + +

    + +
    + +

    A "check the balance of this account" directive. This directive asserts that +the declared account should have a known number of units of a particular +currency at the beginning of its date. This is essentially an assertion, and +corresponds to the final "Statement Balance" line of a real-world statement. +These assertions act as checkpoints to help ensure that you have entered your +transactions correctly.

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    accountstr

    A string, the account whose balance to check at the given date.

    amountAmount

    An Amount, the number of units of the given currency you're +expecting 'account' to have at this date.

    diff_amountOptional[beancount.core.amount.Amount]

    None if the balance check succeeds. This value is set to +an Amount instance if the balance fails, the amount of the difference.

    toleranceOptional[decimal.Decimal]

    A Decimal object, the amount of tolerance to use in the +verification.

    + + + +
    + + + + + + + + + + + + + + + +
    + + + +
    +beancount.core.data.dtypes.Balance.__getnewargs__(self) + + + special + + +
    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/core/data.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Balance.__new__(_cls, meta, date, account, amount, tolerance, diff_amount) + + + special + staticmethod + + +
    + +
    + +

    Create new instance of Balance(meta, date, account, amount, tolerance, diff_amount)

    + +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Balance.__repr__(self) + + + special + + +
    + +
    + +

    Return a nicely formatted representation string

    + +
    + Source code in beancount/core/data.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    + +
    + + + + + +
    + +
    + +
    + + + +
    + + + +

    + +beancount.core.data.dtypes.Close (tuple) + + + + +

    + +
    + +

    A "close account" directive.

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    accountstr

    A string, the name of the account that is being closed.

    + + + +
    + + + + + + + + + + + + +
    + + + +
    +beancount.core.data.dtypes.Close.__getnewargs__(self) + + + special + + +
    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/core/data.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Close.__new__(_cls, meta, date, account) + + + special + staticmethod + + +
    + +
    + +

    Create new instance of Close(meta, date, account)

    + +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Close.__repr__(self) + + + special + + +
    + +
    + +

    Return a nicely formatted representation string

    + +
    + Source code in beancount/core/data.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    + +
    + + + + + +
    + +
    + +
    + + + +
    + + + +

    + +beancount.core.data.dtypes.Commodity (tuple) + + + + +

    + +
    + +

    An optional commodity declaration directive. Commodities generally do not need +to be declared, but they may, and this is mainly created as intended to be +used to attach meta-data on a commodity name. Whenever a plugin needs +per-commodity meta-data, you would define such a commodity directive. Another +use is to define a commodity that isn't otherwise (yet) used anywhere in an +input file. (At the moment the date is meaningless but is specified for +coherence with all the other directives; if you can think of a good use case, +let us know).

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    currencystr

    A string, the commodity under consideration.

    + + + +
    + + + + + + + + + + + + +
    + + + +
    +beancount.core.data.dtypes.Commodity.__getnewargs__(self) + + + special + + +
    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/core/data.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Commodity.__new__(_cls, meta, date, currency) + + + special + staticmethod + + +
    + +
    + +

    Create new instance of Commodity(meta, date, currency)

    + +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Commodity.__repr__(self) + + + special + + +
    + +
    + +

    Return a nicely formatted representation string

    + +
    + Source code in beancount/core/data.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    + +
    + + + + + +
    + +
    + +
    + + + +
    + + + +

    + +beancount.core.data.dtypes.Custom (tuple) + + + + +

    + +
    + +

    A custom directive. This directive can be used to implement new experimental +dated features in the Beancount file. This is meant as an intermediate measure +to be used when you would need to implement a new directive in a plugin. These +directives will be parsed liberally... any list of tokens are supported. All +that is required is some unique name for them that acts as a "type". These +directives are included in the stream and a plugin should be able to gather +them.

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    typestr

    A string that represents the type of the directive.

    valuesList

    A list of values of various simple types supported by the grammar. +(Note that this list is not enforced to be consistent for all directives +of the same type by the parser.)

    + + + +
    + + + + + + + + + + + + + +
    + + + +
    +beancount.core.data.dtypes.Custom.__getnewargs__(self) + + + special + + +
    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/core/data.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Custom.__new__(_cls, meta, date, type, values) + + + special + staticmethod + + +
    + +
    + +

    Create new instance of Custom(meta, date, type, values)

    + +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Custom.__repr__(self) + + + special + + +
    + +
    + +

    Return a nicely formatted representation string

    + +
    + Source code in beancount/core/data.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    + +
    + + + + + +
    + +
    + +
    + + + +
    + + + +

    + +beancount.core.data.dtypes.Document (tuple) + + + + +

    + +
    + +

    A document file declaration directive. This directive is used to attach a +statement to an account, at a particular date. A typical usage would be to +render PDF files or scans of your bank statements into the account's journal. +While you can explicitly create those directives in the input syntax, it is +much more convenient to provide Beancount with a root directory to search for +filenames in a hierarchy mirroring the chart of accounts, filenames which +should match the following dated format: "YYYY-MM-DD.*". See options for +detail. Beancount will automatically create these documents directives based +on the file hierarchy, and you can get them by parsing the list of entries.

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    accountstr

    A string, the account which the statement or document is associated +with.

    filenamestr

    The absolute filename of the document file.

    tagsOptional[Set]

    A set of tag strings (without the '#'), or None, if an empty set.

    linksOptional[Set]

    A set of link strings (without the '^'), or None, if an empty set.

    + + + +
    + + + + + + + + + + + + + + + +
    + + + +
    +beancount.core.data.dtypes.Document.__getnewargs__(self) + + + special + + +
    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/core/data.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Document.__new__(_cls, meta, date, account, filename, tags, links) + + + special + staticmethod + + +
    + +
    + +

    Create new instance of Document(meta, date, account, filename, tags, links)

    + +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Document.__repr__(self) + + + special + + +
    + +
    + +

    Return a nicely formatted representation string

    + +
    + Source code in beancount/core/data.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    + +
    + + + + + +
    + +
    + +
    + + + +
    + + + +

    + +beancount.core.data.dtypes.Event (tuple) + + + + +

    + +
    + +

    An "event value change" directive. These directives are used as string +variables that have different values over time. You can use these to track an +address, your location, your current employer, anything you like. The kind of +reporting that is made of these generic events is based on days and a +timeline. For instance, if you need to track the number of days you spend in +each country or state, create a "location" event and whenever you travel, add +an event directive to indicate its new value. You should be able to write +simple scripts against those in order to compute if you were present somewhere +for a particular number of days. Here's an illustrative example usage, in +order to maintain your health insurance coverage in Canada, you need to be +present in the country for 183 days or more, excluding trips of less than 30 +days. There is a similar test to be done in the US by aliens to figure out if +they need to be considered as residents for tax purposes (the so-called +"substantial presence test"). By integrating these directives into your +bookkeeping, you can easily have a little program that computes the tests for +you. This is, of course, entirely optional and somewhat auxiliary to the main +purpose of double-entry bookkeeping, but correlates strongly with the +transactions you insert in it, and so it's a really convenient thing to have +in the same input file.

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    "type"

    A short string, typically a single lowercase word, that defines a +unique variable whose value changes over time. For example, 'location'.

    descriptionstr

    A free-form string, the value of the variable as of the date +of the transaction.

    + + + +
    + + + + + + + + + + + + + +
    + + + +
    +beancount.core.data.dtypes.Event.__getnewargs__(self) + + + special + + +
    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/core/data.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Event.__new__(_cls, meta, date, type, description) + + + special + staticmethod + + +
    + +
    + +

    Create new instance of Event(meta, date, type, description)

    + +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Event.__repr__(self) + + + special + + +
    + +
    + +

    Return a nicely formatted representation string

    + +
    + Source code in beancount/core/data.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    + +
    + + + + + +
    + +
    + +
    + + + +
    + + + +

    + +beancount.core.data.dtypes.Note (tuple) + + + + +

    + +
    + +

    A note directive, a general note that is attached to an account. These are +used to attach text at a particular date in a specific account. The notes can +be anything; a typical use would be to jot down an answer from a phone call to +the institution represented by the account. It should show up in an account's +journal. If you don't want this rendered, use the comment syntax in the input +file, which does not get parsed and stored.

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    accountstr

    A string, the account which the note is to be attached to. This is +never None, notes always have an account they correspond to.

    commentstr

    A free-form string, the text of the note. This can be long if you +want it to.

    + + + +
    + + + + + + + + + + + + + + + +
    + + + +
    +beancount.core.data.dtypes.Note.__getnewargs__(self) + + + special + + +
    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/core/data.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Note.__new__(_cls, meta, date, account, comment, tags, links) + + + special + staticmethod + + +
    + +
    + +

    Create new instance of Note(meta, date, account, comment, tags, links)

    + +
    + +
    + + + +
    + + + +
    +beancount.core.data.dtypes.Note.__repr__(self) + + + special + + +
    + +
    + +

    Return a nicely formatted representation string

    + +
    + Source code in beancount/core/data.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    + +
    + + + + + +
    + +
    + +
    + + + +
    + + + +

    + +beancount.core.data.dtypes.Open (tuple) + + + + +

    + +
    + +

    An "open account" directive.

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    accountstr

    A string, the name of the account that is being opened.

    currenciesList[str]

    A list of strings, currencies that are allowed in this account. +May be None, in which case it means that there are no restrictions on which +currencies may be stored in this account.

    bookingOptional[beancount.core.data.Booking]

    A Booking enum, the booking method to use to disambiguate +postings to this account (when zero or more than one postings match the +specification), or None if not specified. In practice, this attribute will +be should be left unspecified (None) in the vast majority of cases. See +Booking below for a selection of valid methods.

    + + -
    +
    -

    Posting(account, units, cost, price, flag, meta)

    -
    @@ -6410,14 +9076,14 @@

    -

    -beancount.core.data.Posting.__getnewargs__(self) +

    +beancount.core.data.dtypes.Open.__getnewargs__(self) special -
    +

    @@ -6425,10 +9091,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -6440,19 +9106,19 @@

    -

    -beancount.core.data.Posting.__new__(_cls, account, units, cost, price, flag, meta) +

    +beancount.core.data.dtypes.Open.__new__(_cls, meta, date, account, currencies, booking) special staticmethod -
    +

    -

    Create new instance of Posting(account, units, cost, price, flag, meta)

    +

    Create new instance of Open(meta, date, account, currencies, booking)

    @@ -6464,14 +9130,14 @@

    -

    -beancount.core.data.Posting.__repr__(self) +

    +beancount.core.data.dtypes.Open.__repr__(self) special -
    +

    @@ -6479,10 +9145,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -6504,19 +9170,56 @@

    -

    +

    -beancount.core.data.Price (tuple) +beancount.core.data.dtypes.Pad (tuple) -

    +

    -

    Price(meta, date, currency, amount)

    +

    A "pad this account with this other account" directive. This directive +automatically inserts transactions that will make the next chronological +balance directive succeeds. It can be used to fill in missing date ranges of +transactions, as a convenience. You don't have to use this, it's sugar coating +in case you need it, while you're entering past history into your Ledger.

    +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    accountstr

    A string, the name of the account which needs to be filled.

    source_accountstr

    A string, the name of the account which is used to debit from +in order to fill 'account'.

    @@ -6530,18 +9233,22 @@

    + + + +
    -

    -beancount.core.data.Price.__getnewargs__(self) +

    +beancount.core.data.dtypes.Pad.__getnewargs__(self) special -
    +

    @@ -6549,10 +9256,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -6564,19 +9271,19 @@

    -

    -beancount.core.data.Price.__new__(_cls, meta, date, currency, amount) +

    +beancount.core.data.dtypes.Pad.__new__(_cls, meta, date, account, source_account) special staticmethod -
    +

    -

    Create new instance of Price(meta, date, currency, amount)

    +

    Create new instance of Pad(meta, date, account, source_account)

    @@ -6588,14 +9295,14 @@

    -

    -beancount.core.data.Price.__repr__(self) +

    +beancount.core.data.dtypes.Pad.__repr__(self) special -
    +

    @@ -6603,10 +9310,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -6628,18 +9335,50 @@

    -

    +

    -beancount.core.data.Query (tuple) +beancount.core.data.dtypes.Price (tuple) -

    +

    -

    Query(meta, date, name, query_string)

    +

    A price declaration directive. This establishes the price of a currency in +terms of another currency as of the directive's date. A history of the prices +for each currency pairs is built and can be queried within the bookkeeping +system. Note that because Beancount does not store any data at time-of-day +resolution, it makes no sense to have multiple price directives at the same +date. (Beancount will not attempt to solve this problem; this is beyond the +general scope of double-entry bookkeeping and if you need to build a day +trading system, you should probably use something else).

    + +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    currency: A string, the currency that is being priced, e.g. HOOL. + amount: An instance of Amount, the number of units and currency that + 'currency' is worth, for instance 1200.12 USD.

    @@ -6654,18 +9393,22 @@

    + + + +
    -

    -beancount.core.data.Query.__getnewargs__(self) +

    +beancount.core.data.dtypes.Price.__getnewargs__(self) special -
    +

    @@ -6673,10 +9416,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -6688,19 +9431,19 @@

    -

    -beancount.core.data.Query.__new__(_cls, meta, date, name, query_string) +

    +beancount.core.data.dtypes.Price.__new__(_cls, meta, date, currency, amount) special staticmethod -
    +

    -

    Create new instance of Query(meta, date, name, query_string)

    +

    Create new instance of Price(meta, date, currency, amount)

    @@ -6712,14 +9455,14 @@

    -

    -beancount.core.data.Query.__repr__(self) +

    +beancount.core.data.dtypes.Price.__repr__(self) special -
    +

    @@ -6727,10 +9470,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -6752,19 +9495,56 @@

    -

    +

    -beancount.core.data.Transaction (tuple) +beancount.core.data.dtypes.Query (tuple) -

    +

    -

    Transaction(meta, date, flag, payee, narration, tags, links, postings)

    +

    A named query declaration. This directive is used to create pre-canned queries +that can then be automatically run or made available to the shell, or perhaps be +rendered as part of a web interface. The purpose of this routine is to define +useful queries for the context of the particular given Beancount input file.

    +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    The date at which this query should be run. All directives following +this date will be ignored automatically. This is essentially equivalent to +the CLOSE modifier in the shell syntax.

    namestr

    A string, the unique identifier for the query.

    query_stringstr

    The SQL query string to be run or made available.

    @@ -6778,18 +9558,22 @@

    + + + +
    -

    -beancount.core.data.Transaction.__getnewargs__(self) +

    +beancount.core.data.dtypes.Query.__getnewargs__(self) special -
    +

    @@ -6797,10 +9581,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -6812,19 +9596,19 @@

    -

    -beancount.core.data.Transaction.__new__(_cls, meta, date, flag, payee, narration, tags, links, postings) +

    +beancount.core.data.dtypes.Query.__new__(_cls, meta, date, name, query_string) special staticmethod -
    +

    -

    Create new instance of Transaction(meta, date, flag, payee, narration, tags, links, postings)

    +

    Create new instance of Query(meta, date, name, query_string)

    @@ -6836,14 +9620,14 @@

    -

    -beancount.core.data.Transaction.__repr__(self) +

    +beancount.core.data.dtypes.Query.__repr__(self) special -
    +

    @@ -6851,10 +9635,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -6876,19 +9660,79 @@

    -

    +

    -beancount.core.data.TxnPosting (tuple) +beancount.core.data.dtypes.Transaction (tuple) -

    +

    -

    TxnPosting(txn, posting)

    +

    A transaction! This is the main type of object that we manipulate, and the +entire reason this whole project exists in the first place, because +representing these types of structures with a spreadsheet is difficult.

    +

    Attributes:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    metaDict[str, Any]

    See above.

    datedate

    See above.

    flagstr

    A single-character string or None. This user-specified string +represents some custom/user-defined state of the transaction. You can use +this for various purposes. Otherwise common, pre-defined flags are defined +under beancount.core.flags, to flags transactions that are automatically +generated.

    payeeOptional[str]

    A free-form string that identifies the payee, or None, if absent.

    narrationstr

    A free-form string that provides a description for the transaction. +All transactions have at least a narration string, this is never None.

    tagsFrozenSet

    A set of tag strings (without the '#'), or EMPTY_SET.

    linksFrozenSet

    A set of link strings (without the '^'), or EMPTY_SET.

    postingsList[beancount.core.data.Posting]

    A list of Posting instances, the legs of this transaction. See the +doc under Posting above.

    @@ -6902,18 +9746,26 @@

    + + + + + + + +
    -

    -beancount.core.data.TxnPosting.__getnewargs__(self) +

    +beancount.core.data.dtypes.Transaction.__getnewargs__(self) special -
    +

    @@ -6921,10 +9773,10 @@

    Source code in beancount/core/data.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -6936,19 +9788,19 @@

    -

    -beancount.core.data.TxnPosting.__new__(_cls, txn, posting) +

    +beancount.core.data.dtypes.Transaction.__new__(_cls, meta, date, flag, payee, narration, tags, links, postings) special staticmethod -
    +

    -

    Create new instance of TxnPosting(txn, posting)

    +

    Create new instance of Transaction(meta, date, flag, payee, narration, tags, links, postings)

    @@ -6960,14 +9812,14 @@

    -

    -beancount.core.data.TxnPosting.__repr__(self) +

    +beancount.core.data.dtypes.Transaction.__repr__(self) special -
    +
    @@ -6975,10 +9827,10 @@

    Source code in beancount/core/data.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -6988,6 +9840,18 @@

    + + + + + + + + + + + + @@ -7002,7 +9866,7 @@

    -beancount.core.data.create_simple_posting(entry, account, number, currency) +beancount.core.data.create_simple_posting(entry, account, number, currency)

    @@ -7049,31 +9913,31 @@

    Source code in beancount/core/data.py -
    def create_simple_posting(entry, account, number, currency):
    -    """Create a simple posting on the entry, with just a number and currency (no cost).
    -
    -    Args:
    -      entry: The entry instance to add the posting to.
    -      account: A string, the account to use on the posting.
    -      number: A Decimal number or string to use in the posting's Amount.
    -      currency: A string, the currency for the Amount.
    -    Returns:
    -      An instance of Posting, and as a side-effect the entry has had its list of
    -      postings modified with the new Posting instance.
    -    """
    -    if isinstance(account, str):
    -        pass
    -    if number is None:
    -        units = None
    -    else:
    -        if not isinstance(number, Decimal):
    -            number = D(number)
    -        units = Amount(number, currency)
    -    posting = Posting(account, units, None, None, None, None)
    -    if entry is not None:
    -        entry.postings.append(posting)
    -    return posting
    -
    +
    def create_simple_posting(entry, account, number, currency):
    +    """Create a simple posting on the entry, with just a number and currency (no cost).
    +
    +    Args:
    +      entry: The entry instance to add the posting to.
    +      account: A string, the account to use on the posting.
    +      number: A Decimal number or string to use in the posting's Amount.
    +      currency: A string, the currency for the Amount.
    +    Returns:
    +      An instance of Posting, and as a side-effect the entry has had its list of
    +      postings modified with the new Posting instance.
    +    """
    +    if isinstance(account, str):
    +        pass
    +    if number is None:
    +        units = None
    +    else:
    +        if not isinstance(number, Decimal):
    +            number = D(number)
    +        units = Amount(number, currency)
    +    posting = Posting(account, units, None, None, None, None)
    +    if entry is not None:
    +        entry.postings.append(posting)
    +    return posting
    +
    @@ -7086,7 +9950,7 @@

    -beancount.core.data.create_simple_posting_with_cost(entry, account, number, currency, cost_number, cost_currency) +beancount.core.data.create_simple_posting_with_cost(entry, account, number, currency, cost_number, cost_currency)

    @@ -7135,35 +9999,35 @@

    Source code in beancount/core/data.py -
    def create_simple_posting_with_cost(entry, account,
    -                                    number, currency,
    -                                    cost_number, cost_currency):
    -    """Create a simple posting on the entry, with just a number and currency (no cost).
    -
    -    Args:
    -      entry: The entry instance to add the posting to.
    -      account: A string, the account to use on the posting.
    -      number: A Decimal number or string to use in the posting's Amount.
    -      currency: A string, the currency for the Amount.
    -      cost_number: A Decimal number or string to use for the posting's cost Amount.
    -      cost_currency: a string, the currency for the cost Amount.
    -    Returns:
    -      An instance of Posting, and as a side-effect the entry has had its list of
    -      postings modified with the new Posting instance.
    -    """
    -    if isinstance(account, str):
    -        pass
    -    if not isinstance(number, Decimal):
    -        number = D(number)
    -    if cost_number and not isinstance(cost_number, Decimal):
    -        cost_number = D(cost_number)
    -    units = Amount(number, currency)
    -    cost = Cost(cost_number, cost_currency, None, None)
    -    posting = Posting(account, units, cost, None, None, None)
    -    if entry is not None:
    -        entry.postings.append(posting)
    -    return posting
    -
    +
    def create_simple_posting_with_cost(
    +    entry, account, number, currency, cost_number, cost_currency
    +):
    +    """Create a simple posting on the entry, with just a number and currency (no cost).
    +
    +    Args:
    +      entry: The entry instance to add the posting to.
    +      account: A string, the account to use on the posting.
    +      number: A Decimal number or string to use in the posting's Amount.
    +      currency: A string, the currency for the Amount.
    +      cost_number: A Decimal number or string to use for the posting's cost Amount.
    +      cost_currency: a string, the currency for the cost Amount.
    +    Returns:
    +      An instance of Posting, and as a side-effect the entry has had its list of
    +      postings modified with the new Posting instance.
    +    """
    +    if isinstance(account, str):
    +        pass
    +    if not isinstance(number, Decimal):
    +        number = D(number)
    +    if cost_number and not isinstance(cost_number, Decimal):
    +        cost_number = D(cost_number)
    +    units = Amount(number, currency)
    +    cost = Cost(cost_number, cost_currency, None, None)
    +    posting = Posting(account, units, cost, None, None, None)
    +    if entry is not None:
    +        entry.postings.append(posting)
    +    return posting
    +
    @@ -7176,7 +10040,7 @@

    -beancount.core.data.entry_sortkey(entry) +beancount.core.data.entry_sortkey(entry)

    @@ -7222,19 +10086,19 @@

    Source code in beancount/core/data.py -
    def entry_sortkey(entry):
    -    """Sort-key for entries. We sort by date, except that checks
    -    should be placed in front of every list of entries of that same day,
    -    in order to balance linearly.
    -
    -    Args:
    -      entry: An entry instance.
    -    Returns:
    -      A tuple of (date, integer, integer), that forms the sort key for the
    -      entry.
    -    """
    -    return (entry.date, SORT_ORDER.get(type(entry), 0), entry.meta["lineno"])
    -
    +
    def entry_sortkey(entry):
    +    """Sort-key for entries. We sort by date, except that checks
    +    should be placed in front of every list of entries of that same day,
    +    in order to balance linearly.
    +
    +    Args:
    +      entry: An entry instance.
    +    Returns:
    +      A tuple of (date, integer, integer), that forms the sort key for the
    +      entry.
    +    """
    +    return (entry.date, SORT_ORDER.get(type(entry), 0), entry.meta["lineno"])
    +
    @@ -7247,7 +10111,7 @@

    -beancount.core.data.filter_txns(entries) +beancount.core.data.filter_txns(entries)

    @@ -7278,21 +10142,21 @@

    Source code in beancount/core/data.py -
    def filter_txns(entries):
    -    """A generator that yields only the Transaction instances.
    -
    -    This is such an incredibly common operation that it deserves a terse
    -    filtering mechanism.
    -
    -    Args:
    -      entries: A list of directives.
    -    Yields:
    -      A sorted list of only the Transaction directives.
    -    """
    -    for entry in entries:
    -        if isinstance(entry, Transaction):
    -            yield entry
    -
    +
    def filter_txns(entries):
    +    """A generator that yields only the Transaction instances.
    +
    +    This is such an incredibly common operation that it deserves a terse
    +    filtering mechanism.
    +
    +    Args:
    +      entries: A list of directives.
    +    Yields:
    +      A sorted list of only the Transaction directives.
    +    """
    +    for entry in entries:
    +        if isinstance(entry, Transaction):
    +            yield entry
    +
    @@ -7305,7 +10169,7 @@

    -beancount.core.data.find_closest(entries, filename, lineno) +beancount.core.data.find_closest(entries, filename, lineno)

    @@ -7354,31 +10218,31 @@

    Source code in beancount/core/data.py -
    def find_closest(entries, filename, lineno):
    -    """Find the closest entry from entries to (filename, lineno).
    -
    -    Args:
    -      entries: A list of directives.
    -      filename: A string, the name of the ledger file to look for. Be careful
    -        to provide the very same filename, and note that the parser stores the
    -        absolute path of the filename here.
    -      lineno: An integer, the line number closest after the directive we're
    -        looking for. This may be the exact/first line of the directive.
    -    Returns:
    -      The closest entry found in the given file for the given filename, or
    -      None, if none could be found.
    -    """
    -    min_diffline = sys.maxsize
    -    closest_entry = None
    -    for entry in entries:
    -        emeta = entry.meta
    -        if emeta["filename"] == filename and emeta["lineno"] > 0:
    -            diffline = lineno - emeta["lineno"]
    -            if 0 <= diffline < min_diffline:
    -                min_diffline = diffline
    -                closest_entry = entry
    -    return closest_entry
    -
    +
    def find_closest(entries, filename, lineno):
    +    """Find the closest entry from entries to (filename, lineno).
    +
    +    Args:
    +      entries: A list of directives.
    +      filename: A string, the name of the ledger file to look for. Be careful
    +        to provide the very same filename, and note that the parser stores the
    +        absolute path of the filename here.
    +      lineno: An integer, the line number closest after the directive we're
    +        looking for. This may be the exact/first line of the directive.
    +    Returns:
    +      The closest entry found in the given file for the given filename, or
    +      None, if none could be found.
    +    """
    +    min_diffline = sys.maxsize
    +    closest_entry = None
    +    for entry in entries:
    +        emeta = entry.meta
    +        if emeta["filename"] == filename and emeta["lineno"] > 0:
    +            diffline = lineno - emeta["lineno"]
    +            if 0 <= diffline < min_diffline:
    +                min_diffline = diffline
    +                closest_entry = entry
    +    return closest_entry
    +
    @@ -7391,7 +10255,7 @@

    -beancount.core.data.get_entry(posting_or_entry) +beancount.core.data.get_entry(posting_or_entry)

    @@ -7434,18 +10298,20 @@

    Source code in beancount/core/data.py -
    def get_entry(posting_or_entry):
    -    """Return the entry associated with the posting or entry.
    -
    -    Args:
    -      entry: A TxnPosting or entry instance
    -    Returns:
    -      A datetime instance.
    -    """
    -    return (posting_or_entry.txn
    -            if isinstance(posting_or_entry, TxnPosting)
    -            else posting_or_entry)
    -
    +
    def get_entry(posting_or_entry):
    +    """Return the entry associated with the posting or entry.
    +
    +    Args:
    +      entry: A TxnPosting or entry instance
    +    Returns:
    +      A datetime instance.
    +    """
    +    return (
    +        posting_or_entry.txn
    +        if isinstance(posting_or_entry, TxnPosting)
    +        else posting_or_entry
    +    )
    +
    @@ -7458,7 +10324,7 @@

    -beancount.core.data.has_entry_account_component(entry, component) +beancount.core.data.has_entry_account_component(entry, component)

    @@ -7504,21 +10370,21 @@

    Source code in beancount/core/data.py -
    def has_entry_account_component(entry, component):
    -    """Return true if one of the entry's postings has an account component.
    -
    -    Args:
    -      entry: A Transaction entry.
    -      component: A string, a component of an account name. For instance,
    -        ``Food`` in ``Expenses:Food:Restaurant``. All components are considered.
    -    Returns:
    -      Boolean: true if the component is in the account. Note that a component
    -      name must be whole, that is ``NY`` is not in ``Expenses:Taxes:StateNY``.
    -    """
    -    return (isinstance(entry, Transaction) and
    -            any(has_component(posting.account, component)
    -                for posting in entry.postings))
    -
    +
    def has_entry_account_component(entry, component):
    +    """Return true if one of the entry's postings has an account component.
    +
    +    Args:
    +      entry: A Transaction entry.
    +      component: A string, a component of an account name. For instance,
    +        ``Food`` in ``Expenses:Food:Restaurant``. All components are considered.
    +    Returns:
    +      Boolean: true if the component is in the account. Note that a component
    +      name must be whole, that is ``NY`` is not in ``Expenses:Taxes:StateNY``.
    +    """
    +    return isinstance(entry, Transaction) and any(
    +        has_component(posting.account, component) for posting in entry.postings
    +    )
    +
    @@ -7531,7 +10397,7 @@

    -beancount.core.data.iter_entry_dates(entries, date_begin, date_end) +beancount.core.data.iter_entry_dates(entries, date_begin, date_end)

    @@ -7558,101 +10424,28 @@

    Yields: - Instances of the dated directives, between the dates, and in the order in - which they appear.

    - -
    - Source code in beancount/core/data.py -
    def iter_entry_dates(entries, date_begin, date_end):
    -    """Iterate over the entries in a date window.
    -
    -    Args:
    -      entries: A date-sorted list of dated directives.
    -      date_begin: A datetime.date instance, the first date to include.
    -      date_end: A datetime.date instance, one day beyond the last date.
    -    Yields:
    -      Instances of the dated directives, between the dates, and in the order in
    -      which they appear.
    -    """
    -    getdate = lambda entry: entry.date
    -    index_begin = bisect_left_with_key(entries, date_begin, key=getdate)
    -    index_end = bisect_left_with_key(entries, date_end, key=getdate)
    -    for index in range(index_begin, index_end):
    -        yield entries[index]
    -
    -
    - - - - - - -
    - - - -

    -beancount.core.data.new_directive(clsname, fields) - - -

    - -
    - -

    Create a directive class. Do not include default fields. -This should probably be carried out through inheritance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • name – A string, the capitalized name of the directive.

    • -
    • fields (List[Tuple]) – A string or the list of strings, names for the fields -to add to the base tuple.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • <function NamedTuple at 0x78e868efb240> – A type object for the new directive type.

    • -
    -
    + Instances of the dated directives, between the dates, and in the order in + which they appear.

    +
    Source code in beancount/core/data.py -
    def new_directive(clsname, fields: List[Tuple]) -> NamedTuple:
    -    """Create a directive class. Do not include default fields.
    -    This should probably be carried out through inheritance.
    -
    -    Args:
    -      name: A string, the capitalized name of the directive.
    -      fields: A string or the list of strings, names for the fields
    -        to add to the base tuple.
    -    Returns:
    -      A type object for the new directive type.
    -    """
    -    return NamedTuple(
    -        clsname,
    -        [('meta', Meta), ('date', datetime.date)] + fields)
    -
    +
    def iter_entry_dates(entries, date_begin, date_end):
    +    """Iterate over the entries in a date window.
    +
    +    Args:
    +      entries: A date-sorted list of dated directives.
    +      date_begin: A datetime.date instance, the first date to include.
    +      date_end: A datetime.date instance, one day beyond the last date.
    +    Yields:
    +      Instances of the dated directives, between the dates, and in the order in
    +      which they appear.
    +    """
    +    getdate = lambda entry: entry.date
    +    index_begin = bisect_left_with_key(entries, date_begin, key=getdate)
    +    index_end = bisect_left_with_key(entries, date_end, key=getdate)
    +    for index in range(index_begin, index_end):
    +        yield entries[index]
    +
    @@ -7665,7 +10458,7 @@

    -beancount.core.data.new_metadata(filename, lineno, kvlist=None) +beancount.core.data.new_metadata(filename, lineno, kvlist=None)

    @@ -7710,22 +10503,21 @@

    Source code in beancount/core/data.py -
    def new_metadata(filename, lineno, kvlist=None):
    -    """Create a new metadata container from the filename and line number.
    -
    -    Args:
    -      filename: A string, the filename for the creator of this directive.
    -      lineno: An integer, the line number where the directive has been created.
    -      kvlist: An optional container of key-values.
    -    Returns:
    -      A metadata dict.
    -    """
    -    meta = {'filename': filename,
    -            'lineno': lineno}
    -    if kvlist:
    -        meta.update(kvlist)
    -    return meta
    -
    +
    def new_metadata(filename, lineno, kvlist=None):
    +    """Create a new metadata container from the filename and line number.
    +
    +    Args:
    +      filename: A string, the filename for the creator of this directive.
    +      lineno: An integer, the line number where the directive has been created.
    +      kvlist: An optional container of key-values.
    +    Returns:
    +      A metadata dict.
    +    """
    +    meta = {"filename": filename, "lineno": lineno}
    +    if kvlist:
    +        meta.update(kvlist)
    +    return meta
    +

    @@ -7738,7 +10530,7 @@

    -beancount.core.data.posting_has_conversion(posting) +beancount.core.data.posting_has_conversion(posting)

    @@ -7783,20 +10575,19 @@

    Source code in beancount/core/data.py -
    def posting_has_conversion(posting):
    -    """Return true if this position involves a conversion.
    -
    -    A conversion is when there is a price attached to the amount but no cost.
    -    This is used on transactions to convert between units.
    -
    -    Args:
    -      posting: an instance of Posting
    -    Return:
    -      A boolean, true if this posting has a price conversion.
    -    """
    -    return (posting.cost is None and
    -            posting.price is not None)
    -
    +
    def posting_has_conversion(posting):
    +    """Return true if this position involves a conversion.
    +
    +    A conversion is when there is a price attached to the amount but no cost.
    +    This is used on transactions to convert between units.
    +
    +    Args:
    +      posting: an instance of Posting
    +    Return:
    +      A boolean, true if this posting has a price conversion.
    +    """
    +    return posting.cost is None and posting.price is not None
    +
    @@ -7809,7 +10600,7 @@

    -beancount.core.data.posting_sortkey(entry) +beancount.core.data.posting_sortkey(entry)

    @@ -7855,21 +10646,21 @@

    Source code in beancount/core/data.py -
    def posting_sortkey(entry):
    -    """Sort-key for entries or postings. We sort by date, except that checks
    -    should be placed in front of every list of entries of that same day,
    -    in order to balance linearly.
    -
    -    Args:
    -      entry: A Posting or entry instance
    -    Returns:
    -      A tuple of (date, integer, integer), that forms the sort key for the
    -      posting or entry.
    -    """
    -    if isinstance(entry, TxnPosting):
    -        entry = entry.txn
    -    return (entry.date, SORT_ORDER.get(type(entry), 0), entry.meta["lineno"])
    -
    +
    def posting_sortkey(entry):
    +    """Sort-key for entries or postings. We sort by date, except that checks
    +    should be placed in front of every list of entries of that same day,
    +    in order to balance linearly.
    +
    +    Args:
    +      entry: A Posting or entry instance
    +    Returns:
    +      A tuple of (date, integer, integer), that forms the sort key for the
    +      posting or entry.
    +    """
    +    if isinstance(entry, TxnPosting):
    +        entry = entry.txn
    +    return (entry.date, SORT_ORDER.get(type(entry), 0), entry.meta["lineno"])
    +
    @@ -7882,7 +10673,7 @@

    -beancount.core.data.remove_account_postings(account, entries) +beancount.core.data.remove_account_postings(account, entries)

    @@ -7925,24 +10716,27 @@

    Source code in beancount/core/data.py -
    def remove_account_postings(account, entries):
    -    """Remove all postings with the given account.
    -
    -    Args:
    -      account: A string, the account name whose postings we want to remove.
    -    Returns:
    -      A list of entries without the rounding postings.
    -    """
    -    new_entries = []
    -    for entry in entries:
    -        if isinstance(entry, Transaction) and (
    -                any(posting.account == account for posting in entry.postings)):
    -            entry = entry._replace(postings=[posting
    -                                             for posting in entry.postings
    -                                             if posting.account != account])
    -        new_entries.append(entry)
    -    return new_entries
    -
    +
    def remove_account_postings(account, entries):
    +    """Remove all postings with the given account.
    +
    +    Args:
    +      account: A string, the account name whose postings we want to remove.
    +    Returns:
    +      A list of entries without the rounding postings.
    +    """
    +    new_entries = []
    +    for entry in entries:
    +        if isinstance(entry, Transaction) and (
    +            any(posting.account == account for posting in entry.postings)
    +        ):
    +            entry = entry._replace(
    +                postings=[
    +                    posting for posting in entry.postings if posting.account != account
    +                ]
    +            )
    +        new_entries.append(entry)
    +    return new_entries
    +
    @@ -7955,7 +10749,7 @@

    -beancount.core.data.sanity_check_types(entry, allow_none_for_tags_and_links=False) +beancount.core.data.sanity_check_types(entry, allow_none_for_tags_and_links=False)

    @@ -8001,42 +10795,46 @@

    Source code in beancount/core/data.py -
    def sanity_check_types(entry, allow_none_for_tags_and_links=False):
    -    """Check that the entry and its postings has all correct data types.
    -
    -    Args:
    -      entry: An instance of one of the entries to be checked.
    -      allow_none_for_tags_and_links: A boolean, whether to allow plugins to
    -        generate Transaction objects with None as value for the 'tags' or 'links'
    -        attributes.
    -    Raises:
    -      AssertionError: If there is anything that is unexpected, raises an exception.
    -    """
    -    assert isinstance(entry, ALL_DIRECTIVES), "Invalid directive type"
    -    assert isinstance(entry.meta, dict), "Invalid type for meta"
    -    assert 'filename' in entry.meta, "Missing filename in metadata"
    -    assert 'lineno' in entry.meta, "Missing line number in metadata"
    -    assert isinstance(entry.date, datetime.date), "Invalid date type"
    -    if isinstance(entry, Transaction):
    -        assert isinstance(entry.flag, (NoneType, str)), "Invalid flag type"
    -        assert isinstance(entry.payee, (NoneType, str)), "Invalid payee type"
    -        assert isinstance(entry.narration, (NoneType, str)), "Invalid narration type"
    -        set_types = ((NoneType, set, frozenset)
    -                     if allow_none_for_tags_and_links
    -                     else (set, frozenset))
    -        assert isinstance(entry.tags, set_types), (
    -            "Invalid tags type: {}".format(type(entry.tags)))
    -        assert isinstance(entry.links, set_types), (
    -            "Invalid links type: {}".format(type(entry.links)))
    -        assert isinstance(entry.postings, list), "Invalid postings list type"
    -        for posting in entry.postings:
    -            assert isinstance(posting, Posting), "Invalid posting type"
    -            assert isinstance(posting.account, str), "Invalid account type"
    -            assert isinstance(posting.units, (Amount, NoneType)), "Invalid units type"
    -            assert isinstance(posting.cost, (Cost, CostSpec, NoneType)), "Invalid cost type"
    -            assert isinstance(posting.price, (Amount, NoneType)), "Invalid price type"
    -            assert isinstance(posting.flag, (str, NoneType)), "Invalid flag type"
    -
    +
    def sanity_check_types(entry, allow_none_for_tags_and_links=False):
    +    """Check that the entry and its postings has all correct data types.
    +
    +    Args:
    +      entry: An instance of one of the entries to be checked.
    +      allow_none_for_tags_and_links: A boolean, whether to allow plugins to
    +        generate Transaction objects with None as value for the 'tags' or 'links'
    +        attributes.
    +    Raises:
    +      AssertionError: If there is anything that is unexpected, raises an exception.
    +    """
    +    assert isinstance(entry, ALL_DIRECTIVES), "Invalid directive type"
    +    assert isinstance(entry.meta, dict), "Invalid type for meta"
    +    assert "filename" in entry.meta, "Missing filename in metadata"
    +    assert "lineno" in entry.meta, "Missing line number in metadata"
    +    assert isinstance(entry.date, datetime.date), "Invalid date type"
    +    if isinstance(entry, Transaction):
    +        assert isinstance(entry.flag, (NoneType, str)), "Invalid flag type"
    +        assert isinstance(entry.payee, (NoneType, str)), "Invalid payee type"
    +        assert isinstance(entry.narration, (NoneType, str)), "Invalid narration type"
    +        set_types = (
    +            (NoneType, set, frozenset)
    +            if allow_none_for_tags_and_links
    +            else (set, frozenset)
    +        )
    +        assert isinstance(entry.tags, set_types), "Invalid tags type: {}".format(
    +            type(entry.tags)
    +        )
    +        assert isinstance(entry.links, set_types), "Invalid links type: {}".format(
    +            type(entry.links)
    +        )
    +        assert isinstance(entry.postings, list), "Invalid postings list type"
    +        for posting in entry.postings:
    +            assert isinstance(posting, Posting), "Invalid posting type"
    +            assert isinstance(posting.account, str), "Invalid account type"
    +            assert isinstance(posting.units, (Amount, NoneType)), "Invalid units type"
    +            assert isinstance(posting.cost, (Cost, CostSpec, NoneType)), "Invalid cost type"
    +            assert isinstance(posting.price, (Amount, NoneType)), "Invalid price type"
    +            assert isinstance(posting.flag, (str, NoneType)), "Invalid flag type"
    +
    @@ -8049,7 +10847,7 @@

    -beancount.core.data.sorted(entries) +beancount.core.data.sorted(entries)

    @@ -8092,16 +10890,16 @@

    Source code in beancount/core/data.py -
    def sorted(entries):
    -    """A convenience to sort a list of entries, using entry_sortkey().
    -
    -    Args:
    -      entries: A list of directives.
    -    Returns:
    -      A sorted list of directives.
    -    """
    -    return builtins.sorted(entries, key=entry_sortkey)
    -
    +
    def sorted(entries):
    +    """A convenience to sort a list of entries, using entry_sortkey().
    +
    +    Args:
    +      entries: A list of directives.
    +    Returns:
    +      A sorted list of directives.
    +    """
    +    return builtins.sorted(entries, key=entry_sortkey)
    +
    @@ -8114,7 +10912,7 @@

    -beancount.core.data.transaction_has_conversion(transaction) +beancount.core.data.transaction_has_conversion(transaction)

    @@ -8160,24 +10958,25 @@

    Source code in beancount/core/data.py -
    def transaction_has_conversion(transaction):
    -    """Given a Transaction entry, return true if at least one of
    -    the postings has a price conversion (without an associated
    -    cost). These are the source of non-zero conversion balances.
    -
    -    Args:
    -      transaction: an instance of a Transaction entry.
    -    Returns:
    -      A boolean, true if this transaction contains at least one posting with a
    -      price conversion.
    -    """
    -    assert isinstance(transaction, Transaction), (
    -        "Invalid type of entry for transaction: {}".format(transaction))
    -    for posting in transaction.postings:
    -        if posting_has_conversion(posting):
    -            return True
    -    return False
    -
    +
    def transaction_has_conversion(transaction):
    +    """Given a Transaction entry, return true if at least one of
    +    the postings has a price conversion (without an associated
    +    cost). These are the source of non-zero conversion balances.
    +
    +    Args:
    +      transaction: an instance of a Transaction entry.
    +    Returns:
    +      A boolean, true if this transaction contains at least one posting with a
    +      price conversion.
    +    """
    +    assert isinstance(
    +        transaction, Transaction
    +    ), "Invalid type of entry for transaction: {}".format(transaction)
    +    for posting in transaction.postings:
    +        if posting_has_conversion(posting):
    +            return True
    +    return False
    +
    @@ -8379,7 +11178,7 @@

    -beancount.core.display_context.DisplayContext.build(self, alignment=<Align.NATURAL: 1>, precision=<Precision.MOST_COMMON: 1>, commas=None, reserved=0) +beancount.core.display_context.DisplayContext.build(self, alignment=<Align.NATURAL: 1>, precision=<Precision.MOST_COMMON: 1>, commas=None, reserved=0)

    @@ -8411,35 +11210,37 @@

    Source code in beancount/core/display_context.py -
    def build(self,
    -          alignment=Align.NATURAL,
    -          precision=Precision.MOST_COMMON,
    -          commas=None,
    -          reserved=0):
    -    """Build a formatter for the given display context.
    -
    -    Args:
    -      alignment: The desired alignment.
    -      precision: The desired precision.
    -      commas: Whether to render commas or not. If 'None', the default value carried
    -        by the context will be used.
    -      reserved: An integer, the number of extra digits to be allocated in
    -        the maximum width calculations.
    -    """
    -    if commas is None:
    -        commas = self.commas
    -    if alignment == Align.NATURAL:
    -        build_method = self._build_natural
    -    elif alignment == Align.RIGHT:
    -        build_method = self._build_right
    -    elif alignment == Align.DOT:
    -        build_method = self._build_dot
    -    else:
    -        raise ValueError("Unknown alignment: {}".format(alignment))
    -    fmtstrings = build_method(precision, commas, reserved)
    -
    -    return DisplayFormatter(self, precision, fmtstrings)
    -
    +
    def build(
    +    self,
    +    alignment=Align.NATURAL,
    +    precision=Precision.MOST_COMMON,
    +    commas=None,
    +    reserved=0,
    +):
    +    """Build a formatter for the given display context.
    +
    +    Args:
    +      alignment: The desired alignment.
    +      precision: The desired precision.
    +      commas: Whether to render commas or not. If 'None', the default value carried
    +        by the context will be used.
    +      reserved: An integer, the number of extra digits to be allocated in
    +        the maximum width calculations.
    +    """
    +    if commas is None:
    +        commas = self.commas
    +    if alignment == Align.NATURAL:
    +        build_method = self._build_natural
    +    elif alignment == Align.RIGHT:
    +        build_method = self._build_right
    +    elif alignment == Align.DOT:
    +        build_method = self._build_dot
    +    else:
    +        raise ValueError("Unknown alignment: {}".format(alignment))
    +    fmtstrings = build_method(precision, commas, reserved)
    +
    +    return DisplayFormatter(self, precision, fmtstrings)
    +
    @@ -8452,7 +11253,7 @@

    -beancount.core.display_context.DisplayContext.quantize(self, number, currency, precision=<Precision.MOST_COMMON: 1>) +beancount.core.display_context.DisplayContext.quantize(self, number, currency, precision=<Precision.MOST_COMMON: 1>)

    @@ -8497,25 +11298,33 @@

    Source code in beancount/core/display_context.py -
    def quantize(self, number, currency, precision=Precision.MOST_COMMON):
    -    """Quantize the given number to the given precision.
    -
    -    Args:
    -      number: A Decimal instance, the number to be quantized.
    -      currency: A currency string.
    -      precision: Which precision to use.
    -    Returns:
    -      A Decimal instance, the quantized number.
    -    """
    -    assert isinstance(number, Decimal), "Invalid data: {}".format(number)
    -    ccontext = self.ccontexts[currency]
    -    num_fractional_digits = ccontext.get_fractional(precision)
    -    if num_fractional_digits is None:
    -        # Note: We could probably logging.warn() this situation here.
    -        return number
    -    qdigit = Decimal(1).scaleb(-num_fractional_digits)
    -    return number.quantize(qdigit)
    -
    +
    def quantize(self, number, currency, precision=Precision.MOST_COMMON):
    +    """Quantize the given number to the given precision.
    +
    +    Args:
    +      number: A Decimal instance, the number to be quantized.
    +      currency: A currency string.
    +      precision: Which precision to use.
    +    Returns:
    +      A Decimal instance, the quantized number.
    +    """
    +    assert isinstance(number, Decimal), "Invalid data: {}".format(number)
    +    ccontext = self.ccontexts[currency]
    +    num_fractional_digits = ccontext.get_fractional(precision)
    +    if num_fractional_digits is None:
    +        # Note: We could probably logging.warn() this situation here.
    +        return number
    +    qdigit = Decimal(1).scaleb(-num_fractional_digits)
    +
    +    with decimal.localcontext() as ctx:
    +        # Allow precision for numbers as large as 1 billion in addition to
    +        # the required number of fractional digits.
    +        #
    +        # TODO(blais): Review this to assess performance impact, and whether
    +        # we could fold this outside a calling loop.
    +        ctx.prec = num_fractional_digits + 9
    +        return number.quantize(qdigit)
    +
    @@ -8528,7 +11337,7 @@

    -beancount.core.display_context.DisplayContext.set_commas(self, commas) +beancount.core.display_context.DisplayContext.set_commas(self, commas)

    @@ -8539,10 +11348,10 @@

    Source code in beancount/core/display_context.py -
    def set_commas(self, commas):
    -    """Set the default value for rendering commas."""
    -    self.commas = commas
    -
    +
    def set_commas(self, commas):
    +    """Set the default value for rendering commas."""
    +    self.commas = commas
    +
    @@ -8555,7 +11364,7 @@

    -beancount.core.display_context.DisplayContext.update(self, number, currency='__default__') +beancount.core.display_context.DisplayContext.update(self, number, currency='__default__')

    @@ -8583,15 +11392,63 @@

    Source code in beancount/core/display_context.py -
    def update(self, number, currency='__default__'):
    -    """Update the builder with the given number for the given currency.
    -
    -    Args:
    -      number: An instance of Decimal to consider for this currency.
    -      currency: An optional string, the currency this numbers applies to.
    -    """
    -    self.ccontexts[currency].update(number)
    -
    +
    def update(self, number, currency="__default__"):
    +    """Update the builder with the given number for the given currency.
    +
    +    Args:
    +      number: An instance of Decimal to consider for this currency.
    +      currency: An optional string, the currency this numbers applies to.
    +    """
    +    self.ccontexts[currency].update(number)
    +
    + + + + + + + +
    + + + +

    +beancount.core.display_context.DisplayContext.update_from(self, other) + + +

    + +
    + +

    Update the builder with the other given DisplayContext.

    + + + + + + + + + + + + +
    Parameters: +
      +
    • other – Another DisplayContext.

    • +
    +
    +
    + Source code in beancount/core/display_context.py +
    def update_from(self, other):
    +    """Update the builder with the other given DisplayContext.
    +
    +    Args:
    +      other: Another DisplayContext.
    +    """
    +    for currency, ccontext in other.ccontexts.items():
    +        self.ccontexts[currency].update_from(ccontext)
    +
    @@ -8805,7 +11662,7 @@

    -beancount.core.distribution.Distribution.empty(self) +beancount.core.distribution.Distribution.empty(self)

    @@ -8832,14 +11689,14 @@

    Source code in beancount/core/distribution.py -
    def empty(self):
    -    """Return true if the distribution is empty.
    -
    -    Returns:
    -      A boolean.
    -    """
    -    return len(self.hist) == 0
    -
    +
    def empty(self):
    +    """Return true if the distribution is empty.
    +
    +    Returns:
    +      A boolean.
    +    """
    +    return len(self.hist) == 0
    +

    @@ -8852,7 +11709,7 @@

    -beancount.core.distribution.Distribution.max(self) +beancount.core.distribution.Distribution.max(self)

    @@ -8879,17 +11736,17 @@

    Source code in beancount/core/distribution.py -
    def max(self):
    -    """Return the minimum value seen in the distribution.
    -
    -    Returns:
    -      An element of the value type, or None, if the distribution was empty.
    -    """
    -    if not self.hist:
    -        return None
    -    value, _ = sorted(self.hist.items())[-1]
    -    return value
    -
    +
    def max(self):
    +    """Return the minimum value seen in the distribution.
    +
    +    Returns:
    +      An element of the value type, or None, if the distribution was empty.
    +    """
    +    if not self.hist:
    +        return None
    +    value, _ = sorted(self.hist.items())[-1]
    +    return value
    +
    @@ -8902,7 +11759,7 @@

    -beancount.core.distribution.Distribution.min(self) +beancount.core.distribution.Distribution.min(self)

    @@ -8929,17 +11786,17 @@

    Source code in beancount/core/distribution.py -
    def min(self):
    -    """Return the minimum value seen in the distribution.
    -
    -    Returns:
    -      An element of the value type, or None, if the distribution was empty.
    -    """
    -    if not self.hist:
    -        return None
    -    value, _ = sorted(self.hist.items())[0]
    -    return value
    -
    +
    def min(self):
    +    """Return the minimum value seen in the distribution.
    +
    +    Returns:
    +      An element of the value type, or None, if the distribution was empty.
    +    """
    +    if not self.hist:
    +        return None
    +    value, _ = sorted(self.hist.items())[0]
    +    return value
    +
    @@ -8952,7 +11809,7 @@

    -beancount.core.distribution.Distribution.mode(self) +beancount.core.distribution.Distribution.mode(self)

    @@ -8979,22 +11836,22 @@

    Source code in beancount/core/distribution.py -
    def mode(self):
    -    """Return the mode of the distribution.
    -
    -    Returns:
    -      An element of the value type, or None, if the distribution was empty.
    -    """
    -    if not self.hist:
    -        return None
    -    max_value = 0
    -    max_count = 0
    -    for value, count in sorted(self.hist.items()):
    -        if count >= max_count:
    -            max_count = count
    -            max_value = value
    -    return max_value
    -
    +
    def mode(self):
    +    """Return the mode of the distribution.
    +
    +    Returns:
    +      An element of the value type, or None, if the distribution was empty.
    +    """
    +    if not self.hist:
    +        return None
    +    max_value = 0
    +    max_count = 0
    +    for value, count in sorted(self.hist.items()):
    +        if count >= max_count:
    +            max_count = count
    +            max_value = value
    +    return max_value
    +
    @@ -9007,7 +11864,7 @@

    -beancount.core.distribution.Distribution.update(self, value) +beancount.core.distribution.Distribution.update(self, value)

    @@ -9034,14 +11891,62 @@

    Source code in beancount/core/distribution.py -
    def update(self, value):
    -    """Add a sample to the distribution.
    +          
    def update(self, value):
    +    """Add a sample to the distribution.
    +
    +    Args:
    +      value: A value of the function.
    +    """
    +    self.hist[value] += 1
    +
    +
    + - Args: - value: A value of the function. - """ - self.hist[value] += 1 - + + + + +
    + + + +

    +beancount.core.distribution.Distribution.update_from(self, other) + + +

    + +
    + +

    Add samples from the other distribution to this one.

    + + + + + + + + + + + + +
    Parameters: +
      +
    • other – Another distribution.

    • +
    +
    +
    + Source code in beancount/core/distribution.py +
    def update_from(self, other):
    +    """Add samples from the other distribution to this one.
    +
    +    Args:
    +      other: Another distribution.
    +    """
    +    for value, count in other.hist.items():
    +        self.hist[value] += count
    +
    @@ -9107,8 +12012,6 @@

    - -

    @@ -9181,7 +12084,7 @@

    -beancount.core.getters.GetAccounts.Balance(_, entry) +beancount.core.getters.GetAccounts.Balance(_, entry)

    @@ -9224,16 +12127,16 @@

    Source code in beancount/core/getters.py -
    def _one(_, entry):
    -    """Process directives with a single account attribute.
    -
    -    Args:
    -      entry: An instance of a directive.
    -    Returns:
    -      The single account of this directive.
    -    """
    -    return (entry.account,)
    -
    +
    def _one(_, entry):
    +    """Process directives with a single account attribute.
    +
    +    Args:
    +      entry: An instance of a directive.
    +    Returns:
    +      The single account of this directive.
    +    """
    +    return (entry.account,)
    +
    @@ -9246,7 +12149,7 @@

    -beancount.core.getters.GetAccounts.Close(_, entry) +beancount.core.getters.GetAccounts.Close(_, entry)

    @@ -9289,16 +12192,16 @@

    Source code in beancount/core/getters.py -
    def _one(_, entry):
    -    """Process directives with a single account attribute.
    -
    -    Args:
    -      entry: An instance of a directive.
    -    Returns:
    -      The single account of this directive.
    -    """
    -    return (entry.account,)
    -
    +
    def _one(_, entry):
    +    """Process directives with a single account attribute.
    +
    +    Args:
    +      entry: An instance of a directive.
    +    Returns:
    +      The single account of this directive.
    +    """
    +    return (entry.account,)
    +
    @@ -9311,7 +12214,7 @@

    -beancount.core.getters.GetAccounts.Commodity(_, entry) +beancount.core.getters.GetAccounts.Commodity(_, entry)

    @@ -9354,16 +12257,16 @@

    Source code in beancount/core/getters.py -
    def _zero(_, entry):
    -    """Process directives with no accounts.
    -
    -    Args:
    -      entry: An instance of a directive.
    -    Returns:
    -      An empty list
    -    """
    -    return ()
    -
    +
    def _zero(_, entry):
    +    """Process directives with no accounts.
    +
    +    Args:
    +      entry: An instance of a directive.
    +    Returns:
    +      An empty list
    +    """
    +    return ()
    +
    @@ -9376,7 +12279,7 @@

    -beancount.core.getters.GetAccounts.Custom(_, entry) +beancount.core.getters.GetAccounts.Custom(_, entry)

    @@ -9419,16 +12322,16 @@

    Source code in beancount/core/getters.py -
    def _zero(_, entry):
    -    """Process directives with no accounts.
    -
    -    Args:
    -      entry: An instance of a directive.
    -    Returns:
    -      An empty list
    -    """
    -    return ()
    -
    +
    def _zero(_, entry):
    +    """Process directives with no accounts.
    +
    +    Args:
    +      entry: An instance of a directive.
    +    Returns:
    +      An empty list
    +    """
    +    return ()
    +
    @@ -9441,7 +12344,7 @@

    -beancount.core.getters.GetAccounts.Document(_, entry) +beancount.core.getters.GetAccounts.Document(_, entry)

    @@ -9484,16 +12387,16 @@

    Source code in beancount/core/getters.py -
    def _one(_, entry):
    -    """Process directives with a single account attribute.
    -
    -    Args:
    -      entry: An instance of a directive.
    -    Returns:
    -      The single account of this directive.
    -    """
    -    return (entry.account,)
    -
    +
    def _one(_, entry):
    +    """Process directives with a single account attribute.
    +
    +    Args:
    +      entry: An instance of a directive.
    +    Returns:
    +      The single account of this directive.
    +    """
    +    return (entry.account,)
    +
    @@ -9506,7 +12409,7 @@

    -beancount.core.getters.GetAccounts.Event(_, entry) +beancount.core.getters.GetAccounts.Event(_, entry)

    @@ -9549,16 +12452,16 @@

    Source code in beancount/core/getters.py -
    def _zero(_, entry):
    -    """Process directives with no accounts.
    -
    -    Args:
    -      entry: An instance of a directive.
    -    Returns:
    -      An empty list
    -    """
    -    return ()
    -
    +
    def _zero(_, entry):
    +    """Process directives with no accounts.
    +
    +    Args:
    +      entry: An instance of a directive.
    +    Returns:
    +      An empty list
    +    """
    +    return ()
    +
    @@ -9571,7 +12474,7 @@

    -beancount.core.getters.GetAccounts.Note(_, entry) +beancount.core.getters.GetAccounts.Note(_, entry)

    @@ -9614,16 +12517,16 @@

    Source code in beancount/core/getters.py -
    def _one(_, entry):
    -    """Process directives with a single account attribute.
    -
    -    Args:
    -      entry: An instance of a directive.
    -    Returns:
    -      The single account of this directive.
    -    """
    -    return (entry.account,)
    -
    +
    def _one(_, entry):
    +    """Process directives with a single account attribute.
    +
    +    Args:
    +      entry: An instance of a directive.
    +    Returns:
    +      The single account of this directive.
    +    """
    +    return (entry.account,)
    +
    @@ -9636,7 +12539,7 @@

    -beancount.core.getters.GetAccounts.Open(_, entry) +beancount.core.getters.GetAccounts.Open(_, entry)

    @@ -9679,16 +12582,16 @@

    Source code in beancount/core/getters.py -
    def _one(_, entry):
    -    """Process directives with a single account attribute.
    -
    -    Args:
    -      entry: An instance of a directive.
    -    Returns:
    -      The single account of this directive.
    -    """
    -    return (entry.account,)
    -
    +
    def _one(_, entry):
    +    """Process directives with a single account attribute.
    +
    +    Args:
    +      entry: An instance of a directive.
    +    Returns:
    +      The single account of this directive.
    +    """
    +    return (entry.account,)
    +
    @@ -9701,7 +12604,7 @@

    -beancount.core.getters.GetAccounts.Pad(_, entry) +beancount.core.getters.GetAccounts.Pad(_, entry)

    @@ -9744,16 +12647,16 @@

    Source code in beancount/core/getters.py -
    def Pad(_, entry):
    -    """Process a Pad directive.
    -
    -    Args:
    -      entry: An instance of Pad.
    -    Returns:
    -      The two accounts of the Pad directive.
    -    """
    -    return (entry.account, entry.source_account)
    -
    +
    def Pad(_, entry):
    +    """Process a Pad directive.
    +
    +    Args:
    +      entry: An instance of Pad.
    +    Returns:
    +      The two accounts of the Pad directive.
    +    """
    +    return (entry.account, entry.source_account)
    +
    @@ -9766,7 +12669,7 @@

    -beancount.core.getters.GetAccounts.Price(_, entry) +beancount.core.getters.GetAccounts.Price(_, entry)

    @@ -9809,16 +12712,16 @@

    Source code in beancount/core/getters.py -
    def _zero(_, entry):
    -    """Process directives with no accounts.
    -
    -    Args:
    -      entry: An instance of a directive.
    -    Returns:
    -      An empty list
    -    """
    -    return ()
    -
    +
    def _zero(_, entry):
    +    """Process directives with no accounts.
    +
    +    Args:
    +      entry: An instance of a directive.
    +    Returns:
    +      An empty list
    +    """
    +    return ()
    +
    @@ -9831,7 +12734,7 @@

    -beancount.core.getters.GetAccounts.Query(_, entry) +beancount.core.getters.GetAccounts.Query(_, entry)

    @@ -9874,16 +12777,16 @@

    Source code in beancount/core/getters.py -
    def _zero(_, entry):
    -    """Process directives with no accounts.
    -
    -    Args:
    -      entry: An instance of a directive.
    -    Returns:
    -      An empty list
    -    """
    -    return ()
    -
    +
    def _zero(_, entry):
    +    """Process directives with no accounts.
    +
    +    Args:
    +      entry: An instance of a directive.
    +    Returns:
    +      An empty list
    +    """
    +    return ()
    +
    @@ -9896,7 +12799,7 @@

    -beancount.core.getters.GetAccounts.Transaction(_, entry) +beancount.core.getters.GetAccounts.Transaction(_, entry)

    @@ -9925,17 +12828,17 @@

    Source code in beancount/core/getters.py -
    def Transaction(_, entry):
    -    """Process a Transaction directive.
    -
    -    Args:
    -      entry: An instance of Transaction.
    -    Yields:
    -      The accounts of the legs of the transaction.
    -    """
    -    for posting in entry.postings:
    -        yield posting.account
    -
    +
    def Transaction(_, entry):
    +    """Process a Transaction directive.
    +
    +    Args:
    +      entry: An instance of Transaction.
    +    Yields:
    +      The accounts of the legs of the transaction.
    +    """
    +    for posting in entry.postings:
    +        yield posting.account
    +
    @@ -9948,7 +12851,7 @@

    -beancount.core.getters.GetAccounts.get_accounts_use_map(self, entries) +beancount.core.getters.GetAccounts.get_accounts_use_map(self, entries)

    @@ -9992,25 +12895,25 @@

    Source code in beancount/core/getters.py -
    def get_accounts_use_map(self, entries):
    -    """Gather the list of accounts from the list of entries.
    -
    -    Args:
    -      entries: A list of directive instances.
    -    Returns:
    -      A pair of dictionaries of account name to date, one for first date
    -      used and one for last date used. The keys should be identical.
    -    """
    -    accounts_first = {}
    -    accounts_last = {}
    -    for entry in entries:
    -        method = getattr(self, entry.__class__.__name__)
    -        for account_ in method(entry):
    -            if account_ not in accounts_first:
    -                accounts_first[account_] = entry.date
    -            accounts_last[account_] = entry.date
    -    return accounts_first, accounts_last
    -
    +
    def get_accounts_use_map(self, entries):
    +    """Gather the list of accounts from the list of entries.
    +
    +    Args:
    +      entries: A list of directive instances.
    +    Returns:
    +      A pair of dictionaries of account name to date, one for first date
    +      used and one for last date used. The keys should be identical.
    +    """
    +    accounts_first = {}
    +    accounts_last = {}
    +    for entry in entries:
    +        method = getattr(self, entry.__class__.__name__)
    +        for account_ in method(entry):
    +            if account_ not in accounts_first:
    +                accounts_first[account_] = entry.date
    +            accounts_last[account_] = entry.date
    +    return accounts_first, accounts_last
    +
    @@ -10023,7 +12926,7 @@

    -beancount.core.getters.GetAccounts.get_entry_accounts(self, entry) +beancount.core.getters.GetAccounts.get_entry_accounts(self, entry)

    @@ -10068,20 +12971,20 @@

    Source code in beancount/core/getters.py -
    def get_entry_accounts(self, entry):
    -    """Gather all the accounts references by a single directive.
    -
    -    Note: This should get replaced by a method on each directive eventually,
    -    that would be the clean way to do this.
    -
    -    Args:
    -      entry: A directive instance.
    -    Returns:
    -      A set of account name strings.
    -    """
    -    method = getattr(self, entry.__class__.__name__)
    -    return set(method(entry))
    -
    +
    def get_entry_accounts(self, entry):
    +    """Gather all the accounts references by a single directive.
    +
    +    Note: This should get replaced by a method on each directive eventually,
    +    that would be the clean way to do this.
    +
    +    Args:
    +      entry: A directive instance.
    +    Returns:
    +      A set of account name strings.
    +    """
    +    method = getattr(self, entry.__class__.__name__)
    +    return set(method(entry))
    +
    @@ -10105,7 +13008,7 @@

    -beancount.core.getters.get_account_components(entries) +beancount.core.getters.get_account_components(entries)

    @@ -10149,21 +13052,21 @@

    Source code in beancount/core/getters.py -
    def get_account_components(entries):
    -    """Gather all the account components available in the given directives.
    -
    -    Args:
    -      entries: A list of directive instances.
    -    Returns:
    -      A list of strings, the unique account components, including the root
    -      account names.
    -    """
    -    accounts = get_accounts(entries)
    -    components = set()
    -    for account_name in accounts:
    -        components.update(account.split(account_name))
    -    return sorted(components)
    -
    +
    def get_account_components(entries):
    +    """Gather all the account components available in the given directives.
    +
    +    Args:
    +      entries: A list of directive instances.
    +    Returns:
    +      A list of strings, the unique account components, including the root
    +      account names.
    +    """
    +    accounts = get_accounts(entries)
    +    components = set()
    +    for account_name in accounts:
    +        components.update(account.split(account_name))
    +    return sorted(components)
    +
    @@ -10176,7 +13079,7 @@

    -beancount.core.getters.get_account_open_close(entries) +beancount.core.getters.get_account_open_close(entries)

    @@ -10222,33 +13125,33 @@

    Source code in beancount/core/getters.py -
    def get_account_open_close(entries):
    -    """Fetch the open/close entries for each of the accounts.
    -
    -    If an open or close entry happens to be duplicated, accept the earliest
    -    entry (chronologically).
    -
    -    Args:
    -      entries: A list of directive instances.
    -    Returns:
    -      A map of account name strings to pairs of (open-directive, close-directive)
    -      tuples.
    -    """
    -    # A dict of account name to (open-entry, close-entry).
    -    open_close_map = defaultdict(lambda: [None, None])
    -    for entry in entries:
    -        if not isinstance(entry, (Open, Close)):
    -            continue
    -        open_close = open_close_map[entry.account]
    -        index = 0 if isinstance(entry, Open) else 1
    -        previous_entry = open_close[index]
    -        if previous_entry is not None:
    -            if previous_entry.date <= entry.date:
    -                entry = previous_entry
    -        open_close[index] = entry
    -
    -    return dict(open_close_map)
    -
    +
    def get_account_open_close(entries):
    +    """Fetch the open/close entries for each of the accounts.
    +
    +    If an open or close entry happens to be duplicated, accept the earliest
    +    entry (chronologically).
    +
    +    Args:
    +      entries: A list of directive instances.
    +    Returns:
    +      A map of account name strings to pairs of (open-directive, close-directive)
    +      tuples.
    +    """
    +    # A dict of account name to (open-entry, close-entry).
    +    open_close_map = defaultdict(lambda: [None, None])
    +    for entry in entries:
    +        if not isinstance(entry, (Open, Close)):
    +            continue
    +        open_close = open_close_map[entry.account]
    +        index = 0 if isinstance(entry, Open) else 1
    +        previous_entry = open_close[index]
    +        if previous_entry is not None:
    +            if previous_entry.date <= entry.date:
    +                entry = previous_entry
    +        open_close[index] = entry
    +
    +    return dict(open_close_map)
    +
    @@ -10261,7 +13164,7 @@

    -beancount.core.getters.get_accounts(entries) +beancount.core.getters.get_accounts(entries)

    @@ -10304,17 +13207,17 @@

    Source code in beancount/core/getters.py -
    def get_accounts(entries):
    -    """Gather all the accounts references by a list of directives.
    -
    -    Args:
    -      entries: A list of directive instances.
    -    Returns:
    -      A set of account strings.
    -    """
    -    _, accounts_last = _GetAccounts.get_accounts_use_map(entries)
    -    return accounts_last.keys()
    -
    +
    def get_accounts(entries):
    +    """Gather all the accounts references by a list of directives.
    +
    +    Args:
    +      entries: A list of directive instances.
    +    Returns:
    +      A set of account strings.
    +    """
    +    _, accounts_last = _GetAccounts.get_accounts_use_map(entries)
    +    return accounts_last.keys()
    +
    @@ -10327,7 +13230,7 @@

    -beancount.core.getters.get_accounts_use_map(entries) +beancount.core.getters.get_accounts_use_map(entries)

    @@ -10371,17 +13274,17 @@

    Source code in beancount/core/getters.py -
    def get_accounts_use_map(entries):
    -    """Gather all the accounts references by a list of directives.
    -
    -    Args:
    -      entries: A list of directive instances.
    -    Returns:
    -      A pair of dictionaries of account name to date, one for first date
    -      used and one for last date used. The keys should be identical.
    -    """
    -    return _GetAccounts.get_accounts_use_map(entries)
    -
    +
    def get_accounts_use_map(entries):
    +    """Gather all the accounts references by a list of directives.
    +
    +    Args:
    +      entries: A list of directive instances.
    +    Returns:
    +      A pair of dictionaries of account name to date, one for first date
    +      used and one for last date used. The keys should be identical.
    +    """
    +    return _GetAccounts.get_accounts_use_map(entries)
    +
    @@ -10394,7 +13297,7 @@

    -beancount.core.getters.get_active_years(entries) +beancount.core.getters.get_active_years(entries)

    @@ -10423,24 +13326,24 @@

    Source code in beancount/core/getters.py -
    def get_active_years(entries):
    -    """Yield all the years that have at least one entry in them.
    -
    -    Args:
    -      entries: A list of directive instances.
    -    Yields:
    -      Unique dates see in the list of directives.
    -    """
    -    seen = set()
    -    prev_year = None
    -    for entry in entries:
    -        year = entry.date.year
    -        if year != prev_year:
    -            prev_year = year
    -            assert year not in seen
    -            seen.add(year)
    -            yield year
    -
    +
    def get_active_years(entries):
    +    """Yield all the years that have at least one entry in them.
    +
    +    Args:
    +      entries: A list of directive instances.
    +    Yields:
    +      Unique dates see in the list of directives.
    +    """
    +    seen = set()
    +    prev_year = None
    +    for entry in entries:
    +        year = entry.date.year
    +        if year != prev_year:
    +            prev_year = year
    +            assert year not in seen
    +            seen.add(year)
    +            yield year
    +
    @@ -10453,7 +13356,7 @@

    @@ -10496,22 +13399,22 @@

    -beancount.core.getters.get_all_payees(entries) +beancount.core.getters.get_all_payees(entries)

    @@ -10567,22 +13470,22 @@

    Source code in beancount/core/getters.py -
    def get_all_payees(entries):
    -    """Return a list of all the unique payees seen in the given entries.
    -
    -    Args:
    -      entries: A list of directive instances.
    -    Returns:
    -      A set of payee strings.
    -    """
    -    all_payees = set()
    -    for entry in entries:
    -        if not isinstance(entry, Transaction):
    -            continue
    -        all_payees.add(entry.payee)
    -    all_payees.discard(None)
    -    return sorted(all_payees)
    -
    +
    def get_all_payees(entries):
    +    """Return a list of all the unique payees seen in the given entries.
    +
    +    Args:
    +      entries: A list of directive instances.
    +    Returns:
    +      A set of payee strings.
    +    """
    +    all_payees = set()
    +    for entry in entries:
    +        if not isinstance(entry, Transaction):
    +            continue
    +        all_payees.add(entry.payee)
    +    all_payees.discard(None)
    +    return sorted(all_payees)
    +
    @@ -10595,7 +13498,7 @@

    -beancount.core.getters.get_all_tags(entries) +beancount.core.getters.get_all_tags(entries)

    @@ -10638,22 +13541,22 @@

    Source code in beancount/core/getters.py -
    def get_all_tags(entries):
    -    """Return a list of all the tags seen in the given entries.
    -
    -    Args:
    -      entries: A list of directive instances.
    -    Returns:
    -      A set of tag strings.
    -    """
    -    all_tags = set()
    -    for entry in entries:
    -        if not isinstance(entry, Transaction):
    -            continue
    -        if entry.tags:
    -            all_tags.update(entry.tags)
    -    return sorted(all_tags)
    -
    +
    def get_all_tags(entries):
    +    """Return a list of all the tags seen in the given entries.
    +
    +    Args:
    +      entries: A list of directive instances.
    +    Returns:
    +      A set of tag strings.
    +    """
    +    all_tags = set()
    +    for entry in entries:
    +        if not isinstance(entry, Transaction):
    +            continue
    +        if entry.tags:
    +            all_tags.update(entry.tags)
    +    return sorted(all_tags)
    +
    @@ -10665,11 +13568,11 @@

    -

    -beancount.core.getters.get_commodity_map(entries, create_missing=True) +

    +beancount.core.getters.get_commodity_directives(entries) -

    +
    @@ -10686,8 +13589,6 @@

    • entries – A list of directive instances.

    • -
    • create_missing – A boolean, true if you want to automatically generate -missing commodity directives if not present in the output map.

    @@ -10711,60 +13612,16 @@

    Source code in beancount/core/getters.py -
    def get_commodity_map(entries, create_missing=True):
    -    """Create map of commodity names to Commodity entries.
    -
    -    Args:
    -      entries: A list of directive instances.
    -      create_missing: A boolean, true if you want to automatically generate
    -        missing commodity directives if not present in the output map.
    -    Returns:
    -      A map of commodity name strings to Commodity directives.
    -    """
    -    if not entries:
    -        return {}
    -
    -    commodities_map = {}
    -    for entry in entries:
    -        if isinstance(entry, Commodity):
    -            commodities_map[entry.currency] = entry
    -
    -        elif isinstance(entry, Transaction):
    -            for posting in entry.postings:
    -
    -                # Main currency.
    -                units = posting.units
    -                commodities_map.setdefault(units.currency, None)
    -
    -                # Currency in cost.
    -                cost = posting.cost
    -                if cost:
    -                    commodities_map.setdefault(cost.currency, None)
    -
    -                # Currency in price.
    -                price = posting.price
    -                if price:
    -                    commodities_map.setdefault(price.currency, None)
    -
    -        elif isinstance(entry, Balance):
    -            commodities_map.setdefault(entry.amount.currency, None)
    -
    -        elif isinstance(entry, Price):
    -            commodities_map.setdefault(entry.currency, None)
    -
    -    if create_missing:
    -        # Create missing Commodity directives when they haven't been specified explicitly.
    -        # (I think it might be better to always do this from the loader.)
    -        date = entries[0].date
    -        meta = data.new_metadata('<getters>', 0)
    -        commodities_map = {
    -            commodity: (entry
    -                        if entry is not None
    -                        else Commodity(meta, date, commodity))
    -            for commodity, entry in commodities_map.items()}
    -
    -    return commodities_map
    -
    +
    def get_commodity_directives(entries):
    +    """Create map of commodity names to Commodity entries.
    +
    +    Args:
    +      entries: A list of directive instances.
    +    Returns:
    +      A map of commodity name strings to Commodity directives.
    +    """
    +    return {entry.currency: entry for entry in entries if isinstance(entry, Commodity)}
    +

    @@ -10777,7 +13634,7 @@

    -beancount.core.getters.get_dict_accounts(account_names) +beancount.core.getters.get_dict_accounts(account_names)

    @@ -10821,23 +13678,23 @@

    Source code in beancount/core/getters.py -
    def get_dict_accounts(account_names):
    -    """Return a nested dict of all the unique leaf names.
    -    account names are labelled with LABEL=True
    -
    -    Args:
    -      account_names: An iterable of account names (strings)
    -    Returns:
    -      A nested OrderedDict of account leafs
    -    """
    -    leveldict = OrderedDict()
    -    for account_name in account_names:
    -        nested_dict = leveldict
    -        for component in account.split(account_name):
    -            nested_dict = nested_dict.setdefault(component, OrderedDict())
    -        nested_dict[get_dict_accounts.ACCOUNT_LABEL] = True
    -    return leveldict
    -
    +
    def get_dict_accounts(account_names):
    +    """Return a nested dict of all the unique leaf names.
    +    account names are labelled with LABEL=True
    +
    +    Args:
    +      account_names: An iterable of account names (strings)
    +    Returns:
    +      A nested OrderedDict of account leafs
    +    """
    +    leveldict = OrderedDict()
    +    for account_name in account_names:
    +        nested_dict = leveldict
    +        for component in account.split(account_name):
    +            nested_dict = nested_dict.setdefault(component, OrderedDict())
    +        nested_dict[get_dict_accounts.ACCOUNT_LABEL] = True
    +    return leveldict
    +
    @@ -10850,7 +13707,7 @@

    -beancount.core.getters.get_entry_accounts(entry) +beancount.core.getters.get_entry_accounts(entry)

    @@ -10895,19 +13752,19 @@

    Source code in beancount/core/getters.py -
    def get_entry_accounts(entry):
    -    """Gather all the accounts references by a single directive.
    -
    -    Note: This should get replaced by a method on each directive eventually,
    -    that would be the clean way to do this.
    -
    -    Args:
    -      entries: A directive instance.
    -    Returns:
    -      A set of account strings.
    -    """
    -    return _GetAccounts.get_entry_accounts(entry)
    -
    +
    def get_entry_accounts(entry):
    +    """Gather all the accounts references by a single directive.
    +
    +    Note: This should get replaced by a method on each directive eventually,
    +    that would be the clean way to do this.
    +
    +    Args:
    +      entries: A directive instance.
    +    Returns:
    +      A set of account strings.
    +    """
    +    return _GetAccounts.get_entry_accounts(entry)
    +
    @@ -10920,7 +13777,7 @@

    -beancount.core.getters.get_leveln_parent_accounts(account_names, level, nrepeats=0) +beancount.core.getters.get_leveln_parent_accounts(account_names, level, nrepeats=0)

    @@ -10966,27 +13823,25 @@

    Source code in beancount/core/getters.py -
    def get_leveln_parent_accounts(account_names, level, nrepeats=0):
    -    """Return a list of all the unique leaf names at level N in an account hierarchy.
    -
    -    Args:
    -      account_names: A list of account names (strings)
    -      level: The level to cross-cut. 0 is for root accounts.
    -      nrepeats: A minimum number of times a leaf is required to be present in the
    -        the list of unique account names in order to be returned by this function.
    -    Returns:
    -      A list of leaf node names.
    -    """
    -    leveldict = defaultdict(int)
    -    for account_name in set(account_names):
    -        components = account.split(account_name)
    -        if level < len(components):
    -            leveldict[components[level]] += 1
    -    levels = {level_
    -              for level_, count in leveldict.items()
    -              if count > nrepeats}
    -    return sorted(levels)
    -
    +
    def get_leveln_parent_accounts(account_names, level, nrepeats=0):
    +    """Return a list of all the unique leaf names at level N in an account hierarchy.
    +
    +    Args:
    +      account_names: A list of account names (strings)
    +      level: The level to cross-cut. 0 is for root accounts.
    +      nrepeats: A minimum number of times a leaf is required to be present in the
    +        the list of unique account names in order to be returned by this function.
    +    Returns:
    +      A list of leaf node names.
    +    """
    +    leveldict = defaultdict(int)
    +    for account_name in set(account_names):
    +        components = account.split(account_name)
    +        if level < len(components):
    +            leveldict[components[level]] += 1
    +    levels = {level_ for level_, count in leveldict.items() if count > nrepeats}
    +    return sorted(levels)
    +
    @@ -10999,7 +13854,7 @@

    -beancount.core.getters.get_min_max_dates(entries, types=None) +beancount.core.getters.get_min_max_dates(entries, types=None)

    @@ -11044,32 +13899,32 @@

    Source code in beancount/core/getters.py -
    def get_min_max_dates(entries, types=None):
    -    """Return the minimum and maximum dates in the list of entries.
    -
    -    Args:
    -      entries: A list of directive instances.
    -      types: An optional tuple of types to restrict the entries to.
    -    Returns:
    -      A pair of datetime.date dates, the minimum and maximum dates seen in the
    -      directives.
    -    """
    -    date_first = date_last = None
    -
    -    for entry in entries:
    -        if types and not isinstance(entry, types):
    -            continue
    -        date_first = entry.date
    -        break
    -
    -    for entry in reversed(entries):
    -        if types and not isinstance(entry, types):
    -            continue
    -        date_last = entry.date
    -        break
    -
    -    return (date_first, date_last)
    -
    +
    def get_min_max_dates(entries, types=None):
    +    """Return the minimum and maximum dates in the list of entries.
    +
    +    Args:
    +      entries: A list of directive instances.
    +      types: An optional tuple of types to restrict the entries to.
    +    Returns:
    +      A pair of datetime.date dates, the minimum and maximum dates seen in the
    +      directives.
    +    """
    +    date_first = date_last = None
    +
    +    for entry in entries:
    +        if types and not isinstance(entry, types):
    +            continue
    +        date_first = entry.date
    +        break
    +
    +    for entry in reversed(entries):
    +        if types and not isinstance(entry, types):
    +            continue
    +        date_last = entry.date
    +        break
    +
    +    return (date_first, date_last)
    +
    @@ -11082,7 +13937,7 @@

    -beancount.core.getters.get_values_meta(name_to_entries_map, *meta_keys, *, default=None) +beancount.core.getters.get_values_meta(name_to_entries_map, *meta_keys, *, default=None)

    @@ -11134,36 +13989,34 @@

    Source code in beancount/core/getters.py -
    def get_values_meta(name_to_entries_map, *meta_keys, default=None):
    -    """Get a map of the metadata from a map of entries values.
    -
    -    Given a dict of some key to a directive instance (or None), return a mapping
    -    of the key to the metadata extracted from each directive, or a default
    -    value. This can be used to gather a particular piece of metadata from an
    -    accounts map or a commodities map.
    -
    -    Args:
    -      name_to_entries_map: A dict of something to an entry or None.
    -      meta_keys: A list of strings, the keys to fetch from the metadata.
    -      default: The default value to use if the metadata is not available or if
    -        the value/entry is None.
    -    Returns:
    -      A mapping of the keys of name_to_entries_map to the values of the 'meta_keys'
    -      metadata. If there are multiple 'meta_keys', each value is a tuple of them.
    -      On the other hand, if there is only a single one, the value itself is returned.
    -    """
    -    value_map = {}
    -    for key, entry in name_to_entries_map.items():
    -        value_list = []
    -        for meta_key in meta_keys:
    -            value_list.append(entry.meta.get(meta_key, default)
    -                              if entry is not None
    -                              else default)
    -        value_map[key] = (value_list[0]
    -                          if len(meta_keys) == 1
    -                          else tuple(value_list))
    -    return value_map
    -
    +
    def get_values_meta(name_to_entries_map, *meta_keys, default=None):
    +    """Get a map of the metadata from a map of entries values.
    +
    +    Given a dict of some key to a directive instance (or None), return a mapping
    +    of the key to the metadata extracted from each directive, or a default
    +    value. This can be used to gather a particular piece of metadata from an
    +    accounts map or a commodities map.
    +
    +    Args:
    +      name_to_entries_map: A dict of something to an entry or None.
    +      meta_keys: A list of strings, the keys to fetch from the metadata.
    +      default: The default value to use if the metadata is not available or if
    +        the value/entry is None.
    +    Returns:
    +      A mapping of the keys of name_to_entries_map to the values of the 'meta_keys'
    +      metadata. If there are multiple 'meta_keys', each value is a tuple of them.
    +      On the other hand, if there is only a single one, the value itself is returned.
    +    """
    +    value_map = {}
    +    for key, entry in name_to_entries_map.items():
    +        value_list = []
    +        for meta_key in meta_keys:
    +            value_list.append(
    +                entry.meta.get(meta_key, default) if entry is not None else default
    +            )
    +        value_map[key] = value_list[0] if len(meta_keys) == 1 else tuple(value_list)
    +    return value_map
    +
    @@ -11249,7 +14102,7 @@

    -beancount.core.interpolate.BalanceError.__getnewargs__(self) +beancount.core.interpolate.BalanceError.__getnewargs__(self) special @@ -11263,10 +14116,10 @@

    Source code in beancount/core/interpolate.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -11279,7 +14132,7 @@

    -beancount.core.interpolate.BalanceError.__new__(_cls, source, message, entry) +beancount.core.interpolate.BalanceError.__new__(_cls, source, message, entry) special @@ -11303,7 +14156,7 @@

    -beancount.core.interpolate.BalanceError.__repr__(self) +beancount.core.interpolate.BalanceError.__repr__(self) special @@ -11317,10 +14170,10 @@

    Source code in beancount/core/interpolate.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -11344,7 +14197,7 @@

    -beancount.core.interpolate.compute_entries_balance(entries, prefix=None, date=None) +beancount.core.interpolate.compute_entries_balance(entries, prefix=None, date=None)

    @@ -11393,31 +14246,31 @@

    Source code in beancount/core/interpolate.py -
    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
    -
    +
    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
    +
    @@ -11430,7 +14283,7 @@

    -beancount.core.interpolate.compute_entry_context(entries, context_entry) +beancount.core.interpolate.compute_entry_context(entries, context_entry, additional_accounts=None)

    @@ -11454,6 +14307,10 @@

    Source code in beancount/core/interpolate.py -
    def compute_entry_context(entries, context_entry):
    -    """Compute the balances of all accounts referenced by entry up to entry.
    -
    -    This provides the inventory of the accounts to which the entry is to be
    -    applied, before and after.
    -
    -    Args:
    -      entries: A list of directives.
    -      context_entry: The entry for which we want to obtain the before and after
    -        context.
    -    Returns:
    -      Two dicts of account-name to Inventory instance, one which represents the
    -      context before the entry is applied, and one that represents the context
    -      after it has been applied.
    -    """
    -    assert context_entry is not None, "context_entry is missing."
    -
    -    # Get the set of accounts for which to compute the context.
    -    context_accounts = getters.get_entry_accounts(context_entry)
    -
    -    # Iterate over the entries until we find the target one and accumulate the
    -    # balance.
    -    context_before = collections.defaultdict(inventory.Inventory)
    -    for entry in entries:
    -        if entry is context_entry:
    -            break
    -        if isinstance(entry, Transaction):
    -            for posting in entry.postings:
    -                if not any(posting.account == account
    -                           for account in context_accounts):
    -                    continue
    -                balance = context_before[posting.account]
    -                balance.add_position(posting)
    -
    -    # Compute the after context for the entry.
    -    context_after = copy.deepcopy(context_before)
    -    if isinstance(context_entry, Transaction):
    -        for posting in entry.postings:
    -            balance = context_after[posting.account]
    -            balance.add_position(posting)
    -
    -    return context_before, context_after
    -
    +
    def compute_entry_context(entries, context_entry, additional_accounts=None):
    +    """Compute the balances of all accounts referenced by entry up to entry.
    +
    +    This provides the inventory of the accounts to which the entry is to be
    +    applied, before and after.
    +
    +    Args:
    +      entries: A list of directives.
    +      context_entry: The entry for which we want to obtain the before and after
    +        context.
    +      additional_accounts: Additional list of accounts to include in calculating
    +        the balance. This is used when invoked for debugging, in case the booked
    +        & interpolated transaction doesn't have all the accounts we need because
    +        it had an error (the booking code will remove invalid postings).
    +    Returns:
    +      Two dicts of account-name to Inventory instance, one which represents the
    +      context before the entry is applied, and one that represents the context
    +      after it has been applied.
    +    """
    +    assert context_entry is not None, "context_entry is missing."
    +
    +    # Get the set of accounts for which to compute the context.
    +    context_accounts = getters.get_entry_accounts(context_entry)
    +    if additional_accounts:
    +        context_accounts.update(additional_accounts)
    +
    +    # Iterate over the entries until we find the target one and accumulate the
    +    # balance.
    +    context_before = collections.defaultdict(inventory.Inventory)
    +    for entry in entries:
    +        if entry is context_entry:
    +            break
    +        if isinstance(entry, Transaction):
    +            for posting in entry.postings:
    +                if not any(posting.account == account for account in context_accounts):
    +                    continue
    +                balance = context_before[posting.account]
    +                balance.add_position(posting)
    +
    +    # Compute the after context for the entry.
    +    context_after = copy.deepcopy(context_before)
    +    if isinstance(context_entry, Transaction):
    +        for posting in entry.postings:
    +            balance = context_after[posting.account]
    +            balance.add_position(posting)
    +
    +    return context_before, context_after
    +
    @@ -11534,7 +14396,7 @@

    -beancount.core.interpolate.compute_residual(postings) +beancount.core.interpolate.compute_residual(postings)

    @@ -11582,30 +14444,30 @@

    Source code in beancount/core/interpolate.py -
    def compute_residual(postings):
    -    """Compute the residual of a set of complete postings, and the per-currency precision.
    -
    -    This is used to cross-check a balanced transaction.
    -
    -    The precision is the maximum fraction that is being used for each currency
    -    (a dict). We use the currency of the weight amount in order to infer the
    -    quantization precision for each currency. Integer amounts aren't
    -    contributing to the determination of precision.
    -
    -    Args:
    -      postings: A list of Posting instances.
    -    Returns:
    -      An instance of Inventory, with the residual of the given list of postings.
    -    """
    -    inventory = Inventory()
    -    for posting in postings:
    -        # Skip auto-postings inserted to absorb the residual (rounding error).
    -        if posting.meta and posting.meta.get(AUTOMATIC_RESIDUAL, False):
    -            continue
    -        # Add to total residual balance.
    -        inventory.add_amount(convert.get_weight(posting))
    -    return inventory
    -
    +
    def compute_residual(postings):
    +    """Compute the residual of a set of complete postings, and the per-currency precision.
    +
    +    This is used to cross-check a balanced transaction.
    +
    +    The precision is the maximum fraction that is being used for each currency
    +    (a dict). We use the currency of the weight amount in order to infer the
    +    quantization precision for each currency. Integer amounts aren't
    +    contributing to the determination of precision.
    +
    +    Args:
    +      postings: A list of Posting instances.
    +    Returns:
    +      An instance of Inventory, with the residual of the given list of postings.
    +    """
    +    inventory = Inventory()
    +    for posting in postings:
    +        # Skip auto-postings inserted to absorb the residual (rounding error).
    +        if posting.meta and posting.meta.get(AUTOMATIC_RESIDUAL, False):
    +            continue
    +        # Add to total residual balance.
    +        inventory.add_amount(convert.get_weight(posting))
    +    return inventory
    +
    @@ -11618,7 +14480,7 @@

    -beancount.core.interpolate.fill_residual_posting(entry, account_rounding) +beancount.core.interpolate.fill_residual_posting(entry, account_rounding)

    @@ -11670,32 +14532,32 @@

    Source code in beancount/core/interpolate.py -
    def fill_residual_posting(entry, account_rounding):
    -    """If necessary, insert a posting to absorb the residual.
    -    This makes the transaction balance exactly.
    -
    -    Note: This was developed in order to tweak transactions before exporting
    -    them to Ledger. A better method would be to enable the feature that
    -    automatically inserts these rounding postings on all transactions, and so
    -    maybe this method can be deprecated if we do so.
    -
    -    Args:
    -      entry: An instance of a Transaction.
    -      account_rounding: A string, the name of the rounding account that
    -        absorbs residuals / rounding errors.
    -    Returns:
    -      A possibly new, modified entry with a new posting. If a residual
    -      was not needed - the transaction already balanced perfectly - no new
    -      leg is inserted.
    -
    -    """
    -    residual = compute_residual(entry.postings)
    -    if not residual.is_empty():
    -        new_postings = list(entry.postings)
    -        new_postings.extend(get_residual_postings(residual, account_rounding))
    -        entry = entry._replace(postings=new_postings)
    -    return entry
    -
    +
    def fill_residual_posting(entry, account_rounding):
    +    """If necessary, insert a posting to absorb the residual.
    +    This makes the transaction balance exactly.
    +
    +    Note: This was developed in order to tweak transactions before exporting
    +    them to Ledger. A better method would be to enable the feature that
    +    automatically inserts these rounding postings on all transactions, and so
    +    maybe this method can be deprecated if we do so.
    +
    +    Args:
    +      entry: An instance of a Transaction.
    +      account_rounding: A string, the name of the rounding account that
    +        absorbs residuals / rounding errors.
    +    Returns:
    +      A possibly new, modified entry with a new posting. If a residual
    +      was not needed - the transaction already balanced perfectly - no new
    +      leg is inserted.
    +
    +    """
    +    residual = compute_residual(entry.postings)
    +    if not residual.is_empty():
    +        new_postings = list(entry.postings)
    +        new_postings.extend(get_residual_postings(residual, account_rounding))
    +        entry = entry._replace(postings=new_postings)
    +    return entry
    +
    @@ -11708,7 +14570,7 @@

    -beancount.core.interpolate.get_residual_postings(residual, account_rounding) +beancount.core.interpolate.get_residual_postings(residual, account_rounding)

    @@ -11753,22 +14615,22 @@

    Source code in beancount/core/interpolate.py -
    def get_residual_postings(residual, account_rounding):
    -    """Create postings to book the given residuals.
    -
    -    Args:
    -      residual: An Inventory, the residual positions.
    -      account_rounding: A string, the name of the rounding account that
    -        absorbs residuals / rounding errors.
    -    Returns:
    -      A list of new postings to be inserted to reduce the given residual.
    -    """
    -    meta = {AUTOMATIC_META: True,
    -            AUTOMATIC_RESIDUAL: True}
    -    return [Posting(account_rounding, -position.units, position.cost, None, None,
    -                    meta.copy())
    -            for position in residual.get_positions()]
    -
    +
    def get_residual_postings(residual, account_rounding):
    +    """Create postings to book the given residuals.
    +
    +    Args:
    +      residual: An Inventory, the residual positions.
    +      account_rounding: A string, the name of the rounding account that
    +        absorbs residuals / rounding errors.
    +    Returns:
    +      A list of new postings to be inserted to reduce the given residual.
    +    """
    +    meta = {AUTOMATIC_META: True, AUTOMATIC_RESIDUAL: True}
    +    return [
    +        Posting(account_rounding, -position.units, position.cost, None, None, meta.copy())
    +        for position in residual.get_positions()
    +    ]
    +
    @@ -11781,7 +14643,7 @@

    -beancount.core.interpolate.has_nontrivial_balance(posting) +beancount.core.interpolate.has_nontrivial_balance(posting)

    @@ -11824,16 +14686,16 @@

    Source code in beancount/core/interpolate.py -
    def has_nontrivial_balance(posting):
    -    """Return True if a Posting has a balance amount that would have to be calculated.
    -
    -    Args:
    -      posting: A Posting instance.
    -    Returns:
    -      A boolean.
    -    """
    -    return posting.cost or posting.price
    -
    +
    def has_nontrivial_balance(posting):
    +    """Return True if a Posting has a balance amount that would have to be calculated.
    +
    +    Args:
    +      posting: A Posting instance.
    +    Returns:
    +      A boolean.
    +    """
    +    return posting.cost or posting.price
    +
    @@ -11846,7 +14708,7 @@

    -beancount.core.interpolate.infer_tolerances(postings, options_map, use_cost=None) +beancount.core.interpolate.infer_tolerances(postings, options_map, use_cost=None)

    @@ -11920,110 +14782,114 @@

    Source code in beancount/core/interpolate.py -
    def infer_tolerances(postings, options_map, use_cost=None):
    -    """Infer tolerances from a list of postings.
    -
    -    The tolerance is the maximum fraction that is being used for each currency
    -    (a dict). We use the currency of the weight amount in order to infer the
    -    quantization precision for each currency. Integer amounts aren't
    -    contributing to the determination of precision.
    -
    -    The 'use_cost' option allows one to experiment with letting postings at cost
    -    and at price influence the maximum value of the tolerance. It's tricky to
    -    use and alters the definition of the tolerance in a non-trivial way, if you
    -    use it. The tolerance is expanded by the sum of the cost times a fraction 'M'
    -    of the smallest digits in the number of units for all postings held at cost.
    -
    -    For example, in this transaction:
    -
    -        2006-01-17 * "Plan Contribution"
    -          Assets:Investments:VWELX 18.572 VWELX {30.96 USD}
    -          Assets:Investments:VWELX 18.572 VWELX {30.96 USD}
    -          Assets:Investments:Cash -1150.00 USD
    -
    -    The tolerance for units of USD will calculated as the MAXIMUM of:
    -
    -      0.01 * M = 0.005 (from the 1150.00 USD leg)
    -
    -      The sum of
    -        0.001 * M x 30.96 = 0.01548 +
    -        0.001 * M x 30.96 = 0.01548
    -                          = 0.03096
    -
    -    So the tolerance for USD in this case is max(0.005, 0.03096) = 0.03096. Prices
    -    contribute similarly to the maximum tolerance allowed.
    -
    -    Note that 'M' above is the inferred_tolerance_multiplier and its default
    -    value is 0.5.
    -
    -    Args:
    -      postings: A list of Posting instances.
    -      options_map: A dict of options.
    -      use_cost: A boolean, true if we should be using a combination of the smallest
    -        digit of the number times the cost or price in order to infer the tolerance.
    -        If the value is left unspecified (as 'None'), the default value can be
    -        overridden by setting an option.
    -    Returns:
    -      A dict of currency to the tolerated difference amount to be used for it,
    -      e.g. 0.005.
    -    """
    -    if use_cost is None:
    -        use_cost = options_map["infer_tolerance_from_cost"]
    -
    -    inferred_tolerance_multiplier = options_map["inferred_tolerance_multiplier"]
    -
    -    default_tolerances = options_map["inferred_tolerance_default"]
    -    tolerances = default_tolerances.copy()
    -
    -    cost_tolerances = collections.defaultdict(D)
    -    for posting in postings:
    -        # Skip the precision on automatically inferred postings.
    -        if posting.meta and AUTOMATIC_META in posting.meta:
    -            continue
    -        units = posting.units
    -        if not (isinstance(units, Amount) and isinstance(units.number, Decimal)):
    -            continue
    -
    -        # Compute bounds on the number.
    -        currency = units.currency
    -        expo = units.number.as_tuple().exponent
    -        if expo < 0:
    -            # Note: the exponent is a negative value.
    -            tolerance = ONE.scaleb(expo) * inferred_tolerance_multiplier
    -            tolerances[currency] = max(tolerance,
    -                                       tolerances.get(currency, -1024))
    -
    -            if not use_cost:
    -                continue
    -
    -            # Compute bounds on the smallest digit of the number implied as cost.
    -            cost = posting.cost
    -            if cost is not None:
    -                cost_currency = cost.currency
    -                if isinstance(cost, Cost):
    -                    cost_tolerance = min(tolerance * cost.number, MAXIMUM_TOLERANCE)
    -                else:
    -                    assert isinstance(cost, CostSpec)
    -                    cost_tolerance = MAXIMUM_TOLERANCE
    -                    for cost_number in cost.number_total, cost.number_per:
    -                        if cost_number is None or cost_number is MISSING:
    -                            continue
    -                        cost_tolerance = min(tolerance * cost_number, cost_tolerance)
    -                cost_tolerances[cost_currency] += cost_tolerance
    -
    -            # Compute bounds on the smallest digit of the number implied as cost.
    -            price = posting.price
    -            if isinstance(price, Amount) and isinstance(price.number, Decimal):
    -                price_currency = price.currency
    -                price_tolerance = min(tolerance * price.number, MAXIMUM_TOLERANCE)
    -                cost_tolerances[price_currency] += price_tolerance
    -
    -    for currency, tolerance in cost_tolerances.items():
    -        tolerances[currency] = max(tolerance, tolerances.get(currency, -1024))
    -
    -    default = tolerances.pop('*', ZERO)
    -    return defdict.ImmutableDictWithDefault(tolerances, default=default)
    -
    +
    def infer_tolerances(postings, options_map, use_cost=None):
    +    """Infer tolerances from a list of postings.
    +
    +    The tolerance is the maximum fraction that is being used for each currency
    +    (a dict). We use the currency of the weight amount in order to infer the
    +    quantization precision for each currency. Integer amounts aren't
    +    contributing to the determination of precision.
    +
    +    The 'use_cost' option allows one to experiment with letting postings at cost
    +    and at price influence the maximum value of the tolerance. It's tricky to
    +    use and alters the definition of the tolerance in a non-trivial way, if you
    +    use it. The tolerance is expanded by the sum of the cost times a fraction 'M'
    +    of the smallest digits in the number of units for all postings held at cost.
    +
    +    For example, in this transaction:
    +
    +        2006-01-17 * "Plan Contribution"
    +          Assets:Investments:VWELX 18.572 VWELX {30.96 USD}
    +          Assets:Investments:VWELX 18.572 VWELX {30.96 USD}
    +          Assets:Investments:Cash -1150.00 USD
    +
    +    The tolerance for units of USD will calculated as the MAXIMUM of:
    +
    +      0.01 * M = 0.005 (from the 1150.00 USD leg)
    +
    +      The sum of
    +        0.001 * M x 30.96 = 0.01548 +
    +        0.001 * M x 30.96 = 0.01548
    +                          = 0.03096
    +
    +    So the tolerance for USD in this case is max(0.005, 0.03096) = 0.03096. Prices
    +    contribute similarly to the maximum tolerance allowed.
    +
    +    Note that 'M' above is the inferred_tolerance_multiplier and its default
    +    value is 0.5.
    +
    +    Args:
    +      postings: A list of Posting instances.
    +      options_map: A dict of options.
    +      use_cost: A boolean, true if we should be using a combination of the smallest
    +        digit of the number times the cost or price in order to infer the tolerance.
    +        If the value is left unspecified (as 'None'), the default value can be
    +        overridden by setting an option.
    +    Returns:
    +      A dict of currency to the tolerated difference amount to be used for it,
    +      e.g. 0.005.
    +    """
    +    if use_cost is None:
    +        use_cost = options_map["infer_tolerance_from_cost"]
    +
    +    inferred_tolerance_multiplier = options_map["inferred_tolerance_multiplier"]
    +
    +    default_tolerances = options_map["inferred_tolerance_default"]
    +    tolerances = default_tolerances.copy()
    +
    +    cost_tolerances = collections.defaultdict(D)
    +    for posting in postings:
    +        # Skip the precision on automatically inferred postings.
    +        if posting.meta and AUTOMATIC_META in posting.meta:
    +            continue
    +        units = posting.units
    +        if not (isinstance(units, Amount) and isinstance(units.number, Decimal)):
    +            continue
    +
    +        # Compute bounds on the number.
    +        currency = units.currency
    +        expo = units.number.as_tuple().exponent
    +        if expo < 0:
    +            # Note: the exponent is a negative value.
    +            tolerance = ONE.scaleb(expo) * inferred_tolerance_multiplier
    +
    +            # Note that we take the max() and not the min() here because the
    +            # tolerance has a dual purpose: it's used to infer the resolution
    +            # for interpolation (where we might want the min()) and also for
    +            # balance checks (where we favor the looser/larger tolerance).
    +            tolerances[currency] = max(tolerance, tolerances.get(currency, -1024))
    +
    +            if not use_cost:
    +                continue
    +
    +            # Compute bounds on the smallest digit of the number implied as cost.
    +            cost = posting.cost
    +            if cost is not None:
    +                cost_currency = cost.currency
    +                if isinstance(cost, Cost):
    +                    cost_tolerance = min(tolerance * cost.number, MAXIMUM_TOLERANCE)
    +                else:
    +                    assert isinstance(cost, CostSpec)
    +                    cost_tolerance = MAXIMUM_TOLERANCE
    +                    for cost_number in cost.number_total, cost.number_per:
    +                        if cost_number is None or cost_number is MISSING:
    +                            continue
    +                        cost_tolerance = min(tolerance * cost_number, cost_tolerance)
    +                cost_tolerances[cost_currency] += cost_tolerance
    +
    +            # Compute bounds on the smallest digit of the number implied as cost.
    +            price = posting.price
    +            if isinstance(price, Amount) and isinstance(price.number, Decimal):
    +                price_currency = price.currency
    +                price_tolerance = min(tolerance * price.number, MAXIMUM_TOLERANCE)
    +                cost_tolerances[price_currency] += price_tolerance
    +
    +    for currency, tolerance in cost_tolerances.items():
    +        tolerances[currency] = max(tolerance, tolerances.get(currency, -1024))
    +
    +    default = tolerances.pop("*", ZERO)
    +    return defdict.ImmutableDictWithDefault(tolerances, default=default)
    +
    @@ -12036,7 +14902,7 @@

    -beancount.core.interpolate.is_tolerance_user_specified(tolerance) +beancount.core.interpolate.is_tolerance_user_specified(tolerance)

    @@ -12082,20 +14948,20 @@

    Source code in beancount/core/interpolate.py -
    def is_tolerance_user_specified(tolerance):
    -    """Return true if the given tolerance number was user-specified.
    -
    -    This would allow the user to provide a tolerance like # 0.1234 but not
    -    0.123456. This is used to detect whether a tolerance value # is input by the
    -    user and not inferred automatically.
    -
    -    Args:
    -      tolerance: An instance of Decimal.
    -    Returns:
    -      A boolean.
    -    """
    -    return len(tolerance.as_tuple().digits) < MAX_TOLERANCE_DIGITS
    -
    +
    def is_tolerance_user_specified(tolerance):
    +    """Return true if the given tolerance number was user-specified.
    +
    +    This would allow the user to provide a tolerance like # 0.1234 but not
    +    0.123456. This is used to detect whether a tolerance value # is input by the
    +    user and not inferred automatically.
    +
    +    Args:
    +      tolerance: An instance of Decimal.
    +    Returns:
    +      A boolean.
    +    """
    +    return len(tolerance.as_tuple().digits) < MAX_TOLERANCE_DIGITS
    +
    @@ -12108,7 +14974,7 @@

    -beancount.core.interpolate.quantize_with_tolerance(tolerances, currency, number) +beancount.core.interpolate.quantize_with_tolerance(tolerances, currency, number)

    @@ -12153,34 +15019,34 @@

    Source code in beancount/core/interpolate.py -
    def quantize_with_tolerance(tolerances, currency, number):
    -    """Quantize the units using the tolerance dict.
    -
    -    Args:
    -      tolerances: A dict of currency to tolerance Decimalvalues.
    -      number: A number to quantize.
    -      currency: A string currency.
    -    Returns:
    -      A Decimal, the number possibly quantized.
    -    """
    -    # Applying rounding to the default tolerance, if there is one.
    -    tolerance = tolerances.get(currency)
    -    if tolerance:
    -        quantum = (tolerance * 2).normalize()
    -
    -        # If the tolerance is a neat number provided by the user,
    -        # quantize the inferred numbers. See doc on quantize():
    -        #
    -        # Unlike other operations, if the length of the coefficient
    -        # after the quantize operation would be greater than
    -        # precision, then an InvalidOperation is signaled. This
    -        # guarantees that, unless there is an error condition, the
    -        # quantized exponent is always equal to that of the
    -        # right-hand operand.
    -        if is_tolerance_user_specified(quantum):
    -            number = number.quantize(quantum)
    -    return number
    -
    +
    def quantize_with_tolerance(tolerances, currency, number):
    +    """Quantize the units using the tolerance dict.
    +
    +    Args:
    +      tolerances: A dict of currency to tolerance Decimalvalues.
    +      number: A number to quantize.
    +      currency: A string currency.
    +    Returns:
    +      A Decimal, the number possibly quantized.
    +    """
    +    # Applying rounding to the default tolerance, if there is one.
    +    tolerance = tolerances.get(currency)
    +    if tolerance:
    +        quantum = (tolerance * 2).normalize()
    +
    +        # If the tolerance is a neat number provided by the user,
    +        # quantize the inferred numbers. See doc on quantize():
    +        #
    +        # Unlike other operations, if the length of the coefficient
    +        # after the quantize operation would be greater than
    +        # precision, then an InvalidOperation is signaled. This
    +        # guarantees that, unless there is an error condition, the
    +        # quantized exponent is always equal to that of the
    +        # right-hand operand.
    +        if is_tolerance_user_specified(quantum):
    +            number = number.quantize(quantum)
    +    return number
    +
    @@ -12227,6 +15093,14 @@

    This is meant to accommodate both booked and non-booked amounts. The clever trick that we pull to do this is that for positions which aren't booked, we simply leave the 'cost' as None. This is the case for most of the transactions.

    +

    = Conversions =

    +

    If it often desired to convert this inventory into an equivalent position for +its cost, or to just flatten all the positions with the same currency and count +the number of units, or to compute the market value for the inventory at a +specific date. You do these conversions using the reduce() method:

    +

    inventory.reduce(convert.get_cost) + inventory.reduce(convert.get_units) + inventory.reduce(convert.get_value, price_map, date)

    @@ -12241,50 +15115,6 @@

    -
    - - - -

    - -beancount.core.inventory.Booking (Enum) - - - - -

    - -
    - -

    Result of booking a new lot to an existing inventory.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - -
    @@ -12300,25 +15130,8 @@

    -

    An Inventory is a set of positions.

    +

    An Inventory is a set of positions, indexed for efficiency.

    -

    Attributes:

    - - - - - - - - - - - - - - - -
    NameTypeDescription
    positions

    A list of Position instances, held in this Inventory object.

    @@ -12337,7 +15150,7 @@

    -beancount.core.inventory.Inventory.__abs__(self) +beancount.core.inventory.Inventory.__abs__(self) special @@ -12367,14 +15180,14 @@

    Source code in beancount/core/inventory.py -
    def __abs__(self):
    -    """Return an inventory with the absolute value of each position.
    -
    -    Returns:
    -      An instance of Inventory.
    -    """
    -    return Inventory({key: abs(pos) for key, pos in self.items()})
    -
    +
    def __abs__(self):
    +    """Return an inventory with the absolute value of each position.
    +
    +    Returns:
    +      An instance of Inventory.
    +    """
    +    return Inventory({key: abs(pos) for key, pos in self.items()})
    +

    @@ -12387,7 +15200,7 @@

    -beancount.core.inventory.Inventory.__add__(self, other) +beancount.core.inventory.Inventory.__add__(self, other) special @@ -12433,18 +15246,18 @@

    Source code in beancount/core/inventory.py -
    def __add__(self, other):
    -    """Add another inventory to this one. This inventory is not modified.
    -
    -    Args:
    -      other: An instance of Inventory.
    -    Returns:
    -      A new instance of Inventory.
    -    """
    -    new_inventory = self.__copy__()
    -    new_inventory.add_inventory(other)
    -    return new_inventory
    -
    +
    def __add__(self, other):
    +    """Add another inventory to this one. This inventory is not modified.
    +
    +    Args:
    +      other: An instance of Inventory.
    +    Returns:
    +      A new instance of Inventory.
    +    """
    +    new_inventory = self.__copy__()
    +    new_inventory.add_inventory(other)
    +    return new_inventory
    +

    @@ -12458,7 +15271,7 @@

    -beancount.core.inventory.Inventory.__copy__(self) +beancount.core.inventory.Inventory.__copy__(self) special @@ -12488,14 +15301,14 @@

    Source code in beancount/core/inventory.py -
    def __copy__(self):
    -    """A shallow copy of this inventory object.
    -
    -    Returns:
    -      An instance of Inventory, equal to this one.
    -    """
    -    return Inventory(self)
    -
    +
    def __copy__(self):
    +    """A shallow copy of this inventory object.
    +
    +    Returns:
    +      An instance of Inventory, equal to this one.
    +    """
    +    return Inventory(self)
    +
    @@ -12508,7 +15321,7 @@

    -beancount.core.inventory.Inventory.__iadd__(self, other) +beancount.core.inventory.Inventory.__iadd__(self, other) special @@ -12554,25 +15367,25 @@

    Source code in beancount/core/inventory.py -
    def add_inventory(self, other):
    -    """Add all the positions of another Inventory instance to this one.
    -
    -    Args:
    -      other: An instance of Inventory to add to this one.
    -    Returns:
    -      This inventory, modified.
    -    """
    -    if self.is_empty():
    -        # Optimization for empty inventories; if the current one is empty,
    -        # adopt all of the other inventory's positions without running
    -        # through the full aggregation checks. This should be very cheap. We
    -        # can do this because the positions are immutable.
    -        self.update(other)
    -    else:
    -        for position in other.get_positions():
    -            self.add_position(position)
    -    return self
    -
    +
    def add_inventory(self, other):
    +    """Add all the positions of another Inventory instance to this one.
    +
    +    Args:
    +      other: An instance of Inventory to add to this one.
    +    Returns:
    +      This inventory, modified.
    +    """
    +    if self.is_empty():
    +        # Optimization for empty inventories; if the current one is empty,
    +        # adopt all of the other inventory's positions without running
    +        # through the full aggregation checks. This should be very cheap. We
    +        # can do this because the positions are immutable.
    +        self.update(other)
    +    else:
    +        for position in other.get_positions():
    +            self.add_position(position)
    +    return self
    +
    @@ -12585,7 +15398,7 @@

    -beancount.core.inventory.Inventory.__init__(self, positions=None) +beancount.core.inventory.Inventory.__init__(self, positions=None) special @@ -12616,22 +15429,22 @@

    Source code in beancount/core/inventory.py -
    def __init__(self, positions=None):
    -    """Create a new inventory using a list of existing positions.
    -
    -    Args:
    -      positions: A list of Position instances or an existing dict or
    -        Inventory instance.
    -    """
    -    if isinstance(positions, (dict, Inventory)):
    -        dict.__init__(self, positions)
    -    else:
    -        dict.__init__(self)
    -        if positions:
    -            assert isinstance(positions, Iterable)
    -            for position in positions:
    -                self.add_position(position)
    -
    +
    def __init__(self, positions=None):
    +    """Create a new inventory using a list of existing positions.
    +
    +    Args:
    +      positions: A list of Position instances or an existing dict or
    +        Inventory instance.
    +    """
    +    if isinstance(positions, (dict, Inventory)):
    +        dict.__init__(self, positions)
    +    else:
    +        dict.__init__(self)
    +        if positions:
    +            assert isinstance(positions, Iterable)
    +            for position in positions:
    +                self.add_position(position)
    +
    @@ -12644,7 +15457,7 @@

    -beancount.core.inventory.Inventory.__iter__(self) +beancount.core.inventory.Inventory.__iter__(self) special @@ -12658,10 +15471,10 @@

    Source code in beancount/core/inventory.py -
    def __iter__(self):
    -    """Iterate over the positions. Note that there is no guaranteed order."""
    -    return iter(self.values())
    -
    +
    def __iter__(self):
    +    """Iterate over the positions. Note that there is no guaranteed order."""
    +    return iter(self.values())
    +
    @@ -12674,7 +15487,7 @@

    -beancount.core.inventory.Inventory.__lt__(self, other) +beancount.core.inventory.Inventory.__lt__(self, other) special @@ -12688,10 +15501,10 @@

    Source code in beancount/core/inventory.py -
    def __lt__(self, other):
    -    """Inequality comparison operator."""
    -    return sorted(self) < sorted(other)
    -
    +
    def __lt__(self, other):
    +    """Inequality comparison operator."""
    +    return sorted(self) < sorted(other)
    +
    @@ -12704,7 +15517,7 @@

    -beancount.core.inventory.Inventory.__mul__(self, scalar) +beancount.core.inventory.Inventory.__mul__(self, scalar) special @@ -12750,16 +15563,16 @@

    Source code in beancount/core/inventory.py -
    def __mul__(self, scalar):
    -    """Scale/multiply the contents of the inventory.
    -
    -    Args:
    -      scalar: A Decimal.
    -    Returns:
    -      An instance of Inventory.
    -    """
    -    return Inventory({key: pos * scalar for key, pos in self.items()})
    -
    +
    def __mul__(self, scalar):
    +    """Scale/multiply the contents of the inventory.
    +
    +    Args:
    +      scalar: A Decimal.
    +    Returns:
    +      An instance of Inventory.
    +    """
    +    return Inventory({key: pos * scalar for key, pos in self.items()})
    +
    @@ -12772,7 +15585,7 @@

    -beancount.core.inventory.Inventory.__neg__(self) +beancount.core.inventory.Inventory.__neg__(self) special @@ -12802,14 +15615,14 @@

    Source code in beancount/core/inventory.py -
    def __neg__(self):
    -    """Return an inventory with the negative of values of this one.
    -
    -    Returns:
    -      An instance of Inventory.
    -    """
    -    return Inventory({key: -pos for key, pos in self.items()})
    -
    +
    def __neg__(self):
    +    """Return an inventory with the negative of values of this one.
    +
    +    Returns:
    +      An instance of Inventory.
    +    """
    +    return Inventory({key: -pos for key, pos in self.items()})
    +
    @@ -12822,7 +15635,7 @@

    -beancount.core.inventory.Inventory.__repr__(self) +beancount.core.inventory.Inventory.__repr__(self) special @@ -12852,14 +15665,14 @@

    Source code in beancount/core/inventory.py -
    def __str__(self):
    -    """Render as a human-readable string.
    -
    -    Returns:
    -      A string, for human consumption.
    -    """
    -    return self.to_string()
    -
    +
    def __str__(self):
    +    """Render as a human-readable string.
    +
    +    Returns:
    +      A string, for human consumption.
    +    """
    +    return self.to_string()
    +
    @@ -12872,7 +15685,7 @@

    -beancount.core.inventory.Inventory.__str__(self) +beancount.core.inventory.Inventory.__str__(self) special @@ -12902,14 +15715,14 @@

    Source code in beancount/core/inventory.py -
    def __str__(self):
    -    """Render as a human-readable string.
    -
    -    Returns:
    -      A string, for human consumption.
    -    """
    -    return self.to_string()
    -
    +
    def __str__(self):
    +    """Render as a human-readable string.
    +
    +    Returns:
    +      A string, for human consumption.
    +    """
    +    return self.to_string()
    +
    @@ -12922,7 +15735,7 @@

    -beancount.core.inventory.Inventory.add_amount(self, units, cost=None) +beancount.core.inventory.Inventory.add_amount(self, units, cost=None)

    @@ -12960,9 +15773,9 @@

    Returns:
      -
    • A pair of (position, booking) where 'position' is the position that -that was modified BEFORE it was modified, and where 'booking' is a -Booking enum that hints at how the lot was booked to this inventory. +

    • A pair of (position, matched) where 'position' is the position that +that was modified BEFORE it was modified, and where 'matched' is a +MatchResult enum that hints at how the lot was booked to this inventory. Position may be None if there is no corresponding Position object, e.g. the position was deleted.

    @@ -12972,57 +15785,61 @@

    Source code in beancount/core/inventory.py -
    def add_amount(self, units, cost=None):
    -    """Add to this inventory using amount and cost. This adds with strict lot
    -    matching, that is, no partial matches are done on the arguments to the
    -    keys of the inventory.
    -
    -    Args:
    -      units: An Amount instance to add.
    -      cost: An instance of Cost or None, as a key to the inventory.
    -    Returns:
    -      A pair of (position, booking) where 'position' is the position that
    -      that was modified BEFORE it was modified, and where 'booking' is a
    -      Booking enum that hints at how the lot was booked to this inventory.
    -      Position may be None if there is no corresponding Position object,
    -      e.g. the position was deleted.
    -    """
    -    if ASSERTS_TYPES:
    -        assert isinstance(units, Amount), (
    -            "Internal error: {!r} (type: {})".format(units, type(units).__name__))
    -        assert cost is None or isinstance(cost, Cost), (
    -            "Internal error: {!r} (type: {})".format(cost, type(cost).__name__))
    -
    -    # Find the position.
    -    key = (units.currency, cost)
    -    pos = self.get(key, None)
    -
    -    if pos is not None:
    -        # Note: In order to augment or reduce, all the fields have to match.
    -
    -        # Check if reducing.
    -        booking = (Booking.REDUCED
    -                   if not same_sign(pos.units.number, units.number)
    -                   else Booking.AUGMENTED)
    -
    -        # Compute the new number of units.
    -        number = pos.units.number + units.number
    -        if number == ZERO:
    -            # If empty, delete the position.
    -            del self[key]
    -        else:
    -            # Otherwise update it.
    -            self[key] = Position(Amount(number, units.currency), cost)
    -    else:
    -        # If not found, create a new one.
    -        if units.number == ZERO:
    -            booking = Booking.IGNORED
    -        else:
    -            self[key] = Position(units, cost)
    -            booking = Booking.CREATED
    -
    -    return pos, booking
    -
    +
    def add_amount(self, units, cost=None):
    +    """Add to this inventory using amount and cost. This adds with strict lot
    +    matching, that is, no partial matches are done on the arguments to the
    +    keys of the inventory.
    +
    +    Args:
    +      units: An Amount instance to add.
    +      cost: An instance of Cost or None, as a key to the inventory.
    +    Returns:
    +      A pair of (position, matched) where 'position' is the position that
    +      that was modified BEFORE it was modified, and where 'matched' is a
    +      MatchResult enum that hints at how the lot was booked to this inventory.
    +      Position may be None if there is no corresponding Position object,
    +      e.g. the position was deleted.
    +    """
    +    if ASSERTS_TYPES:
    +        assert isinstance(units, Amount), "Internal error: {!r} (type: {})".format(
    +            units, type(units).__name__
    +        )
    +        assert cost is None or isinstance(
    +            cost, Cost
    +        ), "Internal error: {!r} (type: {})".format(cost, type(cost).__name__)
    +
    +    # Find the position.
    +    key = (units.currency, cost)
    +    pos = self.get(key, None)
    +
    +    if pos is not None:
    +        # Note: In order to augment or reduce, all the fields have to match.
    +
    +        # Check if reducing.
    +        booking = (
    +            MatchResult.REDUCED
    +            if not same_sign(pos.units.number, units.number)
    +            else MatchResult.AUGMENTED
    +        )
    +
    +        # Compute the new number of units.
    +        number = pos.units.number + units.number
    +        if number == ZERO:
    +            # If empty, delete the position.
    +            del self[key]
    +        else:
    +            # Otherwise update it.
    +            self[key] = Position(Amount(number, units.currency), cost)
    +    else:
    +        # If not found, create a new one.
    +        if units.number == ZERO:
    +            booking = MatchResult.IGNORED
    +        else:
    +            self[key] = Position(units, cost)
    +            booking = MatchResult.CREATED
    +
    +    return pos, booking
    +
    @@ -13035,7 +15852,7 @@

    -beancount.core.inventory.Inventory.add_inventory(self, other) +beancount.core.inventory.Inventory.add_inventory(self, other)

    @@ -13078,25 +15895,25 @@

    Source code in beancount/core/inventory.py -
    def add_inventory(self, other):
    -    """Add all the positions of another Inventory instance to this one.
    -
    -    Args:
    -      other: An instance of Inventory to add to this one.
    -    Returns:
    -      This inventory, modified.
    -    """
    -    if self.is_empty():
    -        # Optimization for empty inventories; if the current one is empty,
    -        # adopt all of the other inventory's positions without running
    -        # through the full aggregation checks. This should be very cheap. We
    -        # can do this because the positions are immutable.
    -        self.update(other)
    -    else:
    -        for position in other.get_positions():
    -            self.add_position(position)
    -    return self
    -
    +
    def add_inventory(self, other):
    +    """Add all the positions of another Inventory instance to this one.
    +
    +    Args:
    +      other: An instance of Inventory to add to this one.
    +    Returns:
    +      This inventory, modified.
    +    """
    +    if self.is_empty():
    +        # Optimization for empty inventories; if the current one is empty,
    +        # adopt all of the other inventory's positions without running
    +        # through the full aggregation checks. This should be very cheap. We
    +        # can do this because the positions are immutable.
    +        self.update(other)
    +    else:
    +        for position in other.get_positions():
    +            self.add_position(position)
    +    return self
    +
    @@ -13109,7 +15926,7 @@

    -beancount.core.inventory.Inventory.add_position(self, position) +beancount.core.inventory.Inventory.add_position(self, position)

    @@ -13146,8 +15963,8 @@

    • A pair of (position, booking) where 'position' is the position that -that was modified, and where 'booking' is a Booking enum that hints at -how the lot was booked to this inventory.

    • +that was modified, and where 'matched' is a MatchResult enum that +hints at how the lot was booked to this inventory.

    @@ -13155,24 +15972,26 @@

    Source code in beancount/core/inventory.py -
    def add_position(self, position):
    -    """Add using a position (with strict lot matching).
    -    Return True if this position was booked against and reduced another.
    -
    -    Args:
    -      position: The Posting or Position to add to this inventory.
    -    Returns:
    -      A pair of (position, booking) where 'position' is the position that
    -      that was modified, and where 'booking' is a Booking enum that hints at
    -      how the lot was booked to this inventory.
    -    """
    -    if ASSERTS_TYPES:
    -        assert hasattr(position, 'units') and hasattr(position, 'cost'), (
    -            "Invalid type for position: {}".format(position))
    -        assert isinstance(position.cost, (type(None), Cost)), (
    -            "Invalid type for cost: {}".format(position.cost))
    -    return self.add_amount(position.units, position.cost)
    -
    +
    def add_position(self, position):
    +    """Add using a position (with strict lot matching).
    +    Return True if this position was booked against and reduced another.
    +
    +    Args:
    +      position: The Posting or Position to add to this inventory.
    +    Returns:
    +      A pair of (position, booking) where 'position' is the position that
    +      that was modified, and where 'matched' is a MatchResult enum that
    +      hints at how the lot was booked to this inventory.
    +    """
    +    if ASSERTS_TYPES:
    +        assert hasattr(position, "units") and hasattr(
    +            position, "cost"
    +        ), "Invalid type for position: {}".format(position)
    +        assert isinstance(
    +            position.cost, (type(None), Cost)
    +        ), "Invalid type for cost: {}".format(position.cost)
    +    return self.add_amount(position.units, position.cost)
    +
    @@ -13185,7 +16004,7 @@

    -beancount.core.inventory.Inventory.average(self) +beancount.core.inventory.Inventory.average(self)

    @@ -13213,50 +16032,52 @@

    Source code in beancount/core/inventory.py -
    def average(self):
    -    """Average all lots of the same currency together.
    -
    -    Use the minimum date from each aggregated set of lots.
    -
    -    Returns:
    -      An instance of Inventory.
    -    """
    -    groups = collections.defaultdict(list)
    -    for position in self:
    -        key = (position.units.currency,
    -               position.cost.currency if position.cost else None)
    -        groups[key].append(position)
    -
    -    average_inventory = Inventory()
    -    for (currency, cost_currency), positions in groups.items():
    -        total_units = sum(position.units.number
    -                          for position in positions)
    -        # Explicitly skip aggregates when resulting in zero units.
    -        if total_units == ZERO:
    -            continue
    -        units_amount = Amount(total_units, currency)
    -
    -        if cost_currency:
    -            total_cost = sum(convert.get_cost(position).number
    -                             for position in positions)
    -            cost_number = (Decimal('Infinity')
    -                           if total_units == ZERO
    -                           else (total_cost / total_units))
    -            min_date = None
    -            for pos in positions:
    -                pos_date = pos.cost.date if pos.cost else None
    -                if pos_date is not None:
    -                    min_date = (pos_date
    -                                if min_date is None
    -                                else min(min_date, pos_date))
    -            cost = Cost(cost_number, cost_currency, min_date, None)
    -        else:
    -            cost = None
    -
    -        average_inventory.add_amount(units_amount, cost)
    -
    -    return average_inventory
    -
    +
    def average(self):
    +    """Average all lots of the same currency together.
    +
    +    Use the minimum date from each aggregated set of lots.
    +
    +    Returns:
    +      An instance of Inventory.
    +    """
    +    groups = collections.defaultdict(list)
    +    for position in self:
    +        key = (
    +            position.units.currency,
    +            position.cost.currency if position.cost else None,
    +        )
    +        groups[key].append(position)
    +
    +    average_inventory = Inventory()
    +    for (currency, cost_currency), positions in groups.items():
    +        total_units = sum(position.units.number for position in positions)
    +        # Explicitly skip aggregates when resulting in zero units.
    +        if total_units == ZERO:
    +            continue
    +        units_amount = Amount(total_units, currency)
    +
    +        if cost_currency:
    +            total_cost = sum(
    +                convert.get_cost(position).number for position in positions
    +            )
    +            cost_number = (
    +                Decimal("Infinity")
    +                if total_units == ZERO
    +                else (total_cost / total_units)
    +            )
    +            min_date = None
    +            for pos in positions:
    +                pos_date = pos.cost.date if pos.cost else None
    +                if pos_date is not None:
    +                    min_date = pos_date if min_date is None else min(min_date, pos_date)
    +            cost = Cost(cost_number, cost_currency, min_date, None)
    +        else:
    +            cost = None
    +
    +        average_inventory.add_amount(units_amount, cost)
    +
    +    return average_inventory
    +
    @@ -13269,7 +16090,7 @@

    -beancount.core.inventory.Inventory.cost_currencies(self) +beancount.core.inventory.Inventory.cost_currencies(self)

    @@ -13296,16 +16117,14 @@

    Source code in beancount/core/inventory.py -
    def cost_currencies(self):
    -    """Return the list of unit currencies held in this inventory.
    -
    -    Returns:
    -      A set of currency strings.
    -    """
    -    return set(cost.currency
    -               for _, cost in self.keys()
    -               if cost is not None)
    -
    +
    def cost_currencies(self):
    +    """Return the list of unit currencies held in this inventory.
    +
    +    Returns:
    +      A set of currency strings.
    +    """
    +    return set(cost.currency for _, cost in self.keys() if cost is not None)
    +
    @@ -13318,7 +16137,7 @@

    -beancount.core.inventory.Inventory.currencies(self) +beancount.core.inventory.Inventory.currencies(self)

    @@ -13345,14 +16164,14 @@

    Source code in beancount/core/inventory.py -
    def currencies(self):
    -    """Return the list of unit currencies held in this inventory.
    -
    -    Returns:
    -      A list of currency strings.
    -    """
    -    return set(currency for currency, _ in self.keys())
    -
    +
    def currencies(self):
    +    """Return the list of unit currencies held in this inventory.
    +
    +    Returns:
    +      A list of currency strings.
    +    """
    +    return set(currency for currency, _ in self.keys())
    +
    @@ -13365,7 +16184,7 @@

    -beancount.core.inventory.Inventory.currency_pairs(self) +beancount.core.inventory.Inventory.currency_pairs(self)

    @@ -13392,14 +16211,14 @@

    Source code in beancount/core/inventory.py -
    def currency_pairs(self):
    -    """Return the commodities held in this inventory.
    -
    -    Returns:
    -      A set of currency strings.
    -    """
    -    return set(position.currency_pair() for position in self)
    -
    +
    def currency_pairs(self):
    +    """Return the commodities held in this inventory.
    +
    +    Returns:
    +      A set of currency strings.
    +    """
    +    return set(position.currency_pair() for position in self)
    +
    @@ -13412,7 +16231,7 @@

    -beancount.core.inventory.Inventory.from_string(string) +beancount.core.inventory.Inventory.from_string(string) staticmethod @@ -13459,25 +16278,26 @@

    Source code in beancount/core/inventory.py -
    @staticmethod
    -def from_string(string):
    -    """Create an Inventory from a string. This is useful for writing tests.
    -
    -    Args:
    -      string: A comma-separated string of <number> <currency> with an
    -        optional {<number> <currency>} for the cost.
    -    Returns:
    -      A new instance of Inventory with the given balances.
    -    """
    -    new_inventory = Inventory()
    -    # We need to split the comma-separated positions but ignore commas
    -    # occurring within a {...cost...} specification.
    -    position_strs = re.split(
    -        r'([-+]?[0-9,.]+\s+[A-Z]+\s*(?:{[^}]*})?)\s*,?\s*', string)[1::2]
    -    for position_str in position_strs:
    -        new_inventory.add_position(position_from_string(position_str))
    -    return new_inventory
    -
    +
    @staticmethod
    +def from_string(string):
    +    """Create an Inventory from a string. This is useful for writing tests.
    +
    +    Args:
    +      string: A comma-separated string of <number> <currency> with an
    +        optional {<number> <currency>} for the cost.
    +    Returns:
    +      A new instance of Inventory with the given balances.
    +    """
    +    new_inventory = Inventory()
    +    # We need to split the comma-separated positions but ignore commas
    +    # occurring within a {...cost...} specification.
    +    position_strs = re.split(
    +        r"([-+]?[0-9,.]+\s+[A-Z]+\s*(?:{[^}]*})?)\s*,?\s*", string
    +    )[1::2]
    +    for position_str in position_strs:
    +        new_inventory.add_position(position_from_string(position_str))
    +    return new_inventory
    +
    @@ -13490,7 +16310,7 @@

    -beancount.core.inventory.Inventory.get_currency_units(self, currency) +beancount.core.inventory.Inventory.get_currency_units(self, currency)

    @@ -13534,21 +16354,21 @@

    Source code in beancount/core/inventory.py -
    def get_currency_units(self, currency):
    -    """Fetch the total amount across all the position in the given currency.
    -    This may sum multiple lots in the same currency denomination.
    -
    -    Args:
    -      currency: A string, the currency to filter the positions with.
    -    Returns:
    -      An instance of Amount, with the given currency.
    -    """
    -    total_units = ZERO
    -    for position in self:
    -        if position.units.currency == currency:
    -            total_units += position.units.number
    -    return Amount(total_units, currency)
    -
    +
    def get_currency_units(self, currency):
    +    """Fetch the total amount across all the position in the given currency.
    +    This may sum multiple lots in the same currency denomination.
    +
    +    Args:
    +      currency: A string, the currency to filter the positions with.
    +    Returns:
    +      An instance of Amount, with the given currency.
    +    """
    +    total_units = ZERO
    +    for position in self:
    +        if position.units.currency == currency:
    +            total_units += position.units.number
    +    return Amount(total_units, currency)
    +
    @@ -13561,7 +16381,7 @@

    -beancount.core.inventory.Inventory.get_only_position(self) +beancount.core.inventory.Inventory.get_only_position(self)

    @@ -13573,16 +16393,17 @@

    Source code in beancount/core/inventory.py -
    def get_only_position(self):
    -    """Return the first position and assert there are no more.
    -    If the inventory is empty, return None.
    -    """
    -    if len(self) > 0:
    -        if len(self) > 1:
    -            raise AssertionError("Inventory has more than one expected "
    -                                 "position: {}".format(self))
    -        return next(iter(self))
    -
    +
    def get_only_position(self):
    +    """Return the first position and assert there are no more.
    +    If the inventory is empty, return None.
    +    """
    +    if len(self) > 0:
    +        if len(self) > 1:
    +            raise AssertionError(
    +                "Inventory has more than one expected " "position: {}".format(self)
    +            )
    +        return next(iter(self))
    +
    @@ -13595,7 +16416,7 @@

    -beancount.core.inventory.Inventory.get_positions(self) +beancount.core.inventory.Inventory.get_positions(self)

    @@ -13622,14 +16443,14 @@

    Source code in beancount/core/inventory.py -
    def get_positions(self):
    -    """Return the positions in this inventory.
    -
    -    Returns:
    -      A shallow copy of the list of positions.
    -    """
    -    return list(iter(self))
    -
    +
    def get_positions(self):
    +    """Return the positions in this inventory.
    +
    +    Returns:
    +      A shallow copy of the list of positions.
    +    """
    +    return list(iter(self))
    +
    @@ -13642,7 +16463,7 @@

    -beancount.core.inventory.Inventory.is_empty(self) +beancount.core.inventory.Inventory.is_empty(self)

    @@ -13669,14 +16490,14 @@

    Source code in beancount/core/inventory.py -
    def is_empty(self):
    -    """Return true if the inventory is empty, that is, has no positions.
    -
    -    Returns:
    -      A boolean.
    -    """
    -    return len(self) == 0
    -
    +
    def is_empty(self):
    +    """Return true if the inventory is empty, that is, has no positions.
    +
    +    Returns:
    +      A boolean.
    +    """
    +    return len(self) == 0
    +
    @@ -13689,7 +16510,7 @@

    -beancount.core.inventory.Inventory.is_mixed(self) +beancount.core.inventory.Inventory.is_mixed(self)

    @@ -13717,21 +16538,21 @@

    Source code in beancount/core/inventory.py -
    def is_mixed(self):
    -    """Return true if the inventory contains a mix of positive and negative lots for
    -    at least one instrument.
    -
    -    Returns:
    -      A boolean.
    -    """
    -    signs_map = {}
    -    for position in self:
    -        sign = position.units.number >= 0
    -        prev_sign = signs_map.setdefault(position.units.currency, sign)
    -        if sign != prev_sign:
    -            return True
    -    return False
    -
    +
    def is_mixed(self):
    +    """Return true if the inventory contains a mix of positive and negative lots for
    +    at least one instrument.
    +
    +    Returns:
    +      A boolean.
    +    """
    +    signs_map = {}
    +    for position in self:
    +        sign = position.units.number >= 0
    +        prev_sign = signs_map.setdefault(position.units.currency, sign)
    +        if sign != prev_sign:
    +            return True
    +    return False
    +
    @@ -13744,7 +16565,7 @@

    -beancount.core.inventory.Inventory.is_reduced_by(self, ramount) +beancount.core.inventory.Inventory.is_reduced_by(self, ramount)

    @@ -13787,23 +16608,24 @@

    Source code in beancount/core/inventory.py -
    def is_reduced_by(self, ramount):
    -    """Return true if the amount could reduce this inventory.
    -
    -    Args:
    -      ramount: An instance of Amount.
    -    Returns:
    -      A boolean.
    -    """
    -    if ramount.number == ZERO:
    -        return False
    -    for position in self:
    -        units = position.units
    -        if (ramount.currency == units.currency and
    -            not same_sign(ramount.number, units.number)):
    -            return True
    -    return False
    -
    +
    def is_reduced_by(self, ramount):
    +    """Return true if the amount could reduce this inventory.
    +
    +    Args:
    +      ramount: An instance of Amount.
    +    Returns:
    +      A boolean.
    +    """
    +    if ramount.number == ZERO:
    +        return False
    +    for position in self:
    +        units = position.units
    +        if ramount.currency == units.currency and not same_sign(
    +            ramount.number, units.number
    +        ):
    +            return True
    +    return False
    +
    @@ -13816,7 +16638,7 @@

    -beancount.core.inventory.Inventory.is_small(self, tolerances) +beancount.core.inventory.Inventory.is_small(self, tolerances)

    @@ -13860,26 +16682,25 @@

    Source code in beancount/core/inventory.py -
    def is_small(self, tolerances):
    -    """Return true if all the positions in the inventory are small.
    -
    -    Args:
    -      tolerances: A Decimal, the small number of units under which a position
    -        is considered small, or a dict of currency to such epsilon precision.
    -    Returns:
    -      A boolean.
    -    """
    -    if isinstance(tolerances, dict):
    -        for position in self:
    -            tolerance = tolerances.get(position.units.currency, ZERO)
    -            if abs(position.units.number) > tolerance:
    -                return False
    -        small = True
    -    else:
    -        small = not any(abs(position.units.number) > tolerances
    -                        for position in self)
    -    return small
    -
    +
    def is_small(self, tolerances):
    +    """Return true if all the positions in the inventory are small.
    +
    +    Args:
    +      tolerances: A Decimal, the small number of units under which a position
    +        is considered small, or a dict of currency to such epsilon precision.
    +    Returns:
    +      A boolean.
    +    """
    +    if isinstance(tolerances, dict):
    +        for position in self:
    +            tolerance = tolerances.get(position.units.currency, ZERO)
    +            if abs(position.units.number) > tolerance:
    +                return False
    +        small = True
    +    else:
    +        small = not any(abs(position.units.number) > tolerances for position in self)
    +    return small
    +
    @@ -13892,7 +16713,7 @@

    -beancount.core.inventory.Inventory.reduce(self, reducer, *args) +beancount.core.inventory.Inventory.reduce(self, reducer, *args)

    @@ -13920,19 +16741,19 @@

    Source code in beancount/core/inventory.py -
    def reduce(self, reducer, *args):
    -    """Reduce an inventory using one of the conversion functions.
    -
    -    See functions in beancount.core.convert.
    -
    -    Returns:
    -      An instance of Inventory.
    -    """
    -    inventory = Inventory()
    -    for position in self:
    -        inventory.add_amount(reducer(position, *args))
    -    return inventory
    -
    +
    def reduce(self, reducer, *args):
    +    """Reduce an inventory using one of the conversion functions.
    +
    +    See functions in beancount.core.convert.
    +
    +    Returns:
    +      An instance of Inventory.
    +    """
    +    inventory = Inventory()
    +    for position in self:
    +        inventory.add_amount(reducer(position, *args))
    +    return inventory
    +
    @@ -13945,7 +16766,7 @@

    -beancount.core.inventory.Inventory.segregate_units(self, currencies) +beancount.core.inventory.Inventory.segregate_units(self, currencies)

    @@ -13988,23 +16809,72 @@

    Source code in beancount/core/inventory.py -
    def segregate_units(self, currencies):
    -    """Split up the list of positions to the given currencies.
    -
    -    Args:
    -      currencies: A list of currency strings, the currencies to isolate.
    -    Returns:
    -      A dict of currency to Inventory instances.
    -    """
    -    per_currency_dict = {currency: Inventory()
    -                         for currency in currencies}
    -    per_currency_dict[None] = Inventory()
    -    for position in self:
    -        currency = position.units.currency
    -        key = (currency if currency in currencies else None)
    -        per_currency_dict[key].add_position(position)
    -    return per_currency_dict
    -
    +
    def segregate_units(self, currencies):
    +    """Split up the list of positions to the given currencies.
    +
    +    Args:
    +      currencies: A list of currency strings, the currencies to isolate.
    +    Returns:
    +      A dict of currency to Inventory instances.
    +    """
    +    per_currency_dict = {currency: Inventory() for currency in currencies}
    +    per_currency_dict[None] = Inventory()
    +    for position in self:
    +        currency = position.units.currency
    +        key = currency if currency in currencies else None
    +        per_currency_dict[key].add_position(position)
    +    return per_currency_dict
    +
    + + + + + + + +
    + + + +

    +beancount.core.inventory.Inventory.split(self) + + +

    + +
    + +

    Split up the list of positions to their corresponding currencies.

    + + + + + + + + + + + + +
    Returns: +
      +
    • A dict of currency to Inventory instances.

    • +
    +
    +
    + Source code in beancount/core/inventory.py +
    def split(self):
    +    """Split up the list of positions to their corresponding currencies.
    +
    +    Returns:
    +      A dict of currency to Inventory instances.
    +    """
    +    per_currency_dict = collections.defaultdict(Inventory)
    +    for position in self:
    +        per_currency_dict[position.units.currency].add_position(position)
    +    return dict(per_currency_dict)
    +
    @@ -14017,7 +16887,7 @@

    -beancount.core.inventory.Inventory.to_string(self, dformat=<beancount.core.display_context.DisplayFormatter object at 0x78e868ae5e50>, parens=True) +beancount.core.inventory.Inventory.to_string(self, dformat=<beancount.core.display_context.DisplayFormatter object at 0x78fcde6b7290>, parens=True)

    @@ -14061,19 +16931,18 @@

    Source code in beancount/core/inventory.py -
    def to_string(self, dformat=DEFAULT_FORMATTER, parens=True):
    -    """Convert an Inventory instance to a printable string.
    -
    -    Args:
    -      dformat: An instance of DisplayFormatter.
    -      parents: A boolean, true if we should surround the results by parentheses.
    -    Returns:
    -      A formatted string of the quantized amount and symbol.
    -    """
    -    fmt = '({})' if parens else '{}'
    -    return fmt.format(
    -        ', '.join(pos.to_string(dformat) for pos in sorted(self)))
    -
    +
    def to_string(self, dformat=DEFAULT_FORMATTER, parens=True):
    +    """Convert an Inventory instance to a printable string.
    +
    +    Args:
    +      dformat: An instance of DisplayFormatter.
    +      parents: A boolean, true if we should surround the results by parentheses.
    +    Returns:
    +      A formatted string of the quantized amount and symbol.
    +    """
    +    fmt = "({})" if parens else "{}"
    +    return fmt.format(", ".join(pos.to_string(dformat) for pos in sorted(self)))
    +

    @@ -14083,6 +16952,50 @@

    + + + + + + + + +
    + + + +

    + +beancount.core.inventory.MatchResult (Enum) + + + + +

    + +
    + +

    Result of booking a new lot to an existing inventory.

    + + + + +
    + + + + + + + + + + + + + + +
    @@ -14097,7 +17010,7 @@

    -beancount.core.inventory.check_invariants(inv) +beancount.core.inventory.check_invariants(inv)

    @@ -14140,21 +17053,21 @@

    Source code in beancount/core/inventory.py -
    def check_invariants(inv):
    -    """Check the invariants of the Inventory.
    -
    -    Args:
    -      inventory: An instance of Inventory.
    -    Returns:
    -      True if the invariants are respected.
    -    """
    -    # Check that all the keys are unique.
    -    lots = set((pos.units.currency, pos.cost) for pos in inv)
    -    assert len(lots) == len(inv), "Invalid inventory: {}".format(inv)
    -    # Check that none of the amounts is zero.
    -    for pos in inv:
    -        assert pos.units.number != ZERO, "Invalid position size: {}".format(pos)
    -
    +
    def check_invariants(inv):
    +    """Check the invariants of the Inventory.
    +
    +    Args:
    +      inventory: An instance of Inventory.
    +    Returns:
    +      True if the invariants are respected.
    +    """
    +    # Check that all the keys are unique.
    +    lots = set((pos.units.currency, pos.cost) for pos in inv)
    +    assert len(lots) == len(inv), "Invalid inventory: {}".format(inv)
    +    # Check that none of the amounts is zero.
    +    for pos in inv:
    +        assert pos.units.number != ZERO, "Invalid position size: {}".format(pos)
    +

    @@ -14167,7 +17080,7 @@

    -beancount.core.inventory.from_string(string) +beancount.core.inventory.from_string(string)

    @@ -14211,25 +17124,26 @@

    Source code in beancount/core/inventory.py -
    @staticmethod
    -def from_string(string):
    -    """Create an Inventory from a string. This is useful for writing tests.
    -
    -    Args:
    -      string: A comma-separated string of <number> <currency> with an
    -        optional {<number> <currency>} for the cost.
    -    Returns:
    -      A new instance of Inventory with the given balances.
    -    """
    -    new_inventory = Inventory()
    -    # We need to split the comma-separated positions but ignore commas
    -    # occurring within a {...cost...} specification.
    -    position_strs = re.split(
    -        r'([-+]?[0-9,.]+\s+[A-Z]+\s*(?:{[^}]*})?)\s*,?\s*', string)[1::2]
    -    for position_str in position_strs:
    -        new_inventory.add_position(position_from_string(position_str))
    -    return new_inventory
    -
    +
    @staticmethod
    +def from_string(string):
    +    """Create an Inventory from a string. This is useful for writing tests.
    +
    +    Args:
    +      string: A comma-separated string of <number> <currency> with an
    +        optional {<number> <currency>} for the cost.
    +    Returns:
    +      A new instance of Inventory with the given balances.
    +    """
    +    new_inventory = Inventory()
    +    # We need to split the comma-separated positions but ignore commas
    +    # occurring within a {...cost...} specification.
    +    position_strs = re.split(
    +        r"([-+]?[0-9,.]+\s+[A-Z]+\s*(?:{[^}]*})?)\s*,?\s*", string
    +    )[1::2]
    +    for position_str in position_strs:
    +        new_inventory.add_position(position_from_string(position_str))
    +    return new_inventory
    +
    @@ -14298,7 +17212,7 @@

    -beancount.core.number.D(strord=None) +beancount.core.number.D(strord=None)

    @@ -14347,37 +17261,38 @@

    Source code in beancount/core/number.py -
    def D(strord=None):
    -    """Convert a string into a Decimal object.
    -
    -    This is used in parsing amounts from files in the importers. This is the
    -    main function you should use to build all numbers the system manipulates
    -    (never use floating-point in an accounting system). Commas are stripped and
    -    ignored, as they are assumed to be thousands separators (the French comma
    -    separator as decimal is not supported). This function just returns the
    -    argument if it is already a Decimal object, for convenience.
    -
    -    Args:
    -      strord: A string or Decimal instance.
    -    Returns:
    -      A Decimal instance.
    -    """
    -    try:
    -        # Note: try a map lookup and optimize performance here.
    -        if strord is None or strord == '':
    -            return Decimal()
    -        elif isinstance(strord, str):
    -            return Decimal(_CLEAN_NUMBER_RE.sub('', strord))
    -        elif isinstance(strord, Decimal):
    -            return strord
    -        elif isinstance(strord, (int, float)):
    -            return Decimal(strord)
    -        else:
    -            assert strord is None, "Invalid value to convert: {}".format(strord)
    -    except Exception as exc:
    -        raise ValueError("Impossible to create Decimal instance from {!s}: {}".format(
    -                         strord, exc))
    -
    +
    def D(strord=None):
    +    """Convert a string into a Decimal object.
    +
    +    This is used in parsing amounts from files in the importers. This is the
    +    main function you should use to build all numbers the system manipulates
    +    (never use floating-point in an accounting system). Commas are stripped and
    +    ignored, as they are assumed to be thousands separators (the French comma
    +    separator as decimal is not supported). This function just returns the
    +    argument if it is already a Decimal object, for convenience.
    +
    +    Args:
    +      strord: A string or Decimal instance.
    +    Returns:
    +      A Decimal instance.
    +    """
    +    try:
    +        # Note: try a map lookup and optimize performance here.
    +        if strord is None or strord == "":
    +            return Decimal()
    +        elif isinstance(strord, str):
    +            return Decimal(_CLEAN_NUMBER_RE.sub("", strord))
    +        elif isinstance(strord, Decimal):
    +            return strord
    +        elif isinstance(strord, (int, float)):
    +            return Decimal(strord)
    +        else:
    +            assert strord is None, "Invalid value to convert: {}".format(strord)
    +    except Exception as exc:
    +        raise ValueError(
    +            "Impossible to create Decimal instance from {!s}: {}".format(strord, exc)
    +        ) from exc
    +
    @@ -14389,22 +17304,203 @@

    -

    -beancount.core.number.is_fast_decimal(decimal_module) +

    +beancount.core.number.auto_quantize(number, threshold) -

    +

    -

    Return true if a fast C decimal implementation is installed.

    +

    Automatically quantize the number at a given threshold.

    +

    For example, with a threshold of 0.01, this will convert:

    +

    20.899999618530273 20.9 + 20.290000000000000000000000000000 20.29 + 110.90 110.9 + 11.0600004196167 11.06 + 10.539999961853027 10.54 + 134.3300018310547 134.33 + 253.920200000000000000000000000000 253.9202

    Source code in beancount/core/number.py -
    def is_fast_decimal(decimal_module):
    -    "Return true if a fast C decimal implementation is installed."
    -    return isinstance(decimal_module.Decimal().sqrt, types.BuiltinFunctionType)
    -
    +
    def auto_quantize(number: Decimal, threshold: float) -> Decimal:
    +    """Automatically quantize the number at a given threshold.
    +
    +    For example, with a threshold of 0.01, this will convert:
    +
    +      20.899999618530273 20.9
    +      20.290000000000000000000000000000 20.29
    +      110.90 110.9
    +      11.0600004196167 11.06
    +      10.539999961853027 10.54
    +      134.3300018310547 134.33
    +      253.920200000000000000000000000000 253.9202
    +
    +    """
    +    exponent = auto_quantized_exponent(number, threshold)
    +    if exponent != number.as_tuple().exponent:
    +        quant = TEN**exponent
    +        qnumber = number.quantize(quant).normalize()
    +        return qnumber
    +    else:
    +        return number
    +
    +
    +
    + + + + + +
    + + + +

    +beancount.core.number.auto_quantized_exponent(number, threshold) + + +

    + +
    + +

    Automatically infer the exponent that would be used below a given threshold.

    + +
    + Source code in beancount/core/number.py +
    def auto_quantized_exponent(number: Decimal, threshold: float) -> int:
    +    """Automatically infer the exponent that would be used below a given threshold."""
    +    dtuple = number.normalize().as_tuple()
    +    norm = Decimal(dtuple._replace(sign=0, exponent=-len(dtuple.digits)))
    +    low_threshold = threshold
    +    high_threshold = 1.0 - low_threshold
    +    while norm != ZERO:
    +        if not (low_threshold <= norm <= high_threshold):
    +            break
    +        ntuple = norm.scaleb(1).as_tuple()
    +        norm = Decimal(ntuple._replace(digits=ntuple.digits[ntuple.exponent :]))
    +    return dtuple.exponent - norm.as_tuple().exponent
    +
    +
    +
    + +
    + + + +
    + + + +

    +beancount.core.number.infer_quantum_from_list(numbers, threshold=0.01) + + +

    + +
    + +

    Given a list of numbers from floats, infer the common quantization.

    +

    For a series of numbers provided as floats, e.g., prices from a price +source, we'd like to infer what the right quantization that should be used +to avoid rounding errors above some threshold.

    +

    from the numbers. This simple algorithm auto-quantizes all the numbers and +quantizes all of them at the maximum precision that would result in rounding +under the threshold.

    + + + + + + + + + + + + +
    Parameters: +
      +
    • prices – A list of float or Decimal prices to infer from. If floats are +provided, conversion is done naively.

    • +
    • threshold (float) – A fraction, the maximum error to tolerate before stopping the +search.

    • +
    +
    + + + + + + + + + + + +
    Returns: +
      +
    • Optional[decimal.Decimal] – A decimal object to use with decimal.Decimal.quantize().

    • +
    +
    +
    + Source code in beancount/core/number.py +
    def infer_quantum_from_list(
    +    numbers: List[Decimal], threshold: float = 0.01
    +) -> Optional[Decimal]:
    +    """Given a list of numbers from floats, infer the common quantization.
    +
    +    For a series of numbers provided as floats, e.g., prices from a price
    +    source, we'd like to infer what the right quantization that should be used
    +    to avoid rounding errors above some threshold.
    +
    +
    +    from the numbers. This simple algorithm auto-quantizes all the numbers and
    +    quantizes all of them at the maximum precision that would result in rounding
    +    under the threshold.
    +
    +    Args:
    +      prices: A list of float or Decimal prices to infer from. If floats are
    +        provided, conversion is done naively.
    +      threshold: A fraction, the maximum error to tolerate before stopping the
    +        search.
    +    Returns:
    +      A decimal object to use with decimal.Decimal.quantize().
    +
    +    """
    +    # Auto quantize all the numbers.
    +    qnumbers = [auto_quantize(num, threshold) for num in numbers]
    +    exponent = max(num_fractional_digits(n) for n in qnumbers)
    +    return -exponent
    +
    +
    +
    + +
    + + + +
    + + + +

    +beancount.core.number.num_fractional_digits(number) + + +

    + +
    + +

    Return the number of fractional digits.

    + +
    + Source code in beancount/core/number.py +
    def num_fractional_digits(number: Decimal) -> int:
    +    """Return the number of fractional digits."""
    +    return -number.as_tuple().exponent
    +
    @@ -14417,7 +17513,7 @@

    -beancount.core.number.round_to(number, increment) +beancount.core.number.round_to(number, increment)

    @@ -14461,17 +17557,17 @@

    Source code in beancount/core/number.py -
    def round_to(number, increment):
    -    """Round a number *down* to a particular increment.
    -
    -    Args:
    -      number: A Decimal, the number to be rounded.
    -      increment: A Decimal, the size of the increment.
    -    Returns:
    -      A Decimal, the rounded number.
    -    """
    -    return int((number / increment)) * increment
    -
    +
    def round_to(number, increment):
    +    """Round a number *down* to a particular increment.
    +
    +    Args:
    +      number: A Decimal, the number to be rounded.
    +      increment: A Decimal, the size of the increment.
    +    Returns:
    +      A Decimal, the rounded number.
    +    """
    +    return int((number / increment)) * increment
    +

    @@ -14484,7 +17580,7 @@

    -beancount.core.number.same_sign(number1, number2) +beancount.core.number.same_sign(number1, number2)

    @@ -14528,17 +17624,17 @@

    Source code in beancount/core/number.py -
    def same_sign(number1, number2):
    -    """Return true if both numbers have the same sign.
    -
    -    Args:
    -      number1: An instance of Decimal.
    -      number2: An instance of Decimal.
    -    Returns:
    -      A boolean.
    -    """
    -    return (number1 >= 0) == (number2 >= 0)
    -
    +
    def same_sign(number1, number2):
    +    """Return true if both numbers have the same sign.
    +
    +    Args:
    +      number1: An instance of Decimal.
    +      number2: An instance of Decimal.
    +    Returns:
    +      A boolean.
    +    """
    +    return (number1 >= 0) == (number2 >= 0)
    +
    @@ -14622,7 +17718,7 @@

    -beancount.core.position.Cost.__getnewargs__(self) +beancount.core.position.Cost.__getnewargs__(self) special @@ -14636,10 +17732,10 @@

    Source code in beancount/core/position.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -14652,7 +17748,7 @@

    -beancount.core.position.Cost.__new__(_cls, number, currency, date, label) +beancount.core.position.Cost.__new__(_cls, number, currency, date, label) special @@ -14676,7 +17772,7 @@

    -beancount.core.position.Cost.__repr__(self) +beancount.core.position.Cost.__repr__(self) special @@ -14690,10 +17786,10 @@

    Source code in beancount/core/position.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -14746,7 +17842,7 @@

    -beancount.core.position.CostSpec.__getnewargs__(self) +beancount.core.position.CostSpec.__getnewargs__(self) special @@ -14760,10 +17856,10 @@

    Source code in beancount/core/position.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -14776,7 +17872,7 @@

    -beancount.core.position.CostSpec.__new__(_cls, number_per, number_total, currency, date, label, merge) +beancount.core.position.CostSpec.__new__(_cls, number_per, number_total, currency, date, label, merge) special @@ -14800,7 +17896,7 @@

    -beancount.core.position.CostSpec.__repr__(self) +beancount.core.position.CostSpec.__repr__(self) special @@ -14814,10 +17910,10 @@

    Source code in beancount/core/position.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -14895,7 +17991,7 @@

    -beancount.core.position.Position.__abs__(self) +beancount.core.position.Position.__abs__(self) special @@ -14925,14 +18021,14 @@

    Source code in beancount/core/position.py -
    def __abs__(self):
    -    """Return the absolute value of the position.
    -
    -    Returns:
    -      An instance of Position with the absolute units.
    -    """
    -    return Position(amount_abs(self.units), self.cost)
    -
    +
    def __abs__(self):
    +    """Return the absolute value of the position.
    +
    +    Returns:
    +      An instance of Position with the absolute units.
    +    """
    +    return Position(amount_abs(self.units), self.cost)
    +
    @@ -14945,7 +18041,7 @@

    -beancount.core.position.Position.__copy__(self) +beancount.core.position.Position.__copy__(self) special @@ -14976,16 +18072,16 @@

    Source code in beancount/core/position.py -
    def __copy__(self):
    -    """Shallow copy, except for the lot, which can be shared. This is important for
    -    performance reasons; a lot of time is spent here during balancing.
    -
    -    Returns:
    -      A shallow copy of this position.
    -    """
    -    # Note: We use Decimal() for efficiency.
    -    return Position(copy.copy(self.units), copy.copy(self.cost))
    -
    +
    def __copy__(self):
    +    """Shallow copy, except for the lot, which can be shared. This is important for
    +    performance reasons; a lot of time is spent here during balancing.
    +
    +    Returns:
    +      A shallow copy of this position.
    +    """
    +    # Note: We use Decimal() for efficiency.
    +    return Position(copy.copy(self.units), copy.copy(self.cost))
    +
    @@ -14998,7 +18094,7 @@

    -beancount.core.position.Position.__eq__(self, other) +beancount.core.position.Position.__eq__(self, other) special @@ -15046,20 +18142,22 @@

    Source code in beancount/core/position.py -
    def __eq__(self, other):
    -    """Equality comparison with another Position. The objects are considered equal
    -    if both number and lot are matching, and if the number of units is zero
    -    and the other position is None, that is also okay.
    -
    -    Args:
    -      other: An instance of Position, or None.
    -    Returns:
    -      A boolean, true if the positions are equal.
    -    """
    -    return (self.units.number == ZERO
    -            if other is None
    -            else (self.units == other.units and self.cost == other.cost))
    -
    +
    def __eq__(self, other):
    +    """Equality comparison with another Position. The objects are considered equal
    +    if both number and lot are matching, and if the number of units is zero
    +    and the other position is None, that is also okay.
    +
    +    Args:
    +      other: An instance of Position, or None.
    +    Returns:
    +      A boolean, true if the positions are equal.
    +    """
    +    return (
    +        self.units.number == ZERO
    +        if other is None
    +        else (self.units == other.units and self.cost == other.cost)
    +    )
    +
    @@ -15072,7 +18170,7 @@

    -beancount.core.position.Position.__hash__(self) +beancount.core.position.Position.__hash__(self) special @@ -15102,14 +18200,14 @@

    Source code in beancount/core/position.py -
    def __hash__(self):
    -    """Compute a hash for this position.
    -
    -    Returns:
    -      A hash of this position object.
    -    """
    -    return hash((self.units, self.cost))
    -
    +
    def __hash__(self):
    +    """Compute a hash for this position.
    +
    +    Returns:
    +      A hash of this position object.
    +    """
    +    return hash((self.units, self.cost))
    +
    @@ -15122,7 +18220,7 @@

    -beancount.core.position.Position.__lt__(self, other) +beancount.core.position.Position.__lt__(self, other) special @@ -15168,16 +18266,16 @@

    Source code in beancount/core/position.py -
    def __lt__(self, other):
    -    """A less-than comparison operator for positions.
    -
    -    Args:
    -      other: Another instance of Position.
    -    Returns:
    -      True if this positions is smaller than the other position.
    -    """
    -    return self.sortkey() < other.sortkey()
    -
    +
    def __lt__(self, other):
    +    """A less-than comparison operator for positions.
    +
    +    Args:
    +      other: Another instance of Position.
    +    Returns:
    +      True if this positions is smaller than the other position.
    +    """
    +    return self.sortkey() < other.sortkey()
    +
    @@ -15190,7 +18288,7 @@

    -beancount.core.position.Position.__mul__(self, scalar) +beancount.core.position.Position.__mul__(self, scalar) special @@ -15236,16 +18334,16 @@

    Source code in beancount/core/position.py -
    def __mul__(self, scalar):
    -    """Scale/multiply the contents of the position.
    -
    -    Args:
    -      scalar: A Decimal.
    -    Returns:
    -      An instance of Inventory.
    -    """
    -    return Position(amount_mul(self.units, scalar), self.cost)
    -
    +
    def __mul__(self, scalar):
    +    """Scale/multiply the contents of the position.
    +
    +    Args:
    +      scalar: A Decimal.
    +    Returns:
    +      An instance of Inventory.
    +    """
    +    return Position(amount_mul(self.units, scalar), self.cost)
    +
    @@ -15258,7 +18356,7 @@

    -beancount.core.position.Position.__neg__(self) +beancount.core.position.Position.__neg__(self) special @@ -15288,15 +18386,15 @@

    Source code in beancount/core/position.py -
    def get_negative(self):
    -    """Get a copy of this position but with a negative number.
    -
    -    Returns:
    -      An instance of Position which represents the inverse of this Position.
    -    """
    -    # Note: We use Decimal() for efficiency.
    -    return Position(-self.units, self.cost)
    -
    +
    def get_negative(self):
    +    """Get a copy of this position but with a negative number.
    +
    +    Returns:
    +      An instance of Position which represents the inverse of this Position.
    +    """
    +    # Note: We use Decimal() for efficiency.
    +    return Position(-self.units, self.cost)
    +
    @@ -15309,7 +18407,7 @@

    -beancount.core.position.Position.__new__(cls, units, cost=None) +beancount.core.position.Position.__new__(cls, units, cost=None) special @@ -15324,13 +18422,15 @@

    Source code in beancount/core/position.py -
    def __new__(cls, units, cost=None):
    -    assert isinstance(units, Amount), (
    -        "Expected an Amount for units; received '{}'".format(units))
    -    assert cost is None or isinstance(cost, Position.cost_types), (
    -        "Expected a Cost for cost; received '{}'".format(cost))
    -    return _Position.__new__(cls, units, cost)
    -
    +
    def __new__(cls, units, cost=None):
    +    assert isinstance(
    +        units, Amount
    +    ), "Expected an Amount for units; received '{}'".format(units)
    +    assert cost is None or isinstance(
    +        cost, Position.cost_types
    +    ), "Expected a Cost for cost; received '{}'".format(cost)
    +    return _Position.__new__(cls, units, cost)
    +
    @@ -15343,7 +18443,7 @@

    -beancount.core.position.Position.__repr__(self) +beancount.core.position.Position.__repr__(self) special @@ -15373,14 +18473,14 @@

    Source code in beancount/core/position.py -
    def __str__(self):
    -    """Return a string representation of the position.
    -
    -    Returns:
    -      A string, a printable representation of the position.
    -    """
    -    return self.to_string()
    -
    +
    def __str__(self):
    +    """Return a string representation of the position.
    +
    +    Returns:
    +      A string, a printable representation of the position.
    +    """
    +    return self.to_string()
    +
    @@ -15393,7 +18493,7 @@

    -beancount.core.position.Position.__str__(self) +beancount.core.position.Position.__str__(self) special @@ -15423,14 +18523,14 @@

    Source code in beancount/core/position.py -
    def __str__(self):
    -    """Return a string representation of the position.
    -
    -    Returns:
    -      A string, a printable representation of the position.
    -    """
    -    return self.to_string()
    -
    +
    def __str__(self):
    +    """Return a string representation of the position.
    +
    +    Returns:
    +      A string, a printable representation of the position.
    +    """
    +    return self.to_string()
    +
    @@ -15443,7 +18543,7 @@

    -beancount.core.position.Position.currency_pair(self) +beancount.core.position.Position.currency_pair(self)

    @@ -15470,14 +18570,14 @@

    Source code in beancount/core/position.py -
    def currency_pair(self):
    -    """Return the currency pair associated with this position.
    -
    -    Returns:
    -      A pair of a currency string and a cost currency string or None.
    -    """
    -    return (self.units.currency, self.cost.currency if self.cost else None)
    -
    +
    def currency_pair(self):
    +    """Return the currency pair associated with this position.
    +
    +    Returns:
    +      A pair of a currency string and a cost currency string or None.
    +    """
    +    return (self.units.currency, self.cost.currency if self.cost else None)
    +
    @@ -15490,7 +18590,7 @@

    -beancount.core.position.Position.from_amounts(units, cost_amount=None) +beancount.core.position.Position.from_amounts(units, cost_amount=None) staticmethod @@ -15537,23 +18637,26 @@

    Source code in beancount/core/position.py -
    @staticmethod
    -def from_amounts(units, cost_amount=None):
    -    """Create a position from an amount and a cost.
    -
    -    Args:
    -      amount: An amount, that represents the number of units and the lot currency.
    -      cost_amount: If not None, represents the cost amount.
    -    Returns:
    -      A Position instance.
    -    """
    -    assert cost_amount is None or isinstance(cost_amount, Amount), (
    -        "Invalid type for cost: {}".format(cost_amount))
    -    cost = (Cost(cost_amount.number, cost_amount.currency, None, None)
    -            if cost_amount else
    -            None)
    -    return Position(units, cost)
    -
    +
    @staticmethod
    +def from_amounts(units, cost_amount=None):
    +    """Create a position from an amount and a cost.
    +
    +    Args:
    +      amount: An amount, that represents the number of units and the lot currency.
    +      cost_amount: If not None, represents the cost amount.
    +    Returns:
    +      A Position instance.
    +    """
    +    assert cost_amount is None or isinstance(
    +        cost_amount, Amount
    +    ), "Invalid type for cost: {}".format(cost_amount)
    +    cost = (
    +        Cost(cost_amount.number, cost_amount.currency, None, None)
    +        if cost_amount
    +        else None
    +    )
    +    return Position(units, cost)
    +
    @@ -15566,7 +18669,7 @@

    -beancount.core.position.Position.from_string(string) +beancount.core.position.Position.from_string(string) staticmethod @@ -15614,80 +18717,81 @@

    Source code in beancount/core/position.py -
    @staticmethod
    -def from_string(string):
    -    """Create a position from a string specification.
    -
    -    This is a miniature parser used for building tests.
    -
    -    Args:
    -      string: A string of <number> <currency> with an optional {<number>
    -        <currency>} for the cost, similar to the parser syntax.
    -    Returns:
    -      A new instance of Position.
    -    """
    -    match = re.match(
    -        (r'\s*({})\s+({})'
    -         r'(?:\s+{{([^}}]*)}})?'
    -         r'\s*$').format(NUMBER_RE, CURRENCY_RE),
    -        string)
    -    if not match:
    -        raise ValueError("Invalid string for position: '{}'".format(string))
    -
    -    number = D(match.group(1))
    -    currency = match.group(2)
    -
    -    # Parse a cost expression.
    -    cost_number = None
    -    cost_currency = None
    -    date = None
    -    label = None
    -    cost_expression = match.group(3)
    -    if match.group(3):
    -        expressions = [expr.strip() for expr in re.split('[,/]', cost_expression)]
    -        for expr in expressions:
    -
    -            # Match a compound number.
    -            match = re.match(
    -                r'({NUMBER_RE})\s*(?:#\s*({NUMBER_RE}))?\s+({CURRENCY_RE})$'
    -                .format(NUMBER_RE=NUMBER_RE, CURRENCY_RE=CURRENCY_RE),
    -                expr
    -            )
    -            if match:
    -                per_number, total_number, cost_currency = match.group(1, 2, 3)
    -                per_number = D(per_number) if per_number else ZERO
    -                total_number = D(total_number) if total_number else ZERO
    -                if total_number:
    -                    # Calculate the per-unit cost.
    -                    total = number * per_number + total_number
    -                    per_number = total / number
    -                cost_number = per_number
    -                continue
    -
    -            # Match a date.
    -            match = re.match(r'(\d\d\d\d)[-/](\d\d)[-/](\d\d)$', expr)
    -            if match:
    -                date = datetime.date(*map(int, match.group(1, 2, 3)))
    -                continue
    -
    -            # Match a label.
    -            match = re.match(r'"([^"]+)*"$', expr)
    -            if match:
    -                label = match.group(1)
    -                continue
    -
    -            # Match a merge-cost marker.
    -            match = re.match(r'\*$', expr)
    -            if match:
    -                raise ValueError("Merge-code not supported in string constructor.")
    -
    -            raise ValueError("Invalid cost component: '{}'".format(expr))
    -        cost = Cost(cost_number, cost_currency, date, label)
    -    else:
    -        cost = None
    -
    -    return Position(Amount(number, currency), cost)
    -
    +
    @staticmethod
    +def from_string(string):
    +    """Create a position from a string specification.
    +
    +    This is a miniature parser used for building tests.
    +
    +    Args:
    +      string: A string of <number> <currency> with an optional {<number>
    +        <currency>} for the cost, similar to the parser syntax.
    +    Returns:
    +      A new instance of Position.
    +    """
    +    match = re.match(
    +        (r"\s*({})\s+({})" r"(?:\s+{{([^}}]*)}})?" r"\s*$").format(
    +            NUMBER_RE, CURRENCY_RE
    +        ),
    +        string,
    +    )
    +    if not match:
    +        raise ValueError("Invalid string for position: '{}'".format(string))
    +
    +    number = D(match.group(1))
    +    currency = match.group(2)
    +
    +    # Parse a cost expression.
    +    cost_number = None
    +    cost_currency = None
    +    date = None
    +    label = None
    +    cost_expression = match.group(3)
    +    if match.group(3):
    +        expressions = [expr.strip() for expr in re.split("[,/]", cost_expression)]
    +        for expr in expressions:
    +            # Match a compound number.
    +            match = re.match(
    +                r"({NUMBER_RE})\s*(?:#\s*({NUMBER_RE}))?\s+({CURRENCY_RE})$".format(
    +                    NUMBER_RE=NUMBER_RE, CURRENCY_RE=CURRENCY_RE
    +                ),
    +                expr,
    +            )
    +            if match:
    +                per_number, total_number, cost_currency = match.group(1, 2, 3)
    +                per_number = D(per_number) if per_number else ZERO
    +                total_number = D(total_number) if total_number else ZERO
    +                if total_number:
    +                    # Calculate the per-unit cost.
    +                    total = number * per_number + total_number
    +                    per_number = total / number
    +                cost_number = per_number
    +                continue
    +
    +            # Match a date.
    +            match = re.match(r"(\d\d\d\d)[-/](\d\d)[-/](\d\d)$", expr)
    +            if match:
    +                date = datetime.date(*map(int, match.group(1, 2, 3)))
    +                continue
    +
    +            # Match a label.
    +            match = re.match(r'"([^"]+)*"$', expr)
    +            if match:
    +                label = match.group(1)
    +                continue
    +
    +            # Match a merge-cost marker.
    +            match = re.match(r"\*$", expr)
    +            if match:
    +                raise ValueError("Merge-code not supported in string constructor.")
    +
    +            raise ValueError("Invalid cost component: '{}'".format(expr))
    +        cost = Cost(cost_number, cost_currency, date, label)
    +    else:
    +        cost = None
    +
    +    return Position(Amount(number, currency), cost)
    +
    @@ -15700,7 +18804,7 @@

    -beancount.core.position.Position.get_negative(self) +beancount.core.position.Position.get_negative(self)

    @@ -15727,15 +18831,15 @@

    Source code in beancount/core/position.py -
    def get_negative(self):
    -    """Get a copy of this position but with a negative number.
    -
    -    Returns:
    -      An instance of Position which represents the inverse of this Position.
    -    """
    -    # Note: We use Decimal() for efficiency.
    -    return Position(-self.units, self.cost)
    -
    +
    def get_negative(self):
    +    """Get a copy of this position but with a negative number.
    +
    +    Returns:
    +      An instance of Position which represents the inverse of this Position.
    +    """
    +    # Note: We use Decimal() for efficiency.
    +    return Position(-self.units, self.cost)
    +
    @@ -15748,7 +18852,7 @@

    -beancount.core.position.Position.is_negative_at_cost(self) +beancount.core.position.Position.is_negative_at_cost(self)

    @@ -15775,14 +18879,14 @@

    Source code in beancount/core/position.py -
    def is_negative_at_cost(self):
    -    """Return true if the position is held at cost and negative.
    -
    -    Returns:
    -      A boolean.
    -    """
    -    return (self.units.number < ZERO and self.cost is not None)
    -
    +
    def is_negative_at_cost(self):
    +    """Return true if the position is held at cost and negative.
    +
    +    Returns:
    +      A boolean.
    +    """
    +    return self.units.number < ZERO and self.cost is not None
    +
    @@ -15795,7 +18899,7 @@

    -beancount.core.position.Position.sortkey(self) +beancount.core.position.Position.sortkey(self)

    @@ -15824,25 +18928,25 @@

    Source code in beancount/core/position.py -
    def sortkey(self):
    -    """Return a key to sort positions by. This key depends on the order of the
    -    currency of the lot (we want to order common currencies first) and the
    -    number of units.
    -
    -    Returns:
    -      A tuple, used to sort lists of positions.
    -    """
    -    currency = self.units.currency
    -    order_units = CURRENCY_ORDER.get(currency, NCURRENCIES + len(currency))
    -    if self.cost is not None:
    -        cost_number = self.cost.number
    -        cost_currency = self.cost.currency
    -    else:
    -        cost_number = ZERO
    -        cost_currency = ''
    -
    -    return (order_units, cost_number, cost_currency, self.units.number)
    -
    +
    def sortkey(self):
    +    """Return a key to sort positions by. This key depends on the order of the
    +    currency of the lot (we want to order common currencies first) and the
    +    number of units.
    +
    +    Returns:
    +      A tuple, used to sort lists of positions.
    +    """
    +    currency = self.units.currency
    +    order_units = CURRENCY_ORDER.get(currency, NCURRENCIES + len(currency))
    +    if self.cost is not None:
    +        cost_number = self.cost.number
    +        cost_currency = self.cost.currency
    +    else:
    +        cost_number = ZERO
    +        cost_currency = ""
    +
    +    return (order_units, cost_number, cost_currency, self.units.number)
    +
    @@ -15855,7 +18959,7 @@

    -beancount.core.position.Position.to_string(self, dformat=<beancount.core.display_context.DisplayFormatter object at 0x78e868ae5e50>, detail=True) +beancount.core.position.Position.to_string(self, dformat=<beancount.core.display_context.DisplayFormatter object at 0x78fcde6b7290>, detail=True)

    @@ -15866,11 +18970,10 @@

    Source code in beancount/core/position.py -
    def to_string(self, dformat=DEFAULT_FORMATTER, detail=True):
    -    """Render the position to a string.See to_string() for details.
    -    """
    -    return to_string(self, dformat, detail)
    -
    +
    def to_string(self, dformat=DEFAULT_FORMATTER, detail=True):
    +    """Render the position to a string.See to_string() for details."""
    +    return to_string(self, dformat, detail)
    +
    @@ -15894,7 +18997,7 @@

    -beancount.core.position.cost_to_str(cost, dformat, detail=True) +beancount.core.position.cost_to_str(cost, dformat, detail=True)

    @@ -15939,48 +19042,48 @@

    Source code in beancount/core/position.py -
    def cost_to_str(cost, dformat, detail=True):
    -    """Format an instance of Cost or a CostSpec to a string.
    -
    -    Args:
    -      cost: An instance of Cost or CostSpec.
    -      dformat: A DisplayFormatter object.
    -      detail: A boolean, true if we should render the non-amount components.
    -    Returns:
    -      A string, suitable for formatting.
    -    """
    -    strlist = []
    -
    -    if isinstance(cost, Cost):
    -        if isinstance(cost.number, Decimal):
    -            strlist.append(Amount(cost.number, cost.currency).to_string(dformat))
    -        if detail:
    -            if cost.date:
    -                strlist.append(cost.date.isoformat())
    -            if cost.label:
    -                strlist.append('"{}"'.format(cost.label))
    -
    -    elif isinstance(cost, CostSpec):
    -        if isinstance(cost.number_per, Decimal) or isinstance(cost.number_total, Decimal):
    -            amountlist = []
    -            if isinstance(cost.number_per, Decimal):
    -                amountlist.append(dformat.format(cost.number_per))
    -            if isinstance(cost.number_total, Decimal):
    -                amountlist.append('#')
    -                amountlist.append(dformat.format(cost.number_total))
    -            if isinstance(cost.currency, str):
    -                amountlist.append(cost.currency)
    -            strlist.append(' '.join(amountlist))
    -        if detail:
    -            if cost.date:
    -                strlist.append(cost.date.isoformat())
    -            if cost.label:
    -                strlist.append('"{}"'.format(cost.label))
    -            if cost.merge:
    -                strlist.append('*')
    -
    -    return ', '.join(strlist)
    -
    +
    def cost_to_str(cost, dformat, detail=True):
    +    """Format an instance of Cost or a CostSpec to a string.
    +
    +    Args:
    +      cost: An instance of Cost or CostSpec.
    +      dformat: A DisplayFormatter object.
    +      detail: A boolean, true if we should render the non-amount components.
    +    Returns:
    +      A string, suitable for formatting.
    +    """
    +    strlist = []
    +
    +    if isinstance(cost, Cost):
    +        if isinstance(cost.number, Decimal):
    +            strlist.append(Amount(cost.number, cost.currency).to_string(dformat))
    +        if detail:
    +            if cost.date:
    +                strlist.append(cost.date.isoformat())
    +            if cost.label:
    +                strlist.append('"{}"'.format(cost.label))
    +
    +    elif isinstance(cost, CostSpec):
    +        if isinstance(cost.number_per, Decimal) or isinstance(cost.number_total, Decimal):
    +            amountlist = []
    +            if isinstance(cost.number_per, Decimal):
    +                amountlist.append(dformat.format(cost.number_per))
    +            if isinstance(cost.number_total, Decimal):
    +                amountlist.append("#")
    +                amountlist.append(dformat.format(cost.number_total))
    +            if isinstance(cost.currency, str):
    +                amountlist.append(cost.currency)
    +            strlist.append(" ".join(amountlist))
    +        if detail:
    +            if cost.date:
    +                strlist.append(cost.date.isoformat())
    +            if cost.label:
    +                strlist.append('"{}"'.format(cost.label))
    +            if cost.merge:
    +                strlist.append("*")
    +
    +    return ", ".join(strlist)
    +
    @@ -15993,7 +19096,7 @@

    -beancount.core.position.from_amounts(units, cost_amount=None) +beancount.core.position.from_amounts(units, cost_amount=None)

    @@ -16037,23 +19140,26 @@

    Source code in beancount/core/position.py -
    @staticmethod
    -def from_amounts(units, cost_amount=None):
    -    """Create a position from an amount and a cost.
    -
    -    Args:
    -      amount: An amount, that represents the number of units and the lot currency.
    -      cost_amount: If not None, represents the cost amount.
    -    Returns:
    -      A Position instance.
    -    """
    -    assert cost_amount is None or isinstance(cost_amount, Amount), (
    -        "Invalid type for cost: {}".format(cost_amount))
    -    cost = (Cost(cost_amount.number, cost_amount.currency, None, None)
    -            if cost_amount else
    -            None)
    -    return Position(units, cost)
    -
    +
    @staticmethod
    +def from_amounts(units, cost_amount=None):
    +    """Create a position from an amount and a cost.
    +
    +    Args:
    +      amount: An amount, that represents the number of units and the lot currency.
    +      cost_amount: If not None, represents the cost amount.
    +    Returns:
    +      A Position instance.
    +    """
    +    assert cost_amount is None or isinstance(
    +        cost_amount, Amount
    +    ), "Invalid type for cost: {}".format(cost_amount)
    +    cost = (
    +        Cost(cost_amount.number, cost_amount.currency, None, None)
    +        if cost_amount
    +        else None
    +    )
    +    return Position(units, cost)
    +
    @@ -16066,7 +19172,7 @@

    -beancount.core.position.from_string(string) +beancount.core.position.from_string(string)

    @@ -16111,80 +19217,81 @@

    Source code in beancount/core/position.py -
    @staticmethod
    -def from_string(string):
    -    """Create a position from a string specification.
    -
    -    This is a miniature parser used for building tests.
    -
    -    Args:
    -      string: A string of <number> <currency> with an optional {<number>
    -        <currency>} for the cost, similar to the parser syntax.
    -    Returns:
    -      A new instance of Position.
    -    """
    -    match = re.match(
    -        (r'\s*({})\s+({})'
    -         r'(?:\s+{{([^}}]*)}})?'
    -         r'\s*$').format(NUMBER_RE, CURRENCY_RE),
    -        string)
    -    if not match:
    -        raise ValueError("Invalid string for position: '{}'".format(string))
    -
    -    number = D(match.group(1))
    -    currency = match.group(2)
    -
    -    # Parse a cost expression.
    -    cost_number = None
    -    cost_currency = None
    -    date = None
    -    label = None
    -    cost_expression = match.group(3)
    -    if match.group(3):
    -        expressions = [expr.strip() for expr in re.split('[,/]', cost_expression)]
    -        for expr in expressions:
    -
    -            # Match a compound number.
    -            match = re.match(
    -                r'({NUMBER_RE})\s*(?:#\s*({NUMBER_RE}))?\s+({CURRENCY_RE})$'
    -                .format(NUMBER_RE=NUMBER_RE, CURRENCY_RE=CURRENCY_RE),
    -                expr
    -            )
    -            if match:
    -                per_number, total_number, cost_currency = match.group(1, 2, 3)
    -                per_number = D(per_number) if per_number else ZERO
    -                total_number = D(total_number) if total_number else ZERO
    -                if total_number:
    -                    # Calculate the per-unit cost.
    -                    total = number * per_number + total_number
    -                    per_number = total / number
    -                cost_number = per_number
    -                continue
    -
    -            # Match a date.
    -            match = re.match(r'(\d\d\d\d)[-/](\d\d)[-/](\d\d)$', expr)
    -            if match:
    -                date = datetime.date(*map(int, match.group(1, 2, 3)))
    -                continue
    -
    -            # Match a label.
    -            match = re.match(r'"([^"]+)*"$', expr)
    -            if match:
    -                label = match.group(1)
    -                continue
    -
    -            # Match a merge-cost marker.
    -            match = re.match(r'\*$', expr)
    -            if match:
    -                raise ValueError("Merge-code not supported in string constructor.")
    -
    -            raise ValueError("Invalid cost component: '{}'".format(expr))
    -        cost = Cost(cost_number, cost_currency, date, label)
    -    else:
    -        cost = None
    -
    -    return Position(Amount(number, currency), cost)
    -
    +
    @staticmethod
    +def from_string(string):
    +    """Create a position from a string specification.
    +
    +    This is a miniature parser used for building tests.
    +
    +    Args:
    +      string: A string of <number> <currency> with an optional {<number>
    +        <currency>} for the cost, similar to the parser syntax.
    +    Returns:
    +      A new instance of Position.
    +    """
    +    match = re.match(
    +        (r"\s*({})\s+({})" r"(?:\s+{{([^}}]*)}})?" r"\s*$").format(
    +            NUMBER_RE, CURRENCY_RE
    +        ),
    +        string,
    +    )
    +    if not match:
    +        raise ValueError("Invalid string for position: '{}'".format(string))
    +
    +    number = D(match.group(1))
    +    currency = match.group(2)
    +
    +    # Parse a cost expression.
    +    cost_number = None
    +    cost_currency = None
    +    date = None
    +    label = None
    +    cost_expression = match.group(3)
    +    if match.group(3):
    +        expressions = [expr.strip() for expr in re.split("[,/]", cost_expression)]
    +        for expr in expressions:
    +            # Match a compound number.
    +            match = re.match(
    +                r"({NUMBER_RE})\s*(?:#\s*({NUMBER_RE}))?\s+({CURRENCY_RE})$".format(
    +                    NUMBER_RE=NUMBER_RE, CURRENCY_RE=CURRENCY_RE
    +                ),
    +                expr,
    +            )
    +            if match:
    +                per_number, total_number, cost_currency = match.group(1, 2, 3)
    +                per_number = D(per_number) if per_number else ZERO
    +                total_number = D(total_number) if total_number else ZERO
    +                if total_number:
    +                    # Calculate the per-unit cost.
    +                    total = number * per_number + total_number
    +                    per_number = total / number
    +                cost_number = per_number
    +                continue
    +
    +            # Match a date.
    +            match = re.match(r"(\d\d\d\d)[-/](\d\d)[-/](\d\d)$", expr)
    +            if match:
    +                date = datetime.date(*map(int, match.group(1, 2, 3)))
    +                continue
    +
    +            # Match a label.
    +            match = re.match(r'"([^"]+)*"$', expr)
    +            if match:
    +                label = match.group(1)
    +                continue
    +
    +            # Match a merge-cost marker.
    +            match = re.match(r"\*$", expr)
    +            if match:
    +                raise ValueError("Merge-code not supported in string constructor.")
    +
    +            raise ValueError("Invalid cost component: '{}'".format(expr))
    +        cost = Cost(cost_number, cost_currency, date, label)
    +    else:
    +        cost = None
    +
    +    return Position(Amount(number, currency), cost)
    +
    @@ -16197,7 +19304,7 @@

    -beancount.core.position.get_position(posting) +beancount.core.position.get_position(posting)

    @@ -16240,16 +19347,16 @@

    Source code in beancount/core/position.py -
    def get_position(posting):
    -    """Build a Position instance from a Posting instance.
    -
    -    Args:
    -      posting: An instance of Posting.
    -    Returns:
    -      An instance of Position.
    -    """
    -    return Position(posting.units, posting.cost)
    -
    +
    def get_position(posting):
    +    """Build a Position instance from a Posting instance.
    +
    +    Args:
    +      posting: An instance of Posting.
    +    Returns:
    +      An instance of Position.
    +    """
    +    return Position(posting.units, posting.cost)
    +
    @@ -16262,7 +19369,7 @@

    -beancount.core.position.to_string(pos, dformat=<beancount.core.display_context.DisplayFormatter object at 0x78e868ae5e50>, detail=True) +beancount.core.position.to_string(pos, dformat=<beancount.core.display_context.DisplayFormatter object at 0x78fcde6b7290>, detail=True)

    @@ -16309,23 +19416,23 @@

    Source code in beancount/core/position.py -
    def to_string(pos, dformat=DEFAULT_FORMATTER, detail=True):
    -    """Render the Position or Posting instance to a string.
    -
    -    Args:
    -      pos: An instance of Position or Posting.
    -      dformat: An instance of DisplayFormatter.
    -      detail: A boolean, true if we should only render the lot details
    -       beyond the cost (lot-date, label, etc.). If false, we only render
    -       the cost, if present.
    -    Returns:
    -      A string, the rendered position.
    -    """
    -    pos_str = pos.units.to_string(dformat)
    -    if pos.cost is not None:
    -        pos_str = '{} {{{}}}'.format(pos_str, cost_to_str(pos.cost, dformat, detail))
    -    return pos_str
    -
    +
    def to_string(pos, dformat=DEFAULT_FORMATTER, detail=True):
    +    """Render the Position or Posting instance to a string.
    +
    +    Args:
    +      pos: An instance of Position or Posting.
    +      dformat: An instance of DisplayFormatter.
    +      detail: A boolean, true if we should only render the lot details
    +       beyond the cost (lot-date, label, etc.). If false, we only render
    +       the cost, if present.
    +    Returns:
    +      A string, the rendered position.
    +    """
    +    pos_str = pos.units.to_string(dformat)
    +    if pos.cost is not None:
    +        pos_str = "{} {{{}}}".format(pos_str, cost_to_str(pos.cost, dformat, detail))
    +    return pos_str
    +
    @@ -16443,7 +19550,7 @@

    -beancount.core.prices.build_price_map(entries) +beancount.core.prices.build_price_map(entries)

    @@ -16498,84 +19605,83 @@

    Source code in beancount/core/prices.py -
    def build_price_map(entries):
    -    """Build a price map from a list of arbitrary entries.
    -
    -    If multiple prices are found for the same (currency, cost-currency) pair at
    -    the same date, the latest date is kept and the earlier ones (for that day)
    -    are discarded.
    -
    -    If inverse price pairs are found, e.g. USD in AUD and AUD in USD, the
    -    inverse that has the smallest number of price points is converted into the
    -    one that has the most price points. In that way they are reconciled into a
    -    single one.
    -
    -    Args:
    -      entries: A list of directives, hopefully including some Price and/or
    -      Transaction entries.
    -    Returns:
    -      A dict of (currency, cost-currency) keys to sorted lists of (date, number)
    -      pairs, where 'date' is the date the price occurs at and 'number' a Decimal
    -      that represents the price, or rate, between these two
    -      currencies/commodities. Each date occurs only once in the sorted list of
    -      prices of a particular key. All of the inverses are automatically
    -      generated in the price map.
    -    """
    -    # Fetch a list of all the price entries seen in the ledger.
    -    price_entries = [entry
    -                     for entry in entries
    -                     if isinstance(entry, Price)]
    -
    -    # Build a map of exchange rates between these units.
    -    # (base-currency, quote-currency) -> List of (date, rate).
    -    price_map = collections.defaultdict(list)
    -    for price in price_entries:
    -        base_quote = (price.currency, price.amount.currency)
    -        price_map[base_quote].append((price.date, price.amount.number))
    -
    -    # Find pairs of inversed units.
    -    inversed_units = []
    -    for base_quote, values in price_map.items():
    -        base, quote = base_quote
    -        if (quote, base) in price_map:
    -            inversed_units.append(base_quote)
    -
    -    # Find pairs of inversed units, and swallow the conversion with the smaller
    -    # number of rates into the other one.
    -    for base, quote in inversed_units:
    -        bq_prices = price_map[(base, quote)]
    -        qb_prices = price_map[(quote, base)]
    -        remove = ((base, quote)
    -                  if len(bq_prices) < len(qb_prices)
    -                  else (quote, base))
    -        base, quote = remove
    -
    -        remove_list = price_map[remove]
    -        insert_list = price_map[(quote, base)]
    -        del price_map[remove]
    -
    -        inverted_list = [(date, ONE/rate)
    -                         for (date, rate) in remove_list
    -                         if rate != ZERO]
    -        insert_list.extend(inverted_list)
    -
    -    # Unzip and sort each of the entries and eliminate duplicates on the date.
    -    sorted_price_map = PriceMap({
    -        base_quote: list(misc_utils.sorted_uniquify(date_rates, lambda x: x[0], last=True))
    -        for (base_quote, date_rates) in price_map.items()})
    -
    -    # Compute and insert all the inverted rates.
    -    forward_pairs = list(sorted_price_map.keys())
    -    for (base, quote), price_list in list(sorted_price_map.items()):
    -        # Note: You have to filter out zero prices for zero-cost postings, like
    -        # gifted options.
    -        sorted_price_map[(quote, base)] = [
    -            (date, ONE/price) for date, price in price_list
    -            if price != ZERO]
    -
    -    sorted_price_map.forward_pairs = forward_pairs
    -    return sorted_price_map
    -
    +
    def build_price_map(entries):
    +    """Build a price map from a list of arbitrary entries.
    +
    +    If multiple prices are found for the same (currency, cost-currency) pair at
    +    the same date, the latest date is kept and the earlier ones (for that day)
    +    are discarded.
    +
    +    If inverse price pairs are found, e.g. USD in AUD and AUD in USD, the
    +    inverse that has the smallest number of price points is converted into the
    +    one that has the most price points. In that way they are reconciled into a
    +    single one.
    +
    +    Args:
    +      entries: A list of directives, hopefully including some Price and/or
    +      Transaction entries.
    +    Returns:
    +      A dict of (currency, cost-currency) keys to sorted lists of (date, number)
    +      pairs, where 'date' is the date the price occurs at and 'number' a Decimal
    +      that represents the price, or rate, between these two
    +      currencies/commodities. Each date occurs only once in the sorted list of
    +      prices of a particular key. All of the inverses are automatically
    +      generated in the price map.
    +    """
    +    # Fetch a list of all the price entries seen in the ledger.
    +    price_entries = [entry for entry in entries if isinstance(entry, Price)]
    +
    +    # Build a map of exchange rates between these units.
    +    # (base-currency, quote-currency) -> List of (date, rate).
    +    price_map = collections.defaultdict(list)
    +    for price in price_entries:
    +        base_quote = (price.currency, price.amount.currency)
    +        price_map[base_quote].append((price.date, price.amount.number))
    +
    +    # Find pairs of inversed units.
    +    inversed_units = []
    +    for base_quote, values in price_map.items():
    +        base, quote = base_quote
    +        if (quote, base) in price_map:
    +            inversed_units.append(base_quote)
    +
    +    # Find pairs of inversed units, and swallow the conversion with the smaller
    +    # number of rates into the other one.
    +    for base, quote in inversed_units:
    +        bq_prices = price_map[(base, quote)]
    +        qb_prices = price_map[(quote, base)]
    +        remove = (base, quote) if len(bq_prices) < len(qb_prices) else (quote, base)
    +        base, quote = remove
    +
    +        remove_list = price_map[remove]
    +        insert_list = price_map[(quote, base)]
    +        del price_map[remove]
    +
    +        inverted_list = [(date, ONE / rate) for (date, rate) in remove_list if rate != ZERO]
    +        insert_list.extend(inverted_list)
    +
    +    # Unzip and sort each of the entries and eliminate duplicates on the date.
    +    sorted_price_map = PriceMap(
    +        {
    +            base_quote: list(
    +                misc_utils.sorted_uniquify(date_rates, lambda x: x[0], last=True)
    +            )
    +            for (base_quote, date_rates) in price_map.items()
    +        }
    +    )
    +
    +    # Compute and insert all the inverted rates.
    +    forward_pairs = list(sorted_price_map.keys())
    +    for (base, quote), price_list in list(sorted_price_map.items()):
    +        # Note: You have to filter out zero prices for zero-cost postings, like
    +        # gifted options.
    +        sorted_price_map[(quote, base)] = [
    +            (date, ONE / price) for date, price in price_list if price != ZERO
    +        ]
    +
    +    sorted_price_map.forward_pairs = forward_pairs
    +    return sorted_price_map
    +
    @@ -16588,7 +19694,7 @@

    -beancount.core.prices.get_all_prices(price_map, base_quote) +beancount.core.prices.get_all_prices(price_map, base_quote)

    @@ -16651,23 +19757,23 @@

    Source code in beancount/core/prices.py -
    def get_all_prices(price_map, base_quote):
    -    """Return a sorted list of all (date, number) price pairs.
    -
    -    Args:
    -      price_map: A price map, which is a dict of (base, quote) -> list of (date,
    -        number) tuples, as created by build_price_map.
    -      base_quote: A pair of strings, the base currency to lookup, and the quote
    -        currency to lookup, which expresses which units the base currency is
    -        denominated in. This may also just be a string, with a '/' separator.
    -    Returns:
    -      A list of (date, Decimal) pairs, sorted by date.
    -    Raises:
    -      KeyError: If the base/quote could not be found.
    -    """
    -    base_quote = normalize_base_quote(base_quote)
    -    return _lookup_price_and_inverse(price_map, base_quote)
    -
    +
    def get_all_prices(price_map, base_quote):
    +    """Return a sorted list of all (date, number) price pairs.
    +
    +    Args:
    +      price_map: A price map, which is a dict of (base, quote) -> list of (date,
    +        number) tuples, as created by build_price_map.
    +      base_quote: A pair of strings, the base currency to lookup, and the quote
    +        currency to lookup, which expresses which units the base currency is
    +        denominated in. This may also just be a string, with a '/' separator.
    +    Returns:
    +      A list of (date, Decimal) pairs, sorted by date.
    +    Raises:
    +      KeyError: If the base/quote could not be found.
    +    """
    +    base_quote = normalize_base_quote(base_quote)
    +    return _lookup_price_and_inverse(price_map, base_quote)
    +
    @@ -16680,7 +19786,7 @@

    -beancount.core.prices.get_last_price_entries(entries, date) +beancount.core.prices.get_last_price_entries(entries, date)

    @@ -16726,26 +19832,26 @@

    Source code in beancount/core/prices.py -
    def get_last_price_entries(entries, date):
    -    """Run through the entries until the given date and return the last
    -    Price entry encountered for each (currency, cost-currency) pair.
    -
    -    Args:
    -      entries: A list of directives.
    -      date: An instance of datetime.date. If None, the very latest price
    -        is returned.
    -    Returns:
    -      A list of price entries.
    -    """
    -    price_entry_map = {}
    -    for entry in entries:
    -        if date is not None and entry.date >= date:
    -            break
    -        if isinstance(entry, Price):
    -            base_quote = (entry.currency, entry.amount.currency)
    -            price_entry_map[base_quote] = entry
    -    return sorted(price_entry_map.values(), key=data.entry_sortkey)
    -
    +
    def get_last_price_entries(entries, date):
    +    """Run through the entries until the given date and return the last
    +    Price entry encountered for each (currency, cost-currency) pair.
    +
    +    Args:
    +      entries: A list of directives.
    +      date: An instance of datetime.date. If None, the very latest price
    +        is returned.
    +    Returns:
    +      A list of price entries.
    +    """
    +    price_entry_map = {}
    +    for entry in entries:
    +        if date is not None and entry.date >= date:
    +            break
    +        if isinstance(entry, Price):
    +            base_quote = (entry.currency, entry.amount.currency)
    +            price_entry_map[base_quote] = entry
    +    return sorted(price_entry_map.values(), key=data.entry_sortkey)
    +
    @@ -16758,7 +19864,7 @@

    -beancount.core.prices.get_latest_price(price_map, base_quote) +beancount.core.prices.get_latest_price(price_map, base_quote)

    @@ -16806,37 +19912,37 @@

    Source code in beancount/core/prices.py -
    def get_latest_price(price_map, base_quote):
    -    """Return the latest price/rate from a price map for the given base/quote pair.
    -    This is often used to just get the 'current' price if you're looking at the
    -    entire set of entries.
    -
    -    Args:
    -      price_map: A price map, which is a dict of (base, quote) -> list of (date,
    -        number) tuples, as created by build_price_map.
    -    Returns:
    -      A pair of (date, number), where 'date' is a datetime.date instance and
    -      'number' is a Decimal of the price, or rate, at that date. The date is the
    -      latest date which we have an available price for in the price map.
    -    """
    -    base_quote = normalize_base_quote(base_quote)
    -
    -    # Handle the degenerate case of a currency priced into its own.
    -    base, quote = base_quote
    -    if quote is None or base == quote:
    -        return (None, ONE)
    -
    -    # Look up the list and return the latest element. The lists are assumed to
    -    # be sorted.
    -    try:
    -        price_list = _lookup_price_and_inverse(price_map, base_quote)
    -    except KeyError:
    -        price_list = None
    -    if price_list:
    -        return price_list[-1]
    -    else:
    -        return None, None
    -
    +
    def get_latest_price(price_map, base_quote):
    +    """Return the latest price/rate from a price map for the given base/quote pair.
    +    This is often used to just get the 'current' price if you're looking at the
    +    entire set of entries.
    +
    +    Args:
    +      price_map: A price map, which is a dict of (base, quote) -> list of (date,
    +        number) tuples, as created by build_price_map.
    +    Returns:
    +      A pair of (date, number), where 'date' is a datetime.date instance and
    +      'number' is a Decimal of the price, or rate, at that date. The date is the
    +      latest date which we have an available price for in the price map.
    +    """
    +    base_quote = normalize_base_quote(base_quote)
    +
    +    # Handle the degenerate case of a currency priced into its own.
    +    base, quote = base_quote
    +    if quote is None or base == quote:
    +        return (None, ONE)
    +
    +    # Look up the list and return the latest element. The lists are assumed to
    +    # be sorted.
    +    try:
    +        price_list = _lookup_price_and_inverse(price_map, base_quote)
    +    except KeyError:
    +        price_list = None
    +    if price_list:
    +        return price_list[-1]
    +    else:
    +        return None, None
    +
    @@ -16849,7 +19955,7 @@

    -beancount.core.prices.get_price(price_map, base_quote, date=None) +beancount.core.prices.get_price(price_map, base_quote, date=None)

    @@ -16900,43 +20006,43 @@

    Source code in beancount/core/prices.py -
    def get_price(price_map, base_quote, date=None):
    -    """Return the price as of the given date.
    -
    -    If the date is unspecified, return the latest price.
    -
    -    Args:
    -      price_map: A price map, which is a dict of (base, quote) -> list of (date,
    -        number) tuples, as created by build_price_map.
    -      base_quote: A pair of strings, the base currency to lookup, and the quote
    -        currency to lookup, which expresses which units the base currency is
    -        denominated in. This may also just be a string, with a '/' separator.
    -      date: A datetime.date instance, the date at which we want the conversion
    -        rate.
    -    Returns:
    -      A pair of (datetime.date, Decimal) instance. If no price information could
    -      be found, return (None, None).
    -    """
    -    if date is None:
    -        return get_latest_price(price_map, base_quote)
    -
    -    base_quote = normalize_base_quote(base_quote)
    -
    -    # Handle the degenerate case of a currency priced into its own.
    -    base, quote = base_quote
    -    if quote is None or base == quote:
    -        return (None, ONE)
    -
    -    try:
    -        price_list = _lookup_price_and_inverse(price_map, base_quote)
    -        index = bisect_key.bisect_right_with_key(price_list, date, key=lambda x: x[0])
    -        if index == 0:
    -            return None, None
    -        else:
    -            return price_list[index-1]
    -    except KeyError:
    -        return None, None
    -
    +
    def get_price(price_map, base_quote, date=None):
    +    """Return the price as of the given date.
    +
    +    If the date is unspecified, return the latest price.
    +
    +    Args:
    +      price_map: A price map, which is a dict of (base, quote) -> list of (date,
    +        number) tuples, as created by build_price_map.
    +      base_quote: A pair of strings, the base currency to lookup, and the quote
    +        currency to lookup, which expresses which units the base currency is
    +        denominated in. This may also just be a string, with a '/' separator.
    +      date: A datetime.date instance, the date at which we want the conversion
    +        rate.
    +    Returns:
    +      A pair of (datetime.date, Decimal) instance. If no price information could
    +      be found, return (None, None).
    +    """
    +    if date is None:
    +        return get_latest_price(price_map, base_quote)
    +
    +    base_quote = normalize_base_quote(base_quote)
    +
    +    # Handle the degenerate case of a currency priced into its own.
    +    base, quote = base_quote
    +    if quote is None or base == quote:
    +        return (None, ONE)
    +
    +    try:
    +        price_list = _lookup_price_and_inverse(price_map, base_quote)
    +        index = bisect_key.bisect_right_with_key(price_list, date, key=lambda x: x[0])
    +        if index == 0:
    +            return None, None
    +        else:
    +            return price_list[index - 1]
    +    except KeyError:
    +        return None, None
    +
    @@ -16949,7 +20055,7 @@

    -beancount.core.prices.normalize_base_quote(base_quote) +beancount.core.prices.normalize_base_quote(base_quote)

    @@ -16994,23 +20100,227 @@

    Source code in beancount/core/prices.py -
    def normalize_base_quote(base_quote):
    -    """Convert a slash-separated string to a pair of strings.
    -
    -    Args:
    -      base_quote: A pair of strings, the base currency to lookup, and the quote
    -        currency to lookup, which expresses which units the base currency is
    -        denominated in. This may also just be a string, with a '/' separator.
    -    Returns:
    -      A pair of strings.
    -    """
    -    if isinstance(base_quote, str):
    -        base_quote_norm = tuple(base_quote.split('/'))
    -        assert len(base_quote_norm) == 2, base_quote
    -        base_quote = base_quote_norm
    -    assert isinstance(base_quote, tuple), base_quote
    -    return base_quote
    -
    +
    def normalize_base_quote(base_quote):
    +    """Convert a slash-separated string to a pair of strings.
    +
    +    Args:
    +      base_quote: A pair of strings, the base currency to lookup, and the quote
    +        currency to lookup, which expresses which units the base currency is
    +        denominated in. This may also just be a string, with a '/' separator.
    +    Returns:
    +      A pair of strings.
    +    """
    +    if isinstance(base_quote, str):
    +        base_quote_norm = tuple(base_quote.split("/"))
    +        assert len(base_quote_norm) == 2, base_quote
    +        base_quote = base_quote_norm
    +    assert isinstance(base_quote, tuple), base_quote
    +    return base_quote
    +
    +
    + + + + + + +
    + + + +

    +beancount.core.prices.project(orig_price_map, from_currency, to_currency, base_currencies=None) + + +

    + +
    + +

    Project all prices with a quote currency to another quote currency.

    +

    Say you have a price for HOOL in USD and you'd like to convert HOOL to CAD. +If there aren't any (HOOL, CAD) price pairs in the database it will remain +unconverted. Projecting from USD to CAD will compute combined rates and +insert corresponding prices over all base currencies (like HOOL). In this +example, each of the (HOOL, USD) prices would see an inserted (HOOL, CAD) +price inserted at the same date.

    +

    It is common to make these projections when reducing inventories in a ledger +that states multiple operating currency pairs, when for example, one wants +to compute total value of a set of accounts in one of those currencies.

    +

    Please note that:

    +
      +
    • +

      Even if the target pair has existing entries, projection will still be + applied. For example, is there exist some (HOOL, CAD) prices, the + projection in the example above will still insert some new price points to + it.

      +
    • +
    • +

      However, projected prices colliding existing ones at the same date will + not override them.

      +
    • +
    • +

      Projection will fail to insert a new price if the conversion between to + and from currencies has no existing prices (e.g. before its first price + entry).

      +
    • +
    • +

      Perhaps most importantly, we only insert price points at dates where the + base commodity has a price point. In other words, if we have prices for + dates A and C and the rate changes between these dates at date B, we don't + synthesize a new price at date B. A more accurate method to get projected + prices that takes into account varying rates is to do multiple lookups. + We'll eventually add a method to query it via a specified list of + intermediate pairs. {c1bd24f8d4b7}

      +
    • +
    + + + + + + + + + + + + +
    Parameters: +
      +
    • orig_price_map (PriceMap) – An existing price map.

    • +
    • from_currency (str) – The quote currency with existing project points (e.g., USD).

    • +
    • to_currency (str) – The quote currency to insert price points for (e.g., CAD).

    • +
    • base_currencies (Optional[Set[str]]) – An optional set of commodities to restrict the +projections to (e.g., {HOOL}).

    • +
    +
    + + + + + + + + + + + +
    Returns: +
      +
    • PriceMap – A new price map, with the extra projected prices. The original price map +is kept intact.

    • +
    +
    +
    + Source code in beancount/core/prices.py +
    def project(
    +    orig_price_map: PriceMap,
    +    from_currency: Currency,
    +    to_currency: Currency,
    +    base_currencies: Optional[Set[Currency]] = None,
    +) -> PriceMap:
    +    """Project all prices with a quote currency to another quote currency.
    +
    +    Say you have a price for HOOL in USD and you'd like to convert HOOL to CAD.
    +    If there aren't any (HOOL, CAD) price pairs in the database it will remain
    +    unconverted. Projecting from USD to CAD will compute combined rates and
    +    insert corresponding prices over all base currencies (like HOOL). In this
    +    example, each of the (HOOL, USD) prices would see an inserted (HOOL, CAD)
    +    price inserted at the same date.
    +
    +    It is common to make these projections when reducing inventories in a ledger
    +    that states multiple operating currency pairs, when for example, one wants
    +    to compute total value of a set of accounts in one of those currencies.
    +
    +    Please note that:
    +
    +    - Even if the target pair has existing entries, projection will still be
    +      applied. For example, is there exist some (HOOL, CAD) prices, the
    +      projection in the example above will still insert some new price points to
    +      it.
    +
    +    - However, projected prices colliding existing ones at the same date will
    +      not override them.
    +
    +    - Projection will fail to insert a new price if the conversion between to
    +      and from currencies has no existing prices (e.g. before its first price
    +      entry).
    +
    +    - Perhaps most importantly, we only insert price points at dates where the
    +      base commodity has a price point. In other words, if we have prices for
    +      dates A and C and the rate changes between these dates at date B, we don't
    +      synthesize a new price at date B. A more accurate method to get projected
    +      prices that takes into account varying rates is to do multiple lookups.
    +      We'll eventually add a method to query it via a specified list of
    +      intermediate pairs. {c1bd24f8d4b7}
    +
    +    Args:
    +      orig_price_map: An existing price map.
    +      from_currency: The quote currency with existing project points (e.g., USD).
    +      to_currency: The quote currency to insert price points for (e.g., CAD).
    +      base_currencies: An optional set of commodities to restrict the
    +        projections to (e.g., {HOOL}).
    +    Returns:
    +      A new price map, with the extra projected prices. The original price map
    +      is kept intact.
    +    """
    +    # If nothing is requested, return the original map.
    +    if from_currency == to_currency:
    +        return orig_price_map
    +
    +    # Avoid mutating the input map.
    +    price_map = {key: list(value) for key, value in orig_price_map.items()}
    +
    +    # Process the entire database (it's not indexed by quote currency).
    +    currency_pair = (from_currency, to_currency)
    +    for base_quote, prices in list(price_map.items()):
    +        # Filter just the currencies to convert.
    +        base, quote = base_quote
    +        if quote != from_currency:
    +            continue
    +
    +        # Skip currencies not requested if a constraint has been provided.
    +        # {4bb702d82c8a}
    +        if base_currencies and base not in base_currencies:
    +            continue
    +
    +        # Create a mapping of existing prices so we can avoid date collisions.
    +        existing_prices = (
    +            {date for date, _ in price_map[(base, to_currency)]}
    +            if (base, to_currency) in price_map
    +            else set()
    +        )
    +
    +        # Project over each of the prices.
    +        new_projected = []
    +        for date, price in prices:
    +            rate_date, rate = get_price(price_map, currency_pair, date)
    +            if rate is None:
    +                # There is no conversion rate at this time; skip projection.
    +                # {b2b23353275d}.
    +                continue
    +            if rate_date in existing_prices:
    +                # Skip collisions in date. {97a5703ac517}
    +                continue
    +
    +            # Append the new rate.
    +            new_price = price * rate
    +            new_projected.append((date, new_price))
    +
    +        # Make sure the resulting lists are sorted.
    +        if new_projected:
    +            projected = price_map.setdefault((base, to_currency), [])
    +            projected.extend(new_projected)
    +            projected.sort()
    +
    +            inverted = price_map.setdefault((to_currency, base), [])
    +            inverted.extend(
    +                (date, ZERO if rate == ZERO else ONE / rate) for date, rate in new_projected
    +            )
    +            inverted.sort()
    +
    +    return price_map
    +
    @@ -17136,7 +20446,7 @@

    -beancount.core.realization.RealAccount.__eq__(self, other) +beancount.core.realization.RealAccount.__eq__(self, other) special @@ -17182,19 +20492,21 @@

    Source code in beancount/core/realization.py -
    def __eq__(self, other):
    -    """Equality predicate. All attributes are compared.
    -
    -    Args:
    -      other: Another instance of RealAccount.
    -    Returns:
    -      A boolean, True if the two real accounts are equal.
    -    """
    -    return (dict.__eq__(self, other) and
    -            self.account == other.account and
    -            self.balance == other.balance and
    -            self.txn_postings == other.txn_postings)
    -
    +
    def __eq__(self, other):
    +    """Equality predicate. All attributes are compared.
    +
    +    Args:
    +      other: Another instance of RealAccount.
    +    Returns:
    +      A boolean, True if the two real accounts are equal.
    +    """
    +    return (
    +        dict.__eq__(self, other)
    +        and self.account == other.account
    +        and self.balance == other.balance
    +        and self.txn_postings == other.txn_postings
    +    )
    +

    @@ -17207,7 +20519,7 @@

    -beancount.core.realization.RealAccount.__init__(self, account_name, *args, **kwargs) +beancount.core.realization.RealAccount.__init__(self, account_name, *args, **kwargs) special @@ -17237,18 +20549,18 @@

    Source code in beancount/core/realization.py -
    def __init__(self, account_name, *args, **kwargs):
    -    """Create a RealAccount instance.
    -
    -    Args:
    -      account_name: a string, the name of the account. Maybe not be None.
    -    """
    -    super().__init__(*args, **kwargs)
    -    assert isinstance(account_name, str)
    -    self.account = account_name
    -    self.txn_postings = []
    -    self.balance = inventory.Inventory()
    -
    +
    def __init__(self, account_name, *args, **kwargs):
    +    """Create a RealAccount instance.
    +
    +    Args:
    +      account_name: a string, the name of the account. Maybe not be None.
    +    """
    +    super().__init__(*args, **kwargs)
    +    assert isinstance(account_name, str)
    +    self.account = account_name
    +    self.txn_postings = []
    +    self.balance = inventory.Inventory()
    +
    @@ -17261,7 +20573,7 @@

    -beancount.core.realization.RealAccount.__ne__(self, other) +beancount.core.realization.RealAccount.__ne__(self, other) special @@ -17307,16 +20619,16 @@

    Source code in beancount/core/realization.py -
    def __ne__(self, other):
    -    """Not-equality predicate. See __eq__.
    -
    -    Args:
    -      other: Another instance of RealAccount.
    -    Returns:
    -      A boolean, True if the two real accounts are not equal.
    -    """
    -    return not self.__eq__(other)
    -
    +
    def __ne__(self, other):
    +    """Not-equality predicate. See __eq__.
    +
    +    Args:
    +      other: Another instance of RealAccount.
    +    Returns:
    +      A boolean, True if the two real accounts are not equal.
    +    """
    +    return not self.__eq__(other)
    +
    @@ -17329,7 +20641,7 @@

    -beancount.core.realization.RealAccount.__setitem__(self, key, value) +beancount.core.realization.RealAccount.__setitem__(self, key, value) special @@ -17377,25 +20689,28 @@

    Source code in beancount/core/realization.py -
    def __setitem__(self, key, value):
    -    """Prevent the setting of non-string or non-empty keys on this dict.
    -
    -    Args:
    -      key: The dictionary key. Must be a string.
    -      value: The value, must be a RealAccount instance.
    -    Raises:
    -      KeyError: If the key is not a string, or is invalid.
    -      ValueError: If the value is not a RealAccount instance.
    -    """
    -    if not isinstance(key, str) or not key:
    -        raise KeyError("Invalid RealAccount key: '{}'".format(key))
    -    if not isinstance(value, RealAccount):
    -        raise ValueError("Invalid RealAccount value: '{}'".format(value))
    -    if not value.account.endswith(key):
    -        raise ValueError("RealAccount name '{}' inconsistent with key: '{}'".format(
    -            value.account, key))
    -    return super().__setitem__(key, value)
    -
    +
    def __setitem__(self, key, value):
    +    """Prevent the setting of non-string or non-empty keys on this dict.
    +
    +    Args:
    +      key: The dictionary key. Must be a string.
    +      value: The value, must be a RealAccount instance.
    +    Raises:
    +      KeyError: If the key is not a string, or is invalid.
    +      ValueError: If the value is not a RealAccount instance.
    +    """
    +    if not isinstance(key, str) or not key:
    +        raise KeyError("Invalid RealAccount key: '{}'".format(key))
    +    if not isinstance(value, RealAccount):
    +        raise ValueError("Invalid RealAccount value: '{}'".format(value))
    +    if not value.account.endswith(key):
    +        raise ValueError(
    +            "RealAccount name '{}' inconsistent with key: '{}'".format(
    +                value.account, key
    +            )
    +        )
    +    return super().__setitem__(key, value)
    +
    @@ -17408,7 +20723,7 @@

    -beancount.core.realization.RealAccount.copy(self) +beancount.core.realization.RealAccount.copy(self)

    @@ -17438,18 +20753,18 @@

    Source code in beancount/core/realization.py -
    def copy(self):
    -    """Override dict.copy() to clone a RealAccount.
    -
    -    This is only necessary to correctly implement the copy method.
    -    Otherwise, calling .copy() on a RealAccount instance invokes the base
    -    class' method, which return just a dict.
    -
    -    Returns:
    -      A cloned instance of RealAccount, with all members shallow-copied.
    -    """
    -    return copy.copy(self)
    -
    +
    def copy(self):
    +    """Override dict.copy() to clone a RealAccount.
    +
    +    This is only necessary to correctly implement the copy method.
    +    Otherwise, calling .copy() on a RealAccount instance invokes the base
    +    class' method, which return just a dict.
    +
    +    Returns:
    +      A cloned instance of RealAccount, with all members shallow-copied.
    +    """
    +    return copy.copy(self)
    +
    @@ -17473,7 +20788,7 @@

    -beancount.core.realization.compute_balance(real_account, leaf_only=False) +beancount.core.realization.compute_balance(real_account, leaf_only=False)

    @@ -17517,18 +20832,19 @@

    Source code in beancount/core/realization.py -
    def compute_balance(real_account, leaf_only=False):
    -    """Compute the total balance of this account and all its subaccounts.
    -
    -    Args:
    -      real_account: A RealAccount instance.
    -      leaf_only: A boolean flag, true if we should yield only leaves.
    -    Returns:
    -      An Inventory.
    -    """
    -    return functools.reduce(operator.add, [
    -        ra.balance for ra in iter_children(real_account, leaf_only)])
    -
    +
    def compute_balance(real_account, leaf_only=False):
    +    """Compute the total balance of this account and all its subaccounts.
    +
    +    Args:
    +      real_account: A RealAccount instance.
    +      leaf_only: A boolean flag, true if we should yield only leaves.
    +    Returns:
    +      An Inventory.
    +    """
    +    return functools.reduce(
    +        operator.add, [ra.balance for ra in iter_children(real_account, leaf_only)]
    +    )
    +
    @@ -17541,7 +20857,7 @@

    -beancount.core.realization.compute_postings_balance(txn_postings) +beancount.core.realization.compute_postings_balance(txn_postings)

    @@ -17585,23 +20901,23 @@

    Source code in beancount/core/realization.py -
    def compute_postings_balance(txn_postings):
    -    """Compute the balance of a list of Postings's or TxnPosting's positions.
    -
    -    Args:
    -      postings: A list of Posting instances and other directives (which are
    -        skipped).
    -    Returns:
    -      An Inventory.
    -    """
    -    final_balance = inventory.Inventory()
    -    for txn_posting in txn_postings:
    -        if isinstance(txn_posting, Posting):
    -            final_balance.add_position(txn_posting)
    -        elif isinstance(txn_posting, TxnPosting):
    -            final_balance.add_position(txn_posting.posting)
    -    return final_balance
    -
    +
    def compute_postings_balance(txn_postings):
    +    """Compute the balance of a list of Postings's or TxnPosting's positions.
    +
    +    Args:
    +      postings: A list of Posting instances and other directives (which are
    +        skipped).
    +    Returns:
    +      An Inventory.
    +    """
    +    final_balance = inventory.Inventory()
    +    for txn_posting in txn_postings:
    +        if isinstance(txn_posting, Posting):
    +            final_balance.add_position(txn_posting)
    +        elif isinstance(txn_posting, TxnPosting):
    +            final_balance.add_position(txn_posting.posting)
    +    return final_balance
    +
    @@ -17614,7 +20930,7 @@

    -beancount.core.realization.contains(real_account, account_name) +beancount.core.realization.contains(real_account, account_name)

    @@ -17658,17 +20974,17 @@

    Source code in beancount/core/realization.py -
    def contains(real_account, account_name):
    -    """True if the given account node contains the subaccount name.
    -
    -    Args:
    -      account_name: A string, the name of a direct or indirect subaccount of
    -        this node.
    -    Returns:
    -      A boolean, true the name is a child of this node.
    -    """
    -    return get(real_account, account_name) is not None
    -
    +
    def contains(real_account, account_name):
    +    """True if the given account node contains the subaccount name.
    +
    +    Args:
    +      account_name: A string, the name of a direct or indirect subaccount of
    +        this node.
    +    Returns:
    +      A boolean, true the name is a child of this node.
    +    """
    +    return get(real_account, account_name) is not None
    +
    @@ -17681,7 +20997,7 @@

    -beancount.core.realization.dump(root_account) +beancount.core.realization.dump(root_account)

    @@ -17734,90 +21050,88 @@

    Source code in beancount/core/realization.py -
    def dump(root_account):
    -    """Convert a RealAccount node to a line of lines.
    -
    -    Note: this is not meant to be used to produce text reports; the reporting
    -    code should produce an intermediate object that contains the structure of
    -    it, which can then be rendered to ASCII, HTML or CSV formats. This is
    -    intended as a convenient little function for dumping trees of data for
    -    debugging purposes.
    -
    -    Args:
    -      root_account: A RealAccount instance.
    -    Returns:
    -      A list of tuples of (first_line, continuation_line, real_account) where
    -        first_line: A string, the first line to render, which includes the
    -          account name.
    -        continuation_line: A string, further line to render if necessary.
    -        real_account: The RealAccount instance which corresponds to this
    -          line.
    -    """
    -    # Compute all the lines ahead of time in order to calculate the width.
    -    lines = []
    -
    -    # Start with the root node. We push the constant prefix before this node,
    -    # the account name, and the RealAccount instance. We will maintain a stack
    -    # of children nodes to render.
    -    stack = [('', root_account.account, root_account, True)]
    -    while stack:
    -        prefix, name, real_account, is_last = stack.pop(-1)
    -
    -        if real_account is root_account:
    -            # For the root node, we don't want to render any prefix.
    -            first = cont = ''
    -        else:
    -            # Compute the string that precedes the name directly and the one below
    -            # that for the continuation lines.
    -            #  |
    -            #  @@@ Bank1    <----------------
    -            #  @@@ |
    -            #  |   |-- Checking
    -            if is_last:
    -                first = prefix + PREFIX_LEAF_1
    -                cont = prefix + PREFIX_LEAF_C
    -            else:
    -                first = prefix + PREFIX_CHILD_1
    -                cont = prefix + PREFIX_CHILD_C
    -
    -        # Compute the name to render for continuation lines.
    -        #  |
    -        #  |-- Bank1
    -        #  |   @@@       <----------------
    -        #  |   |-- Checking
    -        if len(real_account) > 0:
    -            cont_name = PREFIX_CHILD_C
    -        else:
    -            cont_name = PREFIX_LEAF_C
    -
    -        # Add a line for this account.
    -        if not (real_account is root_account and not name):
    -            lines.append((first + name,
    -                          cont + cont_name,
    -                          real_account))
    -
    -        # Push the children onto the stack, being careful with ordering and
    -        # marking the last node as such.
    -        child_items = sorted(real_account.items(), reverse=True)
    -        if child_items:
    -            child_iter = iter(child_items)
    -            child_name, child_account = next(child_iter)
    -            stack.append((cont, child_name, child_account, True))
    -            for child_name, child_account in child_iter:
    -                stack.append((cont, child_name, child_account, False))
    -
    -    if not lines:
    -        return lines
    -
    -    # Compute the maximum width of the lines and convert all of them to the same
    -    # maximal width. This makes it easy on the client.
    -    max_width = max(len(first_line) for first_line, _, __ in lines)
    -    line_format = '{{:{width}}}'.format(width=max_width)
    -    return [(line_format.format(first_line),
    -             line_format.format(cont_line),
    -             real_node)
    -            for (first_line, cont_line, real_node) in lines]
    -
    +
    def dump(root_account):
    +    """Convert a RealAccount node to a line of lines.
    +
    +    Note: this is not meant to be used to produce text reports; the reporting
    +    code should produce an intermediate object that contains the structure of
    +    it, which can then be rendered to ASCII, HTML or CSV formats. This is
    +    intended as a convenient little function for dumping trees of data for
    +    debugging purposes.
    +
    +    Args:
    +      root_account: A RealAccount instance.
    +    Returns:
    +      A list of tuples of (first_line, continuation_line, real_account) where
    +        first_line: A string, the first line to render, which includes the
    +          account name.
    +        continuation_line: A string, further line to render if necessary.
    +        real_account: The RealAccount instance which corresponds to this
    +          line.
    +    """
    +    # Compute all the lines ahead of time in order to calculate the width.
    +    lines = []
    +
    +    # Start with the root node. We push the constant prefix before this node,
    +    # the account name, and the RealAccount instance. We will maintain a stack
    +    # of children nodes to render.
    +    stack = [("", root_account.account, root_account, True)]
    +    while stack:
    +        prefix, name, real_account, is_last = stack.pop(-1)
    +
    +        if real_account is root_account:
    +            # For the root node, we don't want to render any prefix.
    +            first = cont = ""
    +        else:
    +            # Compute the string that precedes the name directly and the one below
    +            # that for the continuation lines.
    +            #  |
    +            #  @@@ Bank1    <----------------
    +            #  @@@ |
    +            #  |   |-- Checking
    +            if is_last:
    +                first = prefix + PREFIX_LEAF_1
    +                cont = prefix + PREFIX_LEAF_C
    +            else:
    +                first = prefix + PREFIX_CHILD_1
    +                cont = prefix + PREFIX_CHILD_C
    +
    +        # Compute the name to render for continuation lines.
    +        #  |
    +        #  |-- Bank1
    +        #  |   @@@       <----------------
    +        #  |   |-- Checking
    +        if len(real_account) > 0:
    +            cont_name = PREFIX_CHILD_C
    +        else:
    +            cont_name = PREFIX_LEAF_C
    +
    +        # Add a line for this account.
    +        if not (real_account is root_account and not name):
    +            lines.append((first + name, cont + cont_name, real_account))
    +
    +        # Push the children onto the stack, being careful with ordering and
    +        # marking the last node as such.
    +        child_items = sorted(real_account.items(), reverse=True)
    +        if child_items:
    +            child_iter = iter(child_items)
    +            child_name, child_account = next(child_iter)
    +            stack.append((cont, child_name, child_account, True))
    +            for child_name, child_account in child_iter:
    +                stack.append((cont, child_name, child_account, False))
    +
    +    if not lines:
    +        return lines
    +
    +    # Compute the maximum width of the lines and convert all of them to the same
    +    # maximal width. This makes it easy on the client.
    +    max_width = max(len(first_line) for first_line, _, __ in lines)
    +    line_format = "{{:{width}}}".format(width=max_width)
    +    return [
    +        (line_format.format(first_line), line_format.format(cont_line), real_node)
    +        for (first_line, cont_line, real_node) in lines
    +    ]
    +
    @@ -17830,7 +21144,7 @@

    -beancount.core.realization.dump_balances(real_root, dformat, at_cost=False, fullnames=False, file=None) +beancount.core.realization.dump_balances(real_root, dformat, at_cost=False, fullnames=False, file=None)

    @@ -17879,55 +21193,59 @@

    Source code in beancount/core/realization.py -
    def dump_balances(real_root, dformat, at_cost=False, fullnames=False, file=None):
    -    """Dump a realization tree with balances.
    -
    -    Args:
    -      real_root: An instance of RealAccount.
    -      dformat: An instance of DisplayFormatter to format the numbers with.
    -      at_cost: A boolean, if true, render the values at cost.
    -      fullnames: A boolean, if true, don't render a tree of accounts and
    -        render the full account names.
    -      file: A file object to dump the output to. If not specified, we
    -        return the output as a string.
    -    Returns:
    -      A string, the rendered tree, or nothing, if 'file' was provided.
    -    """
    -    if fullnames:
    -        # Compute the maximum account name length;
    -        maxlen = max(len(real_child.account)
    -                     for real_child in iter_children(real_root, leaf_only=True))
    -        line_format = '{{:{width}}} {{}}\n'.format(width=maxlen)
    -    else:
    -        line_format = '{}       {}\n'
    -
    -    output = file or io.StringIO()
    -    for first_line, cont_line, real_account in dump(real_root):
    -        if not real_account.balance.is_empty():
    -            if at_cost:
    -                rinv = real_account.balance.reduce(convert.get_cost)
    -            else:
    -                rinv = real_account.balance.reduce(convert.get_units)
    -            amounts = [position.units for position in rinv.get_positions()]
    -            positions = [amount_.to_string(dformat)
    -                         for amount_ in sorted(amounts, key=amount.sortkey)]
    -        else:
    -            positions = ['']
    -
    -        if fullnames:
    -            for position in positions:
    -                if not position and len(real_account) > 0:
    -                    continue  # Skip parent accounts with no position to render.
    -                output.write(line_format.format(real_account.account, position))
    -        else:
    -            line = first_line
    -            for position in positions:
    -                output.write(line_format.format(line, position))
    -                line = cont_line
    -
    -    if file is None:
    -        return output.getvalue()
    -
    +
    def dump_balances(real_root, dformat, at_cost=False, fullnames=False, file=None):
    +    """Dump a realization tree with balances.
    +
    +    Args:
    +      real_root: An instance of RealAccount.
    +      dformat: An instance of DisplayFormatter to format the numbers with.
    +      at_cost: A boolean, if true, render the values at cost.
    +      fullnames: A boolean, if true, don't render a tree of accounts and
    +        render the full account names.
    +      file: A file object to dump the output to. If not specified, we
    +        return the output as a string.
    +    Returns:
    +      A string, the rendered tree, or nothing, if 'file' was provided.
    +    """
    +    if fullnames:
    +        # Compute the maximum account name length;
    +        maxlen = max(
    +            len(real_child.account)
    +            for real_child in iter_children(real_root, leaf_only=True)
    +        )
    +        line_format = "{{:{width}}} {{}}\n".format(width=maxlen)
    +    else:
    +        line_format = "{}       {}\n"
    +
    +    output = file or io.StringIO()
    +    for first_line, cont_line, real_account in dump(real_root):
    +        if not real_account.balance.is_empty():
    +            if at_cost:
    +                rinv = real_account.balance.reduce(convert.get_cost)
    +            else:
    +                rinv = real_account.balance.reduce(convert.get_units)
    +            amounts = [position.units for position in rinv.get_positions()]
    +            positions = [
    +                amount_.to_string(dformat)
    +                for amount_ in sorted(amounts, key=amount.sortkey)
    +            ]
    +        else:
    +            positions = [""]
    +
    +        if fullnames:
    +            for position in positions:
    +                if not position and len(real_account) > 0:
    +                    continue  # Skip parent accounts with no position to render.
    +                output.write(line_format.format(real_account.account, position))
    +        else:
    +            line = first_line
    +            for position in positions:
    +                output.write(line_format.format(line, position))
    +                line = cont_line
    +
    +    if file is None:
    +        return output.getvalue()
    +
    @@ -17940,7 +21258,7 @@

    -beancount.core.realization.filter(real_account, predicate) +beancount.core.realization.filter(real_account, predicate)

    @@ -17965,7 +21283,7 @@

    • real_account – An instance of RealAccount.

    • -
    • predicate – A callable/function which accepts a real_account and returns +

    • predicate – A callable/function which accepts a RealAccount and returns a boolean. If the function returns True, the node is kept.

    @@ -17990,36 +21308,36 @@

    Source code in beancount/core/realization.py -
    def filter(real_account, predicate):
    -    """Filter a RealAccount tree of nodes by the predicate.
    -
    -    This function visits the tree and applies the predicate on each node. It
    -    returns a partial clone of RealAccount whereby on each node
    -    - either the predicate is true, or
    -    - for at least one child of the node the predicate is true.
    -    All the leaves have the predicate be true.
    -
    -    Args:
    -      real_account: An instance of RealAccount.
    -      predicate: A callable/function which accepts a real_account and returns
    -        a boolean. If the function returns True, the node is kept.
    -    Returns:
    -      A shallow clone of RealAccount is always returned.
    -    """
    -    assert isinstance(real_account, RealAccount)
    -
    -    real_copy = RealAccount(real_account.account)
    -    real_copy.balance = real_account.balance
    -    real_copy.txn_postings = real_account.txn_postings
    -
    -    for child_name, real_child in real_account.items():
    -        real_child_copy = filter(real_child, predicate)
    -        if real_child_copy is not None:
    -            real_copy[child_name] = real_child_copy
    -
    -    if len(real_copy) > 0 or predicate(real_account):
    -        return real_copy
    -
    +
    def filter(real_account, predicate):
    +    """Filter a RealAccount tree of nodes by the predicate.
    +
    +    This function visits the tree and applies the predicate on each node. It
    +    returns a partial clone of RealAccount whereby on each node
    +    - either the predicate is true, or
    +    - for at least one child of the node the predicate is true.
    +    All the leaves have the predicate be true.
    +
    +    Args:
    +      real_account: An instance of RealAccount.
    +      predicate: A callable/function which accepts a RealAccount and returns
    +        a boolean. If the function returns True, the node is kept.
    +    Returns:
    +      A shallow clone of RealAccount is always returned.
    +    """
    +    assert isinstance(real_account, RealAccount)
    +
    +    real_copy = RealAccount(real_account.account)
    +    real_copy.balance = real_account.balance
    +    real_copy.txn_postings = real_account.txn_postings
    +
    +    for child_name, real_child in real_account.items():
    +        real_child_copy = filter(real_child, predicate)
    +        if real_child_copy is not None:
    +            real_copy[child_name] = real_child_copy
    +
    +    if len(real_copy) > 0 or predicate(real_account):
    +        return real_copy
    +
    @@ -18032,7 +21350,7 @@

    -beancount.core.realization.find_last_active_posting(txn_postings) +beancount.core.realization.find_last_active_posting(txn_postings)

    @@ -18079,30 +21397,24 @@

    Source code in beancount/core/realization.py -
    def find_last_active_posting(txn_postings):
    -    """Look at the end of the list of postings, and find the last
    -    posting or entry that is not an automatically added directive.
    -    Note that if the account is closed, the last posting is assumed
    -    to be a Close directive (this is the case if the input is valid
    -    and checks without errors.
    -
    -    Args:
    -      txn_postings: a list of postings or entries.
    -    Returns:
    -      An entry, or None, if the input list was empty.
    -    """
    -    for txn_posting in reversed(txn_postings):
    -        assert not isinstance(txn_posting, Posting)
    -
    -        if not isinstance(txn_posting, (TxnPosting, Open, Close, Pad, Balance, Note)):
    -            continue
    -
    -        # pylint: disable=bad-continuation
    -        if (isinstance(txn_posting, TxnPosting) and
    -            txn_posting.txn.flag == flags.FLAG_UNREALIZED):
    -            continue
    -        return txn_posting
    -
    +
    def find_last_active_posting(txn_postings):
    +    """Look at the end of the list of postings, and find the last
    +    posting or entry that is not an automatically added directive.
    +    Note that if the account is closed, the last posting is assumed
    +    to be a Close directive (this is the case if the input is valid
    +    and checks without errors.
    +
    +    Args:
    +      txn_postings: a list of postings or entries.
    +    Returns:
    +      An entry, or None, if the input list was empty.
    +    """
    +    for txn_posting in reversed(txn_postings):
    +        assert not isinstance(txn_posting, Posting)
    +        if not isinstance(txn_posting, (TxnPosting, Open, Close, Pad, Balance, Note)):
    +            continue
    +        return txn_posting
    +
    @@ -18115,7 +21427,7 @@

    -beancount.core.realization.get(real_account, account_name, default=None) +beancount.core.realization.get(real_account, account_name, default=None)

    @@ -18164,30 +21476,30 @@

    Source code in beancount/core/realization.py -
    def get(real_account, account_name, default=None):
    -    """Fetch the subaccount name from the real_account node.
    -
    -    Args:
    -      real_account: An instance of RealAccount, the parent node to look for
    -        children of.
    -      account_name: A string, the name of a possibly indirect child leaf
    -        found down the tree of 'real_account' nodes.
    -      default: The default value that should be returned if the child
    -        subaccount is not found.
    -    Returns:
    -      A RealAccount instance for the child, or the default value, if the child
    -      is not found.
    -    """
    -    if not isinstance(account_name, str):
    -        raise ValueError
    -    components = account.split(account_name)
    -    for component in components:
    -        real_child = real_account.get(component, default)
    -        if real_child is default:
    -            return default
    -        real_account = real_child
    -    return real_account
    -
    +
    def get(real_account, account_name, default=None):
    +    """Fetch the subaccount name from the real_account node.
    +
    +    Args:
    +      real_account: An instance of RealAccount, the parent node to look for
    +        children of.
    +      account_name: A string, the name of a possibly indirect child leaf
    +        found down the tree of 'real_account' nodes.
    +      default: The default value that should be returned if the child
    +        subaccount is not found.
    +    Returns:
    +      A RealAccount instance for the child, or the default value, if the child
    +      is not found.
    +    """
    +    if not isinstance(account_name, str):
    +        raise ValueError
    +    components = account.split(account_name)
    +    for component in components:
    +        real_child = real_account.get(component, default)
    +        if real_child is default:
    +            return default
    +        real_account = real_child
    +    return real_account
    +
    @@ -18200,7 +21512,7 @@

    -beancount.core.realization.get_or_create(real_account, account_name) +beancount.core.realization.get_or_create(real_account, account_name)

    @@ -18247,31 +21559,31 @@

    Source code in beancount/core/realization.py -
    def get_or_create(real_account, account_name):
    -    """Fetch the subaccount name from the real_account node.
    -
    -    Args:
    -      real_account: An instance of RealAccount, the parent node to look for
    -        children of, or create under.
    -      account_name: A string, the name of the direct or indirect child leaf
    -        to get or create.
    -    Returns:
    -      A RealAccount instance for the child, or the default value, if the child
    -      is not found.
    -    """
    -    if not isinstance(account_name, str):
    -        raise ValueError
    -    components = account.split(account_name)
    -    path = []
    -    for component in components:
    -        path.append(component)
    -        real_child = real_account.get(component, None)
    -        if real_child is None:
    -            real_child = RealAccount(account.join(*path))
    -            real_account[component] = real_child
    -        real_account = real_child
    -    return real_account
    -
    +
    def get_or_create(real_account, account_name):
    +    """Fetch the subaccount name from the real_account node.
    +
    +    Args:
    +      real_account: An instance of RealAccount, the parent node to look for
    +        children of, or create under.
    +      account_name: A string, the name of the direct or indirect child leaf
    +        to get or create.
    +    Returns:
    +      A RealAccount instance for the child, or the default value, if the child
    +      is not found.
    +    """
    +    if not isinstance(account_name, str):
    +        raise ValueError
    +    components = account.split(account_name)
    +    path = []
    +    for component in components:
    +        path.append(component)
    +        real_child = real_account.get(component, None)
    +        if real_child is None:
    +            real_child = RealAccount(account.join(*path))
    +            real_account[component] = real_child
    +        real_account = real_child
    +    return real_account
    +
    @@ -18284,7 +21596,7 @@

    -beancount.core.realization.get_postings(real_account) +beancount.core.realization.get_postings(real_account)

    @@ -18327,22 +21639,22 @@

    Source code in beancount/core/realization.py -
    def get_postings(real_account):
    -    """Return a sorted list a RealAccount's postings and children.
    -
    -    Args:
    -      real_account: An instance of RealAccount.
    -    Returns:
    -      A list of Posting or directories.
    -    """
    -    # We accumulate all the postings at once here instead of incrementally
    -    # because we need to return them sorted.
    -    accumulator = []
    -    for real_child in iter_children(real_account):
    -        accumulator.extend(real_child.txn_postings)
    -    accumulator.sort(key=data.posting_sortkey)
    -    return accumulator
    -
    +
    def get_postings(real_account):
    +    """Return a sorted list a RealAccount's postings and children.
    +
    +    Args:
    +      real_account: An instance of RealAccount.
    +    Returns:
    +      A list of Posting or directories.
    +    """
    +    # We accumulate all the postings at once here instead of incrementally
    +    # because we need to return them sorted.
    +    accumulator = []
    +    for real_child in iter_children(real_account):
    +        accumulator.extend(real_child.txn_postings)
    +    accumulator.sort(key=data.posting_sortkey)
    +    return accumulator
    +
    @@ -18355,7 +21667,7 @@

    -beancount.core.realization.index_key(sequence, value, key, cmp) +beancount.core.realization.index_key(sequence, value, key, cmp)

    @@ -18403,24 +21715,24 @@

    Source code in beancount/core/realization.py -
    def index_key(sequence, value, key, cmp):
    -    """Find the index of the first element in 'sequence' which is equal to 'value'.
    -    If 'key' is specified, the value compared to the value returned by this
    -    function. If the value is not found, return None.
    -
    -    Args:
    -      sequence: The sequence to search.
    -      value: The value to search for.
    -      key: A predicate to call to obtain the value to compare against.
    -      cmp: A comparison predicate.
    -    Returns:
    -      The index of the first element found, or None, if the element was not found.
    -    """
    -    for index, element in enumerate(sequence):
    -        if cmp(key(element), value):
    -            return index
    -    return
    -
    +
    def index_key(sequence, value, key, cmp):
    +    """Find the index of the first element in 'sequence' which is equal to 'value'.
    +    If 'key' is specified, the value compared to the value returned by this
    +    function. If the value is not found, return None.
    +
    +    Args:
    +      sequence: The sequence to search.
    +      value: The value to search for.
    +      key: A predicate to call to obtain the value to compare against.
    +      cmp: A comparison predicate.
    +    Returns:
    +      The index of the first element found, or None, if the element was not found.
    +    """
    +    for index, element in enumerate(sequence):
    +        if cmp(key(element), value):
    +            return index
    +    return
    +
    @@ -18433,7 +21745,7 @@

    -beancount.core.realization.iter_children(real_account, leaf_only=False) +beancount.core.realization.iter_children(real_account, leaf_only=False)

    @@ -18464,29 +21776,29 @@

    Source code in beancount/core/realization.py -
    def iter_children(real_account, leaf_only=False):
    -    """Iterate this account node and all its children, depth-first.
    -
    -    Args:
    -      real_account: An instance of RealAccount.
    -      leaf_only: A boolean flag, true if we should yield only leaves.
    -    Yields:
    -      Instances of RealAccount, beginning with this account. The order is
    -      undetermined.
    -    """
    -    if leaf_only:
    -        if len(real_account) == 0:
    -            yield real_account
    -        else:
    -            for key, real_child in sorted(real_account.items()):
    -                for real_subchild in iter_children(real_child, leaf_only):
    -                    yield real_subchild
    -    else:
    -        yield real_account
    -        for key, real_child in sorted(real_account.items()):
    -            for real_subchild in iter_children(real_child):
    -                yield real_subchild
    -
    +
    def iter_children(real_account, leaf_only=False):
    +    """Iterate this account node and all its children, depth-first.
    +
    +    Args:
    +      real_account: An instance of RealAccount.
    +      leaf_only: A boolean flag, true if we should yield only leaves.
    +    Yields:
    +      Instances of RealAccount, beginning with this account. The order is
    +      undetermined.
    +    """
    +    if leaf_only:
    +        if len(real_account) == 0:
    +            yield real_account
    +        else:
    +            for key, real_child in sorted(real_account.items()):
    +                for real_subchild in iter_children(real_child, leaf_only):
    +                    yield real_subchild
    +    else:
    +        yield real_account
    +        for key, real_child in sorted(real_account.items()):
    +            for real_subchild in iter_children(real_child):
    +                yield real_subchild
    +
    @@ -18499,7 +21811,7 @@

    -beancount.core.realization.iterate_with_balance(txn_postings) +beancount.core.realization.iterate_with_balance(txn_postings)

    @@ -18556,111 +21868,111 @@

    Source code in beancount/core/realization.py -
    def iterate_with_balance(txn_postings):
    -    """Iterate over the entries, accumulating the running balance.
    -
    -    For each entry, this yields tuples of the form:
    -
    -      (entry, postings, change, balance)
    -
    -    entry: This is the directive for this line. If the list contained Posting
    -      instance, this yields the corresponding Transaction object.
    -    postings: A list of postings on this entry that affect the balance. Only the
    -      postings encountered in the input list are included; only those affect the
    -      balance. If 'entry' is not a Transaction directive, this should always be
    -      an empty list. We preserve the original ordering of the postings as they
    -      appear in the input list.
    -    change: An Inventory object that reflects the total change due to the
    -      postings from this entry that appear in the list. For example, if a
    -      Transaction has three postings and two are in the input list, the sum of
    -      the two postings will be in the 'change' Inventory object. However, the
    -      position for the transactions' third posting--the one not included in the
    -      input list--will not be in this inventory.
    -    balance: An Inventory object that reflects the balance *after* adding the
    -      'change' inventory due to this entry's transaction. The 'balance' yielded
    -      is never None, even for entries that do not affect the balance, that is,
    -      with an empty 'change' inventory. It's up to the caller, the one rendering
    -      the entry, to decide whether to render this entry's change for a
    -      particular entry type.
    -
    -    Note that if the input list of postings-or-entries is not in sorted date
    -    order, two postings for the same entry appearing twice with a different date
    -    in between will cause the entry appear twice. This is correct behavior, and
    -    it is expected that in practice this should never happen anyway, because the
    -    list of postings or entries should always be sorted. This method attempts to
    -    detect this and raises an assertion if this is seen.
    -
    -    Args:
    -      txn_postings: A list of postings or directive instances.
    -        Postings affect the balance; other entries do not.
    -    Yields:
    -      Tuples of (entry, postings, change, balance) as described above.
    -    """
    -
    -    # The running balance.
    -    running_balance = inventory.Inventory()
    -
    -    # Previous date.
    -    prev_date = None
    -
    -    # A list of entries at the current date.
    -    date_entries = []
    -
    -    first = lambda pair: pair[0]
    -    for txn_posting in txn_postings:
    -
    -        # Get the posting if we are dealing with one.
    -        assert not isinstance(txn_posting, Posting)
    -        if isinstance(txn_posting, TxnPosting):
    -            posting = txn_posting.posting
    -            entry = txn_posting.txn
    -        else:
    -            posting = None
    -            entry = txn_posting
    -
    -        if entry.date != prev_date:
    -            assert prev_date is None or entry.date > prev_date, (
    -                "Invalid date order for postings: {} > {}".format(prev_date, entry.date))
    -            prev_date = entry.date
    -
    -            # Flush the dated entries.
    -            for date_entry, date_postings in date_entries:
    -                change = inventory.Inventory()
    -                if date_postings:
    -                    # Compute the change due to this transaction and update the
    -                    # total balance at the same time.
    -                    for date_posting in date_postings:
    -                        change.add_position(date_posting)
    -                        running_balance.add_position(date_posting)
    -                yield date_entry, date_postings, change, running_balance
    -
    -            date_entries.clear()
    -            assert not date_entries
    -
    -        if posting is not None:
    -            # De-dup multiple postings on the same transaction entry by
    -            # grouping their positions together.
    -            index = index_key(date_entries, entry, first, operator.is_)
    -            if index is None:
    -                date_entries.append((entry, [posting]))
    -            else:
    -                # We are indeed de-duping!
    -                postings = date_entries[index][1]
    -                postings.append(posting)
    -        else:
    -            # This is a regular entry; nothing to add/remove.
    -            date_entries.append((entry, []))
    -
    -    # Flush the final dated entries if any, same as above.
    -    for date_entry, date_postings in date_entries:
    -        change = inventory.Inventory()
    -        if date_postings:
    -            for date_posting in date_postings:
    -                change.add_position(date_posting)
    -                running_balance.add_position(date_posting)
    -        yield date_entry, date_postings, change, running_balance
    -    date_entries.clear()
    -
    +
    def iterate_with_balance(txn_postings):
    +    """Iterate over the entries, accumulating the running balance.
    +
    +    For each entry, this yields tuples of the form:
    +
    +      (entry, postings, change, balance)
    +
    +    entry: This is the directive for this line. If the list contained Posting
    +      instance, this yields the corresponding Transaction object.
    +    postings: A list of postings on this entry that affect the balance. Only the
    +      postings encountered in the input list are included; only those affect the
    +      balance. If 'entry' is not a Transaction directive, this should always be
    +      an empty list. We preserve the original ordering of the postings as they
    +      appear in the input list.
    +    change: An Inventory object that reflects the total change due to the
    +      postings from this entry that appear in the list. For example, if a
    +      Transaction has three postings and two are in the input list, the sum of
    +      the two postings will be in the 'change' Inventory object. However, the
    +      position for the transactions' third posting--the one not included in the
    +      input list--will not be in this inventory.
    +    balance: An Inventory object that reflects the balance *after* adding the
    +      'change' inventory due to this entry's transaction. The 'balance' yielded
    +      is never None, even for entries that do not affect the balance, that is,
    +      with an empty 'change' inventory. It's up to the caller, the one rendering
    +      the entry, to decide whether to render this entry's change for a
    +      particular entry type.
    +
    +    Note that if the input list of postings-or-entries is not in sorted date
    +    order, two postings for the same entry appearing twice with a different date
    +    in between will cause the entry appear twice. This is correct behavior, and
    +    it is expected that in practice this should never happen anyway, because the
    +    list of postings or entries should always be sorted. This method attempts to
    +    detect this and raises an assertion if this is seen.
    +
    +    Args:
    +      txn_postings: A list of postings or directive instances.
    +        Postings affect the balance; other entries do not.
    +    Yields:
    +      Tuples of (entry, postings, change, balance) as described above.
    +    """
    +
    +    # The running balance.
    +    running_balance = inventory.Inventory()
    +
    +    # Previous date.
    +    prev_date = None
    +
    +    # A list of entries at the current date.
    +    date_entries = []
    +
    +    first = lambda pair: pair[0]
    +    for txn_posting in txn_postings:
    +        # Get the posting if we are dealing with one.
    +        assert not isinstance(txn_posting, Posting)
    +        if isinstance(txn_posting, TxnPosting):
    +            posting = txn_posting.posting
    +            entry = txn_posting.txn
    +        else:
    +            posting = None
    +            entry = txn_posting
    +
    +        if entry.date != prev_date:
    +            assert (
    +                prev_date is None or entry.date > prev_date
    +            ), "Invalid date order for postings: {} > {}".format(prev_date, entry.date)
    +            prev_date = entry.date
    +
    +            # Flush the dated entries.
    +            for date_entry, date_postings in date_entries:
    +                change = inventory.Inventory()
    +                if date_postings:
    +                    # Compute the change due to this transaction and update the
    +                    # total balance at the same time.
    +                    for date_posting in date_postings:
    +                        change.add_position(date_posting)
    +                        running_balance.add_position(date_posting)
    +                yield date_entry, date_postings, change, running_balance
    +
    +            date_entries.clear()
    +            assert not date_entries
    +
    +        if posting is not None:
    +            # De-dup multiple postings on the same transaction entry by
    +            # grouping their positions together.
    +            index = index_key(date_entries, entry, first, operator.is_)
    +            if index is None:
    +                date_entries.append((entry, [posting]))
    +            else:
    +                # We are indeed de-duping!
    +                postings = date_entries[index][1]
    +                postings.append(posting)
    +        else:
    +            # This is a regular entry; nothing to add/remove.
    +            date_entries.append((entry, []))
    +
    +    # Flush the final dated entries if any, same as above.
    +    for date_entry, date_postings in date_entries:
    +        change = inventory.Inventory()
    +        if date_postings:
    +            for date_posting in date_postings:
    +                change.add_position(date_posting)
    +                running_balance.add_position(date_posting)
    +        yield date_entry, date_postings, change, running_balance
    +    date_entries.clear()
    +
    @@ -18673,7 +21985,7 @@

    -beancount.core.realization.postings_by_account(entries) +beancount.core.realization.postings_by_account(entries)

    @@ -18722,47 +22034,45 @@

    Source code in beancount/core/realization.py -
    def postings_by_account(entries):
    -    """Create lists of postings and balances by account.
    -
    -    This routine aggregates postings and entries grouping them by account name.
    -    The resulting lists contain postings in-lieu of Transaction directives, but
    -    the other directives are stored as entries. This yields a list of postings
    -    or other entries by account. All references to accounts are taken into
    -    account.
    -
    -    Args:
    -      entries: A list of directives.
    -    Returns:
    -       A mapping of account name to list of TxnPosting instances or
    -       non-Transaction directives, sorted in the same order as the entries.
    -    """
    -    txn_postings_map = collections.defaultdict(list)
    -    for entry in entries:
    -
    -        if isinstance(entry, Transaction):
    -            # Insert an entry for each of the postings.
    -            for posting in entry.postings:
    -                txn_postings_map[posting.account].append(
    -                    TxnPosting(entry, posting))
    -
    -        elif isinstance(entry, (Open, Close, Balance, Note, Document)):
    -            # Append some other entries in the realized list.
    -            txn_postings_map[entry.account].append(entry)
    -
    -        elif isinstance(entry, Pad):
    -            # Insert the pad entry in both realized accounts.
    -            txn_postings_map[entry.account].append(entry)
    -            txn_postings_map[entry.source_account].append(entry)
    -
    -        elif isinstance(entry, Custom):
    -            # Insert custom entry for each account in its values.
    -            for custom_value in entry.values:
    -                if custom_value.dtype == account.TYPE:
    -                    txn_postings_map[custom_value.value].append(entry)
    -
    -    return txn_postings_map
    -
    +
    def postings_by_account(entries):
    +    """Create lists of postings and balances by account.
    +
    +    This routine aggregates postings and entries grouping them by account name.
    +    The resulting lists contain postings in-lieu of Transaction directives, but
    +    the other directives are stored as entries. This yields a list of postings
    +    or other entries by account. All references to accounts are taken into
    +    account.
    +
    +    Args:
    +      entries: A list of directives.
    +    Returns:
    +       A mapping of account name to list of TxnPosting instances or
    +       non-Transaction directives, sorted in the same order as the entries.
    +    """
    +    txn_postings_map = collections.defaultdict(list)
    +    for entry in entries:
    +        if isinstance(entry, Transaction):
    +            # Insert an entry for each of the postings.
    +            for posting in entry.postings:
    +                txn_postings_map[posting.account].append(TxnPosting(entry, posting))
    +
    +        elif isinstance(entry, (Open, Close, Balance, Note, Document)):
    +            # Append some other entries in the realized list.
    +            txn_postings_map[entry.account].append(entry)
    +
    +        elif isinstance(entry, Pad):
    +            # Insert the pad entry in both realized accounts.
    +            txn_postings_map[entry.account].append(entry)
    +            txn_postings_map[entry.source_account].append(entry)
    +
    +        elif isinstance(entry, Custom):
    +            # Insert custom entry for each account in its values.
    +            for custom_value in entry.values:
    +                if custom_value.dtype == account.TYPE:
    +                    txn_postings_map[custom_value.value].append(entry)
    +
    +    return txn_postings_map
    +
    @@ -18775,7 +22085,7 @@

    -beancount.core.realization.realize(entries, min_accounts=None, compute_balance=True) +beancount.core.realization.realize(entries, min_accounts=None, compute_balance=True)

    @@ -18855,72 +22165,72 @@

    Source code in beancount/core/realization.py -
    def realize(entries, min_accounts=None, compute_balance=True):
    -    r"""Group entries by account, into a "tree" of realized accounts. RealAccount's
    -    are essentially containers for lists of postings and the final balance of
    -    each account, and may be non-leaf accounts (used strictly for organizing
    -    accounts into a hierarchy). This is then used to issue reports.
    -
    -    The lists of postings in each account my be any of the entry types, except
    -    for Transaction, whereby Transaction entries are replaced by the specific
    -    Posting legs that belong to the account. Here's a simple diagram that
    -    summarizes this seemingly complex, but rather simple data structure:
    -
    -       +-------------+ postings  +------+
    -       | RealAccount |---------->| Open |
    -       +-------------+           +------+
    -                                     |
    -                                     v
    -                              +------------+     +-------------+
    -                              | TxnPosting |---->| Transaction |
    -                              +------------+     +-------------+
    -                                     |      \                 \\\
    -                                     v       `\.__          +---------+
    -                                  +-----+         `-------->| Posting |
    -                                  | Pad |                   +---------+
    -                                  +-----+
    -                                     |
    -                                     v
    -                                +---------+
    -                                | Balance |
    -                                +---------+
    -                                     |
    -                                     v
    -                                 +-------+
    -                                 | Close |
    -                                 +-------+
    -                                     |
    -                                     .
    -
    -    Args:
    -      entries: A list of directives.
    -      min_accounts: A list of strings, account names to ensure we create,
    -        regardless of whether there are postings on those accounts or not.
    -        This can be used to ensure the root accounts all exist.
    -      compute_balance: A boolean, true if we should compute the final
    -        balance on the realization.
    -    Returns:
    -      The root RealAccount instance.
    -    """
    -    # Create lists of the entries by account.
    -    txn_postings_map = postings_by_account(entries)
    -
    -    # Create a RealAccount tree and compute the balance for each.
    -    real_root = RealAccount('')
    -    for account_name, txn_postings in txn_postings_map.items():
    -        real_account = get_or_create(real_root, account_name)
    -        real_account.txn_postings = txn_postings
    -        if compute_balance:
    -            real_account.balance = compute_postings_balance(txn_postings)
    -
    -    # Ensure a minimum set of accounts that should exist. This is typically
    -    # called with an instance of AccountTypes to make sure that those exist.
    -    if min_accounts:
    -        for account_name in min_accounts:
    -            get_or_create(real_root, account_name)
    -
    -    return real_root
    -
    +
    def realize(entries, min_accounts=None, compute_balance=True):
    +    r"""Group entries by account, into a "tree" of realized accounts. RealAccount's
    +    are essentially containers for lists of postings and the final balance of
    +    each account, and may be non-leaf accounts (used strictly for organizing
    +    accounts into a hierarchy). This is then used to issue reports.
    +
    +    The lists of postings in each account my be any of the entry types, except
    +    for Transaction, whereby Transaction entries are replaced by the specific
    +    Posting legs that belong to the account. Here's a simple diagram that
    +    summarizes this seemingly complex, but rather simple data structure:
    +
    +       +-------------+ postings  +------+
    +       | RealAccount |---------->| Open |
    +       +-------------+           +------+
    +                                     |
    +                                     v
    +                              +------------+     +-------------+
    +                              | TxnPosting |---->| Transaction |
    +                              +------------+     +-------------+
    +                                     |      \                 \\\
    +                                     v       `\.__          +---------+
    +                                  +-----+         `-------->| Posting |
    +                                  | Pad |                   +---------+
    +                                  +-----+
    +                                     |
    +                                     v
    +                                +---------+
    +                                | Balance |
    +                                +---------+
    +                                     |
    +                                     v
    +                                 +-------+
    +                                 | Close |
    +                                 +-------+
    +                                     |
    +                                     .
    +
    +    Args:
    +      entries: A list of directives.
    +      min_accounts: A list of strings, account names to ensure we create,
    +        regardless of whether there are postings on those accounts or not.
    +        This can be used to ensure the root accounts all exist.
    +      compute_balance: A boolean, true if we should compute the final
    +        balance on the realization.
    +    Returns:
    +      The root RealAccount instance.
    +    """
    +    # Create lists of the entries by account.
    +    txn_postings_map = postings_by_account(entries)
    +
    +    # Create a RealAccount tree and compute the balance for each.
    +    real_root = RealAccount("")
    +    for account_name, txn_postings in txn_postings_map.items():
    +        real_account = get_or_create(real_root, account_name)
    +        real_account.txn_postings = txn_postings
    +        if compute_balance:
    +            real_account.balance = compute_postings_balance(txn_postings)
    +
    +    # Ensure a minimum set of accounts that should exist. This is typically
    +    # called with an instance of AccountTypes to make sure that those exist.
    +    if min_accounts:
    +        for account_name in min_accounts:
    +            get_or_create(real_root, account_name)
    +
    +    return real_root
    +
    @@ -18952,7 +22262,7 @@

    diff --git a/api_reference/beancount.ingest.html b/api_reference/beancount.ingest.html deleted file mode 100644 index 268033d9..00000000 --- a/api_reference/beancount.ingest.html +++ /dev/null @@ -1,7567 +0,0 @@ - - - - - - - - - - - - beancount.ingest - Beancount Documentation - - - - - - - - - - - - - - - -
    - - - - -
    - - - - - -
    -
    -
    -
      -
    • Docs »
    • - - - -
    • API reference »
    • - - - -
    • beancount.ingest
    • -
    • - -
    • -
    - -
    -
    - -
    -
    - -

    beancount.ingest

    - - -
    - - -
    - -

    Code to help identify, extract, and file external downloads.

    -

    This package contains code to help you build importers and drive the process of -identifying which importer to run on an externally downloaded file, extract -transactions from them and file away these files under a clean and rigidly named -hierarchy for preservation.

    - - - -
    - - - - - - - - - - - - -
    - - - -

    - beancount.ingest.cache - - - -

    - -
    - -

    A file wrapper which acts as a cache for on-demand evaluation of conversions.

    -

    This object is used in lieu of a file in order to allow the various importers to -reuse each others' conversion results. Converting file contents, e.g. PDF to -text, can be expensive.

    - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.ingest.cache.contents(filename) - - -

    - -
    - -

    A converter that just reads the entire contents of a file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • num_bytes – The number of bytes to read.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A converter function.

    • -
    -
    -
    - Source code in beancount/ingest/cache.py -
    def contents(filename):
    -    """A converter that just reads the entire contents of a file.
    -
    -    Args:
    -      num_bytes: The number of bytes to read.
    -    Returns:
    -      A converter function.
    -    """
    -    # Attempt to detect the input encoding automatically, using chardet and a
    -    # decent amount of input.
    -    rawdata = open(filename, 'rb').read(HEAD_DETECT_MAX_BYTES)
    -    detected = chardet.detect(rawdata)
    -    encoding = detected['encoding']
    -
    -    # Ignore encoding errors for reading the contents because input files
    -    # routinely break this assumption.
    -    errors = 'ignore'
    -
    -    with open(filename, encoding=encoding, errors=errors) as file:
    -        return file.read()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.cache.get_file(filename) - - -

    - -
    - -

    Create or reuse a globally registered instance of a FileMemo.

    -

    Note: the FileMemo objects' lifetimes are reused for the duration of the -process. This is usually the intended behavior. Always create them by -calling this constructor.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A path string, the absolute name of the file whose memo to create.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A FileMemo instance.

    • -
    -
    -
    - Source code in beancount/ingest/cache.py -
    def get_file(filename):
    -    """Create or reuse a globally registered instance of a FileMemo.
    -
    -    Note: the FileMemo objects' lifetimes are reused for the duration of the
    -    process. This is usually the intended behavior. Always create them by
    -    calling this constructor.
    -
    -    Args:
    -      filename: A path string, the absolute name of the file whose memo to create.
    -    Returns:
    -      A FileMemo instance.
    -
    -    """
    -    assert path.isabs(filename), (
    -        "Path should be absolute in order to guarantee a single call.")
    -    return _CACHE[filename]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.cache.head(num_bytes=8192) - - -

    - -
    - -

    A converter that just reads the first bytes of a file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • num_bytes – The number of bytes to read.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A converter function.

    • -
    -
    -
    - Source code in beancount/ingest/cache.py -
    def head(num_bytes=8192):
    -    """A converter that just reads the first bytes of a file.
    -
    -    Args:
    -      num_bytes: The number of bytes to read.
    -    Returns:
    -      A converter function.
    -    """
    -    def head_reader(filename):
    -        with open(filename, 'rb') as file:
    -            rawdata = file.read(num_bytes)
    -            detected = chardet.detect(rawdata)
    -            encoding = detected['encoding']
    -            return rawdata.decode(encoding)
    -    return head_reader
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.cache.mimetype(filename) - - -

    - -
    - -

    A converter that computes the MIME type of the file.

    - - - - - - - - - - - - -
    Returns: -
      -
    • A converter function.

    • -
    -
    -
    - Source code in beancount/ingest/cache.py -
    def mimetype(filename):
    -    """A converter that computes the MIME type of the file.
    -
    -    Returns:
    -      A converter function.
    -    """
    -    return file_type.guess_file_type(filename)
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.extract - - - -

    - -
    - -

    Extract script.

    -

    Read an import script and a list of downloaded filenames or directories of -downloaded files, and for each of those files, extract transactions from it.

    - - - -
    - - - - - - - - - - - - - -
    - - - -

    -beancount.ingest.extract.add_arguments(parser) - - -

    - -
    - -

    Add arguments for the extract command.

    - -
    - Source code in beancount/ingest/extract.py -
    def add_arguments(parser):
    -    """Add arguments for the extract command."""
    -
    -    parser.add_argument('-e', '-f', '--existing', '--previous', metavar='BEANCOUNT_FILE',
    -                        default=None,
    -                        help=('Beancount file or existing entries for de-duplication '
    -                              '(optional)'))
    -
    -    parser.add_argument('-r', '--reverse', '--descending',
    -                        action='store_const', dest='ascending',
    -                        default=True, const=False,
    -                        help='Write out the entries in descending order')
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.extract.extract(importer_config, files_or_directories, output, entries=None, options_map=None, mindate=None, ascending=True, hooks=None) - - -

    - -
    - -

    Given an importer configuration, search for files that can be imported in the -list of files or directories, run the signature checks on them, and if it -succeeds, run the importer on the file.

    -

    A list of entries for an existing ledger can be provided in order to perform -de-duplication and a minimum date can be provided to filter out old entries.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • importer_config – A list of (regexps, importer) pairs, the configuration.

    • -
    • files_or_directories – A list of strings, filenames or directories to be processed.

    • -
    • output – A file object, to be written to.

    • -
    • entries – A list of directives loaded from the existing file for the newly -extracted entries to be merged in.

    • -
    • options_map – The options parsed from existing file.

    • -
    • mindate – Optional minimum date to output transactions for.

    • -
    • ascending – A boolean, true to print entries in ascending order, false if -descending is desired.

    • -
    • hooks – An optional list of hook functions to apply to the list of extract -(filename, entries) pairs, in order. If not specified, find_duplicate_entries() -is used, automatically.

    • -
    -
    -
    - Source code in beancount/ingest/extract.py -
    def extract(importer_config,
    -            files_or_directories,
    -            output,
    -            entries=None,
    -            options_map=None,
    -            mindate=None,
    -            ascending=True,
    -            hooks=None):
    -    """Given an importer configuration, search for files that can be imported in the
    -    list of files or directories, run the signature checks on them, and if it
    -    succeeds, run the importer on the file.
    -
    -    A list of entries for an existing ledger can be provided in order to perform
    -    de-duplication and a minimum date can be provided to filter out old entries.
    -
    -    Args:
    -      importer_config: A list of (regexps, importer) pairs, the configuration.
    -      files_or_directories: A list of strings, filenames or directories to be processed.
    -      output: A file object, to be written to.
    -      entries: A list of directives loaded from the existing file for the newly
    -        extracted entries to be merged in.
    -      options_map: The options parsed from existing file.
    -      mindate: Optional minimum date to output transactions for.
    -      ascending: A boolean, true to print entries in ascending order, false if
    -        descending is desired.
    -      hooks: An optional list of hook functions to apply to the list of extract
    -        (filename, entries) pairs, in order. If not specified, find_duplicate_entries()
    -        is used, automatically.
    -    """
    -    allow_none_for_tags_and_links = (
    -        options_map and options_map["allow_deprecated_none_for_tags_and_links"])
    -
    -    # Run all the importers and gather their result sets.
    -    new_entries_list = []
    -    for filename, importers in identify.find_imports(importer_config,
    -                                                     files_or_directories):
    -        for importer in importers:
    -            # Import and process the file.
    -            try:
    -                new_entries = extract_from_file(
    -                    filename,
    -                    importer,
    -                    existing_entries=entries,
    -                    min_date=mindate,
    -                    allow_none_for_tags_and_links=allow_none_for_tags_and_links)
    -                new_entries_list.append((filename, new_entries))
    -            except Exception as exc:
    -                logging.exception("Importer %s.extract() raised an unexpected error: %s",
    -                                  importer.name(), exc)
    -                continue
    -
    -    # Find potential duplicate entries in the result sets, either against the
    -    # list of existing ones, or against each other. A single call to this
    -    # function is made on purpose, so that the function be able to merge
    -    # entries.
    -    if hooks is None:
    -        hooks = [find_duplicate_entries]
    -    for hook_fn in hooks:
    -        new_entries_list = hook_fn(new_entries_list, entries)
    -    assert isinstance(new_entries_list, list)
    -    assert all(isinstance(new_entries, tuple) for new_entries in new_entries_list)
    -    assert all(isinstance(new_entries[0], str) for new_entries in new_entries_list)
    -    assert all(isinstance(new_entries[1], list) for new_entries in new_entries_list)
    -
    -    # Print out the results.
    -    output.write(HEADER)
    -    for key, new_entries in new_entries_list:
    -        output.write(identify.SECTION.format(key))
    -        output.write('\n')
    -        if not ascending:
    -            new_entries.reverse()
    -        print_extracted_entries(new_entries, output)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.extract.extract_from_file(filename, importer, existing_entries=None, min_date=None, allow_none_for_tags_and_links=False) - - -

    - -
    - -

    Import entries from file 'filename' with the given matches,

    -

    Also cross-check against a list of provided 'existing_entries' entries, -de-duplicating and possibly auto-categorizing.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – The name of the file to import.

    • -
    • importer – An importer object that matched the file.

    • -
    • existing_entries – A list of existing entries parsed from a ledger, used to -detect duplicates and automatically complete or categorize transactions.

    • -
    • min_date – A date before which entries should be ignored. This is useful -when an account has a valid check/assert; we could just ignore whatever -comes before, if desired.

    • -
    • allow_none_for_tags_and_links – A boolean, whether to allow plugins to -generate Transaction objects with None as value for the 'tags' or 'links' -attributes.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of new imported entries.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • Exception – If there is an error in the importer's extract() method.

    • -
    -
    -
    - Source code in beancount/ingest/extract.py -
    def extract_from_file(filename, importer,
    -                      existing_entries=None,
    -                      min_date=None,
    -                      allow_none_for_tags_and_links=False):
    -    """Import entries from file 'filename' with the given matches,
    -
    -    Also cross-check against a list of provided 'existing_entries' entries,
    -    de-duplicating and possibly auto-categorizing.
    -
    -    Args:
    -      filename: The name of the file to import.
    -      importer: An importer object that matched the file.
    -      existing_entries: A list of existing entries parsed from a ledger, used to
    -        detect duplicates and automatically complete or categorize transactions.
    -      min_date: A date before which entries should be ignored. This is useful
    -        when an account has a valid check/assert; we could just ignore whatever
    -        comes before, if desired.
    -      allow_none_for_tags_and_links: A boolean, whether to allow plugins to
    -        generate Transaction objects with None as value for the 'tags' or 'links'
    -        attributes.
    -    Returns:
    -      A list of new imported entries.
    -    Raises:
    -      Exception: If there is an error in the importer's extract() method.
    -    """
    -    # Extract the entries.
    -    file = cache.get_file(filename)
    -
    -    # Note: Let the exception through on purpose. This makes developing
    -    # importers much easier by rendering the details of the exceptions.
    -    #
    -    # Note: For legacy support, support calling without the existing entries.
    -    kwargs = {}
    -    if 'existing_entries' in inspect.signature(importer.extract).parameters:
    -        kwargs['existing_entries'] = existing_entries
    -    new_entries = importer.extract(file, **kwargs)
    -    if not new_entries:
    -        return []
    -
    -    # Make sure the newly imported entries are sorted; don't trust the importer.
    -    new_entries.sort(key=data.entry_sortkey)
    -
    -    # Ensure that the entries are typed correctly.
    -    for entry in new_entries:
    -        data.sanity_check_types(entry, allow_none_for_tags_and_links)
    -
    -    # Filter out entries with dates before 'min_date'.
    -    if min_date:
    -        new_entries = list(itertools.dropwhile(lambda x: x.date < min_date,
    -                                               new_entries))
    -
    -    return new_entries
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.extract.find_duplicate_entries(new_entries_list, existing_entries) - - -

    - -
    - -

    Flag potentially duplicate entries.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • new_entries_list – A list of pairs of (key, lists of imported entries), one -for each importer. The key identifies the filename and/or importer that -yielded those new entries.

    • -
    • existing_entries – A list of previously existing entries from the target -ledger.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of lists of modified new entries (like new_entries_list), -potentially with modified metadata to indicate those which are duplicated.

    • -
    -
    -
    - Source code in beancount/ingest/extract.py -
    def find_duplicate_entries(new_entries_list, existing_entries):
    -    """Flag potentially duplicate entries.
    -
    -    Args:
    -      new_entries_list: A list of pairs of (key, lists of imported entries), one
    -        for each importer. The key identifies the filename and/or importer that
    -        yielded those new entries.
    -      existing_entries: A list of previously existing entries from the target
    -        ledger.
    -    Returns:
    -      A list of lists of modified new entries (like new_entries_list),
    -      potentially with modified metadata to indicate those which are duplicated.
    -    """
    -    mod_entries_list = []
    -    for key, new_entries in new_entries_list:
    -        # Find similar entries against the existing ledger only.
    -        duplicate_pairs = similar.find_similar_entries(new_entries, existing_entries)
    -
    -        # Add a metadata marker to the extracted entries for duplicates.
    -        duplicate_set = set(id(entry) for entry, _ in duplicate_pairs)
    -        mod_entries = []
    -        for entry in new_entries:
    -            if id(entry) in duplicate_set:
    -                marked_meta = entry.meta.copy()
    -                marked_meta[DUPLICATE_META] = True
    -                entry = entry._replace(meta=marked_meta)
    -            mod_entries.append(entry)
    -        mod_entries_list.append((key, mod_entries))
    -    return mod_entries_list
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.ingest.extract.print_extracted_entries(entries, file) - - -

    - -
    - -

    Print a list of entries.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of extracted entries.

    • -
    • file – A file object to write to.

    • -
    -
    -
    - Source code in beancount/ingest/extract.py -
    def print_extracted_entries(entries, file):
    -    """Print a list of entries.
    -
    -    Args:
    -      entries: A list of extracted entries.
    -      file: A file object to write to.
    -    """
    -    # Print the filename and which modules matched.
    -    # pylint: disable=invalid-name
    -    pr = lambda *args: print(*args, file=file)
    -    pr('')
    -
    -    # Print out the entries.
    -    for entry in entries:
    -        # Check if this entry is a dup, and if so, comment it out.
    -        if DUPLICATE_META in entry.meta:
    -            meta = entry.meta.copy()
    -            meta.pop(DUPLICATE_META)
    -            entry = entry._replace(meta=meta)
    -            entry_string = textwrap.indent(printer.format_entry(entry), '; ')
    -        else:
    -            entry_string = printer.format_entry(entry)
    -        pr(entry_string)
    -
    -    pr('')
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.extract.run(args, _, importers_list, files_or_directories, hooks=None) - - -

    - -
    - -

    Run the subcommand.

    - -
    - Source code in beancount/ingest/extract.py -
    def run(args, _, importers_list, files_or_directories, hooks=None):
    -    """Run the subcommand."""
    -
    -    # Load the ledger, if one is specified.
    -    if args.existing:
    -        entries, _, options_map = loader.load_file(args.existing)
    -    else:
    -        entries, options_map = None, None
    -
    -    extract(importers_list, files_or_directories, sys.stdout,
    -            entries=entries,
    -            options_map=options_map,
    -            mindate=None,
    -            ascending=args.ascending,
    -            hooks=hooks)
    -    return 0
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.file - - - -

    - -
    - -

    Filing script.

    -

    Read an import script and a list of downloaded filenames or directories of -downloaded files, and for each of those files, move the file under an account -corresponding to the filing directory.

    - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.ingest.file.add_arguments(parser) - - -

    - -
    - -

    Add arguments for the extract command.

    - -
    - Source code in beancount/ingest/file.py -
    def add_arguments(parser):
    -    """Add arguments for the extract command."""
    -
    -    parser.add_argument('-o', '--output', '--output-dir', '--destination',
    -                        dest='output_dir', action='store',
    -                        help="The root of the documents tree to move the files to.")
    -
    -    parser.add_argument('-n', '--dry-run', action='store_true',
    -                        help=("Just print where the files would be moved; "
    -                              "don't actually move them."))
    -
    -    parser.add_argument('--no-overwrite', dest='overwrite',
    -                        action='store_false', default=True,
    -                        help="Don't overwrite destination files with the same name.")
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.file.file(importer_config, files_or_directories, destination, dry_run=False, mkdirs=False, overwrite=False, idify=False, logfile=None) - - -

    - -
    - -

    File importable files under a destination directory.

    -

    Given an importer configuration object, search for files that can be -imported under the given list of files or directories and moved them under -the given destination directory with the date computed by the module -prepended to the filename. If the date cannot be extracted, use a reasonable -default for the date (e.g. the last modified time of the file itself).

    -

    If 'mkdirs' is True, create the destination directories before moving the -files.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • importer_config – A list of importer instances that define the config.

    • -
    • files_or_directories – a list of files of directories to walk recursively and -hunt for files to import.

    • -
    • destination – A string, the root destination directory where the files are -to be filed. The files are organized there under a hierarchy mirroring -that of the chart of accounts.

    • -
    • dry_run – A flag, if true, don't actually move the files.

    • -
    • mkdirs – A flag, if true, make all the intervening directories; otherwise, -fail to move files to non-existing dirs.

    • -
    • overwrite – A flag, if true, overwrite an existing destination file.

    • -
    • idify – A flag, if true, remove whitespace and funky characters in the destination -filename.

    • -
    • logfile – A file object to write log entries to, or None, in which case no log is -written out.

    • -
    -
    -
    - Source code in beancount/ingest/file.py -
    def file(importer_config,
    -         files_or_directories,
    -         destination,
    -         dry_run=False,
    -         mkdirs=False,
    -         overwrite=False,
    -         idify=False,
    -         logfile=None):
    -    """File importable files under a destination directory.
    -
    -    Given an importer configuration object, search for files that can be
    -    imported under the given list of files or directories and moved them under
    -    the given destination directory with the date computed by the module
    -    prepended to the filename. If the date cannot be extracted, use a reasonable
    -    default for the date (e.g. the last modified time of the file itself).
    -
    -    If 'mkdirs' is True, create the destination directories before moving the
    -    files.
    -
    -    Args:
    -      importer_config: A list of importer instances that define the config.
    -      files_or_directories: a list of files of directories to walk recursively and
    -        hunt for files to import.
    -      destination: A string, the root destination directory where the files are
    -        to be filed. The files are organized there under a hierarchy mirroring
    -        that of the chart of accounts.
    -      dry_run: A flag, if true, don't actually move the files.
    -      mkdirs: A flag, if true, make all the intervening directories; otherwise,
    -        fail to move files to non-existing dirs.
    -      overwrite: A flag, if true, overwrite an existing destination file.
    -      idify: A flag, if true, remove whitespace and funky characters in the destination
    -        filename.
    -      logfile: A file object to write log entries to, or None, in which case no log is
    -        written out.
    -    """
    -    jobs = []
    -    has_errors = False
    -    for filename, importers in identify.find_imports(importer_config,
    -                                                     files_or_directories,
    -                                                     logfile):
    -        # If we're debugging, print out the match text.
    -        # This option is useful when we're building our importer configuration,
    -        # to figure out which patterns to create as unique signatures.
    -        if not importers:
    -            continue
    -
    -        # Process a single file.
    -        new_fullname = file_one_file(filename, importers, destination, idify, logfile)
    -        if new_fullname is None:
    -            continue
    -
    -        # Check if the destination directory exists.
    -        new_dirname = path.dirname(new_fullname)
    -        if not path.exists(new_dirname) and not mkdirs:
    -            logging.error("Destination directory '{}' does not exist.".format(new_dirname))
    -            has_errors = True
    -            continue
    -
    -        # Check if the destination file already exists; we don't want to clobber
    -        # it by accident.
    -        if not overwrite and path.exists(new_fullname):
    -            logging.error("Destination file '{}' already exists.".format(new_fullname))
    -            has_errors = True
    -            continue
    -
    -        jobs.append((filename, new_fullname))
    -
    -    # Check if any two imported files would be colliding in their destination
    -    # name, before we move anything.
    -    destmap = collections.defaultdict(list)
    -    for src, dest in jobs:
    -        destmap[dest].append(src)
    -    for dest, sources in destmap.items():
    -        if len(sources) != 1:
    -            logging.error("Collision in destination filenames '{}': from {}.".format(
    -                dest, ", ".join(["'{}'".format(source) for source in sources])))
    -            has_errors = True
    -
    -    # If there are any errors, just don't do anything at all. This is a nicer
    -    # behaviour than moving just *some* files.
    -    if dry_run or has_errors:
    -        return
    -
    -    # Actually carry out the moving job.
    -    for old_filename, new_filename in jobs:
    -        move_xdev_file(old_filename, new_filename, mkdirs)
    -
    -    return jobs
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.file.file_one_file(filename, importers, destination, idify=False, logfile=None) - - -

    - -
    - -

    Move a single filename using its matched importers.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the name of the downloaded file to be processed.

    • -
    • importers – A list of importer instances that handle this file.

    • -
    • destination – A string, the root destination directory where the files are -to be filed. The files are organized there under a hierarchy mirroring -that of the chart of accounts.

    • -
    • idify – A flag, if true, remove whitespace and funky characters in the destination -filename.

    • -
    • logfile – A file object to write log entries to, or None, in which case no log is -written out.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The full new destination filename on success, and None if there was an error.

    • -
    -
    -
    - Source code in beancount/ingest/file.py -
    def file_one_file(filename, importers, destination, idify=False, logfile=None):
    -    """Move a single filename using its matched importers.
    -
    -    Args:
    -      filename: A string, the name of the downloaded file to be processed.
    -      importers: A list of importer instances that handle this file.
    -      destination: A string, the root destination directory where the files are
    -        to be filed. The files are organized there under a hierarchy mirroring
    -        that of the chart of accounts.
    -      idify: A flag, if true, remove whitespace and funky characters in the destination
    -        filename.
    -      logfile: A file object to write log entries to, or None, in which case no log is
    -        written out.
    -    Returns:
    -      The full new destination filename on success, and None if there was an error.
    -    """
    -    # Create an object to cache all the conversions between the importers
    -    # and phases and what-not.
    -    file = cache.get_file(filename)
    -
    -    # Get the account corresponding to the file.
    -    file_accounts = []
    -    for index, importer in enumerate(importers):
    -        try:
    -            account_ = importer.file_account(file)
    -        except Exception as exc:
    -            account_ = None
    -            logging.exception("Importer %s.file_account() raised an unexpected error: %s",
    -                              importer.name(), exc)
    -        if account_ is not None:
    -            file_accounts.append(account_)
    -
    -    file_accounts_set = set(file_accounts)
    -    if not file_accounts_set:
    -        logging.error("No account provided by importers: {}".format(
    -            ", ".join(imp.name() for imp in importers)))
    -        return None
    -
    -    if len(file_accounts_set) > 1:
    -        logging.warning("Ambiguous accounts from many importers: {}".format(
    -            ', '.join(file_accounts_set)))
    -        # Note: Don't exit; select the first matching importer's account.
    -
    -    file_account = file_accounts.pop(0)
    -
    -    # Given multiple importers, select the first one that was yielded to
    -    # obtain the date and process the filename.
    -    importer = importers[0]
    -
    -    # Compute the date from the last modified time.
    -    mtime = path.getmtime(filename)
    -    mtime_date = datetime.datetime.fromtimestamp(mtime).date()
    -
    -    # Try to get the file's date by calling a module support function. The
    -    # module may be able to extract the date from the filename, from the
    -    # contents of the file itself (e.g. scraping some text from the PDF
    -    # contents, or grabbing the last line of a CSV file).
    -    try:
    -        date = importer.file_date(file)
    -    except Exception as exc:
    -        logging.exception("Importer %s.file_date() raised an unexpected error: %s",
    -                          importer.name(), exc)
    -        date = None
    -    if date is None:
    -        # Fallback on the last modified time of the file.
    -        date = mtime_date
    -        date_source = 'mtime'
    -    else:
    -        date_source = 'contents'
    -
    -    # Apply filename renaming, if implemented.
    -    # Otherwise clean up the filename.
    -    try:
    -        clean_filename = importer.file_name(file)
    -
    -        # Warn the importer implementor if a name is returned and it's an
    -        # absolute filename.
    -        if clean_filename and (path.isabs(clean_filename) or os.sep in clean_filename):
    -            logging.error(("The importer '%s' file_name() method should return a relative "
    -                           "filename; the filename '%s' is absolute or contains path "
    -                           "separators"),
    -                          importer.name(), clean_filename)
    -    except Exception as exc:
    -        logging.exception("Importer %s.file_name() raised an unexpected error: %s",
    -                          importer.name(), exc)
    -        clean_filename = None
    -    if clean_filename is None:
    -        # If no filename has been provided, use the basename.
    -        clean_filename = path.basename(file.name)
    -    elif re.match(r'\d\d\d\d-\d\d-\d\d', clean_filename):
    -        logging.error("The importer '%s' file_name() method should not date the "
    -                      "returned filename. Implement file_date() instead.")
    -
    -    # We need a simple filename; remove the directory part if there is one.
    -    clean_basename = path.basename(clean_filename)
    -
    -    # Remove whitespace if requested.
    -    if idify:
    -        clean_basename = misc_utils.idify(clean_basename)
    -
    -    # Prepend the date prefix.
    -    new_filename = '{0:%Y-%m-%d}.{1}'.format(date, clean_basename)
    -
    -    # Prepend destination directory.
    -    new_fullname = path.normpath(path.join(destination,
    -                                           file_account.replace(account.sep, os.sep),
    -                                           new_filename))
    -
    -    # Print the filename and which modules matched.
    -    if logfile is not None:
    -        logfile.write('Importer:    {}\n'.format(importer.name() if importer else '-'))
    -        logfile.write('Account:     {}\n'.format(file_account))
    -        logfile.write('Date:        {} (from {})\n'.format(date, date_source))
    -        logfile.write('Destination: {}\n'.format(new_fullname))
    -        logfile.write('\n')
    -
    -    return new_fullname
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.ingest.file.move_xdev_file(src_filename, dst_filename, mkdirs=False) - - -

    - -
    - -

    Move a file, potentially across devices.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • src_filename – A string, the name of the file to copy.

    • -
    • dst_filename – A string, where to copy the file.

    • -
    • mkdirs – A flag, true if we should create a non-existing destination directory.

    • -
    -
    -
    - Source code in beancount/ingest/file.py -
    def move_xdev_file(src_filename, dst_filename, mkdirs=False):
    -    """Move a file, potentially across devices.
    -
    -    Args:
    -      src_filename: A string, the name of the file to copy.
    -      dst_filename: A string, where to copy the file.
    -      mkdirs: A flag, true if we should create a non-existing destination directory.
    -    """
    -    # Create missing directory if required.
    -    dst_dirname = path.dirname(dst_filename)
    -    if mkdirs:
    -        if not path.exists(dst_dirname):
    -            os.makedirs(dst_dirname)
    -    else:
    -        if not path.exists(dst_dirname):
    -            raise OSError("Destination directory '{}' does not exist.".format(dst_dirname))
    -
    -    # Copy the file to its new name.
    -    shutil.copyfile(src_filename, dst_filename)
    -
    -    # Remove the old file. Note that we copy and remove to support
    -    # cross-device moves, because it's sensible that the destination might
    -    # be on an encrypted device.
    -    os.remove(src_filename)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.file.run(args, parser, importers_list, files_or_directories, hooks=None) - - -

    - -
    - -

    Run the subcommand.

    - -
    - Source code in beancount/ingest/file.py -
    def run(args, parser, importers_list, files_or_directories, hooks=None):
    -    """Run the subcommand."""
    -
    -    # If the output directory is not specified, move the files at the root where
    -    # the import configuration file is located. (Providing this default seems
    -    # better than using a required option.)
    -    if args.output_dir is None:
    -        if hasattr(args, 'config'):
    -            args.output_dir = path.dirname(path.abspath(args.config))
    -        else:
    -            import __main__ # pylint: disable=import-outside-toplevel
    -            args.output_dir = path.dirname(path.abspath(__main__.__file__))
    -
    -    # Make sure the output directory exists.
    -    if not path.exists(args.output_dir):
    -        parser.error('Output directory "{}" does not exist.'.format(args.output_dir))
    -
    -    file(importers_list, files_or_directories, args.output_dir,
    -         dry_run=args.dry_run,
    -         mkdirs=True,
    -         overwrite=args.overwrite,
    -         idify=True,
    -         logfile=sys.stdout)
    -    return 0
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.identify - - - -

    - -
    - -

    Identify script.

    -

    Read an import script and a list of downloaded filenames or directories of -2downloaded files, and for each of those files, identify which importer it should -be associated with.

    - - - -
    - - - - - - - - - - - - - -
    - - - -

    -beancount.ingest.identify.add_arguments(parser) - - -

    - -
    - -

    Add arguments for the identify command.

    - -
    - Source code in beancount/ingest/identify.py -
    def add_arguments(parser):
    -    """Add arguments for the identify command."""
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.identify.find_imports(importer_config, files_or_directories, logfile=None) - - -

    - -
    - -

    Given an importer configuration, search for files that can be imported in the -list of files or directories, run the signature checks on them and return a list -of (filename, importers), where 'importers' is a list of importers that matched -the file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • importer_config – a list of importer instances that define the config.

    • -
    • files_or_directories – a list of files of directories to walk recursively and - hunt for files to import.

    • -
    • logfile – A file object to write log entries to, or None, in which case no log is -written out.

    • -
    -

    Yields: - Triples of filename found, textified contents of the file, and list of - importers matching this file.

    - -
    - Source code in beancount/ingest/identify.py -
    def find_imports(importer_config, files_or_directories, logfile=None):
    -    """Given an importer configuration, search for files that can be imported in the
    -    list of files or directories, run the signature checks on them and return a list
    -    of (filename, importers), where 'importers' is a list of importers that matched
    -    the file.
    -
    -    Args:
    -      importer_config: a list of importer instances that define the config.
    -      files_or_directories: a list of files of directories to walk recursively and
    -                            hunt for files to import.
    -      logfile: A file object to write log entries to, or None, in which case no log is
    -        written out.
    -    Yields:
    -      Triples of filename found, textified contents of the file, and list of
    -      importers matching this file.
    -    """
    -    # Iterate over all files found; accumulate the entries by identification.
    -    for filename in file_utils.find_files(files_or_directories):
    -        if logfile is not None:
    -            logfile.write(SECTION.format(filename))
    -            logfile.write('\n')
    -
    -        # Skip files that are simply too large.
    -        size = path.getsize(filename)
    -        if size > FILE_TOO_LARGE_THRESHOLD:
    -            logging.warning("File too large: '{}' ({} bytes); skipping.".format(
    -                filename, size))
    -            continue
    -
    -        # For each of the sources the user has declared, identify which
    -        # match the text.
    -        file = cache.get_file(filename)
    -        matching_importers = []
    -        for importer in importer_config:
    -            try:
    -                matched = importer.identify(file)
    -                if matched:
    -                    matching_importers.append(importer)
    -            except Exception as exc:
    -                logging.exception("Importer %s.identify() raised an unexpected error: %s",
    -                                  importer.name(), exc)
    -
    -        yield (filename, matching_importers)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.identify.identify(importers_list, files_or_directories) - - -

    - -
    - -

    Run the identification loop.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • importers_list – A list of importer instances.

    • -
    • files_or_directories – A list of strings, files or directories.

    • -
    -
    -
    - Source code in beancount/ingest/identify.py -
    def identify(importers_list, files_or_directories):
    -    """Run the identification loop.
    -
    -    Args:
    -      importers_list: A list of importer instances.
    -      files_or_directories: A list of strings, files or directories.
    -    """
    -    logfile = sys.stdout
    -    for filename, importers in find_imports(importers_list, files_or_directories,
    -                                            logfile=logfile):
    -        file = cache.get_file(filename)
    -        for importer in importers:
    -            logfile.write('Importer:    {}\n'.format(importer.name() if importer else '-'))
    -            logfile.write('Account:     {}\n'.format(importer.file_account(file)))
    -            logfile.write('\n')
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.ingest.identify.run(_, __, importers_list, files_or_directories, hooks=None) - - -

    - -
    - -

    Run the subcommand.

    - -
    - Source code in beancount/ingest/identify.py -
    def run(_, __, importers_list, files_or_directories, hooks=None):
    -    """Run the subcommand."""
    -    return identify(importers_list, files_or_directories)
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.importer - - - -

    - -
    - -

    Importer protocol.

    -

    All importers must comply with this interface and implement at least some of its -methods. A configuration consists in a simple list of such importer instances. -The importer processes run through the importers, calling some of its methods in -order to identify, extract and file the downloaded files.

    -

    Each of the methods accept a cache.FileMemo object which has a 'name' attribute -with the filename to process, but which also provides a place to cache -conversions. Use its convert() method whenever possible to avoid carrying out -the same conversion multiple times. See beancount.ingest.cache for more details.

    -

    Synopsis:

    -

    name(): Return a unique identifier for the importer instance. - identify(): Return true if the identifier is able to process the file. - extract(): Extract directives from a file's contents and return of list of entries. - file_account(): Return an account name associated with the given file for this importer. - file_date(): Return a date associated with the downloaded file (e.g., the statement date). - file_name(): Return a cleaned up filename for storage (optional).

    -

    Just to be clear: Although this importer will not raise NotImplementedError -exceptions (it returns default values for each method), you NEED to derive from -it in order to do anything meaningful. Simply instantiating this importer will -not match not provide any useful information. It just defines the protocol for -all importers.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.ingest.importer.ImporterProtocol - - - -

    - -
    - -

    Interface that all source importers need to comply with.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.ingest.importer.ImporterProtocol.__str__(self) - - - special - - -

    - -
    - -

    Return a unique id/name for this importer.

    - - - - - - - - - - - - -
    Returns: -
      -
    • A string which uniquely identifies this importer.

    • -
    -
    -
    - Source code in beancount/ingest/importer.py -
    def name(self):
    -    """Return a unique id/name for this importer.
    -
    -    Returns:
    -      A string which uniquely identifies this importer.
    -    """
    -    cls = self.__class__
    -    return '{}.{}'.format(cls.__module__, cls.__name__)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importer.ImporterProtocol.extract(self, file, existing_entries=None) - - -

    - -
    - -

    Extract transactions from a file.

    -

    If the importer would like to flag a returned transaction as a known -duplicate, it may opt to set the special flag "duplicate" to True, -and the transaction should be treated as a duplicate by the extraction -code. This is a way to let the importer use particular information about -previously imported transactions in order to flag them as duplicates. -For example, if an importer has a way to get a persistent unique id for -each of the imported transactions. (See this discussion for context: -https://groups.google.com/d/msg/beancount/0iV-ipBJb8g/-uk4wsH2AgAJ)

    - - - - - - - - - - - - -
    Parameters: -
      -
    • file – A cache.FileMemo instance.

    • -
    • existing_entries – An optional list of existing directives loaded from -the ledger which is intended to contain the extracted entries. This -is only provided if the user provides them via a flag in the -extractor program.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of new, imported directives (usually mostly Transactions) -extracted from the file.

    • -
    -
    -
    - Source code in beancount/ingest/importer.py -
    def extract(self, file, existing_entries=None):
    -    """Extract transactions from a file.
    -
    -    If the importer would like to flag a returned transaction as a known
    -    duplicate, it may opt to set the special flag "__duplicate__" to True,
    -    and the transaction should be treated as a duplicate by the extraction
    -    code. This is a way to let the importer use particular information about
    -    previously imported transactions in order to flag them as duplicates.
    -    For example, if an importer has a way to get a persistent unique id for
    -    each of the imported transactions. (See this discussion for context:
    -    https://groups.google.com/d/msg/beancount/0iV-ipBJb8g/-uk4wsH2AgAJ)
    -
    -    Args:
    -      file: A cache.FileMemo instance.
    -      existing_entries: An optional list of existing directives loaded from
    -        the ledger which is intended to contain the extracted entries. This
    -        is only provided if the user provides them via a flag in the
    -        extractor program.
    -    Returns:
    -      A list of new, imported directives (usually mostly Transactions)
    -      extracted from the file.
    -    """
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importer.ImporterProtocol.file_account(self, file) - - -

    - -
    - -

    Return an account associated with the given file.

    -

    Note: If you don't implement this method you won't be able to move the -files into its preservation hierarchy; the bean-file command won't -work.

    -

    Also, normally the returned account is not a function of the input -file--just of the importer--but it is provided anyhow.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • file – A cache.FileMemo instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The name of the account that corresponds to this importer.

    • -
    -
    -
    - Source code in beancount/ingest/importer.py -
    def file_account(self, file):
    -    """Return an account associated with the given file.
    -
    -    Note: If you don't implement this method you won't be able to move the
    -    files into its preservation hierarchy; the bean-file command won't
    -    work.
    -
    -    Also, normally the returned account is not a function of the input
    -    file--just of the importer--but it is provided anyhow.
    -
    -    Args:
    -      file: A cache.FileMemo instance.
    -    Returns:
    -      The name of the account that corresponds to this importer.
    -    """
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importer.ImporterProtocol.file_date(self, file) - - -

    - -
    - -

    Attempt to obtain a date that corresponds to the given file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • file – A cache.FileMemo instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A date object, if successful, or None if a date could not be extracted. -(If no date is returned, the file creation time is used. This is the -default.)

    • -
    -
    -
    - Source code in beancount/ingest/importer.py -
    def file_date(self, file):
    -    """Attempt to obtain a date that corresponds to the given file.
    -
    -    Args:
    -      file: A cache.FileMemo instance.
    -    Returns:
    -      A date object, if successful, or None if a date could not be extracted.
    -      (If no date is returned, the file creation time is used. This is the
    -      default.)
    -    """
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importer.ImporterProtocol.file_name(self, file) - - -

    - -
    - -

    A filter that optionally renames a file before filing.

    -

    This is used to make tidy filenames for filed/stored document files. If -you don't implement this and return None, the same filename is used. -Note that if you return a filename, a simple, RELATIVE filename must be -returned, not an absolute filename.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • file – A cache.FileMemo instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The tidied up, new filename to store it as.

    • -
    -
    -
    - Source code in beancount/ingest/importer.py -
    def file_name(self, file):
    -    """A filter that optionally renames a file before filing.
    -
    -    This is used to make tidy filenames for filed/stored document files. If
    -    you don't implement this and return None, the same filename is used.
    -    Note that if you return a filename, a simple, RELATIVE filename must be
    -    returned, not an absolute filename.
    -
    -    Args:
    -      file: A cache.FileMemo instance.
    -    Returns:
    -      The tidied up, new filename to store it as.
    -    """
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importer.ImporterProtocol.identify(self, file) - - -

    - -
    - -

    Return true if this importer matches the given file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • file – A cache.FileMemo instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A boolean, true if this importer can handle this file.

    • -
    -
    -
    - Source code in beancount/ingest/importer.py -
    def identify(self, file):
    -    """Return true if this importer matches the given file.
    -
    -    Args:
    -      file: A cache.FileMemo instance.
    -    Returns:
    -      A boolean, true if this importer can handle this file.
    -    """
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importer.ImporterProtocol.name(self) - - -

    - -
    - -

    Return a unique id/name for this importer.

    - - - - - - - - - - - - -
    Returns: -
      -
    • A string which uniquely identifies this importer.

    • -
    -
    -
    - Source code in beancount/ingest/importer.py -
    def name(self):
    -    """Return a unique id/name for this importer.
    -
    -    Returns:
    -      A string which uniquely identifies this importer.
    -    """
    -    cls = self.__class__
    -    return '{}.{}'.format(cls.__module__, cls.__name__)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.importers - - - - special - - -

    - -
    - - - - -
    - - - - - - - - - - -
    - - - -

    - beancount.ingest.importers.config - - - -

    - -
    - -

    Mixin to add support for configuring importers with multiple accounts.

    -

    This importer implements some simple common functionality to create importers -which accept a long number of account names or regular expressions on the set of -account names. This is inspired by functionality in the importers in the -previous iteration of the ingest code, which used to be its own project.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.ingest.importers.config.ConfigImporterMixin - - - -

    - -
    - -

    A mixin class which supports configuration of account names.

    -

    Mix this into the implementation of a importer.ImporterProtocol.

    - - - - -
    - - - - - - - - - - -
    - - - -
    -beancount.ingest.importers.config.ConfigImporterMixin.__init__(self, config) - - - special - - -
    - -
    - -

    Provide a list of accounts and regexps as configuration to the importer.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • config – A dict of configuration accounts, that must match the values -declared in the class' REQUIRED_CONFIG.

    • -
    -
    -
    - Source code in beancount/ingest/importers/config.py -
    def __init__(self, config):
    -    """Provide a list of accounts and regexps as configuration to the importer.
    -
    -    Args:
    -      config: A dict of configuration accounts, that must match the values
    -        declared in the class' REQUIRED_CONFIG.
    -    """
    -    super().__init__()
    -
    -    # Check that the required configuration values are present.
    -    assert isinstance(config, dict), "Configuration must be a dict type"
    -    if not self._verify_config(config):
    -        raise ValueError("Invalid config {}, requires {}".format(
    -            config, self.REQUIRED_CONFIG))
    -    self.config = config
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.importers.csv - - - -

    - -
    - -

    CSV importer.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.ingest.importers.csv.Col (Enum) - - - - -

    - -
    - -

    The set of interpretable columns.

    - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.ingest.importers.csv.Importer (IdentifyMixin, FilingMixin) - - - - -

    - -
    - -

    Importer for CSV files.

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.ingest.importers.csv.Importer.__init__(self, config, account, currency, regexps=None, skip_lines=0, last4_map=None, categorizer=None, institution=None, debug=False, csv_dialect='excel', dateutil_kwds=None, narration_sep='; ', encoding=None, invert_sign=False, **kwds) - - - special - - -
    - -
    - -

    Constructor.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • config – A dict of Col enum types to the names or indexes of the columns.

    • -
    • account – An account string, the account to post this to.

    • -
    • currency – A currency string, the currency of this account.

    • -
    • regexps – A list of regular expression strings.

    • -
    • skip_lines (int) – Skip first x (garbage) lines of file.

    • -
    • last4_map (Optional[Dict]) – A dict that maps last 4 digits of the card to a friendly string.

    • -
    • categorizer (Optional[Callable]) – A callable that attaches the other posting (usually expenses) -to a transaction with only single posting.

    • -
    • institution (Optional[str]) – An optional name of an institution to rename the files to.

    • -
    • debug (bool) – Whether or not to print debug information

    • -
    • csv_dialect (Union[str, csv.Dialect]) – A csv dialect given either as string or as instance or -subclass of csv.Dialect.

    • -
    • dateutil_kwds (Optional[Dict]) – An optional dict defining the dateutil parser kwargs.

    • -
    • narration_sep (str) – A string, a separator to use for splitting up the payee and -narration fields of a source field.

    • -
    • encoding (Optional[str]) – An optional encoding for the file. Typically useful for files -encoded in 'latin1' instead of 'utf-8' (the default).

    • -
    • invert_sign (Optional[bool]) – If true, invert the amount's sign unconditionally.

    • -
    • **kwds – Extra keyword arguments to provide to the base mixins.

    • -
    -
    -
    - Source code in beancount/ingest/importers/csv.py -
    def __init__(self, config, account, currency,
    -             regexps=None,
    -             skip_lines: int = 0,
    -             last4_map: Optional[Dict] = None,
    -             categorizer: Optional[Callable] = None,
    -             institution: Optional[str] = None,
    -             debug: bool = False,
    -             csv_dialect: Union[str, csv.Dialect] = 'excel',
    -             dateutil_kwds: Optional[Dict] = None,
    -             narration_sep: str = '; ',
    -             encoding: Optional[str] = None,
    -             invert_sign: Optional[bool] = False,
    -             **kwds):
    -    """Constructor.
    -
    -    Args:
    -      config: A dict of Col enum types to the names or indexes of the columns.
    -      account: An account string, the account to post this to.
    -      currency: A currency string, the currency of this account.
    -      regexps: A list of regular expression strings.
    -      skip_lines: Skip first x (garbage) lines of file.
    -      last4_map: A dict that maps last 4 digits of the card to a friendly string.
    -      categorizer: A callable that attaches the other posting (usually expenses)
    -        to a transaction with only single posting.
    -      institution: An optional name of an institution to rename the files to.
    -      debug: Whether or not to print debug information
    -      csv_dialect: A `csv` dialect given either as string or as instance or
    -        subclass of `csv.Dialect`.
    -      dateutil_kwds: An optional dict defining the dateutil parser kwargs.
    -      narration_sep: A string, a separator to use for splitting up the payee and
    -        narration fields of a source field.
    -      encoding: An optional encoding for the file. Typically useful for files
    -        encoded in 'latin1' instead of 'utf-8' (the default).
    -      invert_sign: If true, invert the amount's sign unconditionally.
    -      **kwds: Extra keyword arguments to provide to the base mixins.
    -    """
    -    assert isinstance(config, dict), "Invalid type: {}".format(config)
    -    self.config = config
    -
    -    self.currency = currency
    -    assert isinstance(skip_lines, int)
    -    self.skip_lines = skip_lines
    -    self.last4_map = last4_map or {}
    -    self.debug = debug
    -    self.dateutil_kwds = dateutil_kwds
    -    self.csv_dialect = csv_dialect
    -    self.narration_sep = narration_sep
    -    self.encoding = encoding
    -    self.invert_sign = invert_sign
    -
    -    self.categorizer = categorizer
    -
    -    # Prepare kwds for filing mixin.
    -    kwds['filing'] = account
    -    if institution:
    -        prefix = kwds.get('prefix', None)
    -        assert prefix is None
    -        kwds['prefix'] = institution
    -
    -    # Prepare kwds for identifier mixin.
    -    if isinstance(regexps, str):
    -        regexps = [regexps]
    -    matchers = kwds.setdefault('matchers', [])
    -    matchers.append(('mime', 'text/csv'))
    -    if regexps:
    -        for regexp in regexps:
    -            matchers.append(('content', regexp))
    -
    -    super().__init__(**kwds)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.csv.Importer.extract(self, file, existing_entries=None) - - -
    - -
    - -

    Extract transactions from a file.

    -

    If the importer would like to flag a returned transaction as a known -duplicate, it may opt to set the special flag "duplicate" to True, -and the transaction should be treated as a duplicate by the extraction -code. This is a way to let the importer use particular information about -previously imported transactions in order to flag them as duplicates. -For example, if an importer has a way to get a persistent unique id for -each of the imported transactions. (See this discussion for context: -https://groups.google.com/d/msg/beancount/0iV-ipBJb8g/-uk4wsH2AgAJ)

    - - - - - - - - - - - - -
    Parameters: -
      -
    • file – A cache.FileMemo instance.

    • -
    • existing_entries – An optional list of existing directives loaded from -the ledger which is intended to contain the extracted entries. This -is only provided if the user provides them via a flag in the -extractor program.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of new, imported directives (usually mostly Transactions) -extracted from the file.

    • -
    -
    -
    - Source code in beancount/ingest/importers/csv.py -
    def extract(self, file, existing_entries=None):
    -    account = self.file_account(file)
    -    entries = []
    -
    -    # Normalize the configuration to fetch by index.
    -    iconfig, has_header = normalize_config(
    -        self.config, file.head(), self.csv_dialect, self.skip_lines)
    -
    -    reader = iter(csv.reader(open(file.name, encoding=self.encoding),
    -                             dialect=self.csv_dialect))
    -
    -    # Skip garbage lines
    -    for _ in range(self.skip_lines):
    -        next(reader)
    -
    -    # Skip header, if one was detected.
    -    if has_header:
    -        next(reader)
    -
    -    def get(row, ftype):
    -        try:
    -            return row[iconfig[ftype]] if ftype in iconfig else None
    -        except IndexError:  # FIXME: this should not happen
    -            return None
    -
    -    # Parse all the transactions.
    -    first_row = last_row = None
    -    for index, row in enumerate(reader, 1):
    -        if not row:
    -            continue
    -        if row[0].startswith('#'):
    -            continue
    -
    -        # If debugging, print out the rows.
    -        if self.debug:
    -            print(row)
    -
    -        if first_row is None:
    -            first_row = row
    -        last_row = row
    -
    -        # Extract the data we need from the row, based on the configuration.
    -        date = get(row, Col.DATE)
    -        txn_date = get(row, Col.TXN_DATE)
    -        txn_time = get(row, Col.TXN_TIME)
    -
    -        payee = get(row, Col.PAYEE)
    -        if payee:
    -            payee = payee.strip()
    -
    -        fields = filter(None, [get(row, field)
    -                               for field in (Col.NARRATION1,
    -                                             Col.NARRATION2,
    -                                             Col.NARRATION3)])
    -        narration = self.narration_sep.join(
    -            field.strip() for field in fields).replace('\n', '; ')
    -
    -        tag = get(row, Col.TAG)
    -        tags = {tag} if tag is not None else data.EMPTY_SET
    -
    -        link = get(row, Col.REFERENCE_ID)
    -        links = {link} if link is not None else data.EMPTY_SET
    -
    -        last4 = get(row, Col.LAST4)
    -
    -        balance = get(row, Col.BALANCE)
    -
    -        # Create a transaction
    -        meta = data.new_metadata(file.name, index)
    -        if txn_date is not None:
    -            meta['date'] = parse_date_liberally(txn_date,
    -                                                self.dateutil_kwds)
    -        if txn_time is not None:
    -            meta['time'] = str(dateutil.parser.parse(txn_time).time())
    -        if balance is not None:
    -            meta['balance'] = D(balance)
    -        if last4:
    -            last4_friendly = self.last4_map.get(last4.strip())
    -            meta['card'] = last4_friendly if last4_friendly else last4
    -        date = parse_date_liberally(date, self.dateutil_kwds)
    -        txn = data.Transaction(meta, date, self.FLAG, payee, narration,
    -                               tags, links, [])
    -
    -        # Attach one posting to the transaction
    -        amount_debit, amount_credit = self.get_amounts(iconfig, row)
    -
    -        # Skip empty transactions
    -        if amount_debit is None and amount_credit is None:
    -            continue
    -
    -        for amount in [amount_debit, amount_credit]:
    -            if amount is None:
    -                continue
    -            if self.invert_sign:
    -                amount = -amount
    -            units = Amount(amount, self.currency)
    -            txn.postings.append(
    -                data.Posting(account, units, None, None, None, None))
    -
    -        # Attach the other posting(s) to the transaction.
    -        if isinstance(self.categorizer, collections.abc.Callable):
    -            txn = self.categorizer(txn)
    -
    -        # Add the transaction to the output list
    -        entries.append(txn)
    -
    -    # Figure out if the file is in ascending or descending order.
    -    first_date = parse_date_liberally(get(first_row, Col.DATE),
    -                                      self.dateutil_kwds)
    -    last_date = parse_date_liberally(get(last_row, Col.DATE),
    -                                     self.dateutil_kwds)
    -    is_ascending = first_date < last_date
    -
    -    # Reverse the list if the file is in descending order
    -    if not is_ascending:
    -        entries = list(reversed(entries))
    -
    -    # Add a balance entry if possible
    -    if Col.BALANCE in iconfig and entries:
    -        entry = entries[-1]
    -        date = entry.date + datetime.timedelta(days=1)
    -        balance = entry.meta.get('balance', None)
    -        if balance is not None:
    -            meta = data.new_metadata(file.name, index)
    -            entries.append(
    -                data.Balance(meta, date,
    -                             account, Amount(balance, self.currency),
    -                             None, None))
    -
    -    # Remove the 'balance' metadata.
    -    for entry in entries:
    -        entry.meta.pop('balance', None)
    -
    -    return entries
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.csv.Importer.file_date(self, file) - - -
    - -
    - -

    Get the maximum date from the file.

    - -
    - Source code in beancount/ingest/importers/csv.py -
    def file_date(self, file):
    -    "Get the maximum date from the file."
    -    iconfig, has_header = normalize_config(
    -        self.config, file.head(), self.csv_dialect, self.skip_lines)
    -    if Col.DATE in iconfig:
    -        reader = iter(csv.reader(open(file.name), dialect=self.csv_dialect))
    -        for _ in range(self.skip_lines):
    -            next(reader)
    -        if has_header:
    -            next(reader)
    -        max_date = None
    -        for row in reader:
    -            if not row:
    -                continue
    -            if row[0].startswith('#'):
    -                continue
    -            date_str = row[iconfig[Col.DATE]]
    -            date = parse_date_liberally(date_str, self.dateutil_kwds)
    -            if max_date is None or date > max_date:
    -                max_date = date
    -        return max_date
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.csv.Importer.get_amounts(self, iconfig, row, allow_zero_amounts=False) - - -
    - -
    - -

    See function get_amounts() for details.

    -

    This method is present to allow clients to override it in order to deal -with special cases, e.g., columns with currency symbols in them.

    - -
    - Source code in beancount/ingest/importers/csv.py -
    def get_amounts(self, iconfig, row, allow_zero_amounts=False):
    -    """See function get_amounts() for details.
    -
    -    This method is present to allow clients to override it in order to deal
    -    with special cases, e.g., columns with currency symbols in them.
    -    """
    -    return get_amounts(iconfig, row, allow_zero_amounts)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.ingest.importers.csv.get_amounts(iconfig, row, allow_zero_amounts=False) - - -

    - -
    - -

    Get the amount columns of a row.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • iconfig – A dict of Col to row index.

    • -
    • row – A row array containing the values of the given row.

    • -
    • allow_zero_amounts – Is a transaction with amount D('0.00') okay? If not, -return (None, None).

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of (debit-amount, credit-amount), both of which are either an -instance of Decimal or None, or not available.

    • -
    -
    -
    - Source code in beancount/ingest/importers/csv.py -
    def get_amounts(iconfig, row, allow_zero_amounts=False):
    -    """Get the amount columns of a row.
    -
    -    Args:
    -      iconfig: A dict of Col to row index.
    -      row: A row array containing the values of the given row.
    -      allow_zero_amounts: Is a transaction with amount D('0.00') okay? If not,
    -        return (None, None).
    -    Returns:
    -      A pair of (debit-amount, credit-amount), both of which are either an
    -      instance of Decimal or None, or not available.
    -    """
    -    debit, credit = None, None
    -    if Col.AMOUNT in iconfig:
    -        credit = row[iconfig[Col.AMOUNT]]
    -    else:
    -        debit, credit = [row[iconfig[col]] if col in iconfig else None
    -                         for col in [Col.AMOUNT_DEBIT, Col.AMOUNT_CREDIT]]
    -
    -    # If zero amounts aren't allowed, return null value.
    -    is_zero_amount = ((credit is not None and D(credit) == ZERO) and
    -                      (debit is not None and D(debit) == ZERO))
    -    if not allow_zero_amounts and is_zero_amount:
    -        return (None, None)
    -
    -    return (-D(debit) if debit else None,
    -            D(credit) if credit else None)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importers.csv.normalize_config(config, head, dialect='excel', skip_lines=0) - - -

    - -
    - -

    Using the header line, convert the configuration field name lookups to int indexes.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • config – A dict of Col types to string or indexes.

    • -
    • head – A string, some decent number of bytes of the head of the file.

    • -
    • dialect – A dialect definition to parse the header

    • -
    • skip_lines (int) – Skip first x (garbage) lines of file.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of - A dict of Col types to integer indexes of the fields, and - a boolean, true if the file has a header.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • ValueError – If there is no header and the configuration does not consist -entirely of integer indexes.

    • -
    -
    -
    - Source code in beancount/ingest/importers/csv.py -
    def normalize_config(config, head, dialect='excel', skip_lines: int = 0):
    -    """Using the header line, convert the configuration field name lookups to int indexes.
    -
    -    Args:
    -      config: A dict of Col types to string or indexes.
    -      head: A string, some decent number of bytes of the head of the file.
    -      dialect: A dialect definition to parse the header
    -      skip_lines: Skip first x (garbage) lines of file.
    -    Returns:
    -      A pair of
    -        A dict of Col types to integer indexes of the fields, and
    -        a boolean, true if the file has a header.
    -    Raises:
    -      ValueError: If there is no header and the configuration does not consist
    -        entirely of integer indexes.
    -    """
    -    # Skip garbage lines before sniffing the header
    -    assert isinstance(skip_lines, int)
    -    assert skip_lines >= 0
    -    for _ in range(skip_lines):
    -        head = head[head.find('\n')+1:]
    -
    -    has_header = csv.Sniffer().has_header(head)
    -    if has_header:
    -        header = next(csv.reader(io.StringIO(head), dialect=dialect))
    -        field_map = {field_name.strip(): index
    -                     for index, field_name in enumerate(header)}
    -        index_config = {}
    -        for field_type, field in config.items():
    -            if isinstance(field, str):
    -                field = field_map[field]
    -            index_config[field_type] = field
    -    else:
    -        if any(not isinstance(field, int)
    -               for field_type, field in config.items()):
    -            raise ValueError("CSV config without header has non-index fields: "
    -                             "{}".format(config))
    -        index_config = config
    -    return index_config, has_header
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.importers.fileonly - - - -

    - -
    - -

    A simplistic importer that can be used just to file away some download.

    -

    Sometimes you just want to save and accumulate data

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.ingest.importers.fileonly.Importer (FilingMixin, IdentifyMixin) - - - - -

    - -
    - -

    An importer that supports only matching (identification) and filing.

    - - - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.importers.mixins - - - - special - - -

    - -
    - - - - -
    - - - - - - - - - - -
    - - - -

    - beancount.ingest.importers.mixins.config - - - -

    - -
    - -

    Base class that implements configuration and a filing account.

    - - - -
    - - - - - - - - -
    - - - -
    - -beancount.ingest.importers.mixins.config.ConfigMixin (ImporterProtocol) - - - - -
    - -
    - - - - - -
    - - - - - - - - - - -
    - - - -
    -beancount.ingest.importers.mixins.config.ConfigMixin.__init__(self, **kwds) - - - special - - -
    - -
    - -

    Pull 'config' from kwds.

    - -
    - Source code in beancount/ingest/importers/mixins/config.py -
    def __init__(self, **kwds):
    -    """Pull 'config' from kwds."""
    -
    -    config = kwds.pop('config', None)
    -    schema = self.REQUIRED_CONFIG
    -    if config or schema:
    -        assert config is not None
    -        assert schema is not None
    -        self.config = validate_config(config, config, self)
    -    else:
    -        self.config = None
    -
    -    super().__init__(**kwds)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -
    -beancount.ingest.importers.mixins.config.validate_config(config, schema, importer) - - -
    - -
    - -

    Check the configuration account provided by the user against the accounts -required by the source importer.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • config – A config dict of actual values on an importer.

    • -
    • schema – A dict of declarations of required values.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • ValueError – If the configuration is invalid.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A validated configuration dict.

    • -
    -
    -
    - Source code in beancount/ingest/importers/mixins/config.py -
    def validate_config(config, schema, importer):
    -    """Check the configuration account provided by the user against the accounts
    -    required by the source importer.
    -
    -    Args:
    -      config: A config dict of actual values on an importer.
    -      schema: A dict of declarations of required values.
    -    Raises:
    -      ValueError: If the configuration is invalid.
    -    Returns:
    -      A validated configuration dict.
    -    """
    -    provided_options = set(config)
    -    required_options = set(schema)
    -
    -    for option in (required_options - provided_options):
    -        raise ValueError("Missing value from user configuration for importer {}: {}".format(
    -            importer.__class__.__name__, option))
    -
    -    for option in (provided_options - required_options):
    -        raise ValueError("Unknown value in user configuration for importer {}: {}".format(
    -            importer.__class__.__name__, option))
    -
    -    # FIXME: Validate types as well, including account type as a default.
    -
    -    # FIXME: Here we could validate account names by looking them up from the
    -    # existing ledger.
    -
    -    return config
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.importers.mixins.filing - - - -

    - -
    - -

    Base class that implements filing account.

    -

    It also sports an optional prefix to prepend to the renamed filename. Typically -you can put the name of the institution there, so you get a renamed filename -like this:

    -

    YYYY-MM-DD.institution.Original_File_Name.pdf

    - - - -
    - - - - - - - - -
    - - - -
    - -beancount.ingest.importers.mixins.filing.FilingMixin (ImporterProtocol) - - - - -
    - -
    - - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.ingest.importers.mixins.filing.FilingMixin.__init__(self, **kwds) - - - special - - -
    - -
    - -

    Pull 'filing' and 'prefix' from kwds.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filing – The name of the account to file to.

    • -
    • prefix – The name of the institution prefix to insert.

    • -
    -
    -
    - Source code in beancount/ingest/importers/mixins/filing.py -
    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)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.mixins.filing.FilingMixin.file_account(self, file) - - -
    - -
    - -

    Return an account associated with the given file.

    -

    Note: If you don't implement this method you won't be able to move the -files into its preservation hierarchy; the bean-file command won't -work.

    -

    Also, normally the returned account is not a function of the input -file--just of the importer--but it is provided anyhow.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • file – A cache.FileMemo instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The name of the account that corresponds to this importer.

    • -
    -
    -
    - Source code in beancount/ingest/importers/mixins/filing.py -
    def file_account(self, file):
    -    return self.filing_account
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.mixins.filing.FilingMixin.file_name(self, file) - - -
    - -
    - -

    Return the optional renamed account filename.

    - -
    - Source code in beancount/ingest/importers/mixins/filing.py -
    def file_name(self, file):
    -    """Return the optional renamed account filename."""
    -    supername = super().file_name(file)
    -    if not self.prefix:
    -        return supername
    -    else:
    -        return '.'.join(filter(None, [self.prefix,
    -                                      supername or path.basename(file.name)]))
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.mixins.filing.FilingMixin.name(self) - - -
    - -
    - -

    Include the filing account in the name.

    - -
    - Source code in beancount/ingest/importers/mixins/filing.py -
    def name(self):
    -    """Include the filing account in the name."""
    -    return '{}: "{}"'.format(super().name(), self.filing_account)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.importers.mixins.identifier - - - -

    - -
    - -

    Base class that implements identification using regular expressions.

    - - - -
    - - - - - - - - -
    - - - -
    - -beancount.ingest.importers.mixins.identifier.IdentifyMixin (ImporterProtocol) - - - - -
    - -
    - - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.ingest.importers.mixins.identifier.IdentifyMixin.__init__(self, **kwds) - - - special - - -
    - -
    - -

    Pull 'matchers' and 'converter' from kwds.

    - -
    - Source code in beancount/ingest/importers/mixins/identifier.py -
    def __init__(self, **kwds):
    -    """Pull 'matchers' and 'converter' from kwds."""
    -
    -    self.remap = collections.defaultdict(list)
    -    matchers = kwds.pop('matchers', [])
    -    cls_matchers = getattr(self, 'matchers', [])
    -    assert isinstance(matchers, list)
    -    assert isinstance(cls_matchers, list)
    -    for part, regexp in itertools.chain(matchers, cls_matchers):
    -        assert part in _PARTS, repr(part)
    -        assert isinstance(regexp, str), repr(regexp)
    -        self.remap[part].append(re.compile(regexp))
    -
    -    # Converter is a fn(filename: Text) -> contents: Text.
    -    self.converter = kwds.pop('converter',
    -                              getattr(self, 'converter', None))
    -
    -    super().__init__(**kwds)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.mixins.identifier.IdentifyMixin.identify(self, file) - - -
    - -
    - -

    Return true if this importer matches the given file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • file – A cache.FileMemo instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A boolean, true if this importer can handle this file.

    • -
    -
    -
    - Source code in beancount/ingest/importers/mixins/identifier.py -
    def identify(self, file):
    -    return identify(self.remap, self.converter, file)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -
    -beancount.ingest.importers.mixins.identifier.identify(remap, converter, file) - - -
    - -
    - -

    Identify the contents of a file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • remap – A dict of 'part' to list-of-compiled-regexp objects, where each item is -a specification to match against its part. The 'part' can be one of 'mime', -'filename' or 'content'.

    • -
    • converter – A

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A boolean, true if the file is not rejected by the constraints.

    • -
    -
    -
    - Source code in beancount/ingest/importers/mixins/identifier.py -
    def identify(remap, converter, file):
    -    """Identify the contents of a file.
    -
    -    Args:
    -      remap: A dict of 'part' to list-of-compiled-regexp objects, where each item is
    -        a specification to match against its part. The 'part' can be one of 'mime',
    -        'filename' or 'content'.
    -      converter: A
    -    Returns:
    -      A boolean, true if the file is not rejected by the constraints.
    -    """
    -    if remap.get('mime', None):
    -        mimetype = file.convert(cache.mimetype)
    -        if not all(regexp.search(mimetype)
    -                   for regexp in remap['mime']):
    -            return False
    -
    -    if remap.get('filename', None):
    -        if not all(regexp.search(file.name)
    -                   for regexp in remap['filename']):
    -            return False
    -
    -    if remap.get('content', None):
    -        # If this is a text file, read the whole thing in memory.
    -        text = file.convert(converter or cache.contents)
    -        if not all(regexp.search(text)
    -                   for regexp in remap['content']):
    -            return False
    -
    -    return True
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.importers.ofx - - - -

    - -
    - -

    OFX file format importer for bank and credit card statements.

    -

    https://en.wikipedia.org/wiki/Open_Financial_Exchange

    -

    This importer will parse a single account in the OFX file. Instantiate it -multiple times with different accounts if it has many accounts. It makes more -sense to do it this way so that you can define your importer configuration -account by account.

    -

    Note that this importer is provided as an example and with no guarantees. It's -not really super great. On the other hand, I've been using it for more than five -years over multiple accounts, so it has been useful to me (it works, by some -measure of "works"). If you need a more powerful or compliant OFX importer -please consider either writing one or contributing changes. Also, this importer -does its own very basic parsing; a better one would probably use (and depend on) -the ofxparse module (see https://sites.google.com/site/ofxparse/).

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.ingest.importers.ofx.BalanceType (Enum) - - - - -

    - -
    - -

    Type of Balance directive to be inserted.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.ingest.importers.ofx.Importer (ImporterProtocol) - - - - -

    - -
    - -

    An importer for Open Financial Exchange files.

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.ingest.importers.ofx.Importer.__init__(self, acctid_regexp, account, basename=None, balance_type=<BalanceType.DECLARED: 1>) - - - special - - -
    - -
    - -

    Create a new importer posting to the given account.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • account – An account string, the account onto which to post all the -amounts parsed.

    • -
    • acctid_regexp – A regexp, to match against the <ACCTID> tag of the OFX file.

    • -
    • basename – An optional string, the name of the new files.

    • -
    • balance_type – An enum of type BalanceType.

    • -
    -
    -
    - Source code in beancount/ingest/importers/ofx.py -
    def __init__(self, acctid_regexp, account, basename=None,
    -             balance_type=BalanceType.DECLARED):
    -    """Create a new importer posting to the given account.
    -
    -    Args:
    -      account: An account string, the account onto which to post all the
    -        amounts parsed.
    -      acctid_regexp: A regexp, to match against the <ACCTID> tag of the OFX file.
    -      basename: An optional string, the name of the new files.
    -      balance_type: An enum of type BalanceType.
    -    """
    -    self.acctid_regexp = acctid_regexp
    -    self.account = account
    -    self.basename = basename
    -    self.balance_type = balance_type
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.ofx.Importer.extract(self, file, existing_entries=None) - - -
    - -
    - -

    Extract a list of partially complete transactions from the file.

    - -
    - Source code in beancount/ingest/importers/ofx.py -
    def extract(self, file, existing_entries=None):
    -    """Extract a list of partially complete transactions from the file."""
    -    soup = bs4.BeautifulSoup(file.contents(), 'lxml')
    -    return extract(soup, file.name, self.acctid_regexp, self.account, self.FLAG,
    -                   self.balance_type)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.ofx.Importer.file_account(self, _) - - -
    - -
    - -

    Return the account against which we post transactions.

    - -
    - Source code in beancount/ingest/importers/ofx.py -
    def file_account(self, _):
    -    """Return the account against which we post transactions."""
    -    return self.account
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.ofx.Importer.file_date(self, file) - - -
    - -
    - -

    Return the optional renamed account filename.

    - -
    - Source code in beancount/ingest/importers/ofx.py -
    def file_date(self, file):
    -    """Return the optional renamed account filename."""
    -    return find_max_date(file.contents())
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.ofx.Importer.file_name(self, file) - - -
    - -
    - -

    Return the optional renamed account filename.

    - -
    - Source code in beancount/ingest/importers/ofx.py -
    def file_name(self, file):
    -    """Return the optional renamed account filename."""
    -    if self.basename:
    -        return self.basename + path.splitext(file.name)[1]
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.ofx.Importer.identify(self, file) - - -
    - -
    - -

    Return true if this importer matches the given file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • file – A cache.FileMemo instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A boolean, true if this importer can handle this file.

    • -
    -
    -
    - Source code in beancount/ingest/importers/ofx.py -
    def identify(self, file):
    -    # Match for a compatible MIME type.
    -    if file.mimetype() not in {'application/x-ofx',
    -                               'application/vnd.intu.qbo',
    -                               'application/vnd.intu.qfx'}:
    -        return False
    -
    -    # Match the account id.
    -    return any(re.match(self.acctid_regexp, acctid)
    -               for acctid in find_acctids(file.contents()))
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.ingest.importers.ofx.Importer.name(self) - - -
    - -
    - -

    Include the filing account in the name.

    - -
    - Source code in beancount/ingest/importers/ofx.py -
    def name(self):
    -    """Include the filing account in the name."""
    -    return '{}: "{}"'.format(super().name(), self.file_account(None))
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.ingest.importers.ofx.build_transaction(stmttrn, flag, account, currency) - - -

    - -
    - -

    Build a single transaction.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • stmttrn – A <STMTTRN> bs4.element.Tag.

    • -
    • flag – A single-character string.

    • -
    • account – An account string, the account to insert.

    • -
    • currency – A currency string.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A Transaction instance.

    • -
    -
    -
    - Source code in beancount/ingest/importers/ofx.py -
    def build_transaction(stmttrn, flag, account, currency):
    -    """Build a single transaction.
    -
    -    Args:
    -      stmttrn: A <STMTTRN> bs4.element.Tag.
    -      flag: A single-character string.
    -      account: An account string, the account to insert.
    -      currency: A currency string.
    -    Returns:
    -      A Transaction instance.
    -    """
    -    # Find the date.
    -    date = parse_ofx_time(find_child(stmttrn, 'dtposted')).date()
    -
    -    # There's no distinct payee.
    -    payee = None
    -
    -    # Construct a description that represents all the text content in the node.
    -    name = find_child(stmttrn, 'name', saxutils.unescape)
    -    memo = find_child(stmttrn, 'memo', saxutils.unescape)
    -
    -    # Remove memos duplicated from the name.
    -    if memo == name:
    -        memo = None
    -
    -    # Add the transaction type to the description, unless it's not useful.
    -    trntype = find_child(stmttrn, 'trntype', saxutils.unescape)
    -    if trntype in ('DEBIT', 'CREDIT'):
    -        trntype = None
    -
    -    narration = ' / '.join(filter(None, [name, memo, trntype]))
    -
    -    # Create a single posting for it; the user will have to manually categorize
    -    # the other side.
    -    number = find_child(stmttrn, 'trnamt', D)
    -    units = amount.Amount(number, currency)
    -    posting = data.Posting(account, units, None, None, None, None)
    -
    -    # Build the transaction with a single leg.
    -    fileloc = data.new_metadata('<build_transaction>', 0)
    -    return data.Transaction(fileloc, date, flag, payee, narration,
    -                            data.EMPTY_SET, data.EMPTY_SET, [posting])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importers.ofx.extract(soup, filename, acctid_regexp, account, flag, balance_type) - - -

    - -
    - -

    Extract transactions from an OFX file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • soup – A BeautifulSoup root node.

    • -
    • acctid_regexp – A regular expression string matching the account we're interested in.

    • -
    • account – An account string onto which to post the amounts found in the file.

    • -
    • flag – A single-character string.

    • -
    • balance_type – An enum of type BalanceType.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A sorted list of entries.

    • -
    -
    -
    - Source code in beancount/ingest/importers/ofx.py -
    def extract(soup, filename, acctid_regexp, account, flag, balance_type):
    -    """Extract transactions from an OFX file.
    -
    -    Args:
    -      soup: A BeautifulSoup root node.
    -      acctid_regexp: A regular expression string matching the account we're interested in.
    -      account: An account string onto which to post the amounts found in the file.
    -      flag: A single-character string.
    -      balance_type: An enum of type BalanceType.
    -    Returns:
    -      A sorted list of entries.
    -    """
    -    new_entries = []
    -    counter = itertools.count()
    -    for acctid, currency, transactions, balance in find_statement_transactions(soup):
    -        if not re.match(acctid_regexp, acctid):
    -            continue
    -
    -        # Create Transaction directives.
    -        stmt_entries = []
    -        for stmttrn in transactions:
    -            entry = build_transaction(stmttrn, flag, account, currency)
    -            entry = entry._replace(meta=data.new_metadata(filename, next(counter)))
    -            stmt_entries.append(entry)
    -        stmt_entries = data.sorted(stmt_entries)
    -        new_entries.extend(stmt_entries)
    -
    -        # Create a Balance directive.
    -        if balance and balance_type is not BalanceType.NONE:
    -            date, number = balance
    -            if balance_type is BalanceType.LAST and stmt_entries:
    -                date = stmt_entries[-1].date
    -
    -            # The Balance assertion occurs at the beginning of the date, so move
    -            # it to the following day.
    -            date += datetime.timedelta(days=1)
    -
    -            meta = data.new_metadata(filename, next(counter))
    -            balance_entry = data.Balance(meta, date, account,
    -                                         amount.Amount(number, currency),
    -                                         None, None)
    -            new_entries.append(balance_entry)
    -
    -    return data.sorted(new_entries)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importers.ofx.find_acctids(contents) - - -

    - -
    - -

    Find the list of <ACCTID> tags.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • contents – A string, the contents of the OFX file.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of strings, the contents of the <ACCTID> tags.

    • -
    -
    -
    - Source code in beancount/ingest/importers/ofx.py -
    def find_acctids(contents):
    -    """Find the list of <ACCTID> tags.
    -
    -    Args:
    -      contents: A string, the contents of the OFX file.
    -    Returns:
    -      A list of strings, the contents of the <ACCTID> tags.
    -    """
    -    # Match the account id. Don't bother parsing the entire thing as XML, just
    -    # match the tag for this purpose. This'll work fine enough.
    -    for match in re.finditer('<ACCTID>([^<]*)', contents):
    -        yield match.group(1)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importers.ofx.find_child(node, name, conversion=None) - - -

    - -
    - -

    Find a child under the given node and return its value.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • node – A <STMTTRN> bs4.element.Tag.

    • -
    • name – A string, the name of the child node.

    • -
    • conversion – A callable object used to convert the value to a new data type.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, or None.

    • -
    -
    -
    - Source code in beancount/ingest/importers/ofx.py -
    def find_child(node, name, conversion=None):
    -    """Find a child under the given node and return its value.
    -
    -    Args:
    -      node: A <STMTTRN> bs4.element.Tag.
    -      name: A string, the name of the child node.
    -      conversion: A callable object used to convert the value to a new data type.
    -    Returns:
    -      A string, or None.
    -    """
    -    child = node.find(name)
    -    if not child:
    -        return None
    -    value = child.contents[0].strip()
    -    if conversion:
    -        value = conversion(value)
    -    return value
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importers.ofx.find_currency(soup) - - -

    - -
    - -

    Find the first currency in the XML tree.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • soup – A BeautifulSoup root node.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, the first currency found in the file. Returns None if no currency -is found.

    • -
    -
    -
    - Source code in beancount/ingest/importers/ofx.py -
    def find_currency(soup):
    -    """Find the first currency in the XML tree.
    -
    -    Args:
    -      soup: A BeautifulSoup root node.
    -    Returns:
    -      A string, the first currency found in the file. Returns None if no currency
    -      is found.
    -    """
    -    for stmtrs in soup.find_all(re.compile('.*stmtrs$')):
    -        for currency_node in stmtrs.find_all('curdef'):
    -            currency = currency_node.contents[0]
    -            if currency is not None:
    -                return currency
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importers.ofx.find_max_date(contents) - - -

    - -
    - -

    Extract the report date from the file.

    - -
    - Source code in beancount/ingest/importers/ofx.py -
    def find_max_date(contents):
    -    """Extract the report date from the file."""
    -    soup = bs4.BeautifulSoup(contents, 'lxml')
    -    dates = []
    -    for ledgerbal in soup.find_all('ledgerbal'):
    -        dtasof = ledgerbal.find('dtasof')
    -        dates.append(parse_ofx_time(dtasof.contents[0]).date())
    -    if dates:
    -        return max(dates)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importers.ofx.find_statement_transactions(soup) - - -

    - -
    - -

    Find the statement transaction sections in the file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • soup – A BeautifulSoup root node.

    • -
    -

    Yields: - A trip of - An account id string, - A currency string, - A list of transaction nodes (<STMTTRN> BeautifulSoup tags), and - A (date, balance amount) for the <LEDGERBAL>.

    - -
    - Source code in beancount/ingest/importers/ofx.py -
    def find_statement_transactions(soup):
    -    """Find the statement transaction sections in the file.
    -
    -    Args:
    -      soup: A BeautifulSoup root node.
    -    Yields:
    -      A trip of
    -        An account id string,
    -        A currency string,
    -        A list of transaction nodes (<STMTTRN> BeautifulSoup tags), and
    -        A (date, balance amount) for the <LEDGERBAL>.
    -    """
    -    # Process STMTTRNRS and CCSTMTTRNRS tags.
    -    for stmtrs in soup.find_all(re.compile('.*stmtrs$')):
    -        # For each CURDEF tag.
    -        for currency_node in stmtrs.find_all('curdef'):
    -            currency = currency_node.contents[0].strip()
    -
    -            # Extract ACCTID account information.
    -            acctid_node = stmtrs.find('acctid')
    -            if acctid_node:
    -                acctid = next(acctid_node.children).strip()
    -            else:
    -                acctid = ''
    -
    -            # Get the LEDGERBAL node. There appears to be a single one for all
    -            # transaction lists.
    -            ledgerbal = stmtrs.find('ledgerbal')
    -            balance = None
    -            if ledgerbal:
    -                dtasof = find_child(ledgerbal, 'dtasof', parse_ofx_time).date()
    -                balamt = find_child(ledgerbal, 'balamt', D)
    -                balance = (dtasof, balamt)
    -
    -            # Process transaction lists (regular or credit-card).
    -            for tranlist in stmtrs.find_all(re.compile('(|bank|cc)tranlist')):
    -                yield acctid, currency, tranlist.find_all('stmttrn'), balance
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.importers.ofx.parse_ofx_time(date_str) - - -

    - -
    - -

    Parse an OFX time string and return a datetime object.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • date_str – A string, the date to be parsed.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A datetime.datetime instance.

    • -
    -
    -
    - Source code in beancount/ingest/importers/ofx.py -
    def parse_ofx_time(date_str):
    -    """Parse an OFX time string and return a datetime object.
    -
    -    Args:
    -      date_str: A string, the date to be parsed.
    -    Returns:
    -      A datetime.datetime instance.
    -    """
    -    if len(date_str) < 14:
    -        return datetime.datetime.strptime(date_str[:8], '%Y%m%d')
    -    else:
    -        return datetime.datetime.strptime(date_str[:14], '%Y%m%d%H%M%S')
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.regression - - - -

    - -
    - -

    Support for implementing regression tests on sample files using nose.

    -

    NOTE: This itself is not a regression test. It's a library used to create -regression tests for your importers. Use it like this in your own importer code:

    -

    def test(): - importer = Importer([], { - 'FILE' : 'Assets:US:MyBank:Main', - }) - yield from regression.compare_sample_files(importer, file)

    -

    WARNING: This is deprecated. Nose itself has been deprecated for a while and -Beancount is now using only pytest. Ignore this and use -beancount.ingest.regression_ptest instead.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.ingest.regression.ImportFileTestCase (TestCase) - - - - -

    - -
    - -

    Base class for importer tests that compare output to an expected output -text.

    - - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.ingest.regression.ImportFileTestCase.test_expect_extract(self, filename, msg) - - -

    - -
    - -

    Extract entries from a test file and compare against expected output.

    -

    If an expected file (as <filename>.extract) is not present, we issue a -warning. Missing expected files can be written out by removing them -before running the tests.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the name of the file to import using self.importer.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • AssertionError – If the contents differ from the expected file.

    • -
    -
    -
    - Source code in beancount/ingest/regression.py -
    @test_utils.skipIfRaises(ToolNotInstalled)
    -def test_expect_extract(self, filename, msg):
    -    """Extract entries from a test file and compare against expected output.
    -
    -    If an expected file (as <filename>.extract) is not present, we issue a
    -    warning. Missing expected files can be written out by removing them
    -    before running the tests.
    -
    -    Args:
    -      filename: A string, the name of the file to import using self.importer.
    -    Raises:
    -      AssertionError: If the contents differ from the expected file.
    -
    -    """
    -    # Import the file.
    -    entries = extract.extract_from_file(filename, self.importer, None, None)
    -
    -    # Render the entries to a string.
    -    oss = io.StringIO()
    -    printer.print_entries(entries, file=oss)
    -    string = oss.getvalue()
    -
    -    expect_filename = '{}.extract'.format(filename)
    -    if path.exists(expect_filename):
    -        expect_string = open(expect_filename, encoding='utf-8').read()
    -        self.assertEqual(expect_string.strip(), string.strip())
    -    else:
    -        # Write out the expected file for review.
    -        open(expect_filename, 'w', encoding='utf-8').write(string)
    -        self.skipTest("Expected file not present; generating '{}'".format(
    -            expect_filename))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.regression.ImportFileTestCase.test_expect_file_date(self, filename, msg) - - -

    - -
    - -

    Compute the imported file date and compare to an expected output.

    -

    If an expected file (as <filename>.file_date) is not present, we issue a -warning. Missing expected files can be written out by removing them -before running the tests.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the name of the file to import using self.importer.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • AssertionError – If the contents differ from the expected file.

    • -
    -
    -
    - Source code in beancount/ingest/regression.py -
    @test_utils.skipIfRaises(ToolNotInstalled)
    -def test_expect_file_date(self, filename, msg):
    -    """Compute the imported file date and compare to an expected output.
    -
    -    If an expected file (as <filename>.file_date) is not present, we issue a
    -    warning. Missing expected files can be written out by removing them
    -    before running the tests.
    -
    -    Args:
    -      filename: A string, the name of the file to import using self.importer.
    -    Raises:
    -      AssertionError: If the contents differ from the expected file.
    -    """
    -    # Import the date.
    -    file = cache.get_file(filename)
    -    date = self.importer.file_date(file)
    -    if date is None:
    -        self.fail("No date produced from {}".format(file.name))
    -
    -    expect_filename = '{}.file_date'.format(file.name)
    -    if path.exists(expect_filename) and path.getsize(expect_filename) > 0:
    -        expect_date_str = open(expect_filename, encoding='utf-8').read().strip()
    -        expect_date = datetime.datetime.strptime(expect_date_str, '%Y-%m-%d').date()
    -        self.assertEqual(expect_date, date)
    -    else:
    -        # Write out the expected file for review.
    -        with open(expect_filename, 'w', encoding='utf-8') as outfile:
    -            print(date.strftime('%Y-%m-%d'), file=outfile)
    -        self.skipTest("Expected file not present; generating '{}'".format(
    -            expect_filename))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.regression.ImportFileTestCase.test_expect_file_name(self, filename, msg) - - -

    - -
    - -

    Compute the imported file name and compare to an expected output.

    -

    If an expected file (as <filename>.file_name) is not present, we issue a -warning. Missing expected files can be written out by removing them -before running the tests.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the name of the file to import using self.importer.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • AssertionError – If the contents differ from the expected file.

    • -
    -
    -
    - Source code in beancount/ingest/regression.py -
    @test_utils.skipIfRaises(ToolNotInstalled)
    -def test_expect_file_name(self, filename, msg):
    -    """Compute the imported file name and compare to an expected output.
    -
    -    If an expected file (as <filename>.file_name) is not present, we issue a
    -    warning. Missing expected files can be written out by removing them
    -    before running the tests.
    -
    -    Args:
    -      filename: A string, the name of the file to import using self.importer.
    -    Raises:
    -      AssertionError: If the contents differ from the expected file.
    -    """
    -    # Import the date.
    -    file = cache.get_file(filename)
    -    generated_basename = self.importer.file_name(file)
    -    if generated_basename is None:
    -        self.fail("No filename produced from {}".format(filename))
    -
    -    # Check that we're getting a non-null relative simple filename.
    -    self.assertFalse(path.isabs(generated_basename), generated_basename)
    -    self.assertNotRegex(generated_basename, os.sep)
    -
    -    expect_filename = '{}.file_name'.format(file.name)
    -    if path.exists(expect_filename) and path.getsize(expect_filename) > 0:
    -        expect_filename = open(expect_filename, encoding='utf-8').read().strip()
    -        self.assertEqual(expect_filename, generated_basename)
    -    else:
    -        # Write out the expected file for review.
    -        with open(expect_filename, 'w', encoding='utf-8') as file:
    -            print(generated_basename, file=file)
    -        self.skipTest("Expected file not present; generating '{}'".format(
    -            expect_filename))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.regression.ImportFileTestCase.test_expect_identify(self, filename, msg) - - -

    - -
    - -

    Attempt to identify a file and expect results to be true.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the name of the file to import using self.importer.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • AssertionError – If the contents differ from the expected file.

    • -
    -
    -
    - Source code in beancount/ingest/regression.py -
    @test_utils.skipIfRaises(ToolNotInstalled)
    -def test_expect_identify(self, filename, msg):
    -    """Attempt to identify a file and expect results to be true.
    -
    -    Args:
    -      filename: A string, the name of the file to import using self.importer.
    -    Raises:
    -      AssertionError: If the contents differ from the expected file.
    -    """
    -    file = cache.get_file(filename)
    -    matched = self.importer.identify(file)
    -    self.assertTrue(matched)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.ingest.regression.ToolNotInstalled (OSError) - - - - -

    - -
    - -

    An error to be used by converters when necessary software isn't there.

    -

    Raising this exception from your converter code when the tool is not -installed will make the tests defined in this file skipped instead of -failing. This will happen when you test your converters on different -computers and/or platforms.

    - - - -
    - -
    - - - - -
    - - - -

    -beancount.ingest.regression.compare_sample_files(importer, directory=None, ignore_cls=None) - - -

    - -
    - -

    Compare the sample files under a directory.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • importer – An instance of an Importer.

    • -
    • directory – A string, the directory to scour for sample files or a filename - in that directory. If a directory is not provided, the directory of - the file from which the importer class is defined is used.

    • -
    • ignore_cls – An optional base class of the importer whose methods should -not trigger the addition of a test. For example, if you are deriving -from a base class which is already well-tested, you may not want to have -a regression test case generated for those methods. This was used to -ignore methods provided from a common backwards compatibility support -class.

    • -
    -

    Yields: - Generated tests as per nose's requirements (a callable and arguments for - it).

    - -
    - Source code in beancount/ingest/regression.py -
    @deprecated("Use beancount.ingest.regression_pytest instead")
    -def compare_sample_files(importer, directory=None, ignore_cls=None):
    -    """Compare the sample files under a directory.
    -
    -    Args:
    -      importer: An instance of an Importer.
    -      directory: A string, the directory to scour for sample files or a filename
    -          in that directory. If a directory is not provided, the directory of
    -          the file from which the importer class is defined is used.
    -      ignore_cls: An optional base class of the importer whose methods should
    -        not trigger the addition of a test. For example, if you are deriving
    -        from a base class which is already well-tested, you may not want to have
    -        a regression test case generated for those methods. This was used to
    -        ignore methods provided from a common backwards compatibility support
    -        class.
    -    Yields:
    -      Generated tests as per nose's requirements (a callable and arguments for
    -      it).
    -    """
    -    # If the directory is not specified, use the directory where the importer
    -    # class was defined.
    -    if not directory:
    -        directory = sys.modules[type(importer).__module__].__file__
    -    if path.isfile(directory):
    -        directory = path.dirname(directory)
    -
    -    for filename in find_input_files(directory):
    -        # For each of the methods to be tested, check if there is an actual
    -        # implementation and if so, run a comparison with an expected file.
    -        for name in ['identify',
    -                     'extract',
    -                     'file_date',
    -                     'file_name']:
    -            # Check if the method has been overridden from the protocol
    -            # interface. If so, even if it's provided by concretely inherited
    -            # method, we want to require a test against that method.
    -            func = getattr(importer, name).__func__
    -            if (func is not getattr(ImporterProtocol, name) and
    -                (ignore_cls is None or (func is not getattr(ignore_cls, name, None)))):
    -                method = getattr(ImportFileTestCase(importer),
    -                                 'test_expect_{}'.format(name))
    -                yield (method, filename, name)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.regression.find_input_files(directory) - - -

    - -
    - -

    Find the input files in the module where the class is defined.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • directory – A string, the path to a root directory to check for.

    • -
    -

    Yields: - Strings, the absolute filenames of sample input and expected files.

    - -
    - Source code in beancount/ingest/regression.py -
    def find_input_files(directory):
    -    """Find the input files in the module where the class is defined.
    -
    -    Args:
    -      directory: A string, the path to a root directory to check for.
    -    Yields:
    -      Strings, the absolute filenames of sample input and expected files.
    -    """
    -    for sroot, dirs, files in os.walk(directory):
    -        for filename in files:
    -            if re.match(r'.*\.(extract|file_date|file_name|py|pyc|DS_Store)$', filename):
    -                continue
    -            yield path.join(sroot, filename)
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.regression_pytest - - - -

    - -
    - -

    Support for implementing regression tests on sample files using pytest.

    -

    This module provides definitions for testing a custom importer against a set of -existing downloaded files, running the various importer interface methods on it, -and comparing the output to an expected text file. (Expected test files can be -auto-generated using the --generate option). You use it like this:

    -

    from beancount.ingest import regression_pytest - ... - import mymodule - ...

    -

    # Create your importer instance used for testing. - importer = mymodule.Importer(...)

    -

    # Select a directory where your test files are to be located. - directory = ...

    -

    # Create a test case using the base in this class.

    -

    @regression_pytest.with_importer(importer) - @regression_pytest.with_testdir(directory) - class TestImporter(regtest.ImporterTestBase): - pass

    -

    Also, to add the --generate option to 'pytest', you must create a conftest.py -somewhere in one of the roots above your importers with this module as a plugin:

    -

    pytest_plugins = "beancount.ingest.regression_pytest"

    -

    See beancount/example/ingest for a full working example.

    -

    How to invoke the tests:

    -

    Via pytest. First run your test with the --generate option to generate all the -expected files. Then inspect them visually for correctness. Finally, check them -in to preserve them. You should be able to regress against those correct outputs -in the future. Use version control to your advantage to visualize the -differences.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.ingest.regression_pytest.ImporterTestBase - - - -

    - -
    - - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.ingest.regression_pytest.ImporterTestBase.test_extract(self, importer, file, pytestconfig) - - -

    - -
    - -

    Extract entries from a test file and compare against expected output.

    - -
    - Source code in beancount/ingest/regression_pytest.py -
    def test_extract(self, importer, file, pytestconfig):
    -    """Extract entries from a test file and compare against expected output."""
    -    entries = extract.extract_from_file(file.name, importer, None, None)
    -    oss = io.StringIO()
    -    printer.print_entries(entries, file=oss)
    -    string = oss.getvalue()
    -    compare_contents_or_generate(string, '{}.extract'.format(file.name),
    -                                 pytestconfig.getoption("generate", False))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.regression_pytest.ImporterTestBase.test_file_account(self, importer, file, pytestconfig) - - -

    - -
    - -

    Compute the selected filing account and compare to an expected output.

    - -
    - Source code in beancount/ingest/regression_pytest.py -
    def test_file_account(self, importer, file, pytestconfig):
    -    """Compute the selected filing account and compare to an expected output."""
    -    account = importer.file_account(file) or ''
    -    compare_contents_or_generate(account, '{}.file_account'.format(file.name),
    -                                 pytestconfig.getoption("generate", False))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.regression_pytest.ImporterTestBase.test_file_date(self, importer, file, pytestconfig) - - -

    - -
    - -

    Compute the imported file date and compare to an expected output.

    - -
    - Source code in beancount/ingest/regression_pytest.py -
    def test_file_date(self, importer, file, pytestconfig):
    -    """Compute the imported file date and compare to an expected output."""
    -    date = importer.file_date(file)
    -    string = date.isoformat() if date else ''
    -    compare_contents_or_generate(string, '{}.file_date'.format(file.name),
    -                                 pytestconfig.getoption("generate", False))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.regression_pytest.ImporterTestBase.test_file_name(self, importer, file, pytestconfig) - - -

    - -
    - -

    Compute the imported file name and compare to an expected output.

    - -
    - Source code in beancount/ingest/regression_pytest.py -
    def test_file_name(self, importer, file, pytestconfig):
    -    """Compute the imported file name and compare to an expected output."""
    -    filename = importer.file_name(file) or ''
    -    compare_contents_or_generate(filename, '{}.file_name'.format(file.name),
    -                                 pytestconfig.getoption("generate", False))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.regression_pytest.ImporterTestBase.test_identify(self, importer, file) - - -

    - -
    - -

    Attempt to identify a file and expect results to be true.

    -

    This method does not need to check against an existing expect file. It -is just assumed it should return True if your test is setup well (the -importer should always identify the test file).

    - -
    - Source code in beancount/ingest/regression_pytest.py -
    def test_identify(self, importer, file):
    -    """Attempt to identify a file and expect results to be true.
    -
    -    This method does not need to check against an existing expect file. It
    -    is just assumed it should return True if your test is setup well (the
    -    importer should always identify the test file).
    -    """
    -    assert importer.identify(file)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.ingest.regression_pytest.compare_contents_or_generate(actual_string, expect_fn, generate) - - -

    - -
    - -

    Compare a string to the contents of an expect file.

    -

    Assert if different; auto-generate otherwise.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • actual_string – The expected string contents.

    • -
    • expect_fn – The filename whose contents to read and compare against.

    • -
    • generate – A boolean, true if we are to generate the tests.

    • -
    -
    -
    - Source code in beancount/ingest/regression_pytest.py -
    def compare_contents_or_generate(actual_string, expect_fn, generate):
    -    """Compare a string to the contents of an expect file.
    -
    -    Assert if different; auto-generate otherwise.
    -
    -    Args:
    -      actual_string: The expected string contents.
    -      expect_fn: The filename whose contents to read and compare against.
    -      generate: A boolean, true if we are to generate the tests.
    -    """
    -    if generate:
    -        with open(expect_fn, 'w', encoding='utf-8') as expect_file:
    -            expect_file.write(actual_string)
    -            if actual_string and not actual_string.endswith('\n'):
    -                expect_file.write('\n')
    -        pytest.skip("Generated '{}'".format(expect_fn))
    -    else:
    -        # Run the test on an existing expected file.
    -        assert path.exists(expect_fn), (
    -            "Expected file '{}' is missing. Generate it?".format(expect_fn))
    -        with open(expect_fn, encoding='utf-8') as infile:
    -            expect_string = infile.read()
    -        assert expect_string.strip() == actual_string.strip()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.regression_pytest.find_input_files(directory) - - -

    - -
    - -

    Find the input files in the module where the class is defined.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • directory – A string, the path to a root directory to check for.

    • -
    -

    Yields: - Strings, the absolute filenames of sample input and expected files.

    - -
    - Source code in beancount/ingest/regression_pytest.py -
    def find_input_files(directory):
    -    """Find the input files in the module where the class is defined.
    -
    -    Args:
    -      directory: A string, the path to a root directory to check for.
    -    Yields:
    -      Strings, the absolute filenames of sample input and expected files.
    -    """
    -    for sroot, dirs, files in os.walk(directory):
    -        for filename in files:
    -            if re.match(r'.*\.(extract|file_date|file_name|file_account|py|pyc|DS_Store)$',
    -                        filename):
    -                continue
    -            yield path.join(sroot, filename)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.regression_pytest.pytest_addoption(parser) - - -

    - -
    - -

    Add an option to generate the expected files for the tests.

    - -
    - Source code in beancount/ingest/regression_pytest.py -
    def pytest_addoption(parser):
    -    """Add an option to generate the expected files for the tests."""
    -    group = parser.getgroup("beancount")
    -    group.addoption("--generate", "--gen", action="store_true",
    -                    help="Don't test; rather, generate the expected files")
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.regression_pytest.with_importer(importer) - - -

    - -
    - -

    Parametrizing fixture that provides the importer to test.

    - -
    - Source code in beancount/ingest/regression_pytest.py -
    def with_importer(importer):
    -    """Parametrizing fixture that provides the importer to test."""
    -    return pytest.mark.parametrize("importer", [importer])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.regression_pytest.with_testdir(directory) - - -

    - -
    - -

    Parametrizing fixture that provides files from a directory.

    - -
    - Source code in beancount/ingest/regression_pytest.py -
    def with_testdir(directory):
    -    """Parametrizing fixture that provides files from a directory."""
    -    return pytest.mark.parametrize(
    -        "file", [cache.get_file(fn) for fn in find_input_files(directory)])
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.scripts_utils - - - -

    - -
    - -

    Common front-end to all ingestion tools.

    - - - -
    - - - - - - - - - - - - - - - -
    - - - -

    - -beancount.ingest.scripts_utils.TestScriptsBase (TestTempdirMixin, TestCase) - - - - -

    - -
    - - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.ingest.scripts_utils.TestScriptsBase.setUp(self) - - -

    - -
    - -

    Hook method for setting up the test fixture before exercising it.

    - -
    - Source code in beancount/ingest/scripts_utils.py -
    def setUp(self):
    -    super().setUp()
    -    for filename, contents in self.FILES.items():
    -        absname = path.join(self.tempdir, filename)
    -        os.makedirs(path.dirname(absname), exist_ok=True)
    -        with open(absname, 'w') as file:
    -            file.write(contents)
    -        if filename.endswith('.py') or filename.endswith('.sh'):
    -            os.chmod(absname, stat.S_IRUSR|stat.S_IXUSR)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.ingest.scripts_utils.create_legacy_arguments_parser(description, run_func) - - -

    - -
    - -

    Create an arguments parser for all the ingestion bean-tools.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • description (str) – The program description string.

    • -
    • func – A callable function to run the particular command.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An argparse.Namespace instance with the rest of arguments in 'rest'.

    • -
    -
    -
    - Source code in beancount/ingest/scripts_utils.py -
    def create_legacy_arguments_parser(description: str, run_func: callable):
    -    """Create an arguments parser for all the ingestion bean-tools.
    -
    -    Args:
    -      description: The program description string.
    -      func: A callable function to run the particular command.
    -    Returns:
    -      An argparse.Namespace instance with the rest of arguments in 'rest'.
    -    """
    -    parser = version.ArgumentParser(description=description)
    -
    -    parser.add_argument('config', action='store', metavar='CONFIG_FILENAME',
    -                        help=('Importer configuration file. '
    -                              'This is a Python file with a data structure that '
    -                              'is specific to your accounts'))
    -
    -    parser.add_argument('downloads', nargs='+', metavar='DIR-OR-FILE',
    -                        default=[],
    -                        help='Filenames or directories to search for files to import')
    -
    -    parser.set_defaults(command=run_func)
    -
    -    return parser
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.scripts_utils.ingest(importers_list, detect_duplicates_func=None, hooks=None) - - -

    - -
    - -

    Driver function that calls all the ingestion tools.

    -

    Put a call to this function at the end of your importer configuration to -make your import script; this should be its main function, like this:

    -

    from beancount.ingest.scripts_utils import ingest - my_importers = [ ... ] - ingest(my_importers)

    -

    This more explicit way of invoking the ingestion is now the preferred way to -invoke the various tools, and replaces calling the bean-identify, -bean-extract, bean-file tools with a --config argument. When you call the -import script itself (as as program) it will parse the arguments, expecting -a subcommand ('identify', 'extract' or 'file') and corresponding -subcommand-specific arguments.

    -

    Here you can override some importer values, such as installing a custom -duplicate finding hook, and eventually more. Note that this newer invocation -method is optional and if it is not present, a call to ingest() is generated -implicitly, and it functions as it used to. Future configurable -customization of the ingestion process will be implemented by inserting new -arguments to this function, this is the motivation behind doing this.

    -

    Note that invocation by the three bean-* ingestion tools is still supported, -and calling ingest() explicitly from your import configuration file will not -break these tools either, if you invoke them on it; the values you provide -to this function will be used by those tools.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • importers_list – A list of importer instances. This is used as a -chain-of-responsibility, called on each file.

    • -
    • detect_duplicates_func – (DEPRECATED) An optional function which accepts a -list of lists of imported entries and a list of entries already existing -in the user's ledger. See function find_duplicate_entries(), which is -the default implementation for this. Use 'filter_funcs' instead.

    • -
    • hooks – An optional list of hook functions to apply to the list of extract -(filename, entries) pairs, in order. This replaces -'detect_duplicates_func'.

    • -
    -
    -
    - Source code in beancount/ingest/scripts_utils.py -
    def ingest(importers_list, detect_duplicates_func=None, hooks=None):
    -    """Driver function that calls all the ingestion tools.
    -
    -    Put a call to this function at the end of your importer configuration to
    -    make your import script; this should be its main function, like this:
    -
    -      from beancount.ingest.scripts_utils import ingest
    -      my_importers = [ ... ]
    -      ingest(my_importers)
    -
    -    This more explicit way of invoking the ingestion is now the preferred way to
    -    invoke the various tools, and replaces calling the bean-identify,
    -    bean-extract, bean-file tools with a --config argument. When you call the
    -    import script itself (as as program) it will parse the arguments, expecting
    -    a subcommand ('identify', 'extract' or 'file') and corresponding
    -    subcommand-specific arguments.
    -
    -    Here you can override some importer values, such as installing a custom
    -    duplicate finding hook, and eventually more. Note that this newer invocation
    -    method is optional and if it is not present, a call to ingest() is generated
    -    implicitly, and it functions as it used to. Future configurable
    -    customization of the ingestion process will be implemented by inserting new
    -    arguments to this function, this is the motivation behind doing this.
    -
    -    Note that invocation by the three bean-* ingestion tools is still supported,
    -    and calling ingest() explicitly from your import configuration file will not
    -    break these tools either, if you invoke them on it; the values you provide
    -    to this function will be used by those tools.
    -
    -    Args:
    -      importers_list: A list of importer instances. This is used as a
    -        chain-of-responsibility, called on each file.
    -      detect_duplicates_func: (DEPRECATED) An optional function which accepts a
    -        list of lists of imported entries and a list of entries already existing
    -        in the user's ledger. See function find_duplicate_entries(), which is
    -        the default implementation for this. Use 'filter_funcs' instead.
    -      hooks: An optional list of hook functions to apply to the list of extract
    -        (filename, entries) pairs, in order. This replaces
    -        'detect_duplicates_func'.
    -    """
    -    if detect_duplicates_func is not None:
    -        warnings.warn("Argument 'detect_duplicates_func' is deprecated.")
    -        # Fold it in hooks.
    -        if hooks is None:
    -            hooks = []
    -        hooks.insert(0, detect_duplicates_func)
    -        del detect_duplicates_func
    -
    -    if ingest.args is not None:
    -        # The script has been called from one of the bean-* ingestion tools.
    -        # 'ingest.args' is only set when we're being invoked from one of the
    -        # bean-xxx tools (see below).
    -
    -        # Mark this function as called, so that if it is called from an import
    -        # triggered by one of the ingestion tools, it won't be called again
    -        # afterwards.
    -        ingest.was_called = True
    -
    -        # Use those args rather than to try to parse the command-line arguments
    -        # from a naked ingest() call as a script. {39c7af4f6af5}
    -        args, parser = ingest.args
    -    else:
    -        # The script is called directly. This is the main program of the import
    -        # script itself. This is the new invocation method.
    -        parser = version.ArgumentParser(description=DESCRIPTION)
    -
    -        # Use required on subparsers.
    -        # FIXME: Remove this when we require version 3.7 or above.
    -        kwargs = {}
    -        if sys.version_info >= (3, 7):
    -            kwargs['required'] = True
    -        subparsers = parser.add_subparsers(dest='command', **kwargs)
    -
    -        parser.add_argument('--downloads', '-d', metavar='DIR-OR-FILE',
    -                            action='append', default=[],
    -                            help='Filenames or directories to search for files to import')
    -
    -        for cmdname, module in [('identify', identify),
    -                                ('extract', extract),
    -                                ('file', file)]:
    -            parser_cmd = subparsers.add_parser(cmdname, help=module.DESCRIPTION)
    -            parser_cmd.set_defaults(command=module.run)
    -            module.add_arguments(parser_cmd)
    -
    -        args = parser.parse_args()
    -
    -        if not args.downloads:
    -            args.downloads.append(os.getcwd())
    -
    -        # Implement required ourselves.
    -        # FIXME: Remove this when we require version 3.7 or above.
    -        if not (sys.version_info >= (3, 7)):
    -            if not hasattr(args, 'command'):
    -                parser.error("Subcommand is required.")
    -
    -    abs_downloads = list(map(path.abspath, args.downloads))
    -    args.command(args, parser, importers_list, abs_downloads, hooks=hooks)
    -    return 0
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.scripts_utils.run_import_script_and_ingest(parser, argv=None, importers_attr_name='CONFIG') - - -

    - -
    - -

    Run the import script and optionally call ingest().

    -

    This path is only called when trampolined by one of the bean-* ingestion -tools.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • parser – The parser instance, used only to report errors.

    • -
    • importers_attr_name – The name of the special attribute in the module which -defines the importers list.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An execution return code.

    • -
    -
    -
    - Source code in beancount/ingest/scripts_utils.py -
    def run_import_script_and_ingest(parser, argv=None, importers_attr_name='CONFIG'):
    -    """Run the import script and optionally call ingest().
    -
    -    This path is only called when trampolined by one of the bean-* ingestion
    -    tools.
    -
    -    Args:
    -      parser: The parser instance, used only to report errors.
    -      importers_attr_name: The name of the special attribute in the module which
    -        defines the importers list.
    -    Returns:
    -      An execution return code.
    -    """
    -    args = parser.parse_args(args=argv)
    -
    -    # Check the existence of the config.
    -    if not path.exists(args.config) or path.isdir(args.config):
    -        parser.error("File does not exist: '{}'".format(args.config))
    -
    -    # Check the existence of all specified files.
    -    for filename in args.downloads:
    -        if not path.exists(filename):
    -            parser.error("File does not exist: '{}'".format(filename))
    -
    -    # Reset the state of ingest() being called (for unit tests, which use the
    -    # same runtime with run_with_args).
    -    ingest.was_called = False
    -
    -    # Save the arguments parsed from the command-line as default for
    -    # {39c7af4f6af5}.
    -    ingest.args = args, parser
    -
    -    # Evaluate the importer script/module.
    -    mod = runpy.run_path(args.config)
    -
    -    # If the importer script has already called ingest() within itself, don't
    -    # call it again. We're done. This allows the use to insert an explicit call
    -    # to ingest() while still running the bean-* ingestion tools on the file.
    -    if ingest.was_called:
    -        return 0
    -
    -    # ingest() hasn't been called by the script so we assume it isn't
    -    # present in it. So we now run the ingestion by ourselves here, without
    -    # specifying any of the newer optional arguments.
    -    importers_list = mod[importers_attr_name]
    -    return ingest(importers_list)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.scripts_utils.trampoline_to_ingest(module) - - -

    - -
    - -

    Parse arguments for bean tool, import config script and ingest.

    -

    This function is called by the three bean-* tools to support the older -import files, which only required a CONFIG object to be defined in them.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • module – One of the identify, extract or file module objects.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An execution return code.

    • -
    -
    -
    - Source code in beancount/ingest/scripts_utils.py -
    def trampoline_to_ingest(module):
    -    """Parse arguments for bean tool, import config script and ingest.
    -
    -    This function is called by the three bean-* tools to support the older
    -    import files, which only required a CONFIG object to be defined in them.
    -
    -    Args:
    -      module: One of the identify, extract or file module objects.
    -    Returns:
    -      An execution return code.
    -    """
    -    # Disable debugging logging which is turned on by default in chardet.
    -    logging.getLogger('chardet.charsetprober').setLevel(logging.INFO)
    -    logging.getLogger('chardet.universaldetector').setLevel(logging.INFO)
    -
    -    parser = create_legacy_arguments_parser(module.DESCRIPTION, module.run)
    -    module.add_arguments(parser)
    -    return run_import_script_and_ingest(parser)
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.ingest.similar - - - -

    - -
    - -

    Identify similar entries.

    -

    This can be used during import in order to identify and flag duplicate entries.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.ingest.similar.SimilarityComparator - - - -

    - -
    - -

    Similarity comparator of transactions.

    -

    This comparator needs to be able to handle Transaction instances which are -incomplete on one side, which have slightly different dates, or potentially -slightly different numbers.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.ingest.similar.SimilarityComparator.__call__(self, entry1, entry2) - - - special - - -

    - -
    - -

    Compare two entries, return true if they are deemed similar.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry1 – A first Transaction directive.

    • -
    • entry2 – A second Transaction directive.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A boolean.

    • -
    -
    -
    - Source code in beancount/ingest/similar.py -
    def __call__(self, entry1, entry2):
    -    """Compare two entries, return true if they are deemed similar.
    -
    -    Args:
    -      entry1: A first Transaction directive.
    -      entry2: A second Transaction directive.
    -    Returns:
    -      A boolean.
    -    """
    -    # Check the date difference.
    -    if self.max_date_delta is not None:
    -        delta = ((entry1.date - entry2.date)
    -                 if entry1.date > entry2.date else
    -                 (entry2.date - entry1.date))
    -        if delta > self.max_date_delta:
    -            return False
    -
    -    try:
    -        amounts1 = self.cache[id(entry1)]
    -    except KeyError:
    -        amounts1 = self.cache[id(entry1)] = amounts_map(entry1)
    -    try:
    -        amounts2 = self.cache[id(entry2)]
    -    except KeyError:
    -        amounts2 = self.cache[id(entry2)] = amounts_map(entry2)
    -
    -    # Look for amounts on common accounts.
    -    common_keys = set(amounts1) & set(amounts2)
    -    for key in sorted(common_keys):
    -        # Compare the amounts.
    -        number1 = amounts1[key]
    -        number2 = amounts2[key]
    -        if number1 == ZERO and number2 == ZERO:
    -            break
    -        diff = abs((number1 / number2)
    -                   if number2 != ZERO
    -                   else (number2 / number1))
    -        if diff == ZERO:
    -            return False
    -        if diff < ONE:
    -            diff = ONE/diff
    -        if (diff - ONE) < self.EPSILON:
    -            break
    -    else:
    -        return False
    -
    -    # Here, we have found at least one common account with a close
    -    # amount. Now, we require that the set of accounts are equal or that
    -    # one be a subset of the other.
    -    accounts1 = set(posting.account for posting in entry1.postings)
    -    accounts2 = set(posting.account for posting in entry2.postings)
    -    return accounts1.issubset(accounts2) or accounts2.issubset(accounts1)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.similar.SimilarityComparator.__init__(self, max_date_delta=None) - - - special - - -

    - -
    - -

    Constructor a comparator of entries.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • max_date_delta – A datetime.timedelta instance of the max tolerated -distance between dates.

    • -
    -
    -
    - Source code in beancount/ingest/similar.py -
    def __init__(self, max_date_delta=None):
    -    """Constructor a comparator of entries.
    -    Args:
    -      max_date_delta: A datetime.timedelta instance of the max tolerated
    -        distance between dates.
    -    """
    -    self.cache = {}
    -    self.max_date_delta = max_date_delta
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.ingest.similar.amounts_map(entry) - - -

    - -
    - -

    Compute a mapping of (account, currency) -> Decimal balances.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – A Transaction instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A dict of account -> Amount balance.

    • -
    -
    -
    - Source code in beancount/ingest/similar.py -
    def amounts_map(entry):
    -    """Compute a mapping of (account, currency) -> Decimal balances.
    -
    -    Args:
    -      entry: A Transaction instance.
    -    Returns:
    -      A dict of account -> Amount balance.
    -    """
    -    amounts = collections.defaultdict(D)
    -    for posting in entry.postings:
    -        # Skip interpolated postings.
    -        if posting.meta and interpolate.AUTOMATIC_META in posting.meta:
    -            continue
    -        currency = isinstance(posting.units, amount.Amount) and posting.units.currency
    -        if isinstance(currency, str):
    -            key = (posting.account, currency)
    -            amounts[key] += posting.units.number
    -    return amounts
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ingest.similar.find_similar_entries(entries, source_entries, comparator=None, window_days=2) - - -

    - -
    - -

    Find which entries from a list are potential duplicates of a set.

    -

    Note: If there are multiple entries from 'source_entries' matching an entry -in 'entries', only the first match is returned. Note that this function -could in theory decide to merge some of the imported entries with each -other.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – The list of entries to classify as duplicate or note.

    • -
    • source_entries – The list of entries against which to match. This is the -previous, or existing set of entries to compare against. This may be null -or empty.

    • -
    • comparator – A functor used to establish the similarity of two entries.

    • -
    • window_days – The number of days (inclusive) before or after to scan the -entries to classify against.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of pairs of entries (entry, source_entry) where entry is from -'entries' and is deemed to be a duplicate of source_entry, from -'source_entries'.

    • -
    -
    -
    - Source code in beancount/ingest/similar.py -
    def find_similar_entries(entries, source_entries, comparator=None, window_days=2):
    -    """Find which entries from a list are potential duplicates of a set.
    -
    -    Note: If there are multiple entries from 'source_entries' matching an entry
    -    in 'entries', only the first match is returned. Note that this function
    -    could in theory decide to merge some of the imported entries with each
    -    other.
    -
    -    Args:
    -      entries: The list of entries to classify as duplicate or note.
    -      source_entries: The list of entries against which to match. This is the
    -        previous, or existing set of entries to compare against. This may be null
    -        or empty.
    -      comparator: A functor used to establish the similarity of two entries.
    -      window_days: The number of days (inclusive) before or after to scan the
    -        entries to classify against.
    -    Returns:
    -      A list of pairs of entries (entry, source_entry) where entry is from
    -      'entries' and is deemed to be a duplicate of source_entry, from
    -      'source_entries'.
    -    """
    -    window_head = datetime.timedelta(days=window_days)
    -    window_tail = datetime.timedelta(days=window_days + 1)
    -
    -    if comparator is None:
    -        comparator = SimilarityComparator()
    -
    -    # For each of the new entries, look at existing entries at a nearby date.
    -    duplicates = []
    -    if source_entries is not None:
    -        for entry in data.filter_txns(entries):
    -            for source_entry in data.filter_txns(
    -                    data.iter_entry_dates(source_entries,
    -                                          entry.date - window_head,
    -                                          entry.date + window_tail)):
    -                if comparator(entry, source_entry):
    -                    duplicates.append((entry, source_entry))
    -                    break
    -    return duplicates
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - - -
    - -
    - -
    - -
    -
    - - -
    -
    - -
    - -
    - -
    - - - - « Previous - - - Next » - - -
    - - - - - - - - diff --git a/api_reference/beancount.loader.html b/api_reference/beancount.loader.html index 5ab41f34..895dad61 100644 --- a/api_reference/beancount.loader.html +++ b/api_reference/beancount.loader.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -152,8 +154,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader @@ -281,6 +273,9 @@

    beancount.loader @@ -316,7 +311,7 @@

    -beancount.loader.LoadError.__getnewargs__(self) +beancount.loader.LoadError.__getnewargs__(self) special @@ -330,10 +325,10 @@

    Source code in beancount/loader.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -346,7 +341,7 @@

    -beancount.loader.LoadError.__new__(_cls, source, message, entry) +beancount.loader.LoadError.__new__(_cls, source, message, entry) special @@ -370,7 +365,7 @@

    -beancount.loader.LoadError.__repr__(self) +beancount.loader.LoadError.__repr__(self) special @@ -384,10 +379,10 @@

    Source code in beancount/loader.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -411,7 +406,7 @@

    -beancount.loader.aggregate_options_map(options_map, src_options_map) +beancount.loader.aggregate_options_map(options_map, other_options_map)

    @@ -432,7 +427,8 @@

    • options_map – The target map in which we want to aggregate attributes. Note: This value is mutated in-place.

    • -
    • src_options_map – A source map whose values we'd like to see aggregated.

    • +
    • other_options_map – A list of other options maps, some of whose values +we'd like to see aggregated.

    @@ -440,23 +436,32 @@

    Source code in beancount/loader.py -
    def aggregate_options_map(options_map, src_options_map):
    -    """Aggregate some of the attributes of options map.
    -
    -    Args:
    -      options_map: The target map in which we want to aggregate attributes.
    -        Note: This value is mutated in-place.
    -      src_options_map: A source map whose values we'd like to see aggregated.
    -    """
    -    op_currencies = options_map["operating_currency"]
    -    for currency in src_options_map["operating_currency"]:
    -        if currency not in op_currencies:
    -            op_currencies.append(currency)
    -
    -    commodities = options_map["commodities"]
    -    for currency in src_options_map["commodities"]:
    -        commodities.add(currency)
    -
    +
    def aggregate_options_map(options_map, other_options_map):
    +    """Aggregate some of the attributes of options map.
    +
    +    Args:
    +      options_map: The target map in which we want to aggregate attributes.
    +        Note: This value is mutated in-place.
    +      other_options_map: A list of other options maps, some of whose values
    +        we'd like to see aggregated.
    +    """
    +    options_map = copy.copy(options_map)
    +
    +    currencies = list(options_map["operating_currency"])
    +    for omap in other_options_map:
    +        currencies.extend(omap["operating_currency"])
    +        options_map["dcontext"].update_from(omap["dcontext"])
    +    options_map["operating_currency"] = list(misc_utils.uniquify(currencies))
    +
    +    # Produce a 'pythonpath' value for transformers.
    +    pythonpath = set()
    +    for omap in itertools.chain((options_map,), other_options_map):
    +        if omap.get("insert_pythonpath", False):
    +            pythonpath.add(path.dirname(omap["filename"]))
    +    options_map["pythonpath"] = sorted(pythonpath)
    +
    +    return options_map
    +
    @@ -469,7 +474,7 @@

    -beancount.loader.combine_plugins(*plugin_modules) +beancount.loader.combine_plugins(*plugin_modules)

    @@ -513,21 +518,20 @@

    Source code in beancount/loader.py -
    def combine_plugins(*plugin_modules):
    -    """Combine the plugins from the given plugin modules.
    -
    -    This is used to create plugins of plugins.
    -    Args:
    -      *plugins_modules: A sequence of module objects.
    -    Returns:
    -      A list that can be assigned to the new module's __plugins__ attribute.
    -    """
    -    modules = []
    -    for module in plugin_modules:
    -        modules.extend([getattr(module, name)
    -                        for name in module.__plugins__])
    -    return modules
    -
    +
    def combine_plugins(*plugin_modules):
    +    """Combine the plugins from the given plugin modules.
    +
    +    This is used to create plugins of plugins.
    +    Args:
    +      *plugins_modules: A sequence of module objects.
    +    Returns:
    +      A list that can be assigned to the new module's __plugins__ attribute.
    +    """
    +    modules = []
    +    for module in plugin_modules:
    +        modules.extend([getattr(module, name) for name in module.__plugins__])
    +    return modules
    +
    @@ -540,7 +544,7 @@

    -beancount.loader.compute_input_hash(filenames) +beancount.loader.compute_input_hash(filenames)

    @@ -567,21 +571,21 @@

    Source code in beancount/loader.py -
    def compute_input_hash(filenames):
    -    """Compute a hash of the input data.
    -
    -    Args:
    -      filenames: A list of input files. Order is not relevant.
    -    """
    -    md5 = hashlib.md5()
    -    for filename in sorted(filenames):
    -        md5.update(filename.encode('utf8'))
    -        if not path.exists(filename):
    -            continue
    -        stat = os.stat(filename)
    -        md5.update(struct.pack('dd', stat.st_mtime_ns, stat.st_size))
    -    return md5.hexdigest()
    -
    +
    def compute_input_hash(filenames):
    +    """Compute a hash of the input data.
    +
    +    Args:
    +      filenames: A list of input files. Order is not relevant.
    +    """
    +    md5 = hashlib.md5()
    +    for filename in sorted(filenames):
    +        md5.update(filename.encode("utf8"))
    +        if not path.exists(filename):
    +            continue
    +        stat = os.stat(filename)
    +        md5.update(struct.pack("dd", stat.st_mtime_ns, stat.st_size))
    +    return md5.hexdigest()
    +
    @@ -594,7 +598,7 @@

    -beancount.loader.delete_cache_function(cache_getter, function) +beancount.loader.delete_cache_function(cache_getter, function)

    @@ -639,27 +643,29 @@

    Source code in beancount/loader.py -
    def delete_cache_function(cache_getter, function):
    -    """A wrapper that removes the cached filename.
    -
    -    Args:
    -      cache_getter: A function of one argument, the top-level filename, which
    -        will return the name of the corresponding cache file.
    -      function: A function object to decorate for caching.
    -    Returns:
    -      A decorated function which will delete the cached filename, if it exists.
    -    """
    -    @functools.wraps(function)
    -    def wrapped(toplevel_filename, *args, **kw):
    -        # Delete the cache.
    -        cache_filename = cache_getter(toplevel_filename)
    -        if path.exists(cache_filename):
    -            os.remove(cache_filename)
    -
    -        # Invoke the original function.
    -        return function(toplevel_filename, *args, **kw)
    -    return wrapped
    -
    +
    def delete_cache_function(cache_getter, function):
    +    """A wrapper that removes the cached filename.
    +
    +    Args:
    +      cache_getter: A function of one argument, the top-level filename, which
    +        will return the name of the corresponding cache file.
    +      function: A function object to decorate for caching.
    +    Returns:
    +      A decorated function which will delete the cached filename, if it exists.
    +    """
    +
    +    @functools.wraps(function)
    +    def wrapped(toplevel_filename, *args, **kw):
    +        # Delete the cache.
    +        cache_filename = cache_getter(toplevel_filename)
    +        if path.exists(cache_filename):
    +            os.remove(cache_filename)
    +
    +        # Invoke the original function.
    +        return function(toplevel_filename, *args, **kw)
    +
    +    return wrapped
    +
    @@ -672,7 +678,7 @@

    -beancount.loader.get_cache_filename(pattern, filename) +beancount.loader.get_cache_filename(pattern, filename)

    @@ -717,23 +723,23 @@

    Source code in beancount/loader.py -
    def get_cache_filename(pattern: str, filename: str) -> str:
    -    """Compute the cache filename from a given pattern and the top-level filename.
    -
    -    Args:
    -      pattern: A cache filename or pattern. If the pattern contains '{filename}' this
    -        will get replaced by the top-level filename. This may be absolute or relative.
    -      filename: The top-level filename.
    -    Returns:
    -      The resolved cache filename.
    -    """
    -    abs_filename = path.abspath(filename)
    -    if path.isabs(pattern):
    -        abs_pattern = pattern
    -    else:
    -        abs_pattern = path.join(path.dirname(abs_filename), pattern)
    -    return abs_pattern.format(filename=path.basename(filename))
    -
    +
    def get_cache_filename(pattern: str, filename: str) -> str:
    +    """Compute the cache filename from a given pattern and the top-level filename.
    +
    +    Args:
    +      pattern: A cache filename or pattern. If the pattern contains '{filename}' this
    +        will get replaced by the top-level filename. This may be absolute or relative.
    +      filename: The top-level filename.
    +    Returns:
    +      The resolved cache filename.
    +    """
    +    abs_filename = path.abspath(filename)
    +    if path.isabs(pattern):
    +        abs_pattern = pattern
    +    else:
    +        abs_pattern = path.join(path.dirname(abs_filename), pattern)
    +    return abs_pattern.format(filename=path.basename(filename))
    +
    @@ -746,7 +752,7 @@

    -beancount.loader.initialize(use_cache, cache_filename=None) +beancount.loader.initialize(use_cache, cache_filename=None)

    @@ -757,32 +763,36 @@

    Source code in beancount/loader.py -
    def initialize(use_cache: bool, cache_filename: Optional[str] = None):
    -    """Initialize the loader."""
    -
    -    # Unless an environment variable disables it, use the pickle load cache
    -    # automatically. Note that this works across all Python programs running the
    -    # loader which is why it's located here.
    -    # pylint: disable=invalid-name
    -    global _load_file
    -
    -    # Make a function to compute the cache filename.
    -    cache_pattern = (cache_filename or
    -                     os.getenv('BEANCOUNT_LOAD_CACHE_FILENAME') or
    -                     PICKLE_CACHE_FILENAME)
    -    cache_getter = functools.partial(get_cache_filename, cache_pattern)
    -
    -    if use_cache:
    -        _load_file = pickle_cache_function(cache_getter, PICKLE_CACHE_THRESHOLD,
    -                                           _uncached_load_file)
    -    else:
    -        if cache_filename is not None:
    -            logging.warning("Cache disabled; "
    -                            "Explicitly overridden cache filename %s will be ignored.",
    -                            cache_filename)
    -        _load_file = delete_cache_function(cache_getter,
    -                                           _uncached_load_file)
    -
    +
    def initialize(use_cache: bool, cache_filename: Optional[str] = None):
    +    """Initialize the loader."""
    +
    +    # Unless an environment variable disables it, use the pickle load cache
    +    # automatically. Note that this works across all Python programs running the
    +    # loader which is why it's located here.
    +
    +    global _load_file
    +
    +    # Make a function to compute the cache filename.
    +    cache_pattern = (
    +        cache_filename
    +        or os.getenv("BEANCOUNT_LOAD_CACHE_FILENAME")
    +        or PICKLE_CACHE_FILENAME
    +    )
    +    cache_getter = functools.partial(get_cache_filename, cache_pattern)
    +
    +    if use_cache:
    +        _load_file = pickle_cache_function(
    +            cache_getter, PICKLE_CACHE_THRESHOLD, _uncached_load_file
    +        )
    +    else:
    +        if cache_filename is not None:
    +            logging.warning(
    +                "Cache disabled; "
    +                "Explicitly overridden cache filename %s will be ignored.",
    +                cache_filename,
    +            )
    +        _load_file = delete_cache_function(cache_getter, _uncached_load_file)
    +
    @@ -795,7 +805,7 @@

    -beancount.loader.load_doc(expect_errors=False) +beancount.loader.load_doc(expect_errors=False)

    @@ -844,54 +854,56 @@

    Source code in beancount/loader.py -
    def load_doc(expect_errors=False):
    -    """A factory of decorators that loads the docstring and calls the function with entries.
    -
    -    This is an incredibly convenient tool to write lots of tests. Write a
    -    unittest using the standard TestCase class and put the input entries in the
    -    function's docstring.
    -
    -    Args:
    -      expect_errors: A boolean or None, with the following semantics,
    -        True: Expect errors and fail if there are none.
    -        False: Expect no errors and fail if there are some.
    -        None: Do nothing, no check.
    -    Returns:
    -      A wrapped method that accepts a single 'self' argument.
    -    """
    -    def decorator(fun):
    -        """A decorator that parses the function's docstring as an argument.
    -
    -        Args:
    -          fun: A callable method, that accepts the three return arguments that
    -              load() returns.
    -        Returns:
    -          A decorated test function.
    -        """
    -        @functools.wraps(fun)
    -        def wrapper(self):
    -            entries, errors, options_map = load_string(fun.__doc__, dedent=True)
    -
    -            if expect_errors is not None:
    -                if expect_errors is False and errors:
    -                    oss = io.StringIO()
    -                    printer.print_errors(errors, file=oss)
    -                    self.fail("Unexpected errors found:\n{}".format(oss.getvalue()))
    -                elif expect_errors is True and not errors:
    -                    self.fail("Expected errors, none found:")
    -
    -            # Note: Even if we expected no errors, we call this function with an
    -            # empty 'errors' list. This is so that the interface does not change
    -            # based on the arguments to the decorator, which would be somewhat
    -            # ugly and which would require explanation.
    -            return fun(self, entries, errors, options_map)
    -
    -        wrapper.__input__ = wrapper.__doc__
    -        wrapper.__doc__ = None
    -        return wrapper
    -
    -    return decorator
    -
    +
    def load_doc(expect_errors=False):
    +    """A factory of decorators that loads the docstring and calls the function with entries.
    +
    +    This is an incredibly convenient tool to write lots of tests. Write a
    +    unittest using the standard TestCase class and put the input entries in the
    +    function's docstring.
    +
    +    Args:
    +      expect_errors: A boolean or None, with the following semantics,
    +        True: Expect errors and fail if there are none.
    +        False: Expect no errors and fail if there are some.
    +        None: Do nothing, no check.
    +    Returns:
    +      A wrapped method that accepts a single 'self' argument.
    +    """
    +
    +    def decorator(fun):
    +        """A decorator that parses the function's docstring as an argument.
    +
    +        Args:
    +          fun: A callable method, that accepts the three return arguments that
    +              load() returns.
    +        Returns:
    +          A decorated test function.
    +        """
    +
    +        @functools.wraps(fun)
    +        def wrapper(self):
    +            entries, errors, options_map = load_string(fun.__doc__, dedent=True)
    +
    +            if expect_errors is not None:
    +                if expect_errors is False and errors:
    +                    oss = io.StringIO()
    +                    printer.print_errors(errors, file=oss)
    +                    self.fail("Unexpected errors found:\n{}".format(oss.getvalue()))
    +                elif expect_errors is True and not errors:
    +                    self.fail("Expected errors, none found:")
    +
    +            # Note: Even if we expected no errors, we call this function with an
    +            # empty 'errors' list. This is so that the interface does not change
    +            # based on the arguments to the decorator, which would be somewhat
    +            # ugly and which would require explanation.
    +            return fun(self, entries, errors, options_map)
    +
    +        wrapper.__input__ = wrapper.__doc__
    +        wrapper.__doc__ = None
    +        return wrapper
    +
    +    return decorator
    +
    @@ -904,7 +916,7 @@

    -beancount.loader.load_encrypted_file(filename, log_timings=None, log_errors=None, extra_validations=None, dedent=False, encoding=None) +beancount.loader.load_encrypted_file(filename, log_timings=None, log_errors=None, extra_validations=None, dedent=False, encoding=None)

    @@ -955,30 +967,38 @@

    Source code in beancount/loader.py -
    def load_encrypted_file(filename, log_timings=None, log_errors=None, extra_validations=None,
    -                        dedent=False, encoding=None):
    -    """Load an encrypted Beancount input file.
    -
    -    Args:
    -      filename: The name of an encrypted file to be parsed.
    -      log_timings: See load_string().
    -      log_errors: See load_string().
    -      extra_validations: See load_string().
    -      dedent: See load_string().
    -      encoding: See load_string().
    -    Returns:
    -      A triple of (entries, errors, option_map) where "entries" is a date-sorted
    -      list of entries from the file, "errors" a list of error objects generated
    -      while parsing and validating the file, and "options_map", a dict of the
    -      options parsed from the file.
    -    """
    -    contents = encryption.read_encrypted_file(filename)
    -    return load_string(contents,
    -                       log_timings=log_timings,
    -                       log_errors=log_errors,
    -                       extra_validations=extra_validations,
    -                       encoding=encoding)
    -
    +
    def load_encrypted_file(
    +    filename,
    +    log_timings=None,
    +    log_errors=None,
    +    extra_validations=None,
    +    dedent=False,
    +    encoding=None,
    +):
    +    """Load an encrypted Beancount input file.
    +
    +    Args:
    +      filename: The name of an encrypted file to be parsed.
    +      log_timings: See load_string().
    +      log_errors: See load_string().
    +      extra_validations: See load_string().
    +      dedent: See load_string().
    +      encoding: See load_string().
    +    Returns:
    +      A triple of (entries, errors, option_map) where "entries" is a date-sorted
    +      list of entries from the file, "errors" a list of error objects generated
    +      while parsing and validating the file, and "options_map", a dict of the
    +      options parsed from the file.
    +    """
    +    contents = encryption.read_encrypted_file(filename)
    +    return load_string(
    +        contents,
    +        log_timings=log_timings,
    +        log_errors=log_errors,
    +        extra_validations=extra_validations,
    +        encoding=encoding,
    +    )
    +
    @@ -991,7 +1011,7 @@

    -beancount.loader.load_file(filename, log_timings=None, log_errors=None, extra_validations=None, encoding=None) +beancount.loader.load_file(filename, log_timings=None, log_errors=None, extra_validations=None, encoding=None)

    @@ -1012,7 +1032,8 @@

    • filename – The name of the file to be parsed.

    • log_timings – A file object or function to write timings to, -or None, if it should remain quiet.

    • +or None, if it should remain quiet. (Note that this is intended to use +the logging methods and does not insert a newline.)

    • log_errors – A file object or function to write errors to, or None, if it should remain quiet.

    • extra_validations – A list of extra validation functions to run after loading @@ -1044,42 +1065,43 @@

      Source code in beancount/loader.py -
      def load_file(filename, log_timings=None, log_errors=None, extra_validations=None,
      -              encoding=None):
      -    """Open a Beancount input file, parse it, run transformations and validate.
      -
      -    Args:
      -      filename: The name of the file to be parsed.
      -      log_timings: A file object or function to write timings to,
      -        or None, if it should remain quiet.
      -      log_errors: A file object or function to write errors to,
      -        or None, if it should remain quiet.
      -      extra_validations: A list of extra validation functions to run after loading
      -        this list of entries.
      -      encoding: A string or None, the encoding to decode the input filename with.
      -    Returns:
      -      A triple of (entries, errors, option_map) where "entries" is a date-sorted
      -      list of entries from the file, "errors" a list of error objects generated
      -      while parsing and validating the file, and "options_map", a dict of the
      -      options parsed from the file.
      -    """
      -    filename = path.expandvars(path.expanduser(filename))
      -    if not path.isabs(filename):
      -        filename = path.normpath(path.join(os.getcwd(), filename))
      -
      -    if encryption.is_encrypted_file(filename):
      -        # Note: Caching is not supported for encrypted files.
      -        entries, errors, options_map = load_encrypted_file(
      -            filename,
      -            log_timings, log_errors,
      -            extra_validations, False, encoding)
      -    else:
      -        entries, errors, options_map = _load_file(
      -            filename, log_timings,
      -            extra_validations, encoding)
      -        _log_errors(errors, log_errors)
      -    return entries, errors, options_map
      -
      +
      def load_file(
      +    filename, log_timings=None, log_errors=None, extra_validations=None, encoding=None
      +):
      +    """Open a Beancount input file, parse it, run transformations and validate.
      +
      +    Args:
      +      filename: The name of the file to be parsed.
      +      log_timings: A file object or function to write timings to,
      +        or None, if it should remain quiet. (Note that this is intended to use
      +        the logging methods and does not insert a newline.)
      +      log_errors: A file object or function to write errors to,
      +        or None, if it should remain quiet.
      +      extra_validations: A list of extra validation functions to run after loading
      +        this list of entries.
      +      encoding: A string or None, the encoding to decode the input filename with.
      +    Returns:
      +      A triple of (entries, errors, option_map) where "entries" is a date-sorted
      +      list of entries from the file, "errors" a list of error objects generated
      +      while parsing and validating the file, and "options_map", a dict of the
      +      options parsed from the file.
      +    """
      +    filename = path.expandvars(path.expanduser(filename))
      +    if not path.isabs(filename):
      +        filename = path.normpath(path.join(os.getcwd(), filename))
      +
      +    if encryption.is_encrypted_file(filename):
      +        # Note: Caching is not supported for encrypted files.
      +        entries, errors, options_map = load_encrypted_file(
      +            filename, log_timings, log_errors, extra_validations, False, encoding
      +        )
      +    else:
      +        entries, errors, options_map = _load_file(
      +            filename, log_timings, extra_validations, encoding
      +        )
      +        _log_errors(errors, log_errors)
      +    return entries, errors, options_map
      +
      @@ -1092,7 +1114,7 @@

      -beancount.loader.load_string(string, log_timings=None, log_errors=None, extra_validations=None, dedent=False, encoding=None) +beancount.loader.load_string(string, log_timings=None, log_errors=None, extra_validations=None, dedent=False, encoding=None)

      @@ -1146,34 +1168,40 @@

      Source code in beancount/loader.py -
      def load_string(string, log_timings=None, log_errors=None, extra_validations=None,
      -                dedent=False, encoding=None):
      -
      -    """Open a Beancount input string, parse it, run transformations and validate.
      -
      -    Args:
      -      string: A Beancount input string.
      -      log_timings: A file object or function to write timings to,
      -        or None, if it should remain quiet.
      -      log_errors: A file object or function to write errors to,
      -        or None, if it should remain quiet.
      -      extra_validations: A list of extra validation functions to run after loading
      -        this list of entries.
      -      dedent: A boolean, if set, remove the whitespace in front of the lines.
      -      encoding: A string or None, the encoding to decode the input string with.
      -    Returns:
      -      A triple of (entries, errors, option_map) where "entries" is a date-sorted
      -      list of entries from the string, "errors" a list of error objects
      -      generated while parsing and validating the string, and "options_map", a
      -      dict of the options parsed from the string.
      -    """
      -    if dedent:
      -        string = textwrap.dedent(string)
      -    entries, errors, options_map = _load([(string, False)], log_timings,
      -                                         extra_validations, encoding)
      -    _log_errors(errors, log_errors)
      -    return entries, errors, options_map
      -
      +
      def load_string(
      +    string,
      +    log_timings=None,
      +    log_errors=None,
      +    extra_validations=None,
      +    dedent=False,
      +    encoding=None,
      +):
      +    """Open a Beancount input string, parse it, run transformations and validate.
      +
      +    Args:
      +      string: A Beancount input string.
      +      log_timings: A file object or function to write timings to,
      +        or None, if it should remain quiet.
      +      log_errors: A file object or function to write errors to,
      +        or None, if it should remain quiet.
      +      extra_validations: A list of extra validation functions to run after loading
      +        this list of entries.
      +      dedent: A boolean, if set, remove the whitespace in front of the lines.
      +      encoding: A string or None, the encoding to decode the input string with.
      +    Returns:
      +      A triple of (entries, errors, option_map) where "entries" is a date-sorted
      +      list of entries from the string, "errors" a list of error objects
      +      generated while parsing and validating the string, and "options_map", a
      +      dict of the options parsed from the string.
      +    """
      +    if dedent:
      +        string = textwrap.dedent(string)
      +    entries, errors, options_map = _load(
      +        [(string, False)], log_timings, extra_validations, encoding
      +    )
      +    _log_errors(errors, log_errors)
      +    return entries, errors, options_map
      +
      @@ -1186,7 +1214,7 @@

      -beancount.loader.needs_refresh(options_map) +beancount.loader.needs_refresh(options_map)

      @@ -1230,20 +1258,20 @@

      Source code in beancount/loader.py -
      def needs_refresh(options_map):
      -    """Predicate that returns true if at least one of the input files may have changed.
      -
      -    Args:
      -      options_map: An options dict as per the parser.
      -      mtime: A modified time, to check if it covers the include files in the options_map.
      -    Returns:
      -      A boolean, true if the input is obsoleted by changes in the input files.
      -    """
      -    if options_map is None:
      -        return True
      -    input_hash = compute_input_hash(options_map['include'])
      -    return 'input_hash' not in options_map or input_hash != options_map['input_hash']
      -
      +
      def needs_refresh(options_map):
      +    """Predicate that returns true if at least one of the input files may have changed.
      +
      +    Args:
      +      options_map: An options dict as per the parser.
      +      mtime: A modified time, to check if it covers the include files in the options_map.
      +    Returns:
      +      A boolean, true if the input is obsoleted by changes in the input files.
      +    """
      +    if options_map is None:
      +        return True
      +    input_hash = compute_input_hash(options_map["include"])
      +    return "input_hash" not in options_map or input_hash != options_map["input_hash"]
      +
      @@ -1256,7 +1284,7 @@

      -beancount.loader.pickle_cache_function(cache_getter, time_threshold, function) +beancount.loader.pickle_cache_function(cache_getter, time_threshold, function)

      @@ -1310,81 +1338,85 @@

      Source code in beancount/loader.py -
      def pickle_cache_function(cache_getter, time_threshold, function):
      -    """Decorate a loader function to make it loads its result from a pickle cache.
      -
      -    This considers the first argument as a top-level filename and assumes the
      -    function to be cached returns an (entries, errors, options_map) triple. We
      -    use the 'include' option value in order to check whether any of the included
      -    files has changed. It's essentially a special case for an on-disk memoizer.
      -    If any of the included files are more recent than the cache, the function is
      -    recomputed and the cache refreshed.
      -
      -    Args:
      -      cache_getter: A function of one argument, the top-level filename, which
      -        will return the name of the corresponding cache file.
      -      time_threshold: A float, the number of seconds below which we don't bother
      -        caching.
      -      function: A function object to decorate for caching.
      -    Returns:
      -      A decorated function which will pull its result from a cache file if
      -      it is available.
      -    """
      -    @functools.wraps(function)
      -    def wrapped(toplevel_filename, *args, **kw):
      -        cache_filename = cache_getter(toplevel_filename)
      -
      -        # Read the cache if it exists in order to get the list of files whose
      -        # timestamps to check.
      -        exists = path.exists(cache_filename)
      -        if exists:
      -            with open(cache_filename, 'rb') as file:
      -                try:
      -                    result = pickle.load(file)
      -                except Exception as exc:
      -                    # Note: Not a big fan of doing this, but here we handle all
      -                    # possible exceptions because unpickling of an old or
      -                    # corrupted pickle file manifests as a variety of different
      -                    # exception types.
      -
      -                    # The cache file is corrupted; ignore it and recompute.
      -                    logging.error("Cache file is corrupted: %s; recomputing.", exc)
      -                    result = None
      -
      -                else:
      -                    # Check that the latest timestamp has not been written after the
      -                    # cache file.
      -                    entries, errors, options_map = result
      -                    if not needs_refresh(options_map):
      -                        # All timestamps are legit; cache hit.
      -                        return result
      -
      -        # We failed; recompute the value.
      -        if exists:
      -            try:
      -                os.remove(cache_filename)
      -            except OSError as exc:
      -                # Warn for errors on read-only filesystems.
      -                logging.warning("Could not remove picklecache file %s: %s",
      -                                cache_filename, exc)
      -
      -        time_before = time.time()
      -        result = function(toplevel_filename, *args, **kw)
      -        time_after = time.time()
      -
      -        # Overwrite the cache file if the time it takes to compute it
      -        # justifies it.
      -        if time_after - time_before > time_threshold:
      -            try:
      -                with open(cache_filename, 'wb') as file:
      -                    pickle.dump(result, file)
      -            except Exception as exc:
      -                logging.warning("Could not write to picklecache file %s: %s",
      -                                cache_filename, exc)
      -
      -        return result
      -    return wrapped
      -
      +
      def pickle_cache_function(cache_getter, time_threshold, function):
      +    """Decorate a loader function to make it loads its result from a pickle cache.
      +
      +    This considers the first argument as a top-level filename and assumes the
      +    function to be cached returns an (entries, errors, options_map) triple. We
      +    use the 'include' option value in order to check whether any of the included
      +    files has changed. It's essentially a special case for an on-disk memoizer.
      +    If any of the included files are more recent than the cache, the function is
      +    recomputed and the cache refreshed.
      +
      +    Args:
      +      cache_getter: A function of one argument, the top-level filename, which
      +        will return the name of the corresponding cache file.
      +      time_threshold: A float, the number of seconds below which we don't bother
      +        caching.
      +      function: A function object to decorate for caching.
      +    Returns:
      +      A decorated function which will pull its result from a cache file if
      +      it is available.
      +    """
      +
      +    @functools.wraps(function)
      +    def wrapped(toplevel_filename, *args, **kw):
      +        cache_filename = cache_getter(toplevel_filename)
      +
      +        # Read the cache if it exists in order to get the list of files whose
      +        # timestamps to check.
      +        exists = path.exists(cache_filename)
      +        if exists:
      +            with open(cache_filename, "rb") as file:
      +                try:
      +                    result = pickle.load(file)
      +                except Exception as exc:
      +                    # Note: Not a big fan of doing this, but here we handle all
      +                    # possible exceptions because unpickling of an old or
      +                    # corrupted pickle file manifests as a variety of different
      +                    # exception types.
      +
      +                    # The cache file is corrupted; ignore it and recompute.
      +                    logging.error("Cache file is corrupted: %s; recomputing.", exc)
      +                    result = None
      +
      +                else:
      +                    # Check that the latest timestamp has not been written after the
      +                    # cache file.
      +                    entries, errors, options_map = result
      +                    if not needs_refresh(options_map):
      +                        # All timestamps are legit; cache hit.
      +                        return result
      +
      +        # We failed; recompute the value.
      +        if exists:
      +            try:
      +                os.remove(cache_filename)
      +            except OSError as exc:
      +                # Warn for errors on read-only filesystems.
      +                logging.warning(
      +                    "Could not remove picklecache file %s: %s", cache_filename, exc
      +                )
      +
      +        time_before = time.time()
      +        result = function(toplevel_filename, *args, **kw)
      +        time_after = time.time()
      +
      +        # Overwrite the cache file if the time it takes to compute it
      +        # justifies it.
      +        if time_after - time_before > time_threshold:
      +            try:
      +                with open(cache_filename, "wb") as file:
      +                    pickle.dump(result, file)
      +            except Exception as exc:
      +                logging.warning(
      +                    "Could not write to picklecache file %s: %s", cache_filename, exc
      +                )
      +
      +        return result
      +
      +    return wrapped
      +
      @@ -1397,7 +1429,7 @@

      -beancount.loader.run_transformations(entries, parse_errors, options_map, log_timings) +beancount.loader.run_transformations(entries, parse_errors, options_map, log_timings)

      @@ -1445,80 +1477,108 @@

      Source code in beancount/loader.py -
      def run_transformations(entries, parse_errors, options_map, log_timings):
      -    """Run the various transformations on the entries.
      -
      -    This is where entries are being synthesized, checked, plugins are run, etc.
      -
      -    Args:
      -      entries: A list of directives as read from the parser.
      -      parse_errors: A list of errors so far.
      -      options_map: An options dict as read from the parser.
      -      log_timings: A function to write timing log entries to, or None, if it
      -        should be quiet.
      -    Returns:
      -      A list of modified entries, and a list of errors, also possibly modified.
      -    """
      -    # A list of errors to extend (make a copy to avoid modifying the input).
      -    errors = list(parse_errors)
      -
      -    # Process the plugins.
      -    if options_map['plugin_processing_mode'] == 'raw':
      -        plugins_iter = options_map["plugin"]
      -    elif options_map['plugin_processing_mode'] == 'default':
      -        plugins_iter = itertools.chain(DEFAULT_PLUGINS_PRE,
      -                                       options_map["plugin"],
      -                                       DEFAULT_PLUGINS_POST)
      -    else:
      -        assert "Invalid value for plugin_processing_mode: {}".format(
      -            options_map['plugin_processing_mode'])
      -
      -    for plugin_name, plugin_config in plugins_iter:
      -
      -        # Issue a warning on a renamed module.
      -        renamed_name = RENAMED_MODULES.get(plugin_name, None)
      -        if renamed_name:
      -            warnings.warn("Deprecation notice: Module '{}' has been renamed to '{}'; "
      -                          "please adjust your plugin directive.".format(
      -                              plugin_name, renamed_name))
      -            plugin_name = renamed_name
      -
      -        # Try to import the module.
      -        try:
      -            module = importlib.import_module(plugin_name)
      -            if not hasattr(module, '__plugins__'):
      -                continue
      -
      -            with misc_utils.log_time(plugin_name, log_timings, indent=2):
      -
      -                # Run each transformer function in the plugin.
      -                for function_name in module.__plugins__:
      -                    if isinstance(function_name, str):
      -                        # Support plugin functions provided by name.
      -                        callback = getattr(module, function_name)
      -                    else:
      -                        # Support function types directly, not just names.
      -                        callback = function_name
      -
      -                    if plugin_config is not None:
      -                        entries, plugin_errors = callback(entries, options_map,
      -                                                          plugin_config)
      -                    else:
      -                        entries, plugin_errors = callback(entries, options_map)
      -                    errors.extend(plugin_errors)
      -
      -            # Ensure that the entries are sorted. Don't trust the plugins
      -            # themselves.
      -            entries.sort(key=data.entry_sortkey)
      -
      -        except (ImportError, TypeError) as exc:
      -            # Upon failure, just issue an error.
      -            errors.append(LoadError(data.new_metadata("<load>", 0),
      -                                    'Error importing "{}": {}'.format(
      -                                        plugin_name, str(exc)), None))
      -
      -    return entries, errors
      -
      +
      def run_transformations(entries, parse_errors, options_map, log_timings):
      +    """Run the various transformations on the entries.
      +
      +    This is where entries are being synthesized, checked, plugins are run, etc.
      +
      +    Args:
      +      entries: A list of directives as read from the parser.
      +      parse_errors: A list of errors so far.
      +      options_map: An options dict as read from the parser.
      +      log_timings: A function to write timing log entries to, or None, if it
      +        should be quiet.
      +    Returns:
      +      A list of modified entries, and a list of errors, also possibly modified.
      +    """
      +    # A list of errors to extend (make a copy to avoid modifying the input).
      +    errors = list(parse_errors)
      +
      +    # Process the plugins.
      +    if options_map["plugin_processing_mode"] == "raw":
      +        plugins_iter = options_map["plugin"]
      +    elif options_map["plugin_processing_mode"] == "default":
      +        plugins_iter = itertools.chain(
      +            PLUGINS_PRE, options_map["plugin"], PLUGINS_AUTO, PLUGINS_POST
      +        )
      +    else:
      +        assert "Invalid value for plugin_processing_mode: {}".format(
      +            options_map["plugin_processing_mode"]
      +        )
      +
      +    for plugin_name, plugin_config in plugins_iter:
      +        # Issue a warning on a renamed module.
      +        renamed_name = RENAMED_MODULES.get(plugin_name, None)
      +        if renamed_name:
      +            warnings.warn(
      +                "Deprecation notice: Module '{}' has been renamed to '{}'; "
      +                "please adjust your plugin directive.".format(plugin_name, renamed_name)
      +            )
      +            plugin_name = renamed_name
      +
      +        # Try to import the module.
      +        #
      +        # Note: We intercept import errors and continue but let other plugin
      +        # import time exceptions fail a run, by choice.
      +        try:
      +            module = importlib.import_module(plugin_name)
      +            if not hasattr(module, "__plugins__"):
      +                continue
      +        except ImportError:
      +            # Upon failure, just issue an error.
      +            formatted_traceback = traceback.format_exc().replace("\n", "\n  ")
      +            errors.append(
      +                LoadError(
      +                    data.new_metadata("<load>", 0),
      +                    'Error importing "{}": {}'.format(plugin_name, formatted_traceback),
      +                    None,
      +                )
      +            )
      +            continue
      +
      +        # Apply it.
      +        with misc_utils.log_time(plugin_name, log_timings, indent=2):
      +            # Run each transformer function in the plugin.
      +            for function_name in module.__plugins__:
      +                if isinstance(function_name, str):
      +                    # Support plugin functions provided by name.
      +                    callback = getattr(module, function_name)
      +                else:
      +                    # Support function types directly, not just names.
      +                    callback = function_name
      +
      +                # Provide arguments if config is provided.
      +                # TODO(blais): Make this consistent in v3, not conditional.
      +                args = () if plugin_config is None else (plugin_config,)
      +
      +                # Catch all exceptions raised in running the plugin, except exits.
      +                try:
      +                    entries, plugin_errors = callback(entries, options_map, *args)
      +                    errors.extend(plugin_errors)
      +                except Exception as exc:
      +                    # Allow the user to exit in a plugin.
      +                    if isinstance(exc, SystemExit):
      +                        raise
      +
      +                    # Upon failure, just issue an error.
      +                    formatted_traceback = traceback.format_exc().replace("\n", "\n  ")
      +                    errors.append(
      +                        LoadError(
      +                            data.new_metadata("<load>", 0),
      +                            'Error applying plugin "{}": {}'.format(
      +                                plugin_name, formatted_traceback
      +                            ),
      +                            None,
      +                        )
      +                    )
      +                    continue
      +
      +            # Ensure that the entries are sorted. Don't trust the plugins
      +            # themselves.
      +            entries.sort(key=data.entry_sortkey)
      +
      +    return entries, errors
      +
      @@ -1544,7 +1604,7 @@

      Next - Previous + Previous @@ -1570,7 +1630,7 @@

      - « Previous + « Previous Next » diff --git a/api_reference/beancount.ops.html b/api_reference/beancount.ops.html index 0730b5bd..cc7f493d 100644 --- a/api_reference/beancount.ops.html +++ b/api_reference/beancount.ops.html @@ -105,6 +105,8 @@

    • Calculating Portfolio Returns
    • +
    • Tracking Out-of-Network Medical Claims in Beancount +

  • Documentation for Developers @@ -152,8 +154,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -218,35 +218,15 @@
  • -
  • holdings -
  • @@ -258,7 +238,11 @@
  • get_commodity_lifetimes()
  • +
  • required_daily_prices() +
  • required_weekly_prices() +
  • +
  • trim_intervals()
  • @@ -354,20 +338,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • @@ -495,7 +471,7 @@

    -beancount.ops.balance.BalanceError.__getnewargs__(self) +beancount.ops.balance.BalanceError.__getnewargs__(self) special @@ -509,10 +485,10 @@

    Source code in beancount/ops/balance.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -525,7 +501,7 @@

    -beancount.ops.balance.BalanceError.__new__(_cls, source, message, entry) +beancount.ops.balance.BalanceError.__new__(_cls, source, message, entry) special @@ -549,7 +525,7 @@

    -beancount.ops.balance.BalanceError.__repr__(self) +beancount.ops.balance.BalanceError.__repr__(self) special @@ -563,10 +539,10 @@

    Source code in beancount/ops/balance.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -590,7 +566,7 @@

    -beancount.ops.balance.check(entries, options_map) +beancount.ops.balance.check(entries, options_map)

    @@ -637,132 +613,146 @@

    Source code in beancount/ops/balance.py -
    def check(entries, options_map):
    -    """Process the balance assertion directives.
    -
    -    For each Balance directive, check that their expected balance corresponds to
    -    the actual balance computed at that time and replace failing ones by new
    -    ones with a flag that indicates failure.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A dict of options, parsed from the input file.
    -    Returns:
    -      A pair of a list of directives and a list of balance check errors.
    -    """
    -    new_entries = []
    -    check_errors = []
    -
    -    # This is similar to realization, but performed in a different order, and
    -    # where we only accumulate inventories for accounts that have balance
    -    # assertions in them (this saves on time). Here we process the entries one
    -    # by one along with the balance checks. We use a temporary realization in
    -    # order to hold the incremental tree of balances, so that we can easily get
    -    # the amounts of an account's subaccounts for making checks on parent
    -    # accounts.
    -    real_root = realization.RealAccount('')
    -
    -    # Figure out the set of accounts for which we need to compute a running
    -    # inventory balance.
    -    asserted_accounts = {entry.account
    -                         for entry in entries
    -                         if isinstance(entry, Balance)}
    -
    -    # Add all children accounts of an asserted account to be calculated as well,
    -    # and pre-create these accounts, and only those (we're just being tight to
    -    # make sure).
    -    asserted_match_list = [account.parent_matcher(account_)
    -                           for account_ in asserted_accounts]
    -    for account_ in getters.get_accounts(entries):
    -        if (account_ in asserted_accounts or
    -            any(match(account_) for match in asserted_match_list)):
    -            realization.get_or_create(real_root, account_)
    -
    -    # Get the Open directives for each account.
    -    open_close_map = getters.get_account_open_close(entries)
    -
    -    for entry in entries:
    -        if isinstance(entry, Transaction):
    -            # For each of the postings' accounts, update the balance inventory.
    -            for posting in entry.postings:
    -                real_account = realization.get(real_root, posting.account)
    -
    -                # The account will have been created only if we're meant to track it.
    -                if real_account is not None:
    -                    # Note: Always allow negative lots for the purpose of balancing.
    -                    # This error should show up somewhere else than here.
    -                    real_account.balance.add_position(posting)
    -
    -        elif isinstance(entry, Balance):
    -            # Check that the currency of the balance check is one of the allowed
    -            # currencies for that account.
    -            expected_amount = entry.amount
    -            try:
    -                open, _ = open_close_map[entry.account]
    -            except KeyError:
    -                check_errors.append(
    -                    BalanceError(entry.meta,
    -                                 "Account '{}' does not exist: ".format(entry.account),
    -                                 entry))
    -                continue
    -
    -            if (expected_amount is not None and
    -                open and open.currencies and
    -                expected_amount.currency not in open.currencies):
    -                check_errors.append(
    -                    BalanceError(entry.meta,
    -                                 "Invalid currency '{}' for Balance directive: ".format(
    -                                     expected_amount.currency),
    -                                 entry))
    -
    -            # Sum up the current balances for this account and its
    -            # sub-accounts. We want to support checks for parent accounts
    -            # for the total sum of their subaccounts.
    -            #
    -            # FIXME: Improve the performance further by computing the balance
    -            # for the desired currency only. This won't allow us to cache in
    -            # this way but may be faster, if we're not asserting all the
    -            # currencies. Furthermore, we could probably avoid recomputing the
    -            # balance if a subtree of positions hasn't been invalidated by a new
    -            # position added to the realization. Do this.
    -            real_account = realization.get(real_root, entry.account)
    -            assert real_account is not None, "Missing {}".format(entry.account)
    -            subtree_balance = realization.compute_balance(real_account, leaf_only=False)
    -
    -            # Get only the amount in the desired currency.
    -            balance_amount = subtree_balance.get_currency_units(expected_amount.currency)
    -
    -            # Check if the amount is within bounds of the expected amount.
    -            diff_amount = amount.sub(balance_amount, expected_amount)
    -
    -            # Use the specified tolerance or automatically infer it.
    -            tolerance = get_balance_tolerance(entry, options_map)
    -
    -            if abs(diff_amount.number) > tolerance:
    -                check_errors.append(
    -                    BalanceError(entry.meta,
    -                                 ("Balance failed for '{}': "
    -                                  "expected {} != accumulated {} ({} {})").format(
    -                                      entry.account, expected_amount, balance_amount,
    -                                      abs(diff_amount.number),
    -                                      ('too much'
    -                                       if diff_amount.number > 0
    -                                       else 'too little')),
    -                                 entry))
    -
    -                # Substitute the entry by a failing entry, with the diff_amount
    -                # field set on it. I'm not entirely sure that this is the best
    -                # of ideas, maybe leaving the original check intact and insert a
    -                # new error entry might be more functional or easier to
    -                # understand.
    -                entry = entry._replace(
    -                    meta=entry.meta.copy(),
    -                    diff_amount=diff_amount)
    -
    -        new_entries.append(entry)
    -
    -    return new_entries, check_errors
    -
    +
    def check(entries, options_map):
    +    """Process the balance assertion directives.
    +
    +    For each Balance directive, check that their expected balance corresponds to
    +    the actual balance computed at that time and replace failing ones by new
    +    ones with a flag that indicates failure.
    +
    +    Args:
    +      entries: A list of directives.
    +      options_map: A dict of options, parsed from the input file.
    +    Returns:
    +      A pair of a list of directives and a list of balance check errors.
    +    """
    +    new_entries = []
    +    check_errors = []
    +
    +    # This is similar to realization, but performed in a different order, and
    +    # where we only accumulate inventories for accounts that have balance
    +    # assertions in them (this saves on time). Here we process the entries one
    +    # by one along with the balance checks. We use a temporary realization in
    +    # order to hold the incremental tree of balances, so that we can easily get
    +    # the amounts of an account's subaccounts for making checks on parent
    +    # accounts.
    +    real_root = realization.RealAccount("")
    +
    +    # Figure out the set of accounts for which we need to compute a running
    +    # inventory balance.
    +    asserted_accounts = {entry.account for entry in entries if isinstance(entry, Balance)}
    +
    +    # Add all children accounts of an asserted account to be calculated as well,
    +    # and pre-create these accounts, and only those (we're just being tight to
    +    # make sure).
    +    asserted_match_list = [
    +        account.parent_matcher(account_) for account_ in asserted_accounts
    +    ]
    +    for account_ in getters.get_accounts(entries):
    +        if account_ in asserted_accounts or any(
    +            match(account_) for match in asserted_match_list
    +        ):
    +            realization.get_or_create(real_root, account_)
    +
    +    # Get the Open directives for each account.
    +    open_close_map = getters.get_account_open_close(entries)
    +
    +    for entry in entries:
    +        if isinstance(entry, Transaction):
    +            # For each of the postings' accounts, update the balance inventory.
    +            for posting in entry.postings:
    +                real_account = realization.get(real_root, posting.account)
    +
    +                # The account will have been created only if we're meant to track it.
    +                if real_account is not None:
    +                    # Note: Always allow negative lots for the purpose of balancing.
    +                    # This error should show up somewhere else than here.
    +                    real_account.balance.add_position(posting)
    +
    +        elif isinstance(entry, Balance):
    +            # Check that the currency of the balance check is one of the allowed
    +            # currencies for that account.
    +            expected_amount = entry.amount
    +            try:
    +                open, _ = open_close_map[entry.account]
    +            except KeyError:
    +                check_errors.append(
    +                    BalanceError(
    +                        entry.meta,
    +                        "Invalid reference to unknown account '{}'".format(entry.account),
    +                        entry,
    +                    )
    +                )
    +                continue
    +
    +            if (
    +                expected_amount is not None
    +                and open
    +                and open.currencies
    +                and expected_amount.currency not in open.currencies
    +            ):
    +                check_errors.append(
    +                    BalanceError(
    +                        entry.meta,
    +                        "Invalid currency '{}' for Balance directive: ".format(
    +                            expected_amount.currency
    +                        ),
    +                        entry,
    +                    )
    +                )
    +
    +            # Sum up the current balances for this account and its
    +            # sub-accounts. We want to support checks for parent accounts
    +            # for the total sum of their subaccounts.
    +            #
    +            # FIXME: Improve the performance further by computing the balance
    +            # for the desired currency only. This won't allow us to cache in
    +            # this way but may be faster, if we're not asserting all the
    +            # currencies. Furthermore, we could probably avoid recomputing the
    +            # balance if a subtree of positions hasn't been invalidated by a new
    +            # position added to the realization. Do this.
    +            real_account = realization.get(real_root, entry.account)
    +            assert real_account is not None, "Missing {}".format(entry.account)
    +            subtree_balance = realization.compute_balance(real_account, leaf_only=False)
    +
    +            # Get only the amount in the desired currency.
    +            balance_amount = subtree_balance.get_currency_units(expected_amount.currency)
    +
    +            # Check if the amount is within bounds of the expected amount.
    +            diff_amount = amount.sub(balance_amount, expected_amount)
    +
    +            # Use the specified tolerance or automatically infer it.
    +            tolerance = get_balance_tolerance(entry, options_map)
    +
    +            if abs(diff_amount.number) > tolerance:
    +                check_errors.append(
    +                    BalanceError(
    +                        entry.meta,
    +                        (
    +                            "Balance failed for '{}': "
    +                            "expected {} != accumulated {} ({} {})"
    +                        ).format(
    +                            entry.account,
    +                            expected_amount,
    +                            balance_amount,
    +                            abs(diff_amount.number),
    +                            ("too much" if diff_amount.number > 0 else "too little"),
    +                        ),
    +                        entry,
    +                    )
    +                )
    +
    +                # Substitute the entry by a failing entry, with the diff_amount
    +                # field set on it. I'm not entirely sure that this is the best
    +                # of ideas, maybe leaving the original check intact and insert a
    +                # new error entry might be more functional or easier to
    +                # understand.
    +                entry = entry._replace(meta=entry.meta.copy(), diff_amount=diff_amount)
    +
    +        new_entries.append(entry)
    +
    +    return new_entries, check_errors
    +
    @@ -775,7 +765,7 @@

    -beancount.ops.balance.get_balance_tolerance(balance_entry, options_map) +beancount.ops.balance.get_balance_tolerance(balance_entry, options_map)

    @@ -819,33 +809,33 @@

    Source code in beancount/ops/balance.py -
    def get_balance_tolerance(balance_entry, options_map):
    -    """Get the tolerance amount for a single entry.
    -
    -    Args:
    -      balance_entry: An instance of data.Balance
    -      options_map: An options dict, as per the parser.
    -    Returns:
    -      A Decimal, the amount of tolerance implied by the directive.
    -    """
    -    if balance_entry.tolerance is not None:
    -        # Use the balance-specific tolerance override if it is provided.
    -        tolerance = balance_entry.tolerance
    -
    -    else:
    -        expo = balance_entry.amount.number.as_tuple().exponent
    -        if expo < 0:
    -            # Be generous and always allow twice the multiplier on Balance and
    -            # Pad because the user creates these and the rounding of those
    -            # balances may often be further off than those used within a single
    -            # transaction.
    -            tolerance = options_map["inferred_tolerance_multiplier"] * 2
    -            tolerance = ONE.scaleb(expo) * tolerance
    -        else:
    -            tolerance = ZERO
    -
    -    return tolerance
    -
    +
    def get_balance_tolerance(balance_entry, options_map):
    +    """Get the tolerance amount for a single entry.
    +
    +    Args:
    +      balance_entry: An instance of data.Balance
    +      options_map: An options dict, as per the parser.
    +    Returns:
    +      A Decimal, the amount of tolerance implied by the directive.
    +    """
    +    if balance_entry.tolerance is not None:
    +        # Use the balance-specific tolerance override if it is provided.
    +        tolerance = balance_entry.tolerance
    +
    +    else:
    +        expo = balance_entry.amount.number.as_tuple().exponent
    +        if expo < 0:
    +            # Be generous and always allow twice the multiplier on Balance and
    +            # Pad because the user creates these and the rounding of those
    +            # balances may often be further off than those used within a single
    +            # transaction.
    +            tolerance = options_map["inferred_tolerance_multiplier"] * 2
    +            tolerance = ONE.scaleb(expo) * tolerance
    +        else:
    +            tolerance = ZERO
    +
    +    return tolerance
    +
    @@ -899,7 +889,7 @@

    @@ -928,20 +918,18 @@

    -beancount.ops.basicops.filter_tag(tag, entries) +beancount.ops.basicops.filter_tag(tag, entries)

    @@ -983,21 +971,18 @@

    Source code in beancount/ops/basicops.py -
    def filter_tag(tag, entries):
    -    """Yield all the entries which have the given tag.
    -
    -    Args:
    -      tag: A string, the tag we are interested in.
    -    Yields:
    -      Every entry in 'entries' that tags to 'tag.
    -    """
    -    for entry in entries:
    -        # pylint: disable=bad-continuation
    -        if (isinstance(entry, data.Transaction) and
    -            entry.tags and
    -            tag in entry.tags):
    -            yield entry
    -
    +
    def filter_tag(tag, entries):
    +    """Yield all the entries which have the given tag.
    +
    +    Args:
    +      tag: A string, the tag we are interested in.
    +    Yields:
    +      Every entry in 'entries' that tags to 'tag.
    +    """
    +    for entry in entries:
    +        if isinstance(entry, data.Transaction) and entry.tags and tag in entry.tags:
    +            yield entry
    +
    @@ -1010,7 +995,7 @@

    -beancount.ops.basicops.get_common_accounts(entries) +beancount.ops.basicops.get_common_accounts(entries)

    @@ -1054,34 +1039,34 @@

    Source code in beancount/ops/basicops.py -
    def get_common_accounts(entries):
    -    """Compute the intersection of the accounts on the given entries.
    -
    -    Args:
    -      entries: A list of Transaction entries to process.
    -    Returns:
    -      A set of strings, the names of the common accounts from these
    -      entries.
    -    """
    -    assert all(isinstance(entry, data.Transaction) for entry in entries)
    -
    -    # If there is a single entry, the common accounts to it is all its accounts.
    -    # Note that this also works with no entries (yields an empty set).
    -    if len(entries) < 2:
    -        if entries:
    -            intersection = {posting.account for posting in entries[0].postings}
    -        else:
    -            intersection = set()
    -    else:
    -        entries_iter = iter(entries)
    -        intersection = set(posting.account for posting in next(entries_iter).postings)
    -        for entry in entries_iter:
    -            accounts = set(posting.account for posting in entry.postings)
    -            intersection &= accounts
    -            if not intersection:
    -                break
    -    return intersection
    -
    +
    def get_common_accounts(entries):
    +    """Compute the intersection of the accounts on the given entries.
    +
    +    Args:
    +      entries: A list of Transaction entries to process.
    +    Returns:
    +      A set of strings, the names of the common accounts from these
    +      entries.
    +    """
    +    assert all(isinstance(entry, data.Transaction) for entry in entries)
    +
    +    # If there is a single entry, the common accounts to it is all its accounts.
    +    # Note that this also works with no entries (yields an empty set).
    +    if len(entries) < 2:
    +        if entries:
    +            intersection = {posting.account for posting in entries[0].postings}
    +        else:
    +            intersection = set()
    +    else:
    +        entries_iter = iter(entries)
    +        intersection = set(posting.account for posting in next(entries_iter).postings)
    +        for entry in entries_iter:
    +            accounts = set(posting.account for posting in entry.postings)
    +            intersection &= accounts
    +            if not intersection:
    +                break
    +    return intersection
    +
    @@ -1094,7 +1079,7 @@

    @@ -1137,22 +1122,22 @@

    -beancount.ops.compress.compress(entries, predicate) +beancount.ops.compress.compress(entries, predicate)

    @@ -1266,49 +1251,49 @@

    Source code in beancount/ops/compress.py -
    def compress(entries, predicate):
    -    """Compress multiple transactions into single transactions.
    -
    -    Replace consecutive sequences of Transaction entries that fulfill the given
    -    predicate by a single entry at the date of the last matching entry.
    -    'predicate' is the function that determines if an entry should be
    -    compressed.
    -
    -    This can be used to simply a list of transactions that are similar and occur
    -    frequently. As an example, in a retail FOREX trading account, differential
    -    interest of very small amounts is paid every day; it is not relevant to look
    -    at the full detail of this interest unless there are other transactions. You
    -    can use this to compress it into single entries between other types of
    -    transactions.
    -
    -    Args:
    -      entries: A list of directives.
    -      predicate: A callable which accepts an entry and return true if the entry
    -          is intended to be compressed.
    -    Returns:
    -      A list of directives, with compressible transactions replaced by a summary
    -      equivalent.
    -    """
    -    new_entries = []
    -    pending = []
    -    for entry in entries:
    -        if isinstance(entry, data.Transaction) and predicate(entry):
    -            # Save for compressing later.
    -            pending.append(entry)
    -        else:
    -            # Compress and output all the pending entries.
    -            if pending:
    -                new_entries.append(merge(pending, pending[-1]))
    -                pending.clear()
    -
    -            # Output the differing entry.
    -            new_entries.append(entry)
    -
    -    if pending:
    -        new_entries.append(merge(pending, pending[-1]))
    -
    -    return new_entries
    -
    +
    def compress(entries, predicate):
    +    """Compress multiple transactions into single transactions.
    +
    +    Replace consecutive sequences of Transaction entries that fulfill the given
    +    predicate by a single entry at the date of the last matching entry.
    +    'predicate' is the function that determines if an entry should be
    +    compressed.
    +
    +    This can be used to simply a list of transactions that are similar and occur
    +    frequently. As an example, in a retail FOREX trading account, differential
    +    interest of very small amounts is paid every day; it is not relevant to look
    +    at the full detail of this interest unless there are other transactions. You
    +    can use this to compress it into single entries between other types of
    +    transactions.
    +
    +    Args:
    +      entries: A list of directives.
    +      predicate: A callable which accepts an entry and return true if the entry
    +          is intended to be compressed.
    +    Returns:
    +      A list of directives, with compressible transactions replaced by a summary
    +      equivalent.
    +    """
    +    new_entries = []
    +    pending = []
    +    for entry in entries:
    +        if isinstance(entry, data.Transaction) and predicate(entry):
    +            # Save for compressing later.
    +            pending.append(entry)
    +        else:
    +            # Compress and output all the pending entries.
    +            if pending:
    +                new_entries.append(merge(pending, pending[-1]))
    +                pending.clear()
    +
    +            # Output the differing entry.
    +            new_entries.append(entry)
    +
    +    if pending:
    +        new_entries.append(merge(pending, pending[-1]))
    +
    +    return new_entries
    +
    @@ -1321,7 +1306,7 @@

    -beancount.ops.compress.merge(entries, prototype_txn) +beancount.ops.compress.merge(entries, prototype_txn)

    @@ -1371,60 +1356,73 @@

    Source code in beancount/ops/compress.py -
    def merge(entries, prototype_txn):
    -    """Merge the postings of a list of Transactions into a single one.
    -
    -    Merge postings the given entries into a single entry with the Transaction
    -    attributes of the prototype. Return the new entry. The combined list of
    -    postings are merged if everything about the postings is the same except the
    -    number.
    -
    -    Args:
    -      entries: A list of directives.
    -      prototype_txn: A Transaction which is used to create the compressed
    -          Transaction instance. Its list of postings is ignored.
    -    Returns:
    -      A new Transaction instance which contains all the postings from the input
    -      entries merged together.
    -
    -    """
    -    # Aggregate the postings together. This is a mapping of numberless postings
    -    # to their number of units.
    -    postings_map = collections.defaultdict(Decimal)
    -    for entry in data.filter_txns(entries):
    -        for posting in entry.postings:
    -            # We strip the number off the posting to act as an aggregation key.
    -            key = data.Posting(posting.account,
    -                               Amount(None, posting.units.currency),
    -                               posting.cost,
    -                               posting.price,
    -                               posting.flag,
    -                               None)
    -            postings_map[key] += posting.units.number
    -
    -    # Create a new transaction with the aggregated postings.
    -    new_entry = data.Transaction(prototype_txn.meta,
    -                                 prototype_txn.date,
    -                                 prototype_txn.flag,
    -                                 prototype_txn.payee,
    -                                 prototype_txn.narration,
    -                                 data.EMPTY_SET, data.EMPTY_SET, [])
    -
    -    # Sort for at least some stability of output.
    -    sorted_items = sorted(postings_map.items(),
    -                          key=lambda item: (item[0].account,
    -                                            item[0].units.currency,
    -                                            item[1]))
    -
    -    # Issue the merged postings.
    -    for posting, number in sorted_items:
    -        units = Amount(number, posting.units.currency)
    -        new_entry.postings.append(
    -            data.Posting(posting.account, units, posting.cost, posting.price,
    -                         posting.flag, posting.meta))
    -
    -    return new_entry
    -
    +
    def merge(entries, prototype_txn):
    +    """Merge the postings of a list of Transactions into a single one.
    +
    +    Merge postings the given entries into a single entry with the Transaction
    +    attributes of the prototype. Return the new entry. The combined list of
    +    postings are merged if everything about the postings is the same except the
    +    number.
    +
    +    Args:
    +      entries: A list of directives.
    +      prototype_txn: A Transaction which is used to create the compressed
    +          Transaction instance. Its list of postings is ignored.
    +    Returns:
    +      A new Transaction instance which contains all the postings from the input
    +      entries merged together.
    +
    +    """
    +    # Aggregate the postings together. This is a mapping of numberless postings
    +    # to their number of units.
    +    postings_map = collections.defaultdict(Decimal)
    +    for entry in data.filter_txns(entries):
    +        for posting in entry.postings:
    +            # We strip the number off the posting to act as an aggregation key.
    +            key = data.Posting(
    +                posting.account,
    +                Amount(None, posting.units.currency),
    +                posting.cost,
    +                posting.price,
    +                posting.flag,
    +                None,
    +            )
    +            postings_map[key] += posting.units.number
    +
    +    # Create a new transaction with the aggregated postings.
    +    new_entry = data.Transaction(
    +        prototype_txn.meta,
    +        prototype_txn.date,
    +        prototype_txn.flag,
    +        prototype_txn.payee,
    +        prototype_txn.narration,
    +        data.EMPTY_SET,
    +        data.EMPTY_SET,
    +        [],
    +    )
    +
    +    # Sort for at least some stability of output.
    +    sorted_items = sorted(
    +        postings_map.items(),
    +        key=lambda item: (item[0].account, item[0].units.currency, item[1]),
    +    )
    +
    +    # Issue the merged postings.
    +    for posting, number in sorted_items:
    +        units = Amount(number, posting.units.currency)
    +        new_entry.postings.append(
    +            data.Posting(
    +                posting.account,
    +                units,
    +                posting.cost,
    +                posting.price,
    +                posting.flag,
    +                posting.meta,
    +            )
    +        )
    +
    +    return new_entry
    +
    @@ -1506,7 +1504,7 @@

    -beancount.ops.documents.DocumentError.__getnewargs__(self) +beancount.ops.documents.DocumentError.__getnewargs__(self) special @@ -1520,10 +1518,10 @@

    Source code in beancount/ops/documents.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -1536,7 +1534,7 @@

    -beancount.ops.documents.DocumentError.__new__(_cls, source, message, entry) +beancount.ops.documents.DocumentError.__new__(_cls, source, message, entry) special @@ -1560,7 +1558,7 @@

    -beancount.ops.documents.DocumentError.__repr__(self) +beancount.ops.documents.DocumentError.__repr__(self) special @@ -1574,10 +1572,10 @@

    Source code in beancount/ops/documents.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -1601,7 +1599,7 @@

    -beancount.ops.documents.find_documents(directory, input_filename, accounts_only=None, strict=False) +beancount.ops.documents.find_documents(directory, input_filename, accounts_only=None, strict=False)

    @@ -1653,83 +1651,101 @@

    Source code in beancount/ops/documents.py -
    def find_documents(directory, input_filename, accounts_only=None, strict=False):
    -    """Find dated document files under the given directory.
    -
    -    If a restricting set of accounts is provided in 'accounts_only', only return
    -    entries that correspond to one of the given accounts.
    -
    -    Args:
    -      directory: A string, the name of the root of the directory hierarchy to be searched.
    -      input_filename: The name of the file to be used for the Document directives. This is
    -        also used to resolve relative directory names.
    -      accounts_only: A set of valid accounts strings to search for.
    -      strict: A boolean, set to true if you want to generate errors on documents
    -        found in accounts not provided in accounts_only. This is only meaningful
    -        if accounts_only is specified.
    -    Returns:
    -      A list of new Document objects that were created from the files found, and a list
    -      of new errors generated.
    -
    -    """
    -    errors = []
    -
    -    # Compute the documents directory name relative to the beancount input
    -    # file itself.
    -    if not path.isabs(directory):
    -        input_directory = path.dirname(input_filename)
    -        directory = path.abspath(path.normpath(path.join(input_directory,
    -                                                         directory)))
    -
    -    # If the directory does not exist, just generate an error and return.
    -    if not path.exists(directory):
    -        meta = data.new_metadata(input_filename, 0)
    -        error = DocumentError(
    -            meta, "Document root '{}' does not exist".format(directory), None)
    -        return ([], [error])
    -
    -    # Walk the hierarchy of files.
    -    entries = []
    -    for root, account_name, dirs, files in account.walk(directory):
    -
    -        # Look for files that have a dated filename.
    -        for filename in files:
    -            match = re.match(r'(\d\d\d\d)-(\d\d)-(\d\d).(.*)', filename)
    -            if not match:
    -                continue
    -
    -            # If a restricting set of accounts was specified, skip document
    -            # directives found in accounts with no corresponding account name.
    -            if accounts_only is not None and not account_name in accounts_only:
    -                if strict:
    -                    if any(account_name.startswith(account) for account in accounts_only):
    -                        errors.append(DocumentError(
    -                            data.new_metadata(input_filename, 0),
    -                            "Document '{}' found in child account {}".format(
    -                                filename, account_name), None))
    -                    elif any(account.startswith(account_name) for account in accounts_only):
    -                        errors.append(DocumentError(
    -                            data.new_metadata(input_filename, 0),
    -                            "Document '{}' found in parent account {}".format(
    -                                filename, account_name), None))
    -                continue
    -
    -            # Create a new directive.
    -            meta = data.new_metadata(input_filename, 0)
    -            try:
    -                date = datetime.date(*map(int, match.group(1, 2, 3)))
    -            except ValueError as exc:
    -                errors.append(DocumentError(
    -                    data.new_metadata(input_filename, 0),
    -                    "Invalid date on document file '{}': {}".format(
    -                        filename, exc), None))
    -            else:
    -                entry = data.Document(meta, date, account_name, path.join(root, filename),
    -                                      data.EMPTY_SET, data.EMPTY_SET)
    -                entries.append(entry)
    -
    -    return (entries, errors)
    -
    +
    def find_documents(directory, input_filename, accounts_only=None, strict=False):
    +    """Find dated document files under the given directory.
    +
    +    If a restricting set of accounts is provided in 'accounts_only', only return
    +    entries that correspond to one of the given accounts.
    +
    +    Args:
    +      directory: A string, the name of the root of the directory hierarchy to be searched.
    +      input_filename: The name of the file to be used for the Document directives. This is
    +        also used to resolve relative directory names.
    +      accounts_only: A set of valid accounts strings to search for.
    +      strict: A boolean, set to true if you want to generate errors on documents
    +        found in accounts not provided in accounts_only. This is only meaningful
    +        if accounts_only is specified.
    +    Returns:
    +      A list of new Document objects that were created from the files found, and a list
    +      of new errors generated.
    +
    +    """
    +    errors = []
    +
    +    # Compute the documents directory name relative to the beancount input
    +    # file itself.
    +    if not path.isabs(directory):
    +        input_directory = path.dirname(input_filename)
    +        directory = path.abspath(path.normpath(path.join(input_directory, directory)))
    +
    +    # If the directory does not exist, just generate an error and return.
    +    if not path.exists(directory):
    +        meta = data.new_metadata(input_filename, 0)
    +        error = DocumentError(
    +            meta, "Document root '{}' does not exist".format(directory), None
    +        )
    +        return ([], [error])
    +
    +    # Walk the hierarchy of files.
    +    entries = []
    +    for root, account_name, dirs, files in account.walk(directory):
    +        # Look for files that have a dated filename.
    +        for filename in files:
    +            match = re.match(r"(\d\d\d\d)-(\d\d)-(\d\d).(.*)", filename)
    +            if not match:
    +                continue
    +
    +            # If a restricting set of accounts was specified, skip document
    +            # directives found in accounts with no corresponding account name.
    +            if accounts_only is not None and account_name not in accounts_only:
    +                if strict:
    +                    if any(account_name.startswith(account) for account in accounts_only):
    +                        errors.append(
    +                            DocumentError(
    +                                data.new_metadata(input_filename, 0),
    +                                "Document '{}' found in child account {}".format(
    +                                    filename, account_name
    +                                ),
    +                                None,
    +                            )
    +                        )
    +                    elif any(account.startswith(account_name) for account in accounts_only):
    +                        errors.append(
    +                            DocumentError(
    +                                data.new_metadata(input_filename, 0),
    +                                "Document '{}' found in parent account {}".format(
    +                                    filename, account_name
    +                                ),
    +                                None,
    +                            )
    +                        )
    +                continue
    +
    +            # Create a new directive.
    +            meta = data.new_metadata(input_filename, 0)
    +            try:
    +                date = datetime.date(*map(int, match.group(1, 2, 3)))
    +            except ValueError as exc:
    +                errors.append(
    +                    DocumentError(
    +                        data.new_metadata(input_filename, 0),
    +                        "Invalid date on document file '{}': {}".format(filename, exc),
    +                        None,
    +                    )
    +                )
    +            else:
    +                entry = data.Document(
    +                    meta,
    +                    date,
    +                    account_name,
    +                    path.join(root, filename),
    +                    data.EMPTY_SET,
    +                    data.EMPTY_SET,
    +                )
    +                entries.append(entry)
    +
    +    return (entries, errors)
    +
    @@ -1742,7 +1758,7 @@

    -beancount.ops.documents.process_documents(entries, options_map) +beancount.ops.documents.process_documents(entries, options_map)

    @@ -1789,40 +1805,40 @@

    Source code in beancount/ops/documents.py -
    def process_documents(entries, options_map):
    -    """Check files for document directives and create documents directives automatically.
    -
    -    Args:
    -      entries: A list of all directives parsed from the file.
    -      options_map: An options dict, as is output by the parser.
    -        We're using its 'filename' option to figure out relative path to
    -        search for documents.
    -    Returns:
    -      A pair of list of all entries (including new ones), and errors
    -      generated during the process of creating document directives.
    -    """
    -    filename = options_map["filename"]
    -
    -    # Detect filenames that should convert into entries.
    -    autodoc_entries = []
    -    autodoc_errors = []
    -    document_dirs = options_map['documents']
    -    if document_dirs:
    -        # Restrict to the list of valid accounts only.
    -        accounts = getters.get_accounts(entries)
    -
    -        # Accumulate all the entries.
    -        for directory in map(path.normpath, document_dirs):
    -            new_entries, new_errors = find_documents(directory, filename, accounts)
    -            autodoc_entries.extend(new_entries)
    -            autodoc_errors.extend(new_errors)
    -
    -    # Merge the two lists of entries and errors. Keep the entries sorted.
    -    entries.extend(autodoc_entries)
    -    entries.sort(key=data.entry_sortkey)
    -
    -    return (entries, autodoc_errors)
    -
    +
    def process_documents(entries, options_map):
    +    """Check files for document directives and create documents directives automatically.
    +
    +    Args:
    +      entries: A list of all directives parsed from the file.
    +      options_map: An options dict, as is output by the parser.
    +        We're using its 'filename' option to figure out relative path to
    +        search for documents.
    +    Returns:
    +      A pair of list of all entries (including new ones), and errors
    +      generated during the process of creating document directives.
    +    """
    +    filename = options_map["filename"]
    +
    +    # Detect filenames that should convert into entries.
    +    autodoc_entries = []
    +    autodoc_errors = []
    +    document_dirs = options_map["documents"]
    +    if document_dirs:
    +        # Restrict to the list of valid accounts only.
    +        accounts = getters.get_accounts(entries)
    +
    +        # Accumulate all the entries.
    +        for directory in map(path.normpath, document_dirs):
    +            new_entries, new_errors = find_documents(directory, filename, accounts)
    +            autodoc_entries.extend(new_entries)
    +            autodoc_errors.extend(new_errors)
    +
    +    # Merge the two lists of entries and errors. Keep the entries sorted.
    +    entries.extend(autodoc_entries)
    +    entries.sort(key=data.entry_sortkey)
    +
    +    return (entries, autodoc_errors)
    +
    @@ -1835,7 +1851,7 @@

    -beancount.ops.documents.verify_document_files_exist(entries, unused_options_map) +beancount.ops.documents.verify_document_files_exist(entries, unused_options_map)

    @@ -1879,26 +1895,27 @@

    Source code in beancount/ops/documents.py -
    def verify_document_files_exist(entries, unused_options_map):
    -    """Verify that the document entries point to existing files.
    -
    -    Args:
    -      entries: a list of directives whose documents need to be validated.
    -      unused_options_map: A parser options dict. We're not using it.
    -    Returns:
    -      The same list of entries, and a list of new errors, if any were encountered.
    -    """
    -    errors = []
    -    for entry in entries:
    -        if not isinstance(entry, data.Document):
    -            continue
    -        if not path.exists(entry.filename):
    -            errors.append(
    -                DocumentError(entry.meta,
    -                              'File does not exist: "{}"'.format(entry.filename),
    -                              entry))
    -    return entries, errors
    -
    +
    def verify_document_files_exist(entries, unused_options_map):
    +    """Verify that the document entries point to existing files.
    +
    +    Args:
    +      entries: a list of directives whose documents need to be validated.
    +      unused_options_map: A parser options dict. We're not using it.
    +    Returns:
    +      The same list of entries, and a list of new errors, if any were encountered.
    +    """
    +    errors = []
    +    for entry in entries:
    +        if not isinstance(entry, data.Document):
    +            continue
    +        if not path.exists(entry.filename):
    +            errors.append(
    +                DocumentError(
    +                    entry.meta, 'File does not exist: "{}"'.format(entry.filename), entry
    +                )
    +            )
    +    return entries, errors
    +
    @@ -1921,46 +1938,16 @@

    - beancount.ops.holdings - - - -

    - -
    - -

    Compute final holdings for a list of entries.

    - - - -
    - - - - - - - +

    + beancount.ops.find_prices -
    - - - -

    - -beancount.ops.holdings.Holding (tuple) - - - -

    +

    -

    Holding(account, number, currency, cost_number, cost_currency, book_value, market_value, price_number, price_date)

    - +

    A library of codes create price fetching jobs from strings and files.

    @@ -1974,117 +1961,26 @@

    -
    - - - -

    -beancount.ops.holdings.Holding.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/ops/holdings.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.ops.holdings.Holding.__new__(_cls, account, number, currency, cost_number, cost_currency, book_value, market_value, price_number, price_date) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of Holding(account, number, currency, cost_number, cost_currency, book_value, market_value, price_number, price_date)

    - -
    - -
    - - - -
    - - - -

    -beancount.ops.holdings.Holding.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/ops/holdings.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -

    - -
    - -
    - - -
    -

    -beancount.ops.holdings.aggregate_holdings_by(holdings, keyfun) +

    +beancount.ops.find_prices.find_balance_currencies(entries, date=None) -

    +

    -

    Aggregate holdings by some key.

    -

    Note that the cost-currency must always be included in the group-key (sums -over multiple currency units do not make sense), so it is appended to the -sort-key automatically.

    +

    Return currencies relevant for the given date.

    +

    This computes the account balances as of the date, and returns the union of: +a) The currencies held at cost, and +b) Currency pairs from previous conversions, but only for currencies with + non-zero balances.

    +

    This is intended to produce the list of currencies whose prices are relevant +at a particular date, based on previous history.

    @@ -2096,8 +1992,8 @@

    @@ -2113,50 +2009,58 @@

    Parameters:
      -
    • keyfun – A callable, which returns the key to aggregate by. This key need -not include the cost-currency.

    • +
    • entries – A list of directives.

    • +
    • date – A datetime.date instance.

    Returns:
      -
    • A list of aggregated holdings.

    • +
    • A set of (base, quote) currencies.

    - Source code in beancount/ops/holdings.py -
    def aggregate_holdings_by(holdings, keyfun):
    -    """Aggregate holdings by some key.
    -
    -    Note that the cost-currency must always be included in the group-key (sums
    -    over multiple currency units do not make sense), so it is appended to the
    -    sort-key automatically.
    -
    -    Args:
    -      keyfun: A callable, which returns the key to aggregate by. This key need
    -        not include the cost-currency.
    -    Returns:
    -      A list of aggregated holdings.
    -    """
    -    # Aggregate the groups of holdings.
    -    grouped = collections.defaultdict(list)
    -    for holding in holdings:
    -        key = (keyfun(holding), holding.cost_currency)
    -        grouped[key].append(holding)
    -    grouped_holdings = (aggregate_holdings_list(key_holdings)
    -                        for key_holdings in grouped.values())
    -
    -    # We could potentially filter out holdings with zero units here. These types
    -    # of holdings might occur on a group with leaked (i.e., non-zero) cost basis
    -    # and zero units. However, sometimes are valid merging of multiple
    -    # currencies may occur, and the number value will be legitimately set to
    -    # ZERO (for various reasons downstream), so we prefer not to ignore the
    -    # holding. Callers must be prepared to deal with a holding of ZERO units and
    -    # a non-zero cost basis. {0ed05c502e63, b/16}
    -    ## nonzero_holdings = (holding
    -    ##                     for holding in grouped_holdings
    -    ##                     if holding.number != ZERO)
    -
    -    # Return the holdings in order.
    -    return sorted(grouped_holdings,
    -                  key=lambda holding: (holding.account, holding.currency))
    -
    + Source code in beancount/ops/find_prices.py +
    def find_balance_currencies(entries, date=None):
    +    """Return currencies relevant for the given date.
    +
    +    This computes the account balances as of the date, and returns the union of:
    +    a) The currencies held at cost, and
    +    b) Currency pairs from previous conversions, but only for currencies with
    +       non-zero balances.
    +
    +    This is intended to produce the list of currencies whose prices are relevant
    +    at a particular date, based on previous history.
    +
    +    Args:
    +      entries: A list of directives.
    +      date: A datetime.date instance.
    +    Returns:
    +      A set of (base, quote) currencies.
    +    """
    +    # Compute the balances.
    +    currencies = set()
    +    currencies_on_books = set()
    +    balances, _ = summarize.balance_by_account(entries, date)
    +    for _, balance in balances.items():
    +        for pos in balance:
    +            if pos.cost is not None:
    +                # Add currencies held at cost.
    +                currencies.add((pos.units.currency, pos.cost.currency))
    +            else:
    +                # Add regular currencies.
    +                currencies_on_books.add(pos.units.currency)
    +
    +    # Create currency pairs from the currencies which are on account balances.
    +    # In order to figure out the quote currencies, we use the list of price
    +    # conversions until this date.
    +    converted = find_currencies_converted(entries, date) | find_currencies_priced(
    +        entries, date
    +    )
    +    for cbase in currencies_on_books:
    +        for base_quote in converted:
    +            base, quote = base_quote
    +            if base == cbase:
    +                currencies.add(base_quote)
    +
    +    return currencies
    +
    @@ -2168,20 +2072,17 @@

    -

    -beancount.ops.holdings.aggregate_holdings_list(holdings) +

    +beancount.ops.find_prices.find_currencies_at_cost(entries, date=None) -

    +

    -

    Aggregate a list of holdings.

    -

    If there are varying 'account', 'currency' or 'cost_currency' attributes, -their values are replaced by '*'. Otherwise they are preserved. Note that -all the cost-currency values must be equal in order for aggregations to -succeed (without this constraint a sum of units in different currencies has -no meaning).

    +

    Return all currencies that were held at cost at some point.

    +

    This returns all of them, even if not on the books at a particular point in +time. This code does not look at account balances.

    @@ -2193,7 +2094,8 @@

    @@ -2209,107 +2111,37 @@

    -
    Parameters:
      -
    • holdings – A list of Holding instances.

    • +
    • entries – A list of directives.

    • +
    • date – A datetime.date instance.

    Returns:
      -
    • A single Holding instance, or None, if there are no holdings in the input -list.

    • +
    • A list of (base, quote) currencies.

    - - - - - - - - - - -
    Exceptions: -
      -
    • ValueError – If multiple cost currencies encountered.

    • -
    -
    - Source code in beancount/ops/holdings.py -
    def aggregate_holdings_list(holdings):
    -    """Aggregate a list of holdings.
    -
    -    If there are varying 'account', 'currency' or 'cost_currency' attributes,
    -    their values are replaced by '*'. Otherwise they are preserved. Note that
    -    all the cost-currency values must be equal in order for aggregations to
    -    succeed (without this constraint a sum of units in different currencies has
    -    no meaning).
    -
    -    Args:
    -      holdings: A list of Holding instances.
    -    Returns:
    -      A single Holding instance, or None, if there are no holdings in the input
    -      list.
    -    Raises:
    -      ValueError: If multiple cost currencies encountered.
    -    """
    -    if not holdings:
    -        return None
    -
    -    # Note: Holding is a bit overspecified with book and market values. We
    -    # recompute them from cost and price numbers here anyhow.
    -    units, total_book_value, total_market_value = ZERO, ZERO, ZERO
    -    accounts = set()
    -    currencies = set()
    -    cost_currencies = set()
    -    price_dates = set()
    -    book_value_seen = False
    -    market_value_seen = False
    -    for holding in holdings:
    -        units += holding.number
    -        accounts.add(holding.account)
    -        price_dates.add(holding.price_date)
    -        currencies.add(holding.currency)
    -        cost_currencies.add(holding.cost_currency)
    -
    -        if holding.book_value is not None:
    -            total_book_value += holding.book_value
    -            book_value_seen = True
    -        elif holding.cost_number is not None:
    -            total_book_value += holding.number * holding.cost_number
    -            book_value_seen = True
    -
    -        if holding.market_value is not None:
    -            total_market_value += holding.market_value
    -            market_value_seen = True
    -        elif holding.price_number is not None:
    -            total_market_value += holding.number * holding.price_number
    -            market_value_seen = True
    -
    -    if book_value_seen:
    -        average_cost = total_book_value / units if units else None
    -    else:
    -        total_book_value = None
    -        average_cost = None
    -
    -    if market_value_seen:
    -        average_price = total_market_value / units if units else None
    -    else:
    -        total_market_value = None
    -        average_price = None
    -
    -    if len(cost_currencies) != 1:
    -        raise ValueError("Cost currencies are not homogeneous for aggregation: {}".format(
    -            ','.join(map(str, cost_currencies))))
    -
    -    units = units if len(currencies) == 1 else ZERO
    -    currency = currencies.pop() if len(currencies) == 1 else '*'
    -    cost_currency = cost_currencies.pop()
    -    account_ = (accounts.pop()
    -                if len(accounts) == 1
    -                else account.commonprefix(accounts))
    -    price_date = price_dates.pop() if len(price_dates) == 1 else None
    -    return Holding(account_, units, currency, average_cost, cost_currency,
    -                   total_book_value, total_market_value, average_price, price_date)
    -
    + Source code in beancount/ops/find_prices.py +
    def find_currencies_at_cost(entries, date=None):
    +    """Return all currencies that were held at cost at some point.
    +
    +    This returns all of them, even if not on the books at a particular point in
    +    time. This code does not look at account balances.
    +
    +    Args:
    +      entries: A list of directives.
    +      date: A datetime.date instance.
    +    Returns:
    +      A list of (base, quote) currencies.
    +    """
    +    currencies = set()
    +    for entry in entries:
    +        if not isinstance(entry, data.Transaction):
    +            continue
    +        if date and entry.date >= date:
    +            break
    +        for posting in entry.postings:
    +            if posting.cost is not None and posting.cost.number is not None:
    +                currencies.add((posting.units.currency, posting.cost.currency))
    +    return currencies
    +
    @@ -2321,16 +2153,18 @@

    -

    -beancount.ops.holdings.convert_to_currency(price_map, target_currency, holdings_list) +

    +beancount.ops.find_prices.find_currencies_converted(entries, date=None) -

    +

    -

    Convert the given list of holdings's fields to a common currency.

    -

    If the rate is not available to convert, leave the fields empty.

    +

    Return currencies from price conversions.

    +

    This function looks at all price conversions that occurred until some date +and produces a list of them. Note: This does not include Price directives, +only postings with price conversions.

    @@ -2342,9 +2176,8 @@

    @@ -2360,76 +2193,40 @@

    Parameters:
      -
    • price_map – A price-map, as built by prices.build_price_map().

    • -
    • target_currency – The target common currency to convert amounts to.

    • -
    • holdings_list – A list of holdings.Holding instances.

    • +
    • entries – A list of directives.

    • +
    • date – A datetime.date instance.

    Returns:
      -
    • A modified list of holdings, with the 'extra' field set to the value in -'currency', or None, if it was not possible to convert.

    • +
    • A list of (base, quote) currencies.

    - Source code in beancount/ops/holdings.py -
    def convert_to_currency(price_map, target_currency, holdings_list):
    -    """Convert the given list of holdings's fields to a common currency.
    -
    -    If the rate is not available to convert, leave the fields empty.
    -
    -    Args:
    -      price_map: A price-map, as built by prices.build_price_map().
    -      target_currency: The target common currency to convert amounts to.
    -      holdings_list: A list of holdings.Holding instances.
    -    Returns:
    -      A modified list of holdings, with the 'extra' field set to the value in
    -      'currency', or None, if it was not possible to convert.
    -    """
    -    # A list of the fields we should convert.
    -    convert_fields = ('cost_number', 'book_value', 'market_value', 'price_number')
    -
    -    new_holdings = []
    -    for holding in holdings_list:
    -        if holding.cost_currency == target_currency:
    -            # The holding is already priced in the target currency; do nothing.
    -            new_holding = holding
    -        else:
    -            if holding.cost_currency is None:
    -                # There is no cost currency; make the holding priced in its own
    -                # units. The price-map should yield a rate of 1.0 and everything
    -                # else works out.
    -                if holding.currency is None:
    -                    raise ValueError("Invalid currency '{}'".format(holding.currency))
    -                holding = holding._replace(cost_currency=holding.currency)
    -
    -                # Fill in with book and market value as well.
    -                if holding.book_value is None:
    -                    holding = holding._replace(book_value=holding.number)
    -                if holding.market_value is None:
    -                    holding = holding._replace(market_value=holding.number)
    -
    -            assert holding.cost_currency, "Missing cost currency: {}".format(holding)
    -            base_quote = (holding.cost_currency, target_currency)
    -
    -            # Get the conversion rate and replace the required numerical
    -            # fields..
    -            _, rate = prices.get_latest_price(price_map, base_quote)
    -            if rate is not None:
    -                new_holding = misc_utils.map_namedtuple_attributes(
    -                    convert_fields,
    -                    lambda number, r=rate: number if number is None else number * r,
    -                    holding)
    -                # Ensure we set the new cost currency after conversion.
    -                new_holding = new_holding._replace(cost_currency=target_currency)
    -            else:
    -                # Could not get the rate... clear every field and set the cost
    -                # currency to None. This enough marks the holding conversion as
    -                # a failure.
    -                new_holding = misc_utils.map_namedtuple_attributes(
    -                    convert_fields, lambda number: None, holding)
    -                new_holding = new_holding._replace(cost_currency=None)
    -
    -        new_holdings.append(new_holding)
    -
    -    return new_holdings
    -
    + Source code in beancount/ops/find_prices.py +
    def find_currencies_converted(entries, date=None):
    +    """Return currencies from price conversions.
    +
    +    This function looks at all price conversions that occurred until some date
    +    and produces a list of them. Note: This does not include Price directives,
    +    only postings with price conversions.
    +
    +    Args:
    +      entries: A list of directives.
    +      date: A datetime.date instance.
    +    Returns:
    +      A list of (base, quote) currencies.
    +    """
    +    currencies = set()
    +    for entry in entries:
    +        if not isinstance(entry, data.Transaction):
    +            continue
    +        if date and entry.date >= date:
    +            break
    +        for posting in entry.postings:
    +            price = posting.price
    +            if posting.cost is not None or price is None:
    +                continue
    +            currencies.add((posting.units.currency, price.currency))
    +    return currencies
    +
    @@ -2441,37 +2238,15 @@

    -

    -beancount.ops.holdings.get_commodities_at_date(entries, options_map, date=None) +

    +beancount.ops.find_prices.find_currencies_priced(entries, date=None) -

    +

    -

    Return a list of commodities present at a particular date.

    -

    This routine fetches the holdings present at a particular date and returns a -list of the commodities held in those holdings. This should define the list -of price date points required to assess the market value of this portfolio.

    -

    Notes:

    -
      -
    • -

      The ticker symbol will be fetched from the corresponding Commodity - directive. If there is no ticker symbol defined for a directive or no - corresponding Commodity directive, the currency is still included, but - 'None' is specified for the symbol. The code that uses this routine should - be free to use the currency name to make an attempt to fetch the currency - using its name, or to ignore it.

      -
    • -
    • -

      The 'cost-currency' is that which is found on the holdings instance and - can be ignored. The 'quote-currency' is that which is declared on the - Commodity directive from its 'quote' metadata field.

      -
    • -
    -

    This is used in a routine that fetches prices from a data source on the -internet (either from LedgerHub, but you can reuse this in your own script -if you build one).

    +

    Return currencies seen in Price directives.

    @@ -2484,8 +2259,7 @@

    @@ -2501,85 +2275,32 @@

    • entries – A list of directives.

    • -
    • date – A datetime.date instance, the date at which to get the list of -relevant holdings.

    • +
    • date – A datetime.date instance.

    Returns:
      -
    • A list of (currency, cost-currency, quote-currency, ticker) tuples, where - currency – The Beancount base currency to fetch a price for. - cost-currency: The cost-currency of the holdings found at the given date. - quote-currency: The currency formally declared as quote currency in the - metadata of Commodity directives. - ticker: The ticker symbol to use for fetching the price (extracted from - the metadata of Commodity directives).

    • +
    • A list of (base, quote) currencies.

    - Source code in beancount/ops/holdings.py -
    def get_commodities_at_date(entries, options_map, date=None):
    -    """Return a list of commodities present at a particular date.
    -
    -    This routine fetches the holdings present at a particular date and returns a
    -    list of the commodities held in those holdings. This should define the list
    -    of price date points required to assess the market value of this portfolio.
    -
    -    Notes:
    -
    -    * The ticker symbol will be fetched from the corresponding Commodity
    -      directive. If there is no ticker symbol defined for a directive or no
    -      corresponding Commodity directive, the currency is still included, but
    -      'None' is specified for the symbol. The code that uses this routine should
    -      be free to use the currency name to make an attempt to fetch the currency
    -      using its name, or to ignore it.
    -
    -    * The 'cost-currency' is that which is found on the holdings instance and
    -      can be ignored. The 'quote-currency' is that which is declared on the
    -      Commodity directive from its 'quote' metadata field.
    -
    -    This is used in a routine that fetches prices from a data source on the
    -    internet (either from LedgerHub, but you can reuse this in your own script
    -    if you build one).
    -
    -    Args:
    -      entries: A list of directives.
    -      date: A datetime.date instance, the date at which to get the list of
    -        relevant holdings.
    -    Returns:
    -      A list of (currency, cost-currency, quote-currency, ticker) tuples, where
    -        currency: The Beancount base currency to fetch a price for.
    -        cost-currency: The cost-currency of the holdings found at the given date.
    -        quote-currency: The currency formally declared as quote currency in the
    -          metadata of Commodity directives.
    -        ticker: The ticker symbol to use for fetching the price (extracted from
    -          the metadata of Commodity directives).
    -    """
    -    # Remove all the entries after the given date, if requested.
    -    if date is not None:
    -        entries = summarize.truncate(entries, date)
    -
    -    # Get the list of holdings at the particular date.
    -    holdings_list = get_final_holdings(entries)
    -
    -    # Obtain the unique list of currencies we need to fetch.
    -    commodities_list = {(holding.currency, holding.cost_currency)
    -                        for holding in holdings_list}
    -
    -    # Add in the associated ticker symbols.
    -    commodities_map = getters.get_commodity_map(entries)
    -    commodities_symbols_list = []
    -    for currency, cost_currency in sorted(commodities_list):
    -        try:
    -            commodity_entry = commodities_map[currency]
    -            ticker = commodity_entry.meta.get('ticker', None)
    -            quote_currency = commodity_entry.meta.get('quote', None)
    -        except KeyError:
    -            ticker = None
    -            quote_currency = None
    -
    -        commodities_symbols_list.append(
    -            (currency, cost_currency, quote_currency, ticker))
    -
    -    return commodities_symbols_list
    -
    + Source code in beancount/ops/find_prices.py +
    def find_currencies_priced(entries, date=None):
    +    """Return currencies seen in Price directives.
    +
    +    Args:
    +      entries: A list of directives.
    +      date: A datetime.date instance.
    +    Returns:
    +      A list of (base, quote) currencies.
    +    """
    +    currencies = set()
    +    for entry in entries:
    +        if not isinstance(entry, data.Price):
    +            continue
    +        if date and entry.date >= date:
    +            break
    +        currencies.add((entry.currency, entry.amount.currency))
    +    return currencies
    +
    @@ -2587,295 +2308,47 @@

    -
    - - -

    -beancount.ops.holdings.get_final_holdings(entries, included_account_types=None, price_map=None, date=None) -

    - -
    - -

    Get a dictionary of the latest holdings by account.

    -

    This basically just flattens the balance sheet's final positions, including -that of equity accounts. If a 'price_map' is provided, insert price -information in the flattened holdings at the latest date, or at the given -date, if one is provided.

    -

    Only the accounts in 'included_account_types' will be included, and this is -always called for Assets and Liabilities only. If left unspecified, holdings -from all account types will be included, including Equity, Income and -Expenses.

    +
    - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • included_account_types – A sequence of strings, the account types to -include in the output. A reasonable example would be -('Assets', 'Liabilities'). If not specified, include all account types.

    • -
    • price_map – A dict of prices, as built by prices.build_price_map().

    • -
    • date – A datetime.date instance, the date at which to price the -holdings. If left unspecified, we use the latest price information.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of dicts, with the following fields

    • -
    -
    -
    - Source code in beancount/ops/holdings.py -
    def get_final_holdings(entries, included_account_types=None, price_map=None, date=None):
    -    """Get a dictionary of the latest holdings by account.
    -
    -    This basically just flattens the balance sheet's final positions, including
    -    that of equity accounts. If a 'price_map' is provided, insert price
    -    information in the flattened holdings at the latest date, or at the given
    -    date, if one is provided.
    -
    -    Only the accounts in 'included_account_types' will be included, and this is
    -    always called for Assets and Liabilities only. If left unspecified, holdings
    -    from all account types will be included, including Equity, Income and
    -    Expenses.
    -
    -    Args:
    -      entries: A list of directives.
    -      included_account_types: A sequence of strings, the account types to
    -        include in the output. A reasonable example would be
    -        ('Assets', 'Liabilities'). If not specified, include all account types.
    -      price_map: A dict of prices, as built by prices.build_price_map().
    -      date: A datetime.date instance, the date at which to price the
    -        holdings. If left unspecified, we use the latest price information.
    -    Returns:
    -      A list of dicts, with the following fields:
    -    """
    -    # Remove the entries inserted by unrealized gains/losses. Those entries do
    -    # affect asset accounts, and we don't want them to appear in holdings.
    -    #
    -    # Note: Perhaps it would make sense to generalize this concept of "inserted
    -    # unrealized gains."
    -    simple_entries = [entry
    -                      for entry in entries
    -                      if (not isinstance(entry, data.Transaction) or
    -                          entry.flag != flags.FLAG_UNREALIZED)]
    -
    -    # Realize the accounts into a tree (because we want the positions by-account).
    -    root_account = realization.realize(simple_entries)
    -
    -    # For each account, look at the list of positions and build a list.
    -    holdings = []
    -    for real_account in sorted(list(realization.iter_children(root_account)),
    -                               key=lambda ra: ra.account):
    -
    -        if included_account_types:
    -            # Skip accounts of invalid types, we only want to reflect the requested
    -            # account types, typically assets and liabilities.
    -            account_type = account_types.get_account_type(real_account.account)
    -            if account_type not in included_account_types:
    -                continue
    -
    -        for pos in real_account.balance.get_positions():
    -            if pos.cost is not None:
    -                # Get price information if we have a price_map.
    -                market_value = None
    -                if price_map is not None:
    -                    base_quote = (pos.units.currency, pos.cost.currency)
    -                    price_date, price_number = prices.get_price(price_map,
    -                                                                base_quote, date)
    -                    if price_number is not None:
    -                        market_value = pos.units.number * price_number
    -                else:
    -                    price_date, price_number = None, None
    -
    -                holding = Holding(real_account.account,
    -                                  pos.units.number,
    -                                  pos.units.currency,
    -                                  pos.cost.number,
    -                                  pos.cost.currency,
    -                                  pos.units.number * pos.cost.number,
    -                                  market_value,
    -                                  price_number,
    -                                  price_date)
    -            else:
    -                holding = Holding(real_account.account,
    -                                  pos.units.number,
    -                                  pos.units.currency,
    -                                  None,
    -                                  pos.units.currency,
    -                                  pos.units.number,
    -                                  pos.units.number,
    -                                  None,
    -                                  None)
    -            holdings.append(holding)
    -
    -    return holdings
    -
    -
    -
    - +
    -

    -beancount.ops.holdings.holding_to_position(holding) +

    + beancount.ops.lifetimes -

    -
    -

    Convert the holding to a position.

    +

    - - - - - - - - - - - -
    Parameters: -
      -
    • holding – An instance of Holding.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Position.

    • -
    -
    -
    - Source code in beancount/ops/holdings.py -
    def holding_to_position(holding):
    -    """Convert the holding to a position.
    -
    -    Args:
    -      holding: An instance of Holding.
    -    Returns:
    -      An instance of Position.
    -    """
    -    return position.Position(
    -        amount.Amount(holding.number, holding.currency),
    -        (position.Cost(holding.cost_number, holding.cost_currency, None, None)
    -         if holding.cost_number
    -         else None))
    -
    -
    - +
    -
    +

    Given a Beancount ledger, compute time intervals where we hold each commodity.

    +

    This script computes, for each commodity, which time intervals it is required at. +This can then be used to identify a list of dates at which we need to fetch prices +in order to properly fill the price database.

    -
    +
    -

    -beancount.ops.holdings.holding_to_posting(holding) -

    -
    -

    Convert the holding to an instance of Posting.

    - - - - - - - - - - - -
    Parameters: -
      -
    • holding – An instance of Holding.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Position.

    • -
    -
    -
    - Source code in beancount/ops/holdings.py -
    def holding_to_posting(holding):
    -    """Convert the holding to an instance of Posting.
    -
    -    Args:
    -      holding: An instance of Holding.
    -    Returns:
    -      An instance of Position.
    -    """
    -    position_ = holding_to_position(holding)
    -    price = (amount.Amount(holding.price_number, holding.cost_currency)
    -             if holding.price_number
    -             else None)
    -    return data.Posting(holding.account, position_.units, position_.cost, price, None, None)
    -
    -
    -
    -
    @@ -2883,15 +2356,15 @@

    -

    -beancount.ops.holdings.reduce_relative(holdings) +

    +beancount.ops.lifetimes.compress_intervals_days(intervals, num_days) -

    +

    -

    Convert the market and book values of the given list of holdings to relative data.

    +

    Compress a list of date pairs to ignore short stretches of unused days.

    @@ -2903,7 +2376,9 @@

    @@ -2919,62 +2394,38 @@

    Parameters:
      -
    • holdings – A list of Holding instances.

    • +
    • intervals – A list of pairs of datetime.date instances.

    • +
    • num_days – An integer, the number of unused days to require for intervals +to be distinct, to allow a gap.

    Returns:
      -
    • A list of holdings instances with the absolute value fields replaced by -fractions of total portfolio. The new list of holdings is sorted by -currency, and the relative fractions are also relative to that currency.

    • +
    • A new dict of lifetimes map where some intervals may have been joined.

    - Source code in beancount/ops/holdings.py -
    def reduce_relative(holdings):
    -    """Convert the market and book values of the given list of holdings to relative data.
    -
    -    Args:
    -      holdings: A list of Holding instances.
    -    Returns:
    -      A list of holdings instances with the absolute value fields replaced by
    -      fractions of total portfolio. The new list of holdings is sorted by
    -      currency, and the relative fractions are also relative to that currency.
    -    """
    -    # Group holdings by value currency.
    -    by_currency = collections.defaultdict(list)
    -    ordering = {}
    -    for index, holding in enumerate(holdings):
    -        ordering.setdefault(holding.cost_currency, index)
    -        by_currency[holding.cost_currency].append(holding)
    -
    -    fractional_holdings = []
    -    for currency in sorted(by_currency, key=ordering.get):
    -        currency_holdings = by_currency[currency]
    -
    -        # Compute total market value for that currency.
    -        total_book_value = ZERO
    -        total_market_value = ZERO
    -        for holding in currency_holdings:
    -            if holding.book_value:
    -                total_book_value += holding.book_value
    -            if holding.market_value:
    -                total_market_value += holding.market_value
    -
    -        # Sort the currency's holdings with decreasing values of market value.
    -        currency_holdings.sort(
    -            key=lambda holding: holding.market_value or ZERO,
    -            reverse=True)
    -
    -        # Output new holdings with the relevant values replaced.
    -        for holding in currency_holdings:
    -            fractional_holdings.append(
    -                holding._replace(book_value=(holding.book_value / total_book_value
    -                                             if holding.book_value is not None
    -                                             else None),
    -                                 market_value=(holding.market_value / total_market_value
    -                                               if holding.market_value is not None
    -                                               else None)))
    -    return fractional_holdings
    -
    + Source code in beancount/ops/lifetimes.py +
    def compress_intervals_days(intervals, num_days):
    +    """Compress a list of date pairs to ignore short stretches of unused days.
    +
    +    Args:
    +      intervals: A list of pairs of datetime.date instances.
    +      num_days: An integer, the number of unused days to require for intervals
    +        to be distinct, to allow a gap.
    +    Returns:
    +      A new dict of lifetimes map where some intervals may have been joined.
    +    """
    +    ignore_interval = datetime.timedelta(days=num_days)
    +    new_intervals = []
    +    iter_intervals = iter(intervals)
    +    last_begin, last_end = next(iter_intervals)
    +    for date_begin, date_end in iter_intervals:
    +        if date_begin - last_end < ignore_interval:
    +            # Compress.
    +            last_end = date_end
    +            continue
    +        new_intervals.append((last_begin, last_end))
    +        last_begin, last_end = date_begin, date_end
    +    new_intervals.append((last_begin, last_end))
    +    return new_intervals
    +
    @@ -2986,15 +2437,15 @@

    -

    -beancount.ops.holdings.scale_holding(holding, scale_factor) +

    +beancount.ops.lifetimes.compress_lifetimes_days(lifetimes_map, num_days) -

    +

    -

    Scale the values of a holding.

    +

    Compress a lifetimes map to ignore short stretches of unused days.

    @@ -3006,8 +2457,8 @@

    @@ -3023,34 +2474,28 @@

    Parameters:
      -
    • holding – An instance of Holding.

    • -
    • scale_factor – A float or Decimal number.

    • +
    • lifetimes_map – A dict of currency intervals as returned by get_commodity_lifetimes.

    • +
    • num_days – An integer, the number of unused days to ignore.

    Returns:
      -
    • A scaled copy of the holding.

    • +
    • A new dict of lifetimes map where some intervals may have been joined.

    - Source code in beancount/ops/holdings.py -
    def scale_holding(holding, scale_factor):
    -    """Scale the values of a holding.
    -
    -    Args:
    -      holding: An instance of Holding.
    -      scale_factor: A float or Decimal number.
    -    Returns:
    -      A scaled copy of the holding.
    -    """
    -    return Holding(
    -        holding.account,
    -        holding.number * scale_factor if holding.number else None,
    -        holding.currency,
    -        holding.cost_number,
    -        holding.cost_currency,
    -        holding.book_value * scale_factor if holding.book_value else None,
    -        holding.market_value * scale_factor if holding.market_value else None,
    -        holding.price_number,
    -        holding.price_date)
    -
    + Source code in beancount/ops/lifetimes.py +
    def compress_lifetimes_days(lifetimes_map, num_days):
    +    """Compress a lifetimes map to ignore short stretches of unused days.
    +
    +    Args:
    +      lifetimes_map: A dict of currency intervals as returned by get_commodity_lifetimes.
    +      num_days: An integer, the number of unused days to ignore.
    +    Returns:
    +      A new dict of lifetimes map where some intervals may have been joined.
    +    """
    +    return {
    +        currency_pair: compress_intervals_days(intervals, num_days)
    +        for currency_pair, intervals in lifetimes_map.items()
    +    }
    +
    @@ -3058,63 +2503,19 @@

    - - - - - - - - - - - -
    - - - -

    - beancount.ops.lifetimes - - - -

    - -
    - -

    Given a Beancount ledger, compute time intervals where we hold each commodity.

    -

    This script computes, for each commodity, which time intervals it is required at. -This can then be used to identify a list of dates at which we need to fetch prices -in order to properly fill the price database.

    - - - -
    - - - - - - - - - - - -
    -

    -beancount.ops.lifetimes.compress_intervals_days(intervals, num_days) +

    +beancount.ops.lifetimes.get_commodity_lifetimes(entries) -

    +

    -

    Compress a list of date pairs to ignore short stretches of unused days.

    +

    Given a list of directives, figure out the life of each commodity.

    @@ -3126,9 +2527,7 @@

    Parameters:

    @@ -3144,7 +2543,9 @@

    Returns:

    @@ -3152,30 +2553,64 @@

    Source code in beancount/ops/lifetimes.py -
    def compress_intervals_days(intervals, num_days):
    -    """Compress a list of date pairs to ignore short stretches of unused days.
    -
    -    Args:
    -      intervals: A list of pairs of datetime.date instances.
    -      num_days: An integer, the number of unused days to require for intervals
    -        to be distinct, to allow a gap.
    -    Returns:
    -      A new dict of lifetimes map where some intervals may have been joined.
    -    """
    -    ignore_interval = datetime.timedelta(days=num_days)
    -    new_intervals = []
    -    iter_intervals = iter(intervals)
    -    last_begin, last_end = next(iter_intervals)
    -    for date_begin, date_end in iter_intervals:
    -        if date_begin - last_end < ignore_interval:
    -            # Compress.
    -            last_end = date_end
    -            continue
    -        new_intervals.append((last_begin, last_end))
    -        last_begin, last_end = date_begin, date_end
    -    new_intervals.append((last_begin, last_end))
    -    return new_intervals
    -
    +
    def get_commodity_lifetimes(entries):
    +    """Given a list of directives, figure out the life of each commodity.
    +
    +    Args:
    +      entries: A list of directives.
    +    Returns:
    +      A dict of (currency, cost-currency) commodity strings to lists of (start,
    +      end) datetime.date pairs. The dates are inclusive of the day the commodity
    +      was seen; the end/last dates are one day _after_ the last date seen.
    +    """
    +    lifetimes = collections.defaultdict(list)
    +
    +    # The current set of active commodities.
    +    commodities = set()
    +
    +    # The current balances across all accounts.
    +    balances = collections.defaultdict(inventory.Inventory)
    +
    +    for entry in entries:
    +        # Process only transaction entries.
    +        if not isinstance(entry, data.Transaction):
    +            continue
    +
    +        # Update the balance of affected accounts and check locally whether that
    +        # triggered a change in the set of commodities.
    +        commodities_changed = False
    +        for posting in entry.postings:
    +            balance = balances[posting.account]
    +            commodities_before = balance.currency_pairs()
    +            balance.add_position(posting)
    +            commodities_after = balance.currency_pairs()
    +            if commodities_after != commodities_before:
    +                commodities_changed = True
    +
    +        # If there was a change in one of the affected account's list of
    +        # commodities, recompute the total set globally. This should not
    +        # occur very frequently.
    +        if commodities_changed:
    +            new_commodities = set(
    +                itertools.chain(*(inv.currency_pairs() for inv in balances.values()))
    +            )
    +            if new_commodities != commodities:
    +                # The new global set of commodities has changed; update our
    +                # the dictionary of intervals.
    +                for currency in new_commodities - commodities:
    +                    lifetimes[currency].append((entry.date, None))
    +
    +                for currency in commodities - new_commodities:
    +                    lifetime = lifetimes[currency]
    +                    begin_date, end_date = lifetime.pop(-1)
    +                    assert end_date is None
    +                    lifetime.append((begin_date, entry.date + ONEDAY))
    +
    +                # Update our current set.
    +                commodities = new_commodities
    +
    +    return lifetimes
    +
    @@ -3187,15 +2622,19 @@

    -beancount.ops.lifetimes.compress_lifetimes_days(lifetimes_map, num_days) +

    +beancount.ops.lifetimes.required_daily_prices(lifetimes_map, date_last, weekdays_only=False) -

    +
    -

    Compress a lifetimes map to ignore short stretches of unused days.

    +

    Enumerate all the commodities and days where the price is required.

    +

    Given a map of lifetimes for a set of commodities, enumerate all the days +for each commodity where it is active. This can be used to connect to a +historical price fetcher routine to fill in missing price entries from an +existing ledger.

      -
    • intervals – A list of pairs of datetime.date instances.

    • -
    • num_days – An integer, the number of unused days to require for intervals -to be distinct, to allow a gap.

    • +
    • entries – A list of directives.

      -
    • A new dict of lifetimes map where some intervals may have been joined.

    • +
    • A dict of (currency, cost-currency) commodity strings to lists of (start, +end) datetime.date pairs. The dates are inclusive of the day the commodity +was seen; the end/last dates are one day after the last date seen.

    @@ -3207,8 +2646,10 @@

    Parameters:

    @@ -3224,7 +2665,7 @@

    Returns:

    @@ -3232,18 +2673,46 @@

    Source code in beancount/ops/lifetimes.py -
    def compress_lifetimes_days(lifetimes_map, num_days):
    -    """Compress a lifetimes map to ignore short stretches of unused days.
    -
    -    Args:
    -      lifetimes_map: A dict of currency intervals as returned by get_commodity_lifetimes.
    -      num_days: An integer, the number of unused days to ignore.
    -    Returns:
    -      A new dict of lifetimes map where some intervals may have been joined.
    -    """
    -    return {currency_pair: compress_intervals_days(intervals, num_days)
    -            for currency_pair, intervals in lifetimes_map.items()}
    -
    +
    def required_daily_prices(lifetimes_map, date_last, weekdays_only=False):
    +    """Enumerate all the commodities and days where the price is required.
    +
    +    Given a map of lifetimes for a set of commodities, enumerate all the days
    +    for each commodity where it is active. This can be used to connect to a
    +    historical price fetcher routine to fill in missing price entries from an
    +    existing ledger.
    +
    +    Args:
    +      lifetimes_map: A dict of currency to active intervals as returned by
    +        get_commodity_lifetimes().
    +      date_last: A datetime.date instance, the last date which we're interested in.
    +      weekdays_only: Option to limit fetching to weekdays only.
    +    Returns:
    +      Tuples of (date, currency, cost-currency).
    +    """
    +    results = []
    +    for currency_pair, intervals in lifetimes_map.items():
    +        if currency_pair[1] is None:
    +            continue
    +        for date_begin, date_end in intervals:
    +            # Find first Weekday starting on or before minimum date.
    +            date = date_begin
    +            if weekdays_only:
    +                diff_days = 4 - date_begin.weekday()
    +                if diff_days < 0:
    +                    date += datetime.timedelta(days=diff_days)
    +
    +            # Iterate over all weekdays.
    +            if date_end is None:
    +                date_end = date_last
    +            while date < date_end:
    +                results.append((date, currency_pair[0], currency_pair[1]))
    +                if weekdays_only and date.weekday() == 4:
    +                    date += 3 * ONEDAY
    +                else:
    +                    date += ONEDAY
    +
    +    return sorted(results)
    +
    @@ -3255,15 +2724,19 @@

    -beancount.ops.lifetimes.get_commodity_lifetimes(entries) +

    +beancount.ops.lifetimes.required_weekly_prices(lifetimes_map, date_last) -

    +
    -

    Given a list of directives, figure out the life of each commodity.

    +

    Enumerate all the commodities and Fridays where the price is required.

    +

    Given a map of lifetimes for a set of commodities, enumerate all the Fridays +for each commodity where it is active. This can be used to connect to a +historical price fetcher routine to fill in missing price entries from an +existing ledger.

      -
    • lifetimes_map – A dict of currency intervals as returned by get_commodity_lifetimes.

    • -
    • num_days – An integer, the number of unused days to ignore.

    • +
    • lifetimes_map – A dict of currency to active intervals as returned by +get_commodity_lifetimes().

    • +
    • date_last – A datetime.date instance, the last date which we're interested in.

    • +
    • weekdays_only – Option to limit fetching to weekdays only.

      -
    • A new dict of lifetimes map where some intervals may have been joined.

    • +
    • Tuples of (date, currency, cost-currency).

    @@ -3275,7 +2748,9 @@

    Parameters:

    @@ -3291,9 +2766,7 @@

    Returns:

    @@ -3301,63 +2774,40 @@

    Source code in beancount/ops/lifetimes.py -
    def get_commodity_lifetimes(entries):
    -    """Given a list of directives, figure out the life of each commodity.
    -
    -    Args:
    -      entries: A list of directives.
    -    Returns:
    -      A dict of (currency, cost-currency) commodity strings to lists of (start,
    -      end) datetime.date pairs. The dates are inclusive of the day the commodity
    -      was seen; the end/last dates are one day _after_ the last date seen.
    -    """
    -    lifetimes = collections.defaultdict(list)
    -
    -    # The current set of active commodities.
    -    commodities = set()
    -
    -    # The current balances across all accounts.
    -    balances = collections.defaultdict(inventory.Inventory)
    -
    -    for entry in entries:
    -        # Process only transaction entries.
    -        if not isinstance(entry, data.Transaction):
    -            continue
    -
    -        # Update the balance of affected accounts and check locally whether that
    -        # triggered a change in the set of commodities.
    -        commodities_changed = False
    -        for posting in entry.postings:
    -            balance = balances[posting.account]
    -            commodities_before = balance.currency_pairs()
    -            balance.add_position(posting)
    -            commodities_after = balance.currency_pairs()
    -            if commodities_after != commodities_before:
    -                commodities_changed = True
    -
    -        # If there was a change in one of the affected account's list of
    -        # commodities, recompute the total set globally. This should not
    -        # occur very frequently.
    -        if commodities_changed:
    -            new_commodities = set(
    -                itertools.chain(*(inv.currency_pairs() for inv in balances.values())))
    -            if new_commodities != commodities:
    -                # The new global set of commodities has changed; update our
    -                # the dictionary of intervals.
    -                for currency in new_commodities - commodities:
    -                    lifetimes[currency].append((entry.date, None))
    -
    -                for currency in commodities - new_commodities:
    -                    lifetime = lifetimes[currency]
    -                    begin_date, end_date = lifetime.pop(-1)
    -                    assert end_date is None
    -                    lifetime.append((begin_date, entry.date + ONEDAY))
    -
    -                # Update our current set.
    -                commodities = new_commodities
    -
    -    return lifetimes
    -
    +
    def required_weekly_prices(lifetimes_map, date_last):
    +    """Enumerate all the commodities and Fridays where the price is required.
    +
    +    Given a map of lifetimes for a set of commodities, enumerate all the Fridays
    +    for each commodity where it is active. This can be used to connect to a
    +    historical price fetcher routine to fill in missing price entries from an
    +    existing ledger.
    +
    +    Args:
    +      lifetimes_map: A dict of currency to active intervals as returned by
    +        get_commodity_lifetimes().
    +      date_last: A datetime.date instance, the last date which we're interested in.
    +    Returns:
    +      Tuples of (date, currency, cost-currency).
    +    """
    +    results = []
    +    for currency_pair, intervals in lifetimes_map.items():
    +        if currency_pair[1] is None:
    +            continue
    +        for date_begin, date_end in intervals:
    +            # Find first Friday before the minimum date.
    +            diff_days = 4 - date_begin.weekday()
    +            if diff_days >= 1:
    +                diff_days -= 7
    +            date = date_begin + datetime.timedelta(days=diff_days)
    +
    +            # Iterate over all Fridays.
    +            if date_end is None:
    +                date_end = date_last
    +            while date < date_end:
    +                results.append((date, currency_pair[0], currency_pair[1]))
    +                date += ONE_WEEK
    +    return sorted(results)
    +
    @@ -3369,19 +2819,16 @@

    -beancount.ops.lifetimes.required_weekly_prices(lifetimes_map, date_last) +

    +beancount.ops.lifetimes.trim_intervals(intervals, trim_start=None, trim_end=None) -

    +
    -

    Enumerate all the commodities and Fridays where the price is required.

    -

    Given a map of lifetimes for a set of commodities, enumerate all the Fridays -for each commodity where it is active. This can be used to connect to a -historical price fetcher routine to fill in missing price entries from an -existing ledger.

    +

    Trim a list of date pairs to be within a start and end date. +Useful in update-style price fetching.

      -
    • entries – A list of directives.

    • +
    • lifetimes_map – A dict of currency to active intervals as returned by +get_commodity_lifetimes().

    • +
    • date_last – A datetime.date instance, the last date which we're interested in.

      -
    • A dict of (currency, cost-currency) commodity strings to lists of (start, -end) datetime.date pairs. The dates are inclusive of the day the commodity -was seen; the end/last dates are one day after the last date seen.

    • +
    • Tuples of (date, currency, cost-currency).

    @@ -3393,9 +2840,9 @@

    @@ -3411,7 +2858,7 @@

    @@ -3419,40 +2866,33 @@

    Parameters:
      -
    • lifetimes_map – A dict of currency to active intervals as returned by -get_commodity_lifetimes().

    • -
    • date_last – A datetime.date instance, the last date which we're interested in.

    • +
    • intervals – A list of pairs of datetime.date instances

    • +
    • trim_start – An inclusive starting date.

    • +
    • trim_end – An exclusive starting date.

    Returns:
      -
    • Tuples of (date, currency, cost-currency).

    • +
    • A list of new intervals (pairs of (date, date)).

    Source code in beancount/ops/lifetimes.py -
    def required_weekly_prices(lifetimes_map, date_last):
    -    """Enumerate all the commodities and Fridays where the price is required.
    -
    -    Given a map of lifetimes for a set of commodities, enumerate all the Fridays
    -    for each commodity where it is active. This can be used to connect to a
    -    historical price fetcher routine to fill in missing price entries from an
    -    existing ledger.
    -
    -    Args:
    -      lifetimes_map: A dict of currency to active intervals as returned by
    -        get_commodity_lifetimes().
    -      date_last: A datetime.date instance, the last date which we're interested in.
    -    Returns:
    -      Tuples of (date, currency, cost-currency).
    -    """
    -    results = []
    -    for currency_pair, intervals in lifetimes_map.items():
    -        if currency_pair[1] is None:
    -            continue
    -        for date_begin, date_end in intervals:
    -            # Find first Friday before the minimum date.
    -            diff_days = 4 - date_begin.weekday()
    -            if diff_days > 1:
    -                diff_days -= 7
    -            date = date_begin + datetime.timedelta(days=diff_days)
    -
    -            # Iterate over all Fridays.
    -            if date_end is None:
    -                date_end = date_last
    -            while date < date_end:
    -                results.append((date, currency_pair[0], currency_pair[1]))
    -                date += ONE_WEEK
    -    return sorted(results)
    -
    +
    def trim_intervals(intervals, trim_start=None, trim_end=None):
    +    """Trim a list of date pairs to be within a start and end date.
    +    Useful in update-style price fetching.
    +
    +    Args:
    +      intervals: A list of pairs of datetime.date instances
    +      trim_start: An inclusive starting date.
    +      trim_end: An exclusive starting date.
    +    Returns:
    +      A list of new intervals (pairs of (date, date)).
    +    """
    +    new_intervals = []
    +    iter_intervals = iter(intervals)
    +    if trim_start is not None and trim_end is not None and trim_end < trim_start:
    +        raise ValueError("Trim end date is before start date")
    +
    +    for date_begin, date_end in iter_intervals:
    +        if trim_start is not None and trim_start > date_begin:
    +            date_begin = trim_start
    +        if trim_end is not None:
    +            if date_end is None or trim_end < date_end:
    +                date_end = trim_end
    +
    +        if date_end is None or date_begin <= date_end:
    +            new_intervals.append((date_begin, date_end))
    +    return new_intervals
    +
    @@ -3534,7 +2974,7 @@

    -beancount.ops.pad.PadError.__getnewargs__(self) +beancount.ops.pad.PadError.__getnewargs__(self) special @@ -3548,10 +2988,10 @@

    Source code in beancount/ops/pad.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -3564,7 +3004,7 @@

    -beancount.ops.pad.PadError.__new__(_cls, source, message, entry) +beancount.ops.pad.PadError.__new__(_cls, source, message, entry) special @@ -3588,7 +3028,7 @@

    -beancount.ops.pad.PadError.__repr__(self) +beancount.ops.pad.PadError.__repr__(self) special @@ -3602,10 +3042,10 @@

    Source code in beancount/ops/pad.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -3629,7 +3069,7 @@

    -beancount.ops.pad.pad(entries, options_map) +beancount.ops.pad.pad(entries, options_map)

    @@ -3682,155 +3122,182 @@

    Source code in beancount/ops/pad.py -
    def pad(entries, options_map):
    -    """Insert transaction entries for to fulfill a subsequent balance check.
    -
    -    Synthesize and insert Transaction entries right after Pad entries in order
    -    to fulfill checks in the padded accounts. Returns a new list of entries.
    -    Note that this doesn't pad across parent-child relationships, it is a very
    -    simple kind of pad. (I have found this to be sufficient in practice, and
    -    simpler to implement and understand.)
    -
    -    Furthermore, this pads for a single currency only, that is, balance checks
    -    are specified only for one currency at a time, and pads will only be
    -    inserted for those currencies.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A parser options dict.
    -    Returns:
    -      A new list of directives, with Pad entries inserted, and a list of new
    -      errors produced.
    -    """
    -    pad_errors = []
    -
    -    # Find all the pad entries and group them by account.
    -    pads = list(misc_utils.filter_type(entries, data.Pad))
    -    pad_dict = misc_utils.groupby(lambda x: x.account, pads)
    -
    -    # Partially realize the postings, so we can iterate them by account.
    -    by_account = realization.postings_by_account(entries)
    -
    -    # A dict of pad -> list of entries to be inserted.
    -    new_entries = {id(pad): [] for pad in pads}
    -
    -    # Process each account that has a padding group.
    -    for account_, pad_list in sorted(pad_dict.items()):
    -
    -        # Last encountered / currency active pad entry.
    -        active_pad = None
    -
    -        # Gather all the postings for the account and its children.
    -        postings = []
    -        is_child = account.parent_matcher(account_)
    -        for item_account, item_postings in by_account.items():
    -            if is_child(item_account):
    -                postings.extend(item_postings)
    -        postings.sort(key=data.posting_sortkey)
    -
    -        # A set of currencies already padded so far in this account.
    -        padded_lots = set()
    -
    -        pad_balance = inventory.Inventory()
    -        for entry in postings:
    -
    -            assert not isinstance(entry, data.Posting)
    -            if isinstance(entry, data.TxnPosting):
    -                # This is a transaction; update the running balance for this
    -                # account.
    -                pad_balance.add_position(entry.posting)
    -
    -            elif isinstance(entry, data.Pad):
    -                if entry.account == account_:
    -                    # Mark this newly encountered pad as active and allow all lots
    -                    # to be padded heretofore.
    -                    active_pad = entry
    -                    padded_lots = set()
    -
    -            elif isinstance(entry, data.Balance):
    -                check_amount = entry.amount
    -
    -                # Compare the current balance amount to the expected one from
    -                # the check entry. IMPORTANT: You need to understand that this
    -                # does not check a single position, but rather checks that the
    -                # total amount for a particular currency (which itself is
    -                # distinct from the cost).
    -                balance_amount = pad_balance.get_currency_units(check_amount.currency)
    -                diff_amount = amount.sub(balance_amount, check_amount)
    -
    -                # Use the specified tolerance or automatically infer it.
    -                tolerance = balance.get_balance_tolerance(entry, options_map)
    -
    -                if abs(diff_amount.number) > tolerance:
    -                    # The check fails; we need to pad.
    -
    -                    # Pad only if pad entry is active and we haven't already
    -                    # padded that lot since it was last encountered.
    -                    if active_pad and (check_amount.currency not in padded_lots):
    -
    -                        # Note: we decide that it's an error to try to pad
    -                        # positions at cost; we check here that all the existing
    -                        # positions with that currency have no cost.
    -                        positions = [pos
    -                                     for pos in pad_balance.get_positions()
    -                                     if pos.units.currency == check_amount.currency]
    -                        for position_ in positions:
    -                            if position_.cost is not None:
    -                                pad_errors.append(
    -                                    PadError(entry.meta,
    -                                             ("Attempt to pad an entry with cost for "
    -                                              "balance: {}".format(pad_balance)),
    -                                             active_pad))
    -
    -                        # Thus our padding lot is without cost by default.
    -                        diff_position = position.Position.from_amounts(
    -                            amount.Amount(check_amount.number - balance_amount.number,
    -                                          check_amount.currency))
    -
    -                        # Synthesize a new transaction entry for the difference.
    -                        narration = ('(Padding inserted for Balance of {} for '
    -                                     'difference {})').format(check_amount, diff_position)
    -                        new_entry = data.Transaction(
    -                            active_pad.meta.copy(), active_pad.date, flags.FLAG_PADDING,
    -                            None, narration, data.EMPTY_SET, data.EMPTY_SET, [])
    -
    -                        new_entry.postings.append(
    -                            data.Posting(active_pad.account,
    -                                         diff_position.units, diff_position.cost,
    -                                         None, None, None))
    -                        neg_diff_position = -diff_position
    -                        new_entry.postings.append(
    -                            data.Posting(active_pad.source_account,
    -                                         neg_diff_position.units, neg_diff_position.cost,
    -                                         None, None, None))
    -
    -                        # Save it for later insertion after the active pad.
    -                        new_entries[id(active_pad)].append(new_entry)
    -
    -                        # Fixup the running balance.
    -                        pos, _ = pad_balance.add_position(diff_position)
    -                        if pos is not None and pos.is_negative_at_cost():
    -                            raise ValueError(
    -                                "Position held at cost goes negative: {}".format(pos))
    -
    -                # Mark this lot as padded. Further checks should not pad this lot.
    -                padded_lots.add(check_amount.currency)
    -
    -    # Insert the newly created entries right after the pad entries that created them.
    -    padded_entries = []
    -    for entry in entries:
    -        padded_entries.append(entry)
    -        if isinstance(entry, data.Pad):
    -            entry_list = new_entries[id(entry)]
    -            if entry_list:
    -                padded_entries.extend(entry_list)
    -            else:
    -                # Generate errors on unused pad entries.
    -                pad_errors.append(
    -                    PadError(entry.meta, "Unused Pad entry", entry))
    -
    -    return padded_entries, pad_errors
    -
    +
    def pad(entries, options_map):
    +    """Insert transaction entries for to fulfill a subsequent balance check.
    +
    +    Synthesize and insert Transaction entries right after Pad entries in order
    +    to fulfill checks in the padded accounts. Returns a new list of entries.
    +    Note that this doesn't pad across parent-child relationships, it is a very
    +    simple kind of pad. (I have found this to be sufficient in practice, and
    +    simpler to implement and understand.)
    +
    +    Furthermore, this pads for a single currency only, that is, balance checks
    +    are specified only for one currency at a time, and pads will only be
    +    inserted for those currencies.
    +
    +    Args:
    +      entries: A list of directives.
    +      options_map: A parser options dict.
    +    Returns:
    +      A new list of directives, with Pad entries inserted, and a list of new
    +      errors produced.
    +    """
    +    pad_errors = []
    +
    +    # Find all the pad entries and group them by account.
    +    pads = list(misc_utils.filter_type(entries, data.Pad))
    +    pad_dict = misc_utils.groupby(lambda x: x.account, pads)
    +
    +    # Partially realize the postings, so we can iterate them by account.
    +    by_account = realization.postings_by_account(entries)
    +
    +    # A dict of pad -> list of entries to be inserted.
    +    new_entries = {id(pad): [] for pad in pads}
    +
    +    # Process each account that has a padding group.
    +    for account_, pad_list in sorted(pad_dict.items()):
    +        # Last encountered / currency active pad entry.
    +        active_pad = None
    +
    +        # Gather all the postings for the account and its children.
    +        postings = []
    +        is_child = account.parent_matcher(account_)
    +        for item_account, item_postings in by_account.items():
    +            if is_child(item_account):
    +                postings.extend(item_postings)
    +        postings.sort(key=data.posting_sortkey)
    +
    +        # A set of currencies already padded so far in this account.
    +        padded_lots = set()
    +
    +        pad_balance = inventory.Inventory()
    +        for entry in postings:
    +            assert not isinstance(entry, data.Posting)
    +            if isinstance(entry, data.TxnPosting):
    +                # This is a transaction; update the running balance for this
    +                # account.
    +                pad_balance.add_position(entry.posting)
    +
    +            elif isinstance(entry, data.Pad):
    +                if entry.account == account_:
    +                    # Mark this newly encountered pad as active and allow all lots
    +                    # to be padded heretofore.
    +                    active_pad = entry
    +                    padded_lots = set()
    +
    +            elif isinstance(entry, data.Balance):
    +                check_amount = entry.amount
    +
    +                # Compare the current balance amount to the expected one from
    +                # the check entry. IMPORTANT: You need to understand that this
    +                # does not check a single position, but rather checks that the
    +                # total amount for a particular currency (which itself is
    +                # distinct from the cost).
    +                balance_amount = pad_balance.get_currency_units(check_amount.currency)
    +                diff_amount = amount.sub(balance_amount, check_amount)
    +
    +                # Use the specified tolerance or automatically infer it.
    +                tolerance = balance.get_balance_tolerance(entry, options_map)
    +
    +                if abs(diff_amount.number) > tolerance:
    +                    # The check fails; we need to pad.
    +
    +                    # Pad only if pad entry is active and we haven't already
    +                    # padded that lot since it was last encountered.
    +                    if active_pad and (check_amount.currency not in padded_lots):
    +                        # Note: we decide that it's an error to try to pad
    +                        # positions at cost; we check here that all the existing
    +                        # positions with that currency have no cost.
    +                        positions = [
    +                            pos
    +                            for pos in pad_balance.get_positions()
    +                            if pos.units.currency == check_amount.currency
    +                        ]
    +                        for position_ in positions:
    +                            if position_.cost is not None:
    +                                pad_errors.append(
    +                                    PadError(
    +                                        entry.meta,
    +                                        (
    +                                            "Attempt to pad an entry with cost for "
    +                                            "balance: {}".format(pad_balance)
    +                                        ),
    +                                        active_pad,
    +                                    )
    +                                )
    +
    +                        # Thus our padding lot is without cost by default.
    +                        diff_position = position.Position.from_amounts(
    +                            amount.Amount(
    +                                check_amount.number - balance_amount.number,
    +                                check_amount.currency,
    +                            )
    +                        )
    +
    +                        # Synthesize a new transaction entry for the difference.
    +                        narration = (
    +                            "(Padding inserted for Balance of {} for " "difference {})"
    +                        ).format(check_amount, diff_position)
    +                        new_entry = data.Transaction(
    +                            active_pad.meta.copy(),
    +                            active_pad.date,
    +                            flags.FLAG_PADDING,
    +                            None,
    +                            narration,
    +                            data.EMPTY_SET,
    +                            data.EMPTY_SET,
    +                            [],
    +                        )
    +
    +                        new_entry.postings.append(
    +                            data.Posting(
    +                                active_pad.account,
    +                                diff_position.units,
    +                                diff_position.cost,
    +                                None,
    +                                None,
    +                                {},
    +                            )
    +                        )
    +                        neg_diff_position = -diff_position
    +                        new_entry.postings.append(
    +                            data.Posting(
    +                                active_pad.source_account,
    +                                neg_diff_position.units,
    +                                neg_diff_position.cost,
    +                                None,
    +                                None,
    +                                {},
    +                            )
    +                        )
    +
    +                        # Save it for later insertion after the active pad.
    +                        new_entries[id(active_pad)].append(new_entry)
    +
    +                        # Fixup the running balance.
    +                        pos, _ = pad_balance.add_position(diff_position)
    +                        if pos is not None and pos.is_negative_at_cost():
    +                            raise ValueError(
    +                                "Position held at cost goes negative: {}".format(pos)
    +                            )
    +
    +                # Mark this lot as padded. Further checks should not pad this lot.
    +                padded_lots.add(check_amount.currency)
    +
    +    # Insert the newly created entries right after the pad entries that created them.
    +    padded_entries = []
    +    for entry in entries:
    +        padded_entries.append(entry)
    +        if isinstance(entry, data.Pad):
    +            entry_list = new_entries[id(entry)]
    +            if entry_list:
    +                padded_entries.extend(entry_list)
    +            else:
    +                # Generate errors on unused pad entries.
    +                pad_errors.append(PadError(entry.meta, "Unused Pad entry", entry))
    +
    +    return padded_entries, pad_errors
    +
    @@ -3887,7 +3354,7 @@

    -beancount.ops.summarize.balance_by_account(entries, date=None) +beancount.ops.summarize.balance_by_account(entries, date=None, compress_unbooked=False)

    @@ -3910,6 +3377,11 @@

  • date – An optional datetime.date instance. If provided, stop accumulating on and after this date. This is useful for summarization before a specific date.

  • +
  • compress_unbooked – For accounts that have a booking method of NONE, +compress their positions into a single average position. This can be +used when you export the full list of positions, because those accounts +will have a myriad of small positions from fees at negative cost and +what-not.

  • @@ -3936,42 +3408,67 @@

    Source code in beancount/ops/summarize.py -
    def balance_by_account(entries, date=None):
    -    """Sum up the balance per account for all entries strictly before 'date'.
    -
    -    Args:
    -      entries: A list of directives.
    -      date: An optional datetime.date instance. If provided, stop accumulating
    -        on and after this date. This is useful for summarization before a
    -        specific date.
    -    Returns:
    -      A pair of a dict of account string to instance Inventory (the balance of
    -      this account before the given date), and the index in the list of entries
    -      where the date was encountered. If all entries are located before the
    -      cutoff date, an index one beyond the last entry is returned.
    -    """
    -    balances = collections.defaultdict(inventory.Inventory)
    -    for index, entry in enumerate(entries):
    -        if date and entry.date >= date:
    -            break
    -
    -        if isinstance(entry, Transaction):
    -            for posting in entry.postings:
    -                account_balance = balances[posting.account]
    -
    -                # Note: We must allow negative lots at cost, because this may be
    -                # used to reduce a filtered list of entries which may not
    -                # include the entries necessary to keep units at cost always
    -                # above zero. The only summation that is guaranteed to be above
    -                # zero is if all the entries are being summed together, no
    -                # entries are filtered, at least for a particular account's
    -                # postings.
    -                account_balance.add_position(posting)
    -    else:
    -        index = len(entries)
    -
    -    return balances, index
    -
    +
    def balance_by_account(entries, date=None, compress_unbooked=False):
    +    """Sum up the balance per account for all entries strictly before 'date'.
    +
    +    Args:
    +      entries: A list of directives.
    +      date: An optional datetime.date instance. If provided, stop accumulating
    +        on and after this date. This is useful for summarization before a
    +        specific date.
    +      compress_unbooked: For accounts that have a booking method of NONE,
    +        compress their positions into a single average position. This can be
    +        used when you export the full list of positions, because those accounts
    +        will have a myriad of small positions from fees at negative cost and
    +        what-not.
    +    Returns:
    +      A pair of a dict of account string to instance Inventory (the balance of
    +      this account before the given date), and the index in the list of entries
    +      where the date was encountered. If all entries are located before the
    +      cutoff date, an index one beyond the last entry is returned.
    +
    +    """
    +    balances = collections.defaultdict(inventory.Inventory)
    +    for index, entry in enumerate(entries):
    +        if date and entry.date >= date:
    +            break
    +
    +        if isinstance(entry, Transaction):
    +            for posting in entry.postings:
    +                account_balance = balances[posting.account]
    +
    +                # Note: We must allow negative lots at cost, because this may be
    +                # used to reduce a filtered list of entries which may not
    +                # include the entries necessary to keep units at cost always
    +                # above zero. The only summation that is guaranteed to be above
    +                # zero is if all the entries are being summed together, no
    +                # entries are filtered, at least for a particular account's
    +                # postings.
    +                account_balance.add_position(posting)
    +    else:
    +        index = len(entries)
    +
    +    # If the account has "NONE" booking method, merge all its postings
    +    # together in order to obtain an accurate cost basis and balance of
    +    # units.
    +    #
    +    # (This is a complex issue.) If you accrued positions without having them
    +    # booked properly against existing cost bases, you have not properly accounted
    +    # for the profit/loss to other postings. This means that the resulting
    +    # profit/loss is merged in the cost basis of the positive and negative
    +    # postings.
    +    if compress_unbooked:
    +        oc_map = getters.get_account_open_close(entries)
    +        accounts_map = {account: dopen for account, (dopen, _) in oc_map.items()}
    +
    +        for account, balance in balances.items():
    +            dopen = accounts_map.get(account, None)
    +            if dopen is not None and dopen.booking is data.Booking.NONE:
    +                average_balance = balance.average()
    +                balances[account] = inventory.Inventory(pos for pos in average_balance)
    +
    +    return balances, index
    +
    @@ -3984,7 +3481,7 @@

    -beancount.ops.summarize.cap(entries, account_types, conversion_currency, account_earnings, account_conversions) +beancount.ops.summarize.cap(entries, account_types, conversion_currency, account_earnings, account_conversions)

    @@ -4038,44 +3535,41 @@

    Source code in beancount/ops/summarize.py -
    def cap(entries,
    -        account_types,
    -        conversion_currency,
    -        account_earnings,
    -        account_conversions):
    -    """Transfer net income to equity and insert a final conversion entry.
    -
    -    This is used to move and nullify balances from the income and expense
    -    accounts to an equity account in order to draw up a balance sheet with a
    -    balance of precisely zero.
    -
    -    Args:
    -      entries: A list of directives.
    -      account_types: An instance of AccountTypes.
    -      conversion_currency: A string, the transfer currency to use for zero prices
    -        on the conversion entry.
    -      account_earnings: A string, the name of the equity account to transfer
    -        final balances of the income and expense accounts to.
    -      account_conversions: A string, the name of the equity account to use as
    -        the source for currency conversions.
    -    Returns:
    -      A modified list of entries, with the income and expense accounts
    -      transferred.
    -    """
    -
    -    # Transfer the balances of income and expense accounts as earnings / net
    -    # income.
    -    income_statement_account_pred = (
    -        lambda account: is_income_statement_account(account, account_types))
    -    entries = transfer_balances(entries, None,
    -                                income_statement_account_pred,
    -                                account_earnings)
    -
    -    # Insert final conversion entries.
    -    entries = conversions(entries, account_conversions, conversion_currency, None)
    -
    -    return entries
    -
    +
    def cap(entries, account_types, conversion_currency, account_earnings, account_conversions):
    +    """Transfer net income to equity and insert a final conversion entry.
    +
    +    This is used to move and nullify balances from the income and expense
    +    accounts to an equity account in order to draw up a balance sheet with a
    +    balance of precisely zero.
    +
    +    Args:
    +      entries: A list of directives.
    +      account_types: An instance of AccountTypes.
    +      conversion_currency: A string, the transfer currency to use for zero prices
    +        on the conversion entry.
    +      account_earnings: A string, the name of the equity account to transfer
    +        final balances of the income and expense accounts to.
    +      account_conversions: A string, the name of the equity account to use as
    +        the source for currency conversions.
    +    Returns:
    +      A modified list of entries, with the income and expense accounts
    +      transferred.
    +    """
    +
    +    # Transfer the balances of income and expense accounts as earnings / net
    +    # income.
    +    income_statement_account_pred = lambda account: is_income_statement_account(
    +        account, account_types
    +    )
    +    entries = transfer_balances(
    +        entries, None, income_statement_account_pred, account_earnings
    +    )
    +
    +    # Insert final conversion entries.
    +    entries = conversions(entries, account_conversions, conversion_currency, None)
    +
    +    return entries
    +
    @@ -4088,7 +3582,7 @@

    -beancount.ops.summarize.cap_opt(entries, options_map) +beancount.ops.summarize.cap_opt(entries, options_map)

    @@ -4133,25 +3627,22 @@

    Source code in beancount/ops/summarize.py -
    def cap_opt(entries, options_map):
    -    """Close by getting all the parameters from an options map.
    -
    -    See cap() for details.
    -
    -    Args:
    -      entries: See cap().
    -      options_map: A parser's option_map.
    -    Returns:
    -      Same as close().
    -    """
    -    account_types = options.get_account_types(options_map)
    -    current_accounts = options.get_current_accounts(options_map)
    -    conversion_currency = options_map['conversion_currency']
    -    return cap(entries,
    -               account_types,
    -               conversion_currency,
    -               *current_accounts)
    -
    +
    def cap_opt(entries, options_map):
    +    """Close by getting all the parameters from an options map.
    +
    +    See cap() for details.
    +
    +    Args:
    +      entries: See cap().
    +      options_map: A parser's option_map.
    +    Returns:
    +      Same as close().
    +    """
    +    account_types = options.get_account_types(options_map)
    +    current_accounts = options.get_current_accounts(options_map)
    +    conversion_currency = options_map["conversion_currency"]
    +    return cap(entries, account_types, conversion_currency, *current_accounts)
    +
    @@ -4164,7 +3655,7 @@

    -beancount.ops.summarize.clamp(entries, begin_date, end_date, account_types, conversion_currency, account_earnings, account_opening, account_conversions) +beancount.ops.summarize.clamp(entries, begin_date, end_date, account_types, conversion_currency, account_earnings, account_opening, account_conversions)

    @@ -4236,70 +3727,75 @@

    Source code in beancount/ops/summarize.py -
    def clamp(entries,
    -          begin_date, end_date,
    -          account_types,
    -          conversion_currency,
    -          account_earnings,
    -          account_opening,
    -          account_conversions):
    -    """Filter entries to include only those during a specified time period.
    -
    -    Firstly, this method will transfer all balances for the income and expense
    -    accounts occurring before the given period begin date to the
    -    'account_earnings' account (earnings before the period, or "retained
    -    earnings") and summarize all of the transactions before that date against
    -    the 'account_opening' account (usually "opening balances"). The resulting
    -    income and expense accounts should have no transactions (since their
    -    balances have been transferred out and summarization of zero balances should
    -    not add any transactions).
    -
    -    Secondly, all the entries after the period end date will be truncated and a
    -    conversion entry will be added for the resulting transactions that reflect
    -    changes occurring between the beginning and end of the exercise period. The
    -    resulting balance of all account should be empty.
    -
    -    Args:
    -      entries: A list of directive tuples.
    -      begin_date: A datetime.date instance, the beginning of the period.
    -      end_date: A datetime.date instance, one day beyond the end of the period.
    -      account_types: An instance of AccountTypes.
    -      conversion_currency: A string, the transfer currency to use for zero prices
    -        on the conversion entry.
    -      account_earnings: A string, the name of the account to transfer
    -        previous earnings from the income statement accounts to the balance
    -        sheet.
    -      account_opening: A string, the name of the account in equity
    -        to transfer previous balances from, in order to initialize account
    -        balances at the beginning of the period. This is typically called an
    -        opening balances account.
    -      account_conversions: A string, the name of the equity account to
    -        book currency conversions against.
    -    Returns:
    -      A new list of entries is returned, and the index that points to the first
    -      original transaction after the beginning date of the period. This index
    -      can be used to generate the opening balances report, which is a balance
    -      sheet fed with only the summarized entries.
    -    """
    -    # Transfer income and expenses before the period to equity.
    -    income_statement_account_pred = (
    -        lambda account: is_income_statement_account(account, account_types))
    -    entries = transfer_balances(entries, begin_date,
    -                                income_statement_account_pred, account_earnings)
    -
    -    # Summarize all the previous balances, after transferring the income and
    -    # expense balances, so all entries for those accounts before the begin date
    -    # should now disappear.
    -    entries, index = summarize(entries, begin_date, account_opening)
    -
    -    # Truncate the entries after this.
    -    entries = truncate(entries, end_date)
    -
    -    # Insert conversion entries.
    -    entries = conversions(entries, account_conversions, conversion_currency, end_date)
    -
    -    return entries, index
    -
    +
    def clamp(
    +    entries,
    +    begin_date,
    +    end_date,
    +    account_types,
    +    conversion_currency,
    +    account_earnings,
    +    account_opening,
    +    account_conversions,
    +):
    +    """Filter entries to include only those during a specified time period.
    +
    +    Firstly, this method will transfer all balances for the income and expense
    +    accounts occurring before the given period begin date to the
    +    'account_earnings' account (earnings before the period, or "retained
    +    earnings") and summarize all of the transactions before that date against
    +    the 'account_opening' account (usually "opening balances"). The resulting
    +    income and expense accounts should have no transactions (since their
    +    balances have been transferred out and summarization of zero balances should
    +    not add any transactions).
    +
    +    Secondly, all the entries after the period end date will be truncated and a
    +    conversion entry will be added for the resulting transactions that reflect
    +    changes occurring between the beginning and end of the exercise period. The
    +    resulting balance of all account should be empty.
    +
    +    Args:
    +      entries: A list of directive tuples.
    +      begin_date: A datetime.date instance, the beginning of the period.
    +      end_date: A datetime.date instance, one day beyond the end of the period.
    +      account_types: An instance of AccountTypes.
    +      conversion_currency: A string, the transfer currency to use for zero prices
    +        on the conversion entry.
    +      account_earnings: A string, the name of the account to transfer
    +        previous earnings from the income statement accounts to the balance
    +        sheet.
    +      account_opening: A string, the name of the account in equity
    +        to transfer previous balances from, in order to initialize account
    +        balances at the beginning of the period. This is typically called an
    +        opening balances account.
    +      account_conversions: A string, the name of the equity account to
    +        book currency conversions against.
    +    Returns:
    +      A new list of entries is returned, and the index that points to the first
    +      original transaction after the beginning date of the period. This index
    +      can be used to generate the opening balances report, which is a balance
    +      sheet fed with only the summarized entries.
    +    """
    +    # Transfer income and expenses before the period to equity.
    +    income_statement_account_pred = lambda account: is_income_statement_account(
    +        account, account_types
    +    )
    +    entries = transfer_balances(
    +        entries, begin_date, income_statement_account_pred, account_earnings
    +    )
    +
    +    # Summarize all the previous balances, after transferring the income and
    +    # expense balances, so all entries for those accounts before the begin date
    +    # should now disappear.
    +    entries, index = summarize(entries, begin_date, account_opening)
    +
    +    # Truncate the entries after this.
    +    entries = truncate(entries, end_date)
    +
    +    # Insert conversion entries.
    +    entries = conversions(entries, account_conversions, conversion_currency, end_date)
    +
    +    return entries, index
    +
    @@ -4312,7 +3808,7 @@

    -beancount.ops.summarize.clamp_opt(entries, begin_date, end_date, options_map) +beancount.ops.summarize.clamp_opt(entries, begin_date, end_date, options_map)

    @@ -4359,27 +3855,35 @@

    Source code in beancount/ops/summarize.py -
    def clamp_opt(entries, begin_date, end_date, options_map):
    -    """Clamp by getting all the parameters from an options map.
    -
    -    See clamp() for details.
    -
    -    Args:
    -      entries: See clamp().
    -      begin_date: See clamp().
    -      end_date: See clamp().
    -      options_map: A parser's option_map.
    -    Returns:
    -      Same as clamp().
    -    """
    -    account_types = options.get_account_types(options_map)
    -    previous_accounts = options.get_previous_accounts(options_map)
    -    conversion_currency = options_map['conversion_currency']
    -    return clamp(entries, begin_date, end_date,
    -                 account_types,
    -                 conversion_currency,
    -                 *previous_accounts)
    -
    +
    def clamp_opt(entries, begin_date, end_date, options_map):
    +    """Clamp by getting all the parameters from an options map.
    +
    +    See clamp() for details.
    +
    +    Args:
    +      entries: See clamp().
    +      begin_date: See clamp().
    +      end_date: See clamp().
    +      options_map: A parser's option_map.
    +    Returns:
    +      Same as clamp().
    +    """
    +    account_types = options.get_account_types(options_map)
    +    previous_earnings, previous_balances, _ = options.get_previous_accounts(options_map)
    +    _, current_conversions = options.get_current_accounts(options_map)
    +
    +    conversion_currency = options_map["conversion_currency"]
    +    return clamp(
    +        entries,
    +        begin_date,
    +        end_date,
    +        account_types,
    +        conversion_currency,
    +        previous_earnings,
    +        previous_balances,
    +        current_conversions,
    +    )
    +
    @@ -4392,7 +3896,7 @@

    -beancount.ops.summarize.clear(entries, date, account_types, account_earnings) +beancount.ops.summarize.clear(entries, date, account_types, account_earnings)

    @@ -4445,38 +3949,37 @@

    Source code in beancount/ops/summarize.py -
    def clear(entries,
    -          date,
    -          account_types,
    -          account_earnings):
    -    """Transfer income and expenses balances at the given date to the equity accounts.
    -
    -    This method insert entries to zero out balances on income and expenses
    -    accounts by transferring them to an equity account.
    -
    -    Args:
    -      entries: A list of directive tuples.
    -      date: A datetime.date instance, one day beyond the end of the period. This
    -        date can be optionally left to None in order to close at the end of the
    -        list of entries.
    -      account_types: An instance of AccountTypes.
    -      account_earnings: A string, the name of the account to transfer
    -        previous earnings from the income statement accounts to the balance
    -        sheet.
    -    Returns:
    -      A new list of entries is returned, and the index that points to one before
    -      the last original transaction before the transfers.
    -    """
    -    index = len(entries)
    -
    -    # Transfer income and expenses before the period to equity.
    -    income_statement_account_pred = (
    -        lambda account: is_income_statement_account(account, account_types))
    -    new_entries = transfer_balances(entries, date,
    -                                    income_statement_account_pred, account_earnings)
    -
    -    return new_entries, index
    -
    +
    def clear(entries, date, account_types, account_earnings):
    +    """Transfer income and expenses balances at the given date to the equity accounts.
    +
    +    This method insert entries to zero out balances on income and expenses
    +    accounts by transferring them to an equity account.
    +
    +    Args:
    +      entries: A list of directive tuples.
    +      date: A datetime.date instance, one day beyond the end of the period. This
    +        date can be optionally left to None in order to close at the end of the
    +        list of entries.
    +      account_types: An instance of AccountTypes.
    +      account_earnings: A string, the name of the account to transfer
    +        previous earnings from the income statement accounts to the balance
    +        sheet.
    +    Returns:
    +      A new list of entries is returned, and the index that points to one before
    +      the last original transaction before the transfers.
    +    """
    +    index = len(entries)
    +
    +    # Transfer income and expenses before the period to equity.
    +    income_statement_account_pred = lambda account: is_income_statement_account(
    +        account, account_types
    +    )
    +    new_entries = transfer_balances(
    +        entries, date, income_statement_account_pred, account_earnings
    +    )
    +
    +    return new_entries, index
    +
    @@ -4489,7 +3992,7 @@

    -beancount.ops.summarize.clear_opt(entries, date, options_map) +beancount.ops.summarize.clear_opt(entries, date, options_map)

    @@ -4500,13 +4003,12 @@

    Source code in beancount/ops/summarize.py -
    def clear_opt(entries, date, options_map):
    -    """Convenience function to clear() using an options map.
    -    """
    -    account_types = options.get_account_types(options_map)
    -    current_accounts = options.get_current_accounts(options_map)
    -    return clear(entries, date, account_types, current_accounts[0])
    -
    +
    def clear_opt(entries, date, options_map):
    +    """Convenience function to clear() using an options map."""
    +    account_types = options.get_account_types(options_map)
    +    current_accounts = options.get_current_accounts(options_map)
    +    return clear(entries, date, account_types, current_accounts[0])
    +
    @@ -4519,7 +4021,7 @@

    -beancount.ops.summarize.close(entries, date, conversion_currency, account_conversions) +beancount.ops.summarize.close(entries, date, conversion_currency, account_conversions)

    @@ -4586,53 +4088,50 @@

    Source code in beancount/ops/summarize.py -
    def close(entries,
    -          date,
    -          conversion_currency,
    -          account_conversions):
    -    """Truncate entries that occur after a particular date and ensure balance.
    -
    -    This method essentially removes entries after a date. It truncates the
    -    future. To do so, it will
    -
    -    1. Remove all entries which occur after 'date', if given.
    -
    -    2. Insert conversion transactions at the end of the list of entries to
    -       ensure that the total balance of all postings sums up to empty.
    -
    -    The result is a list of entries with a total balance of zero, with possibly
    -    non-zero balances for the income/expense accounts. To produce a final
    -    balance sheet, use transfer() to move the net income to the equity accounts.
    -
    -    Args:
    -      entries: A list of directive tuples.
    -      date: A datetime.date instance, one day beyond the end of the period. This
    -        date can be optionally left to None in order to close at the end of the
    -        list of entries.
    -      conversion_currency: A string, the transfer currency to use for zero prices
    -        on the conversion entry.
    -      account_conversions: A string, the name of the equity account to
    -        book currency conversions against.
    -    Returns:
    -      A new list of entries is returned, and the index that points to one beyond
    -      the last original transaction that was provided. Further entries may have
    -      been inserted to normalize conversions and ensure the total balance sums
    -      to zero.
    -    """
    -
    -    # Truncate the entries after the date, if a date has been provided.
    -    if date is not None:
    -        entries = truncate(entries, date)
    -
    -    # Keep an index to the truncated list of entries (before conversions).
    -    index = len(entries)
    -
    -    # Insert a conversions entry to ensure the total balance of all accounts is
    -    # flush zero.
    -    entries = conversions(entries, account_conversions, conversion_currency, date)
    -
    -    return entries, index
    -
    +
    def close(entries, date, conversion_currency, account_conversions):
    +    """Truncate entries that occur after a particular date and ensure balance.
    +
    +    This method essentially removes entries after a date. It truncates the
    +    future. To do so, it will
    +
    +    1. Remove all entries which occur after 'date', if given.
    +
    +    2. Insert conversion transactions at the end of the list of entries to
    +       ensure that the total balance of all postings sums up to empty.
    +
    +    The result is a list of entries with a total balance of zero, with possibly
    +    non-zero balances for the income/expense accounts. To produce a final
    +    balance sheet, use transfer() to move the net income to the equity accounts.
    +
    +    Args:
    +      entries: A list of directive tuples.
    +      date: A datetime.date instance, one day beyond the end of the period. This
    +        date can be optionally left to None in order to close at the end of the
    +        list of entries.
    +      conversion_currency: A string, the transfer currency to use for zero prices
    +        on the conversion entry.
    +      account_conversions: A string, the name of the equity account to
    +        book currency conversions against.
    +    Returns:
    +      A new list of entries is returned, and the index that points to one beyond
    +      the last original transaction that was provided. Further entries may have
    +      been inserted to normalize conversions and ensure the total balance sums
    +      to zero.
    +    """
    +
    +    # Truncate the entries after the date, if a date has been provided.
    +    if date is not None:
    +        entries = truncate(entries, date)
    +
    +    # Keep an index to the truncated list of entries (before conversions).
    +    index = len(entries)
    +
    +    # Insert a conversions entry to ensure the total balance of all accounts is
    +    # flush zero.
    +    entries = conversions(entries, account_conversions, conversion_currency, date)
    +
    +    return entries, index
    +
    @@ -4645,7 +4144,7 @@

    -beancount.ops.summarize.close_opt(entries, date, options_map) +beancount.ops.summarize.close_opt(entries, date, options_map)

    @@ -4656,13 +4155,12 @@

    Source code in beancount/ops/summarize.py -
    def close_opt(entries, date, options_map):
    -    """Convenience function to close() using an options map.
    -    """
    -    conversion_currency = options_map['conversion_currency']
    -    current_accounts = options.get_current_accounts(options_map)
    -    return close(entries, date, conversion_currency, current_accounts[1])
    -
    +
    def close_opt(entries, date, options_map):
    +    """Convenience function to close() using an options map."""
    +    conversion_currency = options_map["conversion_currency"]
    +    current_accounts = options.get_current_accounts(options_map)
    +    return close(entries, date, conversion_currency, current_accounts[1])
    +
    @@ -4675,7 +4173,7 @@

    -beancount.ops.summarize.conversions(entries, conversion_account, conversion_currency, date=None) +beancount.ops.summarize.conversions(entries, conversion_account, conversion_currency, date=None)

    @@ -4724,58 +4222,66 @@

    Source code in beancount/ops/summarize.py -
    def conversions(entries, conversion_account, conversion_currency, date=None):
    -    """Insert a conversion entry at date 'date' at the given account.
    -
    -    Args:
    -      entries: A list of entries.
    -      conversion_account: A string, the account to book against.
    -      conversion_currency: A string, the transfer currency to use for zero prices
    -        on the conversion entry.
    -      date: The date before which to insert the conversion entry. The new
    -        entry will be inserted as the last entry of the date just previous
    -        to this date.
    -    Returns:
    -      A modified list of entries.
    -    """
    -    # Compute the balance at the given date.
    -    conversion_balance = interpolate.compute_entries_balance(entries, date=date)
    -
    -    # Early exit if there is nothing to do.
    -    conversion_cost_balance = conversion_balance.reduce(convert.get_cost)
    -    if conversion_cost_balance.is_empty():
    -        return entries
    -
    -    # Calculate the index and the date for the new entry. We want to store it as
    -    # the last transaction of the day before.
    -    if date is not None:
    -        index = bisect_key.bisect_left_with_key(entries, date, key=lambda entry: entry.date)
    -        last_date = date - datetime.timedelta(days=1)
    -    else:
    -        index = len(entries)
    -        last_date = entries[-1].date
    -
    -    meta = data.new_metadata('<conversions>', -1)
    -    narration = 'Conversion for {}'.format(conversion_balance)
    -    conversion_entry = Transaction(meta, last_date, flags.FLAG_CONVERSIONS,
    -                                   None, narration, data.EMPTY_SET, data.EMPTY_SET, [])
    -    for position in conversion_cost_balance.get_positions():
    -        # Important note: Set the cost to zero here to maintain the balance
    -        # invariant. (This is the only single place we cheat on the balance rule
    -        # in the entire system and this is necessary; see documentation on
    -        # Conversions.)
    -        price = amount.Amount(ZERO, conversion_currency)
    -        neg_pos = -position
    -        conversion_entry.postings.append(
    -            data.Posting(conversion_account, neg_pos.units, neg_pos.cost,
    -                         price, None, None))
    -
    -    # Make a copy of the list of entries and insert the new transaction into it.
    -    new_entries = list(entries)
    -    new_entries.insert(index, conversion_entry)
    -
    -    return new_entries
    -
    +
    def conversions(entries, conversion_account, conversion_currency, date=None):
    +    """Insert a conversion entry at date 'date' at the given account.
    +
    +    Args:
    +      entries: A list of entries.
    +      conversion_account: A string, the account to book against.
    +      conversion_currency: A string, the transfer currency to use for zero prices
    +        on the conversion entry.
    +      date: The date before which to insert the conversion entry. The new
    +        entry will be inserted as the last entry of the date just previous
    +        to this date.
    +    Returns:
    +      A modified list of entries.
    +    """
    +    # Compute the balance at the given date.
    +    conversion_balance = interpolate.compute_entries_balance(entries, date=date)
    +
    +    # Early exit if there is nothing to do.
    +    conversion_cost_balance = conversion_balance.reduce(convert.get_cost)
    +    if conversion_cost_balance.is_empty():
    +        return entries
    +
    +    # Calculate the index and the date for the new entry. We want to store it as
    +    # the last transaction of the day before.
    +    if date is not None:
    +        index = bisect_key.bisect_left_with_key(entries, date, key=lambda entry: entry.date)
    +        last_date = date - datetime.timedelta(days=1)
    +    else:
    +        index = len(entries)
    +        last_date = entries[-1].date
    +
    +    meta = data.new_metadata("<conversions>", -1)
    +    narration = "Conversion for {}".format(conversion_balance)
    +    conversion_entry = Transaction(
    +        meta,
    +        last_date,
    +        flags.FLAG_CONVERSIONS,
    +        None,
    +        narration,
    +        data.EMPTY_SET,
    +        data.EMPTY_SET,
    +        [],
    +    )
    +    for position in conversion_cost_balance.get_positions():
    +        # Important note: Set the cost to zero here to maintain the balance
    +        # invariant. (This is the only single place we cheat on the balance rule
    +        # in the entire system and this is necessary; see documentation on
    +        # Conversions.)
    +        price = amount.Amount(ZERO, conversion_currency)
    +        neg_pos = -position
    +        conversion_entry.postings.append(
    +            data.Posting(conversion_account, neg_pos.units, neg_pos.cost, price, None, None)
    +        )
    +
    +    # Make a copy of the list of entries and insert the new transaction into it.
    +    new_entries = list(entries)
    +    new_entries.insert(index, conversion_entry)
    +
    +    return new_entries
    +
    @@ -4788,7 +4294,7 @@

    -beancount.ops.summarize.create_entries_from_balances(balances, date, source_account, direction, meta, flag, narration_template) +beancount.ops.summarize.create_entries_from_balances(balances, date, source_account, direction, meta, flag, narration_template)

    @@ -4847,60 +4353,61 @@

    Source code in beancount/ops/summarize.py -
    def create_entries_from_balances(balances, date, source_account, direction,
    -                                 meta, flag, narration_template):
    -    """"Create a list of entries from a dict of balances.
    -
    -    This method creates a list of new entries to transfer the amounts in the
    -    'balances' dict to/from another account specified in 'source_account'.
    -
    -    The balancing posting is created with the equivalent at cost. In other
    -    words, if you attempt to balance 10 HOOL {500 USD}, this will synthesize a
    -    posting with this position on one leg, and with 5000 USD on the
    -    'source_account' leg.
    -
    -    Args:
    -      balances: A dict of account name strings to Inventory instances.
    -      date: A datetime.date object, the date at which to create the transaction.
    -      source_account: A string, the name of the account to pull the balances
    -        from. This is the magician's hat to pull the rabbit from.
    -      direction: If 'direction' is True, the new entries transfer TO the
    -        balances account from the source account; otherwise the new entries
    -        transfer FROM the balances into the source account.
    -      meta: A dict to use as metadata for the transactions.
    -      flag: A string, the flag to use for the transactions.
    -      narration_template: A format string for creating the narration. It is
    -        formatted with 'account' and 'date' replacement variables.
    -    Returns:
    -      A list of newly synthesizes Transaction entries.
    -    """
    -    new_entries = []
    -    for account, account_balance in sorted(balances.items()):
    -
    -        # Don't create new entries where there is no balance.
    -        if account_balance.is_empty():
    -            continue
    -
    -        narration = narration_template.format(account=account, date=date)
    -
    -        if not direction:
    -            account_balance = -account_balance
    -
    -        postings = []
    -        new_entry = Transaction(
    -            meta, date, flag, None, narration, data.EMPTY_SET, data.EMPTY_SET, postings)
    -
    -        for position in account_balance.get_positions():
    -            postings.append(data.Posting(account, position.units, position.cost,
    -                                         None, None, None))
    -            cost = -convert.get_cost(position)
    -            postings.append(data.Posting(source_account, cost, None,
    -                                         None, None, None))
    -
    -        new_entries.append(new_entry)
    -
    -    return new_entries
    -
    +
    def create_entries_from_balances(
    +    balances, date, source_account, direction, meta, flag, narration_template
    +):
    +    """ "Create a list of entries from a dict of balances.
    +
    +    This method creates a list of new entries to transfer the amounts in the
    +    'balances' dict to/from another account specified in 'source_account'.
    +
    +    The balancing posting is created with the equivalent at cost. In other
    +    words, if you attempt to balance 10 HOOL {500 USD}, this will synthesize a
    +    posting with this position on one leg, and with 5000 USD on the
    +    'source_account' leg.
    +
    +    Args:
    +      balances: A dict of account name strings to Inventory instances.
    +      date: A datetime.date object, the date at which to create the transaction.
    +      source_account: A string, the name of the account to pull the balances
    +        from. This is the magician's hat to pull the rabbit from.
    +      direction: If 'direction' is True, the new entries transfer TO the
    +        balances account from the source account; otherwise the new entries
    +        transfer FROM the balances into the source account.
    +      meta: A dict to use as metadata for the transactions.
    +      flag: A string, the flag to use for the transactions.
    +      narration_template: A format string for creating the narration. It is
    +        formatted with 'account' and 'date' replacement variables.
    +    Returns:
    +      A list of newly synthesizes Transaction entries.
    +    """
    +    new_entries = []
    +    for account, account_balance in sorted(balances.items()):
    +        # Don't create new entries where there is no balance.
    +        if account_balance.is_empty():
    +            continue
    +
    +        narration = narration_template.format(account=account, date=date)
    +
    +        if not direction:
    +            account_balance = -account_balance
    +
    +        postings = []
    +        new_entry = Transaction(
    +            meta, date, flag, None, narration, data.EMPTY_SET, data.EMPTY_SET, postings
    +        )
    +
    +        for position in account_balance.get_positions():
    +            postings.append(
    +                data.Posting(account, position.units, position.cost, None, None, None)
    +            )
    +            cost = -convert.get_cost(position)
    +            postings.append(data.Posting(source_account, cost, None, None, None, None))
    +
    +        new_entries.append(new_entry)
    +
    +    return new_entries
    +
    @@ -4913,7 +4420,7 @@

    -beancount.ops.summarize.get_open_entries(entries, date) +beancount.ops.summarize.get_open_entries(entries, date)

    @@ -4960,38 +4467,38 @@

    Source code in beancount/ops/summarize.py -
    def get_open_entries(entries, date):
    -    """Gather the list of active Open entries at date.
    -
    -    This returns the list of Open entries that have not been closed at the given
    -    date, in the same order they were observed in the document.
    -
    -    Args:
    -      entries: A list of directives.
    -      date: The date at which to look for an open entry. If not specified, will
    -        return the entries still open at the latest date.
    -    Returns:
    -      A list of Open directives.
    -    """
    -    open_entries = {}
    -    for index, entry in enumerate(entries):
    -        if date is not None and entry.date >= date:
    -            break
    -
    -        if isinstance(entry, Open):
    -            try:
    -                ex_index, ex_entry = open_entries[entry.account]
    -                if entry.date < ex_entry.date:
    -                    open_entries[entry.account] = (index, entry)
    -            except KeyError:
    -                open_entries[entry.account] = (index, entry)
    -
    -        elif isinstance(entry, Close):
    -            # If there is no corresponding open, don't raise an error.
    -            open_entries.pop(entry.account, None)
    -
    -    return [entry for (index, entry) in sorted(open_entries.values())]
    -
    +
    def get_open_entries(entries, date):
    +    """Gather the list of active Open entries at date.
    +
    +    This returns the list of Open entries that have not been closed at the given
    +    date, in the same order they were observed in the document.
    +
    +    Args:
    +      entries: A list of directives.
    +      date: The date at which to look for an open entry. If not specified, will
    +        return the entries still open at the latest date.
    +    Returns:
    +      A list of Open directives.
    +    """
    +    open_entries = {}
    +    for index, entry in enumerate(entries):
    +        if date is not None and entry.date >= date:
    +            break
    +
    +        if isinstance(entry, Open):
    +            try:
    +                ex_index, ex_entry = open_entries[entry.account]
    +                if entry.date < ex_entry.date:
    +                    open_entries[entry.account] = (index, entry)
    +            except KeyError:
    +                open_entries[entry.account] = (index, entry)
    +
    +        elif isinstance(entry, Close):
    +            # If there is no corresponding open, don't raise an error.
    +            open_entries.pop(entry.account, None)
    +
    +    return [entry for (index, entry) in sorted(open_entries.values())]
    +
    @@ -5004,7 +4511,7 @@

    -beancount.ops.summarize.open(entries, date, account_types, conversion_currency, account_earnings, account_opening, account_conversions) +beancount.ops.summarize.open(entries, date, account_types, conversion_currency, account_earnings, account_opening, account_conversions)

    @@ -5087,72 +4594,74 @@

    Source code in beancount/ops/summarize.py -
    def open(entries,
    -         date,
    -         account_types,
    -         conversion_currency,
    -         account_earnings,
    -         account_opening,
    -         account_conversions):
    -    """Summarize entries before a date and transfer income/expenses to equity.
    -
    -    This method essentially prepares a list of directives to contain only
    -    transactions that occur after a particular date. It truncates the past. To
    -    do so, it will
    -
    -    1. Insert conversion transactions at the given open date, then
    -
    -    2. Insert transactions at that date to move accumulated balances from before
    -       that date from the income and expenses accounts to an equity account, and
    -       finally
    -
    -    3. It removes all the transactions previous to the date and replaces them by
    -       opening balances entries to bring the balances to the same amount.
    -
    -    The result is a list of entries for which the income and expense accounts
    -    are beginning with a balance of zero, and all other accounts begin with a
    -    transaction that brings their balance to the expected amount. All the past
    -    has been summarized at that point.
    -
    -    An index is returned to the first transaction past the balance opening
    -    transactions, so you can keep just those in order to render a balance sheet
    -    for only the opening balances.
    -
    -    Args:
    -      entries: A list of directive tuples.
    -      date: A datetime.date instance, the date at which to do this.
    -      account_types: An instance of AccountTypes.
    -      conversion_currency: A string, the transfer currency to use for zero prices
    -        on the conversion entry.
    -      account_earnings: A string, the name of the account to transfer
    -        previous earnings from the income statement accounts to the balance
    -        sheet.
    -      account_opening: A string, the name of the account in equity
    -        to transfer previous balances from, in order to initialize account
    -        balances at the beginning of the period. This is typically called an
    -        opening balances account.
    -      account_conversions: A string, the name of the equity account to
    -        book currency conversions against.
    -    Returns:
    -      A new list of entries is returned, and the index that points to the first
    -      original transaction after the beginning date of the period. This index
    -      can be used to generate the opening balances report, which is a balance
    -      sheet fed with only the summarized entries.
    -
    -    """
    -    # Insert conversion entries.
    -    entries = conversions(entries, account_conversions, conversion_currency, date)
    -
    -    # Transfer income and expenses before the period to equity.
    -    entries, _ = clear(entries, date, account_types, account_earnings)
    -
    -    # Summarize all the previous balances, after transferring the income and
    -    # expense balances, so all entries for those accounts before the begin date
    -    # should now disappear.
    -    entries, index = summarize(entries, date, account_opening)
    -
    -    return entries, index
    -
    +
    def open(
    +    entries,
    +    date,
    +    account_types,
    +    conversion_currency,
    +    account_earnings,
    +    account_opening,
    +    account_conversions,
    +):
    +    """Summarize entries before a date and transfer income/expenses to equity.
    +
    +    This method essentially prepares a list of directives to contain only
    +    transactions that occur after a particular date. It truncates the past. To
    +    do so, it will
    +
    +    1. Insert conversion transactions at the given open date, then
    +
    +    2. Insert transactions at that date to move accumulated balances from before
    +       that date from the income and expenses accounts to an equity account, and
    +       finally
    +
    +    3. It removes all the transactions previous to the date and replaces them by
    +       opening balances entries to bring the balances to the same amount.
    +
    +    The result is a list of entries for which the income and expense accounts
    +    are beginning with a balance of zero, and all other accounts begin with a
    +    transaction that brings their balance to the expected amount. All the past
    +    has been summarized at that point.
    +
    +    An index is returned to the first transaction past the balance opening
    +    transactions, so you can keep just those in order to render a balance sheet
    +    for only the opening balances.
    +
    +    Args:
    +      entries: A list of directive tuples.
    +      date: A datetime.date instance, the date at which to do this.
    +      account_types: An instance of AccountTypes.
    +      conversion_currency: A string, the transfer currency to use for zero prices
    +        on the conversion entry.
    +      account_earnings: A string, the name of the account to transfer
    +        previous earnings from the income statement accounts to the balance
    +        sheet.
    +      account_opening: A string, the name of the account in equity
    +        to transfer previous balances from, in order to initialize account
    +        balances at the beginning of the period. This is typically called an
    +        opening balances account.
    +      account_conversions: A string, the name of the equity account to
    +        book currency conversions against.
    +    Returns:
    +      A new list of entries is returned, and the index that points to the first
    +      original transaction after the beginning date of the period. This index
    +      can be used to generate the opening balances report, which is a balance
    +      sheet fed with only the summarized entries.
    +
    +    """
    +    # Insert conversion entries.
    +    entries = conversions(entries, account_conversions, conversion_currency, date)
    +
    +    # Transfer income and expenses before the period to equity.
    +    entries, _ = clear(entries, date, account_types, account_earnings)
    +
    +    # Summarize all the previous balances, after transferring the income and
    +    # expense balances, so all entries for those accounts before the begin date
    +    # should now disappear.
    +    entries, index = summarize(entries, date, account_opening)
    +
    +    return entries, index
    +
    @@ -5165,7 +4674,7 @@

    -beancount.ops.summarize.open_opt(entries, date, options_map) +beancount.ops.summarize.open_opt(entries, date, options_map)

    @@ -5176,14 +4685,13 @@

    Source code in beancount/ops/summarize.py -
    def open_opt(entries, date, options_map):
    -    """Convenience function to open() using an options map.
    -    """
    -    account_types = options.get_account_types(options_map)
    -    previous_accounts = options.get_previous_accounts(options_map)
    -    conversion_currency = options_map['conversion_currency']
    -    return open(entries, date, account_types, conversion_currency, *previous_accounts)
    -
    +
    def open_opt(entries, date, options_map):
    +    """Convenience function to open() using an options map."""
    +    account_types = options.get_account_types(options_map)
    +    previous_accounts = options.get_previous_accounts(options_map)
    +    conversion_currency = options_map["conversion_currency"]
    +    return open(entries, date, account_types, conversion_currency, *previous_accounts)
    +
    @@ -5196,7 +4704,7 @@

    -beancount.ops.summarize.summarize(entries, date, account_opening) +beancount.ops.summarize.summarize(entries, date, account_opening)

    @@ -5251,56 +4759,62 @@

    Source code in beancount/ops/summarize.py -
    def summarize(entries, date, account_opening):
    -    """Summarize all entries before a date by replacing then with summarization entries.
    -
    -    This function replaces the transactions up to (and not including) the given
    -    date with a opening balance transactions, one for each account. It returns
    -    new entries, all of the transactions before the given date having been
    -    replaced by a few summarization entries, one for each account.
    -
    -    Notes:
    -    - Open entries are preserved for active accounts.
    -    - The last relevant price entry for each (base, quote) pair is preserved.
    -    - All other entries before the cutoff date are culled.
    -
    -    Args:
    -      entries: A list of directives.
    -      date: A datetime.date instance, the cutoff date before which to summarize.
    -      account_opening: A string, the name of the source account to book summarization
    -        entries against.
    -    Returns:
    -      The function returns a list of new entries and the integer index at which
    -      the entries on or after the cutoff date begin.
    -    """
    -    # Compute balances at date.
    -    balances, index = balance_by_account(entries, date)
    -
    -    # We need to insert the entries with a date previous to subsequent checks,
    -    # to maintain ensure the open directives show up before any transaction.
    -    summarize_date = date - datetime.timedelta(days=1)
    -
    -    # Create summarization / opening balance entries.
    -    summarizing_entries = create_entries_from_balances(
    -        balances, summarize_date, account_opening, True,
    -        data.new_metadata('<summarize>', 0), flags.FLAG_SUMMARIZE,
    -        "Opening balance for '{account}' (Summarization)")
    -
    -    # Insert the last price entry for each commodity from before the date.
    -    price_entries = prices.get_last_price_entries(entries, date)
    -
    -    # Gather the list of active open entries at date.
    -    open_entries = get_open_entries(entries, date)
    -
    -    # Compute entries before the date and preserve the entries after the date.
    -    before_entries = sorted(open_entries + price_entries + summarizing_entries,
    -                            key=data.entry_sortkey)
    -    after_entries = entries[index:]
    -
    -    # Return a new list of entries and the index that points after the entries
    -    # were inserted.
    -    return (before_entries + after_entries), len(before_entries)
    -
    +
    def summarize(entries, date, account_opening):
    +    """Summarize all entries before a date by replacing then with summarization entries.
    +
    +    This function replaces the transactions up to (and not including) the given
    +    date with a opening balance transactions, one for each account. It returns
    +    new entries, all of the transactions before the given date having been
    +    replaced by a few summarization entries, one for each account.
    +
    +    Notes:
    +    - Open entries are preserved for active accounts.
    +    - The last relevant price entry for each (base, quote) pair is preserved.
    +    - All other entries before the cutoff date are culled.
    +
    +    Args:
    +      entries: A list of directives.
    +      date: A datetime.date instance, the cutoff date before which to summarize.
    +      account_opening: A string, the name of the source account to book summarization
    +        entries against.
    +    Returns:
    +      The function returns a list of new entries and the integer index at which
    +      the entries on or after the cutoff date begin.
    +    """
    +    # Compute balances at date.
    +    balances, index = balance_by_account(entries, date)
    +
    +    # We need to insert the entries with a date previous to subsequent checks,
    +    # to maintain ensure the open directives show up before any transaction.
    +    summarize_date = date - datetime.timedelta(days=1)
    +
    +    # Create summarization / opening balance entries.
    +    summarizing_entries = create_entries_from_balances(
    +        balances,
    +        summarize_date,
    +        account_opening,
    +        True,
    +        data.new_metadata("<summarize>", 0),
    +        flags.FLAG_SUMMARIZE,
    +        "Opening balance for '{account}' (Summarization)",
    +    )
    +
    +    # Insert the last price entry for each commodity from before the date.
    +    price_entries = prices.get_last_price_entries(entries, date)
    +
    +    # Gather the list of active open entries at date.
    +    open_entries = get_open_entries(entries, date)
    +
    +    # Compute entries before the date and preserve the entries after the date.
    +    before_entries = sorted(
    +        open_entries + price_entries + summarizing_entries, key=data.entry_sortkey
    +    )
    +    after_entries = entries[index:]
    +
    +    # Return a new list of entries and the index that points after the entries
    +    # were inserted.
    +    return (before_entries + after_entries), len(before_entries)
    +
    @@ -5313,7 +4827,7 @@

    -beancount.ops.summarize.transfer_balances(entries, date, account_pred, transfer_account) +beancount.ops.summarize.transfer_balances(entries, date, account_pred, transfer_account)

    @@ -5370,64 +4884,70 @@

    Source code in beancount/ops/summarize.py -
    def transfer_balances(entries, date, account_pred, transfer_account):
    -    """Synthesize transactions to transfer balances from some accounts at a given date.
    -
    -    For all accounts that match the 'account_pred' predicate, create new entries
    -    to transfer the balance at the given date from the account to the transfer
    -    account. This is used to transfer balances from income and expenses from a
    -    previous period to a "retained earnings" account. This is accomplished by
    -    creating new entries.
    -
    -    Note that inserting transfers breaks any following balance checks that are
    -    in the transferred accounts. For this reason, all balance assertion entries
    -    following the cutoff date for those accounts are removed from the list in
    -    output.
    -
    -    Args:
    -      entries: A list of directives.
    -      date: A datetime.date instance, the date at which to make the transfer.
    -      account_pred: A predicate function that, given an account string, returns
    -        true if the account is meant to be transferred.
    -      transfer_account: A string, the name of the source account to be used on
    -        the transfer entries to receive balances at the given date.
    -    Returns:
    -      A new list of entries, with the new transfer entries added in.
    -    """
    -    # Don't bother doing anything if there are no entries.
    -    if not entries:
    -        return entries
    -
    -    # Compute balances at date.
    -    balances, index = balance_by_account(entries, date)
    -
    -    # Filter out to keep only the accounts we want.
    -    transfer_balances = {account: balance
    -                         for account, balance in balances.items()
    -                         if account_pred(account)}
    -
    -    # We need to insert the entries at the end of the previous day.
    -    if date:
    -        transfer_date = date - datetime.timedelta(days=1)
    -    else:
    -        transfer_date = entries[-1].date
    -
    -    # Create transfer entries.
    -    transfer_entries = create_entries_from_balances(
    -        transfer_balances, transfer_date, transfer_account, False,
    -        data.new_metadata('<transfer_balances>', 0), flags.FLAG_TRANSFER,
    -        "Transfer balance for '{account}' (Transfer balance)")
    -
    -    # Remove balance assertions that occur after a transfer on an account that
    -    # has been transferred away; they would break.
    -    after_entries = [entry
    -                     for entry in entries[index:]
    -                     if not (isinstance(entry, balance.Balance) and
    -                             entry.account in transfer_balances)]
    -
    -    # Split the new entries in a new list.
    -    return (entries[:index] + transfer_entries + after_entries)
    -
    +
    def transfer_balances(entries, date, account_pred, transfer_account):
    +    """Synthesize transactions to transfer balances from some accounts at a given date.
    +
    +    For all accounts that match the 'account_pred' predicate, create new entries
    +    to transfer the balance at the given date from the account to the transfer
    +    account. This is used to transfer balances from income and expenses from a
    +    previous period to a "retained earnings" account. This is accomplished by
    +    creating new entries.
    +
    +    Note that inserting transfers breaks any following balance checks that are
    +    in the transferred accounts. For this reason, all balance assertion entries
    +    following the cutoff date for those accounts are removed from the list in
    +    output.
    +
    +    Args:
    +      entries: A list of directives.
    +      date: A datetime.date instance, the date at which to make the transfer.
    +      account_pred: A predicate function that, given an account string, returns
    +        true if the account is meant to be transferred.
    +      transfer_account: A string, the name of the source account to be used on
    +        the transfer entries to receive balances at the given date.
    +    Returns:
    +      A new list of entries, with the new transfer entries added in.
    +    """
    +    # Don't bother doing anything if there are no entries.
    +    if not entries:
    +        return entries
    +
    +    # Compute balances at date.
    +    balances, index = balance_by_account(entries, date)
    +
    +    # Filter out to keep only the accounts we want.
    +    transfer_balances = {
    +        account: balance for account, balance in balances.items() if account_pred(account)
    +    }
    +
    +    # We need to insert the entries at the end of the previous day.
    +    if date:
    +        transfer_date = date - datetime.timedelta(days=1)
    +    else:
    +        transfer_date = entries[-1].date
    +
    +    # Create transfer entries.
    +    transfer_entries = create_entries_from_balances(
    +        transfer_balances,
    +        transfer_date,
    +        transfer_account,
    +        False,
    +        data.new_metadata("<transfer_balances>", 0),
    +        flags.FLAG_TRANSFER,
    +        "Transfer balance for '{account}' (Transfer balance)",
    +    )
    +
    +    # Remove balance assertions that occur after a transfer on an account that
    +    # has been transferred away; they would break.
    +    after_entries = [
    +        entry
    +        for entry in entries[index:]
    +        if not (isinstance(entry, data.Balance) and entry.account in transfer_balances)
    +    ]
    +
    +    # Split the new entries in a new list.
    +    return entries[:index] + transfer_entries + after_entries
    +
    @@ -5440,7 +4960,7 @@

    -beancount.ops.summarize.truncate(entries, date) +beancount.ops.summarize.truncate(entries, date)

    @@ -5484,19 +5004,18 @@

    Source code in beancount/ops/summarize.py -
    def truncate(entries, date):
    -    """Filter out all the entries at and after date. Returns a new list of entries.
    -
    -    Args:
    -      entries: A sorted list of directives.
    -      date: A datetime.date instance.
    -    Returns:
    -      A truncated list of directives.
    -    """
    -    index = bisect_key.bisect_left_with_key(entries, date,
    -                                            key=lambda entry: entry.date)
    -    return entries[:index]
    -
    +
    def truncate(entries, date):
    +    """Filter out all the entries at and after date. Returns a new list of entries.
    +
    +    Args:
    +      entries: A sorted list of directives.
    +      date: A datetime.date instance.
    +    Returns:
    +      A truncated list of directives.
    +    """
    +    index = bisect_key.bisect_left_with_key(entries, date, key=lambda entry: entry.date)
    +    return entries[:index]
    +
    @@ -5587,7 +5106,7 @@

    -beancount.ops.validation.ValidationError.__getnewargs__(self) +beancount.ops.validation.ValidationError.__getnewargs__(self) special @@ -5601,10 +5120,10 @@

    Source code in beancount/ops/validation.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -5617,7 +5136,7 @@

    -beancount.ops.validation.ValidationError.__new__(_cls, source, message, entry) +beancount.ops.validation.ValidationError.__new__(_cls, source, message, entry) special @@ -5641,7 +5160,7 @@

    -beancount.ops.validation.ValidationError.__repr__(self) +beancount.ops.validation.ValidationError.__repr__(self) special @@ -5655,10 +5174,10 @@

    Source code in beancount/ops/validation.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -5682,7 +5201,7 @@

    -beancount.ops.validation.validate(entries, options_map, log_timings=None, extra_validations=None) +beancount.ops.validation.validate(entries, options_map, log_timings=None, extra_validations=None)

    @@ -5730,33 +5249,34 @@

    Source code in beancount/ops/validation.py -
    def validate(entries, options_map, log_timings=None, extra_validations=None):
    -    """Perform all the standard checks on parsed contents.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -      log_timings: An optional function to use for logging the time of individual
    -        operations.
    -      extra_validations: A list of extra validation functions to run after loading
    -        this list of entries.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    validation_tests = VALIDATIONS
    -    if extra_validations:
    -        validation_tests += extra_validations
    -
    -    # Run various validation routines define above.
    -    errors = []
    -    for validation_function in validation_tests:
    -        with misc_utils.log_time('function: {}'.format(validation_function.__name__),
    -                                 log_timings, indent=2):
    -            new_errors = validation_function(entries, options_map)
    -        errors.extend(new_errors)
    -
    -    return errors
    -
    +
    def validate(entries, options_map, log_timings=None, extra_validations=None):
    +    """Perform all the standard checks on parsed contents.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +      log_timings: An optional function to use for logging the time of individual
    +        operations.
    +      extra_validations: A list of extra validation functions to run after loading
    +        this list of entries.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    validation_tests = VALIDATIONS
    +    if extra_validations:
    +        validation_tests += extra_validations
    +
    +    # Run various validation routines define above.
    +    errors = []
    +    for validation_function in validation_tests:
    +        with misc_utils.log_time(
    +            "function: {}".format(validation_function.__name__), log_timings, indent=2
    +        ):
    +            new_errors = validation_function(entries, options_map)
    +        errors.extend(new_errors)
    +
    +    return errors
    +
    @@ -5769,7 +5289,7 @@

    -beancount.ops.validation.validate_active_accounts(entries, unused_options_map) +beancount.ops.validation.validate_active_accounts(entries, unused_options_map)

    @@ -5822,62 +5342,61 @@

    Source code in beancount/ops/validation.py -
    def validate_active_accounts(entries, unused_options_map):
    -    """Check that all references to accounts occurs on active accounts.
    -
    -    We basically check that references to accounts from all directives other
    -    than Open and Close occur at dates the open-close interval of that account.
    -    This should be good for all of the directive types where we can extract an
    -    account name.
    -
    -    Note that this is more strict a check than comparing the dates: we actually
    -    check that no references to account are made on the same day before the open
    -    directive appears for that account. This is a nice property to have, and is
    -    supported by our custom sorting routine that will sort open entries before
    -    transaction entries, given the same date.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    error_pairs = []
    -    active_set = set()
    -    opened_accounts = set()
    -    for entry in entries:
    -        if isinstance(entry, data.Open):
    -            active_set.add(entry.account)
    -            opened_accounts.add(entry.account)
    -
    -        elif isinstance(entry, data.Close):
    -            active_set.discard(entry.account)
    -
    -        else:
    -            for account in getters.get_entry_accounts(entry):
    -                if account not in active_set:
    -                    # Allow document and note directives that occur after an
    -                    # account is closed.
    -                    if (isinstance(entry, ALLOW_AFTER_CLOSE) and
    -                        account in opened_accounts):
    -                        continue
    -
    -                    # Register an error to be logged later, with an appropriate
    -                    # message.
    -                    error_pairs.append((account, entry))
    -
    -    # Refine the error message to disambiguate between the case of an account
    -    # that has never been seen and one that was simply not active at the time.
    -    errors = []
    -    for account, entry in error_pairs:
    -        if account in opened_accounts:
    -            message = "Invalid reference to inactive account '{}'".format(account)
    -        else:
    -            message = "Invalid reference to unknown account '{}'".format(account)
    -        errors.append(ValidationError(entry.meta, message, entry))
    -
    -    return errors
    -
    +
    def validate_active_accounts(entries, unused_options_map):
    +    """Check that all references to accounts occurs on active accounts.
    +
    +    We basically check that references to accounts from all directives other
    +    than Open and Close occur at dates the open-close interval of that account.
    +    This should be good for all of the directive types where we can extract an
    +    account name.
    +
    +    Note that this is more strict a check than comparing the dates: we actually
    +    check that no references to account are made on the same day before the open
    +    directive appears for that account. This is a nice property to have, and is
    +    supported by our custom sorting routine that will sort open entries before
    +    transaction entries, given the same date.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    error_pairs = []
    +    active_set = set()
    +    opened_accounts = set()
    +    for entry in entries:
    +        if isinstance(entry, data.Open):
    +            active_set.add(entry.account)
    +            opened_accounts.add(entry.account)
    +
    +        elif isinstance(entry, data.Close):
    +            active_set.discard(entry.account)
    +
    +        else:
    +            for account in getters.get_entry_accounts(entry):
    +                if account not in active_set:
    +                    # Allow document and note directives that occur after an
    +                    # account is closed.
    +                    if isinstance(entry, ALLOW_AFTER_CLOSE) and account in opened_accounts:
    +                        continue
    +
    +                    # Register an error to be logged later, with an appropriate
    +                    # message.
    +                    error_pairs.append((account, entry))
    +
    +    # Refine the error message to disambiguate between the case of an account
    +    # that has never been seen and one that was simply not active at the time.
    +    errors = []
    +    for account, entry in error_pairs:
    +        if account in opened_accounts:
    +            message = "Invalid reference to inactive account '{}'".format(account)
    +        else:
    +            message = "Invalid reference to unknown account '{}'".format(account)
    +        errors.append(ValidationError(entry.meta, message, entry))
    +
    +    return errors
    +
    @@ -5890,7 +5409,7 @@

    -beancount.ops.validation.validate_check_transaction_balances(entries, options_map) +beancount.ops.validation.validate_check_transaction_balances(entries, options_map)

    @@ -5935,41 +5454,44 @@

    Source code in beancount/ops/validation.py -
    def validate_check_transaction_balances(entries, options_map):
    -    """Check again that all transaction postings balance, as users may have
    -    transformed transactions.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    # Note: this is a bit slow; we could limit our checks to the original
    -    # transactions by using the hash function in the loader.
    -    errors = []
    -    for entry in entries:
    -        if isinstance(entry, Transaction):
    -            # IMPORTANT: This validation is _crucial_ and cannot be skipped.
    -            # This is where we actually detect and warn on unbalancing
    -            # transactions. This _must_ come after the user routines, because
    -            # unbalancing input is legal, as those types of transactions may be
    -            # "fixed up" by a user-plugin. In other words, we want to allow
    -            # users to input unbalancing transactions as long as the final
    -            # transactions objects that appear on the stream (after processing
    -            # the plugins) are balanced. See {9e6c14b51a59}.
    -            #
    -            # Detect complete sets of postings that have residual balance;
    -            residual = interpolate.compute_residual(entry.postings)
    -            tolerances = interpolate.infer_tolerances(entry.postings, options_map)
    -            if not residual.is_small(tolerances):
    -                errors.append(
    -                    ValidationError(entry.meta,
    -                                    "Transaction does not balance: {}".format(residual),
    -                                    entry))
    -
    -    return errors
    -
    +
    def validate_check_transaction_balances(entries, options_map):
    +    """Check again that all transaction postings balance, as users may have
    +    transformed transactions.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    # Note: this is a bit slow; we could limit our checks to the original
    +    # transactions by using the hash function in the loader.
    +    errors = []
    +    for entry in entries:
    +        if isinstance(entry, Transaction):
    +            # IMPORTANT: This validation is _crucial_ and cannot be skipped.
    +            # This is where we actually detect and warn on unbalancing
    +            # transactions. This _must_ come after the user routines, because
    +            # unbalancing input is legal, as those types of transactions may be
    +            # "fixed up" by a user-plugin. In other words, we want to allow
    +            # users to input unbalancing transactions as long as the final
    +            # transactions objects that appear on the stream (after processing
    +            # the plugins) are balanced. See {9e6c14b51a59}.
    +            #
    +            # Detect complete sets of postings that have residual balance;
    +            residual = interpolate.compute_residual(entry.postings)
    +            tolerances = interpolate.infer_tolerances(entry.postings, options_map)
    +            if not residual.is_small(tolerances):
    +                errors.append(
    +                    ValidationError(
    +                        entry.meta,
    +                        "Transaction does not balance: {}".format(residual),
    +                        entry,
    +                    )
    +                )
    +
    +    return errors
    +
    @@ -5982,7 +5504,7 @@

    -beancount.ops.validation.validate_currency_constraints(entries, options_map) +beancount.ops.validation.validate_currency_constraints(entries, options_map)

    @@ -6030,53 +5552,58 @@

    Source code in beancount/ops/validation.py -
    def validate_currency_constraints(entries, options_map):
    -    """Check the currency constraints from account open declarations.
    -
    -    Open directives admit an optional list of currencies that specify the only
    -    types of commodities that the running inventory for this account may
    -    contain. This function checks that all postings are only made in those
    -    commodities.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -
    -    # Get all the open entries with currency constraints.
    -    open_map = {entry.account: entry
    -                for entry in entries
    -                if isinstance(entry, Open) and entry.currencies}
    -
    -    errors = []
    -    for entry in entries:
    -        if not isinstance(entry, Transaction):
    -            continue
    -
    -        for posting in entry.postings:
    -            # Look up the corresponding account's valid currencies; skip the
    -            # check if there are none specified.
    -            try:
    -                open_entry = open_map[posting.account]
    -                valid_currencies = open_entry.currencies
    -                if not valid_currencies:
    -                    continue
    -            except KeyError:
    -                continue
    -
    -            # Perform the check.
    -            if posting.units.currency not in valid_currencies:
    -                errors.append(
    -                    ValidationError(
    -                        entry.meta,
    -                        "Invalid currency {} for account '{}'".format(
    -                            posting.units.currency, posting.account),
    -                        entry))
    -
    -    return errors
    -
    +
    def validate_currency_constraints(entries, options_map):
    +    """Check the currency constraints from account open declarations.
    +
    +    Open directives admit an optional list of currencies that specify the only
    +    types of commodities that the running inventory for this account may
    +    contain. This function checks that all postings are only made in those
    +    commodities.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +
    +    # Get all the open entries with currency constraints.
    +    open_map = {
    +        entry.account: entry
    +        for entry in entries
    +        if isinstance(entry, Open) and entry.currencies
    +    }
    +
    +    errors = []
    +    for entry in entries:
    +        if not isinstance(entry, Transaction):
    +            continue
    +
    +        for posting in entry.postings:
    +            # Look up the corresponding account's valid currencies; skip the
    +            # check if there are none specified.
    +            try:
    +                open_entry = open_map[posting.account]
    +                valid_currencies = open_entry.currencies
    +                if not valid_currencies:
    +                    continue
    +            except KeyError:
    +                continue
    +
    +            # Perform the check.
    +            if posting.units.currency not in valid_currencies:
    +                errors.append(
    +                    ValidationError(
    +                        entry.meta,
    +                        "Invalid currency {} for account '{}'".format(
    +                            posting.units.currency, posting.account
    +                        ),
    +                        entry,
    +                    )
    +                )
    +
    +    return errors
    +
    @@ -6089,7 +5616,7 @@

    -beancount.ops.validation.validate_data_types(entries, options_map) +beancount.ops.validation.validate_data_types(entries, options_map)

    @@ -6137,32 +5664,32 @@

    Source code in beancount/ops/validation.py -
    def validate_data_types(entries, options_map):
    -    """Check that all the data types of the attributes of entries are as expected.
    -
    -    Users are provided with a means to filter the list of entries. They're able to
    -    write code that manipulates those tuple objects without any type constraints.
    -    With discipline, this mostly works, but I know better: check, just to make sure.
    -    This routine checks all the data types and assumptions on entries.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    errors = []
    -    for entry in entries:
    -        try:
    -            data.sanity_check_types(
    -                entry, options_map["allow_deprecated_none_for_tags_and_links"])
    -        except AssertionError as exc:
    -            errors.append(
    -                ValidationError(entry.meta,
    -                                "Invalid data types: {}".format(exc),
    -                                entry))
    -    return errors
    -
    +
    def validate_data_types(entries, options_map):
    +    """Check that all the data types of the attributes of entries are as expected.
    +
    +    Users are provided with a means to filter the list of entries. They're able to
    +    write code that manipulates those tuple objects without any type constraints.
    +    With discipline, this mostly works, but I know better: check, just to make sure.
    +    This routine checks all the data types and assumptions on entries.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    errors = []
    +    for entry in entries:
    +        try:
    +            data.sanity_check_types(
    +                entry, options_map["allow_deprecated_none_for_tags_and_links"]
    +            )
    +        except AssertionError as exc:
    +            errors.append(
    +                ValidationError(entry.meta, "Invalid data types: {}".format(exc), entry)
    +            )
    +    return errors
    +
    @@ -6175,7 +5702,7 @@

    -beancount.ops.validation.validate_documents_paths(entries, options_map) +beancount.ops.validation.validate_documents_paths(entries, options_map)

    @@ -6222,24 +5749,25 @@

    Source code in beancount/ops/validation.py -
    def validate_documents_paths(entries, options_map):
    -    """Check that all filenames in resolved Document entries are absolute filenames.
    -
    -    The processing of document entries is assumed to result in absolute paths.
    -    Relative paths are resolved at the parsing stage and at point we want to
    -    make sure we don't have to do any further processing on them.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    return [ValidationError(entry.meta, "Invalid relative path for entry", entry)
    -            for entry in entries
    -            if (isinstance(entry, Document) and
    -                not path.isabs(entry.filename))]
    -
    +
    def validate_documents_paths(entries, options_map):
    +    """Check that all filenames in resolved Document entries are absolute filenames.
    +
    +    The processing of document entries is assumed to result in absolute paths.
    +    Relative paths are resolved at the parsing stage and at point we want to
    +    make sure we don't have to do any further processing on them.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    return [
    +        ValidationError(entry.meta, "Invalid relative path for entry", entry)
    +        for entry in entries
    +        if (isinstance(entry, Document) and not path.isabs(entry.filename))
    +    ]
    +
    @@ -6252,7 +5780,7 @@

    -beancount.ops.validation.validate_duplicate_balances(entries, unused_options_map) +beancount.ops.validation.validate_duplicate_balances(entries, unused_options_map)

    @@ -6300,42 +5828,44 @@

    Source code in beancount/ops/validation.py -
    def validate_duplicate_balances(entries, unused_options_map):
    -    """Check that balance entries occur only once per day.
    -
    -    Because we do not support time, and the declaration order of entries is
    -    meant to be kept irrelevant, two balance entries with different amounts
    -    should not occur in the file. We do allow two identical balance assertions,
    -    however, because this may occur during import.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    errors = []
    -
    -    # Mapping of (account, currency, date) to Balance entry.
    -    balance_entries = {}
    -    for entry in entries:
    -        if not isinstance(entry, data.Balance):
    -            continue
    -
    -        key = (entry.account, entry.amount.currency, entry.date)
    -        try:
    -            previous_entry = balance_entries[key]
    -            if entry.amount != previous_entry.amount:
    -                errors.append(
    -                    ValidationError(
    -                        entry.meta,
    -                        "Duplicate balance assertion with different amounts",
    -                        entry))
    -        except KeyError:
    -            balance_entries[key] = entry
    -
    -    return errors
    -
    +
    def validate_duplicate_balances(entries, unused_options_map):
    +    """Check that balance entries occur only once per day.
    +
    +    Because we do not support time, and the declaration order of entries is
    +    meant to be kept irrelevant, two balance entries with different amounts
    +    should not occur in the file. We do allow two identical balance assertions,
    +    however, because this may occur during import.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    errors = []
    +
    +    # Mapping of (account, currency, date) to Balance entry.
    +    balance_entries = {}
    +    for entry in entries:
    +        if not isinstance(entry, data.Balance):
    +            continue
    +
    +        key = (entry.account, entry.amount.currency, entry.date)
    +        try:
    +            previous_entry = balance_entries[key]
    +            if entry.amount != previous_entry.amount:
    +                errors.append(
    +                    ValidationError(
    +                        entry.meta,
    +                        "Duplicate balance assertion with different amounts",
    +                        entry,
    +                    )
    +                )
    +        except KeyError:
    +            balance_entries[key] = entry
    +
    +    return errors
    +
    @@ -6348,7 +5878,7 @@

    -beancount.ops.validation.validate_duplicate_commodities(entries, unused_options_map) +beancount.ops.validation.validate_duplicate_commodities(entries, unused_options_map)

    @@ -6392,37 +5922,39 @@

    Source code in beancount/ops/validation.py -
    def validate_duplicate_commodities(entries, unused_options_map):
    -    """Check that commodity entries are unique for each commodity.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    errors = []
    -
    -    # Mapping of (account, currency, date) to Balance entry.
    -    commodity_entries = {}
    -    for entry in entries:
    -        if not isinstance(entry, data.Commodity):
    -            continue
    -
    -        key = entry.currency
    -        try:
    -            previous_entry = commodity_entries[key]
    -            if previous_entry:
    -                errors.append(
    -                    ValidationError(
    -                        entry.meta,
    -                        "Duplicate commodity directives for '{}'".format(key),
    -                        entry))
    -        except KeyError:
    -            commodity_entries[key] = entry
    -
    -    return errors
    -
    +
    def validate_duplicate_commodities(entries, unused_options_map):
    +    """Check that commodity entries are unique for each commodity.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    errors = []
    +
    +    # Mapping of (account, currency, date) to Balance entry.
    +    commodity_entries = {}
    +    for entry in entries:
    +        if not isinstance(entry, data.Commodity):
    +            continue
    +
    +        key = entry.currency
    +        try:
    +            previous_entry = commodity_entries[key]
    +            if previous_entry:
    +                errors.append(
    +                    ValidationError(
    +                        entry.meta,
    +                        "Duplicate commodity directives for '{}'".format(key),
    +                        entry,
    +                    )
    +                )
    +        except KeyError:
    +            commodity_entries[key] = entry
    +
    +    return errors
    +
    @@ -6435,7 +5967,7 @@

    -beancount.ops.validation.validate_open_close(entries, unused_options_map) +beancount.ops.validation.validate_open_close(entries, unused_options_map)

    @@ -6450,12 +5982,12 @@

    duplicate is detected, an error is generated.

  • -

    Close directives may only appears if an open directive has been seen - previous (chronologically).

    +

    Close directives may only appear if an open directive has been seen + previously (chronologically).

  • The date of close directives must be strictly greater than their - corresponding open directive.

    + corresponding open directive.

  • @@ -6494,69 +6026,76 @@

    Source code in beancount/ops/validation.py -
    def validate_open_close(entries, unused_options_map):
    -    """Check constraints on open and close directives themselves.
    -
    -    This method checks two kinds of constraints:
    -
    -    1. An open or a close directive may only show up once for each account. If a
    -       duplicate is detected, an error is generated.
    -
    -    2. Close directives may only appears if an open directive has been seen
    -       previous (chronologically).
    -
    -    3. The date of close directives must be strictly greater than their
    -      corresponding open directive.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    errors = []
    -    open_map = {}
    -    close_map = {}
    -    for entry in entries:
    -
    -        if isinstance(entry, Open):
    -            if entry.account in open_map:
    -                errors.append(
    -                    ValidationError(
    -                        entry.meta,
    -                        "Duplicate open directive for {}".format(entry.account),
    -                        entry))
    -            else:
    -                open_map[entry.account] = entry
    -
    -        elif isinstance(entry, Close):
    -            if entry.account in close_map:
    -                errors.append(
    -                    ValidationError(
    -                        entry.meta,
    -                        "Duplicate close directive for {}".format(entry.account),
    -                        entry))
    -            else:
    -                try:
    -                    open_entry = open_map[entry.account]
    -                    if entry.date <= open_entry.date:
    -                        errors.append(
    -                            ValidationError(
    -                                entry.meta,
    -                                "Internal error: closing date for {} "
    -                                "appears before opening date".format(entry.account),
    -                                entry))
    -                except KeyError:
    -                    errors.append(
    -                        ValidationError(
    -                            entry.meta,
    -                            "Unopened account {} is being closed".format(entry.account),
    -                            entry))
    -
    -                close_map[entry.account] = entry
    -
    -    return errors
    -
    +
    def validate_open_close(entries, unused_options_map):
    +    """Check constraints on open and close directives themselves.
    +
    +    This method checks two kinds of constraints:
    +
    +    1. An open or a close directive may only show up once for each account. If a
    +       duplicate is detected, an error is generated.
    +
    +    2. Close directives may only appear if an open directive has been seen
    +       previously (chronologically).
    +
    +    3. The date of close directives must be strictly greater than their
    +       corresponding open directive.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    errors = []
    +    open_map = {}
    +    close_map = {}
    +    for entry in entries:
    +        if isinstance(entry, Open):
    +            if entry.account in open_map:
    +                errors.append(
    +                    ValidationError(
    +                        entry.meta,
    +                        "Duplicate open directive for {}".format(entry.account),
    +                        entry,
    +                    )
    +                )
    +            else:
    +                open_map[entry.account] = entry
    +
    +        elif isinstance(entry, Close):
    +            if entry.account in close_map:
    +                errors.append(
    +                    ValidationError(
    +                        entry.meta,
    +                        "Duplicate close directive for {}".format(entry.account),
    +                        entry,
    +                    )
    +                )
    +            else:
    +                try:
    +                    open_entry = open_map[entry.account]
    +                    if entry.date < open_entry.date:
    +                        errors.append(
    +                            ValidationError(
    +                                entry.meta,
    +                                "Internal error: closing date for {} "
    +                                "appears before opening date".format(entry.account),
    +                                entry,
    +                            )
    +                        )
    +                except KeyError:
    +                    errors.append(
    +                        ValidationError(
    +                            entry.meta,
    +                            "Unopened account {} is being closed".format(entry.account),
    +                            entry,
    +                        )
    +                    )
    +
    +                close_map[entry.account] = entry
    +
    +    return errors
    +
    diff --git a/api_reference/beancount.parser.html b/api_reference/beancount.parser.html index d3c6204c..c8353a7b 100644 --- a/api_reference/beancount.parser.html +++ b/api_reference/beancount.parser.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -152,8 +154,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -176,6 +176,10 @@
  • book()
  • +
  • convert_lot_specs_to_lots() +
  • +
  • convert_spec_to_cost() +
  • validate_inventory_booking()
  • validate_missing_eliminated() @@ -274,34 +278,52 @@
  • booking_method_FIFO()
  • +
  • booking_method_HIFO() +
  • booking_method_LIFO()
  • booking_method_NONE()
  • booking_method_STRICT()
  • +
  • booking_method_STRICT_WITH_SIZE() +
  • handle_ambiguous_matches()
  • cmptest
  • +
  • context + +
  • grammar
  • +
  • version + +
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • @@ -750,7 +750,7 @@

    -beancount.parser.booking.BookingError.__getnewargs__(self) +beancount.parser.booking.BookingError.__getnewargs__(self) special @@ -764,10 +764,10 @@

    Source code in beancount/parser/booking.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -780,7 +780,7 @@

    -beancount.parser.booking.BookingError.__new__(_cls, source, message, entry) +beancount.parser.booking.BookingError.__new__(_cls, source, message, entry) special @@ -804,7 +804,7 @@

    -beancount.parser.booking.BookingError.__repr__(self) +beancount.parser.booking.BookingError.__repr__(self) special @@ -818,10 +818,10 @@

    Source code in beancount/parser/booking.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -845,7 +845,7 @@

    -beancount.parser.booking.book(incomplete_entries, options_map) +beancount.parser.booking.book(incomplete_entries, options_map, initial_balances=None)

    @@ -867,6 +867,8 @@

  • incomplete_entries – A list of directives, with some postings possibly left with incomplete amounts as produced by the parser.

  • options_map – An options dict as produced by the parser.

  • +
  • initial_balances – A dict of (account, inventory) pairs to start booking from. +This is useful when attempting to book on top of an existing state.

  • @@ -892,33 +894,257 @@

    Source code in beancount/parser/booking.py -
    def book(incomplete_entries, options_map):
    -    """Book inventory lots and complete all positions with incomplete numbers.
    -
    -    Args:
    -      incomplete_entries: A list of directives, with some postings possibly left
    -        with incomplete amounts as produced by the parser.
    -      options_map: An options dict as produced by the parser.
    -    Returns:
    -      A pair of
    -        entries: A list of completed entries with all their postings completed.
    -        errors: New errors produced during interpolation.
    -    """
    -    # Get the list of booking methods for each account.
    -    booking_methods = collections.defaultdict(lambda: options_map["booking_method"])
    -    for entry in incomplete_entries:
    -        if isinstance(entry, data.Open) and entry.booking:
    -            booking_methods[entry.account] = entry.booking
    -
    -    # Do the booking here!
    -    entries, booking_errors = booking_full.book(incomplete_entries, options_map,
    -                                                booking_methods)
    -
    -    # Check for MISSING elements remaining.
    -    missing_errors = validate_missing_eliminated(entries, options_map)
    -
    -    return entries, (booking_errors + missing_errors)
    -
    +
    def book(incomplete_entries, options_map, initial_balances=None):
    +    """Book inventory lots and complete all positions with incomplete numbers.
    +
    +    Args:
    +      incomplete_entries: A list of directives, with some postings possibly left
    +        with incomplete amounts as produced by the parser.
    +      options_map: An options dict as produced by the parser.
    +      initial_balances: A dict of (account, inventory) pairs to start booking from.
    +        This is useful when attempting to book on top of an existing state.
    +    Returns:
    +      A pair of
    +        entries: A list of completed entries with all their postings completed.
    +        errors: New errors produced during interpolation.
    +    """
    +    # Get the list of booking methods for each account.
    +    booking_methods = collections.defaultdict(lambda: options_map["booking_method"])
    +    for entry in incomplete_entries:
    +        if isinstance(entry, data.Open) and entry.booking:
    +            booking_methods[entry.account] = entry.booking
    +
    +    # Do the booking here!
    +    entries, booking_errors = booking_full.book(
    +        incomplete_entries, options_map, booking_methods, initial_balances
    +    )
    +
    +    # Check for MISSING elements remaining.
    +    missing_errors = validate_missing_eliminated(entries, options_map)
    +
    +    return entries, (booking_errors + missing_errors)
    +
    +
    + + + + + + +
    + + + +

    +beancount.parser.booking.convert_lot_specs_to_lots(entries) + + +

    + +
    + +

    For all the entries, convert the posting's position's CostSpec to Cost +instances. In the simple method, the data provided in the CostSpec must +unambiguously provide a way to compute the cost amount.

    +

    This essentially replicates the way the old parser used to work, but +allowing positions to have the fuzzy lot specifications instead of the +resolved ones. We used to simply compute the costs locally, and this gets +rid of the CostSpec to produce the Cost without fuzzy matching. This is only +there for the sake of transition to the new matching logic.

    + + + + + + + + + + + + +
    Parameters: +
      +
    • entries – A list of incomplete directives as per the parser.

    • +
    +
    + + + + + + + + + + + +
    Returns: +
      +
    • A list of entries whose postings's position costs have been converted to +Cost instances but that may still be incomplete.

    • +
    +
    + + + + + + + + + + + +
    Exceptions: +
      +
    • ValueError – If there's a unacceptable number.

    • +
    +
    +
    + Source code in beancount/parser/booking.py +
    def convert_lot_specs_to_lots(entries):
    +    """For all the entries, convert the posting's position's CostSpec to Cost
    +    instances. In the simple method, the data provided in the CostSpec must
    +    unambiguously provide a way to compute the cost amount.
    +
    +    This essentially replicates the way the old parser used to work, but
    +    allowing positions to have the fuzzy lot specifications instead of the
    +    resolved ones. We used to simply compute the costs locally, and this gets
    +    rid of the CostSpec to produce the Cost without fuzzy matching. This is only
    +    there for the sake of transition to the new matching logic.
    +
    +    Args:
    +      entries: A list of incomplete directives as per the parser.
    +    Returns:
    +      A list of entries whose postings's position costs have been converted to
    +      Cost instances but that may still be incomplete.
    +    Raises:
    +      ValueError: If there's a unacceptable number.
    +    """
    +    new_entries = []
    +    errors = []
    +    for entry in entries:
    +        if not isinstance(entry, data.Transaction):
    +            new_entries.append(entry)
    +            continue
    +
    +        new_postings = []
    +        for posting in entry.postings:
    +            try:
    +                units = posting.units
    +                cost_spec = posting.cost
    +                cost = convert_spec_to_cost(units, cost_spec)
    +                if cost_spec is not None and cost is None:
    +                    errors.append(
    +                        BookingError(
    +                            entry.meta, "Cost syntax not supported; cost spec ignored", None
    +                        )
    +                    )
    +
    +                if cost and isinstance(units, amount.Amount):
    +                    # If there is a cost, we don't allow either a cost value of
    +                    # zero, nor a zero number of units. Note that we allow a price
    +                    # of zero as the only special case (for conversion entries), but
    +                    # never for costs.
    +                    if units.number == ZERO:
    +                        raise ValueError('Amount is zero: "{}"'.format(units))
    +                    if cost.number is not None and cost.number < ZERO:
    +                        raise ValueError('Cost is negative: "{}"'.format(cost))
    +            except ValueError as exc:
    +                errors.append(BookingError(entry.meta, str(exc), None))
    +                cost = None
    +            new_postings.append(posting._replace(cost=cost))
    +        new_entries.append(entry._replace(postings=new_postings))
    +    return new_entries, errors
    +
    +
    +
    + +
    + + + +
    + + + +

    +beancount.parser.booking.convert_spec_to_cost(units, cost_spec) + + +

    + +
    + +

    Convert a posting's CostSpec instance to a Cost.

    + + + + + + + + + + + + +
    Parameters: +
      +
    • units – An instance of Amount.

    • +
    • cost_spec – An instance of CostSpec.

    • +
    +
    + + + + + + + + + + + +
    Returns: +
      +
    • An instance of Cost.

    • +
    +
    +
    + Source code in beancount/parser/booking.py +
    def convert_spec_to_cost(units, cost_spec):
    +    """Convert a posting's CostSpec instance to a Cost.
    +
    +    Args:
    +      units: An instance of Amount.
    +      cost_spec: An instance of CostSpec.
    +    Returns:
    +      An instance of Cost.
    +    """
    +    cost = cost_spec
    +    if isinstance(units, amount.Amount):
    +        if cost_spec is not None:
    +            number_per, number_total, cost_currency, date, label, merge = cost_spec
    +
    +            # Compute the cost.
    +            if number_per is not MISSING or number_total is not None:
    +                if number_total is not None:
    +                    # Compute the per-unit cost if there is some total cost
    +                    # component involved.
    +                    units_num = units.number
    +                    cost_total = number_total
    +                    if number_per is not MISSING:
    +                        cost_total += number_per * units_num
    +                    unit_cost = cost_total / abs(units_num)
    +                else:
    +                    unit_cost = number_per
    +                cost = position.Cost(unit_cost, cost_currency, date, label)
    +            else:
    +                cost = None
    +    return cost
    +
    @@ -931,7 +1157,7 @@

    -beancount.parser.booking.validate_inventory_booking(entries, unused_options_map, booking_methods) +beancount.parser.booking.validate_inventory_booking(entries, unused_options_map, booking_methods)

    @@ -984,53 +1210,57 @@

    Source code in beancount/parser/booking.py -
    def validate_inventory_booking(entries, unused_options_map, booking_methods):
    -    """Validate that no position at cost is allowed to go negative.
    -
    -    This routine checks that when a posting reduces a position, existing or not,
    -    that the subsequent inventory does not result in a position with a negative
    -    number of units. A negative number of units would only be required for short
    -    trades of trading spreads on futures, and right now this is not supported.
    -    It would not be difficult to support this, however, but we want to be strict
    -    about it, because being pedantic about this is otherwise a great way to
    -    detect user data entry mistakes.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -      booking_methods: A mapping of account name to booking method, accumulated
    -        in the main loop.
    -    Returns:
    -      A list of errors.
    -
    -    """
    -    errors = []
    -    balances = collections.defaultdict(inventory.Inventory)
    -    for entry in entries:
    -        if isinstance(entry, data.Transaction):
    -            for posting in entry.postings:
    -                # Update the balance of each posting on its respective account
    -                # without allowing booking to a negative position, and if an error
    -                # is encountered, catch it and return it.
    -                running_balance = balances[posting.account]
    -                position_, _ = running_balance.add_position(posting)
    -
    -                # Skip this check if the booking method is set to ignore it.
    -                if booking_methods.get(posting.account, None) == data.Booking.NONE:
    -                    continue
    -
    -                # Check if the resulting inventory is mixed, which is not
    -                # allowed under the STRICT method.
    -                if running_balance.is_mixed():
    -                    errors.append(
    -                        BookingError(
    -                            entry.meta,
    -                            ("Reducing position results in inventory with positive "
    -                             "and negative lots: {}").format(position_),
    -                            entry))
    -
    -    return errors
    -
    +
    def validate_inventory_booking(entries, unused_options_map, booking_methods):
    +    """Validate that no position at cost is allowed to go negative.
    +
    +    This routine checks that when a posting reduces a position, existing or not,
    +    that the subsequent inventory does not result in a position with a negative
    +    number of units. A negative number of units would only be required for short
    +    trades of trading spreads on futures, and right now this is not supported.
    +    It would not be difficult to support this, however, but we want to be strict
    +    about it, because being pedantic about this is otherwise a great way to
    +    detect user data entry mistakes.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +      booking_methods: A mapping of account name to booking method, accumulated
    +        in the main loop.
    +    Returns:
    +      A list of errors.
    +
    +    """
    +    errors = []
    +    balances = collections.defaultdict(inventory.Inventory)
    +    for entry in entries:
    +        if isinstance(entry, data.Transaction):
    +            for posting in entry.postings:
    +                # Update the balance of each posting on its respective account
    +                # without allowing booking to a negative position, and if an error
    +                # is encountered, catch it and return it.
    +                running_balance = balances[posting.account]
    +                position_, _ = running_balance.add_position(posting)
    +
    +                # Skip this check if the booking method is set to ignore it.
    +                if booking_methods.get(posting.account, None) == data.Booking.NONE:
    +                    continue
    +
    +                # Check if the resulting inventory is mixed, which is not
    +                # allowed under the STRICT method.
    +                if running_balance.is_mixed():
    +                    errors.append(
    +                        BookingError(
    +                            entry.meta,
    +                            (
    +                                "Reducing position results in inventory with positive "
    +                                "and negative lots: {}"
    +                            ).format(position_),
    +                            entry,
    +                        )
    +                    )
    +
    +    return errors
    +

    @@ -1043,7 +1273,7 @@

    -beancount.parser.booking.validate_missing_eliminated(entries, unused_options_map) +beancount.parser.booking.validate_missing_eliminated(entries, unused_options_map)

    @@ -1087,31 +1317,34 @@

    Source code in beancount/parser/booking.py -
    def validate_missing_eliminated(entries, unused_options_map):
    -    """Validate that all the missing bits of postings have been eliminated.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of errors.
    -    """
    -    errors = []
    -    for entry in entries:
    -        if isinstance(entry, data.Transaction):
    -            for posting in entry.postings:
    -                units = posting.units
    -                cost = posting.cost
    -                if (MISSING in (units.number, units.currency) or
    -                    cost is not None and MISSING in (cost.number, cost.currency,
    -                                                     cost.date, cost.label)):
    -                    errors.append(
    -                        BookingError(entry.meta,
    -                                     "Transaction has incomplete elements",
    -                                     entry))
    -                    break
    -    return errors
    -
    +
    def validate_missing_eliminated(entries, unused_options_map):
    +    """Validate that all the missing bits of postings have been eliminated.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of errors.
    +    """
    +    errors = []
    +    for entry in entries:
    +        if isinstance(entry, data.Transaction):
    +            for posting in entry.postings:
    +                units = posting.units
    +                cost = posting.cost
    +                if (
    +                    MISSING in (units.number, units.currency)
    +                    or cost is not None
    +                    and MISSING in (cost.number, cost.currency, cost.date, cost.label)
    +                ):
    +                    errors.append(
    +                        BookingError(
    +                            entry.meta, "Transaction has incomplete elements", entry
    +                        )
    +                    )
    +                    break
    +    return errors
    +
    @@ -1247,7 +1480,7 @@

    -beancount.parser.booking_full.CategorizationError.__getnewargs__(self) +beancount.parser.booking_full.CategorizationError.__getnewargs__(self) special @@ -1261,10 +1494,10 @@

    Source code in beancount/parser/booking_full.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -1277,7 +1510,7 @@

    -beancount.parser.booking_full.CategorizationError.__new__(_cls, source, message, entry) +beancount.parser.booking_full.CategorizationError.__new__(_cls, source, message, entry) special @@ -1301,7 +1534,7 @@

    -beancount.parser.booking_full.CategorizationError.__repr__(self) +beancount.parser.booking_full.CategorizationError.__repr__(self) special @@ -1315,10 +1548,10 @@

    Source code in beancount/parser/booking_full.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -1371,7 +1604,7 @@

    -beancount.parser.booking_full.InterpolationError.__getnewargs__(self) +beancount.parser.booking_full.InterpolationError.__getnewargs__(self) special @@ -1385,10 +1618,10 @@

    Source code in beancount/parser/booking_full.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -1401,7 +1634,7 @@

    -beancount.parser.booking_full.InterpolationError.__new__(_cls, source, message, entry) +beancount.parser.booking_full.InterpolationError.__new__(_cls, source, message, entry) special @@ -1425,7 +1658,7 @@

    -beancount.parser.booking_full.InterpolationError.__repr__(self) +beancount.parser.booking_full.InterpolationError.__repr__(self) special @@ -1439,10 +1672,10 @@

    Source code in beancount/parser/booking_full.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -1539,7 +1772,7 @@

    -beancount.parser.booking_full.ReductionError.__getnewargs__(self) +beancount.parser.booking_full.ReductionError.__getnewargs__(self) special @@ -1553,10 +1786,10 @@

    Source code in beancount/parser/booking_full.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -1569,7 +1802,7 @@

    -beancount.parser.booking_full.ReductionError.__new__(_cls, source, message, entry) +beancount.parser.booking_full.ReductionError.__new__(_cls, source, message, entry) special @@ -1593,7 +1826,7 @@

    -beancount.parser.booking_full.ReductionError.__repr__(self) +beancount.parser.booking_full.ReductionError.__repr__(self) special @@ -1607,10 +1840,10 @@

    Source code in beancount/parser/booking_full.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -1663,7 +1896,7 @@

    -beancount.parser.booking_full.Refer.__getnewargs__(self) +beancount.parser.booking_full.Refer.__getnewargs__(self) special @@ -1677,10 +1910,10 @@

    Source code in beancount/parser/booking_full.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -1693,7 +1926,7 @@

    -beancount.parser.booking_full.Refer.__new__(_cls, index, units_currency, cost_currency, price_currency) +beancount.parser.booking_full.Refer.__new__(_cls, index, units_currency, cost_currency, price_currency) special @@ -1717,7 +1950,7 @@

    -beancount.parser.booking_full.Refer.__repr__(self) +beancount.parser.booking_full.Refer.__repr__(self) special @@ -1731,10 +1964,10 @@

    Source code in beancount/parser/booking_full.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -1787,7 +2020,7 @@

    -beancount.parser.booking_full.SelfReduxError.__getnewargs__(self) +beancount.parser.booking_full.SelfReduxError.__getnewargs__(self) special @@ -1801,10 +2034,10 @@

    Source code in beancount/parser/booking_full.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -1817,7 +2050,7 @@

    -beancount.parser.booking_full.SelfReduxError.__new__(_cls, source, message, entry) +beancount.parser.booking_full.SelfReduxError.__new__(_cls, source, message, entry) special @@ -1841,7 +2074,7 @@

    -beancount.parser.booking_full.SelfReduxError.__repr__(self) +beancount.parser.booking_full.SelfReduxError.__repr__(self) special @@ -1855,10 +2088,10 @@

    Source code in beancount/parser/booking_full.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -1882,7 +2115,7 @@

    -beancount.parser.booking_full.book(entries, options_map, methods) +beancount.parser.booking_full.book(entries, options_map, methods, initial_balances=None)

    @@ -1896,16 +2129,16 @@

    Source code in beancount/parser/booking_full.py -
    def book(entries, options_map, methods):
    -    """Interpolate missing data from the entries using the full historical algorithm.
    -    See the internal implementation _book() for details.
    -    This method only stripes some of the return values.
    -
    -    See _book() for arguments and return values.
    -    """
    -    entries, errors, _ = _book(entries, options_map, methods)
    -    return entries, errors
    -
    +
    def book(entries, options_map, methods, initial_balances=None):
    +    """Interpolate missing data from the entries using the full historical algorithm.
    +    See the internal implementation _book() for details.
    +    This method only stripes some of the return values.
    +
    +    See _book() for arguments and return values.
    +    """
    +    entries, errors, _ = _book(entries, options_map, methods, initial_balances)
    +    return entries, errors
    +
    @@ -1918,7 +2151,7 @@

    -beancount.parser.booking_full.book_reductions(entry, group_postings, balances, methods) +beancount.parser.booking_full.book_reductions(entry, group_postings, balances, methods)

    @@ -1986,151 +2219,159 @@

    Source code in beancount/parser/booking_full.py -
    def book_reductions(entry, group_postings, balances,
    -                    methods):
    -    """Book inventory reductions against the ante-balances.
    -
    -    This function accepts a dict of (account, Inventory balance) and for each
    -    posting that is a reduction against its inventory, attempts to find a
    -    corresponding lot or list of lots to reduce the balance with.
    -
    -    * For reducing lots, the CostSpec instance of the posting is replaced by a
    -      Cost instance.
    -
    -    * For augmenting lots, the CostSpec instance of the posting is left alone,
    -      except for its date, which is inherited from the parent Transaction.
    -
    -    Args:
    -      entry: An instance of Transaction. This is only used to refer to when
    -        logging errors.
    -      group_postings: A list of Posting instances for the group.
    -      balances: A dict of account name to inventory contents.
    -      methods: A mapping of account name to their corresponding booking
    -        method enum.
    -    Returns:
    -      A pair of
    -        booked_postings: A list of booked postings, with reducing lots resolved
    -          against specific position in the corresponding accounts'
    -          ante-inventory balances. Note single reducing posting in the input may
    -          result in multiple postings in the output. Also note that augmenting
    -          postings held-at-cost will still refer to 'cost' instances of
    -          CostSpec, left to be interpolated later.
    -        errors: A list of errors, if there were any.
    -    """
    -    errors = []
    -
    -    # A local copy of the balances dictionary which is updated just for the
    -    # duration of this function's updates, in order to take into account the
    -    # cumulative effect of all the postings inferred here
    -    local_balances = {}
    -
    -    empty = inventory.Inventory()
    -    booked_postings = []
    -    for posting in group_postings:
    -        # Process a single posting.
    -        units = posting.units
    -        costspec = posting.cost
    -        account = posting.account
    -
    -        # Note: We ensure there is no mutation on 'balances' to keep this
    -        # function without side-effects. Note that we may be able to optimize
    -        # performance later on by giving up this property.
    -        #
    -        # Also note that if there is no existing balance, then won't be any lot
    -        # reduction because none of the postings will be able to match against
    -        # any currencies of the balance.
    -        previous_balance = balances.get(account, empty)
    -        balance = local_balances.setdefault(account, copy.copy(previous_balance))
    -
    -        # Check if this is a lot held at cost.
    -        if costspec is None or units.number is MISSING:
    -            # This posting is not held at cost; we do nothing.
    -            booked_postings.append(posting)
    -        else:
    -            # This posting is held at cost; figure out if it's a reduction or an
    -            # augmentation.
    -            method = methods[account]
    -            if (method is not Booking.NONE and
    -                balance is not None and
    -                balance.is_reduced_by(units)):
    -                # This posting is a reduction.
    -
    -                # Match the positions.
    -                cost_number = compute_cost_number(costspec, units)
    -                matches = []
    -                for position in balance:
    -                    # Skip inventory contents of a different currency.
    -                    if (units.currency and
    -                        position.units.currency != units.currency):
    -                        continue
    -                    # Skip balance positions not held at cost.
    -                    if position.cost is None:
    -                        continue
    -                    if (cost_number is not None and
    -                        position.cost.number != cost_number):
    -                        continue
    -                    if (isinstance(costspec.currency, str) and
    -                        position.cost.currency != costspec.currency):
    -                        continue
    -                    if (costspec.date and
    -                        position.cost.date != costspec.date):
    -                        continue
    -                    if (costspec.label and
    -                        position.cost.label != costspec.label):
    -                        continue
    -                    matches.append(position)
    -
    -                # Check for ambiguous matches.
    -                if len(matches) == 0:
    -                    errors.append(
    -                        ReductionError(entry.meta,
    -                                       'No position matches "{}" against balance {}'.format(
    -                                           posting, balance),
    -                                       entry))
    -                    return [], errors  # This is irreconcilable, remove these postings.
    -
    -                reduction_postings, matched_postings, ambi_errors = (
    -                    booking_method.handle_ambiguous_matches(entry, posting, matches,
    -                                                            method))
    -                if ambi_errors:
    -                    errors.extend(ambi_errors)
    -                    return [], errors
    -
    -                # Add the reductions to the resulting list of booked postings.
    -                booked_postings.extend(reduction_postings)
    -
    -                # Update the local balance in order to avoid matching against
    -                # the same postings twice when processing multiple postings in
    -                # the same transaction. Note that we only do this for postings
    -                # held at cost because the other postings may need interpolation
    -                # in order to be resolved properly.
    -                for posting in reduction_postings:
    -                    balance.add_position(posting)
    -            else:
    -                # This posting is an augmentation.
    -                #
    -                # Note that we do not convert the CostSpec instances to Cost
    -                # instances, because we want to let the subsequent interpolation
    -                # process able to interpolate either the cost per-unit or the
    -                # total cost, separately.
    -
    -                # Put in the date of the parent Transaction if there is no
    -                # explicit date specified on the spec.
    -                if costspec.date is None:
    -                    dated_costspec = costspec._replace(date=entry.date)
    -                    posting = posting._replace(cost=dated_costspec)
    -
    -                # FIXME: Insert unique ids for trade tracking; right now this
    -                # creates ambiguous matches errors (and it shouldn't).
    -                # # Insert a unique label if there isn't one.
    -                # if posting.cost is not None and posting.cost.label is None:
    -                #     posting = posting._replace(
    -                #         cost=posting.cost._replace(label=unique_label()))
    -
    -                booked_postings.append(posting)
    -
    -    return booked_postings, errors
    -
    +
    def book_reductions(entry, group_postings, balances, methods):
    +    """Book inventory reductions against the ante-balances.
    +
    +    This function accepts a dict of (account, Inventory balance) and for each
    +    posting that is a reduction against its inventory, attempts to find a
    +    corresponding lot or list of lots to reduce the balance with.
    +
    +    * For reducing lots, the CostSpec instance of the posting is replaced by a
    +      Cost instance.
    +
    +    * For augmenting lots, the CostSpec instance of the posting is left alone,
    +      except for its date, which is inherited from the parent Transaction.
    +
    +    Args:
    +      entry: An instance of Transaction. This is only used to refer to when
    +        logging errors.
    +      group_postings: A list of Posting instances for the group.
    +      balances: A dict of account name to inventory contents.
    +      methods: A mapping of account name to their corresponding booking
    +        method enum.
    +    Returns:
    +      A pair of
    +        booked_postings: A list of booked postings, with reducing lots resolved
    +          against specific position in the corresponding accounts'
    +          ante-inventory balances. Note single reducing posting in the input may
    +          result in multiple postings in the output. Also note that augmenting
    +          postings held-at-cost will still refer to 'cost' instances of
    +          CostSpec, left to be interpolated later.
    +        errors: A list of errors, if there were any.
    +    """
    +    errors = []
    +
    +    # A local copy of the balances dictionary which is updated just for the
    +    # duration of this function's updates, in order to take into account the
    +    # cumulative effect of all the postings inferred here
    +    local_balances = {}
    +
    +    empty = inventory.Inventory()
    +    booked_postings = []
    +    for posting in group_postings:
    +        # Process a single posting.
    +        units = posting.units
    +        costspec = posting.cost
    +        account = posting.account
    +
    +        # Note: We ensure there is no mutation on 'balances' to keep this
    +        # function without side-effects. Note that we may be able to optimize
    +        # performance later on by giving up this property.
    +        #
    +        # Also note that if there is no existing balance, then won't be any lot
    +        # reduction because none of the postings will be able to match against
    +        # any currencies of the balance.
    +        if account not in local_balances:
    +            previous_balance = balances.get(account, empty)
    +            local_balances[account] = copy.copy(previous_balance)
    +        balance = local_balances[account]
    +
    +        # Check if this is a lot held at cost.
    +        if costspec is None or units.number is MISSING:
    +            # This posting is not held at cost; we do nothing.
    +            booked_postings.append(posting)
    +        else:
    +            # This posting is held at cost; figure out if it's a reduction or an
    +            # augmentation.
    +            method = methods[account]
    +            if (
    +                method is not Booking.NONE
    +                and balance is not None
    +                and balance.is_reduced_by(units)
    +            ):
    +                # This posting is a reduction.
    +
    +                # Match the positions.
    +                cost_number = compute_cost_number(costspec, units)
    +                matches = []
    +                for position in balance:
    +                    # Skip inventory contents of a different currency.
    +                    if units.currency and position.units.currency != units.currency:
    +                        continue
    +                    # Skip balance positions not held at cost.
    +                    if position.cost is None:
    +                        continue
    +                    if cost_number is not None and position.cost.number != cost_number:
    +                        continue
    +                    if (
    +                        isinstance(costspec.currency, str)
    +                        and position.cost.currency != costspec.currency
    +                    ):
    +                        continue
    +                    if costspec.date and position.cost.date != costspec.date:
    +                        continue
    +                    if costspec.label and position.cost.label != costspec.label:
    +                        continue
    +                    matches.append(position)
    +
    +                # Check for ambiguous matches.
    +                if len(matches) == 0:
    +                    errors.append(
    +                        ReductionError(
    +                            entry.meta,
    +                            'No position matches "{}" against balance {}'.format(
    +                                posting, balance
    +                            ),
    +                            entry,
    +                        )
    +                    )
    +                    return [], errors  # This is irreconcilable, remove these postings.
    +
    +                # TODO(blais): We'll have to change this, as we want to allow
    +                # positions crossing from negative to positive and vice-versa in
    +                # a simple application. See {d3cbd78f1029}.
    +                reduction_postings, matched_postings, ambi_errors = (
    +                    booking_method.handle_ambiguous_matches(entry, posting, matches, method)
    +                )
    +                if ambi_errors:
    +                    errors.extend(ambi_errors)
    +                    return [], errors
    +
    +                # Add the reductions to the resulting list of booked postings.
    +                booked_postings.extend(reduction_postings)
    +
    +                # Update the local balance in order to avoid matching against
    +                # the same postings twice when processing multiple postings in
    +                # the same transaction. Note that we only do this for postings
    +                # held at cost because the other postings may need interpolation
    +                # in order to be resolved properly.
    +                for posting in reduction_postings:
    +                    balance.add_position(posting)
    +            else:
    +                # This posting is an augmentation.
    +                #
    +                # Note that we do not convert the CostSpec instances to Cost
    +                # instances, because we want to let the subsequent interpolation
    +                # process able to interpolate either the cost per-unit or the
    +                # total cost, separately.
    +
    +                # Put in the date of the parent Transaction if there is no
    +                # explicit date specified on the spec.
    +                if costspec.date is None:
    +                    dated_costspec = costspec._replace(date=entry.date)
    +                    posting = posting._replace(cost=dated_costspec)
    +
    +                # FIXME: Insert unique ids for trade tracking; right now this
    +                # creates ambiguous matches errors (and it shouldn't).
    +                # # Insert a unique label if there isn't one.
    +                # if posting.cost is not None and posting.cost.label is None:
    +                #     posting = posting._replace(
    +                #         cost=posting.cost._replace(label=unique_label()))
    +
    +                booked_postings.append(posting)
    +
    +    return booked_postings, errors
    +
    @@ -2143,7 +2384,7 @@

    -beancount.parser.booking_full.categorize_by_currency(entry, balances) +beancount.parser.booking_full.categorize_by_currency(entry, balances)

    @@ -2222,191 +2463,198 @@

    Source code in beancount/parser/booking_full.py -
    def categorize_by_currency(entry, balances):
    -    """Group the postings by the currency they declare.
    -
    -    This is used to prepare the postings for the next stages: Interpolation and
    -    booking will then be carried out separately on each currency group. At the
    -    outset of this routine, we should have distinct groups of currencies without
    -    any ambiguities regarding which currency they need to balance against.
    -
    -    Here's how this works.
    -
    -    - First we apply the constraint that cost-currency and price-currency must
    -      match, if there is both a cost and a price. This reduces the space of
    -      possibilities somewhat.
    -
    -    - If the currency is explicitly specified, we put the posting in that
    -      currency's bucket.
    -
    -    - If not, we have a few methods left to disambiguate the currency:
    -
    -      1. We look at the remaining postings... if they are all of a single
    -         currency, the posting must be in that currency too.
    -
    -      2. If we cannot do that, we inspect the contents of the inventory of the
    -         account for the posting. If all the contents are of a single currency,
    -         we use that one.
    -
    -    Args:
    -      postings: A list of incomplete postings to categorize.
    -      balances: A dict of currency to inventory contents before the transaction is
    -        applied.
    -    Returns:
    -      A list of (currency string, list of tuples) items describing each postings
    -      and its interpolated currencies, and a list of generated errors for
    -      currency interpolation. The entry's original postings are left unmodified.
    -      Each tuple in the value-list contains:
    -        index: The posting index in the original entry.
    -        units_currency: The interpolated currency for units.
    -        cost_currency: The interpolated currency for cost.
    -        price_currency: The interpolated currency for price.
    -    """
    -    errors = []
    -
    -    groups = collections.defaultdict(list)
    -    sortdict = {}
    -    auto_postings = []
    -    unknown = []
    -    for index, posting in enumerate(entry.postings):
    -        units = posting.units
    -        cost = posting.cost
    -        price = posting.price
    -
    -        # Extract and override the currencies locally.
    -        units_currency = (units.currency
    -                          if units is not MISSING and units is not None
    -                          else None)
    -        cost_currency = (cost.currency
    -                         if cost is not MISSING and cost is not None
    -                         else None)
    -        price_currency = (price.currency
    -                          if price is not MISSING and price is not None
    -                          else None)
    -
    -        # First we apply the constraint that cost-currency and price-currency
    -        # must match, if there is both a cost and a price. This reduces the
    -        # space of possibilities somewhat.
    -        if cost_currency is MISSING and isinstance(price_currency, str):
    -            cost_currency = price_currency
    -        if price_currency is MISSING and isinstance(cost_currency, str):
    -            price_currency = cost_currency
    -
    -        refer = Refer(index, units_currency, cost_currency, price_currency)
    -
    -        if units is MISSING and price_currency is None:
    -            # Bucket auto-postings separately from unknown.
    -            auto_postings.append(refer)
    -        else:
    -            # Bucket with what we know so far.
    -            currency = get_bucket_currency(refer)
    -            if currency is not None:
    -                sortdict.setdefault(currency, index)
    -                groups[currency].append(refer)
    -            else:
    -                # If we need to infer the currency, store in unknown.
    -                unknown.append(refer)
    -
    -    # We look at the remaining postings... if they are all of a single currency,
    -    # the posting must be in that currency too.
    -    if unknown and len(unknown) == 1 and len(groups) == 1:
    -        (index, units_currency, cost_currency, price_currency) = unknown.pop()
    -
    -        other_currency = next(iter(groups.keys()))
    -        if price_currency is None and cost_currency is None:
    -            # Infer to the units currency.
    -            units_currency = other_currency
    -        else:
    -            # Infer to the cost and price currencies.
    -            if price_currency is MISSING:
    -                price_currency = other_currency
    -            if cost_currency is MISSING:
    -                cost_currency = other_currency
    -
    -        refer = Refer(index, units_currency, cost_currency, price_currency)
    -        currency = get_bucket_currency(refer)
    -        assert currency is not None
    -        sortdict.setdefault(currency, index)
    -        groups[currency].append(refer)
    -
    -    # Finally, try to resolve all the unknown legs using the inventory contents
    -    # of each account.
    -    for refer in unknown:
    -        (index, units_currency, cost_currency, price_currency) = refer
    -        posting = entry.postings[index]
    -        balance = balances.get(posting.account, None)
    -        if balance is None:
    -            balance = inventory.Inventory()
    -
    -        if units_currency is MISSING:
    -            balance_currencies = balance.currencies()
    -            if len(balance_currencies) == 1:
    -                units_currency = balance_currencies.pop()
    -
    -        if cost_currency is MISSING or price_currency is MISSING:
    -            balance_cost_currencies = balance.cost_currencies()
    -            if len(balance_cost_currencies) == 1:
    -                balance_cost_currency = balance_cost_currencies.pop()
    -                if price_currency is MISSING:
    -                    price_currency = balance_cost_currency
    -                if cost_currency is MISSING:
    -                    cost_currency = balance_cost_currency
    -
    -        refer = Refer(index, units_currency, cost_currency, price_currency)
    -        currency = get_bucket_currency(refer)
    -        if currency is not None:
    -            sortdict.setdefault(currency, index)
    -            groups[currency].append(refer)
    -        else:
    -            errors.append(
    -                CategorizationError(posting.meta,
    -                                    "Failed to categorize posting {}".format(index + 1),
    -                                    entry))
    -
    -    # Fill in missing units currencies if some remain as missing. This may occur
    -    # if we used the cost or price to bucket the currency but the units currency
    -    # was missing.
    -    for currency, refers in groups.items():
    -        for rindex, refer in enumerate(refers):
    -            if refer.units_currency is MISSING:
    -                posting = entry.postings[refer.index]
    -                balance = balances.get(posting.account, None)
    -                if balance is None:
    -                    continue
    -                balance_currencies = balance.currencies()
    -                if len(balance_currencies) == 1:
    -                    refers[rindex] = refer._replace(units_currency=balance_currencies.pop())
    -
    -    # Deal with auto-postings.
    -    if len(auto_postings) > 1:
    -        refer = auto_postings[-1]
    -        posting = entry.postings[refer.index]
    -        errors.append(
    -            CategorizationError(posting.meta,
    -                                "You may not have more than one auto-posting per currency",
    -                                entry))
    -        auto_postings = auto_postings[0:1]
    -    for refer in auto_postings:
    -        for currency in groups.keys():
    -            sortdict.setdefault(currency, refer.index)
    -            groups[currency].append(Refer(refer.index, currency, None, None))
    -
    -    # Issue error for all currencies which we could not resolve.
    -    for currency, refers in groups.items():
    -        for refer in refers:
    -            posting = entry.postings[refer.index]
    -            for currency, name in [(refer.units_currency, 'units'),
    -                                   (refer.cost_currency, 'cost'),
    -                                   (refer.price_currency, 'price')]:
    -                if currency is MISSING:
    -                    errors.append(CategorizationError(
    -                        posting.meta,
    -                        "Could not resolve {} currency".format(name),
    -                        entry))
    -
    -    sorted_groups = sorted(groups.items(), key=lambda item: sortdict[item[0]])
    -    return sorted_groups, errors
    -
    +
    def categorize_by_currency(entry, balances):
    +    """Group the postings by the currency they declare.
    +
    +    This is used to prepare the postings for the next stages: Interpolation and
    +    booking will then be carried out separately on each currency group. At the
    +    outset of this routine, we should have distinct groups of currencies without
    +    any ambiguities regarding which currency they need to balance against.
    +
    +    Here's how this works.
    +
    +    - First we apply the constraint that cost-currency and price-currency must
    +      match, if there is both a cost and a price. This reduces the space of
    +      possibilities somewhat.
    +
    +    - If the currency is explicitly specified, we put the posting in that
    +      currency's bucket.
    +
    +    - If not, we have a few methods left to disambiguate the currency:
    +
    +      1. We look at the remaining postings... if they are all of a single
    +         currency, the posting must be in that currency too.
    +
    +      2. If we cannot do that, we inspect the contents of the inventory of the
    +         account for the posting. If all the contents are of a single currency,
    +         we use that one.
    +
    +    Args:
    +      postings: A list of incomplete postings to categorize.
    +      balances: A dict of currency to inventory contents before the transaction is
    +        applied.
    +    Returns:
    +      A list of (currency string, list of tuples) items describing each postings
    +      and its interpolated currencies, and a list of generated errors for
    +      currency interpolation. The entry's original postings are left unmodified.
    +      Each tuple in the value-list contains:
    +        index: The posting index in the original entry.
    +        units_currency: The interpolated currency for units.
    +        cost_currency: The interpolated currency for cost.
    +        price_currency: The interpolated currency for price.
    +    """
    +    errors = []
    +
    +    groups = collections.defaultdict(list)
    +    sortdict = {}
    +    auto_postings = []
    +    unknown = []
    +    for index, posting in enumerate(entry.postings):
    +        units = posting.units
    +        cost = posting.cost
    +        price = posting.price
    +
    +        # Extract and override the currencies locally.
    +        units_currency = (
    +            units.currency if units is not MISSING and units is not None else None
    +        )
    +        cost_currency = cost.currency if cost is not MISSING and cost is not None else None
    +        price_currency = (
    +            price.currency if price is not MISSING and price is not None else None
    +        )
    +
    +        # First we apply the constraint that cost-currency and price-currency
    +        # must match, if there is both a cost and a price. This reduces the
    +        # space of possibilities somewhat.
    +        if cost_currency is MISSING and isinstance(price_currency, str):
    +            cost_currency = price_currency
    +        if price_currency is MISSING and isinstance(cost_currency, str):
    +            price_currency = cost_currency
    +
    +        refer = Refer(index, units_currency, cost_currency, price_currency)
    +
    +        if units is MISSING and price_currency is None:
    +            # Bucket auto-postings separately from unknown.
    +            auto_postings.append(refer)
    +        else:
    +            # Bucket with what we know so far.
    +            currency = get_bucket_currency(refer)
    +            if currency is not None:
    +                sortdict.setdefault(currency, index)
    +                groups[currency].append(refer)
    +            else:
    +                # If we need to infer the currency, store in unknown.
    +                unknown.append(refer)
    +
    +    # We look at the remaining postings... if they are all of a single currency,
    +    # the posting must be in that currency too.
    +    if unknown and len(unknown) == 1 and len(groups) == 1:
    +        (index, units_currency, cost_currency, price_currency) = unknown.pop()
    +
    +        other_currency = next(iter(groups.keys()))
    +        if price_currency is None and cost_currency is None:
    +            # Infer to the units currency.
    +            units_currency = other_currency
    +        else:
    +            # Infer to the cost and price currencies.
    +            if price_currency is MISSING:
    +                price_currency = other_currency
    +            if cost_currency is MISSING:
    +                cost_currency = other_currency
    +
    +        refer = Refer(index, units_currency, cost_currency, price_currency)
    +        currency = get_bucket_currency(refer)
    +        assert currency is not None
    +        sortdict.setdefault(currency, index)
    +        groups[currency].append(refer)
    +
    +    # Finally, try to resolve all the unknown legs using the inventory contents
    +    # of each account.
    +    for refer in unknown:
    +        (index, units_currency, cost_currency, price_currency) = refer
    +        posting = entry.postings[index]
    +        balance = balances.get(posting.account, None)
    +        if balance is None:
    +            balance = inventory.Inventory()
    +
    +        if units_currency is MISSING:
    +            balance_currencies = balance.currencies()
    +            if len(balance_currencies) == 1:
    +                units_currency = balance_currencies.pop()
    +
    +        if cost_currency is MISSING or price_currency is MISSING:
    +            balance_cost_currencies = balance.cost_currencies()
    +            if len(balance_cost_currencies) == 1:
    +                balance_cost_currency = balance_cost_currencies.pop()
    +                if price_currency is MISSING:
    +                    price_currency = balance_cost_currency
    +                if cost_currency is MISSING:
    +                    cost_currency = balance_cost_currency
    +
    +        refer = Refer(index, units_currency, cost_currency, price_currency)
    +        currency = get_bucket_currency(refer)
    +        if currency is not None:
    +            sortdict.setdefault(currency, index)
    +            groups[currency].append(refer)
    +        else:
    +            errors.append(
    +                CategorizationError(
    +                    posting.meta, "Failed to categorize posting {}".format(index + 1), entry
    +                )
    +            )
    +
    +    # Fill in missing units currencies if some remain as missing. This may occur
    +    # if we used the cost or price to bucket the currency but the units currency
    +    # was missing.
    +    for currency, refers in groups.items():
    +        for rindex, refer in enumerate(refers):
    +            if refer.units_currency is MISSING:
    +                posting = entry.postings[refer.index]
    +                balance = balances.get(posting.account, None)
    +                if balance is None:
    +                    continue
    +                balance_currencies = balance.currencies()
    +                if len(balance_currencies) == 1:
    +                    refers[rindex] = refer._replace(units_currency=balance_currencies.pop())
    +
    +    # Deal with auto-postings.
    +    if len(auto_postings) > 1:
    +        refer = auto_postings[-1]
    +        posting = entry.postings[refer.index]
    +        errors.append(
    +            CategorizationError(
    +                posting.meta,
    +                "You may not have more than one auto-posting per currency",
    +                entry,
    +            )
    +        )
    +        auto_postings = auto_postings[0:1]
    +    for refer in auto_postings:
    +        for currency, glist in groups.items():
    +            sortdict.setdefault(currency, refer.index)
    +            glist.append(Refer(refer.index, currency, None, None))
    +
    +    # Issue error for all currencies which we could not resolve.
    +    for currency, refers in groups.items():
    +        for refer in refers:
    +            posting = entry.postings[refer.index]
    +            for currency, name in [
    +                (refer.units_currency, "units"),
    +                (refer.cost_currency, "cost"),
    +                (refer.price_currency, "price"),
    +            ]:
    +                if currency is MISSING:
    +                    errors.append(
    +                        CategorizationError(
    +                            posting.meta,
    +                            "Could not resolve {} currency".format(name),
    +                            entry,
    +                        )
    +                    )
    +
    +    sorted_groups = sorted(groups.items(), key=lambda item: sortdict[item[0]])
    +    return sorted_groups, errors
    +
    @@ -2419,7 +2667,7 @@

    -beancount.parser.booking_full.compute_cost_number(costspec, units) +beancount.parser.booking_full.compute_cost_number(costspec, units)

    @@ -2464,34 +2712,34 @@

    Source code in beancount/parser/booking_full.py -
    def compute_cost_number(costspec, units):
    -    """Given a CostSpec, return the cost number, if possible to compute.
    -
    -    Args:
    -      costspec: A parsed instance of CostSpec.
    -      units: An instance of Amount for the units of the position.
    -    Returns:
    -      If it is not possible to calculate the cost, return None.
    -      Otherwise, returns a Decimal instance, the per-unit cost.
    -    """
    -    number_per = costspec.number_per
    -    number_total = costspec.number_total
    -    if MISSING in (number_per, number_total):
    -        return None
    -    if number_total is not None:
    -        # Compute the per-unit cost if there is some total cost
    -        # component involved.
    -        cost_total = number_total
    -        units_number = units.number
    -        if number_per is not None:
    -            cost_total += number_per * units_number
    -        unit_cost = cost_total / abs(units_number)
    -    elif number_per is None:
    -        return None
    -    else:
    -        unit_cost = number_per
    -    return unit_cost
    -
    +
    def compute_cost_number(costspec, units):
    +    """Given a CostSpec, return the cost number, if possible to compute.
    +
    +    Args:
    +      costspec: A parsed instance of CostSpec.
    +      units: An instance of Amount for the units of the position.
    +    Returns:
    +      If it is not possible to calculate the cost, return None.
    +      Otherwise, returns a Decimal instance, the per-unit cost.
    +    """
    +    number_per = costspec.number_per
    +    number_total = costspec.number_total
    +    if MISSING in (number_per, number_total):
    +        return None
    +    if number_total is not None:
    +        # Compute the per-unit cost if there is some total cost
    +        # component involved.
    +        cost_total = number_total
    +        units_number = abs(units.number)
    +        if number_per is not None:
    +            cost_total += number_per * units_number
    +        unit_cost = cost_total / units_number
    +    elif number_per is None:
    +        return None
    +    else:
    +        unit_cost = number_per
    +    return unit_cost
    +
    @@ -2504,7 +2752,7 @@

    -beancount.parser.booking_full.convert_costspec_to_cost(posting) +beancount.parser.booking_full.convert_costspec_to_cost(posting)

    @@ -2548,35 +2796,35 @@

    Source code in beancount/parser/booking_full.py -
    def convert_costspec_to_cost(posting):
    -    """Convert an instance of CostSpec to Cost, if present on the posting.
    -
    -    If the posting has no cost, it itself is just returned.
    -
    -    Args:
    -      posting: An instance of Posting.
    -    Returns:
    -      An instance of Posting with a possibly replaced 'cost' attribute.
    -    """
    -    cost = posting.cost
    -    if isinstance(cost, position.CostSpec):
    -        if cost is not None:
    -            units_number = posting.units.number
    -            number_per = cost.number_per
    -            number_total = cost.number_total
    -            if number_total is not None:
    -                # Compute the per-unit cost if there is some total cost
    -                # component involved.
    -                cost_total = number_total
    -                if number_per is not MISSING:
    -                    cost_total += number_per * units_number
    -                unit_cost = cost_total / abs(units_number)
    -            else:
    -                unit_cost = number_per
    -            new_cost = Cost(unit_cost, cost.currency, cost.date, cost.label)
    -            posting = posting._replace(units=posting.units, cost=new_cost)
    -    return posting
    -
    +
    def convert_costspec_to_cost(posting):
    +    """Convert an instance of CostSpec to Cost, if present on the posting.
    +
    +    If the posting has no cost, it itself is just returned.
    +
    +    Args:
    +      posting: An instance of Posting.
    +    Returns:
    +      An instance of Posting with a possibly replaced 'cost' attribute.
    +    """
    +    cost = posting.cost
    +    if isinstance(cost, position.CostSpec):
    +        if cost is not None:
    +            number_per = cost.number_per
    +            number_total = cost.number_total
    +            if number_total is not None:
    +                # Compute the per-unit cost if there is some total cost
    +                # component involved.
    +                units_number = abs(posting.units.number)
    +                cost_total = number_total
    +                if number_per is not MISSING:
    +                    cost_total += number_per * units_number
    +                unit_cost = cost_total / units_number
    +            else:
    +                unit_cost = number_per
    +            new_cost = Cost(unit_cost, cost.currency, cost.date, cost.label)
    +            posting = posting._replace(units=posting.units, cost=new_cost)
    +    return posting
    +
    @@ -2589,7 +2837,7 @@

    -beancount.parser.booking_full.get_bucket_currency(refer) +beancount.parser.booking_full.get_bucket_currency(refer)

    @@ -2632,25 +2880,27 @@

    Source code in beancount/parser/booking_full.py -
    def get_bucket_currency(refer):
    -    """Given currency references for a posting, return the bucket currency.
    -
    -    Args:
    -      refer: An instance of Refer.
    -    Returns:
    -      A currency string.
    -    """
    -    currency = None
    -    if isinstance(refer.cost_currency, str):
    -        currency = refer.cost_currency
    -    elif isinstance(refer.price_currency, str):
    -        currency = refer.price_currency
    -    elif (refer.cost_currency is None and
    -          refer.price_currency is None and
    -          isinstance(refer.units_currency, str)):
    -        currency = refer.units_currency
    -    return currency
    -
    +
    def get_bucket_currency(refer):
    +    """Given currency references for a posting, return the bucket currency.
    +
    +    Args:
    +      refer: An instance of Refer.
    +    Returns:
    +      A currency string.
    +    """
    +    currency = None
    +    if isinstance(refer.cost_currency, str):
    +        currency = refer.cost_currency
    +    elif isinstance(refer.price_currency, str):
    +        currency = refer.price_currency
    +    elif (
    +        refer.cost_currency is None
    +        and refer.price_currency is None
    +        and isinstance(refer.units_currency, str)
    +    ):
    +        currency = refer.units_currency
    +    return currency
    +
    @@ -2663,7 +2913,7 @@

    -beancount.parser.booking_full.has_self_reduction(postings, methods) +beancount.parser.booking_full.has_self_reduction(postings, methods)

    @@ -2708,30 +2958,30 @@

    Source code in beancount/parser/booking_full.py -
    def has_self_reduction(postings, methods):
    -    """Return true if the postings potentially reduce each other at cost.
    -
    -    Args:
    -      postings: A list of postings with uninterpolated CostSpec cost instances.
    -      methods: A mapping of account name to their corresponding booking
    -        method.
    -    Returns:
    -      A boolean, true if there's a potential for self-reduction.
    -    """
    -    # A mapping of (currency, cost-currency) and sign.
    -    cost_changes = {}
    -    for posting in postings:
    -        cost = posting.cost
    -        if cost is None:
    -            continue
    -        if methods[posting.account] is Booking.NONE:
    -            continue
    -        key = (posting.account, posting.units.currency)
    -        sign = 1 if posting.units.number > ZERO else -1
    -        if cost_changes.setdefault(key, sign) != sign:
    -            return True
    -    return False
    -
    +
    def has_self_reduction(postings, methods):
    +    """Return true if the postings potentially reduce each other at cost.
    +
    +    Args:
    +      postings: A list of postings with uninterpolated CostSpec cost instances.
    +      methods: A mapping of account name to their corresponding booking
    +        method.
    +    Returns:
    +      A boolean, true if there's a potential for self-reduction.
    +    """
    +    # A mapping of (currency, cost-currency) and sign.
    +    cost_changes = {}
    +    for posting in postings:
    +        cost = posting.cost
    +        if cost is None:
    +            continue
    +        if methods[posting.account] is Booking.NONE:
    +            continue
    +        key = (posting.account, posting.units.currency)
    +        sign = 1 if posting.units.number > ZERO else -1
    +        if cost_changes.setdefault(key, sign) != sign:
    +            return True
    +    return False
    +
    @@ -2744,7 +2994,7 @@

    -beancount.parser.booking_full.interpolate_group(postings, balances, currency, tolerances) +beancount.parser.booking_full.interpolate_group(postings, balances, currency, tolerances)

    @@ -2783,7 +3033,7 @@

    • A tuple of - postings – A lit of new posting instances. + postings – A list of new posting instances. errors: A list of errors generated during interpolation. interpolated: A boolean, true if we did have to interpolate.

      In the case of an error, this returns the original list of postings, which @@ -2799,227 +3049,259 @@

      Source code in beancount/parser/booking_full.py -
      def interpolate_group(postings, balances, currency, tolerances):
      -    """Interpolate missing numbers in the set of postings.
      -
      -    Args:
      -      postings: A list of Posting instances.
      -      balances: A dict of account to its ante-inventory.
      -      currency: The weight currency of this group, used for reporting errors.
      -      tolerances: A dict of currency to tolerance values.
      -    Returns:
      -      A tuple of
      -        postings: A lit of new posting instances.
      -        errors: A list of errors generated during interpolation.
      -        interpolated: A boolean, true if we did have to interpolate.
      -
      -      In the case of an error, this returns the original list of postings, which
      -      is still incomplete. If an error is returned, you should probably skip the
      -      transaction altogether, or just not include the postings in it. (An
      -      alternative behaviour would be to return only the list of valid postings,
      -      but that would likely result in an unbalanced transaction. We do it this
      -      way by choice.)
      -    """
      -    errors = []
      -
      -    # Figure out which type of amount is missing, by creating a list of
      -    # incomplete postings and which type of units is missing.
      -    incomplete = []
      -    for index, posting in enumerate(postings):
      -        units = posting.units
      -        cost = posting.cost
      -        price = posting.price
      -
      -        # Identify incomplete parts of the Posting components.
      -        if units.number is MISSING:
      -            incomplete.append((MissingType.UNITS, index))
      -
      -        if isinstance(cost, CostSpec):
      -            if cost and cost.number_per is MISSING:
      -                incomplete.append((MissingType.COST_PER, index))
      -            if cost and cost.number_total is MISSING:
      -                incomplete.append((MissingType.COST_TOTAL, index))
      -        else:
      -            # Check that a resolved instance of Cost never needs interpolation.
      -            #
      -            # Note that in theory we could support the interpolation of regular
      -            # per-unit costs in these if we wanted to; but because they're all
      -            # reducing postings that have been booked earlier, those never need
      -            # to be interpolated.
      -            if cost is not None:
      -                assert isinstance(cost.number, Decimal), (
      -                    "Internal error: cost has no number: {}".format(cost))
      -
      -        if price and price.number is MISSING:
      -            incomplete.append((MissingType.PRICE, index))
      -
      -    # The replacement posting for the incomplete posting of this group.
      -    new_posting = None
      -
      -    if len(incomplete) == 0:
      -        # If there are no missing numbers, just convert the CostSpec to Cost and
      -        # return that.
      -        out_postings = [convert_costspec_to_cost(posting)
      -                        for posting in postings]
      -
      -    elif len(incomplete) > 1:
      -        # If there is more than a single value to be interpolated, generate an
      -        # error and return no postings.
      -        _, posting_index = incomplete[0]
      -        errors.append(InterpolationError(
      -            postings[posting_index].meta,
      -            "Too many missing numbers for currency group '{}'".format(currency),
      -            None))
      -        out_postings = []
      -
      -    else:
      -        # If there is a single missing number, calculate it and fill it in here.
      -        missing, index = incomplete[0]
      -        incomplete_posting = postings[index]
      -
      -        # Convert augmenting postings' costs from CostSpec to corresponding Cost
      -        # instances, except for the incomplete posting.
      -        new_postings = [(posting
      -                         if posting is incomplete_posting
      -                         else convert_costspec_to_cost(posting))
      -                        for posting in postings]
      -
      -        # Compute the balance of the other postings.
      -        residual = interpolate.compute_residual(posting
      -                                                for posting in new_postings
      -                                                if posting is not incomplete_posting)
      -        assert len(residual) < 2, "Internal error in grouping postings by currencies."
      -        if not residual.is_empty():
      -            respos = next(iter(residual))
      -            assert respos.cost is None, (
      -                "Internal error; cost appears in weight calculation.")
      -            assert respos.units.currency == currency, (
      -                "Internal error; residual different than currency group.")
      -            weight = -respos.units.number
      -            weight_currency = respos.units.currency
      -        else:
      -            weight = ZERO
      -            weight_currency = currency
      -
      -        if missing == MissingType.UNITS:
      -            units = incomplete_posting.units
      -            cost = incomplete_posting.cost
      -            if cost:
      -                # Handle the special case where we only have total cost.
      -                if cost.number_per == ZERO:
      -                    errors.append(InterpolationError(
      -                        incomplete_posting.meta,
      -                        "Cannot infer per-unit cost only from total", None))
      -                    return postings, errors, True
      -
      -                assert cost.currency == weight_currency, (
      -                    "Internal error; residual currency different than missing currency.")
      -                cost_total = cost.number_total or ZERO
      -                units_number = (weight - cost_total) / cost.number_per
      -
      -            elif incomplete_posting.price:
      -                assert incomplete_posting.price.currency == weight_currency, (
      -                    "Internal error; residual currency different than missing currency.")
      -                units_number = weight / incomplete_posting.price.number
      -
      -            else:
      -                assert units.currency == weight_currency, (
      -                    "Internal error; residual currency different than missing currency.")
      -                units_number = weight
      -
      -            # Quantize the interpolated units if necessary.
      -            units_number = interpolate.quantize_with_tolerance(tolerances,
      -                                                               units.currency,
      -                                                               units_number)
      -
      -            if weight != ZERO:
      -                new_pos = Position(Amount(units_number, units.currency), cost)
      -                new_posting = incomplete_posting._replace(units=new_pos.units,
      -                                                          cost=new_pos.cost)
      -            else:
      -                new_posting = None
      -
      -        elif missing == MissingType.COST_PER:
      -            units = incomplete_posting.units
      -            cost = incomplete_posting.cost
      -            assert cost.currency == weight_currency, (
      -                "Internal error; residual currency different than missing currency.")
      -            if units.number != ZERO:
      -                number_per = (weight - (cost.number_total or ZERO)) / units.number
      -                new_cost = cost._replace(number_per=number_per)
      -                new_pos = Position(units, new_cost)
      -                new_posting = incomplete_posting._replace(units=new_pos.units,
      -                                                          cost=new_pos.cost)
      -            else:
      -                new_posting = None
      -
      -        elif missing == MissingType.COST_TOTAL:
      -            units = incomplete_posting.units
      -            cost = incomplete_posting.cost
      -            assert cost.currency == weight_currency, (
      -                "Internal error; residual currency different than missing currency.")
      -            number_total = (weight - cost.number_per * units.number)
      -            new_cost = cost._replace(number_total=number_total)
      -            new_pos = Position(units, new_cost)
      -            new_posting = incomplete_posting._replace(units=new_pos.units,
      -                                                      cost=new_pos.cost)
      -
      -        elif missing == MissingType.PRICE:
      -            units = incomplete_posting.units
      -            cost = incomplete_posting.cost
      -            if cost is not None:
      -                errors.append(InterpolationError(
      -                    incomplete_posting.meta,
      -                    "Cannot infer price for postings with units held at cost", None))
      -                return postings, errors, True
      -            else:
      -                price = incomplete_posting.price
      -                assert price.currency == weight_currency, (
      -                    "Internal error; residual currency different than missing currency.")
      -                new_price_number = abs(weight / units.number)
      -                new_posting = incomplete_posting._replace(price=Amount(new_price_number,
      -                                                                       price.currency))
      -
      -        else:
      -            assert False, "Internal error; Invalid missing type."
      -
      -        # Replace the number in the posting.
      -        if new_posting is not None:
      -            # Set meta-data on the new posting to indicate it was interpolated.
      -            if new_posting.meta is None:
      -                new_posting = new_posting._replace(meta={})
      -            new_posting.meta[interpolate.AUTOMATIC_META] = True
      -
      -            # Convert augmenting posting costs from CostSpec to a corresponding
      -            # Cost instance.
      -            new_postings[index] = convert_costspec_to_cost(new_posting)
      -        else:
      -            del new_postings[index]
      -        out_postings = new_postings
      -
      -    assert all(not isinstance(posting.cost, CostSpec)
      -               for posting in out_postings)
      -
      -    # Check that units are non-zero and that no cost remains negative; issue an
      -    # error if this is the case.
      -    for posting in out_postings:
      -        if posting.cost is None:
      -            continue
      -        # If there is a cost, we don't allow either a cost value of zero,
      -        # nor a zero number of units. Note that we allow a price of zero as
      -        # the only special case allowed (for conversion entries), but never
      -        # for costs.
      -        if posting.units.number == ZERO:
      -            errors.append(InterpolationError(
      -                posting.meta,
      -                'Amount is zero: "{}"'.format(posting.units), None))
      -        if posting.cost.number < ZERO:
      -            errors.append(InterpolationError(
      -                posting.meta,
      -                'Cost is negative: "{}"'.format(posting.cost), None))
      -
      -    return out_postings, errors, (new_posting is not None)
      -
      +
      def interpolate_group(postings, balances, currency, tolerances):
      +    """Interpolate missing numbers in the set of postings.
      +
      +    Args:
      +      postings: A list of Posting instances.
      +      balances: A dict of account to its ante-inventory.
      +      currency: The weight currency of this group, used for reporting errors.
      +      tolerances: A dict of currency to tolerance values.
      +    Returns:
      +      A tuple of
      +        postings: A list of new posting instances.
      +        errors: A list of errors generated during interpolation.
      +        interpolated: A boolean, true if we did have to interpolate.
      +
      +      In the case of an error, this returns the original list of postings, which
      +      is still incomplete. If an error is returned, you should probably skip the
      +      transaction altogether, or just not include the postings in it. (An
      +      alternative behaviour would be to return only the list of valid postings,
      +      but that would likely result in an unbalanced transaction. We do it this
      +      way by choice.)
      +    """
      +    errors = []
      +
      +    # Figure out which type of amount is missing, by creating a list of
      +    # incomplete postings and which type of units is missing.
      +    incomplete = []
      +    for index, posting in enumerate(postings):
      +        units = posting.units
      +        cost = posting.cost
      +        price = posting.price
      +
      +        # Identify incomplete parts of the Posting components.
      +        if units.number is MISSING:
      +            incomplete.append((MissingType.UNITS, index))
      +
      +        if isinstance(cost, CostSpec):
      +            if cost and cost.number_per is MISSING:
      +                incomplete.append((MissingType.COST_PER, index))
      +            if cost and cost.number_total is MISSING:
      +                incomplete.append((MissingType.COST_TOTAL, index))
      +        else:
      +            # Check that a resolved instance of Cost never needs interpolation.
      +            #
      +            # Note that in theory we could support the interpolation of regular
      +            # per-unit costs in these if we wanted to; but because they're all
      +            # reducing postings that have been booked earlier, those never need
      +            # to be interpolated.
      +            if cost is not None:
      +                assert isinstance(
      +                    cost.number, Decimal
      +                ), "Internal error: cost has no number: {}; on postings: {}".format(
      +                    cost, postings
      +                )
      +
      +        if price and price.number is MISSING:
      +            incomplete.append((MissingType.PRICE, index))
      +
      +    # The replacement posting for the incomplete posting of this group.
      +    new_posting = None
      +
      +    if len(incomplete) == 0:
      +        # If there are no missing numbers, just convert the CostSpec to Cost and
      +        # return that.
      +        out_postings = [convert_costspec_to_cost(posting) for posting in postings]
      +
      +    elif len(incomplete) > 1:
      +        # If there is more than a single value to be interpolated, generate an
      +        # error and return no postings.
      +        _, posting_index = incomplete[0]
      +        errors.append(
      +            InterpolationError(
      +                postings[posting_index].meta,
      +                "Too many missing numbers for currency group '{}'".format(currency),
      +                None,
      +            )
      +        )
      +        out_postings = []
      +
      +    else:
      +        # If there is a single missing number, calculate it and fill it in here.
      +        missing, index = incomplete[0]
      +        incomplete_posting = postings[index]
      +
      +        # Convert augmenting postings' costs from CostSpec to corresponding Cost
      +        # instances, except for the incomplete posting.
      +        new_postings = [
      +            (
      +                posting
      +                if posting is incomplete_posting
      +                else convert_costspec_to_cost(posting)
      +            )
      +            for posting in postings
      +        ]
      +
      +        # Compute the balance of the other postings.
      +        residual = interpolate.compute_residual(
      +            posting for posting in new_postings if posting is not incomplete_posting
      +        )
      +        assert len(residual) < 2, "Internal error in grouping postings by currencies."
      +        if not residual.is_empty():
      +            respos = next(iter(residual))
      +            assert (
      +                respos.cost is None
      +            ), "Internal error; cost appears in weight calculation."
      +            assert (
      +                respos.units.currency == currency
      +            ), "Internal error; residual different than currency group."
      +            weight = -respos.units.number
      +            weight_currency = respos.units.currency
      +        else:
      +            weight = ZERO
      +            weight_currency = currency
      +
      +        if missing == MissingType.UNITS:
      +            units = incomplete_posting.units
      +            cost = incomplete_posting.cost
      +            if cost:
      +                # Handle the special case where we only have total cost.
      +                if cost.number_per == ZERO:
      +                    errors.append(
      +                        InterpolationError(
      +                            incomplete_posting.meta,
      +                            "Cannot infer per-unit cost only from total",
      +                            None,
      +                        )
      +                    )
      +                    return postings, errors, True
      +
      +                assert (
      +                    cost.currency == weight_currency
      +                ), "Internal error; residual currency different than missing currency."
      +                cost_total = cost.number_total or ZERO
      +                units_number = (weight - cost_total) / cost.number_per
      +
      +            elif incomplete_posting.price:
      +                assert (
      +                    incomplete_posting.price.currency == weight_currency
      +                ), "Internal error; residual currency different than missing currency."
      +                units_number = weight / incomplete_posting.price.number
      +
      +            else:
      +                assert (
      +                    units.currency == weight_currency
      +                ), "Internal error; residual currency different than missing currency."
      +                units_number = weight
      +
      +            # Quantize the interpolated units if necessary.
      +            units_number = interpolate.quantize_with_tolerance(
      +                tolerances, units.currency, units_number
      +            )
      +
      +            if weight != ZERO:
      +                new_pos = Position(Amount(units_number, units.currency), cost)
      +                new_posting = incomplete_posting._replace(
      +                    units=new_pos.units, cost=new_pos.cost
      +                )
      +            else:
      +                new_posting = None
      +
      +        elif missing == MissingType.COST_PER:
      +            units = incomplete_posting.units
      +            cost = incomplete_posting.cost
      +            assert (
      +                cost.currency == weight_currency
      +            ), "Internal error; residual currency different than missing currency."
      +            if units.number != ZERO:
      +                number_per = (weight - (cost.number_total or ZERO)) / units.number
      +                new_cost = cost._replace(number_per=number_per)
      +                new_pos = Position(units, new_cost)
      +                new_posting = incomplete_posting._replace(
      +                    units=new_pos.units, cost=new_pos.cost
      +                )
      +            else:
      +                new_posting = None
      +
      +        elif missing == MissingType.COST_TOTAL:
      +            units = incomplete_posting.units
      +            cost = incomplete_posting.cost
      +            assert (
      +                cost.currency == weight_currency
      +            ), "Internal error; residual currency different than missing currency."
      +            number_total = weight - cost.number_per * units.number
      +            new_cost = cost._replace(number_total=number_total)
      +            new_pos = Position(units, new_cost)
      +            new_posting = incomplete_posting._replace(
      +                units=new_pos.units, cost=new_pos.cost
      +            )
      +
      +        elif missing == MissingType.PRICE:
      +            units = incomplete_posting.units
      +            cost = incomplete_posting.cost
      +            if cost is not None:
      +                errors.append(
      +                    InterpolationError(
      +                        incomplete_posting.meta,
      +                        "Cannot infer price for postings with units held at cost",
      +                        None,
      +                    )
      +                )
      +                return postings, errors, True
      +            else:
      +                price = incomplete_posting.price
      +                assert (
      +                    price.currency == weight_currency
      +                ), "Internal error; residual currency different than missing currency."
      +                new_price_number = abs(weight / units.number)
      +                new_posting = incomplete_posting._replace(
      +                    price=Amount(new_price_number, price.currency)
      +                )
      +
      +        else:
      +            assert False, "Internal error; Invalid missing type."
      +
      +        # Replace the number in the posting.
      +        if new_posting is not None:
      +            # Set meta-data on the new posting to indicate it was interpolated.
      +            if new_posting.meta is None:
      +                new_posting = new_posting._replace(meta={})
      +            new_posting.meta[interpolate.AUTOMATIC_META] = True
      +
      +            # Convert augmenting posting costs from CostSpec to a corresponding
      +            # Cost instance.
      +            new_postings[index] = convert_costspec_to_cost(new_posting)
      +        else:
      +            del new_postings[index]
      +        out_postings = new_postings
      +
      +    assert all(not isinstance(posting.cost, CostSpec) for posting in out_postings)
      +
      +    # Check that units are non-zero and that no cost remains negative; issue an
      +    # error if this is the case.
      +    for posting in out_postings:
      +        if posting.cost is None:
      +            continue
      +        # If there is a cost, we don't allow either a cost value of zero,
      +        # nor a zero number of units. Note that we allow a price of zero as
      +        # the only special case allowed (for conversion entries), but never
      +        # for costs.
      +        if posting.units.number == ZERO:
      +            errors.append(
      +                InterpolationError(
      +                    posting.meta, 'Amount is zero: "{}"'.format(posting.units), None
      +                )
      +            )
      +        if posting.cost.number is not None and posting.cost.number < ZERO:
      +            errors.append(
      +                InterpolationError(
      +                    posting.meta, 'Cost is negative: "{}"'.format(posting.cost), None
      +                )
      +            )
      +
      +    return out_postings, errors, (new_posting is not None)
      +
      @@ -3032,7 +3314,7 @@

      -beancount.parser.booking_full.replace_currencies(postings, refer_groups) +beancount.parser.booking_full.replace_currencies(postings, refer_groups)

      @@ -3080,47 +3362,47 @@

      Source code in beancount/parser/booking_full.py -
      def replace_currencies(postings, refer_groups):
      -    """Replace resolved currencies in the entry's Postings.
      -
      -    This essentially applies the findings of categorize_by_currency() to produce
      -    new postings with all currencies resolved.
      -
      -    Args:
      -      postings: A list of Posting instances to replace.
      -      refer_groups: A list of (currency, list of posting references) items as
      -        returned by categorize_by_currency().
      -    Returns:
      -      A new list of items of (currency, list of Postings), postings for which the
      -      currencies have been replaced by their interpolated currency values.
      -    """
      -    new_groups = []
      -    for currency, refers in refer_groups:
      -        new_postings = []
      -        for refer in sorted(refers, key=lambda r: r.index):
      -            posting = postings[refer.index]
      -            units = posting.units
      -            if units is MISSING or units is None:
      -                posting = posting._replace(units=Amount(MISSING, refer.units_currency))
      -            else:
      -                replace = False
      -                cost = posting.cost
      -                price = posting.price
      -                if units.currency is MISSING:
      -                    units = Amount(units.number, refer.units_currency)
      -                    replace = True
      -                if cost and cost.currency is MISSING:
      -                    cost = cost._replace(currency=refer.cost_currency)
      -                    replace = True
      -                if price and price.currency is MISSING:
      -                    price = Amount(price.number, refer.price_currency)
      -                    replace = True
      -                if replace:
      -                    posting = posting._replace(units=units, cost=cost, price=price)
      -            new_postings.append(posting)
      -        new_groups.append((currency, new_postings))
      -    return new_groups
      -
      +
      def replace_currencies(postings, refer_groups):
      +    """Replace resolved currencies in the entry's Postings.
      +
      +    This essentially applies the findings of categorize_by_currency() to produce
      +    new postings with all currencies resolved.
      +
      +    Args:
      +      postings: A list of Posting instances to replace.
      +      refer_groups: A list of (currency, list of posting references) items as
      +        returned by categorize_by_currency().
      +    Returns:
      +      A new list of items of (currency, list of Postings), postings for which the
      +      currencies have been replaced by their interpolated currency values.
      +    """
      +    new_groups = []
      +    for currency, refers in refer_groups:
      +        new_postings = []
      +        for refer in sorted(refers, key=lambda r: r.index):
      +            posting = postings[refer.index]
      +            units = posting.units
      +            if units is MISSING or units is None:
      +                posting = posting._replace(units=Amount(MISSING, refer.units_currency))
      +            else:
      +                replace = False
      +                cost = posting.cost
      +                price = posting.price
      +                if units.currency is MISSING:
      +                    units = Amount(units.number, refer.units_currency)
      +                    replace = True
      +                if cost and cost.currency is MISSING:
      +                    cost = cost._replace(currency=refer.cost_currency)
      +                    replace = True
      +                if price and price.currency is MISSING:
      +                    price = Amount(price.number, refer.price_currency)
      +                    replace = True
      +                if replace:
      +                    posting = posting._replace(units=units, cost=cost, price=price)
      +            new_postings.append(posting)
      +        new_groups.append((currency, new_postings))
      +    return new_groups
      +
      @@ -3133,7 +3415,7 @@

      -beancount.parser.booking_full.unique_label() +beancount.parser.booking_full.unique_label()

      @@ -3144,10 +3426,10 @@

      Source code in beancount/parser/booking_full.py -
      def unique_label() -> Text:
      -    "Return a globally unique label for cost entries."
      -    return str(uuid.uuid4())
      -
      +
      def unique_label() -> str:
      +    "Return a globally unique label for cost entries."
      +    return str(uuid.uuid4())
      +
      @@ -3229,7 +3511,7 @@

      -beancount.parser.booking_method.AmbiguousMatchError.__getnewargs__(self) +beancount.parser.booking_method.AmbiguousMatchError.__getnewargs__(self) special @@ -3243,10 +3525,10 @@

      Source code in beancount/parser/booking_method.py -
      def __getnewargs__(self):
      -    'Return self as a plain tuple.  Used by copy and pickle.'
      -    return _tuple(self)
      -
      +
      def __getnewargs__(self):
      +    'Return self as a plain tuple.  Used by copy and pickle.'
      +    return _tuple(self)
      +
      @@ -3259,7 +3541,7 @@

      -beancount.parser.booking_method.AmbiguousMatchError.__new__(_cls, source, message, entry) +beancount.parser.booking_method.AmbiguousMatchError.__new__(_cls, source, message, entry) special @@ -3283,7 +3565,7 @@

      -beancount.parser.booking_method.AmbiguousMatchError.__repr__(self) +beancount.parser.booking_method.AmbiguousMatchError.__repr__(self) special @@ -3297,10 +3579,10 @@

      Source code in beancount/parser/booking_method.py -
      def __repr__(self):
      -    'Return a nicely formatted representation string'
      -    return self.__class__.__name__ + repr_fmt % self
      -
      +
      def __repr__(self):
      +    'Return a nicely formatted representation string'
      +    return self.__class__.__name__ + repr_fmt % self
      +
      @@ -3324,7 +3606,7 @@

      -beancount.parser.booking_method.booking_method_AVERAGE(entry, posting, matches) +beancount.parser.booking_method.booking_method_AVERAGE(entry, posting, matches)

      @@ -3335,78 +3617,95 @@

      Source code in beancount/parser/booking_method.py -
      def booking_method_AVERAGE(entry, posting, matches):
      -    """AVERAGE booking method implementation."""
      -    booked_reductions = []
      -    booked_matches = []
      -    errors = [AmbiguousMatchError(entry.meta, "AVERAGE method is not supported", entry)]
      -    return booked_reductions, booked_matches, errors, False
      -
      -    # FIXME: Future implementation here.
      -    # pylint: disable=unreachable
      -    if False: # pylint: disable=using-constant-test
      -        # DISABLED - This is the code for AVERAGE, which is currently disabled.
      -
      -        # If there is more than a single match we need to ultimately merge the
      -        # postings. Also, if the reducing posting provides a specific cost, we
      -        # need to update the cost basis as well. Both of these cases are carried
      -        # out by removing all the matches and readding them later on.
      -        if len(matches) == 1 and (
      -                not isinstance(posting.cost.number_per, Decimal) and
      -                not isinstance(posting.cost.number_total, Decimal)):
      -            # There is no cost. Just reduce the one leg. This should be the
      -            # normal case if we always merge augmentations and the user lets
      -            # Beancount deal with the cost.
      -            match = matches[0]
      -            sign = -1 if posting.units.number < ZERO else 1
      -            number = min(abs(match.units.number), abs(posting.units.number))
      -            match_units = Amount(number * sign, match.units.currency)
      -            booked_reductions.append(posting._replace(units=match_units, cost=match.cost))
      -            insufficient = (match_units.number != posting.units.number)
      -        else:
      -            # Merge the matching postings to a single one.
      -            merged_units = inventory.Inventory()
      -            merged_cost = inventory.Inventory()
      -            for match in matches:
      -                merged_units.add_amount(match.units)
      -                merged_cost.add_amount(convert.get_weight(match))
      -            if len(merged_units) != 1 or len(merged_cost) != 1:
      -                errors.append(
      -                    AmbiguousMatchError(
      -                        entry.meta,
      -                        'Cannot merge positions in multiple currencies: {}'.format(
      -                            ', '.join(position.to_string(match_posting)
      -                                      for match_posting in matches)), entry))
      -            else:
      -                if (isinstance(posting.cost.number_per, Decimal) or
      -                    isinstance(posting.cost.number_total, Decimal)):
      -                    errors.append(
      -                        AmbiguousMatchError(
      -                            entry.meta,
      -                            "Explicit cost reductions aren't supported yet: {}".format(
      -                                position.to_string(posting)), entry))
      -                else:
      -                    # Insert postings to remove all the matches.
      -                    booked_reductions.extend(
      -                        posting._replace(units=-match.units, cost=match.cost,
      -                                         flag=flags.FLAG_MERGING)
      -                        for match in matches)
      -                    units = merged_units[0].units
      -                    date = matches[0].cost.date  ## FIXME: Select which one,
      -                                                 ## oldest or latest.
      -                    cost_units = merged_cost[0].units
      -                    cost = Cost(cost_units.number/units.number, cost_units.currency,
      -                                date, None)
      -
      -                    # Insert a posting to refill those with a replacement match.
      -                    booked_reductions.append(
      -                        posting._replace(units=units, cost=cost, flag=flags.FLAG_MERGING))
      -
      -                    # Now, match the reducing request against this lot.
      -                    booked_reductions.append(
      -                        posting._replace(units=posting.units, cost=cost))
      -                    insufficient = abs(posting.units.number) > abs(units.number)
      -
      +
      def booking_method_AVERAGE(entry, posting, matches):
      +    """AVERAGE booking method implementation."""
      +    booked_reductions = []
      +    booked_matches = []
      +    errors = [AmbiguousMatchError(entry.meta, "AVERAGE method is not supported", entry)]
      +    return booked_reductions, booked_matches, errors, False
      +
      +    # FIXME: Future implementation here.
      +
      +    if False:
      +        # DISABLED - This is the code for AVERAGE, which is currently disabled.
      +
      +        # If there is more than a single match we need to ultimately merge the
      +        # postings. Also, if the reducing posting provides a specific cost, we
      +        # need to update the cost basis as well. Both of these cases are carried
      +        # out by removing all the matches and readding them later on.
      +        if len(matches) == 1 and (
      +            not isinstance(posting.cost.number_per, Decimal)
      +            and not isinstance(posting.cost.number_total, Decimal)
      +        ):
      +            # There is no cost. Just reduce the one leg. This should be the
      +            # normal case if we always merge augmentations and the user lets
      +            # Beancount deal with the cost.
      +            match = matches[0]
      +            sign = -1 if posting.units.number < ZERO else 1
      +            number = min(abs(match.units.number), abs(posting.units.number))
      +            match_units = Amount(number * sign, match.units.currency)
      +            booked_reductions.append(posting._replace(units=match_units, cost=match.cost))
      +            _insufficient = match_units.number != posting.units.number
      +        else:
      +            # Merge the matching postings to a single one.
      +            merged_units = inventory.Inventory()
      +            merged_cost = inventory.Inventory()
      +            for match in matches:
      +                merged_units.add_amount(match.units)
      +                merged_cost.add_amount(convert.get_weight(match))
      +            if len(merged_units) != 1 or len(merged_cost) != 1:
      +                errors.append(
      +                    AmbiguousMatchError(
      +                        entry.meta,
      +                        "Cannot merge positions in multiple currencies: {}".format(
      +                            ", ".join(
      +                                position.to_string(match_posting)
      +                                for match_posting in matches
      +                            )
      +                        ),
      +                        entry,
      +                    )
      +                )
      +            else:
      +                if isinstance(posting.cost.number_per, Decimal) or isinstance(
      +                    posting.cost.number_total, Decimal
      +                ):
      +                    errors.append(
      +                        AmbiguousMatchError(
      +                            entry.meta,
      +                            "Explicit cost reductions aren't supported yet: {}".format(
      +                                position.to_string(posting)
      +                            ),
      +                            entry,
      +                        )
      +                    )
      +                else:
      +                    # Insert postings to remove all the matches.
      +                    booked_reductions.extend(
      +                        posting._replace(
      +                            units=-match.units, cost=match.cost, flag=flags.FLAG_MERGING
      +                        )
      +                        for match in matches
      +                    )
      +                    units = merged_units[0].units
      +                    date = matches[0].cost.date  ## FIXME: Select which one,
      +                    ## oldest or latest.
      +                    cost_units = merged_cost[0].units
      +                    cost = Cost(
      +                        cost_units.number / units.number, cost_units.currency, date, None
      +                    )
      +
      +                    # Insert a posting to refill those with a replacement match.
      +                    booked_reductions.append(
      +                        posting._replace(units=units, cost=cost, flag=flags.FLAG_MERGING)
      +                    )
      +
      +                    # Now, match the reducing request against this lot.
      +                    booked_reductions.append(
      +                        posting._replace(units=posting.units, cost=cost)
      +                    )
      +                    _insufficient = abs(posting.units.number) > abs(units.number)
      +
      @@ -3419,7 +3718,7 @@

      -beancount.parser.booking_method.booking_method_FIFO(entry, posting, matches) +beancount.parser.booking_method.booking_method_FIFO(entry, posting, matches)

      @@ -3430,10 +3729,37 @@

      Source code in beancount/parser/booking_method.py -
      def booking_method_FIFO(entry, posting, matches):
      -    """FIFO booking method implementation."""
      -    return _booking_method_xifo(entry, posting, matches, False)
      -
      +
      def booking_method_FIFO(entry, posting, matches):
      +    """FIFO booking method implementation."""
      +    return _booking_method_xifo(entry, posting, matches, "date", False)
      +
      + + + + + + + +
      + + + +

      +beancount.parser.booking_method.booking_method_HIFO(entry, posting, matches) + + +

      + +
      + +

      HIFO booking method implementation.

      + +
      + Source code in beancount/parser/booking_method.py +
      def booking_method_HIFO(entry, posting, matches):
      +    """HIFO booking method implementation."""
      +    return _booking_method_xifo(entry, posting, matches, "number", True)
      +
      @@ -3446,7 +3772,7 @@

      -beancount.parser.booking_method.booking_method_LIFO(entry, posting, matches) +beancount.parser.booking_method.booking_method_LIFO(entry, posting, matches)

      @@ -3457,10 +3783,10 @@

      Source code in beancount/parser/booking_method.py -
      def booking_method_LIFO(entry, posting, matches):
      -    """LIFO booking method implementation."""
      -    return _booking_method_xifo(entry, posting, matches, True)
      -
      +
      def booking_method_LIFO(entry, posting, matches):
      +    """LIFO booking method implementation."""
      +    return _booking_method_xifo(entry, posting, matches, "date", True)
      +

      @@ -3473,7 +3799,7 @@

      -beancount.parser.booking_method.booking_method_NONE(entry, posting, matches) +beancount.parser.booking_method.booking_method_NONE(entry, posting, matches)

      @@ -3484,23 +3810,23 @@

      Source code in beancount/parser/booking_method.py -
      def booking_method_NONE(entry, posting, matches):
      -    """NONE booking method implementation."""
      -
      -    # This never needs to match against any existing positions... we
      -    # disregard the matches, there's never any error. Note that this never
      -    # gets called in practice, we want to treat NONE postings as
      -    # augmentations. Default behaviour is to return them with their original
      -    # CostSpec, and the augmentation code will handle signaling an error if
      -    # there is insufficient detail to carry out the conversion to an
      -    # instance of Cost.
      -
      -    # Note that it's an interesting question whether a reduction on an
      -    # account with NONE method which happens to match a single position
      -    # ought to be matched against it. We don't allow it for now.
      -
      -    return [posting], [], False
      -
      +
      def booking_method_NONE(entry, posting, matches):
      +    """NONE booking method implementation."""
      +
      +    # This never needs to match against any existing positions... we
      +    # disregard the matches, there's never any error. Note that this never
      +    # gets called in practice, we want to treat NONE postings as
      +    # augmentations. Default behaviour is to return them with their original
      +    # CostSpec, and the augmentation code will handle signaling an error if
      +    # there is insufficient detail to carry out the conversion to an
      +    # instance of Cost.
      +
      +    # Note that it's an interesting question whether a reduction on an
      +    # account with NONE method which happens to match a single position
      +    # ought to be matched against it. We don't allow it for now.
      +
      +    return [posting], [], False
      +
      @@ -3513,108 +3839,112 @@

      -beancount.parser.booking_method.booking_method_STRICT(entry, posting, matches) +beancount.parser.booking_method.booking_method_STRICT(entry, posting, matches)

      -

      Strict booking method.

      +

      Strict booking method. This method fails if there are ambiguous matches.

      - - - - - - - - - - - -
      Parameters: -
        -
      • entry – The parent Transaction instance.

      • -
      • posting – An instance of Posting, the reducing posting which we're -attempting to match.

      • -
      • matches – A list of matching Position instances from the ante-inventory. -Those positions are known to already match the 'posting' spec.

      • -
      -
      - - - - - - - - - - - -
      Returns: -
        -
      • A triple of - booked_reductions – A list of matched Posting instances, whose 'cost' - attributes are ensured to be of type Cost. - errors: A list of errors to be generated. - insufficient: A boolean, true if we could not find enough matches - to fulfill the reduction.

      • -
      -
      Source code in beancount/parser/booking_method.py -
      def booking_method_STRICT(entry, posting, matches):
      -    """Strict booking method.
      -
      -    Args:
      -      entry: The parent Transaction instance.
      -      posting: An instance of Posting, the reducing posting which we're
      -        attempting to match.
      -      matches: A list of matching Position instances from the ante-inventory.
      -        Those positions are known to already match the 'posting' spec.
      -    Returns:
      -      A triple of
      -        booked_reductions: A list of matched Posting instances, whose 'cost'
      -          attributes are ensured to be of type Cost.
      -        errors: A list of errors to be generated.
      -        insufficient: A boolean, true if we could not find enough matches
      -          to fulfill the reduction.
      -    """
      -    booked_reductions = []
      -    booked_matches = []
      -    errors = []
      -    insufficient = False
      -    # In strict mode, we require at most a single matching posting.
      -    if len(matches) > 1:
      -        # If the total requested to reduce matches the sum of all the
      -        # ambiguous postings, match against all of them.
      -        sum_matches = sum(p.units.number for p in matches)
      -        if sum_matches == -posting.units.number:
      -            booked_reductions.extend(
      -                posting._replace(units=-match.units, cost=match.cost)
      -                for match in matches)
      -        else:
      -            errors.append(
      -                AmbiguousMatchError(entry.meta,
      -                                    'Ambiguous matches for "{}": {}'.format(
      -                                        position.to_string(posting),
      -                                        ', '.join(position.to_string(match_posting)
      -                                                  for match_posting in matches)),
      -                                    entry))
      -    else:
      -        # Replace the posting's units and cost values.
      -        match = matches[0]
      -        sign = -1 if posting.units.number < ZERO else 1
      -        number = min(abs(match.units.number), abs(posting.units.number))
      -        match_units = Amount(number * sign, match.units.currency)
      -        booked_reductions.append(posting._replace(units=match_units, cost=match.cost))
      -        booked_matches.append(match)
      -        insufficient = (match_units.number != posting.units.number)
      -
      -    return booked_reductions, booked_matches, errors, insufficient
      -
      +
      def booking_method_STRICT(entry, posting, matches):
      +    """Strict booking method. This method fails if there are ambiguous matches."""
      +    booked_reductions = []
      +    booked_matches = []
      +    errors = []
      +    insufficient = False
      +
      +    # In strict mode, we require at most a single matching posting.
      +    if len(matches) > 1:
      +        # If the total requested to reduce matches the sum of all the
      +        # ambiguous postings, match against all of them.
      +        sum_matches = sum(p.units.number for p in matches)
      +        if sum_matches == -posting.units.number:
      +            booked_reductions.extend(
      +                posting._replace(units=-match.units, cost=match.cost) for match in matches
      +            )
      +        else:
      +            errors.append(
      +                AmbiguousMatchError(
      +                    entry.meta,
      +                    'Ambiguous matches for "{}": {}'.format(
      +                        position.to_string(posting),
      +                        ", ".join(
      +                            position.to_string(match_posting) for match_posting in matches
      +                        ),
      +                    ),
      +                    entry,
      +                )
      +            )
      +    else:
      +        # Replace the posting's units and cost values.
      +        match = matches[0]
      +        sign = -1 if posting.units.number < ZERO else 1
      +        number = min(abs(match.units.number), abs(posting.units.number))
      +        match_units = Amount(number * sign, match.units.currency)
      +        booked_reductions.append(posting._replace(units=match_units, cost=match.cost))
      +        booked_matches.append(match)
      +        insufficient = match_units.number != posting.units.number
      +
      +    return booked_reductions, booked_matches, errors, insufficient
      +
      +
      +
      + + + + + +
      + + + +

      +beancount.parser.booking_method.booking_method_STRICT_WITH_SIZE(entry, posting, matches) + + +

      + +
      + +

      Strict booking method, but disambiguate further with sizes.

      +

      This booking method applies the same algorithm as the STRICT method, but if +only one of the ambiguous lots matches the desired size, select that one +automatically.

      + +
      + Source code in beancount/parser/booking_method.py +
      def booking_method_STRICT_WITH_SIZE(entry, posting, matches):
      +    """Strict booking method, but disambiguate further with sizes.
      +
      +    This booking method applies the same algorithm as the STRICT method, but if
      +    only one of the ambiguous lots matches the desired size, select that one
      +    automatically.
      +    """
      +    (booked_reductions, booked_matches, errors, insufficient) = booking_method_STRICT(
      +        entry, posting, matches
      +    )
      +
      +    # If we couldn't match strictly, attempt to find a match with the same
      +    # number of units. If there is one or more of these, accept the oldest lot.
      +    if errors and len(matches) > 1:
      +        number = -posting.units.number
      +        matching_units = [match for match in matches if number == match.units.number]
      +        if matching_units:
      +            matching_units.sort(key=lambda match: match.cost.date)
      +
      +            # Replace the posting's units and cost values.
      +            match = matching_units[0]
      +            booked_reductions.append(posting._replace(units=-match.units, cost=match.cost))
      +            booked_matches.append(match)
      +            insufficient = False
      +            errors = []
      +
      +    return booked_reductions, booked_matches, errors, insufficient
      +
      @@ -3627,7 +3957,7 @@

      -beancount.parser.booking_method.handle_ambiguous_matches(entry, posting, matches, method) +beancount.parser.booking_method.handle_ambiguous_matches(entry, posting, matches, method)

      @@ -3668,10 +3998,12 @@

      Returns:
        -
      • A pair of +

      • A triple of booked_reductions – A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. - errors: A list of errors to be generated.

      • + errors: A list of errors to be generated. + insufficient: A boolean, true if we could not find enough matches to + cover the entire position.

      @@ -3679,42 +4011,49 @@

      Source code in beancount/parser/booking_method.py -
      def handle_ambiguous_matches(entry, posting, matches, method):
      -    """Handle ambiguous matches by dispatching to a particular method.
      -
      -    Args:
      -      entry: The parent Transaction instance.
      -      posting: An instance of Posting, the reducing posting which we're
      -        attempting to match.
      -      matches: A list of matching Position instances from the ante-inventory.
      -        Those positions are known to already match the 'posting' spec.
      -      methods: A mapping of account name to their corresponding booking
      -        method.
      -    Returns:
      -      A pair of
      -        booked_reductions: A list of matched Posting instances, whose 'cost'
      -          attributes are ensured to be of type Cost.
      -        errors: A list of errors to be generated.
      -    """
      -    assert isinstance(method, Booking), (
      -        "Invalid type: {}".format(method))
      -    assert matches, "Internal error: Invalid call with no matches"
      -
      -    #method = globals()['booking_method_{}'.format(method.name)]
      -    method = _BOOKING_METHODS[method]
      -    (booked_reductions,
      -     booked_matches, errors, insufficient) = method(entry, posting, matches)
      -    if insufficient:
      -        errors.append(
      -            AmbiguousMatchError(entry.meta,
      -                           'Not enough lots to reduce "{}": {}'.format(
      -                               position.to_string(posting),
      -                               ', '.join(position.to_string(match_posting)
      -                                         for match_posting in matches)),
      -                           entry))
      -
      -    return booked_reductions, booked_matches, errors
      -
      +
      def handle_ambiguous_matches(entry, posting, matches, method):
      +    """Handle ambiguous matches by dispatching to a particular method.
      +
      +    Args:
      +      entry: The parent Transaction instance.
      +      posting: An instance of Posting, the reducing posting which we're
      +        attempting to match.
      +      matches: A list of matching Position instances from the ante-inventory.
      +        Those positions are known to already match the 'posting' spec.
      +      methods: A mapping of account name to their corresponding booking
      +        method.
      +    Returns:
      +      A triple of
      +        booked_reductions: A list of matched Posting instances, whose 'cost'
      +          attributes are ensured to be of type Cost.
      +        errors: A list of errors to be generated.
      +        insufficient: A boolean, true if we could not find enough matches to
      +        cover the entire position.
      +    """
      +    assert isinstance(method, Booking), "Invalid type: {}".format(method)
      +    assert matches, "Internal error: Invalid call with no matches"
      +
      +    # method = globals()['booking_method_{}'.format(method.name)]
      +    method = _BOOKING_METHODS[method]
      +    (booked_reductions, booked_matches, errors, insufficient) = method(
      +        entry, posting, matches
      +    )
      +    if insufficient:
      +        errors.append(
      +            AmbiguousMatchError(
      +                entry.meta,
      +                'Not enough lots to reduce "{}": {}'.format(
      +                    position.to_string(posting),
      +                    ", ".join(
      +                        position.to_string(match_posting) for match_posting in matches
      +                    ),
      +                ),
      +                entry,
      +            )
      +        )
      +
      +    return booked_reductions, booked_matches, errors
      +

      @@ -3760,48 +4099,54 @@

      - -
      -

      +

      -beancount.parser.cmptest.TestError (Exception) +beancount.parser.cmptest.TestCase (TestCase) -

      +

      -

      Errors within the test implementation itself. These should never occur.

      -
      - +
      + + -
      -

      -beancount.parser.cmptest.assertEqualEntries(expected_entries, actual_entries, failfunc=<function fail at 0x78e8682b9120>, allow_incomplete=False) +
      + + + +

      +beancount.parser.cmptest.TestCase.assertEqualEntries(self, expected_entries, actual_entries, allow_incomplete=False) -

      +

      -

      Compare two lists of entries exactly and print missing entries verbosely if -they occur.

      +

      Check that two lists of entries are equal.

      +

      Entries can be provided either as a list of directives or as a +string. In the latter case, the string is parsed with +beancount.parser.parse_string() and the resulting directives +list is used. If allow_incomplete is True, light-weight +booking is performed before comparing the directive lists, +allowing to compare transactions with incomplete postings.

      @@ -3813,14 +4158,9 @@

      @@ -3844,44 +4184,48 @@

      Parameters:
        -
      • expected_entries – Either a list of directives or a string, in which case the -string is run through beancount.parser.parse_string() and the resulting -list is used.

      • -
      • actual_entries – Same treatment as expected_entries, the other list of -directives to compare to.

      • -
      • failfunc – A function to call on failure.

      • -
      • allow_incomplete – A boolean, true if we allow incomplete inputs and perform -light-weight booking.

      • +
      • expected_entries – Expected entries.

      • +
      • actual_entries – Actual entries.

      • +
      • allow_incomplete – Perform booking before comparison.

      Source code in beancount/parser/cmptest.py -
      def assertEqualEntries(expected_entries, actual_entries,
      -                       failfunc=DEFAULT_FAILFUNC, allow_incomplete=False):
      -    """Compare two lists of entries exactly and print missing entries verbosely if
      -    they occur.
      -
      -    Args:
      -      expected_entries: Either a list of directives or a string, in which case the
      -        string is run through beancount.parser.parse_string() and the resulting
      -        list is used.
      -      actual_entries: Same treatment as expected_entries, the other list of
      -        directives to compare to.
      -      failfunc: A function to call on failure.
      -      allow_incomplete: A boolean, true if we allow incomplete inputs and perform
      -        light-weight booking.
      -    Raises:
      -      AssertionError: If the exception fails.
      -    """
      -    expected_entries = read_string_or_entries(expected_entries, allow_incomplete)
      -    actual_entries = read_string_or_entries(actual_entries, allow_incomplete)
      -
      -    same, expected_missing, actual_missing = compare.compare_entries(expected_entries,
      -                                                                     actual_entries)
      -    if not same:
      -        assert expected_missing or actual_missing, "Missing is missing: {}, {}".format(
      -            expected_missing, actual_missing)
      -        oss = io.StringIO()
      -        if expected_missing:
      -            oss.write("Present in expected set and not in actual set:\n\n")
      -            for entry in expected_missing:
      -                oss.write(printer.format_entry(entry))
      -                oss.write('\n')
      -        if actual_missing:
      -            oss.write("Present in actual set and not in expected set:\n\n")
      -            for entry in actual_missing:
      -                oss.write(printer.format_entry(entry))
      -                oss.write('\n')
      -        failfunc(oss.getvalue())
      -
      +
      def assertEqualEntries(self, expected_entries, actual_entries, allow_incomplete=False):
      +    """Check that two lists of entries are equal.
      +
      +    Entries can be provided either as a list of directives or as a
      +    string.  In the latter case, the string is parsed with
      +    beancount.parser.parse_string() and the resulting directives
      +    list is used. If allow_incomplete is True, light-weight
      +    booking is performed before comparing the directive lists,
      +    allowing to compare transactions with incomplete postings.
      +
      +    Args:
      +      expected_entries: Expected entries.
      +      actual_entries: Actual entries.
      +      allow_incomplete: Perform booking before comparison.
      +
      +    Raises:
      +      AssertionError: If the exception fails.
      +
      +    """
      +    expected_entries = read_string_or_entries(expected_entries, allow_incomplete)
      +    actual_entries = read_string_or_entries(actual_entries, allow_incomplete)
      +
      +    same, expected_missing, actual_missing = compare.compare_entries(
      +        expected_entries, actual_entries
      +    )
      +    if not same:
      +        assert expected_missing or actual_missing, "Missing is missing: {}, {}".format(
      +            expected_missing, actual_missing
      +        )
      +        oss = io.StringIO()
      +        if expected_missing:
      +            oss.write("Present in expected set and not in actual set:\n\n")
      +            for entry in expected_missing:
      +                oss.write(printer.format_entry(entry))
      +                oss.write("\n")
      +        if actual_missing:
      +            oss.write("Present in actual set and not in expected set:\n\n")
      +            for entry in actual_missing:
      +                oss.write(printer.format_entry(entry))
      +                oss.write("\n")
      +        self.fail(oss.getvalue())
      +
      @@ -3889,19 +4233,25 @@

      -
      +
      -

      -beancount.parser.cmptest.assertExcludesEntries(subset_entries, entries, failfunc=<function fail at 0x78e8682b9120>, allow_incomplete=False) +

      +beancount.parser.cmptest.TestCase.assertExcludesEntries(self, subset_entries, entries, allow_incomplete=False) -

      +

      -

      Check that subset_entries is not included in entries and print extra entries.

      +

      Check that subset_entries is not included in entries.

      +

      Entries can be provided either as a list of directives or as a +string. In the latter case, the string is parsed with +beancount.parser.parse_string() and the resulting directives +list is used. If allow_incomplete is True, light-weight +booking is performed before comparing the directive lists, +allowing to compare transactions with incomplete postings.

      @@ -3913,14 +4263,9 @@

      @@ -3944,36 +4289,39 @@

      Parameters:
        -
      • subset_entries – Either a list of directives or a string, in which case the -string is run through beancount.parser.parse_string() and the resulting -list is used.

      • -
      • entries – Same treatment as subset_entries, the other list of -directives to compare to.

      • -
      • failfunc – A function to call on failure.

      • -
      • allow_incomplete – A boolean, true if we allow incomplete inputs and perform -light-weight booking.

      • +
      • subset_entries – Subset entries.

      • +
      • entries – Entries.

      • +
      • allow_incomplete – Perform booking before comparison.

      Source code in beancount/parser/cmptest.py -
      def assertExcludesEntries(subset_entries, entries,
      -                          failfunc=DEFAULT_FAILFUNC, allow_incomplete=False):
      -    """Check that subset_entries is not included in entries and print extra entries.
      -
      -    Args:
      -      subset_entries: Either a list of directives or a string, in which case the
      -        string is run through beancount.parser.parse_string() and the resulting
      -        list is used.
      -      entries: Same treatment as subset_entries, the other list of
      -        directives to compare to.
      -      failfunc: A function to call on failure.
      -      allow_incomplete: A boolean, true if we allow incomplete inputs and perform
      -        light-weight booking.
      -    Raises:
      -      AssertionError: If the exception fails.
      -    """
      -    subset_entries = read_string_or_entries(subset_entries, allow_incomplete)
      -    entries = read_string_or_entries(entries)
      -
      -    excludes, extra = compare.excludes_entries(subset_entries, entries)
      -    if not excludes:
      -        assert extra, "Extra is empty: {}".format(extra)
      -        oss = io.StringIO()
      -        if extra:
      -            oss.write("Extra from from first/excluded set:\n\n")
      -            for entry in extra:
      -                oss.write(printer.format_entry(entry))
      -                oss.write('\n')
      -        failfunc(oss.getvalue())
      -
      +
      def assertExcludesEntries(self, subset_entries, entries, allow_incomplete=False):
      +    """Check that subset_entries is not included in entries.
      +
      +    Entries can be provided either as a list of directives or as a
      +    string. In the latter case, the string is parsed with
      +    beancount.parser.parse_string() and the resulting directives
      +    list is used. If allow_incomplete is True, light-weight
      +    booking is performed before comparing the directive lists,
      +    allowing to compare transactions with incomplete postings.
      +
      +    Args:
      +      subset_entries: Subset entries.
      +      entries: Entries.
      +      allow_incomplete: Perform booking before comparison.
      +
      +    Raises:
      +      AssertionError: If the exception fails.
      +
      +    """
      +    subset_entries = read_string_or_entries(subset_entries, allow_incomplete)
      +    entries = read_string_or_entries(entries)
      +
      +    excludes, extra = compare.excludes_entries(subset_entries, entries)
      +    if not excludes:
      +        assert extra, "Extra is empty: {}".format(extra)
      +        oss = io.StringIO()
      +        if extra:
      +            oss.write("Extra from from first/excluded set:\n\n")
      +            for entry in extra:
      +                oss.write(printer.format_entry(entry))
      +                oss.write("\n")
      +        self.fail(oss.getvalue())
      +
      @@ -3981,19 +4329,25 @@

      -
      +
      -

      -beancount.parser.cmptest.assertIncludesEntries(subset_entries, entries, failfunc=<function fail at 0x78e8682b9120>, allow_incomplete=False) +

      +beancount.parser.cmptest.TestCase.assertIncludesEntries(self, subset_entries, entries, allow_incomplete=False) -

      +

      -

      Check that subset_entries is included in entries and print missing entries.

      +

      Check that subset_entries is included in entries.

      +

      Entries can be provided either as a list of directives or as a +string. In the latter case, the string is parsed with +beancount.parser.parse_string() and the resulting directives +list is used. If allow_incomplete is True, light-weight +booking is performed before comparing the directive lists, +allowing to compare transactions with incomplete postings.

      @@ -4005,14 +4359,9 @@

      @@ -4036,36 +4385,39 @@

      Parameters:
        -
      • subset_entries – Either a list of directives or a string, in which case the -string is run through beancount.parser.parse_string() and the resulting -list is used.

      • -
      • entries – Same treatment as subset_entries, the other list of -directives to compare to.

      • -
      • failfunc – A function to call on failure.

      • -
      • allow_incomplete – A boolean, true if we allow incomplete inputs and perform -light-weight booking.

      • +
      • subset_entries – Subset entries.

      • +
      • entries – Entries.

      • +
      • allow_incomplete – Perform booking before comparison.

      Source code in beancount/parser/cmptest.py -
      def assertIncludesEntries(subset_entries, entries,
      -                          failfunc=DEFAULT_FAILFUNC, allow_incomplete=False):
      -    """Check that subset_entries is included in entries and print missing entries.
      -
      -    Args:
      -      subset_entries: Either a list of directives or a string, in which case the
      -        string is run through beancount.parser.parse_string() and the resulting
      -        list is used.
      -      entries: Same treatment as subset_entries, the other list of
      -        directives to compare to.
      -      failfunc: A function to call on failure.
      -      allow_incomplete: A boolean, true if we allow incomplete inputs and perform
      -        light-weight booking.
      -    Raises:
      -      AssertionError: If the exception fails.
      -    """
      -    subset_entries = read_string_or_entries(subset_entries, allow_incomplete)
      -    entries = read_string_or_entries(entries)
      -
      -    includes, missing = compare.includes_entries(subset_entries, entries)
      -    if not includes:
      -        assert missing, "Missing is empty: {}".format(missing)
      -        oss = io.StringIO()
      -        if missing:
      -            oss.write("Missing from from expected set:\n\n")
      -            for entry in missing:
      -                oss.write(printer.format_entry(entry))
      -                oss.write('\n')
      -        failfunc(oss.getvalue())
      -
      +
      def assertIncludesEntries(self, subset_entries, entries, allow_incomplete=False):
      +    """Check that subset_entries is included in entries.
      +
      +    Entries can be provided either as a list of directives or as a
      +    string.  In the latter case, the string is parsed with
      +    beancount.parser.parse_string() and the resulting directives
      +    list is used. If allow_incomplete is True, light-weight
      +    booking is performed before comparing the directive lists,
      +    allowing to compare transactions with incomplete postings.
      +
      +    Args:
      +      subset_entries: Subset entries.
      +      entries: Entries.
      +      allow_incomplete: Perform booking before comparison.
      +
      +    Raises:
      +      AssertionError: If the exception fails.
      +
      +    """
      +    subset_entries = read_string_or_entries(subset_entries, allow_incomplete)
      +    entries = read_string_or_entries(entries)
      +
      +    includes, missing = compare.includes_entries(subset_entries, entries)
      +    if not includes:
      +        assert missing, "Missing is empty: {}".format(missing)
      +        oss = io.StringIO()
      +        if missing:
      +            oss.write("Missing from from expected set:\n\n")
      +            for entry in missing:
      +                oss.write(printer.format_entry(entry))
      +                oss.write("\n")
      +        self.fail(oss.getvalue())
      +
      @@ -4073,12 +4425,48 @@

      + + +

      + +
      + + + + + +
      + + + +

      + +beancount.parser.cmptest.TestError (Exception) + + + + +

      + +
      + +

      Errors within the test implementation itself. These should never occur.

      + + + +
      + +
      + + + +

      -beancount.parser.cmptest.read_string_or_entries(entries_or_str, allow_incomplete=False) +beancount.parser.cmptest.read_string_or_entries(entries_or_str, allow_incomplete=False)

      @@ -4123,107 +4511,75 @@

      Source code in beancount/parser/cmptest.py -
      def read_string_or_entries(entries_or_str, allow_incomplete=False):
      -    """Read a string of entries or just entries.
      -
      -    Args:
      -      entries_or_str: Either a list of directives, or a string containing directives.
      -      allow_incomplete: A boolean, true if we allow incomplete inputs and perform
      -        light-weight booking.
      -    Returns:
      -      A list of directives.
      -    """
      -    if isinstance(entries_or_str, str):
      -        entries, errors, options_map = parser.parse_string(
      -            textwrap.dedent(entries_or_str))
      -
      -        if allow_incomplete:
      -            # Do a simplistic local conversion in order to call the comparison.
      -            entries = [_local_booking(entry) for entry in entries]
      -        else:
      -            # Don't accept incomplete entries either.
      -            if any(parser.is_entry_incomplete(entry) for entry in entries):
      -                raise TestError("Entries in assertions may not use interpolation.")
      -
      -            entries, booking_errors = booking.book(entries, options_map)
      -            errors = errors + booking_errors
      -
      -        # Don't tolerate errors.
      -        if errors:
      -            oss = io.StringIO()
      -            printer.print_errors(errors, file=oss)
      -            raise TestError("Unexpected errors in expected: {}".format(oss.getvalue()))
      -
      -    else:
      -        assert isinstance(entries_or_str, list), "Expecting list: {}".format(entries_or_str)
      -        entries = entries_or_str
      -
      -    return entries
      -
      -
      -

      - - - +
      def read_string_or_entries(entries_or_str, allow_incomplete=False):
      +    """Read a string of entries or just entries.
       
      +    Args:
      +      entries_or_str: Either a list of directives, or a string containing directives.
      +      allow_incomplete: A boolean, true if we allow incomplete inputs and perform
      +        light-weight booking.
      +    Returns:
      +      A list of directives.
      +    """
      +    if isinstance(entries_or_str, str):
      +        entries, errors, options_map = parser.parse_string(textwrap.dedent(entries_or_str))
       
      +        if allow_incomplete:
      +            # Do a simplistic local conversion in order to call the comparison.
      +            entries = [_local_booking(entry) for entry in entries]
      +        else:
      +            # Don't accept incomplete entries either.
      +            if any(parser.is_entry_incomplete(entry) for entry in entries):
      +                raise TestError("Entries in assertions may not use interpolation.")
       
      +            entries, booking_errors = booking.book(entries, options_map)
      +            errors = errors + booking_errors
       
      +        # Don't tolerate errors.
      +        if errors:
      +            oss = io.StringIO()
      +            printer.print_errors(errors, file=oss)
      +            raise TestError("Unexpected errors in expected: {}".format(oss.getvalue()))
       
      -  
      + else: + assert isinstance(entries_or_str, list), "Expecting list: {}".format(entries_or_str) + entries = entries_or_str + return entries + + -
      - - - -

      - beancount.parser.grammar - - - -

      - -
      - -

      Builder for Beancount grammar.

      - - - -
      - - +
      +
      +
      -
      +
      -

      - -beancount.parser.grammar.Builder (LexBuilder) - +

      + beancount.parser.context -

      +

      -

      A builder used by the lexer and grammar parser as callbacks to create -the data objects corresponding to rules parsed from the input file.

      - +

      Produce a rendering of the account balances just before and after a +particular entry is applied.

      @@ -4238,19 +4594,19 @@

      -
      +
      -

      -beancount.parser.grammar.Builder.amount(self, number, currency) +

      +beancount.parser.context.render_entry_context(entries, options_map, entry, parsed_entry=None) -

      +

      -

      Process an amount grammar rule.

      +

      Render the context before and after a particular transaction is applied.

      @@ -4262,8 +4618,13 @@

      @@ -4279,28 +4640,162 @@

      Parameters:
        -
      • number – a Decimal instance, the number of the amount.

      • -
      • currency – a currency object (a str, really, see CURRENCY above)

      • +
      • entries – A list of directives.

      • +
      • options_map – A dict of options, as produced by the parser.

      • +
      • entry – The entry instance which should be rendered. (Note that this object is +expected to be in the set of entries, not just structurally equal.)

      • +
      • parsed_entry – An optional incomplete, parsed but not booked nor interpolated +entry. If this is provided, this is used for inspecting the list of prior +accounts and it is also rendered.

      Returns:
        -
      • An instance of Amount.

      • +
      • A multiline string of text, which consists of the context before the +transaction is applied, the transaction itself, and the context after it +is applied. You can just print that, it is in form that is intended to be +consumed by the user.

      - Source code in beancount/parser/grammar.py -
      def amount(self, number, currency):
      -    """Process an amount grammar rule.
      -
      -    Args:
      -      number: a Decimal instance, the number of the amount.
      -      currency: a currency object (a str, really, see CURRENCY above)
      -    Returns:
      -      An instance of Amount.
      -    """
      -    # Update the mapping that stores the parsed precisions.
      -    # Note: This is relatively slow, adds about 70ms because of number.as_tuple().
      -    self.dcupdate(number, currency)
      -    return Amount(number, currency)
      -
      + Source code in beancount/parser/context.py +
      def render_entry_context(entries, options_map, entry, parsed_entry=None):
      +    """Render the context before and after a particular transaction is applied.
      +
      +    Args:
      +      entries: A list of directives.
      +      options_map: A dict of options, as produced by the parser.
      +      entry: The entry instance which should be rendered. (Note that this object is
      +        expected to be in the set of entries, not just structurally equal.)
      +      parsed_entry: An optional incomplete, parsed but not booked nor interpolated
      +        entry. If this is provided, this is used for inspecting the list of prior
      +        accounts and it is also rendered.
      +    Returns:
      +      A multiline string of text, which consists of the context before the
      +      transaction is applied, the transaction itself, and the context after it
      +      is applied. You can just print that, it is in form that is intended to be
      +      consumed by the user.
      +    """
      +    oss = io.StringIO()
      +    pr = functools.partial(print, file=oss)
      +    header = "** {} --------------------------------"
      +
      +    meta = entry.meta
      +    pr(header.format("Transaction Id"))
      +    pr()
      +    pr("Hash:{}".format(compare.hash_entry(entry)))
      +    pr("Location: {}:{}".format(meta["filename"], meta["lineno"]))
      +    pr()
      +    pr()
      +
      +    # Get the list of accounts sorted by the order in which they appear in the
      +    # closest entry.
      +    order = {}
      +    if parsed_entry is None:
      +        parsed_entry = entry
      +    if isinstance(parsed_entry, data.Transaction):
      +        order = {
      +            posting.account: index for index, posting in enumerate(parsed_entry.postings)
      +        }
      +    accounts = sorted(
      +        getters.get_entry_accounts(parsed_entry),
      +        key=lambda account: order.get(account, 10000),
      +    )
      +
      +    # Accumulate the balances of these accounts up to the entry.
      +    balance_before, balance_after = interpolate.compute_entry_context(
      +        entries, entry, additional_accounts=accounts
      +    )
      +
      +    # Create a format line for printing the contents of account balances.
      +    max_account_width = max(map(len, accounts)) if accounts else 1
      +    position_line = "{{:1}} {{:{width}}}  {{:>49}}".format(width=max_account_width)
      +
      +    # Print the context before.
      +    pr(header.format("Balances before transaction"))
      +    pr()
      +    before_hashes = set()
      +    average_costs = {}
      +    for account in accounts:
      +        balance = balance_before[account]
      +
      +        pc_balances = balance.split()
      +        for currency, pc_balance in pc_balances.items():
      +            if len(pc_balance) > 1:
      +                average_costs[account] = pc_balance.average()
      +
      +        positions = balance.get_positions()
      +        for position in positions:
      +            before_hashes.add((account, hash(position)))
      +            pr(position_line.format("", account, str(position)))
      +        if not positions:
      +            pr(position_line.format("", account, ""))
      +        pr()
      +    pr()
      +
      +    # Print average cost per account, if relevant.
      +    if average_costs:
      +        pr(header.format("Average Costs"))
      +        pr()
      +        for account, average_cost in sorted(average_costs.items()):
      +            for position in average_cost:
      +                pr(position_line.format("", account, str(position)))
      +        pr()
      +        pr()
      +
      +    # Print the entry itself.
      +    dcontext = options_map["dcontext"]
      +    pr(header.format("Unbooked Transaction"))
      +    pr()
      +    if parsed_entry:
      +        printer.print_entry(parsed_entry, dcontext, render_weights=True, file=oss)
      +    pr()
      +
      +    pr(header.format("Transaction"))
      +    pr()
      +    printer.print_entry(entry, dcontext, render_weights=True, file=oss)
      +    pr()
      +
      +    if isinstance(entry, data.Transaction):
      +        pr(header.format("Residual and Tolerances"))
      +        pr()
      +
      +        # Print residuals.
      +        residual = interpolate.compute_residual(entry.postings)
      +        if not residual.is_empty():
      +            # Note: We render the residual at maximum precision, for debugging.
      +            pr("Residual: {}".format(residual))
      +
      +        # Dump the tolerances used.
      +        tolerances = interpolate.infer_tolerances(entry.postings, options_map)
      +        if tolerances:
      +            pr(
      +                "Tolerances: {}".format(
      +                    ", ".join(
      +                        "{}={}".format(key, value)
      +                        for key, value in sorted(tolerances.items())
      +                    )
      +                )
      +            )
      +
      +        # Compute the total cost basis.
      +        cost_basis = inventory.Inventory(
      +            pos for pos in entry.postings if pos.cost is not None
      +        ).reduce(convert.get_cost)
      +        if not cost_basis.is_empty():
      +            pr("Basis: {}".format(cost_basis))
      +        pr()
      +        pr()
      +
      +    # Print the context after.
      +    pr(header.format("Balances after transaction"))
      +    pr()
      +    for account in accounts:
      +        positions = balance_after[account].get_positions()
      +        for position in positions:
      +            changed = (account, hash(position)) not in before_hashes
      +            print(
      +                position_line.format("*" if changed else "", account, str(position)),
      +                file=oss,
      +            )
      +        if not positions:
      +            pr(position_line.format("", account, ""))
      +        pr()
      +
      +    return oss.getvalue()
      +
      @@ -4308,22 +4803,19 @@

      -
      +
      -

      -beancount.parser.grammar.Builder.balance(self, filename, lineno, date, account, amount, tolerance, kvlist) +

      +beancount.parser.context.render_file_context(entries, options_map, filename, lineno) -

      +

      -

      Process an assertion directive.

      -

      We produce no errors here by default. We replace the failing ones in the -routine that does the verification later one, that these have succeeded -or failed.

      +

      Render the context before and after a particular transaction is applied.

      @@ -4335,13 +4827,10 @@

      @@ -4357,36 +4846,60 @@

      Parameters:
        -
      • filename – The current filename.

      • -
      • lineno – The current line number.

      • -
      • date – A datetime object.

      • -
      • account – A string, the account to balance.

      • -
      • amount – The expected amount, to be checked.

      • -
      • tolerance – The tolerance number.

      • -
      • kvlist – a list of KeyValue instances.

      • +
      • entries – A list of directives.

      • +
      • options_map – A dict of options, as produced by the parser.

      • +
      • filename – A string, the name of the file from which the transaction was parsed.

      • +
      • lineno – An integer, the line number in the file the transaction was parsed from.

      Returns:
        -
      • A new Balance object.

      • +
      • A multiline string of text, which consists of the context before the +transaction is applied, the transaction itself, and the context after it +is applied. You can just print that, it is in form that is intended to be +consumed by the user.

      - Source code in beancount/parser/grammar.py -
      def balance(self, filename, lineno, date, account, amount, tolerance, kvlist):
      -    """Process an assertion directive.
      -
      -    We produce no errors here by default. We replace the failing ones in the
      -    routine that does the verification later one, that these have succeeded
      -    or failed.
      -
      -    Args:
      -      filename: The current filename.
      -      lineno: The current line number.
      -      date: A datetime object.
      -      account: A string, the account to balance.
      -      amount: The expected amount, to be checked.
      -      tolerance: The tolerance number.
      -      kvlist: a list of KeyValue instances.
      -    Returns:
      -      A new Balance object.
      -    """
      -    diff_amount = None
      -    meta = new_metadata(filename, lineno, kvlist)
      -    return Balance(meta, date, account, amount, tolerance, diff_amount)
      -
      + Source code in beancount/parser/context.py +
      def render_file_context(entries, options_map, filename, lineno):
      +    """Render the context before and after a particular transaction is applied.
      +
      +    Args:
      +      entries: A list of directives.
      +      options_map: A dict of options, as produced by the parser.
      +      filename: A string, the name of the file from which the transaction was parsed.
      +      lineno: An integer, the line number in the file the transaction was parsed from.
      +    Returns:
      +      A multiline string of text, which consists of the context before the
      +      transaction is applied, the transaction itself, and the context after it
      +      is applied. You can just print that, it is in form that is intended to be
      +      consumed by the user.
      +    """
      +    # Find the closest entry.
      +    closest_entry = data.find_closest(entries, filename, lineno)
      +    if closest_entry is None:
      +        raise SystemExit("No entry could be found before {}:{}".format(filename, lineno))
      +
      +    # Run just the parser stage (no booking nor interpolation, which would
      +    # remove the postings) on the input file to produced the corresponding
      +    # unbooked transaction, so that we can get the list of accounts.
      +    if path.exists(filename):
      +        parsed_entries, _, __ = parser.parse_file(filename)
      +
      +        # Note: We cannot bisect as we cannot rely on sorting behavior from the parser.
      +        lineno = closest_entry.meta["lineno"]
      +        closest_parsed_entries = [
      +            parsed_entry
      +            for parsed_entry in parsed_entries
      +            if parsed_entry.meta["lineno"] == lineno
      +        ]
      +        if len(closest_parsed_entries) != 1:
      +            # This is an internal error, this should never occur.
      +            raise RuntimeError(
      +                "Parsed entry corresponding to real entry not found in original filename."
      +            )
      +        closest_parsed_entry = next(iter(closest_parsed_entries))
      +    else:
      +        closest_parsed_entry = None
      +
      +    return render_entry_context(entries, options_map, closest_entry, closest_parsed_entry)
      +
      @@ -4394,101 +4907,102 @@

      -
      -

      -beancount.parser.grammar.Builder.build_grammar_error(self, filename, lineno, exc_value, exc_type=None, exc_traceback=None) +

      + +

      + + + + + +
      + + + +

      + beancount.parser.grammar -

      + +

      -

      Build a grammar error and appends it to the list of pending errors.

      +

      Builder for Beancount grammar.

      - - - - - - - - - - - -
      Parameters: -
        -
      • filename – The current filename

      • -
      • lineno – The current line number

      • -
      • excvalue – The exception value, or a str, the message of the error.

      • -
      • exc_type – An exception type, if an exception occurred.

      • -
      • exc_traceback – A traceback object.

      • -
      -
      -
      - Source code in beancount/parser/grammar.py -
      def build_grammar_error(self, filename, lineno, exc_value,
      -                        exc_type=None, exc_traceback=None):
      -    """Build a grammar error and appends it to the list of pending errors.
      -
      -    Args:
      -      filename: The current filename
      -      lineno: The current line number
      -      excvalue: The exception value, or a str, the message of the error.
      -      exc_type: An exception type, if an exception occurred.
      -      exc_traceback: A traceback object.
      -    """
      -    if exc_type is not None:
      -        assert not isinstance(exc_value, str)
      -        strings = traceback.format_exception_only(exc_type, exc_value)
      -        tblist = traceback.extract_tb(exc_traceback)
      -        filename, lineno, _, __ = tblist[0]
      -        message = '{} ({}:{})'.format(strings[0], filename, lineno)
      -    else:
      -        message = str(exc_value)
      -    meta = new_metadata(filename, lineno)
      -    self.errors.append(
      -        ParserSyntaxError(meta, message, None))
      -
      -
      -
      - +
      -
      -

      -beancount.parser.grammar.Builder.close(self, filename, lineno, date, account, kvlist) -

      -
      -

      Process a close directive.

      - - - - - - - - -
      Parameters: -
        -
      • filename – The current filename.

      • -
      • lineno – The current line number.

      • -
      • date – A datetime object.

      • -
      • account – A string, the name of the account.

      • -
      • kvlist – a list of KeyValue instances.

      • +
        + + + +

        + +beancount.parser.grammar.Builder (LexBuilder) + + + + +

        + +
        + +

        A builder used by the lexer and grammar parser as callbacks to create +the data objects corresponding to rules parsed from the input file.

        + + + + +
        + + + + + + + + + + +
        + + + +

        +beancount.parser.grammar.Builder.account(self, filename, lineno, account) + + +

        + +
        + +

        Check account name validity.

        + + + + + + + + + + @@ -4504,7 +5018,7 @@

        @@ -4512,21 +5026,23 @@

        Parameters: +
          +
        • account – a str, the account name.

        Returns:
          -
        • A new Close object.

        • +
        • A string, the account name.

        Source code in beancount/parser/grammar.py -
        def close(self, filename, lineno, date, account, kvlist):
        -    """Process a close directive.
        -
        -    Args:
        -      filename: The current filename.
        -      lineno: The current line number.
        -      date: A datetime object.
        -      account: A string, the name of the account.
        -      kvlist: a list of KeyValue instances.
        -    Returns:
        -      A new Close object.
        -    """
        -    meta = new_metadata(filename, lineno, kvlist)
        -    return Close(meta, date, account)
        -
        +
        def account(self, filename, lineno, account):
        +    """Check account name validity.
        +
        +    Args:
        +      account: a str, the account name.
        +    Returns:
        +      A string, the account name.
        +    """
        +    if not self.account_regexp.match(account):
        +        meta = new_metadata(filename, lineno)
        +        self.errors.append(
        +            ParserError(meta, "Invalid account name: {}".format(account), None)
        +        )
        +    # Intern account names. This should reduces memory usage a
        +    # fair bit because these strings are repeated liberally.
        +    return self.accounts.setdefault(account, account)
        +
        @@ -4538,15 +5054,15 @@

        -

        -beancount.parser.grammar.Builder.commodity(self, filename, lineno, date, currency, kvlist) +

        +beancount.parser.grammar.Builder.amount(self, filename, lineno, number, currency) -

        +
        -

        Process a close directive.

        +

        Process an amount grammar rule.

        @@ -4558,11 +5074,8 @@

        @@ -4578,7 +5091,7 @@

        @@ -4586,21 +5099,20 @@

        Parameters:
          -
        • filename – The current filename.

        • -
        • lineno – The current line number.

        • -
        • date – A datetime object.

        • -
        • currency – A string, the commodity being declared.

        • -
        • kvlist – a list of KeyValue instances.

        • +
        • number – a Decimal instance, the number of the amount.

        • +
        • currency – a currency object (a str, really, see CURRENCY above)

        Returns:
          -
        • A new Close object.

        • +
        • An instance of Amount.

        Source code in beancount/parser/grammar.py -
        def commodity(self, filename, lineno, date, currency, kvlist):
        -    """Process a close directive.
        -
        -    Args:
        -      filename: The current filename.
        -      lineno: The current line number.
        -      date: A datetime object.
        -      currency: A string, the commodity being declared.
        -      kvlist: a list of KeyValue instances.
        -    Returns:
        -      A new Close object.
        -    """
        -    meta = new_metadata(filename, lineno, kvlist)
        -    return Commodity(meta, date, currency)
        -
        +
        def amount(self, filename, lineno, number, currency):
        +    """Process an amount grammar rule.
        +
        +    Args:
        +      number: a Decimal instance, the number of the amount.
        +      currency: a currency object (a str, really, see CURRENCY above)
        +    Returns:
        +      An instance of Amount.
        +    """
        +    # Update the mapping that stores the parsed precisions.
        +    # Note: This is relatively slow, adds about 70ms because of number.as_tuple().
        +    self._dcupdate(number, currency)
        +    return Amount(number, currency)
        +
        @@ -4612,15 +5124,18 @@

        -

        -beancount.parser.grammar.Builder.compound_amount(self, number_per, number_total, currency) +

        +beancount.parser.grammar.Builder.balance(self, filename, lineno, date, account, amount, tolerance, kvlist) -

        +
        -

        Process an amount grammar rule.

        +

        Process an assertion directive.

        +

        We produce no errors here by default. We replace the failing ones in the +routine that does the verification later one, that these have succeeded +or failed.

        @@ -4632,9 +5147,13 @@

        Parameters:

        @@ -4650,8 +5169,7 @@

        Returns:

        @@ -4659,26 +5177,28 @@

        Source code in beancount/parser/grammar.py -
        def compound_amount(self, number_per, number_total, currency):
        -    """Process an amount grammar rule.
        -
        -    Args:
        -      number_per: a Decimal instance, the number of the cost per share.
        -      number_total: a Decimal instance, the number of the cost over all shares.
        -      currency: a currency object (a str, really, see CURRENCY above)
        -    Returns:
        -      A triple of (Decimal, Decimal, currency string) to be processed further when
        -      creating the final per-unit cost number.
        -    """
        -    # Update the mapping that stores the parsed precisions.
        -    # Note: This is relatively slow, adds about 70ms because of number.as_tuple().
        -    self.dcupdate(number_per, currency)
        -    self.dcupdate(number_total, currency)
        -
        -    # Note that we are not able to reduce the value to a number per-share
        -    # here because we only get the number of units in the full lot spec.
        -    return CompoundAmount(number_per, number_total, currency)
        -
        +
        def balance(self, filename, lineno, date, account, amount, tolerance, kvlist):
        +    """Process an assertion directive.
        +
        +    We produce no errors here by default. We replace the failing ones in the
        +    routine that does the verification later one, that these have succeeded
        +    or failed.
        +
        +    Args:
        +      filename: The current filename.
        +      lineno: The current line number.
        +      date: A datetime object.
        +      account: A string, the account to balance.
        +      amount: The expected amount, to be checked.
        +      tolerance: The tolerance number.
        +      kvlist: a list of KeyValue instances.
        +    Returns:
        +      A new Balance object.
        +    """
        +    diff_amount = None
        +    meta = new_metadata(filename, lineno, kvlist)
        +    return Balance(meta, date, account, amount, tolerance, diff_amount)
        +
        @@ -4690,22 +5210,61 @@

        -beancount.parser.grammar.Builder.cost_merge(self, _) +

        +beancount.parser.grammar.Builder.build_grammar_error(self, filename, lineno, exc_value, exc_type=None, exc_traceback=None) -

        +
        -

        Create a 'merge cost' token.

        +

        Build a grammar error and appends it to the list of pending errors.

        +
          -
        • number_per – a Decimal instance, the number of the cost per share.

        • -
        • number_total – a Decimal instance, the number of the cost over all shares.

        • -
        • currency – a currency object (a str, really, see CURRENCY above)

        • +
        • filename – The current filename.

        • +
        • lineno – The current line number.

        • +
        • date – A datetime object.

        • +
        • account – A string, the account to balance.

        • +
        • amount – The expected amount, to be checked.

        • +
        • tolerance – The tolerance number.

        • +
        • kvlist – a list of KeyValue instances.

          -
        • A triple of (Decimal, Decimal, currency string) to be processed further when -creating the final per-unit cost number.

        • +
        • A new Balance object.

        + + + + + + + + + + +
        Parameters: +
          +
        • filename – The current filename

        • +
        • lineno – The current line number

        • +
        • excvalue – The exception value, or a str, the message of the error.

        • +
        • exc_type – An exception type, if an exception occurred.

        • +
        • exc_traceback – A traceback object.

        • +
        +
        Source code in beancount/parser/grammar.py -
        def cost_merge(self, _):
        -    """Create a 'merge cost' token."""
        -    return MERGE_COST
        -
        +
        def build_grammar_error(
        +    self, filename, lineno, exc_value, exc_type=None, exc_traceback=None
        +):
        +    """Build a grammar error and appends it to the list of pending errors.
        +
        +    Args:
        +      filename: The current filename
        +      lineno: The current line number
        +      excvalue: The exception value, or a str, the message of the error.
        +      exc_type: An exception type, if an exception occurred.
        +      exc_traceback: A traceback object.
        +    """
        +    if exc_type is not None:
        +        assert not isinstance(exc_value, str)
        +        strings = traceback.format_exception_only(exc_type, exc_value)
        +        tblist = traceback.extract_tb(exc_traceback)
        +        filename, lineno, _, __ = tblist[0]
        +        message = "{} ({}:{})".format(strings[0], filename, lineno)
        +    else:
        +        message = str(exc_value)
        +    meta = new_metadata(filename, lineno)
        +    self.errors.append(ParserSyntaxError(meta, message, None))
        +
        @@ -4717,15 +5276,15 @@

        -

        -beancount.parser.grammar.Builder.cost_spec(self, cost_comp_list, is_total) +

        +beancount.parser.grammar.Builder.close(self, filename, lineno, date, account, kvlist) -

        +
        -

        Process a cost_spec grammar rule.

        +

        Process a close directive.

        @@ -4737,11 +5296,11 @@

        @@ -4757,8 +5316,7 @@

        @@ -4766,93 +5324,21 @@

        Parameters:
          -
        • cost_comp_list – A list of CompoundAmount, a datetime.date, or -label ID strings.

        • -
        • is_total – Assume only the total cost is specified; reject the <number> # <number> - syntax, that is, no compound amounts may be specified. This is used to support - the {{...}} syntax.

        • +
        • filename – The current filename.

        • +
        • lineno – The current line number.

        • +
        • date – A datetime object.

        • +
        • account – A string, the name of the account.

        • +
        • kvlist – a list of KeyValue instances.

        Returns:
          -
        • A cost-info tuple of CompoundAmount, lot date and label string. Any of these -may be set to a sentinel indicating "unset".

        • +
        • A new Close object.

        Source code in beancount/parser/grammar.py -
        def cost_spec(self, cost_comp_list, is_total):
        -    """Process a cost_spec grammar rule.
        -
        -    Args:
        -      cost_comp_list: A list of CompoundAmount, a datetime.date, or
        -        label ID strings.
        -      is_total: Assume only the total cost is specified; reject the <number> # <number>
        -          syntax, that is, no compound amounts may be specified. This is used to support
        -          the {{...}} syntax.
        -    Returns:
        -      A cost-info tuple of CompoundAmount, lot date and label string. Any of these
        -      may be set to a sentinel indicating "unset".
        -    """
        -    if not cost_comp_list:
        -        return CostSpec(MISSING, None, MISSING, None, None, False)
        -    assert isinstance(cost_comp_list, list), (
        -        "Internal error in parser: {}".format(cost_comp_list))
        -
        -    compound_cost = None
        -    date_ = None
        -    label = None
        -    merge = None
        -    for comp in cost_comp_list:
        -        if isinstance(comp, CompoundAmount):
        -            if compound_cost is None:
        -                compound_cost = comp
        -            else:
        -                self.errors.append(
        -                    ParserError(self.get_lexer_location(),
        -                                "Duplicate cost: '{}'.".format(comp), None))
        -
        -        elif isinstance(comp, date):
        -            if date_ is None:
        -                date_ = comp
        -            else:
        -                self.errors.append(
        -                    ParserError(self.get_lexer_location(),
        -                                "Duplicate date: '{}'.".format(comp), None))
        -
        -        elif comp is MERGE_COST:
        -            if merge is None:
        -                merge = True
        -                self.errors.append(
        -                    ParserError(self.get_lexer_location(),
        -                                "Cost merging is not supported yet", None))
        -            else:
        -                self.errors.append(
        -                    ParserError(self.get_lexer_location(),
        -                                "Duplicate merge-cost spec", None))
        -
        -        else:
        -            assert isinstance(comp, str), (
        -                "Currency component is not string: '{}'".format(comp))
        -            if label is None:
        -                label = comp
        -            else:
        -                self.errors.append(
        -                    ParserError(self.get_lexer_location(),
        -                                "Duplicate label: '{}'.".format(comp), None))
        -
        -    # If there was a cost_comp_list, thus a "{...}" cost basis spec, you must
        -    # indicate that by creating a CompoundAmount(), always.
        -
        -    if compound_cost is None:
        -        number_per, number_total, currency = MISSING, None, MISSING
        -    else:
        -        number_per, number_total, currency = compound_cost
        -        if is_total:
        -            if number_total is not None:
        -                self.errors.append(
        -                    ParserError(
        -                        self.get_lexer_location(),
        -                        ("Per-unit cost may not be specified using total cost "
        -                         "syntax: '{}'; ignoring per-unit cost").format(compound_cost),
        -                        None))
        -                # Ignore per-unit number.
        -                number_per = ZERO
        -            else:
        -                # There's a single number specified; interpret it as a total cost.
        -                number_total = number_per
        -                number_per = ZERO
        -
        -    if merge is None:
        -        merge = False
        -
        -    return CostSpec(number_per, number_total, currency, date_, label, merge)
        -
        +
        def close(self, filename, lineno, date, account, kvlist):
        +    """Process a close directive.
        +
        +    Args:
        +      filename: The current filename.
        +      lineno: The current line number.
        +      date: A datetime object.
        +      account: A string, the name of the account.
        +      kvlist: a list of KeyValue instances.
        +    Returns:
        +      A new Close object.
        +    """
        +    meta = new_metadata(filename, lineno, kvlist)
        +    return Close(meta, date, account)
        +
        @@ -4864,15 +5350,15 @@

        -

        -beancount.parser.grammar.Builder.custom(self, filename, lineno, date, dir_type, custom_values, kvlist) +

        +beancount.parser.grammar.Builder.commodity(self, filename, lineno, date, currency, kvlist) -

        +
        -

        Process a custom directive.

        +

        Process a close directive.

        @@ -4884,11 +5370,10 @@

        @@ -4905,7 +5390,7 @@

        @@ -4913,22 +5398,21 @@

        Parameters:
          -
        • filename – the current filename.

        • -
        • lineno – the current line number.

        • -
        • date – a datetime object.

        • -
        • dir_type – A string, a type for the custom directive being parsed.

        • -
        • custom_values – A list of the various tokens seen on the same line.

        • +
        • filename – The current filename.

        • +
        • lineno – The current line number.

        • +
        • date – A datetime object.

        • +
        • currency – A string, the commodity being declared.

        • kvlist – a list of KeyValue instances.

        Returns:
          -
        • A new Custom object.

        • +
        • A new Close object.

        Source code in beancount/parser/grammar.py -
        def custom(self, filename, lineno, date, dir_type, custom_values, kvlist):
        -    """Process a custom directive.
        -
        -    Args:
        -      filename: the current filename.
        -      lineno: the current line number.
        -      date: a datetime object.
        -      dir_type: A string, a type for the custom directive being parsed.
        -      custom_values: A list of the various tokens seen on the same line.
        -      kvlist: a list of KeyValue instances.
        -    Returns:
        -      A new Custom object.
        -    """
        -    meta = new_metadata(filename, lineno, kvlist)
        -    return Custom(meta, date, dir_type, custom_values)
        -
        +
        def commodity(self, filename, lineno, date, currency, kvlist):
        +    """Process a close directive.
        +
        +    Args:
        +      filename: The current filename.
        +      lineno: The current line number.
        +      date: A datetime object.
        +      currency: A string, the commodity being declared.
        +      kvlist: a list of KeyValue instances.
        +    Returns:
        +      A new Close object.
        +    """
        +    meta = new_metadata(filename, lineno, kvlist)
        +    return Commodity(meta, date, currency)
        +
        @@ -4940,15 +5424,15 @@

        -

        -beancount.parser.grammar.Builder.custom_value(self, value, dtype=None) +

        +beancount.parser.grammar.Builder.compound_amount(self, filename, lineno, number_per, number_total, currency) -

        +
        -

        Create a custom value object, along with its type.

        +

        Process an amount grammar rule.

        @@ -4960,7 +5444,9 @@

        @@ -4976,8 +5462,8 @@

        @@ -4985,19 +5471,26 @@

        Parameters:
          -
        • value – One of the accepted custom values.

        • +
        • number_per – a Decimal instance, the number of the cost per share.

        • +
        • number_total – a Decimal instance, the number of the cost over all shares.

        • +
        • currency – a currency object (a str, really, see CURRENCY above)

        Returns:
          -
        • A pair of (value, dtype) where 'dtype' is the datatype is that of the -value.

        • +
        • A triple of (Decimal, Decimal, currency string) to be processed further when +creating the final per-unit cost number.

        Source code in beancount/parser/grammar.py -
        def custom_value(self, value, dtype=None):
        -    """Create a custom value object, along with its type.
        -
        -    Args:
        -      value: One of the accepted custom values.
        -    Returns:
        -      A pair of (value, dtype) where 'dtype' is the datatype is that of the
        -      value.
        -    """
        -    if dtype is None:
        -        dtype = type(value)
        -    return ValueType(value, dtype)
        -
        +
        def compound_amount(self, filename, lineno, number_per, number_total, currency):
        +    """Process an amount grammar rule.
        +
        +    Args:
        +      number_per: a Decimal instance, the number of the cost per share.
        +      number_total: a Decimal instance, the number of the cost over all shares.
        +      currency: a currency object (a str, really, see CURRENCY above)
        +    Returns:
        +      A triple of (Decimal, Decimal, currency string) to be processed further when
        +      creating the final per-unit cost number.
        +    """
        +    # Update the mapping that stores the parsed precisions.
        +    # Note: This is relatively slow, adds about 70ms because of number.as_tuple().
        +    self._dcupdate(number_per, currency)
        +    self._dcupdate(number_total, currency)
        +
        +    # Note that we are not able to reduce the value to a number per-share
        +    # here because we only get the number of units in the full lot spec.
        +    return CompoundAmount(number_per, number_total, currency)
        +
        @@ -5009,23 +5502,22 @@

        -

        -beancount.parser.grammar.Builder.dcupdate(self, number, currency) +

        +beancount.parser.grammar.Builder.cost_merge(self, filename, lineno, _) -

        +
        -

        Update the display context.

        +

        Create a 'merge cost' token.

        Source code in beancount/parser/grammar.py -
        def dcupdate(self, number, currency):
        -    """Update the display context."""
        -    if isinstance(number, Decimal) and currency and currency is not MISSING:
        -        self._dcupdate(number, currency)
        -
        +
        def cost_merge(self, filename, lineno, _):
        +    """Create a 'merge cost' token."""
        +    return MERGE_COST
        +
        @@ -5037,15 +5529,15 @@

        -

        -beancount.parser.grammar.Builder.document(self, filename, lineno, date, account, document_filename, tags_links, kvlist) +

        +beancount.parser.grammar.Builder.cost_spec(self, filename, lineno, cost_comp_list, is_total) -

        +
        -

        Process a document directive.

        +

        Process a cost_spec grammar rule.

        @@ -5057,13 +5549,11 @@

        @@ -5079,7 +5569,8 @@

        @@ -5087,28 +5578,119 @@

        Parameters:
          -
        • filename – the current filename.

        • -
        • lineno – the current line number.

        • -
        • date – a datetime object.

        • -
        • account – an Account instance.

        • -
        • document_filename – a str, the name of the document file.

        • -
        • tags_links – The current TagsLinks accumulator.

        • -
        • kvlist – a list of KeyValue instances.

        • +
        • cost_comp_list – A list of CompoundAmount, a datetime.date, or +label ID strings.

        • +
        • is_total – Assume only the total cost is specified; reject the <number> # <number> + syntax, that is, no compound amounts may be specified. This is used to support + the {{...}} syntax.

        Returns:
          -
        • A new Document object.

        • +
        • A cost-info tuple of CompoundAmount, lot date and label string. Any of these +may be set to a sentinel indicating "unset".

        Source code in beancount/parser/grammar.py -
        def document(self, filename, lineno, date, account, document_filename, tags_links,
        -             kvlist):
        -    """Process a document directive.
        -
        -    Args:
        -      filename: the current filename.
        -      lineno: the current line number.
        -      date: a datetime object.
        -      account: an Account instance.
        -      document_filename: a str, the name of the document file.
        -      tags_links: The current TagsLinks accumulator.
        -      kvlist: a list of KeyValue instances.
        -    Returns:
        -      A new Document object.
        -    """
        -    meta = new_metadata(filename, lineno, kvlist)
        -    if not path.isabs(document_filename):
        -        document_filename = path.abspath(path.join(path.dirname(filename),
        -                                                   document_filename))
        -    tags, links = self.finalize_tags_links(tags_links.tags, tags_links.links)
        -    return Document(meta, date, account, document_filename, tags, links)
        -
        +
        def cost_spec(self, filename, lineno, cost_comp_list, is_total):
        +    """Process a cost_spec grammar rule.
        +
        +    Args:
        +      cost_comp_list: A list of CompoundAmount, a datetime.date, or
        +        label ID strings.
        +      is_total: Assume only the total cost is specified; reject the <number> # <number>
        +          syntax, that is, no compound amounts may be specified. This is used to support
        +          the {{...}} syntax.
        +    Returns:
        +      A cost-info tuple of CompoundAmount, lot date and label string. Any of these
        +      may be set to a sentinel indicating "unset".
        +    """
        +    if not cost_comp_list:
        +        return CostSpec(MISSING, None, MISSING, None, None, False)
        +    assert isinstance(cost_comp_list, list), "Internal error in parser: {}".format(
        +        cost_comp_list
        +    )
        +
        +    compound_cost = None
        +    date_ = None
        +    label = None
        +    merge = None
        +    for comp in cost_comp_list:
        +        if isinstance(comp, CompoundAmount):
        +            if compound_cost is None:
        +                compound_cost = comp
        +            else:
        +                self.errors.append(
        +                    ParserError(
        +                        new_metadata(filename, lineno),
        +                        "Duplicate cost: '{}'.".format(comp),
        +                        None,
        +                    )
        +                )
        +
        +        elif isinstance(comp, date):
        +            if date_ is None:
        +                date_ = comp
        +            else:
        +                self.errors.append(
        +                    ParserError(
        +                        new_metadata(filename, lineno),
        +                        "Duplicate date: '{}'.".format(comp),
        +                        None,
        +                    )
        +                )
        +
        +        elif comp is MERGE_COST:
        +            if merge is None:
        +                merge = True
        +                self.errors.append(
        +                    ParserError(
        +                        new_metadata(filename, lineno),
        +                        "Cost merging is not supported yet",
        +                        None,
        +                    )
        +                )
        +            else:
        +                self.errors.append(
        +                    ParserError(
        +                        new_metadata(filename, lineno),
        +                        "Duplicate merge-cost spec",
        +                        None,
        +                    )
        +                )
        +
        +        else:
        +            assert isinstance(
        +                comp, str
        +            ), "Currency component is not string: '{}'".format(comp)
        +            if label is None:
        +                label = comp
        +            else:
        +                self.errors.append(
        +                    ParserError(
        +                        new_metadata(filename, lineno),
        +                        "Duplicate label: '{}'.".format(comp),
        +                        None,
        +                    )
        +                )
        +
        +    # If there was a cost_comp_list, thus a "{...}" cost basis spec, you must
        +    # indicate that by creating a CompoundAmount(), always.
        +
        +    if compound_cost is None:
        +        number_per, number_total, currency = MISSING, None, MISSING
        +    else:
        +        number_per, number_total, currency = compound_cost
        +        if is_total:
        +            if number_total is not None:
        +                self.errors.append(
        +                    ParserError(
        +                        new_metadata(filename, lineno),
        +                        (
        +                            "Per-unit cost may not be specified using total cost "
        +                            "syntax: '{}'; ignoring per-unit cost"
        +                        ).format(compound_cost),
        +                        None,
        +                    )
        +                )
        +                # Ignore per-unit number.
        +                number_per = ZERO
        +            else:
        +                # There's a single number specified; interpret it as a total cost.
        +                number_total = number_per
        +                number_per = ZERO
        +
        +    if merge is None:
        +        merge = False
        +
        +    return CostSpec(number_per, number_total, currency, date_, label, merge)
        +
        @@ -5120,15 +5702,15 @@

        -

        -beancount.parser.grammar.Builder.event(self, filename, lineno, date, event_type, description, kvlist) +

        +beancount.parser.grammar.Builder.custom(self, filename, lineno, date, dir_type, custom_values, kvlist) -

        +
        -

        Process an event directive.

        +

        Process a custom directive.

        @@ -5143,8 +5725,8 @@

      • filename – the current filename.

      • lineno – the current line number.

      • date – a datetime object.

      • -
      • event_type – a str, the name of the event type.

      • -
      • description – a str, the event value, the contents.

      • +
      • dir_type – A string, a type for the custom directive being parsed.

      • +
      • custom_values – A list of the various tokens seen on the same line.

      • kvlist – a list of KeyValue instances.

      • @@ -5161,7 +5743,7 @@

        @@ -5169,22 +5751,22 @@

        Returns:
          -
        • A new Event object.

        • +
        • A new Custom object.

        Source code in beancount/parser/grammar.py -
        def event(self, filename, lineno, date, event_type, description, kvlist):
        -    """Process an event directive.
        -
        -    Args:
        -      filename: the current filename.
        -      lineno: the current line number.
        -      date: a datetime object.
        -      event_type: a str, the name of the event type.
        -      description: a str, the event value, the contents.
        -      kvlist: a list of KeyValue instances.
        -    Returns:
        -      A new Event object.
        -    """
        -    meta = new_metadata(filename, lineno, kvlist)
        -    return Event(meta, date, event_type, description)
        -
        +
        def custom(self, filename, lineno, date, dir_type, custom_values, kvlist):
        +    """Process a custom directive.
        +
        +    Args:
        +      filename: the current filename.
        +      lineno: the current line number.
        +      date: a datetime object.
        +      dir_type: A string, a type for the custom directive being parsed.
        +      custom_values: A list of the various tokens seen on the same line.
        +      kvlist: a list of KeyValue instances.
        +    Returns:
        +      A new Custom object.
        +    """
        +    meta = new_metadata(filename, lineno, kvlist)
        +    return Custom(meta, date, dir_type, custom_values)
        +
        @@ -5196,16 +5778,32 @@

        -

        -beancount.parser.grammar.Builder.finalize(self) +

        +beancount.parser.grammar.Builder.custom_value(self, filename, lineno, value, dtype=None) -

        +
        -

        Finalize the parser, check for final errors and return the triple.

        +

        Create a custom value object, along with its type.

        + + + + + + + + + + + +
        Parameters: +
          +
        • value – One of the accepted custom values.

        • +
        +
        @@ -5216,10 +5814,8 @@

        @@ -5227,35 +5823,19 @@

        Returns:
          -
        • A triple of - entries – A list of parsed directives, which may need completion. - errors: A list of errors, hopefully empty. - options_map: A dict of options.

        • +
        • A pair of (value, dtype) where 'dtype' is the datatype is that of the +value.

        Source code in beancount/parser/grammar.py -
        def finalize(self):
        -    """Finalize the parser, check for final errors and return the triple.
        -
        -    Returns:
        -      A triple of
        -        entries: A list of parsed directives, which may need completion.
        -        errors: A list of errors, hopefully empty.
        -        options_map: A dict of options.
        -    """
        -    # If the user left some tags unbalanced, issue an error.
        -    for tag in self.tags:
        -        meta = new_metadata(self.options['filename'], 0)
        -        self.errors.append(
        -            ParserError(meta, "Unbalanced pushed tag: '{}'".format(tag), None))
        -
        -    # If the user left some metadata unpopped, issue an error.
        -    for key, value_list in self.meta.items():
        -        meta = new_metadata(self.options['filename'], 0)
        -        self.errors.append(
        -            ParserError(meta, (
        -                "Unbalanced metadata key '{}'; leftover metadata '{}'").format(
        -                    key, ', '.join(value_list)), None))
        -
        -    # Weave the commas option in the DisplayContext itself, so it propagates
        -    # everywhere it is used automatically.
        -    self.dcontext.set_commas(self.options['render_commas'])
        -
        -    return (self.get_entries(), self.errors, self.get_options())
        -
        +
        def custom_value(self, filename, lineno, value, dtype=None):
        +    """Create a custom value object, along with its type.
        +
        +    Args:
        +      value: One of the accepted custom values.
        +    Returns:
        +      A pair of (value, dtype) where 'dtype' is the datatype is that of the
        +      value.
        +    """
        +    if dtype is None:
        +        dtype = type(value)
        +    return ValueType(value, dtype)
        +
        @@ -5267,15 +5847,15 @@

        -

        +beancount.parser.grammar.Builder.document(self, filename, lineno, date, account, document_filename, tags_links, kvlist) -

        +
        -

        Finally amend tags and links and return final objects to be inserted.

        +

        Process a document directive.

        @@ -5287,8 +5867,13 @@ @@ -5304,7 +5889,7 @@ @@ -5312,20 +5897,30 @@

        +beancount.parser.grammar.Builder.event(self, filename, lineno, date, event_type, description, kvlist) -

        +
        -

        Return the accumulated entries.

        +

        Process an event directive.

        +
          -
        • tags – A set of tag strings (warning: this gets mutated in-place).

        • -
        • links – A set of link strings.

        • +
        • filename – the current filename.

        • +
        • lineno – the current line number.

        • +
        • date – a datetime object.

        • +
        • account – an Account instance.

        • +
        • document_filename – a str, the name of the document file.

        • +
        • tags_links – The current TagsLinks accumulator.

        • +
        • kvlist – a list of KeyValue instances.

          -
        • A sanitized pair of (tags, links).

        • +
        • A new Document object.

        + + + + + + + + + + +
        Parameters: +
          +
        • filename – the current filename.

        • +
        • lineno – the current line number.

        • +
        • date – a datetime object.

        • +
        • event_type – a str, the name of the event type.

        • +
        • description – a str, the event value, the contents.

        • +
        • kvlist – a list of KeyValue instances.

        • +
        +
        @@ -5357,7 +5973,7 @@

        @@ -5365,14 +5981,22 @@

        Returns:
          -
        • A list of sorted directives.

        • +
        • A new Event object.

        Source code in beancount/parser/grammar.py -
        def get_entries(self):
        -    """Return the accumulated entries.
        +          
        def event(self, filename, lineno, date, event_type, description, kvlist):
        +    """Process an event directive.
         
        -    Returns:
        -      A list of sorted directives.
        -    """
        -    return sorted(self.entries, key=data.entry_sortkey)
        -
        + Args: + filename: the current filename. + lineno: the current line number. + date: a datetime object. + event_type: a str, the name of the event type. + description: a str, the event value, the contents. + kvlist: a list of KeyValue instances. + Returns: + A new Event object. + """ + meta = new_metadata(filename, lineno, kvlist) + return Event(meta, date, event_type, description) +
        @@ -5384,22 +6008,119 @@

        -

        -beancount.parser.grammar.Builder.get_invalid_account(self) +

        +beancount.parser.grammar.Builder.finalize(self) -

        +
        -

        See base class.

        +

        Finalize the parser, check for final errors and return the triple.

        + + + + + + + + + + + + +
        Returns: +
          +
        • A triple of + entries – A list of parsed directives, which may need completion. + errors: A list of errors, hopefully empty. + options_map: A dict of options.

        • +
        +
        +
        + Source code in beancount/parser/grammar.py +
        def finalize(self):
        +    """Finalize the parser, check for final errors and return the triple.
        +
        +    Returns:
        +      A triple of
        +        entries: A list of parsed directives, which may need completion.
        +        errors: A list of errors, hopefully empty.
        +        options_map: A dict of options.
        +    """
        +    # If the user left some tags unbalanced, issue an error.
        +    for tag in self.tags:
        +        meta = new_metadata(self.options["filename"], 0)
        +        self.errors.append(
        +            ParserError(meta, "Unbalanced pushed tag: '{}'".format(tag), None)
        +        )
        +
        +    # If the user left some metadata unpopped, issue an error.
        +    for key, value_list in self.meta.items():
        +        meta = new_metadata(self.options["filename"], 0)
        +        self.errors.append(
        +            ParserError(
        +                meta,
        +                ("Unbalanced metadata key '{}'; leftover metadata '{}'").format(
        +                    key, ", ".join(value_list)
        +                ),
        +                None,
        +            )
        +        )
        +
        +    # Weave the commas option in the DisplayContext itself, so it propagates
        +    # everywhere it is used automatically.
        +    self.dcontext.set_commas(self.options["render_commas"])
        +
        +    return (self.get_entries(), self.errors, self.get_options())
        +
        +
        +
        + +
        + + + +
        + + +

        +beancount.parser.grammar.Builder.get_entries(self) + + +

        + +
        + +

        Return the accumulated entries.

        + + + + + + + + + + + + +
        Returns: +
          +
        • A list of sorted directives.

        • +
        +
        Source code in beancount/parser/grammar.py -
        def get_invalid_account(self):
        -    """See base class."""
        -    return account.join(self.options['name_equity'], 'InvalidAccountName')
        -
        +
        def get_entries(self):
        +    """Return the accumulated entries.
        +
        +    Returns:
        +      A list of sorted directives.
        +    """
        +    return sorted(self.entries, key=data.entry_sortkey)
        +
        @@ -5412,7 +6133,7 @@

        -beancount.parser.grammar.Builder.get_long_string_maxlines(self) +beancount.parser.grammar.Builder.get_long_string_maxlines(self)

        @@ -5423,10 +6144,10 @@

        Source code in beancount/parser/grammar.py -
        def get_long_string_maxlines(self):
        -    """See base class."""
        -    return self.options['long_string_maxlines']
        -
        +
        def get_long_string_maxlines(self):
        +    """See base class."""
        +    return self.options["long_string_maxlines"]
        +

        @@ -5439,7 +6160,7 @@

        -beancount.parser.grammar.Builder.get_options(self) +beancount.parser.grammar.Builder.get_options(self)

        @@ -5466,29 +6187,17 @@

      Source code in beancount/parser/grammar.py -
      def get_options(self):
      -    """Return the final options map.
      -
      -    Returns:
      -      A dict of option names to options.
      -    """
      -    # Build and store the inferred DisplayContext instance.
      -    self.options['dcontext'] = self.dcontext
      -
      -    # Add the full list of seen commodities.
      -    #
      -    # IMPORTANT: This is currently where the list of all commodities seen
      -    # from the parser lives. The
      -    # beancount.core.getters.get_commodities_map() routine uses this to
      -    # automatically generate a full list of directives. An alternative would
      -    # be to implement a plugin that enforces the generate of these
      -    # post-parsing so that they are always guaranteed to live within the
      -    # flow of entries. This would allow us to keep all the data in that list
      -    # of entries and to avoid depending on the options to store that output.
      -    self.options['commodities'] = self.commodities
      -
      -    return self.options
      -
      +
      def get_options(self):
      +    """Return the final options map.
      +
      +    Returns:
      +      A dict of option names to options.
      +    """
      +    # Build and store the inferred DisplayContext instance.
      +    self.options["dcontext"] = self.dcontext
      +
      +    return self.options
      +
      @@ -5501,7 +6210,7 @@

      -beancount.parser.grammar.Builder.handle_list(self, object_list, new_object) +beancount.parser.grammar.Builder.handle_list(self, filename, lineno, object_list, new_object)

      @@ -5545,21 +6254,21 @@

      Source code in beancount/parser/grammar.py -
      def handle_list(self, object_list, new_object):
      -    """Handle a recursive list grammar rule, generically.
      -
      -    Args:
      -      object_list: the current list of objects.
      -      new_object: the new object to be added.
      -    Returns:
      -      The new, updated list of objects.
      -    """
      -    if object_list is None:
      -        object_list = []
      -    if new_object is not None:
      -        object_list.append(new_object)
      -    return object_list
      -
      +
      def handle_list(self, filename, lineno, object_list, new_object):
      +    """Handle a recursive list grammar rule, generically.
      +
      +    Args:
      +      object_list: the current list of objects.
      +      new_object: the new object to be added.
      +    Returns:
      +      The new, updated list of objects.
      +    """
      +    if object_list is None:
      +        object_list = []
      +    if new_object is not None:
      +        object_list.append(new_object)
      +    return object_list
      +

      @@ -5572,7 +6281,7 @@

      -beancount.parser.grammar.Builder.include(self, filename, lineno, include_filename) +beancount.parser.grammar.Builder.include(self, filename, lineno, include_filename)

      @@ -5601,16 +6310,16 @@

      Source code in beancount/parser/grammar.py -
      def include(self, filename, lineno, include_filename):
      -    """Process an include directive.
      -
      -    Args:
      -      filename: current filename.
      -      lineno: current line number.
      -      include_name: A string, the name of the file to include.
      -    """
      -    self.options['include'].append(include_filename)
      -
      +
      def include(self, filename, lineno, include_filename):
      +    """Process an include directive.
      +
      +    Args:
      +      filename: current filename.
      +      lineno: current line number.
      +      include_name: A string, the name of the file to include.
      +    """
      +    self.options["include"].append(include_filename)
      +

      @@ -5623,7 +6332,7 @@

      -beancount.parser.grammar.Builder.key_value(self, key, value) +beancount.parser.grammar.Builder.key_value(self, filename, lineno, key, value)

      @@ -5670,20 +6379,20 @@

      Source code in beancount/parser/grammar.py -
      def key_value(self, key, value):
      -    """Process a document directive.
      -
      -    Args:
      -      filename: The current filename.
      -      lineno: The current line number.
      -      date: A datetime object.
      -      account: A string, the account the document relates to.
      -      document_filename: A str, the name of the document file.
      -    Returns:
      -      A new KeyValue object.
      -    """
      -    return KeyValue(key, value)
      -
      +
      def key_value(self, filename, lineno, key, value):
      +    """Process a document directive.
      +
      +    Args:
      +      filename: The current filename.
      +      lineno: The current line number.
      +      date: A datetime object.
      +      account: A string, the account the document relates to.
      +      document_filename: A str, the name of the document file.
      +    Returns:
      +      A new KeyValue object.
      +    """
      +    return KeyValue(key, value)
      +
      @@ -5696,7 +6405,7 @@

      -beancount.parser.grammar.Builder.note(self, filename, lineno, date, account, comment, kvlist) +beancount.parser.grammar.Builder.note(self, filename, lineno, date, account, comment, tags_links, kvlist)

      @@ -5744,22 +6453,23 @@

      Source code in beancount/parser/grammar.py -
      def note(self, filename, lineno, date, account, comment, kvlist):
      -    """Process a note directive.
      -
      -    Args:
      -      filename: The current filename.
      -      lineno: The current line number.
      -      date: A datetime object.
      -      account: A string, the account to attach the note to.
      -      comment: A str, the note's comments contents.
      -      kvlist: a list of KeyValue instances.
      -    Returns:
      -      A new Note object.
      -    """
      -    meta = new_metadata(filename, lineno, kvlist)
      -    return Note(meta, date, account, comment)
      -
      +
      def note(self, filename, lineno, date, account, comment, tags_links, kvlist):
      +    """Process a note directive.
      +
      +    Args:
      +      filename: The current filename.
      +      lineno: The current line number.
      +      date: A datetime object.
      +      account: A string, the account to attach the note to.
      +      comment: A str, the note's comments contents.
      +      kvlist: a list of KeyValue instances.
      +    Returns:
      +      A new Note object.
      +    """
      +    meta = new_metadata(filename, lineno, kvlist)
      +    tags, links = self._finalize_tags_links(tags_links.tags, tags_links.links)
      +    return Note(meta, date, account, comment, tags, links)
      +
      @@ -5772,7 +6482,7 @@

      -beancount.parser.grammar.Builder.open(self, filename, lineno, date, account, currencies, booking_str, kvlist) +beancount.parser.grammar.Builder.open(self, filename, lineno, date, account, currencies, booking_str, kvlist)

      @@ -5821,41 +6531,41 @@

      Source code in beancount/parser/grammar.py -
      def open(self, filename, lineno, date, account, currencies, booking_str, kvlist):
      -    """Process an open directive.
      -
      -    Args:
      -      filename: The current filename.
      -      lineno: The current line number.
      -      date: A datetime object.
      -      account: A string, the name of the account.
      -      currencies: A list of constraint currencies.
      -      booking_str: A string, the booking method, or None if none was specified.
      -      kvlist: a list of KeyValue instances.
      -    Returns:
      -      A new Open object.
      -    """
      -    meta = new_metadata(filename, lineno, kvlist)
      -    error = False
      -    if booking_str:
      -        try:
      -            # Note: Somehow the 'in' membership operator is not defined on Enum.
      -            booking = Booking[booking_str]
      -        except KeyError:
      -            # If the per-account method is invalid, set it to the global
      -            # default method and continue.
      -            booking = self.options['booking_method']
      -            error = True
      -    else:
      -        booking = None
      -
      -    entry = Open(meta, date, account, currencies, booking)
      -    if error:
      -        self.errors.append(ParserError(meta,
      -                                       "Invalid booking method: {}".format(booking_str),
      -                                       entry))
      -    return entry
      -
      +
      def open(self, filename, lineno, date, account, currencies, booking_str, kvlist):
      +    """Process an open directive.
      +
      +    Args:
      +      filename: The current filename.
      +      lineno: The current line number.
      +      date: A datetime object.
      +      account: A string, the name of the account.
      +      currencies: A list of constraint currencies.
      +      booking_str: A string, the booking method, or None if none was specified.
      +      kvlist: a list of KeyValue instances.
      +    Returns:
      +      A new Open object.
      +    """
      +    meta = new_metadata(filename, lineno, kvlist)
      +    error = False
      +    if booking_str:
      +        try:
      +            # Note: Somehow the 'in' membership operator is not defined on Enum.
      +            booking = Booking[booking_str]
      +        except KeyError:
      +            # If the per-account method is invalid, set it to the global
      +            # default method and continue.
      +            booking = self.options["booking_method"]
      +            error = True
      +    else:
      +        booking = None
      +
      +    entry = Open(meta, date, account, currencies, booking)
      +    if error:
      +        self.errors.append(
      +            ParserError(meta, "Invalid booking method: {}".format(booking_str), entry)
      +        )
      +    return entry
      +
      @@ -5868,7 +6578,7 @@

      -beancount.parser.grammar.Builder.option(self, filename, lineno, key, value) +beancount.parser.grammar.Builder.option(self, filename, lineno, key, value)

      @@ -5898,86 +6608,86 @@

      Source code in beancount/parser/grammar.py -
      def option(self, filename, lineno, key, value):
      -    """Process an option directive.
      -
      -    Args:
      -      filename: current filename.
      -      lineno: current line number.
      -      key: option's key (str)
      -      value: option's value
      -    """
      -    if key not in self.options:
      -        meta = new_metadata(filename, lineno)
      -        self.errors.append(
      -            ParserError(meta, "Invalid option: '{}'".format(key), None))
      -
      -    elif key in options.READ_ONLY_OPTIONS:
      -        meta = new_metadata(filename, lineno)
      -        self.errors.append(
      -            ParserError(meta, "Option '{}' may not be set".format(key), None))
      -
      -    else:
      -        option_descriptor = options.OPTIONS[key]
      -
      -        # Issue a warning if the option is deprecated.
      -        if option_descriptor.deprecated:
      -            assert isinstance(option_descriptor.deprecated, str), "Internal error."
      -            meta = new_metadata(filename, lineno)
      -            self.errors.append(
      -                DeprecatedError(meta, option_descriptor.deprecated, None))
      -
      -        # Rename the option if it has an alias.
      -        if option_descriptor.alias:
      -            key = option_descriptor.alias
      -            option_descriptor = options.OPTIONS[key]
      -
      -        # Convert the value, if necessary.
      -        if option_descriptor.converter:
      -            try:
      -                value = option_descriptor.converter(value)
      -            except ValueError as exc:
      -                meta = new_metadata(filename, lineno)
      -                self.errors.append(
      -                    ParserError(meta,
      -                                "Error for option '{}': {}".format(key, exc),
      -                                None))
      -                return
      -
      -        option = self.options[key]
      -        if isinstance(option, list):
      -            # Append to a list of values.
      -            option.append(value)
      -
      -        elif isinstance(option, dict):
      -            # Set to a dict of values.
      -            if not (isinstance(value, tuple) and len(value) == 2):
      -                self.errors.append(
      -                    ParserError(
      -                        meta, "Error for option '{}': {}".format(key, value), None))
      -                return
      -            dict_key, dict_value = value
      -            option[dict_key] = dict_value
      -
      -        elif isinstance(option, bool):
      -            # Convert to a boolean.
      -            if not isinstance(value, bool):
      -                value = (value.lower() in {'true', 'on'}) or (value == '1')
      -            self.options[key] = value
      -
      -        else:
      -            # Set the value.
      -            self.options[key] = value
      -
      -        # Refresh the list of valid account regexps as we go along.
      -        if key.startswith('name_'):
      -            # Update the set of valid account types.
      -            self.account_regexp = valid_account_regexp(self.options)
      -        elif key == 'insert_pythonpath':
      -            # Insert the PYTHONPATH to this file when and only if you
      -            # encounter this option.
      -            sys.path.insert(0, path.dirname(filename))
      -
      +
      def option(self, filename, lineno, key, value):
      +    """Process an option directive.
      +
      +    Args:
      +      filename: current filename.
      +      lineno: current line number.
      +      key: option's key (str)
      +      value: option's value
      +    """
      +    if key not in self.options:
      +        meta = new_metadata(filename, lineno)
      +        self.errors.append(ParserError(meta, "Invalid option: '{}'".format(key), None))
      +
      +    elif key in options.READ_ONLY_OPTIONS:
      +        meta = new_metadata(filename, lineno)
      +        self.errors.append(
      +            ParserError(meta, "Option '{}' may not be set".format(key), None)
      +        )
      +
      +    else:
      +        option_descriptor = options.OPTIONS[key]
      +
      +        # Issue a warning if the option is deprecated.
      +        if option_descriptor.deprecated:
      +            assert isinstance(option_descriptor.deprecated, str), "Internal error."
      +            meta = new_metadata(filename, lineno)
      +            self.errors.append(
      +                DeprecatedError(meta, option_descriptor.deprecated, None)
      +            )
      +
      +        # Rename the option if it has an alias.
      +        if option_descriptor.alias:
      +            key = option_descriptor.alias
      +            option_descriptor = options.OPTIONS[key]
      +
      +        # Convert the value, if necessary.
      +        if option_descriptor.converter:
      +            try:
      +                value = option_descriptor.converter(value)
      +            except ValueError as exc:
      +                meta = new_metadata(filename, lineno)
      +                self.errors.append(
      +                    ParserError(
      +                        meta, "Error for option '{}': {}".format(key, exc), None
      +                    )
      +                )
      +                return
      +
      +        option = self.options[key]
      +        if isinstance(option, list):
      +            # Append to a list of values.
      +            option.append(value)
      +
      +        elif isinstance(option, dict):
      +            # Set to a dict of values.
      +            if not (isinstance(value, tuple) and len(value) == 2):
      +                self.errors.append(
      +                    ParserError(
      +                        meta, "Error for option '{}': {}".format(key, value), None
      +                    )
      +                )
      +                return
      +            dict_key, dict_value = value
      +            option[dict_key] = dict_value
      +
      +        elif isinstance(option, bool):
      +            # Convert to a boolean.
      +            if not isinstance(value, bool):
      +                value = (value.lower() in {"true", "on"}) or (value == "1")
      +            self.options[key] = value
      +
      +        else:
      +            # Set the value.
      +            self.options[key] = value
      +
      +        # Refresh the list of valid account regexps as we go along.
      +        if key.startswith("name_"):
      +            # Update the set of valid account types.
      +            self.account_regexp = valid_account_regexp(self.options)
      +
      @@ -5990,7 +6700,7 @@

      -beancount.parser.grammar.Builder.pad(self, filename, lineno, date, account, source_account, kvlist) +beancount.parser.grammar.Builder.pad(self, filename, lineno, date, account, source_account, kvlist)

      @@ -6038,22 +6748,22 @@

      Source code in beancount/parser/grammar.py -
      def pad(self, filename, lineno, date, account, source_account, kvlist):
      -    """Process a pad directive.
      -
      -    Args:
      -      filename: The current filename.
      -      lineno: The current line number.
      -      date: A datetime object.
      -      account: A string, the account to be padded.
      -      source_account: A string, the account to pad from.
      -      kvlist: a list of KeyValue instances.
      -    Returns:
      -      A new Pad object.
      -    """
      -    meta = new_metadata(filename, lineno, kvlist)
      -    return Pad(meta, date, account, source_account)
      -
      +
      def pad(self, filename, lineno, date, account, source_account, kvlist):
      +    """Process a pad directive.
      +
      +    Args:
      +      filename: The current filename.
      +      lineno: The current line number.
      +      date: A datetime object.
      +      account: A string, the account to be padded.
      +      source_account: A string, the account to pad from.
      +      kvlist: a list of KeyValue instances.
      +    Returns:
      +      A new Pad object.
      +    """
      +    meta = new_metadata(filename, lineno, kvlist)
      +    return Pad(meta, date, account, source_account)
      +
      @@ -6066,7 +6776,7 @@

      -beancount.parser.grammar.Builder.pipe_deprecated_error(self, filename, lineno) +beancount.parser.grammar.Builder.pipe_deprecated_error(self, filename, lineno)

      @@ -6094,19 +6804,18 @@

      Source code in beancount/parser/grammar.py -
      def pipe_deprecated_error(self, filename, lineno):
      -    """Issue a 'Pipe deprecated' error.
      -
      -    Args:
      -      filename: The current filename
      -      lineno: The current line number
      -    """
      -    if self.options['allow_pipe_separator']:
      -        return
      -    meta = new_metadata(filename, lineno)
      -    self.errors.append(
      -        ParserSyntaxError(meta, "Pipe symbol is deprecated.", None))
      -
      +
      def pipe_deprecated_error(self, filename, lineno):
      +    """Issue a 'Pipe deprecated' error.
      +
      +    Args:
      +      filename: The current filename
      +      lineno: The current line number
      +    """
      +    if self.options["allow_pipe_separator"]:
      +        return
      +    meta = new_metadata(filename, lineno)
      +    self.errors.append(ParserSyntaxError(meta, "Pipe symbol is deprecated.", None))
      +
      @@ -6119,7 +6828,7 @@

      -beancount.parser.grammar.Builder.plugin(self, filename, lineno, plugin_name, plugin_config) +beancount.parser.grammar.Builder.plugin(self, filename, lineno, plugin_name, plugin_config)

      @@ -6150,18 +6859,18 @@

      Source code in beancount/parser/grammar.py -
      def plugin(self, filename, lineno, plugin_name, plugin_config):
      -    """Process a plugin directive.
      -
      -    Args:
      -      filename: current filename.
      -      lineno: current line number.
      -      plugin_name: A string, the name of the plugin module to import.
      -      plugin_config: A string or None, an optional configuration string to
      -        pass in to the plugin module.
      -    """
      -    self.options['plugin'].append((plugin_name, plugin_config))
      -
      +
      def plugin(self, filename, lineno, plugin_name, plugin_config):
      +    """Process a plugin directive.
      +
      +    Args:
      +      filename: current filename.
      +      lineno: current line number.
      +      plugin_name: A string, the name of the plugin module to import.
      +      plugin_config: A string or None, an optional configuration string to
      +        pass in to the plugin module.
      +    """
      +    self.options["plugin"].append((plugin_name, plugin_config))
      +
      @@ -6174,7 +6883,7 @@

      -beancount.parser.grammar.Builder.popmeta(self, key) +beancount.parser.grammar.Builder.popmeta(self, filename, lineno, key)

      @@ -6201,26 +6910,27 @@

      Source code in beancount/parser/grammar.py -
      def popmeta(self, key):
      -    """Removed a key off the current set of stacks.
      -
      -    Args:
      -      key: A string, a key to be removed from the meta dict.
      -    """
      -    try:
      -        if key not in self.meta:
      -            raise IndexError
      -        value_list = self.meta[key]
      -        value_list.pop(-1)
      -        if not value_list:
      -            self.meta.pop(key)
      -    except IndexError:
      -        meta = new_metadata(self.options['filename'], 0)
      -        self.errors.append(
      -            ParserError(meta,
      -                        "Attempting to pop absent metadata key: '{}'".format(key),
      -                        None))
      -
      +
      def popmeta(self, filename, lineno, key):
      +    """Removed a key off the current set of stacks.
      +
      +    Args:
      +      key: A string, a key to be removed from the meta dict.
      +    """
      +    try:
      +        if key not in self.meta:
      +            raise IndexError
      +        value_list = self.meta[key]
      +        value_list.pop(-1)
      +        if not value_list:
      +            self.meta.pop(key)
      +    except IndexError:
      +        meta = new_metadata(filename, lineno)
      +        self.errors.append(
      +            ParserError(
      +                meta, "Attempting to pop absent metadata key: '{}'".format(key), None
      +            )
      +        )
      +
      @@ -6233,7 +6943,7 @@

      -beancount.parser.grammar.Builder.poptag(self, tag) +beancount.parser.grammar.Builder.poptag(self, filename, lineno, tag)

      @@ -6260,19 +6970,20 @@

      Source code in beancount/parser/grammar.py -
      def poptag(self, tag):
      -    """Pop a tag off the current set of stacks.
      -
      -    Args:
      -      tag: A string, a tag to be removed from the current set of tags.
      -    """
      -    try:
      -        self.tags.remove(tag)
      -    except ValueError:
      -        meta = new_metadata(self.options['filename'], 0)
      -        self.errors.append(
      -            ParserError(meta, "Attempting to pop absent tag: '{}'".format(tag), None))
      -
      +
      def poptag(self, filename, lineno, tag):
      +    """Pop a tag off the current set of stacks.
      +
      +    Args:
      +      tag: A string, a tag to be removed from the current set of tags.
      +    """
      +    try:
      +        self.tags.remove(tag)
      +    except ValueError:
      +        meta = new_metadata(filename, lineno)
      +        self.errors.append(
      +            ParserError(meta, "Attempting to pop absent tag: '{}'".format(tag), None)
      +        )
      +
      @@ -6285,7 +6996,7 @@

      -beancount.parser.grammar.Builder.posting(self, filename, lineno, account, units, cost, price, istotal, flag) +beancount.parser.grammar.Builder.posting(self, filename, lineno, account, units, cost, price, istotal, flag)

      @@ -6336,67 +7047,91 @@

      Source code in beancount/parser/grammar.py -
      def posting(self, filename, lineno, account, units, cost, price, istotal, flag):
      -    """Process a posting grammar rule.
      -
      -    Args:
      -      filename: the current filename.
      -      lineno: the current line number.
      -      account: A string, the account of the posting.
      -      units: An instance of Amount for the units.
      -      cost: An instance of CostSpec for the cost.
      -      price: Either None, or an instance of Amount that is the cost of the position.
      -      istotal: A bool, True if the price is for the total amount being parsed, or
      -               False if the price is for each lot of the position.
      -      flag: A string, one-character, the flag associated with this posting.
      -    Returns:
      -      A new Posting object, with no parent entry.
      -    """
      -    meta = new_metadata(filename, lineno)
      -
      -    # Prices may not be negative.
      -    if price and isinstance(price.number, Decimal) and price.number < ZERO:
      -        self.errors.append(
      -            ParserError(meta, (
      -                "Negative prices are not allowed: {} "
      -                "(see http://furius.ca/beancount/doc/bug-negative-prices "
      -                "for workaround)"
      -            ).format(price), None))
      -        # Fix it and continue.
      -        price = Amount(abs(price.number), price.currency)
      -
      -    # If the price is specified for the entire amount, compute the effective
      -    # price here and forget about that detail of the input syntax.
      -    if istotal:
      -        if units.number == ZERO:
      -            number = ZERO
      -        else:
      -            number = price.number
      -            if number is not MISSING:
      -                number = number/abs(units.number)
      -        price = Amount(number, price.currency)
      -
      -    # Note: Allow zero prices because we need them for round-trips for
      -    # conversion entries.
      -    #
      -    # if price is not None and price.number == ZERO:
      -    #     self.errors.append(
      -    #         ParserError(meta, "Price is zero: {}".format(price), None))
      -
      -    # If both cost and price are specified, the currencies must match, or
      -    # that is an error.
      -    if (cost is not None and
      -        price is not None and
      -        isinstance(cost.currency, str) and
      -        isinstance(price.currency, str) and
      -        cost.currency != price.currency):
      -        self.errors.append(
      -            ParserError(meta,
      -                        "Cost and price currencies must match: {} != {}".format(
      -                            cost.currency, price.currency), None))
      -
      -    return Posting(account, units, cost, price, chr(flag) if flag else None, meta)
      -
      +
      def posting(self, filename, lineno, account, units, cost, price, istotal, flag):
      +    """Process a posting grammar rule.
      +
      +    Args:
      +      filename: the current filename.
      +      lineno: the current line number.
      +      account: A string, the account of the posting.
      +      units: An instance of Amount for the units.
      +      cost: An instance of CostSpec for the cost.
      +      price: Either None, or an instance of Amount that is the cost of the position.
      +      istotal: A bool, True if the price is for the total amount being parsed, or
      +               False if the price is for each lot of the position.
      +      flag: A string, one-character, the flag associated with this posting.
      +    Returns:
      +      A new Posting object, with no parent entry.
      +    """
      +    meta = new_metadata(filename, lineno)
      +
      +    # Prices may not be negative.
      +    if price and isinstance(price.number, Decimal) and price.number < ZERO:
      +        self.errors.append(
      +            ParserError(
      +                meta,
      +                (
      +                    "Negative prices are not allowed: {} "
      +                    "(see http://furius.ca/beancount/doc/bug-negative-prices "
      +                    "for workaround)"
      +                ).format(price),
      +                None,
      +            )
      +        )
      +        # Fix it and continue.
      +        price = Amount(abs(price.number), price.currency)
      +
      +    # If the price is specified for the entire amount, compute the effective
      +    # price here and forget about that detail of the input syntax.
      +    if istotal:
      +        if units.number is MISSING:
      +            # Note: we could potentially do a better job and attempt to fix
      +            # this up after interpolation, but this syntax is pretty rare
      +            # anyway.
      +            self.errors.append(
      +                ParserError(
      +                    meta,
      +                    ("Total price on a posting without units: {}.").format(price),
      +                    None,
      +                )
      +            )
      +            price = None
      +        else:
      +            price_number = price.number
      +            if price_number is not MISSING:
      +                price_number = (
      +                    ZERO if units.number == ZERO else price_number / abs(units.number)
      +                )
      +                price = Amount(price_number, price.currency)
      +
      +    # Note: Allow zero prices because we need them for round-trips for
      +    # conversion entries.
      +    #
      +    # if price is not None and price.number == ZERO:
      +    #     self.errors.append(
      +    #         ParserError(meta, "Price is zero: {}".format(price), None))
      +
      +    # If both cost and price are specified, the currencies must match, or
      +    # that is an error.
      +    if (
      +        cost is not None
      +        and price is not None
      +        and isinstance(cost.currency, str)
      +        and isinstance(price.currency, str)
      +        and cost.currency != price.currency
      +    ):
      +        self.errors.append(
      +            ParserError(
      +                meta,
      +                "Cost and price currencies must match: {} != {}".format(
      +                    cost.currency, price.currency
      +                ),
      +                None,
      +            )
      +        )
      +
      +    return Posting(account, units, cost, price, chr(flag) if flag else None, meta)
      +
      @@ -6409,7 +7144,7 @@

      -beancount.parser.grammar.Builder.price(self, filename, lineno, date, currency, amount, kvlist) +beancount.parser.grammar.Builder.price(self, filename, lineno, date, currency, amount, kvlist)

      @@ -6457,22 +7192,22 @@

      Source code in beancount/parser/grammar.py -
      def price(self, filename, lineno, date, currency, amount, kvlist):
      -    """Process a price directive.
      -
      -    Args:
      -      filename: the current filename.
      -      lineno: the current line number.
      -      date: a datetime object.
      -      currency: the currency to be priced.
      -      amount: an instance of Amount, that is the price of the currency.
      -      kvlist: a list of KeyValue instances.
      -    Returns:
      -      A new Price object.
      -    """
      -    meta = new_metadata(filename, lineno, kvlist)
      -    return Price(meta, date, currency, amount)
      -
      +
      def price(self, filename, lineno, date, currency, amount, kvlist):
      +    """Process a price directive.
      +
      +    Args:
      +      filename: the current filename.
      +      lineno: the current line number.
      +      date: a datetime object.
      +      currency: the currency to be priced.
      +      amount: an instance of Amount, that is the price of the currency.
      +      kvlist: a list of KeyValue instances.
      +    Returns:
      +      A new Price object.
      +    """
      +    meta = new_metadata(filename, lineno, kvlist)
      +    return Price(meta, date, currency, amount)
      +
      @@ -6485,7 +7220,7 @@

      -beancount.parser.grammar.Builder.pushmeta(self, key, value) +beancount.parser.grammar.Builder.pushmeta(self, filename, lineno, key_value)

      @@ -6512,14 +7247,15 @@

      Source code in beancount/parser/grammar.py -
      def pushmeta(self, key, value):
      -    """Set a metadata field on the current key-value pairs to be added to transactions.
      +          
      def pushmeta(self, filename, lineno, key_value):
      +    """Set a metadata field on the current key-value pairs to be added to transactions.
       
      -    Args:
      -      key_value: A KeyValue instance, to be added to the dict of metadata.
      -    """
      -    self.meta[key].append(value)
      -
      + Args: + key_value: A KeyValue instance, to be added to the dict of metadata. + """ + key, value = key_value + self.meta[key].append(value) +
      @@ -6532,7 +7268,7 @@

      -beancount.parser.grammar.Builder.pushtag(self, tag) +beancount.parser.grammar.Builder.pushtag(self, filename, lineno, tag)

      @@ -6560,16 +7296,16 @@

      Source code in beancount/parser/grammar.py -
      def pushtag(self, tag):
      -    """Push a tag on the current set of tags.
      +          
      def pushtag(self, filename, lineno, tag):
      +    """Push a tag on the current set of tags.
       
      -    Note that this does not need to be stack ordered.
      +    Note that this does not need to be stack ordered.
       
      -    Args:
      -      tag: A string, a tag to be added.
      -    """
      -    self.tags.append(tag)
      -
      + Args: + tag: A string, a tag to be added. + """ + self.tags.append(tag) +
      @@ -6582,7 +7318,7 @@

      -beancount.parser.grammar.Builder.query(self, filename, lineno, date, query_name, query_string, kvlist) +beancount.parser.grammar.Builder.query(self, filename, lineno, date, query_name, query_string, kvlist)

      @@ -6630,22 +7366,22 @@

      Source code in beancount/parser/grammar.py -
      def query(self, filename, lineno, date, query_name, query_string, kvlist):
      -    """Process a document directive.
      -
      -    Args:
      -      filename: the current filename.
      -      lineno: the current line number.
      -      date: a datetime object.
      -      query_name: a str, the name of the query.
      -      query_string: a str, the SQL query itself.
      -      kvlist: a list of KeyValue instances.
      -    Returns:
      -      A new Query object.
      -    """
      -    meta = new_metadata(filename, lineno, kvlist)
      -    return Query(meta, date, query_name, query_string)
      -
      +
      def query(self, filename, lineno, date, query_name, query_string, kvlist):
      +    """Process a document directive.
      +
      +    Args:
      +      filename: the current filename.
      +      lineno: the current line number.
      +      date: a datetime object.
      +      query_name: a str, the name of the query.
      +      query_string: a str, the SQL query itself.
      +      kvlist: a list of KeyValue instances.
      +    Returns:
      +      A new Query object.
      +    """
      +    meta = new_metadata(filename, lineno, kvlist)
      +    return Query(meta, date, query_name, query_string)
      +
      @@ -6658,7 +7394,7 @@

      -beancount.parser.grammar.Builder.store_result(self, entries) +beancount.parser.grammar.Builder.store_result(self, filename, lineno, entries)

      @@ -6685,15 +7421,17 @@

      Source code in beancount/parser/grammar.py -
      def store_result(self, entries):
      -    """Start rule stores the final result here.
      -
      -    Args:
      -      entries: A list of entries to store.
      -    """
      -    if entries:
      -        self.entries = entries
      -
      +
      def store_result(self, filename, lineno, entries):
      +    """Start rule stores the final result here.
      +
      +    Args:
      +      entries: A list of entries to store.
      +    """
      +    if entries:
      +        self.entries = entries
      +    # Also record the name of the processed file.
      +    self.options["filename"] = filename
      +
      @@ -6706,7 +7444,7 @@

      @@ -6750,86 +7488,18 @@ @@ -6886,18 +7556,18 @@ @@ -6937,14 +7607,14 @@

      -beancount.parser.grammar.Builder.transaction(self, filename, lineno, date, flag, txn_strings, tags_links, posting_or_kv_list) +beancount.parser.grammar.Builder.transaction(self, filename, lineno, date, flag, txn_strings, tags_links, posting_or_kv_list)

      @@ -7013,192 +7683,128 @@

      Source code in beancount/parser/grammar.py -
      def transaction(self, filename, lineno, date, flag, txn_strings, tags_links,
      -                posting_or_kv_list):
      -    """Process a transaction directive.
      -
      -    All the postings of the transaction are available at this point, and so the
      -    the transaction is balanced here, incomplete postings are completed with the
      -    appropriate position, and errors are being accumulated on the builder to be
      -    reported later on.
      -
      -    This is the main routine that takes up most of the parsing time; be very
      -    careful with modifications here, they have an impact on performance.
      -
      -    Args:
      -      filename: the current filename.
      -      lineno: the current line number.
      -      date: a datetime object.
      -      flag: a str, one-character, the flag associated with this transaction.
      -      txn_strings: A list of strings, possibly empty, possibly longer.
      -      tags_links: A TagsLinks namedtuple of tags, and/or links.
      -      posting_or_kv_list: a list of Posting or KeyValue instances, to be inserted in
      -        this transaction, or None, if no postings have been declared.
      -    Returns:
      -      A new Transaction object.
      -    """
      -    meta = new_metadata(filename, lineno)
      -
      -    # Separate postings and key-values.
      -    explicit_meta = {}
      -    postings = []
      -    tags, links = tags_links.tags, tags_links.links
      -    if posting_or_kv_list:
      -        last_posting = None
      -        for posting_or_kv in posting_or_kv_list:
      -            if isinstance(posting_or_kv, Posting):
      -                postings.append(posting_or_kv)
      -                last_posting = posting_or_kv
      -            elif isinstance(posting_or_kv, TagsLinks):
      -                if postings:
      -                    self.errors.append(ParserError(
      -                        meta,
      -                        "Tags or links not allowed after first " +
      -                        "Posting: {}".format(posting_or_kv), None))
      -                else:
      -                    tags.update(posting_or_kv.tags)
      -                    links.update(posting_or_kv.links)
      -            else:
      -                if last_posting is None:
      -                    value = explicit_meta.setdefault(posting_or_kv.key,
      -                                                     posting_or_kv.value)
      -                    if value is not posting_or_kv.value:
      -                        self.errors.append(ParserError(
      -                            meta, "Duplicate metadata field on entry: {}".format(
      -                                posting_or_kv), None))
      -                else:
      -                    if last_posting.meta is None:
      -                        last_posting = last_posting._replace(meta={})
      -                        postings.pop(-1)
      -                        postings.append(last_posting)
      -
      -                    value = last_posting.meta.setdefault(posting_or_kv.key,
      -                                                         posting_or_kv.value)
      -                    if value is not posting_or_kv.value:
      -                        self.errors.append(ParserError(
      -                            meta, "Duplicate posting metadata field: {}".format(
      -                                posting_or_kv), None))
      -
      -    # Freeze the tags & links or set to default empty values.
      -    tags, links = self.finalize_tags_links(tags, links)
      -
      -    # Initialize the metadata fields from the set of active values.
      -    if self.meta:
      -        for key, value_list in self.meta.items():
      -            meta[key] = value_list[-1]
      -
      -    # Add on explicitly defined values.
      -    if explicit_meta:
      -        meta.update(explicit_meta)
      -
      -    # Unpack the transaction fields.
      -    payee_narration = self.unpack_txn_strings(txn_strings, meta)
      -    if payee_narration is None:
      -        return None
      -    payee, narration = payee_narration
      -
      -    # We now allow a single posting when its balance is zero, so we
      -    # commented out the check below. If a transaction has a single posting
      -    # with a non-zero balance, it'll get caught below in the booking code.
      -    #
      -    # # Detect when a transaction does not have at least two legs.
      -    # if postings is None or len(postings) < 2:
      -    #     self.errors.append(
      -    #         ParserError(meta,
      -    #                     "Transaction with only one posting: {}".format(postings),
      -    #                     None))
      -    #     return None
      -
      -    # If there are no postings, make sure we insert a list object.
      -    if postings is None:
      -        postings = []
      -
      -    # Create the transaction.
      -    return Transaction(meta, date, chr(flag),
      -                       payee, narration, tags, links, postings)
      -
      -
      - - - - - - -
      - - - -

      -beancount.parser.grammar.Builder.unpack_txn_strings(self, txn_strings, meta) - - -

      - -
      - -

      Unpack a tags_links accumulator to its payee and narration fields.

      - - - - - - - - - - - - -
      Parameters: -
        -
      • txn_strings – A list of strings.

      • -
      • meta – A metadata dict for errors generated in this routine.

      • -
      -
      - - - - - - - - - - - -
      Returns: -
        -
      • A pair of (payee, narration) strings or None objects, or None, if -there was an error.

      • -
      -
      -
      - Source code in beancount/parser/grammar.py -
      def unpack_txn_strings(self, txn_strings, meta):
      -    """Unpack a tags_links accumulator to its payee and narration fields.
      -
      -    Args:
      -      txn_strings: A list of strings.
      -      meta: A metadata dict for errors generated in this routine.
      -    Returns:
      -      A pair of (payee, narration) strings or None objects, or None, if
      -      there was an error.
      -    """
      -    num_strings = 0 if txn_strings is None else len(txn_strings)
      -    if num_strings == 1:
      -        payee, narration = None, txn_strings[0]
      -    elif num_strings == 2:
      -        payee, narration = txn_strings
      -    elif num_strings == 0:
      -        payee, narration = None, ""
      -    else:
      -        self.errors.append(
      -            ParserError(meta,
      -                        "Too many strings on transaction description: {}".format(
      -                            txn_strings), None))
      -        return None
      -    return payee, narration
      -
      +
      def transaction(
      +    self, filename, lineno, date, flag, txn_strings, tags_links, posting_or_kv_list
      +):
      +    """Process a transaction directive.
      +
      +    All the postings of the transaction are available at this point, and so the
      +    the transaction is balanced here, incomplete postings are completed with the
      +    appropriate position, and errors are being accumulated on the builder to be
      +    reported later on.
      +
      +    This is the main routine that takes up most of the parsing time; be very
      +    careful with modifications here, they have an impact on performance.
      +
      +    Args:
      +      filename: the current filename.
      +      lineno: the current line number.
      +      date: a datetime object.
      +      flag: a str, one-character, the flag associated with this transaction.
      +      txn_strings: A list of strings, possibly empty, possibly longer.
      +      tags_links: A TagsLinks namedtuple of tags, and/or links.
      +      posting_or_kv_list: a list of Posting or KeyValue instances, to be inserted in
      +        this transaction, or None, if no postings have been declared.
      +    Returns:
      +      A new Transaction object.
      +    """
      +    meta = new_metadata(filename, lineno)
      +
      +    # Separate postings and key-values.
      +    explicit_meta = {}
      +    postings = []
      +    tags, links = tags_links.tags, tags_links.links
      +    if posting_or_kv_list:
      +        last_posting = None
      +        for posting_or_kv in posting_or_kv_list:
      +            if isinstance(posting_or_kv, Posting):
      +                postings.append(posting_or_kv)
      +                last_posting = posting_or_kv
      +            elif isinstance(posting_or_kv, TagsLinks):
      +                if postings:
      +                    self.errors.append(
      +                        ParserError(
      +                            meta,
      +                            "Tags or links not allowed after first "
      +                            + "Posting: {}".format(posting_or_kv),
      +                            None,
      +                        )
      +                    )
      +                else:
      +                    tags.update(posting_or_kv.tags)
      +                    links.update(posting_or_kv.links)
      +            else:
      +                if last_posting is None:
      +                    value = explicit_meta.setdefault(
      +                        posting_or_kv.key, posting_or_kv.value
      +                    )
      +                    if value is not posting_or_kv.value:
      +                        self.errors.append(
      +                            ParserError(
      +                                meta,
      +                                "Duplicate metadata field on entry: {}".format(
      +                                    posting_or_kv
      +                                ),
      +                                None,
      +                            )
      +                        )
      +                else:
      +                    if last_posting.meta is None:
      +                        last_posting = last_posting._replace(meta={})
      +                        postings.pop(-1)
      +                        postings.append(last_posting)
      +
      +                    value = last_posting.meta.setdefault(
      +                        posting_or_kv.key, posting_or_kv.value
      +                    )
      +                    if value is not posting_or_kv.value:
      +                        self.errors.append(
      +                            ParserError(
      +                                meta,
      +                                "Duplicate posting metadata field: {}".format(
      +                                    posting_or_kv
      +                                ),
      +                                None,
      +                            )
      +                        )
      +
      +    # Freeze the tags & links or set to default empty values.
      +    tags, links = self._finalize_tags_links(tags, links)
      +
      +    # Initialize the metadata fields from the set of active values.
      +    if self.meta:
      +        for key, value_list in self.meta.items():
      +            meta[key] = value_list[-1]
      +
      +    # Add on explicitly defined values.
      +    if explicit_meta:
      +        meta.update(explicit_meta)
      +
      +    # Unpack the transaction fields.
      +    payee_narration = self._unpack_txn_strings(txn_strings, meta)
      +    if payee_narration is None:
      +        return None
      +    payee, narration = payee_narration
      +
      +    # We now allow a single posting when its balance is zero, so we
      +    # commented out the check below. If a transaction has a single posting
      +    # with a non-zero balance, it'll get caught below in the booking code.
      +    #
      +    # # Detect when a transaction does not have at least two legs.
      +    # if postings is None or len(postings) < 2:
      +    #     self.errors.append(
      +    #         ParserError(meta,
      +    #                     "Transaction with only one posting: {}".format(postings),
      +    #                     None))
      +    #     return None
      +
      +    # If there are no postings, make sure we insert a list object.
      +    if postings is None:
      +        postings = []
      +
      +    # Create the transaction.
      +    return Transaction(meta, date, chr(flag), payee, narration, tags, links, postings)
      +
      @@ -7251,7 +7857,7 @@

      -beancount.parser.grammar.CompoundAmount.__getnewargs__(self) +beancount.parser.grammar.CompoundAmount.__getnewargs__(self) special @@ -7265,10 +7871,10 @@

      Source code in beancount/parser/grammar.py -
      def __getnewargs__(self):
      -    'Return self as a plain tuple.  Used by copy and pickle.'
      -    return _tuple(self)
      -
      +
      def __getnewargs__(self):
      +    'Return self as a plain tuple.  Used by copy and pickle.'
      +    return _tuple(self)
      +

      @@ -7281,7 +7887,7 @@

      -beancount.parser.grammar.CompoundAmount.__new__(_cls, number_per, number_total, currency) +beancount.parser.grammar.CompoundAmount.__new__(_cls, number_per, number_total, currency) special @@ -7305,7 +7911,7 @@

      -beancount.parser.grammar.CompoundAmount.__repr__(self) +beancount.parser.grammar.CompoundAmount.__repr__(self) special @@ -7319,10 +7925,10 @@

      Source code in beancount/parser/grammar.py -
      def __repr__(self):
      -    'Return a nicely formatted representation string'
      -    return self.__class__.__name__ + repr_fmt % self
      -
      +
      def __repr__(self):
      +    'Return a nicely formatted representation string'
      +    return self.__class__.__name__ + repr_fmt % self
      +
      @@ -7375,7 +7981,7 @@

      -beancount.parser.grammar.DeprecatedError.__getnewargs__(self) +beancount.parser.grammar.DeprecatedError.__getnewargs__(self) special @@ -7389,10 +7995,10 @@

      Source code in beancount/parser/grammar.py -
      def __getnewargs__(self):
      -    'Return self as a plain tuple.  Used by copy and pickle.'
      -    return _tuple(self)
      -
      +
      def __getnewargs__(self):
      +    'Return self as a plain tuple.  Used by copy and pickle.'
      +    return _tuple(self)
      +
      @@ -7405,7 +8011,7 @@

      -beancount.parser.grammar.DeprecatedError.__new__(_cls, source, message, entry) +beancount.parser.grammar.DeprecatedError.__new__(_cls, source, message, entry) special @@ -7429,7 +8035,7 @@

      -beancount.parser.grammar.DeprecatedError.__repr__(self) +beancount.parser.grammar.DeprecatedError.__repr__(self) special @@ -7443,10 +8049,10 @@

      Source code in beancount/parser/grammar.py -
      def __repr__(self):
      -    'Return a nicely formatted representation string'
      -    return self.__class__.__name__ + repr_fmt % self
      -
      +
      def __repr__(self):
      +    'Return a nicely formatted representation string'
      +    return self.__class__.__name__ + repr_fmt % self
      +
      @@ -7499,7 +8105,7 @@

      -beancount.parser.grammar.KeyValue.__getnewargs__(self) +beancount.parser.grammar.KeyValue.__getnewargs__(self) special @@ -7513,10 +8119,10 @@

      Source code in beancount/parser/grammar.py -
      def __getnewargs__(self):
      -    'Return self as a plain tuple.  Used by copy and pickle.'
      -    return _tuple(self)
      -
      +
      def __getnewargs__(self):
      +    'Return self as a plain tuple.  Used by copy and pickle.'
      +    return _tuple(self)
      +
      @@ -7529,7 +8135,7 @@

      -beancount.parser.grammar.KeyValue.__new__(_cls, key, value) +beancount.parser.grammar.KeyValue.__new__(_cls, key, value) special @@ -7553,7 +8159,7 @@

      -beancount.parser.grammar.KeyValue.__repr__(self) +beancount.parser.grammar.KeyValue.__repr__(self) special @@ -7567,10 +8173,10 @@

      Source code in beancount/parser/grammar.py -
      def __repr__(self):
      -    'Return a nicely formatted representation string'
      -    return self.__class__.__name__ + repr_fmt % self
      -
      +
      def __repr__(self):
      +    'Return a nicely formatted representation string'
      +    return self.__class__.__name__ + repr_fmt % self
      +
      @@ -7623,7 +8229,7 @@

      -beancount.parser.grammar.ParserError.__getnewargs__(self) +beancount.parser.grammar.ParserError.__getnewargs__(self) special @@ -7637,10 +8243,10 @@

      Source code in beancount/parser/grammar.py -
      def __getnewargs__(self):
      -    'Return self as a plain tuple.  Used by copy and pickle.'
      -    return _tuple(self)
      -
      +
      def __getnewargs__(self):
      +    'Return self as a plain tuple.  Used by copy and pickle.'
      +    return _tuple(self)
      +
      @@ -7653,7 +8259,7 @@

      -beancount.parser.grammar.ParserError.__new__(_cls, source, message, entry) +beancount.parser.grammar.ParserError.__new__(_cls, source, message, entry) special @@ -7677,7 +8283,7 @@

      -beancount.parser.grammar.ParserError.__repr__(self) +beancount.parser.grammar.ParserError.__repr__(self) special @@ -7691,10 +8297,10 @@

      Source code in beancount/parser/grammar.py -
      def __repr__(self):
      -    'Return a nicely formatted representation string'
      -    return self.__class__.__name__ + repr_fmt % self
      -
      +
      def __repr__(self):
      +    'Return a nicely formatted representation string'
      +    return self.__class__.__name__ + repr_fmt % self
      +
      @@ -7747,7 +8353,7 @@

      -beancount.parser.grammar.ParserSyntaxError.__getnewargs__(self) +beancount.parser.grammar.ParserSyntaxError.__getnewargs__(self) special @@ -7761,10 +8367,10 @@

      Source code in beancount/parser/grammar.py -
      def __getnewargs__(self):
      -    'Return self as a plain tuple.  Used by copy and pickle.'
      -    return _tuple(self)
      -
      +
      def __getnewargs__(self):
      +    'Return self as a plain tuple.  Used by copy and pickle.'
      +    return _tuple(self)
      +
      @@ -7777,7 +8383,7 @@

      -beancount.parser.grammar.ParserSyntaxError.__new__(_cls, source, message, entry) +beancount.parser.grammar.ParserSyntaxError.__new__(_cls, source, message, entry) special @@ -7801,7 +8407,7 @@

      -beancount.parser.grammar.ParserSyntaxError.__repr__(self) +beancount.parser.grammar.ParserSyntaxError.__repr__(self) special @@ -7815,10 +8421,10 @@

      Source code in beancount/parser/grammar.py -
      def __repr__(self):
      -    'Return a nicely formatted representation string'
      -    return self.__class__.__name__ + repr_fmt % self
      -
      +
      def __repr__(self):
      +    'Return a nicely formatted representation string'
      +    return self.__class__.__name__ + repr_fmt % self
      +
      @@ -7871,7 +8477,7 @@

      -beancount.parser.grammar.TagsLinks.__getnewargs__(self) +beancount.parser.grammar.TagsLinks.__getnewargs__(self) special @@ -7885,10 +8491,10 @@

      Source code in beancount/parser/grammar.py -
      def __getnewargs__(self):
      -    'Return self as a plain tuple.  Used by copy and pickle.'
      -    return _tuple(self)
      -
      +
      def __getnewargs__(self):
      +    'Return self as a plain tuple.  Used by copy and pickle.'
      +    return _tuple(self)
      +
      @@ -7901,7 +8507,7 @@

      -beancount.parser.grammar.TagsLinks.__new__(_cls, tags, links) +beancount.parser.grammar.TagsLinks.__new__(_cls, tags, links) special @@ -7925,7 +8531,7 @@

      -beancount.parser.grammar.TagsLinks.__repr__(self) +beancount.parser.grammar.TagsLinks.__repr__(self) special @@ -7939,10 +8545,10 @@

      Source code in beancount/parser/grammar.py -
      def __repr__(self):
      -    'Return a nicely formatted representation string'
      -    return self.__class__.__name__ + repr_fmt % self
      -
      +
      def __repr__(self):
      +    'Return a nicely formatted representation string'
      +    return self.__class__.__name__ + repr_fmt % self
      +
      @@ -7995,7 +8601,7 @@

      -beancount.parser.grammar.ValueType.__getnewargs__(self) +beancount.parser.grammar.ValueType.__getnewargs__(self) special @@ -8009,10 +8615,10 @@

      Source code in beancount/parser/grammar.py -
      def __getnewargs__(self):
      -    'Return self as a plain tuple.  Used by copy and pickle.'
      -    return _tuple(self)
      -
      +
      def __getnewargs__(self):
      +    'Return self as a plain tuple.  Used by copy and pickle.'
      +    return _tuple(self)
      +
      @@ -8025,7 +8631,7 @@

      -beancount.parser.grammar.ValueType.__new__(_cls, value, dtype) +beancount.parser.grammar.ValueType.__new__(_cls, value, dtype) special @@ -8049,7 +8655,7 @@

      -beancount.parser.grammar.ValueType.__repr__(self) +beancount.parser.grammar.ValueType.__repr__(self) special @@ -8063,10 +8669,10 @@

      Source code in beancount/parser/grammar.py -
      def __repr__(self):
      -    'Return a nicely formatted representation string'
      -    return self.__class__.__name__ + repr_fmt % self
      -
      +
      def __repr__(self):
      +    'Return a nicely formatted representation string'
      +    return self.__class__.__name__ + repr_fmt % self
      +
      @@ -8090,7 +8696,7 @@

      -beancount.parser.grammar.valid_account_regexp(options) +beancount.parser.grammar.valid_account_regexp(options)

      @@ -8133,27 +8739,26 @@

      Source code in beancount/parser/grammar.py -
      def valid_account_regexp(options):
      -    """Build a regexp to validate account names from the options.
      -
      -    Args:
      -      options: A dict of options, as per beancount.parser.options.
      -    Returns:
      -      A string, a regular expression that will match all account names.
      -    """
      -    names = map(options.__getitem__, ('name_assets',
      -                                      'name_liabilities',
      -                                      'name_equity',
      -                                      'name_income',
      -                                      'name_expenses'))
      -
      -    # Replace the first term of the account regular expression with the specific
      -    # names allowed under the options configuration. This code is kept in sync
      -    # with {5672c7270e1e}.
      -    return re.compile("(?:{})(?:{}{})+".format('|'.join(names),
      -                                               account.sep,
      -                                               account.ACC_COMP_NAME_RE))
      -
      +
      def valid_account_regexp(options):
      +    """Build a regexp to validate account names from the options.
      +
      +    Args:
      +      options: A dict of options, as per beancount.parser.options.
      +    Returns:
      +      A string, a regular expression that will match all account names.
      +    """
      +    names = map(
      +        options.__getitem__,
      +        ("name_assets", "name_liabilities", "name_equity", "name_income", "name_expenses"),
      +    )
      +
      +    # Replace the first term of the account regular expression with the specific
      +    # names allowed under the options configuration. This code is kept in sync
      +    # with {5672c7270e1e}.
      +    return regex.compile(
      +        "(?:{})(?:{}{})+".format("|".join(names), account.sep, account.ACC_COMP_NAME_RE)
      +    )
      +
      @@ -8206,7 +8811,7 @@

      -beancount.parser.hashsrc.check_parser_source_files() +beancount.parser.hashsrc.check_parser_source_files(parser_module)

      @@ -8219,527 +8824,29 @@

      - Source code in beancount/parser/hashsrc.py -
      def check_parser_source_files():
      -    """Check the extension module's source hash and issue a warning if the
      -    current source differs from that of the module.
      -
      -    If the source files aren't located in the Python source directory, ignore
      -    the warning, we're probably running this from an installed based, in which
      -    case we don't need to check anything (this check is useful only for people
      -    running directly from source).
      -    """
      -    parser_source_hash = hash_parser_source_files()
      -    if parser_source_hash is None:
      -        return
      -    # pylint: disable=import-outside-toplevel
      -    from . import _parser
      -    if _parser.SOURCE_HASH and _parser.SOURCE_HASH != parser_source_hash:
      -        warnings.warn(
      -            ("The Beancount parser C extension module is out-of-date ('{}' != '{}'). "
      -             "You need to rebuild.").format(_parser.SOURCE_HASH, parser_source_hash))
      -
      - - - - - - - -
      - - - -

      -beancount.parser.hashsrc.hash_parser_source_files() - - -

      - -
      - -

      Compute a unique hash of the parser's Python code in order to bake that into -the extension module. This is used at load-time to verify that the extension -module and the corresponding Python codes match each other. If not, it -issues a warning that you should rebuild your extension module.

      - - - - - - - - - - - - -
      Returns: -
        -
      • A string, the hexadecimal unique hash of relevant source code that should -trigger a recompilation.

      • -
      -
      -
      - Source code in beancount/parser/hashsrc.py -
      def hash_parser_source_files():
      -    """Compute a unique hash of the parser's Python code in order to bake that into
      -    the extension module. This is used at load-time to verify that the extension
      -    module and the corresponding Python codes match each other. If not, it
      -    issues a warning that you should rebuild your extension module.
      -
      -    Returns:
      -      A string, the hexadecimal unique hash of relevant source code that should
      -      trigger a recompilation.
      -    """
      -    md5 = hashlib.md5()
      -    for filename in PARSER_SOURCE_FILES:
      -        fullname = path.join(path.dirname(__file__), filename)
      -        if not path.exists(fullname):
      -            return None
      -        with open(fullname, 'rb') as file:
      -            md5.update(file.read())
      -    # Note: Prepend a character in front of the hash because under Windows MSDEV
      -    # removes escapes, and if the hash starts with a number it fails to
      -    # recognize this is a string. A small compromise for portability.
      -    return md5.hexdigest()
      -
      -
      -
      - -
      - - - - - - - - - - - - - - -
      - - - -

      - beancount.parser.lexer - - - -

      - -
      - -

      Beancount syntax lexer.

      - - - -
      - - - - - - - - - -
      - - - -

      - -beancount.parser.lexer.LexBuilder - - - -

      - -
      - -

      A builder used only for building lexer objects.

      - -

      Attributes:

      - - - - - - - - - - - - - - - -
      NameTypeDescription
      long_string_maxlines_default

      Number of lines for a string to trigger a - warning. This is meant to help users detecting dangling quotes in - their source.

      - - - -
      - - - - - - - - - -
      - - - -

      -beancount.parser.lexer.LexBuilder.ACCOUNT(self, account_name) - - -

      - -
      - -

      Process an ACCOUNT token.

      -

      This function attempts to reuse an existing account if one exists, -otherwise creates one on-demand.

      - - - - - - - - - - - - -
      Parameters: -
        -
      • account_name – a str, the valid name of an account.

      • -
      -
      - - - - - - - - - - - -
      Returns: -
        -
      • A string, the name of the account.

      • -
      -
      -
      - Source code in beancount/parser/lexer.py -
      def ACCOUNT(self, account_name):
      -    """Process an ACCOUNT token.
      -
      -    This function attempts to reuse an existing account if one exists,
      -    otherwise creates one on-demand.
      -
      -    Args:
      -      account_name: a str, the valid name of an account.
      -    Returns:
      -      A string, the name of the account.
      -    """
      -    # Check account name validity.
      -    if not self.account_regexp.match(account_name):
      -        raise ValueError("Invalid account name: {}".format(account_name))
      -
      -    # Reuse (intern) account strings as much as possible. This potentially
      -    # reduces memory usage a fair bit, because these strings are repeated
      -    # liberally.
      -    return self.accounts.setdefault(account_name, account_name)
      -
      -
      -
      - -
      - - - -
      - - - -

      -beancount.parser.lexer.LexBuilder.CURRENCY(self, currency_name) - - -

      - -
      - -

      Process a CURRENCY token.

      - - - - - - - - - - - - -
      Parameters: -
        -
      • currency_name – the name of the currency.

      • -
      -
      - - - - - - - - - - - -
      Returns: -
        -
      • A new currency object; for now, these are simply represented -as the currency name.

      • -
      -
      -
      - Source code in beancount/parser/lexer.py -
      def CURRENCY(self, currency_name):
      -    """Process a CURRENCY token.
      -
      -    Args:
      -      currency_name: the name of the currency.
      -    Returns:
      -      A new currency object; for now, these are simply represented
      -      as the currency name.
      -    """
      -    self.commodities.add(currency_name)
      -    return currency_name
      -
      -
      -
      - -
      - - - -
      - - - -

      -beancount.parser.lexer.LexBuilder.DATE(self, year, month, day) - - -

      - -
      - -

      Process a DATE token.

      - - - - - - - - - - - - -
      Parameters: -
        -
      • year – integer year.

      • -
      • month – integer month.

      • -
      • day – integer day

      • -
      -
      - - - - - - - - - - - -
      Returns: -
        -
      • A new datetime object.

      • -
      -
      -
      - Source code in beancount/parser/lexer.py -
      def DATE(self, year, month, day):
      -    """Process a DATE token.
      -
      -    Args:
      -      year: integer year.
      -      month: integer month.
      -      day: integer day
      -    Returns:
      -      A new datetime object.
      -    """
      -    return datetime.date(year, month, day)
      -
      -
      -
      - -
      - - - -
      - - - -

      -beancount.parser.lexer.LexBuilder.KEY(self, ident) - - -

      - -
      - -

      Process an identifier token.

      - - - - - - - - - - - - -
      Parameters: -
        -
      • ident – a str, the name of the key string.

      • -
      -
      - - - - - - - - - - - -
      Returns: -
        -
      • The link string itself. For now we don't need to represent this by -an object.

      • -
      -
      -
      - Source code in beancount/parser/lexer.py -
      def KEY(self, ident):
      -    """Process an identifier token.
      -
      -    Args:
      -      ident: a str, the name of the key string.
      -    Returns:
      -      The link string itself. For now we don't need to represent this by
      -      an object.
      -    """
      -    return ident
      -
      -
      -
      - -
      - - - -
      - - - - - -
      - -

      Process a LINK token.

      - - - - - - - - - - - - -
      Parameters: -
        -
      • link – a str, the name of the string.

      • -
      -
      - - - - - - - - - - - -
      Returns: -
        -
      • The link string itself. For now we don't need to represent this by -an object.

      • -
      -
      +
      - Source code in beancount/parser/lexer.py -
      def LINK(self, link):
      -    """Process a LINK token.
      -
      -    Args:
      -      link: a str, the name of the string.
      -    Returns:
      -      The link string itself. For now we don't need to represent this by
      -      an object.
      -    """
      -    return link
      -
      + Source code in beancount/parser/hashsrc.py +
      def check_parser_source_files(parser_module: types.ModuleType):
      +    """Check the extension module's source hash and issue a warning if the
      +    current source differs from that of the module.
      +
      +    If the source files aren't located in the Python source directory, ignore
      +    the warning, we're probably running this from an installed based, in which
      +    case we don't need to check anything (this check is useful only for people
      +    running directly from source).
      +    """
      +    parser_source_hash = hash_parser_source_files()
      +    if parser_source_hash is None:
      +        return
      +    if parser_module.SOURCE_HASH and parser_module.SOURCE_HASH != parser_source_hash:
      +        warnings.warn(
      +            (
      +                "The Beancount parser C extension module is out-of-date ('{}' != '{}'). "
      +                "You need to rebuild."
      +            ).format(parser_module.SOURCE_HASH, parser_source_hash)
      +        )
      +
      @@ -8747,86 +8854,35 @@
      -

      Process a NUMBER token. Convert into Decimal.

      +

      Generate an include file for the parser source hash.

      - - - - - - - - - - - -
      Parameters: -
        -
      • number – a str, the number to be converted.

      • -
      -
      - - - - - - - - - - - -
      Returns: -
        -
      • A Decimal instance built of the number string.

      • -
      -
      - Source code in beancount/parser/lexer.py -
      def NUMBER(self, number):
      -    """Process a NUMBER token. Convert into Decimal.
      -
      -    Args:
      -      number: a str, the number to be converted.
      -    Returns:
      -      A Decimal instance built of the number string.
      -    """
      -    # Note: We don't use D() for efficiency here.
      -    # The lexer will only yield valid number strings.
      -    if ',' in number:
      -        # Extract the integer part and check the commas match the
      -        # locale-aware formatted version. This
      -        match = re.match(r"([\d,]*)(\.\d*)?$", number)
      -        if not match:
      -            # This path is never taken because the lexer will parse a comma
      -            # in the fractional part as two NUMBERs with a COMMA token in
      -            # between.
      -            self.errors.append(
      -                LexerError(self.get_lexer_location(),
      -                           "Invalid number format: '{}'".format(number), None))
      -        else:
      -            int_string, float_string = match.groups()
      -            reformatted_number = r"{:,.0f}".format(int(int_string.replace(",", "")))
      -            if int_string != reformatted_number:
      -                self.errors.append(
      -                    LexerError(self.get_lexer_location(),
      -                               "Invalid commas: '{}'".format(number), None))
      -
      -        number = number.replace(',', '')
      -    return Decimal(number)
      -
      + Source code in beancount/parser/hashsrc.py +
      def gen_include():
      +    """Generate an include file for the parser source hash."""
      +    return textwrap.dedent(
      +        """\
      +      #ifndef __BEANCOUNT_PARSER_PARSE_SOURCE_HASH_H__
      +      #define __BEANCOUNT_PARSER_PARSE_SOURCE_HASH_H__
      +
      +      #define PARSER_SOURCE_HASH {source_hash}
      +
      +      #endif // __BEANCOUNT_PARSER_PARSE_SOURCE_HASH_H__
      +    """.format(source_hash=hash_parser_source_files())
      +    )
      +
      @@ -8834,36 +8890,23 @@

      -
      +
      -

      -beancount.parser.lexer.LexBuilder.STRING(self, string) +

      +beancount.parser.hashsrc.hash_parser_source_files() -

      +

      -

      Process a STRING token.

      +

      Compute a unique hash of the parser's Python code in order to bake that into +the extension module. This is used at load-time to verify that the extension +module and the corresponding Python codes match each other. If not, it +issues a warning that you should rebuild your extension module.

      - - - - - - - - - - - -
      Parameters: -
        -
      • string – the string to process.

      • -
      -
      @@ -8874,36 +8917,37 @@

      Returns:
        -
      • The string. Nothing to be done or cleaned up. Eventually we might -do some decoding here.

      • +
      • A string, the hexadecimal unique hash of relevant source code that should +trigger a recompilation.

      - Source code in beancount/parser/lexer.py -
      def STRING(self, string):
      -    """Process a STRING token.
      -
      -    Args:
      -      string: the string to process.
      -    Returns:
      -      The string. Nothing to be done or cleaned up. Eventually we might
      -      do some decoding here.
      -    """
      -    # If a multiline string, warm over a certain number of lines.
      -    if '\n' in string:
      -        num_lines = string.count('\n') + 1
      -        if num_lines > self.long_string_maxlines_default:
      -            # This is just a warning; accept the string anyhow.
      -            self.errors.append(
      -                LexerError(
      -                    self.get_lexer_location(),
      -                    "String too long ({} lines); possible error".format(num_lines),
      -                    None))
      -    return string
      -
      + Source code in beancount/parser/hashsrc.py +
      def hash_parser_source_files():
      +    """Compute a unique hash of the parser's Python code in order to bake that into
      +    the extension module. This is used at load-time to verify that the extension
      +    module and the corresponding Python codes match each other. If not, it
      +    issues a warning that you should rebuild your extension module.
      +
      +    Returns:
      +      A string, the hexadecimal unique hash of relevant source code that should
      +      trigger a recompilation.
      +    """
      +    md5 = hashlib.md5()
      +    for filename in PARSER_SOURCE_FILES:
      +        fullname = path.join(path.dirname(__file__), filename)
      +        if not path.exists(fullname):
      +            return None
      +        with open(fullname, "rb") as file:
      +            md5.update(file.read())
      +    # Note: Prepend a character in front of the hash because under Windows MSDEV
      +    # removes escapes, and if the hash starts with a number it fails to
      +    # recognize this is a string. A small compromise for portability.
      +    return md5.hexdigest()
      +
      @@ -8911,125 +8955,72 @@

      -
      -

      -beancount.parser.lexer.LexBuilder.TAG(self, tag) + +

      + +

      + +
      + + + +
      + + + +

      + beancount.parser.lexer + -

      +

      -

      Process a TAG token.

      +

      Beancount syntax lexer.

      - - - - - - - - - - - -
      Parameters: -
        -
      • tag – a str, the tag to be processed.

      • -
      -
      - - - - - - - - - - - -
      Returns: -
        -
      • The tag string itself. For now we don't need an object to represent -those; keeping it simple.

      • -
      -
      -
      - Source code in beancount/parser/lexer.py -
      def TAG(self, tag):
      -    """Process a TAG token.
      -
      -    Args:
      -      tag: a str, the tag to be processed.
      -    Returns:
      -      The tag string itself. For now we don't need an object to represent
      -      those; keeping it simple.
      -    """
      -    return tag
      -
      -
      -
      - +
      -
      -

      -beancount.parser.lexer.LexBuilder.build_lexer_error(self, message, exc_type=None) -

      + +
      + + + +

      + +beancount.parser.lexer.LexBuilder + + + +

      -

      Build a lexer error and appends it to the list of pending errors.

      +

      A builder used only for building lexer objects.

      + + + + +
      + + + + + - - - - - - - - - - - -
      Parameters: -
        -
      • message – The message of the error.

      • -
      • exc_type – An exception type, if an exception occurred.

      • -
      -
      -
      - Source code in beancount/parser/lexer.py -
      def build_lexer_error(self, message, exc_type=None): # {0e31aeca3363}
      -    """Build a lexer error and appends it to the list of pending errors.
      -
      -    Args:
      -      message: The message of the error.
      -      exc_type: An exception type, if an exception occurred.
      -    """
      -    if not isinstance(message, str):
      -        message = str(message)
      -    if exc_type is not None:
      -        message = '{}: {}'.format(exc_type.__name__, message)
      -    self.errors.append(
      -        LexerError(self.get_lexer_location(), message, None))
      -
      -
      -
      -
      @@ -9037,18 +9028,15 @@

      -beancount.parser.lexer.LexBuilder.get_invalid_account(self) +

      +beancount.parser.lexer.LexBuilder.build_lexer_error(self, filename, lineno, message) -

      +

      -

      Return the name of an invalid account placeholder.

      -

      When an account name is not deemed a valid one, replace it by -this account name. This can be overridden by the parser to -take into account the options.

      +

      Build a lexer error and appends it to the list of pending errors.

      @@ -9057,29 +9045,25 @@

      - - + +
      Returns: + Parameters:
        -
      • A string, the name of the root/type for invalid account names.

      • +
      • message – The message of the error.

      -
      Source code in beancount/parser/lexer.py -
      def get_invalid_account(self):
      -    """Return the name of an invalid account placeholder.
      -
      -    When an account name is not deemed a valid one, replace it by
      -    this account name. This can be overridden by the parser to
      -    take into account the options.
      +          
      def build_lexer_error(self, filename, lineno, message):  # {0e31aeca3363}
      +    """Build a lexer error and appends it to the list of pending errors.
       
      -    Returns:
      -      A string, the name of the root/type for invalid account names.
      -    """
      -    return 'Equity:InvalidAccountName'
      -
      + Args: + message: The message of the error. + """ + self.errors.append(LexerError(new_metadata(filename, lineno), str(message), None)) +
      @@ -9089,7 +9073,6 @@

      -beancount.parser.lexer.LexerError.__getnewargs__(self) +beancount.parser.lexer.LexerError.__getnewargs__(self) special @@ -9147,10 +9130,10 @@

      Source code in beancount/parser/lexer.py -
      def __getnewargs__(self):
      -    'Return self as a plain tuple.  Used by copy and pickle.'
      -    return _tuple(self)
      -
      +
      def __getnewargs__(self):
      +    'Return self as a plain tuple.  Used by copy and pickle.'
      +    return _tuple(self)
      +
      @@ -9163,7 +9146,7 @@

      -beancount.parser.lexer.LexerError.__new__(_cls, source, message, entry) +beancount.parser.lexer.LexerError.__new__(_cls, source, message, entry) special @@ -9187,7 +9170,7 @@

      -beancount.parser.lexer.LexerError.__repr__(self) +beancount.parser.lexer.LexerError.__repr__(self) special @@ -9201,10 +9184,10 @@

      Source code in beancount/parser/lexer.py -
      def __repr__(self):
      -    'Return a nicely formatted representation string'
      -    return self.__class__.__name__ + repr_fmt % self
      -
      +
      def __repr__(self):
      +    'Return a nicely formatted representation string'
      +    return self.__class__.__name__ + repr_fmt % self
      +
      @@ -9228,7 +9211,7 @@

      -beancount.parser.lexer.lex_iter(file, builder=None, encoding=None) +beancount.parser.lexer.lex_iter(file, builder=None)

      @@ -9250,45 +9233,46 @@

    • file – A string, the filename to run the lexer on, or a file object.

    • builder – A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors).

    • -
    • encoding – A string (or None), the default encoding to use for strings.

    Yields: - Tuples of the token (a string), the matched text (a string), and the line - no (an integer).

    + All the tokens in the input file as (token, lineno, text, + value) tuples where token is a string representing the + token kind, lineno is the line number in the input file + where the token was matched, mathed is a bytes object + containing the exact text matched, and value is the semantic + value of the token or None.

    Source code in beancount/parser/lexer.py -
    def lex_iter(file, builder=None, encoding=None):
    -    """An iterator that yields all the tokens in the given file.
    -
    -    Args:
    -      file: A string, the filename to run the lexer on, or a file object.
    -      builder: A builder of your choice. If not specified, a LexBuilder is
    -        used and discarded (along with its errors).
    -      encoding: A string (or None), the default encoding to use for strings.
    -    Yields:
    -      Tuples of the token (a string), the matched text (a string), and the line
    -      no (an integer).
    -    """
    -    if isinstance(file, str):
    -        filename = file
    -    else:
    -        filename = file.name
    -    if builder is None:
    -        builder = LexBuilder()
    -    _parser.lexer_initialize(filename, builder, encoding)
    -    try:
    -        while 1:
    -            token_tuple = _parser.lexer_next()
    -            if token_tuple is None:
    -                break
    -            yield token_tuple
    -    finally:
    -        _parser.lexer_finalize()
    -
    +
    def lex_iter(file, builder=None):
    +    """An iterator that yields all the tokens in the given file.
    +
    +    Args:
    +      file: A string, the filename to run the lexer on, or a file object.
    +      builder: A builder of your choice. If not specified, a LexBuilder is
    +        used and discarded (along with its errors).
    +    Yields:
    +      All the tokens in the input file as ``(token, lineno, text,
    +      value)`` tuples where ``token`` is a string representing the
    +      token kind, ``lineno`` is the line number in the input file
    +      where the token was matched, ``mathed`` is a bytes object
    +      containing the exact text matched, and ``value`` is the semantic
    +      value of the token or None.
    +    """
    +    with contextlib.ExitStack() as ctx:
    +        # It would be more appropriate here to check for io.RawIOBase but
    +        # that does not work for io.BytesIO despite it implementing the
    +        # readinto() method.
    +        if not isinstance(file, io.IOBase):
    +            file = ctx.enter_context(open(file, "rb"))
    +        if builder is None:
    +            builder = LexBuilder()
    +        parser = _parser.Parser(builder)
    +        yield from parser.lex(file)
    +
    @@ -9301,14 +9285,14 @@

    -beancount.parser.lexer.lex_iter_string(string, builder=None, encoding=None) +beancount.parser.lexer.lex_iter_string(string, builder=None, **kwargs)

    -

    Parse an input string and print the tokens to an output file.

    +

    An iterator that yields all the tokens in the given string.

    @@ -9320,10 +9304,7 @@

    @@ -9339,7 +9320,7 @@

    @@ -9347,23 +9328,19 @@

    Parameters:
      -
    • input_string – a str or bytes, the contents of the ledger to be parsed.

    • -
    • builder – A builder of your choice. If not specified, a LexBuilder is -used and discarded (along with its errors).

    • -
    • encoding – A string (or None), the default encoding to use for strings.

    • +
    • string – a str or bytes, the contents of the ledger to be parsed.

    Returns:
      -
    • A iterator on the string. See lex_iter() for details.

    • +
    • An iterator, see lex_iter() for details.

    Source code in beancount/parser/lexer.py -
    def lex_iter_string(string, builder=None, encoding=None):
    -    """Parse an input string and print the tokens to an output file.
    -
    -    Args:
    -      input_string: a str or bytes, the contents of the ledger to be parsed.
    -      builder: A builder of your choice. If not specified, a LexBuilder is
    -        used and discarded (along with its errors).
    -      encoding: A string (or None), the default encoding to use for strings.
    -    Returns:
    -      A iterator on the string. See lex_iter() for details.
    -    """
    -    tmp_file = tempfile.NamedTemporaryFile('w' if isinstance(string, str) else 'wb')
    -    tmp_file.write(string)
    -    tmp_file.flush()
    -    # Note: We pass in the file object in order to keep it alive during parsing.
    -    return lex_iter(tmp_file, builder, encoding)
    -
    +
    def lex_iter_string(string, builder=None, **kwargs):
    +    """An iterator that yields all the tokens in the given string.
    +
    +    Args:
    +      string: a str or bytes, the contents of the ledger to be parsed.
    +    Returns:
    +      An iterator, see ``lex_iter()`` for details.
    +    """
    +    if not isinstance(string, bytes):
    +        string = string.encode("utf8")
    +    file = io.BytesIO(string)
    +    yield from lex_iter(file, builder=builder, **kwargs)
    +
    @@ -9451,7 +9428,7 @@

    -beancount.parser.options.OptDesc.__getnewargs__(self) +beancount.parser.options.OptDesc.__getnewargs__(self) special @@ -9465,10 +9442,10 @@

    Source code in beancount/parser/options.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -9481,7 +9458,7 @@

    -beancount.parser.options.OptDesc.__new__(_cls, name, default_value, example_value, converter, deprecated, alias) +beancount.parser.options.OptDesc.__new__(_cls, name, default_value, example_value, converter, deprecated, alias) special @@ -9505,7 +9482,7 @@

    -beancount.parser.options.OptDesc.__repr__(self) +beancount.parser.options.OptDesc.__repr__(self) special @@ -9519,10 +9496,10 @@

    Source code in beancount/parser/options.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -9575,7 +9552,7 @@

    -beancount.parser.options.OptGroup.__getnewargs__(self) +beancount.parser.options.OptGroup.__getnewargs__(self) special @@ -9589,10 +9566,10 @@

    Source code in beancount/parser/options.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -9605,7 +9582,7 @@

    -beancount.parser.options.OptGroup.__new__(_cls, description, options) +beancount.parser.options.OptGroup.__new__(_cls, description, options) special @@ -9629,7 +9606,7 @@

    -beancount.parser.options.OptGroup.__repr__(self) +beancount.parser.options.OptGroup.__repr__(self) special @@ -9643,10 +9620,10 @@

    Source code in beancount/parser/options.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -9670,7 +9647,7 @@

    -beancount.parser.options.Opt(name, default_value, example_value=<object object at 0x78e86917d920>, converter=None, deprecated=False, alias=None) +beancount.parser.options.Opt(name, default_value, example_value=<object object at 0x78fcdfec0a20>, converter=None, deprecated=False, alias=None)

    @@ -9718,27 +9695,101 @@

    Source code in beancount/parser/options.py -
    def Opt(name, default_value,
    -        example_value=UNSET,
    -        converter=None,
    -        deprecated=False,
    -        alias=None):
    -    """Alternative constructor for OptDesc, with default values.
    -
    -    Args:
    -      name: See OptDesc.
    -      default_value: See OptDesc.
    -      example_value: See OptDesc.
    -      converter: See OptDesc.
    -      deprecated: See OptDesc.
    -      alias: See OptDesc.
    -    Returns:
    -      An instance of OptDesc.
    -    """
    -    if example_value is UNSET:
    -        example_value = default_value
    -    return OptDesc(name, default_value, example_value, converter, deprecated, alias)
    -
    +
    def Opt(
    +    name, default_value, example_value=UNSET, converter=None, deprecated=False, alias=None
    +):
    +    """Alternative constructor for OptDesc, with default values.
    +
    +    Args:
    +      name: See OptDesc.
    +      default_value: See OptDesc.
    +      example_value: See OptDesc.
    +      converter: See OptDesc.
    +      deprecated: See OptDesc.
    +      alias: See OptDesc.
    +    Returns:
    +      An instance of OptDesc.
    +    """
    +    if example_value is UNSET:
    +        example_value = default_value
    +    return OptDesc(name, default_value, example_value, converter, deprecated, alias)
    +
    +
    + + + + + + +
    + + + +

    +beancount.parser.options.get_account_types(options) + + +

    + +
    + +

    Extract the account type names from the parser's options.

    + + + + + + + + + + + + +
    Parameters: +
      +
    • options – a dict of ledger options.

    • +
    +
    + + + + + + + + + + + +
    Returns: +
      +
    • An instance of AccountTypes, that contains all the prefixes.

    • +
    +
    +
    + Source code in beancount/parser/options.py +
    def get_account_types(options):
    +    """Extract the account type names from the parser's options.
    +
    +    Args:
    +      options: a dict of ledger options.
    +    Returns:
    +      An instance of AccountTypes, that contains all the prefixes.
    +    """
    +    return account_types.AccountTypes(
    +        *[
    +            options[key]
    +            for key in (
    +                "name_assets",
    +                "name_liabilities",
    +                "name_equity",
    +                "name_income",
    +                "name_expenses",
    +            )
    +        ]
    +    )
    +
    @@ -9750,15 +9801,15 @@

    -

    -beancount.parser.options.get_account_types(options) +

    +beancount.parser.options.get_current_accounts(options) -

    +

    -

    Extract the account type names from the parser's options.

    +

    Return account names for the current earnings and conversion accounts.

    @@ -9786,7 +9837,8 @@

    @@ -9794,22 +9846,22 @@

    Returns:
      -
    • An instance of AccountTypes, that contains all the prefixes.

    • +
    • A tuple of 2 account objects, one for booking current earnings, and one +for current conversions.

    Source code in beancount/parser/options.py -
    def get_account_types(options):
    -    """Extract the account type names from the parser's options.
    -
    -    Args:
    -      options: a dict of ledger options.
    -    Returns:
    -      An instance of AccountTypes, that contains all the prefixes.
    -    """
    -    return account_types.AccountTypes(
    -        *[options[key]
    -          for key in ("name_assets",
    -                      "name_liabilities",
    -                      "name_equity",
    -                      "name_income",
    -                      "name_expenses")])
    -
    +
    def get_current_accounts(options):
    +    """Return account names for the current earnings and conversion accounts.
    +
    +    Args:
    +      options: a dict of ledger options.
    +    Returns:
    +      A tuple of 2 account objects, one for booking current earnings, and one
    +      for current conversions.
    +    """
    +    equity = options["name_equity"]
    +    account_current_earnings = account.join(equity, options["account_current_earnings"])
    +    account_current_conversions = account.join(
    +        equity, options["account_current_conversions"]
    +    )
    +    return (account_current_earnings, account_current_conversions)
    +
    @@ -9821,15 +9873,15 @@

    -

    -beancount.parser.options.get_current_accounts(options) +

    +beancount.parser.options.get_previous_accounts(options) -

    +

    -

    Return account names for the current earnings and conversion accounts.

    +

    Return account names for the previous earnings, balances and conversion accounts.

    @@ -9857,8 +9909,8 @@

    @@ -9866,23 +9918,27 @@

    Returns:
      -
    • A tuple of 2 account objects, one for booking current earnings, and one -for current conversions.

    • +
    • A tuple of 3 account objects, for booking previous earnings, +previous balances, and previous conversions.

    Source code in beancount/parser/options.py -
    def get_current_accounts(options):
    -    """Return account names for the current earnings and conversion accounts.
    -
    -    Args:
    -      options: a dict of ledger options.
    -    Returns:
    -      A tuple of 2 account objects, one for booking current earnings, and one
    -      for current conversions.
    -    """
    -    equity = options['name_equity']
    -    account_current_earnings = account.join(equity,
    -                                            options['account_current_earnings'])
    -    account_current_conversions = account.join(equity,
    -                                               options['account_current_conversions'])
    -    return (account_current_earnings,
    -            account_current_conversions)
    -
    +
    def get_previous_accounts(options):
    +    """Return account names for the previous earnings, balances and conversion accounts.
    +
    +    Args:
    +      options: a dict of ledger options.
    +    Returns:
    +      A tuple of 3 account objects, for booking previous earnings,
    +      previous balances, and previous conversions.
    +    """
    +    equity = options["name_equity"]
    +    account_previous_earnings = account.join(equity, options["account_previous_earnings"])
    +    account_previous_balances = account.join(equity, options["account_previous_balances"])
    +    account_previous_conversions = account.join(
    +        equity, options["account_previous_conversions"]
    +    )
    +    return (
    +        account_previous_earnings,
    +        account_previous_balances,
    +        account_previous_conversions,
    +    )
    +
    @@ -9894,15 +9950,15 @@

    -

    -beancount.parser.options.get_previous_accounts(options) +

    +beancount.parser.options.get_unrealized_account(options) -

    +

    -

    Return account names for the previous earnings, balances and conversion accounts.

    +

    Return the full account name for the unrealized account.

    @@ -9930,8 +9986,8 @@

    @@ -9939,26 +9995,18 @@

    Returns:
      -
    • A tuple of 3 account objects, for booking previous earnings, -previous balances, and previous conversions.

    • +
    • A tuple of 2 account objects, one for booking current earnings, and one +for current conversions.

    Source code in beancount/parser/options.py -
    def get_previous_accounts(options):
    -    """Return account names for the previous earnings, balances and conversion accounts.
    -
    -    Args:
    -      options: a dict of ledger options.
    -    Returns:
    -      A tuple of 3 account objects, for booking previous earnings,
    -      previous balances, and previous conversions.
    -    """
    -    equity = options['name_equity']
    -    account_previous_earnings = account.join(equity,
    -                                             options['account_previous_earnings'])
    -    account_previous_balances = account.join(equity,
    -                                             options['account_previous_balances'])
    -    account_previous_conversions = account.join(equity,
    -                                                options['account_previous_conversions'])
    -    return (account_previous_earnings,
    -            account_previous_balances,
    -            account_previous_conversions)
    -
    +
    def get_unrealized_account(options):
    +    """Return the full account name for the unrealized account.
    +
    +    Args:
    +      options: a dict of ledger options.
    +    Returns:
    +      A tuple of 2 account objects, one for booking current earnings, and one
    +      for current conversions.
    +    """
    +    income = options["name_income"]
    +    return account.join(income, options["account_unrealized_gains"])
    +
    @@ -9971,7 +10019,7 @@

    -beancount.parser.options.list_options() +beancount.parser.options.list_options()

    @@ -9998,37 +10046,39 @@

    Source code in beancount/parser/options.py -
    def list_options():
    -    """Produce a formatted text of the available options and their description.
    -
    -    Returns:
    -      A string, formatted nicely to be printed in 80 columns.
    -    """
    -    oss = io.StringIO()
    -    for group in PUBLIC_OPTION_GROUPS:
    -        for desc in group.options:
    -            oss.write('option "{}" "{}"\n'.format(desc.name, desc.example_value))
    -            if desc.deprecated:
    -                oss.write(textwrap.fill(
    -                    "THIS OPTION IS DEPRECATED: {}".format(desc.deprecated),
    -                    initial_indent="  ",
    -                    subsequent_indent="  "))
    -                oss.write('\n\n')
    -        description = ' '.join(line.strip()
    -                               for line in group.description.strip().splitlines())
    -        oss.write(textwrap.fill(description,
    -                                initial_indent='  ',
    -                                subsequent_indent='  '))
    -        oss.write('\n')
    -
    -        if isinstance(desc.default_value, (list, dict, set)):
    -            oss.write('\n')
    -            oss.write('  (This option may be supplied multiple times.)\n')
    -
    -        oss.write('\n\n')
    -
    -    return oss.getvalue()
    -
    +
    def list_options():
    +    """Produce a formatted text of the available options and their description.
    +
    +    Returns:
    +      A string, formatted nicely to be printed in 80 columns.
    +    """
    +    oss = io.StringIO()
    +    for group in PUBLIC_OPTION_GROUPS:
    +        for desc in group.options:
    +            oss.write('option "{}" "{}"\n'.format(desc.name, desc.example_value))
    +            if desc.deprecated:
    +                oss.write(
    +                    textwrap.fill(
    +                        "THIS OPTION IS DEPRECATED: {}".format(desc.deprecated),
    +                        initial_indent="  ",
    +                        subsequent_indent="  ",
    +                    )
    +                )
    +                oss.write("\n\n")
    +        description = " ".join(
    +            line.strip() for line in group.description.strip().splitlines()
    +        )
    +        oss.write(textwrap.fill(description, initial_indent="  ", subsequent_indent="  "))
    +        oss.write("\n")
    +
    +        if isinstance(desc.default_value, (list, dict, set)):
    +            oss.write("\n")
    +            oss.write("  (This option may be supplied multiple times.)\n")
    +
    +        oss.write("\n\n")
    +
    +    return oss.getvalue()
    +
    @@ -10041,7 +10091,7 @@

    -beancount.parser.options.options_validate_booking_method(value) +beancount.parser.options.options_validate_booking_method(value)

    @@ -10100,21 +10150,21 @@

    Source code in beancount/parser/options.py -
    def options_validate_booking_method(value):
    -    """Validate a booking method name.
    -
    -    Args:
    -      value: A string, the value provided as option.
    -    Returns:
    -      The new value, converted, if the conversion is successful.
    -    Raises:
    -      ValueError: If the value is invalid.
    -    """
    -    try:
    -        return data.Booking[value]
    -    except KeyError as exc:
    -        raise ValueError(str(exc))
    -
    +
    def options_validate_booking_method(value):
    +    """Validate a booking method name.
    +
    +    Args:
    +      value: A string, the value provided as option.
    +    Returns:
    +      The new value, converted, if the conversion is successful.
    +    Raises:
    +      ValueError: If the value is invalid.
    +    """
    +    try:
    +        return data.Booking[value]
    +    except KeyError as exc:
    +        raise ValueError(str(exc)) from exc
    +
    @@ -10127,7 +10177,7 @@

    -beancount.parser.options.options_validate_boolean(value) +beancount.parser.options.options_validate_boolean(value)

    @@ -10186,18 +10236,18 @@

    Source code in beancount/parser/options.py -
    def options_validate_boolean(value):
    -    """Validate a boolean option.
    -
    -    Args:
    -      value: A string, the value provided as option.
    -    Returns:
    -      The new value, converted, if the conversion is successful.
    -    Raises:
    -      ValueError: If the value is invalid.
    -    """
    -    return value.lower() in ('1', 'true', 'yes')
    -
    +
    def options_validate_boolean(value):
    +    """Validate a boolean option.
    +
    +    Args:
    +      value: A string, the value provided as option.
    +    Returns:
    +      The new value, converted, if the conversion is successful.
    +    Raises:
    +      ValueError: If the value is invalid.
    +    """
    +    return value.lower() in ("1", "true", "yes")
    +
    @@ -10210,7 +10260,7 @@

    -beancount.parser.options.options_validate_plugin(value) +beancount.parser.options.options_validate_plugin(value)

    @@ -10269,26 +10319,26 @@

    Source code in beancount/parser/options.py -
    def options_validate_plugin(value):
    -    """Validate the plugin option.
    -
    -    Args:
    -      value: A string, the value provided as option.
    -    Returns:
    -      The new value, converted, if the conversion is successful.
    -    Raises:
    -      ValueError: If the value is invalid.
    -    """
    -    # Process the 'plugin' option specially: accept an optional
    -    # argument from it. NOTE: We will eventually phase this out and
    -    # replace it by a dedicated 'plugin' directive.
    -    match = re.match('(.*):(.*)', value)
    -    if match:
    -        plugin_name, plugin_config = match.groups()
    -    else:
    -        plugin_name, plugin_config = value, None
    -    return (plugin_name, plugin_config)
    -
    +
    def options_validate_plugin(value):
    +    """Validate the plugin option.
    +
    +    Args:
    +      value: A string, the value provided as option.
    +    Returns:
    +      The new value, converted, if the conversion is successful.
    +    Raises:
    +      ValueError: If the value is invalid.
    +    """
    +    # Process the 'plugin' option specially: accept an optional
    +    # argument from it. NOTE: We will eventually phase this out and
    +    # replace it by a dedicated 'plugin' directive.
    +    match = re.match("(.*):(.*)", value)
    +    if match:
    +        plugin_name, plugin_config = match.groups()
    +    else:
    +        plugin_name, plugin_config = value, None
    +    return (plugin_name, plugin_config)
    +
    @@ -10301,7 +10351,7 @@

    -beancount.parser.options.options_validate_processing_mode(value) +beancount.parser.options.options_validate_processing_mode(value)

    @@ -10360,20 +10410,20 @@

    Source code in beancount/parser/options.py -
    def options_validate_processing_mode(value):
    -    """Validate the options processing mode.
    -
    -    Args:
    -      value: A string, the value provided as option.
    -    Returns:
    -      The new value, converted, if the conversion is successful.
    -    Raises:
    -      ValueError: If the value is invalid.
    -    """
    -    if value not in ('raw', 'default'):
    -        raise ValueError("Invalid value '{}'".format(value))
    -    return value
    -
    +
    def options_validate_processing_mode(value):
    +    """Validate the options processing mode.
    +
    +    Args:
    +      value: A string, the value provided as option.
    +    Returns:
    +      The new value, converted, if the conversion is successful.
    +    Raises:
    +      ValueError: If the value is invalid.
    +    """
    +    if value not in ("raw", "default"):
    +        raise ValueError("Invalid value '{}'".format(value))
    +    return value
    +
    @@ -10386,7 +10436,7 @@

    -beancount.parser.options.options_validate_tolerance(value) +beancount.parser.options.options_validate_tolerance(value)

    @@ -10445,18 +10495,18 @@

    Source code in beancount/parser/options.py -
    def options_validate_tolerance(value):
    -    """Validate the tolerance option.
    -
    -    Args:
    -      value: A string, the value provided as option.
    -    Returns:
    -      The new value, converted, if the conversion is successful.
    -    Raises:
    -      ValueError: If the value is invalid.
    -    """
    -    return D(value)
    -
    +
    def options_validate_tolerance(value):
    +    """Validate the tolerance option.
    +
    +    Args:
    +      value: A string, the value provided as option.
    +    Returns:
    +      The new value, converted, if the conversion is successful.
    +    Raises:
    +      ValueError: If the value is invalid.
    +    """
    +    return D(value)
    +
    @@ -10469,7 +10519,7 @@

    -beancount.parser.options.options_validate_tolerance_map(value) +beancount.parser.options.options_validate_tolerance_map(value)

    @@ -10528,24 +10578,24 @@

    Source code in beancount/parser/options.py -
    def options_validate_tolerance_map(value):
    -    """Validate an option with a map of currency/tolerance pairs in a string.
    -
    -    Args:
    -      value: A string, the value provided as option.
    -    Returns:
    -      The new value, converted, if the conversion is successful.
    -    Raises:
    -      ValueError: If the value is invalid.
    -    """
    -    # Process the setting of a key-value, whereby the value is a Decimal
    -    # representation.
    -    match = re.match('(.*):(.*)', value)
    -    if not match:
    -        raise ValueError("Invalid value '{}'".format(value))
    -    currency, tolerance_str = match.groups()
    -    return (currency, D(tolerance_str))
    -
    +
    def options_validate_tolerance_map(value):
    +    """Validate an option with a map of currency/tolerance pairs in a string.
    +
    +    Args:
    +      value: A string, the value provided as option.
    +    Returns:
    +      The new value, converted, if the conversion is successful.
    +    Raises:
    +      ValueError: If the value is invalid.
    +    """
    +    # Process the setting of a key-value, whereby the value is a Decimal
    +    # representation.
    +    match = re.match("(.*):(.*)", value)
    +    if not match:
    +        raise ValueError("Invalid value '{}'".format(value))
    +    currency, tolerance_str = match.groups()
    +    return (currency, D(tolerance_str))
    +
    @@ -10672,7 +10722,7 @@

    -beancount.parser.parser.is_entry_incomplete(entry) +beancount.parser.parser.is_entry_incomplete(entry)

    @@ -10715,19 +10765,19 @@

    Source code in beancount/parser/parser.py -
    def is_entry_incomplete(entry):
    -    """Detect the presence of elided amounts in Transactions.
    -
    -    Args:
    -      entries: A directive.
    -    Returns:
    -      A boolean, true if there are some missing portions of any postings found.
    -    """
    -    if isinstance(entry, data.Transaction):
    -        if any(is_posting_incomplete(posting) for posting in entry.postings):
    -            return True
    -    return False
    -
    +
    def is_entry_incomplete(entry):
    +    """Detect the presence of elided amounts in Transactions.
    +
    +    Args:
    +      entries: A directive.
    +    Returns:
    +      A boolean, true if there are some missing portions of any postings found.
    +    """
    +    if isinstance(entry, data.Transaction):
    +        if any(is_posting_incomplete(posting) for posting in entry.postings):
    +            return True
    +    return False
    +
    @@ -10740,7 +10790,7 @@

    -beancount.parser.parser.is_posting_incomplete(posting) +beancount.parser.parser.is_posting_incomplete(posting)

    @@ -10784,33 +10834,35 @@

    Source code in beancount/parser/parser.py -
    def is_posting_incomplete(posting):
    -    """Detect the presence of any elided amounts in a Posting.
    -
    -    If any of the possible amounts are missing, this returns True.
    -
    -    Args:
    -      entries: A directive.
    -    Returns:
    -      A boolean, true if there are some missing portions of any postings found.
    -    """
    -    units = posting.units
    -    if (units is MISSING or
    -        units.number is MISSING or
    -        units.currency is MISSING):
    -        return True
    -    price = posting.price
    -    if (price is MISSING or
    -        price is not None and (price.number is MISSING or
    -                               price.currency is MISSING)):
    -        return True
    -    cost = posting.cost
    -    if cost is not None and (cost.number_per is MISSING or
    -                             cost.number_total is MISSING or
    -                             cost.currency is MISSING):
    -        return True
    -    return False
    -
    +
    def is_posting_incomplete(posting):
    +    """Detect the presence of any elided amounts in a Posting.
    +
    +    If any of the possible amounts are missing, this returns True.
    +
    +    Args:
    +      entries: A directive.
    +    Returns:
    +      A boolean, true if there are some missing portions of any postings found.
    +    """
    +    units = posting.units
    +    if units is MISSING or units.number is MISSING or units.currency is MISSING:
    +        return True
    +    price = posting.price
    +    if (
    +        price is MISSING
    +        or price is not None
    +        and (price.number is MISSING or price.currency is MISSING)
    +    ):
    +        return True
    +    cost = posting.cost
    +    if cost is not None and (
    +        cost.number_per is MISSING
    +        or cost.number_total is MISSING
    +        or cost.currency is MISSING
    +    ):
    +        return True
    +    return False
    +
    @@ -10823,7 +10875,7 @@

    -beancount.parser.parser.parse_doc(expect_errors=False, allow_incomplete=False) +beancount.parser.parser.parse_doc(expect_errors=False, allow_incomplete=False)

    @@ -10875,69 +10927,70 @@

    Source code in beancount/parser/parser.py -
    def parse_doc(expect_errors=False, allow_incomplete=False):
    -    """Factory of decorators that parse the function's docstring as an argument.
    -
    -    Note that the decorators thus generated only run the parser on the tests,
    -    not the loader, so is no validation, balance checks, nor plugins applied to
    -    the parsed text.
    -
    -    Args:
    -      expect_errors: A boolean or None, with the following semantics,
    -        True: Expect errors and fail if there are none.
    -        False: Expect no errors and fail if there are some.
    -        None: Do nothing, no check.
    -      allow_incomplete: A boolean, if true, allow incomplete input. Otherwise
    -        barf if the input would require interpolation. The default value is set
    -        not to allow it because we want to minimize the features tests depend on.
    -    Returns:
    -      A decorator for test functions.
    -    """
    -    def decorator(fun):
    -        """A decorator that parses the function's docstring as an argument.
    -
    -        Args:
    -          fun: the function object to be decorated.
    -        Returns:
    -          A decorated test function.
    -        """
    -        filename = inspect.getfile(fun)
    -        lines, lineno = inspect.getsourcelines(fun)
    -
    -        # decorator line + function definition line (I realize this is largely
    -        # imperfect, but it's only for reporting in our tests) - empty first line
    -        # stripped away.
    -        lineno += 1
    -
    -        @functools.wraps(fun)
    -        def wrapper(self):
    -            assert fun.__doc__ is not None, (
    -                "You need to insert a docstring on {}".format(fun.__name__))
    -            entries, errors, options_map = parse_string(fun.__doc__,
    -                                                        report_filename=filename,
    -                                                        report_firstline=lineno,
    -                                                        dedent=True)
    -
    -            if not allow_incomplete and any(is_entry_incomplete(entry)
    -                                            for entry in entries):
    -                self.fail("parse_doc() may not use interpolation.")
    -
    -            if expect_errors is not None:
    -                if expect_errors is False and errors:
    -                    oss = io.StringIO()
    -                    printer.print_errors(errors, file=oss)
    -                    self.fail("Unexpected errors found:\n{}".format(oss.getvalue()))
    -                elif expect_errors is True and not errors:
    -                    self.fail("Expected errors, none found:")
    -
    -            return fun(self, entries, errors, options_map)
    -
    -        wrapper.__input__ = wrapper.__doc__
    -        wrapper.__doc__ = None
    -        return wrapper
    -
    -    return decorator
    -
    +
    def parse_doc(expect_errors=False, allow_incomplete=False):
    +    """Factory of decorators that parse the function's docstring as an argument.
    +
    +    Note that the decorators thus generated only run the parser on the tests,
    +    not the loader, so is no validation, balance checks, nor plugins applied to
    +    the parsed text.
    +
    +    Args:
    +      expect_errors: A boolean or None, with the following semantics,
    +        True: Expect errors and fail if there are none.
    +        False: Expect no errors and fail if there are some.
    +        None: Do nothing, no check.
    +      allow_incomplete: A boolean, if true, allow incomplete input. Otherwise
    +        barf if the input would require interpolation. The default value is set
    +        not to allow it because we want to minimize the features tests depend on.
    +    Returns:
    +      A decorator for test functions.
    +    """
    +
    +    def decorator(fun):
    +        """A decorator that parses the function's docstring as an argument.
    +
    +        Args:
    +          fun: the function object to be decorated.
    +        Returns:
    +          A decorated test function.
    +        """
    +        filename = inspect.getfile(fun)
    +        lines, lineno = inspect.getsourcelines(fun)
    +
    +        # Skip over decorator invocation and function definition. This
    +        # is imperfect as it assumes that each consumes exactly one
    +        # line, but this is by far the most common case, and this is
    +        # mainly used in test, thus it is good enough.
    +        lineno += 2
    +
    +        @functools.wraps(fun)
    +        def wrapper(self):
    +            assert fun.__doc__ is not None, "You need to insert a docstring on {}".format(
    +                fun.__name__
    +            )
    +            entries, errors, options_map = parse_string(
    +                fun.__doc__, report_filename=filename, report_firstline=lineno, dedent=True
    +            )
    +
    +            if not allow_incomplete and any(
    +                is_entry_incomplete(entry) for entry in entries
    +            ):
    +                self.fail("parse_doc() may not use interpolation.")
    +
    +            if expect_errors is not None:
    +                if expect_errors is False and errors:
    +                    oss = io.StringIO()
    +                    printer.print_errors(errors, file=oss)
    +                    self.fail("Unexpected errors found:\n{}".format(oss.getvalue()))
    +                elif expect_errors is True and not errors:
    +                    self.fail("Expected errors, none found:")
    +
    +            return fun(self, entries, errors, options_map)
    +
    +        return wrapper
    +
    +    return decorator
    +
    @@ -10950,7 +11003,7 @@

    -beancount.parser.parser.parse_file(filename, **kw) +beancount.parser.parser.parse_file(file, report_filename=None, report_firstline=1, encoding=None, debug=False, **kw)

    @@ -10970,7 +11023,7 @@

    Parameters:
      -
    • filename – the name of the file to be parsed.

    • +
    • file – file object or path to the file to be parsed.

    • kw – a dict of keywords to be applied to the C parser.

    @@ -10998,24 +11051,36 @@

    Source code in beancount/parser/parser.py -
    def parse_file(filename, **kw):
    -    """Parse a beancount input file and return Ledger with the list of
    -    transactions and tree of accounts.
    -
    -    Args:
    -      filename: the name of the file to be parsed.
    -      kw: a dict of keywords to be applied to the C parser.
    -    Returns:
    -      A tuple of (
    -        list of entries parsed in the file,
    -        list of errors that were encountered during parsing, and
    -        a dict of the option values that were parsed from the file.)
    -    """
    -    abs_filename = path.abspath(filename) if filename else None
    -    builder = grammar.Builder(abs_filename)
    -    _parser.parse_file(filename, builder, **kw)
    -    return builder.finalize()
    -
    +
    def parse_file(
    +    file, report_filename=None, report_firstline=1, encoding=None, debug=False, **kw
    +):
    +    """Parse a beancount input file and return Ledger with the list of
    +    transactions and tree of accounts.
    +
    +    Args:
    +      file: file object or path to the file to be parsed.
    +      kw: a dict of keywords to be applied to the C parser.
    +    Returns:
    +      A tuple of (
    +        list of entries parsed in the file,
    +        list of errors that were encountered during parsing, and
    +        a dict of the option values that were parsed from the file.)
    +    """
    +    if encoding is not None and codecs.lookup(encoding).name != "utf-8":
    +        raise ValueError("Only UTF-8 encoded files are supported.")
    +    with contextlib.ExitStack() as ctx:
    +        if file == "-":
    +            file = sys.stdin.buffer
    +        # It would be more appropriate here to check for io.RawIOBase but
    +        # that does not work for io.BytesIO despite it implementing the
    +        # readinto() method.
    +        elif not isinstance(file, io.IOBase):
    +            file = ctx.enter_context(open(file, "rb"))
    +        builder = grammar.Builder()
    +        parser = _parser.Parser(builder, debug=debug)
    +        parser.parse(file, filename=report_filename, lineno=report_firstline, **kw)
    +    return builder.finalize()
    +
    @@ -11028,7 +11093,7 @@

    -beancount.parser.parser.parse_many(string, level=0) +beancount.parser.parser.parse_many(string, level=0)

    @@ -11088,28 +11153,28 @@

    Source code in beancount/parser/parser.py -
    def parse_many(string, level=0):
    -    """Parse a string with a snippet of Beancount input and replace vars from caller.
    -
    -    Args:
    -      string: A string with some Beancount input.
    -      level: The number of extra stacks to ignore.
    -    Returns:
    -      A list of entries.
    -    Raises:
    -      AssertionError: If there are any errors.
    -    """
    -    # Get the locals in the stack for the callers and produce the final text.
    -    frame = inspect.stack()[level+1]
    -    varkwds = frame[0].f_locals
    -    input_string = textwrap.dedent(string.format(**varkwds))
    -
    -    # Parse entries and check there are no errors.
    -    entries, errors, __ = parse_string(input_string)
    -    assert not errors
    -
    -    return entries
    -
    +
    def parse_many(string, level=0):
    +    """Parse a string with a snippet of Beancount input and replace vars from caller.
    +
    +    Args:
    +      string: A string with some Beancount input.
    +      level: The number of extra stacks to ignore.
    +    Returns:
    +      A list of entries.
    +    Raises:
    +      AssertionError: If there are any errors.
    +    """
    +    # Get the locals in the stack for the callers and produce the final text.
    +    frame = inspect.stack()[level + 1]
    +    varkwds = frame[0].f_locals
    +    input_string = textwrap.dedent(string.format(**varkwds))
    +
    +    # Parse entries and check there are no errors.
    +    entries, errors, __ = parse_string(input_string)
    +    assert not errors
    +
    +    return entries
    +
    @@ -11122,7 +11187,7 @@

    -beancount.parser.parser.parse_one(string) +beancount.parser.parser.parse_one(string)

    @@ -11182,21 +11247,21 @@

    Source code in beancount/parser/parser.py -
    def parse_one(string):
    -    """Parse a string with single Beancount directive and replace vars from caller.
    -
    -    Args:
    -      string: A string with some Beancount input.
    -      level: The number of extra stacks to ignore.
    -    Returns:
    -      A list of entries.
    -    Raises:
    -      AssertionError: If there are any errors.
    -    """
    -    entries = parse_many(string, level=1)
    -    assert len(entries) == 1
    -    return entries[0]
    -
    +
    def parse_one(string):
    +    """Parse a string with single Beancount directive and replace vars from caller.
    +
    +    Args:
    +      string: A string with some Beancount input.
    +      level: The number of extra stacks to ignore.
    +    Returns:
    +      A list of entries.
    +    Raises:
    +      AssertionError: If there are any errors.
    +    """
    +    entries = parse_many(string, level=1)
    +    assert len(entries) == 1
    +    return entries[0]
    +
    @@ -11209,7 +11274,7 @@

    -beancount.parser.parser.parse_string(string, report_filename=None, **kw) +beancount.parser.parser.parse_string(string, report_filename=None, dedent=False, **kw)

    @@ -11233,8 +11298,8 @@

  • report_filename – A string, the source filename from which this string has been extracted, if any. This is stored in the metadata of the parsed entries.

  • -
  • **kw – See parse.c. This function parses out 'dedent' which removes -whitespace from the front of the text (default is False).

  • +
  • dedent – Whether to run textwrap.dedent() on the string before parsing.

  • +
  • **kw – See parse.c.

  • @@ -11258,26 +11323,29 @@

    Source code in beancount/parser/parser.py -
    def parse_string(string, report_filename=None, **kw):
    -    """Parse a beancount input file and return Ledger with the list of
    -    transactions and tree of accounts.
    -
    -    Args:
    -      string: A string, the contents to be parsed instead of a file's.
    -      report_filename: A string, the source filename from which this string
    -        has been extracted, if any. This is stored in the metadata of the
    -        parsed entries.
    -      **kw: See parse.c. This function parses out 'dedent' which removes
    -        whitespace from the front of the text (default is False).
    -    Return:
    -      Same as the output of parse_file().
    -    """
    -    if kw.pop('dedent', None):
    -        string = textwrap.dedent(string)
    -    builder = grammar.Builder(report_filename or '<string>')
    -    _parser.parse_string(string, builder, report_filename=report_filename, **kw)
    -    return builder.finalize()
    -
    +
    def parse_string(string, report_filename=None, dedent=False, **kw):
    +    """Parse a beancount input file and return Ledger with the list of
    +    transactions and tree of accounts.
    +
    +    Args:
    +      string: A string, the contents to be parsed instead of a file's.
    +      report_filename: A string, the source filename from which this string
    +        has been extracted, if any. This is stored in the metadata of the
    +        parsed entries.
    +      dedent: Whether to run textwrap.dedent() on the string before parsing.
    +      **kw: See parse.c.
    +    Return:
    +      Same as the output of parse_file().
    +    """
    +    if dedent:
    +        string = textwrap.dedent(string)
    +    if isinstance(string, str):
    +        string = string.encode("utf8")
    +    if report_filename is None:
    +        report_filename = "<string>"
    +    file = io.BytesIO(string)
    +    return parse_file(file, report_filename=report_filename, **kw)
    +
    @@ -11365,6 +11433,17 @@

    An integer, the minimum width to leave for the account name.

    + + prefix + +

    User-specific prefix for custom indentation (for Fava).

    + + + stringify_invalid_types + +

    If a metadata value is invalid, force a conversion to +string for printout.

    + @@ -11399,7 +11478,7 @@

    -beancount.parser.printer.EntryPrinter.__call__(self, obj) +beancount.parser.printer.EntryPrinter.__call__(self, obj) special @@ -11445,19 +11524,23 @@

    Source code in beancount/parser/printer.py -
    def __call__(self, obj):
    -    """Render a directive.
    -
    -    Args:
    -      obj: The directive to be rendered.
    -    Returns:
    -      A string, the rendered directive.
    -    """
    -    oss = io.StringIO()
    -    method = getattr(self, obj.__class__.__name__)
    -    method(obj, oss)
    -    return oss.getvalue()
    -
    +
    def __call__(self, obj):
    +    """Render a directive.
    +
    +    Args:
    +      obj: The directive to be rendered.
    +    Returns:
    +      A string, the rendered directive.
    +    """
    +    oss = io.StringIO()
    +
    +    # We write optional entry source for every entry type, hence writing it here
    +    self.write_entry_source(obj.meta, oss, prefix="")
    +
    +    method = getattr(self, obj.__class__.__name__)
    +    method(obj, oss)
    +    return oss.getvalue()
    +
    @@ -11471,7 +11554,7 @@

    -beancount.parser.printer.EntryPrinter.render_posting_strings(self, posting) +beancount.parser.printer.EntryPrinter.render_posting_strings(self, posting)

    @@ -11519,40 +11602,105 @@

    Source code in beancount/parser/printer.py -
    def render_posting_strings(self, posting):
    -    """This renders the three components of a posting: the account and its optional
    -    posting flag, the position, and finally, the weight of the position. The
    -    purpose is to align these in the caller.
    -
    -    Args:
    -      posting: An instance of Posting, the posting to render.
    -    Returns:
    -      A tuple of
    -        flag_account: A string, the account name including the flag.
    -        position_str: A string, the rendered position string.
    -        weight_str: A string, the rendered weight of the posting.
    -    """
    -    # Render a string of the flag and the account.
    -    flag = '{} '.format(posting.flag) if posting.flag else ''
    -    flag_account = flag + posting.account
    -
    -    # Render a string with the amount and cost and optional price, if
    -    # present. Also render a string with the weight.
    -    weight_str = ''
    -    if isinstance(posting.units, amount.Amount):
    -        position_str = position.to_string(posting, self.dformat)
    -        # Note: we render weights at maximum precision, for debugging.
    -        if posting.cost is None or (isinstance(posting.cost, position.Cost) and
    -                                    isinstance(posting.cost.number, Decimal)):
    -            weight_str = str(convert.get_weight(posting))
    -    else:
    -        position_str = ''
    -
    -    if posting.price is not None:
    -        position_str += ' @ {}'.format(posting.price.to_string(self.dformat_max))
    -
    -    return flag_account, position_str, weight_str
    -
    +
    def render_posting_strings(self, posting):
    +    """This renders the three components of a posting: the account and its optional
    +    posting flag, the position, and finally, the weight of the position. The
    +    purpose is to align these in the caller.
    +
    +    Args:
    +      posting: An instance of Posting, the posting to render.
    +    Returns:
    +      A tuple of
    +        flag_account: A string, the account name including the flag.
    +        position_str: A string, the rendered position string.
    +        weight_str: A string, the rendered weight of the posting.
    +    """
    +    # Render a string of the flag and the account.
    +    flag = "{} ".format(render_flag(posting.flag)) if posting.flag else ""
    +    flag_account = flag + posting.account
    +
    +    # Render a string with the amount and cost and optional price, if
    +    # present. Also render a string with the weight.
    +    weight_str = ""
    +    if isinstance(posting.units, amount.Amount):
    +        position_str = position.to_string(posting, self.dformat)
    +        # Note: we render weights at maximum precision, for debugging.
    +        if posting.cost is None or (
    +            isinstance(posting.cost, position.Cost)
    +            and isinstance(posting.cost.number, Decimal)
    +        ):
    +            weight_str = str(convert.get_weight(posting))
    +    else:
    +        position_str = ""
    +
    +    if posting.price is not None:
    +        position_str += " @ {}".format(posting.price.to_string(self.dformat_max))
    +
    +    return flag_account, position_str, weight_str
    +
    + + + + + + + +
    + + + +

    +beancount.parser.printer.EntryPrinter.write_entry_source(self, meta, oss, prefix=None) + + +

    + +
    + +

    Write source file and line number in a format interpretable as a message +location for Emacs, VSCode or other editors. As this is for +"debugging" purposes, this information will be commented out by a +semicolon.

    + + + + + + + + + + + + +
    Parameters: +
      +
    • meta – A dict that contains the metadata for this directive.

    • +
    • oss – A file object to write to.

    • +
    • prefix – User-specific prefix for custom indentation

    • +
    +
    +
    + Source code in beancount/parser/printer.py +
    def write_entry_source(self, meta, oss, prefix=None):
    +    """Write source file and line number in a format interpretable as a message
    +    location for Emacs, VSCode or other editors. As this is for
    +    "debugging" purposes, this information will be commented out by a
    +    semicolon.
    +
    +    Args:
    +      meta: A dict that contains the metadata for this directive.
    +      oss: A file object to write to.
    +      prefix: User-specific prefix for custom indentation
    +    """
    +    if not self.write_source:
    +        return
    +
    +    if prefix is None:
    +        prefix = self.prefix
    +
    +    oss.write("{}; source: {}\n".format(prefix, render_source(meta)))
    +
    @@ -11565,7 +11713,7 @@

    -beancount.parser.printer.EntryPrinter.write_metadata(self, meta, oss, prefix=None) +beancount.parser.printer.EntryPrinter.write_metadata(self, meta, oss, prefix=None)

    @@ -11593,35 +11741,44 @@

    Source code in beancount/parser/printer.py -
    def write_metadata(self, meta, oss, prefix=None):
    -    """Write metadata to the file object, excluding filename and line number.
    -
    -    Args:
    -      meta: A dict that contains the metadata for this directive.
    -      oss: A file object to write to.
    -    """
    -    if meta is None:
    -        return
    -    if prefix is None:
    -        prefix = self.prefix
    -    for key, value in sorted(meta.items()):
    -        if key not in self.META_IGNORE:
    -            value_str = None
    -            if isinstance(value, str):
    -                value_str = '"{}"'.format(misc_utils.escape_string(value))
    -            elif isinstance(value, (Decimal, datetime.date, amount.Amount)):
    -                value_str = str(value)
    -            elif isinstance(value, bool):
    -                value_str = 'TRUE' if value else 'FALSE'
    -            elif isinstance(value, (dict, inventory.Inventory)):
    -                pass # Ignore dicts, don't print them out.
    -            elif value is None:
    -                value_str = ''  # Render null metadata as empty, on purpose.
    -            else:
    -                raise ValueError("Unexpected value: '{!r}'".format(value))
    -            if value_str is not None:
    -                oss.write("{}{}: {}\n".format(prefix, key, value_str))
    -
    +
    def write_metadata(self, meta, oss, prefix=None):
    +    """Write metadata to the file object, excluding filename and line number.
    +
    +    Args:
    +      meta: A dict that contains the metadata for this directive.
    +      oss: A file object to write to.
    +    """
    +    if meta is None:
    +        return
    +    if prefix is None:
    +        prefix = self.prefix
    +
    +    # Note: meta.items() is assumed stable from 3.7 onwards; we're not sorting
    +    # on purpose in order to keep the original insertion order in print.
    +    for key, value in meta.items():
    +        if key not in self.META_IGNORE and not key.startswith("__"):
    +            value_str = None
    +            if isinstance(value, str):
    +                value_str = '"{}"'.format(misc_utils.escape_string(value))
    +            elif isinstance(value, (Decimal, datetime.date, amount.Amount, enum.Enum)):
    +                value_str = str(value)
    +            elif isinstance(value, bool):
    +                value_str = "TRUE" if value else "FALSE"
    +            elif isinstance(value, (dict, inventory.Inventory)):
    +                pass  # Ignore dicts, don't print them out.
    +            elif value is None:
    +                value_str = ""  # Render null metadata as empty, on purpose.
    +            else:
    +                if self.stringify_invalid_types:
    +                    # This is only intended to be used during development,
    +                    # when debugging for custom values of data types
    +                    # attached directly and not coming from the parser.
    +                    value_str = str(value)
    +                else:
    +                    raise ValueError("Unexpected value: '{!r}'".format(value))
    +            if value_str is not None:
    +                oss.write("{}{}: {}\n".format(prefix, key, value_str))
    +

    @@ -11645,7 +11802,7 @@

    -beancount.parser.printer.align_position_strings(strings) +beancount.parser.printer.align_position_strings(strings)

    @@ -11701,68 +11858,69 @@

    Source code in beancount/parser/printer.py -
    def align_position_strings(strings):
    -    """A helper used to align rendered amounts positions to their first currency
    -    character (an uppercase letter). This class accepts a list of rendered
    -    positions and calculates the necessary width to render them stacked in a
    -    column so that the first currency word aligns. It does not go beyond that
    -    (further currencies, e.g. for the price or cost, are not aligned).
    -
    -    This is perhaps best explained with an example. The following positions will
    -    be aligned around the column marked with '^':
    -
    -              45 HOOL {504.30 USD}
    -               4 HOOL {504.30 USD, 2014-11-11}
    -            9.95 USD
    -       -22473.32 CAD @ 1.10 USD
    -                 ^
    -
    -    Strings without a currency character will be rendered flush left.
    -
    -    Args:
    -      strings: A list of rendered position or amount strings.
    -    Returns:
    -      A pair of a list of aligned strings and the width of the aligned strings.
    -    """
    -    # Maximum length before the alignment character.
    -    max_before = 0
    -    # Maximum length after the alignment character.
    -    max_after = 0
    -    # Maximum length of unknown strings.
    -    max_unknown = 0
    -
    -    string_items = []
    -    search = re.compile('[A-Z]').search
    -    for string in strings:
    -        match = search(string)
    -        if match:
    -            index = match.start()
    -            if index != 0:
    -                max_before = max(index, max_before)
    -                max_after = max(len(string) - index, max_after)
    -                string_items.append((index, string))
    -                continue
    -        # else
    -        max_unknown = max(len(string), max_unknown)
    -        string_items.append((None, string))
    -
    -    # Compute formatting string.
    -    max_total = max(max_before + max_after, max_unknown)
    -    max_after_prime = max_total - max_before
    -    fmt = "{{:>{0}}}{{:{1}}}".format(max_before, max_after_prime).format
    -    fmt_unknown = "{{:<{0}}}".format(max_total).format
    -
    -    # Align the strings and return them.
    -    aligned_strings = []
    -    for index, string in string_items:
    -        if index is not None:
    -            string = fmt(string[:index], string[index:])
    -        else:
    -            string = fmt_unknown(string)
    -        aligned_strings.append(string)
    -
    -    return aligned_strings, max_total
    -
    +
    def align_position_strings(strings):
    +    """A helper used to align rendered amounts positions to their first currency
    +    character (an uppercase letter). This class accepts a list of rendered
    +    positions and calculates the necessary width to render them stacked in a
    +    column so that the first currency word aligns. It does not go beyond that
    +    (further currencies, e.g. for the price or cost, are not aligned).
    +
    +    This is perhaps best explained with an example. The following positions will
    +    be aligned around the column marked with '^':
    +
    +              45 HOOL {504.30 USD}
    +               4 HOOL {504.30 USD, 2014-11-11}
    +            9.95 USD
    +       -22473.32 CAD @ 1.10 USD
    +                 ^
    +
    +    Strings without a currency character will be rendered flush left.
    +
    +    Args:
    +      strings: A list of rendered position or amount strings.
    +    Returns:
    +      A pair of a list of aligned strings and the width of the aligned strings.
    +    """
    +    # Maximum length before the alignment character.
    +    max_before = 0
    +    # Maximum length after the alignment character.
    +    max_after = 0
    +    # Maximum length of unknown strings.
    +    max_unknown = 0
    +
    +    string_items = []
    +    search = re.compile("[A-Z]").search
    +    for string in strings:
    +        match = search(string)
    +        if match:
    +            index = match.start()
    +            if index != 0:
    +                max_before = max(index, max_before)
    +                max_after = max(len(string) - index, max_after)
    +                string_items.append((index, string))
    +                continue
    +        # else
    +        max_unknown = max(len(string), max_unknown)
    +        string_items.append((None, string))
    +
    +    # Compute formatting string.
    +    max_total = max(max_before + max_after, max_unknown)
    +    max_after_prime = max_total - max_before
    +    fmt = "{{:>{0}}}{{:{1}}}".format(max_before, max_after_prime).format
    +    fmt_unknown = "{{:<{0}}}".format(max_total).format
    +
    +    # Align the strings and return them.
    +
    +    aligned_strings = []
    +    for index, string in string_items:
    +        if index is not None:
    +            string = fmt(string[:index], string[index:])
    +        else:
    +            string = fmt_unknown(string)
    +        aligned_strings.append(string)
    +
    +    return aligned_strings, max_total
    +
    @@ -11775,7 +11933,7 @@

    -beancount.parser.printer.format_entry(entry, dcontext=None, render_weights=False, prefix=None) +beancount.parser.printer.format_entry(entry, dcontext=None, render_weights=False, prefix=None, write_source=False)

    @@ -11797,6 +11955,11 @@

  • entry – An entry instance.

  • dcontext – An instance of DisplayContext used to format the numbers.

  • render_weights – A boolean, true to render the weights for debugging.

  • +
  • write_source – If true a source file and line number will be written for +each entry in a format interpretable as a message location for Emacs, +VSCode or other editors. As this is for +"debugging" purposes, this information will be commented out by a +semicolon.

  • @@ -11820,18 +11983,27 @@

    Source code in beancount/parser/printer.py -
    def format_entry(entry, dcontext=None, render_weights=False, prefix=None):
    -    """Format an entry into a string in the same input syntax the parser accepts.
    -
    -    Args:
    -      entry: An entry instance.
    -      dcontext: An instance of DisplayContext used to format the numbers.
    -      render_weights: A boolean, true to render the weights for debugging.
    -    Returns:
    -      A string, the formatted entry.
    -    """
    -    return EntryPrinter(dcontext, render_weights, prefix=prefix)(entry)
    -
    +
    def format_entry(
    +    entry, dcontext=None, render_weights=False, prefix=None, write_source=False
    +):
    +    """Format an entry into a string in the same input syntax the parser accepts.
    +
    +    Args:
    +      entry: An entry instance.
    +      dcontext: An instance of DisplayContext used to format the numbers.
    +      render_weights: A boolean, true to render the weights for debugging.
    +      write_source: If true a source file and line number will be written for
    +        each entry in a format interpretable as a message location for Emacs,
    +        VSCode or other editors. As this is for
    +        "debugging" purposes, this information will be commented out by a
    +        semicolon.
    +    Returns:
    +      A string, the formatted entry.
    +    """
    +    return EntryPrinter(dcontext, render_weights, prefix=prefix, write_source=write_source)(
    +        entry
    +    )
    +
    @@ -11844,7 +12016,7 @@

    -beancount.parser.printer.format_error(error) +beancount.parser.printer.format_error(error)

    @@ -11889,26 +12061,26 @@

    Source code in beancount/parser/printer.py -
    def format_error(error):
    -    """Given an error objects, return a formatted string for it.
    -
    -    Args:
    -      error: a namedtuple objects representing an error. It has to have an
    -        'entry' attribute that may be either a single directive object or a
    -        list of directive objects.
    -    Returns:
    -      A string, the errors rendered.
    -    """
    -    oss = io.StringIO()
    -    oss.write('{} {}\n'.format(render_source(error.source), error.message))
    -    if error.entry is not None:
    -        entries = error.entry if isinstance(error.entry, list) else [error.entry]
    -        error_string = '\n'.join(format_entry(entry) for entry in entries)
    -        oss.write('\n')
    -        oss.write(textwrap.indent(error_string, '   '))
    -        oss.write('\n')
    -    return oss.getvalue()
    -
    +
    def format_error(error):
    +    """Given an error objects, return a formatted string for it.
    +
    +    Args:
    +      error: a namedtuple objects representing an error. It has to have an
    +        'entry' attribute that may be either a single directive object or a
    +        list of directive objects.
    +    Returns:
    +      A string, the errors rendered.
    +    """
    +    oss = io.StringIO()
    +    oss.write("{} {}\n".format(render_source(error.source), error.message))
    +    if error.entry is not None:
    +        entries = error.entry if isinstance(error.entry, list) else [error.entry]
    +        error_string = "\n".join(format_entry(entry) for entry in entries)
    +        oss.write("\n")
    +        oss.write(textwrap.indent(error_string, "   "))
    +        oss.write("\n")
    +    return oss.getvalue()
    +
    @@ -11921,7 +12093,7 @@

    -beancount.parser.printer.print_entries(entries, dcontext=None, render_weights=False, file=None, prefix=None) +beancount.parser.printer.print_entries(entries, dcontext=None, render_weights=False, file=None, prefix=None, write_source=False)

    @@ -11944,6 +12116,11 @@

  • dcontext – An instance of DisplayContext used to format the numbers.

  • render_weights – A boolean, true to render the weights for debugging.

  • file – An optional file object to write the entries to.

  • +
  • prefix – User-specific prefix for custom indentation (for Fava).

  • +
  • write_source – If true a source file and line number will be written for +each entry in a format interpretable as a message location for Emacs, +VSCode or other editors. This is usefull for "debugging" peurposes, +especially in a multi-file setup

  • @@ -11951,36 +12128,48 @@

    Source code in beancount/parser/printer.py -
    def print_entries(entries, dcontext=None, render_weights=False, file=None, prefix=None):
    -    """A convenience function that prints a list of entries to a file.
    -
    -    Args:
    -      entries: A list of directives.
    -      dcontext: An instance of DisplayContext used to format the numbers.
    -      render_weights: A boolean, true to render the weights for debugging.
    -      file: An optional file object to write the entries to.
    -    """
    -    assert isinstance(entries, list), "Entries is not a list: {}".format(entries)
    -    output = file or (codecs.getwriter("utf-8")(sys.stdout.buffer)
    -                      if hasattr(sys.stdout, 'buffer') else
    -                      sys.stdout)
    -
    -    if prefix:
    -        output.write(prefix)
    -    previous_type = type(entries[0]) if entries else None
    -    eprinter = EntryPrinter(dcontext, render_weights)
    -    for entry in entries:
    -        # Insert a newline between transactions and between blocks of directives
    -        # of the same type.
    -        entry_type = type(entry)
    -        if (entry_type in (data.Transaction, data.Commodity) or
    -            entry_type is not previous_type):
    -            output.write('\n')
    -            previous_type = entry_type
    -
    -        string = eprinter(entry)
    -        output.write(string)
    -
    +
    def print_entries(
    +    entries, dcontext=None, render_weights=False, file=None, prefix=None, write_source=False
    +):
    +    """A convenience function that prints a list of entries to a file.
    +
    +    Args:
    +      entries: A list of directives.
    +      dcontext: An instance of DisplayContext used to format the numbers.
    +      render_weights: A boolean, true to render the weights for debugging.
    +      file: An optional file object to write the entries to.
    +      prefix: User-specific prefix for custom indentation (for Fava).
    +      write_source: If true a source file and line number will be written for
    +        each entry in a format interpretable as a message location for Emacs,
    +        VSCode or other editors. This is usefull for "debugging" peurposes,
    +        especially in a multi-file setup
    +    """
    +    assert isinstance(entries, list), "Entries is not a list: {}".format(entries)
    +    output = file or (
    +        codecs.getwriter("utf-8")(sys.stdout.buffer)
    +        if hasattr(sys.stdout, "buffer")
    +        else sys.stdout
    +    )
    +
    +    if prefix:
    +        output.write(prefix)
    +    previous_type = type(entries[0]) if entries else None
    +    eprinter = EntryPrinter(dcontext, render_weights, write_source=write_source)
    +    for entry in entries:
    +        # Insert a newline between transactions and between blocks of directives
    +        # of the same type.
    +        entry_type = type(entry)
    +        if (
    +            entry_type in (data.Transaction, data.Commodity)
    +            or entry_type is not previous_type
    +            or write_source
    +        ):
    +            output.write("\n")
    +            previous_type = entry_type
    +
    +        string = eprinter(entry)
    +        output.write(string)
    +
    @@ -11993,7 +12182,7 @@

    -beancount.parser.printer.print_entry(entry, dcontext=None, render_weights=False, file=None) +beancount.parser.printer.print_entry(entry, dcontext=None, render_weights=False, file=None, write_source=False)

    @@ -12016,6 +12205,10 @@

  • dcontext – An instance of DisplayContext used to format the numbers.

  • render_weights – A boolean, true to render the weights for debugging.

  • file – An optional file object to write the entries to.

  • +
  • write_source – If true a source file and line number will be written for +each entry in a format interpretable as a message location for Emacs, +VSCode or other editors. This is usefull for "debugging" purposes, +especially in a multi-file setup

  • @@ -12023,21 +12216,29 @@

    Source code in beancount/parser/printer.py -
    def print_entry(entry, dcontext=None, render_weights=False, file=None):
    -    """A convenience function that prints a single entry to a file.
    -
    -    Args:
    -      entry: A directive entry.
    -      dcontext: An instance of DisplayContext used to format the numbers.
    -      render_weights: A boolean, true to render the weights for debugging.
    -      file: An optional file object to write the entries to.
    -    """
    -    output = file or (codecs.getwriter("utf-8")(sys.stdout.buffer)
    -                      if hasattr(sys.stdout, 'buffer') else
    -                      sys.stdout)
    -    output.write(format_entry(entry, dcontext, render_weights))
    -    output.write('\n')
    -
    +
    def print_entry(entry, dcontext=None, render_weights=False, file=None, write_source=False):
    +    """A convenience function that prints a single entry to a file.
    +
    +    Args:
    +      entry: A directive entry.
    +      dcontext: An instance of DisplayContext used to format the numbers.
    +      render_weights: A boolean, true to render the weights for debugging.
    +      file: An optional file object to write the entries to.
    +      write_source: If true a source file and line number will be written for
    +        each entry in a format interpretable as a message location for Emacs,
    +        VSCode or other editors. This is usefull for "debugging" purposes,
    +        especially in a multi-file setup
    +    """
    +    # TODO(blais): DO remove this now, it's a huge annoyance not to be able to
    +    # print in-between other statements.
    +    output = file or (
    +        codecs.getwriter("utf-8")(sys.stdout.buffer)
    +        if hasattr(sys.stdout, "buffer")
    +        else sys.stdout
    +    )
    +    output.write(format_entry(entry, dcontext, render_weights, write_source=write_source))
    +    output.write("\n")
    +
    @@ -12050,7 +12251,7 @@

    -beancount.parser.printer.print_error(error, file=None) +beancount.parser.printer.print_error(error, file=None)

    @@ -12078,17 +12279,17 @@

    Source code in beancount/parser/printer.py -
    def print_error(error, file=None):
    -    """A convenience function that prints a single error to a file.
    -
    -    Args:
    -      error: An error object.
    -      file: An optional file object to write the errors to.
    -    """
    -    output = file or sys.stdout
    -    output.write(format_error(error))
    -    output.write('\n')
    -
    +
    def print_error(error, file=None):
    +    """A convenience function that prints a single error to a file.
    +
    +    Args:
    +      error: An error object.
    +      file: An optional file object to write the errors to.
    +    """
    +    output = file or sys.stdout
    +    output.write(format_error(error))
    +    output.write("\n")
    +
    @@ -12101,7 +12302,7 @@

    -beancount.parser.printer.print_errors(errors, file=None, prefix=None) +beancount.parser.printer.print_errors(errors, file=None, prefix=None)

    @@ -12129,20 +12330,49 @@

    Source code in beancount/parser/printer.py -
    def print_errors(errors, file=None, prefix=None):
    -    """A convenience function that prints a list of errors to a file.
    -
    -    Args:
    -      errors: A list of errors.
    -      file: An optional file object to write the errors to.
    -    """
    -    output = file or sys.stdout
    -    if prefix:
    -        output.write(prefix)
    -    for error in errors:
    -        output.write(format_error(error))
    -        output.write('\n')
    -
    +
    def print_errors(errors, file=None, prefix=None):
    +    """A convenience function that prints a list of errors to a file.
    +
    +    Args:
    +      errors: A list of errors.
    +      file: An optional file object to write the errors to.
    +    """
    +    output = file or sys.stdout
    +    if prefix:
    +        output.write(prefix)
    +    for error in errors:
    +        output.write(format_error(error))
    +        output.write("\n")
    +
    +
    + + + + + + +
    + + + +

    +beancount.parser.printer.render_flag(inflag) + + +

    + +
    + +

    Render a flag, which can be None, a symbol of a character to a string.

    + +
    + Source code in beancount/parser/printer.py +
    def render_flag(inflag: Optional[str]) -> str:
    +    """Render a flag, which can be None, a symbol of a character to a string."""
    +    if not inflag:
    +        return ""
    +    return inflag
    +
    @@ -12155,7 +12385,7 @@

    -beancount.parser.printer.render_source(meta) +beancount.parser.printer.render_source(meta)

    @@ -12200,18 +12430,145 @@

    Source code in beancount/parser/printer.py -
    def render_source(meta):
    -    """Render the source for errors in a way that it will be both detected by
    -    Emacs and align and rendered nicely.
    -
    -    Args:
    -      meta: A dict with the metadata.
    -    Returns:
    -      A string, rendered to be interpretable as a message location for Emacs or
    -      other editors.
    -    """
    -    return '{}:{:8}'.format(meta['filename'], '{}:'.format(meta['lineno']))
    -
    +
    def render_source(meta):
    +    """Render the source for errors in a way that it will be both detected by
    +    Emacs and align and rendered nicely.
    +
    +    Args:
    +      meta: A dict with the metadata.
    +    Returns:
    +      A string, rendered to be interpretable as a message location for Emacs or
    +      other editors.
    +    """
    +    return "{}:{:8}".format(meta["filename"], "{}:".format(meta["lineno"]))
    +
    +
    +

    + + + + + + + + + + + + + + + + +
    + + + +

    + beancount.parser.version + + + +

    + +
    + +

    Implement common options across all programs.

    + + + +
    + + + + + + + + + + + +
    + + + +

    +beancount.parser.version.compute_version_string(version, changeset, timestamp) + + +

    + +
    + +

    Compute a version string from the changeset and timestamp baked in the parser.

    + + + + + + + + + + + + +
    Parameters: +
      +
    • version – A string, the version number.

    • +
    • changeset – A string, a version control string identifying the commit of the version.

    • +
    • timestamp – An integer, the UNIX epoch timestamp of the changeset.

    • +
    +
    + + + + + + + + + + + +
    Returns: +
      +
    • A human-readable string for the version.

    • +
    +
    +
    + Source code in beancount/parser/version.py +
    def compute_version_string(version, changeset, timestamp):
    +    """Compute a version string from the changeset and timestamp baked in the parser.
    +
    +    Args:
    +      version: A string, the version number.
    +      changeset: A string, a version control string identifying the commit of the version.
    +      timestamp: An integer, the UNIX epoch timestamp of the changeset.
    +    Returns:
    +      A human-readable string for the version.
    +    """
    +    # Shorten changeset.
    +    if changeset:
    +        if re.match("hg:", changeset):
    +            changeset = changeset[:15]
    +        elif re.match("git:", changeset):
    +            changeset = changeset[:12]
    +
    +    # Convert timestamp to a date.
    +    date = None
    +    if timestamp > 0:
    +        date = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).date()
    +
    +    version = "Beancount {}".format(version)
    +    if changeset or date:
    +        version = "{} ({})".format(
    +            version, "; ".join(map(str, filter(None, [changeset, date])))
    +        )
    +
    +    return version
    +
    diff --git a/api_reference/beancount.plugins.html b/api_reference/beancount.plugins.html index cfe3ddd1..3bc551e5 100644 --- a/api_reference/beancount.plugins.html +++ b/api_reference/beancount.plugins.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -152,8 +154,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -172,46 +172,6 @@
  • -
  • book_conversions - -
  • check_average_cost
  • +
  • ConfigError + +
  • +
  • get_commodity_map_ex() +
  • validate_commodity_directives()
  • +
  • check_drained + +
  • +
  • close_tree + +
  • coherent_cost
  • -
  • divert_expenses - -
  • -
  • exclude_tag - -
  • -
  • fill_account - -
  • -
  • fix_payees - -
  • -
  • forecast - -
  • implicit_prices
  • -
  • ira_contribs - -
  • leafonly
  • -
  • mark_unverified - -
  • -
  • merge_meta - -
  • noduplicates
  • -
  • split_expenses - -
  • -
  • tag_pending - -
  • unique_prices
  • -
  • unrealized - -
  • - -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports
  • beancount.scripts
  • @@ -530,8 +398,6 @@
  • beancount.utils
  • -
  • beancount.web -
  • @@ -681,17 +547,16 @@

    -beancount.plugins.auto_accounts.auto_insert_open(entries, unused_options_map) +beancount.plugins.auto_accounts.auto_insert_open(entries, unused_options_map)

    -

    Insert implicitly defined prices from Transactions.

    -

    Explicit price entries are simply maintained in the output list. Prices from -postings with costs or with prices from Transaction entries are synthesized -as new Price entries in the list of entries output.

    +

    Insert Open directives for accounts not opened.

    +

    Open directives are inserted at the date of the first entry. Open directives +for unused accounts are removed.

    @@ -703,7 +568,7 @@

    Parameters:

    @@ -720,7 +585,7 @@

    Returns:

    @@ -729,40 +594,36 @@

    Source code in beancount/plugins/auto_accounts.py -
    def auto_insert_open(entries, unused_options_map):
    -    """Insert implicitly defined prices from Transactions.
    -
    -    Explicit price entries are simply maintained in the output list. Prices from
    -    postings with costs or with prices from Transaction entries are synthesized
    -    as new Price entries in the list of entries output.
    -
    -    Args:
    -      entries: A list of directives. We're interested only in the Transaction instances.
    -      unused_options_map: A parser options dict.
    -    Returns:
    -      A list of entries, possibly with more Price entries than before, and a
    -      list of errors.
    -    """
    -    opened_accounts = {entry.account
    -                       for entry in entries
    -                       if isinstance(entry, data.Open)}
    -
    -    new_entries = []
    -    accounts_first, _ = getters.get_accounts_use_map(entries)
    -    for index, (account, date_first_used) in enumerate(sorted(accounts_first.items())):
    -        if account not in opened_accounts:
    -            meta = data.new_metadata('<auto_accounts>', index)
    -            new_entries.append(data.Open(meta, date_first_used, account,
    -                                         None, None))
    -
    -    if new_entries:
    -        new_entries.extend(entries)
    -        new_entries.sort(key=data.entry_sortkey)
    -    else:
    -        new_entries = entries
    -
    -    return new_entries, []
    -
    +
    def auto_insert_open(entries, unused_options_map):
    +    """Insert Open directives for accounts not opened.
    +
    +    Open directives are inserted at the date of the first entry. Open directives
    +    for unused accounts are removed.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: A parser options dict.
    +    Returns:
    +      A list of entries, possibly with more Open entries than before, and a
    +      list of errors.
    +    """
    +    opened_accounts = {entry.account for entry in entries if isinstance(entry, data.Open)}
    +
    +    new_entries = []
    +    accounts_first, _ = getters.get_accounts_use_map(entries)
    +    for index, (account, date_first_used) in enumerate(sorted(accounts_first.items())):
    +        if account not in opened_accounts:
    +            meta = data.new_metadata("<auto_accounts>", index)
    +            new_entries.append(data.Open(meta, date_first_used, account, None, None))
    +
    +    if new_entries:
    +        new_entries.extend(entries)
    +        new_entries.sort(key=data.entry_sortkey)
    +    else:
    +        new_entries = entries
    +
    +    return new_entries, []
    +
    @@ -785,90 +646,24 @@

    - beancount.plugins.book_conversions +

    + beancount.plugins.check_average_cost -

    +
    -

    A plugin that automatically converts postings at price to postings held at -cost, applying an automatic booking algorithm in assigning the cost bases and -matching lots.

    -

    This plugin restricts itself to applying these transformations within a -particular account, which you provide. For each of those accounts, it also -requires a corresponding Income account to book the profit/loss of reducing -lots (i.e., sales):

    -
    plugin "beancount.plugins.book_conversions" "Assets:Bitcoin,Income:Bitcoin"
    -
    -

    Then, simply input the transactions with price conversion. We use "Bitcoins" in -this example, converting Bitcoin purchases that were carried out as currency -into maintaining these with cost basis, for tax reporting purposes:

    -
    2015-09-04 * "Buy some bitcoins"
    -  Assets:Bank          -1000.00 USD
    -  Assets:Bitcoin       4.333507 BTC @ 230.76 USD
    -
    -2015-09-05 * "Buy some more bitcoins at a different price"
    -  Assets:Bank          -1000.00 USD
    -  Assets:Bitcoin       4.345747 BTC @ 230.11 USD
    -
    -2015-09-20 * "Use (sell) some bitcoins"
    -  Assets:Bitcoin       -6.000000 BTC @ 230.50 USD
    -  Expenses:Something
    -
    -

    The result is that cost bases are inserted on augmenting lots:

    -
    2015-09-04 * "Buy some bitcoins"
    -  Assets:Bitcoin  4.333507 BTC {230.76 USD} @ 230.76 USD
    -  Assets:Bank     -1000.00 USD
    -
    -2015-09-05 * "Buy some more bitcoins at a different price"
    -  Assets:Bitcoin  4.345747 BTC {230.11 USD} @ 230.11 USD
    -  Assets:Bank     -1000.00 USD
    -
    -

    While on reducing lots, matching FIFO lots are automatically found and the -corresponding cost basis added:

    -
    2015-09-20 * "Use (sell) some bitcoins"
    -  Assets:Bitcoin          -4.333507 BTC {230.76 USD} @ 230.50 USD
    -  Assets:Bitcoin          -1.666493 BTC {230.11 USD} @ 230.50 USD
    -  Income:Bitcoin         0.47677955 USD
    -  Expenses:Something  1383.00000000 USD
    -
    -

    Note that multiple lots were required to fulfill the sale quantity here. As in -this example, this may result in multiple lots being created for a single one.

    -

    Finally, Beancount will eventually support booking methods built-in, but this is -a quick method that shows how to hack your own booking method via -transformations of the postings that run in a plugin.

    -

    Implementation notes:

    -
      -
    • -

      This code uses the FIFO method only for now. However, it would be very easy to - customize it to provide other booking methods, e.g. LIFO, or otherwise. This - will be added eventually, and I'm hoping to reuse the same inventory - abstractions that will be used to implement the fallback booking methods from - the booking proposal review (http://furius.ca/beancount/doc/proposal-booking).

      -
    • -
    • -

      Instead of keeping a list of (Position, Transaction) pairs for the pending - FIFO lots, we really ought to use a beancount.core.inventory.Inventory - instance. However, the class does not contain sufficient data to carry out - FIFO booking at the moment. A newer implementation, living in the "booking" - branch, does, and will be used in the future.

      -
    • -
    • -

      This code assumes that a positive number of units is an augmenting lot and a - reducing one has a negative number of units, though we never call them that - way on purpose (to eventually allow this code to handle short positions). This - is not strictly true; however, we would need an Inventory in order to figrue - this out. This will be done in the future and is not difficult to do.

      -
    • -
    -

    IMPORTANT:

    -

    This plugin was developed before the booking methods (FIFO, LIFO, and others) - were fully implemented in Beancount. It was built to answer a question on the - mailing-list about FIFO booking. You probably don't need to use them anymore. - Always prefer to use the native syntax instead of this.

    +

    A plugin that ensures cost basis is preserved in unbooked transactions.

    +

    This is intended to be used in accounts using the "NONE" booking method, to +manually ensure that the sum total of the cost basis of reducing legs matches +the average of what's in the account inventory. This is a partial first step +toward implementing the "AVERAGE" booking method. In other words, this plugins +provides assertions that will constrain you to approximate what the "AVERAGE" +booking method will do, manually, and not to leak too much cost basis through +unmatching bookings without checks. (Note the contrived context here: Ideally +the "NONE" booking method would simply not exist.)

    @@ -888,18 +683,18 @@

    -

    +

    -beancount.plugins.book_conversions.BookConversionError (tuple) +beancount.plugins.check_average_cost.MatchBasisError (tuple) -

    +
    -

    BookConversionError(source, message, entry)

    +

    MatchBasisError(source, message, entry)

    @@ -918,25 +713,25 @@

    -beancount.plugins.book_conversions.BookConversionError.__getnewargs__(self) +

    +beancount.plugins.check_average_cost.MatchBasisError.__getnewargs__(self) special -

    +

    Return self as a plain tuple. Used by copy and pickle.

    - Source code in beancount/plugins/book_conversions.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    + Source code in beancount/plugins/check_average_cost.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -948,19 +743,19 @@

    -beancount.plugins.book_conversions.BookConversionError.__new__(_cls, source, message, entry) +

    +beancount.plugins.check_average_cost.MatchBasisError.__new__(_cls, source, message, entry) special staticmethod -

    +
    -

    Create new instance of BookConversionError(source, message, entry)

    +

    Create new instance of MatchBasisError(source, message, entry)

    @@ -972,25 +767,25 @@

    -beancount.plugins.book_conversions.BookConversionError.__repr__(self) +

    +beancount.plugins.check_average_cost.MatchBasisError.__repr__(self) special -

    +

    Return a nicely formatted representation string

    - Source code in beancount/plugins/book_conversions.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    + Source code in beancount/plugins/check_average_cost.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -1008,2705 +803,189 @@

    +
    -

    - -beancount.plugins.book_conversions.ConfigError (tuple) - +

    +beancount.plugins.check_average_cost.validate_average_cost(entries, options_map, config_str=None) -

    +

    -

    ConfigError(source, message, entry)

    +

    Check that reducing legs on unbooked postings are near the average cost basis.

    + +
      -
    • entries – A list of directives. We're interested only in the Transaction instances.

    • +
    • entries – A list of directives.

    • unused_options_map – A parser options dict.

      -
    • A list of entries, possibly with more Price entries than before, and a +

    • A list of entries, possibly with more Open entries than before, and a list of errors.

    + + + + + + + + + + +
    Parameters: +
      +
    • entries – A list of directives.

    • +
    • unused_options_map – An options map.

    • +
    • config_str – The configuration as a string version of a float.

    • +
    +
    + + + + + + + + + + + +
    Returns: +
      +
    • A list of new errors, if any were found.

    • +
    +
    +
    + Source code in beancount/plugins/check_average_cost.py +
    def validate_average_cost(entries, options_map, config_str=None):
    +    """Check that reducing legs on unbooked postings are near the average cost basis.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +      config_str: The configuration as a string version of a float.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    # Initialize tolerance bounds.
    +    if config_str and config_str.strip():
    +        config_obj = eval(config_str, {}, {})
    +        if not isinstance(config_obj, float):
    +            raise RuntimeError(
    +                "Invalid configuration for check_average_cost: " "must be a float"
    +            )
    +        tolerance = config_obj
    +    else:
    +        tolerance = DEFAULT_TOLERANCE
    +    min_tolerance = D(1 - tolerance)
    +    max_tolerance = D(1 + tolerance)
    +
    +    errors = []
    +    ocmap = getters.get_account_open_close(entries)
    +    balances = collections.defaultdict(inventory.Inventory)
    +    for entry in entries:
    +        if isinstance(entry, Transaction):
    +            for posting in entry.postings:
    +                dopen = ocmap.get(posting.account, None)
    +                # Only process accounts with a NONE booking value.
    +                if dopen and dopen[0] and dopen[0].booking == Booking.NONE:
    +                    balance = balances[
    +                        (
    +                            posting.account,
    +                            posting.units.currency,
    +                            posting.cost.currency if posting.cost else None,
    +                        )
    +                    ]
    +                    if posting.units.number < ZERO:
    +                        average = balance.average().get_only_position()
    +                        if average is not None:
    +                            number = average.cost.number
    +                            min_valid = number * min_tolerance
    +                            max_valid = number * max_tolerance
    +                            if not (min_valid <= posting.cost.number <= max_valid):
    +                                errors.append(
    +                                    MatchBasisError(
    +                                        entry.meta,
    +                                        (
    +                                            "Cost basis on reducing posting is too far from "
    +                                            "the average cost ({} vs. {})".format(
    +                                                posting.cost.number, average.cost.number
    +                                            )
    +                                        ),
    +                                        entry,
    +                                    )
    +                                )
    +                    balance.add_position(posting)
    +    return entries, errors
    +
    +
    +
    +
    -
    +
    +
    + +
    -
    +

    + beancount.plugins.check_closing -

    -beancount.plugins.book_conversions.ConfigError.__getnewargs__(self) - - special - -

    +

    -

    Return self as a plain tuple. Used by copy and pickle.

    +

    A plugin that automatically inserts a balance check on a tagged closing posting.

    +

    Some postings are known to the user to be "closing trades", which means that the +resulting position of the instrument just after the trade should be zero. For +instance, this is the case for most ordinary options trading, only one lot of a +particular instrument is held, and eventually expires or gets sold off in its +entirely. One would like to confirm that, and the way to do this in Beancount is +to insert a balance check.

    +

    This plugin allows you to do that more simply, by inserting metadata. For +example, this transaction:

    +
    2018-02-16 * "SOLD -14 QQQ 100 16 FEB 18 160 CALL @5.31"
    +  Assets:US:Brokerage:Main:Options     -1400 QQQ180216C160 {2.70 USD} @ 5.31 USD
    +    closing: TRUE
    +  Expenses:Financial:Commissions       17.45 USD
    +  Expenses:Financial:Fees               0.42 USD
    +  Assets:US:Brokerage:Main:Cash      7416.13 USD
    +  Income:US:Brokerage:Main:PnL
    +
    +

    Would expand into the following two directives:

    +
    2018-02-16 * "SOLD -14 QQQ 100 16 FEB 18 160 CALL @5.31"
    +  Assets:US:Brokerage:Main:Options     -1400 QQQ180216C160 {2.70 USD} @ 5.31 USD
    +  Expenses:Financial:Commissions       17.45 USD
    +  Expenses:Financial:Fees               0.42 USD
    +  Assets:US:Brokerage:Main:Cash      7416.13 USD
    +  Income:US:Brokerage:Main:PnL
     
    -        
    - Source code in beancount/plugins/book_conversions.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    +2018-02-17 balance Assets:US:Brokerage:Main:Options  0 QQQ180216C160
     
    -
    -
    +

    Insert the closing line when you know you're closing the position.

    - +
    + -
    -

    -beancount.plugins.book_conversions.ConfigError.__new__(_cls, source, message, entry) - - special - staticmethod - - -

    - -
    - -

    Create new instance of ConfigError(source, message, entry)

    - -
    - -
    - - - -
    - - - -

    -beancount.plugins.book_conversions.ConfigError.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/plugins/book_conversions.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - - - - - - - - -
    - - - -

    -beancount.plugins.book_conversions.augment_inventory(pending_lots, posting, entry, eindex) - - -

    - -
    - -

    Add the lots from the given posting to the running inventory.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • pending_lots – A list of pending ([number], Posting, Transaction) to be matched. -The number is modified in-place, destructively.

    • -
    • posting – The posting whose position is to be added.

    • -
    • entry – The parent transaction.

    • -
    • eindex – The index of the parent transaction housing this posting.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A new posting with cost basis inserted to be added to a transformed transaction.

    • -
    -
    -
    - Source code in beancount/plugins/book_conversions.py -
    def augment_inventory(pending_lots, posting, entry, eindex):
    -    """Add the lots from the given posting to the running inventory.
    -
    -    Args:
    -      pending_lots: A list of pending ([number], Posting, Transaction) to be matched.
    -        The number is modified in-place, destructively.
    -      posting: The posting whose position is to be added.
    -      entry: The parent transaction.
    -      eindex: The index of the parent transaction housing this posting.
    -    Returns:
    -      A new posting with cost basis inserted to be added to a transformed transaction.
    -    """
    -    number = posting.units.number
    -    new_posting = posting._replace(
    -        units=copy.copy(posting.units),
    -        cost=position.Cost(posting.price.number,
    -                           posting.price.currency,
    -                           entry.date,
    -                           None))
    -    pending_lots.append(([number], new_posting, eindex))
    -    return new_posting
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.book_conversions.book_price_conversions(entries, assets_account, income_account) - - -

    - -
    - -

    Rewrite transactions to insert cost basis according to a booking method.

    -

    See module docstring for full details.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of entry instances.

    • -
    • assets_account – An account string, the name of the account to process.

    • -
    • income_account – An account string, the name of the account to use for booking -realized profit/loss.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of - entries – A list of new, modified entries. - errors: A list of errors generated by this plugin. - matches: A list of (number, augmenting-posting, reducing-postings) for all - matched lots.

    • -
    -
    -
    - Source code in beancount/plugins/book_conversions.py -
    def book_price_conversions(entries, assets_account, income_account):
    -    """Rewrite transactions to insert cost basis according to a booking method.
    -
    -    See module docstring for full details.
    -
    -    Args:
    -      entries: A list of entry instances.
    -      assets_account: An account string, the name of the account to process.
    -      income_account: An account string, the name of the account to use for booking
    -        realized profit/loss.
    -    Returns:
    -      A tuple of
    -        entries: A list of new, modified entries.
    -        errors: A list of errors generated by this plugin.
    -        matches: A list of (number, augmenting-posting, reducing-postings) for all
    -          matched lots.
    -    """
    -    # Pairs of (Position, Transaction) instances used to match augmenting
    -    # entries with reducing ones.
    -    pending_lots = []
    -
    -    # A list of pairs of matching (augmenting, reducing) postings.
    -    all_matches = []
    -
    -    new_entries = []
    -    errors = []
    -    for eindex, entry in enumerate(entries):
    -
    -        # Figure out if this transaction has postings in Bitcoins without a cost.
    -        # The purpose of this plugin is to fixup those.
    -        if isinstance(entry, data.Transaction) and any(is_matching(posting, assets_account)
    -                                                       for posting in entry.postings):
    -
    -            # Segregate the reducing lots, augmenting lots and other lots.
    -            augmenting, reducing, other = [], [], []
    -            for pindex, posting in enumerate(entry.postings):
    -                if is_matching(posting, assets_account):
    -                    out = augmenting if posting.units.number >= ZERO else reducing
    -                else:
    -                    out = other
    -                out.append(posting)
    -
    -            # We will create a replacement list of postings with costs filled
    -            # in, possibly more than the original list, to account for the
    -            # different lots.
    -            new_postings = []
    -
    -            # Convert all the augmenting postings to cost basis.
    -            for posting in augmenting:
    -                new_postings.append(augment_inventory(pending_lots, posting, entry, eindex))
    -
    -            # Then process reducing postings.
    -            if reducing:
    -                # Process all the reducing postings, booking them to matching lots.
    -                pnl = inventory.Inventory()
    -                for posting in reducing:
    -                    rpostings, matches, posting_pnl, new_errors = (
    -                        reduce_inventory(pending_lots, posting, eindex))
    -                    new_postings.extend(rpostings)
    -                    all_matches.extend(matches)
    -                    errors.extend(new_errors)
    -                    pnl.add_amount(amount.Amount(posting_pnl, posting.price.currency))
    -
    -                # If some reducing lots were seen in this transaction, insert an
    -                # Income leg to absorb the P/L. We need to do this for each currency
    -                # which incurred P/L.
    -                if not pnl.is_empty():
    -                    for pos in pnl:
    -                        meta = data.new_metadata('<book_conversions>', 0)
    -                        new_postings.append(
    -                            data.Posting(income_account,
    -                                         -pos.units, None,
    -                                         None, None, meta))
    -
    -            # Third, add back all the other unrelated legs in.
    -            for posting in other:
    -                new_postings.append(posting)
    -
    -            # Create a replacement entry.
    -            entry = entry._replace(postings=new_postings)
    -
    -        new_entries.append(entry)
    -
    -    # Add matching metadata to all matching postings.
    -    mod_matches = link_entries_with_metadata(new_entries, all_matches)
    -
    -    # Resolve the indexes to their possibly modified Transaction instances.
    -    matches = [(data.TxnPosting(new_entries[aug_index], aug_posting),
    -                data.TxnPosting(new_entries[red_index], red_posting))
    -               for (aug_index, aug_posting), (red_index, red_posting) in mod_matches]
    -
    -    return new_entries, errors, matches
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.book_conversions.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.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • 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.

    • -
    -
    -
    - Source code in beancount/plugins/book_conversions.py -
    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
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.book_conversions.extract_trades(entries) - - -

    - -
    - -

    Find all the matching trades from the metadata attached to postings.

    -

    This inspects all the postings and pairs them up using the special metadata -field that was added by this plugin when booking matching lots, and returns -pairs of those postings.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – The list of directives to extract from.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of (number, augmenting-posting, reducing-posting).

    • -
    -
    -
    - Source code in beancount/plugins/book_conversions.py -
    def extract_trades(entries):
    -    """Find all the matching trades from the metadata attached to postings.
    -
    -    This inspects all the postings and pairs them up using the special metadata
    -    field that was added by this plugin when booking matching lots, and returns
    -    pairs of those postings.
    -
    -    Args:
    -      entries: The list of directives to extract from.
    -    Returns:
    -      A list of (number, augmenting-posting, reducing-posting).
    -    """
    -    trade_map = collections.defaultdict(list)
    -    for index, entry in enumerate(entries):
    -        if not isinstance(entry, data.Transaction):
    -            continue
    -        for posting in entry.postings:
    -            links_str = posting.meta.get(META, None)
    -            if links_str:
    -                links = links_str.split(',')
    -                for link in links:
    -                    trade_map[link].append((index, entry, posting))
    -
    -    # Sort matches according to the index of the first entry, drop the index
    -    # used for doing this, and convert the objects to tuples..
    -    trades = [(data.TxnPosting(augmenting[1], augmenting[2]),
    -               data.TxnPosting(reducing[1], reducing[2]))
    -              for augmenting, reducing in sorted(trade_map.values())]
    -
    -    # Sanity check.
    -    for matches in trades:
    -        assert len(matches) == 2
    -
    -    return trades
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.book_conversions.is_matching(posting, account) - - -

    - -
    - -

    "Identify if the given posting is one to be booked.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • posting – An instance of a Posting.

    • -
    • account – The account name configured.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A boolean, true if this posting is one that we should be adding a cost to.

    • -
    -
    -
    - Source code in beancount/plugins/book_conversions.py -
    def is_matching(posting, account):
    -    """"Identify if the given posting is one to be booked.
    -
    -    Args:
    -      posting: An instance of a Posting.
    -      account: The account name configured.
    -    Returns:
    -      A boolean, true if this posting is one that we should be adding a cost to.
    -    """
    -    return (posting.account == account and
    -            posting.cost is None and
    -            posting.price is not None)
    -
    -
    -
    - -
    - - - -
    - - - - - -
    - -

    Modify the entries in-place to add matching links to postings.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – The list of entries to modify.

    • -
    • all_matches – A list of pairs of (augmenting-posting, reducing-posting).

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of pairs of (index, Posting) for the new (augmenting, reducing) -annotated postings.

    • -
    -
    -
    - Source code in beancount/plugins/book_conversions.py -
    def link_entries_with_metadata(entries, all_matches):
    -    """Modify the entries in-place to add matching links to postings.
    -
    -    Args:
    -      entries: The list of entries to modify.
    -      all_matches: A list of pairs of (augmenting-posting, reducing-posting).
    -    Returns:
    -      A list of pairs of (index, Posting) for the new (augmenting, reducing)
    -      annotated postings.
    -    """
    -    # Allocate trade names and compute a map of posting to trade names.
    -    link_map = collections.defaultdict(list)
    -    for (aug_index, aug_posting), (red_index, red_posting) in all_matches:
    -        link = 'trade-{}'.format(str(uuid.uuid4()).split('-')[-1])
    -        link_map[id(aug_posting)].append(link)
    -        link_map[id(red_posting)].append(link)
    -
    -    # Modify the postings.
    -    postings_repl_map = {}
    -    for entry in entries:
    -        if isinstance(entry, data.Transaction):
    -            for index, posting in enumerate(entry.postings):
    -                links = link_map.pop(id(posting), None)
    -                if links:
    -                    new_posting = posting._replace(meta=posting.meta.copy())
    -                    new_posting.meta[META] = ','.join(links)
    -                    entry.postings[index] = new_posting
    -                    postings_repl_map[id(posting)] = new_posting
    -
    -    # Just a sanity check.
    -    assert not link_map, "Internal error: not all matches found."
    -
    -    # Return a list of the modified postings (mapping the old matches to the
    -    # newly created ones).
    -    return [((aug_index, postings_repl_map[id(aug_posting)]),
    -             (red_index, postings_repl_map[id(red_posting)]))
    -            for (aug_index, aug_posting), (red_index, red_posting) in all_matches]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.book_conversions.main() - - -

    - -
    - -

    Extract trades from metadata-annotated postings and report on them.

    - -
    - Source code in beancount/plugins/book_conversions.py -
    def main():
    -    """Extract trades from metadata-annotated postings and report on them.
    -    """
    -    logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s')
    -    parser = version.ArgumentParser(description=__doc__.strip())
    -    parser.add_argument('filename', help='Beancount input filename')
    -
    -    oparser = parser.add_argument_group('Outputs')
    -    oparser.add_argument('-o', '--output', action='store',
    -                         help="Filename to output results to (default goes to stdout)")
    -    oparser.add_argument('-f', '--format', default='text',
    -                         choices=['text', 'csv'],
    -                         help="Output format to render to (text, csv)")
    -
    -    args = parser.parse_args()
    -
    -    # Load the input file.
    -    entries, errors, options_map = loader.load_file(args.filename)
    -
    -    # Get the list of trades.
    -    trades = extract_trades(entries)
    -
    -    # Produce a table of all the trades.
    -    columns = ('units currency cost_currency '
    -               'buy_date buy_price sell_date sell_price pnl').split()
    -    header = ['Units', 'Currency', 'Cost Currency',
    -              'Buy Date', 'Buy Price', 'Sell Date', 'Sell Price',
    -              'P/L']
    -    body = []
    -    for aug, red in trades:
    -        units = -red.posting.units.number
    -        buy_price = aug.posting.price.number
    -        sell_price = red.posting.price.number
    -        pnl = (units * (sell_price - buy_price)).quantize(buy_price)
    -        body.append([
    -            -red.posting.units.number,
    -            red.posting.units.currency,
    -            red.posting.price.currency,
    -            aug.txn.date.isoformat(), buy_price,
    -            red.txn.date.isoformat(), sell_price,
    -            pnl
    -            ])
    -    trades_table = table.Table(columns, header, body)
    -
    -    # Render the table as text or CSV.
    -    outfile = open(args.output, 'w') if args.output else sys.stdout
    -    table.render_table(trades_table, outfile, args.format)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.book_conversions.reduce_inventory(pending_lots, posting, eindex) - - -

    - -
    - -

    Match a reducing posting against a list of lots (using FIFO order).

    - - - - - - - - - - - - -
    Parameters: -
      -
    • pending_lots – A list of pending ([number], Posting, Transaction) to be matched. -The number is modified in-place, destructively.

    • -
    • posting – The posting whose position is to be added.

    • -
    • eindex – The index of the parent transaction housing this posting.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of - postings – A list of new Posting instances corresponding to the given - posting, that were booked to the current list of lots. - matches: A list of pairs of (augmenting-posting, reducing-posting). - pnl: A Decimal, the P/L incurred in reducing these lots. - errors: A list of new errors generated in reducing these lots.

    • -
    -
    -
    - Source code in beancount/plugins/book_conversions.py -
    def reduce_inventory(pending_lots, posting, eindex):
    -    """Match a reducing posting against a list of lots (using FIFO order).
    -
    -    Args:
    -      pending_lots: A list of pending ([number], Posting, Transaction) to be matched.
    -        The number is modified in-place, destructively.
    -      posting: The posting whose position is to be added.
    -      eindex: The index of the parent transaction housing this posting.
    -    Returns:
    -      A tuple of
    -        postings: A list of new Posting instances corresponding to the given
    -          posting, that were booked to the current list of lots.
    -        matches: A list of pairs of (augmenting-posting, reducing-posting).
    -        pnl: A Decimal, the P/L incurred in reducing these lots.
    -        errors: A list of new errors generated in reducing these lots.
    -    """
    -    new_postings = []
    -    matches = []
    -    pnl = ZERO
    -    errors = []
    -
    -    match_number = -posting.units.number
    -    match_currency = posting.units.currency
    -    cost_currency = posting.price.currency
    -    while match_number != ZERO:
    -
    -        # Find the first lot with matching currency.
    -        for fnumber, fposting, findex in pending_lots:
    -            funits = fposting.units
    -            fcost = fposting.cost
    -            if (funits.currency == match_currency and
    -                fcost and fcost.currency == cost_currency):
    -                assert fnumber[0] > ZERO, "Internal error, zero lot"
    -                break
    -        else:
    -            errors.append(
    -                BookConversionError(posting.meta,
    -                          "Could not match position {}".format(posting), None))
    -            break
    -
    -        # Reduce the pending lots.
    -        number = min(match_number, fnumber[0])
    -        cost = fcost
    -        match_number -= number
    -        fnumber[0] -= number
    -        if fnumber[0] == ZERO:
    -            pending_lots.pop(0)
    -
    -        # Add a corresponding posting.
    -        rposting = posting._replace(
    -            units=amount.Amount(-number, posting.units.currency),
    -            cost=copy.copy(cost))
    -        new_postings.append(rposting)
    -
    -        # Update the P/L.
    -        pnl += number * (posting.price.number - cost.number)
    -
    -        # Add to the list of matches.
    -        matches.append(((findex, fposting),
    -                        (eindex, rposting)))
    -
    -    return new_postings, matches, pnl, errors
    -
    -
    -
    - -
    - - - - - - - - - - - - - - -
    - - - -

    - beancount.plugins.check_average_cost - - - -

    - -
    - -

    A plugin that ensures cost basis is preserved in unbooked transactions.

    -

    This is intended to be used in accounts using the "NONE" booking method, to -manually ensure that the sum total of the cost basis of reducing legs matches -the average of what's in the account inventory. This is a partial first step -toward implementing the "AVERAGE" booking method. In other words, this plugins -provides assertions that will constrain you to approximate what the "AVERAGE" -booking method will do, manually, and not to leak too much cost basis through -unmatching bookings without checks. (Note the contrived context here: Ideally -the "NONE" booking method would simply not exist.)

    - - - -
    - - - - - - - - - - - -
    - - - -

    - -beancount.plugins.check_average_cost.MatchBasisError (tuple) - - - - -

    - -
    - -

    MatchBasisError(source, message, entry)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.plugins.check_average_cost.MatchBasisError.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/plugins/check_average_cost.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.check_average_cost.MatchBasisError.__new__(_cls, source, message, entry) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of MatchBasisError(source, message, entry)

    - -
    - -
    - - - -
    - - - -

    -beancount.plugins.check_average_cost.MatchBasisError.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/plugins/check_average_cost.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.plugins.check_average_cost.validate_average_cost(entries, options_map, config_str=None) - - -

    - -
    - -

    Check that reducing legs on unbooked postings are near the average cost basis.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • unused_options_map – An options map.

    • -
    • config_str – The configuration as a string version of a float.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of new errors, if any were found.

    • -
    -
    -
    - Source code in beancount/plugins/check_average_cost.py -
    def validate_average_cost(entries, options_map, config_str=None):
    -    """Check that reducing legs on unbooked postings are near the average cost basis.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -      config_str: The configuration as a string version of a float.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    # Initialize tolerance bounds.
    -    if config_str and config_str.strip():
    -        # pylint: disable=eval-used
    -        config_obj = eval(config_str, {}, {})
    -        if not isinstance(config_obj, float):
    -            raise RuntimeError("Invalid configuration for check_average_cost: "
    -                               "must be a float")
    -        tolerance = config_obj
    -    else:
    -        tolerance = DEFAULT_TOLERANCE
    -    min_tolerance = D(1 - tolerance)
    -    max_tolerance = D(1 + tolerance)
    -
    -    errors = []
    -    ocmap = getters.get_account_open_close(entries)
    -    balances = collections.defaultdict(inventory.Inventory)
    -    for entry in entries:
    -        if isinstance(entry, Transaction):
    -            for posting in entry.postings:
    -                dopen = ocmap.get(posting.account, None)
    -                # Only process accounts with a NONE booking value.
    -                if dopen and dopen[0] and dopen[0].booking == Booking.NONE:
    -                    balance = balances[(posting.account,
    -                                        posting.units.currency,
    -                                        posting.cost.currency if posting.cost else None)]
    -                    if posting.units.number < ZERO:
    -                        average = balance.average().get_only_position()
    -                        if average is not None:
    -                            number = average.cost.number
    -                            min_valid = number * min_tolerance
    -                            max_valid = number * max_tolerance
    -                            if not (min_valid <= posting.cost.number <= max_valid):
    -                                errors.append(
    -                                    MatchBasisError(
    -                                        entry.meta,
    -                                        ("Cost basis on reducing posting is too far from "
    -                                         "the average cost ({} vs. {})".format(
    -                                             posting.cost.number, average.cost.number)),
    -                                        entry))
    -                    balance.add_position(posting)
    -    return entries, errors
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.plugins.check_closing - - - -

    - -
    - -

    A plugin that automatically inserts a balance check on a tagged closing posting.

    -

    Some postings are known to the user to be "closing trades", which means that the -resulting position of the instrument just after the trade should be zero. For -instance, this is the case for most ordinary options trading, only one lot of a -particular instrument is held, and eventually expires or gets sold off in its -entirely. One would like to confirm that, and the way to do this in Beancount is -to insert a balance check.

    -

    This plugin allows you to do that more simply, by inserting metadata. For -example, this transaction:

    -
    2018-02-16 * "SOLD -14 QQQ 100 16 FEB 18 160 CALL @5.31"
    -  Assets:US:Brokerage:Main:Options     -1400 QQQ180216C160 {2.70 USD} @ 5.31 USD
    -    closing: TRUE
    -  Expenses:Financial:Commissions       17.45 USD
    -  Expenses:Financial:Fees               0.42 USD
    -  Assets:US:Brokerage:Main:Cash      7416.13 USD
    -  Income:US:Brokerage:Main:PnL
    -
    -

    Would expand into the following two directives:

    -
    2018-02-16 * "SOLD -14 QQQ 100 16 FEB 18 160 CALL @5.31"
    -  Assets:US:Brokerage:Main:Options     -1400 QQQ180216C160 {2.70 USD} @ 5.31 USD
    -  Expenses:Financial:Commissions       17.45 USD
    -  Expenses:Financial:Fees               0.42 USD
    -  Assets:US:Brokerage:Main:Cash      7416.13 USD
    -  Income:US:Brokerage:Main:PnL
    -
    -2018-02-17 balance Assets:US:Brokerage:Main:Options  0 QQQ180216C160
    -
    -

    Insert the closing line when you know you're closing the position.

    - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.plugins.check_closing.check_closing(entries, options_map) - - -

    - -
    - -

    Expand 'closing' metadata to a zero balance check.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • unused_options_map – An options map.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of new errors, if any were found.

    • -
    -
    -
    - Source code in beancount/plugins/check_closing.py -
    def check_closing(entries, options_map):
    -    """Expand 'closing' metadata to a zero balance check.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    new_entries = []
    -    for entry in entries:
    -        if isinstance(entry, data.Transaction):
    -            for posting in entry.postings:
    -                if posting.meta and posting.meta.get('closing', False):
    -                    # Remove the metadata.
    -                    meta = posting.meta.copy()
    -                    del meta['closing']
    -                    entry = entry._replace(meta=meta)
    -
    -                    # Insert a balance.
    -                    date = entry.date + datetime.timedelta(days=1)
    -                    balance = data.Balance(data.new_metadata("<check_closing>", 0),
    -                                           date, posting.account,
    -                                           amount.Amount(ZERO, posting.units.currency),
    -                                           None, None)
    -                    new_entries.append(balance)
    -        new_entries.append(entry)
    -    return new_entries, []
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.plugins.check_commodity - - - -

    - -
    - -

    A plugin that verifies that all seen commodities have a Commodity directive.

    -

    This is useful if you're a bit pedantic and like to make sure that you're -declared attributes for each of the commodities you use. It's useful if you use -the portfolio export, for example.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.plugins.check_commodity.CheckCommodityError (tuple) - - - - -

    - -
    - -

    CheckCommodityError(source, message, entry)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.plugins.check_commodity.CheckCommodityError.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/plugins/check_commodity.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.check_commodity.CheckCommodityError.__new__(_cls, source, message, entry) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of CheckCommodityError(source, message, entry)

    - -
    - -
    - - - -
    - - - -

    -beancount.plugins.check_commodity.CheckCommodityError.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/plugins/check_commodity.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.plugins.check_commodity.validate_commodity_directives(entries, options_map) - - -

    - -
    - -

    Find all commodities used and ensure they have a corresponding Commodity directive.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • unused_options_map – An options map.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of new errors, if any were found.

    • -
    -
    -
    - Source code in beancount/plugins/check_commodity.py -
    def validate_commodity_directives(entries, options_map):
    -    """Find all commodities used and ensure they have a corresponding Commodity directive.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    commodities_used = options_map['commodities']
    -    errors = []
    -
    -    meta = data.new_metadata('<check_commodity>', 0)
    -
    -    commodity_map = getters.get_commodity_map(entries, create_missing=False)
    -    for currency in commodities_used:
    -        commodity_entry = commodity_map.get(currency, None)
    -        if commodity_entry is None:
    -            errors.append(
    -                CheckCommodityError(
    -                    meta,
    -                    "Missing Commodity directive for '{}'".format(currency),
    -                    None))
    -
    -    return entries, errors
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.plugins.coherent_cost - - - -

    - -
    - -

    This plugin validates that currencies held at cost aren't ever converted at -price and vice-versa. This is usually the case, and using it will prevent users -from making the mistake of selling a lot without specifying it via its cost -basis.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.plugins.coherent_cost.CoherentCostError (tuple) - - - - -

    - -
    - -

    CoherentCostError(source, message, entry)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.plugins.coherent_cost.CoherentCostError.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/plugins/coherent_cost.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.coherent_cost.CoherentCostError.__new__(_cls, source, message, entry) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of CoherentCostError(source, message, entry)

    - -
    - -
    - - - -
    - - - -

    -beancount.plugins.coherent_cost.CoherentCostError.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/plugins/coherent_cost.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.plugins.coherent_cost.validate_coherent_cost(entries, unused_options_map) - - -

    - -
    - -

    Check that all currencies are either used at cost or not at all, but never both.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • unused_options_map – An options map.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of new errors, if any were found.

    • -
    -
    -
    - Source code in beancount/plugins/coherent_cost.py -
    def validate_coherent_cost(entries, unused_options_map):
    -    """Check that all currencies are either used at cost or not at all, but never both.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    errors = []
    -
    -    with_cost = {}
    -    without_cost = {}
    -    for entry in data.filter_txns(entries):
    -        for posting in entry.postings:
    -            target_set = without_cost if posting.cost is None else with_cost
    -            currency = posting.units.currency
    -            target_set.setdefault(currency, entry)
    -
    -    for currency in set(with_cost) & set(without_cost):
    -        errors.append(
    -            CoherentCostError(
    -                without_cost[currency].meta,
    -                "Currency '{}' is used both with and without cost".format(currency),
    -                with_cost[currency]))
    -        # Note: We really ought to include both of the first transactions here.
    -
    -    return entries, errors
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.plugins.commodity_attr - - - -

    - -
    - -

    A plugin that asserts that all Commodity directives have a particular -attribute and that it is part of a set of enum values.

    -

    The configuration must be a mapping of attribute name to list of valid values, -like this:

    -
    plugin "beancount.plugins.commodity_attr" "{
    -  'sector': ['Technology', 'Financials', 'Energy'],
    -  'name': None,
    -}"
    -
    -

    The plugin issues an error if a Commodity directive is missing the attribute, or -if the attribute value is not in the valid set. If you'd like to just ensure the -attribute is set, set the list of valid values to None, as in the 'name' -attribute in the example above.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.plugins.commodity_attr.CommodityError (tuple) - - - - -

    - -
    - -

    CommodityError(source, message, entry)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.plugins.commodity_attr.CommodityError.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/plugins/commodity_attr.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.commodity_attr.CommodityError.__new__(_cls, source, message, entry) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of CommodityError(source, message, entry)

    - -
    - -
    - - - -
    - - - -

    -beancount.plugins.commodity_attr.CommodityError.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/plugins/commodity_attr.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.plugins.commodity_attr.ConfigError (tuple) - - - - -

    - -
    - -

    ConfigError(source, message, entry)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.plugins.commodity_attr.ConfigError.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/plugins/commodity_attr.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.commodity_attr.ConfigError.__new__(_cls, source, message, entry) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of ConfigError(source, message, entry)

    - -
    - -
    - - - -
    - - - -

    -beancount.plugins.commodity_attr.ConfigError.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/plugins/commodity_attr.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.plugins.commodity_attr.validate_commodity_attr(entries, unused_options_map, config_str) - - -

    - -
    - -

    Check that all Commodity directives have a valid attribute.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • unused_options_map – An options map.

    • -
    • config_str – A configuration string.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of new errors, if any were found.

    • -
    -
    -
    - Source code in beancount/plugins/commodity_attr.py -
    def validate_commodity_attr(entries, unused_options_map, config_str):
    -    """Check that all Commodity directives have a valid attribute.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -      config_str: A configuration string.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    errors = []
    -
    -    # pylint: disable=eval-used
    -    config_obj = eval(config_str, {}, {})
    -    if not isinstance(config_obj, dict):
    -        errors.append(ConfigError(
    -            data.new_metadata('<commodity_attr>', 0),
    -            "Invalid configuration for commodity_attr plugin; skipping.", None))
    -        return entries, errors
    -
    -    validmap = {attr: frozenset(values) if values is not None else None
    -                for attr, values in config_obj.items()}
    -    for entry in entries:
    -        if not isinstance(entry, data.Commodity):
    -            continue
    -        for attr, values in validmap.items():
    -            value = entry.meta.get(attr, None)
    -            if value is None:
    -                errors.append(CommodityError(
    -                    entry.meta,
    -                    "Missing attribute '{}' for Commodity directive {}".format(
    -                        attr, entry.currency), None))
    -                continue
    -            if values and value not in values:
    -                errors.append(CommodityError(
    -                    entry.meta,
    -                    "Invalid attribute '{}' for Commodity".format(value) +
    -                    " directive {}; valid options: {}".format(
    -                        entry.currency, ', '.join(values)), None))
    -
    -    return entries, errors
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.plugins.currency_accounts - - - -

    - -
    - -

    An implementation of currency accounts.

    -

    This is an automatic implementation of the method described here: -https://www.mathstat.dal.ca/~selinger/accounting/tutorial.html

    -

    You enable it just like this:

    -
    plugin "beancount.plugins.currency_accounts" "Equity:CurrencyAccounts"
    -
    -

    Accounts will be automatically created under the given base account, with the -currency name appended to it, e.g.,

    -
    Equity:CurrencyAccounts:CAD
    -Equity:CurrencyAccounts:USD
    -
    -

    etc., where used. You can have a look at the account balances with a query like -this:

    -
    bean-query $L "select account, sum(position), convert(sum(position), 'USD')
    -               where date &gt;= 2018-01-01 and  account ~ 'CurrencyAccounts' "
    -
    -

    The sum total of the converted amounts should be a number not too large:

    -
    bean-query $L "select convert(sum(position), 'USD')
    -               where date &gt;= 2018-01-01 and  account ~ 'CurrencyAccounts'"
    -
    -

    WARNING: This is a prototype. Note the FIXMEs in the code below, which indicate -some potential problems.

    - - - -
    - - - - - - - - - - - - - -
    - - - -

    -beancount.plugins.currency_accounts.get_neutralizing_postings(curmap, base_account, new_accounts) - - -

    - -
    - -

    Process an entry.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • curmap – A dict of currency to a list of Postings of this transaction.

    • -
    • base_account – A string, the root account name to insert.

    • -
    • new_accounts – A set, a mutable accumulator of new account names.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A modified entry, with new postings inserted to rebalance currency trading -accounts.

    • -
    -
    -
    - Source code in beancount/plugins/currency_accounts.py -
    def get_neutralizing_postings(curmap, base_account, new_accounts):
    -    """Process an entry.
    -
    -    Args:
    -      curmap: A dict of currency to a list of Postings of this transaction.
    -      base_account: A string, the root account name to insert.
    -      new_accounts: A set, a mutable accumulator of new account names.
    -    Returns:
    -      A modified entry, with new postings inserted to rebalance currency trading
    -      accounts.
    -    """
    -    new_postings = []
    -    for currency, postings in curmap.items():
    -        # Compute the per-currency balance.
    -        inv = inventory.Inventory()
    -        for posting in postings:
    -            inv.add_amount(convert.get_cost(posting))
    -        if inv.is_empty():
    -            new_postings.extend(postings)
    -            continue
    -
    -        # Re-insert original postings and remove price conversions.
    -        #
    -        # Note: This may cause problems if the implicit_prices plugin is
    -        # configured to run after this one, or if you need the price annotations
    -        # for some scripting or serious work.
    -        #
    -        # FIXME: We need to handle these important cases (they're not frivolous,
    -        # this is a prototype), probably by inserting some exceptions with
    -        # collaborating code in the booking (e.g. insert some metadata that
    -        # disables price conversions on those postings).
    -        #
    -        # FIXME(2): Ouch! Some of the residual seeps through here, where there
    -        # are more than a single currency block. This needs fixing too. You can
    -        # easily mitigate some of this to some extent, by excluding transactions
    -        # which don't have any price conversion in them.
    -        for pos in postings:
    -            if pos.price is not None:
    -                pos = pos._replace(price=None)
    -            new_postings.append(pos)
    -
    -        # Insert the currency trading accounts postings.
    -        amount = inv.get_only_position().units
    -        acc = account.join(base_account, currency)
    -        new_accounts.add(acc)
    -        new_postings.append(
    -            Posting(acc, -amount, None, None, None, None))
    -
    -    return new_postings
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.currency_accounts.group_postings_by_weight_currency(entry) - - -

    - -
    - -

    Return where this entry might require adjustment.

    - -
    - Source code in beancount/plugins/currency_accounts.py -
    def group_postings_by_weight_currency(entry: Transaction):
    -    """Return where this entry might require adjustment."""
    -    curmap = collections.defaultdict(list)
    -    has_price = False
    -    for posting in entry.postings:
    -        currency = posting.units.currency
    -        if posting.cost:
    -            currency = posting.cost.currency
    -            if posting.price:
    -                assert posting.price.currency == currency
    -            elif posting.price:
    -                has_price = True
    -                currency = posting.price.currency
    -        if posting.price:
    -            has_price = True
    -        curmap[currency].append(posting)
    -    return curmap, has_price
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.currency_accounts.insert_currency_trading_postings(entries, options_map, config) - - -

    - -
    - -

    Insert currency trading postings.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • 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.

    • -
    -
    -
    - Source code in beancount/plugins/currency_accounts.py -
    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
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.plugins.divert_expenses - - - -

    - -
    - -

    For tagged transactions, convert expenses to a single account.

    -

    This plugin allows you to select a tag and it automatically converts all the -Expenses postings to use a single account. For example, with this input:

    -
    plugin "divert_expenses" "['kid', 'Expenses:Child']"
    -
    -2018-01-28 * "CVS" "Formula" #kid
    -  Liabilities:CreditCard      -10.27 USD
    -  Expenses:Food:Grocery        10.27 USD
    -
    -

    It will output:

    -

    2018-01-28 * "CVS" "Formula" #kid - Liabilities:CreditCard -10.27 USD - Expenses:Child 10.27 USD

    -

    You can limit the diversion to one posting only, like this:

    -
    2018-05-05 * "CVS/PHARMACY" "" #kai
    -  Liabilities:CreditCard        -66.38 USD
    -  Expenses:Pharmacy              21.00 USD  ;; Vitamins for Kai
    -  Expenses:Pharmacy              45.38 USD
    -    divert: FALSE
    -
    -

    See unit test for details.

    -

    See this thread for context: -https://docs.google.com/drawings/d/18fTrrGlmz0jFbfcGGHTffbdRwbmST8r9_3O26Dd1Xww/edit?usp=sharing

    - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.plugins.divert_expenses.divert_expenses(entries, options_map, config_str) - - -

    - -
    - -

    Divert expenses.

    -

    Explicit price entries are simply maintained in the output list. Prices from -postings with costs or with prices from Transaction entries are synthesized -as new Price entries in the list of entries output.

    - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives. We're interested only in the Transaction instances.

    • -
    • options_map – A parser options dict.

    • -
    • config_str – A configuration string, which is intended to be a list of two strings, -a tag, and an account to replace expenses with.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A modified list of entries.

    • -
    -
    -
    - Source code in beancount/plugins/divert_expenses.py -
    def divert_expenses(entries, options_map, config_str):
    -    """Divert expenses.
    -
    -    Explicit price entries are simply maintained in the output list. Prices from
    -    postings with costs or with prices from Transaction entries are synthesized
    -    as new Price entries in the list of entries output.
    -
    -    Args:
    -      entries: A list of directives. We're interested only in the Transaction instances.
    -      options_map: A parser options dict.
    -      config_str: A configuration string, which is intended to be a list of two strings,
    -        a tag, and an account to replace expenses with.
    -    Returns:
    -      A modified list of entries.
    -    """
    -    # pylint: disable=eval-used
    -    config_obj = eval(config_str, {}, {})
    -    if not isinstance(config_obj, dict):
    -        raise RuntimeError("Invalid plugin configuration: should be a single dict.")
    -    tag = config_obj['tag']
    -    replacement_account = config_obj['account']
    -
    -    acctypes = options.get_account_types(options_map)
    -
    -    new_entries = []
    -    errors = []
    -    for entry in entries:
    -        if isinstance(entry, Transaction) and tag in entry.tags:
    -            entry = replace_diverted_accounts(entry, replacement_account, acctypes)
    -        new_entries.append(entry)
    -
    -    return new_entries, errors
    -
    -
    -
    -
    @@ -3714,15 +993,15 @@

    -beancount.plugins.divert_expenses.replace_diverted_accounts(entry, replacement_account, acctypes) +

    +beancount.plugins.check_closing.check_closing(entries, options_map) -

    +

    -

    Replace the Expenses accounts from the entry.

    +

    Expand 'closing' metadata to a zero balance check.

    @@ -3734,9 +1013,8 @@

    Parameters:

    @@ -3752,35 +1030,48 @@

    Returns:

      -
    • entry – A Transaction directive.

    • -
    • replacement_account – A string, the account to use for replacement.

    • -
    • acctypes – An AccountTypes instance.

    • +
    • entries – A list of directives.

    • +
    • unused_options_map – An options map.

      -
    • A possibly entry directive.

    • +
    • A list of new errors, if any were found.

    - Source code in beancount/plugins/divert_expenses.py -
    def replace_diverted_accounts(entry, replacement_account, acctypes):
    -    """Replace the Expenses accounts from the entry.
    -
    -    Args:
    -      entry: A Transaction directive.
    -      replacement_account: A string, the account to use for replacement.
    -      acctypes: An AccountTypes instance.
    -    Returns:
    -      A possibly entry directive.
    -    """
    -    new_postings = []
    -    for posting in entry.postings:
    -        divert = posting.meta.get('divert', None) if posting.meta else None
    -        if (divert is True or (
    -                divert is None and
    -                account_types.is_account_type(acctypes.expenses, posting.account))):
    -            posting = posting._replace(account=replacement_account,
    -                                       meta={'diverted_account': posting.account})
    -        new_postings.append(posting)
    -    return entry._replace(postings=new_postings)
    -
    + Source code in beancount/plugins/check_closing.py +
    def check_closing(entries, options_map):
    +    """Expand 'closing' metadata to a zero balance check.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    new_entries = []
    +    for entry in entries:
    +        if isinstance(entry, data.Transaction):
    +            for i, posting in enumerate(entry.postings):
    +                if posting.meta and posting.meta.get("closing", False):
    +                    # Remove the metadata.
    +                    meta = posting.meta.copy()
    +                    del meta["closing"]
    +                    posting = posting._replace(meta=meta)
    +                    entry.postings[i] = posting
    +
    +                    # Insert a balance.
    +                    date = entry.date + datetime.timedelta(days=1)
    +                    balance = data.Balance(
    +                        data.new_metadata("<check_closing>", 0),
    +                        date,
    +                        posting.account,
    +                        amount.Amount(ZERO, posting.units.currency),
    +                        None,
    +                        None,
    +                    )
    +                    new_entries.append(balance)
    +        new_entries.append(entry)
    +    return new_entries, []
    +
    @@ -3803,22 +1094,28 @@

    - beancount.plugins.exclude_tag +

    + beancount.plugins.check_commodity -

    +

    -

    Exclude #virtual tags.

    -

    This is used to demonstrate excluding a set of transactions from a particular -tag. In this example module, the tag name is fixed, but if we integrated this we -could provide a way to choose which tags to exclude. This is simply just another -mechanism for selecting a subset of transactions.

    -

    See discussion here for details: -https://groups.google.com/d/msg/ledger-cli/N8Slh2t45K0/aAz0i3Be4LYJ

    +

    A plugin that verifies that all seen commodities have a Commodity directive.

    +

    This is useful if you're a bit pedantic and like to make sure that you're +declared attributes for each of the commodities you use. It's useful if you use +the portfolio export, for example.

    +

    You can provide a mapping of (account-regexp, currency-regexp) as options, to +specify which commodities to ignore from this plugin selectively. Use this +sparingly, as it is an out from the checks that this plugin provides. However, +in an active options trading account, a ton of products get inserted and the +number of commodity directives can be overwhelming and it's not productive to +declare each of the options contracts - names with an embedded strike and +expiration date, such as 'SPX_121622P3300' - individually.

    +

    Note that if a symbol has been ignored in at least one account, it will +therefore be further in all Price directives and Metadata values.

    @@ -3835,135 +1132,23 @@

    -
    - - - -

    -beancount.plugins.exclude_tag.exclude_tag(entries, options_map) - - -

    - -
    - -

    Select all transactions that do not have a #virtual tag.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of entry instances.

    • -
    • options_map – A dict of options parsed from the file.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of entries and errors.

    • -
    -
    -
    - Source code in beancount/plugins/exclude_tag.py -
    def exclude_tag(entries, options_map):
    -    """Select all transactions that do not have a #virtual tag.
    -
    -    Args:
    -      entries: A list of entry instances.
    -      options_map: A dict of options parsed from the file.
    -    Returns:
    -      A tuple of entries and errors.
    -    """
    -    filtered_entries = [entry
    -                        for entry in entries
    -                        if (not isinstance(entry, data.Transaction) or
    -                            not entry.tags or
    -                            EXCLUDED_TAG not in entry.tags)]
    -    return (filtered_entries, [])
    -
    -
    -
    - -
    - - - - - - -

    - - - - - - - -
    - - - -

    - beancount.plugins.fill_account - - - -

    - -
    - -

    Insert an posting with a default account when there is only a single posting.

    -

    This is convenient to use in files which have mostly expenses, such as during a trip. -Set the name of the default account to fill in as an option.

    - - - -
    - - - - - - - - -
    -

    +

    -beancount.plugins.fill_account.FillAccountError (tuple) +beancount.plugins.check_commodity.CheckCommodityError (tuple) -

    +

    -

    FillAccountError(source, message, entry)

    +

    CheckCommodityError(source, message, entry)

    @@ -3982,184 +1167,25 @@

    -beancount.plugins.fill_account.FillAccountError.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/plugins/fill_account.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.fill_account.FillAccountError.__new__(_cls, source, message, entry) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of FillAccountError(source, message, entry)

    - -
    - -
    - - - -
    - - - -

    -beancount.plugins.fill_account.FillAccountError.__repr__(self) +

    +beancount.plugins.check_commodity.CheckCommodityError.__getnewargs__(self) special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/plugins/fill_account.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - - - - - - - - - - -
    - - - -

    -beancount.plugins.fill_account.fill_account(entries, unused_options_map, insert_account) - - -

    - -
    - -

    Insert an posting with a default account when there is only a single posting.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • 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.

    • -
    -
    + + +

    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    +
    - Source code in beancount/plugins/fill_account.py -
    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, []
    -
    + Source code in beancount/plugins/check_commodity.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -4167,72 +1193,67 @@

    +
    -
    - - - - +

    +beancount.plugins.check_commodity.CheckCommodityError.__new__(_cls, source, message, entry) + + special + staticmethod + +

    -
    +
    +

    Create new instance of CheckCommodityError(source, message, entry)

    +
    -

    - beancount.plugins.fix_payees +

    -

    +
    -
    -

    Rename payees based on a set of rules.

    -

    This can be used to clean up dirty imported payee names.

    -

    This plugin accepts a list of rules in this format:

    -
    plugin "beancount.plugins.fix_payees" "[
    -    (PAYEE, MATCH1, MATCH2, ...),
    -]"
    -
    -

    Each of the "MATCH" clauses is a string, in the format:

    -
    "A:&lt;regexp&gt;" : Match the account name.
    -"D:&lt;regexp&gt;" : Match the payee or the narration.
    -
    -

    The plugin matches the Transactions in the file and if there is a -case-insensitive match against the regular expression (we use re.search()), -replaces the payee name by "PAYEE". If multiple rules match, only the first rule -is used.

    -

    For example:

    -
    plugin "beancount.plugins.fix_payees" "[
     
    -    ("T-Mobile USA",
    -     "A:Expenses:Communications:Phone",
    -     "D:t-mobile"),
    +

    +beancount.plugins.check_commodity.CheckCommodityError.__repr__(self) - ("Con Edison", - "A:Expenses:Home:Electricity", - "D:con ?ed"), + + special + - ("Birreria @ Eataly", - "D:EATALY BIRRERIA"), +

    -]" -
    +
    +

    Return a nicely formatted representation string

    +
    + Source code in beancount/plugins/check_commodity.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    -
    +
    +
    +
    + @@ -4240,18 +1261,18 @@

    -

    +

    -beancount.plugins.fix_payees.FixPayeesError (tuple) +beancount.plugins.check_commodity.ConfigError (tuple) -

    +

    -

    FixPayeesError(source, message, entry)

    +

    ConfigError(source, message, entry)

    @@ -4270,25 +1291,25 @@

    -

    -beancount.plugins.fix_payees.FixPayeesError.__getnewargs__(self) +

    +beancount.plugins.check_commodity.ConfigError.__getnewargs__(self) special -

    +

    Return self as a plain tuple. Used by copy and pickle.

    - Source code in beancount/plugins/fix_payees.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    + Source code in beancount/plugins/check_commodity.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -4300,19 +1321,19 @@

    -beancount.plugins.fix_payees.FixPayeesError.__new__(_cls, source, message, entry) +

    +beancount.plugins.check_commodity.ConfigError.__new__(_cls, source, message, entry) special staticmethod -

    +

    -

    Create new instance of FixPayeesError(source, message, entry)

    +

    Create new instance of ConfigError(source, message, entry)

    @@ -4324,25 +1345,25 @@

    -beancount.plugins.fix_payees.FixPayeesError.__repr__(self) +

    +beancount.plugins.check_commodity.ConfigError.__repr__(self) special -

    +

    Return a nicely formatted representation string

    - Source code in beancount/plugins/fix_payees.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    + Source code in beancount/plugins/check_commodity.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -4365,15 +1386,103 @@

    -beancount.plugins.fix_payees.fix_payees(entries, options_map, config) +

    +beancount.plugins.check_commodity.get_commodity_map_ex(entries, metadata=False) + + +

    + +
    + +

    Find and extract commodities in the stream of directives.

    + +
    + Source code in beancount/plugins/check_commodity.py +
    def get_commodity_map_ex(entries, metadata=False):
    +    """Find and extract commodities in the stream of directives."""
    +
    +    # Find commodity names in metadata.
    +    #
    +    # TODO(dnicolodi) Unfortunately detecting commodities in metadata
    +    # values may result in false positives: common used string are
    +    # matched by the regular expression. Revisit this when commodities
    +    # will be represented with their own type.
    +    ignore = set(["filename", "lineno", "__automatic__"])
    +    regexp = re.compile(CURRENCY_RE)
    +
    +    def currencies_in_meta(entry):
    +        if entry.meta is not None:
    +            for key, value in entry.meta.items():
    +                if isinstance(value, str) and key not in ignore:
    +                    if regexp.fullmatch(value):
    +                        yield value
    +
    +    commodities_map = {}
    +    occurrences = set()
    +    for entry in entries:
    +        if isinstance(entry, data.Commodity):
    +            commodities_map[entry.currency] = entry
    +
    +        elif isinstance(entry, data.Open):
    +            if entry.currencies:
    +                for currency in entry.currencies:
    +                    occurrences.add((entry.account, currency))
    +
    +        elif isinstance(entry, data.Transaction):
    +            for posting in entry.postings:
    +                # Main currency.
    +                units = posting.units
    +                occurrences.add((posting.account, units.currency))
    +
    +                # Currency in cost.
    +                cost = posting.cost
    +                if cost:
    +                    occurrences.add((posting.account, cost.currency))
    +
    +                # Currency in price.
    +                price = posting.price
    +                if price:
    +                    occurrences.add((posting.account, price.currency))
    +
    +                # Currency in posting metadata.
    +                if metadata:
    +                    for currency in currencies_in_meta(posting):
    +                        occurrences.add((posting.account, currency))
    +
    +        elif isinstance(entry, data.Balance):
    +            occurrences.add((entry.account, entry.amount.currency))
    +
    +        elif isinstance(entry, data.Price):
    +            occurrences.add((PRICE_CONTEXT, entry.currency))
    +            occurrences.add((PRICE_CONTEXT, entry.amount.currency))
    +
    +        # Entry metadata.
    +        if metadata:
    +            for currency in currencies_in_meta(entry):
    +                occurrences.add((METADATA_CONTEXT, currency))
    +
    +    return occurrences, commodities_map
    +
    +
    +
    + + + + + +
    + + + +

    +beancount.plugins.check_commodity.validate_commodity_directives(entries, options_map, config_str=None) -

    +

    -

    Rename payees based on a set of rules. See module docstring for details.

    +

    Find all commodities used and ensure they have a corresponding Commodity directive.

    @@ -4385,10 +1494,9 @@

    @@ -4404,91 +1512,121 @@

    Parameters:
      -
    • entries – a list of entry instances

    • -
    • options_map – a dict of options parsed from the file

    • -
    • config – A configuration string, which is intended to be a list of -(PAYEE, MATCH, ...) rules. See module docstring for details.

    • +
    • entries – A list of directives.

    • +
    • options_map – An options map.

    • +
    • config_str – The configuration as a string version of a float.

    Returns:
      -
    • A tuple of entries and errors.

    • +
    • A list of new errors, if any were found.

    - Source code in beancount/plugins/fix_payees.py -
    def fix_payees(entries, options_map, config):
    -    """Rename payees based on a set of rules. See module docstring for details.
    -
    -    Args:
    -      entries: a list of entry instances
    -      options_map: a dict of options parsed from the file
    -      config: A configuration string, which is intended to be a list of
    -        (PAYEE, MATCH, ...) rules. See module docstring for details.
    -    Returns:
    -      A tuple of entries and errors.
    -    """
    -    errors = []
    -    if config.strip():
    -        try:
    -            expr = ast.literal_eval(config)
    -        except (SyntaxError, ValueError):
    -            meta = data.new_metadata(options_map['filename'], 0)
    -            errors.append(FixPayeesError(meta,
    -                                         "Syntax error in config: {}".format(config),
    -                                         None))
    -            return entries, errors
    -    else:
    -        return entries, errors
    -
    -    # Pre-compile the regular expressions for performance.
    -    rules = []
    -    for rule in ast.literal_eval(config):
    -        clauses = iter(rule)
    -        new_payee = next(clauses)
    -        regexps = []
    -        for clause in clauses:
    -            match = re.match('([AD]):(.*)', clause)
    -            if not match:
    -                meta = data.new_metadata(options_map['filename'], 0)
    -                errors.append(FixPayeesError(meta,
    -                                             "Invalid clause: {}".format(clause),
    -                                             None))
    -                continue
    -            command, regexp = match.groups()
    -            regexps.append((command, re.compile(regexp, re.I).search))
    -        new_rule = [new_payee] + regexps
    -        rules.append(tuple(new_rule))
    -
    -    # Run the rules over the transaction objects.
    -    new_entries = []
    -    replaced_entries = {rule[0]: [] for rule in rules}
    -    for entry in entries:
    -        if isinstance(entry, data.Transaction):
    -            for rule in rules:
    -                clauses = iter(rule)
    -                new_payee = next(clauses)
    -
    -                # Attempt to match all the clauses.
    -                for clause in clauses:
    -                    command, func = clause
    -                    if command == 'D':
    -                        if not ((entry.payee is not None and func(entry.payee)) or
    -                                (entry.narration is not None and func(entry.narration))):
    -                            break
    -                    elif command == 'A':
    -                        if not any(func(posting.account) for posting in entry.postings):
    -                            break
    -                else:
    -                    # Make the replacement.
    -                    entry = entry._replace(payee=new_payee)
    -                    replaced_entries[new_payee].append(entry)
    -        new_entries.append(entry)
    -
    -    if _DEBUG:
    -        # Print debugging info.
    -        for payee, repl_entries in sorted(replaced_entries.items(),
    -                                          key=lambda x: len(x[1]),
    -                                          reverse=True):
    -            print('{:60}: {}'.format(payee, len(repl_entries)))
    -
    -    return new_entries, errors
    -
    + Source code in beancount/plugins/check_commodity.py +
    def validate_commodity_directives(entries, options_map, config_str=None):
    +    """Find all commodities used and ensure they have a corresponding Commodity directive.
    +
    +    Args:
    +      entries: A list of directives.
    +      options_map: An options map.
    +      config_str: The configuration as a string version of a float.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    errors = []
    +
    +    if config_str:
    +        config_obj = eval(config_str, {}, {})
    +        if not isinstance(config_obj, dict):
    +            errors.append(
    +                ConfigError(
    +                    data.new_metadata("<commodity_attr>", 0),
    +                    "Invalid configuration for check_commodity plugin; skipping.",
    +                    None,
    +                )
    +            )
    +            return entries, errors
    +    else:
    +        config_obj = {}
    +
    +    # Compile the regular expressions, producing an error if invalid.
    +    ignore_map = {}
    +    for key, value in config_obj.items():
    +        kv = []
    +        for pattern in key, value:
    +            try:
    +                kv.append(re.compile(pattern).match)
    +            except re.error:
    +                meta = data.new_metadata("<check_commodity>", 0)
    +                errors.append(
    +                    CheckCommodityError(
    +                        meta, "Invalid regexp: '{}' for {}".format(value, key), None
    +                    )
    +                )
    +        if len(kv) == 2:
    +            ignore_map[kv[0]] = kv[1]
    +
    +    # Get all the occurrences of commodities and a mapping of the directives.
    +    #
    +    # TODO(blais): Establish a distinction at the parser level for commodities
    +    # and strings, so that we can turn detection of them in metadata.
    +    occurrences, commodity_map = get_commodity_map_ex(entries, metadata=False)
    +
    +    # Process all currencies with context.
    +    issued = set()
    +    ignored = set()
    +    anonymous = set()
    +    for context, currency in sorted(occurrences):
    +        if context in ANONYMOUS:
    +            anonymous.add((context, currency))
    +            continue
    +        commodity_entry = commodity_map.get(currency, None)
    +
    +        # Skip if the commodity was declared, or if an error for that commodity
    +        # has already been issued.
    +        if commodity_entry is not None or currency in issued:
    +            continue
    +
    +        # If any of the ignore patterns matches, ignore and record ignored.
    +        if any(
    +            (context_re(context) and currency_re(currency))
    +            for context_re, currency_re in ignore_map.items()
    +        ):
    +            ignored.add(currency)
    +            continue
    +
    +        # Issue error.
    +        meta = data.new_metadata("<check_commodity>", 0)
    +        errors.append(
    +            CheckCommodityError(
    +                meta,
    +                "Missing Commodity directive for '{}' in '{}'".format(currency, context),
    +                None,
    +            )
    +        )
    +
    +        # Process it only once.
    +        issued.add(currency)
    +
    +    # Process all currencies out of context, automatically ignoring those which
    +    # have already been issued with account context..
    +    for context, currency in sorted(anonymous):
    +        commodity_entry = commodity_map.get(currency, None)
    +
    +        # Skip if (a) the commodity was declared, any of the ignore patterns
    +        # matches, or an error for that commodity has already been issued.
    +        if commodity_entry is not None or currency in issued or currency in ignored:
    +            continue
    +
    +        # Issue error.
    +        meta = data.new_metadata("<check_commodity>", 0)
    +        errors.append(
    +            CheckCommodityError(
    +                meta,
    +                "Missing Commodity directive for '{}' in '{}'".format(currency, context),
    +                None,
    +            )
    +        )
    +
    +    return entries, errors
    +
    @@ -4511,55 +1649,30 @@

    -

    - beancount.plugins.forecast +

    + beancount.plugins.check_drained -

    +

    -

    An example of adding a forecasting feature to Beancount via a plugin.

    -

    This entry filter plugin uses existing syntax to define and automatically -inserted transactions in the future based on a convention. It serves mostly as -an example of how you can experiment by creating and installing a local filter, -and not so much as a serious forecasting feature (though the experiment is a -good way to get something more general kickstarted eventually, I think the -concept would generalize nicely and should eventually be added as a common -feature of Beancount).

    -

    A user can create a transaction like this:

    -
    2014-03-08 # "Electricity bill [MONTHLY]"
    -  Expenses:Electricity                      50.10 USD
    -  Assets:Checking                          -50.10 USD
    -
    -

    and new transactions will be created monthly for the following year. -Note the use of the '#' flag and the word 'MONTHLY' which defines the -periodicity.

    -

    The number of recurrences can optionally be specified either by providing an -end date or by specifying the number of times that the transaction will be -repeated. For example:

    -
    2014-03-08 # "Electricity bill [MONTHLY UNTIL 2019-12-31]"
    -  Expenses:Electricity                      50.10 USD
    -  Assets:Checking                          -50.10 USD
    -
    -2014-03-08 # "Electricity bill [MONTHLY REPEAT 10 TIMES]"
    -  Expenses:Electricity                      50.10 USD
    -  Assets:Checking                          -50.10 USD
    -
    -

    Transactions can also be repeated at yearly intervals, e.g.:

    -
    2014-03-08 # "Electricity bill [YEARLY REPEAT 10 TIMES]"
    -  Expenses:Electricity                      50.10 USD
    -  Assets:Checking                          -50.10 USD
    +      

    Insert a balance check for zero before balance sheets accounts are closed.

    +

    For balance sheet accounts with a Close directive (Assets, Liabilities & +Equity), insert Balance directives just after its closing date, for all the +commodities that have appeared in that account and that are declared as legal on +it as well. This performs the equivalent of the following transformation:

    +
    2018-02-01 open  Assets:Project:Cash      USD,CAD
    +...
    +2020-02-01 close Assets:Project:Cash
     
    -

    Other examples:

    -
    2014-03-08 # "Electricity bill [WEEKLY SKIP 1 TIME REPEAT 10 TIMES]"
    -  Expenses:Electricity                      50.10 USD
    -  Assets:Checking                          -50.10 USD
    -
    -2014-03-08 # "Electricity bill [DAILY SKIP 3 TIMES REPEAT 1 TIME]"
    -  Expenses:Electricity                      50.10 USD
    -  Assets:Checking                          -50.10 USD
    +

    !!! to + 2018-02-01 open Assets:Project:Cash USD,CAD + ...

    +
    2020-02-01 close Assets:Project:Cash
    +2020-02-02 balance Assets:Project:Cash    0 USD
    +2020-02-02 balance Assets:Project:Cash    0 CAD
     
    @@ -4576,21 +1689,20 @@

    +
    -

    -beancount.plugins.forecast.forecast_plugin(entries, options_map) +

    +beancount.plugins.check_drained.check_drained(entries, options_map) -

    +

    -

    An example filter that piggybacks on top of the Beancount input syntax to -insert forecast entries automatically. This functions accepts the return -value of beancount.loader.load_file() and must return the same type of output.

    +

    Check that closed accounts are empty.

    @@ -4602,8 +1714,8 @@

    @@ -4619,83 +1731,74 @@

    Parameters:
      -
    • entries – a list of entry instances

    • -
    • options_map – a dict of options parsed from the file

    • +
    • entries – A list of directives.

    • +
    • unused_options_map – An options map.

    Returns:
      -
    • A tuple of entries and errors.

    • +
    • A list of new errors, if any were found.

    - Source code in beancount/plugins/forecast.py -
    def forecast_plugin(entries, options_map):
    -    """An example filter that piggybacks on top of the Beancount input syntax to
    -    insert forecast entries automatically. This functions accepts the return
    -    value of beancount.loader.load_file() and must return the same type of output.
    -
    -    Args:
    -      entries: a list of entry instances
    -      options_map: a dict of options parsed from the file
    -    Returns:
    -      A tuple of entries and errors.
    -    """
    -
    -    # Find the last entry's date.
    -    date_today = entries[-1].date
    -
    -    # Filter out forecast entries from the list of valid entries.
    -    forecast_entries = []
    -    filtered_entries = []
    -    for entry in entries:
    -        outlist = (forecast_entries
    -                   if (isinstance(entry, data.Transaction) and entry.flag == '#')
    -                   else filtered_entries)
    -        outlist.append(entry)
    -
    -    # Generate forecast entries up to the end of the current year.
    -    new_entries = []
    -    for entry in forecast_entries:
    -        # Parse the periodicity.
    -        match = re.search(r'(^.*)\[(MONTHLY|YEARLY|WEEKLY|DAILY)'
    -                          r'(\s+SKIP\s+([1-9][0-9]*)\s+TIME.?)'
    -                          r'?(\s+REPEAT\s+([1-9][0-9]*)\s+TIME.?)'
    -                          r'?(\s+UNTIL\s+([0-9\-]+))?\]', entry.narration)
    -        if not match:
    -            new_entries.append(entry)
    -            continue
    -        forecast_narration = match.group(1).strip()
    -        forecast_interval = (
    -            rrule.YEARLY if match.group(2).strip() == 'YEARLY'
    -            else rrule.WEEKLY if match.group(2).strip() == 'WEEKLY'
    -            else rrule.DAILY if match.group(2).strip() == 'DAILY'
    -            else rrule.MONTHLY)
    -        forecast_periodicity = {'dtstart': entry.date}
    -        if match.group(6):  # e.g., [MONTHLY REPEAT 3 TIMES]:
    -            forecast_periodicity['count'] = int(match.group(6))
    -        elif match.group(8):  # e.g., [MONTHLY UNTIL 2020-01-01]:
    -            forecast_periodicity['until'] = datetime.datetime.strptime(
    -                match.group(8), '%Y-%m-%d').date()
    -        else:
    -            # e.g., [MONTHLY]
    -            forecast_periodicity['until'] = datetime.date(
    -                datetime.date.today().year, 12, 31)
    -
    -        if match.group(4):
    -            # SKIP
    -            forecast_periodicity['interval'] = int(match.group(4)) + 1
    -
    -        # Generate a new entry for each forecast date.
    -        forecast_dates = [dt.date() for dt in rrule.rrule(forecast_interval,
    -                                                          **forecast_periodicity)]
    -        for forecast_date in forecast_dates:
    -            forecast_entry = entry._replace(date=forecast_date,
    -                                            narration=forecast_narration)
    -            new_entries.append(forecast_entry)
    -
    -    # Make sure the new entries inserted are sorted.
    -    new_entries.sort(key=data.entry_sortkey)
    -
    -    return (filtered_entries + new_entries, [])
    -
    + Source code in beancount/plugins/check_drained.py +
    def check_drained(entries, options_map):
    +    """Check that closed accounts are empty.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    acctypes = options.get_account_types(options_map)
    +    is_covered = functools.partial(
    +        account_types.is_balance_sheet_account, account_types=acctypes
    +    )
    +
    +    new_entries = []
    +    currencies = collections.defaultdict(set)
    +    balances = collections.defaultdict(set)
    +    for entry in entries:
    +        if isinstance(entry, data.Transaction):
    +            # Accumulate all the currencies seen in each account over time.
    +            for posting in entry.postings:
    +                if is_covered(posting.account):
    +                    currencies[posting.account].add(posting.units.currency)
    +
    +        elif isinstance(entry, data.Open):
    +            # Accumulate all the currencies declared in the account opening.
    +            if is_covered(entry.account) and entry.currencies:
    +                for currency in entry.currencies:
    +                    currencies[entry.account].add(currency)
    +
    +        elif isinstance(entry, data.Balance):
    +            # Ignore balances where directives are present.
    +            if is_covered(entry.account):
    +                balances[entry.account].add((entry.date, entry.amount.currency))
    +
    +        if isinstance(entry, data.Close):
    +            if is_covered(entry.account):
    +                for currency in currencies[entry.account]:
    +                    # Skip balance insertion due to the presence of an explicit one.
    +                    if (entry.date, currency) in balances[entry.account]:
    +                        continue
    +
    +                    # Insert a balance directive.
    +                    balance_entry = data.Balance(
    +                        # Note: We use the close directive's meta so that
    +                        # balance errors direct the user to the corresponding
    +                        # close directive.
    +                        entry.meta,
    +                        entry.date + ONE_DAY,
    +                        entry.account,
    +                        amount.Amount(ZERO, currency),
    +                        None,
    +                        None,
    +                    )
    +                    new_entries.append(balance_entry)
    +
    +        new_entries.append(entry)
    +
    +    return new_entries, []
    +
    @@ -4718,152 +1821,44 @@

    -

    - beancount.plugins.implicit_prices - - - -

    - -
    - -

    This plugin synthesizes Price directives for all Postings with a price or -directive or if it is an augmenting posting, has a cost directive.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.plugins.implicit_prices.ImplicitPriceError (tuple) - - - - -

    - -
    - -

    ImplicitPriceError(source, message, entry)

    - - - - -
    - - - - - - - - +

    + beancount.plugins.close_tree -
    - - - -

    -beancount.plugins.implicit_prices.ImplicitPriceError.__getnewargs__(self) - - special - -

    +

    -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/plugins/implicit_prices.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    +      

    This plugin inserts close directives for all of an account's descendants when +an account is closed. Unopened parent accounts can also be closed. Any +explicitly specified close is left untouched.

    +

    For example, given this::

    +
    2017-11-10 open Assets:Brokerage:AAPL
    +2017-11-10 open Assets:Brokerage:ORNG
    +2018-11-10 close Assets:Brokerage  ; this does not necessarily need to be opened
     
    -
    -
    - -
    - - - -
    - - - -

    -beancount.plugins.implicit_prices.ImplicitPriceError.__new__(_cls, source, message, entry) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of ImplicitPriceError(source, message, entry)

    - -
    - -
    - - - -
    - - - -

    -beancount.plugins.implicit_prices.ImplicitPriceError.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/plugins/implicit_prices.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    +

    the plugin turns it into::

    +
    2017-11-10 open Assets:Brokerage:AAPL
    +2017-11-10 open Assets:Brokerage:ORNG
    +2018-11-10 close Assets:Brokerage:AAPL
    +2018-11-10 close Assets:Brokerage:ORNG
    +
    +

    Invoke this plugin after any plugins that generate open directives for account trees +that you want to auto close. An example is the auto_accounts plugin that ships with +Beancount::

    +
    plugin "beancount.plugins.auto_accounts"
    +plugin "beancount.plugins.close_tree"
     
    -
    -
    -
    +
    -
    -
    -
    @@ -4872,18 +1867,15 @@

    -beancount.plugins.implicit_prices.add_implicit_prices(entries, unused_options_map) +

    +beancount.plugins.close_tree.close_tree(entries, unused_options_map) -

    +

    -

    Insert implicitly defined prices from Transactions.

    -

    Explicit price entries are simply maintained in the output list. Prices from -postings with costs or with prices from Transaction entries are synthesized -as new Price entries in the list of entries output.

    +

    Insert close entries for all subaccounts of a closed account.

    @@ -4895,7 +1887,7 @@

    Parameters:

    @@ -4912,213 +1904,116 @@

    Returns:

      -
    • entries – A list of directives. We're interested only in the Transaction instances.

    • +
    • entries – A list of directives. We're interested only in the Open/Close instances.

    • unused_options_map – A parser options dict.

      -
    • A list of entries, possibly with more Price entries than before, and a -list of errors.

    • +
    • A tuple of entries and errors.

    - Source code in beancount/plugins/implicit_prices.py -
    def add_implicit_prices(entries, unused_options_map):
    -    """Insert implicitly defined prices from Transactions.
    -
    -    Explicit price entries are simply maintained in the output list. Prices from
    -    postings with costs or with prices from Transaction entries are synthesized
    -    as new Price entries in the list of entries output.
    -
    -    Args:
    -      entries: A list of directives. We're interested only in the Transaction instances.
    -      unused_options_map: A parser options dict.
    -    Returns:
    -      A list of entries, possibly with more Price entries than before, and a
    -      list of errors.
    -    """
    -    new_entries = []
    -    errors = []
    -
    -    # A dict of (date, currency, cost-currency) to price entry.
    -    new_price_entry_map = {}
    -
    -    balances = collections.defaultdict(inventory.Inventory)
    -    for entry in entries:
    -        # Always replicate the existing entries.
    -        new_entries.append(entry)
    -
    -        if isinstance(entry, Transaction):
    -            # Inspect all the postings in the transaction.
    -            for posting in entry.postings:
    -                units = posting.units
    -                cost = posting.cost
    -
    -                # Check if the position is matching against an existing
    -                # position.
    -                _, booking = balances[posting.account].add_position(posting)
    -
    -                # Add prices when they're explicitly specified on a posting. An
    -                # explicitly specified price may occur in a conversion, e.g.
    -                #      Assets:Account    100 USD @ 1.10 CAD
    -                # or, if a cost is also specified, as the current price of the
    -                # underlying instrument, e.g.
    -                #      Assets:Account    100 HOOL {564.20} @ {581.97} USD
    -                if posting.price is not None:
    -                    meta = data.new_metadata(entry.meta["filename"], entry.meta["lineno"])
    -                    price_entry = data.Price(meta, entry.date,
    -                                             units.currency,
    -                                             posting.price)
    -
    -                # Add costs, when we're not matching against an existing
    -                # position. This happens when we're just specifying the cost,
    -                # e.g.
    -                #      Assets:Account    100 HOOL {564.20}
    -                elif (cost is not None and
    -                      booking != inventory.Booking.REDUCED):
    -                    meta = data.new_metadata(entry.meta["filename"], entry.meta["lineno"])
    -                    price_entry = data.Price(meta, entry.date,
    -                                             units.currency,
    -                                             amount.Amount(cost.number,
    -                                                           cost.currency))
    -
    -                else:
    -                    price_entry = None
    -
    -                if price_entry is not None:
    -                    key = (price_entry.date,
    -                           price_entry.currency,
    -                           price_entry.amount.number,  # Ideally should be removed.
    -                           price_entry.amount.currency)
    -                    try:
    -                        new_price_entry_map[key]
    -
    -                        ## Do not fail for now. We still have many valid use
    -                        ## cases of duplicate prices on the same date, for
    -                        ## example, stock splits, or trades on two dates with
    -                        ## two separate reported prices. We need to figure out a
    -                        ## more elegant solution for this in the long term.
    -                        ## Keeping both for now. We should ideally not use the
    -                        ## number in the de-dup key above.
    -                        #
    -                        # dup_entry = new_price_entry_map[key]
    -                        # if price_entry.amount.number == dup_entry.amount.number:
    -                        #     # Skip duplicates.
    -                        #     continue
    -                        # else:
    -                        #     errors.append(
    -                        #         ImplicitPriceError(
    -                        #             entry.meta,
    -                        #             "Duplicate prices for {} on {}".format(entry,
    -                        #                                                    dup_entry),
    -                        #             entry))
    -                    except KeyError:
    -                        new_price_entry_map[key] = price_entry
    -                        new_entries.append(price_entry)
    -
    -    return new_entries, errors
    -
    + Source code in beancount/plugins/close_tree.py +
    def close_tree(entries, unused_options_map):
    +    """Insert close entries for all subaccounts of a closed account.
    +
    +    Args:
    +      entries: A list of directives. We're interested only in the Open/Close instances.
    +      unused_options_map: A parser options dict.
    +    Returns:
    +      A tuple of entries and errors.
    +    """
    +
    +    new_entries = []
    +    errors = []
    +
    +    opens = set(entry.account for entry in entries if isinstance(entry, Open))
    +    closes = set(entry.account for entry in entries if isinstance(entry, Close))
    +
    +    for entry in entries:
    +        if isinstance(entry, Close):
    +            subaccounts = [
    +                account
    +                for account in opens
    +                if account.startswith(entry.account + ":") and account not in closes
    +            ]
    +            for subacc in subaccounts:
    +                meta = data.new_metadata("<beancount.plugins.close_tree>", 0)
    +                close_entry = data.Close(meta, entry.date, subacc)
    +                new_entries.append(close_entry)
    +                # So we don't attempt to re-close a grandchild that a child closed
    +                closes.add(subacc)
    +            if entry.account in opens:
    +                new_entries.append(entry)
    +        else:
    +            new_entries.append(entry)
    +
    +    return new_entries, errors
    +
    - + + + + + + + + + + + + + + + +
    + + + +

    + beancount.plugins.coherent_cost + + + +

    + +
    + +

    This plugin validates that currencies held at cost aren't ever converted at +price and vice-versa. This is usually the case, and using it will prevent users +from making the mistake of selling a lot without specifying it via its cost +basis.

    + +
    -
    -
    -
    -
    + +
    -

    - beancount.plugins.ira_contribs +

    + +beancount.plugins.coherent_cost.CoherentCostError (tuple) + -

    +

    -

    Automatically adding IRA contributions postings.

    -

    This plugin looks for increasing postings on specified accounts ('+' sign for -Assets and Expenses accounts, '-' sign for the others), or postings with a -particular flag on them and when it finds some, inserts a pair of postings on -that transaction of the corresponding amounts in a different currency. The -currency is intended to be an imaginary currency used to track the number of -dollars contributed to a retirement account over time.

    -

    For example, a possible configuration could be:

    -
    plugin "beancount.plugins.ira_contribs" "{
    -    'currency': 'IRAUSD',
    -    'flag': 'M',
    -    'accounts': {
    -
    -        'Income:US:Acme:Match401k': (
    -            'Assets:US:Federal:Match401k',
    -            'Expenses:Taxes:TY{year}:US:Federal:Match401k'),
    -
    -        ('C', 'Assets:US:Fidelity:PreTax401k:Cash'): (
    -            'Assets:US:Federal:PreTax401k',
    -            'Expenses:Taxes:TY{year}:US:Federal:PreTax401k'),
    -     }
    -}"
    -
    -

    Note: In this example, the configuration that triggers on the -"Income:US:Acme:Match401k" account does not require a flag for those accounts; -the configuration for the "Assets:US:Fidelity:PreTax401k:Cash" account requires -postings to have a "C" flag to trigger an insertion.

    -

    Given a transaction like the following, which would be typical for a salary -entry where the employer is automatically diverting some of the pre-tax money to -a retirement account (in this example, at Fidelity):

    -
    2013-02-15 * "ACME INC       PAYROLL"
    -  Income:US:Acme:Salary                   ...
    -  ...
    -  Assets:US:BofA:Checking                 ...
    -  Assets:US:Fidelity:PreTax401k:Cash      620.50 USD
    -  ...
    -
    -

    A posting with account 'Assets:US:Fidelity:PreTax401k:Cash', which is configured -to match, would be found. The configuration above instructs the plugin to -automatically insert new postings like this:

    -
    2013-02-15 * "ACME INC       PAYROLL"
    -  ...
    -  Assets:US:Fidelity:PreTax401k:Cash              620.50 USD
    -  M Assets:US:Federal:PreTax401k                 -620.50 IRAUSD
    -  M Expenses:Taxes:TY2013:US:Federal:PreTax401k   620.50 IRAUSD
    -  ...
    -
    -

    Notice that the "{year}" string in the configuration's account names is -automatically replaced by the current year in the account name. This is useful -if you maintain separate tax accounts per year.

    -

    Furthermore, as in the configuration example above, you may have multiple -matching entries to trigger multiple insertions. For example, the employer may -also match the employee's retirement contribution by depositing some money in -the retirement account:

    -
    2013-02-15 * "BUYMF - MATCH" "Employer match, invested in SaveEasy 2030 fund"
    -  Assets:US:Fidelity:Match401k:SE2030   34.793 SE2030 {17.834 USD}
    -  Income:US:Acme:Match401k             -620.50 USD
    -
    -

    In this example the funds get reported as invested immediately (an intermediate -deposit into a cash account does not take place). The plugin configuration would -match against the 'Income:US:Acme:Match401k' account and since it increases its -value (the normal balance of an Income account is negative), postings would be -inserted like this:

    -
    2013-02-15 * "BUYMF - MATCH" "Employer match, invested in SaveEasy 2030 fund"
    -  Assets:US:Fidelity:Match401k:SE2030              34.793 SE2030 {17.834 USD}
    -  Income:US:Acme:Match401k                        -620.50 USD
    -  M Assets:US:Federal:Match401k                   -620.50 IRAUSD
    -  M Expenses:Taxes:TY2013:US:Federal:Match401k     620.50 IRAUSD
    -
    -

    Note that the special dict keys 'currency' and 'flag' are used to -specify which currency to use for the inserted postings, and if set, which flag -to mark these postings with.

    +

    CoherentCostError(source, message, entry)

    + @@ -5132,141 +2027,83 @@

    +
    + +

    +beancount.plugins.coherent_cost.CoherentCostError.__getnewargs__(self) -
    + + special + + +

    +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/plugins/coherent_cost.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + +
    + + + +
    -

    -beancount.plugins.ira_contribs.add_ira_contribs(entries, options_map, config_str) +

    +beancount.plugins.coherent_cost.CoherentCostError.__new__(_cls, source, message, entry) + + + special + staticmethod + -

    +

    -

    Add legs for 401k employer match contributions.

    -

    See module docstring for an example configuration.

    +

    Create new instance of CoherentCostError(source, message, entry)

    + +
    + +
    + + + +
    + + + +

    +beancount.plugins.coherent_cost.CoherentCostError.__repr__(self) + + + special + + +

    + +
    + +

    Return a nicely formatted representation string

    - - - - - - - - - - - -
    Parameters: -
      -
    • entries – a list of entry instances

    • -
    • options_map – a dict of options parsed from the file

    • -
    • config_str – A configuration string, which is intended to be a Python dict -mapping match-accounts to a pair of (negative-account, position-account) -account names.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of entries and errors.

    • -
    -
    - Source code in beancount/plugins/ira_contribs.py -
    def add_ira_contribs(entries, options_map, config_str):
    -    """Add legs for 401k employer match contributions.
    -
    -    See module docstring for an example configuration.
    -
    -    Args:
    -      entries: a list of entry instances
    -      options_map: a dict of options parsed from the file
    -      config_str: A configuration string, which is intended to be a Python dict
    -        mapping match-accounts to a pair of (negative-account, position-account)
    -        account names.
    -    Returns:
    -      A tuple of entries and errors.
    -    """
    -    # Parse and extract configuration values.
    -    # FIXME: Use ast.literal_eval() here; you need to convert this code and the getters.
    -    # FIXME: Also, don't raise a RuntimeError, return an error object; review
    -    # this for all the plugins.
    -    # FIXME: This too is temporary.
    -    # pylint: disable=eval-used
    -    config_obj = eval(config_str, {}, {})
    -    if not isinstance(config_obj, dict):
    -        raise RuntimeError("Invalid plugin configuration: should be a single dict.")
    -
    -    # Currency of the inserted postings.
    -    currency = config_obj.pop('currency', 'UNKNOWN')
    -
    -    # Flag to attach to the inserted postings.
    -    insert_flag = config_obj.pop('flag', None)
    -
    -    # A dict of account names that trigger the insertion of postings to pairs of
    -    # inserted accounts when triggered.
    -    accounts = config_obj.pop('accounts', {})
    -
    -    # Convert the key in the accounts configuration for matching.
    -    account_transforms = {}
    -    for key, config in accounts.items():
    -        if isinstance(key, str):
    -            flag = None
    -            account = key
    -        else:
    -            assert isinstance(key, tuple)
    -            flag, account = key
    -        account_transforms[account] = (flag, config)
    -
    -    new_entries = []
    -    for entry in entries:
    -        if isinstance(entry, data.Transaction):
    -            orig_entry = entry
    -            for posting in entry.postings:
    -                if (posting.units is not MISSING and
    -                    (posting.account in account_transforms) and
    -                    (account_types.get_account_sign(posting.account) *
    -                     posting.units.number > 0)):
    -
    -                    # Get the new account legs to insert.
    -                    required_flag, (neg_account,
    -                                    pos_account) = account_transforms[posting.account]
    -                    assert posting.cost is None
    -
    -                    # Check required flag if present.
    -                    if (required_flag is None or
    -                        (required_flag and required_flag == posting.flag)):
    -                        # Insert income/expense entries for 401k.
    -                        entry = add_postings(
    -                            entry,
    -                            amount.Amount(abs(posting.units.number), currency),
    -                            neg_account.format(year=entry.date.year),
    -                            pos_account.format(year=entry.date.year),
    -                            insert_flag)
    -
    -            if DEBUG and orig_entry is not entry:
    -                printer.print_entry(orig_entry)
    -                printer.print_entry(entry)
    -
    -        new_entries.append(entry)
    -
    -    return new_entries, []
    -
    + Source code in beancount/plugins/coherent_cost.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -5274,19 +2111,30 @@

    + +

    + + + + + +
    -

    -beancount.plugins.ira_contribs.add_postings(entry, amount_, neg_account, pos_account, flag) +

    +beancount.plugins.coherent_cost.validate_coherent_cost(entries, unused_options_map) -

    +

    -

    Insert positive and negative postings of a position in an entry.

    +

    Check that all currencies are either used at cost or not at all, but never both.

    @@ -5298,11 +2146,8 @@

    @@ -5318,31 +2163,45 @@

    Parameters:
      -
    • entry – A Transaction instance.

    • -
    • amount_ – An Amount instance to create the position, with positive number.

    • -
    • neg_account – An account for the posting with the negative amount.

    • -
    • pos_account – An account for the posting with the positive amount.

    • -
    • flag – A string, that is to be set as flag for the new postings.

    • +
    • entries – A list of directives.

    • +
    • unused_options_map – An options map.

    Returns:
      -
    • A new, modified entry.

    • +
    • A list of new errors, if any were found.

    - Source code in beancount/plugins/ira_contribs.py -
    def add_postings(entry, amount_, neg_account, pos_account, flag):
    -    """Insert positive and negative postings of a position in an entry.
    -
    -    Args:
    -      entry: A Transaction instance.
    -      amount_: An Amount instance to create the position, with positive number.
    -      neg_account: An account for the posting with the negative amount.
    -      pos_account: An account for the posting with the positive amount.
    -      flag: A string, that is to be set as flag for the new postings.
    -    Returns:
    -      A new, modified entry.
    -    """
    -    return entry._replace(postings=entry.postings + [
    -        data.Posting(neg_account, -amount_, None, None, flag, None),
    -        data.Posting(pos_account, amount_, None, None, flag, None),
    -        ])
    -
    + Source code in beancount/plugins/coherent_cost.py +
    def validate_coherent_cost(entries, unused_options_map):
    +    """Check that all currencies are either used at cost or not at all, but never both.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    errors = []
    +
    +    with_cost = {}
    +    without_cost = {}
    +    for entry in data.filter_txns(entries):
    +        for posting in entry.postings:
    +            target_set = without_cost if posting.cost is None else with_cost
    +            currency = posting.units.currency
    +            target_set.setdefault(currency, entry)
    +
    +    for currency in set(with_cost) & set(without_cost):
    +        errors.append(
    +            CoherentCostError(
    +                without_cost[currency].meta,
    +                "Currency '{}' is used both with and without cost".format(currency),
    +                with_cost[currency],
    +            )
    +        )
    +        # Note: We really ought to include both of the first transactions here.
    +
    +    return entries, errors
    +
    @@ -5365,21 +2224,28 @@

    -

    - beancount.plugins.leafonly +

    + beancount.plugins.commodity_attr -

    +

    -

    A plugin that issues errors when amounts are posted to non-leaf accounts, -that is, accounts with child accounts.

    -

    This is an extra constraint that you may want to apply optionally. If you -install this plugin, it will issue errors for all accounts that have -postings to non-leaf accounts. Some users may want to disallow this and -enforce that only leaf accounts may have postings on them.

    +

    A plugin that asserts that all Commodity directives have a particular +attribute and that it is part of a set of enum values.

    +

    The configuration must be a mapping of attribute name to list of valid values, +like this:

    +
    plugin "beancount.plugins.commodity_attr" "{
    +  'sector': ['Technology', 'Financials', 'Energy'],
    +  'name': None,
    +}"
    +
    +

    The plugin issues an error if a Commodity directive is missing the attribute, or +if the attribute value is not in the valid set. If you'd like to just ensure the +attribute is set, set the list of valid values to None, as in the 'name' +attribute in the example above.

    @@ -5398,18 +2264,18 @@

    -

    +

    -beancount.plugins.leafonly.LeafOnlyError (tuple) +beancount.plugins.commodity_attr.CommodityError (tuple) -

    +

    -

    LeafOnlyError(source, message, entry)

    +

    CommodityError(source, message, entry)

    @@ -5428,25 +2294,25 @@

    -

    -beancount.plugins.leafonly.LeafOnlyError.__getnewargs__(self) +

    +beancount.plugins.commodity_attr.CommodityError.__getnewargs__(self) special -

    +

    Return self as a plain tuple. Used by copy and pickle.

    - Source code in beancount/plugins/leafonly.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    + Source code in beancount/plugins/commodity_attr.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -5458,19 +2324,19 @@

    -beancount.plugins.leafonly.LeafOnlyError.__new__(_cls, source, message, entry) +

    +beancount.plugins.commodity_attr.CommodityError.__new__(_cls, source, message, entry) special staticmethod -

    +

    -

    Create new instance of LeafOnlyError(source, message, entry)

    +

    Create new instance of CommodityError(source, message, entry)

    @@ -5482,25 +2348,25 @@

    -beancount.plugins.leafonly.LeafOnlyError.__repr__(self) +

    +beancount.plugins.commodity_attr.CommodityError.__repr__(self) special -

    +

    Return a nicely formatted representation string

    - Source code in beancount/plugins/leafonly.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    + Source code in beancount/plugins/commodity_attr.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -5518,87 +2384,59 @@

    -
    +

    + +beancount.plugins.commodity_attr.ConfigError (tuple) + -

    -beancount.plugins.leafonly.validate_leaf_only(entries, unused_options_map) -

    +

    -

    Check for non-leaf accounts that have postings on them.

    +

    ConfigError(source, message, entry)

    + + + + +
    + + + + + + + + + +
    + + + +

    +beancount.plugins.commodity_attr.ConfigError.__getnewargs__(self) + + + special + + +

    + +
    + +

    Return self as a plain tuple. Used by copy and pickle.

    - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • unused_options_map – An options map.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of new errors, if any were found.

    • -
    -
    - Source code in beancount/plugins/leafonly.py -
    def validate_leaf_only(entries, unused_options_map):
    -    """Check for non-leaf accounts that have postings on them.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    real_root = realization.realize(entries, compute_balance=False)
    -
    -    default_meta = data.new_metadata('<leafonly>', 0)
    -    open_close_map = None # Lazily computed.
    -    errors = []
    -    for real_account in realization.iter_children(real_root):
    -        if len(real_account) > 0 and real_account.txn_postings:
    -
    -            if open_close_map is None:
    -                open_close_map = getters.get_account_open_close(entries)
    -
    -            try:
    -                open_entry = open_close_map[real_account.account][0]
    -            except KeyError:
    -                open_entry = None
    -            errors.append(LeafOnlyError(
    -                open_entry.meta if open_entry else default_meta,
    -                "Non-leaf account '{}' has postings on it".format(real_account.account),
    -                open_entry))
    -
    -    return entries, errors
    -
    + Source code in beancount/plugins/commodity_attr.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -5606,10 +2444,23 @@

    +
    -
    +

    +beancount.plugins.commodity_attr.ConfigError.__new__(_cls, source, message, entry) + + + special + staticmethod + + +

    + +
    + +

    Create new instance of ConfigError(source, message, entry)

    @@ -5617,43 +2468,43 @@

    -
    - +
    -

    - beancount.plugins.mark_unverified +

    +beancount.plugins.commodity_attr.ConfigError.__repr__(self) + + special + -

    +

    -

    Add metadata to Postings which occur after their last Balance directives.

    -

    Some people use Balance directives as a way to indicate that all postings before -them are verified. They want to compute balances in each account as of the date -of that last Balance directives. One way to do that is to use this plugin to -mark the postings which occur after and to then filter them out using a WHERE -clause on that metadata:

    -
    SELECT account, sum(position) WHERE NOT meta("unverified")
    -
    -

    Note that doing such a filtering may result in a list of balances which may not -add to zero.

    -

    Also, postings for accounts without a single Balance directive on them will not -be marked as unverified as all (otherwise all the postings would be marked, this -would make no sense).

    - +

    Return a nicely formatted representation string

    +
    + Source code in beancount/plugins/commodity_attr.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    -
    +
    +
    +
    +
    @@ -5662,15 +2513,15 @@

    -

    -beancount.plugins.mark_unverified.mark_unverified(entries, options_map) +

    +beancount.plugins.commodity_attr.validate_commodity_attr(entries, unused_options_map, config_str) -

    +

    -

    Add metadata to postings after the last Balance entry. See module doc.

    +

    Check that all Commodity directives have a valid attribute.

    @@ -5682,8 +2533,9 @@

    Parameters:

    @@ -5699,50 +2551,71 @@

    Returns:

      -
    • entries – A list of data directives.

    • -
    • options_map – A dict of options, that confirms to beancount.parser.options.

    • +
    • entries – A list of directives.

    • +
    • unused_options_map – An options map.

    • +
    • config_str – A configuration string.

      -
    • 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.

    • +
    • A list of new errors, if any were found.

    - Source code in beancount/plugins/mark_unverified.py -
    def mark_unverified(entries, options_map):
    -    """Add metadata to postings after the last Balance entry. See module doc.
    -
    -    Args:
    -      entries: A list of data directives.
    -      options_map: A dict of options, that confirms to beancount.parser.options.
    -    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.
    -    """
    -    # The last Balance directive seen for each account.
    -    last_balances = {}
    -    for entry in entries:
    -        if isinstance(entry, data.Balance):
    -            last_balances[entry.account] = entry
    -
    -    new_entries = []
    -    for entry in entries:
    -        if isinstance(entry, data.Transaction):
    -            postings = entry.postings
    -            new_postings = postings
    -            for index, posting in enumerate(postings):
    -                balance = last_balances.get(posting.account, None)
    -                if balance and balance.date <= entry.date:
    -                    if new_postings is postings:
    -                        new_postings = postings.copy()
    -                    new_meta = posting.meta.copy()
    -                    new_meta['unverified'] = True
    -                    new_postings[index] = posting._replace(meta=new_meta)
    -            if new_postings is not postings:
    -                entry = entry._replace(postings=new_postings)
    -        new_entries.append(entry)
    -
    -    return new_entries, []
    -
    + Source code in beancount/plugins/commodity_attr.py +
    def validate_commodity_attr(entries, unused_options_map, config_str):
    +    """Check that all Commodity directives have a valid attribute.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +      config_str: A configuration string.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    errors = []
    +
    +    config_obj = eval(config_str, {}, {})
    +    if not isinstance(config_obj, dict):
    +        errors.append(
    +            ConfigError(
    +                data.new_metadata("<commodity_attr>", 0),
    +                "Invalid configuration for commodity_attr plugin; skipping.",
    +                None,
    +            )
    +        )
    +        return entries, errors
    +
    +    validmap = {
    +        attr: frozenset(values) if values is not None else None
    +        for attr, values in config_obj.items()
    +    }
    +    for entry in entries:
    +        if not isinstance(entry, data.Commodity):
    +            continue
    +        for attr, values in validmap.items():
    +            value = entry.meta.get(attr, None)
    +            if value is None:
    +                errors.append(
    +                    CommodityError(
    +                        entry.meta,
    +                        "Missing attribute '{}' for Commodity directive {}".format(
    +                            attr, entry.currency
    +                        ),
    +                        None,
    +                    )
    +                )
    +                continue
    +            if values and value not in values:
    +                errors.append(
    +                    CommodityError(
    +                        entry.meta,
    +                        "Invalid value '{}' for attribute {}, Commodity".format(value, attr)
    +                        + " directive {}; valid options: {}".format(
    +                            entry.currency, ", ".join(values)
    +                        ),
    +                        None,
    +                    )
    +                )
    +
    +    return entries, errors
    +
    @@ -5765,31 +2638,37 @@

    - beancount.plugins.merge_meta +

    + beancount.plugins.currency_accounts -

    +

    -

    Merge the metadata from a second file into the current set of entries.

    -

    This is useful if you like to keep more sensitive private data, such as account -numbers or passwords, in a second, possibly encrypted file. This can be used to -generate a will, for instance, for your loved ones to be able to figure where -all your assets are in case you pass away. You can store all the super secret -stuff in a more closely guarded, hidden away separate file.

    -

    The metadata from

    -
      -
    • Open directives: Account name must match.
    • -
    • Close directives: Account name must match.
    • -
    • Commodity directives: Currency must match.
    • -
    -

    are copied over. Metadata from the external file conflicting with that present -in the main file overwrites it (external data wins).

    -

    WARNING! If you include an encrypted file and the main file is not encrypted, - the contents extraction from the encrypted file may appear in the cache.

    +

    An implementation of currency accounts.

    +

    This is an automatic implementation of the method described here: +https://www.mathstat.dal.ca/~selinger/accounting/tutorial.html

    +

    You enable it just like this:

    +
    plugin "beancount.plugins.currency_accounts" "Equity:CurrencyAccounts"
    +
    +

    Accounts will be automatically created under the given base account, with the +currency name appended to it, e.g.,

    +
    Equity:CurrencyAccounts:CAD
    +Equity:CurrencyAccounts:USD
    +
    +

    etc., where used. You can have a look at the account balances with a query like +this:

    +
    bean-query $L "select account, sum(position), convert(sum(position), 'USD')
    +               where date &gt;= 2018-01-01 and  account ~ 'CurrencyAccounts' "
    +
    +

    The sum total of the converted amounts should be a number not too large:

    +
    bean-query $L "select convert(sum(position), 'USD')
    +               where date &gt;= 2018-01-01 and  account ~ 'CurrencyAccounts'"
    +
    +

    WARNING: This is a prototype. Note the FIXMEs in the code below, which indicate +some potential problems.

    @@ -5805,19 +2684,21 @@

    + +
    -

    -beancount.plugins.merge_meta.merge_meta(entries, options_map, config) +

    +beancount.plugins.currency_accounts.get_neutralizing_postings(curmap, base_account, new_accounts) -

    +

    -

    Load a secondary file and merge its metadata in our given set of entries.

    +

    Process an entry.

    @@ -5829,9 +2710,9 @@

    @@ -5847,56 +2728,64 @@

    Parameters:
      -
    • entries – A list of directives. We're interested only in the Transaction instances.

    • -
    • unused_options_map – A parser options dict.

    • -
    • config – The plugin configuration string.

    • +
    • curmap – A dict of currency to a list of Postings of this transaction.

    • +
    • base_account – A string, the root account name to insert.

    • +
    • new_accounts – A set, a mutable accumulator of new account names.

    Returns:
      -
    • A list of entries, with more metadata attached to them.

    • +
    • A modified entry, with new postings inserted to rebalance currency trading +accounts.

    - Source code in beancount/plugins/merge_meta.py -
    def merge_meta(entries, options_map, config):
    -    """Load a secondary file and merge its metadata in our given set of entries.
    -
    -    Args:
    -      entries: A list of directives. We're interested only in the Transaction instances.
    -      unused_options_map: A parser options dict.
    -      config: The plugin configuration string.
    -    Returns:
    -      A list of entries, with more metadata attached to them.
    -    """
    -    external_filename = config
    -    new_entries = list(entries)
    -
    -    ext_entries, ext_errors, ext_options_map = loader.load_file(external_filename)
    -
    -    # Map Open and Close directives.
    -    oc_map = getters.get_account_open_close(entries)
    -    ext_oc_map = getters.get_account_open_close(ext_entries)
    -    for account in set(oc_map.keys()) & set(ext_oc_map.keys()):
    -        open_entry, close_entry = oc_map[account]
    -        ext_open_entry, ext_close_entry = ext_oc_map[account]
    -        if open_entry and ext_open_entry:
    -            open_entry.meta.update(ext_open_entry.meta)
    -        if close_entry and ext_close_entry:
    -            close_entry.meta.update(ext_close_entry.meta)
    -
    -    # Map Commodity directives.
    -    comm_map = getters.get_commodity_map(entries, False)
    -    ext_comm_map = getters.get_commodity_map(ext_entries, False)
    -    for currency in set(comm_map) & set(ext_comm_map):
    -        comm_entry = comm_map[currency]
    -        ext_comm_entry = ext_comm_map[currency]
    -        if comm_entry and ext_comm_entry:
    -            comm_entry.meta.update(ext_comm_entry.meta)
    -
    -    # Note: We cannot include the external file in the list of inputs so that a
    -    # change of it triggers a cache rebuild because side-effects on options_map
    -    # aren't cascaded through. This is something that should be defined better
    -    # in the plugin interface and perhaps improved upon.
    -
    -    return new_entries, ext_errors
    -
    + Source code in beancount/plugins/currency_accounts.py +
    def get_neutralizing_postings(curmap, base_account, new_accounts):
    +    """Process an entry.
    +
    +    Args:
    +      curmap: A dict of currency to a list of Postings of this transaction.
    +      base_account: A string, the root account name to insert.
    +      new_accounts: A set, a mutable accumulator of new account names.
    +    Returns:
    +      A modified entry, with new postings inserted to rebalance currency trading
    +      accounts.
    +    """
    +    new_postings = []
    +    for currency, postings in curmap.items():
    +        # Compute the per-currency balance.
    +        inv = inventory.Inventory()
    +        for posting in postings:
    +            inv.add_amount(convert.get_cost(posting))
    +        if inv.is_empty():
    +            new_postings.extend(postings)
    +            continue
    +
    +        # Re-insert original postings and remove price conversions.
    +        #
    +        # Note: This may cause problems if the implicit_prices plugin is
    +        # configured to run after this one, or if you need the price annotations
    +        # for some scripting or serious work.
    +        #
    +        # FIXME: We need to handle these important cases (they're not frivolous,
    +        # this is a prototype), probably by inserting some exceptions with
    +        # collaborating code in the booking (e.g. insert some metadata that
    +        # disables price conversions on those postings).
    +        #
    +        # FIXME(2): Ouch! Some of the residual seeps through here, where there
    +        # are more than a single currency block. This needs fixing too. You can
    +        # easily mitigate some of this to some extent, by excluding transactions
    +        # which don't have any price conversion in them.
    +        for pos in postings:
    +            if pos.price is not None:
    +                pos = pos._replace(price=None)
    +            new_postings.append(pos)
    +
    +        # Insert the currency trading accounts postings.
    +        amount = inv.get_only_position().units
    +        acc = account.join(base_account, currency)
    +        new_accounts.add(acc)
    +        new_postings.append(Posting(acc, -amount, None, None, None, None))
    +
    +    return new_postings
    +
    @@ -5904,43 +2793,44 @@

    +
    -
    - -

    - - - - - -
    - - - -

    - beancount.plugins.noduplicates - +

    +beancount.plugins.currency_accounts.group_postings_by_weight_currency(entry) -

    +

    -

    This plugin validates that there are no duplicate transactions.

    - - - -
    - - - - - - +

    Return where this entry might require adjustment.

    +
    + Source code in beancount/plugins/currency_accounts.py +
    def group_postings_by_weight_currency(entry: Transaction):
    +    """Return where this entry might require adjustment."""
    +    curmap = collections.defaultdict(list)
    +    has_price = False
    +    for posting in entry.postings:
    +        currency = posting.units.currency
    +        if posting.cost:
    +            currency = posting.cost.currency
    +            if posting.price:
    +                assert posting.price.currency == currency
    +            elif posting.price:
    +                has_price = True
    +                currency = posting.price.currency
    +        if posting.price:
    +            has_price = True
    +        curmap[currency].append(posting)
    +    return curmap, has_price
    +
    +
    +
    +
    @@ -5948,15 +2838,15 @@

    -

    -beancount.plugins.noduplicates.validate_no_duplicates(entries, unused_options_map) +

    +beancount.plugins.currency_accounts.insert_currency_trading_postings(entries, options_map, config) -

    +

    -

    Check that the entries are unique, by computing hashes.

    +

    Insert currency trading postings.

    @@ -5970,6 +2860,7 @@

  • entries – A list of directives.

  • unused_options_map – An options map.

  • +
  • config – The base account name for currency trading accounts.

  • @@ -5992,19 +2883,44 @@

    - Source code in beancount/plugins/noduplicates.py -
    def validate_no_duplicates(entries, unused_options_map):
    -    """Check that the entries are unique, by computing hashes.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    unused_hashes, errors = compare.hash_entries(entries, exclude_meta=True)
    -    return entries, errors
    -
    + Source code in beancount/plugins/currency_accounts.py +
    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
    +
    @@ -6027,16 +2943,17 @@

    - beancount.plugins.nounused +

    + beancount.plugins.implicit_prices -

    +
    -

    This plugin validates that there are no unused accounts.

    +

    This plugin synthesizes Price directives for all Postings with a price or +directive or if it is an augmenting posting, has a cost directive.

    @@ -6051,22 +2968,23 @@

    +
    -

    +

    -beancount.plugins.nounused.UnusedAccountError (tuple) +beancount.plugins.implicit_prices.ImplicitPriceError (tuple) -

    +

    -

    UnusedAccountError(source, message, entry)

    +

    ImplicitPriceError(source, message, entry)

    @@ -6085,25 +3003,25 @@

    -

    -beancount.plugins.nounused.UnusedAccountError.__getnewargs__(self) +

    +beancount.plugins.implicit_prices.ImplicitPriceError.__getnewargs__(self) special -

    +

    Return self as a plain tuple. Used by copy and pickle.

    - Source code in beancount/plugins/nounused.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    + Source code in beancount/plugins/implicit_prices.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -6115,19 +3033,19 @@

    -beancount.plugins.nounused.UnusedAccountError.__new__(_cls, source, message, entry) +

    +beancount.plugins.implicit_prices.ImplicitPriceError.__new__(_cls, source, message, entry) special staticmethod -

    +
    -

    Create new instance of UnusedAccountError(source, message, entry)

    +

    Create new instance of ImplicitPriceError(source, message, entry)

    @@ -6139,25 +3057,25 @@

    -beancount.plugins.nounused.UnusedAccountError.__repr__(self) +

    +beancount.plugins.implicit_prices.ImplicitPriceError.__repr__(self) special -

    +

    Return a nicely formatted representation string

    - Source code in beancount/plugins/nounused.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    + Source code in beancount/plugins/implicit_prices.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -6180,24 +3098,18 @@

    -beancount.plugins.nounused.validate_unused_accounts(entries, unused_options_map) +

    +beancount.plugins.implicit_prices.add_implicit_prices(entries, unused_options_map) -

    +
    -

    Check that all accounts declared open are actually used.

    -

    We check that all of the accounts that are open are at least referred to by -another directive. These are probably unused, so issue a warning (we like to -be pedantic). Note that an account that is open and then closed is -considered used--this is a valid use case that may occur in reality. If you -have a use case for an account to be open but never used, you can quiet that -warning by initializing the account with a balance asserts or a pad -directive, or even use a note will be sufficient.

    -

    (This is probably a good candidate for optional inclusion as a "pedantic" -plugin.)

    +

    Insert implicitly defined prices from Transactions.

    +

    Explicit price entries are simply maintained in the output list. Prices from +postings with costs or with prices from Transaction entries are synthesized +as new Price entries in the list of entries output.

    @@ -6209,8 +3121,8 @@

    Parameters:

    @@ -6226,53 +3138,117 @@

    Returns:

      -
    • entries – A list of directives.

    • -
    • unused_options_map – An options map.

    • +
    • entries – A list of directives. We're interested only in the Transaction instances.

    • +
    • unused_options_map – A parser options dict.

      -
    • A list of new errors, if any were found.

    • +
    • A list of entries, possibly with more Price entries than before, and a +list of errors.

    - Source code in beancount/plugins/nounused.py -
    def validate_unused_accounts(entries, unused_options_map):
    -    """Check that all accounts declared open are actually used.
    -
    -    We check that all of the accounts that are open are at least referred to by
    -    another directive. These are probably unused, so issue a warning (we like to
    -    be pedantic). Note that an account that is open and then closed is
    -    considered used--this is a valid use case that may occur in reality. If you
    -    have a use case for an account to be open but never used, you can quiet that
    -    warning by initializing the account with a balance asserts or a pad
    -    directive, or even use a note will be sufficient.
    -
    -    (This is probably a good candidate for optional inclusion as a "pedantic"
    -    plugin.)
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    # Find all the accounts referenced by entries which are not Open, and the
    -    # open directives for error reporting below.
    -    open_map = {}
    -    referenced_accounts = set()
    -    for entry in entries:
    -        if isinstance(entry, data.Open):
    -            open_map[entry.account] = entry
    -            continue
    -        referenced_accounts.update(getters.get_entry_accounts(entry))
    -
    -    # Create a list of suitable errors, with the location of the Open directives
    -    # corresponding to the unused accounts.
    -    errors = [UnusedAccountError(open_entry.meta,
    -                                 "Unused account '{}'".format(account),
    -                                 open_entry)
    -              for account, open_entry in open_map.items()
    -              if account not in referenced_accounts]
    -    return entries, errors
    -
    + Source code in beancount/plugins/implicit_prices.py +
    def add_implicit_prices(entries, unused_options_map):
    +    """Insert implicitly defined prices from Transactions.
    +
    +    Explicit price entries are simply maintained in the output list. Prices from
    +    postings with costs or with prices from Transaction entries are synthesized
    +    as new Price entries in the list of entries output.
    +
    +    Args:
    +      entries: A list of directives. We're interested only in the Transaction instances.
    +      unused_options_map: A parser options dict.
    +    Returns:
    +      A list of entries, possibly with more Price entries than before, and a
    +      list of errors.
    +    """
    +    new_entries = []
    +    errors = []
    +
    +    # A dict of (date, currency, cost-currency) to price entry.
    +    new_price_entry_map = {}
    +
    +    balances = collections.defaultdict(inventory.Inventory)
    +    for entry in entries:
    +        # Always replicate the existing entries.
    +        new_entries.append(entry)
    +
    +        if isinstance(entry, Transaction):
    +            # Inspect all the postings in the transaction.
    +            for posting in entry.postings:
    +                units = posting.units
    +                cost = posting.cost
    +
    +                # Check if the position is matching against an existing
    +                # position.
    +                _, booking = balances[posting.account].add_position(posting)
    +
    +                # Add prices when they're explicitly specified on a posting. An
    +                # explicitly specified price may occur in a conversion, e.g.
    +                #      Assets:Account    100 USD @ 1.10 CAD
    +                # or, if a cost is also specified, as the current price of the
    +                # underlying instrument, e.g.
    +                #      Assets:Account    100 HOOL {564.20} @ {581.97} USD
    +                if posting.price is not None:
    +                    meta = data.new_metadata(entry.meta["filename"], entry.meta["lineno"])
    +                    meta[METADATA_FIELD] = "from_price"
    +                    price_entry = data.Price(
    +                        meta, entry.date, units.currency, posting.price
    +                    )
    +
    +                # Add costs, when we're not matching against an existing
    +                # position. This happens when we're just specifying the cost,
    +                # e.g.
    +                #      Assets:Account    100 HOOL {564.20}
    +                elif cost is not None and booking != inventory.MatchResult.REDUCED:
    +                    # TODO(blais): What happens here if the account has no
    +                    # booking strategy? Do we end up inserting a price for the
    +                    # reducing leg?  Check.
    +                    meta = data.new_metadata(entry.meta["filename"], entry.meta["lineno"])
    +                    meta[METADATA_FIELD] = "from_cost"
    +                    price_entry = data.Price(
    +                        meta,
    +                        entry.date,
    +                        units.currency,
    +                        amount.Amount(cost.number, cost.currency),
    +                    )
    +                else:
    +                    price_entry = None
    +
    +                if price_entry is not None:
    +                    key = (
    +                        price_entry.date,
    +                        price_entry.currency,
    +                        price_entry.amount.number,  # Ideally should be removed.
    +                        price_entry.amount.currency,
    +                    )
    +                    try:
    +                        new_price_entry_map[key]
    +
    +                        ## Do not fail for now. We still have many valid use
    +                        ## cases of duplicate prices on the same date, for
    +                        ## example, stock splits, or trades on two dates with
    +                        ## two separate reported prices. We need to figure out a
    +                        ## more elegant solution for this in the long term.
    +                        ## Keeping both for now. We should ideally not use the
    +                        ## number in the de-dup key above.
    +                        #
    +                        # dup_entry = new_price_entry_map[key]
    +                        # if price_entry.amount.number == dup_entry.amount.number:
    +                        #     # Skip duplicates.
    +                        #     continue
    +                        # else:
    +                        #     errors.append(
    +                        #         ImplicitPriceError(
    +                        #             entry.meta,
    +                        #             "Duplicate prices for {} on {}".format(entry,
    +                        #                                                    dup_entry),
    +                        #             entry))
    +                    except KeyError:
    +                        new_price_entry_map[key] = price_entry
    +                        new_entries.append(price_entry)
    +
    +    return new_entries, errors
    +
    @@ -6295,34 +3271,21 @@

    - beancount.plugins.onecommodity +

    + beancount.plugins.leafonly -

    +

    -

    A plugin that issues errors when more than one commodity is used in an account.

    -

    For investments or trading accounts, it can make it easier to filter the action -around a single stock by using the name of the stock as the leaf of the account -name.

    -

    Notes:

    -
      -
    • -

      The plugin will automatically skip accounts that have explicitly declared - commodities in their Open directive.

      -
    • -
    • -

      You can also set the metadata "onecommodity: FALSE" on an account's Open - directive to skip the checks for that account.

      -
    • -
    • -

      If provided, the configuration should be a regular expression restricting the - set of accounts to check.

      -
    • -
    +

    A plugin that issues errors when amounts are posted to non-leaf accounts, +that is, accounts with child accounts.

    +

    This is an extra constraint that you may want to apply optionally. If you +install this plugin, it will issue errors for all accounts that have +postings to non-leaf accounts. Some users may want to disallow this and +enforce that only leaf accounts may have postings on them.

    @@ -6341,18 +3304,18 @@

    -

    +

    -beancount.plugins.onecommodity.OneCommodityError (tuple) +beancount.plugins.leafonly.LeafOnlyError (tuple) -

    +

    -

    OneCommodityError(source, message, entry)

    +

    LeafOnlyError(source, message, entry)

    @@ -6371,25 +3334,25 @@

    -beancount.plugins.onecommodity.OneCommodityError.__getnewargs__(self) +

    +beancount.plugins.leafonly.LeafOnlyError.__getnewargs__(self) special -

    +

    Return self as a plain tuple. Used by copy and pickle.

    - Source code in beancount/plugins/onecommodity.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    + Source code in beancount/plugins/leafonly.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -6401,19 +3364,19 @@

    -beancount.plugins.onecommodity.OneCommodityError.__new__(_cls, source, message, entry) +

    +beancount.plugins.leafonly.LeafOnlyError.__new__(_cls, source, message, entry) special staticmethod -

    +

    -

    Create new instance of OneCommodityError(source, message, entry)

    +

    Create new instance of LeafOnlyError(source, message, entry)

    @@ -6425,25 +3388,25 @@

    -beancount.plugins.onecommodity.OneCommodityError.__repr__(self) +

    +beancount.plugins.leafonly.LeafOnlyError.__repr__(self) special -

    +

    Return a nicely formatted representation string

    -
    - Source code in beancount/plugins/onecommodity.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    + Source code in beancount/plugins/leafonly.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -6466,19 +3429,15 @@

    -beancount.plugins.onecommodity.validate_one_commodity(entries, unused_options_map, config=None) +

    +beancount.plugins.leafonly.validate_leaf_only(entries, unused_options_map) -

    +

    -

    Check that each account has units in only a single commodity.

    -

    This is an extra constraint that you may want to apply optionally, despite -Beancount's ability to support inventories and aggregations with more than -one commodity. I believe this also matches GnuCash's model, where each -account has a single commodity attached to it.

    +

    Check for non-leaf accounts that have postings on them.

    @@ -6492,8 +3451,6 @@

  • entries – A list of directives.

  • unused_options_map – An options map.

  • -
  • config – The plugin configuration string, a regular expression to match -against the subset of accounts to check.

  • @@ -6516,98 +3473,40 @@

    - Source code in beancount/plugins/onecommodity.py -
    def validate_one_commodity(entries, unused_options_map, config=None):
    -    """Check that each account has units in only a single commodity.
    -
    -    This is an extra constraint that you may want to apply optionally, despite
    -    Beancount's ability to support inventories and aggregations with more than
    -    one commodity. I believe this also matches GnuCash's model, where each
    -    account has a single commodity attached to it.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -      config: The plugin configuration string, a regular expression to match
    -        against the subset of accounts to check.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    accounts_re = re.compile(config) if config else None
    -
    -    # Mappings of account name to lists of currencies for each units and cost.
    -    units_map = collections.defaultdict(set)
    -    cost_map = collections.defaultdict(set)
    -
    -    # Mappings to use just for getting a relevant source.
    -    units_source_map = {}
    -    cost_source_map = {}
    -
    -    # Gather the set of accounts to skip from the Open directives.
    -    skip_accounts = set()
    -    for entry in entries:
    -        if not isinstance(entry, data.Open):
    -            continue
    -        if (not entry.meta.get("onecommodity", True) or
    -            (accounts_re and not accounts_re.match(entry.account)) or
    -            (entry.currencies and len(entry.currencies) > 1)):
    -            skip_accounts.add(entry.account)
    -
    -    # Accumulate all the commodities used.
    -    for entry in entries:
    -        if isinstance(entry, data.Transaction):
    -            for posting in entry.postings:
    -                if posting.account in skip_accounts:
    -                    continue
    -
    -                units = posting.units
    -                units_map[posting.account].add(units.currency)
    -                if len(units_map[posting.account]) > 1:
    -                    units_source_map[posting.account] = entry
    -
    -                cost = posting.cost
    -                if cost:
    -                    cost_map[posting.account].add(cost.currency)
    -                    if len(cost_map[posting.account]) > 1:
    -                        units_source_map[posting.account] = entry
    -
    -        elif isinstance(entry, data.Balance):
    -            if entry.account in skip_accounts:
    -                continue
    -
    -            units_map[entry.account].add(entry.amount.currency)
    -            if len(units_map[entry.account]) > 1:
    -                units_source_map[entry.account] = entry
    -
    -        elif isinstance(entry, data.Open):
    -            if entry.currencies and len(entry.currencies) > 1:
    -                skip_accounts.add(entry.account)
    -
    -    # Check units.
    -    errors = []
    -    for account, currencies in units_map.items():
    -        if account in skip_accounts:
    -            continue
    -        if len(currencies) > 1:
    -            errors.append(OneCommodityError(
    -                units_source_map[account].meta,
    -                "More than one currency in account '{}': {}".format(
    -                    account, ','.join(currencies)),
    -                None))
    -
    -    # Check costs.
    -    for account, currencies in cost_map.items():
    -        if account in skip_accounts:
    -            continue
    -        if len(currencies) > 1:
    -            errors.append(OneCommodityError(
    -                cost_source_map[account].meta,
    -                "More than one cost currency in account '{}': {}".format(
    -                    account, ','.join(currencies)),
    -                None))
    -
    -    return entries, errors
    -
    + Source code in beancount/plugins/leafonly.py +
    def validate_leaf_only(entries, unused_options_map):
    +    """Check for non-leaf accounts that have postings on them.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    real_root = realization.realize(entries, compute_balance=False)
    +
    +    default_meta = data.new_metadata("<leafonly>", 0)
    +    open_close_map = None  # Lazily computed.
    +    errors = []
    +    for real_account in realization.iter_children(real_root):
    +        if len(real_account) > 0 and real_account.txn_postings:
    +            if open_close_map is None:
    +                open_close_map = getters.get_account_open_close(entries)
    +
    +            try:
    +                open_entry = open_close_map[real_account.account][0]
    +            except KeyError:
    +                open_entry = None
    +            errors.append(
    +                LeafOnlyError(
    +                    open_entry.meta if open_entry else default_meta,
    +                    "Non-leaf account '{}' has postings on it".format(real_account.account),
    +                    open_entry,
    +                )
    +            )
    +
    +    return entries, errors
    +
    @@ -6630,18 +3529,16 @@

    - beancount.plugins.pedantic +

    + beancount.plugins.noduplicates -

    +
    -

    A plugin of plugins which triggers are all the pedantic plugins.

    -

    In a sense, this is the inverse of "pedantic." This is useful when doing some -types of quick and dirty tests.

    +

    This plugin validates that there are no duplicate transactions.

    @@ -6657,108 +3554,99 @@

    +
    -
    - -

    - - - - - -
    - - - -

    - beancount.plugins.sellgains - +

    +beancount.plugins.noduplicates.validate_no_duplicates(entries, unused_options_map) -

    +
    -

    A plugin that cross-checks declared gains against prices on lot sales.

    -

    When you sell stock, the gains can be automatically implied by the corresponding -cash amounts. For example, in the following transaction the 2nd and 3rd postings -should match the value of the stock sold:

    -
    1999-07-31 * "Sell"
    -  Assets:US:BRS:Company:ESPP          -81 ADSK {26.3125 USD}
    -  Assets:US:BRS:Company:Cash      2141.36 USD
    -  Expenses:Financial:Fees            0.08 USD
    -  Income:US:Company:ESPP:PnL      -10.125 USD
    -
    -

    The cost basis is checked against: 2141.36 + 008 + -10.125. That is, the balance -checks computes

    -

    -81 x 26.3125 = -2131.3125 + - 2141.36 + - 0.08 + - -10.125

    -

    and checks that the residual is below a small tolerance.

    -

    But... usually the income leg isn't given to you in statements. Beancount can -automatically infer it using the balance, which is convenient, like this:

    -
    1999-07-31 * "Sell"
    -  Assets:US:BRS:Company:ESPP          -81 ADSK {26.3125 USD}
    -  Assets:US:BRS:Company:Cash      2141.36 USD
    -  Expenses:Financial:Fees            0.08 USD
    -  Income:US:Company:ESPP:PnL
    -
    -

    Additionally, most often you have the sales prices given to you on your -transaction confirmation statement, so you can enter this:

    -
    1999-07-31 * "Sell"
    -  Assets:US:BRS:Company:ESPP          -81 ADSK {26.3125 USD} @ 26.4375 USD
    -  Assets:US:BRS:Company:Cash      2141.36 USD
    -  Expenses:Financial:Fees            0.08 USD
    -  Income:US:Company:ESPP:PnL
    -
    -

    So in theory, if the price is given (26.4375 USD), we could verify that the -proceeds from the sale at the given price match non-Income postings. That is, -verify that

    -

    -81 x 26.4375 = -2141.4375 + - 2141.36 + - 0.08 +

    -

    is below a small tolerance value. So this plugin does this.

    -

    In general terms, it does the following: For transactions with postings that -have a cost and a price, it verifies that the sum of the positions on all -postings to non-income accounts is below tolerance.

    -

    This provides yet another level of verification and allows you to elide the -income amounts, knowing that the price is there to provide an extra level of -error-checking in case you enter a typo.

    - +

    Check that the entries are unique, by computing hashes.

    +
    + + + + + + + + + + +
    Parameters: +
      +
    • entries – A list of directives.

    • +
    • unused_options_map – An options map.

    • +
    +
    + + + + + + + + + + + +
    Returns: +
      +
    • A list of new errors, if any were found.

    • +
    +
    +
    + Source code in beancount/plugins/noduplicates.py +
    def validate_no_duplicates(entries, unused_options_map):
    +    """Check that the entries are unique, by computing hashes.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    unused_hashes, errors = compare.hash_entries(entries, exclude_meta=True)
    +    return entries, errors
    +
    +
    +
    -
    +
    + + + -
    +
    -

    - -beancount.plugins.sellgains.SellGainsError (tuple) - +

    + beancount.plugins.nounused -

    +

    -

    SellGainsError(source, message, entry)

    - +

    This plugin validates that there are no unused accounts.

    @@ -6772,57 +3660,34 @@

    -
    - - -

    -beancount.plugins.sellgains.SellGainsError.__getnewargs__(self) +
    - - special - -

    -
    +

    + +beancount.plugins.nounused.UnusedAccountError (tuple) + -

    Return self as a plain tuple. Used by copy and pickle.

    -
    - Source code in beancount/plugins/sellgains.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -

    -
    +

    +
    +

    UnusedAccountError(source, message, entry)

    -
    -

    -beancount.plugins.sellgains.SellGainsError.__new__(_cls, source, message, entry) +
    - - special - staticmethod - -

    -
    -

    Create new instance of SellGainsError(source, message, entry)

    -
    -
    @@ -6830,166 +3695,25 @@

    -beancount.plugins.sellgains.SellGainsError.__repr__(self) +

    +beancount.plugins.nounused.UnusedAccountError.__getnewargs__(self) special -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/plugins/sellgains.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - - - - - - - - -
    - - - -

    -beancount.plugins.sellgains.validate_sell_gains(entries, options_map) - - -

    +

    -

    Check the sum of asset account totals for lots sold with a price on them.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • unused_options_map – An options map.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of new errors, if any were found.

    • -
    -
    +

    Return self as a plain tuple. Used by copy and pickle.

    +
    - Source code in beancount/plugins/sellgains.py -
    def validate_sell_gains(entries, options_map):
    -    """Check the sum of asset account totals for lots sold with a price on them.
    -
    -    Args:
    -      entries: A list of directives.
    -      unused_options_map: An options map.
    -    Returns:
    -      A list of new errors, if any were found.
    -    """
    -    errors = []
    -    acc_types = options.get_account_types(options_map)
    -    proceed_types = set([acc_types.assets,
    -                         acc_types.liabilities,
    -                         acc_types.equity,
    -                         acc_types.expenses])
    -
    -    for entry in entries:
    -        if not isinstance(entry, data.Transaction):
    -            continue
    -
    -        # Find transactions whose lots at cost all have a price.
    -        postings_at_cost = [posting
    -                            for posting in entry.postings
    -                            if posting.cost is not None]
    -        if not postings_at_cost or not all(posting.price is not None
    -                                           for posting in postings_at_cost):
    -            continue
    -
    -        # Accumulate the total expected proceeds and the sum of the asset and
    -        # expenses legs.
    -        total_price = inventory.Inventory()
    -        total_proceeds = inventory.Inventory()
    -        for posting in entry.postings:
    -            # If the posting is held at cost, add the priced value to the balance.
    -            if posting.cost is not None:
    -                assert posting.price is not None
    -                price = posting.price
    -                total_price.add_amount(amount.mul(price, -posting.units.number))
    -            else:
    -                # Otherwise, use the weight and ignore postings to Income accounts.
    -                atype = account_types.get_account_type(posting.account)
    -                if atype in proceed_types:
    -                    total_proceeds.add_amount(convert.get_weight(posting))
    -
    -        # Compare inventories, currency by currency.
    -        dict_price = {pos.units.currency: pos.units.number
    -                      for pos in total_price}
    -        dict_proceeds = {pos.units.currency: pos.units.number
    -                         for pos in total_proceeds}
    -
    -        tolerances = interpolate.infer_tolerances(entry.postings, options_map)
    -        invalid = False
    -        for currency, price_number in dict_price.items():
    -            # Accept a looser than usual tolerance because rounding occurs
    -            # differently. Also, it would be difficult for the user to satisfy
    -            # two sets of constraints manually.
    -            tolerance = tolerances.get(currency) * EXTRA_TOLERANCE_MULTIPLIER
    -
    -            proceeds_number = dict_proceeds.pop(currency, ZERO)
    -            diff = abs(price_number - proceeds_number)
    -            if diff > tolerance:
    -                invalid = True
    -                break
    -
    -        if invalid or dict_proceeds:
    -            errors.append(
    -                SellGainsError(
    -                    entry.meta,
    -                    "Invalid price vs. proceeds/gains: {} vs. {}".format(
    -                        total_price, total_proceeds),
    -                    entry))
    -
    -    return entries, errors
    -
    + Source code in beancount/plugins/nounused.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -6997,10 +3721,23 @@

    - +

    +beancount.plugins.nounused.UnusedAccountError.__new__(_cls, source, message, entry) + + + special + staticmethod + + +

    + +
    + +

    Create new instance of UnusedAccountError(source, message, entry)

    @@ -7008,49 +3745,43 @@

    - +
    -

    - beancount.plugins.split_expenses +

    +beancount.plugins.nounused.UnusedAccountError.__repr__(self) + + special + -

    +

    -

    Split expenses of a Beancount ledger between multiple people.

    -

    This plugin is given a list of names. It assumes that any Expenses account whose -components do not include any of the given names are to be split between the -members. It goes through all the transactions and converts all such postings -into multiple postings, one for each member.

    -

    For example, given the names 'Martin' and 'Caroline', the following transaction:

    -
    2015-02-01 * "Aqua Viva Tulum - two nights"
    -   Income:Caroline:CreditCard      -269.00 USD
    -   Expenses:Accommodation
    -
    -

    Will be converted to this:

    -
    2015-02-01 * "Aqua Viva Tulum - two nights"
    -  Income:Caroline:CreditCard       -269.00 USD
    -  Expenses:Accommodation:Martin     134.50 USD
    -  Expenses:Accommodation:Caroline   134.50 USD
    -
    -

    After these transformations, all account names should include the name of a -member. You can generate reports for a particular person by filtering postings -to accounts with a component by their name.

    - +

    Return a nicely formatted representation string

    +
    + Source code in beancount/plugins/nounused.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    +
    +
    -
    +
    + + + @@ -7059,15 +3790,24 @@

    -

    -beancount.plugins.split_expenses.get_participants(filename, options_map) +

    +beancount.plugins.nounused.validate_unused_accounts(entries, unused_options_map) -

    +

    -

    Get the list of participants from the plugin configuration in the input file.

    +

    Check that all accounts declared open are actually used.

    +

    We check that all of the accounts that are open are at least referred to by +another directive. These are probably unused, so issue a warning (we like to +be pedantic). Note that an account that is open and then closed is +considered used--this is a valid use case that may occur in reality. If you +have a use case for an account to be open but never used, you can quiet that +warning by initializing the account with a balance asserts or a pad +directive, or even use a note will be sufficient.

    +

    (This is probably a good candidate for optional inclusion as a "pedantic" +plugin.)

    @@ -7079,7 +3819,8 @@

    Parameters:

    @@ -7095,49 +3836,55 @@

    Returns:

    -
      -
    • options_map – The options map, as produced by the parser.

    • +
    • entries – A list of directives.

    • +
    • unused_options_map – An options map.

      -
    • A list of strings, the names of participants as they should appear in the -account names.

    • +
    • A list of new errors, if any were found.

    - - - - - - - - - - -
    Exceptions: -
      -
    • KeyError – If the configuration does not contain configuration for the list

    • -
    -
    - Source code in beancount/plugins/split_expenses.py -
    def get_participants(filename, options_map):
    -    """Get the list of participants from the plugin configuration in the input file.
    -
    -    Args:
    -      options_map: The options map, as produced by the parser.
    -    Returns:
    -      A list of strings, the names of participants as they should appear in the
    -      account names.
    -    Raises:
    -      KeyError: If the configuration does not contain configuration for the list
    -      of participants.
    -    """
    -    plugin_options = dict(options_map["plugin"])
    -    try:
    -        return plugin_options["beancount.plugins.split_expenses"].split()
    -    except KeyError:
    -        raise KeyError("Could not find the split_expenses plugin configuration.")
    -
    + Source code in beancount/plugins/nounused.py +
    def validate_unused_accounts(entries, unused_options_map):
    +    """Check that all accounts declared open are actually used.
    +
    +    We check that all of the accounts that are open are at least referred to by
    +    another directive. These are probably unused, so issue a warning (we like to
    +    be pedantic). Note that an account that is open and then closed is
    +    considered used--this is a valid use case that may occur in reality. If you
    +    have a use case for an account to be open but never used, you can quiet that
    +    warning by initializing the account with a balance asserts or a pad
    +    directive, or even use a note will be sufficient.
    +
    +    (This is probably a good candidate for optional inclusion as a "pedantic"
    +    plugin.)
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    # Find all the accounts referenced by entries which are not Open, and the
    +    # open directives for error reporting below.
    +    open_map = {}
    +    referenced_accounts = set()
    +    for entry in entries:
    +        if isinstance(entry, data.Open):
    +            open_map[entry.account] = entry
    +            continue
    +        referenced_accounts.update(getters.get_entry_accounts(entry))
    +
    +    # Create a list of suitable errors, with the location of the Open directives
    +    # corresponding to the unused accounts.
    +    errors = [
    +        UnusedAccountError(
    +            open_entry.meta, "Unused account '{}'".format(account), open_entry
    +        )
    +        for account, open_entry in open_map.items()
    +        if account not in referenced_accounts
    +    ]
    +    return entries, errors
    +
    @@ -7145,507 +3892,170 @@

    -

    -beancount.plugins.split_expenses.main() + + -

    + -
    -

    Generate final reports for a shared expenses on a trip or project.

    -

    For each of many participants, generate a detailed list of expenses, -contributions, a categorized summary of expenses, and a final balance. Also -produce a global list of final balances so that participants can reconcile -between each other.

    -
    - Source code in beancount/plugins/split_expenses.py -
    def main():
    -    """Generate final reports for a shared expenses on a trip or project.
    -
    -    For each of many participants, generate a detailed list of expenses,
    -    contributions, a categorized summary of expenses, and a final balance. Also
    -    produce a global list of final balances so that participants can reconcile
    -    between each other.
    -    """
    -
    -    logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s')
    -    parser = version.ArgumentParser(description=__doc__.strip())
    -    parser.add_argument('filename', help='Beancount input filename')
    -
    -    parser.add_argument('-c', '--currency', action='store',
    -                        help="Convert all the amounts to a single common currency")
    -
    -    oparser = parser.add_argument_group('Outputs')
    -
    -    oparser.add_argument('-o', '--output-text', '--text', action='store',
    -                         help="Render results to text boxes")
    -    oparser.add_argument('--output-csv', '--csv', action='store',
    -                         help="Render results to CSV files")
    -    oparser.add_argument('--output-stdout', '--stdout', action='store_true',
    -                         help="Render results to stdout")
    -
    -    args = parser.parse_args()
    -
    -    # Ensure the directories exist.
    -    for directory in [args.output_text, args.output_csv]:
    -        if directory and not path.exists(directory):
    -            os.makedirs(directory, exist_ok=True)
    -
    -    # Load the input file and get the list of participants.
    -    entries, errors, options_map = loader.load_file(args.filename)
    -    participants = get_participants(args.filename, options_map)
    -
    -    for participant in participants:
    -        print("Participant: {}".format(participant))
    -
    -        save_query("balances", participant, entries, options_map, r"""
    -          SELECT
    -            PARENT(account) AS account,
    -            CONV[SUM(position)] AS amount
    -          WHERE account ~ ':\b{}'
    -          GROUP BY 1
    -          ORDER BY 2 DESC
    -        """, participant, boxed=False, args=args)
    -
    -        save_query("expenses", participant, entries, options_map, r"""
    -          SELECT
    -            date, flag, description,
    -            PARENT(account) AS account,
    -            JOINSTR(links) AS links,
    -            CONV[position] AS amount,
    -            CONV[balance] AS balance
    -          WHERE account ~ 'Expenses.*\b{}'
    -        """, participant, args=args)
    -
    -        save_query("income", participant, entries, options_map, r"""
    -          SELECT
    -            date, flag, description,
    -            account,
    -            JOINSTR(links) AS links,
    -            CONV[position] AS amount,
    -            CONV[balance] AS balance
    -          WHERE account ~ 'Income.*\b{}'
    -        """, participant, args=args)
    -
    -    save_query("final", None, entries, options_map, r"""
    -      SELECT
    -        GREP('\b({})\b', account) AS participant,
    -        CONV[SUM(position)] AS balance
    -      GROUP BY 1
    -      ORDER BY 2
    -    """, '|'.join(participants), args=args)
    -
    -    # FIXME: Make this output to CSV files and upload to a spreadsheet.
    -    # FIXME: Add a fixed with option. This requires changing adding this to the
    -    # the renderer to be able to have elastic space and line splitting..
    -
    -
    -
    +
    -
    +

    + beancount.plugins.onecommodity -
    +

    -

    -beancount.plugins.split_expenses.save_query(title, participant, entries, options_map, sql_query, *format_args, *, boxed=True, spaced=False, args=None) +
    +

    A plugin that issues errors when more than one commodity is used in an account.

    +

    For investments or trading accounts, it can make it easier to filter the action +around a single stock by using the name of the stock as the leaf of the account +name.

    +

    Notes:

    +
      +
    • +

      The plugin will automatically skip accounts that have explicitly declared + commodities in their Open directive.

      +
    • +
    • +

      You can also set the metadata "onecommodity: FALSE" on an account's Open + directive to skip the checks for that account.

      +
    • +
    • +

      If provided, the configuration should be a regular expression restricting the + set of accounts to check.

      +
    • +
    -

    -
    -

    Save the multiple files for this query.

    +
    - - - - - - - - - - - -
    Parameters: -
      -
    • title – A string, the title of this particular report to render.

    • -
    • participant – A string, the name of the participant under consideration.

    • -
    • entries – A list of directives (as per the loader).

    • -
    • options_map – A dict of options (as per the loader).

    • -
    • sql_query – A string with the SQL query, possibly with some placeholders left for -*format_args to replace.

    • -
    • *format_args – A tuple of arguments to be formatted into the SQL query string. -This is provided as a convenience.

    • -
    • boxed – A boolean, true if we should render the results in a fancy-looking ASCII box.

    • -
    • spaced – If true, leave an empty line between each of the rows. This is useful if the -results have a lot of rows that render over multiple lines.

    • -
    • args – A dummy object with the following attributes: -output_text: An optional directory name, to produce a text rendering of - the report. -output_csv: An optional directory name, to produce a CSV rendering of - the report. -output_stdout: A boolean, if true, also render the output to stdout. -currency: An optional currency (a string). If you use this, you should - wrap query targets to be converted with the pseudo-function - "CONV[...]" and it will get replaced to CONVERT(..., CURRENCY) - automatically.

    • -
    -
    -
    - Source code in beancount/plugins/split_expenses.py -
    def save_query(title, participant, entries, options_map, sql_query, *format_args,
    -               boxed=True, spaced=False, args=None):
    -    """Save the multiple files for this query.
    -
    -    Args:
    -      title: A string, the title of this particular report to render.
    -      participant: A string, the name of the participant under consideration.
    -      entries: A list of directives (as per the loader).
    -      options_map: A dict of options (as per the loader).
    -      sql_query: A string with the SQL query, possibly with some placeholders left for
    -        *format_args to replace.
    -      *format_args: A tuple of arguments to be formatted into the SQL query string.
    -        This is provided as a convenience.
    -      boxed: A boolean, true if we should render the results in a fancy-looking ASCII box.
    -      spaced: If true, leave an empty line between each of the rows. This is useful if the
    -        results have a lot of rows that render over multiple lines.
    -      args: A dummy object with the following attributes:
    -        output_text: An optional directory name, to produce a text rendering of
    -          the report.
    -        output_csv: An optional directory name, to produce a CSV rendering of
    -          the report.
    -        output_stdout: A boolean, if true, also render the output to stdout.
    -        currency: An optional currency (a string). If you use this, you should
    -          wrap query targets to be converted with the pseudo-function
    -          "CONV[...]" and it will get replaced to CONVERT(..., CURRENCY)
    -          automatically.
    -    """
    -    # Replace CONV() to convert the currencies or not; if so, replace to
    -    # CONVERT(..., currency).
    -    replacement = (r'\1'
    -                   if args.currency is None else
    -                   r'CONVERT(\1, "{}")'.format(args.currency))
    -    sql_query = re.sub(r'CONV\[(.*?)\]', replacement, sql_query)
    -
    -    # Run the query.
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    sql_query, *format_args,
    -                                    numberify=True)
    -
    -    # The base of all filenames.
    -    filebase = title.replace(' ', '_')
    -
    -    fmtopts = dict(boxed=boxed,
    -                   spaced=spaced)
    -
    -    # Output the text files.
    -    if args.output_text:
    -        basedir = (path.join(args.output_text, participant)
    -                   if participant
    -                   else args.output_text)
    -        os.makedirs(basedir, exist_ok=True)
    -        filename = path.join(basedir, filebase + '.txt')
    -        with open(filename, 'w') as file:
    -            query_render.render_text(rtypes, rrows, options_map['dcontext'],
    -                                     file, **fmtopts)
    -
    -    # Output the CSV files.
    -    if args.output_csv:
    -        basedir = (path.join(args.output_csv, participant)
    -                   if participant
    -                   else args.output_csv)
    -        os.makedirs(basedir, exist_ok=True)
    -        filename = path.join(basedir, filebase + '.csv')
    -        with open(filename, 'w') as file:
    -            query_render.render_csv(rtypes, rrows, options_map['dcontext'],
    -                                    file, expand=False)
    -
    -    if args.output_stdout:
    -        # Write out the query to stdout.
    -        query_render.render_text(rtypes, rrows, options_map['dcontext'],
    -                                 sys.stdout, **fmtopts)
    -
    -
    -
    -
    -
    -

    -beancount.plugins.split_expenses.split_expenses(entries, options_map, config) -

    +
    -
    -

    Split postings according to expenses (see module docstring for details).

    - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives. We're interested only in the Transaction instances.

    • -
    • unused_options_map – A parser options dict.

    • -
    • config – The plugin configuration string.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of entries, with potentially more accounts and potentially more -postings with smaller amounts.

    • -
    -
    -
    - Source code in beancount/plugins/split_expenses.py -
    def split_expenses(entries, options_map, config):
    -    """Split postings according to expenses (see module docstring for details).
    -
    -    Args:
    -      entries: A list of directives. We're interested only in the Transaction instances.
    -      unused_options_map: A parser options dict.
    -      config: The plugin configuration string.
    -    Returns:
    -      A list of entries, with potentially more accounts and potentially more
    -      postings with smaller amounts.
    -    """
    -
    -    # Validate and sanitize configuration.
    -    if isinstance(config, str):
    -        members = config.split()
    -    elif isinstance(config, (tuple, list)):
    -        members = config
    -    else:
    -        raise RuntimeError("Invalid plugin configuration: configuration for split_expenses "
    -                           "should be a string or a sequence.")
    -
    -    acctypes = options.get_account_types(options_map)
    -    def is_expense_account(account):
    -        return account_types.get_account_type(account) == acctypes.expenses
    -
    -    # A predicate to quickly identify if an account contains the name of a
    -    # member.
    -    is_individual_account = re.compile('|'.join(map(re.escape, members))).search
    -
    -    # Existing and previously unseen accounts.
    -    new_accounts = set()
    -
    -    # Filter the entries and transform transactions.
    -    new_entries = []
    -    for entry in entries:
    -        if isinstance(entry, data.Transaction):
    -            new_postings = []
    -            for posting in entry.postings:
    -                if (is_expense_account(posting.account) and
    -                    not is_individual_account(posting.account)):
    -
    -                    # Split this posting into multiple postings.
    -                    split_units = amount.Amount(posting.units.number / len(members),
    -                                                posting.units.currency)
    -
    -                    for member in members:
    -                        # Mark the account as new if never seen before.
    -                        subaccount = account.join(posting.account, member)
    -                        new_accounts.add(subaccount)
    -
    -                        # Ensure the modified postings are marked as
    -                        # automatically calculated, so that the resulting
    -                        # calculated amounts aren't used to affect inferred
    -                        # tolerances.
    -                        meta = posting.meta.copy() if posting.meta else {}
    -                        meta[interpolate.AUTOMATIC_META] = True
    -
    -                        # Add a new posting for each member, to a new account
    -                        # with the name of this member.
    -                        new_postings.append(
    -                            posting._replace(meta=meta,
    -                                             account=subaccount,
    -                                             units=split_units,
    -                                             cost=posting.cost))
    -                else:
    -                    new_postings.append(posting)
    -
    -            # Modify the entry in-place, replace its postings.
    -            entry = entry._replace(postings=new_postings)
    -
    -        new_entries.append(entry)
    -
    -    # Create Open directives for new subaccounts if necessary.
    -    oc_map = getters.get_account_open_close(entries)
    -    open_date = entries[0].date
    -    meta = data.new_metadata('<split_expenses>', 0)
    -    open_entries = []
    -    for new_account in new_accounts:
    -        if new_account not in oc_map:
    -            entry = data.Open(meta, open_date, new_account, None, None)
    -            open_entries.append(entry)
    -
    -    return open_entries + new_entries, []
    -
    -
    -
    +

    + +beancount.plugins.onecommodity.OneCommodityError (tuple) + -

    +

    +
    +

    OneCommodityError(source, message, entry)

    + + + + +
    -
    -
    - -
    +
    + -

    - beancount.plugins.tag_pending +

    +beancount.plugins.onecommodity.OneCommodityError.__getnewargs__(self) + + special + -

    +

    -

    An example of tracking unpaid payables or receivables.

    -

    A user with lots of invoices to track may want to produce a report of pending or -incomplete payables or receivables. Beancount does not by default offer such a -dedicated feature, but it is easy to build one by using existing link attributes -on transactions. This is an example on how to implement that with a plugin.

    -

    For example, assuming the user enters linked transactions like this:

    -
    2013-03-28 * "Bill for datacenter electricity"  ^invoice-27a30ab61191
    -  Expenses:Electricity                        450.82 USD
    -  Liabilities:AccountsPayable
    -
    -2013-04-15 * "Paying electricity company" ^invoice-27a30ab61191
    -  Assets:Checking                           -450.82 USD
    -  Liabilities:AccountsPayable
    -
    -

    Transactions are grouped by link ("invoice-27a30ab61191") and then the -intersection of their common accounts is automatically calculated -("Liabilities:AccountsPayable"). We then add up the balance of all the postings -for this account in this link group and check if the sum is zero. If there is a -residual amount in this balance, we mark the associated entries as incomplete by -inserting a #PENDING tag on them. The user can then use that tag to navigate to -the corresponding view in the web interface, or just find the entries and -produce a listing of them.

    +

    Return self as a plain tuple. Used by copy and pickle.

    + +
    + Source code in beancount/plugins/onecommodity.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    +
    +
    + -
    + +
    +

    +beancount.plugins.onecommodity.OneCommodityError.__new__(_cls, source, message, entry) + + + special + staticmethod + +

    +
    +

    Create new instance of OneCommodityError(source, message, entry)

    +
    +
    -
    +
    -

    -beancount.plugins.tag_pending.tag_pending_plugin(entries, options_map) +

    +beancount.plugins.onecommodity.OneCommodityError.__repr__(self) + + special + -

    +

    -

    A plugin that finds and tags pending transactions.

    +

    Return a nicely formatted representation string

    - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of entry instances.

    • -
    • options_map – A dict of options parsed from the file.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of entries and errors.

    • -
    -
    - Source code in beancount/plugins/tag_pending.py -
    def tag_pending_plugin(entries, options_map):
    -    """A plugin that finds and tags pending transactions.
    -
    -    Args:
    -      entries: A list of entry instances.
    -      options_map: A dict of options parsed from the file.
    -    Returns:
    -      A tuple of entries and errors.
    -    """
    -    return (tag_pending_transactions(entries, 'PENDING'), [])
    -
    + Source code in beancount/plugins/onecommodity.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -7653,22 +4063,34 @@

    -

    -beancount.plugins.tag_pending.tag_pending_transactions(entries, tag_name='PENDING') +

    +beancount.plugins.onecommodity.validate_one_commodity(entries, unused_options_map, config=None) -

    +

    -

    Filter out incomplete linked transactions to a transfer account.

    -

    Given a list of entries, group the entries by their link and compute the -balance of the intersection of their common accounts. If the balance does -not sum to zero, insert a 'tag_name' tag in the entries.

    +

    Check that each account has units in only a single commodity.

    +

    This is an extra constraint that you may want to apply optionally, despite +Beancount's ability to support inventories and aggregations with more than +one commodity. I believe this also matches GnuCash's model, where each +account has a single commodity attached to it.

    @@ -7680,9 +4102,10 @@

    Parameters:

    @@ -7698,57 +4121,115 @@

    Returns:

      -
    • entries – A list of directives/transactions to process.

    • -
    • tag_name – A string, the name of the tag to be inserted if a linked group -of entries is found not to match

    • +
    • entries – A list of directives.

    • +
    • unused_options_map – An options map.

    • +
    • config – The plugin configuration string, a regular expression to match +against the subset of accounts to check.

      -
    • A modified set of entries, possibly tagged as pending.

    • +
    • A list of new errors, if any were found.

    - Source code in beancount/plugins/tag_pending.py -
    def tag_pending_transactions(entries, tag_name='PENDING'):
    -    """Filter out incomplete linked transactions to a transfer account.
    -
    -    Given a list of entries, group the entries by their link and compute the
    -    balance of the intersection of their common accounts. If the balance does
    -    not sum to zero, insert a 'tag_name' tag in the entries.
    -
    -    Args:
    -      entries: A list of directives/transactions to process.
    -      tag_name: A string, the name of the tag to be inserted if a linked group
    -        of entries is found not to match
    -    Returns:
    -      A modified set of entries, possibly tagged as pending.
    -
    -    """
    -    link_groups = basicops.group_entries_by_link(entries)
    -
    -    pending_entry_ids = set()
    -    for link, link_entries in link_groups.items():
    -        assert link_entries
    -        if len(link_entries) == 1:
    -            # If a single entry is present, it is assumed incomplete.
    -            pending_entry_ids.add(id(link_entries[0]))
    -        else:
    -            # Compute the sum total balance of the common accounts.
    -            common_accounts = basicops.get_common_accounts(link_entries)
    -            common_balance = inventory.Inventory()
    -            for entry in link_entries:
    -                for posting in entry.postings:
    -                    if posting.account in common_accounts:
    -                        common_balance.add_position(posting)
    -
    -            # Mark entries as pending if a residual balance is found.
    -            if not common_balance.is_empty():
    -                for entry in link_entries:
    -                    pending_entry_ids.add(id(entry))
    -
    -    # Insert tags if marked.
    -    return [(entry._replace(tags=(entry.tags or set()) | set((tag_name,)))
    -             if id(entry) in pending_entry_ids
    -             else entry)
    -            for entry in entries]
    -
    + Source code in beancount/plugins/onecommodity.py +
    def validate_one_commodity(entries, unused_options_map, config=None):
    +    """Check that each account has units in only a single commodity.
    +
    +    This is an extra constraint that you may want to apply optionally, despite
    +    Beancount's ability to support inventories and aggregations with more than
    +    one commodity. I believe this also matches GnuCash's model, where each
    +    account has a single commodity attached to it.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +      config: The plugin configuration string, a regular expression to match
    +        against the subset of accounts to check.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    accounts_re = re.compile(config) if config else None
    +
    +    # Mappings of account name to lists of currencies for each units and cost.
    +    units_map = collections.defaultdict(set)
    +    cost_map = collections.defaultdict(set)
    +
    +    # Mappings to use just for getting a relevant source.
    +    units_source_map = {}
    +    cost_source_map = {}
    +
    +    # Gather the set of accounts to skip from the Open directives.
    +    skip_accounts = set()
    +    for entry in entries:
    +        if not isinstance(entry, data.Open):
    +            continue
    +        if (
    +            not entry.meta.get("onecommodity", True)
    +            or (accounts_re and not accounts_re.match(entry.account))
    +            or (entry.currencies and len(entry.currencies) > 1)
    +        ):
    +            skip_accounts.add(entry.account)
    +
    +    # Accumulate all the commodities used.
    +    for entry in entries:
    +        if isinstance(entry, data.Transaction):
    +            for posting in entry.postings:
    +                if posting.account in skip_accounts:
    +                    continue
    +
    +                units = posting.units
    +                units_map[posting.account].add(units.currency)
    +                if len(units_map[posting.account]) > 1:
    +                    units_source_map[posting.account] = entry
    +
    +                cost = posting.cost
    +                if cost:
    +                    cost_map[posting.account].add(cost.currency)
    +                    if len(cost_map[posting.account]) > 1:
    +                        cost_source_map[posting.account] = entry
    +
    +        elif isinstance(entry, data.Balance):
    +            if entry.account in skip_accounts:
    +                continue
    +
    +            units_map[entry.account].add(entry.amount.currency)
    +            if len(units_map[entry.account]) > 1:
    +                units_source_map[entry.account] = entry
    +
    +        elif isinstance(entry, data.Open):
    +            if entry.currencies and len(entry.currencies) > 1:
    +                skip_accounts.add(entry.account)
    +
    +    # Check units.
    +    errors = []
    +    for account, currencies in units_map.items():
    +        if account in skip_accounts:
    +            continue
    +        if len(currencies) > 1:
    +            errors.append(
    +                OneCommodityError(
    +                    units_source_map[account].meta,
    +                    "More than one currency in account '{}': {}".format(
    +                        account, ",".join(currencies)
    +                    ),
    +                    None,
    +                )
    +            )
    +
    +    # Check costs.
    +    for account, currencies in cost_map.items():
    +        if account in skip_accounts:
    +            continue
    +        if len(currencies) > 1:
    +            errors.append(
    +                OneCommodityError(
    +                    cost_source_map[account].meta,
    +                    "More than one cost currency in account '{}': {}".format(
    +                        account, ",".join(currencies)
    +                    ),
    +                    None,
    +                )
    +            )
    +
    +    return entries, errors
    +
    @@ -7771,26 +4252,101 @@

    - beancount.plugins.unique_prices +

    + beancount.plugins.pedantic -

    +

    -

    This module adds validation that there is a single price defined per -date and base/quote currencies. If multiple conflicting price values are -declared, an error is generated. Note that multiple price entries with the -same number do not generate an error.

    -

    This is meant to be turned on if you want to use a very strict mode for -entering prices, and may not be realistic usage. For example, if you have -(1) a transaction with an implicitly generated price during the day (from -its cost) and (2) a separate explicit price directive that declares a -different price for the day's closing price, this would generate an error. -I'm not certain this will be useful in the long run, so placing it in a -plugin.

    +

    A plugin of plugins which triggers all the pedantic plugins.

    + + + +
    + + + + + + + + + + + + + + +
    + +
    + + + + + +
    + + + +

    + beancount.plugins.sellgains + + + +

    + +
    + +

    A plugin that cross-checks declared gains against prices on lot sales.

    +

    When you sell stock, the gains can be automatically implied by the corresponding +cash amounts. For example, in the following transaction the 2nd and 3rd postings +should match the value of the stock sold:

    +
    1999-07-31 * "Sell"
    +  Assets:US:BRS:Company:ESPP          -81 ADSK {26.3125 USD}
    +  Assets:US:BRS:Company:Cash      2141.36 USD
    +  Expenses:Financial:Fees            0.08 USD
    +  Income:US:Company:ESPP:PnL      -10.125 USD
    +
    +

    The cost basis is checked against: 2141.36 + 008 + -10.125. That is, the balance +checks computes

    +

    -81 x 26.3125 = -2131.3125 + + 2141.36 + + 0.08 + + -10.125

    +

    and checks that the residual is below a small tolerance.

    +

    But... usually the income leg isn't given to you in statements. Beancount can +automatically infer it using the balance, which is convenient, like this:

    +
    1999-07-31 * "Sell"
    +  Assets:US:BRS:Company:ESPP          -81 ADSK {26.3125 USD}
    +  Assets:US:BRS:Company:Cash      2141.36 USD
    +  Expenses:Financial:Fees            0.08 USD
    +  Income:US:Company:ESPP:PnL
    +
    +

    Additionally, most often you have the sales prices given to you on your +transaction confirmation statement, so you can enter this:

    +
    1999-07-31 * "Sell"
    +  Assets:US:BRS:Company:ESPP          -81 ADSK {26.3125 USD} @ 26.4375 USD
    +  Assets:US:BRS:Company:Cash      2141.36 USD
    +  Expenses:Financial:Fees            0.08 USD
    +  Income:US:Company:ESPP:PnL
    +
    +

    So in theory, if the price is given (26.4375 USD), we could verify that the +proceeds from the sale at the given price match non-Income postings. That is, +verify that

    +

    -81 x 26.4375 = -2141.4375 + + 2141.36 + + 0.08 +

    +

    is below a small tolerance value. So this plugin does this.

    +

    In general terms, it does the following: For transactions with postings that +have a cost and a price, it verifies that the sum of the positions on all +postings to non-income accounts is below tolerance.

    +

    This provides yet another level of verification and allows you to elide the +income amounts, knowing that the price is there to provide an extra level of +error-checking in case you enter a typo.

    @@ -7805,22 +4361,23 @@

    +
    -

    +

    -beancount.plugins.unique_prices.UniquePricesError (tuple) +beancount.plugins.sellgains.SellGainsError (tuple) -

    +

    -

    UniquePricesError(source, message, entry)

    +

    SellGainsError(source, message, entry)

    @@ -7839,25 +4396,25 @@

    -beancount.plugins.unique_prices.UniquePricesError.__getnewargs__(self) +

    +beancount.plugins.sellgains.SellGainsError.__getnewargs__(self) special -

    +

    Return self as a plain tuple. Used by copy and pickle.

    - Source code in beancount/plugins/unique_prices.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    + Source code in beancount/plugins/sellgains.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -7869,19 +4426,19 @@

    -beancount.plugins.unique_prices.UniquePricesError.__new__(_cls, source, message, entry) +

    +beancount.plugins.sellgains.SellGainsError.__new__(_cls, source, message, entry) special staticmethod -

    +

    -

    Create new instance of UniquePricesError(source, message, entry)

    +

    Create new instance of SellGainsError(source, message, entry)

    @@ -7893,25 +4450,25 @@

    -beancount.plugins.unique_prices.UniquePricesError.__repr__(self) +

    +beancount.plugins.sellgains.SellGainsError.__repr__(self) special -

    +

    Return a nicely formatted representation string

    - Source code in beancount/plugins/unique_prices.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    + Source code in beancount/plugins/sellgains.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -7934,15 +4491,15 @@

    -beancount.plugins.unique_prices.validate_unique_prices(entries, unused_options_map) +

    +beancount.plugins.sellgains.validate_sell_gains(entries, options_map) -

    +

    -

    Check that there is only a single price per day for a particular base/quote.

    +

    Check the sum of asset account totals for lots sold with a price on them.

    @@ -7954,8 +4511,8 @@

    Parameters:

    @@ -7971,49 +4528,89 @@

    Returns:

      -
    • entries – A list of directives. We're interested only in the Transaction instances.

    • -
    • unused_options_map – A parser options dict.

    • +
    • entries – A list of directives.

    • +
    • unused_options_map – An options map.

      -
    • The list of input entries, and a list of new UniquePricesError instances generated.

    • +
    • A list of new errors, if any were found.

    - Source code in beancount/plugins/unique_prices.py -
    def validate_unique_prices(entries, unused_options_map):
    -    """Check that there is only a single price per day for a particular base/quote.
    -
    -    Args:
    -      entries: A list of directives. We're interested only in the Transaction instances.
    -      unused_options_map: A parser options dict.
    -    Returns:
    -      The list of input entries, and a list of new UniquePricesError instances generated.
    -    """
    -    new_entries = []
    -    errors = []
    -
    -    prices = collections.defaultdict(list)
    -    for entry in entries:
    -        if not isinstance(entry, data.Price):
    -            continue
    -        key = (entry.date, entry.currency, entry.amount.currency)
    -        prices[key].append(entry)
    -
    -    errors = []
    -    for price_entries in prices.values():
    -        if len(price_entries) > 1:
    -            number_map = {price_entry.amount.number: price_entry
    -                          for price_entry in price_entries}
    -            if len(number_map) > 1:
    -                # Note: This should be a list of entries for better error
    -                # reporting. (Later.)
    -                error_entry = next(iter(number_map.values()))
    -                errors.append(
    -                    UniquePricesError(error_entry.meta,
    -                                      "Disagreeing price entries",
    -                                      price_entries))
    -
    -    return new_entries, errors
    -
    + Source code in beancount/plugins/sellgains.py +
    def validate_sell_gains(entries, options_map):
    +    """Check the sum of asset account totals for lots sold with a price on them.
    +
    +    Args:
    +      entries: A list of directives.
    +      unused_options_map: An options map.
    +    Returns:
    +      A list of new errors, if any were found.
    +    """
    +    errors = []
    +    acc_types = options.get_account_types(options_map)
    +    proceed_types = set(
    +        [acc_types.assets, acc_types.liabilities, acc_types.equity, acc_types.expenses]
    +    )
    +
    +    for entry in entries:
    +        if not isinstance(entry, data.Transaction):
    +            continue
    +
    +        # Find transactions whose lots at cost all have a price.
    +        postings_at_cost = [
    +            posting for posting in entry.postings if posting.cost is not None
    +        ]
    +        if not postings_at_cost or not all(
    +            posting.price is not None for posting in postings_at_cost
    +        ):
    +            continue
    +
    +        # Accumulate the total expected proceeds and the sum of the asset and
    +        # expenses legs.
    +        total_price = inventory.Inventory()
    +        total_proceeds = inventory.Inventory()
    +        for posting in entry.postings:
    +            # If the posting is held at cost, add the priced value to the balance.
    +            if posting.cost is not None:
    +                assert posting.price is not None
    +                price = posting.price
    +                total_price.add_amount(amount.mul(price, -posting.units.number))
    +            else:
    +                # Otherwise, use the weight and ignore postings to Income accounts.
    +                atype = account_types.get_account_type(posting.account)
    +                if atype in proceed_types:
    +                    total_proceeds.add_amount(convert.get_weight(posting))
    +
    +        # Compare inventories, currency by currency.
    +        dict_price = {pos.units.currency: pos.units.number for pos in total_price}
    +        dict_proceeds = {pos.units.currency: pos.units.number for pos in total_proceeds}
    +
    +        tolerances = interpolate.infer_tolerances(entry.postings, options_map)
    +        invalid = False
    +        for currency, price_number in dict_price.items():
    +            # Accept a looser than usual tolerance because rounding occurs
    +            # differently. Also, it would be difficult for the user to satisfy
    +            # two sets of constraints manually.
    +            tolerance = tolerances.get(currency) * EXTRA_TOLERANCE_MULTIPLIER
    +
    +            proceeds_number = dict_proceeds.pop(currency, ZERO)
    +            diff = abs(price_number - proceeds_number)
    +            if diff > tolerance:
    +                invalid = True
    +                break
    +
    +        if invalid or dict_proceeds:
    +            errors.append(
    +                SellGainsError(
    +                    entry.meta,
    +                    "Invalid price vs. proceeds/gains: {} vs. {}; difference: {}".format(
    +                        total_price, total_proceeds, (total_price + -total_proceeds)
    +                    ),
    +                    entry,
    +                )
    +            )
    +
    +    return entries, errors
    +
    @@ -8036,22 +4633,26 @@

    - beancount.plugins.unrealized +

    + beancount.plugins.unique_prices -

    +

    -

    Compute unrealized gains.

    -

    The configuration for this plugin is a single string, the name of the subaccount -to add to post the unrealized gains to, like this:

    -

    plugin "beancount.plugins.unrealized" "Unrealized"

    -

    If you don't specify a name for the subaccount (the configuration value is -optional), by default it inserts the unrealized gains in the same account that -is being adjusted.

    +

    This module adds validation that there is a single price defined per +date and base/quote currencies. If multiple conflicting price values are +declared, an error is generated. Note that multiple price entries with the +same number do not generate an error.

    +

    This is meant to be turned on if you want to use a very strict mode for +entering prices, and may not be realistic usage. For example, if you have +(1) a transaction with an implicitly generated price during the day (from +its cost) and (2) a separate explicit price directive that declares a +different price for the day's closing price, this would generate an error. +I'm not certain this will be useful in the long run, so placing it in a +plugin.

    @@ -8070,18 +4671,18 @@

    -

    +

    -beancount.plugins.unrealized.UnrealizedError (tuple) +beancount.plugins.unique_prices.UniquePricesError (tuple) -

    +

    -

    UnrealizedError(source, message, entry)

    +

    UniquePricesError(source, message, entry)

    @@ -8100,25 +4701,25 @@

    -

    -beancount.plugins.unrealized.UnrealizedError.__getnewargs__(self) +

    +beancount.plugins.unique_prices.UniquePricesError.__getnewargs__(self) special -

    +

    Return self as a plain tuple. Used by copy and pickle.

    - Source code in beancount/plugins/unrealized.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    + Source code in beancount/plugins/unique_prices.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -8130,19 +4731,19 @@

    -beancount.plugins.unrealized.UnrealizedError.__new__(_cls, source, message, entry) +

    +beancount.plugins.unique_prices.UniquePricesError.__new__(_cls, source, message, entry) special staticmethod -

    +

    -

    Create new instance of UnrealizedError(source, message, entry)

    +

    Create new instance of UniquePricesError(source, message, entry)

    @@ -8154,25 +4755,25 @@

    -beancount.plugins.unrealized.UnrealizedError.__repr__(self) +

    +beancount.plugins.unique_prices.UniquePricesError.__repr__(self) special -

    +

    Return a nicely formatted representation string

    - Source code in beancount/plugins/unrealized.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    + Source code in beancount/plugins/unique_prices.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -8195,225 +4796,15 @@

    -beancount.plugins.unrealized.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.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • 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.

    • -
    -
    -
    - Source code in beancount/plugins/unrealized.py -
    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)
    -
    -
    -
    - - - - - -
    - - - -

    -beancount.plugins.unrealized.get_unrealized_entries(entries) +

    +beancount.plugins.unique_prices.validate_unique_prices(entries, unused_options_map) -

    +

    -

    Return entries automatically created for unrealized gains.

    +

    Check that there is only a single price per day for a particular base/quote.

    @@ -8425,7 +4816,8 @@

    Parameters:

    @@ -8441,27 +4833,50 @@

    Returns:

      -
    • entries – A list of directives.

    • +
    • entries – A list of directives. We're interested only in the Transaction instances.

    • +
    • unused_options_map – A parser options dict.

      -
    • A list of directives, all of which are in the original list.

    • +
    • The list of input entries, and a list of new UniquePricesError instances generated.

    - Source code in beancount/plugins/unrealized.py -
    def get_unrealized_entries(entries):
    -    """Return entries automatically created for unrealized gains.
    -
    -    Args:
    -      entries: A list of directives.
    -    Returns:
    -      A list of directives, all of which are in the original list.
    -    """
    -    return [entry
    -            for entry in entries
    -            if (isinstance(entry, data.Transaction) and
    -                entry.flag == flags.FLAG_UNREALIZED)]
    -
    + Source code in beancount/plugins/unique_prices.py +
    def validate_unique_prices(entries, unused_options_map):
    +    """Check that there is only a single price per day for a particular base/quote.
    +
    +    Args:
    +      entries: A list of directives. We're interested only in the Transaction instances.
    +      unused_options_map: A parser options dict.
    +    Returns:
    +      The list of input entries, and a list of new UniquePricesError instances generated.
    +    """
    +    errors = []
    +
    +    prices = collections.defaultdict(list)
    +    for entry in entries:
    +        if not isinstance(entry, data.Price):
    +            continue
    +        key = (entry.date, entry.currency, entry.amount.currency)
    +        prices[key].append(entry)
    +
    +    errors = []
    +    for price_entries in prices.values():
    +        if len(price_entries) > 1:
    +            number_map = {
    +                price_entry.amount.number: price_entry for price_entry in price_entries
    +            }
    +            if len(number_map) > 1:
    +                # Note: This should be a list of entries for better error
    +                # reporting. (Later.)
    +                error_entry = next(iter(number_map.values()))
    +                errors.append(
    +                    UniquePricesError(
    +                        error_entry.meta, "Disagreeing price entries", price_entries
    +                    )
    +                )
    +
    +    return entries, errors
    +
    @@ -8493,7 +4908,7 @@

    « Previous - Next » + Next » diff --git a/api_reference/beancount.prices.html b/api_reference/beancount.prices.html deleted file mode 100644 index 1cbbb6da..00000000 --- a/api_reference/beancount.prices.html +++ /dev/null @@ -1,4117 +0,0 @@ - - - - - - - - - - - - beancount.prices - Beancount Documentation - - - - - - - - - - - - - - - -
    - - - - -
    - - - - - -
    -
    -
    -
      -
    • Docs »
    • - - - -
    • API reference »
    • - - - -
    • beancount.prices
    • -
    • - -
    • -
    - -
    -
    - -
    -
    - -

    beancount.prices

    - - -
    - - -
    - -

    Fetch prices from the internet and output them as Beancount price directives.

    -

    This script accepts a list of Beancount input filenames, and fetches prices -required to compute market values for current positions:

    -

    bean-price /home/joe/finances/joe.beancount

    -

    The list of fetching jobs to carry out is derived automatically from the input -file (see section below for full details). It is also possible to provide a list -of specific price fetching jobs to run, e.g.,

    -

    bean-price -e google/TSE:XUS yahoo/AAPL mysources.morningstar/RBF1005

    -

    The general format of each of these "source strings" is

    -

    <module>/[^]<ticker>

    -

    The "module" is the name of a Python module that contains a Source class which -can be instantiated and connect to a data source to extract price data. These -modules are automatically imported by name and instantiated in order to pull the -price from a particular data source. This allows you to write your own -supplementary fetcher codes without having to modify this script.

    -

    Default implementations are provided to provide access to prices from Yahoo! -Finance or Google Finance, which cover a large universe of common public -investment types (e.g. stock tickers). As a convenience, the module name is -always first searched under the "beancount.prices.sources" package, where those -default source implementations live. This is how, for example, in order to use -the provided Google Finance data fetcher you don't have to write -"beancount.prices.sources.yahoo/AAPL" but simply "yahoo/AAPL".

    -

    Date

    -

    By default, this script will fetch prices at the latest available date & time. -You can use an option to fetch historical prices for a desired date instead:

    -

    bean-price --date=2015-02-03

    -

    Inverse

    -

    Sometimes, prices are available for the inverse of an instrument. This is often -the case for currencies. For example, the price of "CAD" in USD" is provided by -the USD/CAD market, which gives the price of a US dollar in Canadian dollars. In -order specify this, you can prepend "^" to the instrument to instruct the driver -to compute the inverse of the given price:

    -

    bean-price -e USD:google/^CURRENCY:USDCAD

    -

    If a source price is to be inverted, like this, the precision could be different -than what is fetched. For instance, if the price of USD/CAD is 1.32759, it would -output be this from the above directive:

    -

    2015-10-28 price CAD 0.753244601119 USD

    -

    By default, inverted rates will be rounded similarly to how other Price -directives were rounding those numbers.

    -

    Swap Inverted

    -

    If you prefer to have the output Price entries with swapped currencies instead -of inverting the rate itself, you can use the --swap-inverted option. In the -previous example for the price of CAD, it would output this:

    -

    2015-10-28 price USD 1.32759 CAD

    -

    This works since the Beancount price database computes and interpolates the -reciprocals automatically for all pairs of commodities in its database.

    -

    Prices Needed for a Beancount File

    -

    You can also provide a filename to extract the list of tickers to fetch from a -Beancount input file, e.g.:

    -

    bean-price /home/joe/finances/joe.beancount

    -

    There are many ways to extract a list of commodities with needed prices from a -Beancount input file:

    -
      -
    • -

      Prices for all the holdings that were seen held-at-cost at a particular date.

      -
    • -
    • -

      Prices for holdings held at a particular date which were price converted from - some other commodity in the past (i.e., for currencies).

      -
    • -
    • -

      The list of all Commodity directives present in the file. For each of those - holdings, the corresponding Commodity directive is consulted and its "ticker" - metadata field is used to specify where to attempt to fetch prices. You should - have directives like this in your input file:

      -

      2007-07-20 commodity VEA - price: "google/NYSEARCA:VEA"

      -
    • -
    -

    The "price" metadata can be a comma-separated list of sources to try out, in - which case each of the sources will be looked at :

    -
    2007-07-20 commodity VEA
    -  price: "google/CURRENCY:USDCAD,yahoo/USDCAD"
    -
    -
      -
    • Existing price directives for the same data are excluded by default, since the - price is already in the file.
    • -
    -

    By default, the list of tickers to be fetched includes only the intersection of -these lists. The general intent of the user of this script is to fetch missing -prices, and only needed ones, for a particular date.

    -
      -
    • Use the --date option to change the applied date.
    • -
    • Use the --all option to fetch the entire set of prices, regardless - of holdings and date.
    • -
    • Use --clobber to ignore existing price directives.
    • -
    -

    You can also print the list of prices to be fetched with the --dry-run option, -which stops short of actually fetching the missing prices (it just prints the -list of fetches it would otherwise attempt).

    -

    Caching

    -

    Prices are automatically cached. You can disable the cache with an option:

    -

    bean-price --no-cache

    -

    You can also instruct the script to clear the cache before fetching its prices:

    -

    bean-price --clear-cache

    -

    About Sources and Data Availability

    -

    IMPORTANT: Note that each source may support a different routine for getting its -latest data and for fetching historical/dated data, and that each of these may -differ in their support. For example, Google Finance does not support fetching -historical data for its CURRENCY:* instruments.

    - - - -
    - - - - - - - - - - - - -
    - - - -

    - beancount.prices.find_prices - - - -

    - -
    - -

    A library of codes create price fetching jobs from strings and files.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.prices.find_prices.DatedPrice (tuple) - - - - -

    - -
    - -

    DatedPrice(base, quote, date, sources)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.prices.find_prices.DatedPrice.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/prices/find_prices.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.DatedPrice.__new__(_cls, base, quote, date, sources) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of DatedPrice(base, quote, date, sources)

    - -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.DatedPrice.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/prices/find_prices.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.prices.find_prices.PriceSource (tuple) - - - - -

    - -
    - -

    PriceSource(module, symbol, invert)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.prices.find_prices.PriceSource.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/prices/find_prices.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.PriceSource.__new__(_cls, module, symbol, invert) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of PriceSource(module, symbol, invert)

    - -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.PriceSource.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/prices/find_prices.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.prices.find_prices.find_balance_currencies(entries, date=None) - - -

    - -
    - -

    Return currencies relevant for the given date.

    -

    This computes the account balances as of the date, and returns the union of: -a) The currencies held at cost, and -b) Currency pairs from previous conversions, but only for currencies with - non-zero balances.

    -

    This is intended to produce the list of currencies whose prices are relevant -at a particular date, based on previous history.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • date – A datetime.date instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A set of (base, quote) currencies.

    • -
    -
    -
    - Source code in beancount/prices/find_prices.py -
    def find_balance_currencies(entries, date=None):
    -    """Return currencies relevant for the given date.
    -
    -    This computes the account balances as of the date, and returns the union of:
    -    a) The currencies held at cost, and
    -    b) Currency pairs from previous conversions, but only for currencies with
    -       non-zero balances.
    -
    -    This is intended to produce the list of currencies whose prices are relevant
    -    at a particular date, based on previous history.
    -
    -    Args:
    -      entries: A list of directives.
    -      date: A datetime.date instance.
    -    Returns:
    -      A set of (base, quote) currencies.
    -    """
    -    # Compute the balances.
    -    currencies = set()
    -    currencies_on_books = set()
    -    balances, _ = summarize.balance_by_account(entries, date)
    -    for _, balance in balances.items():
    -        for pos in balance:
    -            if pos.cost is not None:
    -                # Add currencies held at cost.
    -                currencies.add((pos.units.currency, pos.cost.currency))
    -            else:
    -                # Add regular currencies.
    -                currencies_on_books.add(pos.units.currency)
    -
    -    # Create currency pairs from the currencies which are on account balances.
    -    # In order to figure out the quote currencies, we use the list of price
    -    # conversions until this date.
    -    converted = (find_currencies_converted(entries, date) |
    -                 find_currencies_priced(entries, date))
    -    for cbase in currencies_on_books:
    -        for base_quote in converted:
    -            base, quote = base_quote
    -            if base == cbase:
    -                currencies.add(base_quote)
    -
    -    return currencies
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.find_currencies_at_cost(entries) - - -

    - -
    - -

    Return all currencies that were held at cost at some point.

    -

    This returns all of them, even if not on the books at a particular point in -time. This code does not look at account balances.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • date – A datetime.date instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of (base, quote) currencies.

    • -
    -
    -
    - Source code in beancount/prices/find_prices.py -
    def find_currencies_at_cost(entries):
    -    """Return all currencies that were held at cost at some point.
    -
    -    This returns all of them, even if not on the books at a particular point in
    -    time. This code does not look at account balances.
    -
    -    Args:
    -      entries: A list of directives.
    -      date: A datetime.date instance.
    -    Returns:
    -      A list of (base, quote) currencies.
    -    """
    -    currencies = set()
    -    for entry in entries:
    -        if not isinstance(entry, data.Transaction):
    -            continue
    -        for posting in entry.postings:
    -            if posting.cost is not None and posting.cost.number is not None:
    -                currencies.add((posting.units.currency, posting.cost.currency))
    -    return currencies
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.find_currencies_converted(entries, date=None) - - -

    - -
    - -

    Return currencies from price conversions.

    -

    This function looks at all price conversions that occurred until some date -and produces a list of them. Note: This does not include Price directives, -only postings with price conversions.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • date – A datetime.date instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of (base, quote) currencies.

    • -
    -
    -
    - Source code in beancount/prices/find_prices.py -
    def find_currencies_converted(entries, date=None):
    -    """Return currencies from price conversions.
    -
    -    This function looks at all price conversions that occurred until some date
    -    and produces a list of them. Note: This does not include Price directives,
    -    only postings with price conversions.
    -
    -    Args:
    -      entries: A list of directives.
    -      date: A datetime.date instance.
    -    Returns:
    -      A list of (base, quote) currencies.
    -    """
    -    currencies = set()
    -    for entry in entries:
    -        if not isinstance(entry, data.Transaction):
    -            continue
    -        if date and entry.date >= date:
    -            break
    -        for posting in entry.postings:
    -            price = posting.price
    -            if posting.cost is not None or price is None:
    -                continue
    -            currencies.add((posting.units.currency, price.currency))
    -    return currencies
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.find_currencies_declared(entries, date=None) - - -

    - -
    - -

    Return currencies declared in Commodity directives.

    -

    If a 'price' metadata field is provided, include all the quote currencies -there-in. Otherwise, the Commodity directive is ignored.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • date – A datetime.date instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of (base, quote, list of PriceSource) currencies. The list of -(base, quote) pairs is guaranteed to be unique.

    • -
    -
    -
    - Source code in beancount/prices/find_prices.py -
    def find_currencies_declared(entries, date=None):
    -    """Return currencies declared in Commodity directives.
    -
    -    If a 'price' metadata field is provided, include all the quote currencies
    -    there-in. Otherwise, the Commodity directive is ignored.
    -
    -    Args:
    -      entries: A list of directives.
    -      date: A datetime.date instance.
    -    Returns:
    -      A list of (base, quote, list of PriceSource) currencies. The list of
    -      (base, quote) pairs is guaranteed to be unique.
    -    """
    -    currencies = []
    -    for entry in entries:
    -        if not isinstance(entry, data.Commodity):
    -            continue
    -        if date and entry.date >= date:
    -            break
    -
    -        # Here we have to infer which quote currencies the commodity is for
    -        # (maybe down the road this should be better handled by providing a list
    -        # of quote currencies in the Commodity directive itself).
    -        #
    -        # First, we look for a "price" metadata field, which defines conversions
    -        # for various currencies. Each of these quote currencies generates a
    -        # pair in the output.
    -        source_str = entry.meta.get('price', None)
    -        if source_str is not None:
    -            if source_str == "":
    -                logging.debug("Skipping ignored currency (with empty price): %s",
    -                              entry.currency)
    -                continue
    -            try:
    -                source_map = parse_source_map(source_str)
    -            except ValueError:
    -                logging.warning("Ignoring currency with invalid 'price' source: %s",
    -                                entry.currency)
    -            else:
    -                for quote, psources in source_map.items():
    -                    currencies.append((entry.currency, quote, psources))
    -        else:
    -            # Otherwise we simply ignore the declaration. That is, a Commodity
    -            # directive without any "price" metadata would not register as a
    -            # declared currency.
    -            logging.debug("Ignoring currency with no metadata: %s", entry.currency)
    -
    -    return currencies
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.find_currencies_priced(entries, date=None) - - -

    - -
    - -

    Return currencies seen in Price directives.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • date – A datetime.date instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of (base, quote) currencies.

    • -
    -
    -
    - Source code in beancount/prices/find_prices.py -
    def find_currencies_priced(entries, date=None):
    -    """Return currencies seen in Price directives.
    -
    -    Args:
    -      entries: A list of directives.
    -      date: A datetime.date instance.
    -    Returns:
    -      A list of (base, quote) currencies.
    -    """
    -    currencies = set()
    -    for entry in entries:
    -        if not isinstance(entry, data.Price):
    -            continue
    -        if date and entry.date >= date:
    -            break
    -        currencies.add((entry.currency, entry.amount.currency))
    -    return currencies
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.format_dated_price_str(dprice) - - -

    - -
    - -

    Convert a dated price to a one-line printable string.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • dprice – A DatedPrice instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The string for a DatedPrice instance.

    • -
    -
    -
    - Source code in beancount/prices/find_prices.py -
    def format_dated_price_str(dprice):
    -    """Convert a dated price to a one-line printable string.
    -
    -    Args:
    -      dprice: A DatedPrice instance.
    -    Returns:
    -      The string for a DatedPrice instance.
    -    """
    -    psstrs = ['{}({}{})'.format(psource.module.__name__,
    -                                '1/' if psource.invert else '',
    -                                psource.symbol)
    -              for psource in dprice.sources]
    -    base_quote = '{} /{}'.format(dprice.base, dprice.quote)
    -    return '{:<32} @ {:10} [ {} ]'.format(
    -        base_quote,
    -        dprice.date.isoformat() if dprice.date else 'latest',
    -        ','.join(psstrs))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.get_price_jobs_at_date(entries, date=None, inactive=False, undeclared_source=None) - - -

    - -
    - -

    Get a list of prices to fetch from a stream of entries.

    -

    The active holdings held on the given date are included.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the name of a file to process.

    • -
    • date – A datetime.date instance.

    • -
    • inactive – Include currencies with no balance at the given date. The default -is to only include those currencies which have a non-zero balance.

    • -
    • undeclared_source – A string, the name of the default source module to use to -pull prices for commodities without a price source metadata on their -Commodity directive declaration.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of DatedPrice instances.

    • -
    -
    -
    - Source code in beancount/prices/find_prices.py -
    def get_price_jobs_at_date(entries, date=None, inactive=False, undeclared_source=None):
    -    """Get a list of prices to fetch from a stream of entries.
    -
    -    The active holdings held on the given date are included.
    -
    -    Args:
    -      filename: A string, the name of a file to process.
    -      date: A datetime.date instance.
    -      inactive: Include currencies with no balance at the given date. The default
    -        is to only include those currencies which have a non-zero balance.
    -      undeclared_source: A string, the name of the default source module to use to
    -        pull prices for commodities without a price source metadata on their
    -        Commodity directive declaration.
    -    Returns:
    -      A list of DatedPrice instances.
    -
    -    """
    -    # Find the list of declared currencies, and from it build a mapping for
    -    # tickers for each (base, quote) pair. This is the only place tickers
    -    # appear.
    -    declared_triples = find_currencies_declared(entries, date)
    -    currency_map = {(base, quote): psources
    -                    for base, quote, psources in declared_triples}
    -
    -    # Compute the initial list of currencies to consider.
    -    if undeclared_source:
    -        # Use the full set of possible currencies.
    -        cur_at_cost = find_currencies_at_cost(entries)
    -        cur_converted = find_currencies_converted(entries, date)
    -        cur_priced = find_currencies_priced(entries, date)
    -        currencies = cur_at_cost | cur_converted | cur_priced
    -        log_currency_list("Currency held at cost", cur_at_cost)
    -        log_currency_list("Currency converted", cur_converted)
    -        log_currency_list("Currency priced", cur_priced)
    -        default_source = import_source(undeclared_source)
    -    else:
    -        # Use the currencies from the Commodity directives.
    -        currencies = set(currency_map.keys())
    -        default_source = None
    -
    -    log_currency_list("Currencies in primary list", currencies)
    -
    -    # By default, restrict to only the currencies with non-zero balances at the
    -    # given date.
    -    if not inactive:
    -        balance_currencies = find_balance_currencies(entries, date)
    -        log_currency_list("Currencies held in assets", balance_currencies)
    -        currencies = currencies & balance_currencies
    -
    -    log_currency_list("Currencies to fetch", currencies)
    -
    -    # Build up the list of jobs to fetch prices for.
    -    jobs = []
    -    for base_quote in currencies:
    -        psources = currency_map.get(base_quote, None)
    -        base, quote = base_quote
    -
    -        # If there are no sources, create a default one.
    -        if not psources:
    -            psources = [PriceSource(default_source, base, False)]
    -
    -        jobs.append(DatedPrice(base, quote, date, psources))
    -    return sorted(jobs)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.import_source(module_name) - - -

    - -
    - -

    Import the source module defined by the given name.

    -

    The default location is handled here.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • short_module_name – A string, the name of a Python module, which may -be within the default package or a full name.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A corresponding Python module object.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • ImportError – If the module cannot be imported.

    • -
    -
    -
    - Source code in beancount/prices/find_prices.py -
    def import_source(module_name):
    -    """Import the source module defined by the given name.
    -
    -    The default location is handled here.
    -
    -    Args:
    -      short_module_name: A string, the name of a Python module, which may
    -        be within the default package or a full name.
    -    Returns:
    -      A corresponding Python module object.
    -    Raises:
    -      ImportError: If the module cannot be imported.
    -    """
    -    default_name = '{}.{}'.format(DEFAULT_PACKAGE, module_name)
    -    try:
    -        __import__(default_name)
    -        return sys.modules[default_name]
    -    except ImportError:
    -        try:
    -            __import__(module_name)
    -            return sys.modules[module_name]
    -        except ImportError as exc:
    -            raise ImportError('Could not find price source module "{}": {}'.format(
    -                module_name, exc))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.log_currency_list(message, currencies) - - -

    - -
    - -

    Log a list of currencies to debug output.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • message – A message string to prepend.

    • -
    • currencies – A list of (base, quote) currency pair.

    • -
    -
    -
    - Source code in beancount/prices/find_prices.py -
    def log_currency_list(message, currencies):
    -    """Log a list of currencies to debug output.
    -
    -    Args:
    -      message: A message string to prepend.
    -      currencies: A list of (base, quote) currency pair.
    -    """
    -    logging.debug("-------- {}:".format(message))
    -    for base, quote in currencies:
    -        logging.debug("  {:>32}".format('{} /{}'.format(base, quote)))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.parse_single_source(source) - - -

    - -
    - -

    Parse a single source string.

    -

    Source specifications follow the syntax:

    -

    <module>/[^]<ticker>

    -

    The <module> is resolved against the Python path, but first looked up -under the package where the default price extractors lie.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • source – A single source string specification.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A PriceSource tuple, or

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • ValueError – If invalid.

    • -
    -
    -
    - Source code in beancount/prices/find_prices.py -
    def parse_single_source(source):
    -    """Parse a single source string.
    -
    -    Source specifications follow the syntax:
    -
    -      <module>/[^]<ticker>
    -
    -    The <module> is resolved against the Python path, but first looked up
    -    under the package where the default price extractors lie.
    -
    -    Args:
    -      source: A single source string specification.
    -    Returns:
    -      A PriceSource tuple, or
    -    Raises:
    -      ValueError: If invalid.
    -    """
    -    match = re.match(r'([a-zA-Z]+[a-zA-Z0-9\._]+)/(\^?)([a-zA-Z0-9:=_\-\.]+)$', source)
    -    if not match:
    -        raise ValueError('Invalid source name: "{}"'.format(source))
    -    short_module_name, invert, symbol = match.groups()
    -    module = import_source(short_module_name)
    -    return PriceSource(module, symbol, bool(invert))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.find_prices.parse_source_map(source_map_spec) - - -

    - -
    - -

    Parse a source map specification string.

    -

    Source map specifications allow the specification of multiple sources for -multiple quote currencies and follow the following syntax:

    -

    <currency1>:<source1>,<source2>,... <currency2>:<source1>,...

    -

    Where a <source> itself follows:

    -

    <module>/[^]<ticker>

    -

    The <module> is resolved against the Python path, but first looked up under -the package where the default price extractors lie. The presence of a '^' -character indicates that we should use the inverse of the rate pull from -this source.

    -

    For example, for prices of AAPL in USD:

    -

    USD:google/NASDAQ:AAPL,yahoo/AAPL

    -

    Or for the exchange rate of a currency, such as INR in USD or in CAD:

    -

    USD:google/^CURRENCY:USDINR CAD:google/^CURRENCY:CADINR

    - - - - - - - - - - - - -
    Parameters: -
      -
    • source_map_spec – A string, a full source map specification to be parsed.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • FIXME – TODO

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • ValueError – If an invalid pattern has been specified.

    • -
    -
    -
    - Source code in beancount/prices/find_prices.py -
    def parse_source_map(source_map_spec):
    -    """Parse a source map specification string.
    -
    -    Source map specifications allow the specification of multiple sources for
    -    multiple quote currencies and follow the following syntax:
    -
    -       <currency1>:<source1>,<source2>,... <currency2>:<source1>,...
    -
    -    Where a <source> itself follows:
    -
    -       <module>/[^]<ticker>
    -
    -    The <module> is resolved against the Python path, but first looked up under
    -    the package where the default price extractors lie. The presence of a '^'
    -    character indicates that we should use the inverse of the rate pull from
    -    this source.
    -
    -    For example, for prices of AAPL in USD:
    -
    -       USD:google/NASDAQ:AAPL,yahoo/AAPL
    -
    -    Or for the exchange rate of a currency, such as INR in USD or in CAD:
    -
    -       USD:google/^CURRENCY:USDINR CAD:google/^CURRENCY:CADINR
    -
    -    Args:
    -      source_map_spec: A string, a full source map specification to be parsed.
    -    Returns:
    -      FIXME: TODO
    -    Raises:
    -      ValueError: If an invalid pattern has been specified.
    -    """
    -    source_map = collections.defaultdict(list)
    -    for source_list_spec in re.split('[ ;]', source_map_spec):
    -        match = re.match('({}):(.*)$'.format(amount.CURRENCY_RE),
    -                         source_list_spec)
    -        if not match:
    -            raise ValueError('Invalid source map pattern: "{}"'.format(source_list_spec))
    -
    -        currency, source_strs = match.groups()
    -        source_map[currency].extend(
    -            parse_single_source(source_str)
    -            for source_str in source_strs.split(','))
    -    return source_map
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.prices.price - - - -

    - -
    - -

    Driver code for the price script.

    - - - -
    - - - - - - - - - - - - - -
    - - - -

    -beancount.prices.price.fetch_cached_price(source, symbol, date) - - -

    - -
    - -

    Call Source to fetch a price, but look and/or update the cache first.

    -

    This function entirely deals with caching and correct expiration. It keeps -old prices if they were fetched in the past, and it quickly expires -intra-day prices if they are fetched on the same day.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • source – A Python module object.

    • -
    • symbol – A string, the ticker to fetch.

    • -
    • date – A datetime.date instance, None if we're to fetch the latest date.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A SourcePrice instance.

    • -
    -
    -
    - Source code in beancount/prices/price.py -
    def fetch_cached_price(source, symbol, date):
    -    """Call Source to fetch a price, but look and/or update the cache first.
    -
    -    This function entirely deals with caching and correct expiration. It keeps
    -    old prices if they were fetched in the past, and it quickly expires
    -    intra-day prices if they are fetched on the same day.
    -
    -    Args:
    -      source: A Python module object.
    -      symbol: A string, the ticker to fetch.
    -      date: A datetime.date instance, None if we're to fetch the latest date.
    -    Returns:
    -      A SourcePrice instance.
    -    """
    -    # Compute a suitable timestamp from the date, if specified.
    -    if date is not None:
    -        # We query as for 4pm for the given date of the current timezone, if
    -        # specified.
    -        query_time = datetime.time(16, 0, 0)
    -        time_local = datetime.datetime.combine(date, query_time, tzinfo=tz.tzlocal())
    -        time = time_local.astimezone(tz.tzutc())
    -    else:
    -        time = None
    -
    -    if _CACHE is None:
    -        # The cache is disabled; just call and return.
    -        result = (source.get_latest_price(symbol)
    -                  if time is None else
    -                  source.get_historical_price(symbol, time))
    -
    -    else:
    -        # The cache is enabled and we have to compute the current/latest price.
    -        # Try to fetch from the cache but miss if the price is too old.
    -        md5 = hashlib.md5()
    -        md5.update(str((type(source).__module__, symbol, date)).encode('utf-8'))
    -        key = md5.hexdigest()
    -        timestamp_now = int(now().timestamp())
    -        try:
    -            timestamp_created, result_naive = _CACHE[key]
    -
    -            # Convert naive timezone to UTC, which is what the cache is always
    -            # assumed to store. (The reason for this is that timezones from
    -            # aware datetime objects cannot be serialized properly due to bug.)
    -            if result_naive.time is not None:
    -                result = result_naive._replace(
    -                    time=result_naive.time.replace(tzinfo=tz.tzutc()))
    -            else:
    -                result = result_naive
    -
    -            if (timestamp_now - timestamp_created) > _CACHE.expiration.total_seconds():
    -                raise KeyError
    -        except KeyError:
    -            logging.info("Fetching: %s (time: %s)", symbol, time)
    -            try:
    -                result = (source.get_latest_price(symbol)
    -                          if time is None else
    -                          source.get_historical_price(symbol, time))
    -            except ValueError as exc:
    -                logging.error("Error fetching %s: %s", symbol, exc)
    -                result = None
    -
    -            # Make sure the timezone is UTC and make naive before serialization.
    -            if result and result.time is not None:
    -                time_utc = result.time.astimezone(tz.tzutc())
    -                time_naive = time_utc.replace(tzinfo=None)
    -                result_naive = result._replace(time=time_naive)
    -            else:
    -                result_naive = result
    -
    -            if result_naive is not None:
    -                _CACHE[key] = (timestamp_now, result_naive)
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.price.fetch_price(dprice, swap_inverted=False) - - -

    - -
    - -

    Fetch a price for the DatedPrice job.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • dprice – A DatedPrice instances.

    • -
    • swap_inverted – A boolean, true if we should invert currencies instead of -rate for an inverted price source.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A Price entry corresponding to the output of the jobs processed.

    • -
    -
    -
    - Source code in beancount/prices/price.py -
    def fetch_price(dprice, swap_inverted=False):
    -    """Fetch a price for the DatedPrice job.
    -
    -    Args:
    -      dprice: A DatedPrice instances.
    -      swap_inverted: A boolean, true if we should invert currencies instead of
    -        rate for an inverted price source.
    -    Returns:
    -      A Price entry corresponding to the output of the jobs processed.
    -
    -    """
    -    for psource in dprice.sources:
    -        try:
    -            source = psource.module.Source()
    -        except AttributeError:
    -            continue
    -        srcprice = fetch_cached_price(source, psource.symbol, dprice.date)
    -        if srcprice is not None:
    -            break
    -    else:
    -        if dprice.sources:
    -            logging.error("Could not fetch for job: %s", dprice)
    -        return None
    -
    -    base = dprice.base
    -    quote = dprice.quote or srcprice.quote_currency
    -    price = srcprice.price
    -
    -    # Invert the rate if requested.
    -    if psource.invert:
    -        if swap_inverted:
    -            base, quote = quote, base
    -        else:
    -            price = ONE/price
    -
    -    assert base is not None
    -    fileloc = data.new_metadata('<{}>'.format(type(psource.module).__name__), 0)
    -
    -    # The datetime instance is required to be aware. We always convert to the
    -    # user's timezone before extracting the date. This means that if the market
    -    # returns a timestamp for a particular date, once we convert to the user's
    -    # timezone the returned date may be different by a day. The intent is that
    -    # whatever we print is assumed coherent with the user's timezone. See
    -    # discussion at
    -    # https://groups.google.com/d/msg/beancount/9j1E_HLEMBQ/fYRuCQK_BwAJ
    -    srctime = srcprice.time
    -    if srctime.tzinfo is None:
    -        raise ValueError("Time returned by the price source is not timezone aware.")
    -    date = srctime.astimezone(tz.tzlocal()).date()
    -
    -    return data.Price(fileloc, date, base,
    -                      amount.Amount(price, quote or UNKNOWN_CURRENCY))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.price.filter_redundant_prices(price_entries, existing_entries, diffs=False) - - -

    - -
    - -

    Filter out new entries that are redundant from an existing set.

    -

    If the price differs, we override it with the new entry only on demand. This -is because this would create conflict with existing price entries when -parsing, if the new entries are simply inserted into the input.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • price_entries – A list of newly created, proposed to be added Price directives.

    • -
    • existing_entries – A list of existing entries we are proposing to add to.

    • -
    • diffs – A boolean, true if we should output differing price entries -at the same date.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A filtered list of remaining entries, and a list of ignored entries.

    • -
    -
    -
    - Source code in beancount/prices/price.py -
    def filter_redundant_prices(price_entries, existing_entries, diffs=False):
    -    """Filter out new entries that are redundant from an existing set.
    -
    -    If the price differs, we override it with the new entry only on demand. This
    -    is because this would create conflict with existing price entries when
    -    parsing, if the new entries are simply inserted into the input.
    -
    -    Args:
    -      price_entries: A list of newly created, proposed to be added Price directives.
    -      existing_entries: A list of existing entries we are proposing to add to.
    -      diffs: A boolean, true if we should output differing price entries
    -        at the same date.
    -    Returns:
    -      A filtered list of remaining entries, and a list of ignored entries.
    -    """
    -    # Note: We have to be careful with the dates, because requesting the latest
    -    # price for a date may yield the price at a previous date. Clobber needs to
    -    # take this into account. See {1cfa25e37fc1}.
    -    existing_prices = {(entry.date, entry.currency): entry
    -                       for entry in existing_entries
    -                       if isinstance(entry, data.Price)}
    -    filtered_prices = []
    -    ignored_prices = []
    -    for entry in price_entries:
    -        key = (entry.date, entry.currency)
    -        if key in existing_prices:
    -            if diffs:
    -                existing_entry = existing_prices[key]
    -                if existing_entry.amount == entry.amount:
    -                    output = ignored_prices
    -            else:
    -                output = ignored_prices
    -        else:
    -            output = filtered_prices
    -        output.append(entry)
    -    return filtered_prices, ignored_prices
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.prices.price.now() - - -

    - -
    - -

    Indirection in order to be able to mock it out in the tests.

    - -
    - Source code in beancount/prices/price.py -
    def now():
    -    "Indirection in order to be able to mock it out in the tests."
    -    return datetime.datetime.now(datetime.timezone.utc)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.price.process_args() - - -

    - -
    - -

    Process the arguments. This also initializes the logging module.

    - - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of – args: The argparse receiver of command-line arguments. - jobs: A list of DatedPrice job objects. - entries: A list of all the parsed entries.

    • -
    -
    -
    - Source code in beancount/prices/price.py -
    def process_args():
    -    """Process the arguments. This also initializes the logging module.
    -
    -    Returns:
    -      A tuple of:
    -        args: The argparse receiver of command-line arguments.
    -        jobs: A list of DatedPrice job objects.
    -        entries: A list of all the parsed entries.
    -    """
    -    parser = version.ArgumentParser(description=beancount.prices.__doc__.splitlines()[0])
    -
    -    # Input sources or filenames.
    -    parser.add_argument('sources', nargs='+', help=(
    -        'A list of filenames (or source "module/symbol", if -e is '
    -        'specified) from which to create a list of jobs.'))
    -
    -    parser.add_argument('-e', '--expressions', '--expression', action='store_true', help=(
    -        'Interpret the arguments as "module/symbol" source strings.'))
    -
    -    # Regular options.
    -    parser.add_argument('-v', '--verbose', action='count', help=(
    -        "Print out progress log. Specify twice for debugging info."))
    -
    -    parser.add_argument('-d', '--date', action='store',
    -                        type=date_utils.parse_date_liberally, help=(
    -        "Specify the date for which to fetch the prices."))
    -
    -    parser.add_argument('-i', '--inactive', action='store_true', help=(
    -        "Select all commodities from input files, not just the ones active on the date"))
    -
    -    parser.add_argument('-u', '--undeclared', action='store', help=(
    -        "Include commodities viewed in the file even without a "
    -        "corresponding Commodity directive, from this default source. "
    -        "The currency name itself is used as the lookup symbol in this default source."))
    -
    -    parser.add_argument('-c', '--clobber', action='store_true', help=(
    -        "Do not skip prices which are already present in input files; fetch them anyway."))
    -
    -    parser.add_argument('-a', '--all', action='store_true', help=(
    -        "A shorthand for --inactive, --undeclared, --clobber."))
    -
    -    parser.add_argument('-s', '--swap-inverted', action='store_true', help=(
    -        "For inverted sources, swap currencies instead of inverting the rate. "
    -        "For example, if fetching the rate for CAD from 'USD:google/^CURRENCY:USDCAD' "
    -        "results in 1.25, by default we would output \"price CAD  0.8000 USD\". "
    -        "Using this option we would instead output \" price USD   1.2500 CAD\"."))
    -
    -    parser.add_argument('-n', '--dry-run', action='store_true', help=(
    -        "Don't actually fetch the prices, just print the list of the ones to be fetched."))
    -
    -    # Caching options.
    -    cache_group = parser.add_argument_group('cache')
    -    cache_filename = path.join(tempfile.gettempdir(),
    -                               "{}.cache".format(path.basename(sys.argv[0])))
    -    cache_group.add_argument('--cache', dest='cache_filename',
    -                             action='store', default=cache_filename,
    -                             help="Enable the cache and with the given cache name.")
    -    cache_group.add_argument('--no-cache', dest='cache_filename',
    -                             action='store_const', const=None,
    -                             help="Disable the price cache.")
    -
    -    cache_group.add_argument('--clear-cache', action='store_true',
    -                             help="Clear the cache prior to startup")
    -
    -    args = parser.parse_args()
    -
    -    verbose_levels = {None: logging.WARN,
    -                      0: logging.WARN,
    -                      1: logging.INFO,
    -                      2: logging.DEBUG}
    -    logging.basicConfig(level=verbose_levels[args.verbose],
    -                        format='%(levelname)-8s: %(message)s')
    -
    -    if args.all:
    -        args.inactive = args.clobber = True
    -        args.undeclared = DEFAULT_SOURCE
    -
    -    # Setup for processing.
    -    setup_cache(args.cache_filename, args.clear_cache)
    -
    -    # Get the list of DatedPrice jobs to get from the arguments.
    -    logging.info("Processing at date: %s", args.date or datetime.date.today())
    -    jobs = []
    -    all_entries = []
    -    dcontext = None
    -    if args.expressions:
    -        # Interpret the arguments as price sources.
    -        for source_str in args.sources:
    -            psources = []
    -            try:
    -                psource_map = find_prices.parse_source_map(source_str)
    -            except ValueError:
    -                extra = "; did you provide a filename?" if path.exists(source_str) else ''
    -                msg = ('Invalid source "{{}}"{}. '.format(extra) +
    -                       'Supported format is "CCY:module/SYMBOL"')
    -                parser.error(msg.format(source_str))
    -            else:
    -                for currency, psources in psource_map.items():
    -                    jobs.append(find_prices.DatedPrice(
    -                        psources[0].symbol, currency, args.date, psources))
    -    else:
    -        # Interpret the arguments as Beancount input filenames.
    -        for filename in args.sources:
    -            if not path.exists(filename) or not path.isfile(filename):
    -                parser.error('File does not exist: "{}"; '
    -                             'did you mean to use -e?'.format(filename))
    -                continue
    -            logging.info('Loading "%s"', filename)
    -            entries, errors, options_map = loader.load_file(filename, log_errors=sys.stderr)
    -            if dcontext is None:
    -                dcontext = options_map['dcontext']
    -            jobs.extend(
    -                find_prices.get_price_jobs_at_date(
    -                    entries, args.date, args.inactive, args.undeclared))
    -            all_entries.extend(entries)
    -
    -    return args, jobs, data.sorted(all_entries), dcontext
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.price.reset_cache() - - -

    - -
    - -

    Reset the cache to its uninitialized state.

    - -
    - Source code in beancount/prices/price.py -
    def reset_cache():
    -    """Reset the cache to its uninitialized state."""
    -    global _CACHE
    -    if _CACHE is not None:
    -        _CACHE.close()
    -    _CACHE = None
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.price.setup_cache(cache_filename, clear_cache) - - -

    - -
    - -

    Setup the results cache.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • cache_filename – A string or None, the filename for the cache.

    • -
    • clear_cache – A boolean, if true, delete the cache before beginning.

    • -
    -
    -
    - Source code in beancount/prices/price.py -
    def setup_cache(cache_filename, clear_cache):
    -    """Setup the results cache.
    -
    -    Args:
    -      cache_filename: A string or None, the filename for the cache.
    -      clear_cache: A boolean, if true, delete the cache before beginning.
    -    """
    -    if clear_cache and cache_filename and path.exists(cache_filename):
    -        logging.info("Clearing cache %s", cache_filename)
    -        os.remove(cache_filename)
    -
    -    if cache_filename:
    -        logging.info('Using price cache at "%s" (with indefinite expiration)',
    -                     cache_filename)
    -
    -        global _CACHE
    -        _CACHE = shelve.open(cache_filename, 'c')
    -        _CACHE.expiration = DEFAULT_EXPIRATION
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.prices.source - - - -

    - -
    - -

    Interface definition for all price sources.

    -

    This module describes the contract to be fulfilled by all implementations of -price sources.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.prices.source.Source - - - -

    - -
    - -

    Interface to be implemented by all price sources.

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.prices.source.Source.get_historical_price(self, ticker, time) - - -

    - -
    - -

    Return the historical price found for the symbol at the given date.

    -

    This could be the price of the close of the day, for instance. We assume -that there is some single price representative of the day.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • ticker – A string, the ticker to be fetched by the source. This ticker -may include structure, such as the exchange code. Also note that -this ticker is source-specified, and is not necessarily the same -value as the commodity symbol used in the Beancount file.

    • -
    • time – The timestamp at which to query for the price. This is a -timezone-aware timestamp you can convert to any timezone. For past -dates we query for a time that is equivalent to 4pm in the user's -timezone.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A SourcePrice instance. If the price could not be fetched, None is -returned and another source should be consulted. There is never any -guarantee that a price source will be able to fetch its value; client -code must be able to handle this. Also note that the price's returned -time must be timezone-aware.

    • -
    -
    -
    - Source code in beancount/prices/source.py -
    def get_historical_price(self, ticker, time):
    -    """Return the historical price found for the symbol at the given date.
    -
    -    This could be the price of the close of the day, for instance. We assume
    -    that there is some single price representative of the day.
    -
    -    Args:
    -      ticker: A string, the ticker to be fetched by the source. This ticker
    -        may include structure, such as the exchange code. Also note that
    -        this ticker is source-specified, and is not necessarily the same
    -        value as the commodity symbol used in the Beancount file.
    -      time: The timestamp at which to query for the price. This is a
    -        timezone-aware timestamp you can convert to any timezone. For past
    -        dates we query for a time that is equivalent to 4pm in the user's
    -        timezone.
    -    Returns:
    -      A SourcePrice instance. If the price could not be fetched, None is
    -      returned and another source should be consulted. There is never any
    -      guarantee that a price source will be able to fetch its value; client
    -      code must be able to handle this. Also note that the price's returned
    -      time must be timezone-aware.
    -    """
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.source.Source.get_latest_price(self, ticker) - - -

    - -
    - -

    Fetch the current latest price. The date may differ.

    -

    This routine attempts to fetch the most recent available price, and -returns the actual date of the quoted price, which may differ from the -date this call is made at. {1cfa25e37fc1}

    - - - - - - - - - - - - -
    Parameters: -
      -
    • ticker – A string, the ticker to be fetched by the source. This ticker -may include structure, such as the exchange code. Also note that -this ticker is source-specified, and is not necessarily the same -value as the commodity symbol used in the Beancount file.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A SourcePrice instance. If the price could not be fetched, None is -returned and another source should be consulted. There is never any -guarantee that a price source will be able to fetch its value; client -code must be able to handle this. Also note that the price's returned -time must be timezone-aware.

    • -
    -
    -
    - Source code in beancount/prices/source.py -
    def get_latest_price(self, ticker):
    -    """Fetch the current latest price. The date may differ.
    -
    -    This routine attempts to fetch the most recent available price, and
    -    returns the actual date of the quoted price, which may differ from the
    -    date this call is made at. {1cfa25e37fc1}
    -
    -    Args:
    -      ticker: A string, the ticker to be fetched by the source. This ticker
    -        may include structure, such as the exchange code. Also note that
    -        this ticker is source-specified, and is not necessarily the same
    -        value as the commodity symbol used in the Beancount file.
    -    Returns:
    -      A SourcePrice instance. If the price could not be fetched, None is
    -      returned and another source should be consulted. There is never any
    -      guarantee that a price source will be able to fetch its value; client
    -      code must be able to handle this. Also note that the price's returned
    -      time must be timezone-aware.
    -    """
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.prices.source.SourcePrice (tuple) - - - - -

    - -
    - -

    SourcePrice(price, time, quote_currency)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.prices.source.SourcePrice.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/prices/source.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.source.SourcePrice.__new__(_cls, price, time, quote_currency) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of SourcePrice(price, time, quote_currency)

    - -
    - -
    - - - -
    - - - -

    -beancount.prices.source.SourcePrice.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/prices/source.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.prices.sources - - - - special - - -

    - -
    - -

    Implementation of various price extractors.

    -

    This package is looked up by the driver script to figure out which extractor to -use.

    - - - -
    - - - - - - - - - - - - -
    - - - -

    - beancount.prices.sources.coinbase - - - -

    - -
    - -

    A source fetching cryptocurrency prices from Coinbase.

    -

    Valid tickers are in the form "XXX-YYY", such as "BTC-USD".

    -

    Here is the API documentation: -https://developers.coinbase.com/api/v2

    -

    For example: -https://api.coinbase.com/v2/prices/BTC-GBP/spot

    -

    Timezone information: Input and output datetimes are specified via UTC -timestamps.

    - - - -
    - - - - - - - -
    - - - -

    - -beancount.prices.sources.coinbase.CoinbaseError (ValueError) - - - - -

    - -
    - -

    An error from the Coinbase API.

    - - - -
    - -
    - - - -
    - - - -

    - -beancount.prices.sources.coinbase.Source (Source) - - - - -

    - -
    - -

    Coinbase API price extractor.

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.prices.sources.coinbase.Source.get_historical_price(self, ticker, time) - - -
    - -
    - -

    See contract in beancount.prices.source.Source.

    - -
    - Source code in beancount/prices/sources/coinbase.py -
    def get_historical_price(self, ticker, time):
    -    """See contract in beancount.prices.source.Source."""
    -    raise NotImplementedError(
    -        "As of Feb 2019, historical prices are not supported on Coinbase. "
    -        "Please check the API to see if this has changed: "
    -        "https://developers.coinbase.com/apo/v2")
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.prices.sources.coinbase.Source.get_latest_price(self, ticker) - - -
    - -
    - -

    See contract in beancount.prices.source.Source.

    - -
    - Source code in beancount/prices/sources/coinbase.py -
    def get_latest_price(self, ticker):
    -    """See contract in beancount.prices.source.Source."""
    -    return fetch_quote(ticker)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.prices.sources.coinbase.fetch_quote(ticker) - - -

    - -
    - -

    Fetch a quote from Coinbase.

    - -
    - Source code in beancount/prices/sources/coinbase.py -
    def fetch_quote(ticker):
    -    """Fetch a quote from Coinbase."""
    -    url = "https://api.coinbase.com/v2/prices/{}/spot".format(ticker.lower())
    -    response = requests.get(url)
    -    if response.status_code != requests.codes.ok:
    -        raise CoinbaseError("Invalid response ({}): {}".format(response.status_code,
    -                                                               response.text))
    -    result = response.json()
    -
    -    price = D(result['data']['amount']).quantize(D('0.01'))
    -    time = datetime.datetime.now(tz.tzutc())
    -    currency = result['data']['currency']
    -
    -    return source.SourcePrice(price, time, currency)
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.prices.sources.iex - - - -

    - -
    - -

    Fetch prices from the IEX 1.0 public API.

    -

    This is a really fantastic exchange API with a lot of relevant information.

    -

    Timezone information: There is currency no support for historical prices. The -output datetime is provided as a UNIX timestamp.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.prices.sources.iex.IEXError (ValueError) - - - - -

    - -
    - -

    An error from the IEX API.

    - - - -
    - -
    - - - -
    - - - -

    - -beancount.prices.sources.iex.Source (Source) - - - - -

    - -
    - -

    IEX API price extractor.

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.prices.sources.iex.Source.get_historical_price(self, ticker, time) - - -
    - -
    - -

    See contract in beancount.prices.source.Source.

    - -
    - Source code in beancount/prices/sources/iex.py -
    def get_historical_price(self, ticker, time):
    -    """See contract in beancount.prices.source.Source."""
    -    raise NotImplementedError(
    -        "This is now implemented at https://iextrading.com/developers/docs/#hist and "
    -        "needs to be added here.")
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.prices.sources.iex.Source.get_latest_price(self, ticker) - - -
    - -
    - -

    See contract in beancount.prices.source.Source.

    - -
    - Source code in beancount/prices/sources/iex.py -
    def get_latest_price(self, ticker):
    -    """See contract in beancount.prices.source.Source."""
    -    return fetch_quote(ticker)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.prices.sources.iex.fetch_quote(ticker) - - -

    - -
    - -

    Fetch the latest price for the given ticker.

    - -
    - Source code in beancount/prices/sources/iex.py -
    def fetch_quote(ticker):
    -    """Fetch the latest price for the given ticker."""
    -
    -    url = "https://api.iextrading.com/1.0/tops/last?symbols={}".format(ticker.upper())
    -    response = requests.get(url)
    -    if response.status_code != requests.codes.ok:
    -        raise IEXError("Invalid response ({}): {}".format(
    -            response.status_code, response.text))
    -
    -    results = response.json()
    -    if len(results) != 1:
    -        raise IEXError("Invalid number of responses from IEX: {}".format(
    -            response.text))
    -    result = results[0]
    -
    -    price = D(result['price']).quantize(D('0.01'))
    -
    -    # IEX is American markets.
    -    us_timezone = tz.gettz("America/New_York")
    -    time = datetime.datetime.fromtimestamp(result['time'] / 1000)
    -    time = time.astimezone(us_timezone)
    -
    -    # As far as can tell, all the instruments on IEX are priced in USD.
    -    return source.SourcePrice(price, time, 'USD')
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.prices.sources.oanda - - - -

    - -
    - -

    A source fetching currency prices from OANDA.

    -

    Valid tickers are in the form "XXX_YYY", such as "EUR_USD".

    -

    Here is the API documentation: -https://developer.oanda.com/rest-live/rates/

    -

    For example: -https://api-fxtrade.oanda.com/v1/candles?instrument=EUR_USD&granularity=D&start=2016-03-27T00%3A00%3A00Z&end=2016-04-04T00%3A00%3A00Z&candleFormat=midpoint

    -

    Timezone information: Input and output datetimes are specified via UTC -timestamps.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.prices.sources.oanda.Source (Source) - - - - -

    - -
    - -

    OANDA price source extractor.

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.prices.sources.oanda.Source.get_historical_price(self, ticker, time) - - -
    - -
    - -

    See contract in beancount.prices.source.Source.

    - -
    - Source code in beancount/prices/sources/oanda.py -
    def get_historical_price(self, ticker, time):
    -    """See contract in beancount.prices.source.Source."""
    -    time = time.astimezone(tz.tzutc())
    -    query_interval_begin = (time - datetime.timedelta(days=5))
    -    query_interval_end = (time + datetime.timedelta(days=1))
    -    params_dict = {
    -        'instrument': ticker,
    -        'granularity': 'H2',  # Every two hours.
    -        'candleFormat': 'midpoint',
    -        'start': query_interval_begin.isoformat('T'),
    -        'end': query_interval_end.isoformat('T'),
    -    }
    -    return _fetch_price(params_dict, time)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.prices.sources.oanda.Source.get_latest_price(self, ticker) - - -
    - -
    - -

    See contract in beancount.prices.source.Source.

    - -
    - Source code in beancount/prices/sources/oanda.py -
    def get_latest_price(self, ticker):
    -    """See contract in beancount.prices.source.Source."""
    -    time = datetime.datetime.now(tz.tzutc())
    -    params_dict = {
    -        'instrument': ticker,
    -        'granularity': 'S5',  # Every two hours.
    -        'count': '10',
    -        'candleFormat': 'midpoint',
    -    }
    -    return _fetch_price(params_dict, time)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.prices.sources.quandl - - - -

    - -
    - -

    Fetch prices from Quandl's simple URL-based API.

    -

    Quandl is a useful source of alternative data and it offers a simple REST API -that serves CSV and JSON and XML formats. There's also a Python client library, -but we specifically avoid using that here, in order to keep Beancount -dependency-free.

    -

    Many of the datasets are freely available, which is why this is included here. -You can get information about the available databases and associated lists of -symbols you can use here: https://www.quandl.com/search

    -

    If you have a paid account and would like to be able to access the premium -databases from the Quandl site, you can set QUANDL_API_KEY environment variable.

    -

    Use the "<DATABASE_CODE>:<DATASET_CODE>" format to refer to Quandl symbols. -Note that their symbols are usually identified by "<DATABASE_CODE>/<DATASET_CODE>".

    -

    (For now, this supports only the Time-Series API. There is also a Tables API, -which could easily get integrated. We would just have to encode the -'datatable_code' and 'format' and perhaps other fields in the ticker name.)

    -

    Timezone information: Input and output datetimes are limited to dates, and I -believe the dates are presumed to live in the timezone of each particular data -source. (It's unclear, not documented.)

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.prices.sources.quandl.QuandlError (ValueError) - - - - -

    - -
    - -

    An error from the Quandl API.

    - - - -
    - -
    - - - -
    - - - -

    - -beancount.prices.sources.quandl.Source (Source) - - - - -

    - -
    - -

    Quandl API price extractor.

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.prices.sources.quandl.Source.get_historical_price(self, ticker, time) - - -
    - -
    - -

    See contract in beancount.prices.source.Source.

    - -
    - Source code in beancount/prices/sources/quandl.py -
    def get_historical_price(self, ticker, time):
    -    """See contract in beancount.prices.source.Source."""
    -    return fetch_time_series(ticker, time)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.prices.sources.quandl.Source.get_latest_price(self, ticker) - - -
    - -
    - -

    See contract in beancount.prices.source.Source.

    - -
    - Source code in beancount/prices/sources/quandl.py -
    def get_latest_price(self, ticker):
    -    """See contract in beancount.prices.source.Source."""
    -    return fetch_time_series(ticker)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.prices.sources.quandl.fetch_time_series(ticker, time=None) - - -

    - -
    - -

    Fetch

    - -
    - Source code in beancount/prices/sources/quandl.py -
    def fetch_time_series(ticker, time=None):
    -    """Fetch"""
    -    # Create request payload.
    -    database, dataset = parse_ticker(ticker)
    -    url = "https://www.quandl.com/api/v3/datasets/{}/{}.json".format(database, dataset)
    -    payload = {"limit": 1}
    -    if time is not None:
    -        date = time.date()
    -        payload["start_date"] = (date - datetime.timedelta(days=10)).isoformat()
    -        payload["end_date"] = date.isoformat()
    -
    -    # Add API key, if it is set in the environment.
    -    if 'QUANDL_API_KEY' in os.environ:
    -        payload['api_key'] = os.environ['QUANDL_API_KEY']
    -
    -    # Fetch and process errors.
    -    response = requests.get(url, params=payload)
    -    if response.status_code != requests.codes.ok:
    -        raise QuandlError("Invalid response ({}): {}".format(response.status_code,
    -                                                             response.text))
    -    result = response.json()
    -    if 'quandl_error' in result:
    -        raise QuandlError(result['quandl_error']['message'])
    -
    -    # Parse result container.
    -    dataset = result['dataset']
    -    column_names = dataset['column_names']
    -    date_index = column_names.index('Date')
    -    try:
    -        data_index = column_names.index('Adj. Close')
    -    except ValueError:
    -        data_index = column_names.index('Close')
    -    data = dataset['data'][0]
    -
    -    # Gather time and assume it's in UTC timezone (Quandl does not provide the
    -    # market's timezone).
    -    time = datetime.datetime.strptime(data[date_index], '%Y-%m-%d')
    -    time = time.replace(tzinfo=tz.tzutc())
    -
    -    # Gather price.
    -    # Quantize with the same precision default rendering of floats occur.
    -    price_float = data[data_index]
    -    price = D(price_float)
    -    match = re.search(r'(\..*)', str(price_float))
    -    if match:
    -        price = price.quantize(D(match.group(1)))
    -
    -    # Note: There is no currency information in the response (surprising).
    -    return source.SourcePrice(price, time, None)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.sources.quandl.parse_ticker(ticker) - - -

    - -
    - -

    Convert ticker to Quandl codes.

    - -
    - Source code in beancount/prices/sources/quandl.py -
    def parse_ticker(ticker):
    -    """Convert ticker to Quandl codes."""
    -    if not re.match(r"[A-Z0-9]+:[A-Z0-9]+$", ticker):
    -        raise ValueError('Invalid code. Use "<DATABASE>/<DATASET>" format.')
    -    return tuple(ticker.split(":"))
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.prices.sources.yahoo - - - -

    - -
    - -

    Fetch prices from Yahoo Finance's CSV API.

    -

    As of late 2017, the older Yahoo finance API deprecated. In particular, the -ichart endpoint is gone, and the download endpoint requires a cookie (which -could be gotten - here's some documentation for that -http://blog.bradlucas.com/posts/2017-06-02-new-yahoo-finance-quote-download-url/).

    -

    We're using both the v7 and v8 APIs here, both of which are, as far as I can -tell, undocumented:

    -

    https://query1.finance.yahoo.com/v7/finance/quote -https://query1.finance.yahoo.com/v8/finance/chart/SYMBOL

    -

    Timezone information: Input and output datetimes are specified via UNIX -timestamps, but the timezone of the particular market is included in the output.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.prices.sources.yahoo.Source (Source) - - - - -

    - -
    - -

    Yahoo Finance CSV API price extractor.

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.prices.sources.yahoo.Source.get_historical_price(self, ticker, time) - - -
    - -
    - -

    See contract in beancount.prices.source.Source.

    - -
    - Source code in beancount/prices/sources/yahoo.py -
    def get_historical_price(self, ticker, time):
    -    """See contract in beancount.prices.source.Source."""
    -    if requests is None:
    -        raise YahooError("You must install the 'requests' library.")
    -    url = "https://query1.finance.yahoo.com/v8/finance/chart/{}".format(ticker)
    -    dt_start = time - datetime.timedelta(days=5)
    -    dt_end = time
    -    payload = {
    -        'period1': int(dt_start.timestamp()),
    -        'period2': int(dt_end.timestamp()),
    -        'interval': '1d',
    -    }
    -    payload.update(_DEFAULT_PARAMS)
    -    response = requests.get(url, params=payload)
    -    result = parse_response(response)
    -
    -    meta = result['meta']
    -    timezone = datetime.timezone(datetime.timedelta(hours=meta['gmtoffset'] / 3600),
    -                                 meta['exchangeTimezoneName'])
    -
    -    timestamp_array = result['timestamp']
    -    close_array = result['indicators']['quote'][0]['close']
    -    series = [(datetime.datetime.fromtimestamp(timestamp, tz=timezone), D(price))
    -              for timestamp, price in zip(timestamp_array, close_array)]
    -
    -    # Get the latest data returned.
    -    latest = None
    -    for data_dt, price in sorted(series):
    -        if data_dt >= time:
    -            break
    -        latest = data_dt, price
    -    if latest is None:
    -        raise YahooError("Could not find price before {} in {}".format(time, series))
    -
    -    currency = result['meta']['currency']
    -    return source.SourcePrice(price, data_dt, currency)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.prices.sources.yahoo.Source.get_latest_price(self, ticker) - - -
    - -
    - -

    See contract in beancount.prices.source.Source.

    - -
    - Source code in beancount/prices/sources/yahoo.py -
    def get_latest_price(self, ticker):
    -    """See contract in beancount.prices.source.Source."""
    -
    -    url = "https://query1.finance.yahoo.com/v7/finance/quote"
    -    fields = ['symbol', 'regularMarketPrice', 'regularMarketTime']
    -    payload = {
    -        'symbols': ticker,
    -        'fields': ','.join(fields),
    -        'exchange': 'NYSE',
    -    }
    -    payload.update(_DEFAULT_PARAMS)
    -    response = requests.get(url, params=payload)
    -    result = parse_response(response)
    -    try:
    -        price = D(result['regularMarketPrice'])
    -
    -        timezone = datetime.timezone(
    -            datetime.timedelta(hours=result['gmtOffSetMilliseconds'] / 3600000),
    -            result['exchangeTimezoneName'])
    -        trade_time = datetime.datetime.fromtimestamp(result['regularMarketTime'],
    -                                                     tz=timezone)
    -    except KeyError:
    -        raise YahooError("Invalid response from Yahoo: {}".format(repr(result)))
    -
    -    currency = parse_currency(result)
    -
    -    return source.SourcePrice(price, trade_time, currency)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.prices.sources.yahoo.YahooError (ValueError) - - - - -

    - -
    - -

    An error from the Yahoo API.

    - - - -
    - -
    - - - - -
    - - - -

    -beancount.prices.sources.yahoo.parse_currency(result) - - -

    - -
    - -

    Infer the currency from the result.

    - -
    - Source code in beancount/prices/sources/yahoo.py -
    def parse_currency(result: Dict[str, Any]) -> str:
    -    """Infer the currency from the result."""
    -    if 'market' not in result:
    -        return None
    -    return _MARKETS.get(result['market'], None)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.prices.sources.yahoo.parse_response(response) - - -

    - -
    - -

    Process as response from Yahoo.

    - - - - - - - - - - - - -
    Exceptions: -
      -
    • YahooError – If there is an error in the response.

    • -
    -
    -
    - Source code in beancount/prices/sources/yahoo.py -
    def parse_response(response: requests.models.Response) -> Dict:
    -    """Process as response from Yahoo.
    -
    -    Raises:
    -      YahooError: If there is an error in the response.
    -    """
    -    json = response.json(parse_float=D)
    -    content = next(iter(json.values()))
    -    if response.status_code != requests.codes.ok:
    -        raise YahooError("Status {}: {}".format(response.status_code, content['error']))
    -    if len(json) != 1:
    -        raise YahooError("Invalid format in response from Yahoo; many keys: {}".format(
    -            ','.join(json.keys())))
    -    if content['error'] is not None:
    -        raise YahooError("Error fetching Yahoo data: {}".format(content['error']))
    -    return content['result'][0]
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - - -
    - -
    - -
    - - - - -
    - -
    - -
    - -
    -
    - - -
    -
    - -
    - -
    - -
    - - - - « Previous - - - Next » - - -
    - - - - - - - - diff --git a/api_reference/beancount.query.html b/api_reference/beancount.query.html deleted file mode 100644 index 391b5e24..00000000 --- a/api_reference/beancount.query.html +++ /dev/null @@ -1,20171 +0,0 @@ - - - - - - - - - - - - beancount.query - Beancount Documentation - - - - - - - - - - - - - - - -
    - - - - -
    - - - - - -
    -
    -
    -
      -
    • Docs »
    • - - - -
    • API reference »
    • - - - -
    • beancount.query
    • -
    • - -
    • -
    - -
    -
    - -
    -
    - -

    beancount.query

    - - -
    - - -
    - -

    Transaction and postings filtering syntax parser.

    - - - -
    - - - - - - - - - - - - -
    - - - -

    - beancount.query.numberify - - - -

    - -
    - -

    Code to split table columns containing amounts and inventories into number columns.

    -

    For example, given a column with this content:

    -
    ----- amount ------
    -101.23 USD
    -   200 JPY
    - 99.23 USD
    - 38.34 USD, 100 JPY
    -
    -

    We can convert this into two columns and remove the currencies:

    -
    -amount (USD)- -amount (JPY)-
    -        101.23
    -                          200
    -         99.23
    -         38.34            100
    -
    -

    The point is that the columns should be typed as numbers to make this importable -into a spreadsheet and able to be processed.

    -

    Notes:

    -
      -
    • -

      This handles the Amount, Position and Inventory datatypes. There is code to - automatically recognize columns containing such types from a table of strings - and convert such columns to their corresponding guessed data types.

      -
    • -
    • -

      The per-currency columns are ordered in decreasing order of the number of - instances of numbers seen for each currency. So if the most numbers you have - in a column are USD, then the USD column renders first.

      -
    • -
    • -

      Cost basis specifications should be unmodified and reported to a dedicated - extra column, like this:

      -

      ----- amount ------ -1 AAPL {21.23 USD}

      -
    • -
    -

    We can convert this into two columns and remove the currencies:

    -
    -amount (AAPL)- -Cost basis-
    -              1 {21.23 USD}
    -
    -

    (Eventually we might support the conversion of cost amounts as well, but they - may contain other information, such as a label or a date, so for now we don't - convert them. I'm not sure there's a good practical use case in doing that - yet.)

    -
      -
    • -

      We may provide some options to break out only some of the currencies into - columns, in order to handle the case where an inventory contains a large - number of currencies and we want to only operate on a restricted set of - operating currencies.

      -
    • -
    • -

      If you provide a DisplayFormatter object to the numberification routine, they - quantize each column according to their currency's precision. It is - recommended that you do that.

      -
    • -
    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.query.numberify.AmountConverter - - - -

    - -
    - -

    A converter that extracts the number of an amount for a specific currency.

    - - - - -
    - - - - - - - -
    - - - -

    - -beancount.query.numberify.AmountConverter.dtype - - - -

    - -
    - -

    Construct a new Decimal object. 'value' can be an integer, string, tuple, -or another Decimal object. If no value is given, return Decimal('0'). The -context does not affect the conversion and is only passed to determine if -the InvalidOperation trap is active.

    - - - -
    - -
    - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.numberify.IdentityConverter - - - -

    - -
    - -

    A converter that simply copies its column.

    - - - - -
    - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.numberify.InventoryConverter - - - -

    - -
    - -

    A converter that extracts the number of a inventory for a specific currency. -If there are multiple lots we aggregate by currency.

    - - - - -
    - - - - - - - -
    - - - -

    - -beancount.query.numberify.InventoryConverter.dtype - - - -

    - -
    - -

    Construct a new Decimal object. 'value' can be an integer, string, tuple, -or another Decimal object. If no value is given, return Decimal('0'). The -context does not affect the conversion and is only passed to determine if -the InvalidOperation trap is active.

    - - - -
    - -
    - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.numberify.PositionConverter - - - -

    - -
    - -

    A converter that extracts the number of a position for a specific currency.

    - - - - -
    - - - - - - - -
    - - - -

    - -beancount.query.numberify.PositionConverter.dtype - - - -

    - -
    - -

    Construct a new Decimal object. 'value' can be an integer, string, tuple, -or another Decimal object. If no value is given, return Decimal('0'). The -context does not affect the conversion and is only passed to determine if -the InvalidOperation trap is active.

    - - - -
    - -
    - - - - - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.query.numberify.convert_col_Amount(name, drows, index) - - -

    - -
    - -

    Create converters for a column of type Amount.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • name – A string, the column name.

    • -
    • drows – The table of objects.

    • -
    • index – The column number.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of Converter instances, one for each of the currency types found.

    • -
    -
    -
    - Source code in beancount/query/numberify.py -
    def convert_col_Amount(name, drows, index):
    -    """Create converters for a column of type Amount.
    -
    -    Args:
    -      name: A string, the column name.
    -      drows: The table of objects.
    -      index: The column number.
    -    Returns:
    -      A list of Converter instances, one for each of the currency types found.
    -    """
    -    currency_map = collections.defaultdict(int)
    -    for drow in drows:
    -        vamount = drow[index]
    -        if vamount and vamount.currency:
    -            currency_map[vamount.currency] += 1
    -    return [AmountConverter('{} ({})'.format(name, currency), index, currency)
    -            for currency, _ in sorted(currency_map.items(),
    -                                      key=lambda item: (item[1], item[0]),
    -                                      reverse=True)]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.numberify.convert_col_Inventory(name, drows, index) - - -

    - -
    - -

    Create converters for a column of type Inventory.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • name – A string, the column name.

    • -
    • drows – The table of objects.

    • -
    • index – The column number.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of Converter instances, one for each of the currency types found.

    • -
    -
    -
    - Source code in beancount/query/numberify.py -
    def convert_col_Inventory(name, drows, index):
    -    """Create converters for a column of type Inventory.
    -
    -    Args:
    -      name: A string, the column name.
    -      drows: The table of objects.
    -      index: The column number.
    -    Returns:
    -      A list of Converter instances, one for each of the currency types found.
    -    """
    -    currency_map = collections.defaultdict(int)
    -    for drow in drows:
    -        inv = drow[index]
    -        for currency in inv.currencies():
    -            currency_map[currency] += 1
    -    return [InventoryConverter('{} ({})'.format(name, currency), index, currency)
    -            for currency, _ in sorted(currency_map.items(),
    -                                      key=lambda item: (item[1], item[0]),
    -                                      reverse=True)]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.numberify.convert_col_Position(name, drows, index) - - -

    - -
    - -

    Create converters for a column of type Position.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • name – A string, the column name.

    • -
    • drows – The table of objects.

    • -
    • index – The column number.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of Converter instances, one for each of the currency types found.

    • -
    -
    -
    - Source code in beancount/query/numberify.py -
    def convert_col_Position(name, drows, index):
    -    """Create converters for a column of type Position.
    -
    -    Args:
    -      name: A string, the column name.
    -      drows: The table of objects.
    -      index: The column number.
    -    Returns:
    -      A list of Converter instances, one for each of the currency types found.
    -    """
    -    currency_map = collections.defaultdict(int)
    -    for drow in drows:
    -        pos = drow[index]
    -        if pos and pos.units.currency:
    -            currency_map[pos.units.currency] += 1
    -    return [PositionConverter('{} ({})'.format(name, currency), index, currency)
    -            for currency, _ in sorted(currency_map.items(),
    -                                      key=lambda item: (item[1], item[0]),
    -                                      reverse=True)]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.numberify.numberify_results(dtypes, drows, dformat=None) - - -

    - -
    - -

    Number rows containing Amount, Position or Inventory types.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • result_types – A list of items describing the names and data types of the items in -each column.

    • -
    • result_rows – A list of ResultRow instances.

    • -
    • dformat – An optional DisplayFormatter. If set, quantize the numbers by -their currency-specific precision when converting the Amount's, -Position's or Inventory'es..

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of modified (result_types, result_rows) with converted datatypes.

    • -
    -
    -
    - Source code in beancount/query/numberify.py -
    def numberify_results(dtypes, drows, dformat=None):
    -    """Number rows containing Amount, Position or Inventory types.
    -
    -    Args:
    -      result_types: A list of items describing the names and data types of the items in
    -        each column.
    -      result_rows: A list of ResultRow instances.
    -      dformat: An optional DisplayFormatter. If set, quantize the numbers by
    -        their currency-specific precision when converting the Amount's,
    -        Position's or Inventory'es..
    -    Returns:
    -      A pair of modified (result_types, result_rows) with converted datatypes.
    -    """
    -    # Build an array of converters.
    -    converters = []
    -    for index, col_desc in enumerate(dtypes):
    -        name, dtype = col_desc
    -        convert_col_fun = CONVERTING_TYPES.get(dtype, None)
    -        if convert_col_fun is None:
    -            converters.append(IdentityConverter(name, dtype, index))
    -        else:
    -            col_converters = convert_col_fun(name, drows, index)
    -            converters.extend(col_converters)
    -
    -    # Derive the output types from the expected outputs from the converters
    -    # themselves.
    -    otypes = [(c.name, c.dtype) for c in converters]
    -
    -    # Convert the input rows by processing them through the converters.
    -    orows = []
    -    for drow in drows:
    -        orow = []
    -        for converter in converters:
    -            orow.append(converter(drow, dformat))
    -        orows.append(orow)
    -
    -    return otypes, orows
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - - -
    - - - -

    - beancount.query.query - - - -

    - -
    - -

    A library to run queries. This glues together all the parts of the query engine.

    - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.query.query.run_query(entries, options_map, query, *format_args, *, numberify=False) - - -

    - -
    - -

    Compile and execute a query, return the result types and rows.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of entries, as produced by the loader.

    • -
    • options_map – A dict of options, as produced by the loader.

    • -
    • query – A string, a single BQL query, optionally containing some new-style -(e.g., {}) formatting specifications.

    • -
    • format_args – A tuple of arguments to be formatted in the query. This is -just provided as a convenience.

    • -
    • numberify – If true, numberify the results before returning them.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of result types and result rows.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • ParseError – If the statement cannot be parsed.

    • -
    • CompilationError – If the statement cannot be compiled.

    • -
    -
    -
    - Source code in beancount/query/query.py -
    def run_query(entries, options_map, query, *format_args, numberify=False):
    -    """Compile and execute a query, return the result types and rows.
    -
    -    Args:
    -      entries: A list of entries, as produced by the loader.
    -      options_map: A dict of options, as produced by the loader.
    -      query: A string, a single BQL query, optionally containing some new-style
    -        (e.g., {}) formatting specifications.
    -      format_args: A tuple of arguments to be formatted in the query. This is
    -        just provided as a convenience.
    -      numberify: If true, numberify the results before returning them.
    -    Returns:
    -      A pair of result types and result rows.
    -    Raises:
    -      ParseError: If the statement cannot be parsed.
    -      CompilationError: If the statement cannot be compiled.
    -    """
    -    env_targets = query_env.TargetsEnvironment()
    -    env_entries = query_env.FilterEntriesEnvironment()
    -    env_postings = query_env.FilterPostingsEnvironment()
    -
    -    # Apply formatting to the query.
    -    formatted_query = query.format(*format_args)
    -
    -    # Parse the statement.
    -    parser = query_parser.Parser()
    -    statement = parser.parse(formatted_query)
    -
    -    # Compile the SELECT statement.
    -    c_query = query_compile.compile(statement,
    -                                    env_targets,
    -                                    env_postings,
    -                                    env_entries)
    -
    -    # Execute it to obtain the result rows.
    -    rtypes, rrows = query_execute.execute_query(c_query, entries, options_map)
    -
    -    # Numberify the results, if requested.
    -    if numberify:
    -        dformat = options_map['dcontext'].build()
    -        rtypes, rrows = numberify_lib.numberify_results(rtypes, rrows, dformat)
    -
    -    return rtypes, rrows
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.query.query_compile - - - -

    - -
    - -

    Interpreter for the query language's AST.

    -

    This code accepts the abstract syntax tree produced by the query parser, -resolves the column and function names, compiles and interpreter and prepares a -query to be run against a list of entries.

    - - - -
    - - - - - - - - - - - - - -
    - - - -

    - -beancount.query.query_compile.CompilationEnvironment - - - -

    - -
    - -

    Base class for all compilation contexts. A compilation context provides -column accessors specific to the particular row objects that we will access.

    - - - - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.query.query_compile.CompilationEnvironment.get_column(self, name) - - -

    - -
    - -

    Return a column accessor for the given named column.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • name – A string, the name of the column to access.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def get_column(self, name):
    -    """Return a column accessor for the given named column.
    -    Args:
    -      name: A string, the name of the column to access.
    -    """
    -    try:
    -        return self.columns[name]()
    -    except KeyError:
    -        raise CompilationError("Invalid column name '{}' in {} context.".format(
    -            name, self.context_name))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.CompilationEnvironment.get_function(self, name, operands) - - -

    - -
    - -

    Return a function accessor for the given named function.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • name – A string, the name of the function to access.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def get_function(self, name, operands):
    -    """Return a function accessor for the given named function.
    -    Args:
    -      name: A string, the name of the function to access.
    -    """
    -    try:
    -        key = tuple([name] + [operand.dtype for operand in operands])
    -        return self.functions[key](operands)
    -    except KeyError:
    -        # If not found with the operands, try just looking it up by name.
    -        try:
    -            return self.functions[name](operands)
    -        except KeyError:
    -            signature = '{}({})'.format(name,
    -                                        ', '.join(operand.dtype.__name__
    -                                                  for operand in operands))
    -            raise CompilationError("Invalid function '{}' in {} context".format(
    -                signature, self.context_name))
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_compile.CompilationError (Exception) - - - - -

    - -
    - -

    A compiler/interpreter error.

    - - - -
    - -
    - - - - -
    - - - -

    - -beancount.query.query_compile.EvalAggregator (EvalFunction) - - - - -

    - -
    - -

    Base class for all aggregator evaluator types.

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_compile.EvalAggregator.__call__(self, context) - - - special - - -

    - -
    - -

    Return the value on evaluation.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • context – The evaluation object to which the evaluation need to apply. -This is either an entry, a Posting instance, or a particular result -set row from a sub-select. This is the provider for the underlying -data.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The final aggregated value.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def __call__(self, context):
    -    """Return the value on evaluation.
    -
    -    Args:
    -      context: The evaluation object to which the evaluation need to apply.
    -        This is either an entry, a Posting instance, or a particular result
    -        set row from a sub-select. This is the provider for the underlying
    -        data.
    -    Returns:
    -      The final aggregated value.
    -    """
    -    # Return None by default.
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalAggregator.allocate(self, allocator) - - -

    - -
    - -

    Allocate handles to store data for a node's aggregate storage.

    -

    This is called once before beginning aggregations. If you need any -kind of per-aggregate storage during the computation phase, get it -in this method.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • allocator – An instance of Allocator, on which you can call allocate() to -obtain a handle for a slot to store data on store objects later on.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def allocate(self, allocator):
    -    """Allocate handles to store data for a node's aggregate storage.
    -
    -    This is called once before beginning aggregations. If you need any
    -    kind of per-aggregate storage during the computation phase, get it
    -    in this method.
    -
    -    Args:
    -      allocator: An instance of Allocator, on which you can call allocate() to
    -        obtain a handle for a slot to store data on store objects later on.
    -    """
    -    # Do nothing by default.
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalAggregator.finalize(self, store) - - -

    - -
    - -

    Finalize this node's aggregate data and return it.

    -

    For aggregate methods, this finalizes the node and returns the final -value. The context node will be the alloc instead of the context object.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def finalize(self, store):
    -    """Finalize this node's aggregate data and return it.
    -
    -    For aggregate methods, this finalizes the node and returns the final
    -    value. The context node will be the alloc instead of the context object.
    -
    -    Args:
    -      store: An object indexable by handles appropriated during allocate().
    -    """
    -    # Do nothing by default.
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalAggregator.initialize(self, store) - - -

    - -
    - -

    Initialize this node's aggregate data. If the node is not an aggregate, -simply initialize the subnodes. Override this method in the aggregator -if you need data for storage.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def initialize(self, store):
    -    """Initialize this node's aggregate data. If the node is not an aggregate,
    -    simply initialize the subnodes. Override this method in the aggregator
    -    if you need data for storage.
    -
    -    Args:
    -      store: An object indexable by handles appropriated during allocate().
    -    """
    -    # Do nothing by default.
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalAggregator.update(self, store, context) - - -

    - -
    - -

    Evaluate this node. This is designed to recurse on its children.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    • context – The object to which the evaluation need to apply (see call).

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def update(self, store, context):
    -    """Evaluate this node. This is designed to recurse on its children.
    -
    -    Args:
    -      store: An object indexable by handles appropriated during allocate().
    -      context: The object to which the evaluation need to apply (see __call__).
    -    """
    -    # Do nothing by default.
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    - -beancount.query.query_compile.EvalColumn (EvalNode) - - - - -

    - -
    - -

    Base class for all column accessors.

    - - - -
    - -
    - - - - - - - -
    - - - -

    - -beancount.query.query_compile.EvalFrom (tuple) - - - - -

    - -
    - -

    EvalFrom(c_expr, open, close, clear)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_compile.EvalFrom.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/query/query_compile.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalFrom.__new__(_cls, c_expr, open, close, clear) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of EvalFrom(c_expr, open, close, clear)

    - -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalFrom.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/query/query_compile.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalFrom._asdict(self) - - - private - - -

    - -
    - -

    Return a new dict which maps field names to their values.

    - -
    - Source code in beancount/query/query_compile.py -
    def _asdict(self):
    -    'Return a new dict which maps field names to their values.'
    -    return _dict(_zip(self._fields, self))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalFrom._make(iterable) - - - classmethod - private - - -

    - -
    - -

    Make a new EvalFrom object from a sequence or iterable

    - -
    - Source code in beancount/query/query_compile.py -
    @classmethod
    -def _make(cls, iterable):
    -    result = tuple_new(cls, iterable)
    -    if _len(result) != num_fields:
    -        raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalFrom._replace(/, self, **kwds) - - - private - - -

    - -
    - -

    Return a new EvalFrom object replacing specified fields with new values

    - -
    - Source code in beancount/query/query_compile.py -
    def _replace(self, /, **kwds):
    -    result = self._make(_map(kwds.pop, field_names, self))
    -    if kwds:
    -        raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
    -    return result
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_compile.EvalFunction (EvalNode) - - - - -

    - -
    - -

    Base class for all function objects.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - - - - - - - -
    - - - -

    - -beancount.query.query_compile.EvalNode - - - -

    - -
    - - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.query.query_compile.EvalNode.__call__(self, context) - - - special - - -

    - -
    - -

    Evaluate this node. This is designed to recurse on its children. -All subclasses must override and implement this method.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • context – The evaluation object to which the evaluation need to apply. -This is either an entry, a Posting instance, or a particular result -set row from a sub-select. This is the provider for the underlying -data.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The evaluated value for this sub-expression tree.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def __call__(self, context):
    -    """Evaluate this node. This is designed to recurse on its children.
    -    All subclasses must override and implement this method.
    -
    -    Args:
    -      context: The evaluation object to which the evaluation need to apply.
    -        This is either an entry, a Posting instance, or a particular result
    -        set row from a sub-select. This is the provider for the underlying
    -        data.
    -    Returns:
    -      The evaluated value for this sub-expression tree.
    -    """
    -    raise NotImplementedError
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalNode.__eq__(self, other) - - - special - - -

    - -
    - -

    Override the equality operator to compare the data type and a all attributes -of this node. This is used by tests for comparing nodes.

    - -
    - Source code in beancount/query/query_compile.py -
    def __eq__(self, other):
    -    """Override the equality operator to compare the data type and a all attributes
    -    of this node. This is used by tests for comparing nodes.
    -    """
    -    return (isinstance(other, type(self))
    -            and all(
    -                getattr(self, attribute) == getattr(other, attribute)
    -                for attribute in self.__slots__))
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.query.query_compile.EvalNode.__repr__(self) - - - special - - -

    - -
    - -

    Return str(self).

    - -
    - Source code in beancount/query/query_compile.py -
    def __str__(self):
    -    return "{}({})".format(type(self).__name__,
    -                           ', '.join(repr(getattr(self, child))
    -                                     for child in self.__slots__))
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.query.query_compile.EvalNode.childnodes(self) - - -

    - -
    - -

    Returns the child nodes of this node. -Yields: - A list of EvalNode instances.

    - -
    - Source code in beancount/query/query_compile.py -
    def childnodes(self):
    -    """Returns the child nodes of this node.
    -    Yields:
    -      A list of EvalNode instances.
    -    """
    -    for attr in self.__slots__:
    -        child = getattr(self, attr)
    -        if isinstance(child, EvalNode):
    -            yield child
    -        elif isinstance(child, list):
    -            for element in child:
    -                if isinstance(element, EvalNode):
    -                    yield element
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    - -beancount.query.query_compile.EvalPrint (tuple) - - - - -

    - -
    - -

    EvalPrint(c_from,)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_compile.EvalPrint.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/query/query_compile.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalPrint.__new__(_cls, c_from) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of EvalPrint(c_from,)

    - -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalPrint.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/query/query_compile.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalPrint._asdict(self) - - - private - - -

    - -
    - -

    Return a new dict which maps field names to their values.

    - -
    - Source code in beancount/query/query_compile.py -
    def _asdict(self):
    -    'Return a new dict which maps field names to their values.'
    -    return _dict(_zip(self._fields, self))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalPrint._make(iterable) - - - classmethod - private - - -

    - -
    - -

    Make a new EvalPrint object from a sequence or iterable

    - -
    - Source code in beancount/query/query_compile.py -
    @classmethod
    -def _make(cls, iterable):
    -    result = tuple_new(cls, iterable)
    -    if _len(result) != num_fields:
    -        raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalPrint._replace(/, self, **kwds) - - - private - - -

    - -
    - -

    Return a new EvalPrint object replacing specified fields with new values

    - -
    - Source code in beancount/query/query_compile.py -
    def _replace(self, /, **kwds):
    -    result = self._make(_map(kwds.pop, field_names, self))
    -    if kwds:
    -        raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
    -    return result
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_compile.EvalQuery (tuple) - - - - -

    - -
    - -

    EvalQuery(c_targets, c_from, c_where, group_indexes, order_indexes, ordering, limit, distinct, flatten)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_compile.EvalQuery.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/query/query_compile.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalQuery.__new__(_cls, c_targets, c_from, c_where, group_indexes, order_indexes, ordering, limit, distinct, flatten) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of EvalQuery(c_targets, c_from, c_where, group_indexes, order_indexes, ordering, limit, distinct, flatten)

    - -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalQuery.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/query/query_compile.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalQuery._asdict(self) - - - private - - -

    - -
    - -

    Return a new dict which maps field names to their values.

    - -
    - Source code in beancount/query/query_compile.py -
    def _asdict(self):
    -    'Return a new dict which maps field names to their values.'
    -    return _dict(_zip(self._fields, self))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalQuery._make(iterable) - - - classmethod - private - - -

    - -
    - -

    Make a new EvalQuery object from a sequence or iterable

    - -
    - Source code in beancount/query/query_compile.py -
    @classmethod
    -def _make(cls, iterable):
    -    result = tuple_new(cls, iterable)
    -    if _len(result) != num_fields:
    -        raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalQuery._replace(/, self, **kwds) - - - private - - -

    - -
    - -

    Return a new EvalQuery object replacing specified fields with new values

    - -
    - Source code in beancount/query/query_compile.py -
    def _replace(self, /, **kwds):
    -    result = self._make(_map(kwds.pop, field_names, self))
    -    if kwds:
    -        raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
    -    return result
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    - -beancount.query.query_compile.EvalTarget (tuple) - - - - -

    - -
    - -

    EvalTarget(c_expr, name, is_aggregate)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_compile.EvalTarget.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/query/query_compile.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalTarget.__new__(_cls, c_expr, name, is_aggregate) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of EvalTarget(c_expr, name, is_aggregate)

    - -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalTarget.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/query/query_compile.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalTarget._asdict(self) - - - private - - -

    - -
    - -

    Return a new dict which maps field names to their values.

    - -
    - Source code in beancount/query/query_compile.py -
    def _asdict(self):
    -    'Return a new dict which maps field names to their values.'
    -    return _dict(_zip(self._fields, self))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalTarget._make(iterable) - - - classmethod - private - - -

    - -
    - -

    Make a new EvalTarget object from a sequence or iterable

    - -
    - Source code in beancount/query/query_compile.py -
    @classmethod
    -def _make(cls, iterable):
    -    result = tuple_new(cls, iterable)
    -    if _len(result) != num_fields:
    -        raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.EvalTarget._replace(/, self, **kwds) - - - private - - -

    - -
    - -

    Return a new EvalTarget object replacing specified fields with new values

    - -
    - Source code in beancount/query/query_compile.py -
    def _replace(self, /, **kwds):
    -    result = self._make(_map(kwds.pop, field_names, self))
    -    if kwds:
    -        raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
    -    return result
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    - -beancount.query.query_compile.ResultSetEnvironment (CompilationEnvironment) - - - - -

    - -
    - -

    An execution context that provides access to attributes from a result set.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.query.query_compile.ResultSetEnvironment.get_column(self, name) - - -

    - -
    - -

    Override the column getter to provide a single attribute getter.

    - -
    - Source code in beancount/query/query_compile.py -
    def get_column(self, name):
    -    """Override the column getter to provide a single attribute getter.
    -    """
    -    # FIXME: How do we figure out the data type here? We need the context.
    -    return AttributeColumn(name)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.query.query_compile._get_columns_and_aggregates(node, columns, aggregates) - - - private - - -

    - -
    - -

    Walk down a tree of nodes and fetch the column accessors and aggregates.

    -

    This function ignores all nodes under aggregate nodes.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • node – An instance of EvalNode.

    • -
    • columns – An accumulator for columns found so far.

    • -
    • aggregate – An accumulator for aggregate notes found so far.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def _get_columns_and_aggregates(node, columns, aggregates):
    -    """Walk down a tree of nodes and fetch the column accessors and aggregates.
    -
    -    This function ignores all nodes under aggregate nodes.
    -
    -    Args:
    -      node: An instance of EvalNode.
    -      columns: An accumulator for columns found so far.
    -      aggregate: An accumulator for aggregate notes found so far.
    -    """
    -    if isinstance(node, EvalAggregator):
    -        aggregates.append(node)
    -    elif isinstance(node, EvalColumn):
    -        columns.append(node)
    -    else:
    -        for child in node.childnodes():
    -            _get_columns_and_aggregates(child, columns, aggregates)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.compile(statement, targets_environ, postings_environ, entries_environ) - - -

    - -
    - -

    Prepare an AST any of the statement into an executable statement.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • statement – An instance of the parser's Select, Balances, Journal or Print.

    • -
    • targets_environ – A compilation environment for evaluating targets.

    • -
    • postings_environ – : A compilation environment for evaluating postings filters.

    • -
    • entries_environ – : A compilation environment for evaluating entry filters.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of EvalQuery or EvalPrint, ready to be executed.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • CompilationError – If the statement cannot be compiled, or is not one of the -supported statements.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def compile(statement, targets_environ, postings_environ, entries_environ):
    -    """Prepare an AST any of the statement into an executable statement.
    -
    -    Args:
    -      statement: An instance of the parser's Select, Balances, Journal or Print.
    -      targets_environ: A compilation environment for evaluating targets.
    -      postings_environ: : A compilation environment for evaluating postings filters.
    -      entries_environ: : A compilation environment for evaluating entry filters.
    -    Returns:
    -      An instance of EvalQuery or EvalPrint, ready to be executed.
    -    Raises:
    -      CompilationError: If the statement cannot be compiled, or is not one of the
    -        supported statements.
    -    """
    -    if isinstance(statement, query_parser.Balances):
    -        statement = transform_balances(statement)
    -    elif isinstance(statement, query_parser.Journal):
    -        statement = transform_journal(statement)
    -
    -    if isinstance(statement, query_parser.Select):
    -        c_query = compile_select(statement,
    -                                 targets_environ, postings_environ, entries_environ)
    -    elif isinstance(statement, query_parser.Print):
    -        c_query = compile_print(statement, entries_environ)
    -    else:
    -        raise CompilationError(
    -            "Cannot compile a statement of type '{}'".format(type(statement)))
    -
    -    return c_query
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.compile_expression(expr, environ) - - -

    - -
    - -

    Bind an expression to its execution context.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • expr – The root node of an expression.

    • -
    • environ – An CompilationEnvironment instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The root node of a bound expression.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def compile_expression(expr, environ):
    -    """Bind an expression to its execution context.
    -
    -    Args:
    -      expr: The root node of an expression.
    -      environ: An CompilationEnvironment instance.
    -    Returns:
    -      The root node of a bound expression.
    -    """
    -    # Convert column references to the context.
    -    if isinstance(expr, query_parser.Column):
    -        c_expr = environ.get_column(expr.name)
    -
    -    elif isinstance(expr, query_parser.Function):
    -        c_operands = [compile_expression(operand, environ)
    -                      for operand in expr.operands]
    -        c_expr = environ.get_function(expr.fname, c_operands)
    -
    -    elif isinstance(expr, query_parser.UnaryOp):
    -        node_type = OPERATORS[type(expr)]
    -        c_expr = node_type(compile_expression(expr.operand, environ))
    -
    -    elif isinstance(expr, query_parser.BinaryOp):
    -        node_type = OPERATORS[type(expr)]
    -        c_expr = node_type(compile_expression(expr.left, environ),
    -                           compile_expression(expr.right, environ))
    -
    -    elif isinstance(expr, query_parser.Constant):
    -        c_expr = EvalConstant(expr.value)
    -
    -    else:
    -        assert False, "Invalid expression to compile: {}".format(expr)
    -
    -    return c_expr
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.compile_from(from_clause, environ) - - -

    - -
    - -

    Compiled a From clause as provided by the parser, in the given environment.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • select – An instance of query_parser.Select.

    • -
    • environ – : A compilation context for evaluating entry filters.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Query, ready to be executed.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def compile_from(from_clause, environ):
    -    """Compiled a From clause as provided by the parser, in the given environment.
    -
    -    Args:
    -      select: An instance of query_parser.Select.
    -      environ: : A compilation context for evaluating entry filters.
    -    Returns:
    -      An instance of Query, ready to be executed.
    -    """
    -    if from_clause is not None:
    -        c_expression = (compile_expression(from_clause.expression, environ)
    -                        if from_clause.expression is not None
    -                        else None)
    -
    -        # Check that the from clause does not contain aggregates.
    -        if c_expression is not None and is_aggregate(c_expression):
    -            raise CompilationError("Aggregates are not allowed in from clause")
    -
    -        if (isinstance(from_clause.open, datetime.date) and
    -            isinstance(from_clause.close, datetime.date) and
    -            from_clause.open > from_clause.close):
    -            raise CompilationError("Invalid dates: CLOSE date must follow OPEN date")
    -
    -        c_from = EvalFrom(c_expression,
    -                          from_clause.open,
    -                          from_clause.close,
    -                          from_clause.clear)
    -    else:
    -        c_from = None
    -
    -    return c_from
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.compile_group_by(group_by, c_targets, environ) - - -

    - -
    - -

    Process a group-by clause.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • group_by – A GroupBy instance as provided by the parser.

    • -
    • c_targets – A list of compiled target expressions.

    • -
    • environ – A compilation context to be used to evaluate GROUP BY expressions.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of - new_targets – A list of new compiled target nodes. - group_indexes: If the query is an aggregate query, a list of integer - indexes to be used for processing grouping. Note that this list may be - empty (in the case of targets with only aggregates). On the other hand, - if this is not an aggregated query, this is set to None. So do - distinguish the empty list vs. None.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def compile_group_by(group_by, c_targets, environ):
    -    """Process a group-by clause.
    -
    -    Args:
    -      group_by: A GroupBy instance as provided by the parser.
    -      c_targets: A list of compiled target expressions.
    -      environ: A compilation context to be used to evaluate GROUP BY expressions.
    -    Returns:
    -      A tuple of
    -       new_targets: A list of new compiled target nodes.
    -       group_indexes: If the query is an aggregate query, a list of integer
    -         indexes to be used for processing grouping. Note that this list may be
    -         empty (in the case of targets with only aggregates). On the other hand,
    -         if this is not an aggregated query, this is set to None. So do
    -         distinguish the empty list vs. None.
    -    """
    -    new_targets = copy.copy(c_targets)
    -    c_target_expressions = [c_target.c_expr for c_target in c_targets]
    -
    -    group_indexes = []
    -    if group_by:
    -        # Check that HAVING is not supported yet.
    -        if group_by and group_by.having is not None:
    -            raise CompilationError("The HAVING clause is not supported yet")
    -
    -        assert group_by.columns, "Internal error with GROUP-BY parsing"
    -
    -        # Compile group-by expressions and resolve them to their targets if
    -        # possible. A GROUP-BY column may be one of the following:
    -        #
    -        # * A reference to a target by name.
    -        # * A reference to a target by index (starting at one).
    -        # * A new, non-aggregate expression.
    -        #
    -        # References by name are converted to indexes. New expressions are
    -        # inserted into the list of targets as invisible targets.
    -        targets_name_map = {target.name: index
    -                            for index, target in enumerate(c_targets)}
    -        for column in group_by.columns:
    -            index = None
    -
    -            # Process target references by index.
    -            if isinstance(column, int):
    -                index = column - 1
    -                if not (0 <= index < len(c_targets)):
    -                    raise CompilationError(
    -                        "Invalid GROUP-BY column index {}".format(column))
    -
    -            else:
    -                # Process target references by name. These will be parsed as
    -                # simple Column expressions. If they refer to a target name, we
    -                # resolve them.
    -                if isinstance(column, query_parser.Column):
    -                    name = column.name
    -                    index = targets_name_map.get(name, None)
    -
    -                # Otherwise we compile the expression and add it to the list of
    -                # targets to evaluate and index into that new target.
    -                if index is None:
    -                    c_expr = compile_expression(column, environ)
    -
    -                    # Check if the new expression is an aggregate.
    -                    aggregate = is_aggregate(c_expr)
    -                    if aggregate:
    -                        raise CompilationError(
    -                            "GROUP-BY expressions may not be aggregates: '{}'".format(
    -                                column))
    -
    -                    # Attempt to reconcile the expression with one of the existing
    -                    # target expressions.
    -                    try:
    -                        index = c_target_expressions.index(c_expr)
    -                    except ValueError:
    -                        # Add the new target. 'None' for the target name implies it
    -                        # should be invisible, not to be rendered.
    -                        index = len(new_targets)
    -                        new_targets.append(EvalTarget(c_expr, None, aggregate))
    -                        c_target_expressions.append(c_expr)
    -
    -            assert index is not None, "Internal error, could not index group-by reference."
    -            group_indexes.append(index)
    -
    -            # Check that the group-by column references a non-aggregate.
    -            c_expr = new_targets[index].c_expr
    -            if is_aggregate(c_expr):
    -                raise CompilationError(
    -                    "GROUP-BY expressions may not reference aggregates: '{}'".format(
    -                        column))
    -
    -            # Check that the group-by column has a supported hashable type.
    -            if not is_hashable_type(c_expr):
    -                raise CompilationError(
    -                    "GROUP-BY a non-hashable type is not supported: '{}'".format(
    -                        column))
    -
    -
    -    else:
    -        # If it does not have a GROUP-BY clause...
    -        aggregate_bools = [c_target.is_aggregate for c_target in c_targets]
    -        if any(aggregate_bools):
    -            # If the query is an aggregate query, check that all the targets are
    -            # aggregates.
    -            if all(aggregate_bools):
    -                assert group_indexes == []
    -            else:
    -                # If some of the targets aren't aggregates, automatically infer
    -                # that they are to be implicit group by targets. This makes for
    -                # a much more convenient syntax for our lightweight SQL, where
    -                # grouping is optional.
    -                if SUPPORT_IMPLICIT_GROUPBY:
    -                    group_indexes = [index
    -                                     for index, c_target in enumerate(c_targets)
    -                                     if not c_target.is_aggregate]
    -                else:
    -                    raise CompilationError(
    -                        "Aggregate query without a GROUP-BY should have only aggregates")
    -        else:
    -            # This is not an aggregate query; don't set group_indexes to
    -            # anything useful, we won't need it.
    -            group_indexes = None
    -
    -    return new_targets[len(c_targets):], group_indexes
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.compile_order_by(order_by, c_targets, environ) - - -

    - -
    - -

    Process an order-by clause.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • order_by – A OrderBy instance as provided by the parser.

    • -
    • c_targets – A list of compiled target expressions.

    • -
    • environ – A compilation context to be used to evaluate ORDER BY expressions.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of - new_targets – A list of new compiled target nodes. - order_indexes: A list of integer indexes to be used for processing ordering.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def compile_order_by(order_by, c_targets, environ):
    -    """Process an order-by clause.
    -
    -    Args:
    -      order_by: A OrderBy instance as provided by the parser.
    -      c_targets: A list of compiled target expressions.
    -      environ: A compilation context to be used to evaluate ORDER BY expressions.
    -    Returns:
    -      A tuple of
    -       new_targets: A list of new compiled target nodes.
    -       order_indexes: A list of integer indexes to be used for processing ordering.
    -    """
    -    new_targets = copy.copy(c_targets)
    -    c_target_expressions = [c_target.c_expr for c_target in c_targets]
    -    order_indexes = []
    -
    -    # Compile order-by expressions and resolve them to their targets if
    -    # possible. A ORDER-BY column may be one of the following:
    -    #
    -    # * A reference to a target by name.
    -    # * A reference to a target by index (starting at one).
    -    # * A new expression, aggregate or not.
    -    #
    -    # References by name are converted to indexes. New expressions are
    -    # inserted into the list of targets as invisible targets.
    -    targets_name_map = {target.name: index
    -                        for index, target in enumerate(c_targets)}
    -    for column in order_by.columns:
    -        index = None
    -
    -        # Process target references by index.
    -        if isinstance(column, int):
    -            index = column - 1
    -            if not (0 <= index < len(c_targets)):
    -                raise CompilationError(
    -                    "Invalid ORDER-BY column index {}".format(column))
    -
    -        else:
    -            # Process target references by name. These will be parsed as
    -            # simple Column expressions. If they refer to a target name, we
    -            # resolve them.
    -            if isinstance(column, query_parser.Column):
    -                name = column.name
    -                index = targets_name_map.get(name, None)
    -
    -            # Otherwise we compile the expression and add it to the list of
    -            # targets to evaluate and index into that new target.
    -            if index is None:
    -                c_expr = compile_expression(column, environ)
    -
    -                # Attempt to reconcile the expression with one of the existing
    -                # target expressions.
    -                try:
    -                    index = c_target_expressions.index(c_expr)
    -                except ValueError:
    -                    # Add the new target. 'None' for the target name implies it
    -                    # should be invisible, not to be rendered.
    -                    index = len(new_targets)
    -                    new_targets.append(EvalTarget(c_expr, None, is_aggregate(c_expr)))
    -                    c_target_expressions.append(c_expr)
    -
    -        assert index is not None, "Internal error, could not index order-by reference."
    -        order_indexes.append(index)
    -
    -    return (new_targets[len(c_targets):], order_indexes)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.compile_print(print_stmt, env_entries) - - -

    - -
    - -

    Compile a Print statement.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • statement – An instance of query_parser.Print.

    • -
    • entries_environ – : A compilation environment for evaluating entry filters.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of EvalPrint, ready to be executed.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def compile_print(print_stmt, env_entries):
    -    """Compile a Print statement.
    -
    -    Args:
    -      statement: An instance of query_parser.Print.
    -      entries_environ: : A compilation environment for evaluating entry filters.
    -    Returns:
    -      An instance of EvalPrint, ready to be executed.
    -    """
    -    c_from = compile_from(print_stmt.from_clause, env_entries)
    -    return EvalPrint(c_from)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.compile_select(select, targets_environ, postings_environ, entries_environ) - - -

    - -
    - -

    Prepare an AST for a Select statement into a very rudimentary execution tree. -The execution tree mostly looks much like an AST, but with some nodes -replaced with knowledge specific to an execution context and eventually some -basic optimizations.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • select – An instance of query_parser.Select.

    • -
    • targets_environ – A compilation environment for evaluating targets.

    • -
    • postings_environ – A compilation environment for evaluating postings filters.

    • -
    • entries_environ – A compilation environment for evaluating entry filters.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of EvalQuery, ready to be executed.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def compile_select(select, targets_environ, postings_environ, entries_environ):
    -    """Prepare an AST for a Select statement into a very rudimentary execution tree.
    -    The execution tree mostly looks much like an AST, but with some nodes
    -    replaced with knowledge specific to an execution context and eventually some
    -    basic optimizations.
    -
    -    Args:
    -      select: An instance of query_parser.Select.
    -      targets_environ: A compilation environment for evaluating targets.
    -      postings_environ: A compilation environment for evaluating postings filters.
    -      entries_environ: A compilation environment for evaluating entry filters.
    -    Returns:
    -      An instance of EvalQuery, ready to be executed.
    -    """
    -
    -    # Process the FROM clause and figure out the execution environment for the
    -    # targets and the where clause.
    -    from_clause = select.from_clause
    -    if isinstance(from_clause, query_parser.Select):
    -        c_from = None
    -        environ_target = ResultSetEnvironment()
    -        environ_where = ResultSetEnvironment()
    -
    -        # Remove this when we add support for nested queries.
    -        raise CompilationError("Queries from nested SELECT are not supported yet")
    -
    -    if from_clause is None or isinstance(from_clause, query_parser.From):
    -        # Bind the from clause contents.
    -        c_from = compile_from(from_clause, entries_environ)
    -        environ_target = targets_environ
    -        environ_where = postings_environ
    -
    -    else:
    -        raise CompilationError("Unexpected from clause in AST: {}".format(from_clause))
    -
    -    # Compile the targets.
    -    c_targets = compile_targets(select.targets, environ_target)
    -
    -    # Bind the WHERE expression to the execution environment.
    -    if select.where_clause is not None:
    -        c_where = compile_expression(select.where_clause, environ_where)
    -
    -        # Aggregates are disallowed in this clause. Check for this.
    -        # NOTE: This should never trigger if the compilation environment does not
    -        # contain any aggregate. Just being manic and safe here.
    -        if is_aggregate(c_where):
    -            raise CompilationError("Aggregates are disallowed in WHERE clause")
    -    else:
    -        c_where = None
    -
    -    # Process the GROUP-BY clause.
    -    new_targets, group_indexes = compile_group_by(select.group_by,
    -                                                  c_targets,
    -                                                  environ_target)
    -    if new_targets:
    -        c_targets.extend(new_targets)
    -
    -    # Process the ORDER-BY clause.
    -    if select.order_by is not None:
    -        (new_targets, order_indexes) = compile_order_by(select.order_by,
    -                                                        c_targets,
    -                                                        environ_target)
    -        if new_targets:
    -            c_targets.extend(new_targets)
    -        ordering = select.order_by.ordering
    -    else:
    -        order_indexes = None
    -        ordering = None
    -
    -    # If this is an aggregate query (it groups, see list of indexes), check that
    -    # the set of non-aggregates match exactly the group indexes. This should
    -    # always be the case at this point, because we have added all the necessary
    -    # targets to the list of group-by expressions and should have resolved all
    -    # the indexes.
    -    if group_indexes is not None:
    -        non_aggregate_indexes = set(index
    -                                    for index, c_target in enumerate(c_targets)
    -                                    if not c_target.is_aggregate)
    -        if non_aggregate_indexes != set(group_indexes):
    -            missing_names = ['"{}"'.format(c_targets[index].name)
    -                             for index in non_aggregate_indexes - set(group_indexes)]
    -            raise CompilationError(
    -                "All non-aggregates must be covered by GROUP-BY clause in aggregate query; "
    -                "the following targets are missing: {}".format(",".join(missing_names)))
    -
    -    # Check that PIVOT-BY is not supported yet.
    -    if select.pivot_by is not None:
    -        raise CompilationError("The PIVOT BY clause is not supported yet")
    -
    -    return EvalQuery(c_targets,
    -                     c_from,
    -                     c_where,
    -                     group_indexes,
    -                     order_indexes,
    -                     ordering,
    -                     select.limit,
    -                     select.distinct,
    -                     select.flatten)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.compile_targets(targets, environ) - - -

    - -
    - -

    Compile the targets and check for their validity. Process wildcard.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • targets – A list of target expressions from the parser.

    • -
    • environ – A compilation context for the targets.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of compiled target expressions with resolved names.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def compile_targets(targets, environ):
    -    """Compile the targets and check for their validity. Process wildcard.
    -
    -    Args:
    -      targets: A list of target expressions from the parser.
    -      environ: A compilation context for the targets.
    -    Returns:
    -      A list of compiled target expressions with resolved names.
    -    """
    -    # Bind the targets expressions to the execution context.
    -    if isinstance(targets, query_parser.Wildcard):
    -        # Insert the full list of available columns.
    -        targets = [query_parser.Target(query_parser.Column(name), None)
    -                   for name in environ.wildcard_columns]
    -
    -    # Compile targets.
    -    c_targets = []
    -    target_names = set()
    -    for target in targets:
    -        c_expr = compile_expression(target.expression, environ)
    -        target_name = find_unique_name(
    -            target.name or query_parser.get_expression_name(target.expression),
    -            target_names)
    -        target_names.add(target_name)
    -        c_targets.append(EvalTarget(c_expr, target_name, is_aggregate(c_expr)))
    -
    -    # Figure out if this query is an aggregate query and check validity of each
    -    # target's aggregation type.
    -    for index, c_target in enumerate(c_targets):
    -        columns, aggregates = get_columns_and_aggregates(c_target.c_expr)
    -
    -        # Check for mixed aggregates and non-aggregates.
    -        if columns and aggregates:
    -            raise CompilationError(
    -                "Mixed aggregates and non-aggregates are not allowed")
    -
    -        if aggregates:
    -            # Check for aggregates of aggregates.
    -            for aggregate in aggregates:
    -                for child in aggregate.childnodes():
    -                    if is_aggregate(child):
    -                        raise CompilationError(
    -                            "Aggregates of aggregates are not allowed")
    -
    -    return c_targets
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.find_unique_name(name, allocated_set) - - -

    - -
    - -

    Come up with a unique name for 'name' amongst 'allocated_set'.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • name – A string, the prefix of the name to find a unique for.

    • -
    • allocated_set – A set of string, the set of already allocated names.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A unique name. 'allocated_set' is unmodified.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def find_unique_name(name, allocated_set):
    -    """Come up with a unique name for 'name' amongst 'allocated_set'.
    -
    -    Args:
    -      name: A string, the prefix of the name to find a unique for.
    -      allocated_set: A set of string, the set of already allocated names.
    -    Returns:
    -      A unique name. 'allocated_set' is unmodified.
    -    """
    -    # Make sure the name is unique.
    -    prefix = name
    -    i = 1
    -    while name in allocated_set:
    -        name = '{}_{}'.format(prefix, i)
    -        i += 1
    -    return name
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.get_columns_and_aggregates(node) - - -

    - -
    - -

    Find the columns and aggregate nodes below this tree.

    -

    All nodes under aggregate nodes are ignored.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • node – An instance of EvalNode.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of (columns, aggregates), both of which are lists of EvalNode instances. - columns – The list of all columns accessed not under an aggregate node. - aggregates: The list of all aggregate nodes.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def get_columns_and_aggregates(node):
    -    """Find the columns and aggregate nodes below this tree.
    -
    -    All nodes under aggregate nodes are ignored.
    -
    -    Args:
    -      node: An instance of EvalNode.
    -    Returns:
    -      A pair of (columns, aggregates), both of which are lists of EvalNode instances.
    -        columns: The list of all columns accessed not under an aggregate node.
    -        aggregates: The list of all aggregate nodes.
    -    """
    -    columns = []
    -    aggregates = []
    -    _get_columns_and_aggregates(node, columns, aggregates)
    -    return columns, aggregates
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.is_aggregate(node) - - -

    - -
    - -

    Return true if the node is an aggregate.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • node – An instance of EvalNode.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A boolean.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def is_aggregate(node):
    -    """Return true if the node is an aggregate.
    -
    -    Args:
    -      node: An instance of EvalNode.
    -    Returns:
    -      A boolean.
    -    """
    -    # Note: We could be a tiny bit more efficient here, but it doesn't matter
    -    # much. Performance of the query compilation matters very little overall.
    -    _, aggregates = get_columns_and_aggregates(node)
    -    return bool(aggregates)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.is_hashable_type(node) - - -

    - -
    - -

    Return true if the node is of a hashable type.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • node – An instance of EvalNode.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A boolean.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def is_hashable_type(node):
    -    """Return true if the node is of a hashable type.
    -
    -    Args:
    -      node: An instance of EvalNode.
    -    Returns:
    -      A boolean.
    -    """
    -    return not issubclass(node.dtype, inventory.Inventory)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.transform_balances(balances) - - -

    - -
    - -

    Translate a Balances entry into an uncompiled Select statement.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • balances – An instance of a Balance object.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of an uncompiled Select object.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def transform_balances(balances):
    -    """Translate a Balances entry into an uncompiled Select statement.
    -
    -    Args:
    -      balances: An instance of a Balance object.
    -    Returns:
    -      An instance of an uncompiled Select object.
    -    """
    -    ## FIXME: Change the aggregation rules to allow GROUP-BY not to include the
    -    ## non-aggregate ORDER-BY columns, so we could just GROUP-BY accounts here
    -    ## instead of having to include the sort-key. I think it should be fine if
    -    ## the first or last sort-order value gets used, because it would simplify
    -    ## the input statement.
    -
    -    cooked_select = query_parser.Parser().parse("""
    -
    -      SELECT account, SUM({}(position))
    -      GROUP BY account, ACCOUNT_SORTKEY(account)
    -      ORDER BY ACCOUNT_SORTKEY(account)
    -
    -    """.format(balances.summary_func or ""))
    -
    -    return query_parser.Select(cooked_select.targets,
    -                               balances.from_clause,
    -                               balances.where_clause,
    -                               cooked_select.group_by,
    -                               cooked_select.order_by,
    -                               None, None, None, None)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile.transform_journal(journal) - - -

    - -
    - -

    Translate a Journal entry into an uncompiled Select statement.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • journal – An instance of a Journal object.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of an uncompiled Select object.

    • -
    -
    -
    - Source code in beancount/query/query_compile.py -
    def transform_journal(journal):
    -    """Translate a Journal entry into an uncompiled Select statement.
    -
    -    Args:
    -      journal: An instance of a Journal object.
    -    Returns:
    -      An instance of an uncompiled Select object.
    -    """
    -    cooked_select = query_parser.Parser().parse("""
    -
    -        SELECT
    -           date,
    -           flag,
    -           MAXWIDTH(payee, 48),
    -           MAXWIDTH(narration, 80),
    -           account,
    -           {summary_func}(position),
    -           {summary_func}(balance)
    -        {where}
    -
    -    """.format(where=('WHERE account ~ "{}"'.format(journal.account)
    -                      if journal.account
    -                      else ''),
    -               summary_func=journal.summary_func or ''))
    -
    -    return query_parser.Select(cooked_select.targets,
    -                               journal.from_clause,
    -                               cooked_select.where_clause,
    -                               None, None, None, None, None, None)
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.query.query_compile_test - - - -

    - -
    - - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.query.query_compile_test.CompileSelectBase (TestCase) - - - - -

    - -
    - - - - - -
    - - - - - - - - - - - - - -
    - - - -

    -beancount.query.query_compile_test.CompileSelectBase.assertCompile(self, expected, query, debug=False) - - -

    - -
    - -

    Assert parsed and compiled contents from 'query' is 'expected'.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • expected – An expected AST to compare against the parsed value.

    • -
    • query – An SQL query to be parsed.

    • -
    • debug – A boolean, if true, print extra debugging information on the console.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • AssertionError – If the actual AST does not match the expected one.

    • -
    -
    -
    - Source code in beancount/query/query_compile_test.py -
    def assertCompile(self, expected, query, debug=False):
    -    """Assert parsed and compiled contents from 'query' is 'expected'.
    -
    -    Args:
    -      expected: An expected AST to compare against the parsed value.
    -      query: An SQL query to be parsed.
    -      debug: A boolean, if true, print extra debugging information on the console.
    -    Raises:
    -      AssertionError: If the actual AST does not match the expected one.
    -    """
    -    actual = self.compile(query)
    -    if debug:
    -        print()
    -        print()
    -        print(actual)
    -        print()
    -    try:
    -        self.assertEqual(expected, actual)
    -        return actual
    -    except AssertionError:
    -        print()
    -        print("Expected: {}".format(expected))
    -        print("Actual  : {}".format(actual))
    -        raise
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile_test.CompileSelectBase.assertIndexes(self, query, expected_simple_indexes, expected_aggregate_indexes, expected_group_indexes, expected_order_indexes) - - -

    - -
    - -

    Check the four lists of indexes for comparison.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • query – An instance of EvalQuery, a compiled query statement.

    • -
    • expected_simple_indexes – The expected visible non-aggregate indexes.

    • -
    • expected_aggregate_indexes – The expected visible aggregate indexes.

    • -
    • expected_group_indexes – The expected group_indexes.

    • -
    • expected_order_indexes – The expected order_indexes.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • AssertionError – if the check fails.

    • -
    -
    -
    - Source code in beancount/query/query_compile_test.py -
    def assertIndexes(self,
    -                  query,
    -                  expected_simple_indexes,
    -                  expected_aggregate_indexes,
    -                  expected_group_indexes,
    -                  expected_order_indexes):
    -    """Check the four lists of indexes for comparison.
    -
    -    Args:
    -      query: An instance of EvalQuery, a compiled query statement.
    -      expected_simple_indexes: The expected visible non-aggregate indexes.
    -      expected_aggregate_indexes: The expected visible aggregate indexes.
    -      expected_group_indexes: The expected group_indexes.
    -      expected_order_indexes: The expected order_indexes.
    -    Raises:
    -      AssertionError: if the check fails.
    -    """
    -    # Compute the list of _visible_ aggregates and non-aggregates.
    -    simple_indexes = [index
    -                      for index, c_target in enumerate(query.c_targets)
    -                      if c_target.name and not qc.is_aggregate(c_target.expression)]
    -    aggregate_indexes = [index
    -                         for index, c_target in enumerate(query.c_targets)
    -                         if c_target.name and qc.is_aggregate(c_target.expression)]
    -
    -    self.assertEqual(set(expected_simple_indexes), set(simple_indexes))
    -
    -    self.assertEqual(set(expected_aggregate_indexes), set(aggregate_indexes))
    -
    -    self.assertEqual(
    -        set(expected_group_indexes) if expected_group_indexes is not None else None,
    -        set(query.group_indexes) if query.group_indexes is not None else None)
    -
    -    self.assertEqual(
    -        set(expected_order_indexes) if expected_order_indexes is not None else None,
    -        set(query.order_indexes) if query.order_indexes is not None else None)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile_test.CompileSelectBase.assertSelectInvariants(self, query) - - -

    - -
    - -

    Assert the invariants on the query.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • query – An instance of EvalQuery, a compiled query statement.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • AssertionError – if the check fails.

    • -
    -
    -
    - Source code in beancount/query/query_compile_test.py -
    def assertSelectInvariants(self, query):
    -    """Assert the invariants on the query.
    -
    -    Args:
    -      query: An instance of EvalQuery, a compiled query statement.
    -    Raises:
    -      AssertionError: if the check fails.
    -    """
    -    # Check that the group references cover all the simple indexes.
    -    if query.group_indexes is not None:
    -        non_aggregate_indexes = [index
    -                                 for index, c_target in enumerate(query.c_targets)
    -                                 if not qc.is_aggregate(c_target.c_expr)]
    -
    -        self.assertEqual(set(non_aggregate_indexes), set(query.group_indexes),
    -                         "Invalid indexes: {}".format(query))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_compile_test.CompileSelectBase.compile(self, query) - - -

    - -
    - -

    Parse one query and compile it.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • query – An SQL query to be parsed.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The AST.

    • -
    -
    -
    - Source code in beancount/query/query_compile_test.py -
    def compile(self, query):
    -    """Parse one query and compile it.
    -
    -    Args:
    -      query: An SQL query to be parsed.
    -    Returns:
    -      The AST.
    -    """
    -    statement = self.parse(query)
    -    c_query = qc.compile(statement,
    -                         self.xcontext_targets,
    -                         self.xcontext_postings,
    -                         self.xcontext_entries)
    -    if isinstance(c_query, qp.Select):
    -        self.assertSelectInvariants(c_query)
    -    return c_query
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.query.query_compile_test.CompileSelectBase.setUp(self) - - -

    - -
    - -

    Hook method for setting up the test fixture before exercising it.

    - -
    - Source code in beancount/query/query_compile_test.py -
    def setUp(self):
    -    self.parser = qp.Parser()
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.query.query_env - - - -

    - -
    - -

    Environment object for compiler.

    -

    This module contains the various column accessors and function evaluators that -are made available by the query compiler via their compilation context objects. -Define new columns and functions here.

    - - - -
    - - - - - - - - - - - - - -
    - - - -

    - -beancount.query.query_env.AbsDecimal (EvalFunction) - - - - -

    - -
    - -

    Compute the length of the argument. This works on sequences.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.AbsInventory (EvalFunction) - - - - -

    - -
    - -

    Compute the length of the argument. This works on sequences.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.AbsPosition (EvalFunction) - - - - -

    - -
    - -

    Compute the length of the argument. This works on sequences.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.AccountColumn (EvalColumn) - - - - -

    - -
    - -

    The account of the posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.AccountSortKey (EvalFunction) - - - - -

    - -
    - -

    Get a string to sort accounts in order taking into account the types.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.AnyMeta (EvalFunction) - - - - -

    - -
    - -

    Get metadata from the posting or its parent transaction's metadata if not present.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.BalanceColumn (EvalColumn) - - - - -

    - -
    - -

    The balance for the posting. These can be summed into inventories.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.CloseDate (EvalFunction) - - - - -

    - -
    - -

    Get the date of the close directive of the account.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Coalesce (EvalFunction) - - - - -

    - -
    - -

    Return the first non-null argument

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.ConvertAmount (EvalFunction) - - - - -

    - -
    - -

    Coerce an amount to a particular currency.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.ConvertAmountWithDate (EvalFunction) - - - - -

    - -
    - -

    Coerce an amount to a particular currency.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.ConvertInventory (EvalFunction) - - - - -

    - -
    - -

    Coerce an inventory to a particular currency.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.ConvertInventoryWithDate (EvalFunction) - - - - -

    - -
    - -

    Coerce an inventory to a particular currency.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.ConvertPosition (EvalFunction) - - - - -

    - -
    - -

    Coerce an amount to a particular currency.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.ConvertPositionWithDate (EvalFunction) - - - - -

    - -
    - -

    Coerce an amount to a particular currency.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.CostCurrencyColumn (EvalColumn) - - - - -

    - -
    - -

    The cost currency of the posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.CostDateColumn (EvalColumn) - - - - -

    - -
    - -

    The cost currency of the posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.CostInventory (EvalFunction) - - - - -

    - -
    - -

    Get the cost of an inventory.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.CostLabelColumn (EvalColumn) - - - - -

    - -
    - -

    The cost currency of the posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.CostNumberColumn (EvalColumn) - - - - -

    - -
    - -

    The number of cost units of the posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.CostPosition (EvalFunction) - - - - -

    - -
    - -

    Get the cost of a position.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Count (EvalAggregator) - - - - -

    - -
    - -

    Count the number of occurrences of the argument.

    - - - - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.query.query_env.Count.allocate(self, allocator) - - -

    - -
    - -

    Allocate handles to store data for a node's aggregate storage.

    -

    This is called once before beginning aggregations. If you need any -kind of per-aggregate storage during the computation phase, get it -in this method.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • allocator – An instance of Allocator, on which you can call allocate() to -obtain a handle for a slot to store data on store objects later on.

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def allocate(self, allocator):
    -    self.handle = allocator.allocate()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.Count.initialize(self, store) - - -

    - -
    - -

    Initialize this node's aggregate data. If the node is not an aggregate, -simply initialize the subnodes. Override this method in the aggregator -if you need data for storage.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def initialize(self, store):
    -    store[self.handle] = 0
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.Count.update(self, store, unused_ontext) - - -

    - -
    - -

    Evaluate this node. This is designed to recurse on its children.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    • context – The object to which the evaluation need to apply (see call).

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def update(self, store, unused_ontext):
    -    store[self.handle] += 1
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Currency (EvalFunction) - - - - -

    - -
    - -

    Extract the currency from an Amount.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.CurrencyColumn (EvalColumn) - - - - -

    - -
    - -

    The currency of the posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.CurrencyMeta (EvalFunction) - - - - -

    - -
    - -

    Get the metadata dict of the commodity directive of the currency.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Date (EvalFunction) - - - - -

    - -
    - -

    Construct a date with year, month, day arguments

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.DateAdd (EvalFunction) - - - - -

    - -
    - -

    Adds/subtracts number of days from the given date

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.DateColumn (EvalColumn) - - - - -

    - -
    - -

    The date of the parent transaction for this posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.DateDiff (EvalFunction) - - - - -

    - -
    - -

    Calculates the difference (in days) between two dates

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.DateEntryColumn (EvalColumn) - - - - -

    - -
    - -

    The date of the directive.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Day (EvalFunction) - - - - -

    - -
    - -

    Extract the day from a date.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.DayColumn (EvalColumn) - - - - -

    - -
    - -

    The day of the date of the parent transaction for this posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.DayEntryColumn (EvalColumn) - - - - -

    - -
    - -

    The day of the date of the directive.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.DescriptionColumn (EvalColumn) - - - - -

    - -
    - -

    A combination of the payee + narration for the transaction of this posting.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.DescriptionEntryColumn (EvalColumn) - - - - -

    - -
    - -

    A combination of the payee + narration of the transaction, if present.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.EntryMeta (EvalFunction) - - - - -

    - -
    - -

    Get some metadata key of the parent directive (Transaction).

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.FileLocationColumn (EvalColumn) - - - - -

    - -
    - -

    The filename:lineno where the posting was parsed from or created.

    -

    If you select this column as the first column, because it renders like -errors, Emacs is able to pick those up and you can navigate between an -arbitrary list of transactions with next-error and previous-error.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.FilenameColumn (EvalColumn) - - - - -

    - -
    - -

    The filename where the posting was parsed from or created.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.FilenameEntryColumn (EvalColumn) - - - - -

    - -
    - -

    The filename where the directive was parsed from or created.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.FilterCurrencyInventory (EvalFunction) - - - - -

    - -
    - -

    Filter an inventory to just the specified currency.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.FilterCurrencyPosition (EvalFunction) - - - - -

    - -
    - -

    Filter an inventory to just the specified currency.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.FilterEntriesEnvironment (CompilationEnvironment) - - - - -

    - -
    - -

    An execution context that provides access to attributes on Transactions -and other entry types.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.FilterPostingsEnvironment (CompilationEnvironment) - - - - -

    - -
    - -

    An execution context that provides access to attributes on Postings.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.FindFirst (EvalFunction) - - - - -

    - -
    - -

    Filter a string sequence by regular expression and return the first match.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.First (EvalAggregator) - - - - -

    - -
    - -

    Keep the first of the values seen.

    - - - - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.query.query_env.First.allocate(self, allocator) - - -

    - -
    - -

    Allocate handles to store data for a node's aggregate storage.

    -

    This is called once before beginning aggregations. If you need any -kind of per-aggregate storage during the computation phase, get it -in this method.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • allocator – An instance of Allocator, on which you can call allocate() to -obtain a handle for a slot to store data on store objects later on.

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def allocate(self, allocator):
    -    self.handle = allocator.allocate()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.First.initialize(self, store) - - -

    - -
    - -

    Initialize this node's aggregate data. If the node is not an aggregate, -simply initialize the subnodes. Override this method in the aggregator -if you need data for storage.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def initialize(self, store):
    -    store[self.handle] = None
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.First.update(self, store, context) - - -

    - -
    - -

    Evaluate this node. This is designed to recurse on its children.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    • context – The object to which the evaluation need to apply (see call).

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def update(self, store, context):
    -    if store[self.handle] is None:
    -        value = self.eval_args(context)[0]
    -        store[self.handle] = value
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.FlagColumn (EvalColumn) - - - - -

    - -
    - -

    The flag of the parent transaction for this posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.FlagEntryColumn (EvalColumn) - - - - -

    - -
    - -

    The flag the transaction.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.GetItemStr (EvalFunction) - - - - -

    - -
    - -

    Get the string value of a dict. The value is always converted to a string.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Grep (EvalFunction) - - - - -

    - -
    - -

    Match a group against a string and return only the matched portion.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.GrepN (EvalFunction) - - - - -

    - -
    - -

    Match a pattern with subgroups against a string and return the subgroup at the index

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.IdColumn (EvalColumn) - - - - -

    - -
    - -

    The unique id of the parent transaction for this posting.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.IdEntryColumn (EvalColumn) - - - - -

    - -
    - -

    Unique id of a directive.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.JoinStr (EvalFunction) - - - - -

    - -
    - -

    Join a sequence of strings to a single comma-separated string.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Last (EvalAggregator) - - - - -

    - -
    - -

    Keep the last of the values seen.

    - - - - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.query.query_env.Last.allocate(self, allocator) - - -

    - -
    - -

    Allocate handles to store data for a node's aggregate storage.

    -

    This is called once before beginning aggregations. If you need any -kind of per-aggregate storage during the computation phase, get it -in this method.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • allocator – An instance of Allocator, on which you can call allocate() to -obtain a handle for a slot to store data on store objects later on.

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def allocate(self, allocator):
    -    self.handle = allocator.allocate()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.Last.initialize(self, store) - - -

    - -
    - -

    Initialize this node's aggregate data. If the node is not an aggregate, -simply initialize the subnodes. Override this method in the aggregator -if you need data for storage.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def initialize(self, store):
    -    store[self.handle] = None
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.Last.update(self, store, context) - - -

    - -
    - -

    Evaluate this node. This is designed to recurse on its children.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    • context – The object to which the evaluation need to apply (see call).

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def update(self, store, context):
    -    value = self.eval_args(context)[0]
    -    store[self.handle] = value
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Leaf (EvalFunction) - - - - -

    - -
    - -

    Get the name of the leaf subaccount.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Length (EvalFunction) - - - - -

    - -
    - -

    Compute the length of the argument. This works on sequences.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.LineNoColumn (EvalColumn) - - - - -

    - -
    - -

    The line number from the file the posting was parsed from.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.LineNoEntryColumn (EvalColumn) - - - - -

    - -
    - -

    The line number from the file the directive was parsed from.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.LinksColumn (EvalColumn) - - - - -

    - -
    - -

    The set of links of the parent transaction for this posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.LinksEntryColumn (EvalColumn) - - - - -

    - -
    - -

    The set of links of the transaction.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.MatchAccount (EvalFunction) - - - - -

    - -
    - -

    A predicate, true if the transaction has at least one posting matching -the regular expression argument.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Max (EvalAggregator) - - - - -

    - -
    - -

    Compute the maximum of the values.

    - - - - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.query.query_env.Max.allocate(self, allocator) - - -

    - -
    - -

    Allocate handles to store data for a node's aggregate storage.

    -

    This is called once before beginning aggregations. If you need any -kind of per-aggregate storage during the computation phase, get it -in this method.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • allocator – An instance of Allocator, on which you can call allocate() to -obtain a handle for a slot to store data on store objects later on.

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def allocate(self, allocator):
    -    self.handle = allocator.allocate()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.Max.initialize(self, store) - - -

    - -
    - -

    Initialize this node's aggregate data. If the node is not an aggregate, -simply initialize the subnodes. Override this method in the aggregator -if you need data for storage.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def initialize(self, store):
    -    store[self.handle] = self.dtype()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.Max.update(self, store, context) - - -

    - -
    - -

    Evaluate this node. This is designed to recurse on its children.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    • context – The object to which the evaluation need to apply (see call).

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def update(self, store, context):
    -    value = self.eval_args(context)[0]
    -    if value > store[self.handle]:
    -        store[self.handle] = value
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.MaxWidth (EvalFunction) - - - - -

    - -
    - -

    Convert the argument to a substring. This can be used to ensure maximum width

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Meta (EvalFunction) - - - - -

    - -
    - -

    Get some metadata key of the Posting.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Min (EvalAggregator) - - - - -

    - -
    - -

    Compute the minimum of the values.

    - - - - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.query.query_env.Min.allocate(self, allocator) - - -

    - -
    - -

    Allocate handles to store data for a node's aggregate storage.

    -

    This is called once before beginning aggregations. If you need any -kind of per-aggregate storage during the computation phase, get it -in this method.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • allocator – An instance of Allocator, on which you can call allocate() to -obtain a handle for a slot to store data on store objects later on.

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def allocate(self, allocator):
    -    self.handle = allocator.allocate()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.Min.initialize(self, store) - - -

    - -
    - -

    Initialize this node's aggregate data. If the node is not an aggregate, -simply initialize the subnodes. Override this method in the aggregator -if you need data for storage.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def initialize(self, store):
    -    store[self.handle] = self.dtype()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.Min.update(self, store, context) - - -

    - -
    - -

    Evaluate this node. This is designed to recurse on its children.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    • context – The object to which the evaluation need to apply (see call).

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def update(self, store, context):
    -    value = self.eval_args(context)[0]
    -    if value < store[self.handle]:
    -        store[self.handle] = value
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Month (EvalFunction) - - - - -

    - -
    - -

    Extract the month from a date.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.MonthColumn (EvalColumn) - - - - -

    - -
    - -

    The month of the date of the parent transaction for this posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.MonthEntryColumn (EvalColumn) - - - - -

    - -
    - -

    The month of the date of the directive.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.NarrationColumn (EvalColumn) - - - - -

    - -
    - -

    The narration of the parent transaction for this posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.NarrationEntryColumn (EvalColumn) - - - - -

    - -
    - -

    The narration of the transaction.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - - - - - -
    - - - -

    - -beancount.query.query_env.Number (EvalFunction) - - - - -

    - -
    - -

    Extract the number from an Amount.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.NumberColumn (EvalColumn) - - - - -

    - -
    - -

    The number of units of the posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.OnlyInventory (EvalFunction) - - - - -

    - -
    - -

    Get one currency's amount from the inventory.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.OpenDate (EvalFunction) - - - - -

    - -
    - -

    Get the date of the open directive of the account.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.OpenMeta (EvalFunction) - - - - -

    - -
    - -

    Get the metadata dict of the open directive of the account.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.OtherAccountsColumn (EvalColumn) - - - - -

    - -
    - -

    The list of other accounts in the transaction, excluding that of this posting.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Parent (EvalFunction) - - - - -

    - -
    - -

    Get the parent name of the account.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.ParseDate (EvalFunction) - - - - -

    - -
    - -

    Construct a date with year, month, day arguments

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.PayeeColumn (EvalColumn) - - - - -

    - -
    - -

    The payee of the parent transaction for this posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.PayeeEntryColumn (EvalColumn) - - - - -

    - -
    - -

    The payee of the transaction.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.PosSignAmount (EvalFunction) - - - - -

    - -
    - -

    Correct sign of an Amount based on the usual balance of associated account.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.PosSignDecimal (EvalFunction) - - - - -

    - -
    - -

    Correct sign of an Amount based on the usual balance of associated account.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.PosSignInventory (EvalFunction) - - - - -

    - -
    - -

    Correct sign of an Amount based on the usual balance of associated account.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.PosSignPosition (EvalFunction) - - - - -

    - -
    - -

    Correct sign of an Amount based on the usual balance of associated account.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.PositionColumn (EvalColumn) - - - - -

    - -
    - -

    The position for the posting. These can be summed into inventories.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.PostingFlagColumn (EvalColumn) - - - - -

    - -
    - -

    The flag of the posting itself.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Price (EvalFunction) - - - - -

    - -
    - -

    Fetch a price for something at a particular date

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.PriceColumn (EvalColumn) - - - - -

    - -
    - -

    The price attached to the posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.PriceWithDate (EvalFunction) - - - - -

    - -
    - -

    Fetch a price for something at a particular date

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Quarter (EvalFunction) - - - - -

    - -
    - -

    Extract the quarter from a date.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Root (EvalFunction) - - - - -

    - -
    - -

    Get the root name(s) of the account.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.SafeDiv (EvalFunction) - - - - -

    - -
    - -

    A division operation that swallows dbz exceptions and outputs 0 instead.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - - -
    - - - -

    - -beancount.query.query_env.Str (EvalFunction) - - - - -

    - -
    - -

    Convert the argument to a string.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Subst (EvalFunction) - - - - -

    - -
    - -

    Substitute leftmost non-overlapping occurrences of pattern by replacement.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Sum (EvalAggregator) - - - - -

    - -
    - -

    Calculate the sum of the numerical argument.

    - - - - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.query.query_env.Sum.allocate(self, allocator) - - -

    - -
    - -

    Allocate handles to store data for a node's aggregate storage.

    -

    This is called once before beginning aggregations. If you need any -kind of per-aggregate storage during the computation phase, get it -in this method.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • allocator – An instance of Allocator, on which you can call allocate() to -obtain a handle for a slot to store data on store objects later on.

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def allocate(self, allocator):
    -    self.handle = allocator.allocate()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.Sum.initialize(self, store) - - -

    - -
    - -

    Initialize this node's aggregate data. If the node is not an aggregate, -simply initialize the subnodes. Override this method in the aggregator -if you need data for storage.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def initialize(self, store):
    -    store[self.handle] = self.dtype()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.Sum.update(self, store, context) - - -

    - -
    - -

    Evaluate this node. This is designed to recurse on its children.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    • context – The object to which the evaluation need to apply (see call).

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def update(self, store, context):
    -    value = self.eval_args(context)[0]
    -    if value is not None:
    -        store[self.handle] += value
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.SumAmount (SumBase) - - - - -

    - -
    - -

    Calculate the sum of the amount. The result is an Inventory.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.query.query_env.SumAmount.update(self, store, context) - - -

    - -
    - -

    Evaluate this node. This is designed to recurse on its children.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    • context – The object to which the evaluation need to apply (see call).

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def update(self, store, context):
    -    value = self.eval_args(context)[0]
    -    store[self.handle].add_amount(value)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.SumBase (EvalAggregator) - - - - -

    - -
    - - - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.query.query_env.SumBase.allocate(self, allocator) - - -

    - -
    - -

    Allocate handles to store data for a node's aggregate storage.

    -

    This is called once before beginning aggregations. If you need any -kind of per-aggregate storage during the computation phase, get it -in this method.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • allocator – An instance of Allocator, on which you can call allocate() to -obtain a handle for a slot to store data on store objects later on.

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def allocate(self, allocator):
    -    self.handle = allocator.allocate()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env.SumBase.initialize(self, store) - - -

    - -
    - -

    Initialize this node's aggregate data. If the node is not an aggregate, -simply initialize the subnodes. Override this method in the aggregator -if you need data for storage.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def initialize(self, store):
    -    store[self.handle] = inventory.Inventory()
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.SumInventory (SumBase) - - - - -

    - -
    - -

    Calculate the sum of the inventories. The result is an Inventory.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.query.query_env.SumInventory.update(self, store, context) - - -

    - -
    - -

    Evaluate this node. This is designed to recurse on its children.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    • context – The object to which the evaluation need to apply (see call).

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def update(self, store, context):
    -    value = self.eval_args(context)[0]
    -    store[self.handle].add_inventory(value)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.SumPosition (SumBase) - - - - -

    - -
    - -

    Calculate the sum of the position. The result is an Inventory.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.query.query_env.SumPosition.update(self, store, context) - - -

    - -
    - -

    Evaluate this node. This is designed to recurse on its children.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • store – An object indexable by handles appropriated during allocate().

    • -
    • context – The object to which the evaluation need to apply (see call).

    • -
    -
    -
    - Source code in beancount/query/query_env.py -
    def update(self, store, context):
    -    value = self.eval_args(context)[0]
    -    store[self.handle].add_position(value)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.TagsColumn (EvalColumn) - - - - -

    - -
    - -

    The set of tags of the parent transaction for this posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.TagsEntryColumn (EvalColumn) - - - - -

    - -
    - -

    The set of tags of the transaction.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.TargetsEnvironment (FilterPostingsEnvironment) - - - - -

    - -
    - -

    An execution context that provides access to attributes on Postings.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Today (EvalFunction) - - - - -

    - -
    - -

    Today's date

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.TypeColumn (EvalColumn) - - - - -

    - -
    - -

    The data type of the parent transaction for this posting.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.TypeEntryColumn (EvalColumn) - - - - -

    - -
    - -

    The data type of the directive.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.UnitsInventory (EvalFunction) - - - - -

    - -
    - -

    Get the number of units of an inventory (stripping cost).

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.UnitsPosition (EvalFunction) - - - - -

    - -
    - -

    Get the number of units of a position (stripping cost).

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.ValueInventory (EvalFunction) - - - - -

    - -
    - -

    Coerce an inventory to its market value at the current date.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.ValueInventoryWithDate (EvalFunction) - - - - -

    - -
    - -

    Coerce an inventory to its market value at a particular date.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.ValuePosition (EvalFunction) - - - - -

    - -
    - -

    Convert a position to its cost currency at the market value.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.ValuePositionWithDate (EvalFunction) - - - - -

    - -
    - -

    Convert a position to its cost currency at the market value of a particular date.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Weekday (EvalFunction) - - - - -

    - -
    - -

    Extract a 3-letter weekday from a date.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.WeightColumn (EvalColumn) - - - - -

    - -
    - -

    The computed weight used for this posting.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.Year (EvalFunction) - - - - -

    - -
    - -

    Extract the year from a date.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.YearColumn (EvalColumn) - - - - -

    - -
    - -

    The year of the date of the parent transaction for this posting.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.YearEntryColumn (EvalColumn) - - - - -

    - -
    - -

    The year of the date of the directive.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env.YearMonth (EvalFunction) - - - - -

    - -
    - -

    Extract the year and month from a date.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_env._Neg (EvalFunction) - - - - - private - - -

    - -
    - -

    Compute the negative value of the argument. This works on various types.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.query.query_env_test - - - -

    - -
    - - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.query.query_env_test.TestEnv (TestCase) - - - - -

    - -
    - - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_env_test.TestEnv.test_AnyMeta(self, entries, _, options_map) - - -

    - -
    - -

    2016-11-20 * - name: "TheName" - address: "1 Wrong Way" - empty: "NotEmpty" - Assets:Banking 1 USD - color: "Green" - address: "1 Right Way" - empty:

    - -
    - Source code in beancount/query/query_env_test.py -
    @parser.parse_doc()
    -def test_AnyMeta(self, entries, _, options_map):
    -    """
    -    2016-11-20 *
    -      name: "TheName"
    -      address: "1 Wrong Way"
    -      empty: "NotEmpty"
    -      Assets:Banking          1 USD
    -        color: "Green"
    -        address: "1 Right Way"
    -        empty:
    -    """
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT ANY_META("name") as m')
    -    self.assertEqual([('TheName',)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT ANY_META("color") as m')
    -    self.assertEqual([('Green',)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT ANY_META("address") as m')
    -    self.assertEqual([('1 Right Way',)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT ANY_META("empty") as m')
    -    self.assertEqual([(None,)], rrows)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env_test.TestEnv.test_Coalesce(self, entries, _, options_map) - - -

    - -
    - -

    2016-11-20 * - Assets:Banking 1 USD

    - -
    - Source code in beancount/query/query_env_test.py -
    @parser.parse_doc()
    -def test_Coalesce(self, entries, _, options_map):
    -    """
    -    2016-11-20 *
    -      Assets:Banking          1 USD
    -    """
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT COALESCE(account, price) as m')
    -    self.assertEqual([('Assets:Banking',)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT COALESCE(price, account) as m')
    -    self.assertEqual([('Assets:Banking',)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT COALESCE(price, cost_number) as m')
    -    self.assertEqual([(None,)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT COALESCE(narration, account) as m')
    -    self.assertEqual([('',)], rrows)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env_test.TestEnv.test_Date(self, entries, _, options_map) - - -

    - -
    - -

    2016-11-20 * "ok" - Assets:Banking 1 USD

    - -
    - Source code in beancount/query/query_env_test.py -
    @parser.parse_doc()
    -def test_Date(self, entries, _, options_map):
    -    """
    -    2016-11-20 * "ok"
    -      Assets:Banking          1 USD
    -    """
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT date(2020, 1, 2) as m')
    -    self.assertEqual([(datetime.date(2020, 1, 2),)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT date(year, month, 1) as m')
    -    self.assertEqual([(datetime.date(2016, 11, 1),)], rrows)
    -
    -    with self.assertRaisesRegex(ValueError, "day is out of range for month"):
    -        rtypes, rrows = query.run_query(entries, options_map,
    -                                        'SELECT date(2020, 2, 32) as m')
    -
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT date("2020-01-02") as m')
    -    self.assertEqual([(datetime.date(2020, 1, 2),)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT date("2016/11/1") as m')
    -    self.assertEqual([(datetime.date(2016, 11, 1),)], rrows)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env_test.TestEnv.test_DateDiffAdjust(self, entries, _, options_map) - - -

    - -
    - -

    2016-11-20 * "ok" - Assets:Banking -1 STOCK { 5 USD, 2016-10-30 }

    - -
    - Source code in beancount/query/query_env_test.py -
    @parser.parse_doc()
    -def test_DateDiffAdjust(self, entries, _, options_map):
    -    """
    -    2016-11-20 * "ok"
    -      Assets:Banking          -1 STOCK { 5 USD, 2016-10-30 }
    -    """
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT date_diff(date, cost_date) as m')
    -    self.assertEqual([(21,)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT date_diff(cost_date, date) as m')
    -    self.assertEqual([(-21,)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT date_add(date, 1) as m')
    -    self.assertEqual([(datetime.date(2016, 11, 21),)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map,
    -                                    'SELECT date_add(date, -1) as m')
    -    self.assertEqual([(datetime.date(2016, 11, 19),)], rrows)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env_test.TestEnv.test_GrepN(self, entries, _, options_map) - - -

    - -
    - -

    2016-11-20 * "prev match in context next" - Assets:Banking 1 USD

    - -
    - Source code in beancount/query/query_env_test.py -
    @parser.parse_doc()
    -def test_GrepN(self, entries, _, options_map):
    -    """
    -    2016-11-20 * "prev match in context next"
    -      Assets:Banking          1 USD
    -    """
    -    rtypes, rrows = query.run_query(entries, options_map, '''
    -      SELECT GREPN("in", narration, 0) as m
    -    ''')
    -    self.assertEqual([('in',)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map, '''
    -      SELECT GREPN("match (.*) context", narration, 1) as m
    -    ''')
    -    self.assertEqual([('in',)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map, '''
    -      SELECT GREPN("(.*) in (.*)", narration, 2) as m
    -    ''')
    -    self.assertEqual([('context next',)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map, '''
    -      SELECT GREPN("ab(at)hing", "abathing", 1) as m
    -    ''')
    -    self.assertEqual([('at',)], rrows)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_env_test.TestEnv.test_Subst(self, entries, _, options_map) - - -

    - -
    - -

    2016-11-20 * "I love candy" - Assets:Banking -1 USD

    -

    2016-11-21 * "Buy thing thing" - Assets:Cash -1 USD

    - -
    - Source code in beancount/query/query_env_test.py -
    @parser.parse_doc()
    -def test_Subst(self, entries, _, options_map):
    -    """
    -    2016-11-20 * "I love candy"
    -      Assets:Banking       -1 USD
    -
    -    2016-11-21 * "Buy thing thing"
    -      Assets:Cash          -1 USD
    -    """
    -    rtypes, rrows = query.run_query(entries, options_map, '''
    -      SELECT SUBST("[Cc]andy", "carrots", narration) as m where date = 2016-11-20
    -    ''')
    -    self.assertEqual([('I love carrots',)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map, '''
    -      SELECT SUBST("thing", "t", narration) as m where date = 2016-11-21
    -    ''')
    -    self.assertEqual([('Buy t t',)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map, '''
    -      SELECT SUBST("random", "t", narration) as m where date = 2016-11-21
    -    ''')
    -    self.assertEqual([('Buy thing thing',)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map, '''
    -      SELECT SUBST("(love)", "\\1 \\1", narration) as m where date = 2016-11-20
    -    ''')
    -    self.assertEqual([('I love love candy',)], rrows)
    -
    -    rtypes, rrows = query.run_query(entries, options_map, '''
    -      SELECT SUBST("Assets:.*", "Savings", account) as a, str(sum(position)) as p
    -    ''')
    -    self.assertEqual([('Savings', '(-2 USD)')], rrows)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.query.query_execute - - - -

    - -
    - -

    Execution of interpreter on data rows.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.query.query_execute.Allocator - - - -

    - -
    - -

    A helper class to count slot allocations and return unique handles to them.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.query.query_execute.Allocator.allocate(self) - - -

    - -
    - -

    Allocate a new slot to store row aggregation information.

    - - - - - - - - - - - - -
    Returns: -
      -
    • A unique handle used to index into an row-aggregation store (an integer).

    • -
    -
    -
    - Source code in beancount/query/query_execute.py -
    def allocate(self):
    -    """Allocate a new slot to store row aggregation information.
    -
    -    Returns:
    -      A unique handle used to index into an row-aggregation store (an integer).
    -    """
    -    handle = self.size
    -    self.size += 1
    -    return handle
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_execute.Allocator.create_store(self) - - -

    - -
    - -

    Create a new row-aggregation store suitable to contain all the node allocations.

    - - - - - - - - - - - - -
    Returns: -
      -
    • A store that can accommodate and be indexed by all the allocated slot handles.

    • -
    -
    -
    - Source code in beancount/query/query_execute.py -
    def create_store(self):
    -    """Create a new row-aggregation store suitable to contain all the node allocations.
    -
    -    Returns:
    -      A store that can accommodate and be indexed by all the allocated slot handles.
    -    """
    -    return [None] * self.size
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_execute.RowContext - - - -

    - -
    - -

    A dumb container for information used by a row expression.

    - - - - -
    - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.query.query_execute.create_row_context(entries, options_map) - - -

    - -
    - -

    Create the context container which we will use to evaluate rows.

    - -
    - Source code in beancount/query/query_execute.py -
    def create_row_context(entries, options_map):
    -    """Create the context container which we will use to evaluate rows."""
    -    context = RowContext()
    -    context.balance = inventory.Inventory()
    -
    -    # Initialize some global properties for use by some of the accessors.
    -    context.options_map = options_map
    -    context.account_types = options.get_account_types(options_map)
    -    context.open_close_map = getters.get_account_open_close(entries)
    -    context.commodity_map = getters.get_commodity_map(entries)
    -    context.price_map = prices.build_price_map(entries)
    -
    -    return context
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_execute.execute_print(c_print, entries, options_map, file) - - -

    - -
    - -

    Print entries from a print statement specification.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • c_print – An instance of a compiled EvalPrint statement.

    • -
    • entries – A list of directives.

    • -
    • options_map – A parser's option_map.

    • -
    • file – The output file to print to.

    • -
    -
    -
    - Source code in beancount/query/query_execute.py -
    def execute_print(c_print, entries, options_map, file):
    -    """Print entries from a print statement specification.
    -
    -    Args:
    -      c_print: An instance of a compiled EvalPrint statement.
    -      entries: A list of directives.
    -      options_map: A parser's option_map.
    -      file: The output file to print to.
    -    """
    -    if c_print and c_print.c_from is not None:
    -        context = create_row_context(entries, options_map)
    -        entries = filter_entries(c_print.c_from, entries, options_map, context)
    -
    -    # Create a context that renders all numbers with their natural
    -    # precision, but honors the commas option. This is kept in sync with
    -    # {2c694afe3140} to avoid a dependency.
    -    dcontext = display_context.DisplayContext()
    -    dcontext.set_commas(options_map['dcontext'].commas)
    -    printer.print_entries(entries, dcontext, file=file)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_execute.execute_query(query, entries, options_map) - - -

    - -
    - -

    Given a compiled select statement, execute the query.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • query – An instance of a query_compile.Query

    • -
    • entries – A list of directives.

    • -
    • options_map – A parser's option_map.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of – result_types: A list of (name, data-type) item pairs. - result_rows: A list of ResultRow tuples of length and types described by - 'result_types'.

    • -
    -
    -
    - Source code in beancount/query/query_execute.py -
    def execute_query(query, entries, options_map):
    -    """Given a compiled select statement, execute the query.
    -
    -    Args:
    -      query: An instance of a query_compile.Query
    -      entries: A list of directives.
    -      options_map: A parser's option_map.
    -    Returns:
    -      A pair of:
    -        result_types: A list of (name, data-type) item pairs.
    -        result_rows: A list of ResultRow tuples of length and types described by
    -          'result_types'.
    -    """
    -    # Figure out the result types that describe what we return.
    -    result_types = [(target.name, target.c_expr.dtype)
    -                    for target in query.c_targets
    -                    if target.name is not None]
    -
    -    # Create a class for each final result.
    -    # pylint: disable=invalid-name
    -    ResultRow = collections.namedtuple('ResultRow',
    -                                       [target.name
    -                                        for target in query.c_targets
    -                                        if target.name is not None])
    -
    -    # Pre-compute lists of the expressions to evaluate.
    -    group_indexes = (set(query.group_indexes)
    -                     if query.group_indexes is not None
    -                     else query.group_indexes)
    -
    -    # Indexes of the columns for result rows and order rows.
    -    result_indexes = [index
    -                      for index, c_target in enumerate(query.c_targets)
    -                      if c_target.name]
    -    order_indexes = query.order_indexes
    -
    -    # Figure out if we need to compute balance.
    -    uses_balance = any(uses_balance_column(c_expr)
    -                       for c_expr in itertools.chain(
    -                               [c_target.c_expr for c_target in query.c_targets],
    -                               [query.c_where] if query.c_where else []))
    -
    -    context = create_row_context(entries, options_map)
    -
    -    # Filter the entries using the FROM clause.
    -    filt_entries = (filter_entries(query.c_from, entries, options_map, context)
    -                    if query.c_from is not None else
    -                    entries)
    -
    -    # Dispatch between the non-aggregated queries and aggregated queries.
    -    c_where = query.c_where
    -    schwartz_rows = []
    -
    -    # Precompute a list of expressions to be evaluated.
    -    c_target_exprs = [c_target.c_expr for c_target in query.c_targets]
    -
    -    if query.group_indexes is None:
    -        # This is a non-aggregated query.
    -
    -        # Iterate over all the postings once and produce schwartzian rows.
    -        for entry in misc_utils.filter_type(filt_entries, data.Transaction):
    -            context.entry = entry
    -            for posting in entry.postings:
    -                context.posting = posting
    -                if c_where is None or c_where(context):
    -                    # Compute the balance.
    -                    if uses_balance:
    -                        context.balance.add_position(posting)
    -
    -                    # Evaluate all the values.
    -                    values = [c_expr(context) for c_expr in c_target_exprs]
    -
    -                    # Compute result and sort-key objects.
    -                    result = ResultRow._make(values[index]
    -                                             for index in result_indexes)
    -                    sortkey = row_sortkey(order_indexes, values, c_target_exprs)
    -                    schwartz_rows.append((sortkey, result))
    -    else:
    -        # This is an aggregated query.
    -
    -        # Precompute lists of non-aggregate and aggregate expressions to
    -        # evaluate. For aggregate targets, we hunt down the aggregate
    -        # sub-expressions to evaluate, to avoid recursion during iteration.
    -        c_nonaggregate_exprs = []
    -        c_aggregate_exprs = []
    -        for index, c_expr in enumerate(c_target_exprs):
    -            if index in group_indexes:
    -                c_nonaggregate_exprs.append(c_expr)
    -            else:
    -                _, aggregate_exprs = query_compile.get_columns_and_aggregates(c_expr)
    -                c_aggregate_exprs.extend(aggregate_exprs)
    -        # Note: it is possible that there are no aggregates to compute here. You could
    -        # have all columns be non-aggregates and group-by the entire list of columns.
    -
    -        # Pre-allocate handles in aggregation nodes.
    -        allocator = Allocator()
    -        for c_expr in c_aggregate_exprs:
    -            c_expr.allocate(allocator)
    -
    -        # Iterate over all the postings to evaluate the aggregates.
    -        agg_store = {}
    -        for entry in misc_utils.filter_type(filt_entries, data.Transaction):
    -            context.entry = entry
    -            for posting in entry.postings:
    -                context.posting = posting
    -                if c_where is None or c_where(context):
    -                    # Compute the balance.
    -                    if uses_balance:
    -                        context.balance.add_position(posting)
    -
    -                    # Compute the non-aggregate expressions.
    -                    row_key = tuple(c_expr(context)
    -                                    for c_expr in c_nonaggregate_exprs)
    -
    -                    # Get an appropriate store for the unique key of this row.
    -                    try:
    -                        store = agg_store[row_key]
    -                    except KeyError:
    -                        # This is a row; create a new store.
    -                        store = allocator.create_store()
    -                        for c_expr in c_aggregate_exprs:
    -                            c_expr.initialize(store)
    -                        agg_store[row_key] = store
    -
    -                    # Update the aggregate expressions.
    -                    for c_expr in c_aggregate_exprs:
    -                        c_expr.update(store, context)
    -
    -        # Iterate over all the aggregations to produce the schwartzian rows.
    -        for key, store in agg_store.items():
    -            key_iter = iter(key)
    -            values = []
    -
    -            # Finalize the store.
    -            for c_expr in c_aggregate_exprs:
    -                c_expr.finalize(store)
    -            context.store = store
    -
    -            for index, c_expr in enumerate(c_target_exprs):
    -                if index in group_indexes:
    -                    value = next(key_iter)
    -                else:
    -                    value = c_expr(context)
    -                values.append(value)
    -
    -            # Compute result and sort-key objects.
    -            result = ResultRow._make(values[index]
    -                                     for index in result_indexes)
    -            sortkey = row_sortkey(order_indexes, values, c_target_exprs)
    -            schwartz_rows.append((sortkey, result))
    -
    -    # Order results if requested.
    -    if order_indexes is not None:
    -        schwartz_rows.sort(key=operator.itemgetter(0),
    -                           reverse=(query.ordering == 'DESC'))
    -
    -    # Extract final results, in sorted order at this point.
    -    result_rows = [x[1] for x in schwartz_rows]
    -
    -    # Apply distinct.
    -    if query.distinct:
    -        result_rows = list(misc_utils.uniquify(result_rows))
    -
    -    # Apply limit.
    -    if query.limit is not None:
    -        result_rows = result_rows[:query.limit]
    -
    -    # Flatten inventories if requested.
    -    if query.flatten:
    -        result_types, result_rows = flatten_results(result_types, result_rows)
    -
    -    return (result_types, result_rows)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_execute.filter_entries(c_from, entries, options_map, context) - - -

    - -
    - -

    Filter the entries by the given compiled FROM clause.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • c_from – A compiled From clause instance.

    • -
    • entries – A list of directives.

    • -
    • options_map – A parser's option_map.

    • -
    • context – A prototype of RowContext to use for evaluation.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of filtered entries.

    • -
    -
    -
    - Source code in beancount/query/query_execute.py -
    def filter_entries(c_from, entries, options_map, context):
    -    """Filter the entries by the given compiled FROM clause.
    -
    -    Args:
    -      c_from: A compiled From clause instance.
    -      entries: A list of directives.
    -      options_map: A parser's option_map.
    -      context: A prototype of RowContext to use for evaluation.
    -    Returns:
    -      A list of filtered entries.
    -    """
    -    assert c_from is None or isinstance(c_from, query_compile.EvalFrom)
    -    assert isinstance(entries, list)
    -
    -    context = copy.copy(context)
    -
    -    if c_from is None:
    -        return entries
    -
    -    # Process the OPEN clause.
    -    if c_from.open is not None:
    -        assert isinstance(c_from.open, datetime.date)
    -        open_date = c_from.open
    -        entries, index = summarize.open_opt(entries, open_date, options_map)
    -
    -    # Process the CLOSE clause.
    -    if c_from.close is not None:
    -        if isinstance(c_from.close, datetime.date):
    -            close_date = c_from.close
    -            entries, index = summarize.close_opt(entries, close_date, options_map)
    -        elif c_from.close is True:
    -            entries, index = summarize.close_opt(entries, None, options_map)
    -
    -    # Process the CLEAR clause.
    -    if c_from.clear is not None:
    -        entries, index = summarize.clear_opt(entries, None, options_map)
    -
    -    # Filter the entries with the FROM clause's expression.
    -    c_expr = c_from.c_expr
    -    if c_expr is not None:
    -        # A simple function receives a context; how come close_date() is
    -        # accepted in the context of a FROM clause? It shouldn't be.
    -        new_entries = []
    -        for entry in entries:
    -            context.entry = entry
    -            if c_expr(context):
    -                new_entries.append(entry)
    -        entries = new_entries
    -
    -    return entries
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_execute.flatten_results(result_types, result_rows) - - -

    - -
    - -

    Convert inventories in result types to have a row for each.

    -

    This routine will expand all result lines with an inventory into a new line -for each position.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • result_types – A list of (name, data-type) item pairs.

    • -
    • result_rows – A list of ResultRow tuples of length and types described by -'result_types'.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • result_types – A list of (name, data-type) item pairs. There should be no - Inventory types anymore. -result_rows: A list of ResultRow tuples of length and types described by - 'result_types'. All inventories from the input should have been converted - to Position types.

    • -
    -
    -
    - Source code in beancount/query/query_execute.py -
    def flatten_results(result_types, result_rows):
    -    """Convert inventories in result types to have a row for each.
    -
    -    This routine will expand all result lines with an inventory into a new line
    -    for each position.
    -
    -    Args:
    -        result_types: A list of (name, data-type) item pairs.
    -        result_rows: A list of ResultRow tuples of length and types described by
    -          'result_types'.
    -    Returns:
    -        result_types: A list of (name, data-type) item pairs. There should be no
    -          Inventory types anymore.
    -        result_rows: A list of ResultRow tuples of length and types described by
    -          'result_types'. All inventories from the input should have been converted
    -          to Position types.
    -    """
    -    indexes = set(index
    -                  for index, (name, result_type) in enumerate(result_types)
    -                  if result_type is inventory.Inventory)
    -    if not indexes:
    -        return (result_types, result_rows)
    -
    -    # pylint: disable=invalid-name
    -    ResultRow = type(result_rows[0])
    -
    -    # We have to make at least some conversions.
    -    num_columns = len(result_types)
    -    output_rows = []
    -    for result_row in result_rows:
    -        max_rows = max(len(result_row[icol]) for icol in indexes)
    -        for irow in range(max_rows):
    -            output_row = []
    -            for icol in range(num_columns):
    -                value = result_row[icol]
    -                if icol in indexes:
    -                    value = value[irow] if irow < len(value) else None
    -                output_row.append(value)
    -            output_rows.append(ResultRow._make(output_row))
    -
    -    # Convert the types.
    -    output_types = [(name, (position.Position
    -                            if result_type is inventory.Inventory
    -                            else result_type))
    -                    for name, result_type in result_types]
    -
    -    return output_types, output_rows
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_execute.row_sortkey(order_indexes, values, c_exprs) - - -

    - -
    - -

    Generate a sortkey for the given values.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • order_indexes – The indexes by which the rows should be sorted.

    • -
    • values – The computed values in the row.

    • -
    • c_exprs – The matching c_expr's.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple, the sortkey.

    • -
    -
    -
    - Source code in beancount/query/query_execute.py -
    def row_sortkey(order_indexes, values, c_exprs):
    -    """Generate a sortkey for the given values.
    -
    -    Args:
    -      order_indexes: The indexes by which the rows should be sorted.
    -      values: The computed values in the row.
    -      c_exprs: The matching c_expr's.
    -    Returns:
    -      A tuple, the sortkey.
    -    """
    -    if order_indexes is None:
    -        return None
    -    key = []
    -    for index in order_indexes:
    -        value = values[index]
    -        key.append(_MIN_VALUES.get(c_exprs[index].dtype, None)
    -                   if value is None
    -                   else value)
    -    return tuple(key)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_execute.uses_balance_column(c_expr) - - -

    - -
    - -

    Return true if the expression accesses the special 'balance' column.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • c_expr – A compiled expression tree (an EvalNode node).

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A boolean, true if the expression contains a BalanceColumn node.

    • -
    -
    -
    - Source code in beancount/query/query_execute.py -
    def uses_balance_column(c_expr):
    -    """Return true if the expression accesses the special 'balance' column.
    -
    -    Args:
    -      c_expr: A compiled expression tree (an EvalNode node).
    -    Returns:
    -      A boolean, true if the expression contains a BalanceColumn node.
    -    """
    -    return (isinstance(c_expr, query_env.BalanceColumn) or
    -            any(uses_balance_column(c_node) for c_node in c_expr.childnodes()))
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.query.query_execute_test - - - -

    - -
    - - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.query.query_execute_test.QueryBase (TestCase) - - - - -

    - -
    - - - - - -
    - - - - - - - - - - - - - - - -
    - - - -

    -beancount.query.query_execute_test.QueryBase.compile(self, bql_string) - - -

    - -
    - -

    Parse a query and compile it.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • bql_string – An SQL query to be parsed.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A compiled EvalQuery node.

    • -
    -
    -
    - Source code in beancount/query/query_execute_test.py -
    def compile(self, bql_string):
    -    """Parse a query and compile it.
    -
    -    Args:
    -      bql_string: An SQL query to be parsed.
    -    Returns:
    -      A compiled EvalQuery node.
    -    """
    -    return qc.compile_select(self.parse(bql_string),
    -                             self.xcontext_targets,
    -                             self.xcontext_postings,
    -                             self.xcontext_entries)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_execute_test.QueryBase.parse(self, bql_string) - - -

    - -
    - -

    Parse a query.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • bql_string – An SQL query to be parsed.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A parsed statement (Select() node).

    • -
    -
    -
    - Source code in beancount/query/query_execute_test.py -
    def parse(self, bql_string):
    -    """Parse a query.
    -
    -    Args:
    -      bql_string: An SQL query to be parsed.
    -    Returns:
    -      A parsed statement (Select() node).
    -    """
    -    return self.parser.parse(bql_string.strip())
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_execute_test.QueryBase.setUp(self) - - -

    - -
    - -

    Hook method for setting up the test fixture before exercising it.

    - -
    - Source code in beancount/query/query_execute_test.py -
    def setUp(self):
    -    super().setUp()
    -    self.parser = query_parser.Parser()
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.query.query_parser - - - -

    - -
    - -

    Parser for Beancount Query Language.

    - - - -
    - - - - - - - - - - - - - - - - - - - - - - -
    - - - -

    - -beancount.query.query_parser.Balances (tuple) - - - - -

    - -
    - -

    Balances(summary_func, from_clause, where_clause)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_parser.Balances.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/query/query_parser.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Balances.__new__(_cls, summary_func, from_clause, where_clause) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of Balances(summary_func, from_clause, where_clause)

    - -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Balances.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/query/query_parser.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Balances._asdict(self) - - - private - - -

    - -
    - -

    Return a new dict which maps field names to their values.

    - -
    - Source code in beancount/query/query_parser.py -
    def _asdict(self):
    -    'Return a new dict which maps field names to their values.'
    -    return _dict(_zip(self._fields, self))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Balances._make(iterable) - - - classmethod - private - - -

    - -
    - -

    Make a new Balances object from a sequence or iterable

    - -
    - Source code in beancount/query/query_parser.py -
    @classmethod
    -def _make(cls, iterable):
    -    result = tuple_new(cls, iterable)
    -    if _len(result) != num_fields:
    -        raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Balances._replace(/, self, **kwds) - - - private - - -

    - -
    - -

    Return a new Balances object replacing specified fields with new values

    - -
    - Source code in beancount/query/query_parser.py -
    def _replace(self, /, **kwds):
    -    result = self._make(_map(kwds.pop, field_names, self))
    -    if kwds:
    -        raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
    -    return result
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - -
    - - - -

    - -beancount.query.query_parser.Errors (tuple) - - - - -

    - -
    - -

    Errors()

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_parser.Errors.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/query/query_parser.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Errors.__new__(_cls) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of Errors()

    - -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Errors.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/query/query_parser.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Errors._asdict(self) - - - private - - -

    - -
    - -

    Return a new dict which maps field names to their values.

    - -
    - Source code in beancount/query/query_parser.py -
    def _asdict(self):
    -    'Return a new dict which maps field names to their values.'
    -    return _dict(_zip(self._fields, self))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Errors._make(iterable) - - - classmethod - private - - -

    - -
    - -

    Make a new Errors object from a sequence or iterable

    - -
    - Source code in beancount/query/query_parser.py -
    @classmethod
    -def _make(cls, iterable):
    -    result = tuple_new(cls, iterable)
    -    if _len(result) != num_fields:
    -        raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Errors._replace(/, self, **kwds) - - - private - - -

    - -
    - -

    Return a new Errors object replacing specified fields with new values

    - -
    - Source code in beancount/query/query_parser.py -
    def _replace(self, /, **kwds):
    -    result = self._make(_map(kwds.pop, field_names, self))
    -    if kwds:
    -        raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
    -    return result
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_parser.Explain (tuple) - - - - -

    - -
    - -

    Explain(statement,)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_parser.Explain.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/query/query_parser.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Explain.__new__(_cls, statement) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of Explain(statement,)

    - -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Explain.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/query/query_parser.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Explain._asdict(self) - - - private - - -

    - -
    - -

    Return a new dict which maps field names to their values.

    - -
    - Source code in beancount/query/query_parser.py -
    def _asdict(self):
    -    'Return a new dict which maps field names to their values.'
    -    return _dict(_zip(self._fields, self))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Explain._make(iterable) - - - classmethod - private - - -

    - -
    - -

    Make a new Explain object from a sequence or iterable

    - -
    - Source code in beancount/query/query_parser.py -
    @classmethod
    -def _make(cls, iterable):
    -    result = tuple_new(cls, iterable)
    -    if _len(result) != num_fields:
    -        raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Explain._replace(/, self, **kwds) - - - private - - -

    - -
    - -

    Return a new Explain object replacing specified fields with new values

    - -
    - Source code in beancount/query/query_parser.py -
    def _replace(self, /, **kwds):
    -    result = self._make(_map(kwds.pop, field_names, self))
    -    if kwds:
    -        raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
    -    return result
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    - -beancount.query.query_parser.Journal (tuple) - - - - -

    - -
    - -

    Journal(account, summary_func, from_clause)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_parser.Journal.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/query/query_parser.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Journal.__new__(_cls, account, summary_func, from_clause) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of Journal(account, summary_func, from_clause)

    - -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Journal.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/query/query_parser.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Journal._asdict(self) - - - private - - -

    - -
    - -

    Return a new dict which maps field names to their values.

    - -
    - Source code in beancount/query/query_parser.py -
    def _asdict(self):
    -    'Return a new dict which maps field names to their values.'
    -    return _dict(_zip(self._fields, self))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Journal._make(iterable) - - - classmethod - private - - -

    - -
    - -

    Make a new Journal object from a sequence or iterable

    - -
    - Source code in beancount/query/query_parser.py -
    @classmethod
    -def _make(cls, iterable):
    -    result = tuple_new(cls, iterable)
    -    if _len(result) != num_fields:
    -        raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Journal._replace(/, self, **kwds) - - - private - - -

    - -
    - -

    Return a new Journal object replacing specified fields with new values

    - -
    - Source code in beancount/query/query_parser.py -
    def _replace(self, /, **kwds):
    -    result = self._make(_map(kwds.pop, field_names, self))
    -    if kwds:
    -        raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
    -    return result
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    - -beancount.query.query_parser.Lexer - - - -

    - -
    - -

    PLY lexer for the Beancount Query Language.

    - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - -

    -beancount.query.query_parser.Lexer.t_DATE(self, token) - - -

    - -
    - -

    (#(\"[^\"]\"|\'[^\']\')|\d\d\d\d-\d\d-\d\d)

    - -
    - Source code in beancount/query/query_parser.py -
    def t_DATE(self, token):
    -    r"(\#(\"[^\"]*\"|\'[^\']*\')|\d\d\d\d-\d\d-\d\d)"
    -    if token.value[0] == '#':
    -        token.value = dateutil.parser.parse(token.value[2:-1]).date()
    -    else:
    -        token.value = datetime.datetime.strptime(token.value, '%Y-%m-%d').date()
    -    return token
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Lexer.t_DECIMAL(self, token) - - -

    - -
    - -

    [-+]?([0-9]+.[0-9]|[0-9].[0-9]+)

    - -
    - Source code in beancount/query/query_parser.py -
    def t_DECIMAL(self, token):
    -    r"[-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)"
    -    token.value = D(token.value)
    -    return token
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Lexer.t_ID(self, token) - - -

    - -
    - -

    [a-zA-Z][a-zA-Z_]*

    - -
    - Source code in beancount/query/query_parser.py -
    def t_ID(self, token):
    -    "[a-zA-Z][a-zA-Z_]*"
    -    utoken = token.value.upper()
    -    if utoken in self.keywords:
    -        token.type = utoken
    -        token.value = token.value.upper()
    -    else:
    -        token.value = token.value.lower()
    -    return token
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Lexer.t_INTEGER(self, token) - - -

    - -
    - -

    [-+]?[0-9]+

    - -
    - Source code in beancount/query/query_parser.py -
    def t_INTEGER(self, token):
    -    r"[-+]?[0-9]+"
    -    token.value = int(token.value)
    -    return token
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Lexer.t_STRING(self, token) - - -

    - -
    - -

    ("[^"]"|'[^']')

    - -
    - Source code in beancount/query/query_parser.py -
    def t_STRING(self, token):
    -    "(\"[^\"]*\"|\'[^\']*\')"
    -    token.value = token.value[1:-1]
    -    return token
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - - - - - -
    - - - -

    - -beancount.query.query_parser.ParseError (Exception) - - - - -

    - -
    - -

    A parser error.

    - - - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_parser.Parser (SelectParser) - - - - -

    - -
    - -

    PLY parser for the Beancount Query Language's full command syntax.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.query.query_parser.Parser.p_balances_statement(self, p) - - -

    - -
    - -

    balances_statement : BALANCES summary_func from where

    - -
    - Source code in beancount/query/query_parser.py -
    def p_balances_statement(self, p):
    -    """
    -    balances_statement : BALANCES summary_func from where
    -    """
    -    p[0] = Balances(p[2], p[3], p[4])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Parser.p_delimiter(self, p) - - -

    - -
    - -

    delimiter : SEMI - | empty

    - -
    - Source code in beancount/query/query_parser.py -
    def p_delimiter(self, p):
    -    """
    -    delimiter : SEMI
    -              | empty
    -    """
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Parser.p_errors_statement(self, p) - - -

    - -
    - -

    errors_statement : ERRORS

    - -
    - Source code in beancount/query/query_parser.py -
    def p_errors_statement(self, p):
    -    """
    -    errors_statement : ERRORS
    -    """
    -    p[0] = Errors()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Parser.p_explain_statement(self, p) - - -

    - -
    - -

    top_statement : EXPLAIN statement delimiter

    - -
    - Source code in beancount/query/query_parser.py -
    def p_explain_statement(self, p):
    -    "top_statement : EXPLAIN statement delimiter"
    -    p[0] = Explain(p[2])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Parser.p_journal_statement(self, p) - - -

    - -
    - -

    journal_statement : JOURNAL summary_func from - | JOURNAL account summary_func from

    - -
    - Source code in beancount/query/query_parser.py -
    def p_journal_statement(self, p):
    -    """
    -    journal_statement : JOURNAL summary_func from
    -                      | JOURNAL account summary_func from
    -    """
    -    p[0] = Journal(None, p[2], p[3]) if len(p) == 4 else Journal(p[2], p[3], p[4])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Parser.p_print_statement(self, p) - - -

    - -
    - -

    print_statement : PRINT from

    - -
    - Source code in beancount/query/query_parser.py -
    def p_print_statement(self, p):
    -    """
    -    print_statement : PRINT from
    -    """
    -    p[0] = Print(p[2])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Parser.p_regular_statement(self, p) - - -

    - -
    - -

    top_statement : statement delimiter

    - -
    - Source code in beancount/query/query_parser.py -
    def p_regular_statement(self, p):
    -    "top_statement : statement delimiter"
    -    p[0] = p[1]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Parser.p_reload_statement(self, p) - - -

    - -
    - -

    reload_statement : RELOAD

    - -
    - Source code in beancount/query/query_parser.py -
    def p_reload_statement(self, p):
    -    """
    -    reload_statement : RELOAD
    -    """
    -    p[0] = Reload()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Parser.p_run_statement(self, p) - - -

    - -
    - -

    run_statement : RUN ID - | RUN STRING - | RUN ASTERISK - | RUN empty

    - -
    - Source code in beancount/query/query_parser.py -
    def p_run_statement(self, p):
    -    """
    -    run_statement : RUN ID
    -                  | RUN STRING
    -                  | RUN ASTERISK
    -                  | RUN empty
    -    """
    -    p[0] = RunCustom(p[2])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Parser.p_statement(self, p) - - -

    - -
    - -

    statement : select_statement - | balances_statement - | journal_statement - | print_statement - | run_statement - | errors_statement - | reload_statement

    - -
    - Source code in beancount/query/query_parser.py -
    def p_statement(self, p):
    -    """
    -    statement : select_statement
    -              | balances_statement
    -              | journal_statement
    -              | print_statement
    -              | run_statement
    -              | errors_statement
    -              | reload_statement
    -    """
    -    p[0] = p[1]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Parser.p_summary_func(self, p) - - -

    - -
    - -

    summary_func : empty - | AT ID

    - -
    - Source code in beancount/query/query_parser.py -
    def p_summary_func(self, p):
    -    """
    -    summary_func : empty
    -                 | AT ID
    -    """
    -    p[0] = p[2] if len(p) == 3 else None
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_parser.Print (tuple) - - - - -

    - -
    - -

    Print(from_clause,)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_parser.Print.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/query/query_parser.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Print.__new__(_cls, from_clause) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of Print(from_clause,)

    - -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Print.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/query/query_parser.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Print._asdict(self) - - - private - - -

    - -
    - -

    Return a new dict which maps field names to their values.

    - -
    - Source code in beancount/query/query_parser.py -
    def _asdict(self):
    -    'Return a new dict which maps field names to their values.'
    -    return _dict(_zip(self._fields, self))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Print._make(iterable) - - - classmethod - private - - -

    - -
    - -

    Make a new Print object from a sequence or iterable

    - -
    - Source code in beancount/query/query_parser.py -
    @classmethod
    -def _make(cls, iterable):
    -    result = tuple_new(cls, iterable)
    -    if _len(result) != num_fields:
    -        raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Print._replace(/, self, **kwds) - - - private - - -

    - -
    - -

    Return a new Print object replacing specified fields with new values

    - -
    - Source code in beancount/query/query_parser.py -
    def _replace(self, /, **kwds):
    -    result = self._make(_map(kwds.pop, field_names, self))
    -    if kwds:
    -        raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
    -    return result
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_parser.Reload (tuple) - - - - -

    - -
    - -

    Reload()

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_parser.Reload.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/query/query_parser.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Reload.__new__(_cls) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of Reload()

    - -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Reload.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/query/query_parser.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Reload._asdict(self) - - - private - - -

    - -
    - -

    Return a new dict which maps field names to their values.

    - -
    - Source code in beancount/query/query_parser.py -
    def _asdict(self):
    -    'Return a new dict which maps field names to their values.'
    -    return _dict(_zip(self._fields, self))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Reload._make(iterable) - - - classmethod - private - - -

    - -
    - -

    Make a new Reload object from a sequence or iterable

    - -
    - Source code in beancount/query/query_parser.py -
    @classmethod
    -def _make(cls, iterable):
    -    result = tuple_new(cls, iterable)
    -    if _len(result) != num_fields:
    -        raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Reload._replace(/, self, **kwds) - - - private - - -

    - -
    - -

    Return a new Reload object replacing specified fields with new values

    - -
    - Source code in beancount/query/query_parser.py -
    def _replace(self, /, **kwds):
    -    result = self._make(_map(kwds.pop, field_names, self))
    -    if kwds:
    -        raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
    -    return result
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_parser.RunCustom (tuple) - - - - -

    - -
    - -

    RunCustom(query_name,)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_parser.RunCustom.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/query/query_parser.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.RunCustom.__new__(_cls, query_name) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of RunCustom(query_name,)

    - -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.RunCustom.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/query/query_parser.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.RunCustom._asdict(self) - - - private - - -

    - -
    - -

    Return a new dict which maps field names to their values.

    - -
    - Source code in beancount/query/query_parser.py -
    def _asdict(self):
    -    'Return a new dict which maps field names to their values.'
    -    return _dict(_zip(self._fields, self))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.RunCustom._make(iterable) - - - classmethod - private - - -

    - -
    - -

    Make a new RunCustom object from a sequence or iterable

    - -
    - Source code in beancount/query/query_parser.py -
    @classmethod
    -def _make(cls, iterable):
    -    result = tuple_new(cls, iterable)
    -    if _len(result) != num_fields:
    -        raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.RunCustom._replace(/, self, **kwds) - - - private - - -

    - -
    - -

    Return a new RunCustom object replacing specified fields with new values

    - -
    - Source code in beancount/query/query_parser.py -
    def _replace(self, /, **kwds):
    -    result = self._make(_map(kwds.pop, field_names, self))
    -    if kwds:
    -        raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
    -    return result
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_parser.Select (tuple) - - - - -

    - -
    - -

    Select(targets, from_clause, where_clause, group_by, order_by, pivot_by, limit, distinct, flatten)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_parser.Select.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/query/query_parser.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Select.__new__(_cls, targets, from_clause, where_clause, group_by, order_by, pivot_by, limit, distinct, flatten) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of Select(targets, from_clause, where_clause, group_by, order_by, pivot_by, limit, distinct, flatten)

    - -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Select.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/query/query_parser.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Select._asdict(self) - - - private - - -

    - -
    - -

    Return a new dict which maps field names to their values.

    - -
    - Source code in beancount/query/query_parser.py -
    def _asdict(self):
    -    'Return a new dict which maps field names to their values.'
    -    return _dict(_zip(self._fields, self))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Select._make(iterable) - - - classmethod - private - - -

    - -
    - -

    Make a new Select object from a sequence or iterable

    - -
    - Source code in beancount/query/query_parser.py -
    @classmethod
    -def _make(cls, iterable):
    -    result = tuple_new(cls, iterable)
    -    if _len(result) != num_fields:
    -        raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
    -    return result
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.Select._replace(/, self, **kwds) - - - private - - -

    - -
    - -

    Return a new Select object replacing specified fields with new values

    - -
    - Source code in beancount/query/query_parser.py -
    def _replace(self, /, **kwds):
    -    result = self._make(_map(kwds.pop, field_names, self))
    -    if kwds:
    -        raise ValueError(f'Got unexpected field names: {list(kwds)!r}')
    -    return result
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.query_parser.SelectParser (Lexer) - - - - -

    - -
    - -

    PLY parser for the Beancount Query Language's SELECT statement.

    - - - - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.handle_comma_separated_list(self, p) - - -

    - -
    - -

    Handle a list of 0, 1 or more comma-separated values.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • p – A grammar object.

    • -
    -
    -
    - Source code in beancount/query/query_parser.py -
    def handle_comma_separated_list(self, p):
    -    """Handle a list of 0, 1 or more comma-separated values.
    -    Args:
    -      p: A grammar object.
    -    """
    -    if len(p) == 2:
    -        return [] if p[1] is None else [p[1]]
    -    else:
    -        return p[1] + [p[3]]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_account(self, p) - - -

    - -
    - -

    account : STRING

    - -
    - Source code in beancount/query/query_parser.py -
    def p_account(self, p):
    -    """
    -    account : STRING
    -    """
    -    p[0] = p[1]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_boolean(self, p) - - -

    - -
    - -

    boolean : TRUE - | FALSE

    - -
    - Source code in beancount/query/query_parser.py -
    def p_boolean(self, p):
    -    """
    -    boolean : TRUE
    -            | FALSE
    -    """
    -    p[0] = (p[1] == 'TRUE')
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_column(self, p) - - -

    - -
    - -

    column : ID

    - -
    - Source code in beancount/query/query_parser.py -
    def p_column(self, p):
    -    """
    -    column : ID
    -    """
    -    p[0] = Column(p[1])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_column_list(self, p) - - -

    - -
    - -

    column_list : column - | column_list COMMA column

    - -
    - Source code in beancount/query/query_parser.py -
    def p_column_list(self, p):
    -    """
    -    column_list : column
    -                | column_list COMMA column
    -    """
    -    p[0] = self.handle_comma_separated_list(p)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_constant(self, p) - - -

    - -
    - -

    constant : NULL - | boolean - | INTEGER - | DECIMAL - | STRING - | DATE

    - -
    - Source code in beancount/query/query_parser.py -
    def p_constant(self, p):
    -    """
    -    constant : NULL
    -             | boolean
    -             | INTEGER
    -             | DECIMAL
    -             | STRING
    -             | DATE
    -    """
    -    p[0] = Constant(p[1] if p[1] != 'NULL' else None)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_distinct(self, p) - - -

    - -
    - -

    distinct : empty - | DISTINCT

    - -
    - Source code in beancount/query/query_parser.py -
    def p_distinct(self, p):
    -    """
    -    distinct : empty
    -             | DISTINCT
    -    """
    -    p[0] = True if p[1] == 'DISTINCT' else None
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_empty(self, _) - - -

    - -
    - -

    empty :

    - -
    - Source code in beancount/query/query_parser.py -
    def p_empty(self, _):
    -    """
    -    empty :
    -    """
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expr_index(self, p) - - -

    - -
    - -

    expr_index : expression - | INTEGER

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expr_index(self, p):
    -    """
    -    expr_index : expression
    -               | INTEGER
    -    """
    -    p[0] = p[1]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expr_index_list(self, p) - - -

    - -
    - -

    expr_index_list : expr_index - | expr_index_list COMMA expr_index

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expr_index_list(self, p):
    -    """
    -    expr_index_list : expr_index
    -                    | expr_index_list COMMA expr_index
    -    """
    -    p[0] = self.handle_comma_separated_list(p)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_add(self, p) - - -

    - -
    - -

    expression : expression PLUS expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_add(self, p):
    -    "expression : expression PLUS expression"
    -    p[0] = Add(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_and(self, p) - - -

    - -
    - -

    expression : expression AND expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_and(self, p):
    -    "expression : expression AND expression"
    -    p[0] = And(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_column(self, p) - - -

    - -
    - -

    expression : column

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_column(self, p):
    -    "expression : column"
    -    p[0] = p[1]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_constant(self, p) - - -

    - -
    - -

    expression : constant

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_constant(self, p):
    -    "expression : constant"
    -    p[0] = p[1]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_contains(self, p) - - -

    - -
    - -

    expression : expression IN expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_contains(self, p):
    -    "expression : expression IN expression"
    -    p[0] = Contains(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_div(self, p) - - -

    - -
    - -

    expression : expression SLASH expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_div(self, p):
    -    "expression : expression SLASH expression"
    -    p[0] = Div(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_eq(self, p) - - -

    - -
    - -

    expression : expression EQ expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_eq(self, p):
    -    "expression : expression EQ expression"
    -    p[0] = Equal(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_function(self, p) - - -

    - -
    - -

    expression : ID LPAREN expression_list_opt RPAREN

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_function(self, p):
    -    "expression : ID LPAREN expression_list_opt RPAREN"
    -    p[0] = Function(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_gt(self, p) - - -

    - -
    - -

    expression : expression GT expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_gt(self, p):
    -    "expression : expression GT expression"
    -    p[0] = Greater(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_gte(self, p) - - -

    - -
    - -

    expression : expression GTE expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_gte(self, p):
    -    "expression : expression GTE expression"
    -    p[0] = GreaterEq(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_list(self, p) - - -

    - -
    - -

    expression_list : expression - | expression_list COMMA expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_list(self, p):
    -    """
    -    expression_list : expression
    -                    | expression_list COMMA expression
    -    """
    -    p[0] = self.handle_comma_separated_list(p)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_list_opt(self, p) - - -

    - -
    - -

    expression_list_opt : empty - | expression - | expression_list COMMA expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_list_opt(self, p):
    -    """
    -    expression_list_opt : empty
    -                        | expression
    -                        | expression_list COMMA expression
    -    """
    -    p[0] = self.handle_comma_separated_list(p)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_lt(self, p) - - -

    - -
    - -

    expression : expression LT expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_lt(self, p):
    -    "expression : expression LT expression"
    -    p[0] = Less(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_lte(self, p) - - -

    - -
    - -

    expression : expression LTE expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_lte(self, p):
    -    "expression : expression LTE expression"
    -    p[0] = LessEq(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_match(self, p) - - -

    - -
    - -

    expression : expression TILDE expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_match(self, p):
    -    "expression : expression TILDE expression"
    -    p[0] = Match(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_mul(self, p) - - -

    - -
    - -

    expression : expression ASTERISK expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_mul(self, p):
    -    "expression : expression ASTERISK expression"
    -    p[0] = Mul(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_ne(self, p) - - -

    - -
    - -

    expression : expression NE expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_ne(self, p):
    -    "expression : expression NE expression"
    -    p[0] = Not(Equal(p[1], p[3]))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_not(self, p) - - -

    - -
    - -

    expression : NOT expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_not(self, p):
    -    "expression : NOT expression"
    -    p[0] = Not(p[2])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_or(self, p) - - -

    - -
    - -

    expression : expression OR expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_or(self, p):
    -    "expression : expression OR expression"
    -    p[0] = Or(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_paren(self, p) - - -

    - -
    - -

    expression : LPAREN expression RPAREN

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_paren(self, p):
    -    "expression : LPAREN expression RPAREN"
    -    p[0] = p[2]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_expression_sub(self, p) - - -

    - -
    - -

    expression : expression MINUS expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_expression_sub(self, p):
    -    "expression : expression MINUS expression"
    -    p[0] = Sub(p[1], p[3])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_flatten(self, p) - - -

    - -
    - -

    flatten : empty - | FLATTEN

    - -
    - Source code in beancount/query/query_parser.py -
    def p_flatten(self, p):
    -    """
    -    flatten : empty
    -            | FLATTEN
    -    """
    -    p[0] = True if p[1] == 'FLATTEN' else None
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_from(self, p) - - -

    - -
    - -

    from : empty - | FROM opt_expression opt_open opt_close opt_clear

    - -
    - Source code in beancount/query/query_parser.py -
    def p_from(self, p):
    -    """
    -    from : empty
    -         | FROM opt_expression opt_open opt_close opt_clear
    -    """
    -    if len(p) != 2:
    -        if all(p[i] is None for i in range(2, 6)):
    -            raise ParseError("Empty FROM expression is not allowed")
    -        p[0] = From(p[2], p[3], p[4], p[5])
    -    else:
    -        p[0] = None
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_from_subselect(self, p) - - -

    - -
    - -

    from_subselect : from - | FROM LPAREN select_statement RPAREN

    - -
    - Source code in beancount/query/query_parser.py -
    def p_from_subselect(self, p):
    -    """
    -    from_subselect : from
    -                   | FROM LPAREN select_statement RPAREN
    -    """
    -    if len(p) == 2:
    -        p[0] = p[1]
    -    else:
    -        p[0] = p[3]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_group_by(self, p) - - -

    - -
    - -

    group_by : empty - | GROUP BY expr_index_list having

    - -
    - Source code in beancount/query/query_parser.py -
    def p_group_by(self, p):
    -    """
    -    group_by : empty
    -             | GROUP BY expr_index_list having
    -    """
    -    p[0] = GroupBy(p[3], p[4]) if len(p) != 2 else None
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_having(self, p) - - -

    - -
    - -

    having : empty - | HAVING expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_having(self, p):
    -    """
    -    having : empty
    -           | HAVING expression
    -    """
    -    p[0] = p[2] if len(p) == 3 else None
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_limit(self, p) - - -

    - -
    - -

    limit : empty - | LIMIT INTEGER

    - -
    - Source code in beancount/query/query_parser.py -
    def p_limit(self, p):
    -    """
    -    limit : empty
    -          | LIMIT INTEGER
    -    """
    -    p[0] = p[2] if len(p) == 3 else None
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_opt_clear(self, p) - - -

    - -
    - -

    opt_clear : empty - | CLEAR

    - -
    - Source code in beancount/query/query_parser.py -
    def p_opt_clear(self, p):
    -    """
    -    opt_clear : empty
    -              | CLEAR
    -    """
    -    p[0] = True if (p[1] == 'CLEAR') else None
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_opt_close(self, p) - - -

    - -
    - -

    opt_close : empty - | CLOSE - | CLOSE ON DATE

    - -
    - Source code in beancount/query/query_parser.py -
    def p_opt_close(self, p):
    -    """
    -    opt_close : empty
    -              | CLOSE
    -              | CLOSE ON DATE
    -    """
    -    p[0] = p[3] if len(p) == 4 else (True
    -                                     if (p[1] == 'CLOSE') else
    -                                     self.default_close_date)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_opt_expression(self, p) - - -

    - -
    - -

    opt_expression : empty - | expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_opt_expression(self, p):
    -    """
    -    opt_expression : empty
    -                   | expression
    -    """
    -    p[0] = p[1]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_opt_open(self, p) - - -

    - -
    - -

    opt_open : empty - | OPEN ON DATE

    - -
    - Source code in beancount/query/query_parser.py -
    def p_opt_open(self, p):
    -    """
    -    opt_open : empty
    -             | OPEN ON DATE
    -    """
    -    p[0] = p[3] if len(p) == 4 else None
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_order_by(self, p) - - -

    - -
    - -

    order_by : empty - | ORDER BY expr_index_list ordering

    - -
    - Source code in beancount/query/query_parser.py -
    def p_order_by(self, p):
    -    """
    -    order_by : empty
    -             | ORDER BY expr_index_list ordering
    -    """
    -    p[0] = None if len(p) == 2 else OrderBy(p[3], p[4])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_ordering(self, p) - - -

    - -
    - -

    ordering : empty - | ASC - | DESC

    - -
    - Source code in beancount/query/query_parser.py -
    def p_ordering(self, p):
    -    """
    -    ordering : empty
    -             | ASC
    -             | DESC
    -    """
    -    p[0] = p[1]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_pivot_by(self, p) - - -

    - -
    - -

    pivot_by : empty - | PIVOT BY column_list

    - -
    - Source code in beancount/query/query_parser.py -
    def p_pivot_by(self, p):
    -    """
    -    pivot_by : empty
    -             | PIVOT BY column_list
    -    """
    -    p[0] = PivotBy(p[3]) if len(p) == 4 else None
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_select_statement(self, p) - - -

    - -
    - -

    select_statement : SELECT distinct target_spec from_subselect where group_by order_by pivot_by limit flatten

    - -
    - Source code in beancount/query/query_parser.py -
    def p_select_statement(self, p):
    -    """
    -    select_statement : SELECT distinct target_spec from_subselect where \
    -                       group_by order_by pivot_by limit flatten
    -    """
    -    p[0] = Select(p[3], p[4], p[5], p[6], p[7], p[8], p[9], p[2], p[10])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_target(self, p) - - -

    - -
    - -

    target : expression AS ID - | expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_target(self, p):
    -    """
    -    target : expression AS ID
    -           | expression
    -    """
    -    p[0] = Target(p[1], p[3] if len(p) == 4 else None)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_target_list(self, p) - - -

    - -
    - -

    target_list : target - | target_list COMMA target

    - -
    - Source code in beancount/query/query_parser.py -
    def p_target_list(self, p):
    -    """
    -    target_list : target
    -                | target_list COMMA target
    -    """
    -    p[0] = self.handle_comma_separated_list(p)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_target_spec(self, p) - - -

    - -
    - -

    target_spec : ASTERISK - | target_list

    - -
    - Source code in beancount/query/query_parser.py -
    def p_target_spec(self, p):
    -    """
    -    target_spec : ASTERISK
    -                | target_list
    -    """
    -    p[0] = Wildcard() if p[1] == '*' else p[1]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser.SelectParser.p_where(self, p) - - -

    - -
    - -

    where : empty - | WHERE expression

    - -
    - Source code in beancount/query/query_parser.py -
    def p_where(self, p):
    -    """
    -    where : empty
    -          | WHERE expression
    -    """
    -    if len(p) == 3:
    -        assert p[2], "Empty WHERE clause is not allowed"
    -        p[0] = p[2]
    -
    -
    -
    - -
    - - - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    -beancount.query.query_parser.get_expression_name(expr) - - -

    - -
    - -

    Come up with a reasonable identifier for an expression.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • expr – An expression node.

    • -
    -
    -
    - Source code in beancount/query/query_parser.py -
    def get_expression_name(expr):
    -    """Come up with a reasonable identifier for an expression.
    -
    -    Args:
    -      expr: An expression node.
    -    """
    -    if isinstance(expr, Column):
    -        return expr.name.lower()
    -
    -    elif isinstance(expr, Function):
    -        names = [expr.fname.lower()]
    -        for operand in expr.operands:
    -            names.append(get_expression_name(operand))
    -        return '_'.join(names)
    -
    -    elif isinstance(expr, Constant):
    -        return 'c{}'.format(re.sub('[^a-z0-9]+', '_', str(expr.value)))
    -
    -    elif isinstance(expr, UnaryOp):
    -        return '_'.join([type(expr).__name__.lower(),
    -                         get_expression_name(expr.operand)])
    -
    -    elif isinstance(expr, BinaryOp):
    -        return '_'.join([type(expr).__name__.lower(),
    -                         get_expression_name(expr.left),
    -                         get_expression_name(expr.right)])
    -
    -    else:
    -        assert False, "Unknown expression type."
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.query.query_parser_test - - - -

    - -
    - - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.query.query_parser_test.QueryParserTestBase (TestCase) - - - - -

    - -
    - - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.query.query_parser_test.QueryParserTestBase.assertParse(self, expected, query, debug=False) - - -

    - -
    - -

    Assert parsed contents from 'query' is 'expected'.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • expected – An expected AST to compare against the parsed value.

    • -
    • query – An SQL query to be parsed.

    • -
    • debug – A boolean, if true, print extra debugging information on the console.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • AssertionError – If the actual AST does not match the expected one.

    • -
    -
    -
    - Source code in beancount/query/query_parser_test.py -
    def assertParse(self, expected, query, debug=False):
    -    """Assert parsed contents from 'query' is 'expected'.
    -
    -    Args:
    -      expected: An expected AST to compare against the parsed value.
    -      query: An SQL query to be parsed.
    -      debug: A boolean, if true, print extra debugging information on the console.
    -    Raises:
    -      AssertionError: If the actual AST does not match the expected one.
    -    """
    -    actual = self.parse(query)
    -    if debug:
    -        print()
    -        print()
    -        print(actual)
    -        print()
    -    self.assertEqual(expected, actual)
    -    return actual
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser_test.QueryParserTestBase.parse(self, query) - - -

    - -
    - -

    Parse one query.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • query – An SQL query to be parsed.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The AST.

    • -
    -
    -
    - Source code in beancount/query/query_parser_test.py -
    def parse(self, query):
    -    """Parse one query.
    -
    -    Args:
    -      query: An SQL query to be parsed.
    -    Returns:
    -      The AST.
    -    """
    -    return self.parser.parse(query.strip())
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.query_parser_test.QueryParserTestBase.setUp(self) - - -

    - -
    - -

    Hook method for setting up the test fixture before exercising it.

    - -
    - Source code in beancount/query/query_parser_test.py -
    def setUp(self):
    -    self.parser = qp.Parser()
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - - - - - -
    - - - -

    - -beancount.query.query_parser_test.TestSelectFromBase (QueryParserTestBase) - - - - -

    - -
    - - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.query_parser_test.TestSelectFromBase.setUp(self) - - -

    - -
    - -

    Hook method for setting up the test fixture before exercising it.

    - -
    - Source code in beancount/query/query_parser_test.py -
    def setUp(self):
    -    super().setUp()
    -    self.targets = [qp.Target(qp.Column('a'), None),
    -                    qp.Target(qp.Column('b'), None)]
    -    self.expr = qp.Equal(qp.Column('d'),
    -                         qp.And(
    -                             qp.Function('max', [qp.Column('e')]),
    -                             qp.Constant(17)))
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.query.query_parser_test.qSelect(target_spec=None, from_clause=None, where_clause=None, group_by=None, order_by=None, pivot_by=None, limit=None, distinct=None, flatten=None) - - -

    - -
    - -

    A convenience constructor for writing tests without having to provide all arguments.

    - -
    - Source code in beancount/query/query_parser_test.py -
    def qSelect(target_spec=None,
    -            from_clause=None, where_clause=None,
    -            group_by=None, order_by=None, pivot_by=None,
    -            limit=None, distinct=None, flatten=None):
    -    "A convenience constructor for writing tests without having to provide all arguments."
    -    return qp.Select(target_spec,
    -                     from_clause, where_clause,
    -                     group_by, order_by, pivot_by,
    -                     limit, distinct, flatten)
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - - -
    - - - -

    - beancount.query.shell - - - -

    - -
    - -

    An interactive command-line shell interpreter for the Beancount Query Language.

    - - - -
    - - - - - - - - - - - -
    - - - -

    - -beancount.query.shell.BQLShell (DispatchingShell) - - - - -

    - -
    - -

    An interactive shell interpreter for the Beancount query language.

    - - - - -
    - - - - - - - - - - - - - - - -
    - - - -

    -beancount.query.shell.BQLShell.on_Balances(self, balance) - - -

    - -
    - -

    Select balances of some subset of postings. This command is a -convenience and converts into an equivalent Select statement, designed -to extract the most sensible list of columns for the register of a list -of entries as a table.

    -

    The general form of a JOURNAL statement loosely follows SQL syntax:

    -

    BALANCE [FROM_CLAUSE]

    -

    See the SELECT query help for more details on the FROM clause.

    - -
    - Source code in beancount/query/shell.py -
    def on_Balances(self, balance):
    -    """
    -    Select balances of some subset of postings. This command is a
    -    convenience and converts into an equivalent Select statement, designed
    -    to extract the most sensible list of columns for the register of a list
    -    of entries as a table.
    -
    -    The general form of a JOURNAL statement loosely follows SQL syntax:
    -
    -       BALANCE [FROM_CLAUSE]
    -
    -    See the SELECT query help for more details on the FROM clause.
    -    """
    -    return self.on_Select(balance)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.BQLShell.on_Errors(self, errors_statement) - - -

    - -
    - -

    Print the errors that occurred during parsing.

    - -
    - Source code in beancount/query/shell.py -
    def on_Errors(self, errors_statement):
    -    """
    -    Print the errors that occurred during parsing.
    -    """
    -    if self.errors:
    -        printer.print_errors(self.errors)
    -    else:
    -        print('(No errors)', file=self.outfile)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.BQLShell.on_Explain(self, explain) - - -

    - -
    - -

    Compile and print a compiled statement for debugging.

    - -
    - Source code in beancount/query/shell.py -
    def on_Explain(self, explain):
    -    """
    -    Compile and print a compiled statement for debugging.
    -    """
    -    # pylint: disable=invalid-name
    -    pr = lambda *args: print(*args, file=self.outfile)
    -    pr("Parsed statement:")
    -    pr("  {}".format(explain.statement))
    -    pr()
    -
    -    # Compile the select statement and print it uot.
    -    try:
    -        query = query_compile.compile(explain.statement,
    -                                      self.env_targets,
    -                                      self.env_postings,
    -                                      self.env_entries)
    -    except query_compile.CompilationError as exc:
    -        pr(str(exc).rstrip('.'))
    -        return
    -
    -    pr("Compiled query:")
    -    pr("  {}".format(query))
    -    pr()
    -    pr("Targets:")
    -    for c_target in query.c_targets:
    -        pr("  '{}'{}: {}".format(
    -            c_target.name or '(invisible)',
    -            ' (aggregate)' if query_compile.is_aggregate(c_target.c_expr) else '',
    -            c_target.c_expr.dtype.__name__))
    -    pr()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.BQLShell.on_Journal(self, journal) - - -

    - -
    - -

    Select a journal of some subset of postings. This command is a -convenience and converts into an equivalent Select statement, designed -to extract the most sensible list of columns for the register of a list -of entries as a table.

    -

    The general form of a JOURNAL statement loosely follows SQL syntax:

    -

    JOURNAL <account-regexp> [FROM_CLAUSE]

    -

    See the SELECT query help for more details on the FROM clause.

    - -
    - Source code in beancount/query/shell.py -
    def on_Journal(self, journal):
    -    """
    -    Select a journal of some subset of postings. This command is a
    -    convenience and converts into an equivalent Select statement, designed
    -    to extract the most sensible list of columns for the register of a list
    -    of entries as a table.
    -
    -    The general form of a JOURNAL statement loosely follows SQL syntax:
    -
    -       JOURNAL <account-regexp> [FROM_CLAUSE]
    -
    -    See the SELECT query help for more details on the FROM clause.
    -    """
    -    return self.on_Select(journal)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.BQLShell.on_Print(self, print_stmt) - - -

    - -
    - -

    Print entries in Beancount format.

    -

    The general form of a PRINT statement includes an SQL-like FROM -selector:

    -

    PRINT [FROM <from_expr> ...]

    -

    Where:

    -

    from_expr: A logical expression that matches on the attributes of - the directives. See SELECT command for details (this FROM expression - supports all the same expressions including its OPEN, CLOSE and - CLEAR operations).

    - -
    - Source code in beancount/query/shell.py -
    def on_Print(self, print_stmt):
    -    """
    -    Print entries in Beancount format.
    -
    -    The general form of a PRINT statement includes an SQL-like FROM
    -    selector:
    -
    -       PRINT [FROM <from_expr> ...]
    -
    -    Where:
    -
    -      from_expr: A logical expression that matches on the attributes of
    -        the directives. See SELECT command for details (this FROM expression
    -        supports all the same expressions including its OPEN, CLOSE and
    -        CLEAR operations).
    -
    -    """
    -    # Compile the print statement.
    -    try:
    -        c_print = query_compile.compile(print_stmt,
    -                                        self.env_targets,
    -                                        self.env_postings,
    -                                        self.env_entries)
    -    except query_compile.CompilationError as exc:
    -        print('ERROR: {}.'.format(str(exc).rstrip('.')), file=self.outfile)
    -        return
    -
    -    if self.outfile is sys.stdout:
    -        query_execute.execute_print(c_print, self.entries, self.options_map,
    -                                    file=self.outfile)
    -    else:
    -        with self.get_pager() as file:
    -            query_execute.execute_print(c_print, self.entries, self.options_map, file)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.BQLShell.on_Reload(self, unused_statement=None) - - -

    - -
    - -

    Reload the input file without restarting the shell.

    - -
    - Source code in beancount/query/shell.py -
    def on_Reload(self, unused_statement=None):
    -    """
    -    Reload the input file without restarting the shell.
    -    """
    -    self.entries, self.errors, self.options_map = self.loadfun()
    -    if self.is_interactive:
    -        print_statistics(self.entries, self.options_map, self.outfile)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.BQLShell.on_RunCustom(self, run_stmt) - - -

    - -
    - -

    Run a custom query instead of a SQL command.

    -

    RUN <custom-query-name>

    -

    Where:

    -

    custom-query-name: Should be the name of a custom query to be defined - in the Beancount input file.

    - -
    - Source code in beancount/query/shell.py -
    def on_RunCustom(self, run_stmt):
    -    """
    -    Run a custom query instead of a SQL command.
    -
    -       RUN <custom-query-name>
    -
    -    Where:
    -
    -      custom-query-name: Should be the name of a custom query to be defined
    -        in the Beancount input file.
    -
    -    """
    -    custom_query_map = create_custom_query_map(self.entries)
    -    name = run_stmt.query_name
    -    if name is None:
    -        # List the available queries.
    -        for name in sorted(custom_query_map):
    -            print(name)
    -    elif name == "*":
    -        for name, query in sorted(custom_query_map.items()):
    -            print('{}:'.format(name))
    -            self.run_parser(query.query_string, default_close_date=query.date)
    -            print()
    -            print()
    -    else:
    -        query = None
    -        if name in custom_query_map:
    -            query = custom_query_map[name]
    -        else:  # lookup best query match using name as prefix
    -            queries = [q for q in custom_query_map if q.startswith(name)]
    -            if len(queries) == 1:
    -                name = queries[0]
    -                query = custom_query_map[name]
    -        if query:
    -            statement = self.parser.parse(query.query_string)
    -            self.dispatch(statement)
    -        else:
    -            print("ERROR: Query '{}' not found".format(name))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.BQLShell.on_Select(self, statement) - - -

    - -
    - -

    Extract data from a query on the postings.

    -

    The general form of a SELECT statement loosely follows SQL syntax, with -some mild and idiomatic extensions:

    -

    SELECT [DISTINCT][<targets>|*] - [FROM <from_expr> [OPEN ON <date>] [CLOSE [ON <date>]] [CLEAR]] - [WHERE <where_expr>] - [GROUP BY <groups>] - [ORDER BY <groups> [ASC|DESC]] - [LIMIT num]

    -

    Where:

    -

    targets: A list of desired output attributes from the postings, and - expressions on them. Some of the attributes of the parent transaction - directive are made available in this context as well. Simple functions - (that return a single value per row) and aggregation functions (that - return a single value per group) are available. For the complete - list of supported columns and functions, see help on "targets". - You can also provide a wildcard here, which will select a reasonable - default set of columns for rendering a journal.

    -

    from_expr: A logical expression that matches on the attributes of - the directives (not postings). This allows you to select a subset of - transactions, so the accounting equation is respected for balance - reports. For the complete list of supported columns and functions, - see help on "from".

    -

    where_expr: A logical expression that matches on the attributes of - postings. The available columns are similar to those in the targets - clause, without the aggregation functions.

    -

    OPEN clause: replace all the transactions before the given date by - summarizing entries and transfer Income and Expenses balances to - Equity.

    -

    CLOSE clause: Remove all the transactions after the given date and

    -

    CLEAR: Transfer final Income and Expenses balances to Equity.

    - -
    - Source code in beancount/query/shell.py -
    def on_Select(self, statement):
    -    """
    -    Extract data from a query on the postings.
    -
    -    The general form of a SELECT statement loosely follows SQL syntax, with
    -    some mild and idiomatic extensions:
    -
    -       SELECT [DISTINCT] [<targets>|*]
    -       [FROM <from_expr> [OPEN ON <date>] [CLOSE [ON <date>]] [CLEAR]]
    -       [WHERE <where_expr>]
    -       [GROUP BY <groups>]
    -       [ORDER BY <groups> [ASC|DESC]]
    -       [LIMIT num]
    -
    -    Where:
    -
    -      targets: A list of desired output attributes from the postings, and
    -        expressions on them. Some of the attributes of the parent transaction
    -        directive are made available in this context as well. Simple functions
    -        (that return a single value per row) and aggregation functions (that
    -        return a single value per group) are available. For the complete
    -        list of supported columns and functions, see help on "targets".
    -        You can also provide a wildcard here, which will select a reasonable
    -        default set of columns for rendering a journal.
    -
    -      from_expr: A logical expression that matches on the attributes of
    -        the directives (not postings). This allows you to select a subset of
    -        transactions, so the accounting equation is respected for balance
    -        reports. For the complete list of supported columns and functions,
    -        see help on "from".
    -
    -      where_expr: A logical expression that matches on the attributes of
    -        postings. The available columns are similar to those in the targets
    -        clause, without the aggregation functions.
    -
    -      OPEN clause: replace all the transactions before the given date by
    -        summarizing entries and transfer Income and Expenses balances to
    -        Equity.
    -
    -      CLOSE clause: Remove all the transactions after the given date and
    -
    -      CLEAR: Transfer final Income and Expenses balances to Equity.
    -
    -    """
    -    # Compile the SELECT statement.
    -    try:
    -        c_query = query_compile.compile(statement,
    -                                        self.env_targets,
    -                                        self.env_postings,
    -                                        self.env_entries)
    -    except query_compile.CompilationError as exc:
    -        print('ERROR: {}.'.format(str(exc).rstrip('.')), file=self.outfile)
    -        return
    -
    -    # Execute it to obtain the result rows.
    -    rtypes, rrows = query_execute.execute_query(c_query,
    -                                                self.entries,
    -                                                self.options_map)
    -
    -    # Output the resulting rows.
    -    if not rrows:
    -        print("(empty)", file=self.outfile)
    -    else:
    -        output_format = self.vars['format']
    -        if output_format == 'text':
    -            kwds = dict(boxed=self.vars['boxed'],
    -                        spaced=self.vars['spaced'],
    -                        expand=self.vars['expand'])
    -            if self.outfile is sys.stdout:
    -                with self.get_pager() as file:
    -                    query_render.render_text(rtypes, rrows,
    -                                             self.options_map['dcontext'],
    -                                             file,
    -                                             **kwds)
    -            else:
    -                query_render.render_text(rtypes, rrows,
    -                                         self.options_map['dcontext'],
    -                                         self.outfile,
    -                                         **kwds)
    -
    -        elif output_format == 'csv':
    -            # Numberify CSV output if requested.
    -            if self.vars['numberify']:
    -                dformat = self.options_map['dcontext'].build()
    -                rtypes, rrows = numberify.numberify_results(rtypes, rrows, dformat)
    -
    -            query_render.render_csv(rtypes, rrows,
    -                                    self.options_map['dcontext'],
    -                                    self.outfile,
    -                                    expand=self.vars['expand'])
    -
    -        else:
    -            assert output_format not in _SUPPORTED_FORMATS
    -            print("Unsupported output format: '{}'.".format(output_format),
    -                  file=self.outfile)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.shell.DispatchingShell (Cmd) - - - - -

    - -
    - -

    A usable convenient shell for interpreting commands, with history.

    - - - - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.__init__(self, is_interactive, parser, outfile, default_format, do_numberify) - - - special - - -

    - -
    - -

    Create a shell with history.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • is_interactive – A boolean, true if this serves an interactive tty.

    • -
    • parser – A command parser.

    • -
    • outfile – An output file object to write communications to.

    • -
    • default_format – A string, the default output format.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def __init__(self, is_interactive, parser, outfile, default_format, do_numberify):
    -    """Create a shell with history.
    -
    -    Args:
    -      is_interactive: A boolean, true if this serves an interactive tty.
    -      parser: A command parser.
    -      outfile: An output file object to write communications to.
    -      default_format: A string, the default output format.
    -    """
    -    super().__init__()
    -    if is_interactive and readline is not None:
    -        load_history(path.expanduser(HISTORY_FILENAME))
    -    self.is_interactive = is_interactive
    -    self.parser = parser
    -    self.initialize_vars(default_format, do_numberify)
    -    self.add_help()
    -    self.outfile = outfile
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.add_help(self) - - -

    - -
    - -

    Attach help functions for each of the parsed token handlers.

    - -
    - Source code in beancount/query/shell.py -
    def add_help(self):
    -    "Attach help functions for each of the parsed token handlers."
    -    for attrname, func in list(self.__class__.__dict__.items()):
    -        match = re.match('on_(.*)', attrname)
    -        if not match:
    -            continue
    -        command_name = match.group(1)
    -        setattr(self.__class__, 'help_{}'.format(command_name.lower()),
    -                lambda _, fun=func: print(textwrap.dedent(fun.__doc__).strip(),
    -                                          file=self.outfile))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.cmdloop(self) - - -

    - -
    - -

    Override cmdloop to handle keyboard interrupts.

    - -
    - Source code in beancount/query/shell.py -
    def cmdloop(self):
    -    """Override cmdloop to handle keyboard interrupts."""
    -    while True:
    -        try:
    -            super().cmdloop()
    -            break
    -        except KeyboardInterrupt:
    -            print('\n(Interrupted)', file=self.outfile)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.default(self, line) - - -

    - -
    - -

    Default handling of lines which aren't recognized as native shell commands.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • line – The string to be parsed.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def default(self, line):
    -    """Default handling of lines which aren't recognized as native shell commands.
    -
    -    Args:
    -      line: The string to be parsed.
    -    """
    -    self.run_parser(line)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.dispatch(self, statement) - - -

    - -
    - -

    Dispatch the given statement to a suitable method.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • statement – An instance provided by the parser.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • Whatever the invoked method happens to return.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def dispatch(self, statement):
    -    """Dispatch the given statement to a suitable method.
    -
    -    Args:
    -      statement: An instance provided by the parser.
    -    Returns:
    -      Whatever the invoked method happens to return.
    -    """
    -    try:
    -        method = getattr(self, 'on_{}'.format(type(statement).__name__))
    -    except AttributeError:
    -        print("Internal error: statement '{}' is unsupported.".format(statement),
    -              file=self.outfile)
    -    else:
    -        return method(statement)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.do_EOF(self, _) - - -

    - -
    - -

    Exit the parser.

    - -
    - Source code in beancount/query/shell.py -
    def exit(self, _):
    -    """Exit the parser."""
    -    print('exit', file=self.outfile)
    -    return 1
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.do_clear(self, _) - - -

    - -
    - -

    Clear the history.

    - -
    - Source code in beancount/query/shell.py -
    def do_clear(self, _):
    -    "Clear the history."
    -    readline.clear_history()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.do_exit(self, _) - - -

    - -
    - -

    Exit the parser.

    - -
    - Source code in beancount/query/shell.py -
    def exit(self, _):
    -    """Exit the parser."""
    -    print('exit', file=self.outfile)
    -    return 1
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.do_help(self, command) - - -

    - -
    - -

    Strip superfluous semicolon.

    - -
    - Source code in beancount/query/shell.py -
    def do_help(self, command):
    -    """Strip superfluous semicolon."""
    -    super().do_help(command.rstrip('; \t'))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.do_history(self, _) - - -

    - -
    - -

    Print the command-line history statement.

    - -
    - Source code in beancount/query/shell.py -
    def do_history(self, _):
    -    "Print the command-line history statement."
    -    if readline is not None:
    -        for index, line in enumerate(get_history(self.max_entries)):
    -            print(line, file=self.outfile)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.do_lex(self, line) - - -

    - -
    - -

    Just run the lexer on the following command and print the output.

    - -
    - Source code in beancount/query/shell.py -
    def do_lex(self, line):
    -    "Just run the lexer on the following command and print the output."
    -    try:
    -        self.parser.tokenize(line)
    -    except query_parser.ParseError as exc:
    -        print(exc, file=self.outfile)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.do_parse(self, line) - - -

    - -
    - -

    Just run the parser on the following command and print the output.

    - -
    - Source code in beancount/query/shell.py -
    def do_parse(self, line):
    -    "Just run the parser on the following command and print the output."
    -    print("INPUT: {}".format(repr(line)), file=self.outfile)
    -    try:
    -        statement = self.parser.parse(line, True)
    -        print(statement, file=self.outfile)
    -    except (query_parser.ParseError,
    -            query_compile.CompilationError) as exc:
    -        print(exc, file=self.outfile)
    -    except Exception as exc:
    -        traceback.print_exc(file=self.outfile)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.do_quit(self, _) - - -

    - -
    - -

    Exit the parser.

    - -
    - Source code in beancount/query/shell.py -
    def exit(self, _):
    -    """Exit the parser."""
    -    print('exit', file=self.outfile)
    -    return 1
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.do_set(self, line) - - -

    - -
    - -

    Get/set shell settings variables.

    - -
    - Source code in beancount/query/shell.py -
    def do_set(self, line):
    -    "Get/set shell settings variables."
    -    if not line:
    -        for varname, value in sorted(self.vars.items()):
    -            print('{}: {}'.format(varname, value), file=self.outfile)
    -    else:
    -        components = shlex.split(line)
    -        varname = components[0]
    -        if len(components) == 1:
    -            try:
    -                value = self.vars[varname]
    -                print('{}: {}'.format(varname, value), file=self.outfile)
    -            except KeyError:
    -                print("Variable '{}' does not exist.".format(varname),
    -                      file=self.outfile)
    -        elif len(components) == 2:
    -            value = components[1]
    -            try:
    -                converted_value = self.vars_types[varname](value)
    -                self.vars[varname] = converted_value
    -                print('{}: {}'.format(varname, converted_value), file=self.outfile)
    -            except KeyError:
    -                print("Variable '{}' does not exist.".format(varname),
    -                      file=self.outfile)
    -        else:
    -            print("Invalid number of arguments.", file=self.outfile)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.do_tokenize(self, line) - - -

    - -
    - -

    Just run the lexer on the following command and print the output.

    - -
    - Source code in beancount/query/shell.py -
    def do_lex(self, line):
    -    "Just run the lexer on the following command and print the output."
    -    try:
    -        self.parser.tokenize(line)
    -    except query_parser.ParseError as exc:
    -        print(exc, file=self.outfile)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.emptyline(self) - - -

    - -
    - -

    Do nothing on an empty line.

    - -
    - Source code in beancount/query/shell.py -
    def emptyline(self):
    -    """Do nothing on an empty line."""
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.exit(self, _) - - -

    - -
    - -

    Exit the parser.

    - -
    - Source code in beancount/query/shell.py -
    def exit(self, _):
    -    """Exit the parser."""
    -    print('exit', file=self.outfile)
    -    return 1
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.get_pager(self) - - -

    - -
    - -

    Create and return a context manager to write to, a pager subprocess if required.

    - - - - - - - - - - - - -
    Returns: -
      -
    • A pair of a file object to write to, and a pipe object to wait on (or

    • -
    -

    None if not necessary to wait).

    - -
    - Source code in beancount/query/shell.py -
    def get_pager(self):
    -    """Create and return a context manager to write to, a pager subprocess if required.
    -
    -    Returns:
    -      A pair of a file object to write to, and a pipe object to wait on (or
    -    None if not necessary to wait).
    -    """
    -    if self.is_interactive:
    -        return pager.ConditionalPager(self.vars.get('pager', None),
    -                                      minlines=misc_utils.get_screen_height())
    -    else:
    -        file = (codecs.getwriter("utf-8")(sys.stdout.buffer)
    -                if hasattr(sys.stdout, 'buffer') else
    -                sys.stdout)
    -        return pager.flush_only(file)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.initialize_vars(self, default_format, do_numberify) - - -

    - -
    - -

    Initialize the setting variables of the interactive shell.

    - -
    - Source code in beancount/query/shell.py -
    def initialize_vars(self, default_format, do_numberify):
    -    """Initialize the setting variables of the interactive shell."""
    -    self.vars_types = {
    -        'pager': str,
    -        'format': str,
    -        'boxed': convert_bool,
    -        'spaced': convert_bool,
    -        'expand': convert_bool,
    -        'numberify': convert_bool,
    -        }
    -    self.vars = {
    -        'pager': os.environ.get('PAGER', None),
    -        'format': default_format,
    -        'boxed': False,
    -        'spaced': False,
    -        'expand': False,
    -        'numberify': do_numberify,
    -        }
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.DispatchingShell.run_parser(self, line, default_close_date=None) - - -

    - -
    - -

    Handle statements via our parser instance and dispatch to appropriate methods.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • line – The string to be parsed.

    • -
    • default_close_date – A datetimed.date instance, the default close date.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def run_parser(self, line, default_close_date=None):
    -    """Handle statements via our parser instance and dispatch to appropriate methods.
    -
    -    Args:
    -      line: The string to be parsed.
    -      default_close_date: A datetimed.date instance, the default close date.
    -    """
    -    try:
    -        statement = self.parser.parse(line,
    -                                      default_close_date=default_close_date)
    -        self.dispatch(statement)
    -    except query_parser.ParseError as exc:
    -        print(exc, file=self.outfile)
    -    except Exception as exc:
    -        traceback.print_exc(file=self.outfile)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.query.shell.convert_bool(string) - - -

    - -
    - -

    Convert a string to a boolean.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • string – A string representing a boolean.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The corresponding boolean.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def convert_bool(string):
    -    """Convert a string to a boolean.
    -
    -    Args:
    -      string: A string representing a boolean.
    -    Returns:
    -      The corresponding boolean.
    -    """
    -    return not string.lower() in ('f', 'false', '0')
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.create_custom_query_map(entries) - - -

    - -
    - -

    Extract a mapping of the custom queries from the list of entries.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of entries.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A map of query-name strings to Query directives.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def create_custom_query_map(entries):
    -    """Extract a mapping of the custom queries from the list of entries.
    -
    -    Args:
    -      entries: A list of entries.
    -    Returns:
    -      A map of query-name strings to Query directives.
    -    """
    -    query_map = {}
    -    for entry in entries:
    -        if not isinstance(entry, data.Query):
    -            continue
    -        if entry.name in query_map:
    -            logging.warning("Duplicate query: %s", entry.name)
    -        query_map[entry.name] = entry
    -    return query_map
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.generate_env_attribute_list(env) - - -

    - -
    - -

    Generate a dictionary of rendered attribute lists for help.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • env – An instance of an environment.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A dict with keys 'columns', 'functions' and 'aggregates' to rendered -and formatted strings.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def generate_env_attribute_list(env):
    -    """Generate a dictionary of rendered attribute lists for help.
    -
    -    Args:
    -      env: An instance of an environment.
    -    Returns:
    -      A dict with keys 'columns', 'functions' and 'aggregates' to rendered
    -      and formatted strings.
    -    """
    -    wrapper = textwrap.TextWrapper(initial_indent='  ',
    -                                   subsequent_indent='    ',
    -                                   drop_whitespace=True,
    -                                   width=80)
    -
    -    str_columns = generate_env_attributes(
    -        wrapper, env.columns)
    -    str_simple = generate_env_attributes(
    -        wrapper, env.functions,
    -        lambda node: not issubclass(node, query_compile.EvalAggregator))
    -    str_aggregate = generate_env_attributes(
    -        wrapper, env.functions,
    -        lambda node: issubclass(node, query_compile.EvalAggregator))
    -
    -    return dict(columns=str_columns,
    -                functions=str_simple,
    -                aggregates=str_aggregate)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.generate_env_attributes(wrapper, field_dict, filter_pred=None) - - -

    - -
    - -

    Generate a string of all the help functions of the attributes.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • wrapper – A TextWrapper instance to format the paragraphs.

    • -
    • field_dict – A dict of the field-names to the node instances, fetch from an -environment.

    • -
    • filter_pred – A predicate to filter the desired columns. This is applied to -the evaluator node instances.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A formatted multiline string, ready for insertion in a help text.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def generate_env_attributes(wrapper, field_dict, filter_pred=None):
    -    """Generate a string of all the help functions of the attributes.
    -
    -    Args:
    -      wrapper: A TextWrapper instance to format the paragraphs.
    -      field_dict: A dict of the field-names to the node instances, fetch from an
    -        environment.
    -      filter_pred: A predicate to filter the desired columns. This is applied to
    -        the evaluator node instances.
    -    Returns:
    -      A formatted multiline string, ready for insertion in a help text.
    -    """
    -    # Expand the name if its key has argument types.
    -    #
    -    # FIXME: Render the __intypes__ here nicely instead of the key.
    -    flat_items = []
    -    for name, column_cls in field_dict.items():
    -        if isinstance(name, tuple):
    -            name = name[0]
    -
    -        if issubclass(column_cls, query_compile.EvalFunction):
    -            name = name.upper()
    -            args = []
    -            for dtypes in column_cls.__intypes__:
    -                if isinstance(dtypes, (tuple, list)):
    -                    arg = '|'.join(dtype.__name__ for dtype in dtypes)
    -                else:
    -                    arg = dtypes.__name__
    -                args.append(arg)
    -            name = "{}({})".format(name, ','.join(args))
    -
    -        flat_items.append((name, column_cls))
    -
    -    # Render each of the attributes.
    -    oss = io.StringIO()
    -    for name, column_cls in sorted(flat_items):
    -        if filter_pred and not filter_pred(column_cls):
    -            continue
    -        docstring = column_cls.__doc__ or "[See class {}]".format(column_cls.__name__)
    -        if issubclass(column_cls, query_compile.EvalColumn):
    -            docstring += " Type: {}.".format(column_cls().dtype.__name__)
    -            # if hasattr(column_cls, '__equivalent__'):
    -            #     docstring += " Attribute:{}.".format(column_cls.__equivalent__)
    -
    -        text = re.sub('[ \t]+', ' ', docstring.strip().replace('\n', ' '))
    -        doc = "'{}': {}".format(name, text)
    -        oss.write(wrapper.fill(doc))
    -        oss.write('\n')
    -
    -    return oss.getvalue().rstrip()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.get_history(max_entries) - - -

    - -
    - -

    Return the history in the readline buffer.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • max_entries – An integer, the maximum number of entries to return.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of string, the previous history of commands.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def get_history(max_entries):
    -    """Return the history in the readline buffer.
    -
    -    Args:
    -      max_entries: An integer, the maximum number of entries to return.
    -    Returns:
    -      A list of string, the previous history of commands.
    -    """
    -    num_entries = readline.get_current_history_length()
    -    assert num_entries >= 0
    -    start = max(0, num_entries - max_entries)
    -    return [readline.get_history_item(index+1)
    -            for index in range(start, num_entries)]
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.load_history(filename) - - -

    - -
    - -

    Load the shell's past history.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the name of the file containing the shell history.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def load_history(filename):
    -    """Load the shell's past history.
    -
    -    Args:
    -      filename: A string, the name of the file containing the shell history.
    -    """
    -    readline.parse_and_bind("tab:complete")
    -    if hasattr(readline, "read_history_file"):
    -        try:
    -            readline.read_history_file(filename)
    -        except IOError:
    -            # Don't error on absent file.
    -            pass
    -        atexit.register(save_history, filename)
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.query.shell.print_statistics(entries, options_map, outfile) - - -

    - -
    - -

    Print summary statistics to stdout.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • options_map – An options map. as produced by the parser.

    • -
    • outfile – A file object to write to.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def print_statistics(entries, options_map, outfile):
    -    """Print summary statistics to stdout.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: An options map. as produced by the parser.
    -      outfile: A file object to write to.
    -    """
    -    num_directives, num_transactions, num_postings = summary_statistics(entries)
    -    if 'title' in options_map:
    -        print('Input file: "{}"'.format(options_map['title']), file=outfile)
    -    print("Ready with {} directives ({} postings in {} transactions).".format(
    -        num_directives, num_postings, num_transactions),
    -          file=outfile)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.save_history(filename) - - -

    - -
    - -

    Save the shell history. This should be invoked on exit.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the name of the file to save the history to.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def save_history(filename):
    -    """Save the shell history. This should be invoked on exit.
    -
    -    Args:
    -      filename: A string, the name of the file to save the history to.
    -    """
    -    readline.write_history_file(filename)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.query.shell.summary_statistics(entries) - - -

    - -
    - -

    Calculate basic summary statistics to output a brief welcome message.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of three integers, the total number of directives parsed, the total number -of transactions and the total number of postings there in.

    • -
    -
    -
    - Source code in beancount/query/shell.py -
    def summary_statistics(entries):
    -    """Calculate basic summary statistics to output a brief welcome message.
    -
    -    Args:
    -      entries: A list of directives.
    -    Returns:
    -      A tuple of three integers, the total number of directives parsed, the total number
    -      of transactions and the total number of postings there in.
    -    """
    -    num_directives = len(entries)
    -    num_transactions = 0
    -    num_postings = 0
    -    for entry in entries:
    -        if isinstance(entry, data.Transaction):
    -            num_transactions += 1
    -            num_postings += len(entry.postings)
    -    return (num_directives, num_transactions, num_postings)
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.query.shell_test - - - -

    - -
    - - - - -
    - - - - - - - - - - - - - - -
    - - - -

    - -beancount.query.shell_test.TestShell (TestCase) - - - - -

    - -
    - - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.query.shell_test.TestShell.test_success(self, filename) - - -

    - -
    - -

    2013-01-01 open Assets:Account1 -2013-01-01 open Assets:Account2 -2013-01-01 open Assets:Account3 -2013-01-01 open Equity:Unknown

    -

    2013-04-05 * - Equity:Unknown - Assets:Account1 5000 USD

    -

    2013-04-05 * - Assets:Account1 -3000 USD - Assets:Account2 30 BOOG {100 USD}

    -

    2013-04-05 * - Assets:Account1 -1000 USD - Assets:Account3 800 EUR @ 1.25 USD

    - -
    - Source code in beancount/query/shell_test.py -
    @test_utils.docfile
    -def test_success(self, filename):
    -    """
    -    2013-01-01 open Assets:Account1
    -    2013-01-01 open Assets:Account2
    -    2013-01-01 open Assets:Account3
    -    2013-01-01 open Equity:Unknown
    -
    -    2013-04-05 *
    -      Equity:Unknown
    -      Assets:Account1     5000 USD
    -
    -    2013-04-05 *
    -      Assets:Account1     -3000 USD
    -      Assets:Account2        30 BOOG {100 USD}
    -
    -    2013-04-05 *
    -      Assets:Account1     -1000 USD
    -      Assets:Account3       800 EUR @ 1.25 USD
    -    """
    -    with test_utils.capture('stdout', 'stderr') as (stdout, _):
    -        test_utils.run_with_args(shell.main, [filename, "SELECT 1;"])
    -    self.assertTrue(stdout.getvalue())
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.query.shell_test.TestUseCases (TestCase) - - - - -

    - -
    - -

    Testing all the use cases from the proposal here. -I'm hoping to replace reports by these queries instead.

    - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.query.shell_test.runshell(function) - - -

    - -
    - -

    Decorate a function to run the shell and return the output.

    - -
    - Source code in beancount/query/shell_test.py -
    def runshell(function):
    -    """Decorate a function to run the shell and return the output."""
    -    def test_function(self):
    -        def loadfun():
    -            return entries, errors, options_map
    -        with test_utils.capture('stdout') as stdout:
    -            shell_obj = shell.BQLShell(False, loadfun, sys.stdout)
    -            shell_obj.on_Reload()
    -            shell_obj.onecmd(function.__doc__)
    -        return function(self, stdout.getvalue())
    -    return test_function
    -
    -
    -
    - -
    - - - - - - - -
    - -
    - -
    - - - - -
    - -
    - -
    - -
    -
    - - -
    -
    - -
    - -
    - -
    - - - - « Previous - - - Next » - - -
    - - - - - - - - diff --git a/api_reference/beancount.reports.html b/api_reference/beancount.reports.html deleted file mode 100644 index 28b3d1b2..00000000 --- a/api_reference/beancount.reports.html +++ /dev/null @@ -1,10386 +0,0 @@ - - - - - - - - - - - - beancount.reports - Beancount Documentation - - - - - - - - - - - - - - - -
    - - - - -
    - - - - - -
    -
    -
    -
      -
    • Docs »
    • - - - -
    • API reference »
    • - - - -
    • beancount.reports
    • -
    • - -
    • -
    - -
    -
    - -
    -
    - -

    beancount.reports

    - - -
    - - -
    - -

    Routines to produce various reports, either to HTML or to text.

    - - - -
    - - - - - - - - - - - - -
    - - - -

    - beancount.reports.balance_reports - - - -

    - -
    - -

    Report classes for all reports that display ending balances of accounts.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.reports.balance_reports.BalanceSheetReport (HTMLReport) - - - - -

    - -
    - -

    Print out a balance sheet.

    - - - - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.reports.balance_reports.BalanceSheetReport.render_real_html(cls, real_root, price_map, price_date, options_map, file) - - -

    - -
    - -

    Wrap an htmldiv into our standard HTML template.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • real_root – An instance of RealAccount.

    • -
    • price_map – A price database.

    • -
    • price_date – A date for evaluating prices.

    • -
    • options_map – A dict, options as produced by the parser.

    • -
    • file – A file object to write the output to.

    • -
    -
    -
    - Source code in beancount/reports/balance_reports.py -
    def render_real_html(cls, real_root, price_map, price_date, options_map, file):
    -    """Wrap an htmldiv into our standard HTML template.
    -
    -    Args:
    -      real_root: An instance of RealAccount.
    -      price_map: A price database.
    -      price_date: A date for evaluating prices.
    -      options_map: A dict, options as produced by the parser.
    -      file: A file object to write the output to.
    -    """
    -    template = get_html_template()
    -    oss = io.StringIO()
    -    cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss)
    -    file.write(template.format(body=oss.getvalue(),
    -                               title=''))
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.balance_reports.BalancesReport (HTMLReport) - - - - -

    - -
    - -

    Print out the trial balance of accounts matching an expression.

    - - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.reports.balance_reports.BalancesReport.add_args(parser) - - - classmethod - - -

    - -
    - -

    Add arguments to parse for this report.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • parser – An instance of argparse.ArgumentParser.

    • -
    -
    -
    - Source code in beancount/reports/balance_reports.py -
    @classmethod
    -def add_args(cls, parser):
    -    parser.add_argument('-e', '--filter-expression', '--expression', '--regexp',
    -                        action='store', default=None,
    -                        help="Filter expression for which account balances to display.")
    -
    -    parser.add_argument('-c', '--at-cost', '--cost', action='store_true',
    -                        help="Render values at cost, convert the units to cost value")
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.reports.balance_reports.BalancesReport.render_real_html(cls, real_root, price_map, price_date, options_map, file) - - -

    - -
    - -

    Wrap an htmldiv into our standard HTML template.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • real_root – An instance of RealAccount.

    • -
    • price_map – A price database.

    • -
    • price_date – A date for evaluating prices.

    • -
    • options_map – A dict, options as produced by the parser.

    • -
    • file – A file object to write the output to.

    • -
    -
    -
    - Source code in beancount/reports/balance_reports.py -
    def render_real_html(cls, real_root, price_map, price_date, options_map, file):
    -    """Wrap an htmldiv into our standard HTML template.
    -
    -    Args:
    -      real_root: An instance of RealAccount.
    -      price_map: A price database.
    -      price_date: A date for evaluating prices.
    -      options_map: A dict, options as produced by the parser.
    -      file: A file object to write the output to.
    -    """
    -    template = get_html_template()
    -    oss = io.StringIO()
    -    cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss)
    -    file.write(template.format(body=oss.getvalue(),
    -                               title=''))
    -
    -
    -
    - -
    - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.balance_reports.IncomeStatementReport (HTMLReport) - - - - -

    - -
    - -

    Print out an income statement.

    - - - - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.reports.balance_reports.IncomeStatementReport.render_real_html(cls, real_root, price_map, price_date, options_map, file) - - -

    - -
    - -

    Wrap an htmldiv into our standard HTML template.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • real_root – An instance of RealAccount.

    • -
    • price_map – A price database.

    • -
    • price_date – A date for evaluating prices.

    • -
    • options_map – A dict, options as produced by the parser.

    • -
    • file – A file object to write the output to.

    • -
    -
    -
    - Source code in beancount/reports/balance_reports.py -
    def render_real_html(cls, real_root, price_map, price_date, options_map, file):
    -    """Wrap an htmldiv into our standard HTML template.
    -
    -    Args:
    -      real_root: An instance of RealAccount.
    -      price_map: A price database.
    -      price_date: A date for evaluating prices.
    -      options_map: A dict, options as produced by the parser.
    -      file: A file object to write the output to.
    -    """
    -    template = get_html_template()
    -    oss = io.StringIO()
    -    cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss)
    -    file.write(template.format(body=oss.getvalue(),
    -                               title=''))
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.base - - - -

    - -
    - -

    Base class for all reports classes.

    -

    Each report class should be able to render a filtered list of entries to a -variety of formats. Each report has a name, some command-line options, and -supports some subset of formats.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.reports.base.HTMLReport (Report) - - - - -

    - -
    - -

    A mixin for reports that support forwarding html to htmldiv implementation.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.base.RealizationMeta (type) - - - - -

    - -
    - -

    A metaclass for reports that render a realization.

    -

    The main use of this metaclass is to allow us to create report classes with -render_real_*() methods that accept a RealAccount instance as the basis for -producing a report.

    -

    RealAccount can be expensive to build, and may be pre-computed and kept -around to generate the various reports related to a particular filter of a -subset of transactions, and it would be inconvenient to have to recalculate -it every time we need to produce a report. In particular, this is the case -for the web interface: the user selects a particular subset of transactions -to view, and can then click to the various reports related to this subset of -transactions. This is why this is useful.

    -

    The classes generated with this metaclass respond to the same interface as -the regular report classes, so that if invoked from the command-line, it -will automatically build the realization from the given set of entries. This -metaclass looks at the class' existing render_real_() methods and generate -the corresponding render_() methods automatically.

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.reports.base.RealizationMeta.__new__(mcs, name, bases, namespace) - - - special - staticmethod - - -

    - -
    - -

    Create and return a new object. See help(type) for accurate signature.

    - -
    - Source code in beancount/reports/base.py -
    def __new__(mcs, name, bases, namespace):
    -    new_type = super(RealizationMeta, mcs).__new__(mcs, name, bases, namespace)
    -
    -    # Go through the methods of the new type and look for render_real() methods.
    -    new_methods = {}
    -    for attr, value in new_type.__dict__.items():
    -        match = re.match('render_real_(.*)', attr)
    -        if not match:
    -            continue
    -
    -        # Make sure that if an explicit version of render_*() has already
    -        # been declared, that we don't override it.
    -        render_function_name = 'render_{}'.format(match.group(1))
    -        if render_function_name in new_type.__dict__:
    -            continue
    -
    -        # Define a render_*() method on the class.
    -        def forward_method(self, entries, errors, options_map, file, fwdfunc=value):
    -            account_types = options.get_account_types(options_map)
    -            real_root = realization.realize(entries, account_types)
    -            price_map = prices.build_price_map(entries)
    -            # Note: When we forward, use the latest date (None).
    -            return fwdfunc(self, real_root, price_map, None, options_map, file)
    -        forward_method.__name__ = render_function_name
    -        new_methods[render_function_name] = forward_method
    -
    -    # Update the type with the newly defined methods..
    -    for mname, mvalue in new_methods.items():
    -        setattr(new_type, mname, mvalue)
    -
    -    # Auto-generate other methods if necessary.
    -    if hasattr(new_type, 'render_real_htmldiv'):
    -        setattr(new_type, 'render_real_html', mcs.render_real_html)
    -
    -    return new_type
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.base.RealizationMeta.render_real_html(cls, real_root, price_map, price_date, options_map, file) - - -

    - -
    - -

    Wrap an htmldiv into our standard HTML template.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • real_root – An instance of RealAccount.

    • -
    • price_map – A price database.

    • -
    • price_date – A date for evaluating prices.

    • -
    • options_map – A dict, options as produced by the parser.

    • -
    • file – A file object to write the output to.

    • -
    -
    -
    - Source code in beancount/reports/base.py -
    def render_real_html(cls, real_root, price_map, price_date, options_map, file):
    -    """Wrap an htmldiv into our standard HTML template.
    -
    -    Args:
    -      real_root: An instance of RealAccount.
    -      price_map: A price database.
    -      price_date: A date for evaluating prices.
    -      options_map: A dict, options as produced by the parser.
    -      file: A file object to write the output to.
    -    """
    -    template = get_html_template()
    -    oss = io.StringIO()
    -    cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss)
    -    file.write(template.format(body=oss.getvalue(),
    -                               title=''))
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.base.Report - - - -

    - -
    - -

    Base class for all reports.

    - -

    Attributes:

    - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescription
    names

    A list of strings, the various names of this report. The first name -is taken to be the authoritative name of the report; the rest are -considered aliases.

    parser

    The parser for the command's arguments. This is used to raise errors.

    args

    An object that contains the values of this command's parsed arguments.

    - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.reports.base.Report.__call__(self, entries, errors, options_map, output_format=None, file=None) - - - special - - -

    - -
    - -

    Render a report of filtered entries to any format.

    -

    This function dispatches to a specific method.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    • output_format – A string, the name of the format. If not specified, use -the default format.

    • -
    • file – The file to write the output to.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • If no 'file' is provided, return the contents of the report as a -string.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • ReportError – If the requested format is not supported.

    • -
    -
    -
    - Source code in beancount/reports/base.py -
    def render(self, entries, errors, options_map, output_format=None, file=None):
    -    """Render a report of filtered entries to any format.
    -
    -    This function dispatches to a specific method.
    -
    -    Args:
    -      entries: A list of directives to render.
    -      errors: A list of errors that occurred during processing.
    -      options_map: A dict of options, as produced by the parser.
    -      output_format: A string, the name of the format. If not specified, use
    -        the default format.
    -      file: The file to write the output to.
    -    Returns:
    -      If no 'file' is provided, return the contents of the report as a
    -      string.
    -    Raises:
    -      ReportError: If the requested format is not supported.
    -    """
    -    try:
    -        render_method = getattr(self, 'render_{}'.format(output_format or
    -                                                         self.default_format))
    -    except AttributeError:
    -        raise ReportError("Unsupported format: '{}'".format(output_format))
    -
    -    outfile = io.StringIO() if file is None else file
    -    result = render_method(entries, errors, options_map, outfile)
    -    assert result is None, "Render method must write to file."
    -    if file is None:
    -        return outfile.getvalue()
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.reports.base.Report.add_args(parser) - - - classmethod - - -

    - -
    - -

    Add arguments to parse for this report.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • parser – An instance of argparse.ArgumentParser.

    • -
    -
    -
    - Source code in beancount/reports/base.py -
    @classmethod
    -def add_args(cls, parser):
    -    """Add arguments to parse for this report.
    -
    -    Args:
    -      parser: An instance of argparse.ArgumentParser.
    -    """
    -    # No-op.
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.base.Report.from_args(argv=None, **kwds) - - - classmethod - - -

    - -
    - -

    A convenience method used to create an instance from arguments.

    -

    This creates an instance of the report with default arguments. This is a -convenience that may be used for tests. Our actual script uses subparsers -and invokes add_args() and creates an appropriate instance directly.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • argv – A list of strings, command-line arguments to use to construct the report.

    • -
    • kwds – A dict of other keyword arguments to pass to the report's constructor.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A new instance of the report.

    • -
    -
    -
    - Source code in beancount/reports/base.py -
    @classmethod
    -def from_args(cls, argv=None, **kwds):
    -    """A convenience method used to create an instance from arguments.
    -
    -    This creates an instance of the report with default arguments. This is a
    -    convenience that may be used for tests. Our actual script uses subparsers
    -    and invokes add_args() and creates an appropriate instance directly.
    -
    -    Args:
    -      argv: A list of strings, command-line arguments to use to construct the report.
    -      kwds: A dict of other keyword arguments to pass to the report's constructor.
    -    Returns:
    -      A new instance of the report.
    -    """
    -    parser = version.ArgumentParser()
    -    cls.add_args(parser)
    -    return cls(parser.parse_args(argv or []), parser, **kwds)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.base.Report.get_supported_formats() - - - classmethod - - -

    - -
    - -

    Enumerates the list of supported formats, by inspecting methods of this object.

    - - - - - - - - - - - - -
    Returns: -
      -
    • A list of strings, such as ['html', 'text'].

    • -
    -
    -
    - Source code in beancount/reports/base.py -
    @classmethod
    -def get_supported_formats(cls):
    -    """Enumerates the list of supported formats, by inspecting methods of this object.
    -
    -    Returns:
    -      A list of strings, such as ['html', 'text'].
    -    """
    -    formats = []
    -    for name in dir(cls):
    -        match = re.match('render_([a-z0-9]+)$', name)
    -        if match:
    -            formats.append(match.group(1))
    -    return sorted(formats)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.base.Report.render(self, entries, errors, options_map, output_format=None, file=None) - - -

    - -
    - -

    Render a report of filtered entries to any format.

    -

    This function dispatches to a specific method.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    • output_format – A string, the name of the format. If not specified, use -the default format.

    • -
    • file – The file to write the output to.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • If no 'file' is provided, return the contents of the report as a -string.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • ReportError – If the requested format is not supported.

    • -
    -
    -
    - Source code in beancount/reports/base.py -
    def render(self, entries, errors, options_map, output_format=None, file=None):
    -    """Render a report of filtered entries to any format.
    -
    -    This function dispatches to a specific method.
    -
    -    Args:
    -      entries: A list of directives to render.
    -      errors: A list of errors that occurred during processing.
    -      options_map: A dict of options, as produced by the parser.
    -      output_format: A string, the name of the format. If not specified, use
    -        the default format.
    -      file: The file to write the output to.
    -    Returns:
    -      If no 'file' is provided, return the contents of the report as a
    -      string.
    -    Raises:
    -      ReportError: If the requested format is not supported.
    -    """
    -    try:
    -        render_method = getattr(self, 'render_{}'.format(output_format or
    -                                                         self.default_format))
    -    except AttributeError:
    -        raise ReportError("Unsupported format: '{}'".format(output_format))
    -
    -    outfile = io.StringIO() if file is None else file
    -    result = render_method(entries, errors, options_map, outfile)
    -    assert result is None, "Render method must write to file."
    -    if file is None:
    -        return outfile.getvalue()
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.base.ReportError (Exception) - - - - -

    - -
    - -

    Error that occurred during report generation.

    - - - -
    - -
    - - - -
    - - - -

    - -beancount.reports.base.TableReport (HTMLReport) - - - - -

    - -
    - -

    A base class for reports that supports automatic conversions from Table.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.reports.base.TableReport.generate_table(self, entries, errors, options_map) - - -

    - -
    - -

    Render the report to a Table instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Table, that will get converted to another format.

    • -
    -
    -
    - Source code in beancount/reports/base.py -
    def generate_table(self, entries, errors, options_map):
    -    """Render the report to a Table instance.
    -
    -    Args:
    -      entries: A list of directives to render.
    -      errors: A list of errors that occurred during processing.
    -      options_map: A dict of options, as produced by the parser.
    -    Returns:
    -      An instance of Table, that will get converted to another format.
    -    """
    -    raise NotImplementedError
    -
    -
    -
    - -
    - - - - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.reports.base.get_html_template() - - -

    - -
    - -

    Returns our vanilla HTML template for embedding an HTML div.

    - - - - - - - - - - - - -
    Returns: -
      -
    • A string, with a formatting style placeholders – {title}: for the title of the page. - {body}: for the body, where the div goes.

    • -
    -
    -
    - Source code in beancount/reports/base.py -
    def get_html_template():
    -    """Returns our vanilla HTML template for embedding an HTML div.
    -
    -    Returns:
    -      A string, with a formatting style placeholders:
    -        {title}: for the title of the page.
    -        {body}: for the body, where the div goes.
    -    """
    -    with open(path.join(path.dirname(__file__), 'template.html')) as infile:
    -        return infile.read()
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.context - - - -

    - -
    - -

    Produce a rendering of the account balances just before and after a -particular entry is applied.

    - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.reports.context.render_entry_context(entries, options_map, entry) - - -

    - -
    - -

    Render the context before and after a particular transaction is applied.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    • entry – The entry instance which should be rendered. (Note that this object is -expected to be in the set of entries, not just structurally equal.)

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A multiline string of text, which consists of the context before the -transaction is applied, the transaction itself, and the context after it -is applied. You can just print that, it is in form that is intended to be -consumed by the user.

    • -
    -
    -
    - Source code in beancount/reports/context.py -
    def render_entry_context(entries, options_map, entry):
    -    """Render the context before and after a particular transaction is applied.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A dict of options, as produced by the parser.
    -      entry: The entry instance which should be rendered. (Note that this object is
    -        expected to be in the set of entries, not just structurally equal.)
    -    Returns:
    -      A multiline string of text, which consists of the context before the
    -      transaction is applied, the transaction itself, and the context after it
    -      is applied. You can just print that, it is in form that is intended to be
    -      consumed by the user.
    -    """
    -    oss = io.StringIO()
    -
    -    meta = entry.meta
    -    print("Hash:{}".format(compare.hash_entry(entry)), file=oss)
    -    print("Location: {}:{}".format(meta["filename"], meta["lineno"]), file=oss)
    -
    -    # Get the list of accounts sorted by the order in which they appear in the
    -    # closest entry.
    -    order = {}
    -    if isinstance(entry, data.Transaction):
    -        order = {posting.account: index
    -                 for index, posting in enumerate(entry.postings)}
    -    accounts = sorted(getters.get_entry_accounts(entry),
    -                      key=lambda account: order.get(account, 10000))
    -
    -    # Accumulate the balances of these accounts up to the entry.
    -    balance_before, balance_after = interpolate.compute_entry_context(entries,
    -                                                                      entry)
    -
    -    # Create a format line for printing the contents of account balances.
    -    max_account_width = max(map(len, accounts)) if accounts else 1
    -    position_line = '{{:1}} {{:{width}}}  {{:>49}}'.format(width=max_account_width)
    -
    -    # Print the context before.
    -    print(file=oss)
    -    print("------------ Balances before transaction", file=oss)
    -    print(file=oss)
    -    before_hashes = set()
    -    for account in accounts:
    -        positions = balance_before[account].get_positions()
    -        for position in positions:
    -            before_hashes.add((account, hash(position)))
    -            print(position_line.format('', account, str(position)), file=oss)
    -        if not positions:
    -            print(position_line.format('', account, ''), file=oss)
    -        print(file=oss)
    -
    -    # Print the entry itself.
    -    print(file=oss)
    -    print("------------ Transaction", file=oss)
    -    print(file=oss)
    -    dcontext = options_map['dcontext']
    -    printer.print_entry(entry, dcontext, render_weights=True, file=oss)
    -
    -    if isinstance(entry, data.Transaction):
    -        print(file=oss)
    -
    -        # Print residuals.
    -        residual = interpolate.compute_residual(entry.postings)
    -        if not residual.is_empty():
    -            # Note: We render the residual at maximum precision, for debugging.
    -            print('Residual: {}'.format(residual), file=oss)
    -
    -        # Dump the tolerances used.
    -        tolerances = interpolate.infer_tolerances(entry.postings, options_map)
    -        if tolerances:
    -            print('Tolerances: {}'.format(
    -                ', '.join('{}={}'.format(key, value)
    -                          for key, value in sorted(tolerances.items()))), file=oss)
    -
    -        # Compute the total cost basis.
    -        cost_basis = inventory.Inventory(
    -            pos for pos in entry.postings if pos.cost is not None
    -        ).reduce(convert.get_cost)
    -        if not cost_basis.is_empty():
    -            print('Basis: {}'.format(cost_basis), file=oss)
    -
    -    # Print the context after.
    -    print(file=oss)
    -    print("------------ Balances after transaction", file=oss)
    -    print(file=oss)
    -    for account in accounts:
    -        positions = balance_after[account].get_positions()
    -        for position in positions:
    -            changed = (account, hash(position)) not in before_hashes
    -            print(position_line.format('*' if changed else '', account, str(position)),
    -                  file=oss)
    -        if not positions:
    -            print(position_line.format('', account, ''), file=oss)
    -        print(file=oss)
    -
    -    return oss.getvalue()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.context.render_file_context(entries, options_map, filename, lineno) - - -

    - -
    - -

    Render the context before and after a particular transaction is applied.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    • filename – A string, the name of the file from which the transaction was parsed.

    • -
    • lineno – An integer, the line number in the file the transaction was parsed from.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A multiline string of text, which consists of the context before the -transaction is applied, the transaction itself, and the context after it -is applied. You can just print that, it is in form that is intended to be -consumed by the user.

    • -
    -
    -
    - Source code in beancount/reports/context.py -
    def render_file_context(entries, options_map, filename, lineno):
    -    """Render the context before and after a particular transaction is applied.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A dict of options, as produced by the parser.
    -      filename: A string, the name of the file from which the transaction was parsed.
    -      lineno: An integer, the line number in the file the transaction was parsed from.
    -    Returns:
    -      A multiline string of text, which consists of the context before the
    -      transaction is applied, the transaction itself, and the context after it
    -      is applied. You can just print that, it is in form that is intended to be
    -      consumed by the user.
    -    """
    -    # Find the closest entry.
    -    closest_entry = data.find_closest(entries, filename, lineno)
    -    if closest_entry is None:
    -        raise SystemExit("No entry could be found before {}:{}".format(filename, lineno))
    -
    -    return render_entry_context(entries, options_map, closest_entry)
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.convert_reports - - - -

    - -
    - -

    Format converter reports.

    -

    This module contains reports that can convert an input file into other formats, -such as Ledger.

    - - - -
    - - - - - - - - - - - -
    - - - -

    - -beancount.reports.convert_reports.HLedgerPrinter (LedgerPrinter) - - - - -

    - -
    - -

    Multi-method for printing directives in HLedger format.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.convert_reports.HLedgerReport (Report) - - - - -

    - -
    - -

    Print out the entries in a format that can be parsed by HLedger.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.convert_reports.LedgerPrinter - - - -

    - -
    - -

    Multi-method for printing directives in Ledger format.

    - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.convert_reports.LedgerReport (Report) - - - - -

    - -
    - -

    Print out the entries in a format that can be parsed by Ledger.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.reports.convert_reports.postings_by_type(entry) - - -

    - -
    - -

    Split up the postings by simple, at-cost, at-price.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – An instance of Transaction.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of simple postings, postings with price conversions, postings held at cost.

    • -
    -
    -
    - Source code in beancount/reports/convert_reports.py -
    def postings_by_type(entry):
    -    """Split up the postings by simple, at-cost, at-price.
    -
    -    Args:
    -      entry: An instance of Transaction.
    -    Returns:
    -      A tuple of simple postings, postings with price conversions, postings held at cost.
    -    """
    -    postings_at_cost = []
    -    postings_at_price = []
    -    postings_simple = []
    -    for posting in entry.postings:
    -        if posting.cost:
    -            accumlator = postings_at_cost
    -        elif posting.price:
    -            accumlator = postings_at_price
    -        else:
    -            accumlator = postings_simple
    -        accumlator.append(posting)
    -
    -    return (postings_simple, postings_at_price, postings_at_cost)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.convert_reports.quote(match) - - -

    - -
    - -

    Add quotes around a re.MatchObject.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • match – A MatchObject from the re module.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A quoted string of the match contents.

    • -
    -
    -
    - Source code in beancount/reports/convert_reports.py -
    def quote(match):
    -    """Add quotes around a re.MatchObject.
    -
    -    Args:
    -      match: A MatchObject from the re module.
    -    Returns:
    -      A quoted string of the match contents.
    -    """
    -    currency = match.group(1)
    -    return '"{}"'.format(currency) if re.search(r'[0-9\.]', currency) else currency
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.convert_reports.quote_currency(string) - - -

    - -
    - -

    Quote all the currencies with numbers from the given string.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • string – A string of text.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string of text, with the commodity expressions surrounded with quotes.

    • -
    -
    -
    - Source code in beancount/reports/convert_reports.py -
    def quote_currency(string):
    -    """Quote all the currencies with numbers from the given string.
    -
    -    Args:
    -      string: A string of text.
    -    Returns:
    -      A string of text, with the commodity expressions surrounded with quotes.
    -    """
    -    return re.sub(r'\b({})\b'.format(amount.CURRENCY_RE), quote, string)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.convert_reports.split_currency_conversions(entry) - - -

    - -
    - -

    If the transaction has a mix of conversion at cost and a -currency conversion, split the transaction into two transactions: one -that applies the currency conversion in the same account, and one -that uses the other currency without conversion.

    -

    This is required because Ledger does not appear to be able to grok a -transaction like this one:

    -

    2014-11-02 * "Buy some stock with foreign currency funds" - Assets:CA:Investment:HOOL 5 HOOL {520.0 USD} - Expenses:Commissions 9.95 USD - Assets:CA:Investment:Cash -2939.46 CAD @ 0.8879 USD

    -

    HISTORICAL NOTE: Adding a price directive on the first posting above makes -Ledger accept the transaction. So we will not split the transaction here -now. However, since Ledger's treatment of this type of conflict is subject -to revision (See http://bugs.ledger-cli.org/show_bug.cgi?id=630), we will -keep this code around, it might become useful eventually. See -https://groups.google.com/d/msg/ledger-cli/35hA0Dvhom0/WX8gY_5kHy0J for -details of the discussion.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – An instance of Transaction.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of - converted – boolean, true if a conversion was made. - entries: A list of the original entry if converted was False, - or a list of the split converted entries if True.

    • -
    -
    -
    - Source code in beancount/reports/convert_reports.py -
    def split_currency_conversions(entry):
    -    """If the transaction has a mix of conversion at cost and a
    -    currency conversion, split the transaction into two transactions: one
    -    that applies the currency conversion in the same account, and one
    -    that uses the other currency without conversion.
    -
    -    This is required because Ledger does not appear to be able to grok a
    -    transaction like this one:
    -
    -      2014-11-02 * "Buy some stock with foreign currency funds"
    -        Assets:CA:Investment:HOOL          5 HOOL {520.0 USD}
    -        Expenses:Commissions            9.95 USD
    -        Assets:CA:Investment:Cash   -2939.46 CAD @ 0.8879 USD
    -
    -    HISTORICAL NOTE: Adding a price directive on the first posting above makes
    -    Ledger accept the transaction. So we will not split the transaction here
    -    now. However, since Ledger's treatment of this type of conflict is subject
    -    to revision (See http://bugs.ledger-cli.org/show_bug.cgi?id=630), we will
    -    keep this code around, it might become useful eventually. See
    -    https://groups.google.com/d/msg/ledger-cli/35hA0Dvhom0/WX8gY_5kHy0J for
    -    details of the discussion.
    -
    -    Args:
    -      entry: An instance of Transaction.
    -    Returns:
    -      A pair of
    -        converted: boolean, true if a conversion was made.
    -        entries: A list of the original entry if converted was False,
    -          or a list of the split converted entries if True.
    -    """
    -    assert isinstance(entry, data.Transaction)
    -
    -    (postings_simple, postings_at_price, postings_at_cost) = postings_by_type(entry)
    -
    -    converted = postings_at_cost and postings_at_price
    -    if converted:
    -        # Generate a new entry for each currency conversion.
    -        new_entries = []
    -        replacement_postings = []
    -        for posting_orig in postings_at_price:
    -            weight = convert.get_weight(posting_orig)
    -            posting_pos = data.Posting(posting_orig.account, weight, None,
    -                                       None, None, None)
    -            posting_neg = data.Posting(posting_orig.account, -weight, None,
    -                                       None, None, None)
    -
    -            currency_entry = entry._replace(
    -                postings=[posting_orig, posting_neg],
    -                narration=entry.narration + ' (Currency conversion)')
    -            new_entries.append(currency_entry)
    -            replacement_postings.append(posting_pos)
    -
    -        converted_entry = entry._replace(postings=(
    -            postings_at_cost + postings_simple + replacement_postings))
    -        new_entries.append(converted_entry)
    -    else:
    -        new_entries = [entry]
    -
    -    return converted, new_entries
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.export_reports - - - -

    - -
    - -

    Reports to Export to third-party portfolio sites.

    - - - -
    - - - - - - - - - - - -
    - - - -

    - -beancount.reports.export_reports.ExportEntry (tuple) - - - - -

    - -
    - -

    ExportEntry(symbol, cost_currency, number, cost_number, mutual_fund, memo, holdings)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.reports.export_reports.ExportEntry.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/reports/export_reports.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.export_reports.ExportEntry.__new__(_cls, symbol, cost_currency, number, cost_number, mutual_fund, memo, holdings) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of ExportEntry(symbol, cost_currency, number, cost_number, mutual_fund, memo, holdings)

    - -
    - -
    - - - -
    - - - -

    -beancount.reports.export_reports.ExportEntry.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/reports/export_reports.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.export_reports.ExportPortfolioReport (TableReport) - - - - -

    - -
    - -

    Holdings lists that can be exported to external portfolio management software.

    - - - - -
    - - - - - - - - - - - - - - - - - -
    - - - -

    -beancount.reports.export_reports.ExportPortfolioReport.add_args(parser) - - - classmethod - - -

    - -
    - -

    Add arguments to parse for this report.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • parser – An instance of argparse.ArgumentParser.

    • -
    -
    -
    - Source code in beancount/reports/export_reports.py -
    @classmethod
    -def add_args(cls, parser):
    -    parser.add_argument('-d', '--debug', action='store_true',
    -                        help="Output position export debugging information on stderr.")
    -
    -    parser.add_argument('-p', '--promiscuous', action='store_true',
    -                        help=("Include title and account names in memos. "
    -                              "Use this if you trust wherever you upload."))
    -
    -    parser.add_argument('-a', '--aggregate-by-commodity', action='store_true',
    -                        help=("Group the holdings by account. This may help if your "
    -                              "portfolio fails to import and you have many holdings."))
    -
    -
    -
    - -
    - - - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.reports.export_reports.classify_holdings_for_export(holdings_list, commodities_map) - - -

    - -
    - -

    Figure out what to do for example with each holding.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • holdings_list – A list of Holding instances to be exported.

    • -
    • commodities_map – A dict of commodity to Commodity instances.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of – action_holdings: A list of (symbol, holding) for each holding. 'Symbol' - is the ticker to use for export, and may be "CASH" or "IGNORE" for - holdings to be converted or ignored.

    • -
    -
    -
    - Source code in beancount/reports/export_reports.py -
    def classify_holdings_for_export(holdings_list, commodities_map):
    -    """Figure out what to do for example with each holding.
    -
    -    Args:
    -      holdings_list: A list of Holding instances to be exported.
    -      commodities_map: A dict of commodity to Commodity instances.
    -    Returns:
    -      A pair of:
    -        action_holdings: A list of (symbol, holding) for each holding. 'Symbol'
    -          is the ticker to use for export, and may be "CASH" or "IGNORE" for
    -          holdings to be converted or ignored.
    -    """
    -    # Get the map of commodities to tickers and export meta tags.
    -    exports = getters.get_values_meta(commodities_map, FIELD)
    -
    -    # Classify the holdings based on their commodities' ticker metadata field.
    -    action_holdings = []
    -    for holding in holdings_list:
    -        # Get export field and remove (MONEY:...) specifications.
    -        export = re.sub(r'\(.*\)', '', exports.get(holding.currency, None) or '').strip()
    -        if export:
    -            if export.upper() == "CASH":
    -                action_holdings.append(('CASH', holding))
    -            elif export.upper() == "IGNORE":
    -                action_holdings.append(('IGNORE', holding))
    -            else:
    -                action_holdings.append((export, holding))
    -        else:
    -            logging.warning(("Exporting holding using default commodity name '{}'; this "
    -                             "can potentially break the OFX import. Consider providing "
    -                             "'export' metadata for your commodities.").format(
    -                                 holding.currency))
    -            action_holdings.append((holding.currency, holding))
    -
    -    return action_holdings
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.export_reports.export_holdings(entries, options_map, promiscuous, aggregate_by_commodity=False) - - -

    - -
    - -

    Compute a list of holdings to export.

    -

    Holdings that are converted to cash equivalents will receive a currency of -"CASH:<currency>" where <currency> is the converted cash currency.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • options_map – A dict of options as provided by the parser.

    • -
    • promiscuous – A boolean, true if we should output a promiscuous memo.

    • -
    • aggregate_by_commodity – A boolean, true if we should group the holdings by account.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of - exported – A list of ExportEntry tuples, one for each exported position. - converted: A list of ExportEntry tuples, one for each converted position. - These will contain multiple holdings. - holdings_ignored: A list of Holding instances that were ignored, either - because they were explicitly marked to be ignored, or because we could - not convert them to a money vehicle matching the holding's cost-currency.

    • -
    -
    -
    - Source code in beancount/reports/export_reports.py -
    def export_holdings(entries, options_map, promiscuous, aggregate_by_commodity=False):
    -    """Compute a list of holdings to export.
    -
    -    Holdings that are converted to cash equivalents will receive a currency of
    -    "CASH:<currency>" where <currency> is the converted cash currency.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A dict of options as provided by the parser.
    -      promiscuous: A boolean, true if we should output a promiscuous memo.
    -      aggregate_by_commodity: A boolean, true if we should group the holdings by account.
    -    Returns:
    -      A pair of
    -        exported: A list of ExportEntry tuples, one for each exported position.
    -        converted: A list of ExportEntry tuples, one for each converted position.
    -          These will contain multiple holdings.
    -        holdings_ignored: A list of Holding instances that were ignored, either
    -          because they were explicitly marked to be ignored, or because we could
    -          not convert them to a money vehicle matching the holding's cost-currency.
    -    """
    -    # Get the desired list of holdings.
    -    holdings_list, price_map = holdings_reports.get_assets_holdings(entries, options_map)
    -    commodities_map = getters.get_commodity_map(entries)
    -    dcontext = options_map['dcontext']
    -
    -    # Aggregate the holdings, if requested. Google Finance is notoriously
    -    # finnicky and if you have many holdings this might help.
    -    if aggregate_by_commodity:
    -        holdings_list = holdings.aggregate_holdings_by(holdings_list,
    -                                                       lambda holding: holding.currency)
    -
    -    # Classify all the holdings for export.
    -    action_holdings = classify_holdings_for_export(holdings_list, commodities_map)
    -
    -    # The lists of exported and converted export entries, and the list of
    -    # ignored holdings.
    -    exported = []
    -    converted = []
    -    holdings_ignored = []
    -
    -    # Export the holdings with tickers individually.
    -    for symbol, holding in action_holdings:
    -        if symbol in ("CASH", "IGNORE"):
    -            continue
    -
    -        if holding.cost_number is None:
    -            assert holding.cost_currency in (None, holding.currency)
    -            cost_number = holding.number
    -            cost_currency = holding.currency
    -        else:
    -            cost_number = holding.cost_number
    -            cost_currency = holding.cost_currency
    -
    -        exported.append(
    -            ExportEntry(symbol,
    -                        cost_currency,
    -                        holding.number,
    -                        cost_number,
    -                        is_mutual_fund(symbol),
    -                        holding.account if promiscuous else '',
    -                        [holding]))
    -
    -    # Convert all the cash entries to their book and market value by currency.
    -    cash_holdings_map = collections.defaultdict(list)
    -    for symbol, holding in action_holdings:
    -        if symbol != "CASH":
    -            continue
    -
    -        if holding.cost_currency:
    -            # Accumulate market and book values.
    -            cash_holdings_map[holding.cost_currency].append(holding)
    -        else:
    -            # We cannot price this... no cost currency.
    -            holdings_ignored.append(holding)
    -
    -    # Get the money instruments.
    -    money_instruments = get_money_instruments(commodities_map)
    -
    -    # Convert all the cash values to money instruments, if possible. If not
    -    # possible, we'll just have to ignore those values.
    -
    -    # Go through all the holdings to convert, and for each of those which aren't
    -    # in terms of one of the money instruments, which we can directly add to the
    -    # exported portfolio, attempt to convert them into currencies to one of
    -    # those in the money instruments.
    -    money_values_book = collections.defaultdict(D)
    -    money_values_market = collections.defaultdict(D)
    -    money_values_holdings = collections.defaultdict(list)
    -    for cost_currency, holdings_list in cash_holdings_map.items():
    -        book_value = sum(holding.book_value for holding in holdings_list)
    -        market_value = sum(holding.market_value for holding in holdings_list)
    -
    -        if cost_currency in money_instruments:
    -            # The holding is already in terms of one of the money instruments.
    -            money_values_book[cost_currency] += book_value
    -            money_values_market[cost_currency] += market_value
    -            money_values_holdings[cost_currency].extend(holdings_list)
    -        else:
    -            # The holding is not in terms of one of the money instruments.
    -            # Find the first available price to convert it into one
    -            for money_currency in money_instruments:
    -                base_quote = (cost_currency, money_currency)
    -                _, rate = prices.get_latest_price(price_map, base_quote)
    -                if rate is not None:
    -                    money_values_book[money_currency] += book_value * rate
    -                    money_values_market[money_currency] += market_value * rate
    -                    money_values_holdings[money_currency].extend(holdings_list)
    -                    break
    -            else:
    -                # We could not convert into any of the money commodities. Ignore
    -                # those holdings.
    -                holdings_ignored.extend(holdings_list)
    -
    -    for money_currency in money_values_book.keys():
    -        book_value = money_values_book[money_currency]
    -        market_value = money_values_market[money_currency]
    -        holdings_list = money_values_holdings[money_currency]
    -
    -        symbol = money_instruments[money_currency]
    -
    -        assert isinstance(book_value, Decimal)
    -        assert isinstance(market_value, Decimal)
    -        converted.append(
    -            ExportEntry(symbol,
    -                        money_currency,
    -                        dcontext.quantize(market_value, money_currency),
    -                        dcontext.quantize(book_value / market_value, money_currency),
    -                        is_mutual_fund(symbol),
    -                        '',
    -                        holdings_list))
    -
    -    # Add all ignored holdings to a final list.
    -    for symbol, holding in action_holdings:
    -        if symbol == "IGNORE":
    -            holdings_ignored.append(holding)
    -
    -    return exported, converted, holdings_ignored
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.export_reports.get_money_instruments(commodities_map) - - -

    - -
    - -

    Get the money-market stand-ins for cash positions.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • commodities_map – A map of currency to their corresponding Commodity directives.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A dict of quote currency to the ticker symbol that stands for it, -e.g. {'USD' – 'VMMXX'}.

    • -
    -
    -
    - Source code in beancount/reports/export_reports.py -
    def get_money_instruments(commodities_map):
    -    """Get the money-market stand-ins for cash positions.
    -
    -    Args:
    -      commodities_map: A map of currency to their corresponding Commodity directives.
    -    Returns:
    -      A dict of quote currency to the ticker symbol that stands for it,
    -      e.g. {'USD': 'VMMXX'}.
    -    """
    -    instruments = {}
    -    for currency, entry in commodities_map.items():
    -        export = entry.meta.get(FIELD, '')
    -        paren_match = re.search(r'\((.*)\)', export)
    -        if paren_match:
    -            match = re.match('MONEY:({})'.format(amount.CURRENCY_RE), paren_match.group(1))
    -            if match:
    -                instruments[match.group(1)] = (
    -                    re.sub(r'\(.*\)', '', export).strip() or currency)
    -            else:
    -                logging.error("Invalid money specification: %s", export)
    -
    -    return instruments
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.export_reports.get_symbol(sources, prefer='google') - - -

    - -
    - -

    Filter a source specification to some corresponding ticker.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • source – A comma-separated list of sources as a string, such as -"google/NASDAQ:AAPL,yahoo/AAPL".

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The symbol string.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • ValueError – If the sources does not contain a ticker for the -google source.

    • -
    -
    -
    - Source code in beancount/reports/export_reports.py -
    def get_symbol(sources, prefer='google'):
    -    """Filter a source specification to some corresponding ticker.
    -
    -    Args:
    -      source: A comma-separated list of sources as a string, such as
    -        "google/NASDAQ:AAPL,yahoo/AAPL".
    -    Returns:
    -      The symbol string.
    -    Raises:
    -      ValueError: If the sources does not contain a ticker for the
    -        google source.
    -    """
    -
    -    # If the ticker is a list of <source>/<symbol>, extract the symbol
    -    # from it.
    -    symbol_items = []
    -    for source in map(str.strip, sources.split(',')):
    -        match = re.match('([a-zA-Z][a-zA-Z0-9._]+)/(.*)', source)
    -        if match:
    -            source, symbol = match.groups()
    -        else:
    -            source, symbol = None, source
    -        symbol_items.append((source, symbol))
    -    if not symbol_items:
    -        raise ValueError(
    -            'Invalid source "{}" does not contain a ticker'.format(sources))
    -    symbol_map = dict(symbol_items)
    -    # If not found, return the first symbol in the list of items.
    -    return symbol_map.get(prefer, symbol_items[0][1])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.export_reports.is_mutual_fund(ticker) - - -

    - -
    - -

    Return true if the GFinance ticker is for a mutual fund.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • ticker – A string, the symbol for GFinance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A boolean, true for mutual funds.

    • -
    -
    -
    - Source code in beancount/reports/export_reports.py -
    def is_mutual_fund(ticker):
    -    """Return true if the GFinance ticker is for a mutual fund.
    -
    -    Args:
    -      ticker: A string, the symbol for GFinance.
    -    Returns:
    -      A boolean, true for mutual funds.
    -    """
    -    return bool(re.match('MUTF.*:', ticker))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.export_reports.render_ofx_date(dtime) - - -

    - -
    - -

    Render a datetime to the OFX format.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • dtime – A datetime.datetime instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, rendered to milliseconds.

    • -
    -
    -
    - Source code in beancount/reports/export_reports.py -
    def render_ofx_date(dtime):
    -    """Render a datetime to the OFX format.
    -
    -    Args:
    -      dtime: A datetime.datetime instance.
    -    Returns:
    -      A string, rendered to milliseconds.
    -    """
    -    return '{}.{:03d}'.format(dtime.strftime('%Y%m%d%H%M%S'),
    -                              int(dtime.microsecond / 1000))
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.gviz - - - -

    - -
    - -

    Support for creating Google gviz timeline charts.

    - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.reports.gviz.gviz_timeline(time_array, data_array_map, css_id='chart') - - -

    - -
    - -

    Create a HTML rendering of the given arrays.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • time_array – A sequence of datetime objects.

    • -
    • data_array_map – A dict or list of items of name to - sequence of data points.

    • -
    • css_id – A string, the CSS id attribute of the target node.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • Javascript code for rendering the chart. (It's up to you to -insert the a div with the correct CSS id in your accompanying -HTML page.)

    • -
    -
    -
    - Source code in beancount/reports/gviz.py -
    def gviz_timeline(time_array, data_array_map, css_id='chart'):
    -    """Create a HTML rendering of the given arrays.
    -
    -    Args:
    -      time_array: A sequence of datetime objects.
    -      data_array_map: A dict or list of items of name to
    -                      sequence of data points.
    -      css_id: A string, the CSS id attribute of the target node.
    -    Returns:
    -      Javascript code for rendering the chart. (It's up to you to
    -      insert the a div with the correct CSS id in your accompanying
    -      HTML page.)
    -    """
    -    # Set the order of the data to be output.
    -    if isinstance(data_array_map, dict):
    -        data_array_map = list(data_array_map.items())
    -
    -    # Write preamble.
    -    oss = io.StringIO()
    -
    -    oss.write('<script src="https://www.google.com/jsapi" type="text/javascript">'
    -              '</script>\n')
    -    oss.write('<script type="text/javascript">\n')
    -
    -    oss.write("""\
    -      google.load('visualization', '1', {packages: ['annotatedtimeline']});
    -      function draw() {
    -        var data = new google.visualization.DataTable();
    -      """)
    -
    -    # Declare columns.
    -    oss.write("data.addColumn('{}', '{}');\n".format('datetime', 'Time'))
    -    for name, _ in data_array_map:
    -        oss.write("data.addColumn('{}', '{}');\n".format('number', name))
    -
    -    # Render the rows.
    -    oss.write('data.addRows([\n')
    -
    -    datalists = [x[1] for x in data_array_map]
    -
    -    for dtime, datas in zip(time_array, zip(*datalists)):
    -        js_datetime = ('Date({0.year}, {0.month}, {0.day})').format(dtime)
    -        oss.write('  [new {}, {}],\n'.format(js_datetime, ', '.join(map(str, datas))))
    -    oss.write(']);\n')
    -
    -    oss.write("""
    -        var annotatedtimeline = new google.visualization.AnnotatedTimeLine(
    -            document.getElementById('{css_id}')
    -        );
    -
    -        var options = {{
    -          'legendPosition'    : 'newRow',
    -          'displayAnnotations': true,
    -        }};
    -
    -        annotatedtimeline.draw(data, options);
    -      }}
    -
    -      google.setOnLoadCallback(draw);
    -    """.format(css_id=css_id))
    -
    -    oss.write('</script>\n')
    -
    -    return oss.getvalue()
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.holdings_reports - - - -

    - -
    - -

    Generate reports no holdings.

    - - - -
    - - - - - - - - - - - - -
    - - - -

    - -beancount.reports.holdings_reports.CashReport (TableReport) - - - - -

    - -
    - -

    The list of cash holdings (defined as currency = cost-currency).

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.reports.holdings_reports.CashReport.add_args(parser) - - - classmethod - - -

    - -
    - -

    Add arguments to parse for this report.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • parser – An instance of argparse.ArgumentParser.

    • -
    -
    -
    - Source code in beancount/reports/holdings_reports.py -
    @classmethod
    -def add_args(cls, parser):
    -    parser.add_argument('-c', '--currency',
    -                        action='store', default=None,
    -                        help="Which currency to convert all the holdings to")
    -
    -    parser.add_argument('-i', '--ignored',
    -                        action='store_true',
    -                        help="Report on ignored holdings instead of included ones")
    -
    -    parser.add_argument('-o', '--operating-only',
    -                        action='store_true',
    -                        help="Only report on operating currencies")
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.holdings_reports.CashReport.generate_table(self, entries, errors, options_map) - - -

    - -
    - -

    Render the report to a Table instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Table, that will get converted to another format.

    • -
    -
    -
    - Source code in beancount/reports/holdings_reports.py -
    def generate_table(self, entries, errors, options_map):
    -    holdings_list, price_map = get_assets_holdings(entries, options_map)
    -    holdings_list_orig = holdings_list
    -
    -    # Keep only the holdings where currency is the same as the cost-currency.
    -    holdings_list = [holding
    -                     for holding in holdings_list
    -                     if (holding.currency == holding.cost_currency or
    -                         holding.cost_currency is None)]
    -
    -    # Keep only those holdings held in one of the operating currencies.
    -    if self.args.operating_only:
    -        operating_currencies = set(options_map['operating_currency'])
    -        holdings_list = [holding
    -                         for holding in holdings_list
    -                         if holding.currency in operating_currencies]
    -
    -    # Compute the list of ignored holdings and optionally report on them.
    -    if self.args.ignored:
    -        ignored_holdings = set(holdings_list_orig) - set(holdings_list)
    -        holdings_list = ignored_holdings
    -
    -    # Convert holdings to a unified currency.
    -    if self.args.currency:
    -        holdings_list = holdings.convert_to_currency(price_map, self.args.currency,
    -                                                     holdings_list)
    -
    -    return table.create_table(holdings_list, FIELD_SPEC)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.holdings_reports.HoldingsReport (TableReport) - - - - -

    - -
    - -

    The full list of holdings for Asset and Liabilities accounts.

    - - - - -
    - - - - - - - - - - - - -
    - - - -

    -beancount.reports.holdings_reports.HoldingsReport.add_args(parser) - - - classmethod - - -

    - -
    - -

    Add arguments to parse for this report.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • parser – An instance of argparse.ArgumentParser.

    • -
    -
    -
    - Source code in beancount/reports/holdings_reports.py -
    @classmethod
    -def add_args(cls, parser):
    -    parser.add_argument('-c', '--currency',
    -                        action='store', default=None,
    -                        help="Which currency to convert all the holdings to")
    -
    -    parser.add_argument('-r', '--relative',
    -                        action='store_true',
    -                        help="True if we should render as relative values only")
    -
    -    parser.add_argument('-g', '--groupby', '--by',
    -                        action='store', default=None,
    -                        choices=cls.aggregations.keys(),
    -                        help="How to group the holdings (default is: don't group)")
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.holdings_reports.HoldingsReport.generate_table(self, entries, errors, options_map) - - -

    - -
    - -

    Render the report to a Table instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Table, that will get converted to another format.

    • -
    -
    -
    - Source code in beancount/reports/holdings_reports.py -
    def generate_table(self, entries, errors, options_map):
    -    keywords = self.aggregations[self.args.groupby] if self.args.groupby else {}
    -    return report_holdings(self.args.currency, self.args.relative,
    -                           entries, options_map,
    -                           **keywords)
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.holdings_reports.NetWorthReport (TableReport) - - - - -

    - -
    - -

    Generate a table of total net worth for each operating currency.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.reports.holdings_reports.NetWorthReport.generate_table(self, entries, errors, options_map) - - -

    - -
    - -

    Render the report to a Table instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Table, that will get converted to another format.

    • -
    -
    -
    - Source code in beancount/reports/holdings_reports.py -
    def generate_table(self, entries, errors, options_map):
    -    holdings_list, price_map = get_assets_holdings(entries, options_map)
    -
    -    net_worths = []
    -    for currency in options_map['operating_currency']:
    -
    -        # Convert holdings to a unified currency.
    -        #
    -        # Note: It's entirely possible that the price map does not have all
    -        # the necessary rate conversions here. The resulting holdings will
    -        # simply have no cost when that is the case. We must handle this
    -        # gracefully below.
    -        currency_holdings_list = holdings.convert_to_currency(price_map,
    -                                                              currency,
    -                                                              holdings_list)
    -        if not currency_holdings_list:
    -            continue
    -
    -        holdings_list = holdings.aggregate_holdings_by(
    -            currency_holdings_list, lambda holding: holding.cost_currency)
    -
    -        holdings_list = [holding
    -                         for holding in holdings_list
    -                         if holding.currency and holding.cost_currency]
    -
    -        # If after conversion there are no valid holdings, skip the currency
    -        # altogether.
    -        if not holdings_list:
    -            continue
    -
    -        net_worths.append((currency, holdings_list[0].market_value))
    -
    -    field_spec = [
    -        (0, 'Currency'),
    -        (1, 'Net Worth', '{:,.2f}'.format),
    -    ]
    -    return table.create_table(net_worths, field_spec)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.reports.holdings_reports.get_assets_holdings(entries, options_map, currency=None) - - -

    - -
    - -

    Return holdings for all assets and liabilities.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • options_map – A dict of parsed options.

    • -
    • currency – If specified, a string, the target currency to convert all -holding values to.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of Holding instances and a price-map.

    • -
    -
    -
    - Source code in beancount/reports/holdings_reports.py -
    def get_assets_holdings(entries, options_map, currency=None):
    -    """Return holdings for all assets and liabilities.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A dict of parsed options.
    -      currency: If specified, a string, the target currency to convert all
    -        holding values to.
    -    Returns:
    -      A list of Holding instances and a price-map.
    -    """
    -    # Compute a price map, to perform conversions.
    -    price_map = prices.build_price_map(entries)
    -
    -    # Get the list of holdings.
    -    account_types = options.get_account_types(options_map)
    -    holdings_list = holdings.get_final_holdings(entries,
    -                                                (account_types.assets,
    -                                                 account_types.liabilities),
    -                                                price_map)
    -
    -    # Convert holdings to a unified currency.
    -    if currency:
    -        holdings_list = holdings.convert_to_currency(price_map, currency, holdings_list)
    -
    -    return holdings_list, price_map
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.holdings_reports.get_holdings_entries(entries, options_map) - - -

    - -
    - -

    Summarizes the entries to list of entries representing the final holdings..

    -

    This list includes the latest prices entries as well. This can be used to -load a full snapshot of holdings without including the entire history. This -is a way of summarizing a balance sheet in a way that filters away history.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • options_map – A dict of parsed options.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, the entries to print out.

    • -
    -
    -
    - Source code in beancount/reports/holdings_reports.py -
    def get_holdings_entries(entries, options_map):
    -    """Summarizes the entries to list of entries representing the final holdings..
    -
    -    This list includes the latest prices entries as well. This can be used to
    -    load a full snapshot of holdings without including the entire history. This
    -    is a way of summarizing a balance sheet in a way that filters away history.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A dict of parsed options.
    -    Returns:
    -      A string, the entries to print out.
    -    """
    -
    -    # The entries will be created at the latest date, against an equity account.
    -    latest_date = entries[-1].date
    -    _, equity_account, _ = options.get_previous_accounts(options_map)
    -
    -    # Get all the assets.
    -    holdings_list, _ = get_assets_holdings(entries, options_map)
    -
    -    # Create synthetic entries for them.
    -    holdings_entries = []
    -
    -    for index, holding in enumerate(holdings_list):
    -        meta = data.new_metadata('report_holdings_print', index)
    -        entry = data.Transaction(meta, latest_date, flags.FLAG_SUMMARIZE,
    -                                 None, "", None, None, [])
    -
    -        # Convert the holding to a position.
    -        pos = holdings.holding_to_position(holding)
    -        entry.postings.append(
    -            data.Posting(holding.account, pos.units, pos.cost, None, None, None))
    -
    -        cost = -convert.get_cost(pos)
    -        entry.postings.append(
    -            data.Posting(equity_account, cost, None, None, None, None))
    -
    -        holdings_entries.append(entry)
    -
    -    # Get opening directives for all the accounts.
    -    used_accounts = {holding.account for holding in holdings_list}
    -    open_entries = summarize.get_open_entries(entries, latest_date)
    -    used_open_entries = [open_entry
    -                         for open_entry in open_entries
    -                         if open_entry.account in used_accounts]
    -
    -    # Add an entry for the equity account we're using.
    -    meta = data.new_metadata('report_holdings_print', -1)
    -    used_open_entries.insert(0, data.Open(meta, latest_date, equity_account,
    -                                          None, None))
    -
    -    # Get the latest price entries.
    -    price_entries = prices.get_last_price_entries(entries, None)
    -
    -    return used_open_entries + holdings_entries + price_entries
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.holdings_reports.load_from_csv(fileobj) - - -

    - -
    - -

    Load a list of holdings from a CSV file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • fileobj – A file object.

    • -
    -

    Yields: - Instances of Holding, as read from the file.

    - -
    - Source code in beancount/reports/holdings_reports.py -
    def load_from_csv(fileobj):
    -    """Load a list of holdings from a CSV file.
    -
    -    Args:
    -      fileobj: A file object.
    -    Yields:
    -      Instances of Holding, as read from the file.
    -    """
    -    column_spec = [
    -        ('Account', 'account', None),
    -        ('Units', 'number', D),
    -        ('Currency', 'currency', None),
    -        ('Cost Currency', 'cost_currency', None),
    -        ('Average Cost', 'cost_number', D),
    -        ('Price', 'price_number', D),
    -        ('Book Value', 'book_value', D),
    -        ('Market Value', 'market_value', D),
    -        ('Price Date', 'price_date', None),
    -        ]
    -    column_dict = {name: (attr, converter)
    -                   for name, attr, converter in column_spec}
    -    klass = holdings.Holding
    -
    -    # Create a set of default values for the namedtuple.
    -    defaults_dict = {attr: None for attr in klass._fields}
    -
    -    # Start reading the file.
    -    reader = csv.reader(fileobj)
    -
    -    # Check that the header is readable.
    -    header = next(reader)
    -    attr_converters = []
    -    for header_name in header:
    -        try:
    -            attr_converter = column_dict[header_name]
    -            attr_converters.append(attr_converter)
    -        except KeyError:
    -            raise IOError("Invalid file contents for holdings")
    -
    -    for line in reader:
    -        value_dict = defaults_dict.copy()
    -        for (attr, converter), value in zip(attr_converters, line):
    -            if converter:
    -                value = converter(value)
    -            value_dict[attr] = value
    -        yield holdings.Holding(**value_dict)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.holdings_reports.report_holdings(currency, relative, entries, options_map, aggregation_key=None, sort_key=None) - - -

    - -
    - -

    Generate a detailed list of all holdings.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • currency – A string, a currency to convert to. If left to None, no -conversion is carried out.

    • -
    • relative – A boolean, true if we should reduce this to a relative value.

    • -
    • entries – A list of directives.

    • -
    • options_map – A dict of parsed options.

    • -
    • aggregation_key – A callable use to generate aggregations.

    • -
    • sort_key – A function to use to sort the holdings, if specified.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A Table instance.

    • -
    -
    -
    - Source code in beancount/reports/holdings_reports.py -
    def report_holdings(currency, relative, entries, options_map,
    -                    aggregation_key=None,
    -                    sort_key=None):
    -    """Generate a detailed list of all holdings.
    -
    -    Args:
    -      currency: A string, a currency to convert to. If left to None, no
    -        conversion is carried out.
    -      relative: A boolean, true if we should reduce this to a relative value.
    -      entries: A list of directives.
    -      options_map: A dict of parsed options.
    -      aggregation_key: A callable use to generate aggregations.
    -      sort_key: A function to use to sort the holdings, if specified.
    -    Returns:
    -      A Table instance.
    -    """
    -    holdings_list, _ = get_assets_holdings(entries, options_map, currency)
    -    if aggregation_key:
    -        holdings_list = holdings.aggregate_holdings_by(holdings_list, aggregation_key)
    -
    -    if relative:
    -        holdings_list = holdings.reduce_relative(holdings_list)
    -        field_spec = RELATIVE_FIELD_SPEC
    -    else:
    -        field_spec = FIELD_SPEC
    -
    -    if sort_key:
    -        holdings_list.sort(key=sort_key, reverse=True)
    -
    -    return table.create_table(holdings_list, field_spec)
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.html_formatter - - - -

    - -
    - -

    Base class for HTML formatters.

    -

    This object encapsulates the rendering of various objects to HTML. -You may, and should, derive and override from this object in order to -provide links within a web interface.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.reports.html_formatter.HTMLFormatter - - - -

    - -
    - -

    A trivial formatter object that can be used to format strings as themselves. -This mainly defines an interface to implement.

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.reports.html_formatter.HTMLFormatter.__init__(self, dcontext) - - - special - - -

    - -
    - -

    Create an instance of HTMLFormatter.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • dcontext – DisplayContext to use to render the numbers.

    • -
    -
    -
    - Source code in beancount/reports/html_formatter.py -
    def __init__(self, dcontext):
    -    """Create an instance of HTMLFormatter.
    -
    -    Args:
    -      dcontext: DisplayContext to use to render the numbers.
    -    """
    -    self._dformat = dcontext.build(
    -        precision=display_context.Precision.MOST_COMMON)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.html_formatter.HTMLFormatter.render_account(self, account_name) - - -

    - -
    - -

    Render an account name.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • account_name – A string, the name of the account to render.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string of HTML to be spliced inside an HTML template.

    • -
    -
    -
    - Source code in beancount/reports/html_formatter.py -
    def render_account(self, account_name):
    -    """Render an account name.
    -
    -    Args:
    -      account_name: A string, the name of the account to render.
    -    Returns:
    -      A string of HTML to be spliced inside an HTML template.
    -    """
    -    return account_name
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.html_formatter.HTMLFormatter.render_amount(self, amount) - - -

    - -
    - -

    Render an amount.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • amount – An Amount instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string of HTML to be spliced inside a table cell.

    • -
    -
    -
    - Source code in beancount/reports/html_formatter.py -
    def render_amount(self, amount):
    -    """Render an amount.
    -
    -    Args:
    -      amount: An Amount instance.
    -    Returns:
    -      A string of HTML to be spliced inside a table cell.
    -    """
    -    return amount.to_string(self._dformat)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.html_formatter.HTMLFormatter.render_commodity(self, base_quote) - - -

    - -
    - -

    Render a commodity (base currency / quote currency).

    -

    This is only used when we want the commodity to link to its prices.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • commodity – A pair of strings, the base and quote currency names.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string of HTML to be spliced inside an HTML template.

    • -
    -
    -
    - Source code in beancount/reports/html_formatter.py -
    def render_commodity(self, base_quote):
    -    """Render a commodity (base currency / quote currency).
    -
    -    This is only used when we want the commodity to link to its prices.
    -
    -    Args:
    -      commodity: A pair of strings, the base and quote currency names.
    -    Returns:
    -      A string of HTML to be spliced inside an HTML template.
    -    """
    -    return '{} / {}'.format(*base_quote)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.html_formatter.HTMLFormatter.render_context(self, entry) - - -

    - -
    - -

    Render a reference to context around a transaction (maybe as an HTML link).

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – A directive.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string of HTML to be spliced inside an HTML template.

    • -
    -
    -
    - Source code in beancount/reports/html_formatter.py -
    def render_context(self, entry):
    -    """Render a reference to context around a transaction (maybe as an HTML link).
    -
    -    Args:
    -      entry: A directive.
    -    Returns:
    -      A string of HTML to be spliced inside an HTML template.
    -    """
    -    return ''
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.html_formatter.HTMLFormatter.render_doc(self, filename) - - -

    - -
    - -

    Render a document path.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the filename for the document.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string of HTML to be spliced inside an HTML template.

    • -
    -
    -
    - Source code in beancount/reports/html_formatter.py -
    def render_doc(self, filename):
    -    """Render a document path.
    -
    -    Args:
    -      filename: A string, the filename for the document.
    -    Returns:
    -      A string of HTML to be spliced inside an HTML template.
    -    """
    -    return filename
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.html_formatter.HTMLFormatter.render_event_type(self, event) - - -

    - -
    - -

    Render an event type.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • event – A string, the name of the even type.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string of HTML to be spliced inside an HTML template.

    • -
    -
    -
    - Source code in beancount/reports/html_formatter.py -
    def render_event_type(self, event):
    -    """Render an event type.
    -
    -    Args:
    -      event: A string, the name of the even type.
    -    Returns:
    -      A string of HTML to be spliced inside an HTML template.
    -    """
    -    return event
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.html_formatter.HTMLFormatter.render_inventory(self, inv) - - -

    - -
    - -

    Render an inventory.

    -

    You can use this opportunity to convert the inventory to units or cost -or whatever.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • inv – An Inventory instance.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string of HTML to be spliced inside a table cell.

    • -
    -
    -
    - Source code in beancount/reports/html_formatter.py -
    def render_inventory(self, inv):
    -    """Render an inventory.
    -
    -    You can use this opportunity to convert the inventory to units or cost
    -    or whatever.
    -
    -    Args:
    -      inv: An Inventory instance.
    -    Returns:
    -      A string of HTML to be spliced inside a table cell.
    -    """
    -    return '<br/>'.join(position_.to_string(self._dformat)
    -                        for position_ in sorted(inv))
    -
    -
    -
    - -
    - - - -
    - - - - - -
    - -

    Render a transaction link (maybe as an HTML link).

    - - - - - - - - - - - - -
    Parameters: -
      -
    • link – A string, the name of the link to render.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string of HTML to be spliced inside an HTML template.

    • -
    -
    -
    - Source code in beancount/reports/html_formatter.py -
    def render_link(self, link):
    -    """Render a transaction link (maybe as an HTML link).
    -
    -    Args:
    -      link: A string, the name of the link to render.
    -    Returns:
    -      A string of HTML to be spliced inside an HTML template.
    -    """
    -    return link
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.html_formatter.HTMLFormatter.render_number(self, number, currency) - - -

    - -
    - -

    Render a number for a currency using the formatter's display context.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • number – A Decimal instance, the number to be rendered.

    • -
    • currency – A string, the commodity the number represent.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, the formatted number to render.

    • -
    -
    -
    - Source code in beancount/reports/html_formatter.py -
    def render_number(self, number, currency):
    -    """Render a number for a currency using the formatter's display context.
    -
    -    Args:
    -      number: A Decimal instance, the number to be rendered.
    -      currency: A string, the commodity the number represent.
    -    Returns:
    -      A string, the formatted number to render.
    -    """
    -    return self._dformat.format(number, currency)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.html_formatter.HTMLFormatter.render_source(self, meta) - - -

    - -
    - -

    Render a reference to the source file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • meta – A metadata dict object.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string of HTML to be spliced inside an HTML template.

    • -
    -
    -
    - Source code in beancount/reports/html_formatter.py -
    def render_source(self, meta):
    -    """Render a reference to the source file.
    -
    -    Args:
    -      meta: A metadata dict object.
    -    Returns:
    -      A string of HTML to be spliced inside an HTML template.
    -    """
    -    return printer.render_source(meta)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.journal_html - - - -

    - -
    - -

    HTML rendering routines for serving a lists of postings/entries.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.reports.journal_html.Row (tuple) - - - - -

    - -
    - -

    Row(entry, leg_postings, rowtype, extra_class, flag, description, links, amount_str, balance_str)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.reports.journal_html.Row.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/reports/journal_html.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.journal_html.Row.__new__(_cls, entry, leg_postings, rowtype, extra_class, flag, description, links, amount_str, balance_str) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of Row(entry, leg_postings, rowtype, extra_class, flag, description, links, amount_str, balance_str)

    - -
    - -
    - - - -
    - - - -

    -beancount.reports.journal_html.Row.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/reports/journal_html.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.reports.journal_html.html_entries_table(oss, txn_postings, formatter, render_postings=True) - - -

    - -
    - -

    Render a list of entries into an HTML table, with no running balance.

    -

    This is appropriate for rendering tables of entries for postings with -multiple accounts, whereby computing the running balances makes little -sense.

    -

    (This function returns nothing, it write to oss as a side-effect.)

    - - - - - - - - - - - - -
    Parameters: -
      -
    • oss – A file object to write the output to.

    • -
    • txn_postings – A list of Posting or directive instances.

    • -
    • formatter – An instance of HTMLFormatter, to be render accounts, -inventories, links and docs.

    • -
    • render_postings – A boolean; if true, render the postings as rows under the -main transaction row.

    • -
    -
    -
    - Source code in beancount/reports/journal_html.py -
    def html_entries_table(oss, txn_postings, formatter, render_postings=True):
    -    """Render a list of entries into an HTML table, with no running balance.
    -
    -    This is appropriate for rendering tables of entries for postings with
    -    multiple accounts, whereby computing the running balances makes little
    -    sense.
    -
    -    (This function returns nothing, it write to oss as a side-effect.)
    -
    -    Args:
    -      oss: A file object to write the output to.
    -      txn_postings: A list of Posting or directive instances.
    -      formatter: An instance of HTMLFormatter, to be render accounts,
    -        inventories, links and docs.
    -      render_postings: A boolean; if true, render the postings as rows under the
    -        main transaction row.
    -    """
    -    write = lambda data: (oss.write(data), oss.write('\n'))
    -
    -    write('''
    -      <table class="entry-table">
    -      <thead>
    -        <tr>
    -         <th class="datecell">Date</th>
    -         <th class="flag">F</th>
    -         <th class="description">Narration/Payee</th>
    -         <th class="amount">Amount</th>
    -         <th class="cost">Cost</th>
    -         <th class="price">Price</th>
    -         <th class="balance">Balance</th>
    -        </tr>
    -      </thead>
    -    ''')
    -
    -    for row in iterate_html_postings(txn_postings, formatter):
    -        entry = row.entry
    -
    -        description = row.description
    -        if row.links:
    -            description += render_links(row.links)
    -
    -        # Render a row.
    -        write('''
    -          <tr class="{} {}" title="{}">
    -            <td class="datecell"><a href="{}">{}</a></td>
    -            <td class="flag">{}</td>
    -            <td class="description" colspan="5">{}</td>
    -          </tr>
    -        '''.format(row.rowtype, row.extra_class,
    -                   '{}:{}'.format(entry.meta["filename"], entry.meta["lineno"]),
    -                   formatter.render_context(entry), entry.date,
    -                   row.flag, description))
    -
    -        if render_postings and isinstance(entry, data.Transaction):
    -            for posting in entry.postings:
    -
    -                classes = ['Posting']
    -                if posting.flag == flags.FLAG_WARNING:
    -                    classes.append('warning')
    -
    -                write('''
    -                  <tr class="{}">
    -                    <td class="datecell"></td>
    -                    <td class="flag">{}</td>
    -                    <td class="description">{}</td>
    -                    <td class="amount num">{}</td>
    -                    <td class="cost num">{}</td>
    -                    <td class="price num">{}</td>
    -                    <td class="balance num">{}</td>
    -                  </tr>
    -                '''.format(' '.join(classes),
    -                           posting.flag or '',
    -                           formatter.render_account(posting.account),
    -                           posting.units or '',
    -                           posting.cost or '',
    -                           posting.price or '',
    -                           convert.get_weight(posting)))
    -
    -    write('</table>')
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.journal_html.html_entries_table_with_balance(oss, txn_postings, formatter, render_postings=True) - - -

    - -
    - -

    Render a list of entries into an HTML table, with a running balance.

    -

    (This function returns nothing, it write to oss as a side-effect.)

    - - - - - - - - - - - - -
    Parameters: -
      -
    • oss – A file object to write the output to.

    • -
    • txn_postings – A list of Posting or directive instances.

    • -
    • formatter – An instance of HTMLFormatter, to be render accounts, -inventories, links and docs.

    • -
    • render_postings – A boolean; if true, render the postings as rows under the -main transaction row.

    • -
    -
    -
    - Source code in beancount/reports/journal_html.py -
    def html_entries_table_with_balance(oss, txn_postings, formatter, render_postings=True):
    -    """Render a list of entries into an HTML table, with a running balance.
    -
    -    (This function returns nothing, it write to oss as a side-effect.)
    -
    -    Args:
    -      oss: A file object to write the output to.
    -      txn_postings: A list of Posting or directive instances.
    -      formatter: An instance of HTMLFormatter, to be render accounts,
    -        inventories, links and docs.
    -      render_postings: A boolean; if true, render the postings as rows under the
    -        main transaction row.
    -    """
    -    write = lambda data: (oss.write(data), oss.write('\n'))
    -
    -    write('''
    -      <table class="entry-table">
    -      <thead>
    -        <tr>
    -         <th class="datecell">Date</th>
    -         <th class="flag">F</th>
    -         <th class="description">Narration/Payee</th>
    -         <th class="position">Position</th>
    -         <th class="price">Price</th>
    -         <th class="cost">Cost</th>
    -         <th class="change">Change</th>
    -         <th class="balance">Balance</th>
    -        </tr>
    -      </thead>
    -    ''')
    -
    -    for row in iterate_html_postings(txn_postings, formatter):
    -        entry = row.entry
    -
    -        description = row.description
    -        if row.links:
    -            description += render_links(row.links)
    -
    -        # Render a row.
    -        write('''
    -          <tr class="{} {}" title="{}">
    -            <td class="datecell"><a href="{}">{}</a></td>
    -            <td class="flag">{}</td>
    -            <td class="description" colspan="4">{}</td>
    -            <td class="change num">{}</td>
    -            <td class="balance num">{}</td>
    -          </tr>
    -        '''.format(row.rowtype, row.extra_class,
    -                   '{}:{}'.format(entry.meta["filename"], entry.meta["lineno"]),
    -                   formatter.render_context(entry), entry.date,
    -                   row.flag, description,
    -                   row.amount_str, row.balance_str))
    -
    -        if render_postings and isinstance(entry, data.Transaction):
    -            for posting in entry.postings:
    -
    -                classes = ['Posting']
    -                if posting.flag == flags.FLAG_WARNING:
    -                    classes.append('warning')
    -                if posting in row.leg_postings:
    -                    classes.append('leg')
    -
    -                write('''
    -                  <tr class="{}">
    -                    <td class="datecell"></td>
    -                    <td class="flag">{}</td>
    -                    <td class="description">{}</td>
    -                    <td class="position num">{}</td>
    -                    <td class="price num">{}</td>
    -                    <td class="cost num">{}</td>
    -                    <td class="change num"></td>
    -                    <td class="balance num"></td>
    -                  </tr>
    -                '''.format(' '.join(classes),
    -                           posting.flag or '',
    -                           formatter.render_account(posting.account),
    -                           position.to_string(posting),
    -                           posting.price or '',
    -                           convert.get_weight(posting)))
    -
    -    write('</table>')
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.journal_html.iterate_html_postings(txn_postings, formatter) - - -

    - -
    - -

    Iterate through the list of transactions with rendered HTML strings for each cell.

    -

    This pre-renders all the data for each row to HTML. This is reused by the entries -table rendering routines.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • txn_postings – A list of TxnPosting or directive instances.

    • -
    • formatter – An instance of HTMLFormatter, to be render accounts, -inventories, links and docs.

    • -
    -

    Yields: - Instances of Row tuples. See above.

    - -
    - Source code in beancount/reports/journal_html.py -
    def iterate_html_postings(txn_postings, formatter):
    -    """Iterate through the list of transactions with rendered HTML strings for each cell.
    -
    -    This pre-renders all the data for each row to HTML. This is reused by the entries
    -    table rendering routines.
    -
    -    Args:
    -      txn_postings: A list of TxnPosting or directive instances.
    -      formatter: An instance of HTMLFormatter, to be render accounts,
    -        inventories, links and docs.
    -    Yields:
    -      Instances of Row tuples. See above.
    -    """
    -    for entry_line in realization.iterate_with_balance(txn_postings):
    -        entry, leg_postings, change, entry_balance = entry_line
    -
    -        # Prepare the data to be rendered for this row.
    -        balance_str = formatter.render_inventory(entry_balance)
    -
    -        rowtype = entry.__class__.__name__
    -        flag = ''
    -        extra_class = ''
    -        links = None
    -
    -        if isinstance(entry, data.Transaction):
    -            rowtype = FLAG_ROWTYPES.get(entry.flag, 'Transaction')
    -            extra_class = 'warning' if entry.flag == flags.FLAG_WARNING else ''
    -            flag = entry.flag
    -            description = '<span class="narration">{}</span>'.format(entry.narration)
    -            if entry.payee:
    -                description = ('<span class="payee">{}</span>'
    -                               '<span class="pnsep">|</span>'
    -                               '{}').format(entry.payee, description)
    -            amount_str = formatter.render_inventory(change)
    -
    -            if entry.links and formatter:
    -                links = [formatter.render_link(link) for link in entry.links]
    -
    -        elif isinstance(entry, data.Balance):
    -            # Check the balance here and possibly change the rowtype
    -            if entry.diff_amount is None:
    -                description = 'Balance {} has {}'.format(
    -                    formatter.render_account(entry.account),
    -                    entry.amount)
    -            else:
    -                description = ('Balance in {} fails; '
    -                               'expected = {}, balance = {}, difference = {}').format(
    -                                   formatter.render_account(entry.account),
    -                                   entry.amount,
    -                                   entry_balance.get_currency_units(entry.amount.currency),
    -                                   entry.diff_amount)
    -                extra_class = 'fail'
    -
    -            amount_str = formatter.render_amount(entry.amount)
    -
    -        elif isinstance(entry, (data.Open, data.Close)):
    -            description = '{} {}'.format(entry.__class__.__name__,
    -                                         formatter.render_account(entry.account))
    -            amount_str = ''
    -
    -        elif isinstance(entry, data.Note):
    -            description = '{} {}'.format(entry.__class__.__name__, entry.comment)
    -            amount_str = ''
    -            balance_str = ''
    -
    -        elif isinstance(entry, data.Document):
    -            assert path.isabs(entry.filename)
    -            description = 'Document for {}: {}'.format(
    -                formatter.render_account(entry.account),
    -                formatter.render_doc(entry.filename))
    -            amount_str = ''
    -            balance_str = ''
    -
    -        else:
    -            description = entry.__class__.__name__
    -            amount_str = ''
    -            balance_str = ''
    -
    -        yield Row(entry, leg_postings,
    -                  rowtype, extra_class,
    -                  flag, description, links, amount_str, balance_str)
    -
    -
    -
    - -
    - - - -
    - - - - - -
    - -

    Render Transaction links to HTML.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • links – A list of set of strings, transaction "links" to be rendered.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, a snippet of HTML to be rendering somewhere.

    • -
    -
    -
    - Source code in beancount/reports/journal_html.py -
    def render_links(links):
    -    """Render Transaction links to HTML.
    -
    -    Args:
    -      links: A list of set of strings, transaction "links" to be rendered.
    -    Returns:
    -      A string, a snippet of HTML to be rendering somewhere.
    -    """
    -    return '<span class="links">{}</span>'.format(
    -        ''.join('<a href="{}">^</a>'.format(link)
    -                for link in links))
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.journal_reports - - - -

    - -
    - -

    Report classes for all reports that display ending journals of accounts.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.reports.journal_reports.ConversionsReport (HTMLReport) - - - - -

    - -
    - -

    Print out a report of all conversions.

    - - - - -
    - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.journal_reports.DocumentsReport (HTMLReport) - - - - -

    - -
    - -

    Print out a report of documents.

    - - - - -
    - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.journal_reports.JournalReport (HTMLReport) - - - - -

    - -
    - -

    Print out an account register/journal.

    - - - - -
    - - - - - - - - - - - - - -
    - - - -

    -beancount.reports.journal_reports.JournalReport.add_args(parser) - - - classmethod - - -

    - -
    - -

    Add arguments to parse for this report.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • parser – An instance of argparse.ArgumentParser.

    • -
    -
    -
    - Source code in beancount/reports/journal_reports.py -
    @classmethod
    -def add_args(cls, parser):
    -    parser.add_argument('-a', '--account',
    -                        action='store', default=None,
    -                        help="Account to render")
    -
    -    parser.add_argument('-w', '--width', action='store', type=int, default=0,
    -                        help="The number of characters wide to render the report to")
    -
    -    parser.add_argument('-k', '--precision', action='store', type=int, default=2,
    -                        help="The number of digits to render after the period")
    -
    -    parser.add_argument('-b', '--render-balance', '--balance', action='store_true',
    -                        help="Render a running balance, not just changes")
    -
    -    parser.add_argument('-c', '--at-cost', '--cost', action='store_true',
    -                        help="Render values at cost, convert the units to cost value")
    -
    -    parser.add_argument('-x', '--compact', dest='verbosity', action='store_const',
    -                        const=journal_text.COMPACT, default=journal_text.NORMAL,
    -                        help="Rendering compactly")
    -
    -    parser.add_argument('-X', '--verbose', dest='verbosity', action='store_const',
    -                        const=journal_text.VERBOSE,
    -                        help="Rendering verbosely")
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.journal_reports.JournalReport.get_postings(self, real_root) - - -

    - -
    - -

    Return the postings corresponding to the account filter option.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • real_root – A RealAccount node for the root of all accounts.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of posting or directive instances.

    • -
    -
    -
    - Source code in beancount/reports/journal_reports.py -
    def get_postings(self, real_root):
    -    """Return the postings corresponding to the account filter option.
    -
    -    Args:
    -      real_root: A RealAccount node for the root of all accounts.
    -    Returns:
    -      A list of posting or directive instances.
    -    """
    -    if self.args.account:
    -        real_account = realization.get(real_root, self.args.account)
    -        if real_account is None:
    -            # If the account isn't found, return an empty list of postings.
    -            # Note that this used to return the following error.
    -            # raise base.ReportError(
    -            #     "Invalid account name: {}".format(self.args.account))
    -            return []
    -    else:
    -        real_account = real_root
    -
    -    return realization.get_postings(real_account)
    -
    -
    -
    - -
    - - - - - - -
    - - - -

    -beancount.reports.journal_reports.JournalReport.render_real_html(cls, real_root, price_map, price_date, options_map, file) - - -

    - -
    - -

    Wrap an htmldiv into our standard HTML template.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • real_root – An instance of RealAccount.

    • -
    • price_map – A price database.

    • -
    • price_date – A date for evaluating prices.

    • -
    • options_map – A dict, options as produced by the parser.

    • -
    • file – A file object to write the output to.

    • -
    -
    -
    - Source code in beancount/reports/journal_reports.py -
    def render_real_html(cls, real_root, price_map, price_date, options_map, file):
    -    """Wrap an htmldiv into our standard HTML template.
    -
    -    Args:
    -      real_root: An instance of RealAccount.
    -      price_map: A price database.
    -      price_date: A date for evaluating prices.
    -      options_map: A dict, options as produced by the parser.
    -      file: A file object to write the output to.
    -    """
    -    template = get_html_template()
    -    oss = io.StringIO()
    -    cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss)
    -    file.write(template.format(body=oss.getvalue(),
    -                               title=''))
    -
    -
    -
    - -
    - - - - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.journal_text - - - -

    - -
    - -

    Text rendering routines for serving a lists of postings/entries.

    - - - -
    - - - - - - - - - - - - - - - -
    - - - -

    - -beancount.reports.journal_text.AmountColumnSizer - - - -

    - -
    - -

    A class that computes minimal sizes for columns of numbers and their currencies.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.reports.journal_text.AmountColumnSizer.get_format(self, precision) - - -

    - -
    - -

    Return a format string for the column of numbers.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • precision – An integer, the number of digits to render after the period.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A new-style Python format string, with PREFIX_number and PREFIX_currency named -fields.

    • -
    -
    -
    - Source code in beancount/reports/journal_text.py -
    def get_format(self, precision):
    -    """Return a format string for the column of numbers.
    -
    -    Args:
    -      precision: An integer, the number of digits to render after the period.
    -    Returns:
    -      A new-style Python format string, with PREFIX_number and PREFIX_currency named
    -      fields.
    -    """
    -    return ('{{0:>{width:d}.{precision:d}f}} {{1:<{currency_width}}}').format(
    -        width=1 + self.get_number_width() + 1 + precision,
    -        precision=precision,
    -        currency_width=self.max_currency_width)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.journal_text.AmountColumnSizer.get_generic_format(self, precision) - - -

    - -
    - -

    Return a generic format string for rendering as wide as required. -This can be used to render an empty string in-lieu of a number.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • precision – An integer, the number of digits to render after the period.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A new-style Python format string, with PREFIX_number and PREFIX_currency named -fields.

    • -
    -
    -
    - Source code in beancount/reports/journal_text.py -
    def get_generic_format(self, precision):
    -    """Return a generic format string for rendering as wide as required.
    -    This can be used to render an empty string in-lieu of a number.
    -
    -    Args:
    -      precision: An integer, the number of digits to render after the period.
    -    Returns:
    -      A new-style Python format string, with PREFIX_number and PREFIX_currency named
    -      fields.
    -    """
    -    return '{{{prefix}:<{width}}}'.format(
    -        prefix=self.prefix,
    -        width=1 + self.get_number_width() + 1 + precision + 1 + self.max_currency_width)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.journal_text.AmountColumnSizer.get_number_width(self) - - -

    - -
    - -

    Return the width of the integer part of the max number.

    - - - - - - - - - - - - -
    Returns: -
      -
    • An integer, the number of digits required to render the integral part.

    • -
    -
    -
    - Source code in beancount/reports/journal_text.py -
    def get_number_width(self):
    -    """Return the width of the integer part of the max number.
    -
    -    Returns:
    -      An integer, the number of digits required to render the integral part.
    -    """
    -    return ((math.floor(math.log10(self.max_number)) + 1)
    -            if self.max_number > 0
    -            else 1)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.journal_text.AmountColumnSizer.update(self, number, currency) - - -

    - -
    - -

    Update the sizer with the given number and currency.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • number – A Decimal instance.

    • -
    • currency – A string, the currency to render for it.

    • -
    -
    -
    - Source code in beancount/reports/journal_text.py -
    def update(self, number, currency):
    -    """Update the sizer with the given number and currency.
    -
    -    Args:
    -      number: A Decimal instance.
    -      currency: A string, the currency to render for it.
    -    """
    -    abs_number = abs(number)
    -    if abs_number > self.max_number:
    -        self.max_number = abs_number
    -    currency_width = len(currency)
    -    if currency_width > self.max_currency_width:
    -        self.max_currency_width = currency_width
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.reports.journal_text.get_entry_text_description(entry) - - -

    - -
    - -

    Return the text of a description.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – A directive, of any type.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string to use for the filling the description field in text reports.

    • -
    -
    -
    - Source code in beancount/reports/journal_text.py -
    def get_entry_text_description(entry):
    -    """Return the text of a description.
    -
    -    Args:
    -      entry: A directive, of any type.
    -    Returns:
    -      A string to use for the filling the description field in text reports.
    -    """
    -    if isinstance(entry, data.Transaction):
    -        description = ' | '.join([field
    -                                  for field in [entry.payee, entry.narration]
    -                                  if field is not None])
    -    elif isinstance(entry, data.Balance):
    -        if entry.diff_amount is None:
    -            description = 'PASS - In {}'.format(entry.account)
    -        else:
    -            description = ('FAIL - In {}; '
    -                           'expected = {}, difference = {}').format(
    -                               entry.account,
    -                               entry.amount,
    -                               entry.diff_amount)
    -    elif isinstance(entry, (data.Open, data.Close)):
    -        description = entry.account
    -    elif isinstance(entry, data.Note):
    -        description = entry.comment
    -    elif isinstance(entry, data.Document):
    -        description = entry.filename
    -    else:
    -        description = '-'
    -    return description
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.journal_text.render_posting(posting, number_format) - - -

    - -
    - -

    Render a posting compactly, for text report rendering.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • posting – An instance of Posting.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, the rendered posting.

    • -
    -
    -
    - Source code in beancount/reports/journal_text.py -
    def render_posting(posting, number_format):
    -    """Render a posting compactly, for text report rendering.
    -
    -    Args:
    -      posting: An instance of Posting.
    -    Returns:
    -      A string, the rendered posting.
    -    """
    -    # Note: there's probably no need to redo the work of rendering here... see
    -    # if you can't just simply replace this by Position.to_string().
    -
    -    units = posting.units
    -    strings = [
    -        posting.flag if posting.flag else ' ',
    -        '{:32}'.format(posting.account),
    -        number_format.format(units.number, units.currency)
    -        ]
    -
    -    cost = posting.cost
    -    if cost:
    -        strings.append('{{{}}}'.format(number_format.format(cost.number,
    -                                                            cost.currency).strip()))
    -
    -    price = posting.price
    -    if price:
    -        strings.append('@ {}'.format(number_format.format(price.number,
    -                                                          price.currency).strip()))
    -
    -    return ' '.join(strings)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.journal_text.size_and_render_amounts(postings, at_cost, render_balance) - - -

    - -
    - -

    Iterate through postings and compute sizers and render amounts.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • postings – A list of Posting or directive instances.

    • -
    • at_cost – A boolean, if true, render the cost value, not the actual.

    • -
    • render_balance – A boolean, if true, renders a running balance column.

    • -
    -
    -
    - Source code in beancount/reports/journal_text.py -
    def size_and_render_amounts(postings, at_cost, render_balance):
    -    """Iterate through postings and compute sizers and render amounts.
    -
    -    Args:
    -      postings: A list of Posting or directive instances.
    -      at_cost: A boolean, if true, render the cost value, not the actual.
    -      render_balance: A boolean, if true, renders a running balance column.
    -    """
    -
    -    # Compute the maximum width required to render the change and balance
    -    # columns. In order to carry this out, we will pre-compute all the data to
    -    # render this and save it for later.
    -    change_sizer = AmountColumnSizer('change')
    -    balance_sizer = AmountColumnSizer('balance')
    -
    -    entry_data = []
    -    for entry_line in realization.iterate_with_balance(postings):
    -        entry, leg_postings, change, balance = entry_line
    -
    -        # Convert to cost if necessary. (Note that this agglutinates currencies,
    -        # so we'd rather do make the conversion at this level (inventory) than
    -        # convert the positions or amounts later.)
    -        if at_cost:
    -            change = change.reduce(convert.get_cost)
    -            if render_balance:
    -                balance = balance.reduce(convert.get_cost)
    -
    -        # Compute the amounts and maximum widths for the change column.
    -        change_amounts = []
    -        for position in change.get_positions():
    -            units = position.units
    -            change_amounts.append(units)
    -            change_sizer.update(units.number, units.currency)
    -
    -        # Compute the amounts and maximum widths for the balance column.
    -        balance_amounts = []
    -        if render_balance:
    -            for position in balance.get_positions():
    -                units = position.units
    -                balance_amounts.append(units)
    -                balance_sizer.update(units.number, units.currency)
    -
    -        entry_data.append((entry, leg_postings, change_amounts, balance_amounts))
    -
    -    return (entry_data, change_sizer, balance_sizer)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.journal_text.text_entries_table(oss, postings, width, at_cost, render_balance, precision, verbosity, output_format) - - -

    - -
    - -

    Render a table of postings or directives with an accumulated balance.

    -

    This function has three verbosity modes for rendering: -1. COMPACT: no separating line, no postings -2. NORMAL: a separating line between entries, no postings -3. VERBOSE: renders all the postings in addition to normal.

    -

    The output is written to the 'oss' file object. Nothing is returned.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • oss – A file object to write the output to.

    • -
    • postings – A list of Posting or directive instances.

    • -
    • width – An integer, the width to render the table to.

    • -
    • at_cost – A boolean, if true, render the cost value, not the actual.

    • -
    • render_balance – A boolean, if true, renders a running balance column.

    • -
    • precision – An integer, the number of digits to render after the period.

    • -
    • verbosity – An integer, the verbosity level. See COMPACT, NORMAL, VERBOSE, etc.

    • -
    • output_format – A string, either 'text' or 'csv' for the chosen output format. -This routine's inner loop calculations are complex enough it gets reused by both -formats.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • ValueError – If the width is insufficient to render the description.

    • -
    -
    -
    - Source code in beancount/reports/journal_text.py -
    def text_entries_table(oss, postings,
    -                       width, at_cost, render_balance, precision, verbosity,
    -                       output_format):
    -    """Render a table of postings or directives with an accumulated balance.
    -
    -    This function has three verbosity modes for rendering:
    -    1. COMPACT: no separating line, no postings
    -    2. NORMAL: a separating line between entries, no postings
    -    3. VERBOSE: renders all the postings in addition to normal.
    -
    -    The output is written to the 'oss' file object. Nothing is returned.
    -
    -    Args:
    -      oss: A file object to write the output to.
    -      postings: A list of Posting or directive instances.
    -      width: An integer, the width to render the table to.
    -      at_cost: A boolean, if true, render the cost value, not the actual.
    -      render_balance: A boolean, if true, renders a running balance column.
    -      precision: An integer, the number of digits to render after the period.
    -      verbosity: An integer, the verbosity level. See COMPACT, NORMAL, VERBOSE, etc.
    -      output_format: A string, either 'text' or 'csv' for the chosen output format.
    -        This routine's inner loop calculations are complex enough it gets reused by both
    -        formats.
    -    Raises:
    -      ValueError: If the width is insufficient to render the description.
    -    """
    -    assert output_format in (FORMAT_TEXT, FORMAT_CSV)
    -    if output_format is FORMAT_CSV:
    -        csv_writer = csv.writer(oss)
    -
    -    # Render the changes and balances to lists of amounts and precompute sizes.
    -    entry_data, change_sizer, balance_sizer = size_and_render_amounts(postings,
    -                                                                      at_cost,
    -                                                                      render_balance)
    -
    -    # Render an empty line and compute the width the description should be (the
    -    # description is the only elastic field).
    -    empty_format = '{{date:10}} {{dirtype:5}} {{description}}  {}'.format(
    -        change_sizer.get_generic_format(precision))
    -    if render_balance:
    -        empty_format += '  {}'.format(balance_sizer.get_generic_format(precision))
    -    empty_line = empty_format.format(date='', dirtype='', description='',
    -                                     change='', balance='')
    -    description_width = width - len(empty_line)
    -    if description_width <= 0:
    -        raise ValueError(
    -            "Width not sufficient to render text report ({} chars)".format(width))
    -
    -    # Establish a format string for the final format of all lines.
    -    # pylint: disable=duplicate-string-formatting-argument
    -    line_format = '{{date:10}} {{dirtype:5}} {{description:{:d}.{:d}}}  {}'.format(
    -        description_width, description_width,
    -        change_sizer.get_generic_format(precision))
    -    change_format = change_sizer.get_format(precision)
    -    if render_balance:
    -        line_format += '  {}'.format(balance_sizer.get_generic_format(precision))
    -        balance_format = balance_sizer.get_format(precision)
    -    line_format += '\n'
    -
    -    # Iterate over all the pre-computed data.
    -    for (entry, leg_postings, change_amounts, balance_amounts) in entry_data:
    -
    -        # Render the date.
    -        date = entry.date.isoformat()
    -
    -        # Get the directive type name.
    -        dirtype = TEXT_SHORT_NAME[type(entry)]
    -        if isinstance(entry, data.Transaction) and entry.flag:
    -            dirtype = entry.flag
    -
    -        # Get the description string and split the description line in multiple
    -        # lines.
    -        description = get_entry_text_description(entry)
    -        description_lines = textwrap.wrap(description, width=description_width)
    -
    -        # Ensure at least one line is rendered (for zip_longest).
    -        if not description_lines:
    -            description_lines.append('')
    -
    -        # Render all the amounts in the line.
    -        for (description,
    -             change_amount,
    -             balance_amount) in itertools.zip_longest(description_lines,
    -                                                      change_amounts,
    -                                                      balance_amounts,
    -                                                      fillvalue=''):
    -
    -            change = (change_format.format(change_amount.number,
    -                                           change_amount.currency)
    -                      if change_amount
    -                      else '')
    -
    -            balance = (balance_format.format(balance_amount.number,
    -                                             balance_amount.currency)
    -                       if balance_amount
    -                       else '')
    -
    -            if not description and verbosity >= VERBOSE and leg_postings:
    -                description = '..'
    -
    -            if output_format is FORMAT_TEXT:
    -                oss.write(line_format.format(date=date,
    -                                             dirtype=dirtype,
    -                                             description=description,
    -                                             change=change,
    -                                             balance=balance))
    -            else:
    -                change_number, change_currency = '', ''
    -                if change:
    -                    change_number, change_currency = change.split()
    -
    -                if render_balance:
    -                    balance_number, balance_currency = '', ''
    -                    if balance:
    -                        balance_number, balance_currency = balance.split()
    -
    -                    row = (date, dirtype, description,
    -                           change_number, change_currency,
    -                           balance_number, balance_currency)
    -                else:
    -                    row = (date, dirtype, description,
    -                           change_number, change_currency)
    -                csv_writer.writerow(row)
    -
    -            # Reset the date, directive type and description. Only the first
    -            # line renders these; the other lines render only the amounts.
    -            if date:
    -                date = dirtype = ''
    -
    -        if verbosity >= VERBOSE:
    -            for posting in leg_postings:
    -                posting_str = render_posting(posting, change_format)
    -                if len(posting_str) > description_width:
    -                    posting_str = posting_str[:description_width-3] + '...'
    -
    -                if output_format is FORMAT_TEXT:
    -                    oss.write(line_format.format(date='',
    -                                                 dirtype='',
    -                                                 description=posting_str,
    -                                                 change='',
    -                                                 balance=''))
    -                else:
    -                    row = ('', '', posting_str)
    -                    csv_writer.writerow(row)
    -
    -        if verbosity >= NORMAL:
    -            oss.write('\n')
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.misc_reports - - - -

    - -
    - -

    Miscellaneous report classes.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.reports.misc_reports.AccountsReport (Report) - - - - -

    - -
    - -

    Print out the list of all accounts.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.misc_reports.ActivityReport (HTMLReport) - - - - -

    - -
    - -

    Render the last or recent update activity.

    - - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.reports.misc_reports.ActivityReport.add_args(parser) - - - classmethod - - -

    - -
    - -

    Add arguments to parse for this report.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • parser – An instance of argparse.ArgumentParser.

    • -
    -
    -
    - Source code in beancount/reports/misc_reports.py -
    @classmethod
    -def add_args(cls, parser):
    -    parser.add_argument('-d', '--cutoff',
    -                        action='store', default=None,
    -                        type=date_utils.parse_date_liberally,
    -                        help="Cutoff date where we ignore whatever comes after.")
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.reports.misc_reports.ActivityReport.render_real_html(cls, real_root, price_map, price_date, options_map, file) - - -

    - -
    - -

    Wrap an htmldiv into our standard HTML template.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • real_root – An instance of RealAccount.

    • -
    • price_map – A price database.

    • -
    • price_date – A date for evaluating prices.

    • -
    • options_map – A dict, options as produced by the parser.

    • -
    • file – A file object to write the output to.

    • -
    -
    -
    - Source code in beancount/reports/misc_reports.py -
    def render_real_html(cls, real_root, price_map, price_date, options_map, file):
    -    """Wrap an htmldiv into our standard HTML template.
    -
    -    Args:
    -      real_root: An instance of RealAccount.
    -      price_map: A price database.
    -      price_date: A date for evaluating prices.
    -      options_map: A dict, options as produced by the parser.
    -      file: A file object to write the output to.
    -    """
    -    template = get_html_template()
    -    oss = io.StringIO()
    -    cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss)
    -    file.write(template.format(body=oss.getvalue(),
    -                               title=''))
    -
    -
    -
    - -
    - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.misc_reports.CurrentEventsReport (TableReport) - - - - -

    - -
    - -

    Produce a table of the current values of all event types.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.reports.misc_reports.CurrentEventsReport.generate_table(self, entries, errors, options_map) - - -

    - -
    - -

    Render the report to a Table instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Table, that will get converted to another format.

    • -
    -
    -
    - Source code in beancount/reports/misc_reports.py -
    def generate_table(self, entries, errors, options_map):
    -    events = {}
    -    for entry in entries:
    -        if isinstance(entry, data.Event):
    -            events[entry.type] = entry.description
    -    return table.create_table(list(sorted(events.items())),
    -                              [(0, "Type", self.formatter.render_event_type),
    -                               (1, "Description")])
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.misc_reports.ErrorReport (HTMLReport) - - - - -

    - -
    - -

    Report the errors.

    - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.misc_reports.EventsReport (TableReport) - - - - -

    - -
    - -

    Produce a table of all the values of a particular event.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.reports.misc_reports.EventsReport.add_args(parser) - - - classmethod - - -

    - -
    - -

    Add arguments to parse for this report.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • parser – An instance of argparse.ArgumentParser.

    • -
    -
    -
    - Source code in beancount/reports/misc_reports.py -
    @classmethod
    -def add_args(cls, parser):
    -    parser.add_argument('-e', '--expr',
    -                        action='store', default=None,
    -                        help="A regexp to filer on which events to display.")
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.misc_reports.EventsReport.generate_table(self, entries, errors, options_map) - - -

    - -
    - -

    Render the report to a Table instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Table, that will get converted to another format.

    • -
    -
    -
    - Source code in beancount/reports/misc_reports.py -
    def generate_table(self, entries, errors, options_map):
    -    event_entries = []
    -    for entry in entries:
    -        if not isinstance(entry, data.Event):
    -            continue
    -        if self.args.expr and not re.match(self.args.expr, entry.type):
    -            continue
    -        event_entries.append(entry)
    -    return table.create_table([(entry.date, entry.type, entry.description)
    -                               for entry in event_entries],
    -                              [(0, "Date", datetime.date.isoformat),
    -                               (1, "Type"),
    -                               (2, "Description")])
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.misc_reports.NoopReport (Report) - - - - -

    - -
    - -

    Report nothing.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.misc_reports.PrintReport (Report) - - - - -

    - -
    - -

    Print out the entries.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.misc_reports.StatsDirectivesReport (TableReport) - - - - -

    - -
    - -

    Render statistics on each directive type, the number of entries by type.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.reports.misc_reports.StatsDirectivesReport.generate_table(self, entries, _, __) - - -

    - -
    - -

    Render the report to a Table instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Table, that will get converted to another format.

    • -
    -
    -
    - Source code in beancount/reports/misc_reports.py -
    def generate_table(self, entries, _, __):
    -    entries_by_type = misc_utils.groupby(lambda entry: type(entry).__name__,
    -                                         entries)
    -    nb_entries_by_type = {name: len(entries)
    -                          for name, entries in entries_by_type.items()}
    -    rows = sorted(nb_entries_by_type.items(),
    -                  key=lambda x: x[1], reverse=True)
    -    rows = [(name, str(count)) for (name, count) in rows]
    -    rows.append(('~Total~', str(len(entries))))
    -
    -    return table.create_table(rows, [(0, 'Type'),
    -                                     (1, 'Num Entries', '{:>}'.format)])
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.misc_reports.StatsPostingsReport (TableReport) - - - - -

    - -
    - -

    Render the number of postings for each account.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.reports.misc_reports.StatsPostingsReport.generate_table(self, entries, _, __) - - -

    - -
    - -

    Render the report to a Table instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Table, that will get converted to another format.

    • -
    -
    -
    - Source code in beancount/reports/misc_reports.py -
    def generate_table(self, entries, _, __):
    -    all_postings = [posting
    -                    for entry in entries
    -                    if isinstance(entry, data.Transaction)
    -                    for posting in entry.postings]
    -    postings_by_account = misc_utils.groupby(lambda posting: posting.account,
    -                                             all_postings)
    -    nb_postings_by_account = {key: len(postings)
    -                              for key, postings in postings_by_account.items()}
    -    rows = sorted(nb_postings_by_account.items(), key=lambda x: x[1], reverse=True)
    -    rows = [(name, str(count)) for (name, count) in rows]
    -    rows.append(('~Total~', str(sum(nb_postings_by_account.values()))))
    -
    -    return table.create_table(rows, [(0, 'Account'),
    -                                     (1, 'Num Postings', '{:>}'.format)])
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.price_reports - - - -

    - -
    - -

    Miscellaneous report classes.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.reports.price_reports.CommoditiesReport (TableReport) - - - - -

    - -
    - -

    Print out a list of commodities.

    - - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.reports.price_reports.CommoditiesReport.generate_table(self, entries, errors, options_map) - - -

    - -
    - -

    Render the report to a Table instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Table, that will get converted to another format.

    • -
    -
    -
    - Source code in beancount/reports/price_reports.py -
    def generate_table(self, entries, errors, options_map):
    -    price_map = prices.build_price_map(entries)
    -    return table.create_table([(base_quote,)
    -                               for base_quote in sorted(price_map.forward_pairs)],
    -                              [(0, "Base/Quote", self.formatter.render_commodity)])
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.price_reports.CommodityLifetimes (TableReport) - - - - -

    - -
    - -

    Print out a list of lifetimes of each commodity.

    - - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.reports.price_reports.CommodityLifetimes.add_args(parser) - - - classmethod - - -

    - -
    - -

    Add arguments to parse for this report.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • parser – An instance of argparse.ArgumentParser.

    • -
    -
    -
    - Source code in beancount/reports/price_reports.py -
    @classmethod
    -def add_args(cls, parser):
    -    parser.add_argument('-c', '--compress-days', type=int,
    -                        action='store', default=None,
    -                        help="The number of unused days to allow for continuous usage.")
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.price_reports.CommodityPricesReport (TableReport) - - - - -

    - -
    - -

    Print all the prices for a particular commodity.

    - - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.reports.price_reports.CommodityPricesReport.add_args(parser) - - - classmethod - - -

    - -
    - -

    Add arguments to parse for this report.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • parser – An instance of argparse.ArgumentParser.

    • -
    -
    -
    - Source code in beancount/reports/price_reports.py -
    @classmethod
    -def add_args(cls, parser):
    -    parser.add_argument('-c', '--commodity', '--currency',
    -                        action='store', default=None,
    -                        help="The commodity pair to display.")
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.price_reports.CommodityPricesReport.generate_table(self, entries, errors, options_map) - - -

    - -
    - -

    Render the report to a Table instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Table, that will get converted to another format.

    • -
    -
    -
    - Source code in beancount/reports/price_reports.py -
    def generate_table(self, entries, errors, options_map):
    -    date_rates = self.get_date_rates(entries)
    -    return table.create_table(date_rates,
    -                              [(0, "Date", datetime.date.isoformat),
    -                               (1, "Price", '{:.5f}'.format)])
    -
    -
    -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.price_reports.PriceDBReport (Report) - - - - -

    - -
    - -

    Print out the normalized price entries from the price db. -Normalized means that we print prices in the most common (base, quote) order. -This can be used to rebuild a prices database without having to share the -entire ledger file.

    -

    Only the forward prices are printed; which (base, quote) pair is selected is -selected based on the most common occurrence between (base, quote) and -(quote, base). This is done in the price map.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.price_reports.PricesReport (Report) - - - - -

    - -
    - -

    Print out the unnormalized price entries that we input. -Unnormalized means that we may render both (base,quote) and (quote,base). -This can be used to rebuild a prices database without having to share the -entire ledger file.

    -

    Note: this type of report should be removed once we have filtering on -directive type, this is simply the 'print' report with type:price. Maybe -rename the 'pricedb' report to just 'prices' for simplicity's sake.

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.price_reports.TickerReport (TableReport) - - - - -

    - -
    - -

    Print a parseable mapping of (base, quote, ticker, name) for all commodities.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.reports.price_reports.TickerReport.generate_table(self, entries, errors, options_map) - - -

    - -
    - -

    Render the report to a Table instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to render.

    • -
    • errors – A list of errors that occurred during processing.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An instance of Table, that will get converted to another format.

    • -
    -
    -
    - Source code in beancount/reports/price_reports.py -
    def generate_table(self, entries, errors, options_map):
    -    commodity_map = getters.get_commodity_map(entries)
    -    ticker_info = getters.get_values_meta(commodity_map, 'name', 'ticker', 'quote')
    -
    -    price_rows = [
    -        (currency, cost_currency, ticker, name)
    -        for currency, (name, ticker, cost_currency) in sorted(ticker_info.items())
    -        if ticker]
    -
    -    return table.create_table(price_rows,
    -                              [(0, "Currency"),
    -                               (1, "Cost-Currency"),
    -                               (2, "Symbol"),
    -                               (3, "Name")])
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.report - - - -

    - -
    - -

    Produce various custom implemented reports.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.reports.report.ListFormatsAction (Action) - - - - -

    - -
    - -

    An argparse action that prints all supported formats (for each report).

    - - - - -
    - - - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.reports.report.ListReportsAction (Action) - - - - -

    - -
    - -

    An argparse action that just prints the list of reports and exits.

    - - - - -
    - - - - - - - - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.reports.report.get_all_reports() - - -

    - -
    - -

    Return all report classes.

    - - - - - - - - - - - - -
    Returns: -
      -
    • A list of all available report classes.

    • -
    -
    -
    - Source code in beancount/reports/report.py -
    def get_all_reports():
    -    """Return all report classes.
    -
    -    Returns:
    -      A list of all available report classes.
    -    """
    -    return functools.reduce(operator.add,
    -                            map(lambda module: module.__reports__,
    -                                [balance_reports,
    -                                 journal_reports,
    -                                 holdings_reports,
    -                                 export_reports,
    -                                 price_reports,
    -                                 misc_reports,
    -                                 convert_reports]))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.report.get_list_report_string(only_report=None) - - -

    - -
    - -

    Return a formatted string for the list of supported reports.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • only_report – A string, the name of a single report to produce the help -for. If not specified, list all the available reports.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A help string, or None, if 'only_report' was provided and is not a valid -report name.

    • -
    -
    -
    - Source code in beancount/reports/report.py -
    def get_list_report_string(only_report=None):
    -    """Return a formatted string for the list of supported reports.
    -
    -    Args:
    -      only_report: A string, the name of a single report to produce the help
    -        for. If not specified, list all the available reports.
    -    Returns:
    -      A help string, or None, if 'only_report' was provided and is not a valid
    -      report name.
    -    """
    -    oss = io.StringIO()
    -    num_reports = 0
    -    for report_class in get_all_reports():
    -        # Filter the name
    -        if only_report and only_report not in report_class.names:
    -            continue
    -
    -        # Get the textual description.
    -        description = textwrap.fill(
    -            re.sub(' +', ' ', ' '.join(report_class.__doc__.splitlines())),
    -            initial_indent="    ",
    -            subsequent_indent="    ",
    -            width=80)
    -
    -        # Get the report's arguments.
    -        parser = version.ArgumentParser()
    -        report_ = report_class
    -        report_class.add_args(parser)
    -
    -        # Get the list of supported formats.
    -        ## formats = report_class.get_supported_formats()
    -
    -        oss.write('{}:\n{}\n'.format(','.join(report_.names),
    -                                     description))
    -        num_reports += 1
    -
    -    if not num_reports:
    -        return None
    -    return oss.getvalue()
    -
    -
    -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.table - - - -

    - -
    - -

    Table rendering.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.reports.table.Table (tuple) - - - - -

    - -
    - -

    Table(columns, header, body)

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.reports.table.Table.__getnewargs__(self) - - - special - - -

    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/reports/table.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.table.Table.__new__(_cls, columns, header, body) - - - special - staticmethod - - -

    - -
    - -

    Create new instance of Table(columns, header, body)

    - -
    - -
    - - - -
    - - - -

    -beancount.reports.table.Table.__repr__(self) - - - special - - -

    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/reports/table.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.reports.table.attribute_to_title(fieldname) - - -

    - -
    - -

    Convert programming id into readable field name.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • fieldname – A string, a programming ids, such as 'book_value'.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A readable string, such as 'Book Value.'

    • -
    -
    -
    - Source code in beancount/reports/table.py -
    def attribute_to_title(fieldname):
    -    """Convert programming id into readable field name.
    -
    -    Args:
    -      fieldname: A string, a programming ids, such as 'book_value'.
    -    Returns:
    -      A readable string, such as 'Book Value.'
    -   """
    -    return fieldname.replace('_', ' ').title()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.table.compute_table_widths(rows) - - -

    - -
    - -

    Compute the max character widths of a list of rows.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • rows – A list of rows, which are sequences of strings.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of integers, the maximum widths required to render the columns of -this table.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • IndexError – If the rows are of different lengths.

    • -
    -
    -
    - Source code in beancount/reports/table.py -
    def compute_table_widths(rows):
    -    """Compute the max character widths of a list of rows.
    -
    -    Args:
    -      rows: A list of rows, which are sequences of strings.
    -    Returns:
    -      A list of integers, the maximum widths required to render the columns of
    -      this table.
    -    Raises:
    -      IndexError: If the rows are of different lengths.
    -    """
    -    row_iter = iter(rows)
    -    first_row = next(row_iter)
    -    num_columns = len(first_row)
    -    column_widths = [len(cell) for cell in first_row]
    -    for row in row_iter:
    -        for i, cell in enumerate(row):
    -            if not isinstance(cell, str):
    -                cell = str(cell)
    -            cell_len = len(cell)
    -            if cell_len > column_widths[i]:
    -                column_widths[i] = cell_len
    -        if i+1 != num_columns:
    -            raise IndexError("Invalid number of rows")
    -    return column_widths
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.table.create_table(rows, field_spec=None) - - -

    - -
    - -

    Convert a list of tuples to an table report object.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • rows – A list of tuples.

    • -
    • field_spec – A list of strings, or a list of -(FIELDNAME-OR-INDEX, HEADER, FORMATTER-FUNCTION) -triplets, that selects a subset of the fields is to be rendered as well -as their ordering. If this is a dict, the values are functions to call -on the fields to render them. If a function is set to None, we will just -call str() on the field.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A Table instance.

    • -
    -
    -
    - Source code in beancount/reports/table.py -
    def create_table(rows, field_spec=None):
    -    """Convert a list of tuples to an table report object.
    -
    -    Args:
    -      rows: A list of tuples.
    -      field_spec: A list of strings, or a list of
    -        (FIELDNAME-OR-INDEX, HEADER, FORMATTER-FUNCTION)
    -        triplets, that selects a subset of the fields is to be rendered as well
    -        as their ordering. If this is a dict, the values are functions to call
    -        on the fields to render them. If a function is set to None, we will just
    -        call str() on the field.
    -    Returns:
    -      A Table instance.
    -    """
    -    # Normalize field_spec to a dict.
    -    if field_spec is None:
    -        namedtuple_class = type(rows[0])
    -        field_spec = [(field, None, None)
    -                      for field in namedtuple_class._fields]
    -
    -    elif isinstance(field_spec, (list, tuple)):
    -        new_field_spec = []
    -        for field in field_spec:
    -            if isinstance(field, tuple):
    -                assert len(field) <= 3, field
    -                if len(field) == 1:
    -                    field = field[0]
    -                    new_field_spec.append((field, None, None))
    -                elif len(field) == 2:
    -                    field, header = field
    -                    new_field_spec.append((field, header, None))
    -                elif len(field) == 3:
    -                    new_field_spec.append(field)
    -            else:
    -                if isinstance(field, str):
    -                    title = attribute_to_title(field)
    -                elif isinstance(field, int):
    -                    title = "Field {}".format(field)
    -                else:
    -                    raise ValueError("Invalid type for column name")
    -                new_field_spec.append((field, title, None))
    -
    -        field_spec = new_field_spec
    -
    -    # Ensure a nicely formatted header.
    -    field_spec = [((name, attribute_to_title(name), formatter)
    -                   if header_ is None
    -                   else (name, header_, formatter))
    -                  for (name, header_, formatter) in field_spec]
    -
    -    assert isinstance(field_spec, list), field_spec
    -    assert all(len(x) == 3 for x in field_spec), field_spec
    -
    -    # Compute the column names.
    -    columns = [name for (name, _, __) in field_spec]
    -
    -    # Compute the table header.
    -    header = [header_column for (_, header_column, __) in field_spec]
    -
    -    # Compute the table body.
    -    body = []
    -    for row in rows:
    -        body_row = []
    -        for name, _, formatter in field_spec:
    -            if isinstance(name, str):
    -                value = getattr(row, name)
    -            elif isinstance(name, int):
    -                value = row[name]
    -            else:
    -                raise ValueError("Invalid type for column name")
    -            if value is not None:
    -                if formatter is not None:
    -                    value = formatter(value)
    -                else:
    -                    value = str(value)
    -            else:
    -                value = ''
    -            body_row.append(value)
    -        body.append(body_row)
    -
    -    return Table(columns, header, body)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.table.render_table(table_, output, output_format, css_id=None, css_class=None) - - -

    - -
    - -

    Render the given table to the output file object in the requested format.

    -

    The table gets written out to the 'output' file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • table_ – An instance of Table.

    • -
    • output – A file object you can write to.

    • -
    • output_format – A string, the format to write the table to, -either 'csv', 'txt' or 'html'.

    • -
    • css_id – A string, an optional CSS id for the table object (only used for HTML).

    • -
    • css_class – A string, an optional CSS class for the table object (only used for HTML).

    • -
    -
    -
    - Source code in beancount/reports/table.py -
    def render_table(table_, output, output_format, css_id=None, css_class=None):
    -    """Render the given table to the output file object in the requested format.
    -
    -    The table gets written out to the 'output' file.
    -
    -    Args:
    -      table_: An instance of Table.
    -      output: A file object you can write to.
    -      output_format: A string, the format to write the table to,
    -        either 'csv', 'txt' or 'html'.
    -      css_id: A string, an optional CSS id for the table object (only used for HTML).
    -      css_class: A string, an optional CSS class for the table object (only used for HTML).
    -    """
    -    if output_format in ('txt', 'text'):
    -        text = table_to_text(table_, "  ", formats={'*': '>', 'account': '<'})
    -        output.write(text)
    -
    -    elif output_format in ('csv',):
    -        table_to_csv(table_, file=output)
    -
    -    elif output_format in ('htmldiv', 'html'):
    -
    -        if output_format == 'html':
    -            output.write('<html>\n')
    -            output.write('<body>\n')
    -
    -        output.write('<div id="{}">\n'.format(css_id) if css_id else '<div>\n')
    -        classes = [css_class] if css_class else None
    -        table_to_html(table_, file=output, classes=classes)
    -        output.write('</div>\n')
    -
    -        if output_format == 'html':
    -            output.write('</body>\n')
    -            output.write('</html>\n')
    -
    -    else:
    -        raise NotImplementedError("Unsupported format: {}".format(output_format))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.table.table_to_csv(table, file=None, **kwargs) - - -

    - -
    - -

    Render a Table to a CSV file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • table – An instance of a Table.

    • -
    • file – A file object to write to. If no object is provided, this -function returns a string.

    • -
    • **kwargs – Optional arguments forwarded to csv.writer().

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, the rendered table, or None, if a file object is provided -to write to.

    • -
    -
    -
    - Source code in beancount/reports/table.py -
    def table_to_csv(table, file=None, **kwargs):
    -    """Render a Table to a CSV file.
    -
    -    Args:
    -      table: An instance of a Table.
    -      file: A file object to write to. If no object is provided, this
    -        function returns a string.
    -      **kwargs: Optional arguments forwarded to csv.writer().
    -    Returns:
    -      A string, the rendered table, or None, if a file object is provided
    -      to write to.
    -    """
    -    output_file = file or io.StringIO()
    -
    -    writer = csv.writer(output_file, **kwargs)
    -    if table.header:
    -        writer.writerow(table.header)
    -    writer.writerows(table.body)
    -
    -    if not file:
    -        return output_file.getvalue()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.table.table_to_html(table, classes=None, file=None) - - -

    - -
    - -

    Render a Table to HTML.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • table – An instance of a Table.

    • -
    • classes – A list of string, CSS classes to set on the table.

    • -
    • file – A file object to write to. If no object is provided, this -function returns a string.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, the rendered table, or None, if a file object is provided -to write to.

    • -
    -
    -
    - Source code in beancount/reports/table.py -
    def table_to_html(table, classes=None, file=None):
    -    """Render a Table to HTML.
    -
    -    Args:
    -      table: An instance of a Table.
    -      classes: A list of string, CSS classes to set on the table.
    -      file: A file object to write to. If no object is provided, this
    -        function returns a string.
    -    Returns:
    -      A string, the rendered table, or None, if a file object is provided
    -      to write to.
    -    """
    -    # Initialize file.
    -    oss = io.StringIO() if file is None else file
    -    oss.write('<table class="{}">\n'.format(' '.join(classes or [])))
    -
    -    # Render header.
    -    if table.header:
    -        oss.write('  <thead>\n')
    -        oss.write('    <tr>\n')
    -        for header in table.header:
    -            oss.write('      <th>{}</th>\n'.format(header))
    -        oss.write('    </tr>\n')
    -        oss.write('  </thead>\n')
    -
    -    # Render body.
    -    oss.write('  <tbody>\n')
    -    for row in table.body:
    -        oss.write('    <tr>\n')
    -        for cell in row:
    -            oss.write('      <td>{}</td>\n'.format(cell))
    -        oss.write('    </tr>\n')
    -    oss.write('  </tbody>\n')
    -
    -    # Render footer.
    -    oss.write('</table>\n')
    -    if file is None:
    -        return oss.getvalue()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.table.table_to_text(table, column_interspace=' ', formats=None) - - -

    - -
    - -

    Render a Table to ASCII text.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • table – An instance of a Table.

    • -
    • column_interspace – A string to render between the columns as spacer.

    • -
    • formats – An optional dict of column name to a format character that gets -inserted in a format string specified, like this (where '<char>' is): -{:<char><width>}. A key of '' will provide a default value, like -this, for example: (... formats={'': '>'}).

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, the rendered text table.

    • -
    -
    -
    - Source code in beancount/reports/table.py -
    def table_to_text(table,
    -                  column_interspace=" ",
    -                  formats=None):
    -    """Render a Table to ASCII text.
    -
    -    Args:
    -      table: An instance of a Table.
    -      column_interspace: A string to render between the columns as spacer.
    -      formats: An optional dict of column name to a format character that gets
    -        inserted in a format string specified, like this (where '<char>' is):
    -        {:<char><width>}. A key of '*' will provide a default value, like
    -        this, for example: (... formats={'*': '>'}).
    -    Returns:
    -      A string, the rendered text table.
    -    """
    -    column_widths = compute_table_widths(itertools.chain([table.header],
    -                                                         table.body))
    -
    -    # Insert column format chars and compute line formatting string.
    -    column_formats = []
    -    if formats:
    -        default_format = formats.get('*', None)
    -    for column, width in zip(table.columns, column_widths):
    -        if column and formats:
    -            format_ = formats.get(column, default_format)
    -            if format_:
    -                column_formats.append("{{:{}{:d}}}".format(format_, width))
    -            else:
    -                column_formats.append("{{:{:d}}}".format(width))
    -        else:
    -            column_formats.append("{{:{:d}}}".format(width))
    -
    -    line_format = column_interspace.join(column_formats) + "\n"
    -    separator = line_format.format(*[('-' * width) for width in column_widths])
    -
    -    # Render the header.
    -    oss = io.StringIO()
    -    if table.header:
    -        oss.write(line_format.format(*table.header))
    -
    -    # Render the body.
    -    oss.write(separator)
    -    for row in table.body:
    -        oss.write(line_format.format(*row))
    -    oss.write(separator)
    -
    -    return oss.getvalue()
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.reports.tree_table - - - -

    - -
    - -

    Routines to render an HTML table with a tree of accounts.

    - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.reports.tree_table.is_account_active(real_account) - - -

    - -
    - -

    Return true if the account should be rendered. An active account has -at least one directive that is not an Open directive.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • real_account – An instance of RealAccount.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A boolean, true if the account is active, according to the definition above.

    • -
    -
    -
    - Source code in beancount/reports/tree_table.py -
    def is_account_active(real_account):
    -    """Return true if the account should be rendered. An active account has
    -    at least one directive that is not an Open directive.
    -
    -    Args:
    -      real_account: An instance of RealAccount.
    -    Returns:
    -      A boolean, true if the account is active, according to the definition above.
    -    """
    -    for entry in real_account.txn_postings:
    -        if isinstance(entry, data.Open):
    -            continue
    -        return True
    -    return False
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.tree_table.table_of_balances(real_root, price_map, price_date, operating_currencies, formatter, classes=None) - - -

    - -
    - -

    Render a tree table with the balance of each accounts.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • real_root – A RealAccount node, the root node to render.

    • -
    • price_map – A prices map, a built by build_price_map.

    • -
    • price_date – A datetime.date instance, the date at which to compute market value.

    • -
    • operating_currencies – A list of strings, the operating currencies to render -to their own dedicated columns.

    • -
    • formatter – A object used to render account names and other links.

    • -
    • classes – A list of strings, the CSS classes to attach to the rendered -top-level table object.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string with HTML contents, the rendered table.

    • -
    -
    -
    - Source code in beancount/reports/tree_table.py -
    def table_of_balances(real_root, price_map, price_date,
    -                      operating_currencies, formatter,
    -                      classes=None):
    -    """Render a tree table with the balance of each accounts.
    -
    -    Args:
    -      real_root: A RealAccount node, the root node to render.
    -      price_map: A prices map, a built by build_price_map.
    -      price_date: A datetime.date instance, the date at which to compute market value.
    -      operating_currencies: A list of strings, the operating currencies to render
    -        to their own dedicated columns.
    -      formatter: A object used to render account names and other links.
    -      classes: A list of strings, the CSS classes to attach to the rendered
    -        top-level table object.
    -    Returns:
    -      A string with HTML contents, the rendered table.
    -    """
    -    header = ['Account'] + operating_currencies + ['Other']
    -
    -    # Pre-calculate which accounts should be rendered.
    -    real_active = realization.filter(real_root, is_account_active)
    -    if real_active:
    -        active_set = {real_account.account
    -                      for real_account in realization.iter_children(real_active)}
    -    else:
    -        active_set = set()
    -
    -    balance_totals = inventory.Inventory()
    -    oss = io.StringIO()
    -    classes = list(classes) if classes else []
    -    classes.append('fullwidth')
    -    for real_account, cells, row_classes in tree_table(oss, real_root, formatter,
    -                                                       header, classes):
    -
    -        if real_account is TOTALS_LINE:
    -            line_balance = balance_totals
    -            row_classes.append('totals')
    -        else:
    -            # Check if this account has had activity; if not, skip rendering it.
    -            if (real_account.account not in active_set and
    -                not account_types.is_root_account(real_account.account)):
    -                continue
    -
    -            if real_account.account is None:
    -                row_classes.append('parent-node')
    -
    -            # For each account line, get the final balance of the account at
    -            # latest market value.
    -            line_balance = real_account.balance.reduce(convert.get_value, price_map)
    -
    -            # Update the total balance for the totals line.
    -            balance_totals.add_inventory(line_balance)
    -
    -        # Extract all the positions that the user has identified as operating
    -        # currencies to their own subinventories.
    -        ccy_dict = line_balance.segregate_units(operating_currencies)
    -
    -        # FIXME: This little algorithm is inefficient; rewrite it.
    -        for currency in operating_currencies:
    -            units = ccy_dict[currency].get_currency_units(currency)
    -            cells.append(formatter.render_number(units.number, units.currency)
    -                         if units.number != ZERO
    -                         else '')
    -
    -        # Render all the rest of the inventory in the last cell.
    -        if None in ccy_dict:
    -            ccy_balance = ccy_dict[None]
    -            last_cell = '<br/>'.join(formatter.render_amount(pos.units)
    -                                     for pos in sorted(ccy_balance))
    -        else:
    -            last_cell = ''
    -        cells.append(last_cell)
    -
    -    return oss.getvalue()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.reports.tree_table.tree_table(oss, real_account, formatter, header=None, classes=None) - - -

    - -
    - -

    Generator to a tree of accounts as an HTML table.

    -

    This yields each real_account object in turn and a list object used to -provide the values for the various columns to render.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • oss – a io.StringIO instance, into which we will render the HTML.

    • -
    • real_account – an instance of a RealAccount node.

    • -
    • formatter – A object used to render account names and other links.

    • -
    • header – a list of header columns to render. The first column is special, - and is used for the account name.

    • -
    • classes – a list of CSS class strings to apply to the table element.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A generator of tuples of - real_account – An instance of RealAccount to render as a row - cells: A mutable list object to accumulate values for each column you - want to render. - row_classes: A mutable list object to accumulate css classes that you - want to add for the row.

      -

      You need to append to the given 'cells' object; if you don't append -anything, this tells this routine to skip rendering the row. On the very -last line, the 'real_account' object will be a special sentinel value to -indicate that it is meant to render the totals line: TOTALS_LINE.

    • -
    -
    -
    - Source code in beancount/reports/tree_table.py -
    def tree_table(oss, real_account, formatter, header=None, classes=None):
    -    """Generator to a tree of accounts as an HTML table.
    -
    -    This yields each real_account object in turn and a list object used to
    -    provide the values for the various columns to render.
    -
    -    Args:
    -      oss: a io.StringIO instance, into which we will render the HTML.
    -      real_account: an instance of a RealAccount node.
    -      formatter: A object used to render account names and other links.
    -      header: a list of header columns to render. The first column is special,
    -              and is used for the account name.
    -      classes: a list of CSS class strings to apply to the table element.
    -    Returns:
    -      A generator of tuples of
    -        real_account: An instance of RealAccount to render as a row
    -        cells: A mutable list object to accumulate values for each column you
    -          want to render.
    -        row_classes: A mutable list object to accumulate css classes that you
    -          want to add for the row.
    -
    -      You need to append to the given 'cells' object; if you don't append
    -      anything, this tells this routine to skip rendering the row. On the very
    -      last line, the 'real_account' object will be a special sentinel value to
    -      indicate that it is meant to render the totals line: TOTALS_LINE.
    -    """
    -    write = lambda data: (oss.write(data), oss.write('\n'))
    -
    -    classes = list(classes) if classes else []
    -    classes.append('tree-table')
    -    write('<table class="{}">'.format(' '.join(classes) if classes else ''))
    -
    -    if header:
    -        write('<thead>')
    -        write('<tr>')
    -        header_iter = iter(header)
    -        write('<th class="first">{}</th>'.format(next(header_iter)))
    -        for column in header_iter:
    -            write('<th>{}</th>'.format(column))
    -        write('</tr>')
    -        write('</thead>')
    -
    -    # Note: This code eventually should be reworked to be agnostic regarding
    -    # text or HTML output rendering.
    -    lines = realization.dump(real_account)
    -
    -    # Yield with a None for the final line.
    -    lines.append((None, None, TOTALS_LINE))
    -
    -    for first_line, unused_cont_line, real_acc in lines:
    -        # Let the caller fill in the data to be rendered by adding it to a list
    -        # objects. The caller may return multiple cell values; this will create
    -        # multiple columns.
    -        cells = []
    -        row_classes = []
    -        yield real_acc, cells, row_classes
    -
    -        # If no cells were added, skip the line. If you want to render empty
    -        # cells, append empty strings.
    -        if not cells:
    -            continue
    -
    -        # Render the row
    -        write('<tr class="{}">'.format(' '.join(row_classes)))
    -
    -        if real_acc is TOTALS_LINE:
    -            label = '<span class="totals-label"></span>'
    -        else:
    -            label = (formatter.render_account(real_acc.account)
    -                     if formatter
    -                     else real_acc.account)
    -
    -        write('<td class="tree-node-name">{}</td>'.format(label))
    -
    -        # Add columns for each value rendered.
    -        for cell in cells:
    -            write('<td class="num">{}</td>'.format(cell))
    -
    -        write('</tr>')
    -
    -    write('</table>')
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - - -
    - -
    - -
    - -
    -
    - - -
    -
    - -
    - -
    - -
    - - - - « Previous - - - Next » - - -
    - - - - - - - - diff --git a/api_reference/beancount.scripts.html b/api_reference/beancount.scripts.html index 9001c832..d03e68fb 100644 --- a/api_reference/beancount.scripts.html +++ b/api_reference/beancount.scripts.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -152,8 +154,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -162,36 +162,10 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts @@ -517,24 +397,22 @@

    beancount.scripts -

    - beancount.scripts.bake +

    + beancount.scripts.deps -

    +

    -

    Bake a Beancount input file's web files to a directory hierarchy.

    -

    You provide a Beancount filename, an output directory, and this script -runs a server and a scraper that puts all the files in the directory, -and if your output name has an archive suffix, we automatically the -fetched directory contents to the archive and delete them.

    +

    Check the installation dependencies and report the version numbers of each.

    +

    This is meant to be used as an error diagnostic tool.

    @@ -549,22 +427,19 @@

    - - -
    -

    -beancount.scripts.bake.archive(command_template, directory, archive, quiet=False) +

    +beancount.scripts.deps.check_cdecimal() -

    +

    -

    Archive the directory to the given tar/gz archive filename.

    +

    Check that Python 3.3 or above is installed.

    @@ -573,74 +448,46 @@

    - - - - -
    Parameters: -
      -
    • command_template – A string, the command template to format with in order -to compute the command to run.

    • -
    • directory – A string, the name of the directory to archive.

    • -
    • archive – A string, the name of the file to output.

    • -
    • quiet – A boolean, True to suppress output.

    • -
    -
    - - - - - - - - - + +
    Exceptions: + Returns:
      -
    • IOError – if the directory does not exist or if the archive name already

    • +
    • A triple of (package-name, version-number, sufficient) as per +check_dependencies().

    -
    - Source code in beancount/scripts/bake.py -
    def archive(command_template, directory, archive, quiet=False):
    -    """Archive the directory to the given tar/gz archive filename.
    -
    -    Args:
    -      command_template: A string, the command template to format with in order
    -        to compute the command to run.
    -      directory: A string, the name of the directory to archive.
    -      archive: A string, the name of the file to output.
    -      quiet: A boolean, True to suppress output.
    -    Raises:
    -      IOError: if the directory does not exist or if the archive name already
    -      exists.
    -
    -    """
    -    directory = path.abspath(directory)
    -    archive = path.abspath(archive)
    -    if not path.exists(directory):
    -        raise IOError("Directory to archive '{}' does not exist".format(
    -            directory))
    -    if path.exists(archive):
    -        raise IOError("Output archive name '{}' already exists".format(
    -            archive))
    -
    -    command = command_template.format(directory=directory,
    -                                      dirname=path.dirname(directory),
    -                                      basename=path.basename(directory),
    -                                      archive=archive)
    -
    -    pipe = subprocess.Popen(shlex.split(command),
    -                            shell=False,
    -                            cwd=path.dirname(directory),
    -                            stdout=subprocess.PIPE if quiet else None,
    -                            stderr=subprocess.PIPE if quiet else None)
    -    _, _ = pipe.communicate()
    -    if pipe.returncode != 0:
    -        raise OSError("Archive failure")
    -
    + Source code in beancount/scripts/deps.py +
    def check_cdecimal():
    +    """Check that Python 3.3 or above is installed.
    +
    +    Returns:
    +      A triple of (package-name, version-number, sufficient) as per
    +      check_dependencies().
    +    """
    +    # Note: this code mirrors and should be kept in-sync with that at the top of
    +    # beancount.core.number.
    +
    +    # Try the built-in installation.
    +    import decimal
    +
    +    if is_fast_decimal(decimal):
    +        return ("cdecimal", "{} (built-in)".format(decimal.__version__), True)
    +
    +    # Try an explicitly installed version.
    +    try:
    +        import cdecimal
    +
    +        if is_fast_decimal(cdecimal):
    +            return ("cdecimal", getattr(cdecimal, "__version__", "OKAY"), True)
    +    except ImportError:
    +        pass
    +
    +    # Not found.
    +    return ("cdecimal", None, False)
    +
    @@ -652,15 +499,15 @@

    -

    -beancount.scripts.bake.archive_zip(directory, archive) +

    +beancount.scripts.deps.check_dependencies() -

    +
  • -

    Archive the directory to the given tar/gz archive filename.

    +

    Check the runtime dependencies and report their version numbers.

    @@ -669,45 +516,37 @@

    - - + +
    Parameters: + Returns:
      -
    • directory – A string, the name of the directory to archive.

    • -
    • archive – A string, the name of the file to output.

    • +
    • A list of pairs of (package-name, version-number, sufficient) whereby if a +package has not been installed, its 'version-number' will be set to None. +Otherwise, it will be a string with the version number in it. 'sufficient' +will be True if the version if sufficient for this installation of +Beancount.

    -
    - Source code in beancount/scripts/bake.py -
    def archive_zip(directory, archive):
    -    """Archive the directory to the given tar/gz archive filename.
    -
    -    Args:
    -      directory: A string, the name of the directory to archive.
    -      archive: A string, the name of the file to output.
    -    """
    -    # Figure out optimal level of compression among the supported ones in this
    -    # installation.
    -    for spec, compression in [
    -            ('lzma', zipfile.ZIP_LZMA),
    -            ('bz2', zipfile.ZIP_BZIP2),
    -            ('zlib', zipfile.ZIP_DEFLATED)]:
    -        if importlib.util.find_spec(spec):
    -            zip_compression = compression
    -            break
    -    else:
    -        # Default is no compression.
    -        zip_compression = zipfile.ZIP_STORED
    -
    -    with file_utils.chdir(directory), zipfile.ZipFile(
    -            archive, 'w', compression=zip_compression) as archfile:
    -        for root, dirs, files in os.walk(directory):
    -            for filename in files:
    -                relpath = path.relpath(path.join(root, filename), directory)
    -                archfile.write(relpath)
    -
    + Source code in beancount/scripts/deps.py +
    def check_dependencies():
    +    """Check the runtime dependencies and report their version numbers.
    +
    +    Returns:
    +      A list of pairs of (package-name, version-number, sufficient) whereby if a
    +      package has not been installed, its 'version-number' will be set to None.
    +      Otherwise, it will be a string with the version number in it. 'sufficient'
    +      will be True if the version if sufficient for this installation of
    +      Beancount.
    +    """
    +    return [
    +        check_python(),
    +        check_cdecimal(),
    +        check_import("dateutil"),
    +    ]
    +
    @@ -719,15 +558,15 @@

    -

    -beancount.scripts.bake.bake_to_directory(webargs, output_dir, render_all_pages=True) +

    +beancount.scripts.deps.check_import(package_name, min_version=None, module_name=None) -

    +

    -

    Serve and bake a Beancount's web to a directory.

    +

    Check that a particular module name is installed.

    @@ -739,11 +578,13 @@

    @@ -759,47 +600,49 @@

    Parameters:
      -
    • webargs – An argparse parsed options object with the web app arguments.

    • -
    • output_dir – A directory name. We don't check here whether it exists or not.

    • -
    • quiet – A boolean, True to suppress web server fetch log.

    • -
    • render_all_pages – If true, fetch the full set of pages, not just the subset that -is palatable.

    • +
    • package_name – A string, the name of the package and module to be +imported to verify this works. This should have a version +attribute on it.

    • +
    • min_version – If not None, a string, the minimum version number +we require.

    • +
    • module_name – The name of the module to import if it differs from the +package name.

    Returns:
      -
    • True on success, False otherwise.

    • +
    • A triple of (package-name, version-number, sufficient) as per +check_dependencies().

    - Source code in beancount/scripts/bake.py -
    def bake_to_directory(webargs, output_dir, render_all_pages=True):
    -    """Serve and bake a Beancount's web to a directory.
    -
    -    Args:
    -      webargs: An argparse parsed options object with the web app arguments.
    -      output_dir: A directory name. We don't check here whether it exists or not.
    -      quiet: A boolean, True to suppress web server fetch log.
    -      render_all_pages: If true, fetch the full set of pages, not just the subset that
    -        is palatable.
    -    Returns:
    -      True on success, False otherwise.
    -    """
    -    callback = functools.partial(save_scraped_document, output_dir)
    -
    -    if render_all_pages:
    -        ignore_regexps = None
    -    else:
    -        regexps = [
    -            # Skip the context pages, too slow.
    -            r'/context/',
    -            # Skip the link pages, too slow.
    -            r'/link/',
    -            # Skip the component pages... too many.
    -            r'/view/component/',
    -            # Skip served documents.
    -            r'/.*/doc/',
    -            # Skip monthly pages.
    -            r'/view/year/\d\d\d\d/month/',
    -        ]
    -        ignore_regexps = '({})'.format('|'.join(regexps))
    -
    -    processed_urls, skipped_urls = web.scrape_webapp(webargs, callback, ignore_regexps)
    -
    + Source code in beancount/scripts/deps.py +
    def check_import(package_name, min_version=None, module_name=None):
    +    """Check that a particular module name is installed.
    +
    +    Args:
    +      package_name: A string, the name of the package and module to be
    +        imported to verify this works. This should have a __version__
    +        attribute on it.
    +      min_version: If not None, a string, the minimum version number
    +        we require.
    +      module_name: The name of the module to import if it differs from the
    +        package name.
    +    Returns:
    +      A triple of (package-name, version-number, sufficient) as per
    +      check_dependencies().
    +    """
    +    if module_name is None:
    +        module_name = package_name
    +    try:
    +        __import__(module_name)
    +        module = sys.modules[module_name]
    +        if min_version is not None:
    +            version = module.__version__
    +            assert isinstance(version, str)
    +            is_sufficient = (
    +                parse_version(version) >= parse_version(min_version)
    +                if min_version
    +                else True
    +            )
    +        else:
    +            version, is_sufficient = None, True
    +    except ImportError:
    +        version, is_sufficient = None, False
    +    return (package_name, version, is_sufficient)
    +
    @@ -807,37 +650,20 @@

    -
    -

    -beancount.scripts.bake.normalize_filename(url) +

    +beancount.scripts.deps.check_python() -

    +

    -

    Convert URL paths to filenames. Add .html extension if needed.

    +

    Check that Python 3.7 or above is installed.

    - - - - - - - - - - - -
    Parameters: -
      -
    • url – A string, the url to convert.

    • -
    -
    @@ -848,29 +674,28 @@

    Returns:
      -
    • A string, possibly with an extension appended.

    • +
    • A triple of (package-name, version-number, sufficient) as per +check_dependencies().

    - Source code in beancount/scripts/bake.py -
    def normalize_filename(url):
    -    """Convert URL paths to filenames. Add .html extension if needed.
    -
    -    Args:
    -      url: A string, the url to convert.
    -    Returns:
    -      A string, possibly with an extension appended.
    -    """
    -    if url.endswith('/'):
    -        return path.join(url, 'index.html')
    -    elif BINARY_MATCH(url):
    -        return url
    -    else:
    -        return url if url.endswith('.html') else (url + '.html')
    -
    + Source code in beancount/scripts/deps.py +
    def check_python():
    +    """Check that Python 3.7 or above is installed.
    +
    +    Returns:
    +      A triple of (package-name, version-number, sufficient) as per
    +      check_dependencies().
    +    """
    +    return (
    +        "python3",
    +        ".".join(map(str, sys.version_info[:3])),
    +        sys.version_info[:2] >= (3, 7),
    +    )
    +
    @@ -882,15 +707,19 @@

    -

    +beancount.scripts.deps.check_python_magic() -

    +

    -

    Make all the links in the contents string relative to an URL.

    +

    Check that a recent-enough version of python-magic is installed.

    +

    python-magic is an interface to libmagic, which is used by the 'file' tool +and UNIX to identify file types. Note that there are two Python wrappers +which provide the 'magic' import: python-magic and filemagic. The former is +what we need, which appears to be more recently maintained.

    @@ -899,35 +728,41 @@ - - + +
    Parameters: + Returns:
      -
    • html – An lxml document node.

    • -
    • current_url – A string, the URL of the current page, a path to. -a file or a directory. If the path represents a directory, the -path ends with a /.

    • +
    • A triple of (package-name, version-number, sufficient) as per +check_dependencies().

    -
    - Source code in beancount/scripts/bake.py -
    def relativize_links(html, current_url):
    -    """Make all the links in the contents string relative to an URL.
    -
    -    Args:
    -      html: An lxml document node.
    -      current_url: A string, the URL of the current page, a path to.
    -        a file or a directory. If the path represents a directory, the
    -        path ends with a /.
    -    """
    -    current_dir = path.dirname(current_url)
    -    for element, attribute, link, pos in lxml.html.iterlinks(html):
    -        if path.isabs(link):
    -            relative_link = path.relpath(normalize_filename(link), current_dir)
    -            element.set(attribute, relative_link)
    -
    + Source code in beancount/scripts/deps.py +
    def check_python_magic():
    +    """Check that a recent-enough version of python-magic is installed.
    +
    +    python-magic is an interface to libmagic, which is used by the 'file' tool
    +    and UNIX to identify file types. Note that there are two Python wrappers
    +    which provide the 'magic' import: python-magic and filemagic. The former is
    +    what we need, which appears to be more recently maintained.
    +
    +    Returns:
    +      A triple of (package-name, version-number, sufficient) as per
    +      check_dependencies().
    +    """
    +    try:
    +        import magic
    +
    +        # Check that python-magic and not filemagic is installed.
    +        if not hasattr(magic, "from_file"):
    +            # 'filemagic' is installed; install python-magic.
    +            raise ImportError
    +        return ("python-magic", "OK", True)
    +    except (ImportError, OSError):
    +        return ("python-magic", None, False)
    +
    @@ -939,48 +774,22 @@

    +beancount.scripts.deps.is_fast_decimal(decimal_module) -

    +

    -

    Convert a list of anchors (<a>) from an HTML tree to spans (<span>).

    +

    Return true if a fast C decimal implementation is installed.

    - - - - - - - - - - - -
    Parameters: -
      -
    • html – An lxml document node.

    • -
    • targets – A set of string, targets to be removed.

    • -
    -
    - Source code in beancount/scripts/bake.py -
    def remove_links(html, targets):
    -    """Convert a list of anchors (<a>) from an HTML tree to spans (<span>).
    -
    -    Args:
    -      html: An lxml document node.
    -      targets: A set of string, targets to be removed.
    -    """
    -    for element, attribute, link, pos in lxml.html.iterlinks(html):
    -        if link in targets:
    -            del element.attrib[attribute]
    -            element.tag = 'span'
    -            element.set('class', 'removed-link')
    -
    + Source code in beancount/scripts/deps.py +
    def is_fast_decimal(decimal_module):
    +    "Return true if a fast C decimal implementation is installed."
    +    return isinstance(decimal_module.Decimal().sqrt, types.BuiltinFunctionType)
    +
    @@ -992,17 +801,15 @@

    -beancount.scripts.bake.save_scraped_document(output_dir, url, response, contents, html_root, skipped_urls) +

    +beancount.scripts.deps.list_dependencies(file=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>) -

    +

    -

    Callback function to process a document being scraped.

    -

    This converts the document to have relative links and writes out the file to -the output directory.

    +

    Check the dependencies and produce a listing on the given file.

    @@ -1014,59 +821,31 @@

    Parameters:
      -
    • output_dir – A string, the output directory to write.

    • -
    • url – A string, the originally requested URL.

    • -
    • response – An http response as per urlopen.

    • -
    • contents – Bytes, the content of a response.

    • -
    • html_root – An lxml root node for the document, optionally. If this is provided, -this avoid you having to reprocess it (for performance reasons).

    • -
    • skipped_urls – A set of the links from the file that were skipped.

    • +
    • file – A file object to write the output to.

    - Source code in beancount/scripts/bake.py -
    def save_scraped_document(output_dir, url, response, contents, html_root, skipped_urls):
    -    """Callback function to process a document being scraped.
    -
    -    This converts the document to have relative links and writes out the file to
    -    the output directory.
    -
    -    Args:
    -      output_dir: A string, the output directory to write.
    -      url: A string, the originally requested URL.
    -      response: An http response as per urlopen.
    -      contents: Bytes, the content of a response.
    -      html_root: An lxml root node for the document, optionally. If this is provided,
    -        this avoid you having to reprocess it (for performance reasons).
    -      skipped_urls: A set of the links from the file that were skipped.
    -    """
    -    if response.status != 200:
    -        logging.error("Invalid status: %s", response.status)
    -
    -    # Ignore directories.
    -    if url.endswith('/'):
    -        return
    -
    -    # Note that we're saving the file under the non-redirected URL, because this
    -    # will have to be opened using files and there are no redirects that way.
    -
    -    if response.info().get_content_type() == 'text/html':
    -        if html_root is None:
    -            html_root = lxml.html.document_fromstring(contents)
    -        remove_links(html_root, skipped_urls)
    -        relativize_links(html_root, url)
    -        contents = lxml.html.tostring(html_root, method="html")
    -
    -    # Compute output filename and write out the relativized contents.
    -    output_filename = path.join(output_dir,
    -                                normalize_filename(url).lstrip('/'))
    -    os.makedirs(path.dirname(output_filename), exist_ok=True)
    -    with open(output_filename, 'wb') as outfile:
    -        outfile.write(contents)
    -
    + Source code in beancount/scripts/deps.py +
    def list_dependencies(file=sys.stderr):
    +    """Check the dependencies and produce a listing on the given file.
    +
    +    Args:
    +      file: A file object to write the output to.
    +    """
    +    print("Dependencies:")
    +    for package, version, sufficient in check_dependencies():
    +        print(
    +            "   {:16}: {} {}".format(
    +                package,
    +                version or "NOT INSTALLED",
    +                "(INSUFFICIENT)" if version and not sufficient else "",
    +            ),
    +            file=file,
    +        )
    +
    @@ -1074,166 +853,112 @@

    +
    -
    +

    +beancount.scripts.deps.parse_version(version_str) - - +

    +
    +

    Parse the version string into a comparable tuple.

    -
    +
    + Source code in beancount/scripts/deps.py +
    def parse_version(version_str: str) -> str:
    +    """Parse the version string into a comparable tuple."""
    +    return [int(v) for v in version_str.split(".")]
    +
    +
    +
    +
    -

    - beancount.scripts.check -

    -
    +
    -

    Parse, check and realize a beancount input file.

    -

    This also measures the time it takes to run all these steps.

    + + -
    +
    +

    + beancount.scripts.directories +

    +
    +

    Check that document directories mirror a list of accounts correctly.

    +
    -
    -
    -
    -
    +
    -

    - beancount.scripts.deps - -

    - -
    - -

    Check the installation dependencies and report the version numbers of each.

    -

    This is meant to be used as an error diagnostic tool.

    - - - -
    - - - - - - - - - - -
    +

    + +beancount.scripts.directories.ValidateDirectoryError (Exception) + -

    -beancount.scripts.deps.check_cdecimal() +

    +
    -

    +

    A directory validation error.

    -
    -

    Check that Python 3.3 or above is installed.

    - - - - - - - - - - - -
    Returns: -
      -
    • A triple of (package-name, version-number, sufficient) as per -check_dependencies().

    • -
    -
    -
    - Source code in beancount/scripts/deps.py -
    def check_cdecimal():
    -    """Check that Python 3.3 or above is installed.
    -
    -    Returns:
    -      A triple of (package-name, version-number, sufficient) as per
    -      check_dependencies().
    -    """
    -    # Note: this code mirrors and should be kept in-sync with that at the top of
    -    # beancount.core.number.
    -
    -    # Try the built-in installation.
    -    import decimal
    -    if is_fast_decimal(decimal):
    -        return ('cdecimal', '{} (built-in)'.format(decimal.__version__), True)
    -
    -    # Try an explicitly installed version.
    -    try:
    -        import cdecimal
    -        if is_fast_decimal(cdecimal):
    -            return ('cdecimal', getattr(cdecimal, '__version__', 'OKAY'), True)
    -    except ImportError:
    -        pass
    -
    -    # Not found.
    -    return ('cdecimal', None, False)
    -
    -
    +
    -

    -beancount.scripts.deps.check_dependencies() +

    +beancount.scripts.directories.validate_directories(entries, document_dirs) -

    +

    -

    Check the runtime dependencies and report their version numbers.

    +

    Validate a directory hierarchy against a ledger's account names.

    +

    Read a ledger's list of account names and check that all the capitalized +subdirectory names under the given roots match the account names.

    @@ -1242,55 +967,38 @@

    - - + +
    Returns: + Parameters:
      -
    • A list of pairs of (package-name, version-number, sufficient) whereby if a -package has not been installed, its 'version-number' will be set to None. -Otherwise, it will be a string with the version number in it. 'sufficient' -will be True if the version if sufficient for this installation of -Beancount.

    • +
    • entries – A list of directives.

    • +
    • document_dirs – A list of string, the directory roots to walk and validate.

    -
    - Source code in beancount/scripts/deps.py -
    def check_dependencies():
    -    """Check the runtime dependencies and report their version numbers.
    -
    -    Returns:
    -      A list of pairs of (package-name, version-number, sufficient) whereby if a
    -      package has not been installed, its 'version-number' will be set to None.
    -      Otherwise, it will be a string with the version number in it. 'sufficient'
    -      will be True if the version if sufficient for this installation of
    -      Beancount.
    -    """
    -    return [
    -        # Check for a complete installation of Python itself.
    -        check_python(),
    -        check_cdecimal(),
    -
    -        # Modules we really do need installed.
    -        check_import('dateutil'),
    -        check_import('bottle'),
    -        check_import('ply', module_name='ply.yacc', min_version='3.4'),
    -        check_import('lxml', module_name='lxml.etree', min_version='3'),
    -
    -        # Optionally required to upload data to Google Drive.
    -        check_import('googleapiclient'),
    -        check_import('oauth2client'),
    -        check_import('httplib2'),
    -
    -        # Optionally required to support various price source fetchers.
    -        check_import('requests', min_version='2.0'),
    -
    -        # Optionally required to support imports (identify, extract, file) code.
    -        check_python_magic(),
    -        check_import('beautifulsoup4', module_name='bs4', min_version='4'),
    -        ]
    -
    + Source code in beancount/scripts/directories.py +
    def validate_directories(entries, document_dirs):
    +    """Validate a directory hierarchy against a ledger's account names.
    +
    +    Read a ledger's list of account names and check that all the capitalized
    +    subdirectory names under the given roots match the account names.
    +
    +    Args:
    +      entries: A list of directives.
    +      document_dirs: A list of string, the directory roots to walk and validate.
    +    """
    +
    +    # Get the list of accounts declared in the ledge.
    +    accounts = getters.get_accounts(entries)
    +
    +    # For each of the roots, validate the hierarchy of directories.
    +    for document_dir in document_dirs:
    +        errors = validate_directory(accounts, document_dir)
    +        for error in errors:
    +            print("ERROR: {}".format(error))
    +
    @@ -1302,15 +1010,18 @@

    -

    -beancount.scripts.deps.check_import(package_name, min_version=None, module_name=None) +

    +beancount.scripts.directories.validate_directory(accounts, document_dir) -

    +

    -

    Check that a particular module name is installed.

    +

    Check a directory hierarchy against a list of valid accounts.

    +

    Walk the directory hierarchy, and for all directories with names matching +that of accounts (with ":" replaced with "/"), check that they refer to an +account name declared in the given list.

    @@ -1322,13 +1033,8 @@

    @@ -1344,46 +1050,52 @@

    Parameters:
      -
    • package_name – A string, the name of the package and module to be -imported to verify this works. This should have a version -attribute on it.

    • -
    • min_version – If not None, a string, the minimum version number -we require.

    • -
    • module_name – The name of the module to import if it differs from the -package name.

    • +
    • account – A set or dict of account names.

    • +
    • document_dir – A string, the root directory to walk and validate.

    Returns:
      -
    • A triple of (package-name, version-number, sufficient) as per -check_dependencies().

    • +
    • An errors for each invalid directory name found.

    - Source code in beancount/scripts/deps.py -
    def check_import(package_name, min_version=None, module_name=None):
    -    """Check that a particular module name is installed.
    -
    -    Args:
    -      package_name: A string, the name of the package and module to be
    -        imported to verify this works. This should have a __version__
    -        attribute on it.
    -      min_version: If not None, a string, the minimum version number
    -        we require.
    -      module_name: The name of the module to import if it differs from the
    -        package name.
    -    Returns:
    -      A triple of (package-name, version-number, sufficient) as per
    -      check_dependencies().
    -    """
    -    if module_name is None:
    -        module_name = package_name
    -    try:
    -        __import__(module_name)
    -        module = sys.modules[module_name]
    -        if min_version is not None:
    -            version = module.__version__
    -            assert isinstance(version, str)
    -            is_sufficient = (parse_version(version) >= parse_version(min_version)
    -                             if min_version else True)
    -        else:
    -            version, is_sufficient = None, True
    -    except ImportError:
    -        version, is_sufficient = None, False
    -    return (package_name, version, is_sufficient)
    -
    + Source code in beancount/scripts/directories.py +
    def validate_directory(accounts, document_dir):
    +    """Check a directory hierarchy against a list of valid accounts.
    +
    +    Walk the directory hierarchy, and for all directories with names matching
    +    that of accounts (with ":" replaced with "/"), check that they refer to an
    +    account name declared in the given list.
    +
    +    Args:
    +      account: A set or dict of account names.
    +      document_dir: A string, the root directory to walk and validate.
    +    Returns:
    +      An errors for each invalid directory name found.
    +    """
    +    # Generate all parent accounts in the account_set we're checking against, so
    +    # that parent directories with no corresponding account don't warn.
    +    accounts_with_parents = set(accounts)
    +    for account_ in accounts:
    +        while True:
    +            parent = account.parent(account_)
    +            if not parent:
    +                break
    +            if parent in accounts_with_parents:
    +                break
    +            accounts_with_parents.add(parent)
    +            account_ = parent
    +
    +    errors = []
    +    for directory, account_name, _, _ in account.walk(document_dir):
    +        if account_name not in accounts_with_parents:
    +            errors.append(
    +                ValidateDirectoryError(
    +                    "Invalid directory '{}': no corresponding account '{}'".format(
    +                        directory, account_name
    +                    )
    +                )
    +            )
    +    return errors
    +
    @@ -1391,223 +1103,116 @@

    -
    -

    -beancount.scripts.deps.check_python() +

    + -

    + -
    -

    Check that Python 3.3 or above is installed.

    - - - - - - - - - - - -
    Returns: -
      -
    • A triple of (package-name, version-number, sufficient) as per -check_dependencies().

    • -
    -
    -
    - Source code in beancount/scripts/deps.py -
    def check_python():
    -    """Check that Python 3.3 or above is installed.
    -
    -    Returns:
    -      A triple of (package-name, version-number, sufficient) as per
    -      check_dependencies().
    -    """
    -    return ('python3',
    -            '.'.join(map(str, sys.version_info[:3])),
    -            sys.version_info[:2] >= (3, 3))
    -
    -
    -
    +
    -
    +

    + beancount.scripts.doctor -
    +

    -

    -beancount.scripts.deps.check_python_magic() +
    +

    Debugging tool for those finding bugs in Beancount.

    +

    This tool is able to dump lexer/parser state, and will provide other services in +the name of debugging.

    -

    -
    -

    Check that a recent-enough version of python-magic is installed.

    -

    python-magic is an interface to libmagic, which is used by the 'file' tool -and UNIX to identify file types. Note that there are two Python wrappers -which provide the 'magic' import: python-magic and filemagic. The former is -what we need, which appears to be more recently maintained.

    +
    - - - - - - - - - - - -
    Returns: -
      -
    • A triple of (package-name, version-number, sufficient) as per -check_dependencies().

    • -
    -
    -
    - Source code in beancount/scripts/deps.py -
    def check_python_magic():
    -    """Check that a recent-enough version of python-magic is installed.
    -
    -    python-magic is an interface to libmagic, which is used by the 'file' tool
    -    and UNIX to identify file types. Note that there are two Python wrappers
    -    which provide the 'magic' import: python-magic and filemagic. The former is
    -    what we need, which appears to be more recently maintained.
    -
    -    Returns:
    -      A triple of (package-name, version-number, sufficient) as per
    -      check_dependencies().
    -    """
    -    try:
    -        import magic
    -        # Check that python-magic and not filemagic is installed.
    -        if not hasattr(magic, 'from_file'):
    -            # 'filemagic' is installed; install python-magic.
    -            raise ImportError
    -        return ('python-magic', 'OK', True)
    -    except ImportError:
    -        return ('python-magic', None, False)
    -
    -
    -
    -
    -
    -

    -beancount.scripts.deps.is_fast_decimal(decimal_module) -

    -
    +
    -

    Return true if a fast C decimal implementation is installed.

    -
    - Source code in beancount/scripts/deps.py -
    def is_fast_decimal(decimal_module):
    -    "Return true if a fast C decimal implementation is installed."
    -    return isinstance(decimal_module.Decimal().sqrt, types.BuiltinFunctionType)
    -
    -
    -
    -
    +

    + +beancount.scripts.doctor.FileLocation (ParamType) + -
    +

    +
    -

    -beancount.scripts.deps.list_dependencies(file=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>) -

    -
    +
    -

    Check the dependencies and produce a listing on the given file.

    - - - - - - - - - - - -
    Parameters: -
      -
    • file – A file object to write the output to.

    • -
    -
    -
    - Source code in beancount/scripts/deps.py -
    def list_dependencies(file=sys.stderr):
    -    """Check the dependencies and produce a listing on the given file.
    -
    -    Args:
    -      file: A file object to write the output to.
    -    """
    -    print("Dependencies:")
    -    for package, version, sufficient in check_dependencies():
    -        print("   {:16}: {} {}".format(
    -            package,
    -            version or 'NOT INSTALLED',
    -            "(INSUFFICIENT)" if version and not sufficient else ""),
    -              file=file)
    -
    -
    -
    -
    -
    -

    -beancount.scripts.deps.parse_version(version_str) + +
    -

    + +

    +beancount.scripts.doctor.FileLocation.convert(self, value, param, ctx) + + +

    -

    Parse the version string into a comparable tuple.

    +

    Convert the value to the correct type. This is not called if +the value is None (the missing value).

    +

    This must accept string values from the command line, as well as +values that are already the correct type. It may also convert +other compatible types.

    +

    The param and ctx arguments may be None in certain +situations, such as when converting prompt input.

    +

    If the value cannot be converted, call :meth:fail with a +descriptive message.

    +

    :param value: The value to convert. +:param param: The parameter that is using this type to convert + its value. May be None. +:param ctx: The current context that arrived at this value. May + be None.

    - Source code in beancount/scripts/deps.py -
    def parse_version(version_str: str) -> str:
    -    """Parse the version string into a comparable tuple."""
    -    return [int(v) for v in version_str.split('.')]
    -
    + Source code in beancount/scripts/doctor.py +
    def convert(self, value, param, ctx):
    +    match = re.match(r"(?:(.+):)?(\d+)$", value)
    +    if not match:
    +        self.fail("{!r} is not a valid location".format(value), param, ctx)
    +    filename, lineno = match.groups()
    +    if filename:
    +        filename = os.path.abspath(filename)
    +    return filename, int(lineno)
    +
    @@ -1617,7 +1222,6 @@

    -

    @@ -1626,20 +1230,22 @@

    -
    +
    -

    - beancount.scripts.directories +

    + +beancount.scripts.doctor.FileRegion (ParamType) + -

    +

    -

    Check that document directories mirror a list of accounts correctly.

    + @@ -1653,25 +1259,46 @@

    -
    +
    -

    - -beancount.scripts.directories.ValidateDirectoryError (Exception) - +

    +beancount.scripts.doctor.FileRegion.convert(self, value, param, ctx) -

    +

    -

    A directory validation error.

    - - +

    Convert the value to the correct type. This is not called if +the value is None (the missing value).

    +

    This must accept string values from the command line, as well as +values that are already the correct type. It may also convert +other compatible types.

    +

    The param and ctx arguments may be None in certain +situations, such as when converting prompt input.

    +

    If the value cannot be converted, call :meth:fail with a +descriptive message.

    +

    :param value: The value to convert. +:param param: The parameter that is using this type to convert + its value. May be None. +:param ctx: The current context that arrived at this value. May + be None.

    +
    + Source code in beancount/scripts/doctor.py +
    def convert(self, value, param, ctx):
    +    match = re.match(r"(?:(.+):)?(\d+):(\d+)$", value)
    +    if not match:
    +        self.fail("{!r} is not a valid region".format(value), param, ctx)
    +    filename, start_lineno, end_lineno = match.groups()
    +    if filename:
    +        filename = os.path.abspath(filename)
    +    return filename, int(start_lineno), int(end_lineno)
    +
    +
    @@ -1679,198 +1306,126 @@

    +

    + -

    -beancount.scripts.directories.validate_directories(entries, document_dirs) + -

    -
    +
    -

    Validate a directory hierarchy against a ledger's account names.

    -

    Read a ledger's list of account names and check that all the capitalized -subdirectory names under the given roots match the account names.

    - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • document_dirs – A list of string, the directory roots to walk and validate.

    • -
    -
    -
    - Source code in beancount/scripts/directories.py -
    def validate_directories(entries, document_dirs):
    -    """Validate a directory hierarchy against a ledger's account names.
    -
    -    Read a ledger's list of account names and check that all the capitalized
    -    subdirectory names under the given roots match the account names.
    -
    -    Args:
    -      entries: A list of directives.
    -      document_dirs: A list of string, the directory roots to walk and validate.
    -    """
    -
    -    # Get the list of accounts declared in the ledge.
    -    accounts = getters.get_accounts(entries)
    -
    -    # For each of the roots, validate the hierarchy of directories.
    -    for document_dir in document_dirs:
    -        errors = validate_directory(accounts, document_dir)
    -        for error in errors:
    -            print("ERROR: {}".format(error))
    -
    -
    -
    -
    +

    + +beancount.scripts.doctor.Group (Group) + -
    +

    +
    -

    -beancount.scripts.directories.validate_directory(accounts, document_dir) -

    -
    +
    -

    Check a directory hierarchy against a list of valid accounts.

    -

    Walk the directory hierarchy, and for all directories with names matching -that of accounts (with ":" replaced with "/"), check that they refer to an -account name declared in the given list.

    - - - - - - - - - - - -
    Parameters: -
      -
    • account – A set or dict of account names.

    • -
    • document_dir – A string, the root directory to walk and validate.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An errors for each invalid directory name found.

    • -
    -
    -
    - Source code in beancount/scripts/directories.py -
    def validate_directory(accounts, document_dir):
    -    """Check a directory hierarchy against a list of valid accounts.
    -
    -    Walk the directory hierarchy, and for all directories with names matching
    -    that of accounts (with ":" replaced with "/"), check that they refer to an
    -    account name declared in the given list.
    -
    -    Args:
    -      account: A set or dict of account names.
    -      document_dir: A string, the root directory to walk and validate.
    -    Returns:
    -      An errors for each invalid directory name found.
    -    """
    -    # Generate all parent accounts in the account_set we're checking against, so
    -    # that parent directories with no corresponding account don't warn.
    -    accounts_with_parents = set(accounts)
    -    for account_ in accounts:
    -        while True:
    -            parent = account.parent(account_)
    -            if not parent:
    -                break
    -            if parent in accounts_with_parents:
    -                break
    -            accounts_with_parents.add(parent)
    -            account_ = parent
    -
    -    errors = []
    -    for directory, account_name, _, _ in account.walk(document_dir):
    -        if account_name not in accounts_with_parents:
    -            errors.append(ValidateDirectoryError(
    -                "Invalid directory '{}': no corresponding account '{}'".format(
    -                    directory, account_name)))
    -    return errors
    -
    -
    -
    -
    -
    +
    + + + +

    +beancount.scripts.doctor.Group.command(self, *args, *, alias=None, **kwargs) + + +

    + +
    + +

    A shortcut decorator for declaring and attaching a command to +the group. This takes the same arguments as :func:command and +immediately registers the created command with this group by +calling :meth:add_command.

    +

    To customize the command class used, set the +:attr:command_class attribute.

    +

    .. versionchanged:: 8.1 + This decorator can be applied without parentheses.

    +

    .. versionchanged:: 8.0 + Added the :attr:command_class attribute.

    + +
    + Source code in beancount/scripts/doctor.py +
    def command(self, *args, alias=None, **kwargs):
    +    wrap = click.Group.command(self, *args, **kwargs)
    +
    +    def decorator(f):
    +        cmd = wrap(f)
    +        if alias:
    +            self.aliases[alias] = cmd.name
    +        return cmd
    +
    +    return decorator
    +
    +
    -
    - +
    -

    - beancount.scripts.doctor +

    +beancount.scripts.doctor.Group.get_command(self, ctx, cmd_name) -

    +

    -

    Debugging tool for those finding bugs in Beancount.

    -

    This tool is able to dump lexer/parser state, and will provide other services in -the name of debugging.

    +

    Given a context and a command name, this returns a +:class:Command object if it exists or returns None.

    +
    + Source code in beancount/scripts/doctor.py +
    def get_command(self, ctx, cmd_name):
    +    # aliases
    +    name = self.aliases.get(cmd_name, cmd_name)
    +    # allow to use '_' or '-' in command names.
    +    name = name.replace("_", "-")
    +    return click.Group.get_command(self, ctx, name)
    +
    +
    +
    + -
    +
    + + @@ -1909,7 +1464,7 @@

    -beancount.scripts.doctor.RenderError.__getnewargs__(self) +beancount.scripts.doctor.RenderError.__getnewargs__(self) special @@ -1923,10 +1478,10 @@

    Source code in beancount/scripts/doctor.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -1939,7 +1494,7 @@

    -beancount.scripts.doctor.RenderError.__new__(_cls, source, message, entry) +beancount.scripts.doctor.RenderError.__new__(_cls, source, message, entry) special @@ -1963,7 +1518,7 @@

    -beancount.scripts.doctor.RenderError.__repr__(self) +beancount.scripts.doctor.RenderError.__repr__(self) special @@ -1977,10 +1532,10 @@

    Source code in beancount/scripts/doctor.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -2003,45 +1558,60 @@

    -

    -beancount.scripts.doctor.do_checkdeps(*unused_args) +

    +beancount.scripts.doctor.find_linked_entries(entries, links, follow_links) -

    +

    -

    Report on the runtime dependencies.

    +

    Find all linked entries.

    +

    Note that there is an option here: You can either just look at the links +on the closest entry, or you can include the links of the linked +transactions as well. Whichever one you want depends on how you use your +links. Best would be to query the user (in Emacs) when there are many +links present.

    - - - - - - - - - - - -
    Parameters: -
      -
    • unused_args – Ignored.

    • -
    -
    Source code in beancount/scripts/doctor.py -
    def do_deps(*unused_args):
    -    """Report on the runtime dependencies.
    -
    -    Args:
    -      unused_args: Ignored.
    -    """
    -    from beancount.scripts import deps
    -    deps.list_dependencies(sys.stdout)
    -    print('')
    -    print('Use "pip3 install <package>" to install new packages.')
    -
    +
    def find_linked_entries(entries, links, follow_links: bool):
    +    """Find all linked entries.
    +
    +    Note that there is an option here: You can either just look at the links
    +    on the closest entry, or you can include the links of the linked
    +    transactions as well. Whichever one you want depends on how you use your
    +    links. Best would be to query the user (in Emacs) when there are many
    +    links present.
    +    """
    +    linked_entries = []
    +    if not follow_links:
    +        linked_entries = [
    +            entry
    +            for entry in entries
    +            if (isinstance(entry, data.Transaction) and entry.links and entry.links & links)
    +        ]
    +    else:
    +        links = set(links)
    +        linked_entries = []
    +        while True:
    +            num_linked = len(linked_entries)
    +            linked_entries = [
    +                entry
    +                for entry in entries
    +                if (
    +                    isinstance(entry, data.Transaction)
    +                    and entry.links
    +                    and entry.links & links
    +                )
    +            ]
    +            if len(linked_entries) == num_linked:
    +                break
    +            for entry in linked_entries:
    +                if entry.links:
    +                    links.update(entry.links)
    +    return linked_entries
    +
    @@ -2053,73 +1623,26 @@

    -

    -beancount.scripts.doctor.do_context(filename, args) +

    +beancount.scripts.doctor.find_tagged_entries(entries, tag) -

    +

    -

    Describe the context that a particular transaction is applied to.

    +

    Find all entries with the given tag.

    - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, which consists in the filename.

    • -
    • args – A tuple of the rest of arguments. We're expecting the first argument -to be a string which contains either a lineno integer or a filename:lineno -combination (which can be used if the location is not in the top-level file).

    • -
    -
    Source code in beancount/scripts/doctor.py -
    def do_context(filename, args):
    -    """Describe the context that a particular transaction is applied to.
    -
    -    Args:
    -      filename: A string, which consists in the filename.
    -      args: A tuple of the rest of arguments. We're expecting the first argument
    -        to be a string which contains either a lineno integer or a filename:lineno
    -        combination (which can be used if the location is not in the top-level file).
    -    """
    -    from beancount.reports import context
    -    from beancount import loader
    -
    -    # Check we have the required number of arguments.
    -    if len(args) != 1:
    -        raise SystemExit("Missing line number argument.")
    -
    -    # Load the input files.
    -    entries, errors, options_map = loader.load_file(filename)
    -
    -    # Parse the arguments, get the line number.
    -    match = re.match(r"(.+):(\d+)$", args[0])
    -    if match:
    -        search_filename = path.abspath(match.group(1))
    -        lineno = int(match.group(2))
    -    elif re.match(r"(\d+)$", args[0]):
    -        # Note: Make sure to use the absolute filename used by the parser to
    -        # resolve the file.
    -        search_filename = options_map['filename']
    -        lineno = int(args[0])
    -    else:
    -        raise SystemExit("Invalid format for location.")
    -
    -    str_context = context.render_file_context(entries, options_map,
    -                                              search_filename, lineno)
    -    sys.stdout.write(str_context)
    -
    +
    def find_tagged_entries(entries, tag):
    +    """Find all entries with the given tag."""
    +    return [
    +        entry
    +        for entry in entries
    +        if (isinstance(entry, data.Transaction) and entry.tags and tag in entry.tags)
    +    ]
    +
    @@ -2131,15 +1654,15 @@

    -

    -beancount.scripts.doctor.do_deps(*unused_args) +

    +beancount.scripts.doctor.render_mini_balances(entries, options_map, conversion=None, price_map=None) -

    +

    -

    Report on the runtime dependencies.

    +

    Render a treeified list of the balances for the given transactions.

    @@ -2151,7 +1674,12 @@

    @@ -2159,17 +1687,67 @@

    Parameters:
      -
    • unused_args – Ignored.

    • +
    • entries – A list of selected transactions to render.

    • +
    • options_map – The parsed options.

    • +
    • conversion – Conversion method string, None, 'value' or 'cost'.

    • +
    • price_map – A price map from the original entries. If this isn't provided, +the inventories are rendered directly. If it is, their contents are +converted to market value.

    Source code in beancount/scripts/doctor.py -
    def do_deps(*unused_args):
    -    """Report on the runtime dependencies.
    -
    -    Args:
    -      unused_args: Ignored.
    -    """
    -    from beancount.scripts import deps
    -    deps.list_dependencies(sys.stdout)
    -    print('')
    -    print('Use "pip3 install <package>" to install new packages.')
    -
    +
    def render_mini_balances(entries, options_map, conversion=None, price_map=None):
    +    """Render a treeified list of the balances for the given transactions.
    +
    +    Args:
    +      entries: A list of selected transactions to render.
    +      options_map: The parsed options.
    +      conversion: Conversion method string, None, 'value' or 'cost'.
    +      price_map: A price map from the original entries. If this isn't provided,
    +        the inventories are rendered directly. If it is, their contents are
    +        converted to market value.
    +    """
    +    # Render linked entries (in date order) as errors (for Emacs).
    +    errors = [RenderError(entry.meta, "", entry) for entry in entries]
    +    printer.print_errors(errors)
    +
    +    # Print out balances.
    +    real_root = realization.realize(entries)
    +    dformat = options_map["dcontext"].build(alignment=Align.DOT, reserved=2)
    +
    +    # TODO(blais): I always want to be able to convert at cost. We need
    +    # arguments capability.
    +    #
    +    # TODO(blais): Ideally this conversion inserts a new transactions to
    +    # 'Unrealized' to account for the difference between cost and market value.
    +    # Insert one and update the realization. Add an update() method to the
    +    # realization, given a transaction.
    +    acctypes = options.get_account_types(options_map)
    +    if conversion == "value":
    +        assert price_map is not None
    +
    +        # Warning: Mutate the inventories in-place, converting them to market
    +        # value.
    +        balance_diff = inventory.Inventory()
    +        for real_account in realization.iter_children(real_root):
    +            balance_cost = real_account.balance.reduce(convert.get_cost)
    +            balance_value = real_account.balance.reduce(convert.get_value, price_map)
    +            real_account.balance = balance_value
    +            balance_diff.add_inventory(balance_cost)
    +            balance_diff.add_inventory(-balance_value)
    +        if not balance_diff.is_empty():
    +            account_unrealized = account.join(
    +                acctypes.income, options_map["account_unrealized_gains"]
    +            )
    +            unrealized = realization.get_or_create(real_root, account_unrealized)
    +            unrealized.balance.add_inventory(balance_diff)
    +
    +    elif conversion == "cost":
    +        for real_account in realization.iter_children(real_root):
    +            real_account.balance = real_account.balance.reduce(convert.get_cost)
    +
    +    realization.dump_balances(real_root, dformat, file=sys.stdout)
    +
    +    # Print out net income change.
    +    net_income = inventory.Inventory()
    +    for real_node in realization.iter_children(real_root):
    +        if account_types.is_income_statement_account(real_node.account, acctypes):
    +            net_income.add_inventory(real_node.balance)
    +
    +    print()
    +    print("Net Income: {}".format(-net_income))
    +
    @@ -2181,56 +1759,40 @@

    -

    -beancount.scripts.doctor.do_directories(filename, args) +

    +beancount.scripts.doctor.resolve_region_to_entries(entries, filename, region) -

    +

    -

    Validate a directory hierarchy against a ledger's account names.

    -

    Read a ledger's list of account names and check that all the capitalized -subdirectory names under the given roots match the account names.

    +

    Resolve a filename and region to a list of entries.

    - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the Beancount input filename.

    • -
    • args – The rest of the arguments provided on the command-line, which in this -case will be interpreted as the names of root directories to validate against -the accounts in the given ledger.

    • -
    -
    Source code in beancount/scripts/doctor.py -
    def do_directories(filename, args):
    -    """Validate a directory hierarchy against a ledger's account names.
    -
    -    Read a ledger's list of account names and check that all the capitalized
    -    subdirectory names under the given roots match the account names.
    -
    -    Args:
    -      filename: A string, the Beancount input filename.
    -      args: The rest of the arguments provided on the command-line, which in this
    -        case will be interpreted as the names of root directories to validate against
    -        the accounts in the given ledger.
    -    """
    -    from beancount import loader
    -    from beancount.scripts import directories
    -    entries, _, __ = loader.load_file(filename)
    -    directories.validate_directories(entries, args)
    -
    +
    def resolve_region_to_entries(
    +    entries: List[data.Entries], filename: str, region: Tuple[str, int, int]
    +) -> List[data.Entries]:
    +    """Resolve a filename and region to a list of entries."""
    +
    +    search_filename, first_lineno, last_lineno = region
    +    if search_filename is None:
    +        search_filename = filename
    +
    +    # Find all the entries in the region. (To be clear, this isn't like the
    +    # 'linked' command, none of the links are followed.)
    +    region_entries = [
    +        entry
    +        for entry in data.filter_txns(entries)
    +        if (
    +            entry.meta["filename"] == search_filename
    +            and first_lineno <= entry.meta["lineno"] <= last_lineno
    +        )
    +    ]
    +
    +    return region_entries
    +
    @@ -2238,725 +1800,121 @@

    -
    - - - -

    -beancount.scripts.doctor.do_display_context(filename, args) -

    - -
    -

    Print out the precision inferred from the parsed numbers in the input file.

    +
    - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, which consists in the filename.

    • -
    • args – A tuple of the rest of arguments. We're expecting the first argument -to be an integer as a string.

    • -
    -
    -
    - Source code in beancount/scripts/doctor.py -
    def do_display_context(filename, args):
    -    """Print out the precision inferred from the parsed numbers in the input file.
    -
    -    Args:
    -      filename: A string, which consists in the filename.
    -      args: A tuple of the rest of arguments. We're expecting the first argument
    -        to be an integer as a string.
    -    """
    -    from beancount import loader
    -    entries, errors, options_map = loader.load_file(filename)
    -    dcontext = options_map['dcontext']
    -    sys.stdout.write(str(dcontext))
    -
    -
    -
    +
    + +

    + beancount.scripts.example -

    -beancount.scripts.doctor.do_dump_lexer(filename, unused_args) -

    +

    -

    Dump the lexer output for a Beancount syntax file.

    - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the Beancount input filename.

    • -
    -
    -
    - Source code in beancount/scripts/doctor.py -
    def do_lex(filename, unused_args):
    -    """Dump the lexer output for a Beancount syntax file.
    -
    -    Args:
    -      filename: A string, the Beancount input filename.
    -    """
    -    from beancount.parser import lexer
    -    for token, lineno, text, obj in lexer.lex_iter(filename):
    -        sys.stdout.write('{:12} {:6d} {}\n'.format(
    -            '(None)' if token is None else token, lineno, repr(text)))
    -
    -
    -
    - +
    -
    -

    -beancount.scripts.doctor.do_lex(filename, unused_args) -

    -
    -

    Dump the lexer output for a Beancount syntax file.

    - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the Beancount input filename.

    • -
    -
    -
    - Source code in beancount/scripts/doctor.py -
    def do_lex(filename, unused_args):
    -    """Dump the lexer output for a Beancount syntax file.
    -
    -    Args:
    -      filename: A string, the Beancount input filename.
    -    """
    -    from beancount.parser import lexer
    -    for token, lineno, text, obj in lexer.lex_iter(filename):
    -        sys.stdout.write('{:12} {:6d} {}\n'.format(
    -            '(None)' if token is None else token, lineno, repr(text)))
    -
    -
    -
    -
    -
    -

    -beancount.scripts.doctor.do_linked(filename, args) -

    -
    -

    Print out a list of transactions linked to the one at the given line.

    - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, which consists in the filename.

    • -
    • args – A tuple of the rest of arguments. We're expecting the first argument -to be an integer as a string.

    • -
    -
    -
    - Source code in beancount/scripts/doctor.py -
    def do_linked(filename, args):
    -    """Print out a list of transactions linked to the one at the given line.
    -
    -    Args:
    -      filename: A string, which consists in the filename.
    -      args: A tuple of the rest of arguments. We're expecting the first argument
    -        to be an integer as a string.
    -    """
    -    from beancount.parser import options
    -    from beancount.parser import printer
    -    from beancount.core import account_types
    -    from beancount.core import inventory
    -    from beancount.core import data
    -    from beancount.core import realization
    -    from beancount import loader
    -
    -    # Parse the arguments, get the line number.
    -    if len(args) != 1:
    -        raise SystemExit("Missing line number argument.")
    -    lineno = int(args[0])
    -
    -    # Load the input file.
    -    entries, errors, options_map = loader.load_file(filename)
    -
    -    # Find the closest entry.
    -    closest_entry = data.find_closest(entries, options_map['filename'], lineno)
    -
    -    # Find its links.
    -    if closest_entry is None:
    -        raise SystemExit("No entry could be found before {}:{}".format(filename, lineno))
    -    links = (closest_entry.links
    -             if isinstance(closest_entry, data.Transaction)
    -             else data.EMPTY_SET)
    -    if not links:
    -        linked_entries = [closest_entry]
    -    else:
    -        # Find all linked entries.
    -        #
    -        # Note that there is an option here: You can either just look at the links
    -        # on the closest entry, or you can include the links of the linked
    -        # transactions as well. Whichever one you want depends on how you use your
    -        # links. Best would be to query the user (in Emacs) when there are many
    -        # links present.
    -        follow_links = True
    -        if not follow_links:
    -            linked_entries = [entry
    -                              for entry in entries
    -                              if (isinstance(entry, data.Transaction) and
    -                                  entry.links and
    -                                  entry.links & links)]
    -        else:
    -            links = set(links)
    -            linked_entries = []
    -            while True:
    -                num_linked = len(linked_entries)
    -                linked_entries = [entry
    -                                  for entry in entries
    -                                  if (isinstance(entry, data.Transaction) and
    -                                      entry.links and
    -                                      entry.links & links)]
    -                if len(linked_entries) == num_linked:
    -                    break
    -                for entry in linked_entries:
    -                    if entry.links:
    -                        links.update(entry.links)
    -
    -    # Render linked entries (in date order) as errors (for Emacs).
    -    errors = [RenderError(entry.meta, '', entry)
    -              for entry in linked_entries]
    -    printer.print_errors(errors)
    -
    -    # Print out balances.
    -    real_root = realization.realize(linked_entries)
    -    dformat = options_map['dcontext'].build(alignment=display_context.Align.DOT,
    -                                            reserved=2)
    -    realization.dump_balances(real_root, dformat, file=sys.stdout)
    -
    -    # Print out net income change.
    -    acctypes = options.get_account_types(options_map)
    -    net_income = inventory.Inventory()
    -    for real_node in realization.iter_children(real_root):
    -        if account_types.is_income_statement_account(real_node.account, acctypes):
    -            net_income.add_inventory(real_node.balance)
    -
    -    print()
    -    print('Net Income: {}'.format(-net_income))
    -
    -
    -
    +
    -
    +

    + +beancount.scripts.example.LiberalDate (ParamType) + -
    +

    -

    -beancount.scripts.doctor.do_list_options(*unused_args) +
    -

    -
    - -

    Print out a list of the available options.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • unused_args – Ignored.

    • -
    -
    -
    - Source code in beancount/scripts/doctor.py -
    def do_list_options(*unused_args):
    -    """Print out a list of the available options.
    -
    -    Args:
    -      unused_args: Ignored.
    -    """
    -    from beancount.parser import options
    -    print(options.list_options())
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.scripts.doctor.do_missing_open(filename, args) - - -

    - -
    - -

    Print out Open directives that are missing for the given input file.

    -

    This can be useful during demos in order to quickly generate all the -required Open directives without having to type them manually.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, which consists in the filename.

    • -
    • args – A tuple of the rest of arguments. We're expecting the first argument -to be an integer as a string.

    • -
    -
    -
    - Source code in beancount/scripts/doctor.py -
    def do_missing_open(filename, args):
    -    """Print out Open directives that are missing for the given input file.
    -
    -    This can be useful during demos in order to quickly generate all the
    -    required Open directives without having to type them manually.
    -
    -    Args:
    -      filename: A string, which consists in the filename.
    -      args: A tuple of the rest of arguments. We're expecting the first argument
    -        to be an integer as a string.
    -    """
    -    from beancount.parser import printer
    -    from beancount.core import data
    -    from beancount.core import getters
    -    from beancount import loader
    -
    -    entries, errors, options_map = loader.load_file(filename)
    -
    -    # Get accounts usage and open directives.
    -    first_use_map, _ = getters.get_accounts_use_map(entries)
    -    open_close_map = getters.get_account_open_close(entries)
    -
    -    new_entries = []
    -    for account, first_use_date in first_use_map.items():
    -        if account not in open_close_map:
    -            new_entries.append(
    -                data.Open(data.new_metadata(filename, 0), first_use_date, account,
    -                          None, None))
    -
    -    dcontext = options_map['dcontext']
    -    printer.print_entries(data.sorted(new_entries), dcontext)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.scripts.doctor.do_parse(filename, unused_args) - - -

    - -
    - -

    Run the parser in debug mode.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the Beancount input filename.

    • -
    -
    -
    - Source code in beancount/scripts/doctor.py -
    def do_parse(filename, unused_args):
    -    """Run the parser in debug mode.
    -
    -    Args:
    -      filename: A string, the Beancount input filename.
    -    """
    -    from beancount.parser import parser
    -    entries, errors, _ = parser.parse_file(filename, yydebug=1)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.scripts.doctor.do_print_options(filename, *args) - - -

    - -
    - -

    Print out the actual options parsed from a file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • unused_args – Ignored.

    • -
    -
    -
    - Source code in beancount/scripts/doctor.py -
    def do_print_options(filename, *args):
    -    """Print out the actual options parsed from a file.
    -
    -    Args:
    -      unused_args: Ignored.
    -    """
    -    from beancount import loader
    -    _, __, options_map = loader.load_file(filename)
    -    for key, value in sorted(options_map.items()):
    -        print('{}: {}'.format(key, value))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.scripts.doctor.do_roundtrip(filename, unused_args) - - -

    - -
    - -

    Round-trip test on arbitrary Ledger.

    -

    Read a Ledger's transactions, print them out, re-read them again and compare -them. Both sets of parsed entries should be equal. Both printed files are -output to disk, so you can also run diff on them yourself afterwards.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the Beancount input filename.

    • -
    -
    -
    - Source code in beancount/scripts/doctor.py -
    def do_roundtrip(filename, unused_args):
    -    """Round-trip test on arbitrary Ledger.
    -
    -    Read a Ledger's transactions, print them out, re-read them again and compare
    -    them. Both sets of parsed entries should be equal. Both printed files are
    -    output to disk, so you can also run diff on them yourself afterwards.
    -
    -    Args:
    -      filename: A string, the Beancount input filename.
    -    """
    -    from beancount.parser import printer
    -    from beancount.core import compare
    -    from beancount import loader
    -
    -    round1_filename = round2_filename = None
    -    try:
    -        logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s')
    -        logging.info("Read the entries")
    -        entries, errors, options_map = loader.load_file(filename)
    -        printer.print_errors(errors, file=sys.stderr)
    -
    -        logging.info("Print them out to a file")
    -        basename, extension = path.splitext(filename)
    -        round1_filename = ''.join([basename, '.roundtrip1', extension])
    -        with open(round1_filename, 'w') as outfile:
    -            printer.print_entries(entries, file=outfile)
    -
    -        logging.info("Read the entries from that file")
    -
    -        # Note that we don't want to run any of the auto-generation here, but
    -        # parsing now returns incomplete objects and we assume idempotence on a
    -        # file that was output from the printer after having been processed, so
    -        # it shouldn't add anything new. That is, a processed file printed and
    -        # resolve when parsed again should contain the same entries, i.e.
    -        # nothing new should be generated.
    -        entries_roundtrip, errors, options_map = loader.load_file(round1_filename)
    -
    -        # Print out the list of errors from parsing the results.
    -        if errors:
    -            print(',----------------------------------------------------------------------')
    -            printer.print_errors(errors, file=sys.stdout)
    -            print('`----------------------------------------------------------------------')
    -
    -        logging.info("Print what you read to yet another file")
    -        round2_filename = ''.join([basename, '.roundtrip2', extension])
    -        with open(round2_filename, 'w') as outfile:
    -            printer.print_entries(entries_roundtrip, file=outfile)
    -
    -        logging.info("Compare the original entries with the re-read ones")
    -        same, missing1, missing2 = compare.compare_entries(entries, entries_roundtrip)
    -        if same:
    -            logging.info('Entries are the same. Congratulations.')
    -        else:
    -            logging.error('Entries differ!')
    -            print()
    -            print('\n\nMissing from original:')
    -            for entry in entries:
    -                print(entry)
    -                print(compare.hash_entry(entry))
    -                print(printer.format_entry(entry))
    -                print()
    -
    -            print('\n\nMissing from round-trip:')
    -            for entry in missing2:
    -                print(entry)
    -                print(compare.hash_entry(entry))
    -                print(printer.format_entry(entry))
    -                print()
    -    finally:
    -        for rfilename in (round1_filename, round2_filename):
    -            if path.exists(rfilename):
    -                os.remove(rfilename)
    -
    -
    -
    - -
    - - - -
    +
    -

    -beancount.scripts.doctor.do_validate_html(directory, args) -

    -
    -

    Validate all the HTML files under a directory hierarchy.

    - - - - - - - - - - - -
    Parameters: -
      -
    • directory – A string, the root directory whose contents to validate.

    • -
    • args – A tuple of the rest of arguments.

    • -
    -
    -
    - Source code in beancount/scripts/doctor.py -
    def do_validate_html(directory, args):
    -    """Validate all the HTML files under a directory hierarchy.
    -
    -    Args:
    -      directory: A string, the root directory whose contents to validate.
    -      args: A tuple of the rest of arguments.
    -    """
    -    from beancount.web import scrape
    -    files, missing, empty = scrape.validate_local_links_in_dir(directory)
    -    logging.info('%d files processed', len(files))
    -    for target in missing:
    -        logging.error('Missing %s', target)
    -    for target in empty:
    -        logging.error('Empty %s', target)
    -
    -
    -
    -
    -
    +
    -

    -beancount.scripts.doctor.get_commands() +

    +beancount.scripts.example.LiberalDate.convert(self, value, param, ctx) -

    +

    -

    Return a list of available commands in this file.

    +

    Convert the value to the correct type. This is not called if +the value is None (the missing value).

    +

    This must accept string values from the command line, as well as +values that are already the correct type. It may also convert +other compatible types.

    +

    The param and ctx arguments may be None in certain +situations, such as when converting prompt input.

    +

    If the value cannot be converted, call :meth:fail with a +descriptive message.

    +

    :param value: The value to convert. +:param param: The parameter that is using this type to convert + its value. May be None. +:param ctx: The current context that arrived at this value. May + be None.

    - - - - - - - - - - - -
    Returns: -
      -
    • A list of pairs of (command-name string, docstring).

    • -
    -
    - Source code in beancount/scripts/doctor.py -
    def get_commands():
    -    """Return a list of available commands in this file.
    -
    -    Returns:
    -      A list of pairs of (command-name string, docstring).
    -    """
    -    commands = []
    -    for attr_name, attr_value in globals().items():
    -        match = re.match('do_(.*)', attr_name)
    -        if match:
    -            commands.append((match.group(1),
    -                             misc_utils.first_paragraph(attr_value.__doc__)))
    -    return commands
    -
    + Source code in beancount/scripts/example.py +
    def convert(self, value, param, ctx):
    +    try:
    +        date_utils.parse_date_liberally(value)
    +    except ValueError:
    +        self.fail("{!r} is not a valid date".format(value), param, ctx)
    +
    @@ -2966,58 +1924,12 @@

    - - - - -
    - - - -

    - beancount.scripts.example - - - -

    - -
    - -

    Generate a decently-sized example history, based on some rules.

    -

    This script is used to generate some meaningful input to Beancount, input that -looks as realistic as possible for a moderately complex mock individual. This -can also be used as an input generator for a stress test for performance -evaluation.

    - - - -
    - - - - - - - - - - - - - - - - - - - - @@ -3026,7 +1938,7 @@

    -beancount.scripts.example.check_non_negative(entries, account, currency) +beancount.scripts.example.check_non_negative(entries, account, currency)

    @@ -3071,25 +1983,26 @@

    Source code in beancount/scripts/example.py -
    def check_non_negative(entries, account, currency):
    -    """Check that the balance of the given account never goes negative.
    -
    -    Args:
    -      entries: A list of directives.
    -      account: An account string, the account to check the balance for.
    -      currency: A string, the currency to check minimums for.
    -    Raises:
    -      AssertionError: if the balance goes negative.
    -    """
    -    previous_date = None
    -    for txn_posting, balances in postings_for(data.sorted(entries), [account], before=True):
    -        balance = balances[account]
    -        date = txn_posting.txn.date
    -        if date != previous_date:
    -            assert all(pos.units.number >= ZERO for pos in balance.get_positions()), (
    -                "Negative balance: {} at: {}".format(balance, txn_posting.txn.date))
    -        previous_date = date
    -
    +
    def check_non_negative(entries, account, currency):
    +    """Check that the balance of the given account never goes negative.
    +
    +    Args:
    +      entries: A list of directives.
    +      account: An account string, the account to check the balance for.
    +      currency: A string, the currency to check minimums for.
    +    Raises:
    +      AssertionError: if the balance goes negative.
    +    """
    +    previous_date = None
    +    for txn_posting, balances in postings_for(data.sorted(entries), [account], before=True):
    +        balance = balances[account]
    +        date = txn_posting.txn.date
    +        if date != previous_date:
    +            assert all(
    +                pos.units.number >= ZERO for pos in balance.get_positions()
    +            ), "Negative balance: {} at: {}".format(balance, txn_posting.txn.date)
    +        previous_date = date
    +

    @@ -3102,7 +2015,7 @@

    -beancount.scripts.example.compute_trip_dates(date_begin, date_end) +beancount.scripts.example.compute_trip_dates(date_begin, date_end)

    @@ -3132,38 +2045,38 @@

    Source code in beancount/scripts/example.py -
    def compute_trip_dates(date_begin, date_end):
    -    """Generate dates at reasonable intervals for trips during the given time period.
    -
    -    Args:
    -      date_begin: The start date.
    -      date_end: The end date.
    -    Yields:
    -      Pairs of dates for the trips within the period.
    -    """
    -    # Min and max number of days remaining at home.
    -    days_at_home = (4*30, 13*30)
    -
    -    # Length of trip.
    -    days_trip = (8, 22)
    -
    -    # Number of days to ensure no trip at the beginning and the end.
    -    days_buffer = 21
    -
    -    date_begin += datetime.timedelta(days=days_buffer)
    -    date_end -= datetime.timedelta(days=days_buffer)
    -
    -    date = date_begin
    -    while 1:
    -        duration_at_home = datetime.timedelta(days=random.randint(*days_at_home))
    -        duration_trip = datetime.timedelta(days=random.randint(*days_trip))
    -        date_trip_begin = date + duration_at_home
    -        date_trip_end = date_trip_begin + duration_trip
    -        if date_trip_end >= date_end:
    -            break
    -        yield (date_trip_begin, date_trip_end)
    -        date = date_trip_end
    -
    +
    def compute_trip_dates(date_begin, date_end):
    +    """Generate dates at reasonable intervals for trips during the given time period.
    +
    +    Args:
    +      date_begin: The start date.
    +      date_end: The end date.
    +    Yields:
    +      Pairs of dates for the trips within the period.
    +    """
    +    # Min and max number of days remaining at home.
    +    days_at_home = (4 * 30, 13 * 30)
    +
    +    # Length of trip.
    +    days_trip = (8, 22)
    +
    +    # Number of days to ensure no trip at the beginning and the end.
    +    days_buffer = 21
    +
    +    date_begin += datetime.timedelta(days=days_buffer)
    +    date_end -= datetime.timedelta(days=days_buffer)
    +
    +    date = date_begin
    +    while 1:
    +        duration_at_home = datetime.timedelta(days=random.randint(*days_at_home))
    +        duration_trip = datetime.timedelta(days=random.randint(*days_trip))
    +        date_trip_begin = date + duration_at_home
    +        date_trip_end = date_trip_begin + duration_trip
    +        if date_trip_end >= date_end:
    +            break
    +        yield (date_trip_begin, date_trip_end)
    +        date = date_trip_end
    +

    @@ -3176,7 +2089,7 @@

    -beancount.scripts.example.contextualize_file(contents, employer) +beancount.scripts.example.contextualize_file(contents, employer)

    @@ -3219,43 +2132,42 @@

    Source code in beancount/scripts/example.py -
    def contextualize_file(contents, employer):
    -    """Replace generic strings in the generated file with realistic strings.
    -
    -    Args:
    -      contents: A string, the generic file contents.
    -    Returns:
    -      A string, the contextualized version.
    -    """
    -    replacements = {
    -        'CC': 'US',
    -        'Bank1': 'BofA',
    -        'Bank1_Institution': 'Bank of America',
    -        'Bank1_Address': '123 America Street, LargeTown, USA',
    -        'Bank1_Phone': '+1.012.345.6789',
    -        'CreditCard1': 'Chase:Slate',
    -        'CreditCard2': 'Amex:BlueCash',
    -        'Employer1': employer,
    -        'Retirement': 'Vanguard',
    -        'Retirement_Institution': 'Vanguard Group',
    -        'Retirement_Address': "P.O. Box 1110, Valley Forge, PA 19482-1110",
    -        'Retirement_Phone': "+1.800.523.1188",
    -        'Investment': 'ETrade',
    -
    -        # Commodities
    -        'CCY': 'USD',
    -        'VACHR': 'VACHR',
    -        'DEFCCY': 'IRAUSD',
    -        'MFUND1': 'VBMPX',
    -        'MFUND2': 'RGAGX',
    -        'STK1': 'ITOT',
    -        'STK2': 'VEA',
    -        'STK3': 'VHT',
    -        'STK4': 'GLD',
    -        }
    -    new_contents = replace(contents, replacements)
    -    return new_contents, replacements
    -
    +
    def contextualize_file(contents, employer):
    +    """Replace generic strings in the generated file with realistic strings.
    +
    +    Args:
    +      contents: A string, the generic file contents.
    +    Returns:
    +      A string, the contextualized version.
    +    """
    +    replacements = {
    +        "CC": "US",
    +        "Bank1": "BofA",
    +        "Bank1_Institution": "Bank of America",
    +        "Bank1_Address": "123 America Street, LargeTown, USA",
    +        "Bank1_Phone": "+1.012.345.6789",
    +        "CreditCard1": "Chase:Slate",
    +        "CreditCard2": "Amex:BlueCash",
    +        "Employer1": employer,
    +        "Retirement": "Vanguard",
    +        "Retirement_Institution": "Vanguard Group",
    +        "Retirement_Address": "P.O. Box 1110, Valley Forge, PA 19482-1110",
    +        "Retirement_Phone": "+1.800.523.1188",
    +        "Investment": "ETrade",
    +        # Commodities
    +        "CCY": "USD",
    +        "VACHR": "VACHR",
    +        "DEFCCY": "IRAUSD",
    +        "MFUND1": "VBMPX",
    +        "MFUND2": "RGAGX",
    +        "STK1": "ITOT",
    +        "STK2": "VEA",
    +        "STK3": "VHT",
    +        "STK4": "GLD",
    +    }
    +    new_contents = replace(contents, replacements)
    +    return new_contents, replacements
    +

    @@ -3268,7 +2180,7 @@

    -beancount.scripts.example.date_iter(date_begin, date_end) +beancount.scripts.example.date_iter(date_begin, date_end)

    @@ -3298,22 +2210,22 @@

    Source code in beancount/scripts/example.py -
    def date_iter(date_begin, date_end):
    -    """Generate a sequence of dates.
    -
    -    Args:
    -      date_begin: The start date.
    -      date_end: The end date.
    -    Yields:
    -      Instances of datetime.date.
    -    """
    -    assert date_begin <= date_end
    -    date = date_begin
    -    one_day = datetime.timedelta(days=1)
    -    while date < date_end:
    -        date += one_day
    -        yield date
    -
    +
    def date_iter(date_begin, date_end):
    +    """Generate a sequence of dates.
    +
    +    Args:
    +      date_begin: The start date.
    +      date_end: The end date.
    +    Yields:
    +      Instances of datetime.date.
    +    """
    +    assert date_begin <= date_end
    +    date = date_begin
    +    one_day = datetime.timedelta(days=1)
    +    while date < date_end:
    +        date += one_day
    +        yield date
    +
    @@ -3326,7 +2238,7 @@

    -beancount.scripts.example.date_random_seq(date_begin, date_end, days_min, days_max) +beancount.scripts.example.date_random_seq(date_begin, date_end, days_min, days_max)

    @@ -3358,27 +2270,27 @@

    Source code in beancount/scripts/example.py -
    def date_random_seq(date_begin, date_end, days_min, days_max):
    -    """Generate a sequence of dates with some random increase in days.
    -
    -    Args:
    -      date_begin: The start date.
    -      date_end: The end date.
    -      days_min: The minimum number of days to advance on each iteration.
    -      days_max: The maximum number of days to advance on each iteration.
    -    Yields:
    -      Instances of datetime.date.
    -    """
    -    assert days_min > 0
    -    assert days_min <= days_max
    -    date = date_begin
    -    while date < date_end:
    -        nb_days_forward = random.randint(days_min, days_max)
    -        date += datetime.timedelta(days=nb_days_forward)
    -        if date >= date_end:
    -            break
    -        yield date
    -
    +
    def date_random_seq(date_begin, date_end, days_min, days_max):
    +    """Generate a sequence of dates with some random increase in days.
    +
    +    Args:
    +      date_begin: The start date.
    +      date_end: The end date.
    +      days_min: The minimum number of days to advance on each iteration.
    +      days_max: The maximum number of days to advance on each iteration.
    +    Yields:
    +      Instances of datetime.date.
    +    """
    +    assert days_min > 0
    +    assert days_min <= days_max
    +    date = date_begin
    +    while date < date_end:
    +        nb_days_forward = random.randint(days_min, days_max)
    +        date += datetime.timedelta(days=nb_days_forward)
    +        if date >= date_end:
    +            break
    +        yield date
    +
    @@ -3391,7 +2303,7 @@

    -beancount.scripts.example.delay_dates(date_iter, delay_days_min, delay_days_max) +beancount.scripts.example.delay_dates(date_iter, delay_days_min, delay_days_max)

    @@ -3422,26 +2334,26 @@

    Source code in beancount/scripts/example.py -
    def delay_dates(date_iter, delay_days_min, delay_days_max):
    -    """Delay the dates from the given iterator by some uniformly drawn number of days.
    -
    -    Args:
    -      date_iter: An iterator of datetime.date instances.
    -      delay_days_min: The minimum amount of advance days for the transaction.
    -      delay_days_max: The maximum amount of advance days for the transaction.
    -    Yields:
    -      datetime.date instances.
    -    """
    -    dates = list(date_iter)
    -    last_date = dates[-1]
    -    last_date = last_date.date() if isinstance(last_date, datetime.datetime) else last_date
    -    for dtime in dates:
    -        date = dtime.date() if isinstance(dtime, datetime.datetime) else dtime
    -        date += datetime.timedelta(days=random.randint(delay_days_min, delay_days_max))
    -        if date >= last_date:
    -            break
    -        yield date
    -
    +
    def delay_dates(date_iter, delay_days_min, delay_days_max):
    +    """Delay the dates from the given iterator by some uniformly drawn number of days.
    +
    +    Args:
    +      date_iter: An iterator of datetime.date instances.
    +      delay_days_min: The minimum amount of advance days for the transaction.
    +      delay_days_max: The maximum amount of advance days for the transaction.
    +    Yields:
    +      datetime.date instances.
    +    """
    +    dates = list(date_iter)
    +    last_date = dates[-1]
    +    last_date = last_date.date() if isinstance(last_date, datetime.datetime) else last_date
    +    for dtime in dates:
    +        date = dtime.date() if isinstance(dtime, datetime.datetime) else dtime
    +        date += datetime.timedelta(days=random.randint(delay_days_min, delay_days_max))
    +        if date >= last_date:
    +            break
    +        yield date
    +
    @@ -3454,7 +2366,7 @@

    -beancount.scripts.example.generate_balance_checks(entries, account, date_iter) +beancount.scripts.example.generate_balance_checks(entries, account, date_iter)

    @@ -3500,31 +2412,33 @@

    Source code in beancount/scripts/example.py -
    def generate_balance_checks(entries, account, date_iter):
    -    """Generate balance check entries to the given frequency.
    -
    -    Args:
    -      entries: A list of directives that contain all the transactions for the
    -        accounts.
    -      account: The name of the account for which to generate.
    -      date_iter: Iterator of dates. We generate balance checks at these dates.
    -    Returns:
    -      A list of balance check entries.
    -    """
    -    balance_checks = []
    -    date_iter = iter(date_iter)
    -    next_date = next(date_iter)
    -    with misc_utils.swallow(StopIteration):
    -        for txn_posting, balance in postings_for(entries, [account], before=True):
    -            while txn_posting.txn.date >= next_date:
    -                amount = balance[account].get_currency_units('CCY').number
    -                balance_checks.extend(parse("""
    -                  {next_date} balance {account} {amount} CCY
    -                """, **locals()))
    -                next_date = next(date_iter)
    -
    -    return balance_checks
    -
    +
    def generate_balance_checks(entries, account, date_iter):
    +    """Generate balance check entries to the given frequency.
    +
    +    Args:
    +      entries: A list of directives that contain all the transactions for the
    +        accounts.
    +      account: The name of the account for which to generate.
    +      date_iter: Iterator of dates. We generate balance checks at these dates.
    +    Returns:
    +      A list of balance check entries.
    +    """
    +    balance_checks = []
    +    date_iter = iter(date_iter)
    +    next_date = next(date_iter)
    +    with misc_utils.swallow(StopIteration):
    +        for txn_posting, balance in postings_for(entries, [account], before=True):
    +            while txn_posting.txn.date >= next_date:
    +                amount = balance[account].get_currency_units("CCY").number
    +                balance_checks.extend(
    +                    parse(f"""
    +                  {next_date} balance {account} {amount} CCY
    +                """)
    +                )
    +                next_date = next(date_iter)
    +
    +    return balance_checks
    +
    @@ -3537,7 +2451,7 @@

    -beancount.scripts.example.generate_banking(entries, date_begin, date_end, amount_initial) +beancount.scripts.example.generate_banking(entries, date_begin, date_end, amount_initial)

    @@ -3584,52 +2498,53 @@

    Source code in beancount/scripts/example.py -
    def generate_banking(entries, date_begin, date_end, amount_initial):
    -    """Generate a checking account opening.
    -
    -    Args:
    -      entries: A list of entries which affect this account.
    -      date_begin: A date instance, the beginning date.
    -      date_end: A date instance, the end date.
    -      amount_initial: A Decimal instance, the amount to initialize the checking
    -        account with.
    -    Returns:
    -      A list of directives.
    -    """
    -    amount_initial_neg = -amount_initial
    -    new_entries = parse("""
    -
    -      {date_begin} open Assets:CC:Bank1
    -        institution: "Bank1_Institution"
    -        address: "Bank1_Address"
    -        phone: "Bank1_Phone"
    -
    -      {date_begin} open Assets:CC:Bank1:Checking    CCY
    -        account: "00234-48574897"
    -
    -      ;; {date_begin} open Assets:CC:Bank1:Savings    CCY
    -
    -      {date_begin} * "Opening Balance for checking account"
    -        Assets:CC:Bank1:Checking   {amount_initial} CCY
    -        Equity:Opening-Balances    {amount_initial_neg} CCY
    -
    -    """, **locals())
    -
    -    date_balance = date_begin + datetime.timedelta(days=1)
    -    account = 'Assets:CC:Bank1:Checking'
    -    for txn_posting, balances in postings_for(data.sorted(entries + new_entries),
    -                                              [account], before=True):
    -        if txn_posting.txn.date >= date_balance:
    -            break
    -    amount_balance = balances[account].get_currency_units('CCY').number
    -    bal_entries = parse("""
    -
    -      {date_balance} balance Assets:CC:Bank1:Checking   {amount_balance} CCY
    -
    -    """, **locals())
    -
    -    return new_entries + bal_entries
    -
    +
    def generate_banking(entries, date_begin, date_end, amount_initial):
    +    """Generate a checking account opening.
    +
    +    Args:
    +      entries: A list of entries which affect this account.
    +      date_begin: A date instance, the beginning date.
    +      date_end: A date instance, the end date.
    +      amount_initial: A Decimal instance, the amount to initialize the checking
    +        account with.
    +    Returns:
    +      A list of directives.
    +    """
    +    amount_initial_neg = -amount_initial
    +    new_entries = parse(f"""
    +
    +      {date_begin} open Assets:CC:Bank1
    +        institution: "Bank1_Institution"
    +        address: "Bank1_Address"
    +        phone: "Bank1_Phone"
    +
    +      {date_begin} open Assets:CC:Bank1:Checking    CCY
    +        account: "00234-48574897"
    +
    +      ;; {date_begin} open Assets:CC:Bank1:Savings    CCY
    +
    +      {date_begin} * "Opening Balance for checking account"
    +        Assets:CC:Bank1:Checking   {amount_initial} CCY
    +        Equity:Opening-Balances    {amount_initial_neg} CCY
    +
    +    """)
    +
    +    date_balance = date_begin + datetime.timedelta(days=1)
    +    account = "Assets:CC:Bank1:Checking"
    +    for txn_posting, balances in postings_for(
    +        data.sorted(entries + new_entries), [account], before=True
    +    ):
    +        if txn_posting.txn.date >= date_balance:
    +            break
    +    amount_balance = balances[account].get_currency_units("CCY").number
    +    bal_entries = parse(f"""
    +
    +      {date_balance} balance Assets:CC:Bank1:Checking   {amount_balance} CCY
    +
    +    """)
    +
    +    return new_entries + bal_entries
    +
    @@ -3642,7 +2557,7 @@

    -beancount.scripts.example.generate_banking_expenses(date_begin, date_end, account, rent_amount) +beancount.scripts.example.generate_banking_expenses(date_begin, date_end, account, rent_amount)

    @@ -3688,53 +2603,70 @@

    Source code in beancount/scripts/example.py -
    def generate_banking_expenses(date_begin, date_end, account, rent_amount):
    -    """Generate expenses paid out of a checking account, typically living expenses.
    -
    -    Args:
    -      date_begin: The start date.
    -      date_end: The end date.
    -      account: The checking account to generate expenses to.
    -      rent_amount: The amount of rent.
    -    Returns:
    -      A list of directives.
    -    """
    -    fee_expenses = generate_periodic_expenses(
    -        rrule.rrule(rrule.MONTHLY, bymonthday=4, dtstart=date_begin, until=date_end),
    -        "BANK FEES", "Monthly bank fee",
    -        account, 'Expenses:Financial:Fees',
    -        lambda: D('4.00'))
    -
    -    rent_expenses = generate_periodic_expenses(
    -        delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 2, 5),
    -        "RiverBank Properties", "Paying the rent",
    -        account, 'Expenses:Home:Rent',
    -        lambda: random.normalvariate(float(rent_amount), 0))
    -
    -    electricity_expenses = generate_periodic_expenses(
    -        delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 7, 8),
    -        "EDISON POWER", "",
    -        account, 'Expenses:Home:Electricity',
    -        lambda: random.normalvariate(65, 0))
    -
    -    internet_expenses = generate_periodic_expenses(
    -        delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 20, 22),
    -        "Wine-Tarner Cable", "",
    -        account, 'Expenses:Home:Internet',
    -        lambda: random.normalvariate(80, 0.10))
    -
    -    phone_expenses = generate_periodic_expenses(
    -        delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 17, 19),
    -        "Verizon Wireless", "",
    -        account, 'Expenses:Home:Phone',
    -        lambda: random.normalvariate(60, 10))
    -
    -    return data.sorted(fee_expenses +
    -                       rent_expenses +
    -                       electricity_expenses +
    -                       internet_expenses +
    -                       phone_expenses)
    -
    +
    def generate_banking_expenses(date_begin, date_end, account, rent_amount):
    +    """Generate expenses paid out of a checking account, typically living expenses.
    +
    +    Args:
    +      date_begin: The start date.
    +      date_end: The end date.
    +      account: The checking account to generate expenses to.
    +      rent_amount: The amount of rent.
    +    Returns:
    +      A list of directives.
    +    """
    +    fee_expenses = generate_periodic_expenses(
    +        rrule.rrule(rrule.MONTHLY, bymonthday=4, dtstart=date_begin, until=date_end),
    +        "BANK FEES",
    +        "Monthly bank fee",
    +        account,
    +        "Expenses:Financial:Fees",
    +        lambda: D("4.00"),
    +    )
    +
    +    rent_expenses = generate_periodic_expenses(
    +        delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 2, 5),
    +        "RiverBank Properties",
    +        "Paying the rent",
    +        account,
    +        "Expenses:Home:Rent",
    +        lambda: random.normalvariate(float(rent_amount), 0),
    +    )
    +
    +    electricity_expenses = generate_periodic_expenses(
    +        delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 7, 8),
    +        "EDISON POWER",
    +        "",
    +        account,
    +        "Expenses:Home:Electricity",
    +        lambda: random.normalvariate(65, 0),
    +    )
    +
    +    internet_expenses = generate_periodic_expenses(
    +        delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 20, 22),
    +        "Wine-Tarner Cable",
    +        "",
    +        account,
    +        "Expenses:Home:Internet",
    +        lambda: random.normalvariate(80, 0.10),
    +    )
    +
    +    phone_expenses = generate_periodic_expenses(
    +        delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 17, 19),
    +        "Verizon Wireless",
    +        "",
    +        account,
    +        "Expenses:Home:Phone",
    +        lambda: random.normalvariate(60, 10),
    +    )
    +
    +    return data.sorted(
    +        fee_expenses
    +        + rent_expenses
    +        + electricity_expenses
    +        + internet_expenses
    +        + phone_expenses
    +    )
    +
    @@ -3747,7 +2679,7 @@

    -beancount.scripts.example.generate_clearing_entries(date_iter, payee, narration, entries, account_clear, account_from) +beancount.scripts.example.generate_clearing_entries(date_iter, payee, narration, entries, account_clear, account_from)

    @@ -3795,48 +2727,53 @@

    Source code in beancount/scripts/example.py -
    def generate_clearing_entries(date_iter,
    -                              payee, narration,
    -                              entries, account_clear, account_from):
    -    """Generate entries to clear the value of an account.
    -
    -    Args:
    -      date_iter: An iterator of datetime.date instances.
    -      payee: A string, the payee name to use on the transactions.
    -      narration: A string, the narration to use on the transactions.
    -      entries: A list of entries.
    -      account_clear: The account to clear.
    -      account_from: The source account to clear 'account_clear' from.
    -    Returns:
    -      A list of directives.
    -    """
    -    # The next date we're looking for.
    -    next_date = next(iter(date_iter))
    -
    -    # Iterate over all the postings of the account to clear.
    -    new_entries = []
    -    for txn_posting, balances in postings_for(entries, [account_clear]):
    -        balance_clear = balances[account_clear]
    -
    -        # Check if we need to clear.
    -        if next_date <= txn_posting.txn.date:
    -            pos_amount = balance_clear.get_currency_units('CCY')
    -            neg_amount = -pos_amount
    -            new_entries.extend(parse("""
    -              {next_date} * "{payee}" "{narration}"
    -                {account_clear}     {neg_amount.number:.2f} CCY
    -                {account_from}      {pos_amount.number:.2f} CCY
    -            """, **locals()))
    -            balance_clear.add_amount(neg_amount)
    -
    -            # Advance to the next date we're looking for.
    -            try:
    -                next_date = next(iter(date_iter))
    -            except StopIteration:
    -                break
    -
    -    return new_entries
    -
    +
    def generate_clearing_entries(
    +    date_iter, payee, narration, entries, account_clear, account_from
    +):
    +    """Generate entries to clear the value of an account.
    +
    +    Args:
    +      date_iter: An iterator of datetime.date instances.
    +      payee: A string, the payee name to use on the transactions.
    +      narration: A string, the narration to use on the transactions.
    +      entries: A list of entries.
    +      account_clear: The account to clear.
    +      account_from: The source account to clear 'account_clear' from.
    +    Returns:
    +      A list of directives.
    +    """
    +    new_entries = []
    +
    +    # The next date we're looking for.
    +    date_iter = iter(date_iter)
    +    next_date = next(date_iter, None)
    +    if not next_date:
    +        return new_entries
    +
    +    # Iterate over all the postings of the account to clear.
    +    for txn_posting, balances in postings_for(entries, [account_clear]):
    +        balance_clear = balances[account_clear]
    +
    +        # Check if we need to clear.
    +        if next_date <= txn_posting.txn.date:
    +            pos_amount = balance_clear.get_currency_units("CCY")
    +            neg_amount = -pos_amount
    +            new_entries.extend(
    +                parse(f"""
    +              {next_date} * "{payee}" "{narration}"
    +                {account_clear}     {neg_amount.number:.2f} CCY
    +                {account_from}      {pos_amount.number:.2f} CCY
    +            """)
    +            )
    +            balance_clear.add_amount(neg_amount)
    +
    +            # Advance to the next date we're looking for.
    +            next_date = next(date_iter, None)
    +            if not next_date:
    +                break
    +
    +    return new_entries
    +
    @@ -3849,7 +2786,7 @@

    -beancount.scripts.example.generate_commodity_entries(date_birth) +beancount.scripts.example.generate_commodity_entries(date_birth)

    @@ -3892,63 +2829,63 @@

    Source code in beancount/scripts/example.py -
    def generate_commodity_entries(date_birth):
    -    """Create a list of Commodity entries for all the currencies we're using.
    -
    -    Args:
    -      date_birth: A datetime.date instance, the date of birth of the user.
    -    Returns:
    -      A list of Commodity entries for all the commodities in use.
    -    """
    -    return parse("""
    -
    -        1792-01-01 commodity USD
    -          name: "US Dollar"
    -          export: "CASH"
    -
    -        {date_birth} commodity VACHR
    -          name: "Employer Vacation Hours"
    -          export: "IGNORE"
    -
    -        {date_birth} commodity IRAUSD
    -          name: "US 401k and IRA Contributions"
    -          export: "IGNORE"
    -
    -        2009-05-01 commodity RGAGX
    -          name: "American Funds The Growth Fund of America Class R-6"
    -          export: "MUTF:RGAGX"
    -          price: "USD:google/MUTF:RGAGX"
    -
    -        1995-09-18 commodity VBMPX
    -          name: "Vanguard Total Bond Market Index Fund Institutional Plus Shares"
    -          export: "MUTF:VBMPX"
    -          price: "USD:google/MUTF:VBMPX"
    -
    -        2004-01-20 commodity ITOT
    -          name: "iShares Core S&P Total U.S. Stock Market ETF"
    -          export: "NYSEARCA:ITOT"
    -          price: "USD:google/NYSEARCA:ITOT"
    -
    -        2007-07-20 commodity VEA
    -          name: "Vanguard FTSE Developed Markets ETF"
    -          export: "NYSEARCA:VEA"
    -          price: "USD:google/NYSEARCA:VEA"
    -
    -        2004-01-26 commodity VHT
    -          name: "Vanguard Health Care ETF"
    -          export: "NYSEARCA:VHT"
    -          price: "USD:google/NYSEARCA:VHT"
    -
    -        2004-11-01 commodity GLD
    -          name: "SPDR Gold Trust (ETF)"
    -          export: "NYSEARCA:GLD"
    -          price: "USD:google/NYSEARCA:GLD"
    -
    -        1900-01-01 commodity VMMXX
    -          export: "MUTF:VMMXX (MONEY:USD)"
    -
    -    """, **locals())
    -
    +
    def generate_commodity_entries(date_birth):
    +    """Create a list of Commodity entries for all the currencies we're using.
    +
    +    Args:
    +      date_birth: A datetime.date instance, the date of birth of the user.
    +    Returns:
    +      A list of Commodity entries for all the commodities in use.
    +    """
    +    return parse(f"""
    +
    +        1792-01-01 commodity USD
    +          name: "US Dollar"
    +          export: "CASH"
    +
    +        {date_birth} commodity VACHR
    +          name: "Employer Vacation Hours"
    +          export: "IGNORE"
    +
    +        {date_birth} commodity IRAUSD
    +          name: "US 401k and IRA Contributions"
    +          export: "IGNORE"
    +
    +        2009-05-01 commodity RGAGX
    +          name: "American Funds The Growth Fund of America Class R-6"
    +          export: "MUTF:RGAGX"
    +          price: "USD:google/MUTF:RGAGX"
    +
    +        1995-09-18 commodity VBMPX
    +          name: "Vanguard Total Bond Market Index Fund Institutional Plus Shares"
    +          export: "MUTF:VBMPX"
    +          price: "USD:google/MUTF:VBMPX"
    +
    +        2004-01-20 commodity ITOT
    +          name: "iShares Core S&P Total U.S. Stock Market ETF"
    +          export: "NYSEARCA:ITOT"
    +          price: "USD:google/NYSEARCA:ITOT"
    +
    +        2007-07-20 commodity VEA
    +          name: "Vanguard FTSE Developed Markets ETF"
    +          export: "NYSEARCA:VEA"
    +          price: "USD:google/NYSEARCA:VEA"
    +
    +        2004-01-26 commodity VHT
    +          name: "Vanguard Health Care ETF"
    +          export: "NYSEARCA:VHT"
    +          price: "USD:google/NYSEARCA:VHT"
    +
    +        2004-11-01 commodity GLD
    +          name: "SPDR Gold Trust (ETF)"
    +          export: "NYSEARCA:GLD"
    +          price: "USD:google/NYSEARCA:GLD"
    +
    +        1900-01-01 commodity VMMXX
    +          export: "MUTF:VMMXX (MONEY:USD)"
    +
    +    """)
    +
    @@ -3961,7 +2898,7 @@

    -beancount.scripts.example.generate_employment_income(employer_name, employer_address, annual_salary, account_deposit, account_retirement, date_begin, date_end) +beancount.scripts.example.generate_employment_income(employer_name, employer_address, annual_salary, account_deposit, account_retirement, date_begin, date_end)

    @@ -4011,135 +2948,141 @@

    Source code in beancount/scripts/example.py -
    def generate_employment_income(employer_name,
    -                               employer_address,
    -                               annual_salary,
    -                               account_deposit,
    -                               account_retirement,
    -                               date_begin,
    -                               date_end):
    -    """Generate bi-weekly entries for payroll salary income.
    -
    -    Args:
    -      employer_name: A string, the human-readable name of the employer.
    -      employer_address: A string, the address of the employer.
    -      annual_salary: A Decimal, the annual salary of the employee.
    -      account_deposit: An account string, the account to deposit the salary to.
    -      account_retirement: An account string, the account to deposit retirement
    -        contributions to.
    -      date_begin: The start date.
    -      date_end: The end date.
    -    Returns:
    -      A list of directives, including open directives for the account.
    -    """
    -    preamble = parse("""
    -
    -        {date_begin} event "employer" "{employer_name}, {employer_address}"
    -
    -        {date_begin} open Income:CC:Employer1:Salary           CCY
    -        ;{date_begin} open Income:CC:Employer1:AnnualBonus     CCY
    -        {date_begin} open Income:CC:Employer1:GroupTermLife    CCY
    -
    -        {date_begin} open Income:CC:Employer1:Vacation         VACHR
    -        {date_begin} open Assets:CC:Employer1:Vacation         VACHR
    -        {date_begin} open Expenses:Vacation                    VACHR
    -
    -        {date_begin} open Expenses:Health:Life:GroupTermLife
    -        {date_begin} open Expenses:Health:Medical:Insurance
    -        {date_begin} open Expenses:Health:Dental:Insurance
    -        {date_begin} open Expenses:Health:Vision:Insurance
    -
    -        ;{date_begin} open Expenses:Vacation:Employer
    -
    -    """, **locals())
    -
    -    date_prev = None
    -
    -    contrib_retirement = ZERO
    -    contrib_socsec = ZERO
    -
    -    biweekly_pay = annual_salary / 26
    -    gross = biweekly_pay
    -
    -    medicare = gross * D('0.0231')
    -    federal = gross * D('0.2303')
    -    state = gross * D('0.0791')
    -    city = gross * D('0.0379')
    -    sdi = D('1.12')
    -
    -    lifeinsurance = D('24.32')
    -    dental = D('2.90')
    -    medical = D('27.38')
    -    vision = D('42.30')
    -
    -    fixed = (medicare + federal + state + city + sdi +
    -             dental + medical + vision)
    -
    -    # Calculate vacation hours per-pay.
    -    with decimal.localcontext() as ctx:
    -        ctx.prec = 4
    -        vacation_hrs = (ANNUAL_VACATION_DAYS * D('8')) / D('26')
    -
    -    transactions = []
    -    for dtime in misc_utils.skipiter(
    -            rrule.rrule(rrule.WEEKLY, byweekday=rrule.TH,
    -                        dtstart=date_begin, until=date_end), 2):
    -        date = dtime.date()
    -        year = date.year
    -
    -        if not date_prev or date_prev.year != date.year:
    -            contrib_retirement = RETIREMENT_LIMITS.get(date.year, RETIREMENT_LIMITS[None])
    -            contrib_socsec = D('7000')
    -        date_prev = date
    -
    -        retirement_uncapped = math.ceil((gross * D('0.25')) / 100) * 100
    -        retirement = min(contrib_retirement, retirement_uncapped)
    -        contrib_retirement -= retirement
    -
    -        socsec_uncapped = gross * D('0.0610')
    -        socsec = min(contrib_socsec, socsec_uncapped)
    -        contrib_socsec -= socsec
    -
    -        with decimal.localcontext() as ctx:
    -            ctx.prec = 6
    -            deposit = (gross - retirement - fixed - socsec)
    -
    -        retirement_neg = -retirement
    -        gross_neg = -gross
    -        lifeinsurance_neg = -lifeinsurance
    -        vacation_hrs_neg = -vacation_hrs
    -
    -        template = """
    -            {date} * "{employer_name}" "Payroll"
    -              {account_deposit}                                 {deposit:.2f} CCY
    -              {account_retirement}                              {retirement:.2f} CCY
    -              Assets:CC:Federal:PreTax401k                      {retirement_neg:.2f} DEFCCY
    -              Expenses:Taxes:Y{year}:CC:Federal:PreTax401k      {retirement:.2f} DEFCCY
    -              Income:CC:Employer1:Salary                        {gross_neg:.2f} CCY
    -              Income:CC:Employer1:GroupTermLife                 {lifeinsurance_neg:.2f} CCY
    -              Expenses:Health:Life:GroupTermLife                {lifeinsurance:.2f} CCY
    -              Expenses:Health:Dental:Insurance                  {dental} CCY
    -              Expenses:Health:Medical:Insurance                 {medical} CCY
    -              Expenses:Health:Vision:Insurance                  {vision} CCY
    -              Expenses:Taxes:Y{year}:CC:Medicare                {medicare:.2f} CCY
    -              Expenses:Taxes:Y{year}:CC:Federal                 {federal:.2f} CCY
    -              Expenses:Taxes:Y{year}:CC:State                   {state:.2f} CCY
    -              Expenses:Taxes:Y{year}:CC:CityNYC                 {city:.2f} CCY
    -              Expenses:Taxes:Y{year}:CC:SDI                     {sdi:.2f} CCY
    -              Expenses:Taxes:Y{year}:CC:SocSec                  {socsec:.2f} CCY
    -              Assets:CC:Employer1:Vacation                      {vacation_hrs:.2f} VACHR
    -              Income:CC:Employer1:Vacation                      {vacation_hrs_neg:.2f} VACHR
    -        """
    -        if retirement == ZERO:
    -            # Remove retirement lines.
    -            template = '\n'.join(line
    -                                 for line in template.splitlines()
    -                                 if not re.search(r'\bretirement\b', line))
    -
    -        transactions.extend(parse(template, **locals()))
    -
    -    return preamble + transactions
    -
    +
    def generate_employment_income(
    +    employer_name,
    +    employer_address,
    +    annual_salary,
    +    account_deposit,
    +    account_retirement,
    +    date_begin,
    +    date_end,
    +):
    +    """Generate bi-weekly entries for payroll salary income.
    +
    +    Args:
    +      employer_name: A string, the human-readable name of the employer.
    +      employer_address: A string, the address of the employer.
    +      annual_salary: A Decimal, the annual salary of the employee.
    +      account_deposit: An account string, the account to deposit the salary to.
    +      account_retirement: An account string, the account to deposit retirement
    +        contributions to.
    +      date_begin: The start date.
    +      date_end: The end date.
    +    Returns:
    +      A list of directives, including open directives for the account.
    +    """
    +    preamble = parse(f"""
    +
    +        {date_begin} event "employer" "{employer_name}, {employer_address}"
    +
    +        {date_begin} open Income:CC:Employer1:Salary           CCY
    +        ;{date_begin} open Income:CC:Employer1:AnnualBonus     CCY
    +        {date_begin} open Income:CC:Employer1:GroupTermLife    CCY
    +
    +        {date_begin} open Income:CC:Employer1:Vacation         VACHR
    +        {date_begin} open Assets:CC:Employer1:Vacation         VACHR
    +        {date_begin} open Expenses:Vacation                    VACHR
    +
    +        {date_begin} open Expenses:Health:Life:GroupTermLife
    +        {date_begin} open Expenses:Health:Medical:Insurance
    +        {date_begin} open Expenses:Health:Dental:Insurance
    +        {date_begin} open Expenses:Health:Vision:Insurance
    +
    +        ;{date_begin} open Expenses:Vacation:Employer
    +
    +    """)
    +
    +    date_prev = None
    +
    +    contrib_retirement = ZERO
    +    contrib_socsec = ZERO
    +
    +    biweekly_pay = annual_salary / 26
    +    gross = biweekly_pay
    +
    +    medicare = gross * D("0.0231")
    +    federal = gross * D("0.2303")
    +    state = gross * D("0.0791")
    +    city = gross * D("0.0379")
    +    sdi = D("1.12")
    +
    +    lifeinsurance = D("24.32")
    +    dental = D("2.90")
    +    medical = D("27.38")
    +    vision = D("42.30")
    +
    +    fixed = medicare + federal + state + city + sdi + dental + medical + vision
    +
    +    # Calculate vacation hours per-pay.
    +    with decimal.localcontext() as ctx:
    +        ctx.prec = 4
    +        vacation_hrs = (ANNUAL_VACATION_DAYS * D("8")) / D("26")
    +
    +    transactions = []
    +    for dtime in misc_utils.skipiter(
    +        rrule.rrule(rrule.WEEKLY, byweekday=rrule.TH, dtstart=date_begin, until=date_end), 2
    +    ):
    +        date = dtime.date()
    +        year = date.year
    +
    +        if not date_prev or date_prev.year != date.year:
    +            contrib_retirement = RETIREMENT_LIMITS.get(date.year, RETIREMENT_LIMITS[None])
    +            contrib_socsec = D("7000")
    +        date_prev = date
    +
    +        retirement_uncapped = math.ceil((gross * D("0.25")) / 100) * 100
    +        retirement = min(contrib_retirement, retirement_uncapped)
    +        contrib_retirement -= retirement
    +
    +        socsec_uncapped = gross * D("0.0610")
    +        socsec = min(contrib_socsec, socsec_uncapped)
    +        contrib_socsec -= socsec
    +
    +        with decimal.localcontext() as ctx:
    +            ctx.prec = 6
    +            deposit = gross - retirement - fixed - socsec
    +
    +        retirement_neg = -retirement
    +        gross_neg = -gross
    +        lifeinsurance_neg = -lifeinsurance
    +        vacation_hrs_neg = -vacation_hrs
    +
    +        transactions.extend(
    +            parse(
    +                f"""
    +            {date} * "{employer_name}" "Payroll"
    +              {account_deposit}                                 {deposit:.2f} CCY
    +              """
    +                + (
    +                    ""
    +                    if retirement == ZERO
    +                    else f"""\
    +              {account_retirement}                              {retirement:.2f} CCY
    +              Assets:CC:Federal:PreTax401k                      {retirement_neg:.2f} DEFCCY
    +              Expenses:Taxes:Y{year}:CC:Federal:PreTax401k      {retirement:.2f} DEFCCY
    +              """
    +                )
    +                + f"""\
    +              Income:CC:Employer1:Salary                        {gross_neg:.2f} CCY
    +              Income:CC:Employer1:GroupTermLife                 {lifeinsurance_neg:.2f} CCY
    +              Expenses:Health:Life:GroupTermLife                {lifeinsurance:.2f} CCY
    +              Expenses:Health:Dental:Insurance                  {dental} CCY
    +              Expenses:Health:Medical:Insurance                 {medical} CCY
    +              Expenses:Health:Vision:Insurance                  {vision} CCY
    +              Expenses:Taxes:Y{year}:CC:Medicare                {medicare:.2f} CCY
    +              Expenses:Taxes:Y{year}:CC:Federal                 {federal:.2f} CCY
    +              Expenses:Taxes:Y{year}:CC:State                   {state:.2f} CCY
    +              Expenses:Taxes:Y{year}:CC:CityNYC                 {city:.2f} CCY
    +              Expenses:Taxes:Y{year}:CC:SDI                     {sdi:.2f} CCY
    +              Expenses:Taxes:Y{year}:CC:SocSec                  {socsec:.2f} CCY
    +              Assets:CC:Employer1:Vacation                      {vacation_hrs:.2f} VACHR
    +              Income:CC:Employer1:Vacation                      {vacation_hrs_neg:.2f} VACHR
    +        """
    +            )
    +        )
    +
    +    return preamble + transactions
    +
    @@ -4152,7 +3095,7 @@

    -beancount.scripts.example.generate_expense_accounts(date_birth) +beancount.scripts.example.generate_expense_accounts(date_birth)

    @@ -4195,33 +3138,33 @@

    Source code in beancount/scripts/example.py -
    def generate_expense_accounts(date_birth):
    -    """Generate directives for expense accounts.
    +          
    def generate_expense_accounts(date_birth):
    +    """Generate directives for expense accounts.
     
    -    Args:
    -      date_birth: Birth date of the character.
    -    Returns:
    -      A list of directives.
    -    """
    -    return parse("""
    +    Args:
    +      date_birth: Birth date of the character.
    +    Returns:
    +      A list of directives.
    +    """
    +    return parse(f"""
     
    -      {date_birth} open Expenses:Food:Groceries
    -      {date_birth} open Expenses:Food:Restaurant
    -      {date_birth} open Expenses:Food:Coffee
    -      {date_birth} open Expenses:Food:Alcohol
    +      {date_birth} open Expenses:Food:Groceries
    +      {date_birth} open Expenses:Food:Restaurant
    +      {date_birth} open Expenses:Food:Coffee
    +      {date_birth} open Expenses:Food:Alcohol
     
    -      {date_birth} open Expenses:Transport:Tram
    +      {date_birth} open Expenses:Transport:Tram
     
    -      {date_birth} open Expenses:Home:Rent
    -      {date_birth} open Expenses:Home:Electricity
    -      {date_birth} open Expenses:Home:Internet
    -      {date_birth} open Expenses:Home:Phone
    +      {date_birth} open Expenses:Home:Rent
    +      {date_birth} open Expenses:Home:Electricity
    +      {date_birth} open Expenses:Home:Internet
    +      {date_birth} open Expenses:Home:Phone
     
    -      {date_birth} open Expenses:Financial:Fees
    -      {date_birth} open Expenses:Financial:Commissions
    +      {date_birth} open Expenses:Financial:Fees
    +      {date_birth} open Expenses:Financial:Commissions
     
    -    """, **locals())
    -
    + """) +
    @@ -4234,7 +3177,7 @@

    -beancount.scripts.example.generate_open_entries(date, accounts, currency=None) +beancount.scripts.example.generate_open_entries(date, accounts, currency=None)

    @@ -4279,23 +3222,26 @@

    Source code in beancount/scripts/example.py -
    def generate_open_entries(date, accounts, currency=None):
    -    """Generate a list of Open entries for the given accounts:
    -
    -    Args:
    -      date: A datetime.date instance for the open entries.
    -      accounts: A list of account strings.
    -      currency: An optional currency constraint.
    -    Returns:
    -      A list of Open directives.
    -    """
    -    assert isinstance(accounts, (list, tuple))
    -    return parse(''.join(
    -        '{date} open {account} {currency}\n'.format(date=date,
    -                                                    account=account,
    -                                                    currency=currency or '')
    -        for account in accounts))
    -
    +
    def generate_open_entries(date, accounts, currency=None):
    +    """Generate a list of Open entries for the given accounts:
    +
    +    Args:
    +      date: A datetime.date instance for the open entries.
    +      accounts: A list of account strings.
    +      currency: An optional currency constraint.
    +    Returns:
    +      A list of Open directives.
    +    """
    +    assert isinstance(accounts, (list, tuple))
    +    return parse(
    +        "".join(
    +            "{date} open {account} {currency}\n".format(
    +                date=date, account=account, currency=currency or ""
    +            )
    +            for account in accounts
    +        )
    +    )
    +
    @@ -4308,7 +3254,7 @@

    -beancount.scripts.example.generate_outgoing_transfers(entries, account, account_out, transfer_minimum, transfer_threshold, transfer_increment) +beancount.scripts.example.generate_outgoing_transfers(entries, account, account_out, transfer_minimum, transfer_threshold, transfer_increment)

    @@ -4361,70 +3307,72 @@

    Source code in beancount/scripts/example.py -
    def generate_outgoing_transfers(entries,
    -                                account,
    -                                account_out,
    -                                transfer_minimum,
    -                                transfer_threshold,
    -                                transfer_increment):
    -    """Generate transfers of accumulated funds out of an account.
    -
    -    This monitors the balance of an account and when it is beyond a threshold,
    -    generate out transfers form that account to another account.
    -
    -    Args:
    -      entries: A list of existing entries that affect this account so far.
    -        The generated entries will also affect this account.
    -      account: An account string, the account to monitor.
    -      account_out: An account string, the savings account to make transfers to.
    -      transfer_minimum: The minimum amount of funds to always leave in this account
    -        after a transfer.
    -      transfer_threshold: The minimum amount of funds to be able to transfer out without
    -        breaking the minimum.
    -      transfer_increment: A Decimal, the increment to round transfers to.
    -    Returns:
    -      A list of new directives, the transfers to add to the given account.
    -    """
    -    last_date = entries[-1].date
    -
    -    # Reverse the balance amounts taking into account the minimum balance for
    -    # all time in the future.
    -    amounts = [(balances[account].get_currency_units('CCY').number, txn_posting)
    -               for txn_posting, balances in postings_for(entries, [account])]
    -    reversed_amounts = []
    -    last_amount, _ = amounts[-1]
    -    for current_amount, _ in reversed(amounts):
    -        if current_amount < last_amount:
    -            reversed_amounts.append(current_amount)
    -            last_amount = current_amount
    -        else:
    -            reversed_amounts.append(last_amount)
    -    capped_amounts = reversed(reversed_amounts)
    -
    -    # Create transfers outward where the future allows it.
    -    new_entries = []
    -    offset_amount = ZERO
    -    for current_amount, (_, txn_posting) in zip(capped_amounts, amounts):
    -        if txn_posting.txn.date >= last_date:
    -            break
    -
    -        adjusted_amount = current_amount - offset_amount
    -        if adjusted_amount > (transfer_minimum + transfer_threshold):
    -            amount_transfer = round_to(adjusted_amount - transfer_minimum,
    -                                       transfer_increment)
    -
    -            date = txn_posting.txn.date + datetime.timedelta(days=1)
    -            amount_transfer_neg = -amount_transfer
    -            new_entries.extend(parse("""
    -              {date} * "Transfering accumulated savings to other account"
    -                {account}          {amount_transfer_neg:2f} CCY
    -                {account_out}      {amount_transfer:2f} CCY
    -            """, **locals()))
    -
    -            offset_amount += amount_transfer
    -
    -    return new_entries
    -
    +
    def generate_outgoing_transfers(
    +    entries, account, account_out, transfer_minimum, transfer_threshold, transfer_increment
    +):
    +    """Generate transfers of accumulated funds out of an account.
    +
    +    This monitors the balance of an account and when it is beyond a threshold,
    +    generate out transfers form that account to another account.
    +
    +    Args:
    +      entries: A list of existing entries that affect this account so far.
    +        The generated entries will also affect this account.
    +      account: An account string, the account to monitor.
    +      account_out: An account string, the savings account to make transfers to.
    +      transfer_minimum: The minimum amount of funds to always leave in this account
    +        after a transfer.
    +      transfer_threshold: The minimum amount of funds to be able to transfer out without
    +        breaking the minimum.
    +      transfer_increment: A Decimal, the increment to round transfers to.
    +    Returns:
    +      A list of new directives, the transfers to add to the given account.
    +    """
    +    last_date = entries[-1].date
    +
    +    # Reverse the balance amounts taking into account the minimum balance for
    +    # all time in the future.
    +    amounts = [
    +        (balances[account].get_currency_units("CCY").number, txn_posting)
    +        for txn_posting, balances in postings_for(entries, [account])
    +    ]
    +    reversed_amounts = []
    +    last_amount, _ = amounts[-1]
    +    for current_amount, _ in reversed(amounts):
    +        if current_amount < last_amount:
    +            reversed_amounts.append(current_amount)
    +            last_amount = current_amount
    +        else:
    +            reversed_amounts.append(last_amount)
    +    capped_amounts = reversed(reversed_amounts)
    +
    +    # Create transfers outward where the future allows it.
    +    new_entries = []
    +    offset_amount = ZERO
    +    for current_amount, (_, txn_posting) in zip(capped_amounts, amounts):
    +        if txn_posting.txn.date >= last_date:
    +            break
    +
    +        adjusted_amount = current_amount - offset_amount
    +        if adjusted_amount > (transfer_minimum + transfer_threshold):
    +            amount_transfer = round_to(
    +                adjusted_amount - transfer_minimum, transfer_increment
    +            )
    +
    +            date = txn_posting.txn.date + datetime.timedelta(days=1)
    +            amount_transfer_neg = -amount_transfer
    +            new_entries.extend(
    +                parse(f"""
    +              {date} * "Transfering accumulated savings to other account"
    +                {account}          {amount_transfer_neg:2f} CCY
    +                {account_out}      {amount_transfer:2f} CCY
    +            """)
    +            )
    +
    +            offset_amount += amount_transfer
    +
    +    return new_entries
    +
    @@ -4437,7 +3385,7 @@

    -beancount.scripts.example.generate_periodic_expenses(date_iter, payee, narration, account_from, account_to, amount_generator) +beancount.scripts.example.generate_periodic_expenses(date_iter, payee, narration, account_from, account_to, amount_generator)

    @@ -4486,42 +3434,41 @@

    Source code in beancount/scripts/example.py -
    def generate_periodic_expenses(date_iter,
    -                               payee, narration,
    -                               account_from, account_to,
    -                               amount_generator):
    -    """Generate periodic expense transactions.
    -
    -    Args:
    -      date_iter: An iterator for dates or datetimes.
    -      payee: A string, the payee name to use on the transactions, or a set of such strings
    -        to randomly choose from
    -      narration: A string, the narration to use on the transactions.
    -      account_from: An account string the debited account.
    -      account_to: An account string the credited account.
    -      amount_generator: A callable object to generate variates.
    -    Returns:
    -      A list of directives.
    -    """
    -    new_entries = []
    -    for dtime in date_iter:
    -        date = dtime.date() if isinstance(dtime, datetime.datetime) else dtime
    -        amount = D(amount_generator())
    -        txn_payee = (payee
    -                     if isinstance(payee, str)
    -                     else random.choice(payee))
    -        txn_narration = (narration
    -                         if isinstance(narration, str)
    -                         else random.choice(narration))
    -        amount_neg = -amount
    -        new_entries.extend(parse("""
    -          {date} * "{txn_payee}" "{txn_narration}"
    -            {account_from}    {amount_neg:.2f} CCY
    -            {account_to}      {amount:.2f} CCY
    -        """, **locals()))
    -
    -    return new_entries
    -
    +
    def generate_periodic_expenses(
    +    date_iter, payee, narration, account_from, account_to, amount_generator
    +):
    +    """Generate periodic expense transactions.
    +
    +    Args:
    +      date_iter: An iterator for dates or datetimes.
    +      payee: A string, the payee name to use on the transactions, or a set of such strings
    +        to randomly choose from
    +      narration: A string, the narration to use on the transactions.
    +      account_from: An account string the debited account.
    +      account_to: An account string the credited account.
    +      amount_generator: A callable object to generate variates.
    +    Returns:
    +      A list of directives.
    +    """
    +    new_entries = []
    +    for dtime in date_iter:
    +        date = dtime.date() if isinstance(dtime, datetime.datetime) else dtime
    +        amount = D(amount_generator())
    +        txn_payee = payee if isinstance(payee, str) else random.choice(payee)
    +        txn_narration = (
    +            narration if isinstance(narration, str) else random.choice(narration)
    +        )
    +        amount_neg = -amount
    +        new_entries.extend(
    +            parse(f"""
    +          {date} * "{txn_payee}" "{txn_narration}"
    +            {account_from}    {amount_neg:.2f} CCY
    +            {account_to}      {amount:.2f} CCY
    +        """)
    +        )
    +
    +    return new_entries
    +
    @@ -4534,7 +3481,7 @@

    -beancount.scripts.example.generate_prices(date_begin, date_end, currencies, cost_currency) +beancount.scripts.example.generate_prices(date_begin, date_end, currencies, cost_currency)

    @@ -4580,36 +3527,40 @@

    Source code in beancount/scripts/example.py -
    def generate_prices(date_begin, date_end, currencies, cost_currency):
    -    """Generate weekly or monthly price entries for the given currencies.
    -
    -    Args:
    -      date_begin: The start date.
    -      date_end: The end date.
    -      currencies: A list of currency strings to generate prices for.
    -      cost_currency: A string, the cost currency.
    -    Returns:
    -      A list of Price directives.
    -    """
    -    digits = D('0.01')
    -    entries = []
    -    counter = itertools.count()
    -    for currency in currencies:
    -        start_price = random.uniform(30, 200)
    -        growth = random.uniform(0.02, 0.13) # %/year
    -        mu = growth * (7 / 365)
    -        sigma = random.uniform(0.005, 0.02) # Vol
    -
    -        for dtime, price_float in zip(rrule.rrule(rrule.WEEKLY, byweekday=rrule.FR,
    -                                                  dtstart=date_begin, until=date_end),
    -                                      price_series(start_price, mu, sigma)):
    -            price = D(price_float).quantize(digits)
    -            meta = data.new_metadata(generate_prices.__name__, next(counter))
    -            entry = data.Price(meta, dtime.date(), currency,
    -                               amount.Amount(price, cost_currency))
    -            entries.append(entry)
    -    return entries
    -
    +
    def generate_prices(date_begin, date_end, currencies, cost_currency):
    +    """Generate weekly or monthly price entries for the given currencies.
    +
    +    Args:
    +      date_begin: The start date.
    +      date_end: The end date.
    +      currencies: A list of currency strings to generate prices for.
    +      cost_currency: A string, the cost currency.
    +    Returns:
    +      A list of Price directives.
    +    """
    +    digits = D("0.01")
    +    entries = []
    +    counter = itertools.count()
    +    for currency in currencies:
    +        start_price = random.uniform(30, 200)
    +        growth = random.uniform(0.02, 0.13)  # %/year
    +        mu = growth * (7 / 365)
    +        sigma = random.uniform(0.005, 0.02)  # Vol
    +
    +        for dtime, price_float in zip(
    +            rrule.rrule(
    +                rrule.WEEKLY, byweekday=rrule.FR, dtstart=date_begin, until=date_end
    +            ),
    +            price_series(start_price, mu, sigma),
    +        ):
    +            price = D(price_float).quantize(digits)
    +            meta = data.new_metadata(generate_prices.__name__, next(counter))
    +            entry = data.Price(
    +                meta, dtime.date(), currency, amount.Amount(price, cost_currency)
    +            )
    +            entries.append(entry)
    +    return entries
    +
    @@ -4622,7 +3573,7 @@

    -beancount.scripts.example.generate_regular_credit_expenses(date_birth, date_begin, date_end, account_credit, account_checking) +beancount.scripts.example.generate_regular_credit_expenses(date_birth, date_begin, date_end, account_credit, account_checking)

    @@ -4670,50 +3621,61 @@

    Source code in beancount/scripts/example.py -
    def generate_regular_credit_expenses(date_birth, date_begin, date_end,
    -                                     account_credit,
    -                                     account_checking):
    -    """Generate expenses paid out of a credit card account, including payments to the
    -    credit card.
    -
    -    Args:
    -      date_birth: The user's birth date.
    -      date_begin: The start date.
    -      date_end: The end date.
    -      account_credit: The credit card account to generate expenses against.
    -      account_checking: The checking account to generate payments from.
    -    Returns:
    -      A list of directives.
    -    """
    -    restaurant_expenses = generate_periodic_expenses(
    -        date_random_seq(date_begin, date_end, 1, 5),
    -        RESTAURANT_NAMES, RESTAURANT_NARRATIONS,
    -        account_credit, 'Expenses:Food:Restaurant',
    -        lambda: min(random.lognormvariate(math.log(30), math.log(1.5)),
    -                    random.randint(200, 220)))
    -
    -    groceries_expenses = generate_periodic_expenses(
    -        date_random_seq(date_begin, date_end, 5, 20),
    -        GROCERIES_NAMES, "Buying groceries",
    -        account_credit, 'Expenses:Food:Groceries',
    -        lambda: min(random.lognormvariate(math.log(80), math.log(1.3)),
    -                    random.randint(250, 300)))
    -
    -    subway_expenses = generate_periodic_expenses(
    -        date_random_seq(date_begin, date_end, 27, 33),
    -        "Metro Transport Authority", "Tram tickets",
    -        account_credit, 'Expenses:Transport:Tram',
    -        lambda: D('120.00'))
    -
    -    credit_expenses = data.sorted(restaurant_expenses +
    -                                  groceries_expenses +
    -                                  subway_expenses)
    -
    -    # Entries to open accounts.
    -    credit_preamble = generate_open_entries(date_birth, [account_credit], 'CCY')
    -
    -    return data.sorted(credit_preamble + credit_expenses)
    -
    +
    def generate_regular_credit_expenses(
    +    date_birth, date_begin, date_end, account_credit, account_checking
    +):
    +    """Generate expenses paid out of a credit card account, including payments to the
    +    credit card.
    +
    +    Args:
    +      date_birth: The user's birth date.
    +      date_begin: The start date.
    +      date_end: The end date.
    +      account_credit: The credit card account to generate expenses against.
    +      account_checking: The checking account to generate payments from.
    +    Returns:
    +      A list of directives.
    +    """
    +    restaurant_expenses = generate_periodic_expenses(
    +        date_random_seq(date_begin, date_end, 1, 5),
    +        RESTAURANT_NAMES,
    +        RESTAURANT_NARRATIONS,
    +        account_credit,
    +        "Expenses:Food:Restaurant",
    +        lambda: min(
    +            random.lognormvariate(math.log(30), math.log(1.5)), random.randint(200, 220)
    +        ),
    +    )
    +
    +    groceries_expenses = generate_periodic_expenses(
    +        date_random_seq(date_begin, date_end, 5, 20),
    +        GROCERIES_NAMES,
    +        "Buying groceries",
    +        account_credit,
    +        "Expenses:Food:Groceries",
    +        lambda: min(
    +            random.lognormvariate(math.log(80), math.log(1.3)), random.randint(250, 300)
    +        ),
    +    )
    +
    +    subway_expenses = generate_periodic_expenses(
    +        date_random_seq(date_begin, date_end, 27, 33),
    +        "Metro Transport Authority",
    +        "Tram tickets",
    +        account_credit,
    +        "Expenses:Transport:Tram",
    +        lambda: D("120.00"),
    +    )
    +
    +    credit_expenses = data.sorted(
    +        restaurant_expenses + groceries_expenses + subway_expenses
    +    )
    +
    +    # Entries to open accounts.
    +    credit_preamble = generate_open_entries(date_birth, [account_credit], "CCY")
    +
    +    return data.sorted(credit_preamble + credit_expenses)
    +
    @@ -4726,7 +3688,7 @@

    -beancount.scripts.example.generate_retirement_employer_match(entries, account_invest, account_income) +beancount.scripts.example.generate_retirement_employer_match(entries, account_invest, account_income)

    @@ -4771,38 +3733,40 @@

    Source code in beancount/scripts/example.py -
    def generate_retirement_employer_match(entries, account_invest, account_income):
    -    """Generate employer matching contributions into a retirement account.
    +          
    def generate_retirement_employer_match(entries, account_invest, account_income):
    +    """Generate employer matching contributions into a retirement account.
     
    -    Args:
    -      entries: A list of directives that cover the retirement account.
    -      account_invest: The name of the retirement cash account.
    -      account_income: The name of the income account.
    -    Returns:
    -      A list of new entries generated for employer contributions.
    -    """
    -    match_frac = D('0.50')
    +    Args:
    +      entries: A list of directives that cover the retirement account.
    +      account_invest: The name of the retirement cash account.
    +      account_income: The name of the income account.
    +    Returns:
    +      A list of new entries generated for employer contributions.
    +    """
    +    match_frac = D("0.50")
     
    -    new_entries = parse("""
    +    new_entries = parse(f"""
     
    -      {date} open {account_income}   CCY
    +      {entries[0].date} open {account_income}   CCY
     
    -    """, date=entries[0].date, account_income=account_income)
    +    """)
     
    -    for txn_posting, balances in postings_for(entries, [account_invest]):
    -        amount = txn_posting.posting.units.number * match_frac
    -        amount_neg = -amount
    -        date = txn_posting.txn.date + ONE_DAY
    -        new_entries.extend(parse("""
    +    for txn_posting, balances in postings_for(entries, [account_invest]):
    +        amount = txn_posting.posting.units.number * match_frac
    +        amount_neg = -amount
    +        date = txn_posting.txn.date + ONE_DAY
    +        new_entries.extend(
    +            parse(f"""
     
    -          {date} * "Employer match for contribution"
    -            {account_invest}         {amount:.2f} CCY
    -            {account_income}         {amount_neg:.2f} CCY
    +          {date} * "Employer match for contribution"
    +            {account_invest}         {amount:.2f} CCY
    +            {account_income}         {amount_neg:.2f} CCY
     
    -        """, **locals()))
    +        """)
    +        )
     
    -    return new_entries
    -
    + return new_entries +
    @@ -4815,7 +3779,7 @@

    -beancount.scripts.example.generate_retirement_investments(entries, account, commodities_items, price_map) +beancount.scripts.example.generate_retirement_investments(entries, account, commodities_items, price_map)

    @@ -4862,69 +3826,74 @@

    Source code in beancount/scripts/example.py -
    def generate_retirement_investments(entries, account, commodities_items, price_map):
    -    """Invest money deposited to the given retirement account.
    -
    -    Args:
    -      entries: A list of directives
    -      account: The root account for all retirement investment sub-accounts.
    -      commodities_items: A list of (commodity, fraction to be invested in) items.
    -      price_map: A dict of prices, as per beancount.core.prices.build_price_map().
    -    Returns:
    -      A list of new directives for the given investments. This also generates account
    -      opening directives for the desired investment commodities.
    -    """
    -    open_entries = []
    -    account_cash = join(account, 'Cash')
    -    date_origin = entries[0].date
    -    open_entries.extend(parse("""
    -
    -      {date_origin} open {account} CCY
    -        institution: "Retirement_Institution"
    -        address: "Retirement_Address"
    -        phone: "Retirement_Phone"
    -
    -      {date_origin} open {account_cash} CCY
    -        number: "882882"
    -
    -    """, **locals()))
    -    for currency, _ in commodities_items:
    -        open_entries.extend(parse("""
    -          {date_origin} open {account}:{currency} {currency}
    -            number: "882882"
    -        """, **locals()))
    -
    -    new_entries = []
    -    for txn_posting, balances in postings_for(entries, [account_cash]):
    -        balance = balances[account_cash]
    -        amount_to_invest = balance.get_currency_units('CCY').number
    -
    -        # Find the date the following Monday, the date to invest.
    -        txn_date = txn_posting.txn.date
    -        while txn_date.weekday() != calendar.MONDAY:
    -            txn_date += ONE_DAY
    -
    -        amount_invested = ZERO
    -        for commodity, fraction in commodities_items:
    -            amount_fraction = amount_to_invest * D(fraction)
    -
    -            # Find the price at that date.
    -            _, price = prices.get_price(price_map, (commodity, 'CCY'), txn_date)
    -            units = (amount_fraction / price).quantize(D('0.001'))
    -            amount_cash = (units * price).quantize(D('0.01'))
    -            amount_cash_neg = -amount_cash
    -            new_entries.extend(parse("""
    -
    -              {txn_date} * "Investing {fraction:.0%} of cash in {commodity}"
    -                {account}:{commodity}  {units:.3f} {commodity} {{{price:.2f} CCY}}
    -                {account}:Cash         {amount_cash_neg:.2f} CCY
    -
    -            """, **locals()))
    -
    -            balance.add_amount(amount.Amount(-amount_cash, 'CCY'))
    -
    -    return data.sorted(open_entries + new_entries)
    -
    +
    def generate_retirement_investments(entries, account, commodities_items, price_map):
    +    """Invest money deposited to the given retirement account.
    +
    +    Args:
    +      entries: A list of directives
    +      account: The root account for all retirement investment sub-accounts.
    +      commodities_items: A list of (commodity, fraction to be invested in) items.
    +      price_map: A dict of prices, as per beancount.core.prices.build_price_map().
    +    Returns:
    +      A list of new directives for the given investments. This also generates account
    +      opening directives for the desired investment commodities.
    +    """
    +    open_entries = []
    +    account_cash = join(account, "Cash")
    +    date_origin = entries[0].date
    +    open_entries.extend(
    +        parse(f"""
    +
    +      {date_origin} open {account} CCY
    +        institution: "Retirement_Institution"
    +        address: "Retirement_Address"
    +        phone: "Retirement_Phone"
    +
    +      {date_origin} open {account_cash} CCY
    +        number: "882882"
    +
    +    """)
    +    )
    +    for currency, _ in commodities_items:
    +        open_entries.extend(
    +            parse(f"""
    +          {date_origin} open {account}:{currency} {currency}
    +            number: "882882"
    +        """)
    +        )
    +
    +    new_entries = []
    +    for txn_posting, balances in postings_for(entries, [account_cash]):
    +        balance = balances[account_cash]
    +        amount_to_invest = balance.get_currency_units("CCY").number
    +
    +        # Find the date the following Monday, the date to invest.
    +        txn_date = txn_posting.txn.date
    +        while txn_date.weekday() != calendar.MONDAY:
    +            txn_date += ONE_DAY
    +
    +        for commodity, fraction in commodities_items:
    +            amount_fraction = amount_to_invest * D(fraction)
    +
    +            # Find the price at that date.
    +            _, price = prices.get_price(price_map, (commodity, "CCY"), txn_date)
    +            units = (amount_fraction / price).quantize(D("0.001"))
    +            amount_cash = (units * price).quantize(D("0.01"))
    +            amount_cash_neg = -amount_cash
    +            new_entries.extend(
    +                parse(f"""
    +
    +              {txn_date} * "Investing {fraction:.0%} of cash in {commodity}"
    +                {account}:{commodity}  {units:.3f} {commodity} {{{price:.2f} CCY}}
    +                {account}:Cash         {amount_cash_neg:.2f} CCY
    +
    +            """)
    +            )
    +
    +            balance.add_amount(amount.Amount(-amount_cash, "CCY"))
    +
    +    return data.sorted(open_entries + new_entries)
    +
    @@ -4937,7 +3906,7 @@

    -beancount.scripts.example.generate_tax_accounts(year, date_max) +beancount.scripts.example.generate_tax_accounts(year, date_max)

    @@ -4981,67 +3950,68 @@

    Source code in beancount/scripts/example.py -
    def generate_tax_accounts(year, date_max):
    -    """Generate accounts and contribution directives for a particular tax year.
    -
    -    Args:
    -      year: An integer, the year we're to generate this for.
    -      date_max: The maximum date to produce an entry for.
    -    Returns:
    -      A list of directives.
    -    """
    -    date_year = datetime.date(year, 1, 1)
    -    date_filing = (datetime.date(year + 1, 3, 20) +
    -                   datetime.timedelta(days=random.randint(0, 5)))
    -
    -    date_federal = (date_filing + datetime.timedelta(days=random.randint(0, 4)))
    -    date_state = (date_filing + datetime.timedelta(days=random.randint(0, 4)))
    -
    -    quantum = D('0.01')
    -    amount_federal = D(max(random.normalvariate(500, 120), 12)).quantize(quantum)
    -    amount_federal_neg = -amount_federal
    -    amount_state = D(max(random.normalvariate(300, 100), 10)).quantize(quantum)
    -    amount_state_neg = -amount_state
    -    amount_payable = -(amount_federal + amount_state)
    -
    -    amount_limit = RETIREMENT_LIMITS.get(year, RETIREMENT_LIMITS[None])
    -    amount_limit_neg = -amount_limit
    -
    -    entries = parse("""
    -
    -      ;; Open tax accounts for that year.
    -      {date_year} open Expenses:Taxes:Y{year}:CC:Federal:PreTax401k   DEFCCY
    -      {date_year} open Expenses:Taxes:Y{year}:CC:Medicare             CCY
    -      {date_year} open Expenses:Taxes:Y{year}:CC:Federal              CCY
    -      {date_year} open Expenses:Taxes:Y{year}:CC:CityNYC              CCY
    -      {date_year} open Expenses:Taxes:Y{year}:CC:SDI                  CCY
    -      {date_year} open Expenses:Taxes:Y{year}:CC:State                CCY
    -      {date_year} open Expenses:Taxes:Y{year}:CC:SocSec               CCY
    -
    -      ;; Check that the tax amounts have been fully used.
    -      {date_year} balance Assets:CC:Federal:PreTax401k  0 DEFCCY
    -
    -      {date_year} * "Allowed contributions for one year"
    -        Income:CC:Federal:PreTax401k     {amount_limit_neg} DEFCCY
    -        Assets:CC:Federal:PreTax401k     {amount_limit} DEFCCY
    -
    -      {date_filing} * "Filing taxes for {year}"
    -        Expenses:Taxes:Y{year}:CC:Federal      {amount_federal:.2f} CCY
    -        Expenses:Taxes:Y{year}:CC:State        {amount_state:.2f} CCY
    -        Liabilities:AccountsPayable            {amount_payable:.2f} CCY
    -
    -      {date_federal} * "FEDERAL TAXPYMT"
    -        Assets:CC:Bank1:Checking       {amount_federal_neg:.2f} CCY
    -        Liabilities:AccountsPayable    {amount_federal:.2f} CCY
    -
    -      {date_state} * "STATE TAX & FINANC PYMT"
    -        Assets:CC:Bank1:Checking       {amount_state_neg:.2f} CCY
    -        Liabilities:AccountsPayable    {amount_state:.2f} CCY
    -
    -    """, **locals())
    -
    -    return [entry for entry in entries if entry.date < date_max]
    -
    +
    def generate_tax_accounts(year, date_max):
    +    """Generate accounts and contribution directives for a particular tax year.
    +
    +    Args:
    +      year: An integer, the year we're to generate this for.
    +      date_max: The maximum date to produce an entry for.
    +    Returns:
    +      A list of directives.
    +    """
    +    date_year = datetime.date(year, 1, 1)
    +    date_filing = datetime.date(year + 1, 3, 20) + datetime.timedelta(
    +        days=random.randint(0, 5)
    +    )
    +
    +    date_federal = date_filing + datetime.timedelta(days=random.randint(0, 4))
    +    date_state = date_filing + datetime.timedelta(days=random.randint(0, 4))
    +
    +    quantum = D("0.01")
    +    amount_federal = D(max(random.normalvariate(500, 120), 12)).quantize(quantum)
    +    amount_federal_neg = -amount_federal
    +    amount_state = D(max(random.normalvariate(300, 100), 10)).quantize(quantum)
    +    amount_state_neg = -amount_state
    +    amount_payable = -(amount_federal + amount_state)
    +
    +    amount_limit = RETIREMENT_LIMITS.get(year, RETIREMENT_LIMITS[None])
    +    amount_limit_neg = -amount_limit
    +
    +    entries = parse(f"""
    +
    +      ;; Open tax accounts for that year.
    +      {date_year} open Expenses:Taxes:Y{year}:CC:Federal:PreTax401k   DEFCCY
    +      {date_year} open Expenses:Taxes:Y{year}:CC:Medicare             CCY
    +      {date_year} open Expenses:Taxes:Y{year}:CC:Federal              CCY
    +      {date_year} open Expenses:Taxes:Y{year}:CC:CityNYC              CCY
    +      {date_year} open Expenses:Taxes:Y{year}:CC:SDI                  CCY
    +      {date_year} open Expenses:Taxes:Y{year}:CC:State                CCY
    +      {date_year} open Expenses:Taxes:Y{year}:CC:SocSec               CCY
    +
    +      ;; Check that the tax amounts have been fully used.
    +      {date_year} balance Assets:CC:Federal:PreTax401k  0 DEFCCY
    +
    +      {date_year} * "Allowed contributions for one year"
    +        Income:CC:Federal:PreTax401k     {amount_limit_neg} DEFCCY
    +        Assets:CC:Federal:PreTax401k     {amount_limit} DEFCCY
    +
    +      {date_filing} * "Filing taxes for {year}"
    +        Expenses:Taxes:Y{year}:CC:Federal      {amount_federal:.2f} CCY
    +        Expenses:Taxes:Y{year}:CC:State        {amount_state:.2f} CCY
    +        Liabilities:AccountsPayable            {amount_payable:.2f} CCY
    +
    +      {date_federal} * "FEDERAL TAXPYMT"
    +        Assets:CC:Bank1:Checking       {amount_federal_neg:.2f} CCY
    +        Liabilities:AccountsPayable    {amount_federal:.2f} CCY
    +
    +      {date_state} * "STATE TAX & FINANC PYMT"
    +        Assets:CC:Bank1:Checking       {amount_state_neg:.2f} CCY
    +        Liabilities:AccountsPayable    {amount_state:.2f} CCY
    +
    +    """)
    +
    +    return [entry for entry in entries if entry.date < date_max]
    +
    @@ -5054,7 +4024,7 @@

    -beancount.scripts.example.generate_tax_preamble(date_birth) +beancount.scripts.example.generate_tax_preamble(date_birth)

    @@ -5097,21 +4067,21 @@

    Source code in beancount/scripts/example.py -
    def generate_tax_preamble(date_birth):
    -    """Generate tax declarations not specific to any particular year.
    -
    -    Args:
    -      date_birth: A date instance, the birth date of the character.
    -    Returns:
    -      A list of directives.
    -    """
    -    return parse("""
    -      ;; Tax accounts not specific to a year.
    -      {date_birth} open Income:CC:Federal:PreTax401k     DEFCCY
    -      {date_birth} open Assets:CC:Federal:PreTax401k     DEFCCY
    -
    -    """, **locals())
    -
    +
    def generate_tax_preamble(date_birth):
    +    """Generate tax declarations not specific to any particular year.
    +
    +    Args:
    +      date_birth: A date instance, the birth date of the character.
    +    Returns:
    +      A list of directives.
    +    """
    +    return parse(f"""
    +      ;; Tax accounts not specific to a year.
    +      {date_birth} open Income:CC:Federal:PreTax401k     DEFCCY
    +      {date_birth} open Assets:CC:Federal:PreTax401k     DEFCCY
    +
    +    """)
    +
    @@ -5124,7 +4094,7 @@

    -beancount.scripts.example.generate_taxable_investment(date_begin, date_end, entries, price_map, stocks) +beancount.scripts.example.generate_taxable_investment(date_begin, date_end, entries, price_map, stocks)

    @@ -5172,171 +4142,170 @@

    Source code in beancount/scripts/example.py -
    def generate_taxable_investment(date_begin, date_end, entries, price_map, stocks):
    -    """Generate opening directives and transactions for an investment account.
    -
    -    Args:
    -      date_begin: A date instance, the beginning date.
    -      date_end: A date instance, the end date.
    -      entries: A list of entries that contains at least the transfers to the investment
    -        account's cash account.
    -      price_map: A dict of prices, as per beancount.core.prices.build_price_map().
    -      stocks: A list of strings, the list of commodities to invest in.
    -    Returns:
    -      A list of directives.
    -    """
    -    account = 'Assets:CC:Investment'
    -    account_cash = join(account, 'Cash')
    -    account_gains = 'Income:CC:Investment:Gains'
    -    account_dividends = 'Income:CC:Investment:Dividends'
    -    accounts_stocks = ['Assets:CC:Investment:{}'.format(commodity)
    -                       for commodity in stocks]
    -
    -    open_entries = parse("""
    -      {date_begin} open {account}:Cash    CCY
    -      {date_begin} open {account_gains}    CCY
    -      {date_begin} open {account_dividends}    CCY
    -    """, **locals())
    -    for stock in stocks:
    -        open_entries.extend(parse("""
    -          {date_begin} open {account}:{stock} {stock}
    -        """, **locals()))
    -
    -    # Figure out dates at which dividends should be distributed, near the end of
    -    # each quarter.
    -    days_to = datetime.timedelta(days=3*90-10)
    -    dividend_dates = []
    -    for quarter_begin in iter_quarters(date_begin, date_end):
    -        end_of_quarter = quarter_begin + days_to
    -        if not (date_begin < end_of_quarter < date_end):
    -            continue
    -        dividend_dates.append(end_of_quarter)
    -
    -    # Iterate over all the dates, but merging in the postings for the cash
    -    # account.
    -    min_amount = D('1000.00')
    -    round_amount = D('100.00')
    -    commission = D('8.95')
    -    round_units = D('1')
    -    frac_invest = D('1.00')
    -    frac_dividend = D('0.004')
    -    p_daily_buy = 1./15  # days
    -    p_daily_sell = 1./90  # days
    -
    -    stocks_inventory = inventory.Inventory()
    -    new_entries = []
    -    dividend_date_iter = iter(dividend_dates)
    -    next_dividend_date = next(dividend_date_iter)
    -    for date, balances in iter_dates_with_balance(date_begin, date_end,
    -                                                  entries, [account_cash]):
    -
    -        # Check if we should insert a dividend. Note that we could not factor
    -        # this out because we want to explicitly reinvest the cash dividends and
    -        # we also want the dividends to be proportional to the amount of
    -        # invested stock, so one feeds on the other and vice-versa.
    -        if next_dividend_date and date > next_dividend_date:
    -            # Compute the total balances for the stock accounts in order to
    -            # create a realistic dividend.
    -            total = inventory.Inventory()
    -            for account_stock in accounts_stocks:
    -                total.add_inventory(balances[account_stock])
    -
    -            # Create an entry offering dividends of 1% of the portfolio.
    -            portfolio_cost = total.reduce(convert.get_cost).get_currency_units('CCY').number
    -            amount_cash = (frac_dividend * portfolio_cost).quantize(D('0.01'))
    -            amount_cash_neg = -amount_cash
    -            dividend = parse("""
    -              {next_dividend_date} * "Dividends on portfolio"
    -                {account}:Cash        {amount_cash:.2f} CCY
    -                {account_dividends}   {amount_cash_neg:.2f} CCY
    -            """, **locals())[0]
    -            new_entries.append(dividend)
    -
    -            # Advance the next dividend date.
    -            try:
    -                next_dividend_date = next(dividend_date_iter)
    -            except StopIteration:
    -                next_dividend_date = None
    -
    -        # If the balance is high, buy with high probability.
    -        balance = balances[account_cash]
    -        total_cash = balance.get_currency_units('CCY').number
    -        assert total_cash >= ZERO, ('Cash balance is negative: {}'.format(total_cash))
    -        invest_cash = total_cash * frac_invest - commission
    -        if invest_cash > min_amount:
    -            if random.random() < p_daily_buy:
    -                commodities = random.sample(stocks, random.randint(1, len(stocks)))
    -                lot_amount = round_to(invest_cash / len(commodities), round_amount)
    -
    -                invested_amount = ZERO
    -                for stock in commodities:
    -                    # Find the price at that date.
    -                    _, price = prices.get_price(price_map, (stock, 'CCY'), date)
    -
    -                    units = round_to((lot_amount / price), round_units)
    -                    if units <= ZERO:
    -                        continue
    -                    amount_cash = -(units * price + commission)
    -                    # logging.info('Buying %s %s @ %s CCY = %s CCY',
    -                    #              units, stock, price, units * price)
    -
    -                    buy = parse("""
    -                      {date} * "Buy shares of {stock}"
    -                        {account}:Cash                  {amount_cash:.2f} CCY
    -                        {account}:{stock}               {units:.0f} {stock} {{{price:.2f} CCY}}
    -                        Expenses:Financial:Commissions  {commission:.2f} CCY
    -                    """, **locals())[0]
    -                    new_entries.append(buy)
    -
    -                    account_stock = ':'.join([account, stock])
    -                    balances[account_cash].add_position(buy.postings[0])
    -                    balances[account_stock].add_position(buy.postings[1])
    -                    stocks_inventory.add_position(buy.postings[1])
    -
    -                # Don't sell on days you buy.
    -                continue
    -
    -        # Otherwise, sell with low probability.
    -        if not stocks_inventory.is_empty() and random.random() < p_daily_sell:
    -            # Choose the lot with the highest gain or highest loss.
    -            gains = []
    -            for position in stocks_inventory.get_positions():
    -                base_quote = (position.units.currency, position.cost.currency)
    -                _, price = prices.get_price(price_map, base_quote, date)
    -                if price == position.cost.number:
    -                    continue # Skip lots without movement.
    -                market_value = position.units.number * price
    -                book_value = convert.get_cost(position).number
    -                gain = market_value - book_value
    -                gains.append((gain, market_value, price, position))
    -            if not gains:
    -                continue
    -
    -            # Sell either biggest winner or biggest loser.
    -            biggest = bool(random.random() < 0.5)
    -            lot_tuple = sorted(gains)[0 if biggest else -1]
    -            gain, market_value, price, sell_position = lot_tuple
    -            #logging.info('Selling {} for {}'.format(sell_position, market_value))
    -
    -            sell_position = -sell_position
    -            stock = sell_position.units.currency
    -            amount_cash = market_value - commission
    -            amount_gain = -gain
    -            sell = parse("""
    -              {date} * "Sell shares of {stock}"
    -                {account}:{stock}               {sell_position} @ {price:.2f} CCY
    -                {account}:Cash                  {amount_cash:.2f} CCY
    -                Expenses:Financial:Commissions  {commission:.2f} CCY
    -                {account_gains}                 {amount_gain:.2f} CCY
    -            """, **locals())[0]
    -            new_entries.append(sell)
    -
    -            balances[account_cash].add_position(sell.postings[1])
    -            stocks_inventory.add_position(sell.postings[0])
    -            continue
    -
    -    return open_entries + new_entries
    -
    +
    def generate_taxable_investment(date_begin, date_end, entries, price_map, stocks):
    +    """Generate opening directives and transactions for an investment account.
    +
    +    Args:
    +      date_begin: A date instance, the beginning date.
    +      date_end: A date instance, the end date.
    +      entries: A list of entries that contains at least the transfers to the investment
    +        account's cash account.
    +      price_map: A dict of prices, as per beancount.core.prices.build_price_map().
    +      stocks: A list of strings, the list of commodities to invest in.
    +    Returns:
    +      A list of directives.
    +    """
    +    account = "Assets:CC:Investment"
    +    income = "Income:CC:Investment"
    +    account_cash = join(account, "Cash")
    +    account_gains = "{income}:PnL".format(income=income)
    +    dividends = "Dividend"
    +    accounts_stocks = ["Assets:CC:Investment:{}".format(commodity) for commodity in stocks]
    +
    +    open_entries = parse(f"""
    +      {date_begin} open {account}:Cash    CCY
    +      {date_begin} open {account_gains}    CCY
    +    """)
    +    for stock in stocks:
    +        open_entries.extend(
    +            parse(f"""
    +          {date_begin} open {account}:{stock} {stock}
    +          {date_begin} open {income}:{stock}:{dividends}    CCY
    +        """)
    +        )
    +
    +    # Figure out dates at which dividends should be distributed, near the end of
    +    # each quarter.
    +    days_to = datetime.timedelta(days=3 * 90 - 10)
    +    dividend_dates = []
    +    for quarter_begin in iter_quarters(date_begin, date_end):
    +        end_of_quarter = quarter_begin + days_to
    +        if not (date_begin < end_of_quarter < date_end):
    +            continue
    +        dividend_dates.append(end_of_quarter)
    +
    +    # Iterate over all the dates, but merging in the postings for the cash
    +    # account.
    +    min_amount = D("1000.00")
    +    round_amount = D("100.00")
    +    commission = D("8.95")
    +    round_units = D("1")
    +    frac_invest = D("1.00")
    +    frac_dividend = D("0.004")
    +    p_daily_buy = 1.0 / 15  # days
    +    p_daily_sell = 1.0 / 90  # days
    +
    +    stocks_inventory = inventory.Inventory()
    +    new_entries = []
    +    dividend_date_iter = iter(dividend_dates)
    +    next_dividend_date = next(dividend_date_iter, None)
    +    for date, balances in iter_dates_with_balance(
    +        date_begin, date_end, entries, [account_cash]
    +    ):
    +        # Check if we should insert a dividend. Note that we could not factor
    +        # this out because we want to explicitly reinvest the cash dividends and
    +        # we also want the dividends to be proportional to the amount of
    +        # invested stock, so one feeds on the other and vice-versa.
    +        if next_dividend_date and date > next_dividend_date:
    +            # Compute the total balances for the stock accounts in order to
    +            # create a realistic dividend.
    +            total = inventory.Inventory()
    +            for account_stock in accounts_stocks:
    +                total.add_inventory(balances[account_stock])
    +
    +            # Create an entry offering dividends of 1% of the portfolio.
    +            portfolio_cost = total.reduce(convert.get_cost).get_currency_units("CCY").number
    +            amount_cash = (frac_dividend * portfolio_cost).quantize(D("0.01"))
    +            amount_cash_neg = -amount_cash
    +            stock = random.choice(stocks)
    +            cash_dividend = parse(f"""
    +              {next_dividend_date} * "Dividends on portfolio"
    +                {account}:Cash        {amount_cash:.2f} CCY
    +                {income}:{stock}:{dividends}   {amount_cash_neg:.2f} CCY
    +            """)[0]
    +            new_entries.append(cash_dividend)
    +
    +            # Advance the next dividend date.
    +            next_dividend_date = next(dividend_date_iter, None)
    +
    +        # If the balance is high, buy with high probability.
    +        balance = balances[account_cash]
    +        total_cash = balance.get_currency_units("CCY").number
    +        assert total_cash >= ZERO, "Cash balance is negative: {}".format(total_cash)
    +        invest_cash = total_cash * frac_invest - commission
    +        if invest_cash > min_amount:
    +            if random.random() < p_daily_buy:
    +                commodities = random.sample(stocks, random.randint(1, len(stocks)))
    +                lot_amount = round_to(invest_cash / len(commodities), round_amount)
    +
    +                for stock in commodities:
    +                    # Find the price at that date.
    +                    _, price = prices.get_price(price_map, (stock, "CCY"), date)
    +
    +                    units = round_to((lot_amount / price), round_units)
    +                    if units <= ZERO:
    +                        continue
    +                    amount_cash = -(units * price + commission)
    +                    # logging.info('Buying %s %s @ %s CCY = %s CCY',
    +                    #              units, stock, price, units * price)
    +
    +                    buy = parse(f"""
    +                      {date} * "Buy shares of {stock}"
    +                        {account}:Cash                  {amount_cash:.2f} CCY
    +                        {account}:{stock}               {units:.0f} {stock} {{{price:.2f} CCY}}
    +                        Expenses:Financial:Commissions  {commission:.2f} CCY
    +                    """)[0]
    +                    new_entries.append(buy)
    +
    +                    account_stock = ":".join([account, stock])
    +                    balances[account_cash].add_position(buy.postings[0])
    +                    balances[account_stock].add_position(buy.postings[1])
    +                    stocks_inventory.add_position(buy.postings[1])
    +
    +                # Don't sell on days you buy.
    +                continue
    +
    +        # Otherwise, sell with low probability.
    +        if not stocks_inventory.is_empty() and random.random() < p_daily_sell:
    +            # Choose the lot with the highest gain or highest loss.
    +            gains = []
    +            for position in stocks_inventory.get_positions():
    +                base_quote = (position.units.currency, position.cost.currency)
    +                _, price = prices.get_price(price_map, base_quote, date)
    +                if price == position.cost.number:
    +                    continue  # Skip lots without movement.
    +                market_value = position.units.number * price
    +                book_value = convert.get_cost(position).number
    +                gain = market_value - book_value
    +                gains.append((gain, market_value, price, position))
    +            if not gains:
    +                continue
    +
    +            # Sell either biggest winner or biggest loser.
    +            biggest = bool(random.random() < 0.5)
    +            lot_tuple = sorted(gains)[0 if biggest else -1]
    +            gain, market_value, price, sell_position = lot_tuple
    +            # logging.info('Selling {} for {}'.format(sell_position, market_value))
    +
    +            sell_position = -sell_position
    +            stock = sell_position.units.currency
    +            amount_cash = market_value - commission
    +            amount_gain = -gain
    +            sell = parse(f"""
    +              {date} * "Sell shares of {stock}"
    +                {account}:{stock}               {sell_position} @ {price:.2f} CCY
    +                {account}:Cash                  {amount_cash:.2f} CCY
    +                Expenses:Financial:Commissions  {commission:.2f} CCY
    +                {account_gains}                 {amount_gain:.2f} CCY
    +            """)[0]
    +            new_entries.append(sell)
    +
    +            balances[account_cash].add_position(sell.postings[1])
    +            stocks_inventory.add_position(sell.postings[0])
    +            continue
    +
    +    return open_entries + new_entries
    +
    @@ -5349,7 +4318,7 @@

    -beancount.scripts.example.generate_trip_entries(date_begin, date_end, tag, config, trip_city, home_city, account_credit) +beancount.scripts.example.generate_trip_entries(date_begin, date_end, tag, config, trip_city, home_city, account_credit)

    @@ -5401,56 +4370,61 @@

    Source code in beancount/scripts/example.py -
    def generate_trip_entries(date_begin, date_end,
    -                          tag, config,
    -                          trip_city, home_city,
    -                          account_credit):
    -    """Generate more dense expenses for a trip.
    -
    -    Args:
    -      date_begin: A datetime.date instance, the beginning of the trip.
    -      date_end: A datetime.date instance, the end of the trip.
    -      tag: A string, the name of the tag.
    -      config: A list of (payee name, account name, (mu, 3sigma)), where
    -        mu is the mean of the prices to generate and 3sigma is 3 times
    -        the standard deviation.
    -      trip_city: A string, the capitalized name of the destination city.
    -      home_city: A string, the name of the home city.
    -      account_credit: A string, the name of the credit card account to pay
    -        the expenses from.
    -    Returns:
    -      A list of entries for the trip, all tagged with the given tag.
    -    """
    -    p_day_generate = 0.3
    -
    -    new_entries = []
    -    for date in date_iter(date_begin, date_end):
    -        for payee, account_expense, (mu, sigma3) in config:
    -            if random.random() < p_day_generate:
    -                amount = random.normalvariate(mu, sigma3 / 3.)
    -                amount_neg = -amount
    -                new_entries.extend(parse("""
    -                  {date} * "{payee}" "" #{tag}
    -                    {account_credit}     {amount_neg:.2f} CCY
    -                    {account_expense}    {amount:.2f} CCY
    -                """, **locals()))
    -
    -    # Consume the vacation days.
    -    vacation_hrs = (date_end - date_begin).days * 8 # hrs/day
    -    new_entries.extend(parse("""
    -      {date_end} * "Consume vacation days"
    -        Assets:CC:Employer1:Vacation -{vacation_hrs:.2f} VACHR
    -        Expenses:Vacation             {vacation_hrs:.2f} VACHR
    -    """, **locals()))
    -
    -    # Generate events for the trip.
    -    new_entries.extend(parse("""
    -      {date_begin} event "location" "{trip_city}"
    -      {date_end}   event "location" "{home_city}"
    -    """, **locals()))
    -
    -    return new_entries
    -
    +
    def generate_trip_entries(
    +    date_begin, date_end, tag, config, trip_city, home_city, account_credit
    +):
    +    """Generate more dense expenses for a trip.
    +
    +    Args:
    +      date_begin: A datetime.date instance, the beginning of the trip.
    +      date_end: A datetime.date instance, the end of the trip.
    +      tag: A string, the name of the tag.
    +      config: A list of (payee name, account name, (mu, 3sigma)), where
    +        mu is the mean of the prices to generate and 3sigma is 3 times
    +        the standard deviation.
    +      trip_city: A string, the capitalized name of the destination city.
    +      home_city: A string, the name of the home city.
    +      account_credit: A string, the name of the credit card account to pay
    +        the expenses from.
    +    Returns:
    +      A list of entries for the trip, all tagged with the given tag.
    +    """
    +    p_day_generate = 0.3
    +
    +    new_entries = []
    +    for date in date_iter(date_begin, date_end):
    +        for payee, account_expense, (mu, sigma3) in config:
    +            if random.random() < p_day_generate:
    +                amount = random.normalvariate(mu, sigma3 / 3.0)
    +                amount_neg = -amount
    +                new_entries.extend(
    +                    parse(f"""
    +                  {date} * "{payee}" "" #{tag}
    +                    {account_credit}     {amount_neg:.2f} CCY
    +                    {account_expense}    {amount:.2f} CCY
    +                """)
    +                )
    +
    +    # Consume the vacation days.
    +    vacation_hrs = (date_end - date_begin).days * 8  # hrs/day
    +    new_entries.extend(
    +        parse(f"""
    +      {date_end} * "Consume vacation days"
    +        Assets:CC:Employer1:Vacation -{vacation_hrs:.2f} VACHR
    +        Expenses:Vacation             {vacation_hrs:.2f} VACHR
    +    """)
    +    )
    +
    +    # Generate events for the trip.
    +    new_entries.extend(
    +        parse(f"""
    +      {date_begin} event "location" "{trip_city}"
    +      {date_end}   event "location" "{home_city}"
    +    """)
    +    )
    +
    +    return new_entries
    +
    @@ -5463,7 +4437,7 @@

    -beancount.scripts.example.get_minimum_balance(entries, account, currency) +beancount.scripts.example.get_minimum_balance(entries, account, currency)

    @@ -5508,24 +4482,24 @@

    Source code in beancount/scripts/example.py -
    def get_minimum_balance(entries, account, currency):
    -    """Compute the minimum balance of the given account according to the entries history.
    -
    -    Args:
    -      entries: A list of directives.
    -      account: An account string.
    -      currency: A currency string, for which we want to compute the minimum.
    -    Returns:
    -      A Decimal number, the minimum amount throughout the history of this account.
    -    """
    -    min_amount = ZERO
    -    for _, balances in postings_for(entries, [account]):
    -        balance = balances[account]
    -        current = balance.get_currency_units(currency).number
    -        if current < min_amount:
    -            min_amount = current
    -    return min_amount
    -
    +
    def get_minimum_balance(entries, account, currency):
    +    """Compute the minimum balance of the given account according to the entries history.
    +
    +    Args:
    +      entries: A list of directives.
    +      account: An account string.
    +      currency: A currency string, for which we want to compute the minimum.
    +    Returns:
    +      A Decimal number, the minimum amount throughout the history of this account.
    +    """
    +    min_amount = ZERO
    +    for _, balances in postings_for(entries, [account]):
    +        balance = balances[account]
    +        current = balance.get_currency_units(currency).number
    +        if current < min_amount:
    +            min_amount = current
    +    return min_amount
    +
    @@ -5538,7 +4512,7 @@

    -beancount.scripts.example.iter_dates_with_balance(date_begin, date_end, entries, accounts) +beancount.scripts.example.iter_dates_with_balance(date_begin, date_end, entries, accounts)

    @@ -5573,30 +4547,30 @@

    Source code in beancount/scripts/example.py -
    def iter_dates_with_balance(date_begin, date_end, entries, accounts):
    -    """Iterate over dates, including the balances of the postings iterator.
    -
    -    Args:
    -      postings_iter: An iterator of postings as per postings_for().
    -      date_begin: The start date.
    -      date_end: The end date.
    -    Yields:
    -      Pairs of (data, balances) objects, with
    -        date: A datetime.date instance
    -        balances: An Inventory object, representing the current balance.
    -          You can modify the inventory object to feed back changes in the
    -          balance.
    -    """
    -    balances = collections.defaultdict(inventory.Inventory)
    -    merged_txn_postings = iter(merge_postings(entries, accounts))
    -    txn_posting = next(merged_txn_postings, None)
    -    for date in date_iter(date_begin, date_end):
    -        while txn_posting and txn_posting.txn.date == date:
    -            posting = txn_posting.posting
    -            balances[posting.account].add_position(posting)
    -            txn_posting = next(merged_txn_postings, None)
    -        yield date, balances
    -
    +
    def iter_dates_with_balance(date_begin, date_end, entries, accounts):
    +    """Iterate over dates, including the balances of the postings iterator.
    +
    +    Args:
    +      postings_iter: An iterator of postings as per postings_for().
    +      date_begin: The start date.
    +      date_end: The end date.
    +    Yields:
    +      Pairs of (data, balances) objects, with
    +        date: A datetime.date instance
    +        balances: An Inventory object, representing the current balance.
    +          You can modify the inventory object to feed back changes in the
    +          balance.
    +    """
    +    balances = collections.defaultdict(inventory.Inventory)
    +    merged_txn_postings = iter(merge_postings(entries, accounts))
    +    txn_posting = next(merged_txn_postings, None)
    +    for date in date_iter(date_begin, date_end):
    +        while txn_posting and txn_posting.txn.date == date:
    +            posting = txn_posting.posting
    +            balances[posting.account].add_position(posting)
    +            txn_posting = next(merged_txn_postings, None)
    +        yield date, balances
    +
    @@ -5609,7 +4583,7 @@

    -beancount.scripts.example.iter_quarters(date_begin, date_end) +beancount.scripts.example.iter_quarters(date_begin, date_end)

    @@ -5640,29 +4614,29 @@

    Source code in beancount/scripts/example.py -
    def iter_quarters(date_begin, date_end):
    -    """Iterate over all quarters between begin and end dates.
    -
    -    Args:
    -      date_begin: The start date.
    -      date_end: The end date.
    -    Yields:
    -      Instances of datetime.date at the beginning of the quarters. This will
    -      include the quarter of the beginning date and of the end date.
    -    """
    -    quarter = (date_begin.year, (date_begin.month-1)//3)
    -    quarter_last = (date_end.year, (date_end.month-1)//3)
    -    assert quarter <= quarter_last
    -    while True:
    -        year, trimester = quarter
    -        yield datetime.date(year, trimester*3 + 1, 1)
    -        if quarter == quarter_last:
    -            break
    -        trimester = (trimester + 1) % 4
    -        if trimester == 0:
    -            year += 1
    -        quarter = (year, trimester)
    -
    +
    def iter_quarters(date_begin, date_end):
    +    """Iterate over all quarters between begin and end dates.
    +
    +    Args:
    +      date_begin: The start date.
    +      date_end: The end date.
    +    Yields:
    +      Instances of datetime.date at the beginning of the quarters. This will
    +      include the quarter of the beginning date and of the end date.
    +    """
    +    quarter = (date_begin.year, (date_begin.month - 1) // 3)
    +    quarter_last = (date_end.year, (date_end.month - 1) // 3)
    +    assert quarter <= quarter_last
    +    while True:
    +        year, trimester = quarter
    +        yield datetime.date(year, trimester * 3 + 1, 1)
    +        if quarter == quarter_last:
    +            break
    +        trimester = (trimester + 1) % 4
    +        if trimester == 0:
    +            year += 1
    +        quarter = (year, trimester)
    +
    @@ -5670,13 +4644,12 @@

    -

    -beancount.scripts.example.merge_postings(entries, accounts) +beancount.scripts.example.merge_postings(entries, accounts)

    @@ -5706,27 +4679,29 @@

    Source code in beancount/scripts/example.py -
    def merge_postings(entries, accounts):
    -    """Merge all the postings from the given account names.
    -
    -    Args:
    -      entries: A list of directives.
    -      accounts: A list of account strings to get the balances for.
    -    Yields:
    -      A list of TxnPosting's for all the accounts, in sorted order.
    -    """
    -    real_root = realization.realize(entries)
    -    merged_postings = []
    -    for account in accounts:
    -        real_account = realization.get(real_root, account)
    -        if real_account is None:
    -            continue
    -        merged_postings.extend(txn_posting
    -                               for txn_posting in real_account.txn_postings
    -                               if isinstance(txn_posting, data.TxnPosting))
    -    merged_postings.sort(key=lambda txn_posting: txn_posting.txn.date)
    -    return merged_postings
    -
    +
    def merge_postings(entries, accounts):
    +    """Merge all the postings from the given account names.
    +
    +    Args:
    +      entries: A list of directives.
    +      accounts: A list of account strings to get the balances for.
    +    Yields:
    +      A list of TxnPosting's for all the accounts, in sorted order.
    +    """
    +    real_root = realization.realize(entries)
    +    merged_postings = []
    +    for account in accounts:
    +        real_account = realization.get(real_root, account)
    +        if real_account is None:
    +            continue
    +        merged_postings.extend(
    +            txn_posting
    +            for txn_posting in real_account.txn_postings
    +            if isinstance(txn_posting, data.TxnPosting)
    +        )
    +    merged_postings.sort(key=lambda txn_posting: txn_posting.txn.date)
    +    return merged_postings
    +

    @@ -5739,7 +4714,7 @@

    -beancount.scripts.example.parse(input_string, **replacements) +beancount.scripts.example.parse(input_string)

    @@ -5761,7 +4736,6 @@

    • input_string – Beancount input text.

    • -
    • **replacements – A dict of keywords to replace to their values.

    @@ -5785,37 +4759,27 @@

    Source code in beancount/scripts/example.py -
    def parse(input_string, **replacements):
    -    """Parse some input string and assert no errors.
    -
    -    This parse function does not just create the object, it also triggers local
    -    interpolation to fill in the missing amounts.
    -
    -    Args:
    -      input_string: Beancount input text.
    -      **replacements: A dict of keywords to replace to their values.
    -    Returns:
    -      A list of directive objects.
    -    """
    -    if replacements:
    -        class IgnoreFormatter(string.Formatter):
    -            def check_unused_args(self, used_args, args, kwargs):
    -                pass
    -        formatter = IgnoreFormatter()
    -        formatted_string = formatter.format(input_string, **replacements)
    -    else:
    -        formatted_string = input_string
    -
    -    entries, errors, options_map = parser.parse_string(textwrap.dedent(formatted_string))
    -    if errors:
    -        printer.print_errors(errors, file=sys.stderr)
    -        raise ValueError("Parsed text has errors")
    -
    -    # Interpolation.
    -    entries, unused_balance_errors = booking.book(entries, options_map)
    -
    -    return data.sorted(entries)
    -
    +
    def parse(input_string):
    +    """Parse some input string and assert no errors.
    +
    +    This parse function does not just create the object, it also triggers local
    +    interpolation to fill in the missing amounts.
    +
    +    Args:
    +      input_string: Beancount input text.
    +    Returns:
    +      A list of directive objects.
    +    """
    +    entries, errors, options_map = parser.parse_string(textwrap.dedent(input_string))
    +    if errors:
    +        printer.print_errors(errors, file=sys.stderr)
    +        raise ValueError("Parsed text has errors")
    +
    +    # Interpolation.
    +    entries, unused_balance_errors = booking.book(entries, options_map)
    +
    +    return data.sorted(entries)
    +
    @@ -5828,7 +4792,7 @@

    -beancount.scripts.example.postings_for(entries, accounts, before=False) +beancount.scripts.example.postings_for(entries, accounts, before=False)

    @@ -5861,3087 +4825,58 @@

    posting: An instance of TxnPosting balances: A dict of Inventory balances for the given accounts after applying the posting. These inventory objects can be mutated to adjust - the balance due to generated transactions to be applied later.

    - -
    - Source code in beancount/scripts/example.py -
    def postings_for(entries, accounts, before=False):
    -    """Realize the entries and get the list of postings for the given accounts.
    -
    -    All the non-Posting directives are already filtered out.
    -
    -    Args:
    -      entries: A list of directives.
    -      accounts: A list of account strings to get the balances for.
    -      before: A boolean, if true, yield the balance before the position is applied.
    -        The default is to yield the balance after applying the position.
    -    Yields:
    -      Tuples of:
    -        posting: An instance of TxnPosting
    -        balances: A dict of Inventory balances for the given accounts _after_
    -          applying the posting. These inventory objects can be mutated to adjust
    -          the balance due to generated transactions to be applied later.
    -    """
    -    assert isinstance(accounts, list)
    -    merged_txn_postings = merge_postings(entries, accounts)
    -    balances = collections.defaultdict(inventory.Inventory)
    -    for txn_posting in merged_txn_postings:
    -        if before:
    -            yield txn_posting, balances
    -        posting = txn_posting.posting
    -        balances[posting.account].add_position(posting)
    -        if not before:
    -            yield txn_posting, balances
    -
    -
    - - - - - - -
    - - - -

    -beancount.scripts.example.price_series(start, mu, sigma) - - -

    - -
    - -

    Generate a price series based on a simple stochastic model.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • start – The beginning value.

    • -
    • mu – The per-step drift, in units of value.

    • -
    • sigma – Volatility of the changes.

    • -
    -

    Yields: - Floats, at each step.

    - -
    - Source code in beancount/scripts/example.py -
    def price_series(start, mu, sigma):
    -    """Generate a price series based on a simple stochastic model.
    -
    -    Args:
    -      start: The beginning value.
    -      mu: The per-step drift, in units of value.
    -      sigma: Volatility of the changes.
    -    Yields:
    -      Floats, at each step.
    -    """
    -    value = start
    -    while 1:
    -        yield value
    -        value += random.normalvariate(mu, sigma) * value
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.scripts.example.replace(string, replacements, strip=False) - - -

    - -
    - -

    Apply word-boundaried regular expression replacements to an indented string.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • string – Some input template string.

    • -
    • replacements – A dict of regexp to replacement value.

    • -
    • strip – A boolean, true if we should strip the input.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The input string with the replacements applied to it, with the indentation removed.

    • -
    -
    -
    - Source code in beancount/scripts/example.py -
    def replace(string, replacements, strip=False):
    -    """Apply word-boundaried regular expression replacements to an indented string.
    -
    -    Args:
    -      string: Some input template string.
    -      replacements: A dict of regexp to replacement value.
    -      strip: A boolean, true if we should strip the input.
    -    Returns:
    -      The input string with the replacements applied to it, with the indentation removed.
    -    """
    -    output = textwrap.dedent(string)
    -    if strip:
    -        output = output.strip()
    -    for from_, to_ in replacements.items():
    -        if not isinstance(to_, str) and not callable(to_):
    -            to_ = str(to_)
    -        output = re.sub(r'\b{}\b'.format(from_), to_, output)
    -    return output
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.scripts.example.validate_output(contents, positive_accounts, currency) - - -

    - -
    - -

    Check that the output file validates.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • contents – A string, the output file.

    • -
    • positive_accounts – A list of strings, account names to check for -non-negative balances.

    • -
    • currency – A string, the currency to check minimums for.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • AssertionError – If the output does not validate.

    • -
    -
    -
    - Source code in beancount/scripts/example.py -
    def validate_output(contents, positive_accounts, currency):
    -    """Check that the output file validates.
    -
    -    Args:
    -      contents: A string, the output file.
    -      positive_accounts: A list of strings, account names to check for
    -        non-negative balances.
    -      currency: A string, the currency to check minimums for.
    -    Raises:
    -      AssertionError: If the output does not validate.
    -    """
    -    loaded_entries, _, _ = loader.load_string(
    -        contents,
    -        log_errors=sys.stderr,
    -        extra_validations=validation.HARDCORE_VALIDATIONS)
    -
    -    # Sanity checks: Check that the checking balance never goes below zero.
    -    for account in positive_accounts:
    -        check_non_negative(loaded_entries, account, currency)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.scripts.example.write_example_file(date_birth, date_begin, date_end, reformat, file) - - -

    - -
    - -

    Generate the example file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • date_birth – A datetime.date instance, the birth date of our character.

    • -
    • date_begin – A datetime.date instance, the beginning date at which to generate -transactions.

    • -
    • date_end – A datetime.date instance, the end date at which to generate -transactions.

    • -
    • reformat – A boolean, true if we should apply global reformatting to this file.

    • -
    • file – A file object, where to write out the output.

    • -
    -
    -
    - Source code in beancount/scripts/example.py -
    def write_example_file(date_birth, date_begin, date_end, reformat, file):
    -    """Generate the example file.
    -
    -    Args:
    -      date_birth: A datetime.date instance, the birth date of our character.
    -      date_begin: A datetime.date instance, the beginning date at which to generate
    -        transactions.
    -      date_end: A datetime.date instance, the end date at which to generate
    -        transactions.
    -      reformat: A boolean, true if we should apply global reformatting to this file.
    -      file: A file object, where to write out the output.
    -    """
    -    # The following code entirely writes out the output to generic names, such
    -    # as "Employer1", "Bank1", and "CCY" (for principal currency). Those names
    -    # are purposely chosen to be unique, and only near the very end do we make
    -    # renamings to more specific and realistic names.
    -
    -    # Name of the checking account.
    -    account_opening = 'Equity:Opening-Balances'
    -    account_payable = 'Liabilities:AccountsPayable'
    -    account_checking = 'Assets:CC:Bank1:Checking'
    -    account_credit = 'Liabilities:CC:CreditCard1'
    -    account_retirement = 'Assets:CC:Retirement'
    -    account_investing = 'Assets:CC:Investment:Cash'
    -
    -    # Commodities.
    -    commodity_entries = generate_commodity_entries(date_birth)
    -
    -    # Estimate the rent.
    -    rent_amount = round_to(ANNUAL_SALARY / RENT_DIVISOR, RENT_INCREMENT)
    -
    -    # Get a random employer.
    -    employer_name, employer_address = random.choice(EMPLOYERS)
    -
    -    logging.info("Generating Salary Employment Income")
    -    income_entries = generate_employment_income(employer_name, employer_address,
    -                                                ANNUAL_SALARY,
    -                                                account_checking,
    -                                                join(account_retirement, 'Cash'),
    -                                                date_begin, date_end)
    -
    -    logging.info("Generating Expenses from Banking Accounts")
    -    banking_expenses = generate_banking_expenses(date_begin, date_end,
    -                                                 account_checking, rent_amount)
    -
    -    logging.info("Generating Regular Expenses via Credit Card")
    -    credit_regular_entries = generate_regular_credit_expenses(
    -        date_birth, date_begin, date_end, account_credit, account_checking)
    -
    -    logging.info("Generating Credit Card Expenses for Trips")
    -    trip_entries = []
    -    destinations = sorted(TRIP_DESTINATIONS.items())
    -    destinations.extend(destinations)
    -    random.shuffle(destinations)
    -    for (date_trip_begin, date_trip_end), (destination_name, config) in zip(
    -        compute_trip_dates(date_begin, date_end), destinations):
    -
    -        # Compute a suitable tag.
    -        tag = 'trip-{}-{}'.format(destination_name.lower().replace(' ', '-'),
    -                                  date_trip_begin.year)
    -        #logging.info("%s -- %s %s", tag, date_trip_begin, date_trip_end)
    -
    -        # Remove regular entries during this trip.
    -        credit_regular_entries = [entry
    -                                  for entry in credit_regular_entries
    -                                  if not(date_trip_begin <= entry.date < date_trip_end)]
    -
    -        # Generate entries for the trip.
    -        this_trip_entries = generate_trip_entries(
    -            date_trip_begin, date_trip_end,
    -            tag, config,
    -            destination_name.replace('-', ' ').title(), HOME_NAME,
    -            account_credit)
    -
    -        trip_entries.extend(this_trip_entries)
    -
    -    logging.info("Generating Credit Card Payment Entries")
    -    credit_payments = generate_clearing_entries(
    -        delay_dates(rrule.rrule(rrule.MONTHLY,
    -                                dtstart=date_begin, until=date_end, bymonthday=7), 0, 4),
    -        "CreditCard1", "Paying off credit card",
    -        credit_regular_entries,
    -        account_credit, account_checking)
    -
    -    credit_entries = credit_regular_entries + trip_entries + credit_payments
    -
    -    logging.info("Generating Tax Filings and Payments")
    -    tax_preamble = generate_tax_preamble(date_birth)
    -
    -    # Figure out all the years we need tax accounts for.
    -    years = set()
    -    for account_name in getters.get_accounts(income_entries):
    -        match = re.match(r'Expenses:Taxes:Y(\d\d\d\d)', account_name)
    -        if match:
    -            years.add(int(match.group(1)))
    -
    -    taxes = [(year, generate_tax_accounts(year, date_end)) for year in sorted(years)]
    -    tax_entries = tax_preamble + functools.reduce(operator.add,
    -                                                  (entries
    -                                                   for _, entries in taxes))
    -
    -    logging.info("Generating Opening of Banking Accounts")
    -    # Open banking accounts and gift the checking account with a balance that
    -    # will offset all the amounts to ensure a positive balance throughout its
    -    # lifetime.
    -    entries_for_banking = data.sorted(income_entries +
    -                                      banking_expenses +
    -                                      credit_entries +
    -                                      tax_entries)
    -    minimum = get_minimum_balance(entries_for_banking,
    -                                  account_checking, 'CCY')
    -    banking_entries = generate_banking(entries_for_banking,
    -                                       date_begin, date_end,
    -                                       max(-minimum, ZERO))
    -
    -    logging.info("Generating Transfers to Investment Account")
    -    banking_transfers = generate_outgoing_transfers(
    -        data.sorted(income_entries +
    -                    banking_entries +
    -                    banking_expenses +
    -                    credit_entries +
    -                    tax_entries),
    -        account_checking,
    -        account_investing,
    -        transfer_minimum=D('200'),
    -        transfer_threshold=D('3000'),
    -        transfer_increment=D('500'))
    -
    -    logging.info("Generating Prices")
    -    # Generate price entries for investment currencies and create a price map to
    -    # use for later for generating investment transactions.
    -    funds_allocation = {'MFUND1': 0.40, 'MFUND2': 0.60}
    -    stocks = ['STK1', 'STK2', 'STK3', 'STK4']
    -    price_entries = generate_prices(date_begin, date_end,
    -                                    sorted(funds_allocation.keys()) + stocks, 'CCY')
    -    price_map = prices.build_price_map(price_entries)
    -
    -    logging.info("Generating Employer Match Contribution")
    -    account_match = 'Income:US:Employer1:Match401k'
    -    retirement_match = generate_retirement_employer_match(income_entries,
    -                                                          join(account_retirement, 'Cash'),
    -                                                          account_match)
    -
    -    logging.info("Generating Retirement Investments")
    -    retirement_entries = generate_retirement_investments(
    -        income_entries + retirement_match, account_retirement,
    -        sorted(funds_allocation.items()),
    -        price_map)
    -
    -    logging.info("Generating Taxes Investments")
    -    investment_entries = generate_taxable_investment(date_begin, date_end,
    -                                                     banking_transfers, price_map,
    -                                                     stocks)
    -
    -    logging.info("Generating Expense Accounts")
    -    expense_accounts_entries = generate_expense_accounts(date_birth)
    -
    -    logging.info("Generating Equity Accounts")
    -    equity_entries = generate_open_entries(date_birth, [account_opening,
    -                                                        account_payable])
    -
    -    logging.info("Generating Balance Checks")
    -    credit_checks = generate_balance_checks(credit_entries, account_credit,
    -                                            date_random_seq(date_begin, date_end, 20, 30))
    -
    -    banking_checks = generate_balance_checks(data.sorted(income_entries +
    -                                                         banking_entries +
    -                                                         banking_expenses +
    -                                                         banking_transfers +
    -                                                         credit_entries +
    -                                                         tax_entries),
    -                                             account_checking,
    -                                             date_random_seq(date_begin, date_end, 20, 30))
    -
    -    logging.info("Outputting and Formatting Entries")
    -    dcontext = display_context.DisplayContext()
    -    default_int_digits = 8
    -    for currency, precision in {'USD': 2,
    -                                'CAD': 2,
    -                                'VACHR':0,
    -                                'IRAUSD': 2,
    -                                'VBMPX': 3,
    -                                'RGAGX': 3,
    -                                'ITOT': 0,
    -                                'VEA': 0,
    -                                'VHT': 0,
    -                                'GLD': 0}.items():
    -        int_digits = default_int_digits
    -        if precision > 0:
    -            int_digits += 1 + precision
    -        dcontext.update(D('{{:0{}.{}f}}'.format(int_digits, precision).format(0)), currency)
    -
    -    output = io.StringIO()
    -    def output_section(title, entries):
    -        output.write('\n\n\n{}\n\n'.format(title))
    -        printer.print_entries(data.sorted(entries), dcontext, file=output)
    -
    -    output.write(FILE_PREAMBLE.format(**locals()))
    -    output_section('* Commodities', commodity_entries)
    -    output_section('* Equity Accounts', equity_entries)
    -    output_section('* Banking', data.sorted(banking_entries +
    -                                            banking_expenses +
    -                                            banking_transfers +
    -                                            banking_checks))
    -    output_section('* Credit-Cards', data.sorted(credit_entries +
    -                                                 credit_checks))
    -    output_section('* Taxable Investments', investment_entries)
    -    output_section('* Retirement Investments', data.sorted(retirement_entries +
    -                                                           retirement_match))
    -    output_section('* Sources of Income', income_entries)
    -    output_section('* Taxes', tax_preamble)
    -    for year, entries in taxes:
    -        output_section('** Tax Year {}'.format(year), entries)
    -    output_section('* Expenses', expense_accounts_entries)
    -    output_section('* Prices', price_entries)
    -    output_section('* Cash', [])
    -
    -    logging.info("Contextualizing to Realistic Names")
    -    contents, replacements = contextualize_file(output.getvalue(), employer_name)
    -    if reformat:
    -        contents = format.align_beancount(contents)
    -
    -    logging.info("Writing contents")
    -    file.write(contents)
    -
    -    logging.info("Validating Results")
    -    validate_output(contents,
    -                    [replace(account, replacements)
    -                     for account in [account_checking]],
    -                    replace('CCY', replacements))
    -
    -
    -
    - -
    - - - - - - - - - - - - - - -
    - - - -

    - beancount.scripts.format - - - -

    - -
    - -

    Align a beancount/ledger input file's numbers.

    -

    This reformats at beancount or ledger input file so that the amounts in the -postings are all aligned to the same column. The currency should match.

    -

    Note: this does not parse the Beancount ledger. It simply uses regular -expressions and text manipulations to do its work.

    - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.scripts.format.align_beancount(contents, prefix_width=None, num_width=None, currency_column=None) - - -

    - -
    - -

    Reformat Beancount input to align all the numbers at the same column.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • contents – A string, Beancount input syntax to reformat.

    • -
    • prefix_width – An integer, the width in characters to render the account -name to. If this is not specified, a good value is selected -automatically from the contents of the file.

    • -
    • num_width – An integer, the width to render each number. If this is not -specified, a good value is selected automatically from the contents of -the file.

    • -
    • currency_column – An integer, the column at which to align the currencies. -If given, this overrides the other options.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, reformatted Beancount input with all the number aligned. -No other changes than whitespace changes should be present between that -return value and the input contents.

    • -
    -
    -
    - Source code in beancount/scripts/format.py -
    def align_beancount(contents, prefix_width=None, num_width=None, currency_column=None):
    -    """Reformat Beancount input to align all the numbers at the same column.
    -
    -    Args:
    -      contents: A string, Beancount input syntax to reformat.
    -      prefix_width: An integer, the width in characters to render the account
    -        name to. If this is not specified, a good value is selected
    -        automatically from the contents of the file.
    -      num_width: An integer, the width to render each number. If this is not
    -        specified, a good value is selected automatically from the contents of
    -        the file.
    -      currency_column: An integer, the column at which to align the currencies.
    -        If given, this overrides the other options.
    -    Returns:
    -      A string, reformatted Beancount input with all the number aligned.
    -      No other changes than whitespace changes should be present between that
    -      return value and the input contents.
    -
    -    """
    -    # Find all lines that have a number in them and calculate the maximum length
    -    # of the stripped prefix and the number.
    -    match_pairs = []
    -    for line in contents.splitlines():
    -        match = re.match(
    -            r'([^";]*?)\s+([-+]?\s*[\d,]+(?:\.\d*)?)\s+({}\b.*)'.format(amount.CURRENCY_RE),
    -            line)
    -        if match:
    -            prefix, number, rest = match.groups()
    -            match_pairs.append((prefix, number, rest))
    -        else:
    -            match_pairs.append((line, None, None))
    -
    -    # Normalize whitespace before lines that has some indent and an account
    -    # name.
    -    norm_match_pairs = normalize_indent_whitespace(match_pairs)
    -
    -    if currency_column:
    -        output = io.StringIO()
    -        for prefix, number, rest in norm_match_pairs:
    -            if number is None:
    -                output.write(prefix)
    -            else:
    -                num_of_spaces = currency_column - len(prefix) - len(number) - 4
    -                spaces = ' ' * num_of_spaces
    -                output.write(prefix + spaces + '  ' + number + ' ' + rest)
    -            output.write('\n')
    -        return output.getvalue()
    -
    -    # Compute the maximum widths.
    -    filtered_pairs = [(prefix, number)
    -                      for prefix, number, _ in match_pairs
    -                      if number is not None]
    -
    -    if filtered_pairs:
    -        max_prefix_width = max(len(prefix) for prefix, _ in filtered_pairs)
    -        max_num_width = max(len(number) for _, number in filtered_pairs)
    -    else:
    -        max_prefix_width = 0
    -        max_num_width = 0
    -
    -    # Use user-supplied overrides, if available
    -    if prefix_width:
    -        max_prefix_width = prefix_width
    -    if num_width:
    -        max_num_width = num_width
    -
    -    # Create a format that will admit the maximum width of all prefixes equally.
    -    line_format = '{{:<{prefix_width}}}  {{:>{num_width}}} {{}}'.format(
    -        prefix_width=max_prefix_width,
    -        num_width=max_num_width)
    -
    -    # Process each line to an output buffer.
    -    output = io.StringIO()
    -    for prefix, number, rest in norm_match_pairs:
    -        if number is None:
    -            output.write(prefix)
    -        else:
    -            output.write(line_format.format(prefix.rstrip(), number, rest))
    -        output.write('\n')
    -    formatted_contents = output.getvalue()
    -
    -    # Ensure that the file before and after have only whitespace differences.
    -    # This is a sanity check, to make really sure we never change anything but whitespace,
    -    # so it's safe.
    -    # open('/tmp/before', 'w').write(re.sub(r'[ \t]+', ' ', contents))
    -    # open('/tmp/after', 'w').write(re.sub(r'[ \t]+', ' ', formatted_contents))
    -    old_stripped = re.sub(r'[ \t\n]+', ' ', contents.rstrip())
    -    new_stripped = re.sub(r'[ \t\n]+', ' ', formatted_contents.rstrip())
    -    assert (old_stripped == new_stripped), (old_stripped, new_stripped)
    -
    -    return formatted_contents
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.scripts.format.compute_most_frequent(iterable) - - -

    - -
    - -

    Compute the frequencies of the given elements and return the most frequent.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • iterable – A collection of hashable elements.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The most frequent element. If there are no elements in the iterable, -return None.

    • -
    -
    -
    - Source code in beancount/scripts/format.py -
    def compute_most_frequent(iterable):
    -    """Compute the frequencies of the given elements and return the most frequent.
    -
    -    Args:
    -      iterable: A collection of hashable elements.
    -    Returns:
    -      The most frequent element. If there are no elements in the iterable,
    -      return None.
    -    """
    -    frequencies = collections.Counter(iterable)
    -    if not frequencies:
    -        return None
    -    counts = sorted((count, element)
    -                    for element, count in frequencies.items())
    -    # Note: In case of a tie, this chooses the longest width.
    -    # We could eventually make this an option.
    -    return counts[-1][1]
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.scripts.format.normalize_indent_whitespace(match_pairs) - - -

    - -
    - -

    Normalize whitespace before lines that has some indent and an account name.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • match_pairs – A list of (prefix, number, rest) tuples.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • Another list of (prefix, number, rest) tuples, where prefix may have been -adjusted with a different whitespace prefix.

    • -
    -
    -
    - Source code in beancount/scripts/format.py -
    def normalize_indent_whitespace(match_pairs):
    -    """Normalize whitespace before lines that has some indent and an account name.
    -
    -    Args:
    -      match_pairs: A list of (prefix, number, rest) tuples.
    -    Returns:
    -      Another list of (prefix, number, rest) tuples, where prefix may have been
    -      adjusted with a different whitespace prefix.
    -    """
    -    # Compute most frequent account name prefix.
    -    match_posting = re.compile(r'([ \t]+)({}.*)'.format(account.ACCOUNT_RE)).match
    -    width = compute_most_frequent(
    -        len(match.group(1))
    -        for match in (match_posting(prefix)
    -                      for prefix, _, _ in match_pairs)
    -        if match is not None)
    -    norm_format = ' ' * (width or 0) + '{}'
    -
    -    # Make the necessary adjustments.
    -    adjusted_pairs = []
    -    for tup in match_pairs:
    -        prefix, number, rest = tup
    -        match = match_posting(prefix)
    -        if match is not None:
    -            tup = (norm_format.format(match.group(2)), number, rest)
    -        adjusted_pairs.append(tup)
    -    return adjusted_pairs
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.scripts.sql - - - -

    - -
    - -

    Convert a Beancount ledger into an SQL database.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.scripts.sql.BalanceWriter (DirectiveWriter) - - - - -

    - -
    - - - - - -
    - - - - - - - - -
    - - - -

    - -beancount.scripts.sql.BalanceWriter.type (tuple) - - - - -

    - -
    - -

    Balance(meta, date, account, amount, tolerance, diff_amount)

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.scripts.sql.BalanceWriter.type.__getnewargs__(self) - - - special - - -
    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/scripts/sql.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.BalanceWriter.type.__new__(_cls, meta, date, account, amount, tolerance, diff_amount) - - - special - staticmethod - - -
    - -
    - -

    Create new instance of Balance(meta, date, account, amount, tolerance, diff_amount)

    - -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.BalanceWriter.type.__repr__(self) - - - special - - -
    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/scripts/sql.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    -beancount.scripts.sql.BalanceWriter.get_detail(self, entry) - - -

    - -
    - -

    Provide data to store for details table.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – An instance of the desired directive.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of the values corresponding to the columns declared in the -'columns' attribute.

    • -
    -
    -
    - Source code in beancount/scripts/sql.py -
    def get_detail(self, entry):
    -    return (entry.account,
    -            entry.amount.number,
    -            entry.amount.currency,
    -            entry.diff_amount.currency if entry.diff_amount else None,
    -            entry.diff_amount.currency if entry.diff_amount else None)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.scripts.sql.CloseWriter (DirectiveWriter) - - - - -

    - -
    - - - - - -
    - - - - - - - - -
    - - - -

    - -beancount.scripts.sql.CloseWriter.type (tuple) - - - - -

    - -
    - -

    Close(meta, date, account)

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.scripts.sql.CloseWriter.type.__getnewargs__(self) - - - special - - -
    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/scripts/sql.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.CloseWriter.type.__new__(_cls, meta, date, account) - - - special - staticmethod - - -
    - -
    - -

    Create new instance of Close(meta, date, account)

    - -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.CloseWriter.type.__repr__(self) - - - special - - -
    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/scripts/sql.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    -beancount.scripts.sql.CloseWriter.get_detail(self, entry) - - -

    - -
    - -

    Provide data to store for details table.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – An instance of the desired directive.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of the values corresponding to the columns declared in the -'columns' attribute.

    • -
    -
    -
    - Source code in beancount/scripts/sql.py -
    def get_detail(self, entry):
    -    return (entry.account,)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.scripts.sql.DirectiveWriter - - - -

    - -
    - -

    A base class for writers of directives. -This is used to factor out code for all the simple directives types -(all types except Transaction).

    - - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.scripts.sql.DirectiveWriter.__call__(self, connection, entries) - - - special - - -

    - -
    - -

    Create a table for a directives.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • connection – A DBAPI-2.0 Connection object.

    • -
    • entries – A list of directives.

    • -
    -
    -
    - Source code in beancount/scripts/sql.py -
    def __call__(self, connection, entries):
    -    """Create a table for a directives.
    -
    -    Args:
    -      connection: A DBAPI-2.0 Connection object.
    -      entries: A list of directives.
    -    """
    -    with connection:
    -        columns_text = ','.join(self.columns.strip().splitlines())
    -        connection.execute("""
    -          CREATE TABLE {name}_detail (
    -            id 			INTEGER PRIMARY KEY,
    -            {columns}
    -          );
    -        """.format(name=self.name,
    -                   columns=columns_text))
    -
    -        connection.execute("""
    -          CREATE VIEW {name} AS
    -            SELECT * FROM entry JOIN {name}_detail USING (id);
    -        """.format(name=self.name))
    -
    -    with connection:
    -        for eid, entry in enumerate(entries):
    -            if not isinstance(entry, self.type):
    -                continue
    -
    -            # Store common data.
    -            connection.execute("""
    -              INSERT INTO entry VALUES (?, ?, ?, ?, ?);
    -            """, (eid, entry.date, self.name,
    -                  entry.meta["filename"], entry.meta["lineno"]))
    -
    -            # Store detail data.
    -            detail_data = self.get_detail(entry)
    -            row_data = (eid,) + detail_data
    -            query = """
    -              INSERT INTO {name}_detail VALUES ({placeholder});
    -            """.format(name=self.name,
    -                       placeholder=','.join(['?'] * (1 + len(detail_data))))
    -            connection.execute(query, row_data)
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.scripts.sql.DirectiveWriter.get_detail(self, entry) - - -

    - -
    - -

    Provide data to store for details table.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – An instance of the desired directive.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of the values corresponding to the columns declared in the -'columns' attribute.

    • -
    -
    -
    - Source code in beancount/scripts/sql.py -
    def get_detail(self, entry):
    -    """Provide data to store for details table.
    -
    -    Args:
    -      entry: An instance of the desired directive.
    -    Returns:
    -      A tuple of the values corresponding to the columns declared in the
    -      'columns' attribute.
    -    """
    -    raise NotImplementedError
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.scripts.sql.DocumentWriter (DirectiveWriter) - - - - -

    - -
    - - - - - -
    - - - - - - - - -
    - - - -

    - -beancount.scripts.sql.DocumentWriter.type (tuple) - - - - -

    - -
    - -

    Document(meta, date, account, filename, tags, links)

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.scripts.sql.DocumentWriter.type.__getnewargs__(self) - - - special - - -
    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/scripts/sql.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.DocumentWriter.type.__new__(_cls, meta, date, account, filename, tags, links) - - - special - staticmethod - - -
    - -
    - -

    Create new instance of Document(meta, date, account, filename, tags, links)

    - -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.DocumentWriter.type.__repr__(self) - - - special - - -
    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/scripts/sql.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    -beancount.scripts.sql.DocumentWriter.get_detail(self, entry) - - -

    - -
    - -

    Provide data to store for details table.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – An instance of the desired directive.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of the values corresponding to the columns declared in the -'columns' attribute.

    • -
    -
    -
    - Source code in beancount/scripts/sql.py -
    def get_detail(self, entry):
    -    return (entry.account,
    -            entry.filename)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.scripts.sql.EventWriter (DirectiveWriter) - - - - -

    - -
    - - - - - -
    - - - - - - - - -
    - - - -

    - -beancount.scripts.sql.EventWriter.type (tuple) - - - - -

    - -
    - -

    Event(meta, date, type, description)

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.scripts.sql.EventWriter.type.__getnewargs__(self) - - - special - - -
    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/scripts/sql.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.EventWriter.type.__new__(_cls, meta, date, type, description) - - - special - staticmethod - - -
    - -
    - -

    Create new instance of Event(meta, date, type, description)

    - -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.EventWriter.type.__repr__(self) - - - special - - -
    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/scripts/sql.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    -beancount.scripts.sql.EventWriter.get_detail(self, entry) - - -

    - -
    - -

    Provide data to store for details table.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – An instance of the desired directive.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of the values corresponding to the columns declared in the -'columns' attribute.

    • -
    -
    -
    - Source code in beancount/scripts/sql.py -
    def get_detail(self, entry):
    -    return (entry.type,
    -            entry.description)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.scripts.sql.NoteWriter (DirectiveWriter) - - - - -

    - -
    - - - - - -
    - - - - - - - - -
    - - - -

    - -beancount.scripts.sql.NoteWriter.type (tuple) - - - - -

    - -
    - -

    Note(meta, date, account, comment)

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.scripts.sql.NoteWriter.type.__getnewargs__(self) - - - special - - -
    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/scripts/sql.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.NoteWriter.type.__new__(_cls, meta, date, account, comment) - - - special - staticmethod - - -
    - -
    - -

    Create new instance of Note(meta, date, account, comment)

    - -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.NoteWriter.type.__repr__(self) - - - special - - -
    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/scripts/sql.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    -beancount.scripts.sql.NoteWriter.get_detail(self, entry) - - -

    - -
    - -

    Provide data to store for details table.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – An instance of the desired directive.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of the values corresponding to the columns declared in the -'columns' attribute.

    • -
    -
    -
    - Source code in beancount/scripts/sql.py -
    def get_detail(self, entry):
    -    return (entry.account,
    -            entry.comment)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.scripts.sql.OpenWriter (DirectiveWriter) - - - - -

    - -
    - - - - - -
    - - - - - - - - -
    - - - -

    - -beancount.scripts.sql.OpenWriter.type (tuple) - - - - -

    - -
    - -

    Open(meta, date, account, currencies, booking)

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.scripts.sql.OpenWriter.type.__getnewargs__(self) - - - special - - -
    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/scripts/sql.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.OpenWriter.type.__new__(_cls, meta, date, account, currencies, booking) - - - special - staticmethod - - -
    - -
    - -

    Create new instance of Open(meta, date, account, currencies, booking)

    - -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.OpenWriter.type.__repr__(self) - - - special - - -
    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/scripts/sql.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    -beancount.scripts.sql.OpenWriter.get_detail(self, entry) - - -

    - -
    - -

    Provide data to store for details table.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – An instance of the desired directive.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of the values corresponding to the columns declared in the -'columns' attribute.

    • -
    -
    -
    - Source code in beancount/scripts/sql.py -
    def get_detail(self, entry):
    -    return (entry.account,
    -            ','.join(entry.currencies or []))
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.scripts.sql.PadWriter (DirectiveWriter) - - - - -

    - -
    - - - - - -
    - - - - - - - - -
    - - - -

    - -beancount.scripts.sql.PadWriter.type (tuple) - - - - -

    - -
    - -

    Pad(meta, date, account, source_account)

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.scripts.sql.PadWriter.type.__getnewargs__(self) - - - special - - -
    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/scripts/sql.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.PadWriter.type.__new__(_cls, meta, date, account, source_account) - - - special - staticmethod - - -
    - -
    - -

    Create new instance of Pad(meta, date, account, source_account)

    - -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.PadWriter.type.__repr__(self) - - - special - - -
    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/scripts/sql.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    -beancount.scripts.sql.PadWriter.get_detail(self, entry) - - -

    - -
    - -

    Provide data to store for details table.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – An instance of the desired directive.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of the values corresponding to the columns declared in the -'columns' attribute.

    • -
    -
    -
    - Source code in beancount/scripts/sql.py -
    def get_detail(self, entry):
    -    return (entry.account, entry.source_account)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.scripts.sql.PriceWriter (DirectiveWriter) - - - - -

    - -
    - - - - - -
    - - - - - - - - -
    - - - -

    - -beancount.scripts.sql.PriceWriter.type (tuple) - - - - -

    - -
    - -

    Price(meta, date, currency, amount)

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.scripts.sql.PriceWriter.type.__getnewargs__(self) - - - special - - -
    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/scripts/sql.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.PriceWriter.type.__new__(_cls, meta, date, currency, amount) - - - special - staticmethod - - -
    - -
    - -

    Create new instance of Price(meta, date, currency, amount)

    - -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.PriceWriter.type.__repr__(self) - - - special - - -
    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/scripts/sql.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    -beancount.scripts.sql.PriceWriter.get_detail(self, entry) - - -

    - -
    - -

    Provide data to store for details table.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entry – An instance of the desired directive.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of the values corresponding to the columns declared in the -'columns' attribute.

    • -
    -
    -
    - Source code in beancount/scripts/sql.py -
    def get_detail(self, entry):
    -    return (entry.currency,
    -            entry.amount.number,
    -            entry.amount.currency)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.scripts.sql.QueryWriter (DirectiveWriter) - - - - -

    - -
    - - - - - -
    - - - - - - - - -
    - - - -

    - -beancount.scripts.sql.QueryWriter.type (tuple) - - - - -

    - -
    - -

    Query(meta, date, name, query_string)

    - - - - -
    - - - - - - - - - -
    - - - -
    -beancount.scripts.sql.QueryWriter.type.__getnewargs__(self) - - - special - - -
    - -
    - -

    Return self as a plain tuple. Used by copy and pickle.

    - -
    - Source code in beancount/scripts/sql.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    -
    -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.QueryWriter.type.__new__(_cls, meta, date, name, query_string) - - - special - staticmethod - - -
    - -
    - -

    Create new instance of Query(meta, date, name, query_string)

    - -
    - -
    - - - -
    - - - -
    -beancount.scripts.sql.QueryWriter.type.__repr__(self) - - - special - - -
    - -
    - -

    Return a nicely formatted representation string

    - -
    - Source code in beancount/scripts/sql.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    -
    -
    - -
    - - - - - -
    + the balance due to generated transactions to be applied later.

    +
    + Source code in beancount/scripts/example.py +
    def postings_for(entries, accounts, before=False):
    +    """Realize the entries and get the list of postings for the given accounts.
    +
    +    All the non-Posting directives are already filtered out.
    +
    +    Args:
    +      entries: A list of directives.
    +      accounts: A list of account strings to get the balances for.
    +      before: A boolean, if true, yield the balance before the position is applied.
    +        The default is to yield the balance after applying the position.
    +    Yields:
    +      Tuples of:
    +        posting: An instance of TxnPosting
    +        balances: A dict of Inventory balances for the given accounts _after_
    +          applying the posting. These inventory objects can be mutated to adjust
    +          the balance due to generated transactions to be applied later.
    +    """
    +    assert isinstance(accounts, list)
    +    merged_txn_postings = merge_postings(entries, accounts)
    +    balances = collections.defaultdict(inventory.Inventory)
    +    for txn_posting in merged_txn_postings:
    +        if before:
    +            yield txn_posting, balances
    +        posting = txn_posting.posting
    +        balances[posting.account].add_position(posting)
    +        if not before:
    +            yield txn_posting, balances
    +
    +
    - - -
    +
    -

    -beancount.scripts.sql.QueryWriter.get_detail(self, entry) +

    +beancount.scripts.example.price_series(start, mu, sigma) -

    +

    -

    Provide data to store for details table.

    +

    Generate a price series based on a simple stochastic model.

    @@ -8953,35 +4888,33 @@

    -
    Parameters:
      -
    • entry – An instance of the desired directive.

    • +
    • start – The beginning value.

    • +
    • mu – The per-step drift, in units of value.

    • +
    • sigma – Volatility of the changes.

    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of the values corresponding to the columns declared in the -'columns' attribute.

    • -
    -
    +

    Yields: + Floats, at each step.

    +
    - Source code in beancount/scripts/sql.py -
    def get_detail(self, entry):
    -    return (entry.name,
    -            entry.query_string)
    -
    + Source code in beancount/scripts/example.py +
    def price_series(start, mu, sigma):
    +    """Generate a price series based on a simple stochastic model.
    +
    +    Args:
    +      start: The beginning value.
    +      mu: The per-step drift, in units of value.
    +      sigma: Volatility of the changes.
    +    Yields:
    +      Floats, at each step.
    +    """
    +    value = start
    +    while 1:
    +        yield value
    +        value += random.normalvariate(mu, sigma) * value
    +
    @@ -8989,30 +4922,19 @@

    - - - - - - - - - - -
    -

    -beancount.scripts.sql.adapt_decimal(number) +

    +beancount.scripts.example.replace(string, replacements, strip=False) -

    +

    -

    Adapt a Decimal instance to a string for creating queries.

    +

    Apply word-boundaried regular expression replacements to an indented string.

    @@ -9024,7 +4946,9 @@

    @@ -9040,24 +4964,33 @@

    Parameters:
      -
    • number – An instance of Decimal.

    • +
    • string – Some input template string.

    • +
    • replacements – A dict of regexp to replacement value.

    • +
    • strip – A boolean, true if we should strip the input.

    Returns:
      -
    • A string.

    • +
    • The input string with the replacements applied to it, with the indentation removed.

    - Source code in beancount/scripts/sql.py -
    def adapt_decimal(number):
    -    """Adapt a Decimal instance to a string for creating queries.
    -
    -    Args:
    -      number: An instance of Decimal.
    -    Returns:
    -      A string.
    -    """
    -    return str(number)
    -
    + Source code in beancount/scripts/example.py +
    def replace(string, replacements, strip=False):
    +    """Apply word-boundaried regular expression replacements to an indented string.
    +
    +    Args:
    +      string: Some input template string.
    +      replacements: A dict of regexp to replacement value.
    +      strip: A boolean, true if we should strip the input.
    +    Returns:
    +      The input string with the replacements applied to it, with the indentation removed.
    +    """
    +    output = textwrap.dedent(string)
    +    if strip:
    +        output = output.strip()
    +    for from_, to_ in replacements.items():
    +        if not isinstance(to_, str) and not callable(to_):
    +            to_ = str(to_)
    +        output = re.sub(r"\b{}\b".format(from_), to_, output)
    +    return output
    +
    @@ -9069,15 +5002,15 @@

    -

    -beancount.scripts.sql.convert_decimal(string) +

    +beancount.scripts.example.validate_output(contents, positive_accounts, currency) -

    +

    -

    Convert a Decimal string to a Decimal instance.

    +

    Check that the output file validates.

    @@ -9089,7 +5022,10 @@

    @@ -9102,27 +5038,36 @@

    - - + +
    Parameters:
      -
    • string – A decimal number in a string.

    • +
    • contents – A string, the output file.

    • +
    • positive_accounts – A list of strings, account names to check for +non-negative balances.

    • +
    • currency – A string, the currency to check minimums for.

    Returns: + Exceptions:
      -
    • An instance of Decimal.

    • +
    • AssertionError – If the output does not validate.

    -
    - Source code in beancount/scripts/sql.py -
    def convert_decimal(string):
    -    """Convert a Decimal string to a Decimal instance.
    -
    -    Args:
    -      string: A decimal number in a string.
    -    Returns:
    -      An instance of Decimal.
    -    """
    -    return Decimal(string)
    -
    + Source code in beancount/scripts/example.py +
    def validate_output(contents, positive_accounts, currency):
    +    """Check that the output file validates.
    +
    +    Args:
    +      contents: A string, the output file.
    +      positive_accounts: A list of strings, account names to check for
    +        non-negative balances.
    +      currency: A string, the currency to check minimums for.
    +    Raises:
    +      AssertionError: If the output does not validate.
    +    """
    +    loaded_entries, _, _ = loader.load_string(
    +        contents, log_errors=sys.stderr, extra_validations=validation.HARDCORE_VALIDATIONS
    +    )
    +
    +    # Sanity checks: Check that the checking balance never goes below zero.
    +    for account in positive_accounts:
    +        check_non_negative(loaded_entries, account, currency)
    +
    @@ -9130,20 +5075,19 @@

    -
    -

    -beancount.scripts.sql.output_common(connection, unused_entries) +

    +beancount.scripts.example.write_example_file(date_birth, date_begin, date_end, reformat, file) -

    +

    -

    Create a table of common data for all entries.

    +

    Generate the example file.

    @@ -9155,33 +5099,284 @@

    Parameters:
      -
    • connection – A DBAPI-2.0 Connection object.

    • -
    • entries – A list of directives.

    • +
    • date_birth – A datetime.date instance, the birth date of our character.

    • +
    • date_begin – A datetime.date instance, the beginning date at which to generate +transactions.

    • +
    • date_end – A datetime.date instance, the end date at which to generate +transactions.

    • +
    • reformat – A boolean, true if we should apply global reformatting to this file.

    • +
    • file – A file object, where to write out the output.

    - Source code in beancount/scripts/sql.py -
    def output_common(connection, unused_entries):
    -    """Create a table of common data for all entries.
    -
    -    Args:
    -      connection: A DBAPI-2.0 Connection object.
    -      entries: A list of directives.
    -    """
    -    with connection:
    -        connection.execute("""
    -          CREATE TABLE entry (
    -            id 			INTEGER PRIMARY KEY,
    -            date 		DATE,
    -            type                CHARACTER(8),
    -            source_filename	STRING,
    -            source_lineno	INTEGER
    -          );
    -        """)
    -
    + Source code in beancount/scripts/example.py +
    def write_example_file(date_birth, date_begin, date_end, reformat, file):
    +    """Generate the example file.
    +
    +    Args:
    +      date_birth: A datetime.date instance, the birth date of our character.
    +      date_begin: A datetime.date instance, the beginning date at which to generate
    +        transactions.
    +      date_end: A datetime.date instance, the end date at which to generate
    +        transactions.
    +      reformat: A boolean, true if we should apply global reformatting to this file.
    +      file: A file object, where to write out the output.
    +    """
    +    # The following code entirely writes out the output to generic names, such
    +    # as "Employer1", "Bank1", and "CCY" (for principal currency). Those names
    +    # are purposely chosen to be unique, and only near the very end do we make
    +    # renamings to more specific and realistic names.
    +
    +    # Name of the checking account.
    +    account_opening = "Equity:Opening-Balances"
    +    account_payable = "Liabilities:AccountsPayable"
    +    account_checking = "Assets:CC:Bank1:Checking"
    +    account_credit = "Liabilities:CC:CreditCard1"
    +    account_retirement = "Assets:CC:Retirement"
    +    account_investing = "Assets:CC:Investment:Cash"
    +
    +    # Commodities.
    +    commodity_entries = generate_commodity_entries(date_birth)
    +
    +    # Estimate the rent.
    +    rent_amount = round_to(ANNUAL_SALARY / RENT_DIVISOR, RENT_INCREMENT)
    +
    +    # Get a random employer.
    +    employer_name, employer_address = random.choice(EMPLOYERS)
    +
    +    logging.info("Generating Salary Employment Income")
    +    income_entries = generate_employment_income(
    +        employer_name,
    +        employer_address,
    +        ANNUAL_SALARY,
    +        account_checking,
    +        join(account_retirement, "Cash"),
    +        date_begin,
    +        date_end,
    +    )
    +
    +    logging.info("Generating Expenses from Banking Accounts")
    +    banking_expenses = generate_banking_expenses(
    +        date_begin, date_end, account_checking, rent_amount
    +    )
    +
    +    logging.info("Generating Regular Expenses via Credit Card")
    +    credit_regular_entries = generate_regular_credit_expenses(
    +        date_birth, date_begin, date_end, account_credit, account_checking
    +    )
    +
    +    logging.info("Generating Credit Card Expenses for Trips")
    +    trip_entries = []
    +    destinations = sorted(TRIP_DESTINATIONS.items())
    +    destinations.extend(destinations)
    +    random.shuffle(destinations)
    +    for (date_trip_begin, date_trip_end), (destination_name, config) in zip(
    +        compute_trip_dates(date_begin, date_end), destinations
    +    ):
    +        # Compute a suitable tag.
    +        tag = "trip-{}-{}".format(
    +            destination_name.lower().replace(" ", "-"), date_trip_begin.year
    +        )
    +        # logging.info("%s -- %s %s", tag, date_trip_begin, date_trip_end)
    +
    +        # Remove regular entries during this trip.
    +        credit_regular_entries = [
    +            entry
    +            for entry in credit_regular_entries
    +            if not (date_trip_begin <= entry.date < date_trip_end)
    +        ]
    +
    +        # Generate entries for the trip.
    +        this_trip_entries = generate_trip_entries(
    +            date_trip_begin,
    +            date_trip_end,
    +            tag,
    +            config,
    +            destination_name.replace("-", " ").title(),
    +            HOME_NAME,
    +            account_credit,
    +        )
    +
    +        trip_entries.extend(this_trip_entries)
    +
    +    logging.info("Generating Credit Card Payment Entries")
    +    credit_payments = generate_clearing_entries(
    +        delay_dates(
    +            rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end, bymonthday=7),
    +            0,
    +            4,
    +        ),
    +        "CreditCard1",
    +        "Paying off credit card",
    +        credit_regular_entries,
    +        account_credit,
    +        account_checking,
    +    )
    +
    +    credit_entries = credit_regular_entries + trip_entries + credit_payments
    +
    +    logging.info("Generating Tax Filings and Payments")
    +    tax_preamble = generate_tax_preamble(date_birth)
    +
    +    # Figure out all the years we need tax accounts for.
    +    years = set()
    +    for account_name in getters.get_accounts(income_entries):
    +        match = re.match(r"Expenses:Taxes:Y(\d\d\d\d)", account_name)
    +        if match:
    +            years.add(int(match.group(1)))
    +
    +    taxes = [(year, generate_tax_accounts(year, date_end)) for year in sorted(years)]
    +    tax_entries = tax_preamble + functools.reduce(
    +        operator.add, (entries for _, entries in taxes)
    +    )
    +
    +    logging.info("Generating Opening of Banking Accounts")
    +    # Open banking accounts and gift the checking account with a balance that
    +    # will offset all the amounts to ensure a positive balance throughout its
    +    # lifetime.
    +    entries_for_banking = data.sorted(
    +        income_entries + banking_expenses + credit_entries + tax_entries
    +    )
    +    minimum = get_minimum_balance(entries_for_banking, account_checking, "CCY")
    +    banking_entries = generate_banking(
    +        entries_for_banking, date_begin, date_end, max(-minimum, ZERO)
    +    )
    +
    +    logging.info("Generating Transfers to Investment Account")
    +    banking_transfers = generate_outgoing_transfers(
    +        data.sorted(
    +            income_entries
    +            + banking_entries
    +            + banking_expenses
    +            + credit_entries
    +            + tax_entries
    +        ),
    +        account_checking,
    +        account_investing,
    +        transfer_minimum=D("200"),
    +        transfer_threshold=D("3000"),
    +        transfer_increment=D("500"),
    +    )
    +
    +    logging.info("Generating Prices")
    +    # Generate price entries for investment currencies and create a price map to
    +    # use for later for generating investment transactions.
    +    funds_allocation = {"MFUND1": 0.40, "MFUND2": 0.60}
    +    stocks = ["STK1", "STK2", "STK3", "STK4"]
    +    price_entries = generate_prices(
    +        date_begin, date_end, sorted(funds_allocation.keys()) + stocks, "CCY"
    +    )
    +    price_map = prices.build_price_map(price_entries)
    +
    +    logging.info("Generating Employer Match Contribution")
    +    account_match = "Income:US:Employer1:Match401k"
    +    retirement_match = generate_retirement_employer_match(
    +        income_entries, join(account_retirement, "Cash"), account_match
    +    )
    +
    +    logging.info("Generating Retirement Investments")
    +    retirement_entries = generate_retirement_investments(
    +        income_entries + retirement_match,
    +        account_retirement,
    +        sorted(funds_allocation.items()),
    +        price_map,
    +    )
    +
    +    logging.info("Generating Taxes Investments")
    +    investment_entries = generate_taxable_investment(
    +        date_begin, date_end, banking_transfers, price_map, stocks
    +    )
    +
    +    logging.info("Generating Expense Accounts")
    +    expense_accounts_entries = generate_expense_accounts(date_birth)
    +
    +    logging.info("Generating Equity Accounts")
    +    equity_entries = generate_open_entries(date_birth, [account_opening, account_payable])
    +
    +    logging.info("Generating Balance Checks")
    +    credit_checks = generate_balance_checks(
    +        credit_entries, account_credit, date_random_seq(date_begin, date_end, 20, 30)
    +    )
    +
    +    banking_checks = generate_balance_checks(
    +        data.sorted(
    +            income_entries
    +            + banking_entries
    +            + banking_expenses
    +            + banking_transfers
    +            + credit_entries
    +            + tax_entries
    +        ),
    +        account_checking,
    +        date_random_seq(date_begin, date_end, 20, 30),
    +    )
    +
    +    logging.info("Outputting and Formatting Entries")
    +    dcontext = display_context.DisplayContext()
    +    default_int_digits = 8
    +    for currency, precision in {
    +        "USD": 2,
    +        "CAD": 2,
    +        "VACHR": 0,
    +        "IRAUSD": 2,
    +        "VBMPX": 3,
    +        "RGAGX": 3,
    +        "ITOT": 0,
    +        "VEA": 0,
    +        "VHT": 0,
    +        "GLD": 0,
    +    }.items():
    +        int_digits = default_int_digits
    +        if precision > 0:
    +            int_digits += 1 + precision
    +        dcontext.update(D("{{:0{}.{}f}}".format(int_digits, precision).format(0)), currency)
    +
    +    output = io.StringIO()
    +
    +    def output_section(title, entries):
    +        output.write("\n\n\n{}\n\n".format(title))
    +        printer.print_entries(data.sorted(entries), dcontext, file=output)
    +
    +    output.write(FILE_PREAMBLE.format(**locals()))
    +    output_section("* Commodities", commodity_entries)
    +    output_section("* Equity Accounts", equity_entries)
    +    output_section(
    +        "* Banking",
    +        data.sorted(
    +            banking_entries + banking_expenses + banking_transfers + banking_checks
    +        ),
    +    )
    +    output_section("* Credit-Cards", data.sorted(credit_entries + credit_checks))
    +    output_section("* Taxable Investments", investment_entries)
    +    output_section(
    +        "* Retirement Investments", data.sorted(retirement_entries + retirement_match)
    +    )
    +    output_section("* Sources of Income", income_entries)
    +    output_section("* Taxes", tax_preamble)
    +    for year, entries in taxes:
    +        output_section("** Tax Year {}".format(year), entries)
    +    output_section("* Expenses", expense_accounts_entries)
    +    output_section("* Prices", price_entries)
    +    output_section("* Cash", [])
    +
    +    logging.info("Contextualizing to Realistic Names")
    +    contents, replacements = contextualize_file(output.getvalue(), employer_name)
    +    if reformat:
    +        contents = format.align_beancount(contents)
    +
    +    logging.info("Writing contents")
    +    file.write(contents)
    +
    +    logging.info("Validating Results")
    +    validate_output(
    +        contents,
    +        [replace(account, replacements) for account in [account_checking]],
    +        replace("CCY", replacements),
    +    )
    +
    @@ -9189,19 +5384,59 @@

    + + + + + + + + + + + +
    + + + +

    + beancount.scripts.format + + + +

    + +
    + + + + +
    + + + + + + + + + + + +
    -

    -beancount.scripts.sql.output_transactions(connection, entries) +

    +beancount.scripts.format.align_beancount(contents, prefix_width=None, num_width=None, currency_column=None) -

    +

    -

    Create a table for transactions and fill in the data.

    +

    Reformat Beancount input to align all the numbers at the same column.

    @@ -9213,90 +5448,135 @@

    +
    Parameters:
      -
    • connection – A DBAPI-2.0 Connection object.

    • -
    • entries – A list of directives.

    • +
    • contents – A string, Beancount input syntax to reformat.

    • +
    • prefix_width – An integer, the width in characters to render the account +name to. If this is not specified, a good value is selected +automatically from the contents of the file.

    • +
    • num_width – An integer, the width to render each number. If this is not +specified, a good value is selected automatically from the contents of +the file.

    • +
    • currency_column – An integer, the column at which to align the currencies. +If given, this overrides the other options.

    + + + + + + + + + + +
    Returns: +
      +
    • A string, reformatted Beancount input with all the number aligned. +No other changes than whitespace changes should be present between that +return value and the input contents.

    • +
    +
    - Source code in beancount/scripts/sql.py -
    def output_transactions(connection, entries):
    -    """Create a table for transactions and fill in the data.
    -
    -    Args:
    -      connection: A DBAPI-2.0 Connection object.
    -      entries: A list of directives.
    -    """
    -    with connection:
    -        connection.execute("""
    -          CREATE TABLE transactions_detail (
    -            id 			INTEGER PRIMARY KEY,
    -            flag 		CHARACTER(1),
    -            payee 		VARCHAR,
    -            narration 		VARCHAR,
    -            tags                VARCHAR, -- Comma-separated
    -            links               VARCHAR  -- Comma-separated
    -          );
    -        """)
    -
    -        connection.execute("""
    -          CREATE VIEW transactions AS
    -            SELECT * FROM entry JOIN transactions_detail USING (id);
    -        """)
    -
    -        connection.execute("""
    -          CREATE TABLE postings (
    -            posting_id		INTEGER PRIMARY KEY,
    -            id 			INTEGER,
    -            flag                CHARACTER(1),
    -            account             VARCHAR,
    -            number              DECIMAL(16, 6),
    -            currency            CHARACTER(10),
    -            cost_number         DECIMAL(16, 6),
    -            cost_currency       CHARACTER(10),
    -            cost_date           DATE,
    -            cost_label          VARCHAR,
    -            price_number        DECIMAL(16, 6),
    -            price_currency      CHARACTER(10),
    -            FOREIGN KEY(id) REFERENCES entries(id)
    -          );
    -        """)
    -
    -    postings_count = iter(itertools.count())
    -    with connection:
    -        for eid, entry in enumerate(entries):
    -            if not isinstance(entry, data.Transaction):
    -                continue
    -            connection.execute("""
    -              insert into entry values (?, ?, ?, ?, ?);
    -            """, (eid, entry.date, 'txn', entry.meta["filename"], entry.meta["lineno"]))
    -
    -            connection.execute("""
    -              insert into transactions_detail values (?, ?, ?, ?, ?, ?);
    -            """, (eid, entry.flag, entry.payee, entry.narration,
    -                  ','.join(entry.tags or ()), ','.join(entry.links or ())))
    -
    -            for posting in entry.postings:
    -                pid = next(postings_count)
    -                units = posting.units
    -                cost = posting.cost
    -                price = posting.price
    -                connection.execute("""
    -                  INSERT INTO postings VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
    -                """, (pid, eid,
    -                      posting.flag,
    -                      posting.account,
    -                      units.number,
    -                      units.currency,
    -                      cost.number if cost else None,
    -                      cost.currency if cost else None,
    -                      cost.date if cost else None,
    -                      cost.label if cost else None,
    -                      price.number if price else None,
    -                      price.currency if price else None))
    -
    + Source code in beancount/scripts/format.py +
    def align_beancount(contents, prefix_width=None, num_width=None, currency_column=None):
    +    """Reformat Beancount input to align all the numbers at the same column.
    +
    +    Args:
    +      contents: A string, Beancount input syntax to reformat.
    +      prefix_width: An integer, the width in characters to render the account
    +        name to. If this is not specified, a good value is selected
    +        automatically from the contents of the file.
    +      num_width: An integer, the width to render each number. If this is not
    +        specified, a good value is selected automatically from the contents of
    +        the file.
    +      currency_column: An integer, the column at which to align the currencies.
    +        If given, this overrides the other options.
    +    Returns:
    +      A string, reformatted Beancount input with all the number aligned.
    +      No other changes than whitespace changes should be present between that
    +      return value and the input contents.
    +
    +    """
    +    # Find all lines that have a number in them and calculate the maximum length
    +    # of the stripped prefix and the number.
    +    match_pairs = []
    +    for line in contents.splitlines():
    +        match = regex.match(
    +            rf'(^\d[^";]*?|\s+{account.ACCOUNT_RE})\s+'
    +            rf"({PARENTHESIZED_BINARY_OP_RE}|{NUMBER_RE})\s+"
    +            rf"((?:{amount.CURRENCY_RE})\b.*)",
    +            line,
    +        )
    +        if match:
    +            prefix, number, rest = match.groups()
    +            match_pairs.append((prefix, number, rest))
    +        else:
    +            match_pairs.append((line, None, None))
    +
    +    # Normalize whitespace before lines that has some indent and an account
    +    # name.
    +    norm_match_pairs = normalize_indent_whitespace(match_pairs)
    +
    +    if currency_column:
    +        output = io.StringIO()
    +        for prefix, number, rest in norm_match_pairs:
    +            if number is None:
    +                output.write(prefix)
    +            else:
    +                num_of_spaces = currency_column - len(prefix) - len(number) - 4
    +                spaces = " " * num_of_spaces
    +                output.write(prefix + spaces + "  " + number + " " + rest)
    +            output.write("\n")
    +        return output.getvalue()
    +
    +    # Compute the maximum widths.
    +    filtered_pairs = [
    +        (prefix, number) for prefix, number, _ in match_pairs if number is not None
    +    ]
    +
    +    if filtered_pairs:
    +        max_prefix_width = max(len(prefix) for prefix, _ in filtered_pairs)
    +        max_num_width = max(len(number) for _, number in filtered_pairs)
    +    else:
    +        max_prefix_width = 0
    +        max_num_width = 0
    +
    +    # Use user-supplied overrides, if available
    +    if prefix_width:
    +        max_prefix_width = prefix_width
    +    if num_width:
    +        max_num_width = num_width
    +
    +    # Create a format that will admit the maximum width of all prefixes equally.
    +    line_format = "{{:<{prefix_width}}}  {{:>{num_width}}} {{}}".format(
    +        prefix_width=max_prefix_width, num_width=max_num_width
    +    )
    +
    +    # Process each line to an output buffer.
    +    output = io.StringIO()
    +    for prefix, number, rest in norm_match_pairs:
    +        if number is None:
    +            output.write(prefix)
    +        else:
    +            output.write(line_format.format(prefix.rstrip(), number, rest))
    +        output.write("\n")
    +    formatted_contents = output.getvalue()
    +
    +    # Ensure that the file before and after have only whitespace differences.
    +    # This is a sanity check, to make really sure we never change anything but whitespace,
    +    # so it's safe.
    +    # open('/tmp/before', 'w').write(regex.sub(r'[ \t]+', ' ', contents))
    +    # open('/tmp/after', 'w').write(regex.sub(r'[ \t]+', ' ', formatted_contents))
    +    old_stripped = regex.sub(r"[ \t\n]+", " ", contents.rstrip())
    +    new_stripped = regex.sub(r"[ \t\n]+", " ", formatted_contents.rstrip())
    +    assert old_stripped == new_stripped, (old_stripped, new_stripped)
    +
    +    return formatted_contents
    +
    @@ -9308,24 +5588,68 @@

    -

    -beancount.scripts.sql.setup_decimal_support() +

    +beancount.scripts.format.compute_most_frequent(iterable) -

    +

    -

    Setup sqlite3 to support conversions to/from Decimal numbers.

    +

    Compute the frequencies of the given elements and return the most frequent.

    + + + + + + + + + + + +
    Parameters: +
      +
    • iterable – A collection of hashable elements.

    • +
    +
    + + + + + + + + + + + +
    Returns: +
      +
    • The most frequent element. If there are no elements in the iterable, +return None.

    • +
    +
    - Source code in beancount/scripts/sql.py -
    def setup_decimal_support():
    -    """Setup sqlite3 to support conversions to/from Decimal numbers.
    -    """
    -    dbapi.register_adapter(Decimal, adapt_decimal)
    -    dbapi.register_converter("decimal", convert_decimal)
    -
    + Source code in beancount/scripts/format.py +
    def compute_most_frequent(iterable):
    +    """Compute the frequencies of the given elements and return the most frequent.
    +
    +    Args:
    +      iterable: A collection of hashable elements.
    +    Returns:
    +      The most frequent element. If there are no elements in the iterable,
    +      return None.
    +    """
    +    frequencies = collections.Counter(iterable)
    +    if not frequencies:
    +        return None
    +    counts = sorted((count, element) for element, count in frequencies.items())
    +    # Note: In case of a tie, this chooses the longest width.
    +    # We could eventually make this an option.
    +    return counts[-1][1]
    +
    @@ -9333,44 +5657,87 @@

    +
    -
    - - - - - - - -
    - - - -

    - beancount.scripts.tutorial - +

    +beancount.scripts.format.normalize_indent_whitespace(match_pairs) -

    +

    -

    Write output files for the tutorial commands.

    - - - -
    - - - - - - - +

    Normalize whitespace before lines that has some indent and an account name.

    + + + + + + + + + + + +
    Parameters: +
      +
    • match_pairs – A list of (prefix, number, rest) tuples.

    • +
    +
    + + + + + + + + + + + +
    Returns: +
      +
    • Another list of (prefix, number, rest) tuples, where prefix may have been +adjusted with a different whitespace prefix.

    • +
    +
    +
    + Source code in beancount/scripts/format.py +
    def normalize_indent_whitespace(match_pairs):
    +    """Normalize whitespace before lines that has some indent and an account name.
    +
    +    Args:
    +      match_pairs: A list of (prefix, number, rest) tuples.
    +    Returns:
    +      Another list of (prefix, number, rest) tuples, where prefix may have been
    +      adjusted with a different whitespace prefix.
    +    """
    +    # Compute most frequent account name prefix.
    +    match_posting = regex.compile(r"([ \t]+)({}.*)".format(account.ACCOUNT_RE)).match
    +    width = compute_most_frequent(
    +        len(match.group(1))
    +        for match in (match_posting(prefix) for prefix, _, _ in match_pairs)
    +        if match is not None
    +    )
    +    norm_format = " " * (width or 0) + "{}"
    +
    +    # Make the necessary adjustments.
    +    adjusted_pairs = []
    +    for tup in match_pairs:
    +        prefix, number, rest = tup
    +        match = match_posting(prefix)
    +        if match is not None:
    +            tup = (norm_format.format(match.group(2)), number, rest)
    +        adjusted_pairs.append(tup)
    +    return adjusted_pairs
    +
    +
    +
    +
    @@ -9401,7 +5768,7 @@

    Next - Previous + Previous @@ -9427,7 +5794,7 @@

    - « Previous + « Previous Next » diff --git a/api_reference/beancount.tools.html b/api_reference/beancount.tools.html index 3cf06be4..eef357d3 100644 --- a/api_reference/beancount.tools.html +++ b/api_reference/beancount.tools.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -152,8 +154,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -162,12 +162,6 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools @@ -212,8 +206,6 @@
  • beancount.utils
  • -
  • beancount.web -
  • @@ -366,7 +358,7 @@

    -beancount.tools.treeify.Node.__repr__(self) +beancount.tools.treeify.Node.__repr__(self) special @@ -380,9 +372,9 @@

    Source code in beancount/tools/treeify.py -
    def __str__(self):
    -    return '<Node {} {}>'.format(self.name, [node.name for node in self])
    -
    +
    def __str__(self):
    +    return "<Node {} {}>".format(self.name, [node.name for node in self])
    +
    @@ -408,7 +400,7 @@

    -beancount.tools.treeify.create_tree(column_matches, regexp_split) +beancount.tools.treeify.create_tree(column_matches, regexp_split)

    @@ -453,29 +445,29 @@

    Source code in beancount/tools/treeify.py -
    def create_tree(column_matches, regexp_split):
    -    """Build up a tree from a list of matches.
    -
    -    Args:
    -      column_matches: A list of (line-number, name) pairs.
    -      regexp_split: A regular expression string, to use for splitting the names
    -        of components.
    -    Returns:
    -      An instance of Node, the root node of the created tree.
    -    """
    -    root = Node('')
    -    for no, name in column_matches:
    -        parts = re.split(regexp_split, name)
    -        node = root
    -        for part in parts:
    -            last_node = node[-1] if node else None
    -            if last_node is None or last_node.name != part:
    -                last_node = Node(part)
    -                node.append(last_node)
    -            node = last_node
    -        node.nos.append(no)
    -    return root
    -
    +
    def create_tree(column_matches, regexp_split):
    +    """Build up a tree from a list of matches.
    +
    +    Args:
    +      column_matches: A list of (line-number, name) pairs.
    +      regexp_split: A regular expression string, to use for splitting the names
    +        of components.
    +    Returns:
    +      An instance of Node, the root node of the created tree.
    +    """
    +    root = Node("")
    +    for no, name in column_matches:
    +        parts = re.split(regexp_split, name)
    +        node = root
    +        for part in parts:
    +            last_node = node[-1] if node else None
    +            if last_node is None or last_node.name != part:
    +                last_node = Node(part)
    +                node.append(last_node)
    +            node = last_node
    +        node.nos.append(no)
    +    return root
    +
    @@ -488,7 +480,7 @@

    -beancount.tools.treeify.dump_tree(node, file=<_io.StringIO object at 0x78e865153580>, prefix='') +beancount.tools.treeify.dump_tree(node, file=<_io.StringIO object at 0x78fcdeaef7c0>, prefix='')

    @@ -517,20 +509,20 @@

    Source code in beancount/tools/treeify.py -
    def dump_tree(node, file=sys.stdout, prefix=''):
    -    """Render a tree as a tree.
    -
    -    Args:
    -      node: An instance of Node.
    -      file: A file object to write to.
    -      prefix: A prefix string for each of the lines of the children.
    -    """
    -    file.write(prefix)
    -    file.write(node.name)
    -    file.write('\n')
    -    for child in node:
    -        dump_tree(child, file, prefix + '... ')
    -
    +
    def dump_tree(node, file=sys.stdout, prefix=""):
    +    """Render a tree as a tree.
    +
    +    Args:
    +      node: An instance of Node.
    +      file: A file object to write to.
    +      prefix: A prefix string for each of the lines of the children.
    +    """
    +    file.write(prefix)
    +    file.write(node.name)
    +    file.write("\n")
    +    for child in node:
    +        dump_tree(child, file, prefix + "... ")
    +
    @@ -543,7 +535,7 @@

    -beancount.tools.treeify.enum_tree_by_input_line_num(tree_lines) +beancount.tools.treeify.enum_tree_by_input_line_num(tree_lines)

    @@ -572,28 +564,28 @@

    Source code in beancount/tools/treeify.py -
    def enum_tree_by_input_line_num(tree_lines):
    -    """Accumulate the lines of a tree until a line number is found.
    -
    -    Args:
    -      tree_lines: A list of lines as returned by render_tree.
    -    Yields:
    -      Pairs of (line number, list of (line, node)).
    -    """
    -    pending = []
    -    for first_line, cont_line, node in tree_lines:
    -        if not node.nos:
    -            pending.append((first_line, node))
    -        else:
    -            line = first_line
    -            for no in node.nos:
    -                pending.append((line, node))
    -                line = cont_line
    -                yield (no, pending)
    -                pending = []
    -    if pending:
    -        yield (None, pending)
    -
    +
    def enum_tree_by_input_line_num(tree_lines):
    +    """Accumulate the lines of a tree until a line number is found.
    +
    +    Args:
    +      tree_lines: A list of lines as returned by render_tree.
    +    Yields:
    +      Pairs of (line number, list of (line, node)).
    +    """
    +    pending = []
    +    for first_line, cont_line, node in tree_lines:
    +        if not node.nos:
    +            pending.append((first_line, node))
    +        else:
    +            line = first_line
    +            for no in node.nos:
    +                pending.append((line, node))
    +                line = cont_line
    +                yield (no, pending)
    +                pending = []
    +    if pending:
    +        yield (None, pending)
    +
    @@ -606,7 +598,7 @@

    -beancount.tools.treeify.find_column(lines, pattern, delimiter) +beancount.tools.treeify.find_column(lines, pattern, delimiter)

    @@ -660,61 +652,62 @@

    Source code in beancount/tools/treeify.py -
    def find_column(lines, pattern, delimiter):
    -    """Find a valid column with hierarchical data in the text lines.
    -
    -    Args:
    -      lines: A list of strings, the contents of the input.
    -      pattern: A regular expression for the hierarchical entries.
    -      delimiter: A regular expression that dictates how we detect the
    -        end of a column. Normally this is a single space. If the patterns
    -        contain spaces, you will need to increase this.
    -    Returns:
    -      A tuple of
    -        matches: A list of (line-number, name) tuples where 'name' is the
    -          hierarchical string to treeify and line-number is an integer, the
    -          line number where this applies.
    -        left: An integer, the leftmost column.
    -        right: An integer, the rightmost column.
    -      Note that not all line numbers may be present, so you may need to
    -      skip some. However, they are in guaranteed in sorted order.
    -    """
    -    # A mapping of the line beginning position to its match object.
    -    beginnings = collections.defaultdict(list)
    -    pattern_and_whitespace = "({})(?P<ws>{}.|$)".format(pattern, delimiter)
    -    for no, line in enumerate(lines):
    -        for match in re.finditer(pattern_and_whitespace, line):
    -            beginnings[match.start()].append((no, line, match))
    -
    -    # For each potential column found, verify that it is valid. A valid column
    -    # will have the maximum of its content text not overlap with any of the
    -    # following text. We assume that a column will have been formatted to full
    -    # width and that no text following the line overlap with the column, even in
    -    # its trailing whitespace.
    -    #
    -    # In other words, the following example is a violation because "10,990.74"
    -    # overlaps with the end of "Insurance" and so this would not be recognized
    -    # as a valid column:
    -    #
    -    # Expenses:Food:Restaurant     10,990.74 USD
    -    # Expenses:Health:Dental:Insurance   208.80 USD
    -    #
    -    for leftmost_column, column_matches in sorted(beginnings.items()):
    -
    -        # Compute the location of the rightmost column of text.
    -        rightmost_column = max(match.end(1) for _, _, match in column_matches)
    -
    -        # Compute the leftmost location of the content following the column text
    -        # and past its whitespace.
    -        following_column = min(match.end() if match.group('ws') else 10000
    -                               for _, _, match in column_matches)
    -
    -        if rightmost_column < following_column:
    -            # We process only the very first match.
    -            return_matches = [(no, match.group(1).rstrip())
    -                              for no, _, match in column_matches]
    -            return return_matches, leftmost_column, rightmost_column
    -
    +
    def find_column(lines, pattern, delimiter):
    +    """Find a valid column with hierarchical data in the text lines.
    +
    +    Args:
    +      lines: A list of strings, the contents of the input.
    +      pattern: A regular expression for the hierarchical entries.
    +      delimiter: A regular expression that dictates how we detect the
    +        end of a column. Normally this is a single space. If the patterns
    +        contain spaces, you will need to increase this.
    +    Returns:
    +      A tuple of
    +        matches: A list of (line-number, name) tuples where 'name' is the
    +          hierarchical string to treeify and line-number is an integer, the
    +          line number where this applies.
    +        left: An integer, the leftmost column.
    +        right: An integer, the rightmost column.
    +      Note that not all line numbers may be present, so you may need to
    +      skip some. However, they are in guaranteed in sorted order.
    +    """
    +    # A mapping of the line beginning position to its match object.
    +    beginnings = collections.defaultdict(list)
    +    pattern_and_whitespace = "({})(?P<ws>{}.|$)".format(pattern, delimiter)
    +    for no, line in enumerate(lines):
    +        for match in re.finditer(pattern_and_whitespace, line):
    +            beginnings[match.start()].append((no, line, match))
    +
    +    # For each potential column found, verify that it is valid. A valid column
    +    # will have the maximum of its content text not overlap with any of the
    +    # following text. We assume that a column will have been formatted to full
    +    # width and that no text following the line overlap with the column, even in
    +    # its trailing whitespace.
    +    #
    +    # In other words, the following example is a violation because "10,990.74"
    +    # overlaps with the end of "Insurance" and so this would not be recognized
    +    # as a valid column:
    +    #
    +    # Expenses:Food:Restaurant     10,990.74 USD
    +    # Expenses:Health:Dental:Insurance   208.80 USD
    +    #
    +    for leftmost_column, column_matches in sorted(beginnings.items()):
    +        # Compute the location of the rightmost column of text.
    +        rightmost_column = max(match.end(1) for _, _, match in column_matches)
    +
    +        # Compute the leftmost location of the content following the column text
    +        # and past its whitespace.
    +        following_column = min(
    +            match.end() if match.group("ws") else 10000 for _, _, match in column_matches
    +        )
    +
    +        if rightmost_column < following_column:
    +            # We process only the very first match.
    +            return_matches = [
    +                (no, match.group(1).rstrip()) for no, _, match in column_matches
    +            ]
    +            return return_matches, leftmost_column, rightmost_column
    +
    @@ -728,7 +721,7 @@

    -beancount.tools.treeify.render_tree(root) +beancount.tools.treeify.render_tree(root)

    @@ -760,82 +753,80 @@

    Source code in beancount/tools/treeify.py -
    def render_tree(root):
    -    """Render a tree of nodes.
    -
    -    Returns:
    -      A list of tuples of (first_line, continuation_line, node) where
    -        first_line: A string, the first line to render, which includes the
    -          account name.
    -        continuation_line: A string, further line to render if necessary.
    -        node: The Node instance which corresponds to this line.
    -      and an integer, the width of the new columns.
    -    """
    -    # Compute all the lines ahead of time in order to calculate the width.
    -    lines = []
    -
    -    # Start with the root node. We push the constant prefix before this node,
    -    # the account name, and the RealAccount instance. We will maintain a stack
    -    # of children nodes to render.
    -    stack = [('', root.name, root, True)]
    -    while stack:
    -        prefix, name, node, is_last = stack.pop(-1)
    -
    -        if node is root:
    -            # For the root node, we don't want to render any prefix.
    -            first = cont = ''
    -        else:
    -            # Compute the string that precedes the name directly and the one below
    -            # that for the continuation lines.
    -            #  |
    -            #  @@@ Bank1    <----------------
    -            #  @@@ |
    -            #  |   |-- Checking
    -            if is_last:
    -                first = prefix + PREFIX_LEAF_1
    -                cont = prefix + PREFIX_LEAF_C
    -            else:
    -                first = prefix + PREFIX_CHILD_1
    -                cont = prefix + PREFIX_CHILD_C
    -
    -        # Compute the name to render for continuation lines.
    -        #  |
    -        #  |-- Bank1
    -        #  |   @@@       <----------------
    -        #  |   |-- Checking
    -        if len(node) > 0:
    -            cont_name = PREFIX_CHILD_C
    -        else:
    -            cont_name = PREFIX_LEAF_C
    -
    -        # Add a line for this account.
    -        if not (node is root and not name):
    -            lines.append((first + name,
    -                          cont + cont_name,
    -                          node))
    -
    -        # Push the children onto the stack, being careful with ordering and
    -        # marking the last node as such.
    -        if node:
    -            child_items = reversed(node)
    -            child_iter = iter(child_items)
    -            child_node = next(child_iter)
    -            stack.append((cont, child_node.name, child_node, True))
    -            for child_node in child_iter:
    -                stack.append((cont, child_node.name, child_node, False))
    -
    -    if not lines:
    -        return lines
    -
    -    # Compute the maximum width of the lines and convert all of them to the same
    -    # maximal width. This makes it easy on the client.
    -    max_width = max(len(first_line) for first_line, _, __ in lines)
    -    line_format = '{{:{width}}}'.format(width=max_width)
    -    return [(line_format.format(first_line),
    -             line_format.format(cont_line),
    -             node)
    -            for (first_line, cont_line, node) in lines], max_width
    -
    +
    def render_tree(root):
    +    """Render a tree of nodes.
    +
    +    Returns:
    +      A list of tuples of (first_line, continuation_line, node) where
    +        first_line: A string, the first line to render, which includes the
    +          account name.
    +        continuation_line: A string, further line to render if necessary.
    +        node: The Node instance which corresponds to this line.
    +      and an integer, the width of the new columns.
    +    """
    +    # Compute all the lines ahead of time in order to calculate the width.
    +    lines = []
    +
    +    # Start with the root node. We push the constant prefix before this node,
    +    # the account name, and the RealAccount instance. We will maintain a stack
    +    # of children nodes to render.
    +    stack = [("", root.name, root, True)]
    +    while stack:
    +        prefix, name, node, is_last = stack.pop(-1)
    +
    +        if node is root:
    +            # For the root node, we don't want to render any prefix.
    +            first = cont = ""
    +        else:
    +            # Compute the string that precedes the name directly and the one below
    +            # that for the continuation lines.
    +            #  |
    +            #  @@@ Bank1    <----------------
    +            #  @@@ |
    +            #  |   |-- Checking
    +            if is_last:
    +                first = prefix + PREFIX_LEAF_1
    +                cont = prefix + PREFIX_LEAF_C
    +            else:
    +                first = prefix + PREFIX_CHILD_1
    +                cont = prefix + PREFIX_CHILD_C
    +
    +        # Compute the name to render for continuation lines.
    +        #  |
    +        #  |-- Bank1
    +        #  |   @@@       <----------------
    +        #  |   |-- Checking
    +        if len(node) > 0:
    +            cont_name = PREFIX_CHILD_C
    +        else:
    +            cont_name = PREFIX_LEAF_C
    +
    +        # Add a line for this account.
    +        if not (node is root and not name):
    +            lines.append((first + name, cont + cont_name, node))
    +
    +        # Push the children onto the stack, being careful with ordering and
    +        # marking the last node as such.
    +        if node:
    +            child_items = reversed(node)
    +            child_iter = iter(child_items)
    +            child_node = next(child_iter)
    +            stack.append((cont, child_node.name, child_node, True))
    +            for child_node in child_iter:
    +                stack.append((cont, child_node.name, child_node, False))
    +
    +    if not lines:
    +        return lines
    +
    +    # Compute the maximum width of the lines and convert all of them to the same
    +    # maximal width. This makes it easy on the client.
    +    max_width = max(len(first_line) for first_line, _, __ in lines)
    +    line_format = "{{:{width}}}".format(width=max_width)
    +    return [
    +        (line_format.format(first_line), line_format.format(cont_line), node)
    +        for (first_line, cont_line, node) in lines
    +    ], max_width
    +
    @@ -919,7 +910,7 @@

    -beancount.tools.treeify_test.TestTreeifyBase.treeify(self, string, expect_errors=False, options=None) +beancount.tools.treeify_test.TestTreeifyBase.treeify(self, string, expect_errors=False, options=None)

    @@ -965,26 +956,26 @@

    Source code in beancount/tools/treeify_test.py -
    def treeify(self, string, expect_errors=False, options=None):
    -    """Run treeify on the given string and assert no errors.
    -
    -    Args:
    -      string: A string, the input contents to run on.
    -      expect_errors: A boolean, true if we should expect there to be errors.
    -      options: An optional list of options for the subprogram.
    -    Returns:
    -      The converted output string. This fails the test if there were any
    -      errors.
    -    """
    -    returncode, output, errors = treeify(string, options)
    -    actual_errors = returncode != 0 or bool(errors)
    -    if actual_errors != expect_errors:
    -        if expect_errors:
    -            self.fail("Missing expected errors")
    -        else:
    -            self.fail("Unexpected errors: {}".format(errors))
    -    return output
    -
    +
    def treeify(self, string, expect_errors=False, options=None):
    +    """Run treeify on the given string and assert no errors.
    +
    +    Args:
    +      string: A string, the input contents to run on.
    +      expect_errors: A boolean, true if we should expect there to be errors.
    +      options: An optional list of options for the subprogram.
    +    Returns:
    +      The converted output string. This fails the test if there were any
    +      errors.
    +    """
    +    returncode, output, errors = treeify(string, options)
    +    actual_errors = returncode != 0 or bool(errors)
    +    if actual_errors != expect_errors:
    +        if expect_errors:
    +            self.fail("Missing expected errors")
    +        else:
    +            self.fail("Unexpected errors: {}".format(errors))
    +    return output
    +
    @@ -997,7 +988,7 @@

    -beancount.tools.treeify_test.TestTreeifyBase.treeify_equal(self, string, expected, expect_errors=False, options=None) +beancount.tools.treeify_test.TestTreeifyBase.treeify_equal(self, string, expected, expect_errors=False, options=None)

    @@ -1044,32 +1035,32 @@

    Source code in beancount/tools/treeify_test.py -
    def treeify_equal(self, string, expected, expect_errors=False, options=None):
    -    """Assert an expected treeification result.
    -
    -    Args:
    -      string: An expected input contents string to treeify.
    -      expected: A string, the expected treeified output.
    -      expect_errors: A boolean, true if we should expect there to be errors.
    -      options: An optional list of options for the subprogram.
    -    Returns:
    -      The actual output string. This fails the test if there the expected
    -      output differed from the actual.
    -    """
    -    input_ = textwrap.dedent(string)
    -    output = self.treeify(input_, expect_errors, options)
    -    expected = textwrap.dedent(expected)
    -    if DEBUG:
    -        print('-(input)----------------------------------')
    -        print(input_)
    -        print('-(output)---------------------------------')
    -        print(output)
    -        print('-(expected)-------------------------------')
    -        print(expected)
    -        print('------------------------------------------')
    -    self.assertEqual(expected, output)
    -    return output
    -
    +
    def treeify_equal(self, string, expected, expect_errors=False, options=None):
    +    """Assert an expected treeification result.
    +
    +    Args:
    +      string: An expected input contents string to treeify.
    +      expected: A string, the expected treeified output.
    +      expect_errors: A boolean, true if we should expect there to be errors.
    +      options: An optional list of options for the subprogram.
    +    Returns:
    +      The actual output string. This fails the test if there the expected
    +      output differed from the actual.
    +    """
    +    input_ = textwrap.dedent(string)
    +    output = self.treeify(input_, expect_errors, options)
    +    expected = textwrap.dedent(expected)
    +    if DEBUG:
    +        print("-(input)----------------------------------")
    +        print(input_)
    +        print("-(output)---------------------------------")
    +        print(output)
    +        print("-(expected)-------------------------------")
    +        print(expected)
    +        print("------------------------------------------")
    +    self.assertEqual(expected, output)
    +    return output
    +
    @@ -1093,7 +1084,7 @@

    -beancount.tools.treeify_test.treeify(string, options=None) +beancount.tools.treeify_test.treeify(string, options=None)

    @@ -1137,25 +1128,29 @@

    Source code in beancount/tools/treeify_test.py -
    def treeify(string, options=None):
    -    """Run treeify on the string.
    -
    -    Args:
    -      string: The input string to feed treeify.
    -      options: An optional list of options for the subprogram.
    -    Returns:
    -      The treeified string.
    -    """
    -    pipe = subprocess.Popen([PROGRAM] + (options or []),
    -                            shell=False,
    -                            stdin=subprocess.PIPE,
    -                            stdout=subprocess.PIPE,
    -                            stderr=subprocess.PIPE)
    -    output, errors = pipe.communicate(string.encode('utf-8'))
    -    return (pipe.returncode,
    -            output.decode('utf-8') if output else '',
    -            errors.decode('utf-8') if errors else '')
    -
    +
    def treeify(string, options=None):
    +    """Run treeify on the string.
    +
    +    Args:
    +      string: The input string to feed treeify.
    +      options: An optional list of options for the subprogram.
    +    Returns:
    +      The treeified string.
    +    """
    +    pipe = subprocess.Popen(
    +        [PROGRAM] + (options or []),
    +        shell=False,
    +        stdin=subprocess.PIPE,
    +        stdout=subprocess.PIPE,
    +        stderr=subprocess.PIPE,
    +    )
    +    output, errors = pipe.communicate(string.encode("utf-8"))
    +    return (
    +        pipe.returncode,
    +        output.decode("utf-8") if output else "",
    +        errors.decode("utf-8") if errors else "",
    +    )
    +
    diff --git a/api_reference/beancount.utils.html b/api_reference/beancount.utils.html index 41e3c4a5..772d466d 100644 --- a/api_reference/beancount.utils.html +++ b/api_reference/beancount.utils.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -152,8 +154,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -162,12 +162,6 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools @@ -184,26 +178,6 @@
  • -
  • csv_utils - -
  • date_utils @@ -242,12 +214,6 @@
  • -
  • file_type - -
  • file_utils
  • -
  • net_utils - -
  • pager
  • -
  • regexp_utils - -
  • snoop
    • Snoop @@ -402,8 +356,38 @@
  • +
  • table + +
  • test_utils
  • -
  • text_utils - -
  • -
  • version - -
  • -
  • beancount.web -
  • @@ -576,7 +546,7 @@

    -beancount.utils.bisect_key.bisect_left_with_key(sequence, value, key=None) +beancount.utils.bisect_key.bisect_left_with_key(sequence, value, key=None)

    @@ -622,32 +592,31 @@

    Source code in beancount/utils/bisect_key.py -
    def bisect_left_with_key(sequence, value, key=None):
    -    """Find the last element before the given value in a sorted list.
    -
    -    Args:
    -      sequence: A sorted sequence of elements.
    -      value: The value to search for.
    -      key: An optional function used to extract the value from the elements of
    -        sequence.
    -    Returns:
    -      Return the index. May return None.
    -    """
    -    # pylint: disable=invalid-name
    -    if key is None:
    -        key = lambda x: x  # Identity.
    -
    -    lo = 0
    -    hi = len(sequence)
    -
    -    while lo < hi:
    -        mid = (lo + hi) // 2
    -        if key(sequence[mid]) < value:
    -            lo = mid + 1
    -        else:
    -            hi = mid
    -    return lo
    -
    +
    def bisect_left_with_key(sequence, value, key=None):
    +    """Find the last element before the given value in a sorted list.
    +
    +    Args:
    +      sequence: A sorted sequence of elements.
    +      value: The value to search for.
    +      key: An optional function used to extract the value from the elements of
    +        sequence.
    +    Returns:
    +      Return the index. May return None.
    +    """
    +    if key is None:
    +        key = lambda x: x  # Identity.
    +
    +    lo = 0
    +    hi = len(sequence)
    +
    +    while lo < hi:
    +        mid = (lo + hi) // 2
    +        if key(sequence[mid]) < value:
    +            lo = mid + 1
    +        else:
    +            hi = mid
    +    return lo
    +
    @@ -660,7 +629,7 @@

    -beancount.utils.bisect_key.bisect_right_with_key(a, x, key, lo=0, hi=None) +beancount.utils.bisect_key.bisect_right_with_key(a, x, key, lo=0, hi=None)

    @@ -707,31 +676,31 @@

    Source code in beancount/utils/bisect_key.py -
    def bisect_right_with_key(a, x, key, lo=0, hi=None):
    -    """Like bisect.bisect_right, but with a key lookup parameter.
    -
    -    Args:
    -      a: The list to search in.
    -      x: The element to search for.
    -      key: A function, to extract the value from the list.
    -      lo: The smallest index to search.
    -      hi: The largest index to search.
    -    Returns:
    -      As in bisect.bisect_right, an element from list 'a'.
    -    """
    -    # pylint: disable=invalid-name
    -    if lo < 0:
    -        raise ValueError('lo must be non-negative')
    -    if hi is None:
    -        hi = len(a)
    -    while lo < hi:
    -        mid = (lo+hi)//2
    -        if x < key(a[mid]):
    -            hi = mid
    -        else:
    -            lo = mid+1
    -    return lo
    -
    +
    def bisect_right_with_key(a, x, key, lo=0, hi=None):
    +    """Like bisect.bisect_right, but with a key lookup parameter.
    +
    +    Args:
    +      a: The list to search in.
    +      x: The element to search for.
    +      key: A function, to extract the value from the list.
    +      lo: The smallest index to search.
    +      hi: The largest index to search.
    +    Returns:
    +      As in bisect.bisect_right, an element from list 'a'.
    +    """
    +
    +    if lo < 0:
    +        raise ValueError("lo must be non-negative")
    +    if hi is None:
    +        hi = len(a)
    +    while lo < hi:
    +        mid = (lo + hi) // 2
    +        if x < key(a[mid]):
    +            hi = mid
    +        else:
    +            lo = mid + 1
    +    return lo
    +
    @@ -754,16 +723,16 @@

    - beancount.utils.csv_utils +

    + beancount.utils.date_utils -

    +

    -

    Utilities for reading and writing CSV files.

    +

    Parse the date from various formats.

    @@ -782,15 +751,16 @@

    -

    -beancount.utils.csv_utils.as_rows(string) +

    +beancount.utils.date_utils.intimezone(tz_value) -

    +

    -

    Split a string as rows of a CSV file.

    +

    Temporarily reset the value of TZ.

    +

    This is used for testing.

    @@ -802,7 +772,7 @@

    @@ -818,24 +788,37 @@

    Parameters:
      -
    • string – A string to be split, the contents of a CSV file.

    • +
    • tz_value (str) – The value of TZ to set for the duration of this context.

    Returns:
      -
    • Lists of lists of strings.

    • +
    • A contextmanager in the given timezone locale.

    - Source code in beancount/utils/csv_utils.py -
    def as_rows(string):
    -    """Split a string as rows of a CSV file.
    -
    -    Args:
    -      string: A string to be split, the contents of a CSV file.
    -    Returns:
    -      Lists of lists of strings.
    -    """
    -    return list(csv.reader(io.StringIO(textwrap.dedent(string))))
    -
    + Source code in beancount/utils/date_utils.py +
    @contextlib.contextmanager
    +def intimezone(tz_value: str):
    +    """Temporarily reset the value of TZ.
    +
    +    This is used for testing.
    +
    +    Args:
    +      tz_value: The value of TZ to set for the duration of this context.
    +    Returns:
    +      A contextmanager in the given timezone locale.
    +    """
    +    tz_old = os.environ.get("TZ", None)
    +    os.environ["TZ"] = tz_value
    +    time.tzset()
    +    try:
    +        yield
    +    finally:
    +        if tz_old is None:
    +            del os.environ["TZ"]
    +        else:
    +            os.environ["TZ"] = tz_old
    +        time.tzset()
    +
    @@ -847,16 +830,15 @@

    -

    -beancount.utils.csv_utils.csv_clean_header(header_row) +

    +beancount.utils.date_utils.iter_dates(start_date, end_date) -

    +

    -

    Create a new class for the following rows from the header line. -This both normalizes the header line and assign

    +

    Yield all the dates between 'start_date' and 'end_date'.

    @@ -868,54 +850,32 @@

    -
    Parameters:
      -
    • header_row – A list of strings, the row with header titles.

    • +
    • start_date – An instance of datetime.date.

    • +
    • end_date – An instance of datetime.date.

    - - - - - - - - - - - -
    Returns: -
      -
    • A list of strings, with possibly modified (cleaned) row titles, of the -same lengths.

    • -
    -
    +

    Yields: + Instances of datetime.date.

    +
    - Source code in beancount/utils/csv_utils.py -
    def csv_clean_header(header_row):
    -    """Create a new class for the following rows from the header line.
    -    This both normalizes the header line and assign
    -
    -    Args:
    -      header_row: A list of strings, the row with header titles.
    -    Returns:
    -      A list of strings, with possibly modified (cleaned) row titles, of the
    -      same lengths.
    -    """
    -    fieldnames = []
    -    for index, column in enumerate(header_row):
    -        field = column.lower()
    -        field = re.sub(r'\bp/l\b', 'pnl', field)
    -        field = re.sub('[^a-z0-9]', '_', field)
    -        field = field.strip(' _')
    -        field = re.sub('__+', '_', field)
    -        if not field:
    -            field = 'col{}'.format(index)
    -        assert field not in fieldnames, field
    -        fieldnames.append(field)
    -    return fieldnames
    -
    + Source code in beancount/utils/date_utils.py +
    def iter_dates(start_date, end_date):
    +    """Yield all the dates between 'start_date' and 'end_date'.
    +
    +    Args:
    +      start_date: An instance of datetime.date.
    +      end_date: An instance of datetime.date.
    +    Yields:
    +      Instances of datetime.date.
    +    """
    +    oneday = datetime.timedelta(days=1)
    +    date = start_date
    +    while date < end_date:
    +        yield date
    +        date += oneday
    +
    @@ -927,17 +887,15 @@

    -

    -beancount.utils.csv_utils.csv_dict_reader(fileobj, **kw) +

    +beancount.utils.date_utils.next_month(date) -

    +

    -

    Read a CSV file yielding normalized dictionary fields.

    -

    This is basically an alternative constructor for csv.DictReader that -normalized the field names.

    +

    Compute the date at the beginning of the following month from the given date.

    @@ -949,8 +907,7 @@

    @@ -966,30 +923,30 @@

    Parameters:
      -
    • fileobj – A file object to be read.

    • -
    • **kw – Optional arguments forwarded to csv.DictReader.

    • +
    • date – A datetime.date instance.

    Returns:
      -
    • A csv.DictReader object.

    • +
    • A datetime.date instance, the first day of the month following 'date'.

    - Source code in beancount/utils/csv_utils.py -
    def csv_dict_reader(fileobj, **kw):
    -    """Read a CSV file yielding normalized dictionary fields.
    -
    -    This is basically an alternative constructor for csv.DictReader that
    -    normalized the field names.
    -
    -    Args:
    -      fileobj: A file object to be read.
    -      **kw: Optional arguments forwarded to csv.DictReader.
    -    Returns:
    -      A csv.DictReader object.
    -    """
    -    reader = csv.DictReader(fileobj, **kw)
    -    reader.fieldnames = csv_clean_header(reader.fieldnames)
    -    return reader
    -
    + Source code in beancount/utils/date_utils.py +
    def next_month(date):
    +    """Compute the date at the beginning of the following month from the given date.
    +
    +    Args:
    +      date: A datetime.date instance.
    +    Returns:
    +      A datetime.date instance, the first day of the month following 'date'.
    +    """
    +    # Compute the date at the beginning of the following month.
    +    year = date.year
    +    month = date.month + 1
    +    if date.month == 12:
    +        year += 1
    +        month = 1
    +    return datetime.date(year, month, 1)
    +
    @@ -1001,16 +958,15 @@

    -

    -beancount.utils.csv_utils.csv_split_sections(rows) +

    +beancount.utils.date_utils.render_ofx_date(dtime) -

    +

    -

    Given rows, split them in at empty lines. -This is useful for structured CSV files with multiple sections.

    +

    Render a datetime to the OFX format.

    @@ -1022,7 +978,7 @@

    @@ -1038,35 +994,24 @@

    Parameters:
      -
    • rows – A list of rows, which are themselves lists of strings.

    • +
    • dtime – A datetime.datetime instance.

    Returns:
      -
    • A list of sections, which are lists of rows, which are lists of strings.

    • +
    • A string, rendered to milliseconds.

    - Source code in beancount/utils/csv_utils.py -
    def csv_split_sections(rows):
    -    """Given rows, split them in at empty lines.
    -    This is useful for structured CSV files with multiple sections.
    -
    -    Args:
    -      rows: A list of rows, which are themselves lists of strings.
    -    Returns:
    -      A list of sections, which are lists of rows, which are lists of strings.
    -    """
    -    sections = []
    -    current_section = []
    -    for row in rows:
    -        if row:
    -            current_section.append(row)
    -        else:
    -            sections.append(current_section)
    -            current_section = []
    -    if current_section:
    -        sections.append(current_section)
    -    return sections
    -
    + Source code in beancount/utils/date_utils.py +
    def render_ofx_date(dtime):
    +    """Render a datetime to the OFX format.
    +
    +    Args:
    +      dtime: A datetime.datetime instance.
    +    Returns:
    +      A string, rendered to milliseconds.
    +    """
    +    return "{}.{:03d}".format(dtime.strftime("%Y%m%d%H%M%S"), int(dtime.microsecond / 1000))
    +
    @@ -1074,285 +1019,76 @@

    -
    - - - -

    -beancount.utils.csv_utils.csv_split_sections_with_titles(rows) -

    -
    - -

    Given a list of rows, split their sections. If the sections have single -column titles, consume those lines as their names and return a mapping of -section names.

    -

    This is useful for CSV files with multiple sections, where the separator is -a title. We use this to separate the multiple tables within the CSV files.

    +
    - - - - - - - - - - - -
    Parameters: -
      -
    • rows – A list of rows (list-of-strings).

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A list of lists of rows (list-of-strings).

    • -
    -
    -
    - Source code in beancount/utils/csv_utils.py -
    def csv_split_sections_with_titles(rows):
    -    """Given a list of rows, split their sections. If the sections have single
    -    column titles, consume those lines as their names and return a mapping of
    -    section names.
    -
    -    This is useful for CSV files with multiple sections, where the separator is
    -    a title. We use this to separate the multiple tables within the CSV files.
    -
    -    Args:
    -      rows: A list of rows (list-of-strings).
    -    Returns:
    -     A list of lists of rows (list-of-strings).
    -
    -    """
    -    sections_map = {}
    -    for index, section in enumerate(csv_split_sections(rows)):
    -        # Skip too short sections, cannot possibly be a title.
    -        if len(section) < 2:
    -            continue
    -        if len(section[0]) == 1 and len(section[1]) != 1:
    -            name = section[0][0]
    -            section = section[1:]
    -        else:
    -            name = 'Section {}'.format(index)
    -        sections_map[name] = section
    -    return sections_map
    -
    -
    -
    +
    + +

    + beancount.utils.defdict -

    -beancount.utils.csv_utils.csv_tuple_reader(fileobj, **kw) -

    +

    -

    Read a CSV file yielding namedtuple instances. The CSV file must have a -header line.

    +

    An instance of collections.defaultdict whose factory accepts a key.

    +

    Note: This really ought to be an enhancement to Python itself. I should bother +adding this in eventually.

    - - - - - - - - - - - -
    Parameters: -
      -
    • fileobj – A file object to be read.

    • -
    • **kw – Optional arguments forwarded to csv.DictReader.

    • -
    -

    Yields: - Nametuple instances, one for each row.

    -
    - Source code in beancount/utils/csv_utils.py -
    def csv_tuple_reader(fileobj, **kw):
    -    """Read a CSV file yielding namedtuple instances. The CSV file must have a
    -    header line.
    -
    -    Args:
    -      fileobj: A file object to be read.
    -      **kw: Optional arguments forwarded to csv.DictReader.
    -    Yields:
    -      Nametuple instances, one for each row.
    -    """
    -    reader = csv.reader(fileobj, **kw)
    -    ireader = iter(reader)
    -    fieldnames = csv_clean_header(next(ireader))
    -    Tuple = collections.namedtuple('Row', fieldnames)
    -    for row in ireader:
    -        try:
    -            yield Tuple(*row)
    -        except TypeError:
    -            # If there's an error, it's usually from a line that has a 'END OF
    -            # LINE' marker at the end, or some comment line.
    -            assert len(row) in (0, 1)
    -
    -
    -
    - +
    -
    -

    -beancount.utils.csv_utils.iter_sections(fileobj, separating_predicate=None) -

    -
    -

    For a given file object, yield file-like objects for each of the sections -contained therein. A section is defined as a list of lines that don't match -the predicate. For example, if you want to split by empty lines, provide a -predicate that will be true given an empty line, which will cause a new -section to be begun.

    +
    - - - - - - - - - - - -
    Parameters: -
      -
    • fileobj – A file object to read from.

    • -
    • separating_predicate – A boolean predicate that is true on separating lines.

    • -
    -

    Yields: - A list of lines that you can use to iterate.

    -
    - Source code in beancount/utils/csv_utils.py -
    def iter_sections(fileobj, separating_predicate=None):
    -    """For a given file object, yield file-like objects for each of the sections
    -    contained therein. A section is defined as a list of lines that don't match
    -    the predicate. For example, if you want to split by empty lines, provide a
    -    predicate that will be true given an empty line, which will cause a new
    -    section to be begun.
    -
    -    Args:
    -      fileobj: A file object to read from.
    -      separating_predicate: A boolean predicate that is true on separating lines.
    -    Yields:
    -      A list of lines that you can use to iterate.
    -    """
    -    if separating_predicate is None:
    -        separating_predicate = lambda line: bool(line.strip())
    -
    -    lineiter = iter(fileobj)
    -    for line in lineiter:
    -        if separating_predicate(line):
    -            iterator = itertools.chain((line,),
    -                                       iter_until_empty(lineiter,
    -                                                        separating_predicate))
    -            yield iterator
    -            for _ in iterator:
    -                pass
    -
    -
    -
    -
    +

    + +beancount.utils.defdict.DefaultDictWithKey (defaultdict) + -
    +

    +
    +

    A version of defaultdict whose factory accepts the key as an argument. +Note: collections.defaultdict would be improved by supporting this directly, +this is a common occurrence.

    -

    -beancount.utils.csv_utils.iter_until_empty(iterator, separating_predicate=None) -

    -
    +
    + + -

    An iterator of lines that will stop at the first empty line.

    - - - - - - - - - - - -
    Parameters: -
      -
    • iterator – An iterator of lines.

    • -
    • separating_predicate – A boolean predicate that is true on separating lines.

    • -
    -

    Yields: - Non-empty lines. EOF when we hit an empty line.

    -
    - Source code in beancount/utils/csv_utils.py -
    def iter_until_empty(iterator, separating_predicate=None):
    -    """An iterator of lines that will stop at the first empty line.
    -
    -    Args:
    -      iterator: An iterator of lines.
    -      separating_predicate: A boolean predicate that is true on separating lines.
    -    Yields:
    -      Non-empty lines. EOF when we hit an empty line.
    -    """
    -    if separating_predicate is None:
    -        separating_predicate = lambda line: bool(line.strip())
    -
    -    for line in iterator:
    -        if not separating_predicate(line):
    -            break
    -        yield line
    -
    -
    -
    -
    @@ -1367,20 +1103,26 @@

    -
    +
    -

    - beancount.utils.date_utils +

    + +beancount.utils.defdict.ImmutableDictWithDefault (dict) + -

    +

    -

    Parse the date from various formats.

    +

    An immutable dict which returns a default value for missing keys.

    +

    This differs from a defaultdict in that it does not insert a missing default +value when one is materialized (from a missing fetch), and furthermore, the +set method is make unavailable to prevent mutation beyond construction.

    + @@ -1395,20 +1137,129 @@

    -
    -

    -beancount.utils.date_utils.intimezone(tz_value) +
    -

    -
    +

    +beancount.utils.defdict.ImmutableDictWithDefault.__setitem__(self, key, value) -

    Temporarily reset the value of TZ.

    -

    This is used for testing.

    + + special + + +

    + +
    + +

    Disallow mutating the dict in the usual way.

    + +
    + Source code in beancount/utils/defdict.py +
    def __setitem__(self, key, value):
    +    """Disallow mutating the dict in the usual way."""
    +    raise NotImplementedError
    +
    +
    +
    + +
    + + + + +
    + + + +

    +beancount.utils.defdict.ImmutableDictWithDefault.get(self, key, _=None) + + +

    + +
    + +

    Return the value for key if key is in the dictionary, else default.

    + +
    + Source code in beancount/utils/defdict.py +
    def get(self, key, _=None):
    +    return self.__getitem__(key)
    +
    +
    +
    + +
    + + + + + +
    + +

    + +
    + + + + + + + +
    + +
    + + + + + +
    + + + +

    + beancount.utils.encryption + + + +

    + +
    + +

    Support for encrypted tests.

    + + + +
    + + + + + + + + + + +
    + + + +

    +beancount.utils.encryption.is_encrypted_file(filename) + + +

    + +
    + +

    Return true if the given filename contains an encrypted file.

    @@ -1420,7 +1271,7 @@

    @@ -1436,37 +1287,32 @@

    Parameters:
      -
    • tz_value (str) – The value of TZ to set for the duration of this context.

    • +
    • filename – A path string.

    Returns:
      -
    • A contextmanager in the given timezone locale.

    • +
    • A boolean, true if the file contains an encrypted file.

    - Source code in beancount/utils/date_utils.py -
    @contextlib.contextmanager
    -def intimezone(tz_value: str):
    -    """Temporarily reset the value of TZ.
    -
    -    This is used for testing.
    -
    -    Args:
    -      tz_value: The value of TZ to set for the duration of this context.
    -    Returns:
    -      A contextmanager in the given timezone locale.
    -    """
    -    tz_old = os.environ.get('TZ', None)
    -    os.environ['TZ'] = tz_value
    -    time.tzset()
    -    try:
    -        yield
    -    finally:
    -        if tz_old is None:
    -            del os.environ['TZ']
    -        else:
    -            os.environ['TZ'] = tz_old
    -        time.tzset()
    -
    + Source code in beancount/utils/encryption.py +
    def is_encrypted_file(filename):
    +    """Return true if the given filename contains an encrypted file.
    +
    +    Args:
    +      filename: A path string.
    +    Returns:
    +      A boolean, true if the file contains an encrypted file.
    +    """
    +    _, ext = path.splitext(filename)
    +    if ext == ".gpg":
    +        return True
    +    if ext == ".asc":
    +        with open(filename) as encfile:
    +            head = encfile.read(1024)
    +            if re.search("--BEGIN PGP MESSAGE--", head):
    +                return True
    +    return False
    +
    @@ -1478,52 +1324,30 @@

    -

    -beancount.utils.date_utils.iter_dates(start_date, end_date) +

    +beancount.utils.encryption.is_gpg_installed() -

    +

    -

    Yield all the dates between 'start_date' and 'end_date'.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • start_date – An instance of datetime.date.

    • -
    • end_date – An instance of datetime.date.

    • -
    -

    Yields: - Instances of datetime.date.

    +

    Return true if GPG 1.4.x or 2.x are installed, which is what we use and support.

    - Source code in beancount/utils/date_utils.py -
    def iter_dates(start_date, end_date):
    -    """Yield all the dates between 'start_date' and 'end_date'.
    -
    -    Args:
    -      start_date: An instance of datetime.date.
    -      end_date: An instance of datetime.date.
    -    Yields:
    -      Instances of datetime.date.
    -    """
    -    oneday = datetime.timedelta(days=1)
    -    date = start_date
    -    while date < end_date:
    -        yield date
    -        date += oneday
    -
    + Source code in beancount/utils/encryption.py +
    def is_gpg_installed():
    +    """Return true if GPG 1.4.x or 2.x are installed, which is what we use and support."""
    +    try:
    +        pipe = subprocess.Popen(
    +            ["gpg", "--version"], shell=0, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    +        )
    +        out, err = pipe.communicate()
    +        version_text = out.decode("utf8")
    +        return pipe.returncode == 0 and re.match(r"gpg \(GnuPG\) (1\.4|2)\.", version_text)
    +    except OSError:
    +        return False
    +
    @@ -1535,15 +1359,15 @@

    -

    -beancount.utils.date_utils.next_month(date) +

    +beancount.utils.encryption.read_encrypted_file(filename) -

    +
    -

    Compute the date at the beginning of the following month from the given date.

    +

    Decrypt and read an encrypted file without temporary storage.

    @@ -1555,7 +1379,7 @@

    @@ -1571,30 +1395,51 @@

    +
    Parameters:
      -
    • date – A datetime.date instance.

    • +
    • filename – A string, the path to the encrypted file.

    Returns:
      -
    • A datetime.date instance, the first day of the month following 'date'.

    • +
    • A string, the contents of the file.

    + + + + + + + + + + +
    Exceptions: +
      +
    • OSError – If we could not properly decrypt the file.

    • +
    +
    - Source code in beancount/utils/date_utils.py -
    def next_month(date):
    -    """Compute the date at the beginning of the following month from the given date.
    -
    -    Args:
    -      date: A datetime.date instance.
    -    Returns:
    -      A datetime.date instance, the first day of the month following 'date'.
    -    """
    -    # Compute the date at the beginning of the following month.
    -    year = date.year
    -    month = date.month + 1
    -    if date.month == 12:
    -        year += 1
    -        month = 1
    -    return datetime.date(year, month, 1)
    -
    + Source code in beancount/utils/encryption.py +
    def read_encrypted_file(filename):
    +    """Decrypt and read an encrypted file without temporary storage.
    +
    +    Args:
    +      filename: A string, the path to the encrypted file.
    +    Returns:
    +      A string, the contents of the file.
    +    Raises:
    +      OSError: If we could not properly decrypt the file.
    +    """
    +    command = ["gpg", "--batch", "--decrypt", path.realpath(filename)]
    +    pipe = subprocess.Popen(
    +        command, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE
    +    )
    +    contents, errors = pipe.communicate()
    +    if pipe.returncode != 0:
    +        raise OSError(
    +            "Could not decrypt file ({}): {}".format(pipe.returncode, errors.decode("utf8"))
    +        )
    +    return contents.decode("utf-8")
    +
    @@ -1602,21 +1447,58 @@

    + + + + + + + + + + + +
    + + + +

    + beancount.utils.file_utils + + + +

    + +
    + +

    File utilities.

    + + + +
    + + + + + + + + + +
    -

    -beancount.utils.date_utils.parse_date_liberally(string, parse_kwargs_dict=None) +

    +beancount.utils.file_utils.chdir(directory) -

    +

    -

    Parse arbitrary strings to dates.

    -

    This function is intended to support liberal inputs, so that we can use it -in accepting user-specified dates on command-line scripts.

    +

    Temporarily chdir to the given directory.

    @@ -1628,8 +1510,7 @@

    Parameters:

    @@ -1645,31 +1526,30 @@

    Returns:

      -
    • string – A string to parse.

    • -
    • parse_kwargs_dict – Dict of kwargs to pass to dateutil parser.

    • +
    • directory – The directory to switch do.

      -
    • A datetime.date object.

    • +
    • A context manager which restores the cwd after running.

    - Source code in beancount/utils/date_utils.py -
    def parse_date_liberally(string, parse_kwargs_dict=None):
    -    """Parse arbitrary strings to dates.
    -
    -    This function is intended to support liberal inputs, so that we can use it
    -    in accepting user-specified dates on command-line scripts.
    -
    -    Args:
    -      string: A string to parse.
    -      parse_kwargs_dict: Dict of kwargs to pass to dateutil parser.
    -    Returns:
    -      A datetime.date object.
    -    """
    -    # At the moment, rely on the most excellent dateutil.
    -    if parse_kwargs_dict is None:
    -        parse_kwargs_dict = {}
    -    return dateutil.parser.parse(string, **parse_kwargs_dict).date()
    -
    + Source code in beancount/utils/file_utils.py +
    @contextlib.contextmanager
    +def chdir(directory):
    +    """Temporarily chdir to the given directory.
    +
    +    Args:
    +      directory: The directory to switch do.
    +    Returns:
    +      A context manager which restores the cwd after running.
    +    """
    +    cwd = os.getcwd()
    +    os.chdir(directory)
    +    try:
    +        yield cwd
    +    finally:
    +        os.chdir(cwd)
    +
    @@ -1681,15 +1561,16 @@

    -beancount.utils.date_utils.render_ofx_date(dtime) +

    +beancount.utils.file_utils.find_files(fords, ignore_dirs=('.hg', '.svn', '.git'), ignore_files=('.DS_Store',)) -

    +
    -

    Render a datetime to the OFX format.

    +

    Enumerate the files under the given directories, stably.

    +

    Invalid file or directory names will be logged to the error log.

    @@ -1701,41 +1582,44 @@

    -
    Parameters:
      -
    • dtime – A datetime.datetime instance.

    • +
    • fords – A list of strings, file or directory names.

    • +
    • ignore_dirs – A list of strings, filenames or directories to be ignored.

    - - - - - - - - - - - -
    Returns: -
      -
    • A string, rendered to milliseconds.

    • -
    -
    +

    Yields: + Strings, full filenames from the given roots.

    +
    - Source code in beancount/utils/date_utils.py -
    def render_ofx_date(dtime):
    -    """Render a datetime to the OFX format.
    -
    -    Args:
    -      dtime: A datetime.datetime instance.
    -    Returns:
    -      A string, rendered to milliseconds.
    -    """
    -    return '{}.{:03d}'.format(dtime.strftime('%Y%m%d%H%M%S'),
    -                              int(dtime.microsecond / 1000))
    -
    + Source code in beancount/utils/file_utils.py +
    def find_files(fords, ignore_dirs=(".hg", ".svn", ".git"), ignore_files=(".DS_Store",)):
    +    """Enumerate the files under the given directories, stably.
    +
    +    Invalid file or directory names will be logged to the error log.
    +
    +    Args:
    +      fords: A list of strings, file or directory names.
    +      ignore_dirs: A list of strings, filenames or directories to be ignored.
    +    Yields:
    +      Strings, full filenames from the given roots.
    +    """
    +    if isinstance(fords, str):
    +        fords = [fords]
    +    assert isinstance(fords, (list, tuple))
    +    for ford in fords:
    +        if path.isdir(ford):
    +            for root, dirs, filenames in os.walk(ford):
    +                dirs[:] = sorted(dirname for dirname in dirs if dirname not in ignore_dirs)
    +                for filename in sorted(filenames):
    +                    if filename in ignore_files:
    +                        continue
    +                    yield path.join(root, filename)
    +        elif path.isfile(ford) or path.islink(ford):
    +            yield ford
    +        elif not path.exists(ford):
    +            logging.error("File or directory '{}' does not exist.".format(ford))
    +
    @@ -1743,247 +1627,19 @@

    +
    -
    - - +

    +beancount.utils.file_utils.guess_file_format(filename, default=None) - +

    +
    -
    - - - -

    - beancount.utils.defdict - - - -

    - -
    - -

    An instance of collections.defaultdict whose factory accepts a key.

    -

    Note: This really ought to be an enhancement to Python itself. I should bother -adding this in eventually.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.utils.defdict.DefaultDictWithKey (defaultdict) - - - - -

    - -
    - -

    A version of defaultdict whose factory accepts the key as an argument. -Note: collections.defaultdict would be improved by supporting this directly, -this is a common occurrence.

    - - - - -
    - - - - - - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.utils.defdict.ImmutableDictWithDefault (dict) - - - - -

    - -
    - -

    An immutable dict which returns a default value for missing keys.

    -

    This differs from a defaultdict in that it does not insert a missing default -value when one is materialized (from a missing fetch), and furthermore, the -set method is make unavailable to prevent mutation beyond construction.

    - - - - -
    - - - - - - - - - - - - - -
    - - - -

    -beancount.utils.defdict.ImmutableDictWithDefault.__setitem__(self, key, value) - - - special - - -

    - -
    - -

    Disallow mutating the dict in the usual way.

    - -
    - Source code in beancount/utils/defdict.py -
    def __setitem__(self, key, value):
    -    """Disallow mutating the dict in the usual way."""
    -    raise NotImplementedError
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.utils.defdict.ImmutableDictWithDefault.get(self, key, _=None) - - -

    - -
    - -

    Return the value for key if key is in the dictionary, else default.

    - -
    - Source code in beancount/utils/defdict.py -
    def get(self, key, _=None):
    -    return self.__getitem__(key)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.utils.encryption - - - -

    - -
    - -

    Support for encrypted tests.

    - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.utils.encryption.is_encrypted_file(filename) - - -

    - -
    - -

    Return true if the given filename contains an encrypted file.

    +

    Guess the file format from the filename.

    @@ -1995,7 +1651,7 @@

    @@ -2011,66 +1667,35 @@

    Parameters:
      -
    • filename – A path string.

    • +
    • filename – A string, the name of the file. This can be None.

    Returns:
      -
    • A boolean, true if the file contains an encrypted file.

    • +
    • A string, the extension of the format, without a leading period.

    - Source code in beancount/utils/encryption.py -
    def is_encrypted_file(filename):
    -    """Return true if the given filename contains an encrypted file.
    -
    -    Args:
    -      filename: A path string.
    -    Returns:
    -      A boolean, true if the file contains an encrypted file.
    -    """
    -    _, ext = path.splitext(filename)
    -    if ext == '.gpg':
    -        return True
    -    if ext == '.asc':
    -        with open(filename) as encfile:
    -            head = encfile.read(1024)
    -            if re.search('--BEGIN PGP MESSAGE--', head):
    -                return True
    -    return False
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.utils.encryption.is_gpg_installed() - - -

    - -
    - -

    Return true if GPG 1.4.x or 2.x are installed, which is what we use and support.

    - -
    - Source code in beancount/utils/encryption.py -
    def is_gpg_installed():
    -    """Return true if GPG 1.4.x or 2.x are installed, which is what we use and support."""
    -    try:
    -        pipe = subprocess.Popen(['gpg', '--version'], shell=0,
    -                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    -        out, err = pipe.communicate()
    -        version_text = out.decode('utf8')
    -        return pipe.returncode == 0 and re.match(r'gpg \(GnuPG\) (1\.4|2)\.', version_text)
    -    except OSError:
    -        return False
    -
    + Source code in beancount/utils/file_utils.py +
    def guess_file_format(filename, default=None):
    +    """Guess the file format from the filename.
    +
    +    Args:
    +      filename: A string, the name of the file. This can be None.
    +    Returns:
    +      A string, the extension of the format, without a leading period.
    +    """
    +    if filename:
    +        if filename.endswith(".txt") or filename.endswith(".text"):
    +            format = "text"
    +        elif filename.endswith(".csv"):
    +            format = "csv"
    +        elif filename.endswith(".html") or filename.endswith(".xhtml"):
    +            format = "html"
    +        else:
    +            format = default
    +    else:
    +        format = default
    +    return format
    +
    @@ -2082,15 +1707,15 @@

    -

    -beancount.utils.encryption.read_encrypted_file(filename) +

    +beancount.utils.file_utils.path_greedy_split(filename) -

    +
    -

    Decrypt and read an encrypted file without temporary storage.

    +

    Split a path, returning the longest possible extension.

    @@ -2102,7 +1727,7 @@

    @@ -2118,12 +1743,52 @@

    Parameters:
      -
    • filename – A string, the path to the encrypted file.

    • +
    • filename – A string, the filename to split.

    Returns:
      -
    • A string, the contents of the file.

    • +
    • A pair of basename, extension (which includes the leading period).

    +
    + Source code in beancount/utils/file_utils.py +
    def path_greedy_split(filename):
    +    """Split a path, returning the longest possible extension.
    +
    +    Args:
    +      filename: A string, the filename to split.
    +    Returns:
    +      A pair of basename, extension (which includes the leading period).
    +    """
    +    basename = path.basename(filename)
    +    index = basename.find(".")
    +    if index == -1:
    +        extension = None
    +    else:
    +        extension = basename[index:]
    +        basename = basename[:index]
    +    return (path.join(path.dirname(filename), basename), extension)
    +
    +
    +
    + +
    + + + +
    + + + +

    +beancount.utils.file_utils.touch_file(filename, *otherfiles) + + +

    + +
    + +

    Touch a file and wait until its timestamp has been changed.

    + @@ -2131,38 +1796,40 @@

    - +
    Exceptions:Parameters:
      -
    • OSError – If we could not properly decrypt the file.

    • +
    • filename – A string path, the name of the file to touch.

    • +
    • otherfiles – A list of other files to ensure the timestamp is beyond of.

    - Source code in beancount/utils/encryption.py -
    def read_encrypted_file(filename):
    -    """Decrypt and read an encrypted file without temporary storage.
    -
    -    Args:
    -      filename: A string, the path to the encrypted file.
    -    Returns:
    -      A string, the contents of the file.
    -    Raises:
    -      OSError: If we could not properly decrypt the file.
    -    """
    -    command = ['gpg', '--batch', '--decrypt', path.realpath(filename)]
    -    pipe = subprocess.Popen(command,
    -                            shell=False,
    -                            stdout=subprocess.PIPE,
    -                            stderr=subprocess.PIPE)
    -    contents, errors = pipe.communicate()
    -    if pipe.returncode != 0:
    -        raise OSError("Could not decrypt file ({}): {}".format(pipe.returncode,
    -                                                               errors.decode('utf8')))
    -    return contents.decode('utf-8')
    -
    + Source code in beancount/utils/file_utils.py +
    def touch_file(filename, *otherfiles):
    +    """Touch a file and wait until its timestamp has been changed.
    +
    +    Args:
    +      filename: A string path, the name of the file to touch.
    +      otherfiles: A list of other files to ensure the timestamp is beyond of.
    +    """
    +    # Note: You could set os.stat_float_times() but then the main function would
    +    # have to set that up as well. It doesn't help so much, however, since
    +    # filesystems tend to have low resolutions, e.g. one second.
    +    orig_mtime_ns = max(
    +        os.stat(minfile).st_mtime_ns for minfile in (filename,) + otherfiles
    +    )
    +    delay_secs = 0.05
    +    while True:
    +        with open(filename, "a"):
    +            os.utime(filename)
    +        time.sleep(delay_secs)
    +        new_stat = os.stat(filename)
    +        if new_stat.st_mtime_ns > orig_mtime_ns:
    +            break
    +
    @@ -2185,23 +1852,16 @@

    -

    - beancount.utils.file_type +

    + beancount.utils.import_utils -

    +
    -

    Code that can guess a MIME type for a filename.

    -

    This attempts to identify the mime-type of a file suing

    -
      -
    1. The built-in mimetypes library, then
    2. -
    3. python-magic (if available), and finally
    4. -
    5. some custom mappers for datatypes used in financial downloads, such as - Quicken files.
    6. -
    +

    Utilities for importing symbols programmatically.

    @@ -2216,21 +1876,19 @@

    - -
    -

    -beancount.utils.file_type.guess_file_type(filename) +

    +beancount.utils.import_utils.import_symbol(dotted_name) -

    +

    -

    Attempt to guess the type of the input file.

    +

    Import a symbol in an arbitrary module.

    @@ -2242,7 +1900,7 @@

    @@ -2258,43 +1916,48 @@

    +
    Parameters:
      -
    • filename – A string, the name of the file to guess the type for.

    • +
    • dotted_name – A dotted path to a symbol.

    Returns:
      -
    • A suitable mimetype string, or None if we could not guess.

    • +
    • The object referenced by the given name.

    + + + + + + + + + + +
    Exceptions: +
      +
    • ImportError – If the module not not be imported.

    • +
    • AttributeError – If the symbol could not be found in the module.

    • +
    +
    - Source code in beancount/utils/file_type.py -
    def guess_file_type(filename):
    -    """Attempt to guess the type of the input file.
    -
    -    Args:
    -      filename: A string, the name of the file to guess the type for.
    -    Returns:
    -      A suitable mimetype string, or None if we could not guess.
    -    """
    -
    -    # Try the standard mimetypes association.
    -    filetype, _ = mimetypes.guess_type(filename, False)
    -    if filetype:
    -        return filetype
    -
    -    # Try out some extra types that only we know about.
    -    for regexp, mimetype in EXTRA_FILE_TYPES:
    -        if regexp.match(filename):
    -            return mimetype
    -
    -    # Try out libmagic, if it is installed.
    -    if magic:
    -        filetype = magic.from_file(filename, mime=True)
    -        if isinstance(filetype, bytes):
    -            filetype = filetype.decode('utf8')
    -        return filetype
    -    else:
    -        raise ValueError(("Could not identify the type of file '{}'; "
    -                          "try installing python-magic").format(filename))
    -
    + Source code in beancount/utils/import_utils.py +
    def import_symbol(dotted_name):
    +    """Import a symbol in an arbitrary module.
    +
    +    Args:
    +      dotted_name: A dotted path to a symbol.
    +    Returns:
    +      The object referenced by the given name.
    +    Raises:
    +      ImportError: If the module not not be imported.
    +      AttributeError: If the symbol could not be found in the module.
    +    """
    +    comps = dotted_name.split(".")
    +    module_name = ".".join(comps[:-1])
    +    symbol_name = comps[-1]
    +    module = importlib.import_module(module_name)
    +    return getattr(module, symbol_name)
    +
    @@ -2317,16 +1980,28 @@

    -

    - beancount.utils.file_utils +

    + beancount.utils.invariants -

    +
    -

    File utilities.

    +

    Functions to register auxiliary functions on a class' methods to check for invariants.

    +

    This is intended to be used in a test, whereby your test will setup a class to +automatically run invariant verification functions before and after each +function call, to ensure some extra sanity checks that wouldn't be used in +non-tests.

    +

    Example: Instrument the Inventory class with the check_inventory_invariants() +function.

    +

    def setUp(module): + instrument_invariants(Inventory, + check_inventory_invariants, + check_inventory_invariants)

    +

    def tearDown(module): + uninstrument_invariants(Inventory)

    @@ -2345,158 +2020,16 @@

    -

    -beancount.utils.file_utils.chdir(directory) - - -

    - -
    - -

    Temporarily chdir to the given directory.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • directory – The directory to switch do.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A context manager which restores the cwd after running.

    • -
    -
    -
    - Source code in beancount/utils/file_utils.py -
    @contextlib.contextmanager
    -def chdir(directory):
    -    """Temporarily chdir to the given directory.
    -
    -    Args:
    -      directory: The directory to switch do.
    -    Returns:
    -      A context manager which restores the cwd after running.
    -    """
    -    cwd = os.getcwd()
    -    os.chdir(directory)
    -    try:
    -        yield cwd
    -    finally:
    -        os.chdir(cwd)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.utils.file_utils.find_files(fords, ignore_dirs=('.hg', '.svn', '.git'), ignore_files=('.DS_Store',)) - - -

    - -
    - -

    Enumerate the files under the given directories, stably.

    -

    Invalid file or directory names will be logged to the error log.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • fords – A list of strings, file or directory names.

    • -
    • ignore_dirs – A list of strings, filenames or directories to be ignored.

    • -
    -

    Yields: - Strings, full filenames from the given roots.

    - -
    - Source code in beancount/utils/file_utils.py -
    def find_files(fords,
    -               ignore_dirs=('.hg', '.svn', '.git'),
    -               ignore_files=('.DS_Store',)):
    -    """Enumerate the files under the given directories, stably.
    -
    -    Invalid file or directory names will be logged to the error log.
    -
    -    Args:
    -      fords: A list of strings, file or directory names.
    -      ignore_dirs: A list of strings, filenames or directories to be ignored.
    -    Yields:
    -      Strings, full filenames from the given roots.
    -    """
    -    if isinstance(fords, str):
    -        fords = [fords]
    -    assert isinstance(fords, (list, tuple))
    -    for ford in fords:
    -        if path.isdir(ford):
    -            for root, dirs, filenames in os.walk(ford):
    -                dirs[:] = sorted(dirname for dirname in dirs if dirname not in ignore_dirs)
    -                for filename in sorted(filenames):
    -                    if filename in ignore_files:
    -                        continue
    -                    yield path.join(root, filename)
    -        elif path.isfile(ford) or path.islink(ford):
    -            yield ford
    -        elif not path.exists(ford):
    -            logging.error("File or directory '{}' does not exist.".format(ford))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.utils.file_utils.guess_file_format(filename, default=None) +

    +beancount.utils.invariants.instrument_invariants(klass, prefun, postfun) -

    +
    -

    Guess the file format from the filename.

    +

    Instrument the class 'klass' with pre/post invariant +checker functions.

    @@ -2508,51 +2041,35 @@

    -
    Parameters:
      -
    • filename – A string, the name of the file. This can be None.

    • +
    • klass – A class object, whose methods to be instrumented.

    • +
    • prefun – A function that checks invariants pre-call.

    • +
    • postfun – A function that checks invariants pre-call.

    - - - - - - - - - - -
    Returns: -
      -
    • A string, the extension of the format, without a leading period.

    • -
    -
    - Source code in beancount/utils/file_utils.py -
    def guess_file_format(filename, default=None):
    -    """Guess the file format from the filename.
    -
    -    Args:
    -      filename: A string, the name of the file. This can be None.
    -    Returns:
    -      A string, the extension of the format, without a leading period.
    -    """
    -    if filename:
    -        if filename.endswith('.txt') or filename.endswith('.text'):
    -            format = 'text'
    -        elif filename.endswith('.csv'):
    -            format = 'csv'
    -        elif filename.endswith('.html') or filename.endswith('.xhtml'):
    -            format = 'html'
    -        else:
    -            format = default
    -    else:
    -        format = default
    -    return format
    -
    + Source code in beancount/utils/invariants.py +
    def instrument_invariants(klass, prefun, postfun):
    +    """Instrument the class 'klass' with pre/post invariant
    +    checker functions.
    +
    +    Args:
    +      klass: A class object, whose methods to be instrumented.
    +      prefun: A function that checks invariants pre-call.
    +      postfun: A function that checks invariants pre-call.
    +    """
    +    instrumented = {}
    +    for attrname, object_ in klass.__dict__.items():
    +        if attrname.startswith("_"):
    +            continue
    +        if not isinstance(object_, types.FunctionType):
    +            continue
    +        instrumented[attrname] = object_
    +        setattr(klass, attrname, invariant_check(object_, prefun, postfun))
    +    klass.__instrumented = instrumented
    +
    @@ -2564,15 +2081,15 @@

    -

    -beancount.utils.file_utils.path_greedy_split(filename) +

    +beancount.utils.invariants.invariant_check(method, prefun, postfun) -

    +
    -

    Split a path, returning the longest possible extension.

    +

    Decorate a method with the pre/post invariant checkers.

    @@ -2584,7 +2101,9 @@

    @@ -2600,31 +2119,38 @@

    Parameters:
      -
    • filename – A string, the filename to split.

    • +
    • method – An unbound method to instrument.

    • +
    • prefun – A function that checks invariants pre-call.

    • +
    • postfun – A function that checks invariants post-call.

    Returns:
      -
    • A pair of basename, extension (which includes the leading period).

    • +
    • An unbound method, decorated.

    - Source code in beancount/utils/file_utils.py -
    def path_greedy_split(filename):
    -    """Split a path, returning the longest possible extension.
    -
    -    Args:
    -      filename: A string, the filename to split.
    -    Returns:
    -      A pair of basename, extension (which includes the leading period).
    -    """
    -    basename = path.basename(filename)
    -    index = basename.find('.')
    -    if index == -1:
    -        extension = None
    -    else:
    -        extension = basename[index:]
    -        basename = basename[:index]
    -    return (path.join(path.dirname(filename), basename), extension)
    -
    + Source code in beancount/utils/invariants.py +
    def invariant_check(method, prefun, postfun):
    +    """Decorate a method with the pre/post invariant checkers.
    +
    +    Args:
    +      method: An unbound method to instrument.
    +      prefun: A function that checks invariants pre-call.
    +      postfun: A function that checks invariants post-call.
    +    Returns:
    +      An unbound method, decorated.
    +    """
    +    reentrant = []
    +
    +    def new_method(self, *args, **kw):
    +        reentrant.append(None)
    +        if len(reentrant) == 1:
    +            prefun(self)
    +        result = method(self, *args, **kw)
    +        if len(reentrant) == 1:
    +            postfun(self)
    +        reentrant.pop()
    +        return result
    +
    +    return new_method
    +
    @@ -2636,15 +2162,15 @@

    -

    -beancount.utils.file_utils.touch_file(filename, *otherfiles) +

    +beancount.utils.invariants.uninstrument_invariants(klass) -

    +
    -

    Touch a file and wait until its timestamp has been changed.

    +

    Undo the instrumentation for invariants.

    @@ -2656,36 +2182,26 @@

    Parameters:
      -
    • filename – A string path, the name of the file to touch.

    • -
    • otherfiles – A list of other files to ensure the timestamp is beyond of.

    • +
    • klass – A class object, whose methods to be uninstrumented.

    - Source code in beancount/utils/file_utils.py -
    def touch_file(filename, *otherfiles):
    -    """Touch a file and wait until its timestamp has been changed.
    -
    -    Args:
    -      filename: A string path, the name of the file to touch.
    -      otherfiles: A list of other files to ensure the timestamp is beyond of.
    -    """
    -    # Note: You could set os.stat_float_times() but then the main function would
    -    # have to set that up as well. It doesn't help so much, however, since
    -    # filesystems tend to have low resolutions, e.g. one second.
    -    orig_mtime_ns = max(os.stat(minfile).st_mtime_ns
    -                        for minfile in (filename,) + otherfiles)
    -    delay_secs = 0.05
    -    while True:
    -        with open(filename, 'a'):
    -            os.utime(filename)
    -        time.sleep(delay_secs)
    -        new_stat = os.stat(filename)
    -        if new_stat.st_mtime_ns > orig_mtime_ns:
    -            break
    -
    + Source code in beancount/utils/invariants.py +
    def uninstrument_invariants(klass):
    +    """Undo the instrumentation for invariants.
    +
    +    Args:
    +      klass: A class object, whose methods to be uninstrumented.
    +    """
    +    instrumented = getattr(klass, "__instrumented", None)
    +    if instrumented:
    +        for attrname, object_ in instrumented.items():
    +            setattr(klass, attrname, object_)
    +    del klass.__instrumented
    +
    @@ -2708,16 +2224,16 @@

    -

    - beancount.utils.import_utils +

    + beancount.utils.memo -

    +
    -

    Utilities for importing symbols programmatically.

    +

    Memoization utilities.

    @@ -2736,15 +2252,16 @@

    -

    -beancount.utils.import_utils.import_symbol(dotted_name) +

    +beancount.utils.memo.memoize_recent_fileobj(function, cache_filename, expiration=None) -

    +
    -

    Import a symbol in an arbitrary module.

    +

    Memoize recent calls to the given function which returns a file object.

    +

    The results of the cache expire after some time.

    @@ -2756,7 +2273,10 @@

    @@ -2772,48 +2292,85 @@

    -
    Parameters:
      -
    • dotted_name – A dotted path to a symbol.

    • +
    • function – A callable object.

    • +
    • cache_filename – A string, the path to the database file to cache to.

    • +
    • expiration – The time during which the results will be kept valid. Use +'None' to never expire the cache (this is the default).

    Returns:
      -
    • The object referenced by the given name.

    • +
    • A memoized version of the function.

    - - - - - - - - - - -
    Exceptions: -
      -
    • ImportError – If the module not not be imported.

    • -
    • AttributeError – If the symbol could not be found in the module.

    • -
    -
    - Source code in beancount/utils/import_utils.py -
    def import_symbol(dotted_name):
    -    """Import a symbol in an arbitrary module.
    -
    -    Args:
    -      dotted_name: A dotted path to a symbol.
    -    Returns:
    -      The object referenced by the given name.
    -    Raises:
    -      ImportError: If the module not not be imported.
    -      AttributeError: If the symbol could not be found in the module.
    -    """
    -    comps = dotted_name.split('.')
    -    module_name = '.'.join(comps[:-1])
    -    symbol_name = comps[-1]
    -    module = importlib.import_module(module_name)
    -    return getattr(module, symbol_name)
    -
    + Source code in beancount/utils/memo.py +
    def memoize_recent_fileobj(function, cache_filename, expiration=None):
    +    """Memoize recent calls to the given function which returns a file object.
    +
    +    The results of the cache expire after some time.
    +
    +    Args:
    +      function: A callable object.
    +      cache_filename: A string, the path to the database file to cache to.
    +      expiration: The time during which the results will be kept valid. Use
    +        'None' to never expire the cache (this is the default).
    +    Returns:
    +      A memoized version of the function.
    +    """
    +    urlcache = shelve.open(cache_filename, "c")
    +    urlcache.lock = threading.Lock()  # Note: 'shelve' is not thread-safe.
    +
    +    @functools.wraps(function)
    +    def memoized(*args, **kw):
    +        # Encode the arguments, including a date string in order to invalidate
    +        # results over some time.
    +        md5 = hashlib.md5()
    +        md5.update(str(args).encode("utf-8"))
    +        md5.update(str(sorted(kw.items())).encode("utf-8"))
    +
    +        hash_ = md5.hexdigest()
    +        time_now = now()
    +        try:
    +            with urlcache.lock:
    +                time_orig, contents = urlcache[hash_]
    +            if expiration is not None and (time_now - time_orig) > expiration:
    +                raise KeyError
    +        except KeyError:
    +            fileobj = function(*args, **kw)
    +            if fileobj:
    +                contents = fileobj.read()
    +                with urlcache.lock:
    +                    urlcache[hash_] = (time_now, contents)
    +            else:
    +                contents = None
    +
    +        return io.BytesIO(contents) if contents else None
    +
    +    return memoized
    +
    +
    +
    + +
    + + + +
    + + + +

    +beancount.utils.memo.now() + + +

    + +
    + +

    Indirection on datetime.datetime.now() for testing.

    + +
    + Source code in beancount/utils/memo.py +
    def now():
    +    "Indirection on datetime.datetime.now() for testing."
    +    return datetime.datetime.now()
    +
    @@ -2836,28 +2393,16 @@

    -

    - beancount.utils.invariants +

    + beancount.utils.misc_utils -

    +
    -

    Functions to register auxiliary functions on a class' methods to check for invariants.

    -

    This is intended to be used in a test, whereby your test will setup a class to -automatically run invariant verification functions before and after each -function call, to ensure some extra sanity checks that wouldn't be used in -non-tests.

    -

    Example: Instrument the Inventory class with the check_inventory_invariants() -function.

    -

    def setUp(module): - instrument_invariants(Inventory, - check_inventory_invariants, - check_inventory_invariants)

    -

    def tearDown(module): - uninstrument_invariants(Inventory)

    +

    Generic utility packages and functions.

    @@ -2872,20 +2417,53 @@

    -
    +
    -

    -beancount.utils.invariants.instrument_invariants(klass, prefun, postfun) +

    + +beancount.utils.misc_utils.LineFileProxy -

    + +

    -

    Instrument the class 'klass' with pre/post invariant -checker functions.

    +

    A file object that will delegate writing full lines to another logging function. +This may be used for writing data to a logging level without having to worry about +lines.

    + + + + +
    + + + + + + + + + +
    + + + +

    +beancount.utils.misc_utils.LineFileProxy.__init__(self, line_writer, prefix=None, write_newlines=False) + + + special + + +

    + +
    + +

    Construct a new line delegator file object proxy.

    @@ -2897,36 +2475,29 @@

    Parameters:

      -
    • klass – A class object, whose methods to be instrumented.

    • -
    • prefun – A function that checks invariants pre-call.

    • -
    • postfun – A function that checks invariants pre-call.

    • +
    • line_writer – A callable function, used to write to the delegated output.

    • +
    • prefix – An optional string, the prefix to insert before every line.

    • +
    • write_newlines – A boolean, true if we should output the newline characters.

    - Source code in beancount/utils/invariants.py -
    def instrument_invariants(klass, prefun, postfun):
    -    """Instrument the class 'klass' with pre/post invariant
    -    checker functions.
    -
    -    Args:
    -      klass: A class object, whose methods to be instrumented.
    -      prefun: A function that checks invariants pre-call.
    -      postfun: A function that checks invariants pre-call.
    -    """
    -    instrumented = {}
    -    for attrname, object_ in klass.__dict__.items():
    -        if attrname.startswith('_'):
    -            continue
    -        if not isinstance(object_, types.FunctionType):
    -            continue
    -        instrumented[attrname] = object_
    -        setattr(klass, attrname,
    -                invariant_check(object_, prefun, postfun))
    -    klass.__instrumented = instrumented
    -
    + Source code in beancount/utils/misc_utils.py +
    def __init__(self, line_writer, prefix=None, write_newlines=False):
    +    """Construct a new line delegator file object proxy.
    +
    +    Args:
    +      line_writer: A callable function, used to write to the delegated output.
    +      prefix: An optional string, the prefix to insert before every line.
    +      write_newlines: A boolean, true if we should output the newline characters.
    +    """
    +    self.line_writer = line_writer
    +    self.prefix = prefix
    +    self.write_newlines = write_newlines
    +    self.data = []
    +
    @@ -2934,78 +2505,26 @@

    +
    -

    -beancount.utils.invariants.invariant_check(method, prefun, postfun) +

    +beancount.utils.misc_utils.LineFileProxy.close(self) -

    +

    -

    Decorate a method with the pre/post invariant checkers.

    +

    Close the line delegator.

    - - - - - - - - - - - -
    Parameters: -
      -
    • method – An unbound method to instrument.

    • -
    • prefun – A function that checks invariants pre-call.

    • -
    • postfun – A function that checks invariants post-call.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An unbound method, decorated.

    • -
    -
    - Source code in beancount/utils/invariants.py -
    def invariant_check(method, prefun, postfun):
    -    """Decorate a method with the pre/post invariant checkers.
    -
    -    Args:
    -      method: An unbound method to instrument.
    -      prefun: A function that checks invariants pre-call.
    -      postfun: A function that checks invariants post-call.
    -    Returns:
    -      An unbound method, decorated.
    -    """
    -    reentrant = []
    -    def new_method(self, *args, **kw):
    -        reentrant.append(None)
    -        if len(reentrant) == 1:
    -            prefun(self)
    -        result = method(self, *args, **kw)
    -        if len(reentrant) == 1:
    -            postfun(self)
    -        reentrant.pop()
    -        return result
    -    return new_method
    -
    + Source code in beancount/utils/misc_utils.py +
    def close(self):
    +    """Close the line delegator."""
    +    self.flush()
    +
    @@ -3013,19 +2532,55 @@

    -
    +
    -

    -beancount.utils.invariants.uninstrument_invariants(klass) +

    +beancount.utils.misc_utils.LineFileProxy.flush(self) -

    +

    -

    Undo the instrumentation for invariants.

    +

    Flush the data to the line writer.

    + +
    + Source code in beancount/utils/misc_utils.py +
    def flush(self):
    +    """Flush the data to the line writer."""
    +    data = "".join(self.data)
    +    if data:
    +        lines = data.splitlines()
    +        self.data = [lines.pop(-1)] if data[-1] != "\n" else []
    +        for line in lines:
    +            if self.prefix:
    +                line = self.prefix + line
    +            if self.write_newlines:
    +                line += "\n"
    +            self.line_writer(line)
    +
    +
    +
    + +
    + + + +
    + + + +

    +beancount.utils.misc_utils.LineFileProxy.write(self, data) + + +

    + +
    + +

    Write some string data to the output.

    @@ -3037,26 +2592,26 @@

    Parameters:

      -
    • klass – A class object, whose methods to be uninstrumented.

    • +
    • data – A string, with or without newlines.

    - Source code in beancount/utils/invariants.py -
    def uninstrument_invariants(klass):
    -    """Undo the instrumentation for invariants.
    -
    -    Args:
    -      klass: A class object, whose methods to be uninstrumented.
    -    """
    -    instrumented = getattr(klass, '__instrumented', None)
    -    if instrumented:
    -        for attrname, object_ in instrumented.items():
    -            setattr(klass, attrname, object_)
    -    del klass.__instrumented
    -
    + Source code in beancount/utils/misc_utils.py +
    def write(self, data):
    +    """Write some string data to the output.
    +
    +    Args:
    +      data: A string, with or without newlines.
    +    """
    +    if "\n" in data:
    +        self.data.append(data)
    +        self.flush()
    +    else:
    +        self.data.append(data)
    +
    @@ -3066,7 +2621,6 @@

    +
    -

    - beancount.utils.memo +

    + +beancount.utils.misc_utils.TypeComparable -

    +

    -

    Memoization utilities.

    +

    A base class whose equality comparison includes comparing the +type of the instance itself.

    + @@ -3103,20 +2660,102 @@

    + + +

    + +
    + +
    + + + + +
    + + + +

    +beancount.utils.misc_utils.box(name=None, file=None) + + +

    + +
    + +

    A context manager that prints out a box around a block. +This is useful for printing out stuff from tests in a way that is readable.

    + + + + + + + + + + + + +
    Parameters: +
      +
    • name – A string, the name of the box to use.

    • +
    • file – The file object to print to.

    • +
    +

    Yields: + None.

    + +
    + Source code in beancount/utils/misc_utils.py +
    @contextlib.contextmanager
    +def box(name=None, file=None):
    +    """A context manager that prints out a box around a block.
    +    This is useful for printing out stuff from tests in a way that is readable.
    +
    +    Args:
    +      name: A string, the name of the box to use.
    +      file: The file object to print to.
    +    Yields:
    +      None.
    +    """
    +    file = file or sys.stdout
    +    file.write("\n")
    +    if name:
    +        header = ",--------({})--------\n".format(name)
    +        footer = "`{}\n".format("-" * (len(header) - 2))
    +    else:
    +        header = ",----------------\n"
    +        footer = "`----------------\n"
    +
    +    file.write(header)
    +    yield
    +    file.write(footer)
    +    file.flush()
    +
    +
    +
    + +
    + + +
    -

    -beancount.utils.memo.memoize_recent_fileobj(function, cache_filename, expiration=None) +

    +beancount.utils.misc_utils.cmptuple(name, attributes) -

    +
    -

    Memoize recent calls to the given function which returns a file object.

    -

    The results of the cache expire after some time.

    +

    Manufacture a comparable namedtuple class, similar to collections.namedtuple.

    +

    A comparable named tuple is a tuple which compares to False if contents are +equal but the data types are different. We define this to supplement +collections.namedtuple because by default a namedtuple disregards the type +and we want to make precise comparisons for tests.

    @@ -3128,10 +2767,9 @@

    @@ -3147,56 +2785,41 @@

    Parameters:
      -
    • function – A callable object.

    • -
    • cache_filename – A string, the path to the database file to cache to.

    • -
    • expiration – The time during which the results will be kept valid. Use -'None' to never expire the cache (this is the default).

    • +
    • name – The given name of the class.

    • +
    • attributes – A string or tuple of strings, with the names of the +attributes.

    Returns:
      -
    • A memoized version of the function.

    • +
    • A new namedtuple-derived type that compares False with other +tuples with same contents.

    - Source code in beancount/utils/memo.py -
    def memoize_recent_fileobj(function, cache_filename, expiration=None):
    -    """Memoize recent calls to the given function which returns a file object.
    -
    -    The results of the cache expire after some time.
    -
    -    Args:
    -      function: A callable object.
    -      cache_filename: A string, the path to the database file to cache to.
    -      expiration: The time during which the results will be kept valid. Use
    -        'None' to never expire the cache (this is the default).
    -    Returns:
    -      A memoized version of the function.
    -    """
    -    urlcache = shelve.open(cache_filename, 'c')
    -    urlcache.lock = threading.Lock()  # Note: 'shelve' is not thread-safe.
    -    @functools.wraps(function)
    -    def memoized(*args, **kw):
    -        # Encode the arguments, including a date string in order to invalidate
    -        # results over some time.
    -        md5 = hashlib.md5()
    -        md5.update(str(args).encode('utf-8'))
    -        md5.update(str(sorted(kw.items())).encode('utf-8'))
    -
    -        hash_ = md5.hexdigest()
    -        time_now = now()
    -        try:
    -            with urlcache.lock:
    -                time_orig, contents = urlcache[hash_]
    -            if expiration is not None and (time_now - time_orig) > expiration:
    -                raise KeyError
    -        except KeyError:
    -            fileobj = function(*args, **kw)
    -            if fileobj:
    -                contents = fileobj.read()
    -                with urlcache.lock:
    -                    urlcache[hash_] = (time_now, contents)
    -            else:
    -                contents = None
    -
    -        return io.BytesIO(contents) if contents else None
    -    return memoized
    -
    + Source code in beancount/utils/misc_utils.py +
    def cmptuple(name, attributes):
    +    """Manufacture a comparable namedtuple class, similar to collections.namedtuple.
    +
    +    A comparable named tuple is a tuple which compares to False if contents are
    +    equal but the data types are different. We define this to supplement
    +    collections.namedtuple because by default a namedtuple disregards the type
    +    and we want to make precise comparisons for tests.
    +
    +    Args:
    +      name: The given name of the class.
    +      attributes: A string or tuple of strings, with the names of the
    +        attributes.
    +    Returns:
    +      A new namedtuple-derived type that compares False with other
    +      tuples with same contents.
    +    """
    +    base = collections.namedtuple("_{}".format(name), attributes)
    +    return type(
    +        name,
    +        (
    +            TypeComparable,
    +            base,
    +        ),
    +        {},
    +    )
    +
    @@ -3208,22 +2831,85 @@

    -

    -beancount.utils.memo.now() +

    +beancount.utils.misc_utils.compute_unique_clean_ids(strings) -

    +
    -

    Indirection on datetime.datetime.now() for testing.

    +

    Given a sequence of strings, reduce them to corresponding ids without any +funny characters and insure that the list of ids is unique. Yields pairs +of (id, string) for the result.

    + + + + + + + + + + + +
    Parameters: +
      +
    • strings – A list of strings.

    • +
    +
    + + + + + + + + + + + +
    Returns: +
      +
    • A list of (id, string) pairs.

    • +
    +
    - Source code in beancount/utils/memo.py -
    def now():
    -    "Indirection on datetime.datetime.now() for testing."
    -    return datetime.datetime.now()
    -
    + Source code in beancount/utils/misc_utils.py +
    def compute_unique_clean_ids(strings):
    +    """Given a sequence of strings, reduce them to corresponding ids without any
    +    funny characters and insure that the list of ids is unique. Yields pairs
    +    of (id, string) for the result.
    +
    +    Args:
    +      strings: A list of strings.
    +    Returns:
    +      A list of (id, string) pairs.
    +    """
    +    string_set = set(strings)
    +
    +    # Try multiple methods until we get one that has no collisions.
    +    for regexp, replacement in [
    +        (r"[^A-Za-z0-9.-]", "_"),
    +        (r"[^A-Za-z0-9_]", ""),
    +    ]:
    +        seen = set()
    +        idmap = {}
    +        mre = re.compile(regexp)
    +        for string in string_set:
    +            id_ = mre.sub(replacement, string)
    +            if id_ in seen:
    +                break  # Collision.
    +            seen.add(id_)
    +            idmap[id_] = string
    +        else:
    +            break
    +    else:
    +        return None  # Could not find a unique mapping.
    +
    +    return idmap
    +
    @@ -3231,92 +2917,59 @@

    +
    -
    - -

    - -
    - - - -
    - - - -

    - beancount.utils.misc_utils - - - -

    - -
    - -

    Generic utility packages and functions.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.utils.misc_utils.LineFileProxy - +

    +beancount.utils.misc_utils.deprecated(message) -

    +
    -

    A file object that will delegate writing full lines to another logging function. -This may be used for writing data to a logging level without having to worry about -lines.

    - - - - -
    - - - - +

    A decorator generator to mark functions as deprecated and log a warning.

    +
    + Source code in beancount/utils/misc_utils.py +
    def deprecated(message):
    +    """A decorator generator to mark functions as deprecated and log a warning."""
    +
    +    def decorator(func):
    +        @functools.wraps(func)
    +        def new_func(*args, **kwargs):
    +            warnings.warn(
    +                "Call to deprecated function {}: {}".format(func.__name__, message),
    +                category=DeprecationWarning,
    +                stacklevel=2,
    +            )
    +            return func(*args, **kwargs)
    +
    +        return new_func
    +
    +    return decorator
    +
    +
    +
    +
    -
    +
    -

    -beancount.utils.misc_utils.LineFileProxy.__init__(self, line_writer, prefix=None, write_newlines=False) +

    +beancount.utils.misc_utils.dictmap(mdict, keyfun=None, valfun=None) - - special - -

    +
    -

    Construct a new line delegator file object proxy.

    +

    Map a dictionary's value.

    @@ -3328,9 +2981,9 @@

    Parameters:

    @@ -3338,19 +2991,20 @@

    Source code in beancount/utils/misc_utils.py -
    def __init__(self, line_writer, prefix=None, write_newlines=False):
    -    """Construct a new line delegator file object proxy.
    -
    -    Args:
    -      line_writer: A callable function, used to write to the delegated output.
    -      prefix: An optional string, the prefix to insert before every line.
    -      write_newlines: A boolean, true if we should output the newline characters.
    -    """
    -    self.line_writer = line_writer
    -    self.prefix = prefix
    -    self.write_newlines = write_newlines
    -    self.data = []
    -
    +
    def dictmap(mdict, keyfun=None, valfun=None):
    +    """Map a dictionary's value.
    +
    +    Args:
    +      mdict: A dict.
    +      key: A callable to apply to the keys.
    +      value: A callable to apply to the values.
    +    """
    +    if keyfun is None:
    +        keyfun = lambda x: x
    +    if valfun is None:
    +        valfun = lambda x: x
    +    return {keyfun(key): valfun(val) for key, val in mdict.items()}
    +
    @@ -3358,26 +3012,50 @@

    +
    -

    -beancount.utils.misc_utils.LineFileProxy.close(self) +

    +beancount.utils.misc_utils.escape_string(string) -

    +

    -

    Close the line delegator.

    +

    Escape quotes and backslashes in payee and narration.

    + +
      -
    • line_writer – A callable function, used to write to the delegated output.

    • -
    • prefix – An optional string, the prefix to insert before every line.

    • -
    • write_newlines – A boolean, true if we should output the newline characters.

    • +
    • mdict – A dict.

    • +
    • key – A callable to apply to the keys.

    • +
    • value – A callable to apply to the values.

    + + + + + + + + + + +
    Parameters: +
      +
    • string – Any string.

    • +
    +

    Returns. + The input string, with offending characters replaced.

    Source code in beancount/utils/misc_utils.py -
    def close(self):
    -    """Close the line delegator."""
    -    self.flush()
    -
    +
    def escape_string(string):
    +    """Escape quotes and backslashes in payee and narration.
    +
    +    Args:
    +      string: Any string.
    +    Returns.
    +      The input string, with offending characters replaced.
    +    """
    +    return string.replace("\\", r"\\").replace('"', r"\"")
    +
    @@ -3385,35 +3063,55 @@

    -
    +
    -

    -beancount.utils.misc_utils.LineFileProxy.flush(self) +

    +beancount.utils.misc_utils.filter_type(elist, types) -

    +

    -

    Flush the data to the line writer.

    +

    Filter the given list to yield only instances of the given types.

    + + + + + + + + + + + + +
    Parameters: +
      +
    • elist – A sequence of elements.

    • +
    • types – A sequence of types to include in the output list.

    • +
    +

    Yields: + Each element, if it is an instance of 'types'.

    Source code in beancount/utils/misc_utils.py -
    def flush(self):
    -    """Flush the data to the line writer."""
    -    data = ''.join(self.data)
    -    if data:
    -        lines = data.splitlines()
    -        self.data = [lines.pop(-1)] if data[-1] != '\n' else []
    -        for line in lines:
    -            if self.prefix:
    -                line = self.prefix + line
    -            if self.write_newlines:
    -                line += '\n'
    -            self.line_writer(line)
    -
    +
    def filter_type(elist, types):
    +    """Filter the given list to yield only instances of the given types.
    +
    +    Args:
    +      elist: A sequence of elements.
    +      types: A sequence of types to include in the output list.
    +    Yields:
    +      Each element, if it is an instance of 'types'.
    +    """
    +    for element in elist:
    +        if not isinstance(element, types):
    +            continue
    +        yield element
    +
    @@ -3421,19 +3119,20 @@

    -
    +
    -

    -beancount.utils.misc_utils.LineFileProxy.write(self, data) +

    +beancount.utils.misc_utils.first_paragraph(docstring) -

    +

    -

    Write some string data to the output.

    +

    Return the first sentence of a docstring. +The sentence has to be delimited by an empty line.

    @@ -3445,26 +3144,46 @@

    +
    Parameters:
      -
    • data – A string, with or without newlines.

    • +
    • docstring – A doc string.

    + + + + + + + + + + +
    Returns: +
      +
    • A string with just the first sentence on a single line.

    • +
    +
    Source code in beancount/utils/misc_utils.py -
    def write(self, data):
    -    """Write some string data to the output.
    -
    -    Args:
    -      data: A string, with or without newlines.
    -    """
    -    if '\n' in data:
    -        self.data.append(data)
    -        self.flush()
    -    else:
    -        self.data.append(data)
    -
    +
    def first_paragraph(docstring):
    +    """Return the first sentence of a docstring.
    +    The sentence has to be delimited by an empty line.
    +
    +    Args:
    +      docstring: A doc string.
    +    Returns:
    +      A string with just the first sentence on a single line.
    +    """
    +    lines = []
    +    for line in docstring.strip().splitlines():
    +        if not line:
    +            break
    +        lines.append(line.rstrip())
    +    return " ".join(lines)
    +
    @@ -3472,72 +3191,119 @@

    +
    -
    - -

    - -
    - - - -
    - - - -

    - -beancount.utils.misc_utils.TypeComparable - - - -

    - -
    -

    A base class whose equality comparison includes comparing the -type of the instance itself.

    +

    +beancount.utils.misc_utils.get_screen_height() +

    +
    -
    +

    Return the height of the terminal that runs this program.

    + + + + + + + + + + + +
    Returns: +
      +
    • An integer, the number of characters the screen is high. +Return 0 if the terminal cannot be initialized.

    • +
    +
    +
    + Source code in beancount/utils/misc_utils.py +
    def get_screen_height():
    +    """Return the height of the terminal that runs this program.
    +
    +    Returns:
    +      An integer, the number of characters the screen is high.
    +      Return 0 if the terminal cannot be initialized.
    +    """
    +    return _get_screen_value("lines", 0)
    +
    +
    +
    +
    +
    +

    +beancount.utils.misc_utils.get_screen_width() +

    +
    -
    +

    Return the width of the terminal that runs this program.

    + + + + + + + + + + + +
    Returns: +
      +
    • An integer, the number of characters the screen is wide. +Return 0 if the terminal cannot be initialized.

    • +
    +
    +
    + Source code in beancount/utils/misc_utils.py +
    def get_screen_width():
    +    """Return the width of the terminal that runs this program.
    +
    +    Returns:
    +      An integer, the number of characters the screen is wide.
    +      Return 0 if the terminal cannot be initialized.
    +    """
    +    return _get_screen_value("cols", 0)
    +
    +
    -
    -

    -beancount.utils.misc_utils.box(name=None, file=None) +

    +beancount.utils.misc_utils.get_tuple_values(ntuple, predicate, memo=None) -

    +
    -

    A context manager that prints out a box around a block. -This is useful for printing out stuff from tests in a way that is readable.

    +

    Return all members referred to by this namedtuple instance that satisfy the +given predicate. This function also works recursively on its members which +are lists or tuples, and so it can be used for Transaction instances.

    @@ -3549,42 +3315,50 @@

    Parameters:
      -
    • name – A string, the name of the box to use.

    • -
    • file – The file object to print to.

    • +
    • ntuple – A tuple or namedtuple.

    • +
    • predicate – A predicate function that returns true if an attribute is to be +output.

    • +
    • memo – An optional memoizing dictionary. If a tuple has already been seen, the +recursion will be avoided.

    Yields: - None.

    + Attributes of the tuple and its sub-elements if the predicate is true.

    Source code in beancount/utils/misc_utils.py -
    @contextlib.contextmanager
    -def box(name=None, file=None):
    -    """A context manager that prints out a box around a block.
    -    This is useful for printing out stuff from tests in a way that is readable.
    -
    -    Args:
    -      name: A string, the name of the box to use.
    -      file: The file object to print to.
    -    Yields:
    -      None.
    -    """
    -    file = file or sys.stdout
    -    file.write('\n')
    -    if name:
    -        header = ',--------({})--------\n'.format(name)
    -        footer = '`{}\n'.format('-' * (len(header)-2))
    -    else:
    -        header = ',----------------\n'
    -        footer = '`----------------\n'
    -
    -    file.write(header)
    -    yield
    -    file.write(footer)
    -    file.flush()
    -
    +
    def get_tuple_values(ntuple, predicate, memo=None):
    +    """Return all members referred to by this namedtuple instance that satisfy the
    +    given predicate. This function also works recursively on its members which
    +    are lists or tuples, and so it can be used for Transaction instances.
    +
    +    Args:
    +      ntuple: A tuple or namedtuple.
    +      predicate: A predicate function that returns true if an attribute is to be
    +        output.
    +      memo: An optional memoizing dictionary. If a tuple has already been seen, the
    +        recursion will be avoided.
    +    Yields:
    +      Attributes of the tuple and its sub-elements if the predicate is true.
    +    """
    +    if memo is None:
    +        memo = set()
    +    id_ntuple = id(ntuple)
    +    if id_ntuple in memo:
    +        return
    +    memo.add(id_ntuple)
    +
    +    if predicate(ntuple):
    +        yield
    +    for attribute in ntuple:
    +        if predicate(attribute):
    +            yield attribute
    +        if isinstance(attribute, (list, tuple)):
    +            for value in get_tuple_values(attribute, predicate, memo):
    +                yield value
    +
    @@ -3596,19 +3370,16 @@

    -

    -beancount.utils.misc_utils.cmptuple(name, attributes) +

    +beancount.utils.misc_utils.groupby(keyfun, elements) -

    +
    -

    Manufacture a comparable namedtuple class, similar to collections.namedtuple.

    -

    A comparable named tuple is a tuple which compares to False if contents are -equal but the data types are different. We define this to supplement -collections.namedtuple because by default a namedtuple disregards the type -and we want to make precise comparisons for tests.

    +

    Group the elements as a dict of lists, where the key is computed using the +function 'keyfun'.

    @@ -3620,9 +3391,8 @@

    @@ -3638,8 +3408,7 @@

    @@ -3647,25 +3416,24 @@

    Parameters:
      -
    • name – The given name of the class.

    • -
    • attributes – A string or tuple of strings, with the names of the -attributes.

    • +
    • keyfun – A callable, used to obtain the group key from each element.

    • +
    • elements – An iterable of the elements to group.

    Returns:
      -
    • A new namedtuple-derived type that compares False with other -tuples with same contents.

    • +
    • A dict of key to list of sequences.

    Source code in beancount/utils/misc_utils.py -
    def cmptuple(name, attributes):
    -    """Manufacture a comparable namedtuple class, similar to collections.namedtuple.
    -
    -    A comparable named tuple is a tuple which compares to False if contents are
    -    equal but the data types are different. We define this to supplement
    -    collections.namedtuple because by default a namedtuple disregards the type
    -    and we want to make precise comparisons for tests.
    -
    -    Args:
    -      name: The given name of the class.
    -      attributes: A string or tuple of strings, with the names of the
    -        attributes.
    -    Returns:
    -      A new namedtuple-derived type that compares False with other
    -      tuples with same contents.
    -    """
    -    base = collections.namedtuple('_{}'.format(name), attributes)
    -    return type(name, (TypeComparable, base,), {})
    -
    +
    def groupby(keyfun, elements):
    +    """Group the elements as a dict of lists, where the key is computed using the
    +    function 'keyfun'.
    +
    +    Args:
    +      keyfun: A callable, used to obtain the group key from each element.
    +      elements: An iterable of the elements to group.
    +    Returns:
    +      A dict of key to list of sequences.
    +    """
    +    # Note: We could allow a custom aggregation function. Another option is
    +    # provide another method to reduce the list values of a dict, but that can
    +    # be accomplished using a dict comprehension.
    +    grouped = defaultdict(list)
    +    for element in elements:
    +        grouped[keyfun(element)].append(element)
    +    return grouped
    +
    @@ -3677,17 +3445,15 @@

    -

    -beancount.utils.misc_utils.compute_unique_clean_ids(strings) +

    +beancount.utils.misc_utils.idify(string) -

    +
    -

    Given a sequence of strings, reduce them to corresponding ids without any -funny characters and insure that the list of ids is unique. Yields pairs -of (id, string) for the result.

    +

    Replace characters objectionable for a filename with underscores.

    @@ -3699,7 +3465,7 @@

    Parameters:

    @@ -3715,7 +3481,7 @@

    Returns:

    @@ -3723,37 +3489,19 @@

    Source code in beancount/utils/misc_utils.py -
    def compute_unique_clean_ids(strings):
    -    """Given a sequence of strings, reduce them to corresponding ids without any
    -    funny characters and insure that the list of ids is unique. Yields pairs
    -    of (id, string) for the result.
    -
    -    Args:
    -      strings: A list of strings.
    -    Returns:
    -      A list of (id, string) pairs.
    -    """
    -    string_set = set(strings)
    -
    -    # Try multiple methods until we get one that has no collisions.
    -    for regexp, replacement in [(r'[^A-Za-z0-9.-]', '_'),
    -                                (r'[^A-Za-z0-9_]', ''),]:
    -        seen = set()
    -        idmap = {}
    -        mre = re.compile(regexp)
    -        for string in string_set:
    -            id_ = mre.sub(replacement, string)
    -            if id_ in seen:
    -                break  # Collision.
    -            seen.add(id_)
    -            idmap[id_] = string
    -        else:
    -            break
    -    else:
    -        return None # Could not find a unique mapping.
    -
    -    return idmap
    -
    +
    def idify(string):
    +    """Replace characters objectionable for a filename with underscores.
    +
    +    Args:
    +      string: Any string.
    +    Returns:
    +      The input string, with offending characters replaced.
    +    """
    +    for sfrom, sto in [(r"[ \(\)]+", "_"), (r"_*\._*", ".")]:
    +        string = re.sub(sfrom, sto, string)
    +    string = string.strip("_")
    +    return string
    +
    @@ -3765,31 +3513,70 @@

    -beancount.utils.misc_utils.deprecated(message) +

    +beancount.utils.misc_utils.import_curses() -

    +
    -

    A decorator generator to mark functions as deprecated and log a warning.

    +

    Try to import the 'curses' module. +(This is used here in order to override for tests.)

    +
      -
    • strings – A list of strings.

    • +
    • string – Any string.

      -
    • A list of (id, string) pairs.

    • +
    • The input string, with offending characters replaced.

    + + + + + + + + + + +
    Returns: +
      +
    • The curses module, if it was possible to import it.

    • +
    +
    + + + + + + + + + + + +
    Exceptions: +
      +
    • ImportError – If the module could not be imported.

    • +
    +
    Source code in beancount/utils/misc_utils.py -
    def deprecated(message):
    -    """A decorator generator to mark functions as deprecated and log a warning."""
    -    def decorator(func):
    -        @functools.wraps(func)
    -        def new_func(*args, **kwargs):
    -            warnings.warn("Call to deprecated function {}: {}".format(func.__name__,
    -                                                                      message),
    -                          category=DeprecationWarning,
    -                          stacklevel=2)
    -            return func(*args, **kwargs)
    -        return new_func
    -    return decorator
    -
    +
    def import_curses():
    +    """Try to import the 'curses' module.
    +    (This is used here in order to override for tests.)
    +
    +    Returns:
    +      The curses module, if it was possible to import it.
    +    Raises:
    +      ImportError: If the module could not be imported.
    +    """
    +    # Note: There's a recipe for getting terminal size on Windows here, without
    +    # curses, I should probably implement that at some point:
    +    # https://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window
    +    # Also, consider just using 'blessings' instead, which provides this across
    +    # multiple platforms.
    +
    +    import curses
    +
    +    return curses
    +
    @@ -3801,15 +3588,15 @@

    -

    -beancount.utils.misc_utils.dictmap(mdict, keyfun=None, valfun=None) +

    +beancount.utils.misc_utils.is_sorted(iterable, key=<function <lambda> at 0x78fcde6aac00>, cmp=<function <lambda> at 0x78fcde6aaca0>) -

    +
    -

    Map a dictionary's value.

    +

    Return true if the sequence is sorted.

    @@ -3821,30 +3608,50 @@

    +
    Parameters:
      -
    • mdict – A dict.

    • -
    • key – A callable to apply to the keys.

    • -
    • value – A callable to apply to the values.

    • +
    • iterable – An iterable sequence.

    • +
    • key – A function to extract the quantity by which to sort.

    • +
    • cmp – A function that compares two elements of a sequence.

    + + + + + + + + + + +
    Returns: +
      +
    • A boolean, true if the sequence is sorted.

    • +
    +
    Source code in beancount/utils/misc_utils.py -
    def dictmap(mdict, keyfun=None, valfun=None):
    -    """Map a dictionary's value.
    -
    -    Args:
    -      mdict: A dict.
    -      key: A callable to apply to the keys.
    -      value: A callable to apply to the values.
    -    """
    -    if keyfun is None:
    -        keyfun = lambda x: x
    -    if valfun is None:
    -        valfun = lambda x: x
    -    return {keyfun(key): valfun(val) for key, val in mdict.items()}
    -
    +
    def is_sorted(iterable, key=lambda x: x, cmp=lambda x, y: x <= y):
    +    """Return true if the sequence is sorted.
    +
    +    Args:
    +      iterable: An iterable sequence.
    +      key: A function to extract the quantity by which to sort.
    +      cmp: A function that compares two elements of a sequence.
    +    Returns:
    +      A boolean, true if the sequence is sorted.
    +    """
    +    iterator = map(key, iterable)
    +    prev = next(iterator)
    +    for element in iterator:
    +        if not cmp(prev, element):
    +            return False
    +        prev = element
    +    return True
    +
    @@ -3856,15 +3663,15 @@

    -

    -beancount.utils.misc_utils.escape_string(string) +

    +beancount.utils.misc_utils.log_time(operation_name, log_timings, indent=0) -

    +
    -

    Escape quotes and backslashes in payee and narration.

    +

    A context manager that times the block and logs it to info level.

    @@ -3876,27 +3683,45 @@

    -
    Parameters:
      -
    • string – Any string.

    • +
    • operation_name – A string, a label for the name of the operation.

    • +
    • log_timings – A function to write log messages to. If left to None, +no timings are written (this becomes a no-op).

    • +
    • indent – An integer, the indentation level for the format of the timing +line. This is useful if you're logging timing to a hierarchy of +operations.

    Returns. - The input string, with offending characters replaced.

    +

    Yields: + The start time of the operation.

    Source code in beancount/utils/misc_utils.py -
    def escape_string(string):
    -    """Escape quotes and backslashes in payee and narration.
    -
    -    Args:
    -      string: Any string.
    -    Returns.
    -      The input string, with offending characters replaced.
    -    """
    -    return string.replace('\\', r'\\')\
    -                 .replace('"', r'\"')
    -
    +
    @contextlib.contextmanager
    +def log_time(operation_name, log_timings, indent=0):
    +    """A context manager that times the block and logs it to info level.
    +
    +    Args:
    +      operation_name: A string, a label for the name of the operation.
    +      log_timings: A function to write log messages to. If left to None,
    +        no timings are written (this becomes a no-op).
    +      indent: An integer, the indentation level for the format of the timing
    +        line. This is useful if you're logging timing to a hierarchy of
    +        operations.
    +    Yields:
    +      The start time of the operation.
    +    """
    +    time1 = time()
    +    yield time1
    +    time2 = time()
    +    if log_timings:
    +        log_timings(
    +            "Operation: {:48} Time: {}{:6.0f} ms".format(
    +                "'{}'".format(operation_name), "      " * indent, (time2 - time1) * 1000
    +            )
    +        )
    +
    @@ -3908,15 +3733,15 @@

    -

    -beancount.utils.misc_utils.filter_type(elist, types) +

    +beancount.utils.misc_utils.longest(seq) -

    +
    -

    Filter the given list to yield only instances of the given types.

    +

    Return the longest of the given subsequences.

    @@ -3928,31 +3753,45 @@

    -
    Parameters:
      -
    • elist – A sequence of elements.

    • -
    • types – A sequence of types to include in the output list.

    • +
    • seq – An iterable sequence of lists.

    Yields: - Each element, if it is an instance of 'types'.

    - + + + + + + + + + + + + +
    Returns: +
      +
    • The longest list from the sequence.

    • +
    +
    Source code in beancount/utils/misc_utils.py -
    def filter_type(elist, types):
    -    """Filter the given list to yield only instances of the given types.
    -
    -    Args:
    -      elist: A sequence of elements.
    -      types: A sequence of types to include in the output list.
    -    Yields:
    -      Each element, if it is an instance of 'types'.
    -    """
    -    for element in elist:
    -        if not isinstance(element, types):
    -            continue
    -        yield element
    -
    +
    def longest(seq):
    +    """Return the longest of the given subsequences.
    +
    +    Args:
    +      seq: An iterable sequence of lists.
    +    Returns:
    +      The longest list from the sequence.
    +    """
    +    longest, length = None, -1
    +    for element in seq:
    +        len_element = len(element)
    +        if len_element > length:
    +            longest, length = element, len_element
    +    return longest
    +
    @@ -3964,16 +3803,15 @@

    -

    -beancount.utils.misc_utils.first_paragraph(docstring) +

    +beancount.utils.misc_utils.map_namedtuple_attributes(attributes, mapper, object_) -

    +
    -

    Return the first sentence of a docstring. -The sentence has to be delimited by an empty line.

    +

    Map the value of the named attributes of object by mapper.

    @@ -3985,7 +3823,10 @@

    @@ -4001,7 +3842,8 @@

    @@ -4009,22 +3851,22 @@

    Parameters:
      -
    • docstring – A doc string.

    • +
    • attributes – A sequence of string, the attribute names to map.

    • +
    • mapper – A callable that accepts the value of a field and returns +the new value.

    • +
    • object_ – Some namedtuple object with attributes on it.

    Returns:
      -
    • A string with just the first sentence on a single line.

    • +
    • A new instance of the same namedtuple with the named fields mapped by +mapper.

    Source code in beancount/utils/misc_utils.py -
    def first_paragraph(docstring):
    -    """Return the first sentence of a docstring.
    -    The sentence has to be delimited by an empty line.
    -
    -    Args:
    -      docstring: A doc string.
    -    Returns:
    -      A string with just the first sentence on a single line.
    -    """
    -    lines = []
    -    for line in docstring.strip().splitlines():
    -        if not line:
    -            break
    -        lines.append(line.rstrip())
    -    return ' '.join(lines)
    -
    +
    def map_namedtuple_attributes(attributes, mapper, object_):
    +    """Map the value of the named attributes of object by mapper.
    +
    +    Args:
    +      attributes: A sequence of string, the attribute names to map.
    +      mapper: A callable that accepts the value of a field and returns
    +        the new value.
    +      object_: Some namedtuple object with attributes on it.
    +    Returns:
    +      A new instance of the same namedtuple with the named fields mapped by
    +      mapper.
    +    """
    +    return object_._replace(
    +        **{attribute: mapper(getattr(object_, attribute)) for attribute in attributes}
    +    )
    +
    @@ -4036,15 +3878,16 @@

    -

    -beancount.utils.misc_utils.get_screen_height() +

    +beancount.utils.misc_utils.replace_namedtuple_values(ntuple, predicate, mapper, memo=None) -

    +
    -

    Return the height of the terminal that runs this program.

    +

    Recurse through all the members of namedtuples and lists, and for +members that match the given predicate, run them through the given mapper.

    @@ -4053,27 +3896,64 @@

    - - + + -
    Returns: + Parameters:
      -
    • An integer, the number of characters the screen is high. -Return 0 if the terminal cannot be initialized.

    • +
    • ntuple – A namedtuple instance.

    • +
    • predicate – A predicate function that returns true if an attribute is to be +output.

    • +
    • mapper – A callable, that will accept a single argument and return its +replacement value.

    • +
    • memo – An optional memoizing dictionary. If a tuple has already been seen, the +recursion will be avoided.

    -
    +

    Yields: + Attributes of the tuple and its sub-elements if the predicate is true.

    +
    Source code in beancount/utils/misc_utils.py -
    def get_screen_height():
    -    """Return the height of the terminal that runs this program.
    -
    -    Returns:
    -      An integer, the number of characters the screen is high.
    -      Return 0 if the terminal cannot be initialized.
    -    """
    -    return _get_screen_value('lines', 0)
    -
    +
    def replace_namedtuple_values(ntuple, predicate, mapper, memo=None):
    +    """Recurse through all the members of namedtuples and lists, and for
    +    members that match the given predicate, run them through the given mapper.
    +
    +    Args:
    +      ntuple: A namedtuple instance.
    +      predicate: A predicate function that returns true if an attribute is to be
    +        output.
    +      mapper: A callable, that will accept a single argument and return its
    +        replacement value.
    +      memo: An optional memoizing dictionary. If a tuple has already been seen, the
    +        recursion will be avoided.
    +    Yields:
    +      Attributes of the tuple and its sub-elements if the predicate is true.
    +    """
    +    if memo is None:
    +        memo = set()
    +    id_ntuple = id(ntuple)
    +    if id_ntuple in memo:
    +        return None
    +    memo.add(id_ntuple)
    +
    +    if not (type(ntuple) is not tuple and isinstance(ntuple, tuple)):
    +        return ntuple
    +    replacements = {}
    +    for attribute_name, attribute in zip(ntuple._fields, ntuple):
    +        if predicate(attribute):
    +            replacements[attribute_name] = mapper(attribute)
    +        elif type(attribute) is not tuple and isinstance(attribute, tuple):
    +            replacements[attribute_name] = replace_namedtuple_values(
    +                attribute, predicate, mapper, memo
    +            )
    +        elif type(attribute) in (list, tuple):
    +            replacements[attribute_name] = [
    +                replace_namedtuple_values(member, predicate, mapper, memo)
    +                for member in attribute
    +            ]
    +    return ntuple._replace(**replacements)
    +
    @@ -4085,15 +3965,15 @@

    -

    -beancount.utils.misc_utils.get_screen_width() +

    +beancount.utils.misc_utils.skipiter(iterable, num_skip) -

    +
    -

    Return the width of the terminal that runs this program.

    +

    Skip some elements from an iterator.

    @@ -4102,27 +3982,45 @@

    - - + + -
    Returns: + Parameters:
      -
    • An integer, the number of characters the screen is wide. -Return 0 if the terminal cannot be initialized.

    • +
    • iterable – An iterator.

    • +
    • num_skip – The number of elements in the period.

    -
    +

    Yields: + Elements from the iterable, with num_skip elements skipped. + For example, skipiter(range(10), 3) yields [0, 3, 6, 9].

    +
    Source code in beancount/utils/misc_utils.py -
    def get_screen_width():
    -    """Return the width of the terminal that runs this program.
    -
    -    Returns:
    -      An integer, the number of characters the screen is wide.
    -      Return 0 if the terminal cannot be initialized.
    -    """
    -    return _get_screen_value('cols', 0)
    -
    +
    def skipiter(iterable, num_skip):
    +    """Skip some elements from an iterator.
    +
    +    Args:
    +      iterable: An iterator.
    +      num_skip: The number of elements in the period.
    +    Yields:
    +      Elements from the iterable, with num_skip elements skipped.
    +      For example, skipiter(range(10), 3) yields [0, 3, 6, 9].
    +    """
    +    assert num_skip > 0
    +    sit = iter(iterable)
    +    while 1:
    +        try:
    +            value = next(sit)
    +        except StopIteration:
    +            return
    +        yield value
    +        for _ in range(num_skip - 1):
    +            try:
    +                next(sit)
    +            except StopIteration:
    +                return
    +
    @@ -4134,17 +4032,18 @@

    -

    -beancount.utils.misc_utils.get_tuple_values(ntuple, predicate, memo=None) +

    +beancount.utils.misc_utils.sorted_uniquify(iterable, keyfunc=None, last=False) -

    +
    -

    Return all members referred to by this namedtuple instance that satisfy the -given predicate. This function also works recursively on its members which -are lists or tuples, and so it can be used for Transaction instances.

    +

    Given a sequence of elements, sort and remove duplicates of the given key. +Keep either the first or the last (by key) element of a sequence of +key-identical elements. This does not maintain the ordering of the +original elements, they are returned sorted (by key) instead.

    @@ -4156,50 +4055,58 @@

    Parameters:
      -
    • ntuple – A tuple or namedtuple.

    • -
    • predicate – A predicate function that returns true if an attribute is to be -output.

    • -
    • memo – An optional memoizing dictionary. If a tuple has already been seen, the -recursion will be avoided.

    • +
    • iterable – An iterable sequence.

    • +
    • keyfunc – A function that extracts from the elements the sort key +to use and uniquify on. If left unspecified, the identify function +is used and the uniquification occurs on the elements themselves.

    • +
    • last – A boolean, True if we should keep the last item of the same keys. +Otherwise keep the first.

    Yields: - Attributes of the tuple and its sub-elements if the predicate is true.

    + Elements from the iterable.

    Source code in beancount/utils/misc_utils.py -
    def get_tuple_values(ntuple, predicate, memo=None):
    -    """Return all members referred to by this namedtuple instance that satisfy the
    -    given predicate. This function also works recursively on its members which
    -    are lists or tuples, and so it can be used for Transaction instances.
    -
    -    Args:
    -      ntuple: A tuple or namedtuple.
    -      predicate: A predicate function that returns true if an attribute is to be
    -        output.
    -      memo: An optional memoizing dictionary. If a tuple has already been seen, the
    -        recursion will be avoided.
    -    Yields:
    -      Attributes of the tuple and its sub-elements if the predicate is true.
    -    """
    -    if memo is None:
    -        memo = set()
    -    id_ntuple = id(ntuple)
    -    if id_ntuple in memo:
    -        return
    -    memo.add(id_ntuple)
    -
    -    if predicate(ntuple):
    -        yield
    -    for attribute in ntuple:
    -        if predicate(attribute):
    -            yield attribute
    -        if isinstance(attribute, (list, tuple)):
    -            for value in get_tuple_values(attribute, predicate, memo):
    -                yield value
    -
    +
    def sorted_uniquify(iterable, keyfunc=None, last=False):
    +    """Given a sequence of elements, sort and remove duplicates of the given key.
    +    Keep either the first or the last (by key) element of a sequence of
    +    key-identical elements. This does _not_ maintain the ordering of the
    +    original elements, they are returned sorted (by key) instead.
    +
    +    Args:
    +      iterable: An iterable sequence.
    +      keyfunc: A function that extracts from the elements the sort key
    +        to use and uniquify on. If left unspecified, the identify function
    +        is used and the uniquification occurs on the elements themselves.
    +      last: A boolean, True if we should keep the last item of the same keys.
    +        Otherwise keep the first.
    +    Yields:
    +      Elements from the iterable.
    +    """
    +    if keyfunc is None:
    +        keyfunc = lambda x: x
    +    if last:
    +        prev_obj = UNSET
    +        prev_key = UNSET
    +        for obj in sorted(iterable, key=keyfunc):
    +            key = keyfunc(obj)
    +            if key != prev_key and prev_obj is not UNSET:
    +                yield prev_obj
    +            prev_obj = obj
    +            prev_key = key
    +        if prev_obj is not UNSET:
    +            yield prev_obj
    +    else:
    +        prev_key = UNSET
    +        for obj in sorted(iterable, key=keyfunc):
    +            key = keyfunc(obj)
    +            if key != prev_key:
    +                yield obj
    +                prev_key = key
    +
    @@ -4211,16 +4118,16 @@

    -

    -beancount.utils.misc_utils.groupby(keyfun, elements) +

    +beancount.utils.misc_utils.staticvar(varname, initial_value) -

    +
    -

    Group the elements as a dict of lists, where the key is computed using the -function 'keyfun'.

    +

    Returns a decorator that defines a Python function attribute.

    +

    This is used to simulate a static function variable in Python.

    @@ -4232,8 +4139,8 @@

    @@ -4249,7 +4156,7 @@

    @@ -4257,24 +4164,24 @@

    Parameters:
      -
    • keyfun – A callable, used to obtain the group key from each element.

    • -
    • elements – An iterable of the elements to group.

    • +
    • varname – A string, the name of the variable to define.

    • +
    • initial_value – The value to initialize the variable to.

    Returns:
      -
    • A dict of key to list of sequences.

    • +
    • A function decorator.

    Source code in beancount/utils/misc_utils.py -
    def groupby(keyfun, elements):
    -    """Group the elements as a dict of lists, where the key is computed using the
    -    function 'keyfun'.
    -
    -    Args:
    -      keyfun: A callable, used to obtain the group key from each element.
    -      elements: An iterable of the elements to group.
    -    Returns:
    -      A dict of key to list of sequences.
    -    """
    -    # Note: We could allow a custom aggregation function. Another option is
    -    # provide another method to reduce the list values of a dict, but that can
    -    # be accomplished using a dict comprehension.
    -    grouped = defaultdict(list)
    -    for element in elements:
    -        grouped[keyfun(element)].append(element)
    -    return grouped
    -
    +
    def staticvar(varname, initial_value):
    +    """Returns a decorator that defines a Python function attribute.
    +
    +    This is used to simulate a static function variable in Python.
    +
    +    Args:
    +      varname: A string, the name of the variable to define.
    +      initial_value: The value to initialize the variable to.
    +    Returns:
    +      A function decorator.
    +    """
    +
    +    def deco(fun):
    +        setattr(fun, varname, initial_value)
    +        return fun
    +
    +    return deco
    +
    @@ -4286,15 +4193,15 @@

    -

    -beancount.utils.misc_utils.idify(string) +

    +beancount.utils.misc_utils.swallow(*exception_types) -

    +
    -

    Replace characters objectionable for a filename with underscores.

    +

    Catch and ignore certain exceptions.

    @@ -4306,44 +4213,31 @@

    -
    Parameters:
      -
    • string – Any string.

    • +
    • exception_types – A tuple of exception classes to ignore.

    - - - - - - - - - - - -
    Returns: -
      -
    • The input string, with offending characters replaced.

    • -
    -
    +

    Yields: + None.

    +
    Source code in beancount/utils/misc_utils.py -
    def idify(string):
    -    """Replace characters objectionable for a filename with underscores.
    -
    -    Args:
    -      string: Any string.
    -    Returns:
    -      The input string, with offending characters replaced.
    -    """
    -    for sfrom, sto in [(r'[ \(\)]+', '_'),
    -                      (r'_*\._*', '.')]:
    -        string = re.sub(sfrom, sto, string)
    -    string = string.strip('_')
    -    return string
    -
    +
    @contextlib.contextmanager
    +def swallow(*exception_types):
    +    """Catch and ignore certain exceptions.
    +
    +    Args:
    +      exception_types: A tuple of exception classes to ignore.
    +    Yields:
    +      None.
    +    """
    +    try:
    +        yield
    +    except Exception as exc:
    +        if not isinstance(exc, exception_types):
    +            raise
    +
    @@ -4355,16 +4249,19 @@

    -

    -beancount.utils.misc_utils.import_curses() +

    +beancount.utils.misc_utils.uniquify(iterable, keyfunc=None, last=False) -

    +
    -

    Try to import the 'curses' module. -(This is used here in order to override for tests.)

    +

    Given a sequence of elements, remove duplicates of the given key. Keep either +the first or the last element of a sequence of key-identical elements. Order +is maintained as much as possible. This does maintain the ordering of the +original elements, they are returned in the same order as the original +elements.

    @@ -4373,15 +4270,207 @@

    - - + + -
    Returns: + Parameters:
      -
    • The curses module, if it was possible to import it.

    • +
    • iterable – An iterable sequence.

    • +
    • keyfunc – A function that extracts from the elements the sort key +to use and uniquify on. If left unspecified, the identify function +is used and the uniquification occurs on the elements themselves.

    • +
    • last – A boolean, True if we should keep the last item of the same keys. +Otherwise keep the first.

    -
    +

    Yields: + Elements from the iterable.

    + +
    + Source code in beancount/utils/misc_utils.py +
    def uniquify(iterable, keyfunc=None, last=False):
    +    """Given a sequence of elements, remove duplicates of the given key. Keep either
    +    the first or the last element of a sequence of key-identical elements. Order
    +    is maintained as much as possible. This does maintain the ordering of the
    +    original elements, they are returned in the same order as the original
    +    elements.
    +
    +    Args:
    +      iterable: An iterable sequence.
    +      keyfunc: A function that extracts from the elements the sort key
    +        to use and uniquify on. If left unspecified, the identify function
    +        is used and the uniquification occurs on the elements themselves.
    +      last: A boolean, True if we should keep the last item of the same keys.
    +        Otherwise keep the first.
    +    Yields:
    +      Elements from the iterable.
    +    """
    +    if keyfunc is None:
    +        keyfunc = lambda x: x
    +    seen = set()
    +    if last:
    +        unique_reversed_list = []
    +        for obj in reversed(iterable):
    +            key = keyfunc(obj)
    +            if key not in seen:
    +                seen.add(key)
    +                unique_reversed_list.append(obj)
    +        yield from reversed(unique_reversed_list)
    +    else:
    +        for obj in iterable:
    +            key = keyfunc(obj)
    +            if key not in seen:
    +                seen.add(key)
    +                yield obj
    +
    +
    +
    + +
    + + + + + + +
    + +
    + +
    + + + +
    + + + +

    + beancount.utils.pager + + + +

    + +
    + +

    Code to write output to a pager.

    +

    This module contains an object accumulates lines up to a minimum and then +decides whether to flush them to the original output directly if under the +threshold (no pager) or creates a pager and flushes the lines to it if above the +threshold and then forwards all future lines to it. The purpose of this object +is to pipe output to a pager only if the number of lines to be printed exceeds a +minimum number of lines.

    +

    The contextmanager is intended to be used to pipe output to a pager and wait on +the pager to complete before continuing. Simply write to the file object and +upon exit we close the file object. This also silences broken pipe errors +triggered by the user exiting the sub-process, and recovers from a failing pager +command by just using stdout.

    + + + +
    + + + + + + + + + + +
    + + + +

    + +beancount.utils.pager.ConditionalPager + + + +

    + +
    + +

    A proxy file for a pager that only creates a pager after a minimum number of +lines has been printed to it.

    + + + + +
    + + + + + + + + + +
    + + + +

    +beancount.utils.pager.ConditionalPager.__enter__(self) + + + special + + +

    + +
    + +

    Initialize the context manager and return this instance as it.

    + +
    + Source code in beancount/utils/pager.py +
    def __enter__(self):
    +    """Initialize the context manager and return this instance as it."""
    +
    +    # The file and pipe object we're writing to. This gets set after the
    +    # number of accumulated lines reaches the threshold.
    +    if self.minlines:
    +        self.file = None
    +        self.pipe = None
    +    else:
    +        self.file, self.pipe = create_pager(self.command, self.default_file)
    +
    +    # Lines accumulated before the threshold.
    +    self.accumulated_data = []
    +    self.accumulated_lines = 0
    +
    +    # Return this object to be used as the context manager itself.
    +    return self
    +
    +
    +
    + +
    + + + +
    + + + +

    +beancount.utils.pager.ConditionalPager.__exit__(self, type, value, unused_traceback) + + + special + + +

    + +
    + +

    Context manager exit. This flushes the output to our output file.

    + @@ -4389,35 +4478,51 @@

    - +
    Exceptions:Parameters:
      -
    • ImportError – If the module could not be imported.

    • +
    • type – Optional exception type, as per context managers.

    • +
    • value – Optional exception value, as per context managers.

    • +
    • unused_traceback – Optional trace.

    - Source code in beancount/utils/misc_utils.py -
    def import_curses():
    -    """Try to import the 'curses' module.
    -    (This is used here in order to override for tests.)
    -
    -    Returns:
    -      The curses module, if it was possible to import it.
    -    Raises:
    -      ImportError: If the module could not be imported.
    -    """
    -    # Note: There's a recipe for getting terminal size on Windows here, without
    -    # curses, I should probably implement that at some point:
    -    # https://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window
    -    # Also, consider just using 'blessings' instead, which provides this across
    -    # multiple platforms.
    -    # pylint: disable=import-outside-toplevel
    -    import curses
    -    return curses
    -
    + Source code in beancount/utils/pager.py +
    def __exit__(self, type, value, unused_traceback):
    +    """Context manager exit. This flushes the output to our output file.
    +
    +    Args:
    +      type: Optional exception type, as per context managers.
    +      value: Optional exception value, as per context managers.
    +      unused_traceback: Optional trace.
    +    """
    +    try:
    +        if self.file:
    +            # Flush the output file and close it.
    +            self.file.flush()
    +        else:
    +            # Oops... we never reached the threshold. Flush the accumulated
    +            # output to the file.
    +            self.flush_accumulated(self.default_file)
    +
    +        # Wait for the subprocess (if we have one).
    +        if self.pipe:
    +            self.file.close()
    +            self.pipe.wait()
    +
    +    # Absorb broken pipes that may occur on flush or close above.
    +    except BrokenPipeError:
    +        return True
    +
    +    # Absorb broken pipes.
    +    if isinstance(value, BrokenPipeError):
    +        return True
    +    elif value:
    +        raise
    +
    @@ -4425,19 +4530,22 @@

    -
    +
    -

    -beancount.utils.misc_utils.is_sorted(iterable, key=<function <lambda> at 0x78e868b45bc0>, cmp=<function <lambda> at 0x78e868b45c60>) +

    +beancount.utils.pager.ConditionalPager.__init__(self, command, minlines=None) + + special + -

    +

    -

    Return true if the sequence is sorted.

    +

    Create a conditional pager.

    @@ -4449,50 +4557,36 @@

    -
    Parameters:
      -
    • iterable – An iterable sequence.

    • -
    • key – A function to extract the quantity by which to sort.

    • -
    • cmp – A function that compares two elements of a sequence.

    • +
    • command – A string, the shell command to run as a pager.

    • +
    • minlines – If set, the number of lines under which you should not bother starting +a pager. This avoids kicking off a pager if the screen is high enough to +render the contents. If the value is unset, always starts a pager (which is +fine behavior too).

    - - - - - - - - - - -
    Returns: -
      -
    • A boolean, true if the sequence is sorted.

    • -
    -
    - Source code in beancount/utils/misc_utils.py -
    def is_sorted(iterable, key=lambda x: x, cmp=lambda x, y: x <= y):
    -    """Return true if the sequence is sorted.
    -
    -    Args:
    -      iterable: An iterable sequence.
    -      key: A function to extract the quantity by which to sort.
    -      cmp: A function that compares two elements of a sequence.
    -    Returns:
    -      A boolean, true if the sequence is sorted.
    -    """
    -    iterator = map(key, iterable)
    -    prev = next(iterator)
    -    for element in iterator:
    -        if not cmp(prev, element):
    -            return False
    -        prev = element
    -    return True
    -
    + Source code in beancount/utils/pager.py +
    def __init__(self, command, minlines=None):
    +    """Create a conditional pager.
    +
    +    Args:
    +      command: A string, the shell command to run as a pager.
    +      minlines: If set, the number of lines under which you should not bother starting
    +        a pager. This avoids kicking off a pager if the screen is high enough to
    +        render the contents. If the value is unset, always starts a pager (which is
    +        fine behavior too).
    +    """
    +    self.command = command
    +    self.minlines = minlines
    +    self.default_file = (
    +        codecs.getwriter("utf-8")(sys.stdout.buffer)
    +        if hasattr(sys.stdout, "buffer")
    +        else sys.stdout
    +    )
    +
    @@ -4500,19 +4594,20 @@

    -
    +
    -

    -beancount.utils.misc_utils.log_time(operation_name, log_timings, indent=0) +

    +beancount.utils.pager.ConditionalPager.flush_accumulated(self, file) -

    +

    -

    A context manager that times the block and logs it to info level.

    +

    Flush the existing lines to the newly created pager. +This also disabled the accumulator.

    @@ -4524,42 +4619,28 @@

    -
    Parameters:
      -
    • operation_name – A string, a label for the name of the operation.

    • -
    • log_timings – A function to write log messages to. If left to None, -no timings are written (this becomes a no-op).

    • -
    • indent – An integer, the indentation level for the format of the timing -line. This is useful if you're logging timing to a hierarchy of -operations.

    • +
    • file – A file object to flush the accumulated data to.

    Yields: - The start time of the operation.

    - +
    - Source code in beancount/utils/misc_utils.py -
    @contextlib.contextmanager
    -def log_time(operation_name, log_timings, indent=0):
    -    """A context manager that times the block and logs it to info level.
    -
    -    Args:
    -      operation_name: A string, a label for the name of the operation.
    -      log_timings: A function to write log messages to. If left to None,
    -        no timings are written (this becomes a no-op).
    -      indent: An integer, the indentation level for the format of the timing
    -        line. This is useful if you're logging timing to a hierarchy of
    -        operations.
    -    Yields:
    -      The start time of the operation.
    -    """
    -    time1 = time()
    -    yield time1
    -    time2 = time()
    -    if log_timings:
    -        log_timings("Operation: {:48} Time: {}{:6.0f} ms".format(
    -            "'{}'".format(operation_name), '      '*indent, (time2 - time1) * 1000))
    -
    + Source code in beancount/utils/pager.py +
    def flush_accumulated(self, file):
    +    """Flush the existing lines to the newly created pager.
    +    This also disabled the accumulator.
    +
    +    Args:
    +      file: A file object to flush the accumulated data to.
    +    """
    +    if self.accumulated_data:
    +        write = file.write
    +        for data in self.accumulated_data:
    +            write(data)
    +    self.accumulated_data = None
    +    self.accumulated_lines = None
    +
    @@ -4567,19 +4648,19 @@

    -
    +
    -

    -beancount.utils.misc_utils.longest(seq) +

    +beancount.utils.pager.ConditionalPager.write(self, data) -

    +

    -

    Return the longest of the given subsequences.

    +

    Write the data out. Overridden from the file object interface.

    @@ -4591,45 +4672,38 @@

    -
    Parameters:
      -
    • seq – An iterable sequence of lists.

    • +
    • data – A string, data to write to the output.

    - - - - - - - - - - -
    Returns: -
      -
    • The longest list from the sequence.

    • -
    -
    - Source code in beancount/utils/misc_utils.py -
    def longest(seq):
    -    """Return the longest of the given subsequences.
    -
    -    Args:
    -      seq: An iterable sequence of lists.
    -    Returns:
    -      The longest list from the sequence.
    -    """
    -    longest, length = None, -1
    -    for element in seq:
    -        len_element = len(element)
    -        if len_element > length:
    -            longest, length = element, len_element
    -    return longest
    -
    + Source code in beancount/utils/pager.py +
    def write(self, data):
    +    """Write the data out. Overridden from the file object interface.
    +
    +    Args:
    +      data: A string, data to write to the output.
    +    """
    +    if self.file is None:
    +        # Accumulate the new lines.
    +        self.accumulated_lines += data.count("\n")
    +        self.accumulated_data.append(data)
    +
    +        # If we've reached the threshold, create a file.
    +        if self.accumulated_lines > self.minlines:
    +            self.file, self.pipe = create_pager(self.command, self.default_file)
    +            self.flush_accumulated(self.file)
    +    else:
    +        # We've already created a pager subprocess... flush the lines to it.
    +        self.file.write(data)
    +        # try:
    +        # except BrokenPipeError:
    +        #     # Make sure we don't barf on __exit__().
    +        #     self.file = self.pipe = None
    +        #     raise
    +
    @@ -4637,19 +4711,30 @@

    + + +

    + +
    + +
    + + + +
    -

    -beancount.utils.misc_utils.map_namedtuple_attributes(attributes, mapper, object_) +

    +beancount.utils.pager.create_pager(command, file) -

    +
    -

    Map the value of the named attributes of object by mapper.

    +

    Try to create and return a pager subprocess.

    @@ -4661,10 +4746,9 @@

    Parameters:

    @@ -4680,30 +4764,53 @@

    Returns:

      -
    • attributes – A sequence of string, the attribute names to map.

    • -
    • mapper – A callable that accepts the value of a field and returns -the new value.

    • -
    • object_ – Some namedtuple object with attributes on it.

    • +
    • command – A string, the shell command to run as a pager.

    • +
    • file – The file object for the pager write to. This is also used as a +default if we failed to create the pager subprocess.

      -
    • A new instance of the same namedtuple with the named fields mapped by -mapper.

    • +
    • A pair of (file, pipe), a file object and an optional subprocess.Popen instance +to wait on. The pipe instance may be set to None if we failed to create a subprocess.

    - Source code in beancount/utils/misc_utils.py -
    def map_namedtuple_attributes(attributes, mapper, object_):
    -    """Map the value of the named attributes of object by mapper.
    -
    -    Args:
    -      attributes: A sequence of string, the attribute names to map.
    -      mapper: A callable that accepts the value of a field and returns
    -        the new value.
    -      object_: Some namedtuple object with attributes on it.
    -    Returns:
    -      A new instance of the same namedtuple with the named fields mapped by
    -      mapper.
    -    """
    -    return object_._replace(**{attribute: mapper(getattr(object_, attribute))
    -                               for attribute in attributes})
    -
    + Source code in beancount/utils/pager.py +
    def create_pager(command, file):
    +    """Try to create and return a pager subprocess.
    +
    +    Args:
    +      command: A string, the shell command to run as a pager.
    +      file: The file object for the pager write to. This is also used as a
    +        default if we failed to create the pager subprocess.
    +    Returns:
    +      A pair of (file, pipe), a file object and an optional subprocess.Popen instance
    +      to wait on. The pipe instance may be set to None if we failed to create a subprocess.
    +    """
    +
    +    if command is None:
    +        command = os.environ.get("PAGER", DEFAULT_PAGER)
    +    if not command:
    +        command = DEFAULT_PAGER
    +
    +    pipe = None
    +
    +    # In case of using 'less', make sure the charset is set properly. In theory
    +    # you could override this by setting PAGER to "LESSCHARSET=utf-8 less" but
    +    # this shouldn't affect other programs and is unlikely to cause problems, so
    +    # we set it here to make default behavior work for most people (we always
    +    # write UTF-8).
    +    env = os.environ.copy()
    +    env["LESSCHARSET"] = "utf-8"
    +
    +    try:
    +        pipe = subprocess.Popen(
    +            command, shell=True, stdin=subprocess.PIPE, stdout=file, env=env
    +        )
    +    except OSError as exc:
    +        logging.error("Invalid pager: {}".format(exc))
    +    else:
    +        stdin_wrapper = io.TextIOWrapper(pipe.stdin, "utf-8")
    +        file = stdin_wrapper
    +    return file, pipe
    +
    @@ -4715,16 +4822,18 @@

    -beancount.utils.misc_utils.replace_namedtuple_values(ntuple, predicate, mapper, memo=None) +

    +beancount.utils.pager.flush_only(fileobj) -

    +
    -

    Recurse through all the members of namedtuples and lists, and for -members that match the given predicate, run them through the given mapper.

    +

    A contextmanager around a file object that does not close the file.

    +

    This is used to return a context manager on a file object but not close it. +We flush it instead. This is useful in order to provide an alternative to a +pager class as above.

    @@ -4736,60 +4845,34 @@

    Parameters:

      -
    • ntuple – A namedtuple instance.

    • -
    • predicate – A predicate function that returns true if an attribute is to be -output.

    • -
    • mapper – A callable, that will accept a single argument and return its -replacement value.

    • -
    • memo – An optional memoizing dictionary. If a tuple has already been seen, the -recursion will be avoided.

    • +
    • fileobj – A file object, to remain open after running the context manager.

    Yields: - Attributes of the tuple and its sub-elements if the predicate is true.

    + A context manager that yields this object.

    - Source code in beancount/utils/misc_utils.py -
    def replace_namedtuple_values(ntuple, predicate, mapper, memo=None):
    -    """Recurse through all the members of namedtuples and lists, and for
    -    members that match the given predicate, run them through the given mapper.
    -
    -    Args:
    -      ntuple: A namedtuple instance.
    -      predicate: A predicate function that returns true if an attribute is to be
    -        output.
    -      mapper: A callable, that will accept a single argument and return its
    -        replacement value.
    -      memo: An optional memoizing dictionary. If a tuple has already been seen, the
    -        recursion will be avoided.
    -    Yields:
    -      Attributes of the tuple and its sub-elements if the predicate is true.
    -    """
    -    if memo is None:
    -        memo = set()
    -    id_ntuple = id(ntuple)
    -    if id_ntuple in memo:
    -        return None
    -    memo.add(id_ntuple)
    -
    -    # pylint: disable=unidiomatic-typecheck
    -    if not (type(ntuple) is not tuple and isinstance(ntuple, tuple)):
    -        return ntuple
    -    replacements = {}
    -    for attribute_name, attribute in zip(ntuple._fields, ntuple):
    -        if predicate(attribute):
    -            replacements[attribute_name] = mapper(attribute)
    -        elif type(attribute) is not tuple and isinstance(attribute, tuple):
    -            replacements[attribute_name] = replace_namedtuple_values(
    -                attribute, predicate, mapper, memo)
    -        elif type(attribute) in (list, tuple):
    -            replacements[attribute_name] = [
    -                replace_namedtuple_values(member, predicate, mapper, memo)
    -                for member in attribute]
    -    return ntuple._replace(**replacements)
    -
    + Source code in beancount/utils/pager.py +
    @contextlib.contextmanager
    +def flush_only(fileobj):
    +    """A contextmanager around a file object that does not close the file.
    +
    +    This is used to return a context manager on a file object but not close it.
    +    We flush it instead. This is useful in order to provide an alternative to a
    +    pager class as above.
    +
    +    Args:
    +      fileobj: A file object, to remain open after running the context manager.
    +    Yields:
    +      A context manager that yields this object.
    +    """
    +    try:
    +        yield fileobj
    +    finally:
    +        fileobj.flush()
    +
    @@ -4797,173 +4880,121 @@

    -

    -beancount.utils.misc_utils.skipiter(iterable, num_skip) +

    +
    - +
    + + + +
    + + + +

    + beancount.utils.snoop + + + +

    -

    Skip some elements from an iterator.

    +

    Text manipulation utilities.

    - - - - - - - - - - - -
    Parameters: -
      -
    • iterable – An iterator.

    • -
    • num_skip – The number of elements in the period.

    • -
    -

    Yields: - Elements from the iterable, with num_skip elements skipped. - For example, skipiter(range(10), 3) yields [0, 3, 6, 9].

    -
    - Source code in beancount/utils/misc_utils.py -
    def skipiter(iterable, num_skip):
    -    """Skip some elements from an iterator.
    -
    -    Args:
    -      iterable: An iterator.
    -      num_skip: The number of elements in the period.
    -    Yields:
    -      Elements from the iterable, with num_skip elements skipped.
    -      For example, skipiter(range(10), 3) yields [0, 3, 6, 9].
    -    """
    -    assert num_skip > 0
    -    sit = iter(iterable)
    -    while 1:
    -        try:
    -            value = next(sit)
    -        except StopIteration:
    -            return
    -        yield value
    -        for _ in range(num_skip-1):
    -            try:
    -                next(sit)
    -            except StopIteration:
    -                return
    -
    -
    -
    -
    +
    -
    -

    -beancount.utils.misc_utils.sorted_uniquify(iterable, keyfunc=None, last=False) -

    + + + +
    + + + +

    + +beancount.utils.snoop.Snoop + + + +

    -

    Given a sequence of elements, sort and remove duplicates of the given key. -Keep either the first or the last (by key) element of a sequence of -key-identical elements. This does not maintain the ordering of the -original elements, they are returned sorted (by key) instead.

    +

    A snooper callable that just saves the returned values of a +function. This is particularly useful for re.match and re.search in +conditionals, e.g.::

    +

    snoop = Snoop() + ... + if snoop(re.match(r"(\d+)-(\d+)-(\d+)", text)): + year, month, date = snoop.value.group(1, 2, 3)

    - - - - - - - - - +

    Attributes:

    +
    Parameters: -
      -
    • iterable – An iterable sequence.

    • -
    • keyfunc – A function that extracts from the elements the sort key -to use and uniquify on. If left unspecified, the identify function -is used and the uniquification occurs on the elements themselves.

    • -
    • last – A boolean, True if we should keep the last item of the same keys. -Otherwise keep the first.

    • -
    -
    + + + + + + + + + + + + + + + + + -
    NameTypeDescription
    value

    The last value snooped from a function call.

    history

    If 'maxlen' was specified, the last few values +that were snooped.

    Yields: - Elements from the iterable.

    + -
    - Source code in beancount/utils/misc_utils.py -
    def sorted_uniquify(iterable, keyfunc=None, last=False):
    -    """Given a sequence of elements, sort and remove duplicates of the given key.
    -    Keep either the first or the last (by key) element of a sequence of
    -    key-identical elements. This does _not_ maintain the ordering of the
    -    original elements, they are returned sorted (by key) instead.
    -
    -    Args:
    -      iterable: An iterable sequence.
    -      keyfunc: A function that extracts from the elements the sort key
    -        to use and uniquify on. If left unspecified, the identify function
    -        is used and the uniquification occurs on the elements themselves.
    -      last: A boolean, True if we should keep the last item of the same keys.
    -        Otherwise keep the first.
    -    Yields:
    -      Elements from the iterable.
    -    """
    -    if keyfunc is None:
    -        keyfunc = lambda x: x
    -    if last:
    -        prev_obj = UNSET
    -        prev_key = UNSET
    -        for obj in sorted(iterable, key=keyfunc):
    -            key = keyfunc(obj)
    -            if key != prev_key and prev_obj is not UNSET:
    -                yield prev_obj
    -            prev_obj = obj
    -            prev_key = key
    -        if prev_obj is not UNSET:
    -            yield prev_obj
    -    else:
    -        prev_key = UNSET
    -        for obj in sorted(iterable, key=keyfunc):
    -            key = keyfunc(obj)
    -            if key != prev_key:
    -                yield obj
    -                prev_key = key
    -
    -
    -
    -
    +
    -
    -

    -beancount.utils.misc_utils.staticvar(varname, initial_value) -

    + + +
    + + + +

    +beancount.utils.snoop.Snoop.__call__(self, value) + + + special + + +

    -

    Returns a decorator that defines a Python function attribute.

    -

    This is used to simulate a static function variable in Python.

    +

    Save a value to the snooper. This is meant to wrap +a function call.

    @@ -4975,8 +5006,7 @@

    @@ -4992,30 +5022,28 @@

    Parameters:
      -
    • varname – A string, the name of the variable to define.

    • -
    • initial_value – The value to initialize the variable to.

    • +
    • value – The value to push/save.

    Returns:
      -
    • A function decorator.

    • +
    • Value itself.

    - Source code in beancount/utils/misc_utils.py -
    def staticvar(varname, initial_value):
    -    """Returns a decorator that defines a Python function attribute.
    -
    -    This is used to simulate a static function variable in Python.
    -
    -    Args:
    -      varname: A string, the name of the variable to define.
    -      initial_value: The value to initialize the variable to.
    -    Returns:
    -      A function decorator.
    -    """
    -    def deco(fun):
    -        setattr(fun, varname, initial_value)
    -        return fun
    -    return deco
    -
    + Source code in beancount/utils/snoop.py +
    def __call__(self, value):
    +    """Save a value to the snooper. This is meant to wrap
    +    a function call.
    +
    +    Args:
    +      value: The value to push/save.
    +    Returns:
    +      Value itself.
    +    """
    +    self.value = value
    +    if self.history is not None:
    +        self.history.append(value)
    +    return value
    +
    @@ -5023,19 +5051,22 @@

    -
    +
    -

    -beancount.utils.misc_utils.swallow(*exception_types) +

    +beancount.utils.snoop.Snoop.__getattr__(self, attr) + + special + -

    +

    -

    Catch and ignore certain exceptions.

    +

    Forward the attribute to the value.

    @@ -5047,31 +5078,40 @@

    -
    Parameters:
      -
    • exception_types – A tuple of exception classes to ignore.

    • +
    • attr – A string, the name of the attribute.

    Yields: - None.

    - + + + + + + + + + + + + +
    Returns: +
      +
    • The value of the attribute.

    • +
    +
    - Source code in beancount/utils/misc_utils.py -
    @contextlib.contextmanager
    -def swallow(*exception_types):
    -    """Catch and ignore certain exceptions.
    -
    -    Args:
    -      exception_types: A tuple of exception classes to ignore.
    -    Yields:
    -      None.
    -    """
    -    try:
    -        yield
    -    except Exception as exc:
    -        if not isinstance(exc, exception_types):
    -            raise
    -
    + Source code in beancount/utils/snoop.py +
    def __getattr__(self, attr):
    +    """Forward the attribute to the value.
    +
    +    Args:
    +      attr: A string, the name of the attribute.
    +    Returns:
    +      The value of the attribute.
    +    """
    +    return getattr(self.value, attr)
    +
    @@ -5079,23 +5119,22 @@

    -
    +
    -

    -beancount.utils.misc_utils.uniquify(iterable, keyfunc=None, last=False) +

    +beancount.utils.snoop.Snoop.__init__(self, maxlen=None) + + special + -

    +

    -

    Given a sequence of elements, remove duplicates of the given key. Keep either -the first or the last element of a sequence of key-identical elements. Order -is maintained as much as possible. This does maintain the ordering of the -original elements, they are returned in the same order as the original -elements.

    +

    Create a new snooper.

    @@ -5107,56 +5146,24 @@

    -
    Parameters:
      -
    • iterable – An iterable sequence.

    • -
    • keyfunc – A function that extracts from the elements the sort key -to use and uniquify on. If left unspecified, the identify function -is used and the uniquification occurs on the elements themselves.

    • -
    • last – A boolean, True if we should keep the last item of the same keys. -Otherwise keep the first.

    • +
    • maxlen – If specified, an integer, which enables the saving of that

    Yields: - Elements from the iterable.

    - +
    - Source code in beancount/utils/misc_utils.py -
    def uniquify(iterable, keyfunc=None, last=False):
    -    """Given a sequence of elements, remove duplicates of the given key. Keep either
    -    the first or the last element of a sequence of key-identical elements. Order
    -    is maintained as much as possible. This does maintain the ordering of the
    -    original elements, they are returned in the same order as the original
    -    elements.
    -
    -    Args:
    -      iterable: An iterable sequence.
    -      keyfunc: A function that extracts from the elements the sort key
    -        to use and uniquify on. If left unspecified, the identify function
    -        is used and the uniquification occurs on the elements themselves.
    -      last: A boolean, True if we should keep the last item of the same keys.
    -        Otherwise keep the first.
    -    Yields:
    -      Elements from the iterable.
    -    """
    -    if keyfunc is None:
    -        keyfunc = lambda x: x
    -    seen = set()
    -    if last:
    -        unique_reversed_list = []
    -        for obj in reversed(iterable):
    -            key = keyfunc(obj)
    -            if key not in seen:
    -                seen.add(key)
    -                unique_reversed_list.append(obj)
    -        yield from reversed(unique_reversed_list)
    -    else:
    -        for obj in iterable:
    -            key = keyfunc(obj)
    -            if key not in seen:
    -                seen.add(key)
    -                yield obj
    -
    + Source code in beancount/utils/snoop.py +
    def __init__(self, maxlen=None):
    +    """Create a new snooper.
    +
    +    Args:
    +      maxlen: If specified, an integer, which enables the saving of that
    +      number of last values in the history attribute.
    +    """
    +    self.value = None
    +    self.history = collections.deque(maxlen=maxlen) if maxlen else None
    +
    @@ -5166,7 +5173,6 @@

    -

    @@ -5175,108 +5181,51 @@

    -
    +
    -

    - beancount.utils.net_utils +

    +beancount.utils.snoop.snoopify(function) -

    +

    -

    Network utilities.

    - - - -
    - - - - - - - - - - -
    - - +

    Decorate a function as snoopable.

    +

    This is meant to reassign existing functions to a snoopable version of them. +For example, if you wanted 're.match' to be automatically snoopable, just +decorate it like this:

    +

    re.match = snoopify(re.match)

    +

    and then you can just call 're.match' in a conditional and then access +'re.match.value' to get to the last returned value.

    -

    -beancount.utils.net_utils.retrying_urlopen(url, timeout=5, max_retry=5) +
    + Source code in beancount/utils/snoop.py +
    def snoopify(function):
    +    """Decorate a function as snoopable.
     
    +    This is meant to reassign existing functions to a snoopable version of them.
    +    For example, if you wanted 're.match' to be automatically snoopable, just
    +    decorate it like this:
     
    -

    + re.match = snoopify(re.match) -
    + and then you can just call 're.match' in a conditional and then access + 're.match.value' to get to the last returned value. + """ -

    Open and download the given URL, retrying if it times out.

    + @functools.wraps(function) + def wrapper(*args, **kw): + value = function(*args, **kw) + wrapper.value = value + return value - - - - - - - - - - - -
    Parameters: -
      -
    • url – A string, the URL to fetch.

    • -
    • timeout – A timeout after which to stop waiting for a response and return an -error.

    • -
    • max_retry – The maximum number of times to retry.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • The contents of the fetched URL.

    • -
    -
    -
    - Source code in beancount/utils/net_utils.py -
    def retrying_urlopen(url, timeout=5, max_retry=5):
    -    """Open and download the given URL, retrying if it times out.
    -
    -    Args:
    -      url: A string, the URL to fetch.
    -      timeout: A timeout after which to stop waiting for a response and return an
    -        error.
    -      max_retry: The maximum number of times to retry.
    -    Returns:
    -      The contents of the fetched URL.
    -    """
    -    for _ in range(max_retry):
    -        logging.debug("Reading %s", url)
    -        try:
    -            response = request.urlopen(url, timeout=timeout)
    -            if response:
    -                break
    -        except error.URLError:
    -            return None
    -    if response and response.getcode() != 200:
    -        return None
    -    return response
    -
    + wrapper.value = None + return wrapper +
    @@ -5299,27 +5248,16 @@

    -

    - beancount.utils.pager +

    + beancount.utils.table -

    +
    -

    Code to write output to a pager.

    -

    This module contains an object accumulates lines up to a minimum and then -decides whether to flush them to the original output directly if under the -threshold (no pager) or creates a pager and flushes the lines to it if above the -threshold and then forwards all future lines to it. The purpose of this object -is to pipe output to a pager only if the number of lines to be printed exceeds a -minimum number of lines.

    -

    The contextmanager is intended to be used to pipe output to a pager and wait on -the pager to complete before continuing. Simply write to the file object and -upon exit we close the file object. This also silences broken pipe errors -triggered by the user exiting the sub-process, and recovers from a failing pager -command by just using stdout.

    +

    Table rendering.

    @@ -5333,23 +5271,22 @@

    -
    -

    +

    -beancount.utils.pager.ConditionalPager +beancount.utils.table.Table (tuple) + -

    +

    -

    A proxy file for a pager that only creates a pager after a minimum number of -lines has been printed to it.

    +

    Table(columns, header, body)

    @@ -5368,177 +5305,25 @@

    -

    -beancount.utils.pager.ConditionalPager.__enter__(self) - - - special - - -

    - -
    - -

    Initialize the context manager and return this instance as it.

    - -
    - Source code in beancount/utils/pager.py -
    def __enter__(self):
    -    """Initialize the context manager and return this instance as it."""
    -
    -    # The file and pipe object we're writing to. This gets set after the
    -    # number of accumulated lines reaches the threshold.
    -    if self.minlines:
    -        self.file = None
    -        self.pipe = None
    -    else:
    -        self.file, self.pipe = create_pager(self.command, self.default_file)
    -
    -    # Lines accumulated before the threshold.
    -    self.accumulated_data = []
    -    self.accumulated_lines = 0
    -
    -    # Return this object to be used as the context manager itself.
    -    return self
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.utils.pager.ConditionalPager.__exit__(self, type, value, unused_traceback) - - - special - - -

    - -
    - -

    Context manager exit. This flushes the output to our output file.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • type – Optional exception type, as per context managers.

    • -
    • value – Optional exception value, as per context managers.

    • -
    • unused_traceback – Optional trace.

    • -
    -
    -
    - Source code in beancount/utils/pager.py -
    def __exit__(self, type, value, unused_traceback):
    -    """Context manager exit. This flushes the output to our output file.
    -
    -    Args:
    -      type: Optional exception type, as per context managers.
    -      value: Optional exception value, as per context managers.
    -      unused_traceback: Optional trace.
    -    """
    -    try:
    -        if self.file:
    -            # Flush the output file and close it.
    -            self.file.flush()
    -        else:
    -            # Oops... we never reached the threshold. Flush the accumulated
    -            # output to the file.
    -            self.flush_accumulated(self.default_file)
    -
    -        # Wait for the subprocess (if we have one).
    -        if self.pipe:
    -            self.file.close()
    -            self.pipe.wait()
    -
    -    # Absorb broken pipes that may occur on flush or close above.
    -    except BrokenPipeError:
    -        return True
    -
    -    # Absorb broken pipes.
    -    if isinstance(value, BrokenPipeError):
    -        return True
    -    elif value:
    -        raise
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.utils.pager.ConditionalPager.__init__(self, command, minlines=None) +

    +beancount.utils.table.Table.__getnewargs__(self) special -

    +
    -

    Create a conditional pager.

    +

    Return self as a plain tuple. Used by copy and pickle.

    - - - - - - - - - - - -
    Parameters: -
      -
    • command – A string, the shell command to run as a pager.

    • -
    • minlines – If set, the number of lines under which you should not bother starting -a pager. This avoids kicking off a pager if the screen is high enough to -render the contents. If the value is unset, always starts a pager (which is -fine behavior too).

    • -
    -
    - Source code in beancount/utils/pager.py -
    def __init__(self, command, minlines=None):
    -    """Create a conditional pager.
    -
    -    Args:
    -      command: A string, the shell command to run as a pager.
    -      minlines: If set, the number of lines under which you should not bother starting
    -        a pager. This avoids kicking off a pager if the screen is high enough to
    -        render the contents. If the value is unset, always starts a pager (which is
    -        fine behavior too).
    -    """
    -    self.command = command
    -    self.minlines = minlines
    -    self.default_file = (codecs.getwriter("utf-8")(sys.stdout.buffer)
    -                         if hasattr(sys.stdout, 'buffer') else
    -                         sys.stdout)
    -
    + Source code in beancount/utils/table.py +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +
    @@ -5550,50 +5335,20 @@

    -beancount.utils.pager.ConditionalPager.flush_accumulated(self, file) +

    +beancount.utils.table.Table.__new__(_cls, columns, header, body) + + special + staticmethod + -

    +
    -

    Flush the existing lines to the newly created pager. -This also disabled the accumulator.

    +

    Create new instance of Table(columns, header, body)

    - - - - - - - - - - - -
    Parameters: -
      -
    • file – A file object to flush the accumulated data to.

    • -
    -
    -
    - Source code in beancount/utils/pager.py -
    def flush_accumulated(self, file):
    -    """Flush the existing lines to the newly created pager.
    -    This also disabled the accumulator.
    -
    -    Args:
    -      file: A file object to flush the accumulated data to.
    -    """
    -    if self.accumulated_data:
    -        write = file.write
    -        for data in self.accumulated_data:
    -            write(data)
    -    self.accumulated_data = None
    -    self.accumulated_lines = None
    -
    -
    @@ -5604,58 +5359,25 @@

    -beancount.utils.pager.ConditionalPager.write(self, data) +

    +beancount.utils.table.Table.__repr__(self) + + special + -

    +
    -

    Write the data out. Overridden from the file object interface.

    +

    Return a nicely formatted representation string

    - - - - - - - - - - - -
    Parameters: -
      -
    • data – A string, data to write to the output.

    • -
    -
    - Source code in beancount/utils/pager.py -
    def write(self, data):
    -    """Write the data out. Overridden from the file object interface.
    -
    -    Args:
    -      data: A string, data to write to the output.
    -    """
    -    if self.file is None:
    -        # Accumulate the new lines.
    -        self.accumulated_lines += data.count('\n')
    -        self.accumulated_data.append(data)
    -
    -        # If we've reached the threshold, create a file.
    -        if self.accumulated_lines > self.minlines:
    -            self.file, self.pipe = create_pager(self.command, self.default_file)
    -            self.flush_accumulated(self.file)
    -    else:
    -        # We've already created a pager subprocess... flush the lines to it.
    -        self.file.write(data)
    -        # try:
    -        # except BrokenPipeError:
    -        #     # Make sure we don't barf on __exit__().
    -        #     self.file = self.pipe = None
    -        #     raise
    -
    + Source code in beancount/utils/table.py +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +
    @@ -5678,15 +5400,15 @@

    -

    -beancount.utils.pager.create_pager(command, file) +

    +beancount.utils.table.attribute_to_title(fieldname) -

    +
    -

    Try to create and return a pager subprocess.

    +

    Convert programming id into readable field name.

    @@ -5698,9 +5420,7 @@

    @@ -5716,54 +5436,24 @@

    Parameters:
      -
    • command – A string, the shell command to run as a pager.

    • -
    • file – The file object for the pager write to. This is also used as a -default if we failed to create the pager subprocess.

    • +
    • fieldname – A string, a programming ids, such as 'book_value'.

    Returns:
      -
    • A pair of (file, pipe), a file object and an optional subprocess.Popen instance -to wait on. The pipe instance may be set to None if we failed to create a subprocess.

    • +
    • A readable string, such as 'Book Value.'

    - Source code in beancount/utils/pager.py -
    def create_pager(command, file):
    -    """Try to create and return a pager subprocess.
    -
    -    Args:
    -      command: A string, the shell command to run as a pager.
    -      file: The file object for the pager write to. This is also used as a
    -        default if we failed to create the pager subprocess.
    -    Returns:
    -      A pair of (file, pipe), a file object and an optional subprocess.Popen instance
    -      to wait on. The pipe instance may be set to None if we failed to create a subprocess.
    -    """
    -
    -    if command is None:
    -        command = os.environ.get('PAGER', DEFAULT_PAGER)
    -    if not command:
    -        command = DEFAULT_PAGER
    -
    -    pipe = None
    -
    -    # In case of using 'less', make sure the charset is set properly. In theory
    -    # you could override this by setting PAGER to "LESSCHARSET=utf-8 less" but
    -    # this shouldn't affect other programs and is unlikely to cause problems, so
    -    # we set it here to make default behavior work for most people (we always
    -    # write UTF-8).
    -    env = os.environ.copy()
    -    env['LESSCHARSET'] = "utf-8"
    -
    -    try:
    -        pipe = subprocess.Popen(command, shell=True,
    -                                stdin=subprocess.PIPE,
    -                                stdout=file,
    -                                env=env)
    -    except OSError as exc:
    -        logging.error("Invalid pager: {}".format(exc))
    -    else:
    -        stdin_wrapper = io.TextIOWrapper(pipe.stdin, 'utf-8')
    -        file = stdin_wrapper
    -    return file, pipe
    -
    + Source code in beancount/utils/table.py +
    def attribute_to_title(fieldname):
    +    """Convert programming id into readable field name.
    +
    +    Args:
    +      fieldname: A string, a programming ids, such as 'book_value'.
    +    Returns:
    +      A readable string, such as 'Book Value.'
    +    """
    +    return fieldname.replace("_", " ").title()
    +
    @@ -5775,18 +5465,15 @@

    -

    -beancount.utils.pager.flush_only(fileobj) +

    +beancount.utils.table.compute_table_widths(rows) -

    +
    -

    A contextmanager around a file object that does not close the file.

    -

    This is used to return a context manager on a file object but not close it. -We flush it instead. This is useful in order to provide an alternative to a -pager class as above.

    +

    Compute the max character widths of a list of rows.

    @@ -5798,34 +5485,73 @@

    -
    Parameters:
      -
    • fileobj – A file object, to remain open after running the context manager.

    • +
    • rows – A list of rows, which are sequences of strings.

    Yields: - A context manager that yields this object.

    - + + + + + + + + + + + + +
    Returns: +
      +
    • A list of integers, the maximum widths required to render the columns of +this table.

    • +
    +
    + + + + + + + + + + + +
    Exceptions: +
      +
    • IndexError – If the rows are of different lengths.

    • +
    +
    - Source code in beancount/utils/pager.py -
    @contextlib.contextmanager
    -def flush_only(fileobj):
    -    """A contextmanager around a file object that does not close the file.
    -
    -    This is used to return a context manager on a file object but not close it.
    -    We flush it instead. This is useful in order to provide an alternative to a
    -    pager class as above.
    -
    -    Args:
    -      fileobj: A file object, to remain open after running the context manager.
    -    Yields:
    -      A context manager that yields this object.
    -    """
    -    try:
    -        yield fileobj
    -    finally:
    -        fileobj.flush()
    -
    + Source code in beancount/utils/table.py +
    def compute_table_widths(rows):
    +    """Compute the max character widths of a list of rows.
    +
    +    Args:
    +      rows: A list of rows, which are sequences of strings.
    +    Returns:
    +      A list of integers, the maximum widths required to render the columns of
    +      this table.
    +    Raises:
    +      IndexError: If the rows are of different lengths.
    +    """
    +    row_iter = iter(rows)
    +    first_row = next(row_iter)
    +    num_columns = len(first_row)
    +    column_widths = [len(cell) for cell in first_row]
    +    for row in row_iter:
    +        for i, cell in enumerate(row):
    +            if not isinstance(cell, str):
    +                cell = str(cell)
    +            cell_len = len(cell)
    +            if cell_len > column_widths[i]:
    +                column_widths[i] = cell_len
    +        if i + 1 != num_columns:
    +            raise IndexError("Invalid number of rows")
    +    return column_widths
    +
    @@ -5833,60 +5559,19 @@

    - - - -

    - -
    - -
    - - - -
    - - - -

    - beancount.utils.regexp_utils - - - -

    - -
    - -

    Regular expression helpers.

    - - - -
    - - - - - - - - - - -
    -

    -beancount.utils.regexp_utils.re_replace_unicode(regexp) +

    +beancount.utils.table.create_table(rows, field_spec=None) -

    +
    -

    Substitute Unicode Properties in regular expressions with their -corresponding expanded ranges.

    +

    Convert a list of tuples to an table report object.

    @@ -5898,7 +5583,13 @@

    Parameters:

    @@ -5914,27 +5605,99 @@

    Returns:

      -
    • regexp – A regular expression string.

    • +
    • rows – A list of tuples.

    • +
    • field_spec – A list of strings, or a list of +(FIELDNAME-OR-INDEX, HEADER, FORMATTER-FUNCTION) +triplets, that selects a subset of the fields is to be rendered as well +as their ordering. If this is a dict, the values are functions to call +on the fields to render them. If a function is set to None, we will just +call str() on the field.

      -
    • Return the regular expression with Unicode Properties substituted.

    • +
    • A Table instance.

    - Source code in beancount/utils/regexp_utils.py -
    def re_replace_unicode(regexp):
    -    """Substitute Unicode Properties in regular expressions with their
    -    corresponding expanded ranges.
    -
    -    Args:
    -      regexp: A regular expression string.
    -    Returns:
    -      Return the regular expression with Unicode Properties substituted.
    -    """
    -    for category, rangestr in UNICODE_RANGES.items():
    -        regexp = regexp.replace(r'\p{' + category + '}', rangestr)
    -    return regexp
    -
    + Source code in beancount/utils/table.py +
    def create_table(rows, field_spec=None):
    +    """Convert a list of tuples to an table report object.
    +
    +    Args:
    +      rows: A list of tuples.
    +      field_spec: A list of strings, or a list of
    +        (FIELDNAME-OR-INDEX, HEADER, FORMATTER-FUNCTION)
    +        triplets, that selects a subset of the fields is to be rendered as well
    +        as their ordering. If this is a dict, the values are functions to call
    +        on the fields to render them. If a function is set to None, we will just
    +        call str() on the field.
    +    Returns:
    +      A Table instance.
    +    """
    +    # Normalize field_spec to a dict.
    +    if field_spec is None:
    +        namedtuple_class = type(rows[0])
    +        field_spec = [(field, None, None) for field in namedtuple_class._fields]
    +
    +    elif isinstance(field_spec, (list, tuple)):
    +        new_field_spec = []
    +        for field in field_spec:
    +            if isinstance(field, tuple):
    +                assert len(field) <= 3, field
    +                if len(field) == 1:
    +                    field = field[0]
    +                    new_field_spec.append((field, None, None))
    +                elif len(field) == 2:
    +                    field, header = field
    +                    new_field_spec.append((field, header, None))
    +                elif len(field) == 3:
    +                    new_field_spec.append(field)
    +            else:
    +                if isinstance(field, str):
    +                    title = attribute_to_title(field)
    +                elif isinstance(field, int):
    +                    title = "Field {}".format(field)
    +                else:
    +                    raise ValueError("Invalid type for column name")
    +                new_field_spec.append((field, title, None))
    +
    +        field_spec = new_field_spec
    +
    +    # Ensure a nicely formatted header.
    +    field_spec = [
    +        (
    +            (name, attribute_to_title(name), formatter)
    +            if header_ is None
    +            else (name, header_, formatter)
    +        )
    +        for (name, header_, formatter) in field_spec
    +    ]
    +
    +    assert isinstance(field_spec, list), field_spec
    +    assert all(len(x) == 3 for x in field_spec), field_spec
    +
    +    # Compute the column names.
    +    columns = [name for (name, _, __) in field_spec]
    +
    +    # Compute the table header.
    +    header = [header_column for (_, header_column, __) in field_spec]
    +
    +    # Compute the table body.
    +    body = []
    +    for row in rows:
    +        body_row = []
    +        for name, _, formatter in field_spec:
    +            if isinstance(name, str):
    +                value = getattr(row, name)
    +            elif isinstance(name, int):
    +                value = row[name]
    +            else:
    +                raise ValueError("Invalid type for column name")
    +            if value is not None:
    +                if formatter is not None:
    +                    value = formatter(value)
    +                else:
    +                    value = str(value)
    +            else:
    +                value = ""
    +            body_row.append(value)
    +        body.append(body_row)
    +
    +    return Table(columns, header, body)
    +
    @@ -5942,121 +5705,101 @@

    -

    - -
    - -
    - - - -
    - - - -

    - beancount.utils.snoop - - - -

    - -
    - -

    Text manipulation utilities.

    - - - -
    - - - - - - - - - - - -
    - - - -

    - -beancount.utils.snoop.Snoop - +

    +beancount.utils.table.render_table(table_, output, output_format, css_id=None, css_class=None) -

    +
    -

    A snooper callable that just saves the returned values of a -function. This is particularly useful for re.match and re.search in -conditionals, e.g.::

    -

    snoop = Snoop() - ... - if snoop(re.match(r"(\d+)-(\d+)-(\d+)", text)): - year, month, date = snoop.value.group(1, 2, 3)

    - -

    Attributes:

    - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescription
    value

    The last value snooped from a function call.

    history

    If 'maxlen' was specified, the last few values -that were snooped.

    - - - -
    - - - - +

    Render the given table to the output file object in the requested format.

    +

    The table gets written out to the 'output' file.

    + + + + + + + + + + + +
    Parameters: +
      +
    • table_ – An instance of Table.

    • +
    • output – A file object you can write to.

    • +
    • output_format – A string, the format to write the table to, +either 'csv', 'txt' or 'html'.

    • +
    • css_id – A string, an optional CSS id for the table object (only used for HTML).

    • +
    • css_class – A string, an optional CSS class for the table object (only used for HTML).

    • +
    +
    +
    + Source code in beancount/utils/table.py +
    def render_table(table_, output, output_format, css_id=None, css_class=None):
    +    """Render the given table to the output file object in the requested format.
    +
    +    The table gets written out to the 'output' file.
    +
    +    Args:
    +      table_: An instance of Table.
    +      output: A file object you can write to.
    +      output_format: A string, the format to write the table to,
    +        either 'csv', 'txt' or 'html'.
    +      css_id: A string, an optional CSS id for the table object (only used for HTML).
    +      css_class: A string, an optional CSS class for the table object (only used for HTML).
    +    """
    +    if output_format in ("txt", "text"):
    +        text = table_to_text(table_, "  ", formats={"*": ">", "account": "<"})
    +        output.write(text)
    +
    +    elif output_format in ("csv",):
    +        table_to_csv(table_, file=output)
    +
    +    elif output_format in ("htmldiv", "html"):
    +        if output_format == "html":
    +            output.write("<html>\n")
    +            output.write("<body>\n")
    +
    +        output.write('<div id="{}">\n'.format(css_id) if css_id else "<div>\n")
    +        classes = [css_class] if css_class else None
    +        table_to_html(table_, file=output, classes=classes)
    +        output.write("</div>\n")
    +
    +        if output_format == "html":
    +            output.write("</body>\n")
    +            output.write("</html>\n")
    +
    +    else:
    +        raise NotImplementedError("Unsupported format: {}".format(output_format))
    +
    +
    +
    +
    -
    +
    -

    -beancount.utils.snoop.Snoop.__call__(self, value) +

    +beancount.utils.table.table_to_csv(table, file=None, **kwargs) - - special - -

    +
    -

    Save a value to the snooper. This is meant to wrap -a function call.

    +

    Render a Table to a CSV file.

    @@ -6068,7 +5811,10 @@

    @@ -6084,28 +5830,37 @@

    Parameters:
      -
    • value – The value to push/save.

    • +
    • table – An instance of a Table.

    • +
    • file – A file object to write to. If no object is provided, this +function returns a string.

    • +
    • **kwargs – Optional arguments forwarded to csv.writer().

    Returns:
      -
    • Value itself.

    • +
    • A string, the rendered table, or None, if a file object is provided +to write to.

    - Source code in beancount/utils/snoop.py -
    def __call__(self, value):
    -    """Save a value to the snooper. This is meant to wrap
    -    a function call.
    -
    -    Args:
    -      value: The value to push/save.
    -    Returns:
    -      Value itself.
    -    """
    -    self.value = value
    -    if self.history is not None:
    -        self.history.append(value)
    -    return value
    -
    + Source code in beancount/utils/table.py +
    def table_to_csv(table, file=None, **kwargs):
    +    """Render a Table to a CSV file.
    +
    +    Args:
    +      table: An instance of a Table.
    +      file: A file object to write to. If no object is provided, this
    +        function returns a string.
    +      **kwargs: Optional arguments forwarded to csv.writer().
    +    Returns:
    +      A string, the rendered table, or None, if a file object is provided
    +      to write to.
    +    """
    +    output_file = file or io.StringIO()
    +
    +    writer = csv.writer(output_file, **kwargs)
    +    if table.header:
    +        writer.writerow(table.header)
    +    writer.writerows(table.body)
    +
    +    if not file:
    +        return output_file.getvalue()
    +
    @@ -6113,22 +5868,19 @@

    -
    +
    -

    -beancount.utils.snoop.Snoop.__getattr__(self, attr) +

    +beancount.utils.table.table_to_html(table, classes=None, file=None) - - special - -

    +

    -

    Forward the attribute to the value.

    +

    Render a Table to HTML.

    @@ -6140,7 +5892,10 @@

    @@ -6156,24 +5911,54 @@

    Parameters:
      -
    • attr – A string, the name of the attribute.

    • +
    • table – An instance of a Table.

    • +
    • classes – A list of string, CSS classes to set on the table.

    • +
    • file – A file object to write to. If no object is provided, this +function returns a string.

    Returns:
      -
    • The value of the attribute.

    • +
    • A string, the rendered table, or None, if a file object is provided +to write to.

    - Source code in beancount/utils/snoop.py -
    def __getattr__(self, attr):
    -    """Forward the attribute to the value.
    -
    -    Args:
    -      attr: A string, the name of the attribute.
    -    Returns:
    -      The value of the attribute.
    -    """
    -    return getattr(self.value, attr)
    -
    + Source code in beancount/utils/table.py +
    def table_to_html(table, classes=None, file=None):
    +    """Render a Table to HTML.
    +
    +    Args:
    +      table: An instance of a Table.
    +      classes: A list of string, CSS classes to set on the table.
    +      file: A file object to write to. If no object is provided, this
    +        function returns a string.
    +    Returns:
    +      A string, the rendered table, or None, if a file object is provided
    +      to write to.
    +    """
    +    # Initialize file.
    +    oss = io.StringIO() if file is None else file
    +    oss.write('<table class="{}">\n'.format(" ".join(classes or [])))
    +
    +    # Render header.
    +    if table.header:
    +        oss.write("  <thead>\n")
    +        oss.write("    <tr>\n")
    +        for header in table.header:
    +            oss.write("      <th>{}</th>\n".format(header))
    +        oss.write("    </tr>\n")
    +        oss.write("  </thead>\n")
    +
    +    # Render body.
    +    oss.write("  <tbody>\n")
    +    for row in table.body:
    +        oss.write("    <tr>\n")
    +        for cell in row:
    +            oss.write("      <td>{}</td>\n".format(cell))
    +        oss.write("    </tr>\n")
    +    oss.write("  </tbody>\n")
    +
    +    # Render footer.
    +    oss.write("</table>\n")
    +    if file is None:
    +        return oss.getvalue()
    +
    @@ -6181,22 +5966,19 @@

    -
    +
    -

    -beancount.utils.snoop.Snoop.__init__(self, maxlen=None) +

    +beancount.utils.table.table_to_text(table, column_interspace=' ', formats=None) - - special - -

    +

    -

    Create a new snooper.

    +

    Render a Table to ASCII text.

    @@ -6208,26 +5990,80 @@

    +
    Parameters:
      -
    • maxlen – If specified, an integer, which enables the saving of that

    • +
    • table – An instance of a Table.

    • +
    • column_interspace – A string to render between the columns as spacer.

    • +
    • formats – An optional dict of column name to a format character that gets +inserted in a format string specified, like this (where '<char>' is): +{:<char><width>}. A key of '' will provide a default value, like +this, for example: (... formats={'': '>'}).

    + + + + + + + + + + +
    Returns: +
      +
    • A string, the rendered text table.

    • +
    +
    - Source code in beancount/utils/snoop.py -
    def __init__(self, maxlen=None):
    -    """Create a new snooper.
    -
    -    Args:
    -      maxlen: If specified, an integer, which enables the saving of that
    -      number of last values in the history attribute.
    -    """
    -    self.value = None
    -    self.history = (collections.deque(maxlen=maxlen)
    -                    if maxlen
    -                    else None)
    -
    + Source code in beancount/utils/table.py +
    def table_to_text(table, column_interspace=" ", formats=None):
    +    """Render a Table to ASCII text.
    +
    +    Args:
    +      table: An instance of a Table.
    +      column_interspace: A string to render between the columns as spacer.
    +      formats: An optional dict of column name to a format character that gets
    +        inserted in a format string specified, like this (where '<char>' is):
    +        {:<char><width>}. A key of '*' will provide a default value, like
    +        this, for example: (... formats={'*': '>'}).
    +    Returns:
    +      A string, the rendered text table.
    +    """
    +    column_widths = compute_table_widths(itertools.chain([table.header], table.body))
    +
    +    # Insert column format chars and compute line formatting string.
    +    column_formats = []
    +    if formats:
    +        default_format = formats.get("*", None)
    +    for column, width in zip(table.columns, column_widths):
    +        if column and formats:
    +            format_ = formats.get(column, default_format)
    +            if format_:
    +                column_formats.append("{{:{}{:d}}}".format(format_, width))
    +            else:
    +                column_formats.append("{{:{:d}}}".format(width))
    +        else:
    +            column_formats.append("{{:{:d}}}".format(width))
    +
    +    line_format = column_interspace.join(column_formats) + "\n"
    +    separator = line_format.format(*[("-" * width) for width in column_widths])
    +
    +    # Render the header.
    +    oss = io.StringIO()
    +    if table.header:
    +        oss.write(line_format.format(*table.header))
    +
    +    # Render the body.
    +    oss.write(separator)
    +    for row in table.body:
    +        oss.write(line_format.format(*row))
    +    oss.write(separator)
    +
    +    return oss.getvalue()
    +
    @@ -6237,6 +6073,7 @@

    +

    @@ -6245,81 +6082,50 @@

    +
    -
    +

    + beancount.utils.test_utils -

    -beancount.utils.snoop.snoopify(function) -

    +

    -

    Decorate a function as snoopable.

    -

    This is meant to reassign existing functions to a snoopable version of them. -For example, if you wanted 're.match' to be automatically snoopable, just -decorate it like this:

    -

    re.match = snoopify(re.match)

    -

    and then you can just call 're.match' in a conditional and then access -'re.match.value' to get to the last returned value.

    - -
    - Source code in beancount/utils/snoop.py -
    def snoopify(function):
    -    """Decorate a function as snoopable.
    -
    -    This is meant to reassign existing functions to a snoopable version of them.
    -    For example, if you wanted 're.match' to be automatically snoopable, just
    -    decorate it like this:
    -
    -      re.match = snoopify(re.match)
    -
    -    and then you can just call 're.match' in a conditional and then access
    -    're.match.value' to get to the last returned value.
    -    """
    -    @functools.wraps(function)
    -    def wrapper(*args, **kw):
    -        value = function(*args, **kw)
    -        wrapper.value = value
    -        return value
    -    wrapper.value = None
    -    return wrapper
    -
    -
    -
    +

    Support utilities for testing scripts.

    -
    +
    -
    -
    -
    -
    +
    -

    - beancount.utils.test_utils +

    + +beancount.utils.test_utils.ClickTestCase (TestCase) + -

    +
    -

    Support utilities for testing scripts.

    +

    Base class for command-line program test cases.

    + @@ -6334,6 +6140,16 @@

    + + +

    + +
    + +
    + + +
    @@ -6369,7 +6185,7 @@

    -beancount.utils.test_utils.RCall.__getnewargs__(self) +beancount.utils.test_utils.RCall.__getnewargs__(self) special @@ -6383,10 +6199,10 @@

    Source code in beancount/utils/test_utils.py -
    def __getnewargs__(self):
    -    'Return self as a plain tuple.  Used by copy and pickle.'
    -    return _tuple(self)
    -
    +
    def __getnewargs__(self):
    +    'Return self as a plain tuple.  Used by copy and pickle.'
    +    return _tuple(self)
    +

    @@ -6399,7 +6215,7 @@

    -beancount.utils.test_utils.RCall.__new__(_cls, args, kwargs, return_value) +beancount.utils.test_utils.RCall.__new__(_cls, args, kwargs, return_value) special @@ -6423,7 +6239,7 @@

    -beancount.utils.test_utils.RCall.__repr__(self) +beancount.utils.test_utils.RCall.__repr__(self) special @@ -6437,10 +6253,10 @@

    Source code in beancount/utils/test_utils.py -
    def __repr__(self):
    -    'Return a nicely formatted representation string'
    -    return self.__class__.__name__ + repr_fmt % self
    -
    +
    def __repr__(self):
    +    'Return a nicely formatted representation string'
    +    return self.__class__.__name__ + repr_fmt % self
    +

    @@ -6492,7 +6308,7 @@

    -beancount.utils.test_utils.TestCase.assertLines(self, text1, text2, message=None) +beancount.utils.test_utils.TestCase.assertLines(self, text1, text2, message=None)

    @@ -6537,27 +6353,27 @@

    Source code in beancount/utils/test_utils.py -
    def assertLines(self, text1, text2, message=None):
    -    """Compare the lines of text1 and text2, ignoring whitespace.
    -
    -    Args:
    -      text1: A string, the expected text.
    -      text2: A string, the actual text.
    -      message: An optional string message in case the assertion fails.
    -    Raises:
    -      AssertionError: If the exception fails.
    -    """
    -    clean_text1 = textwrap.dedent(text1.strip())
    -    clean_text2 = textwrap.dedent(text2.strip())
    -    lines1 = [line.strip() for line in clean_text1.splitlines()]
    -    lines2 = [line.strip() for line in clean_text2.splitlines()]
    -
    -    # Compress all space longer than 4 spaces to exactly 4.
    -    # This affords us to be even looser.
    -    lines1 = [re.sub('    [ \t]*', '    ', line) for line in lines1]
    -    lines2 = [re.sub('    [ \t]*', '    ', line) for line in lines2]
    -    self.assertEqual(lines1, lines2, message)
    -
    +
    def assertLines(self, text1, text2, message=None):
    +    """Compare the lines of text1 and text2, ignoring whitespace.
    +
    +    Args:
    +      text1: A string, the expected text.
    +      text2: A string, the actual text.
    +      message: An optional string message in case the assertion fails.
    +    Raises:
    +      AssertionError: If the exception fails.
    +    """
    +    clean_text1 = textwrap.dedent(text1.strip())
    +    clean_text2 = textwrap.dedent(text2.strip())
    +    lines1 = [line.strip() for line in clean_text1.splitlines()]
    +    lines2 = [line.strip() for line in clean_text2.splitlines()]
    +
    +    # Compress all space longer than 4 spaces to exactly 4.
    +    # This affords us to be even looser.
    +    lines1 = [re.sub("    [ \t]*", "    ", line) for line in lines1]
    +    lines2 = [re.sub("    [ \t]*", "    ", line) for line in lines2]
    +    self.assertEqual(lines1, lines2, message)
    +

    @@ -6570,7 +6386,7 @@

    -beancount.utils.test_utils.TestCase.assertOutput(self, expected_text) +beancount.utils.test_utils.TestCase.assertOutput(self, expected_text)

    @@ -6584,212 +6400,17 @@

    - - - Parameters: - -
      -
    • expected_text – A string, the text that should have been printed to stdout.

    • -
    - - - - - - - - - - - - - - - -
    Exceptions: -
      -
    • AssertionError – If the text differs.

    • -
    -
    -
    - Source code in beancount/utils/test_utils.py -
    @contextlib.contextmanager
    -def assertOutput(self, expected_text):
    -    """Expect text printed to stdout.
    -
    -    Args:
    -      expected_text: A string, the text that should have been printed to stdout.
    -    Raises:
    -      AssertionError: If the text differs.
    -    """
    -    with capture() as oss:
    -        yield oss
    -    self.assertLines(textwrap.dedent(expected_text), oss.getvalue())
    -
    -
    -

    - -
    - - - - - -
    - -
    - -
    - - - - - -
    - - - -

    -beancount.utils.test_utils.call_command(command) - - -

    - -
    - -

    Run the script with a subprocess.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • script_args – A list of strings, the arguments to the subprocess, -including the script name.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A triplet of (return code integer, stdout ext, stderr text).

    • -
    -
    -
    - Source code in beancount/utils/test_utils.py -
    def call_command(command):
    -    """Run the script with a subprocess.
    -
    -    Args:
    -      script_args: A list of strings, the arguments to the subprocess,
    -        including the script name.
    -    Returns:
    -      A triplet of (return code integer, stdout ext, stderr text).
    -    """
    -    assert isinstance(command, list), command
    -    p = subprocess.Popen(command,
    -                         stdout=subprocess.PIPE,
    -                         stderr=subprocess.PIPE)
    -    stdout, stderr = p.communicate()
    -    return p.returncode, stdout.decode(), stderr.decode()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.utils.test_utils.capture(*attributes) - - -

    - -
    - -

    A context manager that captures what's printed to stdout.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • *attributes – A tuple of strings, the name of the sys attributes to override -with StringIO instances.

    • -
    -

    Yields: - A StringIO string accumulator.

    - -
    - Source code in beancount/utils/test_utils.py -
    def capture(*attributes):
    -    """A context manager that captures what's printed to stdout.
    -
    -    Args:
    -      *attributes: A tuple of strings, the name of the sys attributes to override
    -        with StringIO instances.
    -    Yields:
    -      A StringIO string accumulator.
    -    """
    -    if not attributes:
    -        attributes = 'stdout'
    -    elif len(attributes) == 1:
    -        attributes = attributes[0]
    -    return patch(sys, attributes, io.StringIO)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.utils.test_utils.create_temporary_files(root, contents_map) - - -

    - -
    - -

    Create a number of temporary files under 'root'.

    -

    This routine is used to initialize the contents of multiple files under a -temporary directory.

    - + + + Parameters: + +
      +
    • expected_text – A string, the text that should have been printed to stdout.

    • +
    + + + + @@ -6797,14 +6418,10 @@

    - + @@ -6812,29 +6429,19 @@

    Source code in beancount/utils/test_utils.py -
    def create_temporary_files(root, contents_map):
    -    """Create a number of temporary files under 'root'.
    -
    -    This routine is used to initialize the contents of multiple files under a
    -    temporary directory.
    -
    -    Args:
    -      root: A string, the name of the directory under which to create the files.
    -      contents_map: A dict of relative filenames to their contents. The content
    -        strings will be automatically dedented for convenience. In addition, the
    -        string 'ROOT' in the contents will be automatically replaced by the root
    -        directory name.
    -    """
    -    os.makedirs(root, exist_ok=True)
    -    for relative_filename, contents in contents_map.items():
    -        assert not path.isabs(relative_filename)
    -        filename = path.join(root, relative_filename)
    -        os.makedirs(path.dirname(filename), exist_ok=True)
    -
    -        clean_contents = textwrap.dedent(contents.replace('{root}', root))
    -        with open(filename, 'w') as f:
    -            f.write(clean_contents)
    -
    +
    @contextlib.contextmanager
    +def assertOutput(self, expected_text):
    +    """Expect text printed to stdout.
    +
    +    Args:
    +      expected_text: A string, the text that should have been printed to stdout.
    +    Raises:
    +      AssertionError: If the text differs.
    +    """
    +    with capture() as oss:
    +        yield oss
    +    self.assertLines(textwrap.dedent(expected_text), oss.getvalue())
    +
    @@ -6842,21 +6449,66 @@

    + -

    -beancount.utils.test_utils.docfile(function, **kwargs) + + + + + + + +
    + + + +

    + +beancount.utils.test_utils.TmpFilesTestBase (TestCase) + -

    + +

    -

    A decorator that write the function's docstring to a temporary file -and calls the decorated function with the temporary filename. This is -useful for writing tests.

    +

    A test utility base class that creates and cleans up a directory hierarchy. +This convenience is useful for testing functions that work on files, such as the +documents tests, or the accounts walk.

    + + + + +
    + + + + + + + + + + +
    + + + +

    +beancount.utils.test_utils.TmpFilesTestBase.create_file_hierarchy(test_files, subdir='root') + + + staticmethod + + +

    + +
    + +

    A test utility that creates a hierarchy of files.

    Parameters:Exceptions:
      -
    • root – A string, the name of the directory under which to create the files.

    • -
    • contents_map – A dict of relative filenames to their contents. The content -strings will be automatically dedented for convenience. In addition, the -string 'ROOT' in the contents will be automatically replaced by the root -directory name.

    • +
    • AssertionError – If the text differs.

    @@ -6868,7 +6520,11 @@

    @@ -6884,7 +6540,8 @@

    @@ -6892,29 +6549,34 @@

    Parameters:
      -
    • function – A function to decorate.

    • +
    • test_files – A list of strings, relative filenames to a temporary root +directory. If the filename ends with a '/', we create a directory; +otherwise, we create a regular file.

    • +
    • subdir – A string, the subdirectory name under the temporary directory +location, to create the hierarchy under.

    Returns:
      -
    • The decorated function.

    • +
    • A pair of strings, the temporary directory, and the subdirectory under + that which hosts the root of the tree.

    Source code in beancount/utils/test_utils.py -
    def docfile(function, **kwargs):
    -    """A decorator that write the function's docstring to a temporary file
    -    and calls the decorated function with the temporary filename.  This is
    -    useful for writing tests.
    -
    -    Args:
    -      function: A function to decorate.
    -    Returns:
    -      The decorated function.
    -    """
    -    @functools.wraps(function)
    -    def new_function(self):
    -        allowed = ('buffering', 'encoding', 'newline', 'dir', 'prefix', 'suffix')
    -        if any([key not in allowed for key in kwargs]):
    -            raise ValueError("Invalid kwarg to docfile_extra")
    -        with tempfile.NamedTemporaryFile('w', **kwargs) as file:
    -            text = function.__doc__
    -            file.write(textwrap.dedent(text))
    -            file.flush()
    -            return function(self, file.name)
    -    new_function.__doc__ = None
    -    return new_function
    -
    +
    @staticmethod
    +def create_file_hierarchy(test_files, subdir="root"):
    +    """A test utility that creates a hierarchy of files.
    +
    +    Args:
    +      test_files: A list of strings, relative filenames to a temporary root
    +        directory. If the filename ends with a '/', we create a directory;
    +        otherwise, we create a regular file.
    +      subdir: A string, the subdirectory name under the temporary directory
    +        location, to create the hierarchy under.
    +    Returns:
    +      A pair of strings, the temporary directory, and the subdirectory under
    +        that which hosts the root of the tree.
    +    """
    +    tempdir = tempfile.mkdtemp(prefix="beancount-test-tmpdir.")
    +    root = path.join(tempdir, subdir)
    +    for filename in test_files:
    +        abs_filename = path.join(tempdir, filename)
    +        if filename.endswith("/"):
    +            os.makedirs(abs_filename)
    +        else:
    +            parent_dir = path.dirname(abs_filename)
    +            if not path.exists(parent_dir):
    +                os.makedirs(parent_dir)
    +            with open(abs_filename, "w"):
    +                pass
    +    return tempdir, root
    +
    @@ -6922,52 +6584,25 @@

    -
    +
    -

    -beancount.utils.test_utils.docfile_extra(**kwargs) +

    +beancount.utils.test_utils.TmpFilesTestBase.setUp(self) -

    +

    -

    A decorator identical to @docfile, -but it also takes kwargs for the temporary file, -Kwargs: - e.g. buffering, encoding, newline, dir, prefix, and suffix.

    +

    Hook method for setting up the test fixture before exercising it.

    - - - - - - - - - - - -
    Returns: -
      -
    • docfile

    • -
    -
    Source code in beancount/utils/test_utils.py -
    def docfile_extra(**kwargs):
    -    """
    -    A decorator identical to @docfile,
    -    but it also takes kwargs for the temporary file,
    -    Kwargs:
    -      e.g. buffering, encoding, newline, dir, prefix, and suffix.
    -    Returns:
    -      docfile
    -    """
    -    return functools.partial(docfile, **kwargs)
    -
    +
    def setUp(self):
    +    self.tempdir, self.root = self.create_file_hierarchy(self.TEST_DOCUMENTS)
    +
    @@ -6975,55 +6610,25 @@

    -
    +
    -

    -beancount.utils.test_utils.environ(varname, newvalue) +

    +beancount.utils.test_utils.TmpFilesTestBase.tearDown(self) -

    +

    -

    A context manager which pushes varname's value and restores it later.

    +

    Hook method for deconstructing the test fixture after testing it.

    - - - - - - - - - - - -
    Parameters: -
      -
    • varname – A string, the environ variable name.

    • -
    • newvalue – A string, the desired value.

    • -
    -
    Source code in beancount/utils/test_utils.py -
    @contextlib.contextmanager
    -def environ(varname, newvalue):
    -    """A context manager which pushes varname's value and restores it later.
    -
    -    Args:
    -      varname: A string, the environ variable name.
    -      newvalue: A string, the desired value.
    -    """
    -    oldvalue = os.environ.get(varname, None)
    -    os.environ[varname] = newvalue
    -    yield
    -    if oldvalue is not None:
    -        os.environ[varname] = oldvalue
    -    else:
    -        del os.environ[varname]
    -
    +
    def tearDown(self):
    +    shutil.rmtree(self.tempdir, ignore_errors=True)
    +
    @@ -7031,19 +6636,30 @@

    + + +

    + +
    + +
    + + + +
    -

    -beancount.utils.test_utils.find_python_lib() +

    +beancount.utils.test_utils.capture(*attributes) -

    +
    -

    Return the path to the root of the Python libraries.

    +

    A context manager that captures what's printed to stdout.

    @@ -7052,25 +6668,35 @@

    - - + + -
    Returns: + Parameters:
      -
    • A string, the root directory.

    • +
    • *attributes – A tuple of strings, the name of the sys attributes to override +with StringIO instances.

    -
    +

    Yields: + A StringIO string accumulator.

    +
    Source code in beancount/utils/test_utils.py -
    def find_python_lib():
    -    """Return the path to the root of the Python libraries.
    -
    -    Returns:
    -      A string, the root directory.
    -    """
    -    return path.dirname(path.dirname(path.dirname(__file__)))
    -
    +
    def capture(*attributes):
    +    """A context manager that captures what's printed to stdout.
    +
    +    Args:
    +      *attributes: A tuple of strings, the name of the sys attributes to override
    +        with StringIO instances.
    +    Yields:
    +      A StringIO string accumulator.
    +    """
    +    if not attributes:
    +        attributes = "stdout"
    +    elif len(attributes) == 1:
    +        attributes = attributes[0]
    +    return patch(sys, attributes, io.StringIO)
    +
    @@ -7082,15 +6708,17 @@

    -

    -beancount.utils.test_utils.find_repository_root(filename=None) +

    +beancount.utils.test_utils.create_temporary_files(root, contents_map) -

    +
    -

    Return the path to the repository root.

    +

    Create a number of temporary files under 'root'.

    +

    This routine is used to initialize the contents of multiple files under a +temporary directory.

    @@ -7102,54 +6730,41 @@

    Parameters:

    -
      -
    • filename – A string, the name of a file within the repository.

    • +
    • root – A string, the name of the directory under which to create the files.

    • +
    • contents_map – A dict of relative filenames to their contents. The content +strings will be automatically dedented for convenience. In addition, the +string 'ROOT' in the contents will be automatically replaced by the root +directory name.

    - - - - - - - - - - -
    Returns: -
      -
    • A string, the root directory.

    • -
    -
    Source code in beancount/utils/test_utils.py -
    def find_repository_root(filename=None):
    -    """Return the path to the repository root.
    -
    -    Args:
    -      filename: A string, the name of a file within the repository.
    -    Returns:
    -      A string, the root directory.
    -    """
    -    if filename is None:
    -        filename = __file__
    -
    -    # Support root directory under Bazel.
    -    match = re.match(r"(.*\.runfiles/beancount)/", filename)
    -    if match:
    -        return match.group(1)
    -
    -    while not all(path.exists(path.join(filename, sigfile))
    -                  for sigfile in ('PKG-INFO', 'COPYING', 'README.rst')):
    -        prev_filename = filename
    -        filename = path.dirname(filename)
    -        if prev_filename == filename:
    -            raise ValueError("Failed to find the root directory.")
    -    return filename
    -
    +
    def create_temporary_files(root, contents_map):
    +    """Create a number of temporary files under 'root'.
    +
    +    This routine is used to initialize the contents of multiple files under a
    +    temporary directory.
    +
    +    Args:
    +      root: A string, the name of the directory under which to create the files.
    +      contents_map: A dict of relative filenames to their contents. The content
    +        strings will be automatically dedented for convenience. In addition, the
    +        string 'ROOT' in the contents will be automatically replaced by the root
    +        directory name.
    +    """
    +    os.makedirs(root, exist_ok=True)
    +    for relative_filename, contents in contents_map.items():
    +        assert not path.isabs(relative_filename)
    +        filename = path.join(root, relative_filename)
    +        os.makedirs(path.dirname(filename), exist_ok=True)
    +
    +        clean_contents = textwrap.dedent(contents.replace("{root}", root))
    +        with open(filename, "w") as f:
    +            f.write(clean_contents)
    +
    @@ -7161,18 +6776,17 @@

    -beancount.utils.test_utils.make_failing_importer(*removed_module_names) +

    +beancount.utils.test_utils.docfile(function, **kwargs) -

    +
    -

    Make an importer that raise an ImportError for some modules.

    -

    Use it like this:

    -

    @mock.patch('builtins.import', make_failing_importer('setuptools')) - def test_...

    +

    A decorator that write the function's docstring to a temporary file +and calls the decorated function with the temporary filename. This is +useful for writing tests.

    @@ -7184,7 +6798,7 @@

    Parameters:

    @@ -7200,7 +6814,7 @@

    Returns:

    @@ -7208,25 +6822,32 @@

    Source code in beancount/utils/test_utils.py -
    def make_failing_importer(*removed_module_names):
    -    """Make an importer that raise an ImportError for some modules.
    -
    -    Use it like this:
    -
    -      @mock.patch('builtins.__import__', make_failing_importer('setuptools'))
    -      def test_...
    -
    -    Args:
    -      removed_module_name: The name of the module import that should raise an exception.
    -    Returns:
    -      A decorated test decorator.
    -    """
    -    def failing_import(name, *args, **kwargs):
    -        if name in removed_module_names:
    -            raise ImportError("Could not import {}".format(name))
    -        return builtins.__import__(name, *args, **kwargs)
    -    return failing_import
    -
    +
    def docfile(function, **kwargs):
    +    """A decorator that write the function's docstring to a temporary file
    +    and calls the decorated function with the temporary filename.  This is
    +    useful for writing tests.
    +
    +    Args:
    +      function: A function to decorate.
    +    Returns:
    +      The decorated function.
    +    """
    +    contents = kwargs.pop("contents", None)
    +
    +    @functools.wraps(function)
    +    def new_function(self):
    +        allowed = ("buffering", "encoding", "newline", "dir", "prefix", "suffix")
    +        if any(key not in allowed for key in kwargs):
    +            raise ValueError("Invalid kwarg to docfile_extra")
    +        with tempfile.NamedTemporaryFile("w", **kwargs) as file:
    +            text = contents or function.__doc__
    +            file.write(textwrap.dedent(text))
    +            file.flush()
    +            return function(self, file.name)
    +
    +    new_function.__doc__ = None
    +    return new_function
    +
    @@ -7238,23 +6859,48 @@

    -beancount.utils.test_utils.nottest(func) +

    +beancount.utils.test_utils.docfile_extra(**kwargs) -

    +
    -

    Make the given function not testable.

    +

    A decorator identical to @docfile, +but it also takes kwargs for the temporary file, +Kwargs: + e.g. buffering, encoding, newline, dir, prefix, and suffix.

    +
      -
    • removed_module_name – The name of the module import that should raise an exception.

    • +
    • function – A function to decorate.

      -
    • A decorated test decorator.

    • +
    • The decorated function.

    + + + + + + + + + + +
    Returns: +
      +
    • docfile

    • +
    +
    Source code in beancount/utils/test_utils.py -
    def nottest(func):
    -    "Make the given function not testable."
    -    func.__test__ = False
    -    return func
    -
    +
    def docfile_extra(**kwargs):
    +    """
    +    A decorator identical to @docfile,
    +    but it also takes kwargs for the temporary file,
    +    Kwargs:
    +      e.g. buffering, encoding, newline, dir, prefix, and suffix.
    +    Returns:
    +      docfile
    +    """
    +    return functools.partial(docfile, **kwargs)
    +
    @@ -7266,17 +6912,15 @@

    -

    -beancount.utils.test_utils.patch(obj, attributes, replacement_type) +

    +beancount.utils.test_utils.environ(varname, newvalue) -

    +
    -

    A context manager that temporarily patches an object's attributes.

    -

    All attributes in 'attributes' are saved and replaced by new instances -of type 'replacement_type'.

    +

    A context manager which pushes varname's value and restores it later.

    @@ -7288,49 +6932,31 @@

    -
    Parameters:
      -
    • obj – The object to patch up.

    • -
    • attributes – A string or a sequence of strings, the names of attributes to replace.

    • -
    • replacement_type – A callable to build replacement objects.

    • +
    • varname – A string, the environ variable name.

    • +
    • newvalue – A string, the desired value.

    Yields: - An instance of a list of sequences of 'replacement_type'.

    - +
    Source code in beancount/utils/test_utils.py -
    @contextlib.contextmanager
    -def patch(obj, attributes, replacement_type):
    -    """A context manager that temporarily patches an object's attributes.
    -
    -    All attributes in 'attributes' are saved and replaced by new instances
    -    of type 'replacement_type'.
    -
    -    Args:
    -      obj: The object to patch up.
    -      attributes: A string or a sequence of strings, the names of attributes to replace.
    -      replacement_type: A callable to build replacement objects.
    -    Yields:
    -      An instance of a list of sequences of 'replacement_type'.
    -    """
    -    single = isinstance(attributes, str)
    -    if single:
    -        attributes = [attributes]
    -
    -    saved = []
    -    replacements = []
    -    for attribute in attributes:
    -        replacement = replacement_type()
    -        replacements.append(replacement)
    -        saved.append(getattr(obj, attribute))
    -        setattr(obj, attribute, replacement)
    -
    -    yield replacements[0] if single else replacements
    -
    -    for attribute, saved_attr in zip(attributes, saved):
    -        setattr(obj, attribute, saved_attr)
    -
    +
    @contextlib.contextmanager
    +def environ(varname, newvalue):
    +    """A context manager which pushes varname's value and restores it later.
    +
    +    Args:
    +      varname: A string, the environ variable name.
    +      newvalue: A string, the desired value.
    +    """
    +    oldvalue = os.environ.get(varname, None)
    +    os.environ[varname] = newvalue
    +    yield
    +    if oldvalue is not None:
    +        os.environ[varname] = oldvalue
    +    else:
    +        del os.environ[varname]
    +
    @@ -7342,32 +6968,16 @@

    -

    -beancount.utils.test_utils.record(fun) - - -

    - -
    +

    +beancount.utils.test_utils.find_python_lib() -

    Decorates the function to intercept and record all calls and return values.

    - - - - - - - - - - - -
    Parameters: -
      -
    • fun – A callable to be decorated.

    • -
    -
    +

    + +
    + +

    Return the path to the root of the Python libraries.

    + @@ -7378,7 +6988,7 @@

    @@ -7386,22 +6996,14 @@

    Returns:
      -
    • A wrapper function with a .calls attribute, a list of RCall instances.

    • +
    • A string, the root directory.

    Source code in beancount/utils/test_utils.py -
    def record(fun):
    -    """Decorates the function to intercept and record all calls and return values.
    -
    -    Args:
    -      fun: A callable to be decorated.
    -    Returns:
    -      A wrapper function with a .calls attribute, a list of RCall instances.
    -    """
    -    @functools.wraps(fun)
    -    def wrapped(*args, **kw):
    -        return_value = fun(*args, **kw)
    -        wrapped.calls.append(RCall(args, kw, return_value))
    -        return return_value
    -    wrapped.calls = []
    -    return wrapped
    -
    +
    def find_python_lib():
    +    """Return the path to the root of the Python libraries.
    +
    +    Returns:
    +      A string, the root directory.
    +    """
    +    return path.dirname(path.dirname(path.dirname(__file__)))
    +
    @@ -7413,17 +7015,15 @@

    -

    -beancount.utils.test_utils.run_with_args(function, args) +

    +beancount.utils.test_utils.find_repository_root(filename=None) -

    +
    -

    Run the given function with sys.argv set to argv. The first argument is -automatically inferred to be where the function object was defined. sys.argv -is restored after the function is called.

    +

    Return the path to the repository root.

    @@ -7435,9 +7035,7 @@

    @@ -7453,7 +7051,7 @@

    @@ -7461,29 +7059,29 @@

    Parameters:
      -
    • function – A function object to call with no arguments.

    • -
    • argv – A list of arguments, excluding the script name, to be temporarily -set on sys.argv.

    • +
    • filename – A string, the name of a file within the repository.

    Returns:
      -
    • The return value of the function run.

    • +
    • A string, the root directory.

    Source code in beancount/utils/test_utils.py -
    def run_with_args(function, args):
    -    """Run the given function with sys.argv set to argv. The first argument is
    -    automatically inferred to be where the function object was defined. sys.argv
    -    is restored after the function is called.
    -
    -    Args:
    -      function: A function object to call with no arguments.
    -      argv: A list of arguments, excluding the script name, to be temporarily
    -        set on sys.argv.
    -    Returns:
    -      The return value of the function run.
    -    """
    -    saved_argv = sys.argv
    -    saved_handlers = logging.root.handlers
    -    try:
    -        module = sys.modules[function.__module__]
    -        sys.argv = [module.__file__] + args
    -        logging.root.handlers = []
    -        return function()
    -    finally:
    -        sys.argv = saved_argv
    -        logging.root.handlers = saved_handlers
    -
    +
    def find_repository_root(filename=None):
    +    """Return the path to the repository root.
    +
    +    Args:
    +      filename: A string, the name of a file within the repository.
    +    Returns:
    +      A string, the root directory.
    +    """
    +    if filename is None:
    +        filename = __file__
    +
    +    # Support root directory under Bazel.
    +    match = re.match(r"(.*\.runfiles/beancount)/", filename)
    +    if match:
    +        return match.group(1)
    +
    +    while not path.exists(path.join(filename, "pyproject.toml")):
    +        prev_filename = filename
    +        filename = path.dirname(filename)
    +        if prev_filename == filename:
    +            raise ValueError("Failed to find the root directory.")
    +    return filename
    +
    @@ -7495,15 +7093,18 @@

    -

    -beancount.utils.test_utils.search_words(words, line) +

    +beancount.utils.test_utils.make_failing_importer(*removed_module_names) -

    +
    -

    Search for a sequence of words in a line.

    +

    Make an importer that raise an ImportError for some modules.

    +

    Use it like this:

    +

    @mock.patch('builtins.import', make_failing_importer('setuptools')) + def test_...

    @@ -7515,8 +7116,7 @@

    @@ -7532,7 +7132,7 @@

    @@ -7540,76 +7140,27 @@

    Parameters:
      -
    • words – A list of strings, the words to look for, or a space-separated string.

    • -
    • line – A string, the line to search into.

    • +
    • removed_module_name – The name of the module import that should raise an exception.

    Returns:
      -
    • A MatchObject, or None.

    • +
    • A decorated test decorator.

    Source code in beancount/utils/test_utils.py -
    def search_words(words, line):
    -    """Search for a sequence of words in a line.
    -
    -    Args:
    -      words: A list of strings, the words to look for, or a space-separated string.
    -      line: A string, the line to search into.
    -    Returns:
    -      A MatchObject, or None.
    -    """
    -    if isinstance(words, str):
    -        words = words.split()
    -    return re.search('.*'.join(r'\b{}\b'.format(word) for word in words), line)
    -
    -
    -
    - -
    - +
    def make_failing_importer(*removed_module_names):
    +    """Make an importer that raise an ImportError for some modules.
     
    +    Use it like this:
     
    -  
    - - - -

    -beancount.utils.test_utils.skipIfRaises(*exc_types) - - -

    + @mock.patch('builtins.__import__', make_failing_importer('setuptools')) + def test_... -
    + Args: + removed_module_name: The name of the module import that should raise an exception. + Returns: + A decorated test decorator. + """ -

    A context manager (or decorator) that skips a test if an exception is raised.

    -

    Yields: - Nothing, for you to execute the function code.

    + def failing_import(name, *args, **kwargs): + if name in removed_module_names: + raise ImportError("Could not import {}".format(name)) + return builtins.__import__(name, *args, **kwargs) - - - - - - - - - - - -
    Exceptions: -
      -
    • SkipTest – if the test raised the expected exception.

    • -
    -
    -
    - Source code in beancount/utils/test_utils.py -
    @contextlib.contextmanager
    -def skipIfRaises(*exc_types):
    -    """A context manager (or decorator) that skips a test if an exception is raised.
    -
    -    Args:
    -      exc_type
    -    Yields:
    -      Nothing, for you to execute the function code.
    -    Raises:
    -      SkipTest: if the test raised the expected exception.
    -    """
    -    try:
    -        yield
    -    except exc_types as exception:
    -        raise unittest.SkipTest(exception)
    -
    + return failing_import +
    @@ -7621,49 +7172,23 @@

    -

    -beancount.utils.test_utils.subprocess_env() +

    +beancount.utils.test_utils.nottest(func) -

    +
    -

    Return a dict to use as environment for running subprocesses.

    +

    Make the given function not testable.

    - - - - - - - - - - - -
    Returns: -
      -
    • A string, the root directory.

    • -
    -
    Source code in beancount/utils/test_utils.py -
    def subprocess_env():
    -    """Return a dict to use as environment for running subprocesses.
    -
    -    Returns:
    -      A string, the root directory.
    -    """
    -    # Ensure we have locations to invoke our Python executable and our
    -    # runnable binaries in the test environment to run subprocesses.
    -    binpath = ':'.join([
    -        path.dirname(sys.executable),
    -        path.join(find_repository_root(__file__), 'bin'),
    -        os.environ.get('PATH', '').strip(':')]).strip(':')
    -    return {'PATH': binpath,
    -            'PYTHONPATH': find_python_lib()}
    -
    +
    def nottest(func):
    +    "Make the given function not testable."
    +    func.__test__ = False
    +    return func
    +
    @@ -7675,16 +7200,17 @@

    -

    -beancount.utils.test_utils.tempdir(delete=True, **kw) +

    +beancount.utils.test_utils.patch(obj, attributes, replacement_type) -

    +
    -

    A context manager that creates a temporary directory and deletes its -contents unconditionally once done.

    +

    A context manager that temporarily patches an object's attributes.

    +

    All attributes in 'attributes' are saved and replaced by new instances +of type 'replacement_type'.

    @@ -7696,35 +7222,49 @@

    Parameters:
      -
    • delete – A boolean, true if we want to delete the directory after running.

    • -
    • **kw – Keyword arguments for mkdtemp.

    • +
    • obj – The object to patch up.

    • +
    • attributes – A string or a sequence of strings, the names of attributes to replace.

    • +
    • replacement_type – A callable to build replacement objects.

    Yields: - A string, the name of the temporary directory created.

    + An instance of a list of sequences of 'replacement_type'.

    Source code in beancount/utils/test_utils.py -
    @contextlib.contextmanager
    -def tempdir(delete=True, **kw):
    -    """A context manager that creates a temporary directory and deletes its
    -    contents unconditionally once done.
    -
    -    Args:
    -      delete: A boolean, true if we want to delete the directory after running.
    -      **kw: Keyword arguments for mkdtemp.
    -    Yields:
    -      A string, the name of the temporary directory created.
    -    """
    -    tempdir = tempfile.mkdtemp(prefix="beancount-test-tmpdir.", **kw)
    -    try:
    -        yield tempdir
    -    finally:
    -        if delete:
    -            shutil.rmtree(tempdir, ignore_errors=True)
    -
    +
    @contextlib.contextmanager
    +def patch(obj, attributes, replacement_type):
    +    """A context manager that temporarily patches an object's attributes.
    +
    +    All attributes in 'attributes' are saved and replaced by new instances
    +    of type 'replacement_type'.
    +
    +    Args:
    +      obj: The object to patch up.
    +      attributes: A string or a sequence of strings, the names of attributes to replace.
    +      replacement_type: A callable to build replacement objects.
    +    Yields:
    +      An instance of a list of sequences of 'replacement_type'.
    +    """
    +    single = isinstance(attributes, str)
    +    if single:
    +        attributes = [attributes]
    +
    +    saved = []
    +    replacements = []
    +    for attribute in attributes:
    +        replacement = replacement_type()
    +        replacements.append(replacement)
    +        saved.append(getattr(obj, attribute))
    +        setattr(obj, attribute, replacement)
    +
    +    yield replacements[0] if single else replacements
    +
    +    for attribute, saved_attr in zip(attributes, saved):
    +        setattr(obj, attribute, saved_attr)
    +
    @@ -7732,61 +7272,19 @@

    - - - -

    - -
    - -
    - - - -
    - - - -

    - beancount.utils.text_utils - - - -

    - -
    - -

    Text manipulation utilities.

    - - - -
    - - - - - - - - - -
    -

    -beancount.utils.text_utils.entitize_ampersand(filename) +

    +beancount.utils.test_utils.record(fun) -

    +
    -

    Convert unescaped ampersand characters (&) to XML entities.

    -

    This is used to fix code that has been programmed by bad developers who -didn't think about escaping the entities in their strings. -file does not contain

    +

    Decorates the function to intercept and record all calls and return values.

    @@ -7798,7 +7296,7 @@

    @@ -7814,35 +7312,32 @@

    Parameters:
      -
    • filename – A string, the name of the file to convert.

    • +
    • fun – A callable to be decorated.

    Returns:
      -
    • A self-destructing NamedTemporaryFile object that has been flushed and -which you may read to obtain the fixed contents.

    • +
    • A wrapper function with a .calls attribute, a list of RCall instances.

    - Source code in beancount/utils/text_utils.py -
    def entitize_ampersand(filename):
    -    """Convert unescaped ampersand characters (&) to XML entities.
    -
    -    This is used to fix code that has been programmed by bad developers who
    -    didn't think about escaping the entities in their strings.
    -    file does not contain
    -    Args:
    -      filename: A string, the name of the file to convert.
    -    Returns:
    -      A self-destructing NamedTemporaryFile object that has been flushed and
    -      which you may read to obtain the fixed contents.
    -    """
    -    tidy_file = tempfile.NamedTemporaryFile(suffix='.xls', mode='w', delete=False)
    -    with open(filename) as infile:
    -        contents = infile.read()
    -    new_contents = re.sub('&([^;&]{12})', '&amp;\\1', contents)
    -    tidy_file.write(new_contents)
    -    tidy_file.flush()
    -    return tidy_file
    -
    + Source code in beancount/utils/test_utils.py +
    def record(fun):
    +    """Decorates the function to intercept and record all calls and return values.
    +
    +    Args:
    +      fun: A callable to be decorated.
    +    Returns:
    +      A wrapper function with a .calls attribute, a list of RCall instances.
    +    """
    +
    +    @functools.wraps(fun)
    +    def wrapped(*args, **kw):
    +        return_value = fun(*args, **kw)
    +        wrapped.calls.append(RCall(args, kw, return_value))
    +        return return_value
    +
    +    wrapped.calls = []
    +    return wrapped
    +
    @@ -7854,17 +7349,15 @@

    -

    -beancount.utils.text_utils.replace_number(match) +

    +beancount.utils.test_utils.search_words(words, line) -

    +
    -

    Replace a single number matched from text into X'es. -'match' is a MatchObject from a regular expressions match. -(Use this with re.sub()).

    +

    Search for a sequence of words in a line.

    @@ -7876,7 +7369,8 @@

    @@ -7892,26 +7386,27 @@

    Parameters:
      -
    • match – A MatchObject.

    • +
    • words – A list of strings, the words to look for, or a space-separated string.

    • +
    • line – A string, the line to search into.

    Returns:
      -
    • A replacement string, consisting only of X'es.

    • +
    • A MatchObject, or None.

    - Source code in beancount/utils/text_utils.py -
    def replace_number(match):
    -    """Replace a single number matched from text into X'es.
    -    'match' is a MatchObject from a regular expressions match.
    -    (Use this with re.sub()).
    -
    -    Args:
    -      match: A MatchObject.
    -    Returns:
    -      A replacement string, consisting only of X'es.
    -    """
    -    return re.sub('[0-9]', 'X', match.group(1)) + match.group(2)
    -
    + Source code in beancount/utils/test_utils.py +
    def search_words(words, line):
    +    """Search for a sequence of words in a line.
    +
    +    Args:
    +      words: A list of strings, the words to look for, or a space-separated string.
    +      line: A string, the line to search into.
    +    Returns:
    +      A MatchObject, or None.
    +    """
    +    if isinstance(words, str):
    +        words = words.split()
    +    return re.search(".*".join(r"\b{}\b".format(word) for word in words), line)
    +
    @@ -7923,18 +7418,17 @@

    -

    -beancount.utils.text_utils.replace_numbers(text) +

    +beancount.utils.test_utils.skipIfRaises(*exc_types) -

    +
    -

    Replace all numbers found within text.

    -

    Note that this is a heuristic used to filter out private numbers from web -pages in incognito mode and thus may not be perfect. It should let through -numbers which are part of URLs.

    +

    A context manager (or decorator) that skips a test if an exception is raised.

    +

    Yields: + Nothing, for you to execute the function code.

    @@ -7943,48 +7437,33 @@

    - + -
    Parameters:Exceptions:
      -
    • text – An input string object.

    • +
    • SkipTest – if the test raised the expected exception.

    - - - - - - - - - - -
    Returns: -
      -
    • A string, with relevant numbers hopefully replaced with X'es.

    • -
    -
    - Source code in beancount/utils/text_utils.py -
    def replace_numbers(text):
    -    """Replace all numbers found within text.
    -
    -    Note that this is a heuristic used to filter out private numbers from web
    -    pages in incognito mode and thus may not be perfect. It should let through
    -    numbers which are part of URLs.
    -
    -    Args:
    -      text: An input string object.
    -    Returns:
    -      A string, with relevant numbers hopefully replaced with X'es.
    -
    -    """
    -    return re.sub(r'\b([0-9,]+(?:\.[0-9]*)?)\b([ \t<]+[^0-9,.]|$)', replace_number, text)
    -
    + Source code in beancount/utils/test_utils.py +
    @contextlib.contextmanager
    +def skipIfRaises(*exc_types):
    +    """A context manager (or decorator) that skips a test if an exception is raised.
    +
    +    Args:
    +      exc_type
    +    Yields:
    +      Nothing, for you to execute the function code.
    +    Raises:
    +      SkipTest: if the test raised the expected exception.
    +    """
    +    try:
    +        yield
    +    except exc_types as exception:
    +        raise unittest.SkipTest(exception)
    +
    @@ -7992,76 +7471,20 @@

    - - - -

    - -
    - -
    - - - -
    - - - -

    - beancount.utils.version - - - -

    - -
    - -

    Implement common options across all programs.

    - - - -
    - - - - - - - - - -
    -

    -beancount.utils.version.ArgumentParser(*args, **kwargs) +

    +beancount.utils.test_utils.subprocess_env() -

    +
    -

    Add a standard --version option to an ArgumentParser.

    +

    Return a dict to use as environment for running subprocesses.

    - - - - - - - - - - - -
    Parameters: -
      -
    • *args – Arguments for the parser.

    • -
    • *kwargs – Keyword arguments for the parser.

    • -
    -
    @@ -8072,33 +7495,31 @@

    Returns:
      -
    • An instance of ArgumentParser, with our default options set.

    • +
    • A string, the root directory.

    - Source code in beancount/utils/version.py -
    def ArgumentParser(*args, **kwargs):
    -    """Add a standard --version option to an ArgumentParser.
    -
    -    Args:
    -      *args: Arguments for the parser.
    -      *kwargs: Keyword arguments for the parser.
    -    Returns:
    -      An instance of ArgumentParser, with our default options set.
    -    """
    -    parser = argparse.ArgumentParser(*args, **kwargs)
    -
    -    parser.add_argument('--version', '-V', action='version',
    -                        version=compute_version_string(
    -                            beancount.__version__,
    -                            _parser.__vc_changeset__,
    -                            _parser.__vc_timestamp__))
    -
    -    return parser
    -
    + Source code in beancount/utils/test_utils.py +
    def subprocess_env():
    +    """Return a dict to use as environment for running subprocesses.
    +
    +    Returns:
    +      A string, the root directory.
    +    """
    +    # Ensure we have locations to invoke our Python executable and our
    +    # runnable binaries in the test environment to run subprocesses.
    +    binpath = ":".join(
    +        [
    +            path.dirname(sys.executable),
    +            path.join(find_repository_root(__file__), "bin"),
    +            os.environ.get("PATH", "").strip(":"),
    +        ]
    +    ).strip(":")
    +    return {"PATH": binpath, "PYTHONPATH": find_python_lib()}
    +
    @@ -8110,15 +7531,16 @@

    -

    -beancount.utils.version.compute_version_string(version, changeset, timestamp) +

    +beancount.utils.test_utils.tempdir(delete=True, **kw) -

    +
    -

    Compute a version string from the changeset and timestamp baked in the parser.

    +

    A context manager that creates a temporary directory and deletes its +contents unconditionally once done.

    @@ -8130,61 +7552,35 @@

    -
    Parameters:
      -
    • version – A string, the version number.

    • -
    • changeset – A string, a version control string identifying the commit of the version.

    • -
    • timestamp – An integer, the UNIX epoch timestamp of the changeset.

    • +
    • delete – A boolean, true if we want to delete the directory after running.

    • +
    • **kw – Keyword arguments for mkdtemp.

    - - - - - - - - - - - -
    Returns: -
      -
    • A human-readable string for the version.

    • -
    -
    +

    Yields: + A string, the name of the temporary directory created.

    +
    - Source code in beancount/utils/version.py -
    def compute_version_string(version, changeset, timestamp):
    -    """Compute a version string from the changeset and timestamp baked in the parser.
    -
    -    Args:
    -      version: A string, the version number.
    -      changeset: A string, a version control string identifying the commit of the version.
    -      timestamp: An integer, the UNIX epoch timestamp of the changeset.
    -    Returns:
    -      A human-readable string for the version.
    -    """
    -    # Shorten changeset.
    -    if changeset:
    -        if re.match('hg:', changeset):
    -            changeset = changeset[:15]
    -        elif re.match('git:', changeset):
    -            changeset = changeset[:12]
    -
    -    # Convert timestamp to a date.
    -    date = None
    -    if timestamp > 0:
    -        date = datetime.datetime.utcfromtimestamp(timestamp).date()
    -
    -    version = 'Beancount {}'.format(version)
    -    if changeset or date:
    -        version = '{} ({})'.format(
    -            version, '; '.join(map(str, filter(None, [changeset, date]))))
    -
    -    return version
    -
    + Source code in beancount/utils/test_utils.py +
    @contextlib.contextmanager
    +def tempdir(delete=True, **kw):
    +    """A context manager that creates a temporary directory and deletes its
    +    contents unconditionally once done.
    +
    +    Args:
    +      delete: A boolean, true if we want to delete the directory after running.
    +      **kw: Keyword arguments for mkdtemp.
    +    Yields:
    +      A string, the name of the temporary directory created.
    +    """
    +    tempdir = tempfile.mkdtemp(prefix="beancount-test-tmpdir.", **kw)
    +    try:
    +        yield tempdir
    +    finally:
    +        if delete:
    +            shutil.rmtree(tempdir, ignore_errors=True)
    +
    @@ -8216,8 +7612,6 @@

    diff --git a/api_reference/beancount.web.html b/api_reference/beancount.web.html deleted file mode 100644 index 42a16228..00000000 --- a/api_reference/beancount.web.html +++ /dev/null @@ -1,5209 +0,0 @@ - - - - - - - - - - - - beancount.web - Beancount Documentation - - - - - - - - - - - - - - - -
    - - - - -
    - - - - - -
    -
    -
    -
      -
    • Docs »
    • - - - -
    • API reference »
    • - - - -
    • beancount.web
    • -
    • - -
    • -
    - -
    -
    - -
    -
    - -

    beancount.web

    - - -
    - - -
    - -

    Web server reporting front-end.

    - - - -
    - - - - - - - - - - - - -
    - - - -

    - beancount.web.bottle_utils - - - -

    - -
    - -

    Bottle utilities, mostly helpers to do mounts on top of dynamic routes.

    - - - -
    - - - - - - - - - - -
    - - - -

    - -beancount.web.bottle_utils.AttrMapper - - - -

    - -
    - -

    A URL mapper that allows attribute access for view-links. -This is used in templates.

    - - - - -
    - - - - - - - - - - -
    - - - -

    -beancount.web.bottle_utils.AttrMapper.__init__(self, mapper_function) - - - special - - -

    - -
    - -

    Constructor for an attribute mapper.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • mapper_function – A function to apply on attribute lookup, and -upon calling .build().

    • -
    -
    -
    - Source code in beancount/web/bottle_utils.py -
    def __init__(self, mapper_function):
    -    """Constructor for an attribute mapper.
    -
    -    Args:
    -      mapper_function: A function to apply on attribute lookup, and
    -        upon calling .build().
    -    """
    -    self.mapper_function = mapper_function
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.web.bottle_utils.internal_redirect(app, path_depth) - - -

    - -
    - -

    A version of bottle's mountpoint_wrapper() that we call explicitly.

    -

    Bottle supports a mount() method that allows on to install an application on -a subpath of the main application. However, it does this on a fixed path. We -want to manually intercept the lazy creation or fetching of a view and call -for a redirect explicitly (via bottle's mountpoint_wrapper() function). -However, this function is hidden within the scope of a the Bottle.mount() -method; if it were defined globally we would just use it, but it is not. So -we copy if here. This is directly lifted from Bottle.mount() and edited -minimally.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • app – A Bottle instance.

    • -
    • path_depth – The number of request path components to skip for the mount. -For example, if our subapplication is mount on /view/all, then the path -depth is 2.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A Bottle HTTPResponse object.

    • -
    -
    - - - - - - - - - - - -
    Exceptions: -
      -
    • Exception – Any exception, depending on the callback.

    • -
    -
    -
    - Source code in beancount/web/bottle_utils.py -
    def internal_redirect(app, path_depth):
    -    """A version of bottle's mountpoint_wrapper() that we call explicitly.
    -
    -    Bottle supports a mount() method that allows on to install an application on
    -    a subpath of the main application. However, it does this on a fixed path. We
    -    want to manually intercept the lazy creation or fetching of a view and call
    -    for a redirect explicitly (via bottle's mountpoint_wrapper() function).
    -    However, this function is hidden within the scope of a the Bottle.mount()
    -    method; if it were defined globally we would just use it, but it is not. So
    -    we copy if here. This is directly lifted from Bottle.mount() and edited
    -    minimally.
    -
    -    Args:
    -      app: A Bottle instance.
    -      path_depth: The number of request path components to skip for the mount.
    -        For example, if our subapplication is mount on /view/all, then the path
    -        depth is 2.
    -    Returns:
    -      A Bottle HTTPResponse object.
    -    Raises:
    -      Exception: Any exception, depending on the callback.
    -    """
    -    # pylint: disable=invalid-name
    -    try:
    -        request.path_shift(path_depth)
    -        rs = bottle.HTTPResponse([])
    -        def start_response(status, headerlist, exc_info=None):
    -            if exc_info:
    -                try:
    -                    _raise(*exc_info)
    -                finally:
    -                    exc_info = None
    -            rs.status = status
    -            for name, value in headerlist: rs.add_header(name, value)
    -            return rs.body.append
    -        body = app(request.environ, start_response)
    -        if body and rs.body: body = itertools.chain(rs.body, body)
    -        rs.body = body or rs.body
    -        return rs
    -    finally:
    -        request.path_shift(-path_depth)
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.web.scrape - - - -

    - -
    - - - - -
    - - - - - - - - - - -
    - - - - - -
    - -

    Find links targets in HTML text.

    -

    This deals with both absolute and relative links, and it external links to -external sites.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • html – An lxml document node.

    • -
    • html_path – The URL of the document node.

    • -
    -

    Yields: - URL strings, where found.

    - -
    - Source code in beancount/web/scrape.py -
    def iterlinks(html, html_path):
    -    """Find links targets in HTML text.
    -
    -    This deals with both absolute and relative links, and it external links to
    -    external sites.
    -
    -    Args:
    -      html: An lxml document node.
    -      html_path: The URL of the document node.
    -    Yields:
    -      URL strings, where found.
    -
    -    """
    -    html_dir = path.dirname(html_path)
    -    for element, attribute, link, pos in lxml.html.iterlinks(html):
    -        url = urllib.parse.urlparse(link)
    -        if url.scheme or url.netloc:
    -            continue  # Skip external urls.
    -        link = url.path
    -        if not link:
    -            continue
    -        if not path.isabs(link):
    -            link = path.join(html_dir, link)
    -        yield link
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.scrape.scrape_urls(url_format, callback, ignore_regexp=None) - - -

    - -
    - -

    Recursively scrape pages from a web address.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • url_format – The pattern for building links from relative paths.

    • -
    • callback – A callback function to invoke on each page to validate it. -The function is called with the response and the url as arguments. -This function should trigger an error on failure (via an exception).

    • -
    • ignore_regexp – A regular expression string, the urls to ignore.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A set of all the processed URLs and a set of all the skipped URLs.

    • -
    -
    -
    - Source code in beancount/web/scrape.py -
    def scrape_urls(url_format, callback, ignore_regexp=None):
    -    """Recursively scrape pages from a web address.
    -
    -    Args:
    -      url_format: The pattern for building links from relative paths.
    -      callback: A callback function to invoke on each page to validate it.
    -        The function is called with the response and the url as arguments.
    -        This function should trigger an error on failure (via an exception).
    -      ignore_regexp: A regular expression string, the urls to ignore.
    -    Returns:
    -      A set of all the processed URLs and a set of all the skipped URLs.
    -    """
    -    # The set of all URLs seen so far.
    -    seen = set()
    -
    -    # The list of all URLs to process. We use a list here so we have
    -    # reproducible order if we repeat the test.
    -    process_list = ["/"]
    -
    -    # A set of all the URLs processed and skipped everywhere.
    -    all_processed_urls = set()
    -    all_skipped_urls = set()
    -
    -    # Loop over all URLs remaining to process.
    -    while process_list:
    -        url = process_list.pop()
    -
    -        logging.debug("Processing: %s", url)
    -        all_processed_urls.add(url)
    -
    -        # Fetch the URL and check its return status.
    -        response = urllib.request.urlopen(url_format.format(url))
    -
    -        # Generate errors on redirects.
    -        redirected_url = urllib.parse.urlparse(response.geturl()).path
    -        if redirected_url != url:
    -            logging.error("Redirected: %s -> %s", url, redirected_url)
    -
    -        # Read the contents. This can only be done once.
    -        response_contents = response.read()
    -
    -        skipped_urls = set()
    -        content_type = response.info().get_content_type()
    -        if content_type == 'text/html':
    -            # Process all the links in the page and register all the unseen links to
    -            # be processed.
    -            html_root = lxml.html.document_fromstring(response_contents)
    -            for link in iterlinks(html_root, url):
    -
    -                # Skip URLs to be ignored.
    -                if ignore_regexp and re.match(ignore_regexp, link):
    -                    logging.debug("Skipping: %s", link)
    -                    skipped_urls.add(link)
    -                    all_skipped_urls.add(link)
    -                    continue
    -
    -                # Check if link has already been seen.
    -                if link in seen:
    -                    logging.debug('Seen: "%s"', link)
    -                    continue
    -
    -                # Schedule the link for scraping.
    -                logging.debug('Scheduling: "%s"', link)
    -                process_list.append(link)
    -                seen.add(link)
    -
    -        else:
    -            html_root = None
    -
    -        # Call back for processing.
    -        callback(url, response, response_contents, html_root, skipped_urls)
    -
    -    return all_processed_urls, all_skipped_urls
    -
    -
    -
    - -
    - - - -
    - - - - - -
    - -

    Open and parse the given HTML filename and verify all local targets exist.

    -

    This checks that all the files pointed to by the file we're processing are -files that exist on disk. This can be used to validate that a baked output -does not have links to files that do not exist, that all the links are valid.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • filename – A string, the name of the HTML file to process.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of – missing: A set of strings, the names of links to files that do not exist. - empty: A boolean, true if this file is empty.

    • -
    -
    -
    - Source code in beancount/web/scrape.py -
    def validate_local_links(filename):
    -    """Open and parse the given HTML filename and verify all local targets exist.
    -
    -    This checks that all the files pointed to by the file we're processing are
    -    files that exist on disk. This can be used to validate that a baked output
    -    does not have links to files that do not exist, that all the links are valid.
    -
    -    Args:
    -      filename: A string, the name of the HTML file to process.
    -    Returns:
    -      A pair of:
    -        missing: A set of strings, the names of links to files that do not exist.
    -        empty: A boolean, true if this file is empty.
    -    """
    -    filedir = path.dirname(filename)
    -    contents = open(filename, 'rb').read()
    -
    -    empty = len(contents) == 0
    -    missing = set()
    -    if not empty:
    -        html = lxml.html.document_fromstring(contents)
    -        if html is not None:
    -            for element, attribute, link, pos in lxml.html.iterlinks(html):
    -                urlpath = urllib.parse.urlparse(link)
    -                if urlpath.scheme or urlpath.netloc:
    -                    continue
    -                if path.isabs(urlpath.path):
    -                    continue
    -                target = path.normpath(path.join(filedir, urlpath.path))
    -                if not path.exists(target):
    -                    missing.add(target)
    -
    -    return missing, empty
    -
    -
    -
    - -
    - - - -
    - - - - - -
    - -

    Find all the files under the given directory and validate all their links.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • directory – A string, the root directory whose files to process.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A tuple of – files: A list of all the filenames found and processed. - missing: A set of strings, the names of links to files that do not exist. - empty: A boolean, true if this file is empty.

    • -
    -
    -
    - Source code in beancount/web/scrape.py -
    def validate_local_links_in_dir(directory):
    -    """Find all the files under the given directory and validate all their links.
    -
    -    Args:
    -      directory: A string, the root directory whose files to process.
    -    Returns:
    -      A tuple of:
    -        files: A list of all the filenames found and processed.
    -        missing: A set of strings, the names of links to files that do not exist.
    -        empty: A boolean, true if this file is empty.
    -    """
    -    logging.basicConfig(level=logging.INFO,
    -                        format='%(levelname)-8s: %(message)s')
    -    allfiles = []
    -    missing, empty = set(), set()
    -    for root, dirs, files in os.walk(directory):
    -        for filename in files:
    -            afilename = path.join(root, filename)
    -            allfiles.append(afilename)
    -            logging.info("Validating: '%s'", afilename)
    -            missing, is_empty = validate_local_links(afilename)
    -            if is_empty:
    -                empty.add(afilename)
    -    return allfiles, missing, empty
    -
    -
    -
    - -
    - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.web.views - - - -

    - -
    - -

    Views are filters on the global list of entries, which produces a subset of entries.

    - - - -
    - - - - - - - - - -
    - - - -

    - -beancount.web.views.AllView (View) - - - - -

    - -
    - -

    A view that includes all the entries, unmodified.

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.web.views.AllView.apply_filter(self, entries, options_map) - - -

    - -
    - -

    Filter the list of entries.

    -

    This is used to obtain the filtered list of entries.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to filter.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of - 1. a list of filtered entries, and - 2. an integer, the index at which the beginning of the entries for - the period begin, one directive past the opening - balances/initialization entries. - 3. a datetime.date instance, the date at which to evaluate the value - of assets held at cost.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def apply_filter(self, entries, options_map):
    -    return (entries, None, None)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.web.views.ComponentView (View) - - - - -

    - -
    - -

    A view that includes transactions with at least one posting with an account -that includes a given component.

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.web.views.ComponentView.__init__(self, entries, options_map, title, component) - - - special - - -

    - -
    - -

    Create a view clamped to one year.

    -

    Note: this is the only view where the entries are summarized and -clamped.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    • title – A string, the title of this view.

    • -
    • component – A string, the name of an account component to include.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def __init__(self, entries, options_map, title, component):
    -    """Create a view clamped to one year.
    -
    -    Note: this is the only view where the entries are summarized and
    -    clamped.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A dict of options, as produced by the parser.
    -      title: A string, the title of this view.
    -      component: A string, the name of an account component to include.
    -    """
    -    assert isinstance(component, str)
    -    self.component = component
    -    View.__init__(self, entries, options_map, title)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.views.ComponentView.apply_filter(self, entries, options_map) - - -

    - -
    - -

    Filter the list of entries.

    -

    This is used to obtain the filtered list of entries.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to filter.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of - 1. a list of filtered entries, and - 2. an integer, the index at which the beginning of the entries for - the period begin, one directive past the opening - balances/initialization entries. - 3. a datetime.date instance, the date at which to evaluate the value - of assets held at cost.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def apply_filter(self, entries, options_map):
    -    component = self.component
    -    component_entries = [entry
    -                         for entry in entries
    -                         if data.has_entry_account_component(entry, component)]
    -
    -    return component_entries, None, None
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.web.views.EmptyView (View) - - - - -

    - -
    - -

    An empty view, for testing.

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.web.views.EmptyView.__init__(self, entries, options_map, title, *args, **kw) - - - special - - -

    - -
    - -

    Create an empty view.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    • title – A string, the title of this view.

    • -
    • *args – Ignored.

    • -
    • **kw – Ignored.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def __init__(self, entries, options_map, title, *args, **kw):
    -    """Create an empty view.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A dict of options, as produced by the parser.
    -      title: A string, the title of this view.
    -      *args: Ignored.
    -      **kw: Ignored.
    -    """
    -    View.__init__(self, entries, options_map, title)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.views.EmptyView.apply_filter(self, _, __) - - -

    - -
    - -

    Return the list of entries unmodified.

    - -
    - Source code in beancount/web/views.py -
    def apply_filter(self, _, __):
    -    "Return the list of entries unmodified."
    -    return ([], None, None)
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    - -beancount.web.views.MonthView (View) - - - - -

    - -
    - -

    A view of the entries for a single month.

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.web.views.MonthView.__init__(self, entries, options_map, title, year, month) - - - special - - -

    - -
    - -

    Create a view clamped to one month.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    • title – A string, the title of this view.

    • -
    • year – An integer, the year of period.

    • -
    • month – An integer, the month to be used as year end.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def __init__(self, entries, options_map, title, year, month):
    -    """Create a view clamped to one month.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A dict of options, as produced by the parser.
    -      title: A string, the title of this view.
    -      year: An integer, the year of period.
    -      month: An integer, the month to be used as year end.
    -    """
    -    self.year = year
    -    self.month = month
    -    View.__init__(self, entries, options_map, title)
    -
    -    self.monthly = MonthNavigation.FULL
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.views.MonthView.apply_filter(self, entries, options_map) - - -

    - -
    - -

    Filter the list of entries.

    -

    This is used to obtain the filtered list of entries.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to filter.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of - 1. a list of filtered entries, and - 2. an integer, the index at which the beginning of the entries for - the period begin, one directive past the opening - balances/initialization entries. - 3. a datetime.date instance, the date at which to evaluate the value - of assets held at cost.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def apply_filter(self, entries, options_map):
    -    # Clamp to the desired period.
    -    begin_date = datetime.date(self.year, self.month, 1)
    -    end_date = date_utils.next_month(begin_date)
    -
    -    with misc_utils.log_time('clamp', logging.info):
    -        entries, index = summarize.clamp_opt(entries,
    -                                             begin_date, end_date,
    -                                             options_map)
    -    return entries, index, end_date
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.web.views.PayeeView (View) - - - - -

    - -
    - -

    A view that includes entries with some specific payee.

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.web.views.PayeeView.__init__(self, entries, options_map, title, payee) - - - special - - -

    - -
    - -

    Create a view clamped to one year.

    -

    Note: this is the only view where the entries are summarized and -clamped.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    • title – A string, the title of this view.

    • -
    • payee – A string, the payee whose transactions to include.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def __init__(self, entries, options_map, title, payee):
    -    """Create a view clamped to one year.
    -
    -    Note: this is the only view where the entries are summarized and
    -    clamped.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A dict of options, as produced by the parser.
    -      title: A string, the title of this view.
    -      payee: A string, the payee whose transactions to include.
    -    """
    -    assert isinstance(payee, str)
    -    self.payee = payee
    -    View.__init__(self, entries, options_map, title)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.views.PayeeView.apply_filter(self, entries, options_map) - - -

    - -
    - -

    Filter the list of entries.

    -

    This is used to obtain the filtered list of entries.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to filter.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of - 1. a list of filtered entries, and - 2. an integer, the index at which the beginning of the entries for - the period begin, one directive past the opening - balances/initialization entries. - 3. a datetime.date instance, the date at which to evaluate the value - of assets held at cost.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def apply_filter(self, entries, options_map):
    -    payee = self.payee
    -    payee_entries = [entry
    -                     for entry in entries
    -                     if isinstance(entry, data.Transaction) and (entry.payee == payee)]
    -
    -    return payee_entries, None, None
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.web.views.TagView (View) - - - - -

    - -
    - -

    A view that includes only entries some specific tags.

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.web.views.TagView.__init__(self, entries, options_map, title, tags) - - - special - - -

    - -
    - -

    Create a view with only entries tagged with the given tags.

    -

    Note: this is the only view where the entries are summarized and -clamped.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    • title – A string, the title of this view.

    • -
    • tags – A set of strings, the tags to include. Entries with at least -one of these tags will be included in the output.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def __init__(self, entries, options_map, title, tags):
    -    """Create a view with only entries tagged with the given tags.
    -
    -    Note: this is the only view where the entries are summarized and
    -    clamped.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A dict of options, as produced by the parser.
    -      title: A string, the title of this view.
    -      tags: A set of strings, the tags to include. Entries with at least
    -        one of these tags will be included in the output.
    -    """
    -    assert isinstance(tags, (set, frozenset, list, tuple))
    -    self.tags = tags
    -    View.__init__(self, entries, options_map, title)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.views.TagView.apply_filter(self, entries, options_map) - - -

    - -
    - -

    Filter the list of entries.

    -

    This is used to obtain the filtered list of entries.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to filter.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of - 1. a list of filtered entries, and - 2. an integer, the index at which the beginning of the entries for - the period begin, one directive past the opening - balances/initialization entries. - 3. a datetime.date instance, the date at which to evaluate the value - of assets held at cost.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def apply_filter(self, entries, options_map):
    -    tags = self.tags
    -    tagged_entries = [
    -        entry
    -        for entry in entries
    -        if isinstance(entry, data.Transaction) and entry.tags and (entry.tags & tags)]
    -
    -    return tagged_entries, None, None
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.web.views.View - - - -

    - -
    - -

    A container for filtering a subset of entries and realizing that for -display.

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.web.views.View.__init__(self, all_entries, options_map, title) - - - special - - -

    - -
    - -

    Build a View instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • all_entries – The full list of directives as output from the loader.

    • -
    • options_map – The options dict, as output by the parser.

    • -
    • title – A string, the title of this view to render.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def __init__(self, all_entries, options_map, title):
    -    """Build a View instance.
    -
    -    Args:
    -      all_entries: The full list of directives as output from the loader.
    -      options_map: The options dict, as output by the parser.
    -      title: A string, the title of this view to render.
    -    """
    -
    -    # A reference to the full list of padded entries.
    -    self.all_entries = all_entries
    -
    -    # List of filtered entries for this view, and index at the beginning
    -    # of the period transactions, past the opening balances. These are
    -    # computed in _initialize().
    -    self.entries = None
    -    self.opening_entries = None
    -    self.closing_entries = None
    -
    -    # Title.
    -    self.title = title
    -
    -    # Realization of the filtered entries to display. These are computed in
    -    # _initialize().
    -    self.real_accounts = None
    -    self.opening_real_accounts = None
    -    self.closing_real_accounts = None
    -
    -    # Monthly navigation style.
    -    self.monthly = MonthNavigation.NONE
    -
    -    # Realize now, we don't need to do this lazily because we create these
    -    # view objects on-demand and cache them.
    -    self._initialize(options_map)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.views.View.apply_filter(self, entries) - - -

    - -
    - -

    Filter the list of entries.

    -

    This is used to obtain the filtered list of entries.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to filter.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of - 1. a list of filtered entries, and - 2. an integer, the index at which the beginning of the entries for - the period begin, one directive past the opening - balances/initialization entries. - 3. a datetime.date instance, the date at which to evaluate the value - of assets held at cost.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def apply_filter(self, entries):
    -    """Filter the list of entries.
    -
    -    This is used to obtain the filtered list of entries.
    -
    -    Args:
    -      entries: A list of directives to filter.
    -    Returns:
    -      A pair of
    -        1. a list of filtered entries, and
    -        2. an integer, the index at which the beginning of the entries for
    -          the period begin, one directive past the opening
    -          balances/initialization entries.
    -        3. a datetime.date instance, the date at which to evaluate the value
    -          of assets held at cost.
    -    """
    -    raise NotImplementedError
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - -beancount.web.views.YearView (View) - - - - -

    - -
    - -

    A view of the entries for a single year.

    - - - - -
    - - - - - - - - - -
    - - - -

    -beancount.web.views.YearView.__init__(self, entries, options_map, title, year, first_month=1) - - - special - - -

    - -
    - -

    Create a view clamped to one year.

    -

    Note: this is the only view where the entries are summarized and -clamped.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives.

    • -
    • options_map – A dict of options, as produced by the parser.

    • -
    • title – A string, the title of this view.

    • -
    • year – An integer, the year of the exercise period.

    • -
    • first_month – The calendar month (starting with 1) with which the year opens.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def __init__(self, entries, options_map, title, year, first_month=1):
    -    """Create a view clamped to one year.
    -
    -    Note: this is the only view where the entries are summarized and
    -    clamped.
    -
    -    Args:
    -      entries: A list of directives.
    -      options_map: A dict of options, as produced by the parser.
    -      title: A string, the title of this view.
    -      year: An integer, the year of the exercise period.
    -      first_month: The calendar month (starting with 1) with which the year opens.
    -    """
    -    self.year = year
    -    self.first_month = first_month
    -    if not (1 <= first_month <= 12):
    -        raise ValueError("Invalid month: {}".format(first_month))
    -    View.__init__(self, entries, options_map, title)
    -
    -    self.monthly = MonthNavigation.COMPACT
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.views.YearView.apply_filter(self, entries, options_map) - - -

    - -
    - -

    Filter the list of entries.

    -

    This is used to obtain the filtered list of entries.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • entries – A list of directives to filter.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A pair of - 1. a list of filtered entries, and - 2. an integer, the index at which the beginning of the entries for - the period begin, one directive past the opening - balances/initialization entries. - 3. a datetime.date instance, the date at which to evaluate the value - of assets held at cost.

    • -
    -
    -
    - Source code in beancount/web/views.py -
    def apply_filter(self, entries, options_map):
    -    # Clamp to the desired period.
    -    begin_date = datetime.date(self.year, self.first_month, 1)
    -    end_date = datetime.date(self.year+1, self.first_month, 1)
    -    with misc_utils.log_time('clamp', logging.info):
    -        entries, index = summarize.clamp_opt(entries,
    -                                             begin_date, end_date,
    -                                             options_map)
    -    return entries, index, end_date
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - - - - -
    - -
    - -
    - - - -
    - - - -

    - beancount.web.web - - - -

    - -
    - -

    Web server for Beancount ledgers. -This uses the Bottle single-file micro web framework (with no plugins).

    - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - -

    - -beancount.web.web.HTMLFormatter (HTMLFormatter) - - - - -

    - -
    - -

    A formatter object that can be used to render accounts links.

    - -

    Attributes:

    - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescription
    build_url

    A function used to render links to a Bottle application.

    leafonly

    a boolean, if true, render only the name of the leaf nodes.

    - - - -
    - - - - - - - - - - - -
    - - - -

    -beancount.web.web.HTMLFormatter.build_global(self, *args, **kwds) - - -

    - -
    - -

    Render to global application.

    - -
    - Source code in beancount/web/web.py -
    def build_global(self, *args, **kwds):
    -    "Render to global application."
    -    return app.router.build(*args, **kwds)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.HTMLFormatter.render_account(self, account_name) - - -

    - -
    - -

    See base class.

    - -
    - Source code in beancount/web/web.py -
    def render_account(self, account_name):
    -    """See base class."""
    -    if self.view_links:
    -        if self.leaf_only:
    -            # Calculate the number of components to figure out the indent to
    -            # render at.
    -            components = account.split(account_name)
    -            indent = '{:.1f}'.format(len(components) * self.EMS_PER_COMPONENT)
    -            anchor = '<a href="{}" class="account">{}</a>'.format(
    -                self.build_url('journal',
    -                               account_name=self.account_xform.render(account_name)),
    -                account.leaf(account_name))
    -            return '<span "account" style="padding-left: {}em">{}</span>'.format(
    -                indent, anchor)
    -        else:
    -            anchor = '<a href="{}" class="account">{}</a>'.format(
    -                self.build_url('journal',
    -                               account_name=self.account_xform.render(account_name)),
    -                account_name)
    -            return '<span "account">{}</span>'.format(anchor)
    -    else:
    -        if self.leaf_only:
    -            # Calculate the number of components to figure out the indent to
    -            # render at.
    -            components = account.split(account_name)
    -            indent = '{:.1f}'.format(len(components) * self.EMS_PER_COMPONENT)
    -            account_name = account.leaf(account_name)
    -            return '<span "account" style="padding-left: {}em">{}</span>'.format(
    -                indent, account_name)
    -        else:
    -            return '<span "account">{}</span>'.format(account_name)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.HTMLFormatter.render_commodity(self, base_quote) - - -

    - -
    - -

    See base class.

    - -
    - Source code in beancount/web/web.py -
    def render_commodity(self, base_quote):
    -    """See base class."""
    -    base, quote = base_quote
    -    return '<a href="{}">{} / {}</a>'.format(
    -        self.build_url('prices', base=base, quote=quote), base, quote)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.HTMLFormatter.render_context(self, entry) - - -

    - -
    - -

    See base class.

    - -
    - Source code in beancount/web/web.py -
    def render_context(self, entry):
    -    """See base class."""
    -    # Note: rendering to global application.
    -    # Note(2): we could avoid rendering links to summarizing and transfer
    -    # entries which are not going to be found.
    -    return self.build_global('context', ehash=compare.hash_entry(entry))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.HTMLFormatter.render_doc(self, filename) - - -

    - -
    - -

    See base class.

    - -
    - Source code in beancount/web/web.py -
    def render_doc(self, filename):
    -    """See base class."""
    -    return '<a href="{}" class="filename">{}</a>'.format(
    -        self.build_url('doc', filename=filename.lstrip('/')),
    -        path.basename(filename))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.HTMLFormatter.render_event_type(self, event) - - -

    - -
    - -

    See base class.

    - -
    - Source code in beancount/web/web.py -
    def render_event_type(self, event):
    -    """See base class."""
    -    return '<a href="{}">{}</a>'.format(self.build_url('event', event=event),
    -                                        event)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.HTMLFormatter.render_inventory(self, inv) - - -

    - -
    - -

    Override this formatter to convert the inventory to units only.

    - -
    - Source code in beancount/web/web.py -
    def render_inventory(self, inv):
    -    """Override this formatter to convert the inventory to units only."""
    -    return super().render_inventory(inv.reduce(convert.get_units))
    -
    -
    -
    - -
    - - - -
    - - - - - -
    - -

    See base class.

    - -
    - Source code in beancount/web/web.py -
    def render_link(self, link):
    -    """See base class."""
    -    # Note: rendering to global application.
    -    return self.build_global('link', link=link)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.HTMLFormatter.render_source(self, source) - - -

    - -
    - -

    See base class.

    - -
    - Source code in beancount/web/web.py -
    def render_source(self, source):
    -    """See base class."""
    -    return '{}#{}'.format(app.get_url('source'), source['lineno'])
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - - -
    - - - -

    -beancount.web.web.activity() - - -

    - -
    - -

    Render the update activity.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/activity', name='activity')
    -def activity():
    -    "Render the update activity."
    -    return render_global(
    -        pagetitle="Update Activity",
    -        contents=render_real_report(misc_reports.ActivityReport,
    -                                    request.view.real_accounts,
    -                                    app.price_map,
    -                                    request.view.price_date,
    -                                    leaf_only=False))
    -
    -
    -
    - -
    - - - - - - -
    - - - -

    -beancount.web.web.auto_reload_input_file(callback) - - -

    - -
    - -

    A plugin that automatically reloads the input file if it changed since the -last page was loaded.

    - -
    - Source code in beancount/web/web.py -
    def auto_reload_input_file(callback):
    -    """A plugin that automatically reloads the input file if it changed since the
    -    last page was loaded."""
    -    def wrapper(*posargs, **kwargs):
    -        filename = app.args.filename
    -
    -        if loader.needs_refresh(app.options):
    -            logging.info('Reloading...')
    -
    -            # Save the source for later, to render.
    -            with open(filename, encoding='utf8') as f:
    -                app.source = f.read()
    -
    -            # Parse the beancount file.
    -            entries, errors, options_map = loader.load_file(filename)
    -
    -            # Print out the list of errors.
    -            if errors:
    -                # pylint: disable=unsupported-assignment-operation
    -                request.params['render_overlay'] = True
    -                print(',----------------------------------------------------------------')
    -                printer.print_errors(errors, file=sys.stdout)
    -                print('`----------------------------------------------------------------')
    -
    -            # Save globals in the global app.
    -            app.entries = entries
    -            app.errors = errors
    -            app.options = options_map
    -            app.account_types = options.get_account_types(options_map)
    -
    -            # Pre-compute the price database.
    -            app.price_map = prices.build_price_map(entries)
    -
    -            # Pre-compute the list of active years.
    -            app.active_years = list(getters.get_active_years(entries))
    -
    -            # Reset the view cache.
    -            app.views.clear()
    -
    -        else:
    -            # For now, the overlay is a link to the errors page. Always render
    -            # it on the right when there are errors.
    -            if app.errors:
    -                # pylint: disable=unsupported-assignment-operation
    -                request.params['render_overlay'] = True
    -
    -        return callback(*posargs, **kwargs)
    -    return wrapper
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.balsheet() - - -

    - -
    - -

    Balance sheet.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/balsheet', name='balsheet')
    -def balsheet():
    -    "Balance sheet."
    -    return render_view(pagetitle="Balance Sheet",
    -                       contents=render_real_report(balance_reports.BalanceSheetReport,
    -                                                   request.view.closing_real_accounts,
    -                                                   app.price_map,
    -                                                   request.view.price_date,
    -                                                   leaf_only=True))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.commodities() - - -

    - -
    - -

    Render a list commodities with list their prices page.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/commodities', name='commodities')
    -def commodities():
    -    "Render a list commodities with list their prices page."
    -    html_table = render_report(price_reports.CommoditiesReport, request.view.entries,
    -                               [],
    -                               css_id='price-index')
    -    return render_view(
    -        pagetitle="Commodities",
    -        contents=html_table)
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.web.web.context_(ehash=None) - - -

    - -
    - -

    Render the before & after context around a transaction entry.

    - -
    - Source code in beancount/web/web.py -
    @app.route('/context/<ehash:re:[a-fA-F0-9]*>', name='context')
    -def context_(ehash=None):
    -    "Render the before & after context around a transaction entry."
    -
    -    matching_entries = [entry
    -                        for entry in app.entries
    -                        if ehash == compare.hash_entry(entry)]
    -
    -    oss = io.StringIO()
    -    if len(matching_entries) == 0:
    -        print("ERROR: Could not find matching entry for '{}'".format(ehash),
    -              file=oss)
    -
    -    elif len(matching_entries) > 1:
    -        print("ERROR: Ambiguous entries for '{}'".format(ehash),
    -              file=oss)
    -        print(file=oss)
    -        dcontext = app.options['dcontext']
    -        printer.print_entries(matching_entries, dcontext, file=oss)
    -
    -    else:
    -        entry = matching_entries[0]
    -
    -        # Render the context.
    -        oss.write("<pre>\n")
    -        oss.write(context.render_entry_context(app.entries, app.options, entry))
    -        oss.write("</pre>\n")
    -
    -        # Render the filelinks.
    -        if FILELINK_PROTOCOL:
    -            meta = entry.meta
    -            uri = FILELINK_PROTOCOL.format(filename=meta.get('filename'),
    -                                           lineno=meta.get('lineno'))
    -            oss.write('<div class="filelink"><a href="{}">{}</a></div>'.format(uri, 'Open'))
    -
    -    return render_global(
    -        pagetitle="Context: {}".format(ehash),
    -        contents=oss.getvalue())
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.conversions_() - - -

    - -
    - -

    Render the list of transactions with conversions.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/conversions', name='conversions')
    -def conversions_():
    -    "Render the list of transactions with conversions."
    -    return render_view(
    -        pagetitle="Conversions",
    -        contents=render_report(journal_reports.ConversionsReport,
    -                               request.view.entries, leaf_only=False))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.doc(filename=None) - - -

    - -
    - -

    Serve static filenames for documents directives.

    - -
    - Source code in beancount/web/web.py -
    @app.route('/doc/<filename:re:.*>', name=doc_name)
    -def doc(filename=None):
    -    "Serve static filenames for documents directives."
    -
    -    filename = '/' + filename
    -
    -    # Check that there is a document directive that has this filename.
    -    # This is for security; we don't want to be able to serve just any file.
    -    for entry in misc_utils.filter_type(app.entries, data.Document):
    -        if entry.filename == filename:
    -            break
    -    else:
    -        raise bottle.HTTPError(404, "Not found")
    -
    -    # Just serve the file ourselves.
    -    return bottle.static_file(path.basename(filename),
    -                              path.dirname(filename))
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.web.web.documents() - - -

    - -
    - -

    Render a tree with all the documents found.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/documents', name='documents')
    -def documents():
    -    "Render a tree with all the documents found."
    -    return render_view(
    -        pagetitle="Documents",
    -        contents=render_report(journal_reports.DocumentsReport,
    -                               request.view.entries, leaf_only=False))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.errors() - - -

    - -
    - -

    Report error encountered during parsing, checking and realization.

    - -
    - Source code in beancount/web/web.py -
    @app.route('/errors', name='errors')
    -def errors():
    -    "Report error encountered during parsing, checking and realization."
    -    return render_global(
    -        pagetitle="Errors",
    -        contents=render_report(misc_reports.ErrorReport, [], leaf_only=False))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.event(event=None) - - -

    - -
    - -

    Render all values of a particular event.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route(r'/event/<event:re:([A-Za-z0-9\-_/.]+)?>', name='event')
    -def event(event=None):
    -    "Render all values of a particular event."
    -    if not event:
    -        bottle.redirect(app.get_url('event_index'))
    -    return render_view(
    -        pagetitle="Event: {}".format(event),
    -        contents=render_report(misc_reports.EventsReport, app.entries,
    -                               ['--expr', event]))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.event_index() - - -

    - -
    - -

    Render the latest values of all events and an index.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/event', name='event_index')
    -def event_index():
    -    "Render the latest values of all events and an index."
    -    return render_view(
    -        pagetitle="Events Index",
    -        contents=render_report(misc_reports.CurrentEventsReport,
    -                               request.view.entries))
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.web.web.get_all_view(app) - - -

    - -
    - -

    Return a view of all transactions.

    - - - - - - - - - - - - -
    Returns: -
      -
    • An instance of AllView, that covers all transactions.

    • -
    -
    -
    - Source code in beancount/web/web.py -
    def get_all_view(app):
    -    """Return a view of all transactions.
    -
    -    Returns:
    -      An instance of AllView, that covers all transactions.
    -    """
    -    return views.AllView(app.entries, app.options, 'All Transactions')
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.handle_view(path_depth) - - -

    - -
    - -

    A decorator for handlers which create views lazily. -If you decorate a method with this, the wrapper does the redirect -handling and your method is just a factory for a View instance, -which is cached.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • path_depth – An integer, the number of components that form the view id.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A decorator function, used to wrap view handlers.

    • -
    -
    -
    - Source code in beancount/web/web.py -
    def handle_view(path_depth):
    -    """A decorator for handlers which create views lazily.
    -    If you decorate a method with this, the wrapper does the redirect
    -    handling and your method is just a factory for a View instance,
    -    which is cached.
    -
    -    Args:
    -      path_depth: An integer, the number of components that form the view id.
    -    Returns:
    -      A decorator function, used to wrap view handlers.
    -    """
    -    def view_populator(callback):
    -        def wrapper(*args, **kwargs):
    -            components = request.path.split('/')
    -            viewid = '/'.join(components[:path_depth+1])
    -            try:
    -                # Try fetching the view from the cache.
    -                view = app.views[viewid]
    -            except KeyError:
    -                # We need to create the view.
    -                view = app.views[viewid] = callback(*args, **kwargs)
    -
    -            # Save the view for the subrequest and redirect. populate_view()
    -            # picks this up and saves it in request.view.
    -            request.environ['VIEW'] = view
    -            return bottle_utils.internal_redirect(viewapp, path_depth)
    -        return wrapper
    -    return view_populator
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.holdings_() - - -

    - -
    - -

    Render a detailed table of all holdings.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/equity/holdings', name='holdings')
    -def holdings_():
    -    "Render a detailed table of all holdings."
    -    html_table = render_report(holdings_reports.HoldingsReport, request.view.entries,
    -                               [],
    -                               css_class='holdings detail-table sortable', center=True)
    -    return render_view(
    -        pagetitle="Holdings",
    -        contents=html_table,
    -        scripts='<script src="/third_party/sorttable.js"></script>')
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.holdings_byaccount() - - -

    - -
    - -

    Render a table of holdings by account.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/equity/holdings_byaccount', name='holdings_byaccount')
    -def holdings_byaccount():
    -    "Render a table of holdings by account."
    -
    -    html_table = render_report(holdings_reports.HoldingsReport, request.view.entries,
    -                               ['--by', 'account'],
    -                               css_class='holdings detail-table sortable', center=True)
    -    return render_view(
    -        pagetitle="Holdings by Account",
    -        contents=html_table,
    -        scripts='<script src="/third_party/sorttable.js"></script>')
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.holdings_bycommodity() - - -

    - -
    - -

    Render a table of holdings by commodity.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/equity/holdings_bycommodity', name='holdings_bycommodity')
    -def holdings_bycommodity():
    -    "Render a table of holdings by commodity."
    -
    -    html_table = render_report(holdings_reports.HoldingsReport, request.view.entries,
    -                               ['--by', 'commodity'],
    -                               css_class='holdings detail-table sortable', center=True)
    -    return render_view(
    -        pagetitle="Holdings by Commodity",
    -        contents=html_table,
    -        scripts='<script src="/third_party/sorttable.js"></script>')
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.holdings_bycurrency() - - -

    - -
    - -

    Render a table of holdings by currency.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/equity/holdings_bycurrency', name='holdings_bycurrency')
    -def holdings_bycurrency():
    -    "Render a table of holdings by currency."
    -
    -    html_table = render_report(holdings_reports.HoldingsReport, request.view.entries,
    -                               ['--by', 'currency'],
    -                               css_class='holdings detail-table sortable', center=True)
    -    return render_view(
    -        pagetitle="Holdings by Currency",
    -        contents=html_table,
    -        scripts='<script src="/third_party/sorttable.js"></script>')
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.holdings_byrootaccount() - - -

    - -
    - -

    Render a table of holdings by account.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/equity/holdings_byrootaccount', name='holdings_byrootaccount')
    -def holdings_byrootaccount():
    -    "Render a table of holdings by account."
    -
    -    html_table = render_report(holdings_reports.HoldingsReport, request.view.entries,
    -                               ['--by', 'root-account'],
    -                               css_class='holdings detail-table sortable', center=True)
    -    return render_view(
    -        pagetitle="Holdings by Account",
    -        contents=html_table,
    -        scripts='<script src="/third_party/sorttable.js"></script>')
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.incognito(callback) - - -

    - -
    - -

    A plugin that converts all numbers rendered into X's, in order -to hide the actual values in the ledger. This is used for doing -public demos using my real ledger, where I don't necessarily -want to share the detail of my financial life with the viewers -but when I still want an interesting ledger, with enough -detail that looks realistic.

    - -
    - Source code in beancount/web/web.py -
    def incognito(callback):
    -    """A plugin that converts all numbers rendered into X's, in order
    -    to hide the actual values in the ledger. This is used for doing
    -    public demos using my real ledger, where I don't necessarily
    -    want to share the detail of my financial life with the viewers
    -    but when I still want an interesting ledger, with enough
    -    detail that looks realistic."""
    -
    -    def wrapper(*posargs, **kwargs):
    -        contents = callback(*posargs, **kwargs)
    -        # pylint: disable=bad-continuation
    -        if (response.content_type in ('text/html', '') and
    -            isinstance(contents, str)):
    -            contents = text_utils.replace_numbers(contents)
    -        return contents
    -
    -    return wrapper
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.income() - - -

    - -
    - -

    Income statement.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/income', name='income')
    -def income():
    -    "Income statement."
    -    return render_view(pagetitle="Income Statement",
    -                       contents=render_real_report(balance_reports.IncomeStatementReport,
    -                                                   request.view.real_accounts,
    -                                                   app.price_map,
    -                                                   request.view.price_date,
    -                                                   leaf_only=True))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.index() - - -

    - -
    - -

    Index of all pages, so that navigation need not have all links.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/index', name='index')
    -def index():
    -    "Index of all pages, so that navigation need not have all links."
    -
    -    oss = io.StringIO()
    -    oss.write('<ul>\n')
    -    for title, page in [
    -            ("Balances", "trial"),
    -            ("Balance Sheet", "balsheet"),
    -            ("Opening Balances", "openbal"),
    -            ("Income Statement", "income"),
    -            ("General Journal", "journal_all"),
    -            ("Conversions", "conversions"),
    -            ("Documents", "documents"),
    -            ("Holdings (Full Detail)", "holdings"),
    -            ("Holdings by Account", "holdings_byaccount"),
    -            ("Holdings by Root Account", "holdings_byrootaccount"),
    -            ("Holdings by Commodity", "holdings_bycommodity"),
    -            ("Holdings by Currency", "holdings_bycurrency"),
    -            ("Net Worth", "networth"),
    -            ("Commodities", "commodities"),
    -            ("Events", "event_index"),
    -            ("Activity/Update", "activity"),
    -            ("Statistics (Types)", "stats_types"),
    -            ("Statistics (Postings)", "stats_postings"),
    -    ]:
    -        oss.write('<li><a href={}>{}</a></li>\n'.format(
    -            request.app.get_url(page), title))
    -    oss.write('</ul>\n')
    -
    -    return render_view(
    -        pagetitle="Index",
    -        contents=oss.getvalue())
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.journal_(account_name=None) - - -

    - -
    - -

    A list of all the entries for this account realization.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/journal/<account_name:re:.*>', name='journal')
    -def journal_(account_name=None):
    -    "A list of all the entries for this account realization."
    -    account_name = app.account_xform.parse(account_name)
    -
    -    # Figure out which account to render this from.
    -    real_accounts = request.view.real_accounts
    -    if account_name:
    -        if account_name and account_types.is_balance_sheet_account(account_name,
    -                                                                   app.account_types):
    -            real_accounts = request.view.closing_real_accounts
    -
    -    # Render the report.
    -    args = []
    -    if account_name:
    -        args.append('--account={}'.format(account_name))
    -
    -    render_postings = request.params.get('postings', True)
    -    if isinstance(render_postings, str):
    -        render_postings = render_postings.lower() in ('1', 'true')
    -    if render_postings:
    -        args.append('--verbose')
    -
    -    try:
    -        html_journal = render_real_report(journal_reports.JournalReport,
    -                                          real_accounts,
    -                                          app.price_map,
    -                                          request.view.price_date,
    -                                          args, leaf_only=False)
    -    except KeyError as e:
    -        raise bottle.HTTPError(404, '{}'.format(e))
    -
    -    return render_view(pagetitle='{}'.format(account_name or
    -                                             'General Ledger (All Accounts)'),
    -                       contents=html_journal)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.journal_all() - - -

    - -
    - -

    A list of all the entries in this realization.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/journal/all', name='journal_all')
    -def journal_all():
    -    "A list of all the entries in this realization."
    -    bottle.redirect(request.app.get_url('journal', account_name=''))
    -
    -
    -
    - -
    - - - -
    - - - - - -
    - -

    Serve journals for links.

    - -
    - Source code in beancount/web/web.py -
    @app.route('/link/<link:re:.*>', name='link')
    -def link(link=None):
    -    "Serve journals for links."
    -
    -    linked_entries = basicops.filter_link(link, app.entries)
    -
    -    oss = io.StringIO()
    -    formatter = HTMLFormatter(app.options['dcontext'],
    -                              request.app.get_url, False, app.account_xform,
    -                              view_links=False)
    -    journal_html.html_entries_table_with_balance(oss, linked_entries, formatter)
    -    return render_global(
    -        pagetitle="Link: {}".format(link),
    -        contents=oss.getvalue())
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.main() - - -

    - -
    - -

    Main web service runner. This runs the event loop and blocks indefinitely.

    - -
    - Source code in beancount/web/web.py -
    def main():
    -    """Main web service runner. This runs the event loop and blocks indefinitely."""
    -
    -    argparser = version.ArgumentParser(description=__doc__.strip())
    -    add_web_arguments(argparser)
    -    args = argparser.parse_args()
    -
    -    run_app(args)
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.web.web.month_request(year, month) - - -

    - -
    - -

    Render a URL to a particular month of the context's request.

    - -
    - Source code in beancount/web/web.py -
    def month_request(year, month):
    -    """Render a URL to a particular month of the context's request.
    -    """
    -    if year < app.active_years[0] or year > app.active_years[-1]:
    -        return ''
    -    month = list(calendar.month_abbr).index(month)
    -    month = "{:0>2d}".format(month)
    -    return app.router.build('month', year=year, month=month,
    -            path=request.path[1:])
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.networth() - - -

    - -
    - -

    Render a table of the net worth for this filter.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/equity/networth', name='networth')
    -def networth():
    -    "Render a table of the net worth for this filter."
    -
    -    html_table = render_report(holdings_reports.NetWorthReport, request.view.entries)
    -    return render_view(
    -        pagetitle="Net Worth",
    -        contents=html_table)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.openbal() - - -

    - -
    - -

    Opening balances.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/openbal', name='openbal')
    -def openbal():
    -    "Opening balances."
    -    return render_view(pagetitle="Opening Balances",
    -                       contents=render_real_report(balance_reports.BalanceSheetReport,
    -                                                   request.view.opening_real_accounts,
    -                                                   app.price_map,
    -                                                   request.view.price_date,
    -                                                   leaf_only=True))
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.web.web.populate_view(callback) - - -

    - -
    - -

    A plugin that will populate the request with the current view instance.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • callback – A continuation function to call to handle the request.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A function to call to install view-specific parameters on the request.

    • -
    -
    -
    - Source code in beancount/web/web.py -
    def populate_view(callback):
    -    """A plugin that will populate the request with the current view instance.
    -
    -    Args:
    -      callback: A continuation function to call to handle the request.
    -    Returns:
    -      A function to call to install view-specific parameters on the request.
    -    """
    -    def wrapper(*args, **kwargs):
    -        request.view = request.environ['VIEW']
    -        return callback(*args, **kwargs)
    -    return wrapper
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.prices_values(base=None, quote=None) - - -

    - -
    - -

    Render all the values for a particular price pair.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/prices'
    -               r'/<base:re:[A-Z][A-Z0-9\'\.\_\-]{0,22}[A-Z0-9]>'
    -               r'/<quote:re:[A-Z][A-Z0-9\'\.\_\-]{0,22}[A-Z0-9]>', name='prices')
    -def prices_values(base=None, quote=None):
    -    "Render all the values for a particular price pair."
    -    html_table = render_report(price_reports.CommodityPricesReport, request.view.entries,
    -                               ['--commodity', '{}/{}'.format(base, quote)],
    -                               css_id='price-index')
    -    return render_view(
    -        pagetitle="Price: {} / {}".format(base, quote),
    -        contents=html_table)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.render_global(*args, **kw) - - -

    - -
    - -

    Render the title and contents in our standard template for a global page.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • *args – A tuple of values for the HTML template.

    • -
    • *kw – A dict of optional values for the HTML template.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An HTML string of the rendered template.

    • -
    -
    -
    - Source code in beancount/web/web.py -
    def render_global(*args, **kw):
    -    """Render the title and contents in our standard template for a global page.
    -
    -    Args:
    -      *args: A tuple of values for the HTML template.
    -      *kw: A dict of optional values for the HTML template.
    -    Returns:
    -      An HTML string of the rendered template.
    -    """
    -    response.content_type = 'text/html'
    -    kw['A'] = A # Application mapper
    -    kw['V'] = V # View mapper
    -    kw['title'] = app.options['title']
    -    kw['view_title'] = ''
    -    kw['navigation'] = GLOBAL_NAVIGATION
    -    kw['scripts'] = kw.get('scripts', '')
    -
    -    kw['overlay'] = (
    -        render_overlay('<li><a href="{}">Errors</a></li>'.format(
    -            app.router.build('errors')))
    -        if request.params.pop('render_overlay', True)
    -        else '')
    -    return template.render(*args, **kw)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.render_overlay(contents) - - -

    - -
    - -

    Render an overlay of the navigation with the current errors.

    -

    This is used to bring up new errors on any page when they occur.

    - - - - - - - - - - - - -
    Returns: -
      -
    • A string of HTML for the contents of the errors overlay.

    • -
    -
    -
    - Source code in beancount/web/web.py -
    def render_overlay(contents):
    -    """Render an overlay of the navigation with the current errors.
    -
    -    This is used to bring up new errors on any page when they occur.
    -
    -    Returns:
    -      A string of HTML for the contents of the errors overlay.
    -    """
    -    return '''
    -      <div class="navigation" id="nav-right">
    -        <ul>
    -          {}
    -        </ul>
    -      </div>'''.format(contents)
    -
    -    # It would be nice to have a fancy overlay here, that automatically appears
    -    # after parsing if there are errors and that automatically smoothly fades
    -    # out.
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.render_real_report(report_class, real_root, price_map, price_date, args=None, leaf_only=False) - - -

    - -
    - -

    Instantiate a report and rendering it to a string.

    -

    This is intended to be called in the context of a Bottle view app request -(it uses 'request').

    - - - - - - - - - - - - -
    Parameters: -
      -
    • report_class – A class, the type of the report to render.

    • -
    • real_root – An instance of RealAccount to render.

    • -
    • price_map – A price map as built by build_price_map().

    • -
    • price_date – The date at which to evaluate the prices.

    • -
    • args – A list of strings, the arguments to initialize the report with.

    • -
    • leaf_only – A boolean, whether to render the leaf names only.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, the rendered report.

    • -
    -
    -
    - Source code in beancount/web/web.py -
    def render_real_report(report_class, real_root, price_map, price_date,
    -                       args=None, leaf_only=False):
    -    # pylint: disable=too-many-arguments
    -    """Instantiate a report and rendering it to a string.
    -
    -    This is intended to be called in the context of a Bottle view app request
    -    (it uses 'request').
    -
    -    Args:
    -      report_class: A class, the type of the report to render.
    -      real_root: An instance of RealAccount to render.
    -      price_map: A price map as built by build_price_map().
    -      price_date: The date at which to evaluate the prices.
    -      args: A list of strings, the arguments to initialize the report with.
    -      leaf_only: A boolean, whether to render the leaf names only.
    -    Returns:
    -      A string, the rendered report.
    -    """
    -    formatter = HTMLFormatter(app.options['dcontext'],
    -                              request.app.get_url, leaf_only, app.account_xform)
    -    oss = io.StringIO()
    -    report_ = report_class.from_args(args, formatter=formatter)
    -    report_.render_real_htmldiv(real_root, price_map, price_date, app.options, oss)
    -    return oss.getvalue()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.render_report(report_class, entries, args=None, css_id=None, css_class=None, center=False, leaf_only=True) - - -

    - -
    - -

    Instantiate a report and rendering it to a string.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • report_class – A class, the type of the report to render.

    • -
    • real_root – An instance of RealAccount to render.

    • -
    • args – A list of strings, the arguments to initialize the report with.

    • -
    • css_id – An optional string, the CSS id for the div to render.

    • -
    • css_class – An optional string, the CSS class for the div to render.

    • -
    • center – A boolean flag, if true, wrap the results in a <center> tag.

    • -
    • leaf_only – A boolean, whether to render the leaf names only.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A string, the rendered report.

    • -
    -
    -
    - Source code in beancount/web/web.py -
    def render_report(report_class, entries, args=None,
    -                  css_id=None, css_class=None, center=False, leaf_only=True):
    -    """Instantiate a report and rendering it to a string.
    -
    -    Args:
    -      report_class: A class, the type of the report to render.
    -      real_root: An instance of RealAccount to render.
    -      args: A list of strings, the arguments to initialize the report with.
    -      css_id: An optional string, the CSS id for the div to render.
    -      css_class: An optional string, the CSS class for the div to render.
    -      center: A boolean flag, if true, wrap the results in a <center> tag.
    -      leaf_only: A boolean, whether to render the leaf names only.
    -    Returns:
    -      A string, the rendered report.
    -    """
    -    formatter = HTMLFormatter(app.options['dcontext'],
    -                              request.app.get_url, leaf_only, app.account_xform)
    -    oss = io.StringIO()
    -    if center:
    -        oss.write('<center>\n')
    -    report_ = report_class.from_args(args,
    -                                     formatter=formatter,
    -                                     css_id=css_id,
    -                                     css_class=css_class)
    -    report_.render_htmldiv(entries, app.errors, app.options, oss)
    -    if center:
    -        oss.write('</center>\n')
    -    return oss.getvalue()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.render_view(*args, **kw) - - -

    - -
    - -

    Render the title and contents in our standard template for a view page.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • *args – A tuple of values for the HTML template.

    • -
    • *kw – A dict of optional values for the HTML template.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • An HTML string of the rendered template.

    • -
    -
    -
    - Source code in beancount/web/web.py -
    def render_view(*args, **kw):
    -    """Render the title and contents in our standard template for a view page.
    -
    -    Args:
    -      *args: A tuple of values for the HTML template.
    -      *kw: A dict of optional values for the HTML template.
    -    Returns:
    -      An HTML string of the rendered template.
    -    """
    -    response.content_type = 'text/html'
    -    kw['A'] = A # Application mapper
    -    kw['V'] = V # View mapper
    -    kw['title'] = app.options['title']
    -    kw['view_title'] = ' - ' + request.view.title
    -
    -    overlays = []
    -    if request.params.pop('render_overlay', False):
    -        overlays.append(
    -            '<li><a href="{}">Errors</a></li>'.format(app.router.build('errors')))
    -
    -    # Render navigation, with monthly navigation option.
    -    oss = io.StringIO()
    -    oss.write(APP_NAVIGATION.render(A=A, V=V, view_title=request.view.title))
    -    if request.view.monthly is views.MonthNavigation.COMPACT:
    -        overlays.append(
    -            '<li><a href="{}">Monthly</a></li>'.format(M.Jan))
    -    elif request.view.monthly is views.MonthNavigation.FULL:
    -        annual = app.router.build('year',
    -                                  path=DEFAULT_VIEW_REDIRECT,
    -                                  year=request.view.year)
    -        oss.write(APP_NAVIGATION_MONTHLY_FULL.render(M=M, Mp=Mp, Mn=Mn, V=V, annual=annual))
    -    kw['navigation'] = oss.getvalue()
    -    kw['overlay'] = render_overlay(' '.join(overlays))
    -
    -    kw['scripts'] = kw.get('scripts', '')
    -
    -    return template.render(*args, **kw)
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.root() - - -

    - -
    - -

    Redirect the root page to the home page.

    - -
    - Source code in beancount/web/web.py -
    @app.route('/', name='root')
    -def root():
    -    "Redirect the root page to the home page."
    -    bottle.redirect(app.get_url('toc'))
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.web.web.scrape_webapp(webargs, callback, ignore_regexp) - - -

    - -
    - -

    Run a web server on a Beancount file and scrape it.

    -

    This is the main entry point of this module.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • webargs – An argparse.Namespace container of the arguments provided in -web.add_web_arguments().

    • -
    • callback – A callback function to invoke on each page to validate it. -The function is called with the response and the url as arguments. -This function should trigger an error on failure (via an exception).

    • -
    • ignore_regexp – A regular expression string, the urls to ignore.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A set of all the processed URLs and a set of all the skipped URLs.

    • -
    -
    -
    - Source code in beancount/web/web.py -
    def scrape_webapp(webargs, callback, ignore_regexp):
    -    """Run a web server on a Beancount file and scrape it.
    -
    -    This is the main entry point of this module.
    -
    -    Args:
    -      webargs: An argparse.Namespace container of the arguments provided in
    -        web.add_web_arguments().
    -      callback: A callback function to invoke on each page to validate it.
    -        The function is called with the response and the url as arguments.
    -        This function should trigger an error on failure (via an exception).
    -      ignore_regexp: A regular expression string, the urls to ignore.
    -    Returns:
    -      A set of all the processed URLs and a set of all the skipped URLs.
    -    """
    -    url_format = 'http://localhost:{}{{}}'.format(webargs.port)
    -
    -    thread = thread_server_start(webargs)
    -
    -    # Skips:
    -    # - Docs cannot be read for external files.
    -    #
    -    # - Components views... well there are just too many, makes the tests
    -    #   impossibly slow. Just keep the A's so some are covered.
    -    url_lists = scrape.scrape_urls(url_format, callback, ignore_regexp)
    -
    -    thread_server_shutdown(thread)
    -
    -    return url_lists
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.setup_monkey_patch_for_server_shutdown() - - -

    - -
    - -

    Setup globals to steal access to the server reference. -This is required to initiate shutdown, unfortunately. -(Bottle could easily remedy that.)

    - -
    - Source code in beancount/web/web.py -
    def setup_monkey_patch_for_server_shutdown():
    -    """Setup globals to steal access to the server reference.
    -    This is required to initiate shutdown, unfortunately.
    -    (Bottle could easily remedy that.)"""
    -
    -    # Save the original function.
    -    # pylint: disable=import-outside-toplevel
    -    from wsgiref.simple_server import make_server
    -
    -    # Create a decorator that will save the server upon start.
    -    def stealing_make_server(*args, **kw):
    -        global server
    -        server = make_server(*args, **kw)
    -        return server
    -
    -    # Patch up wsgiref itself with the decorated function.
    -    import wsgiref.simple_server
    -    wsgiref.simple_server.make_server = stealing_make_server
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.shutdown() - - -

    - -
    - -

    Request for the server to shutdown.

    - -
    - Source code in beancount/web/web.py -
    def shutdown():
    -    """Request for the server to shutdown."""
    -    server.shutdown()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.source() - - -

    - -
    - -

    Render the source file, allowing scrolling at a specific line.

    - -
    - Source code in beancount/web/web.py -
    @app.route('/source', name='source')
    -def source():
    -    "Render the source file, allowing scrolling at a specific line."
    -
    -    contents = io.StringIO()
    -    if app.args.no_source:
    -        contents.write("Source hidden.")
    -    else:
    -        contents.write('<div id="source">')
    -        for i, line in enumerate(app.source.splitlines()):
    -            lineno = i+1
    -            contents.write(
    -                '<pre id="{lineno}">{lineno}  {line}</pre>\n'.format(
    -                    lineno=lineno, line=line.rstrip()))
    -        contents.write('</div>')
    -
    -    return render_global(
    -        pagetitle="Source",
    -        contents=contents.getvalue()
    -        )
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.stats_postings() - - -

    - -
    - -

    Compute and render statistics about the input file.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/stats_postings', name='stats_postings')
    -def stats_postings():
    -    "Compute and render statistics about the input file."
    -    return render_view(
    -        pagetitle="Postings Statistics",
    -        contents=render_report(misc_reports.StatsPostingsReport,
    -                               request.view.entries))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.stats_types() - - -

    - -
    - -

    Compute and render statistics about the input file.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/stats_types', name='stats_types')
    -def stats_types():
    -    "Compute and render statistics about the input file."
    -    return render_view(
    -        pagetitle="Directives Statistics",
    -        contents=render_report(misc_reports.StatsDirectivesReport,
    -                               request.view.entries))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.style() - - -

    - -
    - -

    Stylesheet for the entire document.

    - -
    - Source code in beancount/web/web.py -
    @app.route('/resources/web.css', name='style')
    -def style():
    -    "Stylesheet for the entire document."
    -    response.content_type = 'text/css'
    -    if app.args.debug:
    -        with open(path.join(path.dirname(__file__), 'web.css')) as f:
    -            global STYLE; STYLE = f.read()
    -    return STYLE
    -
    -
    -
    - -
    - - - - - -
    - - - -

    -beancount.web.web.thread_server_shutdown(thread) - - -

    - -
    - -

    Shutdown the server running in the given thread.

    -

    Unfortunately, in the meantime this has a side-effect on all servers. -This returns after waiting that the thread has stopped.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • thread – A threading.Thread instance.

    • -
    -
    -
    - Source code in beancount/web/web.py -
    def thread_server_shutdown(thread):
    -    """Shutdown the server running in the given thread.
    -
    -    Unfortunately, in the meantime this has a side-effect on all servers.
    -    This returns after waiting that the thread has stopped.
    -
    -    Args:
    -      thread: A threading.Thread instance.
    -    """
    -    # Clean shutdown: request to stop, then join the thread.
    -    # Note that because we daemonize, we could forego this elegant detail.
    -    shutdown()
    -    thread.join()
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.thread_server_start(web_args, **kwargs) - - -

    - -
    - -

    Start a server in a new thread.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • argparse_args – An argparse parsed options object, with all the options -from add_web_arguments().

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A new Thread instance.

    • -
    -
    -
    - Source code in beancount/web/web.py -
    def thread_server_start(web_args, **kwargs):
    -    """Start a server in a new thread.
    -
    -    Args:
    -      argparse_args: An argparse parsed options object, with all the options
    -        from add_web_arguments().
    -    Returns:
    -      A new Thread instance.
    -
    -    """
    -    thread = threading.Thread(
    -        target=run_app,
    -        args=(web_args,),
    -        kwargs=kwargs)
    -    thread.daemon = True # Automatically exit if the process comes dwn.
    -    thread.start()
    -
    -    # Ensure the server has at least started before running the scraper.
    -    wait_ready()
    -    time.sleep(0.1)
    -
    -    return thread
    -
    -
    -
    - -
    - - - - -
    - - - -

    -beancount.web.web.trial() - - -

    - -
    - -

    Trial balance / Chart of Accounts.

    - -
    - Source code in beancount/web/web.py -
    @viewapp.route('/trial', name='trial')
    -def trial():
    -    "Trial balance / Chart of Accounts."
    -    return render_view(
    -        pagetitle="Trial Balance",
    -        contents=render_real_report(balance_reports.BalancesReport,
    -                                    request.view.real_accounts,
    -                                    app.price_map,
    -                                    request.view.price_date,
    -                                    leaf_only=True))
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.url_restrict_generator(url_prefix) - - -

    - -
    - -

    Restrict to only a single prefix.

    - - - - - - - - - - - - -
    Parameters: -
      -
    • url_prefix – A string, a URL prefix to restrict to.

    • -
    • callback – The function to wrap.

    • -
    -
    - - - - - - - - - - - -
    Returns: -
      -
    • A handler decorator.

    • -
    -
    -
    - Source code in beancount/web/web.py -
    def url_restrict_generator(url_prefix):
    -    """Restrict to only a single prefix.
    -
    -    Args:
    -      url_prefix: A string, a URL prefix to restrict to.
    -      callback: The function to wrap.
    -    Returns:
    -      A handler decorator.
    -    """
    -    # A list of URLs that should always be accepted, even when restricted.
    -    allowed_regexps = [re.compile(regexp).match
    -                       for regexp in ['/resources',
    -                                      '/favicon.ico',
    -                                      '/index',
    -                                      '/errors',
    -                                      '/source',
    -                                      '/context',
    -                                      '/third_party']]
    -
    -    def url_restrict_handler(callback):
    -        def wrapper(*args, **kwargs):
    -            if (any(match(request.path) for match in allowed_regexps) or
    -                request.path.startswith(url_prefix)):
    -                return callback(*args, **kwargs)
    -            if request.path == '/':
    -                bottle.redirect(url_prefix)
    -
    -            # Note: we issue a "202 Accepted" status in order to satisfy bean-bake,
    -            # we want to distinguish between an actual 404 error and this case.
    -            raise bottle.HTTPError(202, "URLs restricted to '{}'".format(url_prefix))
    -
    -        return wrapper
    -    return url_restrict_handler
    -
    -
    -
    - -
    - - - -
    - - - -

    -beancount.web.web.wait_ready() - - -

    - -
    - -

    Wait until the 'server' global has been set. -This tells us the server is running.

    - -
    - Source code in beancount/web/web.py -
    def wait_ready():
    -    """Wait until the 'server' global has been set.
    -    This tells us the server is running.
    -    """
    -    while server is None:
    -        time.sleep(0.05)
    -
    -
    -
    - -
    - - - - - - - -
    - -
    - -
    - - - - -
    - -
    - -
    - -
    -
    - - -
    -
    - -
    - -
    - -
    - - - - « Previous - - - -
    - - - - - - - - diff --git a/api_reference/index.html b/api_reference/index.html index deb74651..91ec6e5e 100644 --- a/api_reference/index.html +++ b/api_reference/index.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -154,8 +156,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -164,20 +164,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • @@ -219,18 +211,13 @@

    beancount

    diff --git a/balance_assertions_in_beancount.html b/balance_assertions_in_beancount.html index 34ead125..3ced37c3 100644 --- a/balance_assertions_in_beancount.html +++ b/balance_assertions_in_beancount.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -178,8 +180,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -188,20 +188,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/beancount_cheat_sheet.html b/beancount_cheat_sheet.html index bd43945e..1db7b715 100644 --- a/beancount_cheat_sheet.html +++ b/beancount_cheat_sheet.html @@ -107,6 +107,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -154,8 +156,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -164,20 +164,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/beancount_design_doc.html b/beancount_design_doc.html index 15b46ecc..b96f1c87 100644 --- a/beancount_design_doc.html +++ b/beancount_design_doc.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -270,8 +272,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -280,20 +280,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • @@ -804,7 +796,7 @@

    Stream Invariants

    Loader & Processing Order

    The process of loading a list of entries from an input file is the heart of the project. It is important to understand it in order to understand how Beancount works. Refer to the diagram below.

    -

    +

    It consists of

    1. @@ -875,7 +867,7 @@

      Two Stages of Parsing: Incompl

      A separate step for the interpolation which will have available the inventory balances of each account as inputs. This second step is where the booking algorithms (e.g., FIFO) will be invoked from.

    -

    +

    See the diagram above for reference. Once implemented, everything else should be the same.

    The Printer

    In the same package as the parser lives a printer. This isolates all the functionality that deals with the Beancount “language” in the beancount.parser package: the parser converts from input syntax to data structure and the printer does the reverse. No code outside this package should concern itself with the Beancount syntax.

    diff --git a/beancount_design_doc/media/033922268f9fe3d624c80b311804a81e4990ae07.png b/beancount_design_doc/media/033922268f9fe3d624c80b311804a81e4990ae07.png deleted file mode 100644 index 109cfa2ed2cb9119b1bf0a054afe9b18cf16bdbd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12355 zcmcJ0cQoAZ^Y0>hCqZ-}q!2{!iH#OPltpwaVRa&_mx$h@gy=R(SapNddrMPeWvxzP zmDPgPxhvn_?{`1vd+Ry(+<*4$zP+EBXJ%eA&&)g{=Bcg*EfqTz005xX(tKn901(v? zzE4t+5k56<_jv;VVpLj>9vb;tZRCiE_fYF?5$7v0bN*H`r6b^ z6s^W^P3dS-@t0d|IR z#8#qa1STlGjW=NOCMb>l6rXcu_{~PsN!5CT{nu+?)P|KJVP9jn&a2D{vI@;A{PpUq zoGP4Hw{mRZ1FcIkAccT|DEM-~fFa1vh_%!kv{C90LaC$r6gCTv3*jw3w4B(TS#bgl z14$q##8`*bycS~$+9=jxTKCvwGCxtJEpIc8S`K5s?qu3w;=0v;UkGc z`Ya)#IIiQlF$D)Hgj6dnF*X4|_LCt%MOInT(~4U*im*_NMkCfWR#D%jV`>}soM15l zxp*dp*S$Qen*U~U&8%%9TV}!=F{I4pA$!V~68F9V9dT`?UeXaZwY@?oGWVFDfD_Fd zomWqYu3Cgg{Y%s-pCwV$Dr7UN1aM!Ss^e3wpfR$z!R{)Jx2^ zz5jZE4DJ%ly_Ce0JM?&pzDn0ou)01f~qEk3$b7Bnipa>qx8>& z4A=+O&uy<%WIG>d5w-4_zui{$yi6SVKof3iSg*R@=G5fmjwsh=gS%YYH<#OVD18J+ zW90eDNL0c@%DD|Hxe4M5#2w>5GNs7lnH5&Ok?=cd>J(b{BGg+iA2F?er=t;KP&EMh z>fYchvkPBSK#4D0QPqS~RI>v$;SV@YIN(z~lmrPDtJj;fWJBK;NvPp&m2O_X&*_)Y44p((Df@L(*T8rd@t_E<8GHEl~8l?wUeqv%734 zLyA*#*>Wi+ikU*|ejxDDB>!g2|J$FMfqOI>k7#_vg~Az_s1G9eZ!G(R$~}r+8q2Yr|N>=4DYM#Po&H7q+=9&@e z1-#HmD8LcTx@7|QGQ!5poT7XnjXXng7A}<=P}DuR6etelP>P}COK;7y=n1nM0D@E55=%F-z{da0ex7#8Jk*yGVf16;5_)Huns(eHqL-z3X)*Ag>(Fs^(vHwF{{YbOH(ufsZ z@7AwA#xb_}ky)no@v1cTmbJSRy0#hgF;EC3@yb+mke0$1g)It1B8|6S9GVsRvCWIf z84{kxS1%_So)PD2)s^HIzvhj)f^e^dz;{j<3J#tN}Y?VG3 zb0QF)#>j6ZVpJYr8+ao=Yw|fLL^Qa-i5xK)1Bf7cut7aviA+Uj{p^P)TPfzS9&^jX zktVeJi=!?vUL!o@8gdhHLPdyQUXZ)J?XrBdgKr;3k!eI`NbH?ieOSALyv`lek`xDZ zktG-PCP?#)H@qlka)Cni>y_~;vIu^X&KO%>O@`Lbu%w?K3F6EA3&CHLw#5Qk7mjrj zF2fAC^Va>n(4!F;Bf$#VEdHt*a-CxKuinq`d)It+24nXvc)gV;X%K~63*%Kx3eVZS zhQuDzEWEH{=lcyf{w6jmPociN$r?*=Von;A2CYl$8awJ1!pjFBm<|@WzWf0e8Oe8U z>Uu?+&bCxS(7|q&or+wx>pEB?)%B+Lo&cX`O(;6KnXR5$>;|herR}c`8pQ61m=0;7 zr%+6g{;mcdtSrdMt-eAW|9u9CV+dBYu+bEZi1n{*Gzc@h7Acg(s-QySUVHu=B5WN2 zJ8B2+E(Jw-XQ1U=mimUYB*0O>?NrwhvrUe$$1h#^XdgE#^$ZOeZQMle0|LvkZhQYx zF}{+?Hvyy7KLb^4bXy^^XiMJDtI_4NwI?Mf9`;^kMxWU5U89_JvoGZa1$}sFP$Al@ z`?c4C^~v~?`@ZFUMQakh@}*8+pb+Zbsxvlkh$)_m`J=ewF$1a!oXv@te%cr#Z{t@NkpH|IJ8Ql@8Q@*T}^wgI>&(*RUUjJ=45x_agJZ~ui$-C;wAiwMhIK& z@c0fz>u8QX2(;g^Hc{CzeBW!C)A?JPXkbb_YFIA)*(5y3*E(Dc%aA2 ztoJr1nGGaaXAwPQHO`>ltJvD_oWoBrl4;`#`?+BJZ<_(v`=} zN6@As7RE~gu&GowJ?Vql7sXx!iF1}g%b6-6Z7+*=H||y&rOh^91UqbPG+b!Z+7AYg zV0Ti`Y6e>OCu^YP#ymb7=$($AInZ>&GJGo*ITf7F8=zhea591{N0JcRMj<0jg%YyN z&@kA=#zVvurxLHXfixMAFR&WD|3r(l#x-tv@SB z{(APA3|OI5>k}IJB93KFZ_4pwzT@yj-p}jOIAG8{Ywe>c2{VdQBJQKkA*OS0aohRc zcyzGAYrXKET~V#and7a`YV-BvRtj=>RBx`&iMIXXL<}oKkVpDoI~aJmE3;+h67e~A z(YsxSC4syXc|MdXGOA;puca%^f&xyzXUeGnzOH?4JHR_T{hqY|j^|B*OfE9;Io44+ zxyM35x)%qZAtt(T70}9!d z9qqt?q3Vm)(@zqm?sIy@_VkLXDSErhgFY^(Pru;}JifdGD`hS)`tEhp0b}7Jn$@ZD zpWJoWR>n=er)ApeUz%8=q>6X#`Yd2IzlIUD1zaTH zEmY2yB3N_0Q78|{`d0)D9=Lp|uX^d-nJ?GHC* zp(>}jDz0<|{ufm6VZpB0%Hz2<=bsVl)%C`S$%YZA28ZF{{heH<;5_k%Cqq5*J&~5c z0Jrzc+}`hhlm{Ff^bFr8OR*o&v2TFx)c(3hk}AM=@&0V~*B)+SLy6AWt&uMC{%?t( zz~RVz2b#`%kU2j#^vB_kb=b&Nq*`1KLE|uMS=|)KUkah30~kxeP}TP25Kb96r_XaU ztsb_Q+kkFNI@mQzESK~pFk*bHFa&cX;ituvpfk|m(1hPzi`|-^aE)3U$a|0ReT+5& z?WYY@aQMr-Ry>B2|09)ipVZF+dfINME1qBhJbqR`N|{qlu!;kzDQ} z32?afw)og9?~$u)w{3&9|ARfpI|V0EmtRJ!)GN)MUtooT|=+Jv)7ug z!<1x9Z+`kMfcCWTHW#{ivPdDuWPP4c1Gx2AoP}5mr$~Pno$1yp8>pV3KbpsJ3-i*d z??nUD;FD+~fPd%rT&XGGDvXVqh#w#D^+*T#wmWzA&;^dQ564g2jHWw-a{is_&0ANt zGatqGnuxJo@~-=BCeD;UGokgRU6we$HzR~!7!b=u9qMl<(Eg@NQ)%NmiH5Y5dt1(7 zL3`k)92Tz@_j0jte1gMmrS;%5zuZpy`FS_>P$jFyJh!z>NW0fDENVIDhbe(~pJw1c znUB{33O@Cz7R<86RNIId=miAtSG92dXmHmJd`fQ_+*I3hxFfft?{xTsJ(ZbB$MVXYK^b&s({e@a^x=l|-1PO4mWJBHG{fob2%)?c8|^HuY_~Oe z_THI>=P_PgltQx1(wNVc4|B!5N&>hyYY9AjPD#QXm>GgsEnKkSK=;iAMf?W@nh3!%aix>d+U=wo)yjQd>$*_^x-}4Gk1r2 z&-yzboTkz%jvch?LL(-AXmj+Qd|F!!lvxc)%oN7NP3^#<)3#= zMjpmnU`;~p{3Q`{2YY$+#TsK5yk4C>HhNiTBMcO zk?go@KbRAzEC1za?FgGY(=gee8L~Gj7ZN~#mGBYLxm8U0p)BX!|3Wp~>aW3suf(HI z(s#{W_)t>v!vA5o8lZxivve?KEsU&53SI!5(sy<*eP$`{Q&sL+sNSSYN4K$EW+?Hq z8+N1-cGYFt;_q3bgB?`lyaV<7%g7&#tww`r0$V(@8MjTh@PUYJF*BBz{GP7EF3^ z<~`Y`q#lBj&URTm-Aq()f#TsY{<#F@C2D-IlZ0;(gwZYQN+J5 zZ(oZp*{Vz3?W-Mb9!%u_Y1#u-9nj6+%nLZmk$XlktgDn;&XhpYP?mFZBJ)n^2JD$f zA@nD7f=l}Q(Z--m%g%YtSxfMlv%}}tL{%h@Hk=1U8hVM*RX^VBzUiT&o6^WL`C&Fm z=3KoYuegWhQ#NI`D8Ux?vz|}&d=POEO94k2-^6os5ql~tf!cbK{Td%Y17$w})Y0#LwJuqxTqIT6J`8$OH{q?H;uZ%%80Q^Tc!WQ{kk=06n@sv2hP2} zXl*D$$n-NC#}vp;;BLmQ700Igc}`}$Z4CcGbdi|1?4Kq}L+7oId=eXzJaD}eUo;N0 zT@h67>VlKE+R4^Ax7zK+wN+r$|0qayvYS!$sq%`dQ?XuI@JM63jZm_CRiq*$eEY8? z22prNUe`Bk*SFZ{Twn5TxS%psql`WO$!R2yLm2atjQ=(i5<*$zDl$puI`sh41eb&B z7>SZDiZg`MiS+%?7F=5ysoG8~2f3F@8zvZ?>7X#$Em}^T7(cjS+tRU3(ZZ1^%YPga z|6Nx!8%Miyr~R48rNm8&fntZ8JL zOEWP)-kT=$C0-m&%`N8`KK{GCr%@AWZ2!w;{l8YKa^;(bXx8sV{?Hb=)aSM3k^XYB zD1QEa%^$y2;*;+S15$YZiT%vUT@5|Og(~4UB9_N{D+|+Yz6h`VOb7XD5$ilJx3Wj( zLpDX-oMxPCF`^ghH|_vx_UQ1|RMgu0t@9$gse=F1!HrMYQb~=Ci;*_`%Ou2z$JXXd zZk}aJvw3g$u*f^ex9jmEFtx3O7~@D>Gc0=wJ9=0gW6;s&Qd+M&t5TxKwb3^gO`R?`X~hmaIq zWyV^5SL%&^%~wY<;H~|1v?IwvlmkIL-h5!#2yX0&vm?5Xbph+vI%=sDcve60vOk_} zfwN}HEfdv^xP8OP$26BS0$?mVpqMvM-TYPLN{xXWa&x-@vmJr@=94WuDk6!Tagp%Z zf2l&NK*OW>BG&8VgWBf7kE|8vPfJ(+u%w=Zjl9F2nlN z0Uv@6(RP{J4tgrNUbn|*?|_N3Q)8Spm8Y-eT7?$JK=R(u4-v!Hb_>n@LD`SQJ}Z>j9BL#jgQF<@Eoj_ znVvUlaNEF@WA9nFqG!ChZC$dWUs;5SN$Z5$_3SSwN*jf$7QDYmy$Cr{-OpX)K#1y< z*7GS*orqk!j5NWG;=>!{h`7ilwm6PQZo}`2qFt_gpZaW}FY;Y}M|fH>i5aBM&3lSf zzIRB|JFh)a`4Z&)Y)B+FDq4#3qM%s=N8I~fSPA%v3*!V9vd!OG4Fai%R+`46Pi5X2 zlO}HUI9w%ty){yzn}>6Yq!`4wtN1{};qW`yuYSAJyHB;d&keeqM~7ciew4*?VLT=5 zpO3lAT?G0#`^B^EF3nSfc%AKUic8z>aeWYdV$b!%V%6u%2k92P$jZ^{qcP<@NV4jr3hsaU!y_-*OsegignrUMgfgl%i=2+ns9UD+>Q1IHXDO!z?10LCEuc6(fF?NlI} zBI{r*y47Rn_QytrZLp~qHj!&aat?M`tt_7=nN)aaL_uBVW?hY2!J8IXL-z(Nim6FE zyGL`$s%&rHoY?Gp&A}FAbGE?OM$JXSS>v}rJ`~OnXqj}txdqO@EE(f<^R}uvem`bS z*p{~P9tw|BW}dpk=0{5_oZ3xCOU+a`xdPPlV4)9i>;nrlpK>4$RD-KQwBH`@j}_hm zP)W&=MZ^@px4~3;IW$yL*Ef>*%qb1PW&*z4eD?F~_s&3WZIb-g$-pq$X$*T7gS+g3 zg-j)((jJOjuSk`60Z^UG*&6x4`HV%sqelVE`pKTh>`V+?t+q=uVMKc%#}@NB@2(c( zMJqp7`Wpkqr!3$_to2msW|d-xd>l-n&-bV!M|>nthA! zRA)u6=O7IRT)H)|c;1f2U9Tihvh;hQ8)&#vd9pPe6KBYXe%XEzlf1ZPP8C1?OU9Q| zH!fCn_+DITyKm-IZwTSCp|~ri-mvoN;GePBK0=78rsik72H1x}R@PNJvQ!p#2vzb5 z9t!J^@NlK0y?dAU>@sujc~4bCs80T7*9)n1vYnL+wv~z(+>p&ZF01SEb*gNRSttDw z|9NuV_~an@%-Z{Sl~B&CBh8M}mRMZn!jiL2$&z{;5zHvod||1Rd|T@GT42_j>R1U# zNtTGWsbVq{1qGZwhxU(TPN&%u>B&3W>I)j-;Tj47{ypRV z1qR%I)iVT{UY96MW4G2Ux*ct8ACL#99r7&(j!$@OWDF~1$VJ*wTSql1HqxnV+N2hX zo;D^Yb}wYJT7U0dQt%iKaVBDFFERbM)3E^9^ID))Y{qjTqH4EkKAhRYyw^tXBCvw& zjibQ(i|9~&S!F^skc;5gX!ee}Eo5o|IDzH*{YoBU@Bbw$DXUv|r(lBYW29Zh_mDrK zVR>!`} zEe#|>x%jK7@r;ERY(ZZex#!HMzFGfOA|Xk8`L`-3p!A*Kzn$aWkaKQ(8AlPS)HBc4 z=Xe)vY0v=_`-oe@HH#w(a~-ctomkh`8zNgrI>4RXq{4D5lAlVx&em)RSlbdp$0_9W z>auY0q&3Dow8Brl$jE7xG5SAAyYC5E;R$0_9bEJAnO^9JRkQ92oSl3>M^#T_ER~~J zT6sA6y3geb+b~?5?8mdGzT@A;KCxUMz$nX<;#Z&M z25G1YA1eRJ+DDC;Jg zPT-%W_SlrzebnF84JZc{aCXaj5?xn9;yw0;1GbViru|a7oW!nx0t1K#7}Vf*Lo3ABvrkHLB!9;d&(mK(n{MJz2bUCQn_+LFE{+vBpGlZ<#Jb>DbB+OS>FTz(v zg%H{7T=c@&c_$)Hw-EytLNTkvrUh(2`DQzKoFdRrVrR9*;PbRMxBhPW$*y^N=ATY( z*{SH6)nh@_V+04wu{voV2qnejN%ZvgUtJY{>qw5?xK6mo8(Yj{U?K1S(CZ=Q@fb}< znOZI`N<&Ry=jqaGr@BA>C3Ob08%4u5zp2Xj3PO%UX4hU2(lk$reXq&WqfO$z_sp5nu{|#3<0d4;96S~YMSn44P;w;F@PyVl_k+Lwe(GAt><$RrP}(I3 zTT-HZcL;A*zi|(6M@{&OQ}Merq{NKoWpNtWc39JTtq$*0E@z&=fema0N3^^ZW4VPN zZnMmtVX^CP{HCA4A$5m2i8Ru)a%+Tc(k`h&iY<)Jb+9W;V#joW=WeR7{K6HV7Yr)g z+RsN}YP#>zCcLQ)=rWyP7~uXJ^y#UVlu3OATxZbnW}jv+>4V}@Rca#K=l0As2tw3O zuXI-bEFZ0S5?da@7f;Yt6H;)1A3r-K_o})rr+j(vTSY6qt*z~Nk=QnWqaw?nFf(>e z_CMsn^3jZ7W~*VMW+m@7oVhLrkSnAm4A>7Sh|l zfi7k8MuarT90f)-KXXW<|Cc(h8Bw?t$pc+}9Ny8ZE_c(za0W8zlzO@H+Sk81q5MuJl5RTge# zOq2N6rf!Q~i8mD@Gmj(z61^77dXAaMG4^JQLFX-uU>aPrC#mc+D4BCMEt_SBXM77= zXs%^(j&dHZ8|r$>Z1?J}N6-1T?N1LXocYplQsQB4XPqRQ{V=vF>kMEISv?xZ9OSlQ z-IKWS3PSqu#{ms$7u<0{i^>FBH@;1mT?~$Pj&=p)~y6B8|b_;XoAT{gquX@ws&*?j#NVCtL?CYVbLHxBt znzuZa9_>X88U|JSKdCqKemQ9;ut;2BkoG|pP_SID>T=O#3C^}oiB7(XkuW|P=!ZcT zdzbUjd1OPPfmVpep(1vz-J$S~Sg8F#43|zG`=W33ibbF`4){pHg-JZ6o0ylbxPSq` zQO?b}{@D#gJFKhAsczsjT$;4#d&pz8nu5qBs3}$r})qbSWBa+aJ z_aW&I-y!Y1Uw#lv86z$zd$+A>6TfDQIJ0XgpBU2B6;>x)h#0H7UZ;Mlcozx9-MHP8 z9HUR{5Vr6qpxi8KEVeift#5gu-&SF!q9B*dW=r%mjmeC{1=g&GI~z4gk3Ek7{3X%t z;-`el1#iVABY~e%e|(`p^?}P_)-jb@39M5Sn5X_RxeeR(Yf30d&~Al9igpO-mk!%I zUYtR^16HZ$*6MA11a25kZDbysOJyLuqTM;-PxGfUXyLQELvAuRWLS-2n0k0HAXtBK z>c0B*A^pmCxfbqOWyos^+jK{d)``uo-=VAf_@Q=N3zY=cXt!x9JShk3W$tvAL~bWeaJ{C7(o4E$N!8MsRR2DssJYWgd<7l+xZ=!q@qU&G z3%n%SmFu%i3bFy6h28;gu=2a_82D5<+03^}ss`Ls^|kVK^9^1gXD4DKQa0ZwM+~hP zB3_7nHA(BLA3S+OVs3~-U-{13^MGDbdJ~00;A6hOkKz%{u!vu!2#--x^#51Vt$%~;IMeTJ?ql@FVz{u09 z(l76Hv|BZejb@3Ekq=U+=oy$D(chB`EHSgA8$tZ>?hs(83@NAkIntxWuVLd;lTt0n)*B}g=x&`Bp zTWeM%jidQqXm(h&S!LC&mdITw+%fUhb*5q88`zc*5cm>5&OUHQxPV>S_m26AOUD`g zV73$x3+=-j-h7=rm?h=KdWG)dqLh8%XkLyM_d|%*%ZaiK25j+WEe)o+SM>M=^;Bf3 zM2{FDKHYfnWR6=Y9iroZZKKJ1q3P8odi1*Tt6P!X%qj9&P{>?x#OhRru1HZx+g^$n z;X>8J-gK9NSyAigM}G6zNvef3 zqg?A92I;sv7u@C_B-t1eBJ7d?dailQHd}1?vDW`!WvA-A1EFr8$W^3vcvAE7@x^T0 zaO$rusD@_h>pou{n(pr6L)%4(oQbPWv1*Z8N5BBVa7j6J7oyjCsws(#7zw^>af~bg z0VTrq9hlK45y{6x5*6YwCn~8|s?8%I2rgHh4YNw+oBP z<}V6lvRDhzstkBG`oyXTgHecjOX#aND21KFwfit1-LoCw%W?HOMeZe8#m%Zx0}2i@ zh@Tc^YZdHpc^zDv&43LF>bF&6KC1>r3=uz+6>w5k;yaKOYw2-K*O{C!0i=Sz2mjf& zEqM)@)iF%nx{xs2bjDr1R@R;3HWh$nR0a2H&X9I0B2-*XU-RK^&=SUU zev=U9pv>MzkqprL<+t1IhI%QQ;+k5b#wZ+bU2@q@8%Imv=ntjWNTQG-yoyiiKPzwx zBgx33BF!urPFaSYD&4b~N`7l9Zs0>y^Ujo9+N?f?)r2Ps`J_H$?Eyk3ffqM2VMI6& z(qin{RoTOmn>Vh{-`p@o9mDI}8k-5J%j^P02p^I~THnbBveb)_G~>p9*S!%gc~gDO z1mIGdbx-QdV#WG_N+x|UE}Hs{?0B;v`QpepTW@rmXqKe@E`O0YTUox64C~Z$x~W|* zGqf~0+?c)(rsZjkIC~Bded#+&y?9|fzfHj>4lW8N1@LKgQfy?tjqRhL2BdJukK|F7 zna0-ucvrjrzdtU_Me`^jzdj&9Ih;{X(?DD`N?o%rv{G!lhn{*TT;Ll2D@eGJz?LRb z;zYyyK3`0z2wk|5Y%OR`D*@Pp{1Yy~h?He8SO}*k+N{z}`91{)8Q2Z;JQreC9UC$S zhn!l66D9%KAfAWD6-pNc@=6Ms3@P4EDGC6Mw{jk6c2hLS)-)v_)|pgZPZU_~5xsJ_ zB;+`s1kUEV;MC>4mcVE})OH)Wtcy(tZ!(}Ax)Vv0_tCV=Y4inwp~rrX+^lnUGnNXqZvbOPSE5ZXUJO>COk7LvzwiruPA<% zC6V`9661RBd5PJrS75?S7Za6+9Mk_{TJcVdE*J}fUTYy%FMnW0jk<+m15G_ptXf^^ zTgX~_z|IC^uV!b99T0%_E*&3(jX289$QNta^~n(m(Mu;&zB_}Upthxbqp(&xo}z>d z+}6O{>%e*=*P=BhO%hx=I%I>mQpK25l~w-M*eMjk{le{H@j|G=K0k0${`u*G3pHd zv$#LAtg1kcEbggTyW4M^M|OeObs z!eC+t&d;jh$O^3?fX1sLA42(q`hA9>;lwn9o&C{QuhqBlg21_&Dx$3krW*PbU=zaAS@IB8~*+(8_aQp<|%g)U^>1m{w}W>yXEw%IG38)hMs8dL*sLEny#vy2L_9ng}6 z0(Uz&j%LIO9;vF?#OU@?;L-^worE-^`0a@6g+oR(o+e$$J3bt8#T{btw&Db48=oY z(O6s4$z{6ith|j7_{4Gc>@(Rr0^7$V*mfT+&a4qBTLQ0iGKc-!Z3=jn=Z2L82K-Ki zvbWs@jFT{C@Pj%UOmY*z;^RpI3Imn3MZoGUZGwik_{IyE$WAntDo>-Q?TS!s=a+6U z!kSdUDb|ObFbJV%4Ojf%Pe60ElEM^%%?Zw>!EUJQztGT8*~^mTYa)F+I>ev^)lrh+ zfrqxtEz+iR@;i-Xy#MUD*cHONS?fg&Z3rzR^S2n>@d@*$g-kx;LnUyC`8)B6Oxi1= z-kfdCPZM`9so*sKD14Lt%a%B?!F)$=5vg{B+^rV<_5Fd#LZR%$?u=JJ=T3hY`Y|G2Nh@TB1RWtb zBzZqJ4l`KdnDnyI)c6S8j~EPcLtH?CKyKT-n5(OpQ^jcWFv}&&W8>!5f;Iu=6#&V9 zszpjo(0D@YUMaaaPX)t!H-RV{4$Nl_>HZAG_>cJMb9EOM5_BQKl7t&`a~{ZK)w?Lm z!Wr4Qnp;(hY1y)V=wOlNds8;RmR4Ww^5FC0uZ5cY-@UGjfB=elL&Nu8@Pw+OoVx!o z99ytcv;4|nfO%1}Q-%Bo>`|EyJ0zwpOLy6y3+!nUp_Gyil?M!s z)zLQCsnbW6hFn$y?J{+51&^_gRITP0-uRwC`&0l(F3?y;Bf{PgB7S5d}IMJ6-K42V&kS? zOk3uTVyGc++&-7`gC)P`S=;Q&;H~TBx{QrpT)SZ4KL`jZCDcJ64uKUF+)M>KXppFQ zCVEqX-FE?z0``ew%^If1=ueDMT#{2$D;UIAC(eCTgvsmg*+C+dURlKMQ$tt=AKc@g zSl~?BhC}W)*W6ulFgYKa2#q&8Rm*YMZR(mM-X7PU{ggpNGW%Pfn6%3(YqGXx;joYm zm26)jSRbWnp;{NsoNmp5sVTRHPI`CL?^+YzFfh#l2E*oDtD}ManmTd_|6^+uRDM{h z&1F@E>%t2o#iTYQ@l9j$@UwQqEnqb`q~N&^Vu7iEjTr8{$s#p{!>-3#IYvEHFK8Zm z<5&H40Rqfxe`+~!l+V%A0!VFaED7-$^7IkCF`ko(2oRxpvttkBG!1qrrCeg`#7Xl{ z)&=I@ObrYmOvAi}pGZAcKbwD}QBDqRf$c)llpyz(SF`_soxU zhbmu|*9gHz_o~zHq#pC2)FxUrD`Dn%@R`gNO2~K$78{MvitUl%bL1Fkfs6YQzb&Aj z#Mj!lMav}T$Sy}$`OF7{xTf?xwKEO`e#irnBg@J{ic)m@(;?b;ftQ3IPUqM1$Rdb5 zd_l7IO!uA2WQSrcfHnzC7Tt)23(hZqN3~C76*B#Zyr1&5hKqk`Ny}}95-g54`Alds z*knQqgQ46;8{;?Q&h`Q#<2^GNfq3nS#qv=>k{)cSE7yrp1igp!IGQY$yC zXxE*1IKkYVD&s3DPLtC``=F(w=#lgw8XpMWpx(Q_zs26({_Oy+7H5|*#Z{RuCNj)q zY!myKUhmy;XHzFP{8HV~`s2Z|Lo-X=$g3j#{m7rRti70z?a&~nl4@PgZkL9!m#Wsv zj2CEt?QZ?)*3kE%_3JY}@S)!)woh91g~Dukef4X+;Q>J1RAUdjT|3XB*Nb?%l<%;)Da)!zYHx((F~P~g$z zU#>t48a|9xg@!5;ZjQV<{6762UYY$;Dp3Y7L#X=Cd=UA$>|z^{wPG4HW@+i$COg6G z?H+7?zeYxmQSRZ4UgmTc{Vf((Tm2JycEwL~f=MP2BmjYnnHN!<1|VZcR1n7H1cN@= zEEB?UZ|@{NTYTM)ih=N(r?^|BNRD2+;MN)iivc(WmbB4z~HtTcbakstO1N4A6da6-poog znW=1QZ~Vw%%*&n9NKj9_Vk`*ceQZ$z!pj`DyIUihGwqoRppiicNBpBQ)i1y)ZU@@&{p~Hv zXA}A>g*AlBK>Sn}m9`gEcP1PWTX$Y6YwZpO&Ii{xD~YI&nWSorwve{QcmirBw3c@v z>>T&`XZehHkcz}<+UF7?pU?|iVztuzjkc$Y@%4!4?<%&9Wp*J{T%z_`tz@P5xc4_P z#_lAl!?muT^FbF4(Eu1#dr#id+1lD!t~k-fr8R;L-xgEhc0PYa-7mg`FL$?r`xu|7 ziO8|9oCf>aTU~ubeT!_53WFMAuIUWjp32%tA6uePJjt2TuT}$d)B~)<)3ZkR^2|H+ zz(miu>laYO$@>=SK3^-R!hbZYqEvl;VJ{aOQ(cd`eJ^Ey=jj7pyNiqFVx2(TxQ#X7 z^CJ93_ev32l-Kds&ECV*(9wx8+U7byKb;*R=`DFy>(R9qJ7M3KkBb;B+LnNED5v=1mcUhQpVV!yUU2sIE4@o)bk1=<{bZg!__zUhe3wChv` zuynGHK}%TJp9_^PKjhZSgmLf-Tkzw$9$c~cTz?u;BI8H4?%`bf3S>QS-} z+;%DgSYs60JR8)H!AaB_t29WWgx^1&Zo5F9m^PL$0vTuDNQ^TO0v1bg{3DWg?Ar1D zaZZ|@xH!7p5&DR}_`OBWwf2VMJtn_w#0}Kkt%x2fT+yr?;o=`HtMZ-D+t6X!h2ILs z{%+sCxHRF8#A<;NvfmWbY+R?-tNm^*iirM5z9)EyPxRTve`~|LQ?Y~fo3=VqNyl?X zbtkfr4}ZKN?J$W4Myr!@LYwa0D1I1dVtHTlA71-Eoi+fBQh%0+G-?L^B;fzUS11Zg zT{AnLPoO7eqV7TbC8f0goi;StB?$EYIR(`rTI1K=cX@5>P>_2c2!Wi1yM&c8ym-!Vz9c8qV`McX+45Q?}g!i1H$y5 zvL7kep$}I5S^$GdX&#OR;BzzO9aJX|#`68i)6Q&;zy5lAaD_u&>>uG^rvo&2z~MWb z0v++St$Nz7R07p^ZLTj z>!s$<1WpvUtLwKnr)^SExBX)PR_$wob27%oX7V_+Ic#|sHz;G-EzkBdS-%QUt=Z@w zRZIQJW^Aen*VK28rj1K$5^Db3ssllY#ZTiJzBXeR?=sZb2kCtFDvU1kp`+O^4Yka8 zUQz_an=qATt{h0;z06*@_Rt)XKG*dvlbjzBF4A?-8Yhmg{5sSr@dKH7NN}KTCmye* zKTN>`SG8xfw->UP&r_`On}G}Go8@2mSd(f@gLpNKrDk3NInS7;ssp1n_%Gx1$)~UF z+QhCznj}gWS)bpM8FHWY5s2xK-&OZFm=B#aMbcIyVq-t{3*6tnxS=6=V^Y&^TWvv& zlcj$9av%Q`Pkua**`>wNFDq&!tJ6Ddr{OnaHPS`H*@_rVKC)6t;|Gl5)K=GPpA%RV z81Dtd{y`&aak{%sqCcAMqFKgN-eceJyeIjsrbZS6n7z$>>oss=Ta~8$^sS$>M?%qb(fCr1(7#c%57oS6l=OH=YrL{ezEjM~&{Gp~mp027 zZ}XlbO&A+_l)?bGV*587JLd7WTjsKhkXId}d;K?|Q3sFSysviWp;ThFY;1M`c}N~UcZ*5d_w2M)KCCGBuaMW1xn6|f0TUm< zUb_3UMro0i^+HVZrbn)D1S_O9>JSdH&-mb(sH1;f9S3}-F!pT~r)54>9@E=fGZ3Xd zf&RVs_vBv#Ui6f#6Fw-yGR`U_a?ly7j~UZ|lP<$o(#qSRwsKC{l_aZ>!iot#s91Wr zSdy}By@eF@)t}o@St)}KP$7k0>y$+d(mKrk&=Dc@h#bp&=~C_h6e$n*<*z^awK)V; z(V&pdkRQxkos)pT@2UI_&mKg;PX5v15T{Wm+BSu-o;KJ3V;bc?jyh?h^;Pb1gqErB z>}Rt>LqgfS7j4GQk`IdSm8$7#3(YorU56#BTnc7c3;qX%)c$k~`erN0|5s8-NH_&-f+P%Z{Mfh#X9+UD5kR$BYui>)vK= zuy^$rtqH44iV`iaGO;A>U(B9Kya$8CXYvZ;h1o~CqBea&b*Azhd(FfUW>Bo>8u=eg CZ=opw delta 4280 zcmaJ?2{_bU+fPcV5D{f5l&wO^SR-EhW63fj#$HI)!5I5Liagn3Otu*cV=NgVW-McD zC1VRQvW+|>#*%#-Wt-uXf9KYs%2VatM*lZjy~P{F_Gh^ylx7FRp~KPu#$!)P-S5h1?K{xxyZ2iuP^BB4~Jz;eTN@U7utZJA+ zpU6AfSQ^fl2t#(=6WaJfge`Gi9{dcG2^7jf!+Xt`Nw@GV;iW}LOC^^P;8{4cqvc#r zJ6LqJbacC*LLpVPhjh{IY_>P~TB@kt*knHMrDU%!$k zl>MAiFTBWK5>~(OW<9`f#HQE*PHQClf1bID%#u~~&pi~_ui#_c!1XZr}e;>}i` zul(%`6S|?F_ai)udz#B-bxe|41i= z3T@GyB{^O`U!RlPQhD2ZsZ4co|GDj!C8XL5o%3NlpU8XWsTI-LWwZ^dpKWUtCWFZf zjOkAl5xp!4oK(=}KV4Jiu-}(dX6wC?{YJS2kt|ZFg;Dk0rr$RzR2-{Vn*9~B`Ol3X zXhleJYNl}{GIn`uaa{Zmq7{zwhLXFh>z#%22kge4V?`ZX%p?W;<1Ctxu$dlVMObiQ zJ*c-e+2nb6V&;e_9Us!Dde1@3idqtD7o`e0e|{WIhzRh?ypTQ9U? zjmo7-i`QG zfH^62qtpLOr0-xqA=QOf5+#;<+mD5}r74AIQKgrt%R#5oR&!%cBNNQ+0ykIMJJAaZ zjyhju(P%Q98ZH+O+xY<2hZX8I09~47w(4~QyxS}^_#>!`N;NccyQ)1JEZ>Afd z(oiSJEBUTc`>^LU$c+$%;&X}dKgR+3!aaWKSl61hENgK}*=AV89{Lq8j(Bp%>mGo+ zG3r&-9l)f7ypB43`696^j|mD1aa>g6iA{r`}_fj%jwVr3E8T7D*d|uCDsS5H`b%p>hi><>UEo0NUQ)_DzlO z`0HkG3Ai*ex^DYg`kNo12&(ti)eqO^wKYZwi$p~^IW9{rzI3yT#3Ol8o$4cy-tPTV z;;^b%F0)&P=3lrxsT4ZfnWmVoVD;1A<4jIu9Dnd&(N+=2Q40vRkh}=~R_`)xlBqOi zY-z3#(uV4Lq(HDkFo(!w0GiF)DMjh_#fRL!YqK^Ax$xQyQDi@hFt%7)w#t_Yr`Eye z_cOtYbcHX?j>Tv633FQf4y2hyYtYpXvM;nX=+seIfRB%C7u0a4;4hX6B7KrH6vams zeVJP?qgXe@^&q`yX|+WQ#5#l&y;{`$sHE9LX`VfqzUnmHb+sey{t z=o{9UwchAd!9;M@l)rEurQx%#k+j-TbQb9;K)5a$Xmv-Jyu*;rM%nS3p^mfN3qt!0A2noNmD8Xl8J*?lFQyu&*34UP)b&Hn#4c!gbc+{gOxRCTYfS zMt82y-Tev*g?ltY8c{b=DPM)ObCV^pHomHD}Y(CpWwVC7T7)feFz1^nT z-gLo+JEyXJ5+=ykqPBEYf1LAryJmQ zs$ng&b#N6U=a|mb8*w)&!_T@ol-d-cKRL0t%Rw4%T`N3)%TGRJ%S5=CV^lEL`(-%; zQKmkX(K`LQRIa~sf}1!C(Y&f}c0n|qwA;WGTqZkxu&ed!KDr-C*SwEfCp~?^mQ;`! zw{S#^<$?1%K#{X%=-SDID0`hNxgIzDVD2I4MDcP|ced6$O&aaO6 z;0F~84f>KH(=h!<1H^$IwfO_`x1?tOT-ME0>UfZNqSruUb@(Jhqz za1ugZ%kvtl^l09j1e_p}zVg({S z+P{HcW8i@F^e}z@!~H*~SMVbS@W!$OIC%EPwhm*v@lxKqLl2b=h?@a1v?nQKPnK%EwCd-6t@ z;@+C|X}tF}!{zXmEW4fWsRgTn|47*X?fi_-q!$l|kf9(_OP}}HukL`f(jx;k*8Rv2 zd)=o{*W1`v{HOE`g8wh`L+1SfX}kZw6e=82Y%%i`*}JeupuN43?dub&(m}lEYt-hNOsfAhKe*PY)xco6az&ndgvCnrAKX=h2X#;=yQ~sP z;;@*Q;HCy}Cw`NEYMM((^iA9_59ezO=j&%`J~j`99*?^DF8<_qWSNQxP^3BaGCRBe zs+?JJLtjJ6I(M`1-T&%=-IAtUoIO1q6H;B{uTXheik=h)Qs|uZLEVeATfVuc0=fmw zvQyW2`VKY!f&AttcY7>@a_@k-A0`G0-Y74<`-b@}jqBzxi)3b_mt@=ViLmheT-X-&5&PN%%N zvCnU6VZ$Zwr3YNTz5m_BbblN(VHTGRj4FToLg#mc9*7}Vp>0v8R1d-!qbb|3{Z%`V zy_%V3tb3qT?jpTA79Lf&6wOJR#5t+FfIV?bL~Iek@*q06Eo8l0ckn?C&J+M#T_V9E z)S^(|%pU@QNd|3#2L|WXGM)0ueJv|$X=D!3ideNL$B+^xw#6<#%?jIFs1j$B!A$R| zILdNk=ETrAD<5BcuH@>v#{x_*s}Vl3?%w5obkn;n6fNAuZz_JEfkYh1F;`Bw7oJsI z;WyHTD?w8A%Rvc^EI0>cVIV=seQXZK58fhR1Rjo^EJ!R;pJ)%4_=7P)4s=XQ>bIbi_G{e)iqeeM$*tr~-5 z*&cs7YHkM{5@E5wl)c+oyv-hBRFUY|D6>lJ+YtE}{ zo{0lfK2@)V9p}+1G{xBdc6H%0`L|xH;(X)~v(h-N4CVyI6?Jg~%N<5Hk8UkKV?U=C za4hMp_DRtz(L{jSBwZEju4Ve#L;Ts{+3(5?r%}J|er^6B|>3U5)T;}M2ixbI#sn5L@gswQKP56m}cxb&wsyOtuBkR}VsD!$z( zO)FZjGw3sM@?7(Bc^kwXP0p(2fv!Ie`Rd7J^iHVPx z2cDw|FZc1c!E-8D1ZFkS=mfh$Nl>LF8!IA1{$p7#PobR7g-0dtbu+lt1(Xw7yz>KN zt0HUqaYh8`K?LPb6b2_%rT5=HJ2CbB+Tuc*uhj_6CMe2=zO)1EIbq9VE_|Sutmj*D zvQh`eKd}~?L*K4v0LT9V@G;!km@3|4m(K=zk;-9L)31sp#HgTN(0|-DXY+X@pC2Ba zd@Y>*KW0W`;N~UY7~=-gN)@(G`)bu4ZC>@~w4k-&9z|B;YXszdLd`40TO)@Y+t%{|3Bho2vi- diff --git a/beancount_design_doc/media/973adf6a5ea98d821f74996315976389bb1683dd.png b/beancount_design_doc/media/973adf6a5ea98d821f74996315976389bb1683dd.png new file mode 100644 index 0000000000000000000000000000000000000000..bc3fdc7087bc0a834de2558c99f647661570cc8c GIT binary patch literal 12359 zcmbt*cT`hvw`M@4cMuQ=s5C(#AiYbGqJp#l(g`BHL+HJP-Yh89NDnniQ96WTqltk~ zLqLc?Xwv%xzx%EE&0TY6*3A9qEI6F=zWeO)~mN;L5Z=Xz#E3{o5)uwXQ!u3KA9KC925}bDWI2cyBRIY67UL$YaP?$Yy;^t0! z=>fxeolEhGG6*;>xt*4sxaR3vb`*mV7+lPu^bb7X< zy8Vj}seWz|jIR=HjBm1=Yc;Sw;(%VKbbsB;6w*=VN*y~&k9f5W8$Crkhb9P{$oDkT_$Q1_9*plO+PODB#S~! z^@x1qIdKCWKC@=3Q%AF{ONA877yFUkUQwxRo}yl0#?8@U8iCJ?oEvm+g9Zfp|vZn-1iAk|+}pX5z>SeKhgq8bi8Y9&G!xZI~zr`fW$A zzC)$1u>wNWAnfgtNy9y~0aeV!1Kz-z{xB~Ub$&xke;NZ5Y3@Di+9ONC2|uEBHx*-5 zj)Xm-cV?I68OH@r3x;|MKXFMBVaO65Gp^PTt2M8gd`MVfiWj!He`=C;d`&o0xUo@B zd!>^eheg1Q)zzJ2GoBh?&y3X!`@r{8l=$hk*2_aorziK-9%x!1nahvknJI2m zl;*lLBEn>&GWbw3lK;t&|1ZDxXbpL3^mwuP5??}k*~s36c<47!X$d>V@lqrv?1G07 zHAXWP&6;S%)s#M8d)a6P!{E8$I@N_`#5d#qpslGO9BA#{+v}9p@~O?#d5dW( z2H-bZE#0=M!oL@Rn=1Q~!(8thrD9Z0F{19J#8yd-y!&}H^ksRYIfeyoiTg1g@3TQmR{8-Qm+e$QB|v_b05Jg#+kf7n3kiBwOr z)(8GU)d()GMRrg@Mqkztyqe*7K2}mvoeapqbp!sf3vITR7O$XaIuoQP)*BZ^D$V3q>I|)BietgjBjwgsu2h8L5lxlt@za zd6;}j9i~Z4zIUF?Q34kMsMjNQ;pv#Jo3)AgiAi5c@q)p;Xa!K(@REtkZQEWBUD*l49BX}0=Zc+Gq$ z)bqvVIJd|2WjiSc85DBdT#-eBl!-C4y=gyt%dH|AKh^0_@OnBX6C2Muj^(%;xF4gSynU$}nS>HcMPeF<;Kj7@x+Q z!KBT0O_7b%A-yiruOZQFa-om|Wli~cW}`>88!@rQe1R=*;K9bbiC=Y)?F^azR`U$v}vj*(Z*8TaqzrkqM)`=6k3u(IlVDzKCOc(QLsc2q3Ac0L#KI3j4(;j|!i_Jx0=>)Cmh?rDMAfNVs8>DL)DtMr$DTb< z{pU}SR^M1f+OYv;`QfEX79rLFm^%91PoW}Fx49y#Vi3i)*(@5 zq#Nqs6LNrTKN|>F-hCV66Hf86Dxza2%eQAXq-bJEOXyrJGO*rbHZ)|?x5jpe{BizK zlvzjtQ@BTLanzZ+dBf3O>>CN+aKH<6X2{y&_|LCxpW@4vnks9~Kx=Xxbh*-NkB{=s zMEu92DRfw^+{f*XMDBrryd_BY%;InfRpY0^p$kjC0xg6-wm zoCh5`IND;{3m*({2gdRhE)C>vU=R-9z@93b_tHj;Ce(Oh$B}8(X4=<&G?~`UI1lZP z>f-bFjlf{^KIHfvg^sdPv)|$7+X^G!lzfC$h+a-CWkCnEik2bw&c@8KXFdLCnFrK5 zS6^8Zua1g!P-7Z$s`i$R2;S`CvM+OCTRQyhYr+2|w<;$XVbRF-FvR0EG+)dPd<+DB1&{ zs>g$z&b5r`_IJ8h=-y4wV~Qze~yTC-r3E;}-+t=*?~yF<^)rkUl?uj|s}Z}1+J2H@RV?C=MJ zUvB^MA75mb1DoBhX=4p@Vld%NVCLPL&@!?uUjFkqL{trg4R6^Pp05gHca~RhqDpxo z*1>-~Pa(!+b0!Y|5wtKH;(MV4ne|Wopv&_GFI@PPkBN^-TAeu$-T$qt@R^c>A-F&{ zF|+hTOlhY!<+jCtY>zFUHVCofEu;Fzxy(L(E`$kMmkHx5sUV|*mhw&;4-HVPed-d< z6cOx7TX;VuFEqiX6`6VP$20cIJ%c19gEj;oWY#t4x6q4|a7jtpo4z;lYFk*erTTeR znD=G&M9{frFX>x~UNZ`IB-Uuq^L-r4`lEhFBPAK$dOTTjqh;lM{eN0jSoKEa5FdoE zJ-X{+?{^~ONtp+PM!g4ny^OYe?I)aeB>FGRD1LJCZe69(TyY*^mwo@`XcgZ>zPso< zB-aG8e9g>L(m3J7i*M87{*t)AyU%9r^~x#`9uj(bayWPm#+~Dc458_VjWpnVIc`t-RI?`zd8`&cwc#Ssod4F58O#bWZp`_}puJt*=Oz0f z<>zQY4Bgu7Ly*6y!UBzBS#=Y%5#yzfpZqZBv(SWvP?tq)%nrUYdhCy4%lFHQ5pfpq zXneEk#1Md;?;VU2Ecl&*4YY3}^Pm6zWUA7c$#IuR{2a337BKMtR<) z@WY4rQNcpLg)rFOKr%?a3>aFM+rIdFWU!>HaJ}^LHI>;X)sBxH;o<$f4_|%6Mjfza zi}*rkpKSdss~J5Zrh3P* z+VDmB`#r~Ww0ge#YmL66$Ti(EI-q44TNS+P3-HtatOxhe?#iMf0-G4BmoGcAC3Dw( zw(5=V))zqq8>REs4^xd(^4sak@_0}B~A!JpSa3fr3~D_DF-ku(g7 z?`3n8ii4EU%Rc35a+&xslfc@aod?bF?XeDV82lzU>7%JS6*vFS;43b;0(e=?PDK(b zz{nd%9sM7!g`!*gb&2kCakZRN#;RpB-5T49@!ZEy7#WV+%c;v1Hyk|>3Y={4J*x}- zB=HDnx_OMN({K3$@4Zvr7&JI1+zIBXS>|-~^n7pTJn&p{0 z0>`R+yqyjnk!l*w57%*H{lME^Y8v$vS}dK~sBN1&nk!EL`V){F8EpR$YIR|FIOVqt zE#%7iY3U>H^bwnewbESqj+TWqb6^g3$bX1G`{Suyc_y5PP%`D=yTH?W;^Wve3|~iX z3=4RE;%9AzF3i21tan+Y19UaNSm;afCc1U}_Qa@ z-B@X*x1-d2zPCuiyCi^S&tT@=q{c3CMG4&=wWG%F9C()8tDQ(4S)3Gi&pL%gl%gAV zOWY6=QmX!~TYGf6sX{Ev5Ec*5J-AF>$-S9|8?M<+8v*UTcd_`gF${2Z5@Ml_YGoI) zWQe^8ke1nj8h^ z5OKr!EBBcG`2j-eA$=UFW;rXhkChhvHhz>?uiowxn)DJml4y+Y?sk8}<5r!0w5 zMx+tR8}HrxdUxV+&eNxTjTNB7@K5E@tQ|%(>v;+4*+ZmQ70&RU5xKs4LVv|m1DYE_ z{>#^{Juhm3uKbdhsA^%?12s5(yOv`7TAj0ugC#A6omV&eTuk$89Y8z!pKjyuY=}o` zE4Jl5MWf&HXc5eJ|K0sTsqH1XS`nK9Uylln3$1i>nRER;zHbTx>UQY%Y^bOo?zYZ} zY(E$HXB^#pk1vFQ{ix~S$FzJ%9E&HVibYu)fn_HJZm0u=xYx8vWkRW#w~7wy2<3y11vp+Bnu z&a)YD+Vs4=*d{KqzwW@P=0@D3u3Vf|c-HUdEysMRF)uc5Ww%MmfB?egrxL@dp zkm7^mSERgK^7U#%qRHj$ftkem&BTSv9E4Vh5xk#5+}g8ec2xv9O4dz3q^)t~o-&LK zK2h>!>_=TPsC#I}?|Dn`0hw#{VhXo-x18;`l|*Hny^+Sx^*VVuDv~O%v>?TFo3I88 z3fmGNw;7yg1CF{fdqdtX+QP}L>2PU%Wigxs%B~!^VvOI5qn_E=&W+?K(qtvoBs7?T z{F*UxsW{1h9{?Y3up`IM40139MXY-o$D7FhySSl(aCv;5q0mi>a3h~{!Eg@c=aHE? zCuHd;X9GLF^AF!{CV{xCf;IdLToPP9Vy^C?=&CC*ndvQ+@>k}QdlLpCSXx?-6za~3 zkFo=}LIY(5vq;}&sH2t=Fz>nhy+kpqlr86wj$4GS*fsIGRI(q)WYBud$2Tp6c|Gm> zXI=w3#~T}pp^GnTVl*TKpzFO-L$iTJMl;O~AJPk2LPu@}Oa;Xvop1i|rtQW^GN#dv z!?rQ{SE!;OXOGX$7pc(!2+y8&Mfh0!&MiWd@kORN!1XCM@$`stq}ku`VECkDnsxKN z17-``W`7>>zJaYTNT*X5xW7MKvO|SYB#8v3hHf3a{=eX^eHL*92x}T0k z&61^gIeipdXI3l|r(z*Bph&hm?#FbI6l!KAW)e>D)LJETACkWp+}2^7y1Qe4tF?gd z60dZ}0S!0#XpVV#=s|x|QnlD8l9(9H_r?u{0hYPyEt>w#0W?cV{%)yV7dG-FJ>$Hm zZ-WrcOu#x34@O#s5yvIhzCneS0a%ysNL9iBHCui#qU91OUMgT6Gorf}QXV$;BHuJ3aHvYS}@-B2Wt}b6mT5|^|6iXuXk={gQAIOo0IutcwY(uLBb{$yz z!|LKqvRyvlhk9$WIVvtJQ14(FMF>YB=RqpYM?WsClCk&aHmES1oygZr2gY56R$k~0 z*F89zo_Eh&a>!N}?Nu0)nLzlI#gZvPZgZ#G`2x7O{NYrjE4v@Z7&2tbUu5#`XuiY{ z16Qh;98e&`%rskEx1=G88DUlAl%in__&E-@a}A;)+!Gl{&-KOFd5BQ=H^|jrR9S1D z+bA`CyeRDDp|FumRd?Q*45X^>t5@LR}z5N^QGZDFp#VZ?G6 zZ)Yw{%B;6FnZU@%<8jAM<~?;M+*U2}myUM6$bO7y-Hev-pVyEHJH?@9CJPJ%muqvi zV63TD&_^=7-P_S+7}q?ptT^~S93HxFG;Xue`1GykwLL-v zF+rXE_B!Ld`H$}{`#1W(I6@@XaLN&;cqTn+>PzyQMLqP4_@c&m*N)KjPv6D4EDq$q zFLwDKc$4Se3B&cL`mxYqTMr-t+}Nm)GYjgBO{w00f+?mW`$sf9? z;3jX9NgU$sr5GOZcAaR^q;zk+f$^-TT$B6JpzuhpYn~KC?>?KUCGX1qd|`M_b_5Y= z&Y8!NhP-<&M9%$NFGTg>d9P0G68#W43#AQ1JC^x z!8|(+m`~rTGxB;kJ+nntllqd&K-&OR3`q2y z|B3yHPG$#N+OUIu7<(r+!PB4YIN*~u>BtkH_e=bEeYFn`jc*%@=5-g};-On>wND?I z=AmXmFqidApHCuUU0)0m~?W$Wh}K9b#DyAC?meUtTkKR$_w1&GM-*T2Duuw|q~aihRI)fzvA5u5St!A3fJO-MJ-B zROxs>Ts1_ufKWrf0uw*DG!Uufno64tu$_?Ep~m|2+QFjlK?7&HMbnm zvfk}$u!E7=QOeL%QQ=$IFRb>(-^m;E4+i3Y>aanf{%xHlH~^&dAcd&Na_>XxXZAc2 z5jm6s2llZ=5wjubit~fdX{?h+68A-{^Q5SlF1?x!`VD`?E(MCn`0>@kb0Fn&Z={eV z=d?OQ#N$nK3Oc$FTC#E*Z3C3<7$stYjpOTJ!~NX2CBFrfbG-_RN$`SLHeT%#MHm*LkG?koT)FS0b5u%&@a zF5JTh$f5jDZ{le&MM_V01n*USfMndo3q%vXy8xQ8bRs@~|8fVA#B`x*9EV zZ_Ho#+Xr2^Ac%5PSfc(-JN2uXy^cxR8NJt-mo(8q=e?jdCMpY+sASO+AI{}O| z9F+b2{1;(ff}E1q`Jqljq;*a>6F&mVSuPMPWA3@3UnyrIrU0qQYqGn!KY@DcNK?H( z?&OvrX2=wB6t__OT4~B^P=t#o`{LW5PI*{DEd+If6YQk3E&%O3>086U^}qw-hcQk` zaB&ETlP}Z>^2CoVupWuX zB<=l3w{>=^f4irK+3Ysc{&pFQ#*GW19dfYmzrP^g>zptl=#9%%nk3EC4a*4MszKCb zCQwH{VEkJ?Se>P6xD9zZVKP|vW9wwaSoz~)qKvws+OI8BgY3-iu_xQ}ax){!baQ^y z8@3_VdxFIP;mkRBwwP{ARXt(N| zN2d?wT=-J|`pn${&rc3687s!-G3lZD^@fp%aZ9B& zLvEo;#Zq~raYKb%bus&2y`_;nr9}=^|+1&K7+j6Zklb=(m_YgC2 zc{Ej2D%df}ox^5v*&khv_+U(4*~m849O;|dGv3Y6r`@whgmmhoSml+v;Z_R#ul^z% z+LrR7rFf;mlR!6@@RnG^w|2Qy)i3>(r{0_`1R^#_=b zyW{G6828mJ)!nwKThAv9&(gm4|4X2_IKVyMc^~9yYk3^7b|u$eGsvD8*>pqds5!cx z!)3ELo>NP7HA9Ove?^$;&^j-yGjcO!+cy%FZeA9;CC{dyKWyWTR~iKcNp z{Ndj8Nr7bgy{)RAbuu`~?Hb6UZeqyz-1yX7>h5YvEYrBUh7HCNy^rWOu5=X4R?@sF z3J|_8Bzzg*Vtu=3k7t^{@sK}}T?ejY-w@%(l_a3&ww}1=Ja^KKzgo$Hx!ZS}tNf_a z%n_Q08TnOlBMp`?*t_4WtWl35QgT9f0Q34(R3vqYE8fU7w>K_DqRZvHGP)Ll3FQ?m}0Gxwg|0`?EF?>Y{gksvwp)% zOvUxy==RGr)&1A$>xDZrQ@_gdDh(GTGZe5D_;DR^WJqZkWXlXH#r)L5QpU6EDJd*!F&nu3cspK z1anwL@1^ft#IB8N_SS+5lf@MLAiELgDhBnh8R+2MVCVT~ zxr-+o<(4?PYH_bQ9@J1q2A_nXD_3P*qzh)Q!fj0{=nwrsu9Q07zjx1wKQeP5N6MG& zGTr%kIS1}=PLB5Xs35Jc3uOgdy#>mK2&WP)e&9eo6<;oa5(}i^y6Nx;%dN!fsvCTD z?GqID$&Xk9UsHZc5#P<6C7q|IwzybStl$J}sku*c$6nsG;jFm+NY2heZr%rkaCinx#xO-by@V zT?>J(9dWt1)H~i*Bv(w23OjC(#xsZ8#uleO11k8LB`8Y~5XbZH9pqG!9t;<9hYruY zMS{P*s%-XOj!rx{MbRhmML%;2uks7&!=`rdggsLyr)y;$;y-yyp(DZD3|&9T3ztWc z(ObuT$;Q+%gh<$lkJeL0u$er%r!o=mTDwN)us+jvEC@tYmc{bW=FQ8)LzW~_2tE9Phy z)jei*KfX8}N9-fZR|ceWZ>*eix;|aPsG7%i(BCfGS)Dl!)j6{ch?S%?OBwIEe#bJ2 zM?e|ntP3wSd;0o?O1-0+_H+9lR=@!t-!hT;Wi~{z+mYP+5yLnsapP(-F-7qy{daF? zHd9BMAhwN@!7U-)We7ThnPLtQlw|A!n5}|13uR(=Sm3I>Qkavs*t^p^Wf!JNitslDT$AIXbZHee-qn&j>}sNSQBb!7iJ&}s=HTpX?rOD*4X+u zSn0-C_+$NDV>_KD;*e2n2D`Xn0Ew6>fu2*vmZC))1@AIA?1aM`S%2X=x726LHZY-c zT=c9V>mG1PHIAgr=Cx~z{8D=~$Cuhuk2>LNJQVasB0Fj1iPceEaJUKE<&HQBa60uk zl#kCO<);$@58QYh3*ffstD9p1f(&*-x)_6Q*h)KM^L zh;nfU)ri94O9N!PZ4#>EbTcM_lV(Wi)K@U2_s=PA5Zld1R{@+Ar4Myg*pAqSA1}r5 zD9lG+r)#LuiISC+A`^#fBn#;NJfUzVHgX+@270C6p!Pyxtc?T2(MY(gq|iEVVIs&Li~9zjX3Ec6z;!;#|E{*fXzg)c}g8&VgXE1AzOEq zEd0RkMnOg~B$h5UBk33L3-sCfXFUNv@w(oq3@XQ0D+(3fP8gGd48Cqway8*RuN$2+ z{yJGf77Y(v2!}C*r)t`hK@SP(=gGujLCQ-abl9W@Jdd&CQ$c36A9PeCEC4nbGldR2 zj+ICzBSq z@q~lpS92&LfgyKZ;5$CvlDTdp0_Lo2zWk%p7CG+K2W|;gc(8_uz9K_CEBB(9vqqy3tH!3m-nL0AQpAbDV}v`Y^Wp32 zU~59#>`M;vxciZw#?{6%wZT>{c@qkVMaXtq*tMq38aZpi%E4!uvsXISCwh?W<2;NM zyNc=VpC5Ii)#s;yn#5_?uk5mtEKA`ct6;Q=)R${>328gTo>^UUt4(f{y2WERUHaU2 z?;6>PA5<|v8KCyOJ!Zf{Dpj;!-1q5b;hA!>m(yoKmqb8IB0bycaJaGV=KN#E79-Mj zwzRJ*JcBGjWJGo*wEaW7MFT7#W%t@v0CCh-2jIoxONz@i9&KB&L2&CUl^kjN=dsbl zpfv$pOOi-BNQKJ_wsD)?Hq7xoA|Zl)_>H{%_;$r7vXIfP34m<9MBBo3;<|Zu`kd+_ z-^<>tI3gS5WhH;!b+z|I?xXgP^sBz0=*6keIGe(5)M(q^{- zk&b|^*g$|fVNuM%ce$2F^kI!9m?eY4a7aG?tzW}{qxE6Sq~mv>^ow;!Vew}IH1A|B z-Bg&#;%&FASdKW$_pVwLA-=)k72f1%b4c!P9fuMZhZ(&2QA15xbIzaP(8LYJ2ZYEJ zqU+spDpyBgx8azRg%E=U`EL)2e2#v|)O|PWe#ESydyYTZjo6I$q;#~pk?aQmLB?rO z$5OV4W{f$Ni-&Cp+T%=zMC>&r6XPnHP2kWKM8+s)KpQY2Kd*4HA9Z>SK#+r1Iw^0c z*ohsDY&cTK
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -160,8 +162,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -170,20 +170,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/beancount_language_syntax.html b/beancount_language_syntax.html index 64d0e541..1ef72a0c 100644 --- a/beancount_language_syntax.html +++ b/beancount_language_syntax.html @@ -165,6 +165,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -212,8 +214,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -222,20 +222,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/beancount_options_reference.html b/beancount_options_reference.html index 8bfc3928..fdaf6c32 100644 --- a/beancount_options_reference.html +++ b/beancount_options_reference.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -152,8 +154,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -162,20 +162,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/beancount_query_language.html b/beancount_query_language.html index 6bd0e34a..79f36aa4 100644 --- a/beancount_query_language.html +++ b/beancount_query_language.html @@ -207,6 +207,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -254,8 +256,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -264,20 +264,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/beancount_scripting_plugins.html b/beancount_scripting_plugins.html index d980547a..6ad867dc 100644 --- a/beancount_scripting_plugins.html +++ b/beancount_scripting_plugins.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -180,8 +182,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -190,20 +190,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • @@ -444,7 +436,7 @@

    Going FurtherNext - Previous + Previous @@ -470,7 +462,7 @@

    Going Further - « Previous + « Previous Next » diff --git a/beancount_v3.html b/beancount_v3.html index 61d41b58..a210eff0 100644 --- a/beancount_v3.html +++ b/beancount_v3.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -232,8 +234,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -242,20 +242,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/beancount_v3_dependencies.html b/beancount_v3_dependencies.html index 9cc422af..a36e72a9 100644 --- a/beancount_v3_dependencies.html +++ b/beancount_v3_dependencies.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -162,8 +164,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -172,20 +172,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/beangulp.html b/beangulp.html index 11132a26..1d6d4293 100644 --- a/beangulp.html +++ b/beangulp.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -182,8 +184,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -192,20 +192,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/calculating_portolio_returns.html b/calculating_portolio_returns.html index 30f5f361..bcfa50dc 100644 --- a/calculating_portolio_returns.html +++ b/calculating_portolio_returns.html @@ -189,6 +189,8 @@ +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -236,8 +238,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -246,20 +246,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • @@ -335,7 +327,7 @@

    Overview of MethodFor reporting I'll group those per-account series in logical units of "reports", for example lumping together ones for the same instrument bought in different accounts / brokers, or different instruments that represent the same underlying by merging their cash flows. Or even to group them by "strategy". Basically, by merging the cash flows of multiple accounts, I have a generic way to group any subsets of accounts and compute their returns.

    Using the cash flows, I will then run a simple root finding routine to calculate the average annual rate those flows would have to grow in order to result in their final market value. This provides me with overall returns. This is similar to calculating the Internal Rate of Return. I will do that for the entire time series, but also for sub-intervals within the time series to compute, e.g. calendar returns (i.e., each year or quarter) or cumulative returns for trailing periods. Since cash flows are flagged as dividends or not, I can separate the returns from appreciation from the returns from dividends. Reports with plots are produced for each of the groups.

    Here's a diagram that shows how the "configure", "compute_returns" and "download_prices" scripts work together:

    -

    +

    These will be further detailed below.

    Configuration

    First, a configuration needs to be created to define a list of investments, and groups of those, for which reports will be produced. This configuration is provided as a text-formatted protocol buffer message.

    @@ -756,7 +748,7 @@

    Conclusion - Next + Next Previous @@ -788,7 +780,7 @@

    Conclusion« Previous - Next » + Next » diff --git a/calculating_portolio_returns/media/4f4d457bc907c750277ca40745ed1686f7d9017a.png b/calculating_portolio_returns/media/4f4d457bc907c750277ca40745ed1686f7d9017a.png new file mode 100644 index 0000000000000000000000000000000000000000..ae23f5658f7415e0517f01b99f78ae1f93c3ff28 GIT binary patch literal 36742 zcmeFZWmuG58!kLEFaiSt${<}Th#(*#DP2-BbeD*9cMnJ?DIH2TNQjiQMJgpFjYtmN z&0hFC@AJOz_ium4KEC}O``G@HnLE~f-D_Rzyw3By<|XQ}>{SFM0s?_tm3t(m3W31j z5D4@F_X_x=-M4H50zpIMq{P)d3|G_O$%C3FUr&ab%k$Vsbu;tC968nrzna7tXirRn ze+GK`VGRupYH#CJ-46~#7q@$P;#Tjye}Aj6 zb4L@|4eFJb`&{!CQwS-`g``!G(kbzb3W=tCb`!*cE(R6GAF0|fk5%F$<3*IVY2Z6% zCkcc!i`{8Qbz6dZ;z5CZd|9nRj9vn>EOX9^ACYvt)63|Q011+yC=bSh2xc&Hy?wDa%Z7yB0iHM z4E@jc_;C&uQ@D=U92UEil)*`6^0g&=wqv^;+l)w$I}+kcDJU_$Hn-rw=Qu=N)z%|V zzo$N^^uO>+a63Qs)Tyyac_ZSh>)SR>a_4c~YUmVn)jSAh6Y9&MUpG@J9QDp=UjBo} zRyH1@#&(>^dZbLFJ*eEMrEH)#U6?hTj3d!`u}7d**mWgduqEz}a@WdGi9s{g738;c zVLi8vDK^YVIbW%$)9)9@>k3KiDFaFObbfu~jxbwm0Q0IiaqonlgNoPYRhp1{eXjj< zvq8e*P>EKWCBOaIyJ#Arg>NHUbL~S%)cg)&3H|fkNg9G(Sx)m^11|Ag z%*?1vbo^a4nJK>$+v=7YyWbr#D~<06{SVg03kDk8H_EmP5(3=pXoTG2v!%o3&rgr? zo_5B@GOOGa@Hwo|t#zD}NjSJE9rkG9Tgs3K`gXk8`oxDJ_op4NK7R0>Ym3YS(eH6C zQZI^)Am@}|oAU-SX8RgOsx}|TYpV;#Q!99^F&$8KVh!m*Sxlal~>@nzsno9!<_xZ z3m&Hpo@>jXhF~-OIkNN11sR&+NVX00bkDY*Ts^4|N~}ywDFRgRdvF){L-r?&)Czd_ zq%5u=t89L%vdz7L(03SLCsT&Z#GVW;K5DivrkuuhIm~atF|sk)T_423%s4EmM6~$ z=7LH$CDLZHUgP{^`l9%TXv8pE;KLem`=I7<8gU}15vB;jOn<-NQH+FZ)0~fkAsRuA z&t|d9V{C-I7IKv;bvv5^g{H+{<7Se`KA{o5ig*3CL%LDV8v%`ISdF-rU%6q^AoinO zb}3oe5|`gvSUBX}P2LBZ-AmFx-YIgks^zNQ&$&lNP6=!w?XOP`-E1l^r1V?t_wONuIw*CJ`U zy_dt<(Uw4R+rI3Z)4{gP54S7}A;|#VW)c5$pUwo~?#k0&o!huL>h&M22zI+?>n#R_d`%|% zjt&H?M@ZBfm3h@?&6t&Oi_>-1Ach`#_a=(9TXxwc77mj_A}HeCbd@Wa|=Td$K=ReQzu&ku%y$>FwGL zHIFfFy>WM?QM%j$aNd!ZDVIgyM#B5@{D@L#v ze|`ZEY5W+wRqjGq=FisDT8nbn?^7Mpd!k5`iw76kG@4G2ry%i?%c)Pu^X=9x_C_qr zx^uirR4P^oZyOqNK-UP{b=+#W@u;`w>X66=2Sv^R2dy3hWB7bb#PW3PLS%6f54E!Q zqsKHJ$}~hL7SqAIgr(W@B}4~>^Zw1EdBc{II`q9I1)2))Lp0JB9W2@(y85iz=NZ0m zK*O-vzv1*se}}F0m-joO9w_ni#F`qK1*Y4N-)Jjc!7j|iff}P3o=XHH ziUhE<7x9ppWY~+Lz7SY)60`#HdxmCX=)^|yUy1);oO@+#HF^jH)$j6e6&v@S_|y)K6^wGI%N6EC_`ZN&Fzr3Mrf|-r;L5XR1hAC z`es{+*BBKJxqRRLN7k3g4xx~@)xRSbvFB`Y*IdQlh;BbOAym{I+)L(>`4Q+GS# z7-NPuA~iiFCoh)2)2xjCtjyJLG@Yz;tn|lxKAerjgKzN!-VdyQ*Nu}2VHp&OrkawQ z9Mx`{o-&{k8p(2ppwaU&&&LG3A;wn`ePhZ=Odq&uhysVZqOT$<2%LuZF%vaWE>>!? zYxr3D`?e`-`gDqm-xH21=Y04CPKA2LXTu3M@M~z;j`Z5-e??xa>W9f%kJt6+)!I)_ z{Tw37NrIFgP}Js_x~D(%v=b=X6AbltKC}-lr`o_8TgApp7q<0@5`M5!Sdi|e3K#S_ zKb|slq*fFEm=ITTTke;ZdS<)eE+zfp&BL5?N};zoh$$VeJk1Jp>&ry<5SP0{`)F8I zYV}63-uuRHse`g?m@N7|z0Nn059&N(^ef$#wUc(f2*lq_F{=Ke(jt;Hqw~Re*<>=@ z+2hyCcGF$Da4hkwLR1;0aF(9In>Ueqj0W_l$`n5RR;-W z90(lki23C8L*UC$P@o5;fk?FIdEH&%N~^mm!bh`8xaD5%@$u|2T5MPdrs(AbeXaAf zz#gi88f4};0U6Dt0>528LD3t&I9kN3{bVR&*Pe4GL_UKzxc=zDT*yF%cg0)G-Hw^f zFYUB7qk(0zdq|Hb1o$&eey8l{;U@>d{WsfQ!;|Y?@R7>LGiNcKTt>CFtN?N_Ro6}n zgYDsdEhvDnd0~6`rVgv2vx4-0a@W6=+*n!*r&7m4G-WKUeW_ULlD%(L&+D+bwIAU}Jw}H0RjhZj;#~ZRCIJLOb-Ki9uSSQxAiUy(06A0!QZ= znc=qG%V&&s4cK&eR}n#l%&2xv7lS5p8ix2f{xE(kKYm53^xDn(WLj=9Xb5eUE$0+Z zm67$LIqT0TXT(z9{Y9il!J!y*g^{PQN|VP6T7~vJ@8I&})_Jm6V-b_11r?{b%1Pjz zJwh_Mv}Dqe&X%?k(ykA)A!0^XpG}HG>woRdZJQ~OzB0oPDOvZa8mtZ>^Fl!2y1o}b zOGBHe4o`P~D5nJ0J`;m_U9S=#ULqA2@w-?_4h;MRuWYa>4-HhmJN@bh4q3z7b_bjg zy@=@?iuSK4xR*=>=1&fein^Ecg47vy{{t}}+j({a-P}3u3i-yzT%^qg8**k(O0n+a zZ=|A3fBi7x_xJpLESe`z7-)IiQD{TxL)_#f^xTu!aE+;VjL5!^T%dCOii0qYSTvo2 ztdxE9)$NK?7RkzlxKhD{1Nj2aTc_A^<%6?(&ica9nh#fWecDEa0_%qPcaVttU&7lc zo3UJ3D77sdOlYBA&m7s4Swn`Kd4E<|<^3v7|0-ZgjRPmxp@e8`I)syMe*r;S|vRRj>m{iSN# z@YEI9T-#8+9r2Rm57VC3hW9u^wRFO{&%5hL?T~BVArX4Zp*I|`p4`N%|IU)Y`pajB z-aSguDi*)y<5jgX7xBBA`~}(iVu%o`3c^VR$~>z!DWpww8LMJyf3zk1M-WUD{o|9(gDm2uy(6HGn(&JDY)99Y;S!tj!`2v*gg(ubNSUFI5 z^6wXG6~YkgJ%Qo0iHmdvEMl3t$FGe-G(|Jvj_|!|-gnejh0yR7`(0d!#r=L%$qyC6 zL&Lcb9##?oN)ct8!rEv8=f@wiwe@_wO@qB2TRwO+pNDzAvTGBe7AlSKv!MpZKra@j?R6AEcr< zv(SWzc<`y0DB=drstCMaZ7!^@FN^KJ!}U+`{lEY3F%qL@AD#X6Nmfv8ADV(fGb4(E zd%-V5#J9|Ksc%;I>C@i=b=vQqEIQA3xf1ro+-g4}px~nJN#>kmaDN=n+|M=VT0qP8 zFFGM8aC09N>KYYBfc_c5x(wN^VV5+~w&ED7p^=@@>;UrHQJy93+p zbpAfC<_26G>#{y4(6FyN7>25yw`6#G>@oX?p@_YhkbQ}qfZ6e#{RID6Wf>PiCwd;o zq?D@9s$DUY2N;et#h9C4*R>2i&A$Io78M>;`5j5azUFWxl%&q^YM1<9)nApFA~P;c z&QJ?rP5`|9ruD#jxH099ayo0&@Ocd1l*il|Hs1AJC!}@I=Mu$PZvz02~r&iiM=!DZ)UMm~<{oI@6YHxt_V>J~5lea3OK(|p)lz1qje zyURP@g*FJxxi4XRGx~`EDM$T(9YJct8wTyiEgcXrzr$XBz$L~V9PmJy(@uk6~`u@I@h5Q?S z$Koen!ya&`=61aCIohV_$q>DWyv``^quT6qL^oDtc`HXIGWKgYS@-&6g9ht3xkssS z#6`}D|3samDkx?*0PAz^2w^+wwtsgzA71!-ZoIj* zh9wJW!4pcfC%vos+YQh>qQT)e*u1n9Mrs_Zwud+lOs1aCjlQN1xG<2_o#wF|7HiusUK3{1 zAT%eEmANbW3H*^t+2vD7$k9w~Y|e$agl!jSLsc)wAC!9i_M?j36!Ua}XOeW$4BrR* z0Mn;Q-knjI@XsLtjyYbmMN&zeOp~%|p7v{>$CrLVyvo-9YJPJNOw2cU@ES5!u`nmn zjF%`tBReo5M-AUX?sjSTFi9u{1-7Q3jJ@B%a+$TZQg9z!L%_@>0@l9%A9R7I512G3|CUfSf7ox-$deT?{ z`^slK7wP!6(Q_wt0L%72L-Ul8`B4&k+C8IYeYT8;o~nS`wHj-qYQF-{6ddF%e{V34jpeeeYt z=_#N!ualB8x~>eWoZRwV9euZby3?!B8Ovz4yVPIlcjmP`T>4r!G%HB_`IVwM%XA*g z*ly1mAwk!bTnGd+4)WS!Z-y4;m1d?Gl;WXBh3-g!GTjHawNz?RKYe+?n}SQJEnkw+ zOG_4uD69}phjYMw#)56|QH{;dLATKnZNl{A|C}KJ8up|K-2qfr0wC?X0fqY%5Vb5c zuTV8MI;5xGdn2R1n@rZ+r*r*6!+0 zH5x2&y6N{GTTcXhkE>weU@s}z`5wy{u9zWWKtojlGPZDa+B@n?XuSXT0=s4vMub#+ z!>@w4ac?O$MDq-vqcIa!mzW4R<4Zw6*UcvnajY#lB(XD_ZGMM1K!?TH|D1FQ?SKYQDDH{l9Sl46wQETE98y> z00TBOjy8^>eF2oH%v&)-2q;mQxUg7oWM& z^r!qFNaMqghfR*0|Bpb^7q*;kcUwIZVIr`aTf@yH_ivXHiRf%OKgRSJpCyx_Dgagi zQz#NKW7-xG0|H#+Qgz6A9?gJ%6>d0%ZO3a6xDY-73A8*vyRqrjsK?!UM{|cVr1YQM_zMfZ2_}bzJ(Ou9i!qv~ zYL$}+VrgLuM3>1A!b~@A9zLNcr@6slIPrmygt+?XO7guIFJEZ4^>xK~AR(Y)Bfq{hQl4>aNR3(Q&$05i2`}?sdqc@%k=b(%(-#N96+1pX+Q|c$ z67j5p&|WN)4B9U0FoO=nLx;JYJO1~48c zpZ-^-ab-qx(q&495^|Zg5$uh_$)oy0hQ?8SkBz(62%b%L!Y zS33NLD#*tfVE}p+fU#IpJ+K;lcDOl9a6Xvx&u)+GLZQ>q08JKcoe$(G`0M39iU|h* zISvSt8ROc7$oYVOmWIStL=?5a&08`NDDJbJ#a{pSKtSAKQ7;t3&GcdTXACHGASihh z#%pZ-!LGUa91LciO}EMdOLjP&cOfDB*WG^|$|NN5F@@Xw>8~#@Rf|;f%AJ3AI5N7g zPjFBB?0mo1)~FI6{ST`6jhj!k12AU3D`q%JvrPYJ4`d=GasJUuV1G})4wcGld#p6o z=xGlUlV`62M-$JG<8d#=j--ko@^36dJj6}7=v;T^+$&srTRIHc0Jc-F;>T%%=CaYW zzTQ9bl|)Sj(mAzHx^W1pUjdovf|8WfRaC1?e}wDVw>lxER2~EB#Uy$6p8tT8Qsm2` zgyt@F#h-<#D=Hr9R6Z5rdfMTDEsP%%6SL^Gzv>C5kL7{pTkWkqu={C*Pm#}@5cO2p9!K-x&HD2bfr-A^ zZNh?!3)42~|Ge7&zrpbT1a9vuAiP-FXzljW?oA$rPa zCz?(%rne{B)$fYU47vaG1)qJ|>!ieYXV%I4k)16Q=}Z}KsOC;1=4L#m4KjXLPV#Bp zDc&{OP~@h0{?B;D(ALB$MqIgaH#d;qnLkWF&I%k)N@`OG&@Z~}-=oe_svY3cLw~6H z$^5J1!@KK`K5utS4Gr?@a5rYvTwroiDxE#=bfzCEc?zj<6{&C*olIVn!;X20+~mMc$d{RvUXK7Hh+uJFR639nW*u zC~miiq`RAg2_-gAGsd4ymG75N zEmHIM{=ECZg!icmvyGN-*iPBmODOr=nma$E%bSsM-)!FDw7_!|; zzKP1d18E|#zwN2QzD7})32EK@`qj~@LBMoH;CYh}bKe1SLQqiA(0AT*@%&EBt|r=e zwX=^3HPcb7Kfx_FZ$2JN7|Zw>(_?rA`~67@ye!7!BDJiicRa}r)dbPL zg0%R21vkRAc;3}6G(8|L3PQ--M-OSc|Am!<5!DS?bKOljBL(J2s~awq(1V<7z|!#X5?c7ZTl}Vk&_pPl z+^?_ZhordFPyZ|kRCKF^qtHwYTGKx)ywg&kh(fofaujlvMS;tg62PSQ6pVFXT@3_vq6Cc={}!Q2$>(9w{`dQdTY5mNlgjLB|_fFumxpSg?wI>Zt`jOH|c=bW@=1a@=S<=oc_Y^Ef$OJU3A-5Ymc)1lntB# zUjmM>lRaMalHVv&7ksU|K3p6@fT74r>{nB*^%AL$co_+as}qBE-BGL0by^&CNbq~+ zZS4z5y;@%wzC`>opOZxVjUU^YxAi1-wHT7pKMnt|=>G8K^BdyYNIy*(p z6AuLZ?zE4zZ(Gn;{hz8G?dG)2U(quWM_15TijXh2CB&e9eDoS&Qda8_db8sfV=Nt} zs&oTYa=z;}K``>U4!oqan%7*_YQ=4n5xKi7O&8P8gmg3G zv|{&=l8`6$T09dJ1qreV51vSpCAKr7#Z=g8qu5^J%b6$I1r+*6ire9qw2(}R=h!*@ zx+95D=C%>MIjdGh#YHqN_FCPTb;#C{_${EUHpf2tQFZ69JnOIX9A@g}zdYUQIGPS1u&wW~r+K6apBjMl>Kz~~bsw@7PSRha?4*#3~>%~`OpdLonsqlkG zsr|Fkiq?ana!$az2wX$#+93@TnZvRR4~tZu3+5j`VEXe`1Ezvtmmh|sjP<}lOh^<` z`l}Iu@nNV;zASV$Gt2o+|4$jCI~b9kLJme)-u5{`Xc9trqvN^JM-P=| zul6t_C7GJ}iWHr%`N|P)f6P!vJBvwx+Lxw%=#d%ae0J=5X;pG6Rxi3FdyBPZewt@! zv`fC4YVmLR-IJzgdjWzi*PY0A>EBv8tu*gt7d_p4uFK~zBlhye3)IPcM~q}Fqr!rI z!3Zc!1`hyZE2WzIh~aa{Rkh{AvP-rAAYW!cCOQ7ZG28ye-CJmku<+daN|xujv#`=& z=(Wt4a^ECyjBuIqEhG$(z&YDNJilqiTaH&sDos9Lss0X1nA_IFZ&`%pVsAUmwOw8A z%X)sgD0-n{35=GS!uR==TM=Cs{ud`15i~-YY}QAoz$`bo1?cwjfvOF0B0?U|?WcFO z%HG7^(@`}Dm3n#o?u-!Na2of9^@>;D5jB^9qOI5ma5RPIFjeL-uD|EM9}-z*H6$NG zL|2vwOqeBsv_Xk{feS!w(U`e(4a#ST`Zp^O!y%cA5aFw-yQTYp!Rq+<9A_TviLYFi z2QCZ2G)yLli#(v7(lXj3C~g5LuhI=Z75>mn3&sUIe1p$KGj`>gM&} zwYN2&?60XUgEAxz%%fs6Y*REOx4P@v=#K*B^297Ke;$EKDQ*6xFRMC$0(hRj>mL@8 zxljU#m2SHC{%TPfqk65KL6L5C@fgmh1FJ>6SFms5qhupeB;E+-uBN|M)W&Eg#9&-m z^sLHi`BtlDiKY^8X0GT*4|{wH!qaH|F$lb%DS#B*;r1d-WYb$XC19Y3@emJQ`_d;I zNgE7|Odo7;7?Nkbd8B1zx&W7_j+V%5_h?|kHm%xZsPgOIfmpJ6&k@6Vs1^(G0(M{C zNe307A!Lq2mK*8`tY#|KOR7PAhu%%5Gg_T z^|v)xxRy;%*{|@pDOfoP0nFV`%h53mdwBcY=Cz(R?yYzlD2dwMMbixWCz*&to(XJ zAxUnLVASZL;+lg=;4+hU^-K0g4_7f^>$vz*1=9;y^wBRa3VZK&jN7-ACiXkYD2Osi z17l+*K6v$)?&EoVt$G*BO94-J9hd=E#@d6VN@cO=v0wBh74aIuK2j4yGIi=4vgAMO z5iKe81=;zo9WE_H>MVBP4(Ce?p_F`f_kbVP<%ss1$>A}koHX8|)GwYfgdo%VVEyi^ zK78Tp{DhFz{k3s+40G7A)?fR8yA7(s6{A@jaLu6q?07E@a1}+4=D2ak>0T>$%`ucv z9C3XTCTx+T-*G*h_SjUV36bxdsk@oQ;8QR(gzS)DRIIih@SEhxT97Q)s1LZQeUD?nS5Hn=d8MDI#IvI_YITGF10yBL|NW6pIL}?CzE9}tLv|QgVH7^WzIvLxq!X& zU{lO4DE(ULy0py{Xsq2wnXmOC`<_VG@&w7*;f9b;w^anb>!xsq0E2qI zZqHKLa7+CQ*JO(q(~NyVrZXP=BK0lRFFlCsdpQZ9nPMYgK`WO&xXRE9)qQe3>gAmE z4iS$D-5e&6c*+=}D7I8Ha^VN*xh71WTESMowr2)6$rpv-ESBySLts@rzJ?WSJw&F* z%&ccyD#Of@O2829)ZB$@LKF28gO&Uo!hU=N^)QvXU}0kN{dJWHcLaK3N)?v<<%bS3^A`-ea-F&_K z)po+CJ;Oj>SJA&8?qgt}Oe7^m+$V|VZ@|23b|=`n`K-u+UCn4d?O(_aTl4>Z_dgex z8ZIOtE59Z`_4bUlSDx>DXE)V&*Z*`Yl=J8C_U&o)iQjT5=Rkkj?w6&bh_mHT-d=zC zHv(mq6bF2MgxuEfB{-Oi7UgAX*8}!^KBlkC%G+y24+Vw+g*X41NR)@ET_M9~`{n9% z%XBieJv@h?D0DklQ8`VJacAK>H#jZb@SqNN)9G;2#MCKb`}Riqi@#ne7~Z$k2MU+m zd--LbiA)4J1FKe`UCRzed8Z?))s94)&^iL2Hx%jjFsnQTn9%9s+_##hYXNva zw7NlJN&t9YXm%M$$y_c=D2*Bb$UpD^pFWFL**(l~Y20NE01g@22l~Hi0%zBi>5^q2 zb`;zEfkxByt_pHCzNi-kW{iS#QY84~;*|+>Mb&C6jU;EMJO4c~Kx~JmfxV&#=XCYwd(|?10Rg|$I>aij z6G~k@UcbRj33R%Yv*2Rip}xSBwZt{!Hq5(QwADu6%y~osN~2LtuZ}*!wh)(B;fFQ1dx;Z&xPI$qXfy1f$(`S5CjZm zSNQwdA(&GN8pGN|C{RYk0`6Hl;KsS~8Ng!d=ytB*xiOPTAl@?i9`7QUhx{qO_lm@U_MO5-{DZGCw`A4PvJ{X;XgrwQn)AV2 zFy!q=uf#OV4cWkaN&xK^g(o-0-7&*TG!44dR)}XR=>Q0+TcQy@S6qH+qbopPNbwou zOLXiV&jYEya8Ip4CWI8u^Xwb>P^`2W5RLB)v527pQ7`E&K7;1HSJVC%2I5fNcF~K| z_hUdj6lBpBhrp_O#go?SK#K5T{MZ*D>N3rVX%Hiu$^p|y|My3=|&>fIM z(!1h7A4et_BFohyu@X|#FF=9Ouml}|>cqi~Ir`OHQYd>+JCBhy;SuY4IWK%u=>}fl z6?$|Vm^wq-tpQl1>RC=42K7~fhQSC@brb|yWj*rP76;e*XJwvlPa@mv;}hn~h~t@+ zd}Qc)+|WYIb4}f`MOP&~z6gLX*How99a-nY0AYT|lEBa&;AWDDy|5-CmsOx0p>0Le z^5Gt2?w`UCkdQu5!pXYcPD)CF7|lCu7$y)a6|2;gJa7RYy-< zDYdQzxvF?*BvTw~NN_mnWf%?VbzK(X`j%_RH5k0`5kpm9{|^{6q%T%-0|%R*ACLsx zy%w(G4Q^|yO$A1KcYaur;A$AQ@FVE)1XeNrbeKQwLDex zy_ov~%l#;t94`ykCRm2waZx>4-=~o>VXx=(1Uj=nC5U3KA*xJG^jfikeETIKnV<-X z+wmCNOw?2s3xajw1IDC=6)9%XpUoI31|KTcP^ebqYYP)NnPCtL5+sBdrIRK7Jd6#jhd-MOEgXI+0h<+SWOk^5qE17{!YsnN-dSQmA)#a$1mq(LWG^c z4K+Sn5eM=bQ;(4MGB7n-_1;Qnd+3k$Ui;iE(ns89U7EJzo7Q!bLA znHTHM7C-^*nGN~=ny+NevJ;VUyi5Ztk*&=cfmw>r`4mnEEO;ciCOaptCR|FQlPvgncX-99JV z+HR_K3*#AxMXo)KDd2PZLwboD|KIQa*TrSSSX>-fQU_bQu7((S&XdYzh%_NtHNRE$ zOv^O;<{2lL^k++JPyh?2JOasVth-@r>K19R`K)smB!f1xA7 z8%S(5;Hydk>0a*X)7;~cZqf6@97htC8+%Lrk}R5UnZb-q0IitvYh%@BU~kz5d4hs3 zkGb|!i%C>eR3dOUbpRqN7a;4E>6;h8I^U*b%kWor6Qa(7Oa@=}mIB9S@x_@TumWBB zs(b*;)uwd!`KIV9Si+cr`C~w)b%M6nTtI!X_<~GxY1UoZ&t}$Add;f@Wn8B@YpMbr z!$o?vWd!8+#hSs!I!QbHcSS=}hS~(UNdd4O^BXM}nwB74zPhwAE%v0%1JqI`gp#-T zs8On`;dgQ7UOQKDxfk8jsg%-KT^p}W0!1-7U)Cq>OAu7g(f2<0Zoc;)ETz{1fSA+9 zl<=IDLfGH&(qcyQ+D$Nnm*Av>Lk}T`5JM1yEfOC<#VNzqt{y|R2Z+L=^%+3tQt&%u zPBeP{sKX^(G4^FBuorXxNb5r0@t14`OzZX91}zNB`5=~iv=#_*0`{V zG>Fb63D;Wv<$Q)*P=4Ui&EVV z1)bFj!2kWm+5Jf@-@*0uG+-YpS#$)xR;x0JiodCH5(F)xT*h-CFETTj_PK*+nj@Gq zTu;8>0-SF~PG{2S39h>$IFlfID+slJohhdVbU{yfZwSw^#Qk#Sfbwbr4sgbJWw1zm z&GjFeo+ubk3ACYu%ApwLpLVT92;Fs~Jd%P`jpA57d5-gG2l0^fL$_6J&&`7<9aPc7V|6keQ0Djsh3_Xli%0)G6DCf-R%Xc^3;L3UlK=^kHYxk z9{n0Ij9u0Fkvyz78N6+t0GfH3zdq5%5iKS-Te}IblnZg`WCTK=T;+&S!W;VnZd*y5 z;UZP0g!s<5 zuZPD{qowe!!G;J;SNA(5VWnlqy-WQY?|Js^$|sL?S~4R~ced{R;MH{bF&6xLAto!| z<$HR(N#^Oyss+V(ONf+47o%joR!l>?Y;sN3nfV0%_Z4I!DfjEw!=5WL5Aah5PG{}Q z9><#q-?}Y$GkR5js?A!vWv-b7L9@I*4&*55Xe{?q8fY~)bA7D3x+diBWEW9i zplpvZ)g>dtK_GbMj?yoc%ah`tY`-^70)k!tI0bOc{k?6oU91ed;_Y^u9~fwK|462# z%PD{eUs)0mBZ38%IKDPM-oc;CeKtQ4z1qK7MeyA0&BfG^L9SU-_5eLs3oLDV{rJk} zQmDxLrgC-G49S21M+1Et3t&P3_JT?mT{Sq)d)X2EQDKQcO55xz2YXc9dTDp^fGL;m zXRUqE#C-AzXKI6K&+2#Fc^XTxGglc=2$c-!X5oo2yBWDnj>d~J+j!U4Azw89$Msm7Z6vAFB> z%qXVb7h4+m*ua_le4a`Y4K)OvoV?#LRl@k1{40VkR`FJ8R6iz<&OVf?({tvZj6q;% zCHXD}d_}YPY{iB#=5l;ZqU zBsiQY9m(g+y%cK7i9q<3Kd7x9p@8DrGb-OE>S^<@seM)jX$qjR@9ult<`e5VM_Fi+ z0GtsqKgLhwvmO@BKmWY$-@-81=pj$C=Z$RkdN7i<27#wu+E}jgLF0$N+q2!F?u%zo zx_-3#X7RJ~-%DDCj2ZTp^GroXfrezTxt{us`}F%B;amX z;v^@&l-9xHiB<*Dv2*5AP14urB3U+T!gm&W?pY2rn#mKzJ1=(Kv;5J_cvp=n>D2Rm z>Tvtl6z+nAw>kqUAT^b}Nx7|-Uj@?0UiLI+LIGbHmBFyfs|LyBk(P1n3EoMgW)F+$Av|R2s zzH0UT;KxRkm~oru5OACkH??x}D_$N1lbdWSq?~yT+*z&OB!~&n$kfyJZB{ytGG!nV zG|bsch;P!6F(->Q2ORG`K3y$$OlzST@h_8R-UOm{E$CT#?_R$8c1oHrp9=i6%T!R{ z%s2_N*od%S*;t$a?>Uzr2~1SfR?!_aA{;Ow>qxWc-!$3E;Y6Q*XZ>@>zztBd`AyT= zXRPX8YxHLzXx|gHyN+lCh7BNe&#o$M)M#8h=uNic`TKAnl8J6b-~}IEW0|WLqjCt$ zxXzpZU&AMgn$f~UEMNYd0bdsVp&Jg|Thldh>BYm9X(Or;_-Ds6zKK7faA3Wh6&L$M zaqbnM3+^?F*a!U11GmhuO3R#mUWVQIaToSpcnk>apGX7%@&41#?tDJA7E60YzEso< z(Ca_e(jqoB#oM3d4Jx}*b@Pw5T_9VZ%9_>-{ki>L4{^Z6m)KO&ko-?bOT|B48n7}J zqc%jhwvx6uO1x{>tmXeRN|ElBTas*TE;)aX3=2_4L;jWtIVB=EpW- z_@83~2|O-`$~*O+`xrFqI~J+2m7hA8Ru<`({*LaA(7f>JOkVH#cL2PBMa0QT!T#S9 z1fQ&@{Cqs?onq;;Q=Bf4>r)3N!EdjCr?r!2#x+-_RaNL&TQ!&G6D&jB_uq4);52In zKYL*>={X#}r|0r2XGD}Q&wEbIy*Xym+%9FHX)h{{@AqmG zo#OJjlcGJHc0XM@IRjRMI#*tUvwoKGr-^y4v!?2N7yGsD#qC2JqmFa@18^*8X8kiK z9IP@iL#JP5X7m2( z4?h9t1ED>NlOBT)8ia=D+H6V8dUEhslG^6jTO%TB+ijo>lK8m$*?o zU-5l|1Do@v+V4zvc2^%8)d|m&++bK4rSZC&IB9PGdOOSW`+8jEQHR;0xw`IQL>IBA zq#qwOKOKe>h6m>_5czzkxKiebelz)CU#jcLyN_572zaZKM_#g;qZ=`>-2kZc-{HRwm#?X!NdcqH7@(y1q+WFsw+ zE!lkt5AVu!-UnD+Dg@D zBy{T)b@If!qgav?!e6LJqSQRCYqR>S=4Vq>b>^|w8w zBUk2|&c3*6^o(9X2U&AFd~->dW>Il9`_)L=^Y7VS>aZsp-XtMj^}e{{kZ^Z zP2pT*%2B#PI)pt~K3-*{?8TgRoib_)9^R5uwv{hQra|jyTe;A&MsS_;Cp*dkvvi+g znEMleIG{s&0>5rLR4FwF#*5i z*~<&_3dqhIO4!(R;Q`J zuY=P-!YQ1~ln`eO*s+*)ex*B7p&?5;6QZ160E>0A5yOSuU=j0eM(OQ+@xHfzRWeUI6%=J7R_I4Dh7F3fR|a!L3&2k z9-a6<+I!2eD!ad1loVV@FJJ-EN_U5Js`R1+Bt^PGx=U#k5KxeiMW-}Khl0`|-AH%$ zndtLA@B2TW&OX<^_H{lSJ}&Ni-m`x(#~2e4hVqC`p^fiT*^94%BKmIn*81X80!ovv z&SLK|8{KhUvQMWUO9KrlNNJ&8Z@1PPjkz#0eS5T{3R3QZMT_(|2mNW1{`>+N zs7q2=kO)_hs0Rie#8FvGBNtJ~Dh~#$TE2!fxl@{Kh|>Nw=23f3BTdPMqQD`yk;X2j zVglVpK13cD9rJYTP6Bo$F@|n_F10FYI=5s<@H`4^x@R?71{Vnh?|4L7vZwd!h|@H} zc0?69QSB_996urE)qNLh>y-dovg)y700pi@d9=5BUS%xZ=0-Czdo8pjq!BNkiO*dWtT*9Pv_WBe;iH; zJWi>A#=$N_wAHFY%ywIk4kxWuk*Fn~R>|fVAZU`*hKTdo%~-QEA?|2U zB<8qKEw<)+XCwP7y{`gL4kt2BdV%=_k~ERc;nq=UT+IQLkbaR+Th-##Wl1bbvu9OK z*nB@!OP-0WiP{9#MU(B)QCuLGMyaNYniJ)ZIN{9BduFX5iwaGM2ACU&;MxRHG9KPr z%|JRq^+bMeFS7ZTY9vCfu0i+C@WMGNq@O3(7ImzJC+b|GsRVd%e_XD;b3%h4ZUi|v zi|>y#3g0^hPEk*;V1H&acPGk#Q);7o>iS~|v!Fzpx`&b%U?3oMD5gOum$Cu^D0i7j z?pXq1B#RLf5j1{NEGo%+l8_$VY8G8}-3<*~^lv2094;-H<%3l6I}(xsFF#@hI@qZC z8%5d~X!M@mW0bDV{DG{%Xx@zEspp{NV5>Ti`NJd#r5SUO(>KcQWnsi!2(wm6Q7F4Z z39LS>j3ZDy)VZHPhJ|=HaA#N=c@DqywVlB4B{&fL8|Xch9X+0SJ87CBIzCM0Z$hn|5*8(fme{shPr zC&7EQ?N<=If#{fXOY1mtyG?=KrOC!xSsbU@39H{1=weZPvhVJQ`il%8JgF-pFIQ)S8U98>;5os@ z+V?;1YyJXe^uJx`dbOW{hzj82{0)G>q1=s5_>I2Re}p|CJZid)7oMlj{Tq_~k08MY zDCONoXJA!;=4l;LmxQH2kxbAE)7z}q&1tk2Td z?@>zs(H78=x4}>q7Iag0;4w%$nEXey@!yp*>MD;exAElpk1hk1Gw&3SZ4r-X>C0K5 z5||gKTliEo*Yseasl4U1dSIe_X5i?NUk59HmutEqW&=ZX!yrz^_ofR<1(Bwc?PR*N%GHv*P_@ z!i%2uWx^L-olH(^W8Sq+CdG%F6FR;(?r-*~aEU*3_iE!{Fuqxt*%qzgDY#$$_KFHsO4ZwF4HI8^J`PnK;!E(AP`A3S7{;^#u{U?2&U--suDOx{*JK*`2Jn12t@r%4AHID|4l=tLeY`(Da<<d}D^qGD#V>)`@NAEap|bSY zltLL@2J8w|ImkbiRUH;awXGt{%vNU9tJF2~C`oCuQTvx6B$`1R^IAOqh4ad#CVv_|Yb{(9|wp3E=tEQaTu zYeW0-WsQhHdJwu#4*9<{V-$2ec&a&W6sVSM#rOGTkrlV23rVemQ3Dgzb!m%j#DU*2VRUCdaws0yR&2}|9emgeMACZl zxg8bFYuz0_KBaVe@z}RrM;k7Ao4nYvl(5#wDby%XlNEloL$hek5yw}r(w$sCo6H<2 z;XX9~^mewWH+hH<`CI*pj(uz@{C%XYI6*0W(~`9Fm+OX%LghOCV1}1Hseo7B!(z#x z-?k4jbQ&kRNw2pw^yFki)3wH_PPm_XC`Ip| z1Z$@CRH=8I>A#9E2!;;I)cRlXt7H$eU%n8^ z!+-W{oj;f5`Ni@30UqM-w~=jPmiE$cs=XpnSWahS?u(ZtV}%Ah`I9x-8bt=0R>Qf} z)4~F=BUoPFS4bFAdbwcHPeCIl_;iGFqVBEYv+-HSC+2>GsEd||4_m4l)*Vu;*4)`S zUnb+VnbAj2|3pS!ILBTBsHxKFD2%xa`?b*!pEM$=C|oa85i zGecEk(1z#Z?7?TtVdU?WX06dL^6xELZ;{y;OsUc1+CT8AlY4qKu{_M@u2W2uYPPja zCL>hUQdODREp*Jr;ZryK*5jRSzTeu#$a0P5@hMH(gyzAwlV3$1(}fn74*1E&=4*R7)R zE#+$7hUHELlXRA;w{|^XWjTQyINY<;wuAd#;!TZ!^aMXMI~D^!Z&4PN?Iq8HOZWp* zPsR^_NGq-5T38kWG4~P>l~~Dx^RwH^+KWR?euC2#>`7I+q<4A@4ahju5}dxzAC004 z)WdH0NpHl)lPJ6YN#Iu{ppelk%4+>!GBk7)SN52@vg6LRHiLU|+Gc~VbJ<}VUF^1F z4hc^<_fgGC;1qAYe1dECPs(}n39{{!zruLU>cEagwY|C&I*qAksSS8BbkD%&>f5E2 z`Ugh}3V}WfJ@HLXUCdh!67@&h1pilC%|@2bN^+S*>HXY%-VWy`!H-yBAiJjp&RX1E zX4eOs;Ie!|!!^;-^?-VrGjqs^2rK`?Euz+s3U6>Mn#14)ZaO%wfgE?bZa4T!lJn6F zBvD%EW#2=!{jp=AR`QxPb#%BQ`gn#%ek5V>gIDsP^!VS5b#bSnqsC_Ep@f%ANq)JH z<&Wp{|U*U^H3GY zh21Wpg1vRaUx_w0anfmB z`~FyG#dT5wNRI%LRO<+gWP36rkZF3hlY_uS|2*eIqgM$2#A5N&_Nxku`buWg zCnq^UYN6N1`=mdI9q#!?G}lBQm+X0NM@dU7u4Mop=ysiIXUPU8zTuiSygi5Hv+-~0 zbZvAhd9{pke(jy!)BmaS^H1bdmJmhXO$(&oMOkhC<4sd3LVAAEp)0dPywi(c6P#Q) zZJ6%U12w%=I4-2;J+N1nc*(O_H=>dkeL?k_i6UL0AO6RTyXbf4m`K{jHN}xkr(c@q z>ymI?!zjOZWp>H@z|*1CcC?-->Wpdr_!Iqq&GPRrPM>L-)|EtQF$Uty2y+%6yx(I9 z3P>s?%H=+MA7itcasB*=grqBZbRa6ad9&k){DvEN+9>6vI z>H7D&4AJK0)$9d|X6W^9=Hn>nBXq0W5Z{V{55#ujH2-Wx>p|A_-!J#D71tVh>-P70 zLcmmkiP!L$zMud?$rP`-4=eXLa=2)m7t&J@t}YJK^*$bFOH;7_F|{$rc9iUFo}47? ziDRP(t#Qzs?oV9}U2jAmxsjev)X<+YsJEPEFq{u`mY6PRb4UI6@a*V}swFDfgR`rI zy;pD_V&omv4Oi&=QKG02(SOTK%?p!}VeU*Cb)_~i75sY5sO zmu^KDa4EQ^iYEyy&SD=ZiozyY@rT z3KDx)=@(|U)+=WXp9}G;iP4^PR>K>jvx7^=N*$-wku7ad6GD}(&XW=|&BOBd)%r(G zqI-?i8KQm z{Mb9k3fa)+u}}`>R>A9nE_#nGm7keR4ivoTFDtEeB{gu^U{Gdt>Uu^4!JJ>dej0_U z%5fZSFH5fMRMT>}BM4;E4kvbEP~t+BbEU?;X`dqGDPlN?be4Y{C*|s}H~sRZ7%6^M zqkXadlo!1?2+>@~uM`!ih)L7VF;SsBg-N6ou#wKh8G-P}le!E;Na$HWeq=IH2jksj zF!d9spnS1_fM@+IAQd5qX4VuGDa<{p6cpQx|5vZ{SiYpElT`WNP3$bfN+7jVDKsc-~s`f zu*=!-5G;~#fH>;80SG5U_hUk-*W~Gc8%)5BZr`vM%BWkByE^_QCyB>Adg@n+q4+@I zGoG?nTe|oJ1hgUt0Q~;+T|c;Oq0DlK-Vvx04FC-+E}wH36&*ltA_08Ecawmv1_cOb z6MzLV0pRxqAcC?`T)+L+xDY^lfW~_SfIf9QGbkoeo0yoqs1dxGYY9r@yi3!L{cpdZ zxNO!)X?p`45G}Z0U;&`a_j%;d`{ROgKKbhY_$V~wY}e?0wsbZJVCVR_VyEeXZrdNH zd;yVk*&+Z1oPI77+@hr}Bk_ZQ(8jkdfOg{YK6U7;4aQsG|2wFK!HUSj#{hNjC1#pS zMd1lJIie|#Jjx#(FTGO<2bimGwvqP%1*g}PvQ!+_ZDg42*ttsjDVm-4`Aub zXH%8d+Ith$CEvGz7~dN}kb48*=;5dsIPSeP!FbgY@N_mXp*zeW{bc}We3e$<)(<$J zdI&TqP}(8^uys7}YxQL$lYf7|YM-OaE|>?2lS!c#{pkTKuv~afv`(o}z=|`FNO`n@ zi_{uIa~BKiV3Y1Y$_6vR?P(WIY1%DV-N~3HvKdgnmVH^7+jqr_uepRdy^d{aiRaIc zHuj&01Msnv8f`OeD}ae72N3~$I27=6-cHtd%{N|b69V?@hcO_!e+rnGpP68f+;`dF zq@dG?|LL?-#TJ0#U#1o3dC&g*ZNxn;TU9_)^}uPCYT4a&5c(0*OX0?(0g!CB9MtyC z&TgFNO43FH3t(=G8XMF&NDc$>nu9aIzl8!USKK7bxEU%3bgh;STT7a**M(y6q^3}m z0;>9b<7|fkN8>v^qn>YwkmT zw;Epb??L|uKh}5#@Qxknl0k*_wu`q_V(C9z8|td-;cyABXJ37b0Q&+V;H{QzS49o& zA^z?_E?nprCp#oS<%bBUBoT?YZ9@So`<(@Vz~8O}7$A8(S^@jmLI{Zg9yU2Y6p%#Y zq7=S*9)0$`+>_q}?C;lh({*pa?2iB(z}EnA-My1>AP!W5rYM1&)9qIHhy!2|GyQI^ zoo+x$vH)S%0VIJKfYKO8EeF$BR5izDsO#Q>?Cq{c8sj$WqkRclQa)6yS&+CoR+e?T z{N8ZM1MC_k6v8fe#>U1b0Nj@eP@&w@Zd(%%W5Bf~+V{x$6%q5`b`<)PpQ$3+-j{n_ zPF{dVOv}!$+yMCG3mIXQZ<|g46D9EiAIG;1NPF5{=RPnI*j_pwQyMBW?Ft8yQ$3ex zKn5z&j8<4!*9It%Irp-ZG|+-S85^6zLc`n-K#VE2>phzuJ<#uB&dkh|0Zf~Az@cvk z_Cy9q%`Q0(SKgYGTN49>n8JaYT$$Af^D{$oz#r}5ZY>R(cAv%hYt){Sz;6Zl#yS1L zKVPSbdWFY`oh$fm4`xT`e0dqQSs^7QwdClc0KoC@x)_vF9?W7IKYX2 zv;B-;HIi^kq3Hl-VEGjgiOml17wQJGio93~sJs*DfV^b_?8@MaTTbvTL#hD~%+p+w z#eC}A7o!B0UI9q)^6V{s^Y$JY{(XyKda5|1384aht_4RT^xrW}CB z`v~{96CeRNk<-Hy#6L#tD5eUJ9MsHL&-&^$03o_T&eq&OfHIaIwd@D`dztMde=WGC zg<%iec=2r!h)2aW(d3Q2ar6GiT-7B70+rb5ebPSmoB1%RC4OOEeUE0S5ftfG_10h@ z5E$KV7HCo+5b^S8Wdtk_UJPLREr$@_)le^0cvXRh({6B_&SvK%$*Z<}ow7`G8MJ%% z1y#L)J#!)tEdUqmIFU!O-ns#p*w_h^=Di=|Qjz?bnz{N5v^hLNEcnqCI0~2qT~=dx zVG7bNINdKE64g60RCg(-Gy(>T$&Vziv_3=Mv+RZm?Pk`@!WL*QJ^}!u-vFiD9$VNk z774ooJiQwthYC1iDU5J`AI3gL6$|ps$VcP<-A>_Tl!H>yP#j!9+^X)1q2dssd*Hty!@lJB**mB~!a5ghe+2^XW8c1pySCht*t_{w`g-D01dV2x1NC62&oqyfu@b z=XkZKY!{Qt+i@7{@f8zs5Fdc{%`FnG8Jg>g^xN|#KYEfFQc`bQ1%=!`X9tqeXaeXX zGl;4RAk10U+LDOuMdSWE;r$B;T&!y9t^gu@`Qt+};z9KQa#&W*C#i~bf zjzZGaI!B1b=zR0{l)pi#?_Gu>qu%)zO}Q3c(e(`rL5+vtMn4skbBs-6o-PYTwZ{8* z#wrpvqZw`A0HWIa3)N5kj%}FfX@TuYfwwpLIH7)dV|mvFB%0tZz5AL7T3P^wN zSbG?~80&Qkj4=MAg-8w~{&LbksWP+10qea1j%1XJ?>vum;n0D6D}^D{_of#M8xKJ- z%owx)c#KZqPBNPNy8u7`9Us*nAKivZ%zNyzQf5)5q3D#5cn~cVVu{jU)3!ACL}d@` ziakw999nQ*1!uk^|4#B1Iu4_xAG4Ey#;EAU%8GW1MVKS!!RZ~ja|POxw(}6(Z-G)l zbsJCRGif(zv4xsg0JEN5hh0i$8g}pqj=o; z<_C}twoY3Q@?^um0n*X0d>%B~`+!u)E{N8}#|AyqM;i4h)4@W(oqqbde)n|xP# z(slK!!gf*%rm1YuvA~XHV`|KxC+04wrMu~{DRlqIM$VjwdNiP(!TyQMA4Av}-J@8) zcFFNGDrH3BHcUcyo*nyR&af9qSr%4ds&51?% z^DRjsTU8n9-otL>Zj5g1w6lOPQ=kW^Fv{Vo$I;ynR5nD7=a{nXzaAyBzN^{7GsB93 zX}k603JU|}%Cqs?YLWi4UghEbD~F4cBCErW5ro?xJkV=Iq1fVs5Uh10=G$0hCB|#O@jXT{ z&YmI)@0{{<;)=}F_aebKM9ic8*WQX;cE%x0U0D%vv+j?Cjx$Wd9(U0yehiDIO260Q z5(LyE-rNe{kU6kocD~~?Cfoh$xY+r9SM^x6WTc+xvgM8Yhkt@k7e4g$^#x!L_XDIY z1WOm{fncvw)B0u07Km9SWCKvpA58)t`Fp>C*!cjKQ>yC)nFb=|V05@Yi@Pkih42NL z6p&?4xY+H7C80Wqz|$$!QH>zOqU-Lf_lhyOXbCAP*+n1>4z$}#S|1hQ4TTB;dR%+u z-0=@#PaoYOkCBxN!?iPq`~c2*?t9vK3JiBnf^I6x$Pjs}f)%6FAi_O<>A#-pniXVt zm#+B5-bWXGQHk{y`bI_MZPMF&rKUoennjmKo&CAdky7ucl|+*uoq-p%1o4R#1J3w` z6p3PyQ@a-E()9U0%7jVZxToj>IpgKa2f_~<#xNgrLXCV0OH4a&^y*ylR%$5zu82Ye zQmSC6MI3!iIjTE4U)7$5@S~3y);zH|xZ;-%nM%m}Xr0)p{&o8Y7z_)_b{%hc`F>Hs zEE=Rp&F&r%XrD3)aE7{P(MOwB+*P6L5V=QS4JmzvX`HEV>{kL0rfvt-t45h;mbj%Edp;b!QIL$Di z!>Gm-b%T3Y+N~dx?)o%>H#9FWx!IBT&|>aqOoUueQK+jU&JVOvR_c2&NTtpX@NJP= zB8I_XeLo~&Z$%a3d?Yh0Yd=o)^qKBAvU#~BNh$Cc0)emqd4__JJlIuRplrM(OglJ2 zcOD*5sMMx9v(441GWlIrx|8A=mkhd8BTR&E~#E`gS{mHs0M(`LqE`Mv=>s>U(gSK{;;D0Sjdh@~M-f<)FY`!z(#C>*N z8+>>M(QC-=3PU?upnA)@JLA=Wj~nx; z!;2ns52Jw&**`n`ha1fs)V9W3;h{TEzb=L1)z4+iGd>0PLX=WEJ^c6S zA_uO_8{mk`-jdr~wk9+`g3AUxlVTrm0WGTmkPbSNlNI=9OBjHjhDP9DVU*~1ex1{K z4k+@j8^%%u*pV+m;Bu>neCx*^m9qSkcd~#ign`G+K}47d@)st6qCP4e{NQfnOhN8X0?WUwu1ZL6@cM27zN^7;D0-vLYf6|l^D>KrT@RtNLn!X`wX=!+jAemPc$|4EgqUaytdBVIFQ z>Aj{`qcPb@kpz_pcJ=(llIin>ldDiA{&Z_YpdlKrVN%IgT*A9TzjU>#$G%~V1TS?ItmB7+@WE;Ha0YBJs!IA^*HkM;)h1bA~XT!Q|S|I zLHt}-(W^3>r5pe6^KVu3BUhV15}J`rE)nko$;q*>oZ6d_%(c|zx`cTCwLckJ|0lkV zT$z5Ys=F+xnai~0CDk#9w}Xg}$U1i|U@{TWBGisnIuuU3f7^pCrjWsDza5N|?62lw zy>%;T_0i8x)(XL`8QVc_Gmc#yUfYcapZ7d+js%IC24}oZ(!j+di&M|tX7+q0D%|{D zih`o!)NpZON4q&*+qDP5M>^@QJ73-U%Bw~m$lj%d$39UgA|*Cs+@5WEum@WCf^3K6v21t zP;jhnyeihi_4fSnPannn2QY3{pOg>SgS^t?tX&GNQ>MAq8RMb*e+upb+B8WM#oD9~zN~n! zYdA5(6ANdv`(o#vfcNe9GiCAn(CFg#?2Do7Pu5B?pWjv+#j%Y#pyqwBJTVH-dA{w9 zE}%OUI7YW<8sqmuX;xyRN!Z;mJTVL0$m)gQ6P@Z^O#Z@J+1aM#$-B6?QgktunMq^j zrZ@Di<_Gje_gkOkYx|Nn@q>-_zQ+^q^*uND7&yguNaja2yzQ|CYUAHpYG*w8Ih6RB zn*aN|rd?v|bNU?h^83?X_PUA;R{Annn`C;atl2*VTi}k1Y|Fw!D4_48c zcl0ibcW*ssV1JG6)wD0%#hMbRS7|)^>&BMcGK{w(ElUPIcR%kt)JE*IXv}B-?bP+* zs!-Y5!H~g9k}TXvh5Y0bftQNm(ZtTHTa$oj4(q4cI$zKHb^Uqu?7acLeeb!#rl_lI zJS#bUT{}+Xi5Ukdt#HP%-P%xHv)F{_%*a}C~ zD0yWIF+QW$I3(qs*tq{CZ~)ru;O1Uj0?N;|+VWg@$I*B&@#*cvtEDhBKSI#g6};%H z`i34?&$)cApOjg387H6Z_|zy z82;=jfA>!A4xQ-i^NLRAqekyZnq-WfE+1B6`%^`YVl{bI{c89xsSJ3Khm6F|{Dn@XEVhNt8BU zGOrM&+7V}}9(cKULMO)Qdzuzb$KL9ROX=G4I!lumy>Xxys_5+&U@_JZ3G`tx=-(s9w0D_^q=EchG@q?ZRleLn9Iy&l!U*utq;d1 z%<1XgeX;Fbo+Z=5u9aWNs+y~sORY5d3LjNoId&y)7Wp7f$okbd=V-JYEyBB0gSeSH zW8P%7hAC5Geqj)L&-VyhtwMuzZ+oHAz>$kZNDWE4E_ER$Ab+W*(}z~$PDkyM>Ly- zcYJF;9VJb=V*rF5COOGww`37#59pyfp zRw=rTCz2hJ-oZ{4T^DH$7#Fy~O$sF5s_)_F%U8pyEinr-UpBx0p?;sx@&TuMkW+u1 zvx*1KK8)l!r#&MF2;2Ro^hC~oCYMJ^)d$_YnMFZ89bEms4{MA$Ha!~+D*vI8NA%Fo zI*x#*oT}Npf=181zU1CW{o6?JvLk&d?iY~NSvq>~z_9Lse{`3*=|HjVs^?yEiVD$^V%QYLVPHb-a&D=a_HRES_3{uj#Ysv~O>fem_(U z5GDyFpAFe-H|L#Tv}P3OmXXL^`F#_?;_LFnzTqaroJ#mij%~7M>le0kU{$(l-?hm^ z!?F`qw@71HoJhQ;vwLH_t`f2$Qq{oYrt{H@eV+9P$4qd4d@UxC3% z!mOVpVJ5NbWqcoG74TFxgT`BOk=gBx_=bQoDDKQnhRii2!WpB2TODn0@96Y}#Wzv?}bPef~qlf^VNa3teZ zQf>ED#ePzd<_|{dG7@$)%jw21mQX7m_9PpYw1h) za>d(RQw&03M}(7{=(*0TuQ{h8eHguX(Z85Y7?UHAD3}!k^51ns)7dodU`hFOS7!5zD#pXek z4vb*0U>U<`%TPp~(A_LhtIp6@eF8V4+ENT45XOl_N>W0GPzek1KCx26S44)8K|YgB zNJ$6MIBs^0G>t$j)+U~k;X+rpy6m2-__LVaD{Fy1x>S(kvboe4U(0%%VH%_c<1tIL zxty|q#K(W<`VhEsj+=mu`~!R8EX1Y>`Azf)wx76$`W$%K39%4 zpE@3y74J4D`szKSC5;JF!zN?X7j4D9?>iB5YSK+G-Rqr|gwSGA&oNam=~mS*elfM7 z?MWyPd9QOZUc&vE=y}f>rE^Wk!t!`s##D0*P?y9aag4ihe^X{DYQg=vLvb(>ZI5g* zGEB(@T%&o>$DPC;irgJ`cCksS?(FC4W)01;guLv*Av+=hye|X?-2WJwTCwiJ^*pSX zYKt5J?(|LNM%Zdjw|i1uhU9g*An)r%9aD4c`IX{kDI{?>pQu&Gi3@gFK~&R>?(9)i zWN?0Bd^#p~-Re%w!Syh5_H{#3Dw70ktL~)Hj4l3;*&2m1=7of=^F*Q`2!}6eAcSQ1 zHBc&d?;gChcH3Z@P3<) zML&V=3(SYrOseW_jZap8aJjs$X@eRWY!?YVwR@)jV9|mXhuKFpk1NI;hcMySc@y<# z5i%GIDM@H4q7Fr-ky<}EhgdwXLuSnrE zGvJ{-K0-*4)DA#bfm^?V8O+o#n>PSet^WV6umAr%C_z(juf;UL40<0tR}|$g0yAI_ zu5O*TjXJd+C45%SUXGAgf6RuM`;Dc3+)eV7W^0#Bqv6foD@zi!f|EW@% z!tOg?Prh0!;Z_rFB9L~{Tdk@+COTaq}-$Bqf?1`RkgPRc&PO{Jj}m857h4XBAokb zD6hg4H$`6jD1FbMWG`&8P|^9#e04Xu!2P+<;|ILxkD)U)ueB=|!n3T3atn+T3O+?z zO+6}ZbH4I)*%4(7<5T|rW>N{+K#|Y-yWBw0Nd%{gpJu-9MOe~r+bWl}gN4`McUUnA zr7l15DL?QSXn`he7|BG{z3&9KzLe-5khveb4@^(DBL7&-`%zxYT(<#A9a? z(&;6+gcLQBjdoshnEb^cx8J@y~HbT7<`kS!pR2 zUkYM1)0YFWv8|)Ct1s0Dp@ksA!WXev5>Ln76<%bJ9Vzfm^i0sqbwhF!UA zSZ8;Kq%uCI@%D^<6oRGWM|XxR@BxJt7J9e2wJE=H!w?I*5)Vh(<&Y&6$ z?c~5iQp$5`)`p3B1Jtlj$<qylI=1hn&rTkWb^#&Hr&7>y!`*6JNyN*r9Fw9?z$zuW$zG z(slLRJ}j{R>W95ndooN^;-ZYhHv38;GMW2ng~jYu$*xr#NhJS##FG)LCc_-xA=Ql? z`t1%)#GGO2>j#_H7gVb!?!Q@ku%Jv;UtDxf2l$_xsziBgToo`L)?jz&QWFl|2HD?v zNFvNHa2hEMvbN6%XqKbnaj-u5iTMvU$dEFyaGRm6wZ)W=J^bHd{pzkYQ&+id~BjGtCl z$TD6pNIj{gLy?oeEbBinRe00aty$>Q@cd+EmMrSJ(PqTQO|yqA_ zpT!+AQ)=SQNQj1FG1XH=JxxOi`L+Dh(89aUh14XHllxgKeM>+Nj@apLoAj3#JwhDv z#SK(hIAp08jc?6Z?pGwz!0yJ=WJPbwQT))$U7bvJURsT%7u+y<636~isSPigf9pz= z$LzZ>mvwZG)>8L7Kc0P@vmpNFntXBkgCePR?cYuL6dgjH=o2_uqGS4{3VWor`^1~J z`gpf_bxsIjKE0BARP+g03s#QUD88$C?YK&Af19JsYv%w(kRjmDhMdujdXX{ zJ-qYt{r&g5>)!Rf>#pk`E@obJ&Ux*#_p_hReQ_3BRu)VTj);|Ta6xX=To0+a`ez-Y z;Naro5*)3zvT<>-XmAn#EHc^bysQtF2U(GnWdJIo6K?SW5rHO zd@kXNA47*m;I|P0^|-Lf<#{RGA&OMf(aHO|54x~hR&B1EwH+;p9M6&P!w&V^R$&mnzO(`UfFbUkEgUKy_~~GQZnjg6dtbIv#64pPKNqn4DY&pwGaGFrpX*kdLCs4ga0eIo)fRcDQ|PPCV7>KnZpDb{phcfF7-B3I^<>~gDe5Fet7|-VjK!q3%h>vQMt`X(rKZ7 zg|b@aBXrC@io>iOA(tXlHF^l_J(foBWsVcpjJC0trkCtwt=oJU@1(ti`1)vBmi}L|jg5Ohs<0Ci6K4xS;cx_!-mE?9 zeZ`cw{(5mC75|G&0lR{)Q(mVBxqBNEHTh?jW@*X#t>H9T7Cq^aqCVctx+R&6%1Jr| zbRq?Vw&Rtm^L3Bj-;TVaaI=h%9@i{h(B=6)4F&fzr{`0Z4yMcFRbO@uSTyq(8Dv6H z9Y>SBSqciDpM2Li&Qyq17H{s)RUdGnFBE@uExXnVY^=Ky;!E);G5wa8ff%h!I~z5d zd0jYaU80w@N&Jp7UPXFkd3#Gek}yH61;d(`xlULM4{b+t%<$nvKa9PP%@)u6m6HW> z&p(Hz<-r;=WFy-9R74B?k$RI|Lc5_~%#wJlwFa77xG*wqCK3O1z+12B%ak)+dE1SL zv=}ML(re*|ip8)S7uBzRCo|R%#?Tq6b#u&B{}}ElXzN35e7(^ZqtSoR5z0X>;FP0x z4G)ik>)U({@i(Z(=pq4)0CK5mCnF6qI9PYQkeD}w(UBz|9r>2zZnbAb&p-R(-jbcj zC@XeFS2Udnzl9!IdpS44f{4RzQYe-dx<^@RJ*3Q5VKpERle_m(Iav?bLV64ja=6+# z|Lm~gQj^V_yigy@&F0l4ju)ArKP^W6pgI&KW}+O=m9^sDdOTeVA?3TN*S%!fSdTM` zPxbJ@yfk+jMEK^Yxnh9Sp`bhE4aP^)LJeR&^Jrl@3L9VAi(qiafFkl_dMPJ<><6w0JU|hBhq3kzT zo*kdoe8_e(J_!-5nQK+?(ys7!at=8={)MYvZK#C&6vs|u%v{W<%($Py`lBV9gwd|( z-iyN>nZB233qeW0hxH;p7oKg=tg()Dv~UiD@1URwWa9VeLrO#=&K6EU0;dXOjNWk7 z%oli$)ioG7k5v?-Qi2A|a!;Nfe|GcoxXY*iQ{c;cwjN?o$oDw1U%virAMOf@TBFkN zA1`$gIubw_$g`J5%kmS(d}mkDX>uQ!I!%|+5VzjfbPrmwVB`ypCB$$JgR~$FuVPFz zJ;x3Yg3vv%VJ0q2WXpIeC5nQ?T`r?KI?Jm=FV6$MEbGGlE=I}f zQAi6a_*Y_d~i^s>|H-?|er|6Y+|GHM|#FaH6xipL{m8dnt> z!c&W>z&(P(^!a?k!<>1adeN*w%L!fjuKbT$3SV9iBrV!ov>K*7m$gb3Qjs)i!dI zF3aKcF2<+$h6Urh%hveY<1=9=l;~AVc{$Aj6Qf+Dl+rbr?lg?ft^eJvM75g~SC_ ztBFWA$>ik*Pw#JTazbqy*D z%IkMM9{zILh}e{7_^jE7Q|0EJEMMdKB)atZ&D6=#okw-h+%q4!6ZUi^`#b*ohx=47JU4Hbd3`w1>|fmPXwARn zznJQwWoS>{R4o%7`NCw3x{U4h4Wde)leKq@Zu@sgyNSn*6_08SM0e{)WmNLWBODPC zUtDJ5%`fu9D;|HxI-YyA4i}nKJWjCj@(EU?n^ITHa{a|6Oe}!>F}rmwi1)(d`>T(` zz-2aIycsNiNK7chD^lPkGu>Aa@VHh6yRus(<1PehIAsbkorLsq&Zv06?3x2`SzKo| zh1bI{@$OihPDe{AR!Tw!^>$hu{kIzexUZ(s*L>X+tW|Qkj(UzTM&{7yYQfwJ(K}O@BI^PEw_%Pye2so-`bs9hZ6WQQ z>P(oV*kg%e&RqGDDk@q)DaC%rU7U7&n8~Y}N43fQdoyu~>G=aqm-Jh&z4lNX&n&Rr zK1zw~pF3vnY3_6Riww?iyIZOHWYEfF;4Sm1B=H_t4y9+GZThIx1p1#2mFEeNq%TjkRohWfA@mc+PG!upk+kW8B;a~l^13L}E(k~K0H7PeStkW_z zX-FkFgm!`W`p&<9HOlV+F~Ns-kE*0T4CJOE^dD@Gz=wardog&hI$j>`WTh^P}vWBnHT-(ke1Js7gqW0jry6?RjTBLjpP zv5?|Jii!+VmlPql=ln(c0zp2GM|MHQRGXNiYgo7`!nU5_!aOTEA5+}bU;>^OCzD1F z)av3*(I4^|EhA$!(^>`gDCv)GA7xxn3KnF*C+*CXGfbdvQIy?*6n)jmu-_Ya)@+{sxlS@OH7@HH0E<-><5oGeKA~Z@M5- ztb*dcKy|GB_)iLTK|3a41pPWECSqyoNh5yrEkk+dUF#P7EYU*~-68dNJu`13gl#^< z3?o*Ux4x0zZ1gxkGK3U<-?*M39m;#1!-3e2B?_r3p>)9gT({g(PNNu;uL=7i-9L>G4`a^(xPFo zvrDUlW)abie%#5trM&Hj(XU{EvMjXN!?1wvp1n8Qk=bGFwC%YqTa4k)MfrsMK&tp| zbx+UNcf$Ja1NaeDSi`g^i1lgBD-5UatF7}LF)Z{QH}SM5jxBPn@!9tf5P0dy#TR`- z58kcg<&UXcFuPyA7<-Hsj`ZEE{+?$S1Dr5e;Pt02BSwUV`eX>?_XSr@QOUfg=FcP8y3+Hy|Z&pG)@j(K=eAKAG2HqQQ4 z8*^M}x^p4vk3^+)Y=LjzB!nzB(javhu0$N{(AMvaWcjtG3dpoqW>-;)5Yb59)mjVf zH+*$k8g)0-Q$ifN@({}%%MR<@uTK9_G+a9Q6io&#bnP!Y3jOSYF|?Ll(y4 ziM^un;8XW>vk=DK&))_d+IWRtwk40tMH5(BLkpjwRmYgqwJ18c{%?6VH_xoqhCnjH`zO2oiMNPRWnJXY0?WE`991_T zQ*8OAK<;I1d`-kUBIqw0J!2JaDsK$L;!^0P67%0F32*4l9<)AB{$A?I@pUJ8o8|W{7ok) zB5~}D>)bVAry!yB6gFw}P267{YFNYBAe-_&UFW9Ydde4dTRNCVE|PHsc**qn2+@lp z`qr%F-}aJ0tbzvPGqnUCPkclVw^mdy5NP^6MPwSADM(LG0q;;^*~dg7>V0-sDd9nt z)+duDg?Mgr5dc@)C&C9$5`1UTdiKc3+R89TY6Z=E8{Zh$vwqmOjuaY)BsR{K~)NFnl|M*Df@INo z0#kzN)h;4ej7eRNw1c=QdYM^}|9-jdxpoU-GKnt@Gb~8U6$?3}mOjrnC7AMAyG4!C z=>85*fS3Mpw^n z_AT-Kf9r+a<`KeYvB_hhtjP9=C$L9<4q0MTz;kHEViq2}lc57ui^{*66^bk9Q1Gns zR&x3ovzG54<40#>;oc~4Ux+skAL(v2ee|U?)#u`vr)B4(&7FTvxp7B+yy1$IzHs>- zOzQI>M%0Y;Ge#y-dpjP7hZv4wF5$QS9sVxe;Jf+le&8>XaADxotgE6umlBoH*#VdY$b4kM|6+ipzcv z(TxXxy%u7tk{gIpK#>7|%Sg(i@eb@ydEgzC##~LQS?wlkv-iXj|8r?{ap6~-XGbi% zcE??nl&CC)*o5-!me1l%O|P(t3zU{+RN}d$0lRB09})7Oi>~g8Hrbr4>n+gb&Q;BL zjYrNQmSqT{%E{qoaj#yf#jU%_N%qgDa#R^~KR*eSl9rZB7I5yrctj`ovOwtaY*(%1 zsT_r{duf4jgAw3kvIid{A9tsTuec_F$T9bV_9z_CmCTUF6To8rINn_x5XdA&qQtHd z(iRNvFZan78P+hhM$pAxv8Fq7ZJH;*lQIejjPm#UKE_MB%vMTdG;IluWYd3GMsvUu z^Xl$@v5;*)f5*j6xss=CnJ$}?B^EumMIaQ>vG5y7S`-;TQ(h>J`y6@l@o?J2JFncyO;L}QMM!3q> zj^s)AK8Knlay*t;e3`4373sV>m}@4CfN)j6T+fRkjw*AibGQcy8Wci*i_0SUh`VN18(goZlF33?n9 zz|TPVbI2xTR+DN8qxu;0dE;k1w-v?29ywQcxg$1!7xz$g_&5toeoMVsA;8cD0H7C5@RUrZ0JAU9hXd<3z1qu25fXIvkr)(%rFvlB za_Ob)bsCbR(g&rrWcLknVrYzWU(fxJ}Z2e9P+ z#jinx9qKB=S<_gkcT7r@i{*S|idNsi)9xGB7c3A;Nl8`Ko(-Z;l$LoJ#-G0sxNL1BiJ2=W}z z$wfP_*8!!S%Ov|mQ27C#{y*O9J2reenpHb*5<_uPQi0aeV#Jgd(%U^cS?^Wj=%IK| zYU?|fksvUnN=F(ddaGT04qYM=;?JRIY&>z3-}FpGOrR!lRpnS4#n>^zeWn|sFcJvq z`q}J%wMS#*Rt539R6s+<^23(`#kPj*u#v9Y-^r9t_1gk4#zIKxr9n`W%4u6cK}V~IQN;i{cB`rSIy@SIe*SxDye%gH&ODc| zi2P6Y1J0z^P(*(E*EDfpe%$}*2yY{>I{)+eZ&5^iBybz{zhe%Qg!uIT@i+MP3lusc zePOvI&kofnHae#h^PRi`U_W&(q6)wL8z?zE zx>AI>JPvG@2l8Y4^E4ImG;()5KtO0caY_}~u8V$|6;HwQiR$EQc;u)N zC=5n(%JeXXomB0g*aA04R=V%!6@c>FHGIlUf1f}&srP-Yf?+S&{EIgXODdKV)y_27 z)QsFQ6r|<jlfb3w_>1&pPnTs1?+_BA10=d_Bj{67I4jR#x8 z1;{y`a+BS#davRNw&|2v4YYworOWmncJPkazvo_q$%a$ECuWeziN64L+n`%yP~`?< zA3YYT{vXRyM~XGA4CH6oj+Hk6_i-c+s)L6&+4M^N7_3#OcI8#K5&s$?dK&mKE{ESz zxf;2>L;Nr1DkFi*kGHGcZqaY4RgH@H2a3$$WK#iu?wAeSY^+w1!SVk05HclkzF|)tXR4vA2D#pKX0YzSw zfn4>)wWXd6JG;@+<-3Zb+}EsqjvM_jxx7yHj5E^HH?DF$y(_@{Q7N%JWcEyLwKJZV zfz)C8aTAv@u!Gr5!+HZ%Q!h#V{(~dfiI(|B_HwZZ*G~oBUUJ54WA`_pe8P z;P5N7hwS)2`NA6~{WHcilg|vqRlR#32W|4zj#T=mH|qpowBKkBdjl631s~5cLkOl^ zTzws2(t)3GU{QDW^1hx@dyoHdD2Cv2$({+IY`7^g9NDoXXNn55n{MSLiX}>9Lj|MfBMgY1<{RP99hR5erNZ=b2D&ZF`$|zf3dQI#T+LX z#ymlnB3zvkN=I%c|IspmAnK3fV!MlEEo~FC40EXw=2e+fqQdgDNi?>jwDm48GCe(P z)AcTv7Z7EasfW+1ZFYAHb6L(02|p$t@KTQ5QHl9+XZ>{bgXW2y3te5q0-b2w?bKwt z)ib=|M$U`+OS?${UUoLqQ`xwKu(pQr6sFW9s@b#9*=g4)!_>Kw3102Hd3(m2irh4> zvMom~B>d*3kYc{pMDfO4*-9*%h+m9VQ_8viW3jO<3VsH;cYHegSqgRh_B!d~kTYL? z^Q9nMG9SA=-CVIsETAyf9=qJ{Q5>nN{qVGF=XGr|f0dNRPi9NC`rAW<1gfXz9Z?3# zw(GM(L1ZbHsVDVT^CcZVsorXNQEvKKDioA#%pV#G>Q{du-p)6kPAD!G^U~k3&C|Xk znZUTy8-j~?=VWChpQVXBj*)M0uy{jlG$q-D-MKu`7M5i`R>GC(LAvN*rP;=|$=^#n z{-OTFf40d>GI`=*b)@v<9$b6Z z5PT&pWQTaD$9wrUw*3Xp&8M9bTIi6ByLW-IEik($*CU9@PX#<6wq)p@-^1m&J>ofDg zcM3&uANRFI<|_4bwH(rX{0KO9++rqgqbvqU!%_1WmhRczs^s(j&!&l=ECMj_985%@ zOtinbUrVz@cV@@bl(8^;b=T7DSYpoAXGqm+`6lBlXOIS&|G@9XRpn??DKGk^t~f#4 zMCe}=gg^_W>uyJG{L&tHDjr&u!4p2?cLTWP?rizj9ylnL_W#;r6+1JyeLvnM<*kat{OQnsXXoVkRSqnRQu4<(?X zZbUe9NB1HAgOZ;|9SFMa92KwJI@-qAo`p#Hm7mALVNng=vDZ7>pa*Vg7(S znGQJ2$pDOwx2Uw^RaO!WlZV-}1NCvebl^Q=Fftu~e?SO9p~d}sEDEhVAc)!300$lu zrff$sLjRWr>pWkN&eGn4;;OM+rsXKf9~SG^P$2HdDO@j7R~V6CNAy8d_;AyXr2B!A z{=^^8nP#8Gj@WAP2d4m?+yfdGCWMZ100!8r0(}3?f7Y$a-i-V$z0(V}-?shvD5=uE zwc?*+s-u!7BMDANvUpqdr-kQwhXMG1wP?I(U-0F{ZN#uv00wY22x--AGIV44ZN!oi z2w%a$4LGoJQnZ+z1XNm8<4hX)sSpPSwl*?2{WrUWpRF{q8`J?*5u`rEa2r+$zsZ*Y z{v>yme*hg>Sethbke4yCu*d_TwG!#Q*hvGZ%}@nOY1J8U>eaPlbSV41CDr4ljEK-S zqt)>$$DJl@IzCbDTs7%%8o}(bX3>+m4=Qs&j;BuaVs(%$)*0M67O>XUna2ur-bZMG zATi7N_UIPI;B?wsf8A6adGtGkeeO$_2scyW-fowu0!TjR^NZ_W$Xza``pf}FU}wy( zfxqJGB2Zs_Hxpa}=fnacT?d3E&*PnV{}Q7quft>kEya`b!>NI4W1sVZ%691`w?kzD>Xp3P>6lef)!wy2xv$R2cYCcq+R$<;j$9sk#MwOKDD&=Cp zc;#-gs%5?b&{z$SFWJ4@hQ;+Kc~9G;8QX^H+y zDWx$w-SkS65U-QulmE`;)aAup)7Eg!4m}Z{ljXb4wY!U5`484Tx?f=`r3mSQ0>WLO zA=DVU*(@GwuQtKpKCq+$GCHrp7ZIHP^Dp}c8nHsXvdEdQLHYAv z%)9c^u+j5jX>L1na-^bU~^2!$mG}eo)Y4PLP2vJ6W{!83; zpDhYlfJ%Z|y{FsaBByD~qr``{{X4!;%uK*abvlCtD;lsLd1h;UfPNzly_Iwb_=BV| z37jQR82KO>M3CF%O9FXw^9VwrSHCAgIVcX8G;Lq|{T0o9H$c9_)9Ir^Uao28YvnpN zfBw8#?{%tiQfk?k3GB#i|gna?C7%{X{a#-7t51G6g4PL5fKjU6BDW zO9-ZKLd;@{ut%OX?m(fwcAjQ__JQA4-KZNzCd85G;5$#xw-k)v7k_PW!G}n(p@S<; zqTUJ4r-6L!*r#og{6iT$DS*l%-Fi@F(IdeGn8CBqlviMa{GyULZy@+s&RCpIDjBk9 z6k6i`m^jX}BU+{R@84e$dZcMYOPrHMxJ~42o;ZcZdqQG*9g}NqAwem^P&{f+Ag#(7f-=!3hVO;r!(XlqqpQCVf8(*{-sowU zB9avAss3>bi;itNO*3@=#NnXac8sM`jDD=rAtO`ngG?6SE)8KC8K4GqOTXO4!Mla% zn%+DayOGAkk~rioRYcqh=B6Jk7GBG{Ouf!+2xy+CiV12$o2H} zuq;XJuMB(u#=}1pecw3Z=E;!1s0z%&i6Ty7gU04)^0lBt7DrS*vdf%U^bWOkF z`TAJ_?o@FQMq}{JX<*@-YtMP%6YCcHl1oOdpWp#nybHt@h0n1ZeX$qrShzJNg}z)L z6&89&g*KOBy?)+}w{w2B+a(wg!OOvsD$xhn_T-`T$6sGxaWIPlN@Epv+I7SU-K*(J z+24|`{XBmo=bIPzS`8WOj{Gx9T-o%NO!rqE;p#L_3PL}+CZ^lBC1B#Ew8S~YTTsln zqrrMF50GiUf%EANRlXF$;!$juw5Iwn%W;hmZ5BRjQcKl3Xw=llw2dYW#dXtj5yfDn zhZycz1ZD&lmsJvzm*Ku5s#mafTMaxWTB60KPLsu$m>c!Pz-kBNEBS%z@CzB*Zgv0o z%M&eLCbskn2@-ccjwTgCTHm*^M(%RSB(GtV4=mDp-?JxSFDTG=TNgWlIHn$bYvG28 zaiJ#Ru&m@yX;3hVXqD9D#C_ zus6_2Ochs+(goaGFe=T(1_im*o#Ffzja6F`l=hZxucx8q^-b35tXqmNBssnvJ$y|X zYS4L9Pl4tk*n5Z#Zzi#)O7b<;w)$JAj7zYBYp5(dZYE@{7 z110Fb@xfk^+BnfC4fmAXm@HuH8USq)6?GSfnD*gIP2B;uJ+{T{tT^1~P(D7pLTwmP zTGsNezpaBed7rHAazsRl422aSHKKMkWpGyQ6%FVbsj=*Px(FMo7*4b~IhRT7T*WF0 zhUvyo2-2E=BPM8~*thr#i3DH-r{^5~wSfVXiIDzo{O7#?mxG@Ft&bAaL7^JFy_k(^ zU56=-52AHmM-?NA4sWdGpT7OmI}zMOJh1Bj5r??BV^;6wc2AJmV0T?5&@ujmW%KVD z(|`JZ$NvAVOGPw#8eoVP#<7kMo*pbJ;m*4YN>_nCw5>em?s4{SCYAO*faIbgL{5Ih z*?71`x~NF~iI4Ru;6PPirWjm7Kr4i#^Evy>$v>%A?^(skEaMhwr!wF6StgvC`3k>s z@*7Vs+2UAIH2KVK;r zo(Ui$R{+NInBQCO<9hN9N4M%lmV6>8mYo6EYIXeWElFPU3BY~;k1=!Ejy^aB&~Bg_ zXaL&M#9z(T9Zsq@6@cK}o2^9UTnh-rb^xrOZcR5`3FxC=+X8*Z@fH^KXsP9glSx96 zqeeHOb%WA&8&DUC1o*LRGhQRgG+!jBuL^ zN5JW4pUaER(|p*BaorLV{{hNJ z^QW5)+NYOWUkSM$H)A<_7GmR#%~E`;x`NmPn4p6p3X~SBl0FFjSHrlfCS4q?4yg!F z16@Tl0OM`jv#pvYvhQA{r>D=2mRYY{rf2wz**9+kS&cQbLFK2sI28ZcUZ09unU&_q zXVaiOg9Ki?BD}O8QOqBl)<-p=*J4zgFexc{pBL1#yKIbG`Dhs7yy1%l8A0A44vS|& zgU4af#+oUx+5?RgYE}UOJiAO<63~1=vWi_Hv+7kS=!p^5X@l_OdjZQmi_x ze|o;ynH+gb@*0c;Cjk-+OhF5%hH~188gC>Df!DML80H^77rS&3J2XH^!eJwF;_MA-b}dxuaNZ`P{|GlWpHQM7i1U6 zF7qCU0dykZa8FEUs@`kB8}eJW?L)1{?Z2`}z%>M8P;x&DwQ5V}x=I+VNa>N{IZ{NW zGf~Xy?ZBXm(jbMiKro?e3)-}3NDJs8&~Qv?85x1TDYr!$&3Fh-!UN0n@L15jqnZ0y z*BbZsyZY0O8WvPKb~DLeeqayPUFB#V{n|5aAei$vVE6M%K~oaT8;MEPB0Y-RIY98D zu^uvsg~L_{lII*iYv?%>iJ2a(jaa!&H~PWzP*t%2719)gh5^9!!GZmJEXNEevl&ri zGw+B|I`@hKEB+~4CEC?HWKUH&%3Ag3piNXgT2A+t8SLuzWAy0y zsvtO9Htx4ob~fJr#TV3wL91ENK-`quevNGy0|x&An%m5dwq~ph!J=pc;#nJZFFi4+ zv`1TgPZI=Xz2BBM=38 zW%AKzEx+3;>thw#X6R`C7B5v?Aj{G(3sChYw`w}`2c*EkR}QS#gb*qm9H6XS^qUEk zEz1^e<0a*FQ_GI<2Wr@8ppwAP7sna(bucg$3X0px6OM2#-#UYCl$m39sRGQ0uE0H{ zdwtzORGd>BN{LZ9wM{K5pE5T=@)vT_dW~@|CCO&OfPCypCGklzwu}jn*yIKc%l%TD z9@tw>$58esqB!EXv%@*4?z*rxMIwKO>Mk^>t_cQFJ+zxF2oK^^&@fp#_;x5kp4wW8 z44@4Gxu$^r;KpCG?Nxd3?WsJDSsKi9js$9u_ydOSl};hMr;{d2slUIrmwHqhmU88R-6Zd_ilronXoG8Ss4o1B` z^5DL;q(?$GYmf)FBdXg2qSI==cVqfE021N)vOE7H&GkcaUlI&@i$mHCb-b~ei+-k} z7N`58UWH02Vc<^xu?qgEDE8g;4$~32nFKo@mZXk;ZMVCBGi83!|L@rUx^;O4gMsp~ z_9Y81x%csW>>bJ1cxoUl^a=P-0m$pG`5H8wYMR9X)WM8O^R=vNSnuGni4lBm&mq)V^ElN2${eTe2)%l#BBR{oAH*awE?cQGzOtabLM1Q5k;V9pY zW)MQ!fz1zGdKYUKr|fM`8P^~EzCN2BC4NMAaMf$L{NjAIxZx>CxqCrBW^_Fer(WSl zCl4T61RAJp-y4VF7e=EEJ`MeVg8`L&`m4bDoN)E8qy_XX9|0~gOD2qR2gqmhORp$D z&_|dB`(anzlH(J(TpsT*$xlB4%=X82tyWEX_wGvnj zbkPoFS$`qoAZ~=1McB7~ZeKJO9lg`_~3=rU1fun!n9bR}lLJl}O^{)gt@Fjr- zl#=T*5Cr7KE(3z5MU!?y0rC^TTm^9Qy|L53Zz+gKD`}GAcWGvnQk40Y`3)FQ2binr zIOE*@S3=Q8fHpB)z;#u7UqeEl4=U>LO05E&TL4t$JCLvhlsd0!@gxPDBALF=8#RBL zisLlZ*z8;Ql^B-)isdgO8b?F)K!gJFo#8vbkXTYSL2fulRLUgFS5cP9Y{l%Jq}?81 zk^xz1t+84RsINTU+tg^*C?4-KSD;*uaNJN<+*vScuHyzWkA0#0hSeIsG;zxR=G2=c zX9kce)uqIx-KIqMi@Bdq)f>u$l0Q}q|BN@w5X#$?z@uP@UAtouK-6y3{InmS-wuz> zI&%XvHP4MZjV<8SBd8UV1$c7mKndb1rRPOYqdCyI08qEg>5#4zbN_b1MveR~jkQVi9^X6F|8ra}=Rf?_YV8j`IeeC| zQz&6%PweGc?0hdi>3UA8?rva!oYDu4f|-$X15%N~)_Q&J`8BAEf*twaWLY|RtJAX4 z=mx_~v4?GOB7ux1vHrd&TJ6iDCPX|3 zQvNpULe&2C(I298U0k-N(!7O+kDk*~Sji8q$x&QuC+rN5`Fzg6n5TB1qPH%1Tv(*J z6n|ZG*2qHZ2MP?x+PqA4lhOyj5Mz^!$bQ$sL06gV^nV6my6=WzwF5 zx-he$orcpuyIJg%Xwq}N#jwjtUdfxvHb_vFMEPuwREBBRf(mjx*MrfhqKdm_#Td63=Oy2LCN0d*(ne&suDHO>@wan{V2tIL1L;PF6+ z?W+mJVuOn_nWb)V&Vx5Ui^UczYU-gvC+IGlP1b>qMU;t&0hu;7CH%pqn_c$3?2HF9-tJbN3|rJa>q_M7a# z=3vk|H?(Hqy|OKZ_mqW|Jn#Ed$>K8m&}lq$kmq$As=UQ{l1~SfcDQgjntLmU^ZfQ4 zP%S8}dV!v3AUVJ>&oJw&@p4mlSG?FbVI0UiG(Y?uIB~8C8>@1&jKC;q2)#+7r_iz& zM;*&m8VTj@dp)F8cgssqvAn=y`iwFLm>sD@x%Nx`~ zT&3xp?_Ze2%I7)rIiQppV7{pBu8Qou={lev=&L>&l?PSOok_pm+ULZ}-KCl1A_C*3 zA|ii3CfW4stdKTjVg)Cl#mR@fP#4R<$pt$vzvg+E=ZnkI#pB&}2CNeV?X0PzBtr`@oO|6bQYH)$fv~<`d8UsF;E`(t^Ilm99NrE!q<) z)5wv1+UEy0wVh;R%ii#s62)kfE9=v*%@0`~p`$BJb!CrNGvc$?o7-A>4d9s%1~s4Tu;@JY~4ms~ZaII&s8Y zxuu`D*+0DaQpb}4Tf$#+)qnkxYvIFBsv$A_d?(xE@TAE}ZTP$#10D6t%Ts*x!guwA zoT-ehf&8is8*F6vS>G>at4iFo(&ygR{4>7Up_U)oKQ0QiXM1tQI3S30scf z(p!m3I~bUsu06fbdOh^bLGI4BLf7#e4+lB4E78;qJeKEmIM|ReQn;zni>arn)KmQ( zrAkN&5^GLk5>dFB!6M-pF8qP@r<6S)C~%Bl8wj9%1w}W$01Yf(wk27QX25&zDPsth zLu1(0@Y!wLi+s4A2o&M`E=Bhu?@uH~vS`BFQhD<&y$q7>C7AqVt0W4qIW|d8t%6l`6xv;yf=W)bvzqwkuM@q{iczULjF}N zuB*h%!2UhS zZ+yl|LT^|Rg{EAm*B--qV7}~eHW}JV!?y3UWf!#}_eOE(Z+vn^W~FYdh7hT_b!6ze6TOCF#}EX9au& zi!Kd9P}87@{T#IBm~R@5=+=ciSL6l#hHlE1;vqsloQwGU7-YJ6hm)}~ATN(;BM#>AACz8Pr4JD$cwXSx=WLpbi8Tmic zz(-cTH2IW(ay?zLS0B;)el$f@H!LMO34gw3`OkA)j$eGwQr@rK`5M-i z{h};VRD@gaeD{uNwcx)Cu(r%#)zQXQ2f7^o_>%tUz5!puiCs_&S6RX~9rDyA&qDq` z7g*Ts5gUKSmsIA7rAmpWvw<k$t( zx*U(%2veCjmU)?4WhQ?(D5yO7?#rP!N7iWvXPuXcw2$kP0K7G|wy2Fxhd_T^$Jma& z!@-2!x-$0Ej#)a-Tc_;f+~C~5ma`TA&6VN`?2iKkeA2K&R;J5UJAC5EfWV--*uQNi zKlMlZx~y48533cGmwivn=UfIa>@tT~Lmx%My>y%hWpC z_Miox8kP11hS=m(Rm&M}7RfpM;|cp|&w^GQM#OZ|30`VWJLUYV#*iIJil4?`B%`re zjH7tVvFXUO{bWRzeL5>iU!1d#*koSq{Un*?*jR zDDxkFbuke&VD)U-Rvio(T-RC*+WfXJRW(yjF614vTTK<_XjtpeP~CYH>Pe43AHFG5 zWlC`zhVQ$E0don*k8PS@N$i~%QAftrd45pTEtN48jhcU(Xq?xez)OefA}xQrT=lKY zsC+(+WO@q^di0q+<+BTmmbQ<>ulEO&Cv_!<?(PAY3k={D@?)9UR~|BHB~RG z99*3irwl;UN;UX3 z{Y+|V1B3IYHA@W4mPIT|H42Cedv_7{SYEaK(ul^Q-O{k^?Wi14&@}0Y;tATL;*mZt z97(HkS{9XP*Ur!F%y0{$?fra+B6UK=&j`JkW2QfsRf?`CSyXFcE-_Ni)%%i?r-MTC zSdQ_u$mgi8D)(mV`@DTVE)Tn(nbnn~Nm;s<3LL#m%M~? zV?QxBn8NaVhlU2J&FoEfPonDu3jLZfl1p@2_V**N} zk3Ncg?cY|_olR+U`7ILne`paO3+E^FPJoVWUk~Wv=_ffy4c`_=OpDYCtHf$MhvY5K>6FGg zKijQPgvUpJOn8@kygZMF?(B$+7(1#o%AHA8*)YwWrldAmAzQ!%vrD(rGq7JcX1Q=} zCEl{HEVcjn_{rmAL6JjpAsItK1KuRrrYBj+U3WGx$1{d$IpBQX9baz_`l7fu6d+b* zdi!R*XVEJnO)v|fYZ6i}2rRVNLL z$qw?DCwA7L^gja=(3JLV_ZqF)%nzzjXqdS%k&}fN&(T4A)|Y!zs^~0G)1)^%wnmJ2 zpA6LY+&s!cgRcogbUO7v#hoMy7|J+PYanwN?P=FN2-H?7%U5zR#<$vbHN<+u@o~f( zC15#SWGo72(k5SzP54b?Dw0>B{6``5RqMl=v0nQGSO`37K}ndgS41E!{8{mEL2u~` ziAEU(T8_X$QkG{?R4k7*I*02u0`u~CBG#eQl;pP>d@=8tik>Bx*<`RRI(&)AId>?q zsoKf=AML$mSe0GZHo7Q58d-D+s5C5)P`W`H=`K;ayGuY)Bt(=FSaeE*bSQ{)NOyNP z-$Z@x=Y8Je`}6K&AA5g4K7KB)YhE+Q9AnJ$Jm(cKHLK@4y#|??vOCy_yaC9^qXSB% zv~SJ2{fH)c{O~dn9_=QtdP6wPF4|v!oUf0CyLhn$+zG62bX;A$(|$5=u$vC)MH{f| zdBv<%Jc~U_F-kg$b0ROu4GR%DKmbvTtRPYdkT>*=bY??U@$LK`$=xqms{J}&atJ0jCn+( z{n`FaF7GWQg7s_?h)7JpkEpWrtv0WvUkm|Juu{YnDWly#fsqkS^-hkQ617n^MDm2A zmbM?s4-({kq|{gDXj`FMWv;`e>SwAdxGE{WltFCgxLdS@T5?z&~A9zdnZ7IfCpiZOOcL#w$!a zAP99BWkeRoC(}d-Qi+FNzEU81r2TV_PuKR4 zKaEbRj&Apd<%+*`AJXd-1Oe!*k1*TpzIBM+euf9T%zKS)KW$xKVFZsm71j_wH@Gs@ z+6fnMv3~mo`o1leN`H)K5}q9UG$k3-kc0qL8>-o%d0)F|kf>Ni;BJmpABT9=12d|L zwC5rkhE_^`jjPL&_eGWhs3U=RRH3$2NYhKJIoeQaX(O!-7YSJz%jETR$eHNUYgSEF z_49>Q3VQm!2uBgz+c_4|-xJ%hr&eyK+Wk!g?d0+@V%&cTi3- z`XH*G>Cj3j@u3iUygqwH;#AqmVhG;WX$}=^Go;Bj2=S1=e0@^NU&s@^{(;M4~7&yUuo_1_!P4$Lv^;2u=p&Bbn^_9AB3NVDSwr!JrMH__W^Ssm&y<8&;?kGyDsb4BQ z75PGf%Qqrlk)~nOH#LCumh2*guy1F@c`srm@@Tx|wYrd*<10H`^RGImftm`4noX^)&=XTI)1x`_?2M^YC?MD;s^CsA z#k%+hVAKKcO(bQ84Y+qoJku{B_w&BzA0;um!E7>s1N(bYU%T>Mht&OctbeWUBSYJU z%KCvbER<0ZYMJ)cc!E+2#2msWIC{_V$B zVn9Dlk4V9>!@0BS04>fFAAK@R3HPUsa&hRTy=^eVKS9N9$%{{XJSP7mP^q&o4A}4O zjJ}q4`x|_KmMnbq{Qg7PS^hs9e6nF?GnsGz^#w5wS_9Z@PouL=d$T&6Tz|(Ee?l1` zh4)bj*J}a=9lLmk+Oczl=Ra!o2zojLqOA#5B|AH7o)1C|{{I;dq&MB%#y_A6SC+M1 zpPxm7RK3;`Zpd)?p(F1Jjgqw>9l3#$N1K2zM zx0Q2Y-k|S>^F017mJJG$tuwZv z_GP<}fK%PT+6tzmuEDTrKykRuse!7@VWaHT(8(m^i_(W`-NrZRvj(<8_Ig(uZIpqX z(pw#c!$Ru0VzuWkR&t=xo>C({<9z^F4SIgLz7_#<)+ixh4)gDtlt?O(m z73=unRoq{a>u9NWy1VjeW!vW?y5QAZ@Ars3KZo0RmQObwax+bEbC0{$o+(!|BSAGh zG;YND&50OQ8d$udOXqoWSJ0|&|CDC zY^Q0#oTOva1)M%|(Hn3*4~zGZs&%%e-blcTWh?4oU*gjJYGg#F#nV^;rr;JP%DLxf z-Qk?Zu=w0V_hId$sPfIpS3m0GbuhN@-3K&7R1ok$+F;9E_zOPtQ}t0isfpFEnyR=$ zRe1|>_4t0;+HY%LW0!VN59Hj#iv?I(PsbHNlkO}rX(WVxli_d?M2D_6VH8Kc_0h~xTwVO0*r7TH~d{B#(l6>V|2)S+66XSyX+_YQMam<-E9L>ry zOq(a&xh?SGa`R!?%adnM9Z&dFByiDedA**2*`m14W1Eer1OOwZ1BnU_MI(5sI!%Mz0DVQ?VuH72 zZPG}8QO5%@Ic`qibbMXqz)9_<6d*qdQcLNseBOSh>k^kA(vkR62jokB=TbW#rd@ga zCg@J{DXI6EKSTmbysM$_oZ!LGum)vdKe+GKOO8{wzNv{)Om$WrF|~9 zl#TJjFDCB%(TH_!8-`&ERd67OK2Ye0r1iDry_b*u=QZ-cx#~D0uR^`Rc*pT?ew6*s zAAc?vmr#F88l#q}5O_Y2wIkfgqUUwfmdAy(lPK5|Bl-VnH1}dMA_hr3)}=J~oV^AFF{`GR`$tU!5UGIW2we z6q#^d(Vc6#TcXRv+dV$T73yFWXl*GG@3^U>f9{m~@ZeE}>dnAOez$%8=AIaTMgCIE zPO;8Gtycf(98KIu_9>l>)b8)Jq|auGxUxjH%5Zk>c0OMF?nsj5$!f27xcz*>Ua|Zg zEtzNMSxV?1dI;{%N*Uq73%~(<*3(t9bbxVFDay9FUEGlwIicOG&fIF~x&Kys;ET1^ z*#@lweMI;ke&=bfiHQgOA68S#FLg{am0-!v(;}C~D1yar!Ibbr!-+6cV&)sL(Un`P z>Vks_Gi@oM@zYn*r`ggShe|*&<9AN9kgyLNk1)zx5^-d;HcllMb!7YA*wvcr!K^Dd zFcTW4y*-MtVnX4ROAISWRB33K_xZFB$a^o8 zM!Ue*$7R6RO(sd3j_2Osf$?5R0b3fm?nF-!ln%74!c-sB{z95EA* zqO__rv>Lhr;@0i)1lQA{>Zqu?SJ6e;(}ys4NZD_S-+kT4uzLH#Syav+O2x|5A?U{) z&fE1qB4k|D{fU%jI$4-V)<1U4fQtZ5M254ab9B|HE(bYQW{eE$Wc7SKaJ5 z6!rumSedu^q)!QoB*xYB_Yk2BGUnS7)_Q+Ns|j$?RUUE|W-P4y&alClK*sZSjcVL@ zS>~0SYfi>dO5dIcjdQUY$(*#MrjRi?=dEyMp$@3iw+f$6{`9dBo$Guh`EjIw?_|iR z-@!wNO68vsA3n{h!Iqaz8bC5FFUx=5i&t#G_=%@TM5(SMP%qg3@WN3iH~pU9TYHKv zvIVJBi2lEaJb!Q9U^HPT5xiQjO*}(o(VQrF{R?|KhNlQ4==SCPK1wxH=@Uk{(|A=1 zI8nRu$o}_;TSQ6p{}o}RRN=PpKU z`A+1h1KU>V&y7ld*q`~9&(1noC58eH79tHEv{fG}@_3)<^XW$(yeu>JqA1|N45Io( zQ5fv{a~rLAd(+TEt`V)K%=RtAKQqqa@_e$1HC}G*nmb?>@H^$#_VxfhHV?8a+qc`+ z%qZqd_IP$LiT;rz>1hA;L5?8TkgW^x;H~ob@4=I-x+qCelV7{$oNnIk?+^l5_dB$@p2>C++qyk8@b`N zq`%dLa(Hzn{61YM^4UiuUJdjQmt*~qA39PY|8>#?J(9EHfM0a4QtzG=R~Q+S2jlJ5 zX4oeSk*|WoSoY0<`y7o@H=Y5~`%pkDm$@=%4JxSlwFK3^k>L3pgmjBjj8Rfq| zfb)Xny!%v5`fxUAqQI4j^gyL$OGy6S@58%l(%A32U-CMf49E(VkSXe&4Ru%87Buc4 zu^TZdnXa-?{pu6Ds#|Acje1;|fnm>aDS$?;!4c+(Ml)AdsBq+-t#`BV?lq*myv zXZQ`{*x5zn2}&O3)tp2uDT5pOkP^m~Sq2Zwq>pIQZ-QmZFzTpF?AZcY;()ig)6&9w z3@}w-K6bARm%kf1ryQbHW4oQjn&y&*NgrA?=|f3e@s$7UJ1yU1OJt2 z!Iu=-VZcDHW3lEU`)ZV8V~69g!d`w?F*9IKOz|)y(x9`g_yy^|2Y0%(AcOhi^pD$j z8>!|S?~@K9nO`y*fD=pb@n18htFkkKyt+MGXl=R}<9x2RdXLO*I%zfk8lx>q#=5ediLsZ#&6*)b#4n;Zp^tQw z6AH9M8D%l3J>^|!py^iBvS_*DFV3D_hW)$|lv>%U-LXu#n08%GaZlY@Iq+I;IGH~A z;d#80;<)|QxOpf_{~}rG{$SrTYu!m5&PePSOO}F8`V%y^(H7atf{Vd2Gjv^4d65!} z?-4dM*I$eDFRq5$#qgJC9g0&w%{w!51+@puugBk&HX+O5bb3D6S$E# z`+0Fx%zR7pEQbvzN>L#EzGj7+$E(s!q!lXl!5THwQ7q+!{!%?{jpOm1V#N4h1vL2P zYlUH?hOu`J3IgLA`0ugh_s3HcBvfX3&Cq{XOjUo*xu^YvUbR3;LAlJ#rugbQM?7Zo zv+#6*{>4<9m!c^_R+zwA#S9e+4OX&}+ZiH_PJ$FgG#i2D($Brb9M$x+Q6Gxo+Go`o z7wfXWtJG9b*+YX&9fW&hHu1=uR`t)1%t?r%XnRyih}P*0uyD8^uIOb5Tv!Lx;WNX1 z@N7CmA7K*W7c+D;>Na09^zzs6rvE3eWXp@Fs>a=FdN}h?2W6W7DXE}MuDR5tvkibm z+W?|c0sw8>f&L!jNoQwgI6x$Yfq#YpjEk%;5cR(me!Au20Wz34Ilr5d)p+@{+lvJ7 zM*y(B9bjFSjsb+E?fUAX9Y9*!fP}?T0{|n31C_#1fVT;!6!7?VQ>Y0bFn}RccS!{# z2n$~cQ3q8c{AoNByUOnJN82z!KfDF_g#m=>H*df}#0R1%MBttUJZfs{vd4{fa{-)p z0lb_L;K6YL0}2Y1ga`+-6@j}5cwD7UYtL)+0D4jxz?<1^CZCkrFG$*NPd>FD)pPw0 z8!6IbGB!3A|MIeDu?EP~;g*(`a_D|)bluTcObo)Ezw=MOAn>~ArZY91EtACn!GHU! zbLTUX48+xM&49SZ^77}WwWn3W{)qcty8ufS9zy9M{YlVU+eT>r8?8gRu>TeyB8J{V zrF=TjM7tAMmRy{i%89wH5?6FZ(d@}MQN~484AqU{5Q+-`t`bOPABx5n0AOp%TdpCc z=l*xPcEEOB<6prQ4b@5eGkxxHpsbDokg-qiFrOLF8DUXD3Ui`3%1%(mWIpFg>FDfnHY_;1{_GzGqPB_;L-ouS@){qy*M02wH*h zw-uuBS@ZHhL|Ezz_U^}+-_@}%RMU<0GMm& z@eJ|i07m>ra~Kd{318@p?zW!%Y6p0-Z-4@&*ydQN*d<`D4cw}cuO{ST{>SGG0SVP1 zCiMeAlXpu5CvpR6#!vjHhUtdl;z8ugQY%q)P#RiV382Lyp{x68vu~IanUKY1qOu2n z`PY|M5}@1oQyVX~`T8rZb!sBq#Lg&YS z*&b<rVCNe+^SWT=RD~ zK9U4LwVOneD1qk=?!o_Mz@Fdv|L|j8j)3CIZZ|8uVKecB#FnlVof9x>OSktilIM#s zYBSgB-4EJ;g)6ADUihc_dUDMG#&|nH#9zF~9ILecc)C?JBc88bC=TR~+5ug7=_w#S zL)pF_zB3!jiOEqfOtKOO#89>)0L=`$C22ctjL4VU&gkEQ7z?z%Nc!L;-vA!W;xU-) zZ2&{L=nl}K1h=&F3jmX2xdoJ__+Gp3-I7JTzc<^|dmHf-07T{+U{AZ<>QAOU=K+qg zY!EE&%GI&bj8nkHS#k&HHzEpNCtM>VBV$0@0YHC3j;Y4;btd&v@ytxJ1=ejG+E796^| zYSs^U-u8R*67~QV+7D#M+{v8_K~v@dteyr4n9>05et~Yy^N~@&?)_F40oyd>nXU@_ zGvNl>p>Y6U%Z)h;_My*!%Q6qJtqG}hPHQyk#riCKg>T>Dx|35p2QbW~J#cHqo2qFK zS(gAD{#`#SCxC=}5Jt$t7|jEe%`O3l#^C}G#qj{I^<4}17!XfeiOFGWye#7+``-Si zhgVWO6__L&6@Aas zttoo~uHSJ3s&M>kK%U8I4{TRmTeo!iM&HZ5kXt)swb-B2s{5qd8!QVls}fh4!OuJ2WRwsLNxP%*E#Q&IKR~3I_)l1f4-hS0Bgw! zxE-G|g+MU!VYjkiTL2jOhOOaLl#)%oAn_9Gd%HqCe!xGy2c=hq3G@Gau|Z#;A0H|8C-s&2LyF{ z$FHYKfbm!)!9zg0gNaM!eHb$tJeLQqf0>k|#Q1mcZ}rtxN^EBY&0%kYSIIX8U8nzc!cN0Q5iy{?tQ-JTG+psA0W6%}5{6JTZN+wIX9o*>2hvv&SAm=J&H<*wTivMcWQSuc8 zPN*tYEH?&tXfLGZ*jyGAY@(An+YjHme?@~I#Q7tCbB#c5g623wmOWl_qa+GK5`VUq zQ%LT^+98G`@eEDursztFz&Lo;SQGK>DWLtaqq0OQe);#DEUIg-fDXjcr-!731$4rl z&S0b-d+I>OIu(+PlOT^EjyG)ey8)9S=Rr|ql8__l1F%4W6hs19 zqP(0eM2^eI8@DT*F)$Ia4LSmRg%?_qzr=pEYoqXuy>S)&yZzNBBKqau;Uu-V#-->1 zyV6oJB19}Q5d(EBgbCYjsf*xg@0*_(Sh#SCA^O01;4nI1Q>;*v?}9n|9S_M555;O{ zrg7%7;=zJqec>rS;Q(?lx;bKBb?f4+jN%?(<8?PCvZ>y2HF8cCOA=Qd4#wqWaELN-J&SDrS;&krbIJ3;Hwa*tFXueNMUG z2xmwxriQJY0Jv0( zxhuB@#TNCCACMKxuuEi${v7G|KIh`cpd`th^=zwpCJM04?`Cv;&)1wZ^|xl;40??U(tCuJ<)43dN;((GJzywiW3vJy~a%l~{UmdS@hdRgu86*?xq zm)tYnn{UspbeVO@RGkcb6DO_0%nYt3D|B|voBwCeq>x9Tv<$05-BlGUFz&qSs@B5| zPbf*mRs7NY;c<4o-ZKF%`Sk7XPJ#aFj}Rln*N<&tS_{LgS@4+B5!53VDkb?%u}*$n z1mf?hKfxoUga+0@>?Vkoo$TK?0sZa8$9Hyx=?d=O-V($g<kQJY>H?dY#?)1kcrhNg;C$XsBl?DL^nEb|G}3c44KQ z`G=SQEkNlJHfJ5Su3L_RfYB^N79bT?GRIbL;hJJZ6KlA3=kW5HbYPzSqWTnCMe=vO zN~!p*94?6Ptq$3T<8ObI$_~QDQY33*k#&RNQI;UZrft`uuHW^nuBrusDrFfhPhog~ z;}z3Oue7p&z<5ekjcPEZ|B{GqzR-@Q0Qdx5b!fOe{Lv7_cddvmfGFCHDwh7Z(-C#I zgm#iLI2$+5m3G!0rCI=rB{YDJv2Ms{Y+_=%67AV<{~omndruCLb5>z0VMToMd*PFq z+iQZ}MKg4;6u_3;oY?otqUkyXoi<@>nn7<4`u33Gs1t48f;-msI>>CLk)lwV-O3)x zAvFNz>=P2oF3R-^kE#HnCwR7x{~FpNxUinytrESU9U2y@x)+btn9{aJvQTw0gD`3i zy=u5;yAa2c}6n@+=p(r>G~&0BK* zPcah_kDfoMUx469ik`IwmhMVhACY7F+R;qE{2L7G$@`P~w8b;0#75<#N(+p&y=Q0= zSO7s#>;w#@I(yOsTL}(Hi)}p^-b1{9`yx7BO8^6G=21v4$OEhk;J#Gx>(KnV15p%G zg&Veo;-$b0(TDIiekhi;4pYk^PA9Ig{jH`&k&xRj0!sJ7F8tu@9ynxT{mpy&kS=Iq zD_HAnt1U=jLm1_DOq(H8KJ-%eFWcX^ef%4~JMo|ppLZT2EHjQ4>a6^VmV%nBQYor1 z>D88hN)^uzA|mt8Oho))JMaAC)K@lvd5^s6&x%MGhNj_n;<5}NYq&s)(>l0QWI{p? z46W}B%*9AiJ}YAoPHC!6$5+zuxt{$y6PC}BkRmncGnmu$=h3N_?sVYV4z<}waL<`% zx`l{5mq!@Erk0Cil2dG6yj^zOe*>2_Gjv#pwa)`a${KJ*;ESh|#mj~4U;SP?CgOA= zJuivGMn{C8$ME@A!Y|Dh=fB?v(*oiO|?5Fcze^WsTK8#uxt(Q6EH1Zh@A zTR4*FLpo1O=?RVqR23t@K3*;oIyXE{-L=U>z(W6JUb zJl*!E(HtE5$C7fQFa*Z#xo|K~p)FUVh8fDMWG~47D@@D8{B7i%XcHGG=>xdrlcRY4 z70$io_#H9~L>fkUrhy2``Cn$0P+p{K_W#w7#Sg&KfTHOBUH@Eaer%Fnm8xyUyVTPP zAgn@r`XOrZQLbLfZV7DTI_~YD>6<^^o{YK5+oV|pMmJd+{h-Y0AnA7@9%xwlX_?cG z!l8P*hP3<(bL8ACOV*~o3^oD4Ln1;AR;l9%QO`bb@lKlKWggYPZw^8jzA=j@#qq#<~ z{sxqZ^8Sb_cN3pC4Iu`1KHJveGLM8-D(L}h6v zUm7M~KfuDgQ259#Ps)7sj!bkj;5}tJb5A0BtH-zaf1a*tX%D@21i{4~kbVlg^@%~s zM+XQ09Wzidp#@}h=9!$;f0=|N#2GvG&o{I2-PJ_o`Q62ObuR_vq~H1XM}&vt-)5BF zvl&!Ux}f|kmKSNcecTG%;|P>5q(NNJSN)rlvzU6Ks(P30 z*~8Bq4>3YJDZQl^dq3)l2gUF5Dm*Me6GJ&zf_{kPWCk}VjoOFFU^!&Db}7$aqVZ7e3|*|4zQxi}zzdkz=W@5OMedi}YJVmSZyX z055)i$0+yEPzFH-W5gtv7U5E8WS74PuFhYTpL>dDT%0gn*s!PoojX5^$RP9YdhEDw_QM!LAlMoK4*I}>9~To zQ7YVhn(A0+&Gk$zpc9&jH((JgK^Ati!uCw?AtnoK=zCvsf=SSW_{ue#gbc)ls|Rk< z`QCC#4_RJ~=>^wj*8QT7uOz2^M*C<=-cfDJ!|&~b{gU+W!NFv;y$%tX-UAL)v5)Qs zV|v1-dhVDfrIRO^Jh0zpADZa15<6q*M0>`#gyn6O({KcNHi#@n6$?j$V+KsB_=yqN}i<_P#WEzWcU-tEPC13 z_e{)$rBix7g4?O9k8T7h=s`^0noZJv>R>50TrHp`y8oSFZ;3$gPk5ak-1-{0zu96n%ujG|Sxq+5Ei zXd0tEe$?vw^*Aez()04}kKPLj@yJ>somP2MiS!1XNFxYd4E}P6+;6~l%FChX+)eT z$Ka%cED7;Fn9B`j=$}9uhRqCpGaTN;!->L*_q(JgAgDNp)(~aNr3}Pq7tdi z8K8Dd#PSH3t|2QI9$Si0G#f)8)V_!vy&f7`MrwR;{t*mSMiztc3h#d8N!#HWQ(ITz zh3RtRO@H?O82m~o+?BB4P&cd7cI@HE_7j?yUiIpmPh6T0uC!#iLlwEVkG(pMrBbcm zM=F0EBQ)L|`nB&E{a&9yr;%^rh^+4y+m&&&W{F3*mD`YyYT1Xq4e?@x)+`3i@?$1t z)`GB8w=3*o*bkX!Dt??B=PklDk%!GY23Nz>XYSW|Yyx{C?b-)NuEryD8%wT3Cow5I zEZSQ&9tM@}sXqd7=7sRXN4;6{h6E zQpCX_=DHa+Vjo7y8KgaUm51MXlv($xd*3{$&bYgA34g*d!TK;U;LQ#j zR2eI^VTs&8CL9jY*sJ~~;*-qc}kx$F?RNowjEWJ8h6TGe?t$2o>RuhYTOdFRy5)zs@$+(QDT=_E9xyU_k zb?MWF+JQ5pV&5jAxg(NcLt~f0+cNWEL+86< zD%xk+ZQt}UaxK197muXxFPgyE@1mG#btUEYl{}=_PcoY>$|n}IuPwk-TxPk?Cj;HQ zXWG?`_!|8+`appm5xZdDRpqfM;dej76$p| zw9#p3dv_fE4vYQ*0nwM|EzsGM3g4pW0@&)?Qa0C=%g>IJvSkhpyZdM#?yxIX5#moL zvoSgQ)3gh$y>T=Q$dhXS#^jdqIKe#NP)!p3LX1)Up2`YqJOTMypi^)&#|`18m=7j7 z*E4a|V%n364N|(=BOk467BX!u^>Yq_maOLI077@W{J{AY)_H!hB7yvb)1lKod(U|2 zqe8yW?~jSPe+qHr`Dg3Tx|I%?nl{XbmFuKxya-XQP#5_1D`T>0G9wVi`vi~2uDT$Q z{{5*C3}aOq_d4|1&NPRdfbH{JJ>m@pzl-i`wqWp`2cr+3Ke9 z3?BB6hBMzLXt~vlUS)27q)wN2Aum6j(8Aaxfq^uwe++3{Aa}B7m*d;c>eT>uG56-1 zCKTxSc)x8=oHcZu;!^RFkhn=hQZb=(*Snmmn|mwX6t2BxcBT^^j~;Uc5zL@ywzo_I zhUrw2NySeR4TVet--1f6d1L2C>_o<+Mm7vQUJI!JLkI-t$un^g)vxs^=-r6X9iq;D ztDokWyF)bMQ|&7Pexg#4MY*HPX1q*cdWALtqoj8Nr64fkRrV978wHUFBfa)sH8Vn= zwX%;ljtXJR3StmOijXmC*@m3_F3O67i>9&0<9_jS^z752akgZFro4c?>k=p?;;6+JtOKmV9Vw>w8})L%|r_FiAY8LL`s7PeQ@DUuqb0&tVbA+VCdb=T<%miC!i=>twkI1?VGmWbI~kkV+6 zUg(16oLM|k;S{8AgW;#N>_;>B<=af|YiZ>Szm2~q=Z}u6L2I@+TW^F}g45V)4TrQG zLU_G1$pkaz^G$Lx8MofPgrN{WY%bj!TT@Kd0 z3x|lcj!8;%s6mR%cC`vyn<|&#vp!-sC%xR)^IvEJ0d?!aOZ5Ag0t@Ik$#;2?Kyngy zmROFiXjAoR3juvsytZN>FTK3~9)(xONy$s(r8^l2>%R0VehW{Cu!Mr&^)!klA!hg( zlffa**c5odA4_peqv3{?a(q^;&@{2v3`)+=KOmN8bs37}>=3J|o`ot4EmE^$4@ulM zvNkk;p{ZuM5_yY$96e&X$qN*$tae~4?AceVf}>bH0F6j`P-XPeP};8lwF}CH;gdK9 zVV!Vqt_`kt7&7MQn;FRrN}`?qc#tm)!gFGDA(GEn7y}rDMa2A7#dOT1VGp9Ns?V#E z0xI=9pDp+!eqHJ-+_-CJ8bQsS7$C)AN+{N;7@sW2NW#la{F+_m<@b)>NVxE`59D7M z+!}6hf8jPw-cNy{)Eq3vvQn%C-XR=h$Z^Zb9Lt8|fQnGgGEzC#s?(yRr%NtitN8td zNGfCr93PSKN^)-rIlnw*cUWoQE1lfIK z(Tr%0Al_i_L>aLVEa&y3&yykj)N*IdlO}QBEzp%Z`Vg9(60^7NJ6SEjf5yP?OKoSr zp!4^Lio!Y=A?B6*(7wwl-)!|u1YFSRl+k)1raVFy{*U_&f!xq#eABxV-wcRx z79YZc_Q+yRsve`DE&SU4YO94|N+9oVTh4_dW09VIj+p`CFD~EEmY_SR@xm-#E%gsb zX>_?39+U63aOnsbHK7@91r!hGYl^=Xjq1R~Cb2~M;TR9QN1sNF-3cQkEpHLrHw^>G_kcikbDDJtrw@ykO2yV&~(KH(i!j2Dk$hSeTfQALRQI zdI&TIxU4NI;i*H$G?q_Z)(k->U#&Wd6<;5?C#FXR50Hg%mQN_13U%s=Imr3n+wb}$ zWmd0@s#YUK7x0s0!)7QH;ir^-zC>Z7c0jt4Q5t0i_Ia0QoJ1Vn5TBDI+9-drKTP>& zNhqF*DZN2g`Gd@I3v|PB<=!FOM(xB9>jP1-Oae*WAyF}6uW}SK{9O{?7&;4H^}-F& z25PxW9OqHOeG*!3G#vME7hDX%e*OGP@d|0|^1BhB?A5yzn(oSOszsHy>v~$y;y-qsP zOeokJIh1BKZ--8LiF7HP88c*3=m&=JuurpYF-na*{T<1bzd(PdgNmgB>rSGyp&OG& zm=*~n;+R{Kbz5MZKJfGEn0TVraNf1?(E@ZAQWB!jYaWLly}j4OFyjETk)rS%y~e$p zypbZ;N{hl(TFGXenfYHfJ1$!nT_x%GaBeCN3y1irPPRosr{hhek!n7F8#u)Fd8^~z z-nm8n7Hi(zpzml7nPaYpYG5qZN1-oSdhhieJrBWqi;Ew&Iuuj&sLZQ&R)ich7vw*Q zNvRkX26APX6szZI?Pz2OdTFp#w%5Mg;#J%jux!E|YW8pKe4RmVeWpBX9Y2nb3BEPe zMuxLjDOE4s^Gziq+1i1t%;n0l;V5j*|9yKN1DotWOv1BWkL=@cvj zyn>H!-0l`L(LD=--~Z4;Mzoiwu4^wo64d}z&*~^$eZ(@)$3IgHLQ^D1t%bOE)l<;f zcGWL}T5P~ABw1c#TR^{gKSpRRv@@`>G#5Lghx;h#RLJs^bl2xV2H0qG9@mCmj%Lkx zOl@h8pv6?F)O&8d+819s7+7LQouNTlYIO?dfcr(7+cknvXD zl-G2(xPH3l#`5$oI01gv7shgtC0nRocfAxAPm41YZhh>M?=mH;%<~{>^;5T_gZw)Jd*7$KPO66f5AgC z@r#vn-D0Jhakg|kt`XJfFdK`juQshLaKove8T0Y|u~_)SO)%x*<91H#$C=)fxY%>% zejnxh3j(JuF11)W=cQWFn2R+Za&(+~22v!;HET%qey_RtY0vg1uVP&ftMK`icq`nR zP+-)xZ}8$I+_S)*HWx|0SxD61+tQ^z)VlxJLfR~Ar!{ZnlNSGj%$y^h+7CmVDBX>h z>TOu$d#+30cwcpEs*f`e6y)L0OZKZ{;bZL~oJ>q%i zYyX9zDZnTZ!h9KCFgWXACVte~U)uAka&xE<&PCZKP~8RPPu!csX{E{A+2JxAi#|}d zX((M7Ln#he&Lk|p&_1N%B`iA9&3zl@393ZA2QKD&QF+a(2N0Bt9TpWCiif_=n>z>U z)`b3YLIs;XV9(IJkA{B~X#cRPz^Z+Bd}K!akmkTdkr};MeM4!on^pm(7E>fA@G3Z} z22YuGU2czK$t6TJhkxvpJY`04Uq!LNiMRZO9w+Q9=leb0-)Zt33SPBxD$M;WtU z%ihL6;EdVYUwX0C)+qi`>0o!Ic0-yRa?Br#5uKGui82m`0J;`fnm+{t1 ze_qkr>$2lLV0Ku1s)Jg0Nuul0#TuBm}zspaKzS8TRLZMhn%N)jKYk+;Nx+y{>6 zpGg8MNdXe!wk&#rR=zJ(f1H5u>BfAKZ09CwjkutvX^M2D{#o9xp40W`Cbg?f?ZQ=k6!f?*i*xII%&X4B57p#gOEO4nZh{#HV&Vusp|_0E zb#xSnx7E9Dk>DXo9*i1TYB^TTQ9VLarxvrAs6L-jDR8QxI727+rJDI-jRZJn8&*N- z!*PCMhROy;V(gNE?7SdorCMTATExhh8+%@6)jkCSnKULM?K9ywZ};o^s~4G64+k$% z!XtX$QkxXuNfOGAJ$SM^)Eq+T{z4i!|0_#fu{V#0L^2#_)Ibtl-;yLCMwon-KiOIPDoMARJHQzH{1)42XS)z^pINUZgO{Gomw4SsKzK5o^%k-ls*lO# z^u14H17TkO7O5MA9IzCOO`>fHp1`<;3ceL?7L
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -210,8 +212,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -220,20 +220,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/command_line_accounting_in_context.html b/command_line_accounting_in_context.html index 07237a2b..970e513f 100644 --- a/command_line_accounting_in_context.html +++ b/command_line_accounting_in_context.html @@ -147,6 +147,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -194,8 +196,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -204,20 +204,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/exporting_your_portfolio.html b/exporting_your_portfolio.html index 98cd9eac..35161e7e 100644 --- a/exporting_your_portfolio.html +++ b/exporting_your_portfolio.html @@ -157,6 +157,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -204,8 +206,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -214,20 +214,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/external_contributions.html b/external_contributions.html index 0b25ed48..da9a5891 100644 --- a/external_contributions.html +++ b/external_contributions.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -190,8 +192,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -200,20 +200,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • @@ -348,6 +340,7 @@

    Importersfdavies93/seneca (Frank Davies): Importer for Wise. Multi-currency transfers.

    LaunchPlatform/beanhub-import : New beancount importer with a UI.

    rlan/beancount-multitool (Rick Lan): Beancount Multitool is a command-line-interface (CLI) tool that converts financial data from financial institutions to Beancount files (supported: JA Bank JAネットバンク, Rakuten Card 楽天カード, Rakuten Bank 楽天銀行, SBI Shinsei Bank 新生銀行). Associated post: https://www.linkedin.com/feed/update/urn:li:activity:7198125470662500352/

    +

    LaunchPlatform/beanhub-import (Fang-Pen Lin): Beanhub-import is a simple, declarative, smart, and easy-to-use library for importing extracted transactions from beanhub-extract. It generates Beancount transactions based on predefined rules.

    Converters

    plaid2text: Python Scripts to export Plaid transactions and transform them into Ledger or Beancount syntax formatted files.

    gnucash-to-beancount: A script from Henrique Bastos to convert a GNUcash SQLite database into an equivalent Beancount input file.

    diff --git a/fetching_prices_in_beancount.html b/fetching_prices_in_beancount.html index 5679910b..0c69b051 100644 --- a/fetching_prices_in_beancount.html +++ b/fetching_prices_in_beancount.html @@ -139,6 +139,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -186,8 +188,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -196,20 +196,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/fund_accounting_with_beancount.html b/fund_accounting_with_beancount.html index 1b80f3c1..3a9e6c17 100644 --- a/fund_accounting_with_beancount.html +++ b/fund_accounting_with_beancount.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -178,8 +180,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -188,20 +188,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/getting_started_with_beancount.html b/getting_started_with_beancount.html index a38ed96c..74f16a5a 100644 --- a/getting_started_with_beancount.html +++ b/getting_started_with_beancount.html @@ -149,6 +149,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -196,8 +198,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -206,20 +206,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/health_care_expenses.html b/health_care_expenses.html index 7ca49dde..05d5ca05 100644 --- a/health_care_expenses.html +++ b/health_care_expenses.html @@ -129,6 +129,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -176,8 +178,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -186,20 +186,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/how_inventories_work.html b/how_inventories_work.html index 840f1e94..b61b33b5 100644 --- a/how_inventories_work.html +++ b/how_inventories_work.html @@ -163,6 +163,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -210,8 +212,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -220,20 +220,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/how_we_share_expenses.html b/how_we_share_expenses.html index a452ff91..902e491e 100644 --- a/how_we_share_expenses.html +++ b/how_we_share_expenses.html @@ -139,6 +139,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -186,8 +188,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -196,20 +196,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • @@ -425,7 +417,7 @@

    Reconciling the Child LedgerSummary of the System

    Here’s a diagram that puts in perspective the entire system together:

    -

    +

    I (“Dad”) use Beancount via Emacs, exclusively. Carolyn (“Mom”) only interacts with a single Google Sheets doc with three sheets in it. I pull in Carolyn’s shared expenses from a sheet that she fills in to a ledger which gets included in my personal ledger. I also pull in her expenses for Kyle in a similar document, and from my personal ledger I generate my own expenses for Kyle. Both of these documents are merged in a top-level ledger dedicated to Kyle’s expenses.

    I’m not going to claim it’s simple and that it’s always up-to-date. I tend to update all of these things once/quarter and fix a few input errors each time we review it. I automate much of it via Makefiles, but frankly I don’t update it frequently enough to just remember where all the moving pieces are, so every time, there’s a bit of scrambling and reading my makefiles and figuring out what’s what (in fact, I was motivated to write this to cement my understanding solidly for the future). Amazingly enough, it hasn’t broken at all and I find all the pieces every time and make it work.

    And we have enough loose change between the two of us that it’s never that urgent to reconcile every week. My wife tends to pay more shared expenses and I tend to pay more Kyle’s expenses, so I often end up doing a transfer from one to the other to even things out and it’s relatively close—the shortfall in Kyle expenses makes up for my shortfall on the shared expenses.

    diff --git a/how_we_share_expenses/media/859a53a2976ca14d122483a7f69e419afe2609c0.png b/how_we_share_expenses/media/859a53a2976ca14d122483a7f69e419afe2609c0.png new file mode 100644 index 0000000000000000000000000000000000000000..cdad4f200c3f0947e568aad102816f7e7564b51e GIT binary patch literal 41785 zcmeFZWmME%)HiH{iVP`8Hwco0bO_84N=rx$B_$=zkVC7 z^!tbF^3wZW&xdzCpPvt`#hN+i?0wGJ@!NZ!J;BO~GWYI~-MMn*$~`$*i0YLq*Wg#K zT;0XK1$-jWD=c*7%9|^45D7I`*m@e4E78E%*P^4!3{@g>x)2r?=2)uI_n0b zn@k@9Skv+F@3LaE`1sHTy`iIFp@B-^J)#M^`89y}>!g_Q_?GDNcc^Zr%=Mlgo1QIr zPYkO5`Bv9vs;BMq5Ijk2+Dy0Nyk!csH4=*KrqjD>=xG(WQB1uadGVbe!mBOq#1+|Dh2TsZ9bQk5gTkdn%pQBUneNy?^L(7C-aMQCmxIsZV$;)_)eSCCC_*>R3_w^`cUhzx|dfkKc05GJfGahHaE6((81EMlDEKM zF60I6s8rY_S?zqMDTsbrs%D7?w3x+U*5h3ExOnIeg_MjuM<_1&Q79Qd*~BV<>f`oQ;stB3bK zUGv?B>)iCFsLSDuO2Xp+((i_6GCt~5PFQW<+i}0)C+GgG#8A>xqV|34jK5~m3@OG zxIuIOO!t({O;x?#SW4vJ-s{t0=8nsaFyu6G(TRti;cXbttH)|1w_+!BoIWknqKVM= zO;WPTk>|vPdfcl$s*t?f^-p`}|wTA{8TVI6Et-|-kgG<4(UvGrq_j- z+EneGTgN8$c6dpiiXNdx`udy|!NzQ%t^~j`VJy~u@bBRPtYck{+~C|RZZ$^lx7cdQ z2Et1|v_PKqy2a5Vj`w{qU%XVrE^kn-jld)&$+$a=_00$Q!H4dRg<|T(+=iDNVC{$h zUa&#UnRvO!RGL_#w!*oC>T{KUh~8sXVCQbHUf1 z4ln}rQ2w9@ez4IO8ueo7$~G2v-H*2rWik*M*Run{la6j)!rQd1SJUn*GYYnK(cHgS zt0Ne17JP^V#ZCO$w?_Y*9Z@%}S!M+n6WK$^p&+)}z@Y`jBc0sUTaJDJxqKF`9H}md zB(~j?IA-}S0kH3jbX{dJ7G}wyNCUbReMq3dxp7U(R9YiPqCri=lJ{rd5`_f8wwX!f zVh0uB$`$5rUbujUMT~|wGQ?{m?W2_g)=!dSYu4PE%xaoum&E9RS8wOk(2hchuE&;W ziw-fD&qb`Y(Yq2I5wDY(Kw~qy&byg!TjJ`)0E75;i$5{3wj0Ojt-Zuy>_M9tEraxz zc^ad|o4y8{IaY+e7PcA>-E&jiQ$6PES+(=mt!sr=xC|119>JNBVbz3sJGWyhrFVUn9ZXW>xD<9vV#J5#TB?);inMz4Nzo`|W2`4TSH)1U zn8yC&N9xdTy*CHGa`}hGT8rq#1gcCI_xg7rrqH4wh=e`~JBjFOCcb1YmR1KJcV*&( z)@y)}NeJRgddw4RZO76x8;p{sk?GW7*YzFlIB4=r=+ez z6odEfXj=Fd)2GTLiATyDEYt_JiFmVwT<{>oReA03?E9b6Etpmxk}b?%P0R{~9E7^L z_M;u{q%khCy4O(jNyqWR%|aT86&!E0__3-Js+BuyR@nJ`xG~8GCzUwmF_D?WQ+GHx zh%CuV!iT46y5V5__-p`9rTdz1}B9FnUB@@U@=$*qf9|!%FuyA4zxV%y1GX` zv)Tl*qdE?WqR$j+;e(q1e|`+uYMgYKg?+c?>BJz7ruj5xw-z}mVLr&9^5bCacwuq+ zTt+l5=rDWV@4>agtJe!rQcHnbYSG1D;fLu3Q$@M$vE{1JIYlTKZY~_e!q5Y%I&NuS z-AuThYj|K6FRlrt`=l)pJWt!R-0UKC1{qGAP7#Qaxc7x zNg#o^qfm^=PYpC&TVXX^8y|JUKA_|tp3t*&aI>0;Olu!(z)bUsKb@Rc4Z zEo5{YPsFXd85>D|Lr61`2{it3-S`W3ff}RC3mFDbw7@#`v3;I*Mv};z9jO#a$N9v$ z%o#U4yB4w6lV*Y|N^I`pT*mk&nnOmykeRNXCn;j2LI>KqJ0aLJS%7l3Kh7t0z7;@t z9JzlbCvVu$It1<3e}z{SZya>%lm@mVsV&t!;t!{1e>Uzhe&!9tfxBku0|J3YM~ zkoUIaK%MF}RS<#OTYT&Ekge+~e3{khr-%b@!ub?Ro;U8{Oxcs+6@X%wO9;opJEU8B2ZM*yc*lDM}bF$71KdEdF1T!Prfk{j6z0a~14-_6RYMZ2x zg*|?{ZvO;e7n^q5hpE>aJ%7lVEOHKw(ZuQU4S$HQB5Dyqt3B}r@ZZW*=tV4Mzt%gK z-!C>qj&Oyw6f6s34 zL4lPquhLgL?3kW3;UQFS?-Y!@bS_p@lMSimP7WWN6G1bXq+~Wi4?(F~m3mgQU zsqSyvQCWu;6nnm|ud)FE#9KT9+skeLL+v&XoH-bwR*_Wu!ZC5#!fXO7^(2u1H_+w& zLZ}x}T-K5OyOQvJ7H|eddW}Q+vZl$_oRUe(u=He{Q9FKzn37}cmu)Y?iF{Vr1kPvb z_3J}Q*xT3t>|fJIo@$v^yKudI=pm9bj&j0RTVSo#PnN;Hn6x&Wj{QjUOs34zorX2w z?VEE#0Izj)o^>=#PN}O5tnjD=4Xc|=LKqP2<4!Tn%2!8MwifF$BRj-vP&4GYN+ebq**XAWHx z0l>!JRx1h)tCT5^d^7Wj)_mZs>^x6EN!y?bW$Mmj@348cw3@=Q_C%lUIF*a-W>0CZ zf&MUOA^-hjdcRnbKL?wj#MJAO%IBH~NxOUJ4l7tO^=>Y^p41?!J&$#H7!O}Gb3J#B zQ%w$x^?}vhzoeTMY1L*tN9wcIZ1*}X{}W^D-$Ib{3S-nY(qWSq0Lud~zN<=$d=TL! zZU+s?VQ-WFA*)%EQkR;Vx)?3{GP5L4a!&43Ng%OAjadx%Opmx(1l?m*&P#8Ddh&BS zUWYacE^dy=G7u;}dlou&rQ_Bz%{5~`>_mO_%Hmr~8xb>_h?$pw-uwMUP@ajC=#C!a zr_JYWiR&v=i_f#Ws{kRk=Y35jK+QiJ zv@UZPbU&sDMeiHYCa&|^y9VcCMI|K~=Vi16hqZ%ftKVhzgFxT9AD&_-(i8itL z0NFYD`+hg@|C+5Vk4UWj4lNS2-b`oqSm!2Z!(&Xzv@*VFeSc5+47&fCf&$d$)O&TQ zWtoyzBH*vHG>=J$i*w2*t+6Pq_C@>7fQO#(BnO)YRj`hkpVGQX{?Pa;mK<jZ+5RmC=&4AJ?i^y%9k1mEHH0KYvD#eZ)KGFCnZQ4{CZ~z-BDL5 z>n2_do1k%%_Hz#UKLqliBQ6fVDD%G;vc}}nE)&^BEc2NhIPX=wXN&wSR?)IQtb`PD z!IL!lTeCqifp0_UquUb;^=kX0AZDB_|rTVTp3^sXn(GBp&6n`CVF&`X|ibms~@eA|wy!<;rd{6q< zR*kGNebtg-lZ>W9Kyat}m#|y+&c_YXSEQ#;dkYE*rgDVhN#@@9Lp$$L3<+-FK`?~Q zvZSQsmHT(bjccee8aH%`Vnydd2i5d1c_r=Z%msPO;drWPf3Mp%U+vH#7I0o#TAI?1 zxyF1?+s9BftXp#1cRQ47DQ)f<12u7)OhnU~62gc6n-xla3j`lK;tNBkC~LG_3B4F}KqQVv>%q`zDN zfFTnUdqCy<;FrBKt*`>bR0ADF@NV=QK;%+SVcU@2|Cw;zjj(?f8myZLERIDZbU(-PC# zn)Yz?{?~E%aIZ|z*Qwv)@vJCx7@k$(L8xYnvK$Ni9pk5)7R}>;o^#$Y7!Dkb6M4K!V zBO}Wy^+(t-i6SmlM9cg`KTuQ8`}_O*YGc$W%QD86FTy7&tWP2FLY*=}@xflI&W|_W zb`*Y_0E|xM`%)a^K&Rr=9Bf+D-9P)edm-)%yiNc@rTOdRP227Qa8v9rszBJ`7j@(R zeee%@y?}8S=Rw##@3cOw@=S>M`iDo|m~d0fi^bySWjv9;|H-->_MImLReN@{_RPcZ zdEE1|k}y*YvOq>%5#7O)j0&xwQWu1>Jp*I~TaJqtzO7zXl8Fi}LdG#Fz&wZx9$8?3 zbXim1{8vqM$2%s@h7gVT-=qGy6B7N4>2<$YwYB>H9R;NR2ylK+pUpEjk&F3f)FqK^ z9yqnvuMcPYW43qC_3K=V5nwaL;60-c)ZQm5ATr);h#MYJ!6RQIjM(8kq34w6L7c41 z-nc{WivL}bcv(;lGS0+AVN!szKW1gE zaW?!yw${M@+Z|v!r#yDT#Wu>m@1kX>3qS(XS3H-}T(nmzC!A9@C+ppV_l#-l{9FK% z1S=L67Bz}#LN#8eNAZPKvmpd)tvbMwA(*xbDkuA$)&|*6#Kpz+E3L;g&X3lL7zaH! z%7^lgUw*1x?f)1`#b@;**zvhpnp3=!d}S(qv}~}`iCS4nem1HF?EgW6jNc|7B=Yl# z=*xCIHp+!)8B%mF*vz`ko)N6jC(kD1B5KS9A077XS{|>JKKz zYLu!twoWMp?KAh_g7Z;Q`DQM)Nu65@1OQ~Q`fxc&o9>*`%eLOc01=qkJn3UVD)Zp-#;lZ4*FJ@VHCvzqL{-J1um?Y4CoQ@V(`>`XguXt}Lv7uye12t>}mN`AO;7LW2IT z(~w@)!H5~P=Zrr|{MdZ`>HgT@-8CR69a0iI%q(Z`-h3AaDMHTHY)dg^&Tz~5txWgtqbI3Cu+*~^mOTD5UUd z{Mab0-7FbwSu&0k-DlY!@ih?Grl>l1SDl{fz-w}OFB6Qms%$_ z0rBCeu)Q=6|K5&BB8%F0)0Z!oA6pI*eQGgCwfpu!3_lJ!c3qRxnCXVI(q4{}DND?2rTtM$&eQESYMnN_twuK8>!a32htV=r#a?Pj$1eX_ z;MUQ3iJ(U`1U|Xil_7&VdcttbOHUMI!i!El)l~TK2=voca$KRKt6UYhqO-30!SYq(- z{U1tIOa3yvHg`8>Df#t@d6Pr9iAY9}v+;w2&$d$ygU(zpe$Du~U-=&7aYEBn|IHqt zwt>Z2zuVt@UT=+Y9yY`hwdiMHqa3o}%X)Ja(nk1b4!n9=mw4%;{xyNhk3)lQ#&5rC zy-To&vc&wHwNs#iA+mYW#5`-nR`5 z4YjtEwlEF7yB`#odTws;P5>ab#m>N}Ya=(;+(b(y2Fqb?A2-mV1r$ZhrV+~p=W6>U z@ur=pE7bzSXESd8v3B9kjNnePh*fAiTYNneNPUdUdDHn~fFejmow4_4Vex&$enqvx3d%+40hliSfouw+j^KZZi!sfjE7Njoa>@MOMgi znS4wVNi1-ggtuvbd&ZuO+R-?uwyu(nq|zHU#xq6IF@Zi^ANcC3RTGZd_VL&o&@3=1 zOKFZGTy*|dMjV7;U$WDMdacicj(u`>&rXkZ0IM-+lAvb_4I4kLTEJ3eKc(4K4#z9)qoN!49b%2;aYqQ>!x}C-t<+Qso0Va?-@1vD0RRx5LaFO!k2qxUk zb0Bg3J!n7Yg)~UF4Akm0eGU!1PYez>=a{KT3jcHgL;gDtVu;XB`@q}2sFu(IKr0f+ z{jv#j_xq%Kc;P`}O;_lW?fT`IS4d#4O}RFZI5t?UEA1Tg72>Wu6_uRpPNH3uh?@tKE%P&8A6}8( zB2upnYDE@MBVmgTY#Eng6c|D88o?cggTN=Hf$YC74C?>zl-eX@KV%pr6g~Z*b&rO! zH%i!*w+4mE^!JXfdgr0-!%w@aS%HbJt09ddw+ z(!%vB-Rj$1?k%x|M_NYEWEgxQuDR@B%6samZ}Hn|-rnn+U57hMBzM~0ptTx2s^cO_ z_{csPKLCVOc)izhRas$y@<%oDLfLrXEVi+6$KrJ0D(6@*orG)7+(Lwiu6S2iEr$@^ z^igqx%X_YsF&RQXYF|I;!myi%h1mR(ml~G=+{HO>G=h2FAAtpBl0M1VQ@NJC?i2r{H-hX5N|Njxf_iU96f4~p_Vv_RawHp38 zmA)DV?)kL^Aj2`YF7CG2_PfbsyyiaxwHg8`orfz;wNH=s&U?rC%!L4dTwpK05c~WL zymRdnfr(c;ZD1_R4ge%?Hgv^6Whk9~yf;0h^4vvpCQtDLi4xf)zm^#iXrko~1RU8K z3B{N6>sEz@y!7u|?V>lPuFXI8ggxilWnB>L8 zx_wisZLEjqykr{&SHMeTB5m{RagB|_=jV)M7g<8TYD(aE2w3`RxvVOoN8~NpiHEJ! z_V)J4!c4P*;K*O0m2Cxyn+c2-fni}`rwkh$a>papO6RC^@O(o#S0yvBjcZqLi8;fx zDWKwS1w=&(zMTRjsh7OEc!jlPN5zazj!;uYX~#F|t8^WUT`nGoFwFQ#emHYgnJG$= zIe~7r+(+wEgbDo$Ivi8qRIf1iwcLknbu6)nva?&0ICugRT3zs(TJDBCPhzC12C><3 z0{Jc6$2+nHBUe>Ks%_asOt;unIKg*hfjjthD~spueS2!A7KR`TbK}M6Y2SzF`Lk}^ zvC@Pu>!*Is9+>N|ydCr|*eHC>T53QWU=U@DX*Q*jqq28!hH}5~`82rg*AK4Z%VrfwL z4B(iM9O+}OYBJ;5Iwegre zfCI1o@82zj)G^`$2Z#FHyU&|bIl=)kc{HPD+637|DIup!?6AO*nYREG=Pqcx;klMr ze{Qfp4~Az}a{%|KYS^6G@hGZYRBNN|7l?O9_7&}cE9mfK4`*zSUfWbDY?47^4tTng z!wU?nY8Oq4An^i(^%<}v6PG(uYzEm*m21b`F%_^_bE%9BuB@f!!TVssP6=p*4%29h z{5URYRRwaaHTCnv6KecH9AGNnucigHxmm2l0SS7dI|@;~YliEX{9asj zSgM;2OonH=MI|I?N^PFMd98wVFR{KD*gRI!+Qkc`uTO4(I3ooVOq`RJ;pk4}eW2)1 zp#u4##4BAU_v7iVpSeFaSVSb>iZF1);DkfDLQ7}$kvPKmyB&qa6`l6AT?1 z73dp9Aza-=`yd<53e>JN#sGJlPE?6F@>cV_lS91W{%71S+KBQmdO57^o`QYw55|pt z2DiesjW#!<%w}DdMt(unzEs-*sb-?<^%iL)(7I1NV>HRjEc2P+KrIW07kF9+qc?l7 z)|%>)R0o{F-Nv8>J%szIG!!xqJp9%Q=3#Kb(;M1-o$AXoYuNcMmNs7ng(g(>eb z&=skpnH7k8KnD+MpnV#IO%jifO37Ns*7iipo9PYB8>aayj+QEt4GOCHjR^EP zqLbHw%fwm?iBJX73qssqTCYN|Xd$y#dhnM{ZH=KC3VJe=O!eE<;x2PysAnf`;7B||Zs#`CPRW-;E1eC&h0+LW0YH&+@1}O- zchHc*bC%9MAr7&6A1T1|XMU71W)-&?GSvWcuZ;rlB)6@z$DT*>4*|~hosXYjoR%uh zL00(AJA~CDeu?OOwH%?fBjQ5=MnqxeO$RWh8L*%8v}|l^%1bxv`@85fMcS86y)h>= z3@7C~k50p}#oC4eltRSN^gJp0&NQh#PE7GvU>Kdi^N)s4Ou`BRd(R`0REGPlKpEvn zQ(#V)EKaoh9@QoZRvlyU6U0YPD9A4~KyQR)>L%%%RKOCJ_+nEvjL-4LJqYHuFG94O zcA!vAKe_!{ZycWR1C@_nVQG-|quv+K9WMNgoDT>fOge|F&Hfa;gPp?))Lrz3gYOHY z>+KA28x(+CcHrd*`h(n1hu-lK5$xcH%c-8PMpW!zCSuoR*l6q+|*ecW+4`RncDD}YB< z7Kqcr&>d_TqX$n;PA0lVBxi32Y(|@Hd7-^~4%+O|=*z+qfovk+71@pYNMtJ=B!IP0_UC=`!iJR<=*k1J`O#f3P9ZmTuOR8R{0o)3t{k z%nam8g;5TPoJRxP?$n(QuK(;sfUqL#kv+L$Mh(bY|M3t#;DiPQxg3_&*^ZR`mV@2J zKQt`z`Tddr>Rhy#lWyntP`F~-&o+9%4F?%w>PAVO={y$s7zElyYxq0r+0-t4jRU#l z2?Wvd45}~OJ0FOVN4zQoo}n<{`aR=^$IlNP>|}d!SJF2b^c?7AiH5U#h^q9dxk3hE>iWV6cKk$N?70SpeM{OlXiDWbi_O3qD($We{ z1`3AQIoASEQViy70qWeqQ88)rD)r-nhv*q(1-@_BZ;F(Pqs}S2oB+44M$j@ZQQzex zYmdh<$+sm8eji1ns;vA|{Lm1tS;cZkl&(wh0pQJ@>X?)o%-fO=8Q?$n%i`P*!;+L_ zqnfTvV%EL3JgDo*Oj8U1vSfv*Ipv2r5>+h8Z4yy({WCb}b^Y*w#+a>-e2YSW`ga5y zV{-SDA3k{S;P|5ED0}lmOW1g?IK;vf#&h?ez0Q{5vBaMBaAAo*x+T%=@M01Hi8GKy ze^|TKFvgaHoQtsBmzbz=8cJS4kKP|&%s@Gr+IltF&#V4PAkM zK7K@Whtl{$fVkf>asEQ0p0T6nqsz+5^yVKVu9Mck|GJ!JdGWP+z<*!klfZHBP-GlL zlGZzdfyczTHxyt$p)m>CKurcl7cI*Sr#icD9kYNi6$7q{EV*tvl!>z(5+fr&4>xB! zqU?pT5Y=>dG5bY5z2FQz|1sViBg@-5p2pGpHBxbJW#>|m}ddKrCD?3!2yJBn_C zyK}wrKU^ZKr9f2$MX+O39%By{Q<2V8PmR8Wt>@U!CbyPF{({|u2CX6)1yY-|?_kaz z7G3Y{wH>FkH#TnV^}c(;`i15FOk$#w+F3*0qWKQaLth{7QunvjOlnT*z!JsagY<(= zBAEHA<}$nIRDDI(x`ym?sR{Y8^dW0Eg~%k{r{Y3$hL%%Rh`skk#rv9OQ=@}?i+iz} z#k_Er$h3UsZZCeRR51#smjN0!QmDsidJHn7HO;e=-F zgK5V&{q)rEeuu8^nuF=ud{0iTOPu}=xxgdiFWK00m0 zEIlgkmkxXX>0mPJ)8^ylo@V2(QoL}$>w^1-pF0VUU&!Q!YBEaabcWS)x(RDp%-3Ce zF}M5X(u$bmIGxt=zK<{m9(?Un9V5XViSx_O?jk^b8~S#zVPnn zBe(Ur1D&xNrlMm7z1_W}A#Dpyn0K`a9kr`f=A?12=p!j7*fHNxh$SbxouWYG(LG$W zZ{WQj%xP{uP8_nmEBGZnMbn!pdtS#|SqVkn5=E|SH$9c>uNUly9FNBfjk9-ZSDLJW z_qe;0m)}-SWv)>vYl>G--|np96I*1l^cFpG83-q%HvD*xnqQvUPED$lfzEq5$j4~t z?SeC9Po+F3opbGaNoYgvT-?z&S(%AUi@7Jsb=ProvpAQdKGp6~BM~9p8uFT3t(+JZt9B5#Fi`1@N|>Tij&j=GCKq zOQ99T=*^ll!+Xytu$XLn$sRi4}ARCpYy9bH}BX{v3(^`f9hxKkT&d9C+4xn6Pwyx96oPew$A zb&HT`yY;zZ-&|IBdH<&_co&b&m&>}H2y$m=zu}Z!I zf*kIfWS?O{0`m$dc1jWQ_7~gzNq=X^_IrbG9B#}|=E}AP8PdIWv!d3-&)|;Bm?wzs zXh{@MMJ;QN&+${kyQL<-V7irZ@4ZoRNBb&-`HrOZ&-x38Y(0)byL%-)eEMYGPMW<=%rw0x~^q%EIV`Nw*83+XB5D8Pw`vPwKiHCsOX8o8{ zr?d6oWfBdTa&h$+VM_3}`KN=?+5j)3O@_|->RZ^Oo{{i|Dea0VpyaQm^S?V}2dHSgt(I<*X~6`0c^ z?S~UAX$&z`h@U{Y7(XPf4aUp2V&pg!x6Jc%hkzMCJiFn(7NP+<; zpu?S_Q~_3hbT5Ck|9;l{g0*#>As(&rwRJP~6}t2I+RX&&JK=k`*%d2s82Sw`HeD+= z>{H-PyUy3mY8@up-t}#@>!aiata=CDr0QzW1QQHGZ#%wxd^SwOZTv^|AP*qbbVi#T zQD%$!=&96ez#Ao-r(k$cfS&yV)c>5`C{T8?xI0CAV>HTHGIUWzfhw0--_0QyxJc5$ zmXaX~y%euFk{caF>o$UeZ^#Z60@^s$Ba;|63ew#xv8xl6PT+;L2}D} zK0}FEhvZnQ{Ly~#>M6TPvdE*YlFq`TxNx2Pg(Z!K43dTh75$WG%km|MgX*a>tks)E zR-Y@{yrQIIWZpfHcMmW}ER(3sC0~e%Prv-6Pn-k$_Y%y6N&E*pdYgM>RJmL4UEiCu zz8cE;Nd5lQ{C12BdSSyU+wzmwPU*l=NuKaBho^mkBT$5RoJ|2!O0>}oj5oy~bhvfI z_f7~0dE}|O$a0Mwc3O^3LdUdMvQkHyLnaLm5m;MgI76SYY!VUvMxdl>bWVXPAeH5=v62p2SgjLT3K9#9)c2$wX(AExZ6V#`=K---;!;&%iJx!`tAG| zaW=-HZu#Df@yn({s$1~us#HznZE+5f=%Xijo(!4jW=tI!g~xeyhg5^&8q7g4O2X*Xfz?9LPT;C`V#xq?ei@w|iEjU3 zZXwhxq_b$npde*}@%yUPTNB7lLyqDwM8$H{)zVexcVEO_>zNk?Dl*#WKoXT9XUZJQ z0zN!%k|1im?AIaqDkfnIp+$9{U%;Tm&wv8k!afY@&cFNlD-RBF?yF*vnVg#%!sYZ4 zLfY~}EgEE_MS6C60p($c&t~t(3SYlx*LiPS{;{QDw5+!Esr&qj|Itx?PyE|vuRbuz zO3)WWajxfC6;^t~!uRVv4Z9>Kj1m?326<)>}g3#*tGY9 zx!~qYfqYh`Ld1H!@Epcu5rVs|%TBM6aDB6vFW1c$2_*9HaV-7tRn5L@_Z7@FSUBor{JP}zk6v1FW=`e@$^m|A#^x9w&g8lu1PWBaj}H$IYdgVv!N$8Byugdj0ir+9 zfA(runlVP~?F#tf3%tL+xS4CYZRHQ7!cD0FI+B0r)I}}j8V+yzZPPDRF#7e)Grs+o zKZIZCx1PCJGe0$#zo;{hjEwBfA09p5B{yrP$xQx>gwvqE_RyyQgW)|D!D4dH zo{gaZ$f$Y9_*gr?FT5gjQbo!H`j5^@?W~+3z>iY3SW*U^!~Cr4+KfTJ@ILlSnv}>^*7f|$W{ta zuKGVUD$Bk5ON5a?VXjE97v=yI!V(^-6VZ|v`G|Ym?g1^WNK@FIG2H%3>r^0g(FjFe z+r%W9S*+DI!~fCFY$obW@b|h;vhKq7D?<2CjlzkC4Ey65R=&G=+2PR%kEZMF&Z=;A zL2`6|PF0Qp=z0)Dh;w^nCBqpis_^S<<3tmzcziEApA%m8&EEw2gf5K9JK6b>wOO?h z`{YuAcMnHDtz*9^Ar~Rh#VW)a@?WEu`%9FAA&C0xqUse4xas_?i(ycmikAgQ_ItXZ z!n;Flu*&~2R2tE!?s`k=gR-*BwyZW+VXCnF(-Nl=7S1XT7gyql43gJ>G0TfN*>lRZ z!b*W>VhjR;{DHqCKSt$X&SVORdrY$U3?TeO24zSbq83g;pw4eK3a{WUkO zPOM8^NH`R_#&p&HU)5mO+Iee&fw-g`MB+6@Ep3US`Ob!e?qyfBrWTp!Y3NBOf4l6? z$*;J_@~;Y7%;b^l#vJIlxw$FqP!Y^x0ZBFvkw6T8Zx=nW*}IYq?ToH4A}oN+x4oDv zzprO}Ra@qIm%D5bbx^hN82r`}P`ojs$XYjq@`dj6VN@w~ISa{$BJF9e`u@urTz=9U z$qpFLoJo(s8+h=2=liFRj_NIk3Tu-OGf)0kC%5dE-?C2?!#{MWqc?Srp{c!;pRm^e z0O0^@3mF%UUKv`o58!bN2I!AKm-hy=Jk(4x*ma$JD11#uFkM9U&F@7DQ86Tz?uWGs zWjPmb>KE%77$)0G6E;rzrq@a0Ue=Xlyb$dUd-|s5u2RUE`o5T+CudE2Da_SHN*)Ro^!pgO7S_Nl{&ncAkh1i#i4z^Y17#2iE<$y9(wwN4%#NtH!LYpVQY zvn>Fcv+e_p;!?<@j?Jc{v}}!;QDS<;v2)X9x0{tW0q~ak(zU!Bnh&lP?u4U^+H)wo zj^$+QZVOB01kDJ4>7qAr0M_}g2F@TzEn4@q#!D+)ha2gM&dq;tE9Yr-n1U(e4wH2F z-?;Iw*1yCJc+|<3W)IsH%7l}}I!05V z%ov_XPAe4{+<|x5uAVNU%&}fbiT8VF78pNDWxX}X^B&BklH{zyQcv1idnwQ~5u7CR zbcwJW(Ce9!F7lcp7Igc{zXJO(1SQ40 zb&EN8H>n0LQb|()*D+N$ss3_na>~=@0{NDmhs| z>x2tZ*M4hvh$)84FC^2Nz#1^bZSFP?Y3*bX)ia6=)iJGK0AJLdq9|L3ALEj=vu>AIYLEF?k|*fG2R6Er ztEt1{{$BGk2k*$*^;uk1 zf9KHkPeXZns;tj%3v7~9x4XNW{O%xhSqc3XKmyy>q_PFXxs14zrJwq~QcNU0BM8O) zr!)2T?>uUp3FgiGJqK7t3s7^I^-rvG6mf}`ou>jv5-qWsr2N;p{x_ioE;yD@be%rl z2mpnGu>jPfKoL_p-bMcvrCJeO{q?`DeFoBhg|cvIxyuw+kH`ehSKZH< z*#Gnf|ECkV*(l#?2iQ-x?z$J#$#rr{y=-=cYFvKqa|x&z zp04c03teZSCb;siuB$>75G8qilg>ysv!o>T3%7*%J|A3J`wc$$EfJV_4d_&^nu=n$ z4sKTS=0f72sr;j?|0w5O;x}=D28%j=J*Su^VIFSbZOX zqm{rrxz_-3S||mc`xoD7>jZ8LkW^@ws`HOkdw{6VW&5)1lYe-9r*A2K`n86+?Q0(1+x7G<3HnI zpI5C<=j%+|ntzPA{g8@WF+2!I->d|EuRos5bl+Raa&XugPbkf?zNq^o9oV|JJ6})T zFy z4Nr$A>l%lILjQmQ%gRmf{SEP-DQ|rP>Kt~a$6%z6YkjE7De&y(StZeKgVXZo2))|H z_iSrv!R~XpJhwoZSy@9kW4;4t4~6==ig3ojaD_;56(z65uRvo-VQ1I(Q=9rMh#T)c zvi&y=yUB`-{l0}VrXodCzjhOVhK=|sre4YlnKMOhz+7&sIPb110zK3Qh0V#PMm|l4 zYNoi*%U^SmxCWGr2FeY0QZ9-hj+G5*dyJZ}U%i4$l}G!?&e3`=W5qVC+PyXViTt?I z)#L>#PIF9d;gI-|L@#{XNpvxqxs3F2-EuyORqv#yHyKy9!9@Dlrt5CJ5ADdiK>_^I zYwdP{f}AIYrZAIq6kDV7h>7ND$2WGy{yFwW1(=4$Ab!vGAfR&sSixs%&St)z;dL9_ zFUqtWbCRNIE*er(HG!7vxyx?&3pByXtX92G-7ll>tPnWWV?DI!%U0AlyS;fTtp75; zXsT~sT`}l~q3PSE%848YdwVC?QqTE5`RmSa5|xu;h3-XOOG^t21OFdgZygnN*ZqO2 zD5#W_pmcXhGlVoKA+0nboe~2>D+mHgmvl*YN=pa~DJ?B9bjb{z_l(bT-{1S*weCNx zr8DzA`+LqlyZ2}F^ogqXT#ED9DIk5N-VF7Zv8)Kd@?YCeoq1ng*zRqC@_JovACxBh zb?DZUVm8AvxEIJ{oTZ?+N0_UH4HpDfD+td6;gFqT!3OOaY_~8z&_}VxRW)LK`CH!y z9U=EcTAxjSGH5=Fp)NVV$`i|06D|4bHXBm)P~RG3kF0?H^zyKPQ-4cv2eM8&;q&_I zkmuXa=(zgXNsQ`v-K?A6BCi(?nax+jpR^_AdiDC(w4VLTRV6E#^weAi*UI&(h{X#O z7GBMWQz>NkTee%v>S3q8Z*mhz8K$OKZV$bG|DJNdTO&oi(0RBiIIcN$<`u04u08u* z&&hr*xwZGG!n+Q{_$yCuVX7FN3i!+~pLPAgy{@tIS)JVBwignj&(myINxcU?JiK7v z(;IF`Yd!~mjV{mpM4bLahRih{i}I~=3)Xjo4A#{RzOt;?ZiGtG<$Lqovw6?DILs=y zp>N$K`4edZHtX7Zj;t2Q$NY9Vd&xX=HtxDbgDsv@`ajYY5}Rc3C8d!IKkpoej|U6c z*nJIdI2=zfPH=cs%Qt9UXt%(qI`KgwdyQJ9Gkw>xp0Yw}3as%WKi2aT;fUHF6Tc+>!FEz_0^Wpftp!(7 zDW5as((CM1p0}^V`9uu&H^lqw{wFwu{4jIdjfRNBUm>H#sst=~^FHT(Bktb^?kti4 zp@S{55M)=r4O6C~GmA`~aO`OoEitS0wVJNdk;StHI3_uc>3TJXC(LX5 zHe)AbTV?L)^P76rZv$ZO)x~x@HH&>rd}I$wH;F4JUY$>sd2TkXc>W5AIk>{2visVG zXe&2`ecw6WU@sdQV_!Iz-)orJoj6nUb6uqqH}1dqkYJn*qUv4G^igm%of`IV-FUeE z{;uwXb2gL8;lUmDl9f#jLu|xLHIeS(b+1;v_s04>&z|>xa`qRb#)uQ|qBy!a%C9%p z)2M;-j#p+3w+p9RMtf_ZS0Wsq)>HO_b| zPoQP`WQ!Aoj4!d0NKJgS(4v{P@wrxv(4Rqdof_Vw3bg?}#?qVG-N)g{8Yg}O({9D< z2EgTuQlPfgRxqm<{n7-m6^dN?xqyA~DGe)e8x!{QYq2KYp8e~{F3 z+icXej_P#A8C5I@9Rh8CJF->%(Ioxew8x&wRzwz^JgC0sfKfc-vd)Nie$Tp+J<;lG z7*8mL>sKC`^;yM@W0NqIb!ZE{?6V54Lk%{rPfA`%r91V4HYvzn!^hukR;G{C>noM# zZK-`6<&b|!H#(@(Y_#vumrZ6sO88~}KtaB0%Ok_zdwLX_X?(Zi z6h(wp4$>>>%@oLCB$@v&utlwQAZ#^=-Rr06c5C$m2|8yL*#y?cGxx8Rb?(Q9U#ubv zJroGIg(d#u>hIax=EuaB3|;J2$LA@RxW8#;!#9gov_krhh8g=AFYtM$gd-4kTf=8zxj%^lr;WhX zE&-`@JMFbBm3gzOg!Ljv9Q{*Zo#oDf^f`MHCPs+sg`aB`QSr{t{ac{)Zu@8&)DfAa z`R$YFX!A^f&ernT+&RXxUh>L=pHa()ZNU^aZe&u)?I2}qSu-v(&N+Q)I(H?b@RHz} z>o?(iqstwTrs5yg8Pb>VWg%c1Iwg2#xdr2khz-}bKTosbrZ3AfQ5C74l2Q0rrWK`x zvI(o48L4jr%%u8^ba4X(v2e%wn>^_~uq z(F#?Te3_~;pYpZnFcPv?Q}o8`*f4$O>vKqzw+1fkX8KCgg}RwW0ZhKm5^2)&a}cj@ zoi!|67TNKwovoL+~ElEl5=95QFLuU z--MseH|pxf4ybVHOd}NJyF_P~nZ#G7%yl|()xTA!^|NkUpLe=&Z>SSlzU=!rmKkt& zLV?@Zy>-1UZ2p_jg1~g)E%|BkWZ&9PjDx0~C)PUTGCA%$x-ZCZ1M36TJD-&P*!jJf zM`(&fy!8558!7=Dn2p~u>w_2mt#No23X8((vImh?_3PN$BmGjwp*k!x6ssS@ySsRA zO)fXLJoDLjaGmeKgwLmX(872fVU{f9Q>IkKl-iF}gcfGQlWR^$#GU@wf1I`787?~Z z=9M_%Di-XyG}@3gU=z4Aegw+k+q_eK93XvGUtn6>ad%^|5FW{Lept!oY% zy3p@<``5cyXto{O6denIKyLP@ebJ?2GeiCyJsK&3#4F}4WrG-p?%Ub^e&z6Rb*Z04 z*yDLTvWWpd@v}+Qoc@LcOW6y&O0=0Z<`5;*f1+D>XZM4K55Lcrm3Rg0V4CGgxHzb8?k1r!=IeIsnY6D{1X1?v^w+QgnDVQr$ zt&|a3A&X75Dyyp01W-LkZNIw`rEYau1r^RdV3fM?FYF?rfE8cCZ2;H;%;o&DZ?zqh zpC1buG=WxJDqAD>!stRKe9h0|g3BUv^sSQlj27FIQz&yrudr)I@Z7cBA*y2 zG%pPj|GdqOm=t}16!Ov4BG_E8NbG+HbdA7)^w#8-Gc*(rF zy9TgMd_ji5{Bhb1{|fozhm^V7d!4pg>M@VByfQi0qXw@_;kHcEN?OmSs&H;rC$wmh zd=aaD+wx+H5)t~2RtwSMNN6uprpVXIf%bl6zlg!&8z7*C&_I##oqGEpW}av39K(e# z9h|JchKjoXzK9vfAT-@;wpDfuSdZ-_MoZvBmLk z%+1H+xmP3`pj%f{>%mlBmQxLCS3JgK@C?#P-y8)=v3tuimkE>geUnj&40(TJiIe0n zcuL6%>l{eMSsMnFWJOvCOWQW%pfWK2$F{)qvjceqI;_ds(Q7a$`uZpfZ{ME}k}&{FCf^RQu_o*xMH2 zy^$^iJ^W^2{QaX@mxy}c3-S4NwQtyV3SU}2F38vz7}DADtvfoa+__`l8qH0sydTJ& zU9kE?TPO!7JhqaI!;{o}q{J&ig}z(j?5JIL{!$XPLcoyOJl8MBe)xMP_%Z-B*tb=Q zIWC*XG1t`PzbpV-WAm~by+eo7Dc1E0tM9yEM1{(tqQ`Z@jUEQxv&Z}0x)gKS0}xSK zsb#cNbJy9m-~*8}-7IAzDD)^~@OYEYf~tVx?$&j&9jaP=ti=P`-gf%C$;nq(ErnZ& zGn@c-*6rBPlHY#*;Iy%8w!-7mvZz%ES10S>D!%UEWW%gg>YYdvhO)35Wc%3`CAx~gW!%u_Pz4%hKC}jr#&R^I4 z*3~~ZkBR=p1R|ZN+>F^rNi$dxf)??`n%-{CGA5Ij`1-N0J`^w(T^Zk^_9;s!p3l!4^pV1+ z=DVtsxIDY&=$=c8i9rQl_{hg<8}UWUn!F^&p&!?qlE)j$Baee99DJvc1Xdc zU+5MwknPmQqPFw?*=*q$W0hZ7G`@5bVEQf#8@U9dzMno=($JhD2rkXyZQQQytG>kh z<>-*zrNAM+cAsi|Tro5cr@YS3oNys&_M3dc|wy$&zKS>$O!DX_vv)|boPQ-u@c z>I3Iq#}{eoVM;SvPUq$L!SmuvF;UA*zDM5!;mH~2RNe>_BzWOkfPcLfBDptyoXZJI zZKSznV$Yq6m&$AbccvEUno=4Iq1%XjBJm$bTW*Krh`6fb98qV6A(tPj0~#EzfVd9HT3c!1`0(cR6|;R7B7*QR zYK48+iTs@B`1w*;?6vS(Rps1ihbculmx_^7oJq{_M;E(RTf!nvtD#}tz)ZTPAe=Lq zDe>|&9cbM5-z?Ii$O@_nq2qU#udjviXzwv{bC(5q*DFI?-u$$6aw0scW$4sJxQzTf zZL&j$bQiEeWDY!5s8dAHH<2j4jT_MyJ7=~!4C*2Py3Z!Bp_trU$y=!rlh7wJ{o5RfEKbE)#;yRz!^KYPC7{b?3e?cD2|AH zN?2Qjc@1s(Wl_}|5gAI$y|&x|#rmp}NYK&mj*g%Mj4L`h&DG30VNmf)^{J16I}bLP zaQ|^c!lMr~fwAG92XFaoKAMUUWxdi4T24`FdAX*8tffq=ho8G z6|8CY!a$lZ+&f-p$%Qe!B5Xhm9nJ(xeb`YI6Fv$9xq!gfkc($_G~DUBDcn)%E0asw zpvW6QBDt)2x9B9n{L!i{2kv5_-d4d&hXuZlPbNwR@qYJxz3tBzP~2YFkB5BZHFKJh zYHl&|4@tegzE>nZ))t@+{~n_6bws%#V;FZyNq!+`(J?h`j*W+>;-C=fZ|pN!OpHE} z`9h5^^#P$io*>b)n^jUKm8i;J#;#Fc$Q*xuk#6CbcWWsZ1TbNsa+;y`w?eG8!>e^yVoviLNGKVaG|lZY%YN=82$eH>0cCYVOhYwjVp6}7?sWLmkj{m zinI-mJqfaw7mqs=?gS~dIkBSO_2jqrl?id~qYZx8P2C&H^Y_mCM zK|AtTahNwO1O6P(1ZnbHDwhRuO0As=%Sw(Fw+*pHHVun@iupiQ1e&zc&E0|7z|+m) zOOvvBL9Ll`s%8l>CM5eUyntpgW1i+$ze=GUAaRpAv?6@kCJ@EM{p(pZJjN37Xzhd< z^lefb#MC8M8jrnf`2nBNACntXaLtbi+K(>PJPPwI1ZC`)p#9h>zLhAIU5`Ule(bqx zRVD7+8?fqw60e@ARhT(>lclUK+st|!f|5ax$%aj@WZRO(!i4R~>Fbl()AuJgG~>|c zb|EDZ0Mz8tli}l+;j(lS*eOih-_WEi1MIo5sSdATh8tyYjMmUJZD=5tdb6XPaab)2 zBq`w4?eZ%Nb8<1DTY-5|2Jzz^P}U!gp%eNPek2GoMgoJ^4sJV)Szmj@5t(5ZQL4V6 zlK=UPQVTI=@J{~CmK3JH5kt^RbQ0{1H?;4~WZ3p+7;)!ih#>6suKe-|G9V)Ww12jN zcKw5b+aa~y7&+=R&BhzUOOa1S-fHQ6+X(S1G@ z)d;{T4|!k_?2Y2oS^f@goM|5>dP|oUUTNRF7>VHypiVLNu|=#n2LjIRG)AC=C;8o} zd_Y#%?#_1v?%S48$QN+nO-y@FhXsc(drKQ+JdM?Bt8f5jK?B57K*h+pJiBt>u@gj- zU$<5XWAdYO>|E93W*vRu4YCVjrt;>>&vb>#6;vapm^Y`F-H1{bFN{q=qo{if_dBc< zZaSP%9)l|#I1i!pYZ-LsvZ)%9 zW?SczLWgdanP0d>KC2u=8mz#FN$w%_*IuEYro$W($PSG3e-SiXRp4_ zx9Fr^MAE#!OUKk%oG#Lm_j2JHs=YTjv1)FS=e~9|SfA(BP}u0%VB4^#1>!Gd09y(1 zD&1=2ILLyYMB%`W7B3SgMxE!JH!ON^yH)wNGBGZSn|NZ*qLR(G*o>vZ? zDHY~k61i9p9`iniatq>J%S}V)VEJz>a7b6U!rgtz+W-9I)dJo|woEFeJlhRcR@S%` z%xji21qZ=ZIOE%(+4Ip`4Fi-^{8j;@C$B}L)`CS-_ev^_h5UyxBT#8r+8LKgwGXg+Ofi(Hx8FQ=4W3Vgvb#*?wee<|~%lLRUpBJLc) z29I!UhP}LCIo@~(!K2N2ZHgbK5eZggrv^ZOdVIq&fKqydRJ6mB_B;c&QUY3>aCLa*6iNza&H)`$b(GorlfW4x};z2*O@0rag<8 zD;mhB&PT6la+CVYb*`iy9{!{@b@^eI zGF5H_ju0$3B)sPy*Q0y)?kT`selrgnXB}H3dxO1AkfJm`ujUL_z-;*oiHuexOJ&vjbR&gU818VtA=%w;WpK9xYyLM{H`ZK zVW;Xt`zLQD`Al~xeZ;n1zG~h_Jo?!ohM)VpignQ|glT92`40Y^XJf$@EOuLdXkNbe z;VEzKor7|gljB3Wpyw5Uv9hGU!EISZwW}3fLwb^VGXnCXHOt}8ADAb7D^ey~qGBs# z_UsHKA%{Jr;FxAVViWc-Bj&lOCt8QHTBCXnuZfb)h!vh#EmE97Q);|$nib;enQ z0Ms>Z>xsJ0a8J2M1`QDZr<6?Lx`vI<`Nvt{;EB9Y_oTJU3P+ zXV09QzM?~XDJK~ULLd;B6VN2F^geKkQZeqL)24ynej76sz0Jm$mgvQI@?UpE(IloN zCD{laF$KorzRF6X)3VPz*d#zq}3%~cXhrf$x z#Uh?!740GP9_fJx7#4yLX+B<&7t&bo`Q)y8y7m)}7_ZTx=Z+SXfS@rm4l zmuWyI`VDynVOWXBgQU+%Zt4I$RbK@d-zJY7nA%mt^Tj^XDPA_S?t z+dB+cu&3$FlN&-qh)0*KU1g<|01d;gNgY>~tn}+sAZE&5CW)aeZ(2LnbSR#-3&JLa zJ#!8VEg-pzUl=zT+6TD)q|;7GDqF9~_x*-3dcx6U-NQ<)xz!N5vzO&L_b*X#D${>3 zawyD_#hfa#6|B0;nNumnqrGJ4@$4uV5#9Vfg6^hg?>-wVNhw|l8^jX+3^3EYBH2}M zp`@jg;vrMUSzr4g)FJ=wwiVJFO_EYoi_-Zc$fsCtolqW3JO>74swbLi>YAV)*F5WM zmWv#V3Y(joy)Od;L(1T*$CZ9B&if+@X~Z4&6z<;js@Iet-Hr3Prn^1;Vn_Bd5Sxic zB`!%S2EuB;vm<6u{Cs_W~9Z6!ag@aJ->74f_kD)D&Pojlb-DhbReB$ddf9_?O^SD(O z$1Erw2LAfC_5pnh;+(8LA$8jNF{?-0;|kE#;SGRxRb~;&x*ownm8wv9d?nU8>VU58qvv4;9T+MpE>IMjlCiX^1}VjT_dUpyPRcdn~fCN(#= z=Tl{SUGO5@ey^thQ{c&`>Su(=j>$ncSy`yq$Ic#hBRqkQ9oo^ zt{_1HRF!f5wT55d@MM3tF?{iQ{q)+l2W9r)a`$t#0+4&dh#x^u?-~7KUpRYNGIQvb zwsUITalTNfJAXklbTmFAp5vfLB!Z^D0c4%Be?-$*hbcRT^gr8))$( zbgT_WcaGx;iH@5$_CG~WIJ{n;{w@|5EVZ*@;K*v^HAwRL3ElO2Py8&Zo&^HCaas5DC3+1XXrI+hS-`@1aK_Tyk)2)!a)b_%C7B4&j1 zjd9y7V=WX|8XJ@ZMNpNE@I7&9&3oTw{NpEQ)ZtBJi;ea?ggpZz=hPwbURY!g&7(~d zb;;AH3xX?*_Ey@!e>XalfqUCS0Ok_!_+riIg~-rRr3SIGQId#JL2fFWQd)>^bHzD% z9mefvTkS>00hvG3kw$tW4s~%?D!JJZ?lfaFs854fp zro*U4iGvDjoB2=`2W{up;I41!kUUpSY@ODx6T2q{bI!VzqPu}aK3x$Ma2eKw)oRwu z#gLMRdYT%)Dtl)$Nh^g*c9kf)`fb@z^FzX`uPAQk&!jl|ahnN^Z%wnySj=g;l6+S*{PNE-pMmCaSpc3hE=#}d3fuUs;79A`JzeHh=cLESV@+ZUc^t$_ zG0fdoj^Q=4cZ!2+MeeRa>yvy@+b!}Utc+2xY-9h=u4%Ts&jd=sq#k!wB`ZtWHKR*3^Jf-u|<&E>{^$LxoDGI-kLk{5%Qv(?%lV_<)^jwN6|-*cssZ>(H??c1-Na608x&n2!(cD!*qpwV|z9B|% zIF*a>)Vc%tekWh)qYhE!c{9G%#;uZRcPHHS15%xW=Uhcmsd&#$otmB(PH4rfakj;$P7vV`R-F2T4m=w^YXC82##O8w; zB{zMrtt}WG6rSPy0Qv2YVGyu#PrH&wZ?ev6IjIht&A9Nfp!MP|R?Kl{vD~g_@(iTD zi!gVuu!wv`X>V+FNQ=94bR3?q(`8vq`UW{z=$h7uD}VGI1K)L$Jro|3;E3UpQUZOc z#KJM8{*jpDP25qn&thEb)^Wg0nH!QW)P*I#pvcb@pSow#KpS1?SBC@7zhRr}*wO zBW__@=MYEehZD_JW!E~#>ur^B)?dToTcDLPwT~uW%V;@h7E(4pWI?;=zNO1!{9NOq|3s{CoQ9R?Dsyb zdBP3f0%?o&=Z zNVIl^*uJ-!wB`Dk=d)$1OK#(Qsf_;QgO*C`kOmO<8^6xw0YKbcyTWx&G@dvLKJt2KlhL*3egh zxxk!!q0hyqS2~IZ_FA3`cb!-6c(;s|m2F6ip}BUM@9-DGQftyxJT29r+ovry7FJno zYnto+IoLuzE`Wj+3UY&#)sP}A^yb3%9Q)2XHwP!J7YtrnRa`(q-$ zos($!*y>JZRonF>V$F^`A!DU*pRFD)yna!* z#F3SI^Wx!T{=HFZx5-I6{Y(pv#6qTivaKY+y_cNR{O(1DfG_dIo_sglgQ_*$w_vK@ zDlYwu4>VUtPbSe1H=$~1k<~E#=8P$5#%`V#fCW-LXdb!*$6{0riiN zKT+Lt^R+J(1UUL>L=7_#_|2{rl;URU8XwJ|%Bt?qRZT{w=ZB{RK3oQUf+Ph4O%fmm z#3jwiU*!9AGq~v|yhz*9d>9%CkMYS%Bz3}v6z34fS+c#?X*zB3d^4M~ZB*IzV%;zE zg_W4RZFw2iaF5$G{n<=%pnv3(Y!EePe5*F#^&|lva8o+YVRD|el($la>fp~w@ND5g zu8^R5^1N{ZJMIi(Zloer-x*8BZJ3d?u(qxS>b7OLGwYPt8djvGv(c(@aKw( z!iGnengdT}k38)-BU6c@YzQ;KWg!t+J4;NF_EMH<FD6EypjSQ>t`LkRP9_gKb~RJ$L8{QtM;MfRKSRK{k&FO*4R@w_Aon3h z-^}JRm0mDNqGw89VC)+66t4ZD<8I$z@U}mH<}R6YC;L?_T!xu2Ryj*N-DfU~HmT_sI+(H( z0hfWNZXXA%tHrKQEq;|ULfjm}$xLAFpD&s@LWnrF+Rl}7kH=b)7J?#Yi{!?!v1)h3 z`P^d#vl+SD_0SHd%g$}aUqB(IzE`H2u4qfaB|py4rwmiY`xUNAu+H+%EsUfZQiPdbo^GDG?p@G8BpW=GXsUu;kB=|A!(LDj%;b%3_ z^Lep_g5sL(aFQx4*u=IQj)GmOm4l5`C{S4KG}iQY%a>sY~jhLJDPrvqF*OyHpu zRWso)FMhdvIAc0|LtJe*TxG;dZsf}N+s4-1u~A*!<|rzmV1?XWt~tl?vb1a#3ZfcDN8hEYHJYZMv)bG=DBySG|4Ysqcy;w1sT?os)m4(K zUgA=Us2*;{jCSD>njP1XHBP<}r)Oo|AU-#MU(rsw`PWa(0Vlfm_P_4dcj4bRRbtkR zBpFucy;Xs=0(i zfAX}cW`#5#@vk}1InF+%=+%KJGQl9?Pl@XSs|~s!p2$iB!YQC<{}+0OSo1S$WPGyE z^o*(RO|zj8DJ!P6Uxxj^vMUY9?tg7zXWG-Bsa|28I&Li}u%_;lDC@Z*`U$`NHJ;*A z-S+pIc&~lqVO_kL<*1inlGI-h{Dw-}p0-8|^J+nE$h8zY`C%TMKnh0es7CH-z;zP1 z3{@+aqkqxlHi#gTI4nU7HullKPV`?+u|YD-^D+4*0UlzA5);IOagyOcD*)9X5&D;E z^ts1%A{d#N(&+cE2s3+8PLPzyyUtIa;t!*Q@_gudKm6CpDJiFQJ~CgjK-=BXIXO9f zDn|OY!2}w)VAvEoM1ekKHjqih*9k#|t-IIi(+@Lccs-*xJ_5ECtLA^LNivB5jJRO7 z^tlemG5+YGTH01XgptYr_i5qq=W1$_vhA%8E(a30LIf>DAqe;m@U6}9z%4_2hTy4X zONmV;Jc(9I{oq8v(-wkgS~CyO!Hs?}4hjs&`Fka`L4MRI974bhc!doT8F@G0;wZ$* z*v4k_6_nyg-!krE(xQHm1)?J%i zx?-iVz8n&wITI)X9<5NjbNz!mK`-v%gAD=&l$0#69hqUt*T}+Oi-Dau;O?>CVk)b# zY618(TL_oygAA+YdwblT6T|XOq1m`o4;Av7^|hvk*N#%GqVh}b;;T{+t)b##lEdkg zsrfakdxtHK?hs)T-H&(7R7 zRpI73)?^oHsgKkQ+6524|ALVKX&A!`!jX4K%6>lcLS^QaYkq!)QCNQE_bV2;f|d2? zraB74*I=jszv(O}>yS1TY;#peZW?E5ikh0xbxjVbYU2<$L5>WFDziM}d^_;s*@n*o zA(&I;w_=IpvH#+IWq5#{%ZJ^X7buI72tmm3dU?bXEK#E=zMI%I9?qpiPP6%@HZu@o z75^7gS>~(s#dQzvFZL|=#6|<(EhUO|wzbY^5j_4*y|Ydz;!h}<*fb+2-d~$X0W21u zl$CXtpn7r!8;hl1lNd~8MX!QsU$p@=UGq3%8zcoEjHUQ*tJegXX z7g~J0#ta_(hYIVgv+z<;IXK1i&t^7HgTR}w)t{_ADszfCuFZy+i!HAg&YFgoZpuMK z6Zy0P9=bY2glvj7q|S0EkSi&p0%~3QX-xz z3Hm((LhbgTjZ+$S_989HCg4h?_ca2M)SjB`v@Cl|q~(t-9Q& zIX9-(3gUi_~Kl=S6lj(vjXK8r7!Q@g1bo(Q%E)u;UFlhZ`z2xR& zJpyI#z|BVcsqfY2^L7b83ZC)$ zn@M5|wj-k;z;^=KZA>M-8$!(E7CkA!Hx%*^i0BqDTJ*ZGhpFtY3}%}ZuK%0!803?5 zwyi1^?Jc%fe%)kM;;W0~td~qlfo@=nZIH!xb}ys7D|!a?17!(O+ftnVLJ)k~E0B%5 zMux#r6||_pWnT_YW{!a=Vf+-er|HQM4{Hfzq2u*~&yjmjXMaYJ8ajEv2K8lR(75f~ zWpW|L$Y|c$wZM?0jNXlBW=OV=v;j_OsWd5)Ey1S}yYB4PB&i}sCom7~8kTyOfmF%_ zkop5yO2hKViPJ3V{o?O@punVa63s-oj}5E%9s~z*JWh1Mxpsi{|KGQBe9n)qAA!o1;m#8E;t0i|1ukCh3Zl!$38^O}!6Fl8g+%tA;W~$P&SI%_3V7r}daF^fQZr zCsb%IbGR(3{gF)@hUttOAom4=5B@I0!erN1;CmWXi=mUrA^B}G&7)jCGui2C0z zVDNglF{)j%o~5tr0b|4&ZBGBI=)pi|LTe38&7tUvgRgL1R;Wj;u#{_Des+FLe$}fb zzFPW}$Y}I6`q!34uSdx1q43Pea}-gq5B0b$MMR$XEl(~zM2k1j<733-FWF$?e<A+stbrs}K>U9fjdH!?>#Z&sQGc?^jSC9fyAa zqgn$I|7Bp20^FzD(SGt@ybmhY`61qt!Y^DS*6|g{E5)<;hcivzKEpWbetRT*_Dxt~ zM9MP|@n3foyy`YL91cHD7CZP{Z2Ts84~sb@9x@$x$+XFQ=3Yv6jvpQj7F5LApf!}{ z|1T@i3fXf-bDEnDmz2Z|Xq$$f_&SwZ)n$JsogUR5q@;N3!uU9Ajw3wd`4C3&2^=1)UY~H|Ts)u!rfWw{O3z}bY@a2 zi?TTH=0t#c?cYs`P166vC&a=2RO=q`aCT!RMn*Z$2d1AigH4rx_6ba;dV|Bh za7tsC|Hq>IYsZ3ZyiW&gSZ6q?apFY<2Pko|f~W?5r>JD%?7$(G8-8`roW1NN%^b4y z+bx%6!&jKQvl|xe0h;{(UmpxG8ax81e!*yFUjf+i{@+xCFu|>^eRqW+Mz2H?3r)>x zwFAjv>?~$kYSC$bf&P$qZd^*rJ$i+`VDq;}7{c5s)PsC_@(Cv-|8rOj6r%yq$aU&r z30|+}ZD9gT?K~d+m>qv_MOW9^jjX=UT}k$Bu#=llpF zz-V)7_S{{$7-}Assn5_9mfzarwdEMd#U~2gm&#*3!xr`xQ#xZ;cNN4sx#7N4^H;@{ zpN0PSArJ0<;EECZ(*nnabZ#5I=?)>}vGWu8QHGhiWL5RSy4&PiIrsEBi9jh;yPuQm zh2;QYdf4<3##*qCQwu}d|898aPd7Y7b!&1kt>(6{`jV^yxgd_Hfx%CS!6MNQKjq7w zmy+EH@c3vvaw*o-7WH=RUf?H6)3{Wc57qTk;-$qb7)fGymC zTDK36eu$ZcwH+6u4Qm?)rn5?zHBa|`kp5?fNxhm|P%br|Tiu&KYV-{UUNz_p7&Sf7 z@8|~3PZ$O;D~J$O*Kc+YAD(^^w66$agmqs3=asn6^bRK!Y_39aZKhLxYz`}pjz1GM}94|Eh}KhgZI5X^5gYC=D=$iP-Omk z8VMiTT)~Bme_Bf@R3~C8@;b-YVlA+S_3I^CpF;#VceY9&J%WrjKz%srvu7x#1c99v zg8A|p0j0x&q(-U8(_MGP-K_{Fwe5%F74$z6FEbyI%j9Nc4U`(r7x@=+13p z^y}q|q_@b|_qyR>Kp_4rsdS&%U;Ks~Zif|({~2D6ORO^p;v#-_N>9({Qc{c`ncz`0 zHa4cYv!$|>e6%xjt&dc=_TSZd=UpYIm9+o6H#&OIgMeLlSq=y?-P-BubGI6k6$%0K zL0JLUZIkhAZ#7Zr#%FraTf0ZO9^(FFNx-H>p_42StBKYNH+_Y3}CgrbaEcGfER#0<)_2Ry_ zqS9*VXU3F9K_h^<(FFCmtkpZ%MA;nV5U`2RFHUw1lwIb|3+V2Y>J)9RZq^iW7J%l+ zZ-wJUu9BySnB-p0Z!cYge2f*hGvR&4R>sHey`OE?^WU<85ZKJGuerIGNz(#8xlI|4 ziXa5g*^(Ccivb;{I9-8pI19i=lEwBRLNj@D;`hwBT zxsCsDFig7!s3N2Nz>$VbGtBGpKWDZr`8vy$H3{0S#MwckW7;N5Qy{JTpUh&}7*~ny zBsE}Hs@TJ-9a}j?bp}R8Lr$1wc0#&r6>r!F$}$QK!-EK`i2MwT`xvqdp@b7s6)V|LE0QWS1S^%tG&wXS{WDAZN3k~d^V9TC{r!yS_LEeEOR~I%9b^i3YTL0i%c9G$5OJmHdCDiR9{{$=-)CR!{#%nr83c$C-3iATO$4burE8ecXkI zK^#o}?R}s}e*7gqlH|iI|aR=Vll^v z{!^H;25c-|T(Usq`9!|&F+QkDHf+qjHfD*Pt^#nV3cnW80Tmj?Abxk-`Pr@sQvTyo zg&=}q_&i+jWlYKvc`>$Z-N&=gps?H|#%GJ=m+dL}AKs(w%x-`dsVGLI#0dtH&iMKNboO2GaD7qRN@9pIh+ZNRy$d0Fn?W#o zi6|kWgp3k{3`PwyBzhSHF=`M!k|@ymAw(bLug2&#?~(7~yL&I*b8&v>W}mb7 zK5OsaUTZzidS?CgxZf!mcUpZ&q5AK-8ZdI(XIf%Sc)NC&SHat(>_7Sj`hP?e9-LY2 z^l|aoR0i1k#PN#i%O0mT6&}8e&KO4P$+P|EE`{u!yBt!r@l`?8S`ccnse$`wTp)u}p(&3zgL&y}RuJZ{DX25|IT5_*zU6bJh?Gb8Z-y&+L=ZcS!s zd$XGTNlw(r4*F6a|v>g*0>!QE-Zua-C?`IMT z-+++0#k#*>@yF|bF`r!9sry}29@%O0(8Vy*%Louch0Dq$mQwR1nzbx{Pvc(19gc%f zLU3JvN<=16jGfARucxSlo*Mh_hpSt1CxKSd)egxx%_FPHeQ)b%J-3aP`f1jJ(PnH9 zq%7Uy)m*3G(iR}NH6A-VW+V>MCJ!(Yxd8C?kepZ6++|>M?mYVwXHL);L zRP?b9fR`I9D=lTc7tC{|q&&ZE>mQP^{u)oF-hr#yY^;0WR*L%rlkBPH@JzzHlZM_8 ze)5)=(afo3t1t3th+&(ODW~t-<>VP*xAlhjPg>GkM$Q8fAfBI{?Da%7*_<1=+KFbmu*Ms~v1z>DS;+UKX zLy|S6bykXga(;f;3u0Mf>X<^zV#|!{zb2O+!tmj0Q8mOy$`JZU?=Y5M3%La^s_T7}L$y!}UOeNRZh zs)KA7dajDaWBIwGo5zj-_zM%hS&L+|PdhSIv9D>-IK9f1Y2wap#Rw2Lo ze)3^-ATx}pgeX-_q5>h(Shj5U3dU&R{O0=h)8V&dhJQ(;meIyK7r%LHD8Og zB}D~fDS*^zsqyif0y~&*nimPbGEEr!W~0*nI}?Z5NNNhJzdLYW)nV#HlP{*Rx+>_a zip>L?@Nqy5duc6IR+UEA)jsq2y|)mDTFajeEh~Vb<=IT2RNe64>|)fiGoz& z#*3f)<2-;4DSi7BLg|rD zUKFoYduLE3)799woI7RZF_p`2Gkm1_Z#JIk90Dh)(Fo@=O8~VLptI(tq4@DU$7JnC z`}}||tcLeN&2$FG_sZW%UU8maGZiF8{ROB(WXzPu`Pi{hxx-vTTmQKXZ;y=l*Cgkm z91|#*G$hDh47t!7{lPsU{}{Qmp?KgyXgVqy`|#1*`Yd*DNfAE!$hF}o%tniz>p|Pf zL@B-d2jIurr&+`5zlb!ol^;VtOO+(ZlOoh~jed`k$cz=ivrz_T!{=a~4prgR1al3` z_v1J~?F`%q(v^%ro!8l}q1{#*EC@|_HDezuQWR&4q z7)$p-js&jnmQ`lJooX)r#jA87QEuEMyRg1nf#GkIt$(DC)&bol*Lur0aSRYSD^=Hp zUnuk*OCND)tWXj{2~HR&+Ac;K1cX;B9xf_Q+z+e#4%LV|iw$QVNSpBnCFTM4;1}O| zT`HYjiyO&Lm%O)3`HA~c+?ol&4$H~ho+V*-fs*&;OJZbp81AtStAtYqv&H|>@bswI z=9!oQ$~|mThiCwLYhOBPwHOJ6N$N4H6qUF&~oV??{EDe7N zj?Vlw%dp`3-1m8BR8x7|itoSH#rK?=E+bRqnF6lD*!({zC5P*QspU{kzN2P05ObAj zz#4Bv8ENY~UZ{Vj&WznIb&xED(*3=lsc@z*)h(+6vsWIKA6!n#$QneEt9j2%lB>Db zz)#Kcm%3DpHCu=Wq!*_>)jC7AArH)*Kbp&H zTH-?)a)!!~OcF9T@oP7XgY>CIRZN+gfFOkqjvlE+&pdE-gL-Ao{>cJpgSAb7OYo$( zy6`XH@m%xa%Sw)?_?AOFI8L1b^*t@x3(>@(vxYBef1CNV|GBtp#HtuV54A}+XOd({ zaw3>FS|RFrHL3Ys6FHX(mAT>)V&mC*``4#JjS9+qAeF8KxJTLi+qr%9Z^GX{KV-uw zpCVIECruhvlRv!ej>Fcaj^);IDR^%KJgd!6i`}%|Ts$?BLNEjcoxT%i1<8XvxuQe8 zEiDrc&=^K3n#qyYnl>j`N#@|ai5{?a0GolvaQ7d_bdD}n(xU!p6tO=#K_;NLn|3kD zuyo}{NH}xWGodZc2D;}lh4=UMfRx}6)&imiQ)TdmQHvJzCj6OybB(sjL^BB#o2}M` zyVUSrvAoPz`>npRf$p7d5L)3dnjcvm_J-O*_^2*U!V5128pmDtOL8Zf!V+BR8_pQ@ zJ`8wCc)EAxD9ZyyDfn@expK>a_-=`Z zx)E>iD;4>GV&XHQnhcL-+Zu7@ZbC^2I9CREHFe#^X0PQqZj+P9chGByOl?iK=_M_M zI!@&8qXTorW^r`|RWni*+)l#DS3qu0Q%)vs*bc2nna`Xqh4tJEsD$1W;A@1zU~2v9 z4E!b6TZ1-Auqdn*cIVrE4Ay-h*emWr209*Ou+Dt-sxFLcL$P^nbg5NCw}oiByFz(Ho(;~DZg8}XEw-qM&#a|HNa0(Utg7n6YEh_v?dPe! zlXr^W^>Q*G+`rtY^?aDh7eI&5-nwlh-pbrjo2qFfdf-RXII7%3v2{qUCdEIp!8R<2 zhWmK~{N!bHVnaJn{NZ8>{%^Os4i*QOjaTZ6jAo{mGYld^MZBn&slKr*jv+3h9e&4? z+<*ivKw!LVG~~6fTR_opTeHy~5yfiFW1Yb8L6NO92Fv-gsSs;e1&8`YdX%=+aY!^O z{_7752-BK}b%%V1#KkAF!C2J9E8z2c%x%{VWD9UFVli;xZ1}Zox07djTj4E5hBFL| zYTomWtOI_H&J~UmlqI%E!M5#|9ctT0I)B7UW zv{K^vXU3meQAII4S>F0gYZIU;zxK<(-yp_*8V!Viy`Yfcf4%%$vjBdyk&?H<&fa0 zSm1?|7+B!nTalDhBy3y_>xVTNaL0&YnsRyJ6|qXn>U13Q zk)6ipLR;np8=>>KM5)neyo&AC5|;+%b`4^X($CaErIXsN_ohbLSof3YyDj5*S!C`& z;OsjP58NO~rTNi_oQ-3Eq`|T8XZk#zv@vM9=RCh;`t@qW?w-!wg@i<@Y!w3D!GTui zS!t1>sh^SkS{VeKFnw$Cvy5NcPKQ#buf{x4@~gmw_2Maw@@+hyuWDgfYY+pJYvoOs z^8c%aP3&a+ys*M71x{Z_J}!#C+9SeK^N4Y2 zPytB`aR)MjV8Q2y(;%hekPcsdRK%(ShZqmvTt1!_Cx<*VcHHyMOgjlR!=e{vPo(`w zipWEC{cg$4}r4DfCkpL5O~1cG#QV zDqkeyc!IF8f8`c(Ja+Xy2b<+a7tvlac$Bb2{kWsH(~^JSQ(`q6gCeqg@C=W4Fb6qp z>c%_z?bexTFGX&c+);)P0p+eX>-H5P_qUi^(`@b%lr0Jpyo>g_Os^wi?$XJ?U%PgVNI@R*;@Y(vuxr<@@8Mzt z-$)LKid?(){+a?rQr#W8nU3Rrdw8<5^rSZX#Z6k~kO#PTIJMPo%Cz}#x$@wRPqY41 zy>*+}MDo^k8CBJr@ys{iH(uPnDS7i|`Ny#`_}HXH;AykPGv4}fzqsk~@xEiR0T^$+ z+j{zVdgYGoGcqY$CO>s9SE_W527@ZqDw&)frlQELGO+{S^Q+&YZkFY)iXg#A#)3Sn zQJh0Nr@KIztm!NZ6-A)$=gbjE(16&U~qsT2YaZZ!^PjJGXndEx5v8ZII; zS0^OikG0~v`}Md}Bt#ywDyHJzvw)cro|l&)vh0`4ZlX6IWbHaDByp*C{(p$R2|xDY?IL0gsdb^wzs0@-)W&AK^j^XLy*Sg90xQ0?*$C zi`<1t;;e-3emhSS6NxO$G-rJsgW$e z*Yfe8msJMiWM!`e-5=Ev>ywlajLKo)y!iCgUcHwlw8o}eS4NM#jzOQQB(m8KbM_0f zFLiIWc^-C;bi<408m8togd!L)IK5!#B`tqCj&11^o%nk0#38*7+8-tfK#PjQe)*y8 zsHxaTf*5Mnwm&c8*j#6aAuuZL=5RvNE~V{o3JokDzl35mbi=%w>k3+o0wUsdI4|D| zI(_aE+EEn@S}`x^N;i#VYJv`)pkL5meINPe9)>;znhZ@>{=&*+HJL9iizjNtS42OQ zG(`%S$yFZkpxLFA)I^a=@E%Cu?Y@%Rzb6C%ppkE$!mb2@RctYyzH{BmT>X8h%q)lJ zyOIk^C9U|KG137qEPaWCVDvwyyygk}J^+`1oYKISJ_W3bnjLDUKlISv%dsr1==&jp zyu=@Q#Q1xvmY{@j={;J0f8O*8?FIc@n^^wN-n?shvYjm%8V7XcCHV@c zIL$&;iN~lA{uo?Xt&($1ETWhZGozLoKZ7KLnwYrN%Uq9xR$7`r(v@+?5n$LO|DvL? zqLA$|>90q!P?@K4xPt7KcgvTQXRH9Bk96OjBtTqW`tU~b_#g7vemDB~fpP9gs-myWkJZ%Cqvzb0l-K5!Us5WX%uK4=X0 zw`dLlIN~V@GirUncy)t0rsWlVz2)Yz_rIuuHsYHNwK5CFlFF_yz&e%v51=7?Bfhg1 zUv|t#13f)6674Dd&XT*70OgM!ki-!n0 z+U_NJa!hs@O{B&nGS-)&Y>co%NN8NMtri1>Y+5QQ3l!x zE^$eC>;|_MH%5IHFe6hj=SbdX5QLM0EBND@t#DJMchuAzA|N z7YrWNe{7)s?2uIvo@;jg#(yMjGJWQAeZme)*>Sa606=e;y3i{OA>K_Pttx(l2EnXmI9Pr-y z%X@3|;PJeuhUI3jGQQ=;VIe!e8JEU<-Akk*!vrxie()~V_61Kx!>O6i>#_5+r2!3N z9HO+BUKTW`cH5kd_il`R)A5!cAN3dTsT&DU7J<75-85m4SC`JN#OpV!uPBVfW75Dn zDB+?%D7nx2AuZnq%dek&HH=4Qoa}Lxmglqa_JS`qOQIX?gIT&%vbfO#f;S_fB|JW> zxF7l{KX+rfahy`~NO7qDCL1B^mS!bB?P{4?pS1*$%$*H>CZ!i%7P6{ifJ~K84lwtu!3C zAJho4Q3yYM8b@UH$g*|ieD>zCc_85&G#jtLExT9+SHX5L!o6`JjOsqdPJc|pWm0&? zVn*9&hhTuRR$2-$`Q>oti3N2{Rd*93e4R$gW0;M?8ZHpq?!cp}ajWWy(6HEXdU>sL)l95a8SXj@5zv_mi+%9r*$E_x9+2BrEl|E<*}z<-J}bx2W`g0|e%8Nz5Wb}3DwgOoDy57|_!a0*OU{YU zn^rWBca>q(MIp*7NtO-_LX2rzFozWpIpL3BfL_19p;N`@V*CIb^Ue2ian1Dn&wqZj ztg)wKO3B9`xV56e;4nQ#yz~?|Snz+)KIYC;oFPDD?4Hmeam@xkG(f84e|G6uR8@o{ z^(niU6h+*&^*Ok??d5EHBeV|mi-XL!tWjG*yE3WqwlNoXrr8Q!z6wA3usFqW@_f9j z3s&{8;#)-9{z8Kzzyg3tS$~*x|2rz3GX=2XZo#V#NWT6C5|QJ__vwgt*bx-x57?jY zFR_}@eo`}+rFrVI@)TK$nCZ$y2$T~wb96?r#Cv6|KKQVB4r-8zZ*8D4k{H7&$s}FC z`9n$R&1CSPbd}bOLw3;yoF{H0!FbW~DDuaysSw@0(z8g*(TJz&gjm-v*Y6K;hYcD;&1P4bUm>#bp-Zo=K zGoDs9C!vDHIW z?I3Ui z+|~LNgD9>Xbdpg8+~2f+j@7D2ZQ9)2G>?{ll~YkDwW#p9B9Kxtay&My>D8^#4;yVR z^C!4W!SDYlKztF1TTt-ui-XWvl4a_T?fys|&l5nKq`MG3k4-6Z;p?OzmUl(SUxe3P2EnWMi$`1rmU1>h3C2opA(w$v%H zblZ6*?z6aXWEV#U9J{D^$L}WDU%OQmGm;wlAf=+V+aGh^Za${vAibZOV`F;DmTX`3 zLgv7Mj*g_uW#IZs`zk$?WWZmSXBj)1|1!6|V^GPZk3NinFtZ*S;^ygn4@QeGx=oaK~PtfZdH zP|A*Y-|15a;@p>Nu$AG?lQH5+Q;D(6MteG(u69Q}rtTYmw2UJfL=se<%7JaqBchiC zdw4T|yIz@Jg;I7>-?)D)9Bv_F87KtdA7eR-bViwq741wjoVI(HUw$N61R{CtzW^aY zrT9L|WGf+dTCVn5_NVm~xErRTHj(G71x`3jteS(Uf8T|b)-xU$b)67-aM;>JH3yM` z^MTrdeg&6IUPX&S-1b@LrM#T@2#a6N-M z1bzcRPgu!=gGveBZ8#SkDe?1Qq}|Ct3O679*Wo6%=#JiW4v(J4Zj+G6rR8M?%Mu`x z?)dPBxs=wiRQUcUqS*FUuCA_pe3?KPfBi2~M?fU$9386$uRl{r;Xb_nM3Aa3$tXXifs2e&64T}m+ZT#8twqAs8J+I}Z?RGPWW|N8K|ric|Z*6niz zj+8j^_#M^CJHnv1@pGKl)tYW;74xJTwf>VI zGOc-1v4XmOA3l|*mEsdI_6ag|6aKl~9&2<*NBaEoiPy`Q5RMd(hZ67I#6)lkhmU=<+hvkXD?xroalSd)>(p{mmOFL)stV1dZDi2S# zAczmvXrtv8jT*_qTzn;lFK9P}fL+iDQ;J-yoILwqd`2gM-Rf?y3XcBlo_+aaXlSr8 zR-K_LdlJPtTWTYX^EoI%s7Ed&A;kNI>yvFsM9H`5A{$e!Nu7Z5_6F^Kb-yT-{~uw# z70YJ6jh`X{WL`-BVprwjEx-@W{L5!SqR6D=eg6M1{y4fAegvDNOB%ODxYpDO{VMo- z1XH?Ale?6W2=IR&(m;`7+$|xR3H-3%8dViJ-7J1lH>G2#3}rZ;r!*b0>*@^sy9xuG zPzm~$60?1w(9%rd-&J7XLV5n%9$E^IzyIGV%pn~;D6gaWz!SjJ=Q?_agv)sJrwMnC zg!+4*{gxB69QF%_e&(Aq6tJYa616o>StcG`07XBy(lK>B4_Zd3ivmqe82f*K9F(G z504_`Q`bvFul@K85trBH&bYGY>xHE?2>^!0htqC`8{+c~O^0r%@6jVGOM}uR)g`BW zg0odazZZ+DDtzEeKd#gzxVoEG#5u2JImNoj?CfOr-R1dy<4#K|Pwe5GFUg(fhX6W5 z+_8sey8{~l^Va)P1*!B%CZfAVm*QUT(b_kuaZcY>oC%}h3;D$M>Jt;yQj zgj$<%IEm2QwVu5>od7)E(Zlr~?$)2f1sWU)3@XJsm9mU3Kh-*ivUUd~3`_OvhjatF zH--xntIyhS7_|fJ7r%vjoS*DBxNc2o-eiCU{i?AUuP|a{OLOq4&ZxJ)62dFlEmDF; zGKe_~(LFl4Ot#1@_X!VP#)89mS?5gPz`I5fanlo4=VAaheq~grbr|0`R7U^a(q+MY&+4c=< zAdd4f)5vNB&GN!RD0dAU3Y-Gyj}A0;wA8TG$0@*kAj6jCA@!`-!I-q$<{0u><`e6L z{WN$l4he&eSmJ|L1K06S_uiZVyVpM}rLId83quY!$A3I)7kdh0F$oyJQq+hADRJ@z z4-k%g5bkmcUTM)N)Ewc%i-oP{=dO*=Z00@5(4OOhGcd@xb?-GantzQbwmV&@5L z@OKwu=}LckVIT=3GzY9|+c;_0sFO+I*66uipZmR~`N}!~nRFw-V=;oODy2q8XN{H^ z=tU9xIxZ($=x(i`cXx|j#pPQMCajAS??MmU$LD1lUUifA*klorDc&fcRXg`LQ_pHs zHA-EK5bgKCGd_<|gihAj?!Ui|#aX}Ni##^I_+{ceiN}nO2F!pi_u6j1XsP0GoS9lx zMObaOGz1&_tDY@>EOS?NLTTLszL@vf5i^K{+mTv=b7gsX{R0-x9UIRMGCA$fbkS+w z!fB}rPyD;3bsCp8Q>3XW)R`E(kLGVhvnIW%8tCv6{oO&azZfY2-MstnO$1uYxPZLf zKc-DN^Tmud!JIaP&o?UviY!P6YH7NC@!l@QYwfws0vReZ$W5|`ernP>Gp>`iBjrX5 zzp}qItMJ%td)58^Mq6z0ragctA-Ub_TV!U+6Kmb$`!Z-Fo&n*NG@r|65Xs@IOs@kZ zY{K)7W-NUW-nFzagnvwZV5c9p04+U&hkv?k9hR;4}jLP!C+&{3G+~8X& z38g}<4;L1j(G>#jUgv=p8OFAyg6oSlkK0u1(y@&SUjNK9u72p7^+gPw0;aAgF=*5^ zOG{4H$vI~(;3AXsZ{vx?iGPJAjqZHhJc=EAc)XIS*TX}rJv(J+2H0G{S-D3on8pZq z9v>gqOtC`$TD+0SG5kOX7{=@M8q=wD7=6@VX-JfJe2z@r76lCODFu z9&B0L;`sVAD%auggn#cMA)m75OnikN^!`=R^N8bUb`oTto9;RU2zkYjW4gH$KJ`;) za2v09_RK(NKISO`9A`Wb;0|k425B47a4R(3GtK~vFE_&|YJMr0{fE_$&uiI{;vxmz zlb$=x#l$yDf}c{dGLzUr;QX-`nxj%Y;5?CC*+)$v`=5DgG6n1G*_LKB_OXG9e7$t= zoEd9Ka<<0+mtJYNbR6Z%Ttd0v*MQaA1-qnHGA>Q9Ud!QX(ur3K`h&_ply+0VbU2`I zQzaFE;w(LZ@d(=OI3x+2UDIiK3ws=k;}phtGl;XjHyLFA%x@*L_@Ew zAV2%g8~;|)U(D2^t~PC#mZ3LaYQ}W?j(e=6yA_!={e&lj?Oie_1FZK}k+;OKjX?-g zw2_FaL0ICa$s{`N?|{+J>9G{_XJYH!!Wcq;0V}Re@6{w-GA1v`4sU6!ou3}oUL2-0 z0rr0?gu4FJv4Umhof6#?Mw>p9ZM`fHW2m!a4BGf|NkH|8)8sBv|Enz@trEtu z`S(p>fU*ZU&n*#CV8;eh=>Z6G3*xG=sc&V4r&*y%y?m}i_!j}AGs4}x7o#y9#IDu! z;J4Jb(4SzcBryMruKlNivu`UYWu#J{G!+>i42qL%EcgfH5fU==w?Bz@$7@(FWd^wK zoeah`zhOITgge;s8sK*gf(izW4a}(HnR>ti(Uc3LRx2Z^TKn2e=r1+m#S@7KL>xEl zgtOP_mqOP)l0?A-+s%R9?dMX%JcJJlT*-oE^dun7yZ z^ZI0;q~paAx3?(E8J(Hi_<}YhMKYM&Hf13vssTKsb;^*&9uu@`F@bN`mTG#aYux>0 z@Y!G+j0u0MnHaJ_>hC!XCzgikjMpP*(^v>U@VHptvf63AY>CG< z#d5OBz=0<1F86VeMqJOXv>E5+VC1&pSM1_dWPrMkWsd&LN?xoq3XBPTOF^;$W7Jub$?zn50OJQ>HyQv_v7W=@5UCX=3$a2VWxZS37P`P)cL8 z6K=ZT)am=(j)Udq4}gI;cNye4+`96Uue|iY?&q?wNmSJ(92Y215WopyRUq9&{vekeZa&Q z*r6AS|1*yP|J%|3$5a1n(|?Zszu^Gp!MFThj{d*lz(2v+|8n&I2?sDXcsYnALYs49 zQv>k#E&=bptyOXu@Z^qXf%l@dSRoeFiT3#nIN9gM38gt+_MhoJR5$C5XuZU-I|V+D$ifBF+HsmH2HkTj*T@8||WA+9$8~LXN$pff{+-Ga))}9lv@3 z72aL>vmglbmfAKvuonnNfG&Y&$Smo==FV4VTqlntB;Hx@Wp+fWKKOYPq;m0ye^>r( zcYnX#!zFQjgoH{QDByH&YHCWlZy&QWM+R!Qm1a4l&KKE{*woY{_Gzj%SX=oDw~rwEe^=@e5(xe?m*T~-8{qX=wce0a(YxeRfA0&D^@OB*dO zws#! za`Ub_azQ45U5_vF$T#k?FCux>WSQ(TYTzl`ig=PN$+A7f&Tjj|vH+$hz$iL#RJ{vT z%?ALJKh8y3;$S5Sx6c)mo^Nwr-oJ|pd|3LnFnoFXVaDIMk!xs{TDG&7?IJ2YeY*EC z8Fvz_!>~!ruG6`dT|w0B)vKQgRgL$zELOzN*)9j7PDiGuGJrxo-c9PGF_=}e4SwflLhd}al1>gGTT3Ai|t!!=Gpl~y&;cJ4 zyt6IOH!`VpdI@S4EYM)CvRCppVhG4exbls%0UO@pkb#>%3dQRz;7!`LI_D~<6Z7rL z8;#?sf$Qyi#LRa%)VTTZF&#EX*txs}ciu39+F66FENz#g=X@U`Q5Sv0la+~IHqZTB z-=eYBuVE5)Ospji?3G4L0Y*`#=&$As3Jh=FyopFS6%w~O?C4zCP}xiA@9!7y(8n|h z1rxacp1a?X-|O>XDv*}$r9))0TntHAK3itFXZyiZ#K

    H<4W->A4v!5V@2La`8{7r1NBb*qZtzrWCZx4zK^a&_#LLHb==g2obmXz;W!q-A1^U4i zMLAP`!tpxyqCG3V!G1HoT8{oHwf!54KHi|I>5{AJ&7f{pDEf9EXxcFbSm_Y$$xFn2 zR#a4U2LP#LbO7=C>K`qpzNj46O4REDH1|ZkQ23>8en~MbK+*FQR-?y0(W5wZow}~3 zX|f}&Z2Bo9*$5$DWRAd5+mia?0#4GfB+G4lrKo|8`Q-+T0II)DLJbyrD z^jdeNb9*~cU5$q;CFw%%tE&Yan1WjU7)sb}awP~ykK@OA%~I2UKvwHREvHP(Xn7zb z>xEzC859}9$iseiA|j%i?}BA2#_a;n<8Ar5$%z0ge7l_o2)?0N1}<~+;Gi8&Gr5bhVydL3 z{(jz)>$aa5$cb+6J7^WKiVAb@X7&=ks0M6t3Clodp2NJe)okbqYH6un=`JAo>SjET z7@(w%X$Zn-Fl3hWKE#ipCkWN2;d-+;!}4=wi8Z>c{|4I@jE!i64)oQA&QdgJm7zdh z$yK8!7;7LnLbFqeLFDn5j_VK{Cdk6IezKJbJ?HR>$!qWsnz<$t4S(BTM(8pI?fgvv zKWd+495Y5i(D(E4xuy=aM}D^@GeSM!ax#}91KoGjB5*Tr0m7sa?{JpOD}9?#?MaoM zb+!$bYyRpDBo{)!EEG~4M_*r9y17bANJq%fYs4p)_rl+?oma%^i*x#l%_6?->bAOT z9L8ro9?T#bE)v2n&@!GHWY|&ApjPu+YZo@bf1IQO!9s#a0=B))b_mqO(`ajiWye7z zD-m1R9JF*e$eyGRR()xSg8ji!c}BlbWnwc+t}C!()eQPGRp429F)SaQ-%C2})Aq*2 z2Dw?cP+a0-#eCN+M+3iJ;&4v9`}r9*UnD8TW7n>pUEbMa8@(;zrOF6KA>h&r@Y4`Q zL-+2&l21y7BzURIgpCVN<9SMZbR25SJb5n55`2t_+Tu_i+hK; z)w?knfN8H_LDefz*uKfgvPFZ%y06FMF|qp3tDARAV~nvAbdUphoe@57XD-FQ3L;Z1rzr9!F=F57x=dRrG;X^nkSp z>maSy+L`xu{GvxrfYO;T+%u|k7>idz8@#{%9k58OqCs=>Jz~l)lcT9w4%cr@m2Pg@ z5Hd>3D@F~xTa;%-!%g=-%AmuZJh>u*^@t~)CiwC8$6+`>Ut|=59N&K;AlxA^wXfH2Q@Ov8PhDbEFQmW23{eaUqXO2eDx(TzgIrY zk(sN`f&!q{L_t|uIlN4aZCfH@+>_CCJ9z`>;KaK(f23k2YZ6Ai8xYamu5RtuLI2oO zE-g96-pcaD9%Ib788cWl5!Wp%(p5iiH_kWpu zpZm?M_#NAxo}S4cy)3L5Mym|40|wIbmACIWDd`PCv5+oPh-# zAWH9BqS0T0><>y`5?`#|1Jb@s0dsGTnOih27&`NNV`%H`NrbVRDHI#>av!woM@G5F zBqY>?f&*c2L;A|Ya&X{EDu$L|K4VV=bL~8T!Rarz_<9CB(3{eJ|77V>U1T6LL}ls} zZ~sXxQwPZ#Cnu8l75kb~PK1+4fCK{`Q->U3?8@-xGKzV=TBbJ1ZKRjlw!w#riVEGN zNzw&2G#wOlG64fg#95&Eo#+AWbG)Zt(Ot>f+F}UV53Q%i!$yAvKi&RunS9uXY_%bg z@p%f!+3SGxgBJHkGHyYxR~Jk!{Z&m7`r!7-w2pTu#SBiS10E7wA8Fm^f~UZW&i;)L z6Q~s#SQY8_1s-Kb){ySW<+Ew#0*ewApFp-03nIh$XNohHz|%<{4o*D{B$!-3zrWro zTmi^7u^&+^+czUoLf^9m@p1jcn~4s_{3M41&6%SBYshb-8bPcBtM>hsC(o|Dkx`6_ z;t|FM$`1+<7MhgE`6yLcX5`S@mB`6QIecE!+X7cgbYE`uxnQgxX*)`?xz|Ph4e6r^ zfgMr4%&2=YnVFXo$Hp+XIv#v6=3ZqQ03NIfq|##9m2%7q`*lzi?|EF5tRf%Pc5Mc| z>Amxou^7Kni3fm2Jc@G6i~D&`pSo}H-gnkD2W(a7Jh*+f`|ySJ6?4rx9>X*|^5Vk6 z!X^Oe*ncUx%ENNFOQM#NoY?zSjCY4tK}pq%wx-a=c#r2ZGy_xj?C=*moN_wJ@ZA2b zTd;bx7&aFhN0>${|n=mY}rOKOm-8eBlD1d;J64#D;%1>IOG zDY;v}&}o%YD$sQKvTgV`n}C7sOl{25I9%>%7{|;|&&&n*e$`%IU*}MaPMu(#T%FeP zIZJ-{)UNrP9SNtfnH0cn$HH)n)qD_j%aHOhjE7=F;xZinqXW?YQ1D2($}8V=Pn^JC zN~ssK3WQn356cJg2pC~b1!Kk1XCGElH}7J8&Kdgi^8)jLWQ*zE1zC2Y-7ofM1B1?A=A| z{`RLp(&k$3;WAPB1M!~6armu*$P9_Nfe>AE$J?;XEP@U^S=Avxa@V;cDyPjBU5;$L~<<*Gt|m1;KRGl zGwf>@f~?HO!38=xIiY!$z`4T)-k;`jQiM zY>Yok?RD+A7SpIolS@bKy$vFh`JYJ$ERtEz)@2Z7mR28VSzo4%2rTc%X_WKB4#U!m z*!sK`ELMmHU#_$kSTr|jt{gPTNbuU)wTYD4o$1cLJ$0Bi{i53J(1)Wr5hExuYzi6} za*Oa*Um>C^NNm}!Z+Z8Xe|l?qa&Ibr=Z+|`;*9W0?T=a_MvE<+$wnB31=mpeDd7+c zWBRu6@||hVU(+KkRHdh5&r-`*UCuk4p1WLTvFvfVMUGfY9CttKnq^}7ww9KGwf}=a zi*nQSAjYM^y7g`PyIJk#cL>l`k*>(s2hxV};6ZpvMUFcjAZLsN!RXPvWalST31uVq8wcCtsb4 z_v8;by`YntCnJs6tLsk+^a3nL7IOC=T~5l>sx)@+!UShW}NjthA;b{LPpd-@XO%r8~`>(k|YOBPWQBM;xxoLXjT(B*VUeL_gUw zUEwZ|07}U6wQ$XR{Pn%`XP5u|(P#B|}go^wEYWk+P|p^IHjP zG97lc)2=_X-0Ggq-d_7iD`xXmgh4kLc@BuP6&9P=%5G8KVY|eGQ z=QFz3FyKAhmHt!T(CMZ2%0Y`p#s`m+_}bR8-VvbwYMz`|lW0(USnBk#Wpg5;!8E6_ z#LeB;r0YZXWTWnr=}$XG)?%9!Yo-}V#k6$8W4tCrw1UHjVrOyj-o1`&>qRAAd*M0; zMV5Dm8V*|xM)Rs$w_zLUBL_Tj@~r|adbJI|$i;q@sGWR~CLZ2}dTI^8mXtQqPEaI2 zR;n8nYa7q*S{tMp4(M%{CE!dswze#sI9ngjHSF)LS=81L8dQ57409J>`hg;E688o$ z4IpZjA*H3sz$nD07ct$m=c^9W>plr1yv$}i&v?XxW{S^OpKvsfKV)0Y=T@vyoFOA3 z;U2pq+Z&!2PWLWTym4P=q{2EP3TGEp()eq?KB3!bv|Xf&ZnADn7|3je1TN8VD_ z#fNlV!|4;a8&6(%9b!orf1h%CZlP<%qs=uSF$q)yr8|gvcwX@YXz%=D*`T7s!KMKH z&UvZwb1^rfdMG1px6W*;Y4n3xY4>Dn|AppV<}lHnrb-Pv0YEFb4wje#Xs}rjU zW8J*WORB?g7j7l$TXF4ZH3vY?zR8WenVsB5k|?L-OHZGyhoylzY=?1Xb-R3px%yCj zP;@}`+l73KI}qLtI`^`S*7auQQF3b}DjH213{g&4S*k4)i1GZSgc6Jc_&I?Tl`~+GMLMmvudBY~c6Y#;r$x&$hR66}t8L)_~(?vZ0lFNsBr zgG%k1Pc&VucKP~|)d!`f$wevEuk|z4P#VT`YGY;3}-6$^Vmeyd8Aixqvm*<8S zg`Q{W-Z|Aiz8HC-#E{@NY3p`A@ws>eO~5gOB+#@U8Z|>{Zyis0jfl_P6T7SqA2}E! z8L~vn^ZHs1koHlW`@Gi2G<$`p#dN2(E(rHj6}h|Rn^{f;>78Q-uHk@Y>}kpYzOJ&F zOKA3@4BpC!(yyoF&z{+Yx16OMG(69wX6t?8ZZBis8?zt6+WCN(7em9pv<#tO~ za}ZLAJoo}`Bdn*t=lTjAwXJ#i6;G0vYqgn-k4XM30YdI0YQo%KNtY zC`hT3APcH!OF75vhEIF;?rYUl5)SUujIP(VXj^|W^n9r+xz9GFTD`u^#EiQbV73?1 zj6UfpHrpGMDb?Z3_La382pYy)i2I>Z;ZfbN(SDZIaObm49}Y*6{FHxUO?iPeGI=4y zdQxx0+@-rpM9+vuO)}{@XLc@^$g3|!4e6|9B>Gen8(A-dC-0DfZbwVR5r{;5Bs2n% z4LkK*Q-i3}m>$OzK4Op2f_4s}+KlR(wU9GJVm9T3(*6ayoGR`fEyK!N-?Jmhe(VRu z2OL;yK{JiHve`C;SE{+jEJ8J@v~AbQN=IzDtA`l8%s;eby|n0gYAmsu9O?|2D`QN` z|DjvOzDgfD)52q(wS8VhOx81V{9<b$4ItxMn!>L7c>*MiH8^yNBc`SO#sokd* zG1(EqBkPI_2YH>|x;6FR3-D#thr$oCY(Gvq)k)_SX7IWtSBEjQ1Tw;^yw9_D2o7j5 zM(|l#ELfE9_gT<t(f0nb<^KcsEOYCk^K@8}b0i?2iDBIB6&(30$=+ zO)J2c9`fj4pZk`{-h?Q!sg#eG_cg{Lk0Fhfi&yz42q5P`344q^_HY8iYuv2v=QsXw z;^m802eaB|(44k!!k`@hV0Fj8ocQ}$s;jqQ85d;d38|@`dBEG6|MgGcKyHnKHM9pW z_wnxn=_2ZsrnyiMcQqtd&O`|coykBedM!{lhxWdelE*mcNAT|FTmW<2uQ{zGHK`b# zT>aqp*AE}ob-{W-YjA6_9B#hPU*~Y6>R*%Ah#H@LueZfx;)f~R`LQ6DE z8uWioUr^B0)HHy#T--kYmdt~jzwj3%d(`pWdC2ZIF9u`WzxF7Zn=iMa!_K)*9t#TA zreG*#{^hr)7YyUn;KKwq$E>{%!g-dY6O$C&UWM{9fr=Nzq4Z zyc})h6C7Ad0T|n?kd&HB2(8RO2q+{V2;0-|GCJaIQQ&^_r0Kgc?t=lmh{C_o4+dMf zpkSxZ_oSgvhS`nlpUYRtklY1>ap#;V4-u44D9=lcwkkgCm($mDd6!AJ%?c4m1TMzh z{&U@At?3!vQ?E9&D!tJja$cBY43M9I=)TAQF#1E z_I}gCW+vv6fq2$3L*|AQJ(5+U>Rg`$*)kKqfE(ev2%9{ff9n8cmH8`LwNE3aHVnFG zpQ|08&LUvM|* z9WasXz~1|_6&{Uf{XQM>1vUqqBP5Z=mhGWH{O9U}pJvp{x^F{vQehp8{c8Tm3L-c~ zh&c{+8dd*5IPXG}LX6a(coz-?$)%zLP(Dnl?f)KIRv`a-Eg%|V>6SA*z*>@@k?ov7 z7RC~Fi$0CJpLS)yYr|vbfK(dp;U}g$+zE7(uwZ|1gs^`g`SQXMV*Jzl9^~ z|2+mB^EdZ`!Jas<0viRo|L0hA?B7en!S6VJ69!0SeT-O$t}@aWc1wzTwed<#)4^c- z7dxFC_uOB|TB43OHq^(7fr?~j8zC7%ob7uMH7p;ygk&^$W2u+=I#B2R&#XW&wZtk@ z^hhc!XTgY_hdQ%X<5yQ(A!nVqK3fUZsL~DmfA3DW=>V$-6}EG`J+55F4A(Xpjf{Z}4`d^BhcVh{B^JUm=>8ORrK0J~@h4nzx%_OdKl z#8hPKX7`5O#sTKDzwO6f`|m2J)^+8udOhWX7=!9XyI|Ov0LzbspyTZ!He|q*(XD`rFf~B-+PHRR4n93@Kv&oQznzJ|JY`DSi{yU_7>uLhM>|Ejf ze_{!ZWn131S-Bs|l+)x;4+ms61@K&frlyG?WF86H9A#dxAD{G>OTS5d)RV-Xf!GU1Ih#d zW!e|^>;~UKe#PO|>*kQYsWQxfKQJvXa~yT;>AwaLXvVr{l@6FRb$bz}-?EA`OZ}By z#{G!ziHMTrDPTPKdLGoaWb=CHWWiX~F{Y>UIsywAa%Z~!--`+KQICtL)4k0GQa)%WI1{8 z_kn>~os3#TnO0MIM!wA3LmK0t&$Y>toT`r=Fvd|}TGg*Sk^C@FspQzUStDh$T^~Tt z2e5tV+&D@*{8PI;bd4v5>fXQbU+4hvT|SE-oBGWen0`Oas3vn?W{#F0>t*r(XFeMG zKe}l41Qzt!Z>zjVpz54> zJMqUap>R5+5&Pg*v5B6hcjc^rSF>71FE(Se94$5Fh|b`{Or-lSqHf*%_mNg-_(%0W zoz|Q+#wIfops3T+OjWN_ZO6kdO?s7$J=4Cu1KI@+dH90EMHf3kSF`s*i!%SCVM~u; zPe%;qo94J5@0xtrY{WT`a2~@IyXa0;BFOVu)Xw<+BQz@tU=fUY=+ zAmO&p)$p(Uwf}S#)9C2dPvGsJOhup{E4Y?i-`O9JAK+kr#tVBBcIv?S*pms6$_mwS zjhL=f8V$NQ9N>=^q;FjNPby2hU%FCGOe#As+3-~Yvr4xNy;H!yT}MR*Ca8gpF{L(q z6o<4iuRelRA05kzU}i}Exkt=wKA_c!XJEe4|MtN~?uvBjS^)^_KA7xnGBQ_p{Ac9( zf0Q^M=pL5N`!0|wGifTO$^~GtlnY>hI5D~ft#^&m3s>GQ8s=&kj2UoQ9 zbZ)Em;mwrVg<||rXJ@Wy9Is)6&8NE2_(B)RAdI>R~Y}Cfgqp zZn$>LF+1{O<>a9XS%IA@vWvnHBRwfp4Bq+;h-B5%wuDA;V z?ggp30$_WYq>1^_1$X}$t=6c( z=#=|XMZ`SqXP2GX{fL&@{eEf@lfk7OujZDE&pWf=jwXpEz;Qm%-Th4hbXAACc1Tm~ zaUIug^cytXp{^;JZnzw;z5HU2O?(&K&*f|X&T7zBnr+r`C&=?;Hf8p-CaYb^MaXziX#;qBXH6*{~ znD0r2wdkGaBn=Gdzv14CKMa9E4L$Evu-h2J@o5fVlHQb7WNW!4)0kazqSO?mpdjqe zWp;Y70Q{HC0}^t;ez2E@>&^UpnUR^(xW84z-MkKe*Wdhcsg}xc#&ut5oStEQq_k(h zQnR1 zZ-aBedwRoee%SqTJlBN4`u87gqOM8CYCXc;2@*AIML14-?l<1UM2R{h!T*P@w~mVP z-QI^SLPbCULFq0jX@*d`l|~wo?q*1p?if0ilI~^zX@>4@hK_-uq~B-!obx^B{MLH^ zVJ(;Q+_CT4aqVl19w6Quf@02}QhAtogBc59Vg#0eFJg;O-$y^geh};qOUcl>K=~@v zw%rvLIYTve9ogzMGp4Vxyv`H5L-+CM5GR$|yYbenqXR=$`5A6D`EkuVzP6{;uVxx% z7=P{VzI8ZB{@`6TO74#QAL=bC=RDBIn>afHAu&Xymi8xrwmafMkX#6NGLEW$x|I4?f|rG}BfbZZyed z5+p=%1!TD~V7y>AL-v-KJ7x^?`rBTYN)%Q$+qiz3+k9IAqMIL28ivQ|vNfNT5ESpu zG?oH?Cra-U9%A4mO*eUzR(`+z4Dk>g1aIT8ANR_Al3&c8Vf$oKfUrK-G-uaFc}+ts z_H~Ps^#C6u6J1z}^76YsWSHg~X3+uuX<;A}!)y)x_)(VI zwahQAtKXMFTe=GzUy3UaD)moe>-$d10_nCNVheQ0`oaKI6!I(RU`k^lDqjP79=_-m zDO7wKeLLOn@1CA`5n58Gj?-)!7FTly$tbeWASva7)Lt)?7Oe+_r|g&?YiRbV2IqUL^B_U*Dr-Spb7Rbq3%%2BB=*Q6eeAQ`%<)p$WKWOVzs zaEiG%S7RxqHmJOj6W~xN`Q#1r)9F*)q@ADXXT~w+06Sv~YLa`bGNPqnZN z1jd^(4ut;LFb@EFkJG-cHF@8=dUy9$ONldcAkno7YodkSZ!>kN!~jIZnOFf)1oah| zG;tKlsX};xAeVYI$62}l>Fdst`)b`l46|bSZin0Q(@1?qA{6ChEQsiGP^7dw zMBRQtWs(yHFYTEC`X)2=B>s0?qJBzw3;#zXF*~q8YTXvP<42f^37O>e zt_p3d;zzJ;r*vJ%3Po<;XSnSosb7H*4MEGTW`?hn>PO`lcgekMC6I@N+wJ5@pVb~y zmrng?EaDPBJOkoYyKV{ku8?Y$QAc;|Y}hdc9YTOaC7tt8ob)R`-II(8YNKu<@cPex zGg{Or_yJ}Iv&XREUQ0HvFqK2NL;`c;rK^8Mt?T(|l35g!kJNKEKH|929A<0F zJjl!9!OMe_m%kMAJyaKDag1W5&5+(>q3SKA2vQ+Msfx48___0|K>3gwrOzzD)2R^3v|ef?iQe8o&Q^%{V1585t(c&9`L6ij+_Db^G?j?f{F zWOVG!zkVWnzc@87b1)aajPO$k?WFTVZ z8n|z#Q|r24$2hcY7nyPj8DguZvI%Mn_p(rY|6vXl_>WBHYuA#@#wHan6L!C7xRSup zJLzg#KNwLbU>{vX#W~;DV*?m~>RBSHCd-!V?`9PJv|&{7_AE9SI3jVF5c=*}xhjQ| z@+2u)baGMG4UFqzyC9(_-Jw*Off9Y<`gyA+*q6$F&rb}oD#Af;UjGGQszTiil+hxL zi0d$Mp-ljk1YE72X^wi@eP&5u)jeP!7;rajaUYDPTJZ4iI!T-Cr&%&rN^ah&YRdw4 zLUpV<`Z?XUR7bxG5~0=PO>`~4satsX4-%aNx%u80Qwyqf!VOt9opQH$mnZi>AKdPa zR8GYf2#ouJK==F8lQdp3eMSzComE&00I`MDEWm0~JC-%%l=>VLl# zI1)K3Sx4jCFje60XV%-eSBOPnBr+ z&~iIhQR#TQJxPsNv5R@;^Z6?|3Fz{+8~0_7v)-Ic@3Qs0tLjp65-5|3E)uEnm#8Kxud3W&xk;8`4ZW$`-GK1dZ)SmP zwMgz#keZAdBrv~=EaT|=LG+ZRwxZ3ZNQ2XMKz3p=99}&+*(5fHt=D z_xCG5`*82KX4X{*w#J$3YIQ*w46C4N_~i-1rq2|dlzvu1Rcxjb>wZ;2vpm3XQ)S;Q zJwpl)`AVq@Z?=7DEmbPb*}?^wR;A)eg5dZD5FL()jx^dgrM5$Qc3B`oSRrj=XP*BA zS$?~zrMs$-X8`!9ICbp zl*z0O3o>HG-Ap6TddV6Ja+$cmPvflN?4KXxe)roJ!>E5_92xQz>JcmC`BGB!w{Qr> zc~hm)qugY0&cEcXdyps?5vL);p1UA!OiUNX;`)oJ>6s+eKP;Yn`l+rmM#yORJvF%t zV1cI`H)}10Y`eajTXyo@n7+>cu{|&}x#eAZdRZ~xZ`~ZjMk#;f$Ckyo(W%CrjTI47 zLG;F*(0H`iBXWScTlnnk`_$r%$npAfnx9Pz{r=1+%b{Pjx<-cwD)xkI{0V=Bpm$Xt{G3rzCz!zQclZNwMTySTJ8A*JztXgp0D7dY>` z?s9hcNfCzZTkwL!!M*Mhn1|F1C*FHgey~K8hv4rE!5`E*Nhl-zg)#Ci5Ca5t>zZyv zhd3shl~jb|`>o@J7OjZzHG>uTKE%u#GzdaJb|HLz-#QN2&RHCYL8REcc8|G55;vIB z-FHK)-TSjg^lQzWCsQy;R594v538s2Jk+Kmw3eTm0VvRpH>}zC>>8FdFQQkUh|pgW z7zZ*eEu4$|nLHtk_x3mc-Jc*Q=%}ha*&wTREU5 z=wpfTjx*NUSJQ8FY<04+u!j!)BH~La+u2A8i?eS`Tz4FPb!jKCS8W%U zGjinlC#7E%$hFzT(d00@_$AMbKhVe%`rtf4hp3aV4+QoQN1;5>y9 zw@^l+6!t*CdB-U`@q6`P!U9gk{9?)nCX50Xi6rF`>KsqP&}Qe}*Avhd*P-2u`oyY% z3sS}gxO)ED!$Y1uv2^Rit`o~wUKb#LE^554^ZU*pw;C1o`; zsLnCB-b~Aj1)KAAv^hhMy>3gopE>VY)6(7Vi|-dY zx3-2QVwk}Zo+h~h8KhX<%2a^oPJ>E8ujdsm|BK)gAa(HbyALHBy*4LLH=pHr#G@j` zQqmNU=hS?CK>AjdpVpB@w!v4EDAbRkh)KErP)!E;wn3#?x`to> z%UXLr2LmhRV<}-d=EM4!p=nwZvcH$2IkL@#o{Z$m7|06|m6hCSKUj>7st>IN!sK|2cPugqJXCW`h2*sRg9NW(~*-x-`3{6PVz<4ou;!kYFy z2M2jL&HoNb&{#y|E~J@@6F^rJA&tv5rb}Wv6DE07C*misB5{@Z1%+rflfWn;e^($& zf8LLG`zuKiGngeX@iFb!84C6pusI1u!?b)D@0L8J{!4^_zNYr@Oq)(1?hEZhYFS}h zz<`zp}osywnneWlr);nIin~)s^(#Hw; zcK+(+u_BHW0(*Y?0h7ZvKpHH+&t-sLkgKKDXQspnSq57op(6re5Pz~lAWJL5*j3F8 z$=?0On<91Nnp`FI;)5wj{{_)9-w8V_aff$|(^Na1>LbaTe+w0w71wT?koR<6H33=(T7p zsCH-@BxV9^DgbRIXi{j;>NvCF*-|Af`}{4BH&Rs3chM9fggUgdaF{)L=TYb0N|V@&uT$`E`VvFTpf=yU z#oEkL3GF!2fz~$!W1o#Gt-B=a?jyE<9jlWHA?TXG~r!6_+%rR*BC< z3pz!M^ukK<&CSgldlK4~%SgF@Cyc=X0KdhCyB{u)p1M+>K9tQ&B%BPr$2>y!p?|@Q zMBf{?3P2x-Bm!yB=?SsA&VdQ%uQEwYNyU|4GS4lyKFah$MTr4wqyVNlxB}jU>nOG= zVCQO%xX~{FE{Vze0R4j)NNIdjDTjE{#MpQuj?GUal`Thq{b{BzkipO)BB)lG*f(i; zh?_P8>~E{Tvqspb6GRv#Q^-Tkvnup=mK6car?B6d#;!zRs%H(b_CPeuEYdXr)8t9+ zAUBvBNc^%av{J!=oCDAEG3Apj^{LfQW;uIIx4LSyzuAN?-u`L5n5b3PW0gwY#TC7l z5RM#5+8E4Y1I}&ny+kPGpsTwSDq9y&muY5M-QUfqxk@9LdmZ8pk%!8zQl+^-pDyY( zpB~znhM4s7m@jK7)l_(>XH8i+(v$jTv^}a(sZkP-W1JKd=%;w+0kz5GOuOq-aCg5X zL((4pIF|4;d>zoYB>PHIRN4v!t)kzPw|yzmG73l2zRwPRND3k30mKZj35Or>2H$=d zm!1j01}O6>5;Y|!_K5(7S;NesRAW&laR5bRbn&39z*AhibGhCm7WW0NEQh-~0W0vj z65NAK_3;aTzfW@?OVwo!j46rWqbF@L3k(&0A+!q5DBLq6pWUNO=AHdSmp$JTC@go< zc#Kvi?PnT_`{EtY3>Wym`aSt{P#2X2noP)pcIhC0&*}3}ajLO2oLiC16oAf7DMudj z;_htlp=`)!#A@+}4F1iq=LEr-m^f(9AAhI7q!Tx2@84r>lzS=h<-Pbj8qDwCAHBbi z_UJyE`1cyPw5eo41QlF4yBrSOZzLvsH6=g>P) z;>n0%q$*8+`?9X1FUHIYjrUhrg5WUk54nS3OhQ_4_w*5P%(E~6kO#94oL)cGJqGE1Wt&qTs9lR z$7z;^;JG8@9PMPeCQ&xD?@vx)bDrM!fCi8thzbItzbu%Itc3kUefP9^D!U2Ds zpZBO=lRxuZMOm4Bm^J@*kC*UtnCoVEj0pDH$_y~P^#lWUz9_n(rI2bE#A%n>3&uvmqf^ zbV&)sH_$C*P?#!K35Od5!*+-$zoeA5EwZyvuMG)HAs9hnxXRd3P2fZ8jtnk2^SLJ>7t0@R3K2$UY1@wANn9KFSr;X zhM7+UL|bbgZ$-a=Vykh1g*AZR11V#XGI#c%u#Wozs$j0 z=tlQdAj1IaYraRmt+PnbozVRJ2>e37oCkb=tWlk75S!Zmt012`#ls$lDpJYo;Q7Mc z2e}$)W>U(^%G@Aq%IsB&mg7nZ{h5tHJcV>0KyZN@AkXe@j^NQ1UHFc0 z7_8&SccQ+Ql(gO?(+JE*08l`_#GlG8vt6t@|JtX#L={jFSxviiD((~i*XSpyxjxbX zb#2)6%t44yMbh|H+w&opS%EiTz?4JRJ5dX?*T^YCn!CtAo+)j zk#QRzl-hw!BtQh3Cnpjj5(Y*}L-k0UaEAElGWST!e)!8i38@)kx$oDvY%xpTJ|lBA z^~d6nP5q1C@CB<;@-KnWd}3G~s*qQcy&^rC05^4Sj~AcdkHRnZAI^Rj#FKu(3}%uF zZR<`Dd}~FT`m#nZ7S?_ey+VgzA9>E-N=r>ixxxU(zNWd9I|0=8izpyl2qT-V?`og@ zg=zM$U*uG;3?J?s@JHELy*GVwVR*of6UYW=1;A1S^F@Y~ybxm0V$x#Sv1m&tR3L1k z5}1Z@fZ0H^J-Te7anrC&1-b)Mx}v76$Uv_WC)sa;58V?tTk$=4Xg%40<?PV=vtEZCBWh!*V)BAnBz}dw3}$@}=0tbGRdaa41Ad$- zkLMcqSZrQQ;#52`vFtPT(05-64~e@+?YTLA_COdHbp(|aE}Qn*MWtVT{}#3$$hUM& z!SUPP6(!>WzZ7U!M2UkgT>;zcS1)n!zaS6awzPK~6L9=PVB}M#O9T(N zHuy5Bj+oRM;P+RwbMg>*Bj+cGSF<`3m6|&LdsY#$=?JW%kQq`@3OH1R*P&myZJgfC zNnPB>#=Ws7do-q^qO#w$2yin~%BiaO6|lrElCEvlI4|Fqs_i3Y6L`nfJz{;V6{H@d zhkI)8RJ|uSVpU0+#$s1Q8^Pm8YoX6Pkb!9Qm>yo{QVz_mghJ?RJ;_pV%{l& z5L&>etjG_`Azu&nFGOyM8D6-yUq_0XK0AFoOrVV-e?MRN3!3%EEY(oaN3B7C78NjF0LS9S2U` zbz*5QCp;yIQx2n#flO0EWEhcgI^idmM2xV90Cx961K6Yhs&jK|P&7N$!S|E;OI@=` zC0W1nWay44){mLa5wZKRPvrAyffT*1vDjGrY>6lDu#R*$0>kwb?h;S$f-ifW1&1Sm z#2_-Hr;uVgK}(P~n-#kgYKC2Oh!GJ6NmR1<3tL<7c{##ic2bcmCk0sp z%fy4;eD1I}^qHIiO=)lnoULQHz|BUIV3tVdpNZhaH@N-JcDy}!m9SwS#ZT~09fiO#Ysv!ons%{sn?XZc+xqR(uw%8Qg8F*9Pq z$OA)_AGq6D^|rJL_1030(_o_e2H(Gd9-2%uMBP(JM*u^jH=pD+7cmNqMs~_R@A};u z4nWLJb{H*~k|`VHt5lszFmq&bN=gPI{rvpK_~pdTa4a$|lnAoB_ir%o8?w3fZnia5 zAoTs-5f`LMQ!_F?9xEh}1*oylLvmmcPj}|gu8+E?39GuEqz1bk{@ybbmiUtQo~o3P zK6mjIm4&si)FYT6_k#wD`*#A=7G!;R$eYOLUY}dA`f;$DEE0Nf4s62s%@A^!hkDb# zf#Ir<^mM68;Zao47 zZyb6%b?;KQ3M*GJ-Wf@ppN>o1Ty|4x)>3A8VCR`A1bQQqXnI|0wwcrx#nnw=&Edg2 zXXuAd411?WLw)mRmVTF|{F(I-{CI84+@ibGtmk39_3zm6k@4u!Ho}@af&oIF0@HzV zSAotV0>kv9mdqs65Yf@pz^YHa;%CH0PV8j!krmz)l{bCV1JyBW8 zxh}+06g==h^pNaD|2wAUvWF&zCoxs$M*nJv%0SXxT0uc*0-HL!L)s+y%Uj2~W~a8> zxJk>8uIC<0RvNKBYSoeiVL8&hM1PxTD&tTMAvv}kAw2V(@^2Ny^Jr>5cC2>a?cV9E z*47>P)Sy0~2YsvE-Yp*_o6d+Gv!RiBC(!xk6&;N}v|#3);?Ss$ki6K>k78`Bz{Zt; z5EB1sJ8Q`_M#A0J>~*ldTLluJAIqOq?-WOlUlS0ISMi5VWI=KZfrQOGk4w{h8zJ`@ zwAcOxG_+OG_o($AIqlEdfWo3aL!zSWRDU~Nu!)Z~{Q=%=01R)1dFa3IA<+&uoUQnJ zP;~lKpkihPHD-S63%KbiG9b;O>sW3g?=fVe=?0@9cAV=<_odD3?UMii9h7JrHPp9$ ztSUVjh}P?7)p-qIsksO0S!>!c8K1kc%WR|*4d1;+UK5G8c&u%qv=T0GIpxRiglE0A z!s@#&UTLNt-m{0dwL5XVB0ZH}r!^nOEkP~)m1#_xBkW*@qBoBHN-mm8u{1*;!PTB) zthIhNJ?&X)T2oF}O*tJMUbgOnS=rSoL=8ObAJz>&dhkza%chiK!U$pVr?f`v+6DA; zAZqM?$xs&AW;5B)^cuMZ7o0mD%Qn?lt}ZD`F!g8z;Y~E*MD(J%jRw>vp*~n`W=jWO zs!<@JgbWhvG<5b)tLx4Tp+@0KR(Ih^SpPgC-{4S#!%6Oo*H>t%R^0jEldXH^&BP{c zR5Wj;0|Ye3KX}vl_}zUyw8dQcm!KJ7PeFPLi>d3xlL(Ch^Ix(t zrmJeUd$(Qt_r8rb9PUgRFr55?Cd9+sUBUVjc1)U^k1iBHta$WRl#@#KcvC0sdct(r zJVY2qL;zeG@Cs+bLZ3-Am1$TnWPM*iC?%Z>{&>yZ$T0_Rs#q6`O9j`;4sK=XX|+&; zZpDm#>F$&BjYjRO`dU@onIE0M6~uLi2VOVnWDQs-W?ElEuz(U*+16 zr+xTNRAR2X=`)qxr7^WmoG|w`?r2?Q5BS?OxkWb(|AwXA^r@IrP-QI+@m*{KXMW*p zVMF&b9-;vPTQ7JCj+a$(a_wh`cHBJTC&$6&AmhUL_X*qlYLKu-?R;EO1ZYK^dwJyu z-2CAaLku0xnt89i@b9CKXl8+gftg5o=#%|jJT3Lt92&a{;`>0|1FTVHHFJH2KeTA#VSjF_;1?LIxWVheZiu2&e) zIZ;jDSG5^|cLrqYO>0bA{+M-^{`fFJBsYTpz->x(#aMm&I#g#3tzA9ma@|HJF;8|= zKaY&$IJ;me&-@!5;yRSoer?=xc$!;f@hqNqY2|eVo}z#R%R-qPi`G?*z-d_J_^`do zM74r;0W0G|`N??gV%hq&=o-h8JDP9v9rI2 zO~ORoJa-IVmp`F~(ooZNKx2fF$n_trsqvfLAHF&dpB!t|%W~H~`>chynvnUGn%gDB z%{Fw-_*7yP4&#_jF=DB050O&>(=Aq+T+#0Yzj9QK)bpwc*JX(PUan1_Db5T8QYfxo ztAAw2K(b|Kl-Xdsc%jrLVB)D-3n2gIM@>~Hk`vZvgs;mfcXGA0)@KgPk1=*9{Nvbq zDuqNS51vbrMVmG*p+T-LL4`=GNPutPvzQ^<-qg~K?9Ek_*hCjji=zmy^#tIQND;Yw zuaI7hL8JHhNOzt_t9C6f_rfz$oreN9{%SiheHLGS`6>&OAYux* zC*bvc^VT1PYddbjPQS}9Zkwj>+lX~X`(g`B>y}DSROf2YMND*HGqd`yC%KJga%<23 zRDcWN={Z*E#P6f^uajx?oC}jry+=-40!o>Z*jwEj^l)O&FDXy*s_1aEpeEkw)))EE zWI@lnpGFv)Gh{%@RA)e&-ZX;(Xr%*EDH2!<>#H-35|I1h~X z$W+dsw#lbY(b=vfTklkH4!X##U;|XfQwCtsxHn;wr!2E|8~fjT6AA(pwCb)};X)ji z(T$56XNo1|e%p;ZJCfj0(CG>1MCh)Q^V@>BqgT6VNzh8Bv`rh|Xd!@8df_zWGZMZ& z_nXADkW~G}lRi!Y-4p=)F`!e|Mp`+jBd0pxO)Y6_KrvXz;OmagqysrZO)}@lj#i>z zY)^H4bu~1~qCkpXWYq?T7reWqo26CTqBK#Yt82WtFtLEXHdzOzcD+d(s>zFxajm;@ zm8tJ5PJi~{p*@bss$E>{q=a4&_jnV|2@G3Ipt`gmr^M2wSgXdnywQ4pj#;&@hPQ6} zlx(=YYlE{F#Tv-jT|uE$yQNR zX0ruKtayNH1oq0rCQdf#3#G`&b6KI98U*Li(iDruhWy#8D`#h^ggKqcnxm$vargo&pae<12*1boh7Exf9TzewQIa2jO1!^$i9@>knl!eo75-BkYod@{!R%omlWvE^Rjt5jsk7Xc4I(DS z8aW)jdX)5Z!5P?obw8U(C$*Oxel?PIu>h2RwI{fwZ$DnfbASF`ptdQpXccg%GV@58d;ZWK z4n}+or#|pTQ(KyysAgaxe!FqMwhBw~NjE94b(Lj5l#kte+WS4=5i05&)2&p`nWeRv z4mh7I$chx2)wOlI;kIZyky<$Ao>R|z^)$EOF((kp8UU$>@(e6kOac4yVI-E_RO14q zY}iyP`j}1^6_0YbN>RtX^E21 zC62uU{wlU7+`hJ!Wm4(cQ_dhoLWT=5HeD;JA@?$_U`@+UHd%^?(XC$C%u}obZcrT0W;S^Isx!j zW$oF4|qg;M(( zYxXW=20m3to;Lb?bn|E&T6Z^g*#rEUS62{dPql^bZoqREzzLG62;5cx$G<&DHTw*! zU~EWGcmMjcB`YMb;6rRh>%E@?WCc>~3S_eD!`lyXzzhW9o6nrO-gRR+WqyT$ww8Uz z(}HV(dp|t)6dZ&g_Bg*1$`qgh%dD z8=1}}IzjO8ayZL(B4TLCVSq{Yff5rYXW-F2{ki7R1swBMvzNxm;owKK5VIju000q0 z$HoJ=A@r||kRKiBuWl$V@o3#$+{~j0!M{1hwv3-tp6}EMfd}UhTD7}!-0L&`1*V8* zz(D8b-Y32AuI(C`c6t83WPx2rgu~-8>OuTP0SR=mVmEE?653Ml&V)Ry$x?YFAZtLn zfvHw#CKXuY$R40AG84OSOz`@m!!B-;86KYqp1Z`}+dp%Wz|lO(OXG8d5@R&!H1U~r z2U@dhn4JTE)wZ_oXFWa+h>=L_d{15VTudtHa8xu=k)(b16;`WX=GddbIS}ik_Mo~g z`#D5w^URe8Z5%_KI7v}^P@9>1lXn&iaJ>Q*p?vluVPeFBl?Z%LQPrc9`Pv)!G)T}9 z{<`@JJ&!(D1j_oNXJJMXyrg8968Ac@chVtg(!YO!yu`HY8A|UBjMq{k7ib zz~?WVpS%Uf`iKMOCCVi=3WOWe$B3KE@7^P{K!Pupb6C5{DY|`l=MRltc7IBSd5I2a zRVf!QH1iI*GFh^C82y@)tI>|so7oIHbAiENkwL5#Erg81bQw2vm#r)EgfuTvs{@#P zKsBK|reavTo0~~Xr;8@nyCIb-Bglzfbk!Z9&87Q!)6{w#z94XHFNdRMFE&G9QrFyPGuO0#`_2v~S(S`hl9 z+Rq~WPKJRJ#A7eZ{LQ`qg?`9v{`>%p*4nK0pmMy4s?C@AV75}nfeaxE7bTQccVI{V zRNY2(Y~zhO!1$ojbP2C2_wAUzVd*;H{xpR00d@*&RQU8blNyvby8=e^ThwGw<`z4) z+q*Hq36{eFeo_1QtW~GlMf1&XomB@`mi1Kh;CM8#pXBY~nspsf`RPpv_470Gyb3as zc&T$^q2&Nm7eutF{s+cRGIpfU750^ChN(mu~4Go)v! zax!$?B;(8_QUCX}<7eDt7{3c4=ULtx!T`~H1W>eOlE?Z&=IqAS!eX|&Vu-oo?vc~m z_xZFKYb+qU;&r`ijgz?cGq=B0r~X>tz)tdr8;1vj3(Qx2hMT>vd}dbG{i~{~@ZgQA z6%#pf36Q-y8lIqhn+PafUlMRKj8Vm{P>u-HPKikt3_cGF&M*P?kPJ;^xR~BkbYETI z=IF)x*QinJPRjxQ8$p1!i02^%=dtN&zLVe%i=A8#m@i4o1C@3DZ{=P<=OKMe zMAHDCe+S6aBoKLtW&D@rj9TRopqi(z+UhmcW!nCvbk*an^wFLKA(W)>|EteVZZNPB z&lBC&fd8QihgVwn*aCwfoc@1XGeQC>X*kh`Y~nu0IdYldNrTv6helFK=_H`?5Kz^z z6CG%^kdKay2#YS6&+(tuYAJhc8H7;I4M#u>0H505BS`?TekZIccE|t@*IsNInl_3S zFRIyuEa(mzz-0jv34qGgZQUVeJp}D*xXb86YGaBz;n83P{(S zrc~j*x16HlffevCynlKXOf|6$LI-4{VF-}RRMJ`fYOfcQiQS&BKQ51%@+P>60o**X zq>BHv+WQ;6f~GmPsELJ*KT*c}n)QW*<=Yc8eH=p64A~*{a~sLZP;p>qOH+f3e=&BZ z7yw;yqShHR392Hoc=)_Kl)V^_&P+ttDPT)sq&#y?oL+!G$5?HE2gj;qx+sHsc5CaV zt+hPk<${6mown+L)p?z&Q&`s6gFl-d!C6E529?h?ol8Dp&;wBbWHOYR#H$FXXU!)- zib&G$a@_fyHFubqQa4I&d+u~Mo9D!s$&m7295f2AY#AD)K;`c|$lpfOa zvfHJIXOUXd=syPnGq!D4q3O<5&r?^;#A-T6%n>iuTu$nRP!I;7qzYbl$A#xQJU0t- z3?m%B*r6)PtJSa7ukA}bK#YD@NCBbG=uS~4z&3iH88Ou=aZ+%AADK5e=TAHtjO+x=|_M#_4?a2^`$GhenjiY16sSQQ`$q>39l}Kkr*fZB3eZ@{ zP5CcV4G++Rlau()PQSu-EhY`6l{&A=lzKu{l~J4JT0XtVG|4)v4aoi;p#U(H=`dSF zt?d#SH{pLqk(}3@Rj8=+Hju}uG=z-M@T+EQKe&r|9^SOSrTA)yuYt)mF{{vhBm#Q_ zYvKo5+MH2lLxc@IZ{LXIi?Qn;`|}RZ(Vs{JQ%3kIOD2N8vL`|}Np&v+xmfX??VSiX zgfB^%mG9l7I+788ubL%GsOsQ!B?JibefBrRHy-zAK&8iq1%rIlVY7nJ_q9tbh1|-@ z%fC5oqR*}a!lyZOIPN|_9v=Z2E4LbtZcuvw# zkB6UlT$!P}a1T&#Lk)vXH1i2?5fj{p5L$ms4)7^Y582TLDoTvJz=|k279H%XCsEf;D854UTL(Z{)SFE4A;=zpp;3BHi4?i#m7TOL%Z(%#BwEjU_^|_I2 z8qD@hC-p5*V(qTrD_yA$N=flgfdb_%pP7*EOFj;M&F&mCR72e&)7&_NP2a(b8qL2{ zf+w6{qXCCPFY?F0T{zT`d|fk&P2HjK&o83IZk?Cta5joQGN{6^D^c$SUao~e?@#m`mKPT!MM}5NK%8r)){tp4p0l?$K-ckohL@g24E;mK|F(oRG z;-bhOW}D%9Fh7%CnSy)w{_i6Z7X}1nvObbNH6&q9?3R*~R8lCmwqbcR5%wIx~5@|#BsjsBa8`T&W`uU3@I($TRqG6K{BP16waS61p=dB2Eo&Od~On0m4qgej#c4kAv~Q zrE(C7e2|fuDLvwS%&3Ao#-oYQC!c#LQcdYGHzN^-&5>6NvRV?R5rp1kdTK1P2Re?X z-8r!1Qh*<+3LpS)-jWhzKE$eMe|*G^+C;vRQ+rq)KPf}eH%QuBw8{SSYp%Z_3tC(2 zw_vL>VCD$akhQ99F7l1#pQ!kAd>Q%eX{+<=!9ZFdVFEB!fLUoo963P*X9Ihew9fYc z*MBom^7F1h`>w$H$gC&xn@+od(#9a_NkIKpG;Bea&en^0D3o*0PP7*dH5&6-c=QeG zBh(MxVd?3@RD42i=^G}o>9gliJM~`=eJEX|c=Gu8cqNU9mpM``gp`a{5m%RXhhc|i z$146$*ghtGHvGRfF3?=GIM!lxOu`};*aAvQ_I|hno$kPV#FR=6i;`^BNQ7sEvh$yj zg+2pDLJ07#;>QF6065MIbK}1lj#SK#10$0AuJql|8i|5A+smQ|{6@C+q7a<4Z61K3MA*!T<7m+d0~R*?(}PG*{^qhD|AQ-vag zStBByqR1%u|97faat;p28#){>fHiSu<*HRCb3KB`uqF61_%@BX>(+M-)#2!Scj*M& zXaCU>fwI>XBGS#@PoYWZQ&cyE+hbeLlc7j}YwE8822fXx;$opA zcrG?fIXq7H4sY(Ms6_+P3ku+fmzz|$WN57t*$bQ45{8bFGEU+u3KMjT;eCjK+y`1^P_ zA5Ok+Jst^+;4ygSn7J+|Ce?k_Gje74DfQ;w;8UOma#;KbtvBjL`OmI++Z z>O;RC8_?wY3C8}(FCYr?7o?=F4ONc7|S;@67b9yn~<<7^0+{e zvT9^EhFP+ef^0RxNd(8%?n^3fFya4v-^rGrpD)$xm0@^rr=QvElFH!wiqjx@WBPvQ zkC3dn?um+rA@`2tD#?F?S3RkP#xRaJ|7Y8*LV(9r<>10&W6|fu3cp!sUvUJFLi_%` zjPHL7QOiL8=aOV=eutPbV4oH{IM1j<0LSzx03nO0T#I`y1AKP#|NEIY5EhPu3zzbo zU*RYPkp2pjaji1nF~|6WpGrSSxT&y;_J0!Y)GI|?>l80umb=^Ut7|yMm#9?|miA8` zasbB9>Q_LmcvRmllH}1OA)KY4U1TUjb|mtDQwnWXZ#s=mZu#t+%wt;a?lkJe{iTD* z`NQ3tiDD_XAt-@ds{!uAftV+f4EO?K2MAHejFI}!Wm^Uyy)c6m!BwAs2^Xl?9hz9< ziL7)7w<~h5UF2Ad`@wb=Zy&13T4<3~zBHM=RaB6>YOs{-8mubO)$i-s;*qLR0Q9uX z`|--+fph1Pv6k3TSEMmlmsv2pMhWLVX$}(~_o=Q2XC>iM|LJ9pjb*8-kL4#Z^3W`9 zl3OS;8V_Q2HJh8|hMWL%z3vSO7H`)!Bgr86Sm2Z1``gNFhAHr!DnYg)6 zRj=G3e{#L9&hFx`M?`*r)F_s$p2wL8)>UL@bLw?G8FYINTx4;@Jp10dESmgM3Q3$m z_!Xpix|ML@Dk{54u020~z1cND>;=}Xv}~~gJlGz0V`Yn1=}c)pgDT*BFsV-(9VPUGIbV)r_&r2^Ha1YX&(p=BhT~sfPH*ORxOjxKb=rp-h(^l_Jl4(@C z#~sjTcsi)mqY;J|w zG%bRNb;?iru`w}#t7)j)$)O6|rfTfnl_LA>?@a=|ncBdaEIb+`SF2r|IOukNR&rTq zG0?}OtAs!X#|@VH>B-0KSt|BoaDnTq|10?F5T>g&r9Z~mAe08c+v-wdp}6E=Fy+rF3M540Q4edJ}z%`M~axhGXd~w#lAo40{VXCh}XjW-vDm+S*?_IyH z6bzJ+3_bY;FZ>1&SP73uXhs|sSdHy{Wk1AV(BgDluJgZXZ3CAV@aO^0ar#FsUZ?)$ zme+bd1B4S6IyGk%I|-l1?UEaw$P1=G2gvV!n6T=sM10zeo6T|FzZ8?zn)R*luJJ0y zWDLp9&Mu@ySe+mtW;`b4WovO_1);ak_n#At%Nu^((pLJ8SAUlOmyU4t&&9XnnYFFs zc5sZFFI}lY)9Dd9_m`6&UQ=Anm!V7(Y`S2eKwlL^t}7;#kCF^1q@paJUj5E_cad{$ zKF8F_18~nQk{u3FRArM;f;UGGU0gGH0eCO-tfuu-5?w8q3{nHPwvm-#0j1;lR@&U8 zEob`8L_TltWl!9QW9lPJfDUuOd)s_sV{%etybe0MQIMeT9;o7`#Fzi00|N7~?l#>i zhYjVGY`Y7Bz>Ig0=%jk_c<23HQE7YjG^Six@5{NwMxoCK0@FuKm|JgLzE>D0{q>-kL>2J|KTlM&*yv^Ov44%%8S^COzgG*P?RIX zaIJ)J7e0e8^LRLPww)=6La8o=-UC`c(`sljEyjfliy!Gw6ru|Z-n;yZ!1~{W=7moo~&4^pW3GlzpmOn)QG~x znW(sH9rXX|6fJz#dV%jumQ96MN@sg{3{lIIL!AWcdT zQ1U8O2t|bi=@5`AHFT9C%|>rhL+?!lq((ZSNCyR?grWixiVz~85}FuFXriI_Z}8?{ z|IK&t&Bd%)v(}l}Gv~~nefD{t{m3F$18Pm6od;1?t;knbU|XFZTx=~#B$pP6)%8X} zS}{OHL=nz*VcTexxGjk1ZjIaMq*_T_##cdBF>Ot^e^d;B{$EP}do3sMh1=!i_y``h zv70{V=^4*#m%iVOpRcjg>TdI0?lLXqwr?C6W{q>LG4pav)`=tuo*Co(C!yk6UOVo! za`y2vsRP4iNd*O@@EvQeX<=ylAONaOZEj!!K#ybBQ0cc|b*=}6f8bdYb@5jucNk@W zYBnwYhcob}HMK2kFA6-M69CNYNzo%c7ow;0@_!`A2fN4NQ1v9HIOU5S;Ox^6Ie5^> z)Fp=c=il#uIvyMQ zXqI_4Q%7aP3RO59`Vt6~pL$sfGC&rWdkr;D<(?*#`q+P{FHjJC0okqber0;s_T9Uc z(aoHyg{uKt9c7-!^$k=0{(^jYWw*L}F&Rf~M$F_pmNR#U9Xa?VH7Q>|rN?_@EnxpR zmbL+gW~@5}_#0c|j(#9R4~AG&mR(S^MF}fymJl zo{9#oL7E_)2+riI!h?_2Na=g$mFXe3n|1V!_;LzgzO1q0($huwC@Cg`l}r%99xvw! ze)ap^#oZW(8)yIWVV>WQKMlNaA?AN}A@_M#-!ICS^-lv2hXR9OCLNypDl^kJDYQO@ zlWU$&>fIWb1JW(tO+O*J#28H#CzQJ}zm@syW`NOv-JQ(k%fKsH+c&XWD(gCS?SJk{5{P3>beXhrdunZFKGLm zkyH2%NcxAJfH*pI)goEpaX5e@C(fpL7yHjF>2@CK@ zez-EF9YleWuKVfQ5Pb1^HgS7<(xo@5s(PBoJ-o{A$&h=_czanXN9+DMAA!XUJ4!@N zR8A#)jqbc}G>IqjNdIJHkh#|@yHd_}2dp%pBAf^vnKV8*FP#A=D2ppUGJX1v*L75? zZ5^>nyhKWvK#HbLQ6)*;`mi?B^nDR{Hb+`BvtZpBmyavA*ctz=IvkCeP>2?1CuUv+| zzH-4Wc`iuW622)Fxtkab1)KWmw6Evn-v1WZVAyfvj=W@k7WH~QV77bVe5Cv}OF5mB zfiShJC6ScshB7Oy`n(I2pJ*($r} z@;7(iWhrNp0&Y4cd^3`*vV8u!CYX<0n)WSgyFPGfWpxFl1c<1I*xZVX7mTY^?G7Pi zP#tf8N!k;=AoZ&`3h3aTn=`1~T&pPsW_MA|R5w0;?_3(_2M*I8CoD(S5hubS^l!-#$^f+(KF%Z)1NHly&I6ZoDqwA_4+t zO{$j`t|SQ>%s#(IZ?U4ykjPfi4Srg_C%I<1Sw&VmX%&jeOo(H-S{S5}CoJySdA!Ig zPLvJbI=K9O2*5JU997ii*1CPeE?=)yBh_%(Og7K00Uev-`n6g&8~ho=b@xNx_j2Et zcX%6d35ji1ydRersU8?t@exlQ7*I=^p(ZQw)t3(gW7vL0?tH9-q&fBmJK))yTk^yP zg@H8^ZjUn_Y;K6&vC10*JAa_vf$kImt-1f;d3jh&5418R%drbkL$kQjSCta`jdoi) zl=~zR-~h(K+f%dRw`3@3!)XgWsu#<{oK{5!gP%-Xmz4a=4css-i7GBU-LZ=WT@^Vs z{s!C}2xJKKnqQAy1JuD-+D&i6zbuq=!i>GDW>X*T*=8(QSXKGVPT<8_%CQb{rkM39xdir6>OO3OCHqc1QoUes23v<;s<==shCt&t1KsVUd~Jmmzb*XDGTQ0a`*GM!N;ARQ zDj?}dLXpxTYy=KD!>3D;lY)i(E~p;5F?g^)Hq%dCt7 zPw4fo0X1)*klzbHE7j0QG!K<>{&V-E;tq6C0>S|xCDJZJ#l)(tROR^b7sxJl&06XW zwv&5mmBZ@G^g-a6=j<9N?N07QyR;Cb*To7}WMIjWcR#1i;|-%gt z&-IacK$rQiYNE} z+=-MnJZ#^3Ad5RE0lVb_8wq@Vlx3+(@0VOXK~p|MF?%v3;RR zW62Xg?n%Qcmz*Rj*B3o|$BZufWgL!1MEh}^iTbey{Xq-Hqhd5A89+%-6K5F6Vl!@1 z4ouOS;Ww?GANM?fUmrX{8@`=+kYZ__tOrp-pg*y}pA}S}_k*q#_C}PhbJI;=8d_fO z(gaETP-8!R+8U7a0|!XhsxydjO3~;*Of)1pd8n}(4Al&h91O`nU+<&Rq5#3xfa1r- zTjr{}H+lKlQ#slP%e-m7*65TQJMBKL{wSIKaJQJ3|0<*^n9_P@?m)Hqe6K>Kqgm_Q zYc^bf-7%Ro##fx^MmlseIY$=P0Z~CoQCDhg^G0e&vVn(?CF^1eJegfEK=mfmwbomZlmB*3(mhx<_C&Qbu;|~55ANjpbR#`jINg60 zr$%8BLUB@RGOqfTw#+{6MIb;yP(8s@O_y^~I#!q6XH>Qr@wMZj2XBO0`gUutkb?y( zm4+7o-vit8rMuXWca96e%IqT$q#Qcw?l%yo;a_{5DSJi!K!`JoftXcIglqyE@f<4y z*+H!$&N=ozHTWKT0-%g75s9(FS^VU=U5tZhVyCIGLT|)}#UYRdIKg{om;dbLp4sw$ z*Vr2C3;*Ezr=GD!^1B=GD5(;|3-iWKoziCpj!XtekB}Xma9uq$7@SrSE{H+iD-}T zf0oy-h~CvS%ERx~KhMvXhI`rk4P8uJQ6$dSaVU!feeC%pn@^MCmqeaT>8wxiDX1(3 z5;E9Qd@uk+4N&kGK6{yB%&3>ULzFOr%~M7x+Gtu0zxGnZOeESpNN$NmJLlP_h`?S@ zvDLG>Qc{Qs#A1OI$D7`^uvUN@8zbXI$)5kz{^@*IJF?sdpXbgi zCr>OZlvr7uN5Mmuwo?pgoQp)T1VS%#qDQ%luteq6p{k@ko1<1KYs zy9(IH&e2X;PHl{B+R}zwRr}0lw4?q_rHFBuHZGn@0}lJqKRa)8{a~Qybc9eI1Vf6f zPCp>gH!WrjBl%gYT}8m3n3402lpvHl%$Z9`LD;itg0+1+q7xucga3 z{H`S95e1uIih!5JviD@AbcTsKhY$!59}&gY8dnDfyst0g^^`={<4m#x!TRYWmvDUm zue=EOw|0JeIqKi&MMm^*>b)>lW@!4ARgbji8PEPG#e?bJtXy&GrUp>!ql87Ka*H2) z<@aHVb1`{va904gP1izXx-!(=+`9`LOUh#Inwk*yj3m2>;O0wW*k%v;wZlPRi87FJ z2a`rDNpMOEITt7ln(eQd8G81t2&yOT%^h+IF+2gz(K`ngOE*0E;rKJuG5-Ex-bnMO`mb_)Zw_0#FCz0GPa0f92elX&u6i6ct&$K6v9XqxS3dS8(%G^Z;kM zt9?r;fF=F8D39$CClKzodMaTv-5N@3G#v~a2J9^Wz_*K8=B-~xX}r}z-W^^xaQoyp zEbG*SV1?TmIdf$H)c5rP@|;s_3HLf F{{zbRop%5L diff --git a/importing_external_data.html b/importing_external_data.html index d97efb14..3ff99ad7 100644 --- a/importing_external_data.html +++ b/importing_external_data.html @@ -157,6 +157,8 @@

  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -204,8 +206,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -214,20 +214,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/index.html b/index.html index 54fc30b4..d473054f 100644 --- a/index.html +++ b/index.html @@ -121,6 +121,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -168,8 +170,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -178,20 +178,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • @@ -341,5 +333,5 @@

    About this DocumentationInstalling Python -
  • Installing Beancount using pip -
  • -
  • Installing Beancount using pip from Repository -
  • -
  • Installing Beancount from Source -
  • -
  • Installing from Packages +
  • Installing Beancount
  • Windows Installation
  • @@ -89,14 +83,6 @@
  • Post-Installation Usage
  • -
  • Appendix - -
  • Running Beancount and Generating Reports @@ -145,6 +131,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -192,8 +180,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -202,20 +188,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • @@ -258,16 +236,12 @@
    -

    Installing Beancount (v2)

    -

    Martin Blais - Updated: June 2015

    +

    Installing Beancount

    +

    Martin Blais - Updated: June 2024

    http://furius.ca/beancount/doc/install

    Instructions for downloading and installing Beancount on your computer.

    -
    This document is about Beancount v2; Beancount v3 is in development and uses a completely different build and installation system. For instructions on building v3, see this document.
    -

    Releases

    -

    Beancount is a mature project: the first version was written in 2008. The current rewrite of Beancount is stable. (Technically, this is what I call version 2.x beta).

    -

    I’m still working on this Beancount code every weekend these days, so it is very much in active development and evolving, though the great majority of the basic features are basically unchanging. I’ve built an extensive suite of tests so you can consider the “default” branch of the repository as stable. New features are developed in branches and only merged in the “default” branch when fully stable (the entire battery of tests passes without failures). Changes to “default” are posted to the CHANGES file and a corresponding email is sent to the mailing-list.

    -

    There's a PyPI page.

    +

    Beancount is a mature project: the first version was written in 2008. The current version (v3) of Beancount is stable and under continued maintenance and development. There is a mailing-list and a PyPI page.

    Where to Get It

    This is the official location for the source code:

    @@ -278,65 +252,65 @@

    Where to Get ItHow to Install

    Installing Python

    -

    Beancount uses Python 3.51 or above, which is a pretty recent version of Python (as of this writing), and a few common library dependencies. I try to minimize dependencies, but you do have to install a few. This is very easy.

    -

    First, you should have a working Python install. Install the latest stable version >=3.5 using the download from python.org. Make sure you have the development headers and libraries installed as well (e.g., the “Python.h” header file). For example, on a Debian/Ubuntu system you would install the python3-dev package.

    -

    Beancount supports setuptools since Feb 2016, and you will need to install dependencies. You will want to have the “pip3” tool installed. It’s probably installed by default along with Python3—test this out by invoking “pip3” command. In any case, under a Debian/Ubuntu system you would simply install the python3-pip package.

    -

    Python Dependencies

    -

    Note that in order to build a full working Python install from source, you will probably need to install a host of other development libraries and their corresponding header files, e.g., libxml2, libxslt1, libgdbm, libmp, libssl, etc. Installing those is dependent on your particular distribution and/or OS. Just make sure that your Python installation has all the basic modules compiled for its default configuration.

    -

    Installing Beancount using pip

    +

    Beancount uses Python 3.8 or above, which is a pretty recent version of Python (as of this writing), and a few common library dependencies. I try to minimize dependencies, but you do have to install a few. This is very easy.

    +

    First, you should have a working Python install. Install the latest stable version >=3.8 using the download from python.org. Make sure you have the development headers and libraries installed as well (e.g., the “Python.h” header file). For example, on a Debian/Ubuntu system you would install the python3-dev package.

    +

    Beancount supports setuptools since Feb 2016, and you will need to install dependencies. You will want to have the “pip3” tool installed. It’s installed by default along with Python3 by now—test this out by invoking “python3 -m pip --help” command.

    +

    Installing Beancount

    +

    Installing Beancount using pip

    This is the easiest way to install Beancount. You just install Beancount using

    sudo -H python3 -m pip install beancount
     

    This should automatically download and install all the dependencies.

    Note, however, that this will install the latest version that was pushed to the PyPI repository and not the very latest version available from source. Releases to PyPI are made sporadically but frequently enough not to be too far behind. Consult the CHANGES file if you’d like to find out what is not included since the release date.

    -

    Installing Beancount using pip from Repository

    +

    Installing Beancount using pip from the Repository

    You can also use pip to install Beancount from its source code repository directly:

    sudo -H python3 -m pip install git+https://github.com/beancount/beancount#egg=beancount
     
    -

    Installing Beancount from Source

    -

    Installing from source offers the advantage of providing you with the very latest version of the stable branch (“default”). The default branch is as stable as the released version.

    -

    Obtain the Source Code

    +

    Installing Beancount from Source

    +

    Installing from source offers the advantage of providing you with the very latest version of the stable branch (“master”). The master branch should be as stable as the released version most of the time.

    Get the source code from the official repository:

    git clone https://github.com/beancount/beancount
     
    -

    Install third-party dependencies

    -

    You might need to install some non-Python library dependencies, such as libxml2-dev, libxslt1-dev, and perhaps a few more. Try to build, it should be obvious what’s missing. If on Ubuntu, use apt-get.

    +

    You might need to install some non-Python library dependencies, such as bison and flex and perhaps a few more (you'll find out when you try to build). It should be obvious what’s missing. If on Ubuntu, use apt get to install those.

    If installing on Windows, see the Windows section below.

    -

    Install Beancount from source using pip3

    +
    Build and Install Beancount from source using pip3

    You can then install all the dependencies and Beancount itself using pip:

    cd beancount
     sudo -H python3 -m pip install .
     
    -

    Install Beancount from source using setup.py

    -

    First you’ll need to install dependent libraries. You can do this using pip:

    -
    sudo -H python3 -m pip install python-dateutil bottle ply lxml python-magic beautifulsoup4
    -
    -

    Or equivalently, you may be able to do that using your distribution, e.g., on Ubuntu/Debian:

    -
    sudo apt-get install python3-dateutil python3-bottle python3-ply python3-lxml python3-bs4 …
    -
    -

    Then, you can install the package in your Python library using the usual setup.py invocation:

    -
    cd beancount
    -sudo python3 setup.py install
    -
    -

    Or you can install the package in your user-local Python library using this:

    -
    sudo python3 setup.py install --user
    -
    -

    Remember to add ~/.local/bin to your path to access the local install.

    -

    Installing for Development

    +
    Installing for Development

    If you want to execute the source in-place for making changes to it, you can use the setuptools “develop” command to point to it:

    cd beancount
     sudo python3 setup.py develop
     
    -

    Warning: This modifies a .pth file in your Python installation to point to the path to your clone. You may or may not want this.

    -

    The way I work myself is old-school; I build it locally and setup my environment to find its libraries. You build it like this:

    +

    Warning: This modifies a .pth file in your Python installation to point to the path to your clone. You may or may not want this. I don't do this myself; the way I work with it is the "old school" way; I just build it locally and modify my shell's environment to find its libraries. You build it like this:

    cd beancount
    -python3 setup.py build_ext -i
    +python3 setup.py build_ext -i  # or "make build"
     
    -

    Finally, both the PATH and PYTHONPATH environment variables need to be updated for it:

    +

    and then both the PATH and PYTHONPATH environment variables need to be updated for it like this:

    export PATH=$PATH:/path/to/beancount/bin
     export PYTHONPATH=$PYTHONPATH:/path/to/beancount
     
    -

    Installing from Packages

    +
    Dependencies for Development
    +

    Beancount needs a few more tools for development. If you’re reading this, you’re a developer, so I’ll assume you can figure out how to install packages, skip the detail, and just list what you might need:

    +
      +
    • +

      pytest: for unit tests

      +
    • +
    • +

      ruff: for linting

      +
    • +
    • +

      GNU flex: This lexer generator is needed if you intend to modify the lexer. It generated C code that chops up the input files into tokens for the parser to consume.

      +
    • +
    • +

      GNU bison: This old-school parser generator is needed if you intend to modify the grammar. It generates C code that parses the tokens generated by flex. (I like using old tools like this because they are pretty low on dependencies, just C code. It should work everywhere.)`

      +
    • +
    • +

      python-dateutil : to run the beancount.scripts.example example generator script.

      +
    • +
    +

    Installing from Distribution Packages

    Various distributions may package Beancount. Here are links to those known to exist:

    At this point, you should be able to follow the instructions from the previous sections as is, starting from “Install Beancount using pip”.

    @@ -428,15 +396,10 @@

    With WSLbash on Ubuntu on Windows (installation instructions and more at https://msdn.microsoft.com/commandline/wsl/about).

    This makes beancount installation easy, from bash:

    sudo apt-get install python3-pip
    -sudo apt-get install python3-lxml
     sudo pip3 install m3-cdecimal
     sudo pip3 install beancount --pre
     

    This is not totally “Windows compatible”, as it is running in a pico-process, but provides a convenient way to get the Linux command-line experience on Windows. (Contrib: willwray)

    -

    Notes on lxml

    -

    Some users have reported problems installing lxml, and a solution: when installing lxml with pip (under Cygwin), using this may help:

    -
    STATIC_DEPS=true pip install lxml
    -

    Checking your Install

    You should be able to run the binaries from this document. For example, running bean-check should produce something like this:

    $ bean-check
    @@ -474,149 +437,9 @@ 

    Editor SupportIf You Have Problems

    -

    If you run into any installation problems, file a ticket, email me, or hit the mailing-list.

    +

    If you run into any installation problems, file a ticket or email the mailing-list.

    Post-Installation Usage

    Normally the scripts located under beancount/bin should be automatically installed somewhere in your executable path. For example, you should be able to just run “bean-check” on the command-line.

    -

    Appendix

    -

    If everything works, you can stop reading here. Here I just discuss the various dependencies and why you need them (or why you don’t, some of them are optional). This is of interest to developers and some of this info might help troubleshoot problems if you encounter any.

    -

    Notes on Dependencies

    -

    Python 3.5 or greater

    -

    Python 3.5 is widely available at this point, released more than a year ago. However, my experience with open source distribution tells me that a lot of users are running on old machines that won’t be upgraded for a while. So for those users, you might have to install your own Python… don’t worry, installing Python manually is pretty straightforward.

    -

    On Mac, it can also be installed with one of the various package management suits, like Brew, e.g., with “brew install python3”.

    -

    On an old Linux, download the source from python.org, and then build it like this:

    -
    tar zxcf Python-3.5.2.tgz
    -cd Python-3.5.2
    -./configure
    -make
    -sudo make install
    -
    -

    This should just work. I recommend you install the latest 3.x release (3.5.2 at the time of this writing).

    -

    Note: The reason I require at least version 3.5 is because the cdecimal library which supplies Beancount with its implementation of a Decimal representation has been added to the standard library. We need this library to represent all our numbers. Also, Beancount uses the “typing” type annotations which are included in 3.5 (note: You may be able to install “typing” explicitly if you use an older version; no guarantees however).

    -

    Some users have reported that there are distributions which package the cdecimal support for Python3 separately. This is the case for Arch, and I’ve witnessed missing cdecimal support in some Ubuntu installs as well. A check has been inserted into Beancount in December 2015 for this, and a warning should be issued if your Python installation does not have fast decimal numbers. If you are in such a situation, you can try to install cdecimal explicitly, like this:

    -
    sudo pip3 install m3-cdecimal
    -
    -

    and Beancount will then use it.

    -

    Python Libraries

    -

    If you need to install Python libraries, there are a few different ways. First, there is the easy way: there is a package management tool called “pip3” (or just “pip” for version 2 of Python) and you install libraries like this:

    -
    sudo pip3 install <library-name>
    -
    -

    Installing libraries from their source code is also pretty easy: download and unzip the source code, and then run this command from its root directory:

    -
    sudo python3 setup.py install
    -
    -

    Just make sure the “python3” executable you run when you do that is the same one you will use to run Beancount.

    -

    Here are the libraries Beancount depends on and a short discussion of why.

    -
    python-dateutil
    -

    This library provides Beancount with the ability to parse dates in various formats, it is very convenient for users to have flexible options on the command-line and in the SQL shell.

    -
    bottle
    -

    The Beancount web interface (invoked via bean-web) runs a little self-contained web server accessible locally on your machine. This is implemented using a tiny library that makes it easy to implement such web applications: bottle.py.

    -
    ply
    -

    The query client (bean-query) which is used to extract data tables from a ledger file depends on a parser generator: I use Dave Beazley’s popular PLY library (version 3.4 or above) because it makes it really easy to implement a custom SQL language parser.

    -
    lxml
    -

    A tool is provided to bake a static HTML version of the web interface to a zip file (bean-bake). This is convenient to share files with an accountant who may not have your software installed. The web scraping code that is used to do that used the lxml HTML parsing library.

    -

    Python Libraries for Export (optional)

    -
    google-api-python-client
    -

    Some of the scripts I use to export outputs to Google Drive (as well as scripts to maintain and download documentation from it) are checked into the codebase. You don’t have to install these libraries if you’re not exporting to Google Drive; everything else should work fine without them. These are thus optional.

    -

    If you do install this library, it require recent (as of June 2015) installs of

    -
      -
    • -

      google-api-python-client: A Python client library for Google's discovery based APIs.

      -
    • -
    • -

      oauth2client: This is a client library for accessing resources protected by OAuth 2.0, used by the Google API Python client library.

      -
    • -
    • -

      httplib2: A comprehensive HTTP client library. This is used by oauth2client.

      -
    • -
    -

    These are best installed using the pip3 tool.

    -

    Update: as of 2016, you should be able to install all of the above like this:

    -
    pip3 install google-api-python-client
    -
    -

    IMPORTANT: The support for Python3 is fairly recent, and if you have an old install you might experience some failures, e.g. with missing dependencies, such as a missing “gflags” import. Please install those from recent releases and you should be fine. You can install them manually.

    -

    Python Libraries for Imports (Optional)

    -

    Support for importing identifying, extracting and filing transactions from externally downloaded files is built into Beancount. (This used to be in the LedgerHub project, which has been deprecated.) If you don’t take advantage of those importing tools and libraries, you don’t need these imports.

    -
    python-magic (optional)
    -

    Beancount attempt to identify the types of files using the stdlib “mimetypes” module and some local heuristics for types which aren’t supported, but in addition, it is also useful to install libmagic, which it will use if it is not present:

    -
    pip3 install python-magic
    -
    -

    Note that there exists another, older library which provides libmagic support called “filemagic”. You need “python-magic” and not “filemagic.” More confusingly, under Debian the “python-magic” library is called “filemagic.”

    -
    Other Tools
    -

    It is expected that the user will build their own importers. However, Beancount will provide some modules to help you invoke various external tools. The dependencies you need for these depends on which tool you’ve configured your importer to use.

    -

    Virtualenv Installation

    -

    If you’d like to use virtualenv, you can try this (suggestion by Remy X). First install Python 3.5 or beyond2:

    -
    sudo add-apt-repository ppa:fkrull/deadsnakes
    -sudo apt-get update
    -sudo apt-get install python3.5
    -sudo apt-get install python3.5-dev
    -sudo apt-get install libncurses5-dev
    -
    -

    Then install and activate virtualenv:

    -
    sudo apt-get install virtualenv
    -virtualenv -p /usr/bin/python3.5 bean
    -source bean/bin/activate
    -pip install package-name
    -sudo apt-get install libz-dev  # For lxml to build
    -pip install lxml
    -pip install beancount
    -
    -

    Development Setup

    -

    Installation for Development

    -

    For development, you will want to avoid installing Beancount in your Python distribution and instead just modify your PYTHONPATH environment variable to run it from source:

    -
    export PYTHONPATH=/path/to/install/of/beancount
    -
    -

    Beancount has a few compiled C extension modules. This is just portable C code and should work everywhere. For development, you want to compile the C modules in-place, within the source code, and load them from there. Build the C extension module in-place like this:

    -
    python3 setup.py build_ext -i
    -
    -

    Or equivalently, the root directory has a Makefile that does the same thing and that will rebuild the C code for the lexer and parser if needed (you need flex and bison installed for this):

    -
    make build
    -
    -

    With this, you should be able to run the executables under the bin/ subdirectory. You may want to add this to your PATH as well.

    -

    Dependencies for Development

    -

    Beancount needs a few more tools for development. If you’re reading this, you’re a developer, so I’ll assume you can figure out how to install packages, skip the detail, and just list what you might need:

    -
      -
    • -

      nosetests (nose, or python3-nose): This is the test driver to use for running all the unit tests. You definitely need this.

      -
    • -
    • -

      GNU flex: This lexer generator is needed if you intend to modify the lexer. It generated C code that chops up the input files into tokens for the parser to consume.

      -
    • -
    • -

      GNU bison: This old-school parser generator is needed if you intend to modify the grammar. It generates C code that parses the tokens generated by flex. (I like using old tools like this because they are pretty low on dependencies, just C code. It should work everywhere.)

      -
    • -
    • -

      GNU tar: You need this to test the archival capabilities of bean-bake.

      -
    • -
    • -

      InfoZIP zip (comes with Ubuntu): You need this to test the archival capabilities of bean-bake.

      -
    • -
    • -

      lxml: This XML parsing library is used in the web tests. (Unfortunately, the built-in one fails to parse large XML files.)

      -
    • -
    • -

      pylint >= 1.2: I’m running all the source code through this linter. If you contribute code with the intent of it being integrated and released, it has to pass the linter tests (or I’ll have to make it pass myself).

      -
    • -
    • -

      pyflakes: I’m running all the source code through this logical error detector. If you contribute code with the intent of it being integrated and released, it has to pass those tests (or I’ll have to make it pass myself).

      -
    • -
    • -

      snakefood: I use snakefood to analyze the dependencies between the modules and enforce some ordering, e.g. core cannot import from plugins, for example. There’s a target to run it from the root Makefile.

      -
    • -
    • -

      graphviz: The dependency tree of all the modules is generated by snakefood but graphed by the graphviz tool. You need to install it if you want to look at dependencies.

      -
    • -
    -

    I think that’s about it. You certainly don’t need everything above, but that’s the list of tools I use. If you find anything missing, please leave a comment, I may have missed something.

    -
    -
    -
      -
    1. -

      Some people have reported bugs with the cdecimal library in 3.4. I would recommend actually installing 3.5, which appears to have fixed the problem. Technically, 3.3 will still run, but I’ll deprecate it for 3.5 at some point, probably when the Ubuntu and Mac OS X distributions have it installed by default. 

      -
    2. -
    3. -

      Installing py3.5: http://www.jianshu.com/p/4f4b2ed568f4 

      -
    4. -
    -
    diff --git a/installing_beancount_v3.html b/installing_beancount_v3.html index 02a0b7d8..b47f2e5a 100644 --- a/installing_beancount_v3.html +++ b/installing_beancount_v3.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -178,8 +180,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -188,20 +188,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/ledgerhub_design_doc.html b/ledgerhub_design_doc.html index 993262b9..61ceafe1 100644 --- a/ledgerhub_design_doc.html +++ b/ledgerhub_design_doc.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -180,8 +182,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -190,20 +190,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/objects.inv b/objects.inv index 5221624f3934a876dc7069556184b1a31206093d..5a654bdea7a696413171add02f5f977e08898c34 100644 GIT binary patch delta 6643 zcmVy(s}y9l_GT|q(5t8m8z@1pG+if2_UiJtgGbEnWB=4|I+XOr7uJhX z@ikYxtN4GdW`9_0P22qYfBf>F|913yR{1Y5Q1LE&{!pybYk#R)t{l5+nAUi7(KDrk z6}Z{Tnn1yF>hKO2iAuJu?0WnEi=CschpsY0c8_wmOqZ73%Z`WQM1h4aUsxk*B=avR zIwY^*Y>nhuw*p%+5X+XCY8%`MOV&uFk`nsz+E%9$4(%HO1-`Q88OSTmzULa&um;@N zLM1k$VLgK`%71dhO~+rE+UT-`HGk)&tPZ(`;=RI)SR<9`H(G2uxc|%*q%!3*-_xqN z-{2K&H$pD3VvYR+Ug|ABybp;I|2YV=CzTNPzqa;W3^TlSeFv@B3s;8Mq#~{dfC{vr zp=%okeMjHtSF#tu&HW*Fah=UVyZtA_h_0p#jp|kOwv5iNs-rpt-p=LquRAsCWvW zAC;1ihfK~v;bZy>BB0ju0v*+eCVc!X?FdT=u7R#m@LkmmHP;ku-%nF=_eiQ8%aU2b z4v<$34}SxfjD8>6Q*?qXHn8M&)#1S0SH5^eQOW%O=zS+ z6BBDrx7<++vj>?p;fTyS!9z_u;bP4@Xd_#YZGSzB$gqhGwQ7PwP5MDrL(2hypc#$E ztP?uYuoEoQv>#t!cJ(M4qb4-cqKOGL=bzGmdMZbvF=iwqZ5feJQ-0}rhcTri(U>xl zk*17Ds42g)8e>TdqA_FuBkdT7STougR}9PnOc`KA<{aTrla5rZS(QM|nLLWfq!A4@ zXn$lv&H29rSD>N2N70xwp^@fHOsF~i#xqu>2+re1od9V}I~PD2cuvNedDlUk*8`8p z$WuPl##1oV#I=F!P@1r?0k1ierr1t2kVSU_Cn!E-FlGZBY!BV$%(>kr*sOl1A}iRL30tJ3T`4Pxy}(Dn;$0JujXu^r9#KDp?@x!PCd3!3K{Pd> zBCidq&X|h(6Hy6kqh4TVTs`3GQ1$rvK=WtB>o`7%bHX2f%WyO0)MEgt{b)i zq@b|lP+PBYpyD@fVtR(hmx#|Wr%T0;8Sor@ToffhPZt3R@Z&-ko;_XF!PCdZ3p_dn z>+$3ipT?78NR|Mc0+D$17wXib>XLGnl zEh;)cJgxsodc$9M)0HtH9hHh5=i4f@!$kn6aDT{G`&b|XE$lQ?^|DDL`-g_O6IdFC zVp2L>0>hU;t_XqFK&~jD7D1*GuCE)+nBpO&2?M*%OlB4^q!3YDi+_4x4Vu3ewpC7W z3VpC?Jz?6Wh^;`)=~;MP)4?m*@9F4*^w~ux3?bdE zO_I?Y!Y6Nsh7JzOqknfEj_cCT(Lu@Y&cU6ZPjXC>@aHWfiNrJ~5P`yr?It#y2JSYK zl+~adD5i}f5P2K^-cGrMBGQ0nYdk05C)Vjj#>`DLIx);zbYZ@AQczPPP<{QN~Zjp|#9*o)T@qcJw`qcye4|lP_H?D`q z0EgpY`;q<5lwhDJmAKSsJ__!Jb`@_5d1!_cIK>CtGZk#Gg+ARsL&2>}zQb)Lj0$0X z?IniRuZowA&`5>ObHnNpYsy+|KxqjhU)kT(b~+72YUUEdgRUExb+ivTZXy*=aS!lA zt-y>14{{#UL4T3WV=QX!Gvj{md4)BvBhs2UDo~g?qT#0gI;j{KQzs%WxO5JUivFBq zV=f&yM@C&daE^_M%bX*FqB7^upqR{g^eYvbtQO)zJ*Vi|44p#2k6S`0Nt5wWgZfjv zn=N;4$|GJl1>epE6TW1JWYcJw$r%hZ8aSsK;Xe2kz<01#~7+{C8_-W@O#Fg!_s z(P(DpPFOTx0B+ya}|W|mHdlWkg2ue?l~D|S~-Cg-|Wxt4mXHBR531aVWZs3 zeAF7}Wq*Z^)ABVp!tj<-#7$$c^t5*dxE16aK5b~B)9DNon6t}MZauU;1vA+J{=nfJ zH=f$nq1%c3b%C3praHEVAV8KRs-{OM&#@qgKVEZ#J`oOVxyGmE}Fu9Ah#h^gG< zvQ1jNl)Yd4&X^3`gxel)CG60nZ-&kC4r&S3cdVkbA3jisBGkwgE!N^KxD8Nw7t9C5QfS z>}C%#i5m~thV|hBOPm=H^JqoO)dt|3b$>in|M0txpz)wSq7l461-FBjRUl2_Wjv-e zJn(&JVOFFd?{Kl(%DNp5{DmvA-j8fQ_|hfcE#HTAMaJ7CW9?)aD*T)}i}?<3*z?Au zeGZ<^JfwTJ9KCd<@`9gkC{^*a72r630M&Q=M)9* zG>_1bp7Mx^=^h<4mdayAtB1B_DkTVfpnykEnt9|0Ul-A<#gVtAI2Y7|A{*BBa3KCE zIa6>T{wbHI!dVW4KjoM`RYS2MVSjX*3i_QwnahHUbo>gKOFWdfS!5Ag_KTO07W2@> zH){h?&=42oiloGhvY_S5l~shUvQ^`%(^3+D?0=crQi%+G zBV0p7Tr3BjH2D)$QrQ{!F7SJ%^une6@TZp{#Y)QOv`rcXE8d6C#Vzz*;Cou=dxe&_ z(Dy22w9tzs+~-11Tg;5nuCkHFXFhW2klYtdCFDe$Mn)iSvBp?4$1}*~q&|=+wL)um z{{(MDD>}A3Y`9daTo3UOKYyw^dt&iqpLHWu+@u7r+80GAtNArie3ieZMo|5O+@~?2 z7+tBIYS0(jSxu@kJ1a(3WPgMv;)fl;;mhiqOT`M`+~S^0l+1d5We zgtsV=AVXIUflp@s@4Q*N!rl?pkU5{eKu+|iO6Ej}tcVB(wg+&?w11Sd<1SC5N&^7D zjFF-OG*WS+u0OeYNQXn96UORJhj6?e#Qd(ap?aPqKqJv{`D(Rw>4QIz$V@W6mVSR0 ziHfa?4ZCOOMV`#iiJt>68@A#NS|GL|(3wqO7Vp}X*bH*eK5G`f-M=hD+WcRYCFljN z>QQtCQE>jXeD{x!-+$iy_L0W_j>Ufc^y`Hg_cV$GbhTFA^me?rf*6}emiLm0m%Txl2NE|VX$u6$Pz4-**(y><=HyY-{eQuce z+mfe>i5IFCU!9nb&bm7B9(?Z;c~Ta#pEr*uM~L`3W{)JmFTY;$PIkEYiAJH$xxg}; z&R@vTYtfBofq(JzbRijR3~)}Mk+#c@J$K7?jp!)V;>mX!fjCM|Ns93TA`HxE;Lm(d zS;xAicdcfV<H` zej`@s_$!e@=0(qo1)k#Y{$0B?wcwp-k^uZgDPYk3q6RY1oc8S^!Gud{{J9Q+{pSL7Meu#!8MdP; zhzb1-ihpCSMph_u8H%F$!8ZDHy7eAbBqNoyD5fd|MKHxvnBF&fusx$ER8rGVRR~N! z#Z#ExZ|+a0)huemcd%VNF1w;u6=p>z!s0b<;g_YBgQ`MKL=ArGV#B*~5+6gN{rxnA zuh8%v>Wq~~6`%PIxsrAajf+P9*U0eFsDm8!9DhSeT9-2-z{cea9oe=7y#3c-kqvd~ zk(!re^Co;6#(%iZL|E~qHy^mJAYOOGByqY^4WQ4R!jUdFq5qaPq9SeXq0~lCA~aS{ zbRx5ZYht(wCJsw-&Vy$Oge}D;_q2YNMz8pba9i9cMTRbBUavEg3oTZ0i;&aRP9!^} z4}Tv|nx5{Prj~2$ACv1GmbYkSLIOu`K#3c@UewFYp9@gR$TWXb4VjW4ez^PE{Wd&1 zfsk$Q62oJk)+xkwK{^fpk&($gkDHNpJrCTnZchT19nAw{hqAr$$8tda9AJgpxx4>* zAU`~|ItO<5&AQ9qK!cOB5^)1-syo5-ntxF6Rf(9aY#-XX!vG-?al;eWNBQW6C&5lmKNLa!^OmaH-9)K z7ykU>d!~h69{EHF-4!DN31`GjK#DoBl)EGkX$6nm2I-RSB*o%9YMIbe3Fwp04r<_@ zJzxAAnpGc&oyq8-83b@cy zkJ~~-jmRQlXk)TS5!R6&udhvTI;bki;uK+aJEzACP``9!}(I7sa6d zbx{TDQ=`n-MY-RUspK)f)PKqGRtZ>}Hf0_jzLVu#3iSd*H)z}xJMoIY`;T{_r$N?; zD+)x_;<_TVZd{kbR*+F^^s(?gd+Xh8DcNy&@OjECzI2>qj(1&z$lq>?LS62r8rJJ( za)%3ZIzC^X!1!IyYt|W2p?psBrr9vya_^%`4HerAfx~;Pywie<>$TdY*ut#7bpI(EZ*zT1SX3TecAu zSU~7<&9o_LO4T2`Ab%{>umgFrJq^V63#f9;^oOWH2V}Y>lz)i7TRuY06gphBIDa9y zl}?rRKpp9Tu!8Wk;#bk|&$1dX$wgTHl$`G8Skw{aNqplQl1C3*q6c4mx-k>!S3y`; zQ#|gtp99bT<~Qp?YyS!Oo)O=@Ldyqv_bOzJ>y}HbMOXWDZGSF3WY!+|Obx>}8$yk^ z_q!FH$i2EZM?}l!#pM8W?S#8;2>v>J$J~i9REr;R#b>eNiZvhN<=KGRc7}{@IFm#e z2zJ)u9p5mwxeIRZaYw!_&fZ;RO;DyO760Im`{taY%g_iH%Pvpekc(EfCy;Ha`A-4? z-mpED@i)A)et&wn*gM?BPAk-9T|t_>>f224YZhzVEoVMryXZy&UTINi`#ZBI_ttNEXUmi+E^hUjjLocv>g^4LvV1@ zl#A~J&EWaVCx3)rykB@T=mYp!O~iQkRh^Q%+kZ&mYLmBkq<9A-7PfExFE1jBmC(F0 zgxx zFBkAvI8(fYzx^22EFNe@wXKChKVulG<>Z%Nj@}Gh>u?+NVU`C~=$Y-%Hg>>J?@Qe` z0XU-Z6R$p-Yi-G2*Hf-OCVx|rsUVj4B1xa~v(`OJqO!%`fa zKXioe(ehRztnN7e9~VkcymHU#JX)^)!aZow$yX3f=n7Xs4z%L4o$I*~QpRocZgssYMXljNhueSgXge@op3HkWm+Ol4nYoph({1DG=9wnmSp{Y*ZQS)h zY@=Aeh4bAiny&1Ssw=lXF$1N-tEdglEBGoEE*Qr8$fa1SeQI~sd`;vB$5rRScJRzy zrftmV`Lf0iH9)^8ftv$dknrPkw-;$>hb9fbh_qlM^}vrSr=ydC{5_f^BY)?~4XW;> z>=CudgNpA7_h8kT(DJUu$l}^@l2QQAXml(QZ>#C^ z?#JvVjabp}Z5!&YeUr9Km6Y&qwrJV2t-wq){yIw^%tTEZCn}vZ1{M^YaQlq&Ps^!_ xS5nn*tmdwLEMF}fUo~AQ@zeSn6Plpj>tPiywk>UCg5zDYb@%R@{|EWr!B_U$4s!qi delta 12887 zcmV-dGN{eNH1=eWihsqOOLyzG(x&JAE6!To(`K)p_RR72PW+vHakibL&xi{mAqg=_ za1)>`<<~c^yy!|wZ>a}~WyN`3vQW4e3JcDvOtzIyGTHEd)w};w%DgT3zy9~XSS`|q zFL}eOjQ_92=ns>%E{p&EKmPSU|J&a8EOWoWK*@vu`PQ&XEq`TG^2V~Wf+>YZCpBx7 zw*uE|nG+}&PVV0UBaz9nlvQp1f3daHWm{!h$m&+^nyJ*VYgut$oG7qR>4p^|M>7AG zqC)Zt&X!1?aVxMDJuxkr&eqe8di_% zTByiIG^|I^Nq?HIxUTpIYgQ^vVa?xhDWgNK(eS##ix?xN@f#&p72JQu3bN7ZJKxc& z*l+L(rW+v_Sg^wU0Z-MM_wPfZ#DBIz??@$t`LCsU7tJ&;ZQnsDW*cXyKnWUEmVR8; z;cjd1hBVVAETk+TZNeUx|D>Rr5#E}DTWAVu#aA@g4}S_QtasSsDouc0HnOy2QLWHu zE#Y8XbCR%yLelaaNXx|rNZTu75VdRzAkB_OP=k>`Y!?Wcodz?+;z$4`kN)RV)5zOb zGmb&w!|f9ypw{yQ9o2|NeE6A~!A~Q&0=h=Rw^`BVTv4!HKTXNqA*p7VNoEN%c3l+Q z4_qo%@_$^nb-^(T)rL13QNQV&ik73H zA6%@~koV7-X1Dv3r6j-jdFahu#SjMXBKF{*u3?Xg>Jl9Gv5*R6REMH5X+$Fp8ktyg z+MS48=pD$U5l3X!0Um1F0T*lDR%uy+Z0lG=hJTH0s8u5rYSK?KYfBDT2pZ97%sQYW z4LiU>P5ZQg+0~(Fj2h8Mi$*5YoZqAd^;DKbW6VHC+A<)aru<&>3S&x3qA_J4BTX5Q zP*c9J9AilXqA{cgBkkykSTmX#S6G+>n9{?D%-O-AChe$Lvo-=XXL2YalLj=@pn(ZB z=YQ`lZ$LwPhoUiOL?g`^nNV}Om@t;b6`b3RIsnp`b}WE2@R*D>^R|LEuRV{*$U{EV z#zQdF#970c=BW~8U2wZBsX@C;r|Id3&}-OArPFZ>;VEzr%2Ai1(7Z|Ywo*W5m8Y3q z!2!O$UHgRX1vsJ&AoWI#(IL=qQl}xTaeuvt`JLlzY-?UF8g57%4yBq3$Kuj31RAS^ z57@OU*$T8PFv6p2M#D?F;hk>;8C(e}R@sQEY|XRxK6Da;Hm2Y?45$0vkD?HTstdNW zRL!@59j-lK@=%00#oId9J?ba_`(yvqg!p99iXsPehv!v(wye_PA5Jc$+%{fWGJi!b z)z-G*C6oCqo04e_IP|eE@M0Qwp&+AgVFPOxZ5+zqT)jG)wZaGud(II#;brZ2ZEh~Z z69OgMlH&d84>G*WIVB%ehn59f0S*Y5p`fX7I8gE{*CBZR_9F&In8WSy+YERN-fa>S zpobf#1o&>l5YHa&oZ;!+<`EtpH-A#_Nr0&rX*#iP5`LOgnJscu zfEwtd@I0r3XR_YW(FN(Vi%Muhy4`6cqZfovUbY1t9CRxCI2<=fI7SB@r9K9?em=-p zK*ArlwImY56J`Vo8`JTuI1Su}hNM~BSWq~GNFZ__{?1I|L_?$jJ%9UuOu)_2oHo7o%?shCX{1%FCHHfoh`Dwaq}9z*!&O*6O=4Zg;{ffQFm;Wl%9NrjA71O2jcVYN_BD8=+v}i9bdLP2rEBK@<4n z=npD1SuMn^3ml?H(>V$OH;(e_4vLJA8eAUY?bNEWzeFt|dB_50%=N&jB7L1!M)I;|}=2WVWmfFo!8?pt6&GZ}}0ru4gPTV&7@H`ht zpSoXA@fd;JMW=KI0D=oUDf3SaOj|Y*&^$_j(P(C6L+%T^LAYRwr$5^eF?lP#;g-rU zlfVHi`C&dk^na;_Z*t=kT-ccOG#|Bnby}g~w0y<2(7dD+v6BugJqB6;;|7l5WAp=^ zPNkW^95Nkp^R}!hn9&aK2lfYU@zg5!okWQ01V+pZbxeCsfJ|kR`eW1-aAaW5kZnxP zx2W3*Mqs;@pRLV6rQsC?+^+Du;2NUJ-Zv)D8v}+C{(tkWPi`JEoKe5n+xtH~4M{SV ztvL|on#>sy@e`@GMwm8;W2tM-axAeFrVpKdZ^lI`-|EJ+jhF%oFds+JtlUW>1%A@u z6BA%lH%&!)LKJX)QQp>t$e5lxUzx08CIgjD!*4vNl4;rsg^aeo-Tf?y#`hc<=@cC> zm1cMkCx5#X4cnY=@MvJki`TA89lm(9vBr7G*6P0{GA(P%yI8+`CWQsGm$r zp2LTiMrxVKV#<=h_4HZ$1vkhHyH`RP*OCmc@TAt7eDM8NA$wf_wa};?BGkdZi^5hy zWe3+wv7(_SkM2h_%U7`#6||P06PmYDH%C%!;D6>)8}kYU+BSyzJZQU#(3FPmMM&D! zJq@CUZcoMSkIdNnovE{jM-BkIK)>zKw;fm&TMjlt7+uXGm#S8jw%viZ(4I=vP>B83uIxV@}qMgGaV;X_CuyPG_21_UE zz=voury7Y64Qodllr8C@5XJcUP=%t=9H~%M?U4x8TlnBEJx;#`Z#K{yGh?`0b~RqB zMXm@H-1aghKg1FfO2o82gcAK-^aH0NDu0%mHHxR^Q>sM?d#T1A=~HTIOxp}zsLp0c z$KT9wx$0Iz9OHJiB3jRjn9kX2Y-jN zlqs5z39=N8$9Y+*rX#Ax8XkkCY$)N1S1!hltQy|0!YBTRO{yeo^ta&`EU~fqHqsCO4u~4|KsNgG>?fUSM)D1sr zN@NgbiDx%$6}&d}{~=|rZqg~5-hayI6{6zySQ=PKl%imuWy=6dUj0v@caN^MWLjcY zkj@F%6WI1R6lfjkf)sMeDoTaj#2LrLCZql2(*FXBTr4EZC$4z=KRpdezL+1A)5T7w zU6Wj47Jd4W?pfFw$GIu)Wv$Qc1KkCv3YSpj@A4;M+@{H`6S zyPFEp2%eyV+riT+kf!i79@853e1A4=Sfn8DNa{vr#_ed}8{UZJu76|uMaPtH#pIQT zGcs#g5q zPa&&}6`eoc&Mm}}+kYsFo7=n&)Qvk*ca_P86{ag-I|Y2!UZVR7E-}8aY-A>sM8ki! z!gxyh!TGyQhdw7?))|F5Ttaz6X&QvR%Mvjz8=}BH*B%%>0X`HVYROKdx}4HPdSZqD$ZdYAS-mjl0<^llQ?0s-&GKtQbsZ<%4 z?)`y8Ms#Bfp1M#u5*1Sw3zqS{>%QSKZ6$uTJT2IQ7ifXdhCpXFo|!z93$bcttG+Uw zt?}*tX&KVy|Ew%QFK||mqB97B<6qN&Xcbro!AITwDVbsRm>E=ck`7E1qWUUCve5 zW<{HGMJn=Kk%~0DI|*b@@^h@ps$q6>65})hmi%IqPJ!l(d(3bI71M6-vxf(J@Nk!j zD}Q{O@>r+vLfs|^0XoLPk>P9e-}98Rklo~?I|)I=SK%Ud0(`5qEGgOPh7~Q5aq_IK zCntKDPY-*iG<3cpL%Yc77|cgTx5u=UJMN@tgzQv!&_B@#h{|nV&B|1Cl~5r~j~hDP zl5K5v3&Ph5C*NrV!q79GS-=YjKL(E9QKN3G;zkSUm*;Q)CYG=(FW-J87I}N1R;c&~k-|8GGhU#rYGy+=w^bPPfPaS~ zk6~{2P)@z{{Yo^+681?c;Ij2e4dhaF6#3M3!?@MG6Kv7~In*L3kwX!RBKbGp{TWBO zu(F6y(n1)B5Lkaer!f1A`xRdc23%6(k97#_KNg@Xg4bQ8*_Nsx2J|}=$5@T5P{uM8 zMf1j1>PuWPf)zhgd}YWkrHf$4{M3e%5P7!7L0x3D-E zn<`Q@8D_^Vzd4h-#AqAg>RADk^9i(;qmeNAK%_E z{&zrs{Nu-ur*Dt{A;N!pczYs3|NDvRp>eAb2k38qeEsF=2N4{(#Bl(JEq`qsko~^3 z`x?1od!R$Mx6>&hNBR3ulHT-)2(U9fLPz$c9&i4g3XO~N9aHCJ$h`<3?!n#M{RCL@ zz1E+&${=31$0TvOLk*zM9m0_=H=zF-Lzj^@cTj4hM-dvUM>>(&!8OreVIGFza}M3l z67WIjdTvUDSsHc2H^K&eQh$lqz5QVj0g@fF@^`0(4`bk|C`0FvJUyv}5JL5Kk zdEk;&I})(0XdW0l$gCT8#0uok9+ueH^W95NzIoJh3_S1tqU$6K#eYl&ySfA2iasj% z(FmIcgKK6p`(TAkv$(3dl(oZ`0XkCRFURl=8WlUvn~`ky6MdfH5WXWq6Pm=L;T8!s z%P-pA4)1SMy4;HSzdeIVSgR$HH5z8aq5e1XwLN4!BU9o89e3h;FE1tH`S-alMbcyU z`}Bn|jMP<=Xqfbjsed(0bIyeO3Q;4t6f4qV97x=&_{Ng7#WvO=E-u!t+^rQ|qA$7@ z#DC$!bJO_=Z*hV%XV+KghFi03+R2<#zMu$acW2BBu269Sr7?-HyCZlwN+l948EFxP z>ktUs-tT}jqr_Ic%l3ijIuFT-%Ovm%Q$itI+{;2l?d~LDXn*TFNfFxyACc^H;_kT( z(k1P2+{sBmXF?Appie$J?TdT%h)o{t?~n0Ud{sKDL|T_FDHBwtOOh0|sn_f?F?Uy` zI8;!69~J8{sC|gH z5TI8d55O*_pyI8W>K5W3yHgT&OcdeUgsXF-l1&%+!NqDlKk?x;;$gY9`ww=OPxIor zaM>gIw&q3?R)~ApV=^Sq-z8i0-}8KS49aCm?h|dri+`XI<{BXP@t9{`ilP`rngSkr z$V@()UmpBtWZ!UBgQ$DBqzUaIE{S0~iILjAPoO+)o7QZ{acxOYGzQwfxKCIgJHaRO zuhV*9-#RUX_Nz{5T>c=RlJ-b4ZW0`M-3R!3y(sNvKtd0$H$^=79yWOnzrWaDk#*v- z5K-lrB!3L8C6g4f)n#9j<#~F)%p6idN#luPYl zkI8_Z-zEx8yX;elNAHMxaLfZ&vb%H-*0c?oy??h#k2Oa5>_2qt7_ z6o}fP^NP^M=)4rRMe4LVAM?KxELg~j!?!nw%;bB;NmlaDixBzSMNz2BT~xz*-AJy# zPqgCW4RDO#)jVgF78%Ot6fcSu14iO@Kzn~u@vHrWbl+2WBF8-=2X)yqI#_4zg*s2Q zUw@^Y4`AF>nSLF=0zRhtbCIkuwL#Pj+Pd!vVT0QC;EHW{+U5DAkvJ#~jEqfNX_z@M zJZXR&eugh5ozfZP$pjiI+#`iuxEevt@XG$?7FeJS?gbU1cK5O>wE4X(hi!$&dV|sG zo|~Y&ui-9%e~xp#NHoTZAF*C{N`TDa&VLAkeeH}A+RZvCyVBSHitg?QLbg*L2+cg6 zcSdSP+v%&-Nr=7BB2S5c$tRoV^(AIAzgQQpLtj-3u9lkN|5t0w@%%X=j{n*nT7;Iz zfK6T?V=bZ*8H-RAiTfPx%DUnP&)HJFM9SucblxHrE|HzSn0$*^&W&R?_tA1+(|;)W z)8e_i-NPRMQ`6(?<|z4;I#%Fsxo~a~Z=DoA$=qkICICHWw zO{9_^SYXI_l-kY}OK!Nb8N6&kVt>H~+tyfcU+->rW6w%}1EUeqHLz=8@0bx8E7BRa z`^?v|ca1AerhyyFA7V*{jYnmGBgUhAa^=B(JxH&c>Jif*HlmRBsPyZM1Nfu1tL_PKn{<9}W=YKj@h|p^! zGvhC;-#8>i0T01VggxT8q+d zdpE#Er#&4%k<}u5=M7q7$JSC}UDhEfTxtlugGU|6(K(ct`j7oLzK(miccQsxBd^gg zv*21*>h?~qzpwO?Z@RxciGN6{wp`d?Tr4c{cOpK9rP)b^b}c)}^~AfECM!JWywXDN zXrBK$CRMr@p1?vcAy+ERD;&@X-XLrnq?s(z{;tyrmGUZu=b4GLt`do6Lu+)O!Y#%M zBLHi|{J)#FnibwP1!^Np>yTd?@{|$iE{_$a*R+`@tBq)6W%CRIJb!$OjK57m7Hv80 zl46u(S1P2UQ%4E21fDjmcv!6(zG9jjKQDpzEkZ^`W?@H;RA?zG7pX!nBr4Me-bbpf zb28ICcC{1kXP+Bcl7X*TXunoy`cnI~3Nseluasaa7n!D1DA_xwz)56lQRI~jiGcAU?*0gpQg-CKw0BX7u)&KGl;s^XD4V<> zLD%L50n$eA_c)7zyR0MM#bu)sq02@IN|y~9lr9@2=v+1kkh<)f@g+{v%zE@)oVJr9 z^x95?(ru#%rQb#kI>(I)WS;xOEoZ%QO}PYfOoVg+Chg%WWq(zkvM^=kiePCh87z?z zq3wyv#$LpTTG~9WvJCTLD!$Lkx!6%^$j06sMXOQA5jB;7;swu06|`$vNUDPivknIg zD^`-!i~Ck^p?U!#{+g*ZTiAjL+dOCucih+>dx+;@6s^kNvTZ6>5QwC9qaD5<;izF1 zoxi&J_e+T3M1OXMCu9<%z#IKI*la=ei04(GtDJ8MRv*r*`Y)cV4QoWqsc;wh?9N73 zT>nwy)OfYQIlqA=4-55)XQK3#72@mxJKFO_f`|LOIId6K1m1TTkY8n^|INY1sCHjN zRyG*(7vDh;7~-*zihl~dD2^&w!BvKay0C&iFoK5smw(V}0)sdOn(DU?r+fpFdcMad zdVZxJLpU8pJu-Ov%tUA^$5HwVTkwMDslT}(9U?f9C*s={`z=1Ep=VP>7xZ*=K!GC< z!dh{LCal~64e!_Ct(*lu_&f$qZnIbd9Ke6f7m3 z>IEDZUVn){o5|%D+q%=(#p3Ov9U+>^-k)8vhB|hf_ z=6uen3hKY4sL-YKg@GFd3nXe29Ga?0aBzwi!GGTIfweHNR(g#^ii&@e1TO2rR29O| z-xU>?>Y=|fovk1G8|Z-GEt*GEku9039$Pj=H8$M8L=LWZ@M+FhQK5NMK48dXK491+ zJ|HW!16U$OBV)N#E!a2KG*lDmj!pLPZn>KPN${-pYA~7!`S+Sv@O4cKg|}$m^_n-K zyMLtU>t9%dp1;4?lnrp}x~oh{3}Coawe3Z!SFx%Hwu1wiA_OC#X=+}8ldOUD=?^Fh z(ynt5NCe-axk6TGDH+AhFr8n11M<`Ipm zEtzUmZP^r~YQrVyA37@4Geg5%2TVH8aDQ|{@1W^0hhVC*BGhnS;0g-*z>{D8gc?Z2X6*;6x$B4oy;5{$xeVX&!ovCKbGU%7b4Sde1!ypPa8#I@@D?^RBhmo8o`N6M}Imv z9sujFkH`!o@F{gZp~oI4G;E~cL;iQGG__h63qUE-O(a1gTb{KVm=Z7mlLtRx0A`mv zAX7|A4m7fcyX%h4X9Arc^5fAon|!O`W(p79$Pas|Sd>7W-;65ZC;k}qz5qNKW)ui_ z8_m0Na-KrLwB{8-zR^y?ma>wKet+jAHfj}D{oQ-r@Bu7??w$wK#HEeFu8thV<4>fX z&~Yia$yLDTUEM=BxFYtvc*s$~%65-SmagYe2b`xQlu!wS#slH165qiJUX?iCi<{NgOl6$=q^lxWE9dF_zphI~gL+?0@)Ft{DZW zd^7TCoHNp?yz{^M%ri*u3|MOK>}6=Yv*#0eXD2}9ogJUVJ3Bg+cYZOW<_7JX4w~rn z)5F^n%@bSr)8q5khc9IO=TCI#Z*Nb(KK&r$zrOu={`M;z@8a*Uw>b>SMAW42QjDca zGtJ}nDBdek$>QBJavzk;l6Xchg3+=b`L4H(031^hOgR!HTRJ!B+Kuj z)ZWD(?jg~ZB{SdKH9))yG|l}GN|G&@yZA%bIZ1RY?TIHfV2-Lcg}$jk$(;psDE6vm!R^}UV+9@ zdp?z~zIJJ_k&f!A)Lz=D(70%aC-Tq8Cvwh+C-KY(r*%u3DfEz?tnCfocJtf}n^VQj zpO_(fo)$uB(w8zrb=IHjr<7SpS(rbkmO)=MaZcqf#goa(%-874Cl~g#Tt=?L%UbWq zMYg7?6gJuU{gm$F4S)UZASv_d81JQ4-Bay8wRj|WpISDS)J##rPf7?%b4Fxm4D7mAtRmJqxe2*_!UD*A{nFtL|yaO5W2=WOGNc+9naX zqn>IP)M_D;np`H_okp!;P1auzMyWQut&LJ=%{HQ1QLDI3Vt;B8(_5uk5zh9}3rWTA zAvC^xkXlXF^p$mZNg%n-9h|!s8>#Q`KEB~0+$!GF)7_=I^eT55-UcaE^Nd>RW6c|U z^I)HTI1~ck@90&)hk6E3e@w;O$R3e|p6_-8W? zA}hH=8OoRX`F{$$>Q$|IQ6TD#10}!w{^d0iZ#455n3$Ghc>d~EvmVm>gEIu|{`3PL zJ_ugN!@CeEJi3o|Az%+f7V!92*lfzPKM7Ua#TGN}!&aYtaFV7r2^MDe_k;w?^VEE( z2_>>JFQlYY$oZ1Bg{D*pUrgpcWlAqRH{N;cwt zkahqflKER_UBnA;v>#KpWNv`uGoGX+tC12bUU3_FTx39XF6O6XzZ zjl#N{npyBA*E~MS+83OX;`~y?5Z){O;ycZ6z{ZvBRFQGcz89eOU!zRCd;rWU;ZkvRoeEx(8v;ypF z+~Ym&@EUcvpFCacQoXA=nM0ZRi`3NkGz-_p)9I+X5W3Sz{eQNB8VaY$xArHxDvqf( zNto?=Y?B4@ti2vJq0+L|t(i0#d`>14 zMmlo2O>^k@Z?zHspxRT*a{o%TD0$pm!aQeJ8NhwOJ~w$}Y7K2u&M3f}l5<)#^M8{A z#oxjgv^E^8(A#lO?@qS-#`Rz31F=7jRqpj}wr#e-sag^AI!D#IFXvsMaE+YXoO5eD znc5KQ_@5L8w5tt%_elrij#><%q@=rBE5VKO8cX z2S1m__ECEZT5QZb_Ff9`{(tG57FAy{(tLCq!TM>#Nek+T4AIKvNY(`=(Y+P5)kfwb zb-jBR%r_}(I=i{37CO9*tW}>x!)<-#>dr#|&L5=O?v?>Ckqdb`?;=io;IMo=^L z^NXqi>=74K?qRS_b@xRqx!o;Rq%YUYz+1f&f-m;#dz9Nq>Ho*u-G9YjoZzQl2<=H; zU~eLO2gfd+79c8=rh9rsbLW31Uxd3=0*dz%=`L!&(Q*m0�>>Vi&WD#%20kIO+Wb5grHIthDBWf_&{>xU4+)b0G zXDXP=uk29AWfZaJmS!ZgoGQ@pg4uPyfB39D&*}J5ZfMXhKYxVzhSfZ`u{LeY_lnWT z);xPh`F750g=G1sOS7D3qGSc`tp{-P(e{Q_Ha+@ro`5K4oKl#Fu6Jh4R1u=nlC4Ar z7BHRGk}11aMNz75UO-r=E+0`E;tzoA(6WXHhSZ=Qnfz&5SGx!KDK_AfPZQ4++619= z?}gx2I#dF7N`I!E)v|#Vgrg-t+^8FB_yh1Y^TBI{rcVp6RhTh5oGxM0E7~S5c2-D_ zaqwA(u4kdC9+{@D4)Cfj*pAdPZCTq)jXq=sO%Ev`;aD?VV~s&QE*xrw;R*jj#A7HdVDveiu{N%?F)+tW7zn8tO#qXMr-lLB&kXe>GxazKLtNNj$Dk1}9qgJ}CxW z`+q*Ef?fYUl(9W<&NUMSQsdFyhYNS80Tt~|`)EREIwA#kr6U@6M;eOIxKVc`1MW(D zY-={+9MM> z(4i9Cfet0`4m8l9aiABL!~LgYqTOcIA-MA#DByjkmmqQ-ySj*Xo}LPKpFK3C z1071?UFcAQz=;MTG;Z{}y^;^^MguC^jrP%mZgfNn?nXy6@NP5|p>ZRYvzj1g-hZOw z?Ik2|zsKPNI4Tmf3p8LEIHb=CD6ZQ&nk8NzhLg~(jk#qvujgwii)p6@++UtVxFdLC zxdWoi3?ZM?HqF4ztH2Y%@wlz|bz%We1iQ#&JeF5&>9F=rCQ;dXU@w=j+3m1GOHt*f z_S$52Sa3Xqj5f{myNPG;+zr2P{(p;K!d;vgmp+Og)kKVkpVdkE&z2Q%wZU85QoOAe z6WcfUmnRX)LMWc;^qudpwD_^$OYOrjvEU6W3V0b_ zdDGY>K402K2I3$dm3-n$))xAc`TUou@BikLE^V9$e=f_`rRRwFpmqldGvYT>G2@`+ z#6kb9dS(ib&@q4A?7C#kw||y$PqRnp!*BL7>*?`*vlLeK%H2q4s?_9Diqi#bUqSK` z?&jm}ewZ6=-ocVp%auw;aL}{D`iY*FWb0fs=x6kELHoOcqX*{gBTS(RH8Ufim6?&` zJ3lyrD`?qfYoI6l%po}c81mvTv#tWY#IH825vXzgFWy6>op4n%$9xWGt;~q4r{6$Itn$2_!s~|ls`64R**}V5lBe#fA z)G745`}eLD$!xSHslu{o)tXen9cln8yqTW-QI=*BF*QBsboN4S(LSDJW{TR@sQzUL z4hJMm8}+m0VRc313V$w?^V+m3W)hX-(Pj`|vKr3iDq}UDmt=N2-88Ot;*A?qj@XUT zHkk%nHLPC4`R*?9%nYgQN}OGmq*UlG?12mT#j6b~T8?-4RU_Mav4e9(K7D0<2Gx%W zv$jzLwlI@QW$*-bzmO|;!8!rBT7~^2DAzPpsm*hld*|zN7JuW79*!{%)reeOy^DHA znvoe`=QO@^qVRg9nHHHDkmJc8tSIn<-2`ijiUVhv9n5&1Zgw^mMB5ZIGDThB*K9TM z_U$PkdzqxY=|fhmc!}g22Z6`IW~6T>Q@p_p^-pVTQL~AY61WM=2?;luut^)*vMu6R zkw^L1M}N{eDahZ0No0lE-DvoVZ);qwAM(sEY3y}OLzRUPpxYl>RZ`Z7r*)v@ zHR5ip?*rPrvxKv_W~6FI2^45th4x(cZ&>eSLrHLs+S8IBkEW;q<-d=-CSZ2H#PmSf z`Rp0-b<^QC_ULh9?9_Z}Vt~BScFjl;J$R7#-ev6oYJa#^BV=DhR~rMbb=z<}Hv~;` zJ8NXk4!JipkjZ6++f+H^njw>J7@W?ljc8;A=ekX}keGsWoi!KH%0fa->bCHawFJzVbJ1<)gpi9e7Z>;`Y{&^us(k z0sLLm&wtAwOelg@MYW5%k(u3gHdPglw~Zof58mBS4IBb(`$kHe*^s?#yR3PmfSu1) z@2@|(d)JBaAU7FY?{KTJ11foFR&M9_#RjZ3(8ug{Z^G}Ot%?u6m}CI+cPIY$WMLZM zSrIN1S;>Q$34LXCJ#D>aK6T!tXewXW^&flfynoH`Y4gKOETS`fe>2dnw)GSj0y^go zqsv79!!7{gm|gn3LYh%?ihbZ?aN`Q;^rrlb#zyduo9lstBo9ULr}=QB_m5bK@%+M# z;lo($7QgpoySFYY)9S&Vbwq^K(qnAAHJ)sMIm{9r4Qz#;i?_g68v_orKt}^xpy%Q( z@PEQ~oWm^8(ZCkyxp)h_u@&t70}K52k&1tJ*ozaqHPI{o$4n4muN`6FSMRXM*9Q!r z?LWW0ef{EZ0@pVWb`N;Tq7_A+t_r!Bc>Hq~J)D~f+1JVV-Q^I33f;|BB>Y@vWNdD; zy!8hnC?sq$`YPVQN1|ZI;bd?n+a`lp$A4YIEGC~KyhLlYH2+^ASssq~&sYY(z&7<= ztvS~iTC9Q7^RQn8x(b)VXklqYfrz2PLO|`D8!#^5xVKpVDc zcg!|nWN|xj&8pmn#^Hdn$lcWkVBBIi=ue^CQ9p_$w~hUu@nE|-1vQ7g@o8m-mVXk5 zD>;eMJvjOc+2q_CA> zKAKrN2nVS91cd6Udw}+4Ig-}8pJ1tnZI+q`y_5)1orYr!KW?Id?T51y0PC8-0 z9ct>M*Dm(}Nh$BKHA7WQ9TNrKj%pj*4B0*6OsC
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -194,8 +196,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -204,20 +204,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/rounding_precision_in_beancount.html b/rounding_precision_in_beancount.html index 1da21d0a..b1206613 100644 --- a/rounding_precision_in_beancount.html +++ b/rounding_precision_in_beancount.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -184,8 +186,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -194,20 +194,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/running_beancount_and_generating_reports.html b/running_beancount_and_generating_reports.html index ae4839fc..ad90cd2a 100644 --- a/running_beancount_and_generating_reports.html +++ b/running_beancount_and_generating_reports.html @@ -143,6 +143,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -190,8 +192,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -200,20 +200,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/search.html b/search.html index 0d71e467..35fc39bf 100644 --- a/search.html +++ b/search.html @@ -98,6 +98,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -145,8 +147,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -155,20 +155,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/search/search_index.json b/search/search_index.json index 25aa23b9..015596c0 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"index.html","text":"Beancount User's Manual \uf0c1 This is the top-level page for all documentation related to Beancount. http://furius.ca/beancount/doc/index Documentation for Users \uf0c1 Command-line Accounting in Context : A motivational document that explains what command-line accounting is, why I do it, with a simple example of how I do it. Read this as an introduction. The Double-Entry Counting Method : A gentle introduction to the double-entry method, in terms compatible with the assumptions and simplifications that command-line accounting systems make. Installing Beancount : Instructions for download and installation on your computer, and software release information. Running Beancount and Generating Reports : How to run the Beancount executables and generate reports with it. This contains technical information for Beancount users. Getting Started with Beancount : A text that explains the basics of how to create, initialize and organize your input file, and the process of declaring accounts and adding padding directives. Beancount Language Syntax : A full description of the language syntax with examples. This is the main reference for the Beancount language. Beancount Options Reference : A description and explanation of all the possible option values. Precision & Tolerances : Transactions and Balance assertions tolerance small amounts of imprecision which are inferred from the context. This document explains how this works. Beancount Query Language : A high-level overview of the bean-query command-line client tool that allows you to extract various tables of data from your Beancount ledger file. Beancount Cheat Sheet : A single page \u201ccheat sheet\u201d that summarizes the Beancount syntax. How Inventories Work : An explanation of inventories, their representation, and the detail of how lots are matched to positions held in an inventory. This is preliminary reading for the trading doc. Exporting Your Portfolio : How to export your portfolio to external portfolio tracking websites. Tutorial & Example : A realistic example ledger file that contains a few years of a hypothetical Beancount user's financial life. This can be used to kick the tires on Beancount and see what reports or its web interface look like. This can give a quick idea of what you can get out of using Beancount. Beancount Mailing-list : Questions and discussions specific to Beancount occur there. Of related interest is also the Ledger-CLI Forum which contains lots of more general discussions and acts as a meta-list for command-line accounting topics. Beancount History and Credits: A description of the development history of Beancount and a shout out to its contributors. A Comparison of Beancount and Ledger & HLedger : A qualitative feature comparison between CLI accounting systems. Fetching Prices in Beancount : How to fetch and maintain a price database for your assets using Beancount\u2019s tools. Importing External Data : A description of the process of importing data from external files that can be downloaded from financial institutions and the tools and libraries that Beancount provides to automate some of this. Cookbook & Examples \uf0c1 These documents are examples of using the double-entry method to carry out specific tasks. Use these documents to develop an intuition for how to structure your accounts. Command-line Accounting Cookbook : Various examples of how to book many different kinds of financial transactions. This is undoubtedly the best way to build an intuition for how to best use the double-entry method. Trading with Beancount : An explanation of trading P/L and worked examples of how to deal with various investing and trading scenarios with Beancount. This is a complement to the cookbook. Stock Vesting in Beancount : An example that shows how to deal with restricted stock units and vesting events typical of those offered by technology companies & startups. Sharing Expenses with Beancount : A realistic and detailed example of using the double-entry method for sharing expenses for a trip or project with other people. How We Share Expenses : A more involved description of a continuous system for (a) sharing expenses between partners when both are contributing and (b) sharing expenses to a specific project (our son) who has a ledger of his own. This describes our real system. Health care Expenses (incomplete): A not-quite finished document explaining how to handle the sequence of health care expenses for in and out of network providers in the USA. Tracking Medical Claims : An example structure tracking out-of-network psychotherapy sessions with out-of-pocket payments, a medical claim, and an associated HSA repayment for the uncovered portion. Calculating Portolio Returns : How to compute portfolio returns from a Beancount ledger. This describes work done in an experimental script and the process that was involved in extracting the correct data for it. Documentation for Developers \uf0c1 Beancount Scripting & Plugins : A guide to writing scripts that load your ledger contents in memory and process the directives, and how to write plugins that transform them. Beancount Design Doc : Information about the program\u2019s architecture and design choices, code conventions, invariants and methodology. Read this if you want to get a deeper understanding. LedgerHub Design Doc : The design and architecture of the importing tools and library implemented in beancount.ingest . This used to be a separate project called LedgerHub (now defunct), whose useful parts have been eventually folded into Beancount. This is the somewhat dated original design doc. Source Code : The official repository of the Beancount source code lives at Github since May 2020. (From 2008 to May 2020 it was hosted at Bitbucket). External Contributions : A list of plugins, importers and other codes that build on Beancount\u2019s libraries that other people have made and shared. Enhancement Proposals & Discussions \uf0c1 I occasionally write proposals for enhancements in order to organize and summarize my thoughts before moving forward, and solicit feedback from other users. This is good material to find out what features I\u2019m planning to add, how things work in detail, or compare the differences with other similar software such as Ledger or HLedger. A Proposal for an Improvement on Inventory Booking : A proposal in which I outline the current state of affairs in Ledger and Beancount regarding inventory booking, and a better method for doing it that will support average cost booking and calculating capital gains without commissions. Settlement Dates in Beancount : A discussion of how settlement or auxiliary dates could be used in Beancount, and how they are used in Ledger. Balance Assertions in Beancount : A summary of the different semantics for making balance assertions in Beancount and Ledger and a proposal to extend Beancount\u2019s syntax to support more complex assertions. Fund Accounting with Beancount : A discussion of fund accounting and how best to go about using Beancount to track multiple funds in a single ledger. Rounding & Precision in Beancount : A discussion of important rounding and precision issues for balance checks and a proposal for a better method to infer required precision. ( Implemented ) External Links \uf0c1 Documents, links, blog entries, writings, etc. about Beancount written by other authors.] Beancount Source Code Documentation (Dominik Aumayr): Sphinx-generated source code documentation of the Beancount codebase. The code to produce this is located here . Beancount ou la comptabilit\u00e9 pour les hackers (Cyril Deguet): An overview blog entry (in french). V3 Documentation \uf0c1 As of summer 2020, a rewrite to C++ is underway. These are the documents related to that. Beancount V3 : Goals & Design: Motivation, goals and design for version 3 rewrite to C++. Beancount V3: Changes from V2 : A list of current changes presently done in v3 (on the \"master\" branch) whose documentation hasn't made it to the v2 documentation set. Installing Beancount (v3) : Installation procedure for v3. Beancount V3: Dependencies : Software dependencies for building version 3. Beangulp : The renewed ingestion framework for v3. About this Documentation \uf0c1 You may have noticed that I\u2019m using Google Docs . I realize that this is unusual for an open source project. If you take offense to this , so you know, I do like text formats too: in graduate school I used LaTeX extensively, and for the last decade I was in love with the reStructuredText format, I even wrote Emacs\u2019 support for it. But something happened around 2013: Google Docs became good enough to write solid technical documentation and I\u2019ve begun enjoying its revision and commenting facilities extensively: I am hooked, I love it. For users to be able to suggest a correction or place a comment in context is an incredibly useful feature that helps improve the quality of my writing a lot more than patches or comments on a mailing-list. It also gives users a chance to point out passages that need improvement. I have already received orders of magnitude more feedback on documentation than on any of my other projects; it works. It also looks really good , and this is helping motivate me to write more and better. I think maybe I\u2019m falling in love again with WYSIWYG... Don\u2019t get me wrong: LaTeX and no-markup formats are great, but the world is changing and it is more important to me to have community collaboration and a living document than a crufty TeX file slowly aging in my repository. My aim is to produce quality text that is easy to read, that prints nicely, and most especially these days, that can render well on a mobile device or a tablet so you can read it anywhere. I\u2019m also able to edit it while I\u2019m on the go, which is fun for jotting down ideas or making corrections. Finally, Google Docs has an API that provides access to the doc, so should it be needed, I could eventually download the meta-data and convert it all to another format. I don\u2019t like everything about it, but what it offers, I really do like. If you want to leave comments, I\u2019d appreciate it if you can log into your Google account while you read, so that your name appears next to your comment. Thank you! \u2014 Martin Blais","title":"Index"},{"location":"index.html#beancount-users-manual","text":"This is the top-level page for all documentation related to Beancount. http://furius.ca/beancount/doc/index","title":"Beancount User's Manual"},{"location":"index.html#documentation-for-users","text":"Command-line Accounting in Context : A motivational document that explains what command-line accounting is, why I do it, with a simple example of how I do it. Read this as an introduction. The Double-Entry Counting Method : A gentle introduction to the double-entry method, in terms compatible with the assumptions and simplifications that command-line accounting systems make. Installing Beancount : Instructions for download and installation on your computer, and software release information. Running Beancount and Generating Reports : How to run the Beancount executables and generate reports with it. This contains technical information for Beancount users. Getting Started with Beancount : A text that explains the basics of how to create, initialize and organize your input file, and the process of declaring accounts and adding padding directives. Beancount Language Syntax : A full description of the language syntax with examples. This is the main reference for the Beancount language. Beancount Options Reference : A description and explanation of all the possible option values. Precision & Tolerances : Transactions and Balance assertions tolerance small amounts of imprecision which are inferred from the context. This document explains how this works. Beancount Query Language : A high-level overview of the bean-query command-line client tool that allows you to extract various tables of data from your Beancount ledger file. Beancount Cheat Sheet : A single page \u201ccheat sheet\u201d that summarizes the Beancount syntax. How Inventories Work : An explanation of inventories, their representation, and the detail of how lots are matched to positions held in an inventory. This is preliminary reading for the trading doc. Exporting Your Portfolio : How to export your portfolio to external portfolio tracking websites. Tutorial & Example : A realistic example ledger file that contains a few years of a hypothetical Beancount user's financial life. This can be used to kick the tires on Beancount and see what reports or its web interface look like. This can give a quick idea of what you can get out of using Beancount. Beancount Mailing-list : Questions and discussions specific to Beancount occur there. Of related interest is also the Ledger-CLI Forum which contains lots of more general discussions and acts as a meta-list for command-line accounting topics. Beancount History and Credits: A description of the development history of Beancount and a shout out to its contributors. A Comparison of Beancount and Ledger & HLedger : A qualitative feature comparison between CLI accounting systems. Fetching Prices in Beancount : How to fetch and maintain a price database for your assets using Beancount\u2019s tools. Importing External Data : A description of the process of importing data from external files that can be downloaded from financial institutions and the tools and libraries that Beancount provides to automate some of this.","title":"Documentation for Users"},{"location":"index.html#cookbook-examples","text":"These documents are examples of using the double-entry method to carry out specific tasks. Use these documents to develop an intuition for how to structure your accounts. Command-line Accounting Cookbook : Various examples of how to book many different kinds of financial transactions. This is undoubtedly the best way to build an intuition for how to best use the double-entry method. Trading with Beancount : An explanation of trading P/L and worked examples of how to deal with various investing and trading scenarios with Beancount. This is a complement to the cookbook. Stock Vesting in Beancount : An example that shows how to deal with restricted stock units and vesting events typical of those offered by technology companies & startups. Sharing Expenses with Beancount : A realistic and detailed example of using the double-entry method for sharing expenses for a trip or project with other people. How We Share Expenses : A more involved description of a continuous system for (a) sharing expenses between partners when both are contributing and (b) sharing expenses to a specific project (our son) who has a ledger of his own. This describes our real system. Health care Expenses (incomplete): A not-quite finished document explaining how to handle the sequence of health care expenses for in and out of network providers in the USA. Tracking Medical Claims : An example structure tracking out-of-network psychotherapy sessions with out-of-pocket payments, a medical claim, and an associated HSA repayment for the uncovered portion. Calculating Portolio Returns : How to compute portfolio returns from a Beancount ledger. This describes work done in an experimental script and the process that was involved in extracting the correct data for it.","title":"Cookbook & Examples"},{"location":"index.html#documentation-for-developers","text":"Beancount Scripting & Plugins : A guide to writing scripts that load your ledger contents in memory and process the directives, and how to write plugins that transform them. Beancount Design Doc : Information about the program\u2019s architecture and design choices, code conventions, invariants and methodology. Read this if you want to get a deeper understanding. LedgerHub Design Doc : The design and architecture of the importing tools and library implemented in beancount.ingest . This used to be a separate project called LedgerHub (now defunct), whose useful parts have been eventually folded into Beancount. This is the somewhat dated original design doc. Source Code : The official repository of the Beancount source code lives at Github since May 2020. (From 2008 to May 2020 it was hosted at Bitbucket). External Contributions : A list of plugins, importers and other codes that build on Beancount\u2019s libraries that other people have made and shared.","title":"Documentation for Developers"},{"location":"index.html#enhancement-proposals-discussions","text":"I occasionally write proposals for enhancements in order to organize and summarize my thoughts before moving forward, and solicit feedback from other users. This is good material to find out what features I\u2019m planning to add, how things work in detail, or compare the differences with other similar software such as Ledger or HLedger. A Proposal for an Improvement on Inventory Booking : A proposal in which I outline the current state of affairs in Ledger and Beancount regarding inventory booking, and a better method for doing it that will support average cost booking and calculating capital gains without commissions. Settlement Dates in Beancount : A discussion of how settlement or auxiliary dates could be used in Beancount, and how they are used in Ledger. Balance Assertions in Beancount : A summary of the different semantics for making balance assertions in Beancount and Ledger and a proposal to extend Beancount\u2019s syntax to support more complex assertions. Fund Accounting with Beancount : A discussion of fund accounting and how best to go about using Beancount to track multiple funds in a single ledger. Rounding & Precision in Beancount : A discussion of important rounding and precision issues for balance checks and a proposal for a better method to infer required precision. ( Implemented )","title":"Enhancement Proposals & Discussions"},{"location":"index.html#external-links","text":"Documents, links, blog entries, writings, etc. about Beancount written by other authors.] Beancount Source Code Documentation (Dominik Aumayr): Sphinx-generated source code documentation of the Beancount codebase. The code to produce this is located here . Beancount ou la comptabilit\u00e9 pour les hackers (Cyril Deguet): An overview blog entry (in french).","title":"External Links"},{"location":"index.html#v3-documentation","text":"As of summer 2020, a rewrite to C++ is underway. These are the documents related to that. Beancount V3 : Goals & Design: Motivation, goals and design for version 3 rewrite to C++. Beancount V3: Changes from V2 : A list of current changes presently done in v3 (on the \"master\" branch) whose documentation hasn't made it to the v2 documentation set. Installing Beancount (v3) : Installation procedure for v3. Beancount V3: Dependencies : Software dependencies for building version 3. Beangulp : The renewed ingestion framework for v3.","title":"V3 Documentation"},{"location":"index.html#about-this-documentation","text":"You may have noticed that I\u2019m using Google Docs . I realize that this is unusual for an open source project. If you take offense to this , so you know, I do like text formats too: in graduate school I used LaTeX extensively, and for the last decade I was in love with the reStructuredText format, I even wrote Emacs\u2019 support for it. But something happened around 2013: Google Docs became good enough to write solid technical documentation and I\u2019ve begun enjoying its revision and commenting facilities extensively: I am hooked, I love it. For users to be able to suggest a correction or place a comment in context is an incredibly useful feature that helps improve the quality of my writing a lot more than patches or comments on a mailing-list. It also gives users a chance to point out passages that need improvement. I have already received orders of magnitude more feedback on documentation than on any of my other projects; it works. It also looks really good , and this is helping motivate me to write more and better. I think maybe I\u2019m falling in love again with WYSIWYG... Don\u2019t get me wrong: LaTeX and no-markup formats are great, but the world is changing and it is more important to me to have community collaboration and a living document than a crufty TeX file slowly aging in my repository. My aim is to produce quality text that is easy to read, that prints nicely, and most especially these days, that can render well on a mobile device or a tablet so you can read it anywhere. I\u2019m also able to edit it while I\u2019m on the go, which is fun for jotting down ideas or making corrections. Finally, Google Docs has an API that provides access to the doc, so should it be needed, I could eventually download the meta-data and convert it all to another format. I don\u2019t like everything about it, but what it offers, I really do like. If you want to leave comments, I\u2019d appreciate it if you can log into your Google account while you read, so that your name appears next to your comment. Thank you! \u2014 Martin Blais","title":"About this Documentation"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html","text":"A Comparison of Beancount and Ledger \uf0c1 Martin Blais , September 2014 http://furius.ca/beancount/doc/comparison The question of how Beancount differs from Ledger & HLedger has come up a few times on mailing-lists and in private emails. This document highlights key differences between these systems, as they differ sharply in their design and implementations. Keep in mind that this document is written from the perspective of Beancount and as its author, reflects my own biased views for what the design of a CLI accounting system should be. My purpose here is not to shoot down other systems, but rather to highlight material differences to help newcomers understand how these systems vary in their operation and capabilities, and perhaps to stimulate a fruitful discussion about design choices with the other developers. Philosophical Differences \uf0c1 (See this thread .) First, Ledger is optimistic. It assumes it's easy to input correct data by a user. My experience with data entry of the kind we're doing is that it's impossible to do this right without many automated checks. Sign errors on unasserted accounts are very common, for instance. In contrast, Beancount is highly pessimistic. It assumes the user is unreliable. It imposes a number of constraints on the input. For instance, if you added a share of AAPL at $100 to an empty account it won't let you remove a share of AAPL at $101 from it; you just don't have one. It doesn't assume the user is able or should be relied upon to input transactions in the correct order (dated assertions instead of file-order assertions). It optionally checks that proceeds match sale price (sellgains plugin). And it allows you to place extra constraints on your chart of accounts, e.g. a plugin that refuses postings to non-leaf accounts, or that refuses more than one commodity per account, or that requires you declare all accounts with Open directives; choose your level of pedanticity a-la-carte. It adds more automated cross-checks than the double-entry method provides. After all, cross-checking is why we choose to use the DE method in the first place, why not go hardcore on checking for correctness? Beancount should appeal to anyone who does not trust themselves too much. And because of this, it does not provide support for unbalanced/virtual postings; it's not a shortcoming, it's on purpose. Secondly, there's a design ethos difference. As is evidenced in the user manual, Ledger provides a myriad of options. This surely will be appealing to many, but to me it seems it has grown into a very complicated monolithic tool. How these options interact and some of the semantic consequences of many of these options are confusing and very subtle. Beancount offers a minimalistic approach: while there are some small number of options , it tries really hard not to have them. And those options that do affect the semantics of transactions always appear in the input file (nothing on the command-line) and are distinct from the options of particular tools. Loading a file always results in the same stream of transactions, regardless of the reporting tool that will consume them. The only command-line options present are those which affect the particular behavior of the reporting tool invoked; those never change the semantics of the stream itself. Thirdly, Beancount embraces stream processing to a deeper extent. Its loader creates a single ordered list of directives, and all the directives share some common attributes (a name, a date, metadata). This is all the data. Directives that are considered \"grammar\" in Ledger are defined as ordinary directive objects, e.g. \"Open\" is nothing special in Beancount and does nothing by itself. It's simply used in some routines that apply constraints (an account appears, has an Open directive been witnessed prior?) or that might want to hang per-account metadata to them. Prices are also specified as directives and are embedded in the stream, and can be generated in this way. All internal operations are defined as processing and outputting a stream of directives. This makes it possible to allow a user to insert their own code inside the processing pipeline to carry out arbitrary transformations on this stream of directives\u2014anything is possible, unlimited by the particular semantics of an expression language. It's a mechanism that allows users to build new features by writing a short add-on in Python, which gets run at the core of Beancount, not an API to access its data at the edges. If anything, Beancount's own internal processing will evolve towards doing less and less and moving all the work to these plugins, perhaps even to the extent of allowing plugins to declare the directive types (with the exception of Transaction objects). It is evolving into a shallow driver that just puts together a processing pipeline to produce a stream of directives, with a handy library and functional operations. Specific Differences \uf0c1 Inventory Booking & Cost Basis Treatment \uf0c1 Beancount applies strict rules regarding reductions in positions from inventories tracking the contents of its accounts. This means that you can only take out of an account something that you placed in it previously (in time), or an error will be generated. This is enforced for units \u201cheld at cost\u201d (e.g., stock shares), to ensure that no cost basis can ever leak from accounts, we can detect errors in data entry for trades (which are all too common), and we are able to correctly calculate capital gains. In contrast, Ledger does not implement inventory booking checks over time: all lots are simply accumulated regardless of the previous contents of an inventory (there is no distinction between lot addition vs. reduction). In Beancount, reductions to the contents of an inventory are required to match particular lots with a specified cost basis. In Ledger, the output is slightly misleading about this: in order to simplify the reporting output the user may specify one of a few types of lot merging algorithms. By default, the sum of units of all lots is printed, but by using these options you can tell the reporting generation to consider the cost basis (what it calls \u201cprices\u201d) and/or the dates the lots were created at in which case it will report the set of lots with distinct cost bases and/or dates individually. You can select which type of merging occurs via command-line options, e.g. --lot-dates. Most importantly, this means that it is legal in Ledger to remove from an account a lot that was never added to it. This results in a mix of long and short lots, which do not accurately represent the actual changes that occur in accounts. However, it trivially allows for average cost basis reporting. I believe that this is not only confusing, but also an incorrect treatment of account inventories and have argued it can lead to leakage and incorrect calculations. More details and an example are available here . Furthermore, until recently Ledger was not using cost basis to balance postings in a way that correctly computes capital gains. For this reason I suspect Ledger users have probably not used it to compute and compare their gains against the values reported by their broker. In order for Beancount to detect such errors meaningfully and implement a strict matching discipline when reducing lots, it turns out that the only constraint it needs to apply is that a particular account not be allowed to simultaneously hold long and short positions of the same commodity. For example, all it has to do is enforce that you could not hold a lot of 1 GOOG units and a lot of -1 GOOG units simultaneously in the same inventory (regardless of their acquisition cost). Any reduction of a particular set of units is detected as a \u201clot reduction\u201d and a search is found for a lot that matches the specification of the reducing posting, normally, a search for a lot held at the same cost as the posting specifies. Enforcing this constraint is not of much concern in practice, as there are no cases where you would want both long and short positions in the same account, but if you ever needed this, you could simply resort to using separate accounts to hold your long and short positions (it is already recommended that you use a sub-account to track your positions in any one commodity anyway, this would be quite natural). Finally, Beancount is implementing a proposal that significantly extends the scope of its inventory booking: the syntax will allow a loose specification for lot reductions that makes it easy for users to write postings where there is no ambiguity, as well as supporting booking lots at their average cost as is common in Canada and in all tax-deferred accounts across the world. It will also provide a new syntax for specifying cost per unit and total cost at the same time, a feature which will make it possible to correctly track capital gains without commissions . Currency Conversions \uf0c1 Beancount makes an important semantic distinction between simple currency conversions and conversions of commodities to be held at cost, i.e., for which we want to track the cost basis. For example, converting 20,000 USD to 22,000 CAD is a currency conversion (e.g., between banks), and after inserting the resulting CAD units in the destination account, they are not considered \u201cCAD at a cost basis of 1.1 USD each,\u201d they are simply left as CAD units in the account, with no record of the rate which was used to convert them. This models accurately how the real world operates. On the other hand, converting 5,000 USD into 10 units of GOOG shares with a cost basis of 500 USD each is treated as a distinct operation from a currency conversion: the resulting GOOG units have a particular cost basis attached to them, a memory of the price per unit is kept on the inventory lot (as well as the date) and strict rules are applied to the reduction of such lots (what I call \u201cbooking\u201d) as mentioned previously. This is used to calculate and report capital gains, and most importantly, it detects a lot of errors in data entry. In contrast, Ledger does not distinguish between these two types of conversions . Ledger treats currency conversions the same way as for the conversion of commodities held at cost. The reason that this is not causing as many headaches as one might intuit, is because there is no inventory booking - all lots at whichever conversion rate they occur accumulate in accounts, with positive or negative values - and for simple commodities (e.g. currencies, such as dollars), netting the total amount of units without considering the cost of each unit provides the correct answer. Inspecting the full list of an account\u2019s inventory lots is provided as an option (See Ledger\u2019s \u201c--lot-dates\u201d) and is not the default way to render account balances. I suspect few users make use of the feature: if you did render the list of lots for real-world accounts in which many currency conversions occurred in the past, you would observe a large number of irrelevant lots. I think the cost basis of currency conversions would be best elided instead. HLedger does not parse cost basis syntax and as such does not recognize it. Isolation of Inputs \uf0c1 Beancount reads its entire input only from the text file you provide for it. This isolation is by design. There is no linkage to external data formats nor online services, such as fetchers for historical price values. Fetching and converting external data is disparate enough that I feel it should be the province of separate projects. These problem domains also segment very well and quite naturally: Beancount provides an isolated core which allows you to ingest all the transactional data and derive reports of various aggregations from it, and its syntax is the hinge that connects it to external transaction repositories or price databases. It isolates itself from the ugly details of external sources of data in this way. There are too many external formats for downloadable files that contains transactional information to be able to cover all of them. The data files you can obtain from most institutions are provided in various formats: OFX, Quicken, XLS or CSV, and looking at their contents makes it glaringly obvious that the programmers who built the codes that outputs them did not pay much attention to detail or the standard definition of their format; it\u2019s quite an ugly affair. Those files are almost always very messy, and the details of that messiness varies over time as well as these files evolve. Fetching historical or current price information is a similarly annoying task. While Yahoo and Google Finance are able to provide some basic level of price data for common stocks on US exchanges, when you need to fetch information for instruments traded on foreign exchanges, or instruments typically not traded on exchanges, such as mutual funds, either the data is not available, or if it is, you need to figure out what ticker symbol they decided to map it to, there are few standards for this. You must manually sign the ticker. Finally, it is entirely possible that you want to manage instruments for which there is no existing external price source, so it is necessary that your bookkeeping software provide a mechanism whereby you can manually input price values (both Beancount and Ledger provide a way). The same declaration mechanism is used for caching historical price data, so that Beancount need not require the network at all. Most users will want to write their own scripts for import, but some libraries exist: the beancount.ingest library (within Beancount) provides a framework to automate the identification, extraction of transactions from and filing of downloadable files for various institutions. See its design doc for details. In comparison, Ledger and HLedger support rudimentary conversions of transactions from CSV files as well as automated fetching of current prices that uses an external script you are meant to provide ( getquote ). CSV import is far insufficient for real world usage if you are to track all your accounts, so this needs to be extended. Hooking into an external script is the right thing to do, but Beancount favors taking a strong stance about this issue and instead not to provide code that would trigger any kind of network access nor support any external format as input. In Beancount you are meant to integrate price updates on your own, perhaps with your own script, and maybe bring in the data via an include file. (But if you don't like this, you could also write your own plugin module that could fetch live prices.) Language Syntax \uf0c1 Beancount\u2019s syntax is somewhat simpler and quite a bit more restrictive than Ledger\u2019s. For its 2.0 version, the Beancount syntax was redesigned to be easily specifiable to a parser generator by a grammar. The tokens were simplified in order to make tokenization unambiguous. For example, Currencies must be entirely in capital letters (allowing numbers and some special characters, like \u201c_\u201d or \u201c-\u201d). Currency symbols (such as $ or \u20ac) are not supported. (On the other hand, currencies with numbers require quoting in Ledger.) Account names do not admit spaces (though you can use dashes), and must have at least two components, separated by colons. Description strings must be quoted, like this: \u201cAMEX PMNT\u201d. No freestyle text as strings is supported anymore. Dates are only parsed from ISO8601 format, that is, \u201cYYYY-MM-DD\u201d. Tags must begin with \u201c#\u201d, and links with \u201c^\u201d. Apart from the tag stack, all context information has been removed. There is no account alias, for example, nor is there a notion of \u201capply\u201d as in Ledger (see \u201capply root\u201d and \u201capply tag\u201d). It requires a bit more verbose input\u2014full account names\u2014and so assumes that you have account name completion setup in your editor. As a side effect, these changes make the input syntax look a bit more like a programming language. These restrictions may annoy some users, but overall they make the task of parsing the contents of a ledger simpler and the resulting simplicity will pave the way for people to more easily write parsers in other languages. (Some subtleties remain around parsing indentation, which is meaningful in the syntax, but should be easily addressable in all contexts by building a custom lexer.) Due to its looser and more user-friendly syntax, Ledger uses a custom parser. If you need to parse its contents from another language, the best approach is probably to create bindings into its source code or to use it to export the contents of a ledger to XML and then parse that (which works well). I suspect the parsing method might be reviewed in the next version of Ledger, because using a parser generator is liberating for experimentation. Order Independence \uf0c1 Beancount offers a guarantee that the ordering of directives in an input file is irrelevant to the outcome of its computations. You should be able to organize your input file and reorder any declaration as is most convenient for you without having to worry about how the software will make its calculations. Not even directives that declare accounts (\u201cOpen\u201d) are required to appear before these accounts get used in the file. All directives are parsed and then basically stably sorted before any calculation or validation occurs. This also makes it trivial to implement inclusions of multiple files (you can just concatenate the files if you want). In contrast, Ledger processes the amounts on its postings as it parses the input file. In terms of implementation, this has the advantage that only a single pass is needed to check all the assertions and balances, whereas in Beancount, numerous passes are made on the entire list of directives (this has not been much of a problem in Beancount, however, because even a realistically large number of transactions is rather modest for our computers; most of Beancount\u2019s processing time is due to parsing and numerical calculations). An unfortunate side-effect of Ledger\u2019s method of calculation is that a user must be careful with the order in which the transactions appear in the file. This can be treacherous and difficult to understand when editing a very large input file. This difference is particularly visible in balance assertions . Ledger\u2019s balance assertions are attached to the postings of transaction directives and calculated in file order (I call these \u201cfile assertions\u201d). Beancount\u2019s balance assertions are separate directives that are applied at the beginning of the date for which they are declared, regardless of their position in the file (call these \u201cdated assertions\u201d). I believe that dated assertions are more useful and less error-prone, as they do not depend on the ordering of declarations. On the other hand, Ledger-style file assertions naturally support balance checks on intra-day balances without having to specify the time on transactions, which is impossible with dated assertions. For this reason, a proposal has been written up to consider implementing file assertions in Beancount (in addition to its dated assertions). This will probably be carried out as a plugin. Ledger does not support dated assertions. Account Types \uf0c1 Beancount accounts must have a particular type from one of five categories: Assets, Liabilities, Income, Expenses and Equity. Ledger accounts are not constrained in this way, you can define any root account you desire and there is no requirement to identify an account as belonging to one of these categories. This reflects the more liberal approach of Ledger: its design aims to be a more general \u201ccalculator\u201d for anything you want. No account types are enforced or used by the system. In my experience, I haven\u2019t seen any case where I could not classify one of my accounts in one of those categories. For the more exotic commodities, e.g., \u201cUS dollars allowable to contribute to an IRA\u201d, it might require a bit of imagination to understand which account fits which category, but there is a logic to it: if the account has an absolute value that we care about, then it is an Assets or Liabilities account; if we care only about the transitional values, or the value accumulated during a time period, then it should be an Income or Expenses account. If the sign is positive, then it should be an Assets or Expenses account; conversely, if the sign is negative, it should be a Liabilities or Income account. Equity accounts are almost never used explicitly, and are defined and used by Beancount itself to transfer opening balances, retained earnings and net income to the balance sheet report for a particular reporting period (any period you choose). This principle makes it easy to resolve which type an account should have. I have data from 2008 to 2014 and have been able to represent everything I ever wanted using these five categories. Also, I don\u2019t think asking the user to categorize their accounts in this way is limiting in any way; it just requires a bit of foresight. The reason for requiring these account types is that it allows us to carry out logic based on their types. We can isolate the Income and Expenses accounts and derive an income statement and a single net income value. We can then transfer retained earnings (income from before the period under consideration) and net income (income during the period) to Equity accounts and draw up a balance sheet. We can generate lists of holdings which automatically exclude income and expenses, to compute net worth, value per account, etc. In addition, we can use account types to identify external flows to a group of accounts and compute the correct return on investments for these accounts in a way that can be compared with a target allocation\u2019s market return (note: not integrated yet, but prototyped and spec\u2019ed out, it works). The bottom line is that having account types is a useful attribute, so we enforce that you should choose a type for each account. The absence of account types is probably also why Ledger does not provide a balance sheet or income statement reports, only a trial balance. The advantage is an apparently looser and more permissive naming structure. But also note that requiring types does not itself cause any difference in calculations between the two systems, you can still accumulate any kind of \"beans\" you may want in these accounts, it is no less general. The type is only extra information that Beancount\u2019s reporting makes use of. Transactions Must Balance \uf0c1 Beancount transactions are required to balance, period. I make no compromise in this, there is no way out. The benefit of this is that the sum of the balance amounts of the postings of any subset of transactions is always precisely zero (and I check for it). Ledger allows the user two special kinds of postings: Virtual postings : These are postings input with parentheses around them, and they are not considered in the transaction balance\u2019s sum, you can put any value in them without causing an error. Balanced virtual postings : These postings are input with square brackets around them. These are less permissive: the set of postings in square brackets is enforced to balance within itself. The second case can be shown to be equivalent to two transactions: a transaction with the set of regular postings, and a separate transaction with only the balanced virtual ones (this causes no problem). The first case is the problematic one: in attempting to solve accounting problems, beginning users routinely revert to them as a cop-out instead of modeling their problem with the double-entry method. It is apparent that most adopters of CLI accounting systems are computer scientists and not accounting professionals, and as we are all learning how to create our charts of accounts and fill up our ledgers, we are making mistakes. Oftentimes it is truly not obvious how to solve these problems; it simply requires experience. But the fact is that in 8 years\u2019 worth of usage, there isn\u2019t a single case I have come across that truly required having virtual postings. The first version of Beancount used to have support for virtual postings, but I\u2019ve managed to remove all of them over time. I was always able to come up with a better set of accounts, or to use an imaginary currency that would allow me to track whatever I needed to track. And it has always resulted in a better solution with unexpected and sometimes elegant side-effects. But these systems have to be easy to use, so how do we address this problem? The mailing-list is a good place to begin and ask questions, where people share information regarding how they have solved similar problems (there aren\u2019t that many \u201caccounting problems\u201d per-se in the first place). I am also in the process of documenting all the solutions that I have come up with to solve my own accounting problems in the Beancount Cookbook , basically everything I\u2019ve learned so far; this is work in progress. I hope for this evolving document to become a helpful reference to guide others in coming up with solutions that fit the double-entry accounting framework, and to provide ample examples that will act as templates for others to replicate, to fit in their own data. In the words of Ledger\u2019s author: \u201cIf people don't want to use them [virtual accounts], that's fine. But Ledger is not an accounting tool; it's a tool that may be used to do accounting. As such, I believe virtual accounts serve a role that others with non-accounting problems may wish to fill.\u201d I respectfully beg to differ. Therefore Beancount takes a more radical stance and explicitly avoids supporting virtual postings. If you feel strongly that you should need them, you should use Ledger. Numbers and Precision of Operations \uf0c1 Beancount, Ledger and HLedger all differ in how they represent their numbers internally, and in how they handle the precision of balance checks for a transaction\u2019s postings. First, about how number are represented: Ledger uses rational numbers in an attempt to maintain the full precision of numbers resulting from mathematical operations. This works, but I believe this is perhaps not the most appropriate choice . The great majority of the cases where operations occur involve the conversion from a number of units and a price or a cost to the total value of an account\u2019s posted change (e.g., units x cost = total cost). Our task in representing transactional information is the replication of operations that take place mostly in institutions. These operations always involve the rounding of numbers for units and currencies (banks do apply stochastic rounding), and the correct numbers to be used from the perspective of these institutions, and from the perspective of the government, are indeed the rounded numbers themselves. It is a not a question of mathematical purity, but one of practicality, and our system should do the same that banks do. Therefore, I think that we should always post the rounded numbers to accounts. Using rational numbers is not a limitation in that sense, but we must be careful to store rounded numbers where it matters. I think the approach implemented by Ledger is to keep as much of the original precision as possible. Beancount chooses a decimal number representation to store the numbers parsed from the input with the same precision they are written as. This method suffers from the same problem as using rational numbers does in that the result of mathematical operations between the decimal numbers will currently be stored with their full precision (albeit in decimal). Admittedly, I have yet to apply explicit quantization where required, which would be the correct thing to do. A scheme has to be devised to infer suitable precisions for automatically quantizing the numbers after operations. The decimal representation provides natural opportunities for rounding after operations, and it is a suitable choice for this, implementations even typically provide a context for the precision to take place. Also note that it will never be required to store numbers to an infinite precision: the institutions never do it themselves. HLedger, oddly enough, selects \u201cdouble\u201d fractional binary representation for its prices . This is an unfortunate choice, a worse one than using a precise representation: fractional decimal numbers input by the user are never represented precisely by their corresponding binary form. So all the numbers are incorrect but \u201cclose enough\u201d that it works overall, and the only way to display a clean final result is by rounding to a suitable number of digits at the time of rendering a report. One could argue that the large number of digits provided by a 64-bit double representation is unlikely to cause significant errors given the quantity of operations we make\u2026 but binary rounding error could potentially accumulate, and the numbers are basically all incorrectly stored internally, rounded to their closest binary relative. Given that our task is accounting, why not just represent them correctly? Secondly, when checking that the postings of a transaction balance to zero, with all three systems it is necessary to allow for some tolerance on those amounts. This need is clear when you consider that inputting numbers in a text file implies a limited decimal representation. For example, if you\u2019re going to multiply a number of units and a cost, say both written down with 2 fractional digits, you might end up with a number that has 4 fractional digits, and then you need to compare that result with a cash amount that would typically be entered with only 2 fractional digits. You need to allow for some looseness somehow. The systems differ in how they choose that tolerance: Ledger attempts to automatically derive the precision to use for its balance checks by using recently parsed context (in file order). The precision to be used is that of the last value parsed for the particular commodity under consideration. This can be problematic: it can lead to unnecessary side-effects between transactions which can be difficult to debug . HLedger, on the other hand, uses global precision settings. The whole file is processed first, then the precisions are derived from the most precise numbers seen in the entire input file. At the moment, Beancount uses a constant value for the tolerance used in its balance checking algorithm (0.005 of any unit). This is weak and should, at the very least, be commodity-dependent, if not also dependent on the particular account in which the commodity is used. Ultimately, it depends on the numbers of digits used to represent the particular postings. We have a proposal en route to fix this. I am planning to fix this: Beancount will eventually derive its precision using a method entirely local to each transaction, perhaps with a global value for defaults (this is still in the works - Oct 2014 ). Half of the most precise digit will be the tolerance. This will be derived similarly to HLedger\u2019s method, but for each transaction separately. This will allow the user to use an arbitrary precision, simply by inserting more digits in the input. Only fractional digits will be used to derive precision. No global effect implied by transactions will be applied. No transaction should ever affect any other transaction\u2019s balancing context. Rounding error will be optionally accumulated to an Equity account if you want to monitor it . As for automatically quantizing the numbers resulting from operations, I still need to figure out an automatic method for doing so. Filtering at the Transactional Level \uf0c1 Another area where the systems differ is in that Beancount will not support filtering at the postings level but only at the transaction level. That is, when applying filters to the input data, even filtering that applies predicates to postings, only sets of complete transactions will be produced. This is carried out in order to produce sets of postings whose sum balances to exactly zero. We will not ignore only some postings of a transaction. We will nevertheless allow reports to cull out arbitrary subsets of accounts once all balances have been computed. Ledger filtering works at either the transactional or postings level . I think this is confusing. I don\u2019t really understand why it works this way or how it actually works. (Note that this is not a very relevant point at this moment, because I have yet to implement custom arbitrary filtering; the only filtering available at the moment are \u201cviews\u201d you can obtain through the web interface, but I will soon provide a simple logical expression language to apply custom filters to the parsed transactions as in Ledger, as I think it\u2019s a powerful feature. Until recently, most reports were only rendered through a web interface and it wasn\u2019t as needed as it is now that Beancount implements console reports.) Extension Mechanisms \uf0c1 Both Beancount and Ledger provide mechanisms for writing scripts against their corpus of data. These mechanisms are all useful but different. Ledger provides A custom expression language which it interprets to produce reports A command to export contents to XML A command to export contents to LISP A library of Python bindings which provides access to its C++ data structures (\u2026 others?) (Note: due to its dependencies, C++ features being used and build system, I have found it difficult to build Ledger on my vanilla Ubuntu machine or Mac OS computer. Building it with the Python bindings is even more difficult. If you have the patience, the time and that\u2019s not a problem for you, great, but if you can find a pre-packaged version I would recommend using that instead.) Beancount provides A native Python plugin system whereby you may specify lists of Python modules to import and call to filter and transform the parsed directives to implement new features; An easy loader function that allows you to access the internal data structures resulting from parsing and processing a Beancount ledger. This is also a native Python library. So basically, you must write Python to extend Beancount. I\u2019m planning to provide output to XML and SQL as well (due to the simple data structures I\u2019m using these will be both very simple to implement). Moreover, using Beancount\u2019s own \u201cprinter\u201d module produces text that is guaranteed to parse back into exactly the same data structure (Beancount guarantees round-tripping of its syntax and its data structures). One advantage is that the plugins system allows you to perform in-stream arbitrary transformations of the list of directives produced, and this is a great way to prototype new functionality and easier syntax. For example, you can allocate a special tag that will trigger some arbitrary transformation on such tagged transactions. And these modules don\u2019t have to be integrated with Beancount: they can just live anywhere on your own PYTHONPATH, so you can experiment without having to contribute new features upstream or patch the source code. (Note: Beancount is implemented in Python 3, so you might have to install a recent version in order to work with it, such as Python-3.4, if you don\u2019t have it. At this point, Python 3 is becoming pretty widespread, so I don\u2019t see this as a problem, but you might be using an older OS.) Automated Transactions via Plugins \uf0c1 Ledger provides a special syntax to automatically insert postings on existing transactions , based on some matching criteria. The syntax allows the user to access some of a posting\u2019s data, such as amount and account name. The syntax is specialized to also allow the application of transaction and posting \u201ctags.\u201d Beancount allows you to do the same thing and more via its plugin extension mechanism. Plugins that you write are able to completely modify, create or even delete any object and attribute of any object in the parsed flow of transactions, allowing you to carry out as much automation and summarization as you like. This does not require a special syntax - you simply work in Python, with access to all its features - and you can piggyback your automation on top of existing syntax. Some examples are provided under beancount.plugins.* . There is an argument to be made, however, for the availability of a quick and easy method for specifying the most common case of just adding some postings. I\u2019m not entirely convinced yet, but Beancount may acquire this eventually (you could easily prototype this now in a plugin file if you wanted). No Support for Time or Effective Dates \uf0c1 Beancount does not represent the intra-day time of transactions, its granularity is a day. Ledger allows you to specify the time of transactions down to seconds precision. I choose to limit its scope in the interest of simplicity, and I also think there are few use cases for supporting intra-day operations. Note that while Beancount\u2019s maximum resolution is one day, when it sorts the directives it will maintain the relative order of all transactions that occurs within one day, so it is possible to represent multiple transactions occurring on the same day in Beancount and still do correct inventory bookings. But I believe that if you were to do day-trading you would need a more specialized system to compute intra-day returns and do technical analysis and intraday P/L calculations. Beancount is not suited for those (a lot of other features would be needed if that was its scope). Ledger also has support for effective dates which are essentially an alternative date for the transaction. Reporting features allow Ledger users to use the main date or the alternative date. I used to have this feature in Beancount and I removed it, mainly because I did not want to introduce reporting options, and to handle two dates, say a transaction date and a settlement date, I wanted to enforce that at any point in time all transactions would balance. I also never made much use of it which indicates it was probably futile. Handling postings to occur at different points in time would have created imbalances, or I would have had to come up with a solution that involved \u201climbo\u201d or \u201ctransfer\u201d accounts. I preferred to just remove the feature: in 8 years of data, I have always been able to fudge the dates to make everything balance. This is not a big issue. Note that handling those split transaction and merging them together will be handled; a proposal is underway. Documents \uf0c1 Beancount offers support for integrating a directory hierarchy of documents with the contents of a ledger\u2019s chart of accounts. You can provide a directory path and Beancount will automatically find and create corresponding Document directives for filenames that begin with a date in directories that mirror account names, and attach those documents to those accounts. And given a bit of configuration, the bean-file tool can automatically file downloaded documents to such a directory hierarchy. Ledger\u2019s binding of documents to transactions works by generic meta-data. Users can attach arbitrary key-value pairs to their transactions, and those can be filenames. Beyond that, there is no particular support for document organization. Simpler and More Strict \uf0c1 Finally, Beancount has a generally simpler input syntax than Ledger. There are very few command-line options\u2014and this is on purpose, I want to localize all the input within the file\u2014and the directive syntax is more homogeneous: all transactions begin with a date and a keyword. If the argument of simplicity appeals to you, you might prefer to work with Beancount. I feel that the number of options offered in Ledger is daunting, and I could not claim to understand all of the possible ways they might interact with each other. If this does not worry you, you might prefer to use Ledger. It is also more strict than Ledger. Certain kinds of Beancount inputs are not valid. Any transaction in an account needs to have an open directive to initiate the account, for example (though some of these constraints can be relaxed via optional plugins). If you maintain a Beancount ledger, you can expect to have to normalize it to fix a number of common errors being reported. I view this as a good thing: it detects many potential problems and applies a number of strict constraints to its input which allows us to make reasonable assumptions later on when we process the stream of directives. If you don\u2019t care about precision or detecting potential mistakes, Ledger will allow you to be more liberal. On the other hand if you care to produce a precise and flawless account of transactions, Beancount offers more support in its validation of your inputs. Web Interface \uf0c1 Beancount has a built-in web interface, and there is an external project called Fava which significantly improves on the same theme. This is the default mode for browsing reports. I believe HLedger also has a web interface as well. Missing Features \uf0c1 Beancount generally attempts to minimize the number of features it provides. This is in contrast with Ledger\u2019s implementation, which has received a substantial amount of feature addition in order to experiment with double-entry bookkeeping. There are a large number of options. This reflects a difference in approach: I believe that there is a small core of essential features to be identified and that forward progress is made when we are able to minimize and remove any feature that is not strictly necessary. My goal is to provide the smallest possible kernel of features that will allow one to carry out the full spectrum of bookkeeping activities, and to make it possible for users to extend the system to automate repetitive syntax. But here is a list of features that Beancount does not support that are supported in Ledger, and that I think would be nice to have eventually. The list below is unlikely to be exhaustive. Console Output \uf0c1 Beancount\u2019s original implementation focused on providing a web view for all of its contents. During the 2.0 rewrite I began implementing some console/text outputs, mainly because I want to be able for reports to be exportable to share with others. I have a trial balance view (like Ledger\u2019s \u201cbal\u201d report) but for now the journal view isn\u2019t implemented. Ledger, on the other hand, has always focused on console reports. I\u2019ll make all the reports in Beancount support output to text format first thing after the initial release, as I\u2019m increasingly enjoying text reports. Use bean-query --list-formats to view current status of this. Filtering Language \uf0c1 Beancount does not yet have a filtering language. Until recently, its web interface was the main mode for rendering reports and exploring the contents of its ledger, and it provided limited subsets of transactions in the form of \u201cviews\u201d, e.g., per-year, per-tag, etc. Having a filtering language in particular allows one to do away with many sub-accounts. I want to simplify my chart of accounts so I need this. I\u2019m working on adding a simple logical expression language to do arbitrary filters on the set of Beancount transactions. This is straightforward to implement and a high priority. No Meta-data \uf0c1 Beancount does not currently support meta-data. Ledger users routinely make liberal use of metadata. This has been identified as a powerful feature and a prototype has already been implemented. Meta-data will be supported on any directive type as well as on any posting. A dictionary of key-value pairs will be attached to each of these objects. Supported values will include strings, dates, numbers, currencies and amounts. So far the plan is to restrict Beancount\u2019s own code to make no specific use of meta-data, on purpose. The meta-data will be strictly made available for user-plugins and custom user scripts to make use of. No Arithmetic Expressions \uf0c1 Beancount does not support arbitrary expression evaluation in the syntax, in the places where numbers are allowed in the input. Ledger does. I have had no use for these yet, but I have no particular reason against adding this, I just haven\u2019t implemented it, as I don\u2019t need it myself. I think an implementation would be straightforward and very low risk, a simple change to the parser and there is already a callback for numbers. I think there are many legitimate uses for it. Limited Support for Unicode \uf0c1 Beancount supports UTF8 or and other encodings in strings only (that is, for input that is in quotes). For example, you can enter payees and narrations with non-ASCII characters, but not account names (which aren\u2019t in quotes). Ledger supports other encodings over the entire file. The reason for the lack of a more general encoding support in Beancount is the current limitation of tokenizer tools. I\u2019ve been using GNU flex to implement my lexer and it does not support arbitrary encodings. I just need to write a better lexer and make that work with Bison , it\u2019s not a difficult task. I will eventually write my own lexer manually\u2014this has other advantages\u2014and will write it to support Unicode (Python 3 has full support for this, so all that is required is to modify the lexer, which is one simple compilation unit). This is a relatively easy and isolated task to implement. No Forecasting or Periodic Transactions \uf0c1 Beancount has no support for generating periodic transactions for forecasting, though there is a plugin provided that implements a simplistic form of it to be used as an example for how plugins work (see beancount.plugins.forecast ). Ledger supports periodic transaction generation . I do want to add this to core Beancount eventually, but I want to define the semantics very clearly before I do. Updating one\u2019s ledger is essentially the process of copying and replicating transactional data that happens somewhere else. I don\u2019t believe that regular transactions are that \u201cregular\u201d in reality; in my experience, there is always some amount of small variations in real transactions that makes it impossible to automatically generate a series of transactions by a generator in a way that would allow the user to forgo updating them one-by-one. What it is useful for in my view, is for generating tentative future transactions. I feel strongly that those transactions should be limited not to straddle reconciled history, and that reconciled history should replace any kind of automatically generated transactions. I have some fairly complete ideas for implementing this feature, but I\u2019m not using forecasting myself at the moment, so it\u2019s on the backburner. While in theory you can forecast using Ledger\u2019s periodic transactions, to precisely represent your account history you will likely need to adjust the beginning dates of those transactions every time you append new transactions to your accounts and replace forecasted ones. I don\u2019t find the current semantics of automated transactions in Ledger to be very useful beyond creating an approximation of account contents. (In the meantime, given the ease of extending Beancount with plugins, I suggest you begin experimenting with forecast transactions on your own for now, and if we can derive a generic way to create them, I\u2019d be open to merging that into the main code.)","title":"A Comparison of Beancount and Ledger Hledger"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#a-comparison-of-beancount-and-ledger","text":"Martin Blais , September 2014 http://furius.ca/beancount/doc/comparison The question of how Beancount differs from Ledger & HLedger has come up a few times on mailing-lists and in private emails. This document highlights key differences between these systems, as they differ sharply in their design and implementations. Keep in mind that this document is written from the perspective of Beancount and as its author, reflects my own biased views for what the design of a CLI accounting system should be. My purpose here is not to shoot down other systems, but rather to highlight material differences to help newcomers understand how these systems vary in their operation and capabilities, and perhaps to stimulate a fruitful discussion about design choices with the other developers.","title":"A Comparison of Beancount and Ledger"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#philosophical-differences","text":"(See this thread .) First, Ledger is optimistic. It assumes it's easy to input correct data by a user. My experience with data entry of the kind we're doing is that it's impossible to do this right without many automated checks. Sign errors on unasserted accounts are very common, for instance. In contrast, Beancount is highly pessimistic. It assumes the user is unreliable. It imposes a number of constraints on the input. For instance, if you added a share of AAPL at $100 to an empty account it won't let you remove a share of AAPL at $101 from it; you just don't have one. It doesn't assume the user is able or should be relied upon to input transactions in the correct order (dated assertions instead of file-order assertions). It optionally checks that proceeds match sale price (sellgains plugin). And it allows you to place extra constraints on your chart of accounts, e.g. a plugin that refuses postings to non-leaf accounts, or that refuses more than one commodity per account, or that requires you declare all accounts with Open directives; choose your level of pedanticity a-la-carte. It adds more automated cross-checks than the double-entry method provides. After all, cross-checking is why we choose to use the DE method in the first place, why not go hardcore on checking for correctness? Beancount should appeal to anyone who does not trust themselves too much. And because of this, it does not provide support for unbalanced/virtual postings; it's not a shortcoming, it's on purpose. Secondly, there's a design ethos difference. As is evidenced in the user manual, Ledger provides a myriad of options. This surely will be appealing to many, but to me it seems it has grown into a very complicated monolithic tool. How these options interact and some of the semantic consequences of many of these options are confusing and very subtle. Beancount offers a minimalistic approach: while there are some small number of options , it tries really hard not to have them. And those options that do affect the semantics of transactions always appear in the input file (nothing on the command-line) and are distinct from the options of particular tools. Loading a file always results in the same stream of transactions, regardless of the reporting tool that will consume them. The only command-line options present are those which affect the particular behavior of the reporting tool invoked; those never change the semantics of the stream itself. Thirdly, Beancount embraces stream processing to a deeper extent. Its loader creates a single ordered list of directives, and all the directives share some common attributes (a name, a date, metadata). This is all the data. Directives that are considered \"grammar\" in Ledger are defined as ordinary directive objects, e.g. \"Open\" is nothing special in Beancount and does nothing by itself. It's simply used in some routines that apply constraints (an account appears, has an Open directive been witnessed prior?) or that might want to hang per-account metadata to them. Prices are also specified as directives and are embedded in the stream, and can be generated in this way. All internal operations are defined as processing and outputting a stream of directives. This makes it possible to allow a user to insert their own code inside the processing pipeline to carry out arbitrary transformations on this stream of directives\u2014anything is possible, unlimited by the particular semantics of an expression language. It's a mechanism that allows users to build new features by writing a short add-on in Python, which gets run at the core of Beancount, not an API to access its data at the edges. If anything, Beancount's own internal processing will evolve towards doing less and less and moving all the work to these plugins, perhaps even to the extent of allowing plugins to declare the directive types (with the exception of Transaction objects). It is evolving into a shallow driver that just puts together a processing pipeline to produce a stream of directives, with a handy library and functional operations.","title":"Philosophical Differences"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#specific-differences","text":"","title":"Specific Differences"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#inventory-booking-cost-basis-treatment","text":"Beancount applies strict rules regarding reductions in positions from inventories tracking the contents of its accounts. This means that you can only take out of an account something that you placed in it previously (in time), or an error will be generated. This is enforced for units \u201cheld at cost\u201d (e.g., stock shares), to ensure that no cost basis can ever leak from accounts, we can detect errors in data entry for trades (which are all too common), and we are able to correctly calculate capital gains. In contrast, Ledger does not implement inventory booking checks over time: all lots are simply accumulated regardless of the previous contents of an inventory (there is no distinction between lot addition vs. reduction). In Beancount, reductions to the contents of an inventory are required to match particular lots with a specified cost basis. In Ledger, the output is slightly misleading about this: in order to simplify the reporting output the user may specify one of a few types of lot merging algorithms. By default, the sum of units of all lots is printed, but by using these options you can tell the reporting generation to consider the cost basis (what it calls \u201cprices\u201d) and/or the dates the lots were created at in which case it will report the set of lots with distinct cost bases and/or dates individually. You can select which type of merging occurs via command-line options, e.g. --lot-dates. Most importantly, this means that it is legal in Ledger to remove from an account a lot that was never added to it. This results in a mix of long and short lots, which do not accurately represent the actual changes that occur in accounts. However, it trivially allows for average cost basis reporting. I believe that this is not only confusing, but also an incorrect treatment of account inventories and have argued it can lead to leakage and incorrect calculations. More details and an example are available here . Furthermore, until recently Ledger was not using cost basis to balance postings in a way that correctly computes capital gains. For this reason I suspect Ledger users have probably not used it to compute and compare their gains against the values reported by their broker. In order for Beancount to detect such errors meaningfully and implement a strict matching discipline when reducing lots, it turns out that the only constraint it needs to apply is that a particular account not be allowed to simultaneously hold long and short positions of the same commodity. For example, all it has to do is enforce that you could not hold a lot of 1 GOOG units and a lot of -1 GOOG units simultaneously in the same inventory (regardless of their acquisition cost). Any reduction of a particular set of units is detected as a \u201clot reduction\u201d and a search is found for a lot that matches the specification of the reducing posting, normally, a search for a lot held at the same cost as the posting specifies. Enforcing this constraint is not of much concern in practice, as there are no cases where you would want both long and short positions in the same account, but if you ever needed this, you could simply resort to using separate accounts to hold your long and short positions (it is already recommended that you use a sub-account to track your positions in any one commodity anyway, this would be quite natural). Finally, Beancount is implementing a proposal that significantly extends the scope of its inventory booking: the syntax will allow a loose specification for lot reductions that makes it easy for users to write postings where there is no ambiguity, as well as supporting booking lots at their average cost as is common in Canada and in all tax-deferred accounts across the world. It will also provide a new syntax for specifying cost per unit and total cost at the same time, a feature which will make it possible to correctly track capital gains without commissions .","title":"Inventory Booking & Cost Basis Treatment"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#currency-conversions","text":"Beancount makes an important semantic distinction between simple currency conversions and conversions of commodities to be held at cost, i.e., for which we want to track the cost basis. For example, converting 20,000 USD to 22,000 CAD is a currency conversion (e.g., between banks), and after inserting the resulting CAD units in the destination account, they are not considered \u201cCAD at a cost basis of 1.1 USD each,\u201d they are simply left as CAD units in the account, with no record of the rate which was used to convert them. This models accurately how the real world operates. On the other hand, converting 5,000 USD into 10 units of GOOG shares with a cost basis of 500 USD each is treated as a distinct operation from a currency conversion: the resulting GOOG units have a particular cost basis attached to them, a memory of the price per unit is kept on the inventory lot (as well as the date) and strict rules are applied to the reduction of such lots (what I call \u201cbooking\u201d) as mentioned previously. This is used to calculate and report capital gains, and most importantly, it detects a lot of errors in data entry. In contrast, Ledger does not distinguish between these two types of conversions . Ledger treats currency conversions the same way as for the conversion of commodities held at cost. The reason that this is not causing as many headaches as one might intuit, is because there is no inventory booking - all lots at whichever conversion rate they occur accumulate in accounts, with positive or negative values - and for simple commodities (e.g. currencies, such as dollars), netting the total amount of units without considering the cost of each unit provides the correct answer. Inspecting the full list of an account\u2019s inventory lots is provided as an option (See Ledger\u2019s \u201c--lot-dates\u201d) and is not the default way to render account balances. I suspect few users make use of the feature: if you did render the list of lots for real-world accounts in which many currency conversions occurred in the past, you would observe a large number of irrelevant lots. I think the cost basis of currency conversions would be best elided instead. HLedger does not parse cost basis syntax and as such does not recognize it.","title":"Currency Conversions"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#isolation-of-inputs","text":"Beancount reads its entire input only from the text file you provide for it. This isolation is by design. There is no linkage to external data formats nor online services, such as fetchers for historical price values. Fetching and converting external data is disparate enough that I feel it should be the province of separate projects. These problem domains also segment very well and quite naturally: Beancount provides an isolated core which allows you to ingest all the transactional data and derive reports of various aggregations from it, and its syntax is the hinge that connects it to external transaction repositories or price databases. It isolates itself from the ugly details of external sources of data in this way. There are too many external formats for downloadable files that contains transactional information to be able to cover all of them. The data files you can obtain from most institutions are provided in various formats: OFX, Quicken, XLS or CSV, and looking at their contents makes it glaringly obvious that the programmers who built the codes that outputs them did not pay much attention to detail or the standard definition of their format; it\u2019s quite an ugly affair. Those files are almost always very messy, and the details of that messiness varies over time as well as these files evolve. Fetching historical or current price information is a similarly annoying task. While Yahoo and Google Finance are able to provide some basic level of price data for common stocks on US exchanges, when you need to fetch information for instruments traded on foreign exchanges, or instruments typically not traded on exchanges, such as mutual funds, either the data is not available, or if it is, you need to figure out what ticker symbol they decided to map it to, there are few standards for this. You must manually sign the ticker. Finally, it is entirely possible that you want to manage instruments for which there is no existing external price source, so it is necessary that your bookkeeping software provide a mechanism whereby you can manually input price values (both Beancount and Ledger provide a way). The same declaration mechanism is used for caching historical price data, so that Beancount need not require the network at all. Most users will want to write their own scripts for import, but some libraries exist: the beancount.ingest library (within Beancount) provides a framework to automate the identification, extraction of transactions from and filing of downloadable files for various institutions. See its design doc for details. In comparison, Ledger and HLedger support rudimentary conversions of transactions from CSV files as well as automated fetching of current prices that uses an external script you are meant to provide ( getquote ). CSV import is far insufficient for real world usage if you are to track all your accounts, so this needs to be extended. Hooking into an external script is the right thing to do, but Beancount favors taking a strong stance about this issue and instead not to provide code that would trigger any kind of network access nor support any external format as input. In Beancount you are meant to integrate price updates on your own, perhaps with your own script, and maybe bring in the data via an include file. (But if you don't like this, you could also write your own plugin module that could fetch live prices.)","title":"Isolation of Inputs"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#language-syntax","text":"Beancount\u2019s syntax is somewhat simpler and quite a bit more restrictive than Ledger\u2019s. For its 2.0 version, the Beancount syntax was redesigned to be easily specifiable to a parser generator by a grammar. The tokens were simplified in order to make tokenization unambiguous. For example, Currencies must be entirely in capital letters (allowing numbers and some special characters, like \u201c_\u201d or \u201c-\u201d). Currency symbols (such as $ or \u20ac) are not supported. (On the other hand, currencies with numbers require quoting in Ledger.) Account names do not admit spaces (though you can use dashes), and must have at least two components, separated by colons. Description strings must be quoted, like this: \u201cAMEX PMNT\u201d. No freestyle text as strings is supported anymore. Dates are only parsed from ISO8601 format, that is, \u201cYYYY-MM-DD\u201d. Tags must begin with \u201c#\u201d, and links with \u201c^\u201d. Apart from the tag stack, all context information has been removed. There is no account alias, for example, nor is there a notion of \u201capply\u201d as in Ledger (see \u201capply root\u201d and \u201capply tag\u201d). It requires a bit more verbose input\u2014full account names\u2014and so assumes that you have account name completion setup in your editor. As a side effect, these changes make the input syntax look a bit more like a programming language. These restrictions may annoy some users, but overall they make the task of parsing the contents of a ledger simpler and the resulting simplicity will pave the way for people to more easily write parsers in other languages. (Some subtleties remain around parsing indentation, which is meaningful in the syntax, but should be easily addressable in all contexts by building a custom lexer.) Due to its looser and more user-friendly syntax, Ledger uses a custom parser. If you need to parse its contents from another language, the best approach is probably to create bindings into its source code or to use it to export the contents of a ledger to XML and then parse that (which works well). I suspect the parsing method might be reviewed in the next version of Ledger, because using a parser generator is liberating for experimentation.","title":"Language Syntax"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#order-independence","text":"Beancount offers a guarantee that the ordering of directives in an input file is irrelevant to the outcome of its computations. You should be able to organize your input file and reorder any declaration as is most convenient for you without having to worry about how the software will make its calculations. Not even directives that declare accounts (\u201cOpen\u201d) are required to appear before these accounts get used in the file. All directives are parsed and then basically stably sorted before any calculation or validation occurs. This also makes it trivial to implement inclusions of multiple files (you can just concatenate the files if you want). In contrast, Ledger processes the amounts on its postings as it parses the input file. In terms of implementation, this has the advantage that only a single pass is needed to check all the assertions and balances, whereas in Beancount, numerous passes are made on the entire list of directives (this has not been much of a problem in Beancount, however, because even a realistically large number of transactions is rather modest for our computers; most of Beancount\u2019s processing time is due to parsing and numerical calculations). An unfortunate side-effect of Ledger\u2019s method of calculation is that a user must be careful with the order in which the transactions appear in the file. This can be treacherous and difficult to understand when editing a very large input file. This difference is particularly visible in balance assertions . Ledger\u2019s balance assertions are attached to the postings of transaction directives and calculated in file order (I call these \u201cfile assertions\u201d). Beancount\u2019s balance assertions are separate directives that are applied at the beginning of the date for which they are declared, regardless of their position in the file (call these \u201cdated assertions\u201d). I believe that dated assertions are more useful and less error-prone, as they do not depend on the ordering of declarations. On the other hand, Ledger-style file assertions naturally support balance checks on intra-day balances without having to specify the time on transactions, which is impossible with dated assertions. For this reason, a proposal has been written up to consider implementing file assertions in Beancount (in addition to its dated assertions). This will probably be carried out as a plugin. Ledger does not support dated assertions.","title":"Order Independence"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#account-types","text":"Beancount accounts must have a particular type from one of five categories: Assets, Liabilities, Income, Expenses and Equity. Ledger accounts are not constrained in this way, you can define any root account you desire and there is no requirement to identify an account as belonging to one of these categories. This reflects the more liberal approach of Ledger: its design aims to be a more general \u201ccalculator\u201d for anything you want. No account types are enforced or used by the system. In my experience, I haven\u2019t seen any case where I could not classify one of my accounts in one of those categories. For the more exotic commodities, e.g., \u201cUS dollars allowable to contribute to an IRA\u201d, it might require a bit of imagination to understand which account fits which category, but there is a logic to it: if the account has an absolute value that we care about, then it is an Assets or Liabilities account; if we care only about the transitional values, or the value accumulated during a time period, then it should be an Income or Expenses account. If the sign is positive, then it should be an Assets or Expenses account; conversely, if the sign is negative, it should be a Liabilities or Income account. Equity accounts are almost never used explicitly, and are defined and used by Beancount itself to transfer opening balances, retained earnings and net income to the balance sheet report for a particular reporting period (any period you choose). This principle makes it easy to resolve which type an account should have. I have data from 2008 to 2014 and have been able to represent everything I ever wanted using these five categories. Also, I don\u2019t think asking the user to categorize their accounts in this way is limiting in any way; it just requires a bit of foresight. The reason for requiring these account types is that it allows us to carry out logic based on their types. We can isolate the Income and Expenses accounts and derive an income statement and a single net income value. We can then transfer retained earnings (income from before the period under consideration) and net income (income during the period) to Equity accounts and draw up a balance sheet. We can generate lists of holdings which automatically exclude income and expenses, to compute net worth, value per account, etc. In addition, we can use account types to identify external flows to a group of accounts and compute the correct return on investments for these accounts in a way that can be compared with a target allocation\u2019s market return (note: not integrated yet, but prototyped and spec\u2019ed out, it works). The bottom line is that having account types is a useful attribute, so we enforce that you should choose a type for each account. The absence of account types is probably also why Ledger does not provide a balance sheet or income statement reports, only a trial balance. The advantage is an apparently looser and more permissive naming structure. But also note that requiring types does not itself cause any difference in calculations between the two systems, you can still accumulate any kind of \"beans\" you may want in these accounts, it is no less general. The type is only extra information that Beancount\u2019s reporting makes use of.","title":"Account Types"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#transactions-must-balance","text":"Beancount transactions are required to balance, period. I make no compromise in this, there is no way out. The benefit of this is that the sum of the balance amounts of the postings of any subset of transactions is always precisely zero (and I check for it). Ledger allows the user two special kinds of postings: Virtual postings : These are postings input with parentheses around them, and they are not considered in the transaction balance\u2019s sum, you can put any value in them without causing an error. Balanced virtual postings : These postings are input with square brackets around them. These are less permissive: the set of postings in square brackets is enforced to balance within itself. The second case can be shown to be equivalent to two transactions: a transaction with the set of regular postings, and a separate transaction with only the balanced virtual ones (this causes no problem). The first case is the problematic one: in attempting to solve accounting problems, beginning users routinely revert to them as a cop-out instead of modeling their problem with the double-entry method. It is apparent that most adopters of CLI accounting systems are computer scientists and not accounting professionals, and as we are all learning how to create our charts of accounts and fill up our ledgers, we are making mistakes. Oftentimes it is truly not obvious how to solve these problems; it simply requires experience. But the fact is that in 8 years\u2019 worth of usage, there isn\u2019t a single case I have come across that truly required having virtual postings. The first version of Beancount used to have support for virtual postings, but I\u2019ve managed to remove all of them over time. I was always able to come up with a better set of accounts, or to use an imaginary currency that would allow me to track whatever I needed to track. And it has always resulted in a better solution with unexpected and sometimes elegant side-effects. But these systems have to be easy to use, so how do we address this problem? The mailing-list is a good place to begin and ask questions, where people share information regarding how they have solved similar problems (there aren\u2019t that many \u201caccounting problems\u201d per-se in the first place). I am also in the process of documenting all the solutions that I have come up with to solve my own accounting problems in the Beancount Cookbook , basically everything I\u2019ve learned so far; this is work in progress. I hope for this evolving document to become a helpful reference to guide others in coming up with solutions that fit the double-entry accounting framework, and to provide ample examples that will act as templates for others to replicate, to fit in their own data. In the words of Ledger\u2019s author: \u201cIf people don't want to use them [virtual accounts], that's fine. But Ledger is not an accounting tool; it's a tool that may be used to do accounting. As such, I believe virtual accounts serve a role that others with non-accounting problems may wish to fill.\u201d I respectfully beg to differ. Therefore Beancount takes a more radical stance and explicitly avoids supporting virtual postings. If you feel strongly that you should need them, you should use Ledger.","title":"Transactions Must Balance"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#numbers-and-precision-of-operations","text":"Beancount, Ledger and HLedger all differ in how they represent their numbers internally, and in how they handle the precision of balance checks for a transaction\u2019s postings. First, about how number are represented: Ledger uses rational numbers in an attempt to maintain the full precision of numbers resulting from mathematical operations. This works, but I believe this is perhaps not the most appropriate choice . The great majority of the cases where operations occur involve the conversion from a number of units and a price or a cost to the total value of an account\u2019s posted change (e.g., units x cost = total cost). Our task in representing transactional information is the replication of operations that take place mostly in institutions. These operations always involve the rounding of numbers for units and currencies (banks do apply stochastic rounding), and the correct numbers to be used from the perspective of these institutions, and from the perspective of the government, are indeed the rounded numbers themselves. It is a not a question of mathematical purity, but one of practicality, and our system should do the same that banks do. Therefore, I think that we should always post the rounded numbers to accounts. Using rational numbers is not a limitation in that sense, but we must be careful to store rounded numbers where it matters. I think the approach implemented by Ledger is to keep as much of the original precision as possible. Beancount chooses a decimal number representation to store the numbers parsed from the input with the same precision they are written as. This method suffers from the same problem as using rational numbers does in that the result of mathematical operations between the decimal numbers will currently be stored with their full precision (albeit in decimal). Admittedly, I have yet to apply explicit quantization where required, which would be the correct thing to do. A scheme has to be devised to infer suitable precisions for automatically quantizing the numbers after operations. The decimal representation provides natural opportunities for rounding after operations, and it is a suitable choice for this, implementations even typically provide a context for the precision to take place. Also note that it will never be required to store numbers to an infinite precision: the institutions never do it themselves. HLedger, oddly enough, selects \u201cdouble\u201d fractional binary representation for its prices . This is an unfortunate choice, a worse one than using a precise representation: fractional decimal numbers input by the user are never represented precisely by their corresponding binary form. So all the numbers are incorrect but \u201cclose enough\u201d that it works overall, and the only way to display a clean final result is by rounding to a suitable number of digits at the time of rendering a report. One could argue that the large number of digits provided by a 64-bit double representation is unlikely to cause significant errors given the quantity of operations we make\u2026 but binary rounding error could potentially accumulate, and the numbers are basically all incorrectly stored internally, rounded to their closest binary relative. Given that our task is accounting, why not just represent them correctly? Secondly, when checking that the postings of a transaction balance to zero, with all three systems it is necessary to allow for some tolerance on those amounts. This need is clear when you consider that inputting numbers in a text file implies a limited decimal representation. For example, if you\u2019re going to multiply a number of units and a cost, say both written down with 2 fractional digits, you might end up with a number that has 4 fractional digits, and then you need to compare that result with a cash amount that would typically be entered with only 2 fractional digits. You need to allow for some looseness somehow. The systems differ in how they choose that tolerance: Ledger attempts to automatically derive the precision to use for its balance checks by using recently parsed context (in file order). The precision to be used is that of the last value parsed for the particular commodity under consideration. This can be problematic: it can lead to unnecessary side-effects between transactions which can be difficult to debug . HLedger, on the other hand, uses global precision settings. The whole file is processed first, then the precisions are derived from the most precise numbers seen in the entire input file. At the moment, Beancount uses a constant value for the tolerance used in its balance checking algorithm (0.005 of any unit). This is weak and should, at the very least, be commodity-dependent, if not also dependent on the particular account in which the commodity is used. Ultimately, it depends on the numbers of digits used to represent the particular postings. We have a proposal en route to fix this. I am planning to fix this: Beancount will eventually derive its precision using a method entirely local to each transaction, perhaps with a global value for defaults (this is still in the works - Oct 2014 ). Half of the most precise digit will be the tolerance. This will be derived similarly to HLedger\u2019s method, but for each transaction separately. This will allow the user to use an arbitrary precision, simply by inserting more digits in the input. Only fractional digits will be used to derive precision. No global effect implied by transactions will be applied. No transaction should ever affect any other transaction\u2019s balancing context. Rounding error will be optionally accumulated to an Equity account if you want to monitor it . As for automatically quantizing the numbers resulting from operations, I still need to figure out an automatic method for doing so.","title":"Numbers and Precision of Operations"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#filtering-at-the-transactional-level","text":"Another area where the systems differ is in that Beancount will not support filtering at the postings level but only at the transaction level. That is, when applying filters to the input data, even filtering that applies predicates to postings, only sets of complete transactions will be produced. This is carried out in order to produce sets of postings whose sum balances to exactly zero. We will not ignore only some postings of a transaction. We will nevertheless allow reports to cull out arbitrary subsets of accounts once all balances have been computed. Ledger filtering works at either the transactional or postings level . I think this is confusing. I don\u2019t really understand why it works this way or how it actually works. (Note that this is not a very relevant point at this moment, because I have yet to implement custom arbitrary filtering; the only filtering available at the moment are \u201cviews\u201d you can obtain through the web interface, but I will soon provide a simple logical expression language to apply custom filters to the parsed transactions as in Ledger, as I think it\u2019s a powerful feature. Until recently, most reports were only rendered through a web interface and it wasn\u2019t as needed as it is now that Beancount implements console reports.)","title":"Filtering at the Transactional Level"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#extension-mechanisms","text":"Both Beancount and Ledger provide mechanisms for writing scripts against their corpus of data. These mechanisms are all useful but different. Ledger provides A custom expression language which it interprets to produce reports A command to export contents to XML A command to export contents to LISP A library of Python bindings which provides access to its C++ data structures (\u2026 others?) (Note: due to its dependencies, C++ features being used and build system, I have found it difficult to build Ledger on my vanilla Ubuntu machine or Mac OS computer. Building it with the Python bindings is even more difficult. If you have the patience, the time and that\u2019s not a problem for you, great, but if you can find a pre-packaged version I would recommend using that instead.) Beancount provides A native Python plugin system whereby you may specify lists of Python modules to import and call to filter and transform the parsed directives to implement new features; An easy loader function that allows you to access the internal data structures resulting from parsing and processing a Beancount ledger. This is also a native Python library. So basically, you must write Python to extend Beancount. I\u2019m planning to provide output to XML and SQL as well (due to the simple data structures I\u2019m using these will be both very simple to implement). Moreover, using Beancount\u2019s own \u201cprinter\u201d module produces text that is guaranteed to parse back into exactly the same data structure (Beancount guarantees round-tripping of its syntax and its data structures). One advantage is that the plugins system allows you to perform in-stream arbitrary transformations of the list of directives produced, and this is a great way to prototype new functionality and easier syntax. For example, you can allocate a special tag that will trigger some arbitrary transformation on such tagged transactions. And these modules don\u2019t have to be integrated with Beancount: they can just live anywhere on your own PYTHONPATH, so you can experiment without having to contribute new features upstream or patch the source code. (Note: Beancount is implemented in Python 3, so you might have to install a recent version in order to work with it, such as Python-3.4, if you don\u2019t have it. At this point, Python 3 is becoming pretty widespread, so I don\u2019t see this as a problem, but you might be using an older OS.)","title":"Extension Mechanisms"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#automated-transactions-via-plugins","text":"Ledger provides a special syntax to automatically insert postings on existing transactions , based on some matching criteria. The syntax allows the user to access some of a posting\u2019s data, such as amount and account name. The syntax is specialized to also allow the application of transaction and posting \u201ctags.\u201d Beancount allows you to do the same thing and more via its plugin extension mechanism. Plugins that you write are able to completely modify, create or even delete any object and attribute of any object in the parsed flow of transactions, allowing you to carry out as much automation and summarization as you like. This does not require a special syntax - you simply work in Python, with access to all its features - and you can piggyback your automation on top of existing syntax. Some examples are provided under beancount.plugins.* . There is an argument to be made, however, for the availability of a quick and easy method for specifying the most common case of just adding some postings. I\u2019m not entirely convinced yet, but Beancount may acquire this eventually (you could easily prototype this now in a plugin file if you wanted).","title":"Automated Transactions via Plugins"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#no-support-for-time-or-effective-dates","text":"Beancount does not represent the intra-day time of transactions, its granularity is a day. Ledger allows you to specify the time of transactions down to seconds precision. I choose to limit its scope in the interest of simplicity, and I also think there are few use cases for supporting intra-day operations. Note that while Beancount\u2019s maximum resolution is one day, when it sorts the directives it will maintain the relative order of all transactions that occurs within one day, so it is possible to represent multiple transactions occurring on the same day in Beancount and still do correct inventory bookings. But I believe that if you were to do day-trading you would need a more specialized system to compute intra-day returns and do technical analysis and intraday P/L calculations. Beancount is not suited for those (a lot of other features would be needed if that was its scope). Ledger also has support for effective dates which are essentially an alternative date for the transaction. Reporting features allow Ledger users to use the main date or the alternative date. I used to have this feature in Beancount and I removed it, mainly because I did not want to introduce reporting options, and to handle two dates, say a transaction date and a settlement date, I wanted to enforce that at any point in time all transactions would balance. I also never made much use of it which indicates it was probably futile. Handling postings to occur at different points in time would have created imbalances, or I would have had to come up with a solution that involved \u201climbo\u201d or \u201ctransfer\u201d accounts. I preferred to just remove the feature: in 8 years of data, I have always been able to fudge the dates to make everything balance. This is not a big issue. Note that handling those split transaction and merging them together will be handled; a proposal is underway.","title":"No Support for Time or Effective Dates"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#documents","text":"Beancount offers support for integrating a directory hierarchy of documents with the contents of a ledger\u2019s chart of accounts. You can provide a directory path and Beancount will automatically find and create corresponding Document directives for filenames that begin with a date in directories that mirror account names, and attach those documents to those accounts. And given a bit of configuration, the bean-file tool can automatically file downloaded documents to such a directory hierarchy. Ledger\u2019s binding of documents to transactions works by generic meta-data. Users can attach arbitrary key-value pairs to their transactions, and those can be filenames. Beyond that, there is no particular support for document organization.","title":"Documents"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#simpler-and-more-strict","text":"Finally, Beancount has a generally simpler input syntax than Ledger. There are very few command-line options\u2014and this is on purpose, I want to localize all the input within the file\u2014and the directive syntax is more homogeneous: all transactions begin with a date and a keyword. If the argument of simplicity appeals to you, you might prefer to work with Beancount. I feel that the number of options offered in Ledger is daunting, and I could not claim to understand all of the possible ways they might interact with each other. If this does not worry you, you might prefer to use Ledger. It is also more strict than Ledger. Certain kinds of Beancount inputs are not valid. Any transaction in an account needs to have an open directive to initiate the account, for example (though some of these constraints can be relaxed via optional plugins). If you maintain a Beancount ledger, you can expect to have to normalize it to fix a number of common errors being reported. I view this as a good thing: it detects many potential problems and applies a number of strict constraints to its input which allows us to make reasonable assumptions later on when we process the stream of directives. If you don\u2019t care about precision or detecting potential mistakes, Ledger will allow you to be more liberal. On the other hand if you care to produce a precise and flawless account of transactions, Beancount offers more support in its validation of your inputs.","title":"Simpler and More Strict"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#web-interface","text":"Beancount has a built-in web interface, and there is an external project called Fava which significantly improves on the same theme. This is the default mode for browsing reports. I believe HLedger also has a web interface as well.","title":"Web Interface"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#missing-features","text":"Beancount generally attempts to minimize the number of features it provides. This is in contrast with Ledger\u2019s implementation, which has received a substantial amount of feature addition in order to experiment with double-entry bookkeeping. There are a large number of options. This reflects a difference in approach: I believe that there is a small core of essential features to be identified and that forward progress is made when we are able to minimize and remove any feature that is not strictly necessary. My goal is to provide the smallest possible kernel of features that will allow one to carry out the full spectrum of bookkeeping activities, and to make it possible for users to extend the system to automate repetitive syntax. But here is a list of features that Beancount does not support that are supported in Ledger, and that I think would be nice to have eventually. The list below is unlikely to be exhaustive.","title":"Missing Features"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#console-output","text":"Beancount\u2019s original implementation focused on providing a web view for all of its contents. During the 2.0 rewrite I began implementing some console/text outputs, mainly because I want to be able for reports to be exportable to share with others. I have a trial balance view (like Ledger\u2019s \u201cbal\u201d report) but for now the journal view isn\u2019t implemented. Ledger, on the other hand, has always focused on console reports. I\u2019ll make all the reports in Beancount support output to text format first thing after the initial release, as I\u2019m increasingly enjoying text reports. Use bean-query --list-formats to view current status of this.","title":"Console Output"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#filtering-language","text":"Beancount does not yet have a filtering language. Until recently, its web interface was the main mode for rendering reports and exploring the contents of its ledger, and it provided limited subsets of transactions in the form of \u201cviews\u201d, e.g., per-year, per-tag, etc. Having a filtering language in particular allows one to do away with many sub-accounts. I want to simplify my chart of accounts so I need this. I\u2019m working on adding a simple logical expression language to do arbitrary filters on the set of Beancount transactions. This is straightforward to implement and a high priority.","title":"Filtering Language"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#no-meta-data","text":"Beancount does not currently support meta-data. Ledger users routinely make liberal use of metadata. This has been identified as a powerful feature and a prototype has already been implemented. Meta-data will be supported on any directive type as well as on any posting. A dictionary of key-value pairs will be attached to each of these objects. Supported values will include strings, dates, numbers, currencies and amounts. So far the plan is to restrict Beancount\u2019s own code to make no specific use of meta-data, on purpose. The meta-data will be strictly made available for user-plugins and custom user scripts to make use of.","title":"No Meta-data"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#no-arithmetic-expressions","text":"Beancount does not support arbitrary expression evaluation in the syntax, in the places where numbers are allowed in the input. Ledger does. I have had no use for these yet, but I have no particular reason against adding this, I just haven\u2019t implemented it, as I don\u2019t need it myself. I think an implementation would be straightforward and very low risk, a simple change to the parser and there is already a callback for numbers. I think there are many legitimate uses for it.","title":"No Arithmetic Expressions"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#limited-support-for-unicode","text":"Beancount supports UTF8 or and other encodings in strings only (that is, for input that is in quotes). For example, you can enter payees and narrations with non-ASCII characters, but not account names (which aren\u2019t in quotes). Ledger supports other encodings over the entire file. The reason for the lack of a more general encoding support in Beancount is the current limitation of tokenizer tools. I\u2019ve been using GNU flex to implement my lexer and it does not support arbitrary encodings. I just need to write a better lexer and make that work with Bison , it\u2019s not a difficult task. I will eventually write my own lexer manually\u2014this has other advantages\u2014and will write it to support Unicode (Python 3 has full support for this, so all that is required is to modify the lexer, which is one simple compilation unit). This is a relatively easy and isolated task to implement.","title":"Limited Support for Unicode"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#no-forecasting-or-periodic-transactions","text":"Beancount has no support for generating periodic transactions for forecasting, though there is a plugin provided that implements a simplistic form of it to be used as an example for how plugins work (see beancount.plugins.forecast ). Ledger supports periodic transaction generation . I do want to add this to core Beancount eventually, but I want to define the semantics very clearly before I do. Updating one\u2019s ledger is essentially the process of copying and replicating transactional data that happens somewhere else. I don\u2019t believe that regular transactions are that \u201cregular\u201d in reality; in my experience, there is always some amount of small variations in real transactions that makes it impossible to automatically generate a series of transactions by a generator in a way that would allow the user to forgo updating them one-by-one. What it is useful for in my view, is for generating tentative future transactions. I feel strongly that those transactions should be limited not to straddle reconciled history, and that reconciled history should replace any kind of automatically generated transactions. I have some fairly complete ideas for implementing this feature, but I\u2019m not using forecasting myself at the moment, so it\u2019s on the backburner. While in theory you can forecast using Ledger\u2019s periodic transactions, to precisely represent your account history you will likely need to adjust the beginning dates of those transactions every time you append new transactions to your accounts and replace forecasted ones. I don\u2019t find the current semantics of automated transactions in Ledger to be very useful beyond creating an approximation of account contents. (In the meantime, given the ease of extending Beancount with plugins, I suggest you begin experimenting with forecast transactions on your own for now, and if we can derive a generic way to create them, I\u2019d be open to merging that into the main code.)","title":"No Forecasting or Periodic Transactions"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html","text":"Inventory Booking \uf0c1 A Proposal for an Improvement on Command-Line Bookkeeping Martin Blais, June 2014 http://furius.ca/beancount/doc/proposal-booking Motivation Problem Description Lot Date Average Cost Basis Capital Gains Sans Commissions Cost Basis Adjustments Dealing with Stock Splits Previous Solutions Shortcomings in Ledger Shortcomings in Beancount Requirements Debugging Tools Explicit vs. Implicit Booking Reduction Design Proposal Inventory Input Syntax & Filtering Algorithm Implicit Booking Methods Dates Inserted by Default Matching with No Information Reducing Multiple Lots Examples No Conflict Explicit Selection By Cost Explicit Selection By Date Explicit Selection By Label Explicit Selection By Combination Not Enough Units Redundant Selection of Same Lot Automatic Price Extrapolation Average Cost Booking Future Work Implementation Note Conclusion Motivation \uf0c1 The problem of \u201cinventory booking,\u201d that is, selecting which of an inventory\u2019s trade lots to reduce when closing a position, is a tricky one. So far, in the command-line accounting community, relatively little progress has been made in supporting the flexibility to deal with many common real-life cases. This document offers a discussion of the current state of affairs, describes the common use cases we would like to be able to solve, identifies a set of requirements for a better booking method, and proposes a syntax and implementation design to support these requirements, along with clearly defined semantics for the booking method. Problem Description \uf0c1 The problem under consideration is the problem of deciding, for a double-entry transaction that intends to reduce the size of a position at a particular point in time in a particular account, which of the account inventory lots contained at that point should be reduced. This should be specified using a simple data entry syntax that minimizes the burden on the user. For example, one could enter a position in HOOL stock by buying two lots of it at different points in time: 2014-02-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {500 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD 2014-02-15 * \"Second trade\" Assets:Investments:Stock 8 HOOL {510 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD We will call the two sets of shares \u201ctrade lots\u201d and assume that the underlying system that counts them is keeping track of each of their cost and date of acquisition separately. The question here is, if we were to sell some of this stock, which of the shares should we select? Those from the first trade, or those from the second? This is an important decision, because it has an impact on capital gains, and thus on taxes. (The account of capital gains is described in more detail in the \u201cStock Trading\u201d section of the Beancount cookbook if you need an explanation of this.) Depending on our financial situation, this year\u2019s trading history, and the unrealized gains that a trade would realize, sometimes we may want to select one lot over another. By now, most discount brokers even let you select your specific lot when you place a trade that reduces or closes a position. It is important to emphasize here the two directions of inventory booking: Augmenting a position. This is the process of creating a new trading lot, or adding to an existing trading lot (if the cost and other attributes are identical). This is easy, and amounts to adding a record to some sort of mapping that describes an account\u2019s balance (I call this an \"inventory\"). Reducing a position. This is generally where booking complications take place, and the problem is essentially to figure out which lot of an existing position to remove units from. Most of this document is dedicated to the second direction. Lot Date \uf0c1 Even if the cost of each lot is identical, the acquisition date of the position you\u2019re intending to close matters, because different taxation rules may apply, for example, in the US, positions held for more than 12 months are subject to a significantly lower tax rate (\u201cthe long-term capital gains rate\u201d). For example, if you entered your position like this: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {300 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD 2014-02-15 * \"Second trade at same price\" Assets:Investments:Stock 8 HOOL {300 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD and we are booking a trade for 2014-03-01, selecting the first lot would result in a long-term (low) gain, because two years have passed since acquisition (position held from 2012-05-01 to 2014-03-01) while booking against the second would result in a short-term (high) capital gains tax. So it\u2019s not just a matter of the cost of the lots. Our systems don't aim to file taxes automatically at this point but we would like to enter our data in a way that eventually allows this kind of reporting to take place. Note that this discussion assumes that you were able to decide which of the lots you could trade against. There are several taxation rules that may apply, and in some cases, depending on which country you live in, you may not have a choice, the government may dictate how you are meant to report your gains. In some cases you may even need to be able to apply multiple booking methods to the same account (e.g., if an account is subject to a taxation claim from multiple countries). We will ignore this exotic case in this document, as it is quite rare. Average Cost Basis \uf0c1 Things get more complicated for tax-sheltered accounts. Because there are no tax consequences to closing positions in these accounts, brokers will normally choose to account for the book value of your positions as if they were a single lot. That is, the cost of each share is computed using the weighted average of the cost of the position you are holding. This can equivalently be calculated by summing the total cost of each position and then dividing by the total number of shares. To continue with our first example: 10 HOOL x 500 USD/HOOL = 5000 USD 8 HOOL x 510 USD/HOOL = 4080 USD Cost basis = 5000 USD + 4080 USD = 9080 USD Total number of shares = 10 HOOL + 8 HOOL = 18 HOOL Cost basis per share = 9080 USD / 18 HOOL = 504.44 USD/HOOL So if you were to close some of your position and sell some shares, you would do so at a cost of 504.44 USD/HOOL. The gain you would realize would be relative to this cost; for example if you sold 5 shares at 520 USD/HOOL, your gain would be calculated like this: (520 USD/HOOL - 504.44 USD/HOOL) x 5 HOOL = 77.78 USD This type of booking method is made evident by two kinds of financial events that you might witness occur in these types of accounts: Fees may be withdrawn by selling some shares reported at their current market price. You will see these if you hold a retirement account at Vanguard in the USA. The broker takes a fixed amount of dollars per quarter (5$, with my employer's plan) that is backed out in terms of a small, fractional number of shares of each fund to sell. That transaction is oblivious to capital gains: they simply figure out how many shares to sell to cover their fee and don\u2019t report a gain to you. You have to assume it doesn\u2019t matter. Most people don't track their gains in non-taxable accounts but freaks like us may want to compute the return nonetheless. Cost basis readjustments may occur spontaneously. This is rare, but I have seen this once or twice in the case of mutual funds in a Canadian tax-deferred account: based on some undisclosed details of their trading activity, the fund management has had to issue an adjustment to the cost basis, which you are meant to apply to your positions. You get an email telling you how much the adjustment was for. 99% of people probably don\u2019t blink, don't understand and ignore this message\u2026 perhaps rightly so, as it has no impact on their taxes, but if you want to account for your trading returns in that account, you do need to count it. So we need to be able to add or remove to the cost basis of existing positions. Note that the average cost of your positions changes on every trade and needs to get recalculated every time that you add to an existing position. A problem with this is that if you track the cost per share, these multiple recalculations may result in some errors if you store the amounts using a fixed numerical representation (typically decimal numbers). An alternative method to track the cost in our data structures, one that is more stable numerically, is to simply keep track of the total cost instead of the cost-per-share. Also note that using fractional numbers does not provide a sufficient answer to this problem: in practice, the broker may report a specific cost, one that is rounded to specific number of decimals, and we may want to be able to book our reducing trade using that reported number rather than maintain the idealized amount that a fractional representation would. Both using a fixed decimal or a fractional representation pose problems. We will largely ignore those problems here. In summary: we need to be able to support book against lots at their average cost, and we need to be able to adjust the cost basis of an existing position. I\u2019ve been thinking about a solution to implement this that would not force an account to be marked with a special state. I think we can do this using only inventory manipulations. All we need is a small change to the syntax that allows the user to indicate to the system that it should book against the average cost of all positions. Using the first example above, acquiring the shares would take the same form as previously, that is after buying two lots of 10 and 8 HOOL units, we end up with an inventory that holds two lots, one of 10 HOOL {500 USD} and one of 8 HOOL {510 USD}. However, when it\u2019s time to sell, the following syntax would be used (this is an old idea I\u2019ve been meaning to implement for a while): 2014-02-01 * \"Selling 5 shares at market price 550 USD\" Assets:Investments:Stock -5 HOOL {*} Assets:Investments:Cash 2759.95 USD Expenses:Commissions 9.95 USD Income:Investments:CapitalGains When the \u201c * \u201d is encountered in lieu of the cost, like this, it would: Merge all the lots together and recalculate the average price per share (504.44 USD) Book against this merged inventory, reducing the resulting lot. After the transaction, we would end up with a single position of 13 HOOL {504.44 USD). Adding to this position at a different price would create a new lot, and those would get merged again the next time a reduction occurs at average cost. We do not need to merge lots until there is a reduction. Apart from concerns of accumulating rounding error, this solution is correct mathematically, and maintains the property that accounts aren\u2019t coerced into any specific booking method\u2014a mix of methods could be used on every reduction. This is a nice property, even if not strictly necessary, and even if we want to be able to just specify a \u201cdefault\u201d booking method to use per account and just stick with it throughout. It\u2019s always nice to have the flexibility to support exceptions, because in real life, they sometimes occur. Capital Gains Sans Commissions \uf0c1 We would like to be able to support the automatic calculation of capital gains without the commissions. This problem is described in much detail in the Commissions section of the trading documentation and in a thread on the mailing-list . The essence of the complication that occurs is that one cannot simply subtract the commissions incurred during the reporting period from the gains that include commissions, because the commissions that were incurred to acquire the position must be pro-rated to the shares of the position that are sold. The simplest and most common way to implement this is to include the costs of acquiring the position into the cost basis of the position itself, and deduct the selling costs from the market value when a position is reduced. Whatever new design we come up with must allow us to count these adjusted gains as this is essential to various individuals' situations. In Beancount, I have figured out a solution for this problem, which luckily enough involves only an automated transformation of a transaction flagged by the presence of a special flag on its postings... if and only if I can find a way to specify which lot to book against without having to specify its cost, because once the cost of commissions gets folded into the cost of the position, the adjusted cost does not show up anywhere in the input file, the user would have to calculate it manually, which is unreasonable. In the solution I\u2019m proposing, the following transactions would be transformed automatically, triggered by the presence of the \u201cC\u201d flag: 2014-02-10 * \"Buy\" Assets:US:Invest:Cash -5009.95 USD C Expenses:Commissions 9.95 USD Assets:US:Invest:HOOL 10.00 HOOL {500 USD / aa2ba9695cc7} 2014-04-10 * \"Sell #1\" Assets:US:Invest:HOOL -4.00 HOOL {aa2ba9695cc7} C Expenses:Commissions 9.95 USD Assets:US:Invest:Cash 2110.05 USD Income:US:Invest:Gains 2014-05-10 * \"Sell #2\" Assets:US:Invest:HOOL -6.00 HOOL {aa2ba9695cc7} C Expenses:Commissions 9.95 USD Assets:US:Invest:Cash 3230.05 USD Income:US:Invest:Gains They would be automatically transformed by a plugin into the following and replace the original transactions above: 2014-02-10 * \"Buy\" Assets:US:Invest:Cash -5009.95 USD X Expenses:Commissions 9.95 USD X Income:US:Invest:Rebates -9.95 USD X Assets:US:Invest:HOOL 10.00 HOOL {500.995 USD / aa2ba9695cc7} 2014-04-10 * \"Sell #1\" X Assets:US:Invest:HOOL -4.00 HOOL {aa2ba9695cc7} X Expenses:Commissions 9.95 USD X Income:US:Invest:Rebates -9.95 USD Assets:US:Invest:Cash 2110.05 USD Income:US:Invest:Gains ; Should be (530-500)*4 - 9.95*(4/10) - 9.95 = ; 106.07 USD 2014-05-10 * \"Sell #2\" X Assets:US:Invest:HOOL -6.00 HOOL {aa2ba9695cc7} X Expenses:Commissions 9.95 USD X Income:US:Invest:Rebates -9.95 USD Assets:US:Invest:Cash 3230.05 USD Income:US:Invest:Gains ; Should be (540-500)*6 - 9.95*(6/10) - 9.95 = ; 224.08 USD The \u201cX\u201d flag would just mark the postings that have been transformed, so maybe they can be rendered with a special color or attribute later on. The rebates account is included as a separate counter just to make the transaction balance. The reason I need to relax the disambiguation of trading lots is that the user needs to be able to specify the matching leg without having to specify the cost of the position, because at the point where the position is reduced (2014-04-10), there is no way to figure out what the cost of the original lot was. Just to be clear, in the example above, this means that if all the information have is 4 HOOL and 500 USD, there is no way to back out a cost of 500.995 USD that could specify a match with the opening trade lot, because that happened with 10 HOOLs. So we need to be more explicit about booking. In the example above, I\u2019m selecting the matching lot \u201cby label,\u201d that is, the user has the option to provide a unique lot identifier in the cost specification, and that can later on be used to disambiguate which lot we want to book a reduction/sale against. The example above uses the string \u201c aa2ba9695cc7 \u201d in this way. (An alternative solution to this problem would involve keep track of both the original cost (without commissions), and the actual cost (with commissions), and then finding the lot against the former, but using the latter to balance the transaction. This idea is to allow the user to keep using the cost of a position to select the lot, but I\u2019m not even sure if that is possible, in the presence of various changes to the inventory. More thought is required on this matter.) Cost Basis Adjustments \uf0c1 In order to adjust the cost basis, one can replace the contents of an account explicitly like this: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500 USD} Assets:US:Invest:HOOL 10.00 HOOL {510 USD} Income:US:Invest:Gains This method works well and lets the system automatically compute the gains. But it would be nice to support making adjustments in price units against the total cost of the position, for example, \u201cadd 340.51 USD to the cost basis of this position\u201d. The problem is that the adjusted cost per share now needs to be computed by the user\u2026 it\u2019s inconvenient. Here\u2019s one way we could support this well, however: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500 USD} Assets:US:Invest:HOOL 10.00 HOOL {} Income:US:Invest:Gains -340.51 USD If all the legs are fully specified - they have a calculatable balance - we allow a single price to be elided. This solves this problem well. Dealing with Stock Splits \uf0c1 Accounting for stock splits creates some complications in terms of booking. In general, the problem is that we need to deal with changes in the meaning of a commodity over time. For example, you could do this in Beancount right now and it works: 2014-01-04 * \"Buy some HOOL\" Assets:Investments:Stock 10 HOOL {1000.00 USD} Assets:Investments:Cash 2014-04-17 * \"2:1 split into Class A and Class B shares\" Assets:Investments:Stock -10 HOOL {1000.00 USD} Assets:Investments:Stock 10 HOOL {500.00 USD} Assets:Investments:Stock 10 HOOLL {500.00 USD} One problem with splitting lots this way is that the price and meaning of a HOOL unit before and after the split differs, but let\u2019s just assume we\u2019re okay with that for now (this can be solved by relabeling the commodity by using another name, ideally in a way that is not visible to the user - we have a pending discussion elsewhere ). The issue that concerns booking is, when you sell that position, which cost do you use? The inventory will contain positions at 500 USD, so that\u2019s what you should be using, but is it obvious to the user? We can assume this can be learned with a recipe . Now, what if we automatically attach the transaction date? Does it get reset on the split and now you would have to use 2014-04-17? If so, we could not automatically inspect the list of trades to determine whether this is a long-term vs. short-term trade. We need to somehow preserve some of the attributes of the original event that augmented this position, which includes the original trade date (not the split date) and the original user-specified label on the position, if one was provided. Forcing a Single Method per Account \uf0c1 For accounts that will use the average booking method, it may be useful to allow specifying that an account should only use this booking method . The idea is to avoid data entry errors when we know that an account is only able to use this method. One way to ensure this is to automatically aggregate inventory lots when augmenting a position. This ensures that at any point in time, there is only a single lot for each commodity. If we associated an inventory booking type to each account, we could define a special one that also triggers aggregation upon the addition of a position. Another approach would be to not enforce these aggregations, but to provide a plugin that would check that for those accounts that are marked as using the average cost inventory booking method by default, that only such bookings actually take place. Previous Solutions \uf0c1 This section reviews existing implementations of booking methods in command-line bookkeeping systems and highlights specific shortcomings that motivate why we need to define a new method. Shortcomings in Ledger \uf0c1 (Please note: I\u2019m not deeply familiar with the finer details of the inner workings of Ledger; I inferred its behavior from building test examples and reading the docs. If I got any of this wrong, please do let me know by leaving a comment.) Ledger\u2019s approach to booking is quite liberal. Internally, Ledger does not distinguish between conversions held at cost and regular price conversions : all conversions are assumed held at cost, and the only place trading lots appear is at reporting time (using its --lots option). Its \u201c {} \u201d cost syntax is meant to be used to disambiguate lots on a position reduction , not to be used when acquiring a position. The cost of acquiring a position is specified using the \u201c @ \u201d price notation: 2014/05/01 * Buy some stock (Ledger syntax) Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash This will result in an inventory of 10 HOOL {500 USD}, that is, the cost is always stored in the inventory that holds the position: $ ledger -f l1.lgr bal --lots 10 HOOL {USD500} [2014/05/01] USD-5000 Assets:Investments USD-5000 Cash 10 HOOL {USD500} [2014/05/01] Stock -------------------- 10 HOOL {USD500} [2014/05/01] USD-5000 There is no distinction between conversions at cost and without cost\u2014all conversions are tracked as if \u201cat cost.\u201d As we will see in the next section, this behaviour is distinct from Beancount\u2019s semantics, which requires a conversion held at cost to be specified using the \u201c {} \u201d notation for both its augmenting and reducing postings, and distinguishes between positions held at cost and price conversions. The advantage of the Ledger method is a simpler input mechanism, but it leads to confusing outcomes if you accumulate many conversions of many types of currencies in the same account. An inventory can easily become fragmented in its lot composition. Consider what happens, for instance, if you convert in various directions between 5 different currencies\u2026 you may easily end up with USD held in CAD, JPY, EUR and GBP and vice-versa\u2026 any possible combinations of those are possible. For instance, the following currency conversions will maintain their original cost against their converted currencies: 2014/05/01 Transfer from Canada Assets:CA:Checking -500 CAD Assets:US:Checking 400 USD @ 1.25 CAD 2014/05/15 Transfers from Europe Assets:UK:Checking -200 GBP Assets:US:Checking 500 USD @ 0.4 GBP 2014/05/21 Transfers from Europe Assets:DE:Checking -100 EUR Assets:US:Checking 133.33 USD @ 0.75 EUR This results in the following inventory in the Assets:US:Checking account: $ ledger -f l2.lgr bal --lots US:Checking 500.00 USD {0.4 GBP} [2014/05/15] 133.33 USD {0.75 EUR} [2014/05/21] 400.00 USD {1.25 CAD} [2014/05/01] Assets:US:Checking The currencies are treated like stock. But nobody thinks of their currency balances like this , one normally thinks of currencies held in an account as units in and of themselves, not in terms of their price related to some other currency. In my view, this is confusing. After these conversions, I just think of the balance of that account as 1033.33 USD. A possible rebuttal to this observation is that most of the time a user does not care about inventory lots for accounts with just currencies, so they just happen not print them out. Out of sight, out of mind. But there is no way to distinguish when the rendering routine should render lots or not. If instructed to produce a report, a balance routine should ideally only render lots only for some accounts (e.g., investing accounts, where the cost of units matters) and not for others (e.g. accounts with currencies that were deposited sometimes as a result of conversions). Moreover, when I render a balance sheet, for units held at cost we need to report the book values rather than the number of shares. But for currencies, the currency units themselves need to get reported, always. If you don\u2019t distinguish between the two cases, how do you know which to render? I think the answer is to select which view you want to render using the options. The problem is that neither provides a single unified view that does the correct thing for both types of commodities. The user should not have to specify options to view partially incorrect views in one style or the other, this should be automatic and depend on whether we care about the cost of those units. This gets handled correctly if you distinguish between the two kinds of conversions. But perhaps most importantly, this method does not particularly address the conversion problem : it is still possible to create money out of thin air by making conversions back and forth at different rates: 2014/05/01 Transfer to Canada Assets:US:Checking -40000 USD Assets:CA:Checking 50000 CAD @ 0.80 USD 2014/05/15 Transfer from Canada Assets:CA:Checking -50000 CAD @ 0.85 USD Assets:US:Checking 42500 USD This results in a trial balance with just 2500 USD: $ ledger -f l3.lgr bal 2500 USD Assets:US:Checking This is not a desirable outcome. We should require that the trial balance always sum to zero (except for virtual transactions). After all, this is the aggregate realization of the elegance of the double-entry method: because all transactions sum to zero, the total sum of any subgroup of transactions also sums to zero\u2026 except it doesn\u2019t. In my view, any balance sheet that gets rendered should comply with the accounting equation, precisely. In my explorations with Ledger, I was hoping that by virtue of its inventory tracking method it would somehow naturally deal with these conversions automatically and thus provide a good justification for keeping track of all units at cost, but I witness the same problem showing up. (The conversions issue has been a real bitch of a problem to figure out a solution for in Beancount, but it is working, and I think that the same solution could be applied to Ledger to adjust these sums, without changing its current booking method: automatically insert a special conversion entry at strategic points - when a balance sheet is rendered.) In the end, I have not found that tracking costs for every transaction would appear to have an advantage over forgetting the costs for price conversions, the conversions problem is independent of this. I\u2019d love to hear more support in favour of this design choice. Now, because all conversions maintain their cost, Ledger needs to be quite liberal about booking, because it would be inconvenient to force the user to specify the original rate of conversion for each currency, in the case of commonly occurring currency conversions. This has an unfortunate impact on those conversions which we do want to hold at cost: arbitrary lot reductions are allowed, that is, reductions against lots that do not exist. For example, the following trade does not trigger an error in Ledger: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash 2014/05/15 Sale of an impossible lot Assets:Investments:Stock -10 HOOL {505 USD} @ 510 USD Assets:Investments:Cash This results in an inventory with a long position of 10 HOOL and a short position of 10 HOOL (at a different cost): $ ledger -f l4.lgr bal --lots stock 10 HOOL {USD500} [2014/05/01] -10 HOOL {USD505} Assets:Investments:Stock I think that the reduction of a 505 USD position of HOOL should not have been allowed; Beancount treats this as an error by choice. Ledger clearly appears to support booking against an existing inventory, so surely the intention was to be able to reduce against existing positions. The following transactions do result in an empty inventory, for instance: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash 2014/05/15 Sale of matching lot Assets:Investments:Stock -10 HOOL {500 USD} [2014/05/01] @ 510 USD Assets:Investments:Cash With output: $ ledger -f l5.lgr bal --lots USD100 Equity:Capital Gains As you see, the HOOL position has been eliminated. (Don\u2019t mind the auto-inserted equity entry in the output, I believe this will be fixed in a pending patch .) However, both the date and the cost must be specified for this to work. The following transactions results in a similar problematic long + short position inventory as mentioned above: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash 2014/05/15 Sale of matching lot? Assets:Investments:Stock -10 HOOL {500 USD} @ 510 USD Assets:Investments:Cash With output: $ ledger -f l6.lgr bal --lots 10 HOOL {USD500} [2014/05/01] -10 HOOL {USD500} Assets:Investments:Stock USD100 Equity:Capital Gains -------------------- 10 HOOL {USD500} [2014/05/01] -10 HOOL {USD500} USD100 This is a bit surprising, I expected the lots to book against each other. I suspect this may be an unreported bug, and not intended behaviour. Finally, the \u201c {} \u201d cost syntax is allowed be used on augmenting legs as well. The documentation points to these methods being equivalent . It results in an inventory lot that does not have a date associated with it, but the other leg is not converted to cost: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL {500 USD} Assets:Investments:Cash With output: $ ledger -f l7.lgr bal --lots 0 Assets:Investments -10 HOOL {USD500} Cash 10 HOOL {USD500} Stock -------------------- 0 I probably just don\u2019t understand how this is meant to be used; in Beancount the automatically computed leg would have posted a -5000 USD amount to the Cash account, not shares. What I think is going on, is that Ledger (probably) accumulates all the lots without attempting to match them together to try to make reductions, and then at reporting time - and only then - it matches the lots together based on some reporting options: Group lots by both cost/price and date, using --lots Group lots by cost/price only, using --lot-prices Group lots by date only, using --lot-dates It is not entirely obvious from the documentation how that works, but the behavior is consistent with this. (If this is correct, I believe that to be a problem with its design: the mechanism by which an inventory reduction is booked to an existing set of lots should definitely not be a reporting feature. It needs to occur before processing reports, so that a single and definitive history of inventory bookings can be realized. If variants in bookings are supported - and I don\u2019t think this is a good idea - they should be supported before the reporting phase. In my view, altering inventory booking strategies from command-line options is a bad idea, the strategies in place should be fully defined by the language itself.) Shortcomings in Beancount \uf0c1 Beancount also has various shortcomings, though different ones. In contrast to Ledger, Beancount disambiguates between currency conversions and conversions \u201cheld at cost\u201d: 2014-05-01 * \"Convert some Loonies to Franklins\" Assets:CA:Checking -6000 CAD Assets:Investments:Cash 5000 USD @ 1.2 CAD ; Currency conversion 2014-05-01 * \"Buy some stock\" Assets:Investments:Stock 10 HOOL {500 USD} ; Held at cost Assets:Investments:Cash In the first transaction, the Assets:Investment:Cash account results in an inventory of 5000 USD, with no cost associated to it. However, after the second transaction the Assets:Investment:Stock account has an inventory of 10 HOOL with a cost of 500 USD associated to it. This is perhaps a little bit more complex, and requires more knowledge from the user: there are two kinds of conversions and he has to understand and distinguish between these two cases, and this is not obvious for newcomers to the system. Some user education is required (I\u2019ll admit it would help if I wrote more documentation). Beancount\u2019s method is also less liberal, it requires a strict application of lot reduction. That is, if you enter a transaction that reduces a lot that does not exist, it will output an error. The motivation for this is to make it difficult for the user to make a mistake in data entry. Any reduction of a position in an inventory has to match against exactly one lot. There are a few downsides that result from this choice: It requires the users to always find the cost of the lot to match against. This means that automating the import of transactions requires manual intervention from the user, as he has to go search in the ledger to insert the matching lot. (Note that theoretically the import code could load up the ledger contents to list its holdings, and if unambiguous, could look for the cost itself and insert it in the output. But this makes the import code dependent on the user\u2019s ledger, which most import codes aren't doing). This is an important step, as finding the correct cost is required in order to correctly compute capital gains, but a more flexible method is desired, one that allows the user to be a bit more lazy and not have to put the cost of the existing position when the choice is obvious, e.g., when there is a single lot of that unit to match against. I\u2019d like to relax this method somewhat. The strict requirement to reduce against an existing lot also means that both long and short positions of the same commodities \u201cheld at cost\u201d in the same account cannot exist. This is not much of a problem in practice, however, because short positions are quite rare\u2014few individuals engage in them\u2014and the user could always create separate accounts to hold short positions if needed. In fact, it is already customary to create a dedicated subaccount for each commodity in an investment account, as it naturally organizes trading activity nicely (reports a bit of a mess otherwise, it\u2019s nice to see the total position value grouped by stock, since they offer exposure to different market characteristics). Different accounts should work well, as long as we treat the signs correctly in reductions, i.e., buying stock against an existing short position is seen as a position reduction (the signs are just reversed). Another problem with the Beancount booking method is in how it matches against lots with dates. For example, if you were to try to disambiguate between two lots at the same price, the most obvious input would not work: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {500 USD} Assets:Investments:Cash 2014-02-15 * \"Second trade at very same price\" Assets:Investments:Stock 8 HOOL {500 USD} Assets:Investments:Cash 2014-06-30 * \"Trying to sell\" Assets:Investments:Stock -5 HOOL {500 USD / 2012-05-01} Assets:Investments:Cash This would report a booking error. This is confusing. In this case Beancount, in its desire to be strict, applies strict matching against the inventory lots, and attempting to match units of a lot of (HOOL, 500 USD, 2012-05-01) with units of (HOOL, 500 USD, None ) simply fails. Note that the lot-date syntax is accepted by Beancount on both augmenting and reducing legs, so the \u201csolution\u201d is that the user is forced to specifically provide the lot-date on acquiring the position. This would work, for instance: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {500 USD / 2012-05-01} Assets:Investments:Cash 2014-02-15 * \"Second trade at very same price\" Assets:Investments:Stock 8 HOOL {500 USD} Assets:Investments:Cash 2014-06-30 * \"Trying to sell\" Assets:Investments:Stock -5 HOOL {500 USD / 2012-05-01} Assets:Investments:Cash The inconvenience here is that when the user entered the first trade, he could not predict that he would enter another trade at the same price in the future and typically would not have inserted the lot-date. More likely than not, the user had to go back and revisit that transaction in order to disambiguate this. We can do better. This is a shortcoming of its implementation, which reflects the fact that position reduction was initially regarded as an exact matching problem rather than a fuzzy matching one\u2014it wasn\u2019t clear to me how to handle this safely at the time. This needs to be fixed after this proposal, and what I\u2019m doing with this document is very much a process of clarifying for myself what I want this fuzzy matching to mean, and to ensure that I come up with an unambiguous design that is easy to enter data for. Ledger always automatically attaches the transaction date to the inventory lot, and in my view this is the correct thing to do (below we propose attaching more information as well). Context \uf0c1 [Updated on Nov\u20192014] It is important to distinguish between two types of booking: Booking. The strict type of booking that occurs when we are considering the entire list of transactions. We will call this the \u201cbooking\u201d process, and it should occur just once. This process should be strict: the failure of correctly book a reduction to an existing lot should trigger a fatal error. Inventory Aggregation. When we are considering a subset of entries, it is possible that entries that add some lots are removed, and that other entries that reduce or remove those lots are kept. This creates a situation in which it is necessary to support aggregations over time of changes to an inventory that may not admit a matching lot. If we are to support arbitrary subsets of transactions being aggregated, we must take this into account. This aggregation process should never lead to an error. Let\u2019s take an example to justify why we need the non-booking summarization. Let\u2019s say we have a stock purchase and sale: 2013-11-03 * \"Buy\" Assets:Investments:VEA 10 AAPL {37.45 USD} ;; (A) Assets:Investments:Cash -370.45 USD 2014-08-09 * \"Sell\" Assets:Investments:VEA -5 AAPL {37.45 USD} Assets:Investments:Cash 194.40 USD Income:Investments:PnL If we filter \u201cby year\u201d and only want to view transactions that occurred in 2014, AND we don\u2019t \u201cclose\u201d the previous year, that is, we do not create opening balances entries that would deposit the lots in the account at the beginning of the year - this could happen if we filter by tag, or by some other combination of conditions - then the (A) lot is not present in the inventory to be debited from. We must somehow accommodate a -5 AAPL at that point. When reporting, the user could decide how to summarize and \u201cmatch up as best it can\u201d those legs, but converting them to units or cost, or convert them to a single lot at average-cost. The booking process, however, should admit no such laxity. It should be strict and trigger an error if a lot cannot be matched. This would only be applied on the total set of transactions, and only once, never on any subset. The purpose of the booking stage should be threefold: Match against existing lots and issue errors when that is impossible. Do this, using the method specified by the user. Replace all partially specified lots by a full lot specification, the lot that was matched during the booking process. Insert links on transactions that form a trade: a buy and corresponding sells should be linked together. This essentially identifies trades, and can then be used for reporting later on. (Trades at average cost will require special consideration.) We can view Ledger\u2019s method as having an aggregation method only, lacking a booking stage. The particular method for aggregation is chosen using --lots or --lot-dates or --lot-prices. This would indicate that a \u201cbooking\u201d stage could be tacked on to Ledger without changing much of its general structure: it could be inserted as a pre- or post- process, but it would require Ledger to sort the transactions by date, something it does not currently do (it runs a single pass over its data). Beancount has had both methods for a while, but the separate nature of these two operations has not been explicitly stated so far\u2014instead, the inventory supported an optional flag to raise an error when booking was impossible. Also, inventory aggregation has not been thought as requiring much customization, it was meant to be unified to the booking process. It is now clear that they are two distinct algorithms, and that a few different methods for aggregating can be relevant (though they do not function the same as Ledger\u2019s do. Note: you can view the inventory aggregation method as a GROUP BY within an Inventory object\u2019s lots: which columns do you group over? This requires use cases). In any case, fully specified lots should match against each other by default: we need to support this as the degenerate case to use for simple currencies (not held at cost)... we would not want to accumulate all changes in the same currency as separate lots in an inventory, they need to cross each other as soon as they are applied. We want to change this in order to make the code clearer: there should be a separate \u201cbooking\u201d stage, provided by a plugin , which resolves the partially specific lot reduction using the algorithm outlined in this document, and a separate method for inventory aggregation should not bother with any of those details. In fact, the inventory aggregation could potentially simply become an accumulated list of lots, and the summarization of them could take place a posteriori, with some conceptual similarly to when Ledger applies its aggregation. Just using a simple aggregation becomes more relevant once we begin entering more specific data about lots, such as always having an acquisition date, a label, and possibly even a link to the entry that created the lot. In order to trigger summarization sensibly, functions to convert and summarize accumulated inventories could be provided in the shell, such as \u201cUNITS(inventory)\u201d. Precision for Interpolation \uf0c1 The interpolation capabilities will be extended to cover eliding a single number, any number, in any subset of postings whose \u201cweights\u201d resolve to a particular currency (e.g., \u201call postings with weights in USD\u201d). The interpolation needs to occur at a particular precision. The interpolated number should be quantized automatically, and the number of fractional digits to quantize it to should be automatically inferred from the DisplayContext which is itself derived from the input file. Either the most common or the maximum number of digits witnessed in the file should be used. For a use case, see this thread : On Mon, Nov 24, 2014 at 12:33 PM, ELI wrote: Regarding the last piece on which there seems to be a misunderstanding between you and I, let me provide an standalone example, outside the context of the plugin. Since Vanguard only makes three decimal places of precision available me, I need to have the actual share count calculated from the price and transaction amount. For example, I have a transaction on my activities page with these details: Shares Transacted: 0.000 Share Price: 48.62 Amount: 0.02 The only way to enter this transaction and have it balance would be to manually calculate the Shares Transacted with four decimal places of precision. My preferred way of entering this would be to enter the Share Price and Amount and have Beancount calculate Shares Transacted to a precision associated with my Vanguard account. Additionally, as metadata, I'd record \"Shares Transacted: 0.000\" as history of \"what the statement said\". Maybe you can give me your thoughts on how such a case would be addressed with planned Beancount features or as a plugin I could right? What does the downloadable file contain? 0.000 or 0.0004 or even 0.00041? What this is, most likely, is the quarterly fee that they take, which they price in terms of shares. If this is your Vanguard account, and you sum all the similar such transactions around it, you should observe that it sums to 5$ per quarter. I would log it like this: 2014-11-23 * \"Quarterly fee\" Assets:US:Vanguard:VIIPX -0.00041 VIIPX {* 48.62 USD} Expenses:Financial:Fees 0.02 USD The \"*\" will be required to merge all the lots into a single one (average cost booking) because the 48.62 USD they declare is _not_ one of your lots, but rather just the price of the asset that happened to be there on the day they took the fee. In other words, they take their fee in terms of units of each asset, priced at the current price, deducting against the cost basis of all your lots together (it doesn't matter which because this is a pre-tax account, so no capital gains are reported). Now you're asking, how could I avoid calculating 0.00041 myself? My first answer would be: let's first see if the OFX download from Vanguard includes the precision. That would solve it. I believe it does (I checked my own history of downloads and I have some numbers in there with 4 fractional digits). My second answer would be that - if you don't have the number of shares from the file - after I implement the much needed new inventory booking proposal ( http://furius.ca/beancount/doc/proposal-booking ), the interpolation capabilities will be vastly extended, such that eliding the number of shares would even be possible, like this: 2014-11-23 * \"Quarterly fee\" Assets:US:Vanguard:VIIPX VIIPX {* 48.62 USD} Expenses:Financial:Fees 0.02 USD Now this by itself still would not solve the problem: this would store 0.000411353353 which is limited at 12 fractional digits because of the default context. So that's incorrect. What would need to be done to deal with this is to infer the most common number of digits used on units of VIIPX and to quantize the result to that number of digits (I think this could be safe enough). The number of digits appearing in the file for each currency is already tracked in a DisplayContext object that is stored in the options_map from the parser. I'll have to take that into account in the inventory booking proposal. I'm adding this to the proposal. Requirements \uf0c1 The previous sections introduce the general problem of booking, and point out important shortcomings in both the Beancount and Ledger implementations. This section will present a set of desires for a new and improved booking mechanism for command-line bookkeeping software implementations. Here are reasonable requirements for a new method: Explicit inventory lot selection needs to be supported. The user should be able to indicate precisely which of the lots to reduce a position against, per-transaction. An account or commodity should not have to have a consistent method applied across all its trades. Explicit inventory lot selection should support partial matching , that is, in cases where the lots to match against are ambiguous, the user should be able to supply only minimal information to disambiguate the lots, e.g., the date, or a label, or the cost, or combinations of these. For instance, if only a single lot is available to match against, the user should be able to specify the cost as \u201c {} \u201d, telling the system to book at cost, but essentially saying: \u201cbook against any lot.\u201d This should trigger an error only if there are multiple lots. A variety of informations should be supported to specify an existing lot, and they should be combinable: The cost of acquisition of the position; The date the position was acquired at; A user-provided label. Cases where insufficient information is provided for explicit lot selection is available should admit for a default booking method to be invoked. The user should be able to specify globally and per-account what the default booking method should be. This paves the way for implicit lot selection . A degenerate method should be available that kicks off an error and requires the user to be explicit. Average cost booking needs to be supported, as it is quite common in retirement or tax-sheltered accounts. This is the default method used in Canadian investment accounts. Cost basis adjustments need to be supported as well. The problem should be able to be specified by providing a specific lot (or all of a commodity\u2019s lots at average cost) and a dollar amount to adjust the position by. Stock splits need to be able to maintain some of the original attributes of the position , specifically, the original trade date and the user-specified label, if one has been provided. This should allow a common syntax to specify an original trading lot when reducing a position after a split. Reducing a position needs to always book against an existing lot . I\u2019m preserving the Beancount behavior here, which I\u2019ve argued for previously, as it works quite well and is a great method to avoid data entry mistakes. This naturally defines an invariant for inventories: all positions of a particular commodity held at cost should be of the same sign in one inventory (this can be used as a sanity check). Bookings that change the sign of a number of units should raise an error , unless an explicit exception is requested (and I\u2019m not even convinced that we need it). Note again that bookings only occur for units held at cost, so currency conversions are unaffected by this requirement. Beancount has had this feature for a while, and it has proved useful to detect errors. For example, if an inventory has a position of 8 HOOL {500 USD} you attempt to post a change of -10 units to this lot, the resulting number of units is now negative: -2. This should indicate user error. The only use case I can think for allowing this is the trading of futures spreads and currencies, which would naturally be reported in the same account (a short position in currencies is not regarded as a different instrument in the same way that a short position would); this is the only reason to provide an exception, and I suspect that 99% of users will not need it.] Debugging Tools \uf0c1 Since this is a non-trivial but greatly important part of the process of entering trading data, we should provide tools that list in detail the running balance for an inventory, including all of the detail of its lots. Booking errors being reported should include sufficient context, that is: The transaction with offending posting The offending posting The current state of the inventory before the posting is applied, with all its lots The implicit booking method that is in effect to disambiguate between lots A detailed reason why the booking failed Explicit vs. Implicit Booking Reduction \uf0c1 Another question that may come up in the design of a new booking method is whether we require the user to be explicit about whether he thinks this is an addition to a position, or a reduction. This could be done, for instance, by requiring a slightly different syntax for the cost, for example \u201c {} \u201d would indicate an addition and \u201c [] \u201d a reduction. I\u2019m not convinced that it is necessary to make that distinction, maybe the extra burden on the user is not worth it, but it might be a way to cross-check an expectation against the actual calculations that occur. I\u2019ll drop the idea for now. Design Proposal \uf0c1 This section presents a concrete description of a design that fulfills the previously introduced requirements. We hope to keep this as simple as possible. Inventory \uf0c1 An inventory is simply a list of lot descriptors. Each inventory lot will be defined by: UNITS, (CURRENCY, COST-UNITS, COST-CURRENCY, DATE, LABEL) Fields: UNITS: the number of units of that lot that are held CURRENCY: the type of commodity held, a string COST-UNITS: the number in the price of that lot COST-CURRENCY: the pricing commodity of the cost DATE: the acquisition date of the lot LABEL: an arbitrary user-specified string used to identify this lot If this represents a lot held at cost, after this processing, only the LABEL field should be optionally with a NULL value. Anyone writing code against this inventory could expect that all other values are filled in with non-NULL values (or are otherwise all NULL). Input Syntax & Filtering \uf0c1 The input syntax should allow the specification of cost as any combination of the following informations: The cost per unit , as an amount, such as \u201c 500 USD \u201d A total cost , to be automatically divided by the number of units, like this: {... +9.95 USD} . You should be able to use either cost per unit, total cost, or even combine the two, like this: {500 + 9.95 USD} . This is useful to enter commissions. This syntax also replaces the previous {{ ... }} syntax. The lot-date , as a YYYY-MM-DD date, such as \u201c 2014-06-20 \u201d A label , which is any non-numerical identifier, such as first-apple or some random uuid like \u201c aa2ba9695cc7 \u201d A special marker \u201c * \u201d that indicates to book at the average cost of the inventory balance These following postings should all be valid syntax: ... Assets:Investments:Stock 10 HOOL {500 USD} ; cost Assets:Investments:Stock 10 HOOL {339999615d7a} ; label Assets:Investments:Stock 10 HOOL {2014-05-01} ; lot-date Assets:Investments:Stock 10 HOOL {} ; no information ... Combinations Assets:Investments:Stock 10 HOOL {500 USD, 2014-05-01} Assets:Investments:Stock 10 HOOL {2014-05-01, 339999615d7a} Algorithm \uf0c1 All postings should be processed in a separate step after parsing, in the order of their date, against a running inventory balance for each account. The cost amount should become fully determined at this stage, and if we fail to resolve a cost, an error should be raised and the transaction deleted from the flow of directives (after the program loudly complaining about it). When processing an entry, we should match and filter all inventory lots against all the filters that are provided by the user. In general, after filtering the lots with the user restrictions: If the set of lots is empty, an error should be raised; If there is a single lot, this lot should be selected (success case); If there are multiple lots, the default implicitly booking method for the corresponding account should be invoked. Implicit Booking Methods \uf0c1 If there are multiple matching lots to choose from during booking, the following implicit booking methods could be invoked: STRICT: Select the only lot of the inventory. If there are more than one lots, raise an error. FIFO: Select the lot with the earliest date. LIFO: Select the lot with the latest date. AVERAGE: Book this transaction at average cost. AVERAGE_ONLY: Like AVERAGE but also trigger an aggregation on an addition to a position. This can be used to enforce that the only booking method for all lots is at the average cost. NONE: Don\u2019t perform any inventory booking on this account. Allow a mix of lots for the same commodity or positive and negative numbers in the inventory. (This essentially devolves to the Ledger method of booking.) This method would only get invoked if disambiguation between multiple lots is required, after filtering the lots against the expression provided by the user. The STRICT method is basically the degenerate disambiguation which issues an error if there is any ambiguity and should be the default. There should be a default method that applies to all accounts. The default value should be overridable. In Beancount, we would add a new \u201coption\u201d to allow the user to change this: option \"booking_method\" \"FIFO\" The default value used in a particular account should be specifiable as well, because it is a common case that the method varies by account, and is fixed within the account. In Beancount, this would probably become part of the open directive, something like this: 2003-02-17 open Assets:Ameritrade:HOOL HOOL booking:FIFO (I\u2019m not sure about the syntax.) Resolving Same Date Ambiguity \uf0c1 If the automatic booking method gets invoked to resolve an ambiguous lot reduction, e.g. FIFO, if there are multiple lots at the same date, something needs to be done to resolve which of the lots is to be chosen. The line number at which the transaction appears should be selected. For example, in the following case, a WIDGET Of 8 GBP would be selected: 2014-10-15 * \"buy widgets\" Assets:Inventory 10 WIDGET {} ;; Price inferred to 8 GBP/widget Assets:Cash -80 GBP 2014-10-15 * \"buy another widget\" Assets:Inventory 1 WIDGET {} ;; Price inferred to 9 GBP/widget Assets:Cash -9 GBP 2014-10-16 * \"sell a widget\" Assets:Cash 11 GBP Assets:Inventory -1 WIDGET {} ;; Ambiguous lot Dates Inserted by Default \uf0c1 By default, if an explicit date is not specified in a cost, the date of the transaction that holds the posting should be attached to the trading lot automatically. This date is overridable so that stock splits may be implemented by a transformation to a transaction like this: 2014-01-04 * \"Buy some HOOL\" Assets:Investments:Stock 10 HOOL {1000.00 USD} Assets:Investments:Cash 2014-04-17 * \"2:1 split into Class A and Class B shares\" Assets:Investments:Stock -10 HOOL {1000.00 USD, 2014-01-04} ; reducing Assets:Investments:Stock 10 HOOL {500.00 USD, 2014-01-04} ; augment Assets:Investments:Stock 10 HOOLL {500.00 USD, 2014-01-04} ; augment Matching with No Information \uf0c1 Supplying no information for the cost should be supported and is sometimes useful: in the case of augmenting a position, if all the other legs have values specified, we should be able to automatically infer the cost of the units being traded: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {} Assets:Investments:Cash -5009.95 USD Expenses:Commissions 9.95 USD In the case of reducing a position\u2014and we can figure that out whether that is the case by looking at the inventory at the point of applying the transaction to its account balance\u2014an empty specification should trigger the default booking method. If the method is STRICT, for instance, this would select a lot only if there is a single lot available (the choice is unambiguous), which is probably a common case if one trades infrequently. Other methods will apply as they are defined. Note that the user still has to specify a cost of \u201c {} \u201d in order to inform us that this posting has to be considered \u201cheld at cost.\u201d This is important to disambiguate from a price conversion with no associated cost. Reducing Multiple Lots \uf0c1 If a single trade needs to close multiple existing lots of an inventory, this can dealt with trivially by inserting one posting for each lot. I think this is a totally reasonable requirement. This could represent a single trade, for instance, if your brokerage allows it: 2012-05-01 * \"Closing my position\" Assets:Investments:Stock -10 HOOL {500 USD} Assets:Investments:Stock -12 HOOL {510 USD} Assets:Investments:Cash 12000.00 USD Income:Investments:Gains Note: If the cost basis specification for the lot matches multiple lots of the inventory and the result is unambiguous, the lots will be correctly selected. For example, if the ante-inventory contains just those two lots (22 HOOL in total), you should be able to have a single reducing posting like this: 2012-05-01 * \"Closing my position\" Assets:Investments:Stock -22 HOOL {} Assets:Investments:Cash 12000.00 USD Income:Investments:Gains and they will both be included. On the other hand, if the result is ambiguous (for example, if you have more than these two lots) the booking strategy for that account would be invoked. By default, this strategy will be \"STRICT\" which will generate an error, but if this account's strategy is set to \"FIFO\" (or is not set and the default global strategy is \"FIFO\"), the FIFO lots would be automatically selected for you, as per your wish. Lot Basis Modification \uf0c1 Another idea is to support the modification of a specific lot\u2019s cost basis in a single leg. The way this could work, is by specifying the number of units to modify, and the \u201c+ number currency\u201d syntax would be used to adjust the cost basis by a specific number, like this: 2012-05-01 * \"Adjust cost basis by 250 USD for 5 of the 500 USD units\" Assets:Investments:Stock ~5 HOOL {500 + 250 USD} If the number of units is smaller than the total number of units in the lot, split out the lot before applying the cost basis adjustment. I\u2019m not certain I like this. Not specifying the size could also adjust the entire position (I\u2019m not sure if this makes sense as it departs from the current syntax significantly): 2012-05-01 * \"Adjust cost basis by 250 USD for the 500 USD units\" Assets:Investments:Stock HOOL {500 + 250 USD} In any case, all the fields other than the total cost adjustment would be used to select which lot to adjust. Tag Reuse \uf0c1 We will have to be careful in the inventory booking to warn on reuse of lot labels. Labels should be unique. Examples \uf0c1 Nothing speaks more clearly than concrete examples. If otherwise unspecified, we are assuming that the booking method on the Assets:Investments:Stock account is STRICT . No Conflict \uf0c1 Given the following inventory lots: 21, (HOOL, 500, USD, 2012-05-01, null) 22, (AAPL, 380, USD, 2012-06-01, null) This booking should succeed: 2013-05-01 * Assets:Investments:Stock -10 HOOL {} ... This booking should fail (because the amount is not one of the valid lots): 2013-05-01 * Assets:Investments:Stock -10 HOOL {520 USD} ... This one too should fail (no currency matching lot): 2013-05-01 * Assets:Investments:Stock -10 MSFT {80 USD} ... So should this one (invalid date when a date is specified): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD, 2010-01-01} ... Explicit Selection By Cost \uf0c1 Given the following inventory: 21, (HOOL, 500, USD, 2012-05-01, null) 32, (HOOL, 500, USD, 2012-06-01, \u201cabc\u201d) 25, (HOOL, 510, USD, 2012-06-01, null) This booking should succeed: 2013-05-01 * Assets:Investments:Stock -10 HOOL {510 USD} ... This booking should fail if the stock account\u2019s method is STRICT (ambiguous): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD} ... This booking should succeed if the stock account\u2019s method is FIFO, booking against the lot at 2012-05-01 (earliest): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD} ... Explicit Selection By Date \uf0c1 Given the same inventory as previously, this booking should succeed: 2013-05-01 * Assets:Investments:Stock -10 HOOL {2012-05-01} ... This booking should fail if the method is STRICT (ambiguous) 2013-05-01 * Assets:Investments:Stock -10 HOOL {2012-06-01} ... Explicit Selection By Label \uf0c1 This booking should succeed, because there is a single lot with the \u201cabc\u201c label: 2013-05-01 * Assets:Investments:Stock -10 HOOL {abc} ... If multiple lots have the same label, ambiguous cases may occur; with this inventory, for example: 32, (HOOL, 500, USD, 2012-06-01, \u201cabc\u201d) 31, (HOOL, 510, USD, 2012-07-01, \u201cabc\u201d) The same booking should fail. Explicit Selection By Combination \uf0c1 With the initial inventory, this booking should succeed (unambiguous): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD, 2012-06-01} ... There is only one lot at a cost of 500 USD and at an acquisition date of 2012-06-01. Not Enough Units \uf0c1 The following booking would be unambiguous, but would fail, because there aren\u2019t enough units of the lot in the inventory to subtract from: 2013-05-01 * Assets:Investments:Stock -33 HOOL {500 USD, 2012-06-01} \u2026 Redundant Selection of Same Lot \uf0c1 If two separate postings select the same inventory lot in one transaction, it should be able to work: 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD, 2012-06-01} Assets:Investments:Stock -10 HOOL {abc} ... Of course, if there are not enough shares, it should fail: 2013-05-01 * Assets:Investments:Stock -20 HOOL {500 USD, 2012-06-01} Assets:Investments:Stock -20 HOOL {abc} ... Automatic Price Extrapolation \uf0c1 If all the postings of a transaction can have their balance computed, we allow a single price to be automatically calculated - this should work: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500.00 USD} Assets:US:Invest:HOOL 10.00 HOOL {} Income:US:Invest:Gains -340.51 USD And result in the equivalent of: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500.00 USD} Assets:US:Invest:HOOL 10.00 HOOL {534.51 USD} Income:US:Invest:Gains -340.51 USD Of course, preserving the original date of the lot should work too, so this should work as well: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500.00 USD} Assets:US:Invest:HOOL 10.00 HOOL {2014-02-04} Income:US:Invest:Gains -340.51 USD Average Cost Booking \uf0c1 Augmenting lots with the average cost syntax should fail: 2014-03-15 * \"Buying at average cost, what does this mean?\" Assets:US:Invest:Stock 10.00 HOOL {*} Income:US:Invest:Gains -5000.00 USD Reducing should work, this is the main use case: 2014-03-15 * \"Buying a first lot\" Assets:US:Invest:Stock 10.00 HOOL {500.00 USD} Assert:US:Invest:Cash -5000.00 USD 2014-04-15 * \"Buying a second lot\" Assets:US:Invest:Stock 10.00 HOOL {510.00 USD} Assets:US:Invest:Cash -5100.00 USD 2014-04-28 * \"Obtaining a dividend in stock\" Assets:US:Invest:Stock 1.00 HOOL {520.00 USD} Income:US:Invest:Dividends -520.00 USD 2014-05-20 * \"Sell some stock at average cost\" Assets:US:Invest:Stock -8.00 HOOL {*} Assets:US:Invest:Cash 4240.00 USD Income:US:Invest:Gains ; Automatically calculated: -194.29 USD The effect would be to aggregate the 10 HOOL {500.00 USD} lot, the 10 HOOL {510.00 USD} lot, and the 1.00 HOOL {520.00 USD} lot, together into a single lot of 21 HOOL {505.714285 USD}, and to remove 8 HOOL from this lot, which is 8 HOOL x 505.714285 USD/HOOL = 4045.71 USD. We receive 4240.00 USD, which allows us to automatically compute the capital gain: 4240.00 - 4045.71 = 194.29 USD. After the reduction, a single lot of 13 HOOL {505.714285 USD} remains. It should also work if we have multiple currencies in the account, that is adding the following transaction to the above should not make it faile: 2014-04-15 * \"Buying another stock\" Assets:US:Invest:Stock 15.00 AAPL {300.00 USD} Assets:US:Invest:Cash -4500.00 USD However it should fail if we have the same currency priced in distinct cost currencies in the same account: 2014-03-15 * \"Buying a first lot\" Assets:US:Invest:Stock 10.00 HOOL {500.00 USD} Assert:US:Invest:Cash -5000.00 USD 2014-04-15 * \"Buying a second lot\" Assets:US:Invest:Stock 10.00 HOOL {623.00 CAD} Assets:US:Invest:Cash -62300.00 CAD 2014-05-20 * \"Sell some stock at average cost\" Assets:US:Invest:Stock -8.00 HOOL {*} ; Which HOOL, USD or CAD? Assets:US:Invest:Cash 4240.00 USD Income:US:Invest:Gains But of course, this never happens in practice, so I\u2019m not too concerned. We could potentially support specifying the cost-currency to resolve this case, that is, replacing the sell leg with this: 2014-05-20 * \"Sell some stock at average cost\" Assets:US:Invest:Stock -8.00 HOOL {* USD} Assets:US:Invest:Cash 4240.00 USD Income:US:Invest:Gains I\u2019m quite sure it wouldn\u2019t be useful, but we could go the extra mile and be as general as we can possibly be. Future Work \uf0c1 This proposal does not yet deal with cost basis re-adjustments! We need to way to be able to add or remove a fixed dollar amount to a position\u2019s cost basis, that is, from (1) a lot identifier and (2) a cost-currency amount, we should be able to update the cost of that lot. The transaction still has to balance. Here are some ideas what this might look like: 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock 10.00 HOOL {500.00 USD + 230.00 USD} Income:US:Invest:Gains -230.00 USD The resulting inventory would be 10 HOOL at 523.00 USD per share, and the rest of the original lots, less 10 HOOL at 500.00 USD per share (there might be some of that remaining, that\u2019s fine). This is the equivalent of 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock -10.00 HOOL {500.00 USD} Assets:US:Invest:Stock 10.00 HOOL {523.00 USD} Income:US:Invest:Gains -230.00 USD except you did not have to carry out the calculation. This should therefore be implemented as a transformation. However, this is not super useful, because this is already supported... adjustments are usually performed on lots at average cost, and this is where the problem lie: you\u2019d have to make the calculation yourself! Here\u2019s a relevant use case: 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock 10.00 HOOL {* + 230.00 USD} Income:US:Invest:Gains -230.00 USD This would first average all the lots of HOOL together and recompute the cost basis with the new amount. This should work: 2014-03-15 * \"Buying a first lot\" Assets:US:Invest:Stock 5.00 HOOL {500.00 USD} Assert:US:Invest:Cash -2500.00 USD 2014-04-15 * \"Buying a second lot\" Assets:US:Invest:Stock 5.00 HOOL {520.00 USD} Assets:US:Invest:Cash -2600.00 USD 2014-05-20 * \"Adjust cost basis of the Hooli units\" Assets:US:Invest:Stock 10.00 HOOL {* + 230.00 USD} Income:US:Invest:Gains -230.00 USD Notice how we still have to specify an amount of HOOL shares to be repriced. I like this, but I\u2019m wondering if we should allow specifying \u201call the units\u201d like this (this would be a more radical change to the syntax): 2014-05-20 * \"Adjust cost basis of the Hooli units\" Assets:US:Invest:Stock * HOOL {* + 230.00 USD} Income:US:Invest:Gains -230.00 USD Perhaps the best way to auto-compute cost basis adjustments would be via powerful interpolation, like this: 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock -10.00 HOOL {500 USD} ; reduce Assets:US:Invest:Stock 10.00 HOOL {} ; augment, interpolated Income:US:Invest:Gains -230.00 USD See the Smarter Elision document for more details on a proposal. Inter-Account Booking \uf0c1 Some tax laws require a user to book according to a specific method, and this might apply between all accounts. This means that some sort of transfer needs to be applied in order to handle this correctly. See the separate document for detail. TODO - complete this with more tests for average cost booking! Implementation Notes \uf0c1 Separate Parsing & Interpolation \uf0c1 In order to implement a syntax for reduction that does not specify the cost, we need to make changes to the parser. Because there may be no costs on the posting, it becomes impossible to perform balance checks at parsing time. Therefore, we will need to postpone balance checks to a stage after parsing. This is reasonable and in a way nice: it is best if the Beancount parser does not output many complex errors at that stage. A new \u201cpost-parse\u201d stage will be added, right before running the plugins. Conclusion \uf0c1 This document is work-in-progress. I\u2019d really love to get some feedback in order to improve the suggested method, which is why I put this down in words instead of just writing the code. I think a better semantic can be reached by iterating on this design to produce something that works better in all systems, and this can be used as documentation later on for developers to understand why it is designed this way. Your feedback is much appreciated.","title":"A Proposal for an Improvement on Inventory Booking"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#inventory-booking","text":"A Proposal for an Improvement on Command-Line Bookkeeping Martin Blais, June 2014 http://furius.ca/beancount/doc/proposal-booking Motivation Problem Description Lot Date Average Cost Basis Capital Gains Sans Commissions Cost Basis Adjustments Dealing with Stock Splits Previous Solutions Shortcomings in Ledger Shortcomings in Beancount Requirements Debugging Tools Explicit vs. Implicit Booking Reduction Design Proposal Inventory Input Syntax & Filtering Algorithm Implicit Booking Methods Dates Inserted by Default Matching with No Information Reducing Multiple Lots Examples No Conflict Explicit Selection By Cost Explicit Selection By Date Explicit Selection By Label Explicit Selection By Combination Not Enough Units Redundant Selection of Same Lot Automatic Price Extrapolation Average Cost Booking Future Work Implementation Note Conclusion","title":"Inventory Booking"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#motivation","text":"The problem of \u201cinventory booking,\u201d that is, selecting which of an inventory\u2019s trade lots to reduce when closing a position, is a tricky one. So far, in the command-line accounting community, relatively little progress has been made in supporting the flexibility to deal with many common real-life cases. This document offers a discussion of the current state of affairs, describes the common use cases we would like to be able to solve, identifies a set of requirements for a better booking method, and proposes a syntax and implementation design to support these requirements, along with clearly defined semantics for the booking method.","title":"Motivation"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#problem-description","text":"The problem under consideration is the problem of deciding, for a double-entry transaction that intends to reduce the size of a position at a particular point in time in a particular account, which of the account inventory lots contained at that point should be reduced. This should be specified using a simple data entry syntax that minimizes the burden on the user. For example, one could enter a position in HOOL stock by buying two lots of it at different points in time: 2014-02-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {500 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD 2014-02-15 * \"Second trade\" Assets:Investments:Stock 8 HOOL {510 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD We will call the two sets of shares \u201ctrade lots\u201d and assume that the underlying system that counts them is keeping track of each of their cost and date of acquisition separately. The question here is, if we were to sell some of this stock, which of the shares should we select? Those from the first trade, or those from the second? This is an important decision, because it has an impact on capital gains, and thus on taxes. (The account of capital gains is described in more detail in the \u201cStock Trading\u201d section of the Beancount cookbook if you need an explanation of this.) Depending on our financial situation, this year\u2019s trading history, and the unrealized gains that a trade would realize, sometimes we may want to select one lot over another. By now, most discount brokers even let you select your specific lot when you place a trade that reduces or closes a position. It is important to emphasize here the two directions of inventory booking: Augmenting a position. This is the process of creating a new trading lot, or adding to an existing trading lot (if the cost and other attributes are identical). This is easy, and amounts to adding a record to some sort of mapping that describes an account\u2019s balance (I call this an \"inventory\"). Reducing a position. This is generally where booking complications take place, and the problem is essentially to figure out which lot of an existing position to remove units from. Most of this document is dedicated to the second direction.","title":"Problem Description"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#lot-date","text":"Even if the cost of each lot is identical, the acquisition date of the position you\u2019re intending to close matters, because different taxation rules may apply, for example, in the US, positions held for more than 12 months are subject to a significantly lower tax rate (\u201cthe long-term capital gains rate\u201d). For example, if you entered your position like this: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {300 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD 2014-02-15 * \"Second trade at same price\" Assets:Investments:Stock 8 HOOL {300 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD and we are booking a trade for 2014-03-01, selecting the first lot would result in a long-term (low) gain, because two years have passed since acquisition (position held from 2012-05-01 to 2014-03-01) while booking against the second would result in a short-term (high) capital gains tax. So it\u2019s not just a matter of the cost of the lots. Our systems don't aim to file taxes automatically at this point but we would like to enter our data in a way that eventually allows this kind of reporting to take place. Note that this discussion assumes that you were able to decide which of the lots you could trade against. There are several taxation rules that may apply, and in some cases, depending on which country you live in, you may not have a choice, the government may dictate how you are meant to report your gains. In some cases you may even need to be able to apply multiple booking methods to the same account (e.g., if an account is subject to a taxation claim from multiple countries). We will ignore this exotic case in this document, as it is quite rare.","title":"Lot Date"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#average-cost-basis","text":"Things get more complicated for tax-sheltered accounts. Because there are no tax consequences to closing positions in these accounts, brokers will normally choose to account for the book value of your positions as if they were a single lot. That is, the cost of each share is computed using the weighted average of the cost of the position you are holding. This can equivalently be calculated by summing the total cost of each position and then dividing by the total number of shares. To continue with our first example: 10 HOOL x 500 USD/HOOL = 5000 USD 8 HOOL x 510 USD/HOOL = 4080 USD Cost basis = 5000 USD + 4080 USD = 9080 USD Total number of shares = 10 HOOL + 8 HOOL = 18 HOOL Cost basis per share = 9080 USD / 18 HOOL = 504.44 USD/HOOL So if you were to close some of your position and sell some shares, you would do so at a cost of 504.44 USD/HOOL. The gain you would realize would be relative to this cost; for example if you sold 5 shares at 520 USD/HOOL, your gain would be calculated like this: (520 USD/HOOL - 504.44 USD/HOOL) x 5 HOOL = 77.78 USD This type of booking method is made evident by two kinds of financial events that you might witness occur in these types of accounts: Fees may be withdrawn by selling some shares reported at their current market price. You will see these if you hold a retirement account at Vanguard in the USA. The broker takes a fixed amount of dollars per quarter (5$, with my employer's plan) that is backed out in terms of a small, fractional number of shares of each fund to sell. That transaction is oblivious to capital gains: they simply figure out how many shares to sell to cover their fee and don\u2019t report a gain to you. You have to assume it doesn\u2019t matter. Most people don't track their gains in non-taxable accounts but freaks like us may want to compute the return nonetheless. Cost basis readjustments may occur spontaneously. This is rare, but I have seen this once or twice in the case of mutual funds in a Canadian tax-deferred account: based on some undisclosed details of their trading activity, the fund management has had to issue an adjustment to the cost basis, which you are meant to apply to your positions. You get an email telling you how much the adjustment was for. 99% of people probably don\u2019t blink, don't understand and ignore this message\u2026 perhaps rightly so, as it has no impact on their taxes, but if you want to account for your trading returns in that account, you do need to count it. So we need to be able to add or remove to the cost basis of existing positions. Note that the average cost of your positions changes on every trade and needs to get recalculated every time that you add to an existing position. A problem with this is that if you track the cost per share, these multiple recalculations may result in some errors if you store the amounts using a fixed numerical representation (typically decimal numbers). An alternative method to track the cost in our data structures, one that is more stable numerically, is to simply keep track of the total cost instead of the cost-per-share. Also note that using fractional numbers does not provide a sufficient answer to this problem: in practice, the broker may report a specific cost, one that is rounded to specific number of decimals, and we may want to be able to book our reducing trade using that reported number rather than maintain the idealized amount that a fractional representation would. Both using a fixed decimal or a fractional representation pose problems. We will largely ignore those problems here. In summary: we need to be able to support book against lots at their average cost, and we need to be able to adjust the cost basis of an existing position. I\u2019ve been thinking about a solution to implement this that would not force an account to be marked with a special state. I think we can do this using only inventory manipulations. All we need is a small change to the syntax that allows the user to indicate to the system that it should book against the average cost of all positions. Using the first example above, acquiring the shares would take the same form as previously, that is after buying two lots of 10 and 8 HOOL units, we end up with an inventory that holds two lots, one of 10 HOOL {500 USD} and one of 8 HOOL {510 USD}. However, when it\u2019s time to sell, the following syntax would be used (this is an old idea I\u2019ve been meaning to implement for a while): 2014-02-01 * \"Selling 5 shares at market price 550 USD\" Assets:Investments:Stock -5 HOOL {*} Assets:Investments:Cash 2759.95 USD Expenses:Commissions 9.95 USD Income:Investments:CapitalGains When the \u201c * \u201d is encountered in lieu of the cost, like this, it would: Merge all the lots together and recalculate the average price per share (504.44 USD) Book against this merged inventory, reducing the resulting lot. After the transaction, we would end up with a single position of 13 HOOL {504.44 USD). Adding to this position at a different price would create a new lot, and those would get merged again the next time a reduction occurs at average cost. We do not need to merge lots until there is a reduction. Apart from concerns of accumulating rounding error, this solution is correct mathematically, and maintains the property that accounts aren\u2019t coerced into any specific booking method\u2014a mix of methods could be used on every reduction. This is a nice property, even if not strictly necessary, and even if we want to be able to just specify a \u201cdefault\u201d booking method to use per account and just stick with it throughout. It\u2019s always nice to have the flexibility to support exceptions, because in real life, they sometimes occur.","title":"Average Cost Basis"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#capital-gains-sans-commissions","text":"We would like to be able to support the automatic calculation of capital gains without the commissions. This problem is described in much detail in the Commissions section of the trading documentation and in a thread on the mailing-list . The essence of the complication that occurs is that one cannot simply subtract the commissions incurred during the reporting period from the gains that include commissions, because the commissions that were incurred to acquire the position must be pro-rated to the shares of the position that are sold. The simplest and most common way to implement this is to include the costs of acquiring the position into the cost basis of the position itself, and deduct the selling costs from the market value when a position is reduced. Whatever new design we come up with must allow us to count these adjusted gains as this is essential to various individuals' situations. In Beancount, I have figured out a solution for this problem, which luckily enough involves only an automated transformation of a transaction flagged by the presence of a special flag on its postings... if and only if I can find a way to specify which lot to book against without having to specify its cost, because once the cost of commissions gets folded into the cost of the position, the adjusted cost does not show up anywhere in the input file, the user would have to calculate it manually, which is unreasonable. In the solution I\u2019m proposing, the following transactions would be transformed automatically, triggered by the presence of the \u201cC\u201d flag: 2014-02-10 * \"Buy\" Assets:US:Invest:Cash -5009.95 USD C Expenses:Commissions 9.95 USD Assets:US:Invest:HOOL 10.00 HOOL {500 USD / aa2ba9695cc7} 2014-04-10 * \"Sell #1\" Assets:US:Invest:HOOL -4.00 HOOL {aa2ba9695cc7} C Expenses:Commissions 9.95 USD Assets:US:Invest:Cash 2110.05 USD Income:US:Invest:Gains 2014-05-10 * \"Sell #2\" Assets:US:Invest:HOOL -6.00 HOOL {aa2ba9695cc7} C Expenses:Commissions 9.95 USD Assets:US:Invest:Cash 3230.05 USD Income:US:Invest:Gains They would be automatically transformed by a plugin into the following and replace the original transactions above: 2014-02-10 * \"Buy\" Assets:US:Invest:Cash -5009.95 USD X Expenses:Commissions 9.95 USD X Income:US:Invest:Rebates -9.95 USD X Assets:US:Invest:HOOL 10.00 HOOL {500.995 USD / aa2ba9695cc7} 2014-04-10 * \"Sell #1\" X Assets:US:Invest:HOOL -4.00 HOOL {aa2ba9695cc7} X Expenses:Commissions 9.95 USD X Income:US:Invest:Rebates -9.95 USD Assets:US:Invest:Cash 2110.05 USD Income:US:Invest:Gains ; Should be (530-500)*4 - 9.95*(4/10) - 9.95 = ; 106.07 USD 2014-05-10 * \"Sell #2\" X Assets:US:Invest:HOOL -6.00 HOOL {aa2ba9695cc7} X Expenses:Commissions 9.95 USD X Income:US:Invest:Rebates -9.95 USD Assets:US:Invest:Cash 3230.05 USD Income:US:Invest:Gains ; Should be (540-500)*6 - 9.95*(6/10) - 9.95 = ; 224.08 USD The \u201cX\u201d flag would just mark the postings that have been transformed, so maybe they can be rendered with a special color or attribute later on. The rebates account is included as a separate counter just to make the transaction balance. The reason I need to relax the disambiguation of trading lots is that the user needs to be able to specify the matching leg without having to specify the cost of the position, because at the point where the position is reduced (2014-04-10), there is no way to figure out what the cost of the original lot was. Just to be clear, in the example above, this means that if all the information have is 4 HOOL and 500 USD, there is no way to back out a cost of 500.995 USD that could specify a match with the opening trade lot, because that happened with 10 HOOLs. So we need to be more explicit about booking. In the example above, I\u2019m selecting the matching lot \u201cby label,\u201d that is, the user has the option to provide a unique lot identifier in the cost specification, and that can later on be used to disambiguate which lot we want to book a reduction/sale against. The example above uses the string \u201c aa2ba9695cc7 \u201d in this way. (An alternative solution to this problem would involve keep track of both the original cost (without commissions), and the actual cost (with commissions), and then finding the lot against the former, but using the latter to balance the transaction. This idea is to allow the user to keep using the cost of a position to select the lot, but I\u2019m not even sure if that is possible, in the presence of various changes to the inventory. More thought is required on this matter.)","title":"Capital Gains Sans Commissions"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#cost-basis-adjustments","text":"In order to adjust the cost basis, one can replace the contents of an account explicitly like this: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500 USD} Assets:US:Invest:HOOL 10.00 HOOL {510 USD} Income:US:Invest:Gains This method works well and lets the system automatically compute the gains. But it would be nice to support making adjustments in price units against the total cost of the position, for example, \u201cadd 340.51 USD to the cost basis of this position\u201d. The problem is that the adjusted cost per share now needs to be computed by the user\u2026 it\u2019s inconvenient. Here\u2019s one way we could support this well, however: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500 USD} Assets:US:Invest:HOOL 10.00 HOOL {} Income:US:Invest:Gains -340.51 USD If all the legs are fully specified - they have a calculatable balance - we allow a single price to be elided. This solves this problem well.","title":"Cost Basis Adjustments"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#dealing-with-stock-splits","text":"Accounting for stock splits creates some complications in terms of booking. In general, the problem is that we need to deal with changes in the meaning of a commodity over time. For example, you could do this in Beancount right now and it works: 2014-01-04 * \"Buy some HOOL\" Assets:Investments:Stock 10 HOOL {1000.00 USD} Assets:Investments:Cash 2014-04-17 * \"2:1 split into Class A and Class B shares\" Assets:Investments:Stock -10 HOOL {1000.00 USD} Assets:Investments:Stock 10 HOOL {500.00 USD} Assets:Investments:Stock 10 HOOLL {500.00 USD} One problem with splitting lots this way is that the price and meaning of a HOOL unit before and after the split differs, but let\u2019s just assume we\u2019re okay with that for now (this can be solved by relabeling the commodity by using another name, ideally in a way that is not visible to the user - we have a pending discussion elsewhere ). The issue that concerns booking is, when you sell that position, which cost do you use? The inventory will contain positions at 500 USD, so that\u2019s what you should be using, but is it obvious to the user? We can assume this can be learned with a recipe . Now, what if we automatically attach the transaction date? Does it get reset on the split and now you would have to use 2014-04-17? If so, we could not automatically inspect the list of trades to determine whether this is a long-term vs. short-term trade. We need to somehow preserve some of the attributes of the original event that augmented this position, which includes the original trade date (not the split date) and the original user-specified label on the position, if one was provided.","title":"Dealing with Stock Splits"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#forcing-a-single-method-per-account","text":"For accounts that will use the average booking method, it may be useful to allow specifying that an account should only use this booking method . The idea is to avoid data entry errors when we know that an account is only able to use this method. One way to ensure this is to automatically aggregate inventory lots when augmenting a position. This ensures that at any point in time, there is only a single lot for each commodity. If we associated an inventory booking type to each account, we could define a special one that also triggers aggregation upon the addition of a position. Another approach would be to not enforce these aggregations, but to provide a plugin that would check that for those accounts that are marked as using the average cost inventory booking method by default, that only such bookings actually take place.","title":"Forcing a Single Method per Account"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#previous-solutions","text":"This section reviews existing implementations of booking methods in command-line bookkeeping systems and highlights specific shortcomings that motivate why we need to define a new method.","title":"Previous Solutions"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#shortcomings-in-ledger","text":"(Please note: I\u2019m not deeply familiar with the finer details of the inner workings of Ledger; I inferred its behavior from building test examples and reading the docs. If I got any of this wrong, please do let me know by leaving a comment.) Ledger\u2019s approach to booking is quite liberal. Internally, Ledger does not distinguish between conversions held at cost and regular price conversions : all conversions are assumed held at cost, and the only place trading lots appear is at reporting time (using its --lots option). Its \u201c {} \u201d cost syntax is meant to be used to disambiguate lots on a position reduction , not to be used when acquiring a position. The cost of acquiring a position is specified using the \u201c @ \u201d price notation: 2014/05/01 * Buy some stock (Ledger syntax) Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash This will result in an inventory of 10 HOOL {500 USD}, that is, the cost is always stored in the inventory that holds the position: $ ledger -f l1.lgr bal --lots 10 HOOL {USD500} [2014/05/01] USD-5000 Assets:Investments USD-5000 Cash 10 HOOL {USD500} [2014/05/01] Stock -------------------- 10 HOOL {USD500} [2014/05/01] USD-5000 There is no distinction between conversions at cost and without cost\u2014all conversions are tracked as if \u201cat cost.\u201d As we will see in the next section, this behaviour is distinct from Beancount\u2019s semantics, which requires a conversion held at cost to be specified using the \u201c {} \u201d notation for both its augmenting and reducing postings, and distinguishes between positions held at cost and price conversions. The advantage of the Ledger method is a simpler input mechanism, but it leads to confusing outcomes if you accumulate many conversions of many types of currencies in the same account. An inventory can easily become fragmented in its lot composition. Consider what happens, for instance, if you convert in various directions between 5 different currencies\u2026 you may easily end up with USD held in CAD, JPY, EUR and GBP and vice-versa\u2026 any possible combinations of those are possible. For instance, the following currency conversions will maintain their original cost against their converted currencies: 2014/05/01 Transfer from Canada Assets:CA:Checking -500 CAD Assets:US:Checking 400 USD @ 1.25 CAD 2014/05/15 Transfers from Europe Assets:UK:Checking -200 GBP Assets:US:Checking 500 USD @ 0.4 GBP 2014/05/21 Transfers from Europe Assets:DE:Checking -100 EUR Assets:US:Checking 133.33 USD @ 0.75 EUR This results in the following inventory in the Assets:US:Checking account: $ ledger -f l2.lgr bal --lots US:Checking 500.00 USD {0.4 GBP} [2014/05/15] 133.33 USD {0.75 EUR} [2014/05/21] 400.00 USD {1.25 CAD} [2014/05/01] Assets:US:Checking The currencies are treated like stock. But nobody thinks of their currency balances like this , one normally thinks of currencies held in an account as units in and of themselves, not in terms of their price related to some other currency. In my view, this is confusing. After these conversions, I just think of the balance of that account as 1033.33 USD. A possible rebuttal to this observation is that most of the time a user does not care about inventory lots for accounts with just currencies, so they just happen not print them out. Out of sight, out of mind. But there is no way to distinguish when the rendering routine should render lots or not. If instructed to produce a report, a balance routine should ideally only render lots only for some accounts (e.g., investing accounts, where the cost of units matters) and not for others (e.g. accounts with currencies that were deposited sometimes as a result of conversions). Moreover, when I render a balance sheet, for units held at cost we need to report the book values rather than the number of shares. But for currencies, the currency units themselves need to get reported, always. If you don\u2019t distinguish between the two cases, how do you know which to render? I think the answer is to select which view you want to render using the options. The problem is that neither provides a single unified view that does the correct thing for both types of commodities. The user should not have to specify options to view partially incorrect views in one style or the other, this should be automatic and depend on whether we care about the cost of those units. This gets handled correctly if you distinguish between the two kinds of conversions. But perhaps most importantly, this method does not particularly address the conversion problem : it is still possible to create money out of thin air by making conversions back and forth at different rates: 2014/05/01 Transfer to Canada Assets:US:Checking -40000 USD Assets:CA:Checking 50000 CAD @ 0.80 USD 2014/05/15 Transfer from Canada Assets:CA:Checking -50000 CAD @ 0.85 USD Assets:US:Checking 42500 USD This results in a trial balance with just 2500 USD: $ ledger -f l3.lgr bal 2500 USD Assets:US:Checking This is not a desirable outcome. We should require that the trial balance always sum to zero (except for virtual transactions). After all, this is the aggregate realization of the elegance of the double-entry method: because all transactions sum to zero, the total sum of any subgroup of transactions also sums to zero\u2026 except it doesn\u2019t. In my view, any balance sheet that gets rendered should comply with the accounting equation, precisely. In my explorations with Ledger, I was hoping that by virtue of its inventory tracking method it would somehow naturally deal with these conversions automatically and thus provide a good justification for keeping track of all units at cost, but I witness the same problem showing up. (The conversions issue has been a real bitch of a problem to figure out a solution for in Beancount, but it is working, and I think that the same solution could be applied to Ledger to adjust these sums, without changing its current booking method: automatically insert a special conversion entry at strategic points - when a balance sheet is rendered.) In the end, I have not found that tracking costs for every transaction would appear to have an advantage over forgetting the costs for price conversions, the conversions problem is independent of this. I\u2019d love to hear more support in favour of this design choice. Now, because all conversions maintain their cost, Ledger needs to be quite liberal about booking, because it would be inconvenient to force the user to specify the original rate of conversion for each currency, in the case of commonly occurring currency conversions. This has an unfortunate impact on those conversions which we do want to hold at cost: arbitrary lot reductions are allowed, that is, reductions against lots that do not exist. For example, the following trade does not trigger an error in Ledger: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash 2014/05/15 Sale of an impossible lot Assets:Investments:Stock -10 HOOL {505 USD} @ 510 USD Assets:Investments:Cash This results in an inventory with a long position of 10 HOOL and a short position of 10 HOOL (at a different cost): $ ledger -f l4.lgr bal --lots stock 10 HOOL {USD500} [2014/05/01] -10 HOOL {USD505} Assets:Investments:Stock I think that the reduction of a 505 USD position of HOOL should not have been allowed; Beancount treats this as an error by choice. Ledger clearly appears to support booking against an existing inventory, so surely the intention was to be able to reduce against existing positions. The following transactions do result in an empty inventory, for instance: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash 2014/05/15 Sale of matching lot Assets:Investments:Stock -10 HOOL {500 USD} [2014/05/01] @ 510 USD Assets:Investments:Cash With output: $ ledger -f l5.lgr bal --lots USD100 Equity:Capital Gains As you see, the HOOL position has been eliminated. (Don\u2019t mind the auto-inserted equity entry in the output, I believe this will be fixed in a pending patch .) However, both the date and the cost must be specified for this to work. The following transactions results in a similar problematic long + short position inventory as mentioned above: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash 2014/05/15 Sale of matching lot? Assets:Investments:Stock -10 HOOL {500 USD} @ 510 USD Assets:Investments:Cash With output: $ ledger -f l6.lgr bal --lots 10 HOOL {USD500} [2014/05/01] -10 HOOL {USD500} Assets:Investments:Stock USD100 Equity:Capital Gains -------------------- 10 HOOL {USD500} [2014/05/01] -10 HOOL {USD500} USD100 This is a bit surprising, I expected the lots to book against each other. I suspect this may be an unreported bug, and not intended behaviour. Finally, the \u201c {} \u201d cost syntax is allowed be used on augmenting legs as well. The documentation points to these methods being equivalent . It results in an inventory lot that does not have a date associated with it, but the other leg is not converted to cost: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL {500 USD} Assets:Investments:Cash With output: $ ledger -f l7.lgr bal --lots 0 Assets:Investments -10 HOOL {USD500} Cash 10 HOOL {USD500} Stock -------------------- 0 I probably just don\u2019t understand how this is meant to be used; in Beancount the automatically computed leg would have posted a -5000 USD amount to the Cash account, not shares. What I think is going on, is that Ledger (probably) accumulates all the lots without attempting to match them together to try to make reductions, and then at reporting time - and only then - it matches the lots together based on some reporting options: Group lots by both cost/price and date, using --lots Group lots by cost/price only, using --lot-prices Group lots by date only, using --lot-dates It is not entirely obvious from the documentation how that works, but the behavior is consistent with this. (If this is correct, I believe that to be a problem with its design: the mechanism by which an inventory reduction is booked to an existing set of lots should definitely not be a reporting feature. It needs to occur before processing reports, so that a single and definitive history of inventory bookings can be realized. If variants in bookings are supported - and I don\u2019t think this is a good idea - they should be supported before the reporting phase. In my view, altering inventory booking strategies from command-line options is a bad idea, the strategies in place should be fully defined by the language itself.)","title":"Shortcomings in Ledger"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#shortcomings-in-beancount","text":"Beancount also has various shortcomings, though different ones. In contrast to Ledger, Beancount disambiguates between currency conversions and conversions \u201cheld at cost\u201d: 2014-05-01 * \"Convert some Loonies to Franklins\" Assets:CA:Checking -6000 CAD Assets:Investments:Cash 5000 USD @ 1.2 CAD ; Currency conversion 2014-05-01 * \"Buy some stock\" Assets:Investments:Stock 10 HOOL {500 USD} ; Held at cost Assets:Investments:Cash In the first transaction, the Assets:Investment:Cash account results in an inventory of 5000 USD, with no cost associated to it. However, after the second transaction the Assets:Investment:Stock account has an inventory of 10 HOOL with a cost of 500 USD associated to it. This is perhaps a little bit more complex, and requires more knowledge from the user: there are two kinds of conversions and he has to understand and distinguish between these two cases, and this is not obvious for newcomers to the system. Some user education is required (I\u2019ll admit it would help if I wrote more documentation). Beancount\u2019s method is also less liberal, it requires a strict application of lot reduction. That is, if you enter a transaction that reduces a lot that does not exist, it will output an error. The motivation for this is to make it difficult for the user to make a mistake in data entry. Any reduction of a position in an inventory has to match against exactly one lot. There are a few downsides that result from this choice: It requires the users to always find the cost of the lot to match against. This means that automating the import of transactions requires manual intervention from the user, as he has to go search in the ledger to insert the matching lot. (Note that theoretically the import code could load up the ledger contents to list its holdings, and if unambiguous, could look for the cost itself and insert it in the output. But this makes the import code dependent on the user\u2019s ledger, which most import codes aren't doing). This is an important step, as finding the correct cost is required in order to correctly compute capital gains, but a more flexible method is desired, one that allows the user to be a bit more lazy and not have to put the cost of the existing position when the choice is obvious, e.g., when there is a single lot of that unit to match against. I\u2019d like to relax this method somewhat. The strict requirement to reduce against an existing lot also means that both long and short positions of the same commodities \u201cheld at cost\u201d in the same account cannot exist. This is not much of a problem in practice, however, because short positions are quite rare\u2014few individuals engage in them\u2014and the user could always create separate accounts to hold short positions if needed. In fact, it is already customary to create a dedicated subaccount for each commodity in an investment account, as it naturally organizes trading activity nicely (reports a bit of a mess otherwise, it\u2019s nice to see the total position value grouped by stock, since they offer exposure to different market characteristics). Different accounts should work well, as long as we treat the signs correctly in reductions, i.e., buying stock against an existing short position is seen as a position reduction (the signs are just reversed). Another problem with the Beancount booking method is in how it matches against lots with dates. For example, if you were to try to disambiguate between two lots at the same price, the most obvious input would not work: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {500 USD} Assets:Investments:Cash 2014-02-15 * \"Second trade at very same price\" Assets:Investments:Stock 8 HOOL {500 USD} Assets:Investments:Cash 2014-06-30 * \"Trying to sell\" Assets:Investments:Stock -5 HOOL {500 USD / 2012-05-01} Assets:Investments:Cash This would report a booking error. This is confusing. In this case Beancount, in its desire to be strict, applies strict matching against the inventory lots, and attempting to match units of a lot of (HOOL, 500 USD, 2012-05-01) with units of (HOOL, 500 USD, None ) simply fails. Note that the lot-date syntax is accepted by Beancount on both augmenting and reducing legs, so the \u201csolution\u201d is that the user is forced to specifically provide the lot-date on acquiring the position. This would work, for instance: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {500 USD / 2012-05-01} Assets:Investments:Cash 2014-02-15 * \"Second trade at very same price\" Assets:Investments:Stock 8 HOOL {500 USD} Assets:Investments:Cash 2014-06-30 * \"Trying to sell\" Assets:Investments:Stock -5 HOOL {500 USD / 2012-05-01} Assets:Investments:Cash The inconvenience here is that when the user entered the first trade, he could not predict that he would enter another trade at the same price in the future and typically would not have inserted the lot-date. More likely than not, the user had to go back and revisit that transaction in order to disambiguate this. We can do better. This is a shortcoming of its implementation, which reflects the fact that position reduction was initially regarded as an exact matching problem rather than a fuzzy matching one\u2014it wasn\u2019t clear to me how to handle this safely at the time. This needs to be fixed after this proposal, and what I\u2019m doing with this document is very much a process of clarifying for myself what I want this fuzzy matching to mean, and to ensure that I come up with an unambiguous design that is easy to enter data for. Ledger always automatically attaches the transaction date to the inventory lot, and in my view this is the correct thing to do (below we propose attaching more information as well).","title":"Shortcomings in Beancount"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#context","text":"[Updated on Nov\u20192014] It is important to distinguish between two types of booking: Booking. The strict type of booking that occurs when we are considering the entire list of transactions. We will call this the \u201cbooking\u201d process, and it should occur just once. This process should be strict: the failure of correctly book a reduction to an existing lot should trigger a fatal error. Inventory Aggregation. When we are considering a subset of entries, it is possible that entries that add some lots are removed, and that other entries that reduce or remove those lots are kept. This creates a situation in which it is necessary to support aggregations over time of changes to an inventory that may not admit a matching lot. If we are to support arbitrary subsets of transactions being aggregated, we must take this into account. This aggregation process should never lead to an error. Let\u2019s take an example to justify why we need the non-booking summarization. Let\u2019s say we have a stock purchase and sale: 2013-11-03 * \"Buy\" Assets:Investments:VEA 10 AAPL {37.45 USD} ;; (A) Assets:Investments:Cash -370.45 USD 2014-08-09 * \"Sell\" Assets:Investments:VEA -5 AAPL {37.45 USD} Assets:Investments:Cash 194.40 USD Income:Investments:PnL If we filter \u201cby year\u201d and only want to view transactions that occurred in 2014, AND we don\u2019t \u201cclose\u201d the previous year, that is, we do not create opening balances entries that would deposit the lots in the account at the beginning of the year - this could happen if we filter by tag, or by some other combination of conditions - then the (A) lot is not present in the inventory to be debited from. We must somehow accommodate a -5 AAPL at that point. When reporting, the user could decide how to summarize and \u201cmatch up as best it can\u201d those legs, but converting them to units or cost, or convert them to a single lot at average-cost. The booking process, however, should admit no such laxity. It should be strict and trigger an error if a lot cannot be matched. This would only be applied on the total set of transactions, and only once, never on any subset. The purpose of the booking stage should be threefold: Match against existing lots and issue errors when that is impossible. Do this, using the method specified by the user. Replace all partially specified lots by a full lot specification, the lot that was matched during the booking process. Insert links on transactions that form a trade: a buy and corresponding sells should be linked together. This essentially identifies trades, and can then be used for reporting later on. (Trades at average cost will require special consideration.) We can view Ledger\u2019s method as having an aggregation method only, lacking a booking stage. The particular method for aggregation is chosen using --lots or --lot-dates or --lot-prices. This would indicate that a \u201cbooking\u201d stage could be tacked on to Ledger without changing much of its general structure: it could be inserted as a pre- or post- process, but it would require Ledger to sort the transactions by date, something it does not currently do (it runs a single pass over its data). Beancount has had both methods for a while, but the separate nature of these two operations has not been explicitly stated so far\u2014instead, the inventory supported an optional flag to raise an error when booking was impossible. Also, inventory aggregation has not been thought as requiring much customization, it was meant to be unified to the booking process. It is now clear that they are two distinct algorithms, and that a few different methods for aggregating can be relevant (though they do not function the same as Ledger\u2019s do. Note: you can view the inventory aggregation method as a GROUP BY within an Inventory object\u2019s lots: which columns do you group over? This requires use cases). In any case, fully specified lots should match against each other by default: we need to support this as the degenerate case to use for simple currencies (not held at cost)... we would not want to accumulate all changes in the same currency as separate lots in an inventory, they need to cross each other as soon as they are applied. We want to change this in order to make the code clearer: there should be a separate \u201cbooking\u201d stage, provided by a plugin , which resolves the partially specific lot reduction using the algorithm outlined in this document, and a separate method for inventory aggregation should not bother with any of those details. In fact, the inventory aggregation could potentially simply become an accumulated list of lots, and the summarization of them could take place a posteriori, with some conceptual similarly to when Ledger applies its aggregation. Just using a simple aggregation becomes more relevant once we begin entering more specific data about lots, such as always having an acquisition date, a label, and possibly even a link to the entry that created the lot. In order to trigger summarization sensibly, functions to convert and summarize accumulated inventories could be provided in the shell, such as \u201cUNITS(inventory)\u201d.","title":"Context"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#precision-for-interpolation","text":"The interpolation capabilities will be extended to cover eliding a single number, any number, in any subset of postings whose \u201cweights\u201d resolve to a particular currency (e.g., \u201call postings with weights in USD\u201d). The interpolation needs to occur at a particular precision. The interpolated number should be quantized automatically, and the number of fractional digits to quantize it to should be automatically inferred from the DisplayContext which is itself derived from the input file. Either the most common or the maximum number of digits witnessed in the file should be used. For a use case, see this thread : On Mon, Nov 24, 2014 at 12:33 PM, ELI wrote: Regarding the last piece on which there seems to be a misunderstanding between you and I, let me provide an standalone example, outside the context of the plugin. Since Vanguard only makes three decimal places of precision available me, I need to have the actual share count calculated from the price and transaction amount. For example, I have a transaction on my activities page with these details: Shares Transacted: 0.000 Share Price: 48.62 Amount: 0.02 The only way to enter this transaction and have it balance would be to manually calculate the Shares Transacted with four decimal places of precision. My preferred way of entering this would be to enter the Share Price and Amount and have Beancount calculate Shares Transacted to a precision associated with my Vanguard account. Additionally, as metadata, I'd record \"Shares Transacted: 0.000\" as history of \"what the statement said\". Maybe you can give me your thoughts on how such a case would be addressed with planned Beancount features or as a plugin I could right? What does the downloadable file contain? 0.000 or 0.0004 or even 0.00041? What this is, most likely, is the quarterly fee that they take, which they price in terms of shares. If this is your Vanguard account, and you sum all the similar such transactions around it, you should observe that it sums to 5$ per quarter. I would log it like this: 2014-11-23 * \"Quarterly fee\" Assets:US:Vanguard:VIIPX -0.00041 VIIPX {* 48.62 USD} Expenses:Financial:Fees 0.02 USD The \"*\" will be required to merge all the lots into a single one (average cost booking) because the 48.62 USD they declare is _not_ one of your lots, but rather just the price of the asset that happened to be there on the day they took the fee. In other words, they take their fee in terms of units of each asset, priced at the current price, deducting against the cost basis of all your lots together (it doesn't matter which because this is a pre-tax account, so no capital gains are reported). Now you're asking, how could I avoid calculating 0.00041 myself? My first answer would be: let's first see if the OFX download from Vanguard includes the precision. That would solve it. I believe it does (I checked my own history of downloads and I have some numbers in there with 4 fractional digits). My second answer would be that - if you don't have the number of shares from the file - after I implement the much needed new inventory booking proposal ( http://furius.ca/beancount/doc/proposal-booking ), the interpolation capabilities will be vastly extended, such that eliding the number of shares would even be possible, like this: 2014-11-23 * \"Quarterly fee\" Assets:US:Vanguard:VIIPX VIIPX {* 48.62 USD} Expenses:Financial:Fees 0.02 USD Now this by itself still would not solve the problem: this would store 0.000411353353 which is limited at 12 fractional digits because of the default context. So that's incorrect. What would need to be done to deal with this is to infer the most common number of digits used on units of VIIPX and to quantize the result to that number of digits (I think this could be safe enough). The number of digits appearing in the file for each currency is already tracked in a DisplayContext object that is stored in the options_map from the parser. I'll have to take that into account in the inventory booking proposal. I'm adding this to the proposal.","title":"Precision for Interpolation"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#requirements","text":"The previous sections introduce the general problem of booking, and point out important shortcomings in both the Beancount and Ledger implementations. This section will present a set of desires for a new and improved booking mechanism for command-line bookkeeping software implementations. Here are reasonable requirements for a new method: Explicit inventory lot selection needs to be supported. The user should be able to indicate precisely which of the lots to reduce a position against, per-transaction. An account or commodity should not have to have a consistent method applied across all its trades. Explicit inventory lot selection should support partial matching , that is, in cases where the lots to match against are ambiguous, the user should be able to supply only minimal information to disambiguate the lots, e.g., the date, or a label, or the cost, or combinations of these. For instance, if only a single lot is available to match against, the user should be able to specify the cost as \u201c {} \u201d, telling the system to book at cost, but essentially saying: \u201cbook against any lot.\u201d This should trigger an error only if there are multiple lots. A variety of informations should be supported to specify an existing lot, and they should be combinable: The cost of acquisition of the position; The date the position was acquired at; A user-provided label. Cases where insufficient information is provided for explicit lot selection is available should admit for a default booking method to be invoked. The user should be able to specify globally and per-account what the default booking method should be. This paves the way for implicit lot selection . A degenerate method should be available that kicks off an error and requires the user to be explicit. Average cost booking needs to be supported, as it is quite common in retirement or tax-sheltered accounts. This is the default method used in Canadian investment accounts. Cost basis adjustments need to be supported as well. The problem should be able to be specified by providing a specific lot (or all of a commodity\u2019s lots at average cost) and a dollar amount to adjust the position by. Stock splits need to be able to maintain some of the original attributes of the position , specifically, the original trade date and the user-specified label, if one has been provided. This should allow a common syntax to specify an original trading lot when reducing a position after a split. Reducing a position needs to always book against an existing lot . I\u2019m preserving the Beancount behavior here, which I\u2019ve argued for previously, as it works quite well and is a great method to avoid data entry mistakes. This naturally defines an invariant for inventories: all positions of a particular commodity held at cost should be of the same sign in one inventory (this can be used as a sanity check). Bookings that change the sign of a number of units should raise an error , unless an explicit exception is requested (and I\u2019m not even convinced that we need it). Note again that bookings only occur for units held at cost, so currency conversions are unaffected by this requirement. Beancount has had this feature for a while, and it has proved useful to detect errors. For example, if an inventory has a position of 8 HOOL {500 USD} you attempt to post a change of -10 units to this lot, the resulting number of units is now negative: -2. This should indicate user error. The only use case I can think for allowing this is the trading of futures spreads and currencies, which would naturally be reported in the same account (a short position in currencies is not regarded as a different instrument in the same way that a short position would); this is the only reason to provide an exception, and I suspect that 99% of users will not need it.]","title":"Requirements"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#debugging-tools","text":"Since this is a non-trivial but greatly important part of the process of entering trading data, we should provide tools that list in detail the running balance for an inventory, including all of the detail of its lots. Booking errors being reported should include sufficient context, that is: The transaction with offending posting The offending posting The current state of the inventory before the posting is applied, with all its lots The implicit booking method that is in effect to disambiguate between lots A detailed reason why the booking failed","title":"Debugging Tools"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#explicit-vs-implicit-booking-reduction","text":"Another question that may come up in the design of a new booking method is whether we require the user to be explicit about whether he thinks this is an addition to a position, or a reduction. This could be done, for instance, by requiring a slightly different syntax for the cost, for example \u201c {} \u201d would indicate an addition and \u201c [] \u201d a reduction. I\u2019m not convinced that it is necessary to make that distinction, maybe the extra burden on the user is not worth it, but it might be a way to cross-check an expectation against the actual calculations that occur. I\u2019ll drop the idea for now.","title":"Explicit vs. Implicit Booking Reduction"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#design-proposal","text":"This section presents a concrete description of a design that fulfills the previously introduced requirements. We hope to keep this as simple as possible.","title":"Design Proposal"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#inventory","text":"An inventory is simply a list of lot descriptors. Each inventory lot will be defined by: UNITS, (CURRENCY, COST-UNITS, COST-CURRENCY, DATE, LABEL) Fields: UNITS: the number of units of that lot that are held CURRENCY: the type of commodity held, a string COST-UNITS: the number in the price of that lot COST-CURRENCY: the pricing commodity of the cost DATE: the acquisition date of the lot LABEL: an arbitrary user-specified string used to identify this lot If this represents a lot held at cost, after this processing, only the LABEL field should be optionally with a NULL value. Anyone writing code against this inventory could expect that all other values are filled in with non-NULL values (or are otherwise all NULL).","title":"Inventory"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#input-syntax-filtering","text":"The input syntax should allow the specification of cost as any combination of the following informations: The cost per unit , as an amount, such as \u201c 500 USD \u201d A total cost , to be automatically divided by the number of units, like this: {... +9.95 USD} . You should be able to use either cost per unit, total cost, or even combine the two, like this: {500 + 9.95 USD} . This is useful to enter commissions. This syntax also replaces the previous {{ ... }} syntax. The lot-date , as a YYYY-MM-DD date, such as \u201c 2014-06-20 \u201d A label , which is any non-numerical identifier, such as first-apple or some random uuid like \u201c aa2ba9695cc7 \u201d A special marker \u201c * \u201d that indicates to book at the average cost of the inventory balance These following postings should all be valid syntax: ... Assets:Investments:Stock 10 HOOL {500 USD} ; cost Assets:Investments:Stock 10 HOOL {339999615d7a} ; label Assets:Investments:Stock 10 HOOL {2014-05-01} ; lot-date Assets:Investments:Stock 10 HOOL {} ; no information ... Combinations Assets:Investments:Stock 10 HOOL {500 USD, 2014-05-01} Assets:Investments:Stock 10 HOOL {2014-05-01, 339999615d7a}","title":"Input Syntax & Filtering"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#algorithm","text":"All postings should be processed in a separate step after parsing, in the order of their date, against a running inventory balance for each account. The cost amount should become fully determined at this stage, and if we fail to resolve a cost, an error should be raised and the transaction deleted from the flow of directives (after the program loudly complaining about it). When processing an entry, we should match and filter all inventory lots against all the filters that are provided by the user. In general, after filtering the lots with the user restrictions: If the set of lots is empty, an error should be raised; If there is a single lot, this lot should be selected (success case); If there are multiple lots, the default implicitly booking method for the corresponding account should be invoked.","title":"Algorithm"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#implicit-booking-methods","text":"If there are multiple matching lots to choose from during booking, the following implicit booking methods could be invoked: STRICT: Select the only lot of the inventory. If there are more than one lots, raise an error. FIFO: Select the lot with the earliest date. LIFO: Select the lot with the latest date. AVERAGE: Book this transaction at average cost. AVERAGE_ONLY: Like AVERAGE but also trigger an aggregation on an addition to a position. This can be used to enforce that the only booking method for all lots is at the average cost. NONE: Don\u2019t perform any inventory booking on this account. Allow a mix of lots for the same commodity or positive and negative numbers in the inventory. (This essentially devolves to the Ledger method of booking.) This method would only get invoked if disambiguation between multiple lots is required, after filtering the lots against the expression provided by the user. The STRICT method is basically the degenerate disambiguation which issues an error if there is any ambiguity and should be the default. There should be a default method that applies to all accounts. The default value should be overridable. In Beancount, we would add a new \u201coption\u201d to allow the user to change this: option \"booking_method\" \"FIFO\" The default value used in a particular account should be specifiable as well, because it is a common case that the method varies by account, and is fixed within the account. In Beancount, this would probably become part of the open directive, something like this: 2003-02-17 open Assets:Ameritrade:HOOL HOOL booking:FIFO (I\u2019m not sure about the syntax.)","title":"Implicit Booking Methods"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#resolving-same-date-ambiguity","text":"If the automatic booking method gets invoked to resolve an ambiguous lot reduction, e.g. FIFO, if there are multiple lots at the same date, something needs to be done to resolve which of the lots is to be chosen. The line number at which the transaction appears should be selected. For example, in the following case, a WIDGET Of 8 GBP would be selected: 2014-10-15 * \"buy widgets\" Assets:Inventory 10 WIDGET {} ;; Price inferred to 8 GBP/widget Assets:Cash -80 GBP 2014-10-15 * \"buy another widget\" Assets:Inventory 1 WIDGET {} ;; Price inferred to 9 GBP/widget Assets:Cash -9 GBP 2014-10-16 * \"sell a widget\" Assets:Cash 11 GBP Assets:Inventory -1 WIDGET {} ;; Ambiguous lot","title":"Resolving Same Date Ambiguity"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#dates-inserted-by-default","text":"By default, if an explicit date is not specified in a cost, the date of the transaction that holds the posting should be attached to the trading lot automatically. This date is overridable so that stock splits may be implemented by a transformation to a transaction like this: 2014-01-04 * \"Buy some HOOL\" Assets:Investments:Stock 10 HOOL {1000.00 USD} Assets:Investments:Cash 2014-04-17 * \"2:1 split into Class A and Class B shares\" Assets:Investments:Stock -10 HOOL {1000.00 USD, 2014-01-04} ; reducing Assets:Investments:Stock 10 HOOL {500.00 USD, 2014-01-04} ; augment Assets:Investments:Stock 10 HOOLL {500.00 USD, 2014-01-04} ; augment","title":"Dates Inserted by Default"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#matching-with-no-information","text":"Supplying no information for the cost should be supported and is sometimes useful: in the case of augmenting a position, if all the other legs have values specified, we should be able to automatically infer the cost of the units being traded: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {} Assets:Investments:Cash -5009.95 USD Expenses:Commissions 9.95 USD In the case of reducing a position\u2014and we can figure that out whether that is the case by looking at the inventory at the point of applying the transaction to its account balance\u2014an empty specification should trigger the default booking method. If the method is STRICT, for instance, this would select a lot only if there is a single lot available (the choice is unambiguous), which is probably a common case if one trades infrequently. Other methods will apply as they are defined. Note that the user still has to specify a cost of \u201c {} \u201d in order to inform us that this posting has to be considered \u201cheld at cost.\u201d This is important to disambiguate from a price conversion with no associated cost.","title":"Matching with No Information"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#reducing-multiple-lots","text":"If a single trade needs to close multiple existing lots of an inventory, this can dealt with trivially by inserting one posting for each lot. I think this is a totally reasonable requirement. This could represent a single trade, for instance, if your brokerage allows it: 2012-05-01 * \"Closing my position\" Assets:Investments:Stock -10 HOOL {500 USD} Assets:Investments:Stock -12 HOOL {510 USD} Assets:Investments:Cash 12000.00 USD Income:Investments:Gains Note: If the cost basis specification for the lot matches multiple lots of the inventory and the result is unambiguous, the lots will be correctly selected. For example, if the ante-inventory contains just those two lots (22 HOOL in total), you should be able to have a single reducing posting like this: 2012-05-01 * \"Closing my position\" Assets:Investments:Stock -22 HOOL {} Assets:Investments:Cash 12000.00 USD Income:Investments:Gains and they will both be included. On the other hand, if the result is ambiguous (for example, if you have more than these two lots) the booking strategy for that account would be invoked. By default, this strategy will be \"STRICT\" which will generate an error, but if this account's strategy is set to \"FIFO\" (or is not set and the default global strategy is \"FIFO\"), the FIFO lots would be automatically selected for you, as per your wish.","title":"Reducing Multiple Lots"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#lot-basis-modification","text":"Another idea is to support the modification of a specific lot\u2019s cost basis in a single leg. The way this could work, is by specifying the number of units to modify, and the \u201c+ number currency\u201d syntax would be used to adjust the cost basis by a specific number, like this: 2012-05-01 * \"Adjust cost basis by 250 USD for 5 of the 500 USD units\" Assets:Investments:Stock ~5 HOOL {500 + 250 USD} If the number of units is smaller than the total number of units in the lot, split out the lot before applying the cost basis adjustment. I\u2019m not certain I like this. Not specifying the size could also adjust the entire position (I\u2019m not sure if this makes sense as it departs from the current syntax significantly): 2012-05-01 * \"Adjust cost basis by 250 USD for the 500 USD units\" Assets:Investments:Stock HOOL {500 + 250 USD} In any case, all the fields other than the total cost adjustment would be used to select which lot to adjust.","title":"Lot Basis Modification"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#tag-reuse","text":"We will have to be careful in the inventory booking to warn on reuse of lot labels. Labels should be unique.","title":"Tag Reuse"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#examples","text":"Nothing speaks more clearly than concrete examples. If otherwise unspecified, we are assuming that the booking method on the Assets:Investments:Stock account is STRICT .","title":"Examples"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#no-conflict","text":"Given the following inventory lots: 21, (HOOL, 500, USD, 2012-05-01, null) 22, (AAPL, 380, USD, 2012-06-01, null) This booking should succeed: 2013-05-01 * Assets:Investments:Stock -10 HOOL {} ... This booking should fail (because the amount is not one of the valid lots): 2013-05-01 * Assets:Investments:Stock -10 HOOL {520 USD} ... This one too should fail (no currency matching lot): 2013-05-01 * Assets:Investments:Stock -10 MSFT {80 USD} ... So should this one (invalid date when a date is specified): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD, 2010-01-01} ...","title":"No Conflict"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#explicit-selection-by-cost","text":"Given the following inventory: 21, (HOOL, 500, USD, 2012-05-01, null) 32, (HOOL, 500, USD, 2012-06-01, \u201cabc\u201d) 25, (HOOL, 510, USD, 2012-06-01, null) This booking should succeed: 2013-05-01 * Assets:Investments:Stock -10 HOOL {510 USD} ... This booking should fail if the stock account\u2019s method is STRICT (ambiguous): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD} ... This booking should succeed if the stock account\u2019s method is FIFO, booking against the lot at 2012-05-01 (earliest): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD} ...","title":"Explicit Selection By Cost"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#explicit-selection-by-date","text":"Given the same inventory as previously, this booking should succeed: 2013-05-01 * Assets:Investments:Stock -10 HOOL {2012-05-01} ... This booking should fail if the method is STRICT (ambiguous) 2013-05-01 * Assets:Investments:Stock -10 HOOL {2012-06-01} ...","title":"Explicit Selection By Date"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#explicit-selection-by-label","text":"This booking should succeed, because there is a single lot with the \u201cabc\u201c label: 2013-05-01 * Assets:Investments:Stock -10 HOOL {abc} ... If multiple lots have the same label, ambiguous cases may occur; with this inventory, for example: 32, (HOOL, 500, USD, 2012-06-01, \u201cabc\u201d) 31, (HOOL, 510, USD, 2012-07-01, \u201cabc\u201d) The same booking should fail.","title":"Explicit Selection By Label"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#explicit-selection-by-combination","text":"With the initial inventory, this booking should succeed (unambiguous): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD, 2012-06-01} ... There is only one lot at a cost of 500 USD and at an acquisition date of 2012-06-01.","title":"Explicit Selection By Combination"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#not-enough-units","text":"The following booking would be unambiguous, but would fail, because there aren\u2019t enough units of the lot in the inventory to subtract from: 2013-05-01 * Assets:Investments:Stock -33 HOOL {500 USD, 2012-06-01} \u2026","title":"Not Enough Units"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#redundant-selection-of-same-lot","text":"If two separate postings select the same inventory lot in one transaction, it should be able to work: 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD, 2012-06-01} Assets:Investments:Stock -10 HOOL {abc} ... Of course, if there are not enough shares, it should fail: 2013-05-01 * Assets:Investments:Stock -20 HOOL {500 USD, 2012-06-01} Assets:Investments:Stock -20 HOOL {abc} ...","title":"Redundant Selection of Same Lot"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#automatic-price-extrapolation","text":"If all the postings of a transaction can have their balance computed, we allow a single price to be automatically calculated - this should work: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500.00 USD} Assets:US:Invest:HOOL 10.00 HOOL {} Income:US:Invest:Gains -340.51 USD And result in the equivalent of: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500.00 USD} Assets:US:Invest:HOOL 10.00 HOOL {534.51 USD} Income:US:Invest:Gains -340.51 USD Of course, preserving the original date of the lot should work too, so this should work as well: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500.00 USD} Assets:US:Invest:HOOL 10.00 HOOL {2014-02-04} Income:US:Invest:Gains -340.51 USD","title":"Automatic Price Extrapolation"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#average-cost-booking","text":"Augmenting lots with the average cost syntax should fail: 2014-03-15 * \"Buying at average cost, what does this mean?\" Assets:US:Invest:Stock 10.00 HOOL {*} Income:US:Invest:Gains -5000.00 USD Reducing should work, this is the main use case: 2014-03-15 * \"Buying a first lot\" Assets:US:Invest:Stock 10.00 HOOL {500.00 USD} Assert:US:Invest:Cash -5000.00 USD 2014-04-15 * \"Buying a second lot\" Assets:US:Invest:Stock 10.00 HOOL {510.00 USD} Assets:US:Invest:Cash -5100.00 USD 2014-04-28 * \"Obtaining a dividend in stock\" Assets:US:Invest:Stock 1.00 HOOL {520.00 USD} Income:US:Invest:Dividends -520.00 USD 2014-05-20 * \"Sell some stock at average cost\" Assets:US:Invest:Stock -8.00 HOOL {*} Assets:US:Invest:Cash 4240.00 USD Income:US:Invest:Gains ; Automatically calculated: -194.29 USD The effect would be to aggregate the 10 HOOL {500.00 USD} lot, the 10 HOOL {510.00 USD} lot, and the 1.00 HOOL {520.00 USD} lot, together into a single lot of 21 HOOL {505.714285 USD}, and to remove 8 HOOL from this lot, which is 8 HOOL x 505.714285 USD/HOOL = 4045.71 USD. We receive 4240.00 USD, which allows us to automatically compute the capital gain: 4240.00 - 4045.71 = 194.29 USD. After the reduction, a single lot of 13 HOOL {505.714285 USD} remains. It should also work if we have multiple currencies in the account, that is adding the following transaction to the above should not make it faile: 2014-04-15 * \"Buying another stock\" Assets:US:Invest:Stock 15.00 AAPL {300.00 USD} Assets:US:Invest:Cash -4500.00 USD However it should fail if we have the same currency priced in distinct cost currencies in the same account: 2014-03-15 * \"Buying a first lot\" Assets:US:Invest:Stock 10.00 HOOL {500.00 USD} Assert:US:Invest:Cash -5000.00 USD 2014-04-15 * \"Buying a second lot\" Assets:US:Invest:Stock 10.00 HOOL {623.00 CAD} Assets:US:Invest:Cash -62300.00 CAD 2014-05-20 * \"Sell some stock at average cost\" Assets:US:Invest:Stock -8.00 HOOL {*} ; Which HOOL, USD or CAD? Assets:US:Invest:Cash 4240.00 USD Income:US:Invest:Gains But of course, this never happens in practice, so I\u2019m not too concerned. We could potentially support specifying the cost-currency to resolve this case, that is, replacing the sell leg with this: 2014-05-20 * \"Sell some stock at average cost\" Assets:US:Invest:Stock -8.00 HOOL {* USD} Assets:US:Invest:Cash 4240.00 USD Income:US:Invest:Gains I\u2019m quite sure it wouldn\u2019t be useful, but we could go the extra mile and be as general as we can possibly be.","title":"Average Cost Booking"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#future-work","text":"This proposal does not yet deal with cost basis re-adjustments! We need to way to be able to add or remove a fixed dollar amount to a position\u2019s cost basis, that is, from (1) a lot identifier and (2) a cost-currency amount, we should be able to update the cost of that lot. The transaction still has to balance. Here are some ideas what this might look like: 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock 10.00 HOOL {500.00 USD + 230.00 USD} Income:US:Invest:Gains -230.00 USD The resulting inventory would be 10 HOOL at 523.00 USD per share, and the rest of the original lots, less 10 HOOL at 500.00 USD per share (there might be some of that remaining, that\u2019s fine). This is the equivalent of 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock -10.00 HOOL {500.00 USD} Assets:US:Invest:Stock 10.00 HOOL {523.00 USD} Income:US:Invest:Gains -230.00 USD except you did not have to carry out the calculation. This should therefore be implemented as a transformation. However, this is not super useful, because this is already supported... adjustments are usually performed on lots at average cost, and this is where the problem lie: you\u2019d have to make the calculation yourself! Here\u2019s a relevant use case: 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock 10.00 HOOL {* + 230.00 USD} Income:US:Invest:Gains -230.00 USD This would first average all the lots of HOOL together and recompute the cost basis with the new amount. This should work: 2014-03-15 * \"Buying a first lot\" Assets:US:Invest:Stock 5.00 HOOL {500.00 USD} Assert:US:Invest:Cash -2500.00 USD 2014-04-15 * \"Buying a second lot\" Assets:US:Invest:Stock 5.00 HOOL {520.00 USD} Assets:US:Invest:Cash -2600.00 USD 2014-05-20 * \"Adjust cost basis of the Hooli units\" Assets:US:Invest:Stock 10.00 HOOL {* + 230.00 USD} Income:US:Invest:Gains -230.00 USD Notice how we still have to specify an amount of HOOL shares to be repriced. I like this, but I\u2019m wondering if we should allow specifying \u201call the units\u201d like this (this would be a more radical change to the syntax): 2014-05-20 * \"Adjust cost basis of the Hooli units\" Assets:US:Invest:Stock * HOOL {* + 230.00 USD} Income:US:Invest:Gains -230.00 USD Perhaps the best way to auto-compute cost basis adjustments would be via powerful interpolation, like this: 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock -10.00 HOOL {500 USD} ; reduce Assets:US:Invest:Stock 10.00 HOOL {} ; augment, interpolated Income:US:Invest:Gains -230.00 USD See the Smarter Elision document for more details on a proposal.","title":"Future Work"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#inter-account-booking","text":"Some tax laws require a user to book according to a specific method, and this might apply between all accounts. This means that some sort of transfer needs to be applied in order to handle this correctly. See the separate document for detail. TODO - complete this with more tests for average cost booking!","title":"Inter-Account Booking"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#implementation-notes","text":"","title":"Implementation Notes"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#separate-parsing-interpolation","text":"In order to implement a syntax for reduction that does not specify the cost, we need to make changes to the parser. Because there may be no costs on the posting, it becomes impossible to perform balance checks at parsing time. Therefore, we will need to postpone balance checks to a stage after parsing. This is reasonable and in a way nice: it is best if the Beancount parser does not output many complex errors at that stage. A new \u201cpost-parse\u201d stage will be added, right before running the plugins.","title":"Separate Parsing & Interpolation"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#conclusion","text":"This document is work-in-progress. I\u2019d really love to get some feedback in order to improve the suggested method, which is why I put this down in words instead of just writing the code. I think a better semantic can be reached by iterating on this design to produce something that works better in all systems, and this can be used as documentation later on for developers to understand why it is designed this way. Your feedback is much appreciated.","title":"Conclusion"},{"location":"balance_assertions_in_beancount.html","text":"Balance Assertions in Beancount \uf0c1 Martin Blais, July 2014 http://furius.ca/beancount/doc/proposal-balance This document summarizes the different kinds of semantics for balance assertions in all command-line accounting systems and proposes new syntax for total and file-order running assertions in Beancount. Motivation Partial vs. Complete Assertions File vs. Date Assertions Ordering & Ambiguity Intra-Day Assertions Beginning vs. End of Day Status Proposal File Assertions Complete Assertions Motivation \uf0c1 Both Beancount and Ledger implement balance assertions. These provide the system with checkpoints it can use to verify the integrity of the data entry 1 . Traditional accounting principles state that a user may never change the past\u2014correcting the past involves inserting new entries to undo past mistakes 2 \u2014but we in the command-line accounting community take issue with that: we want to remain able to reconstruct the past and more importantly, to correct past mistakes at the site of their original data entry. Yes, we are dilettantes but we are bent on simplifying the process of bookkeeping by challenging existing concepts and think that this is perfectly reasonable, as long as it does not break known balances. These known balances are what we provide via balance assertions. Another way to look at these balance assertions, is that they are simply the bottom line amounts reported on various account statements, as exemplified in the figure below. Following this thread , we established that there were differing interpretations of balance assertions in the current versions of Ledger (3.0) and Beancount (2.0b). Partial vs. Complete Assertions \uf0c1 An assertion in Beancount currently looks like this: 2012-02-04 balance Assets:CA:Bank:Checking 417.61 CAD The meaning of this directive is: \u201cplease assert that the inventory of account \u2018Assets:CA:Bank:Checking\u2019 contains exactly 417.61 units of CAD at the end of February 3, 2012\u201d (so it\u2019s dated on the 4th because in Beancount balance assertions are specified at the beginning of the date). It says nothing about other commodities in the account\u2019s inventory. For example, if the account contains units of \u201cUSD\u201d, those are unchecked. We will call this interpretation a partial balance assertion or single-commodity assertion . An alternative assertion would be exhaustive: \u201cplease assert that the inventory of account \u2018Assets:CA:Bank:Checking\u2019 contains only 417.61 units of CAD at the end of February 3, 2012.\u201d We call this a complete balance assertion . In order to work, this second type of assertion would have to support the specification of the full contents of an inventory. This is currently not supported. Further note that we are not asserting the cost basis of an inventory, just the number of units. File vs. Date Assertions \uf0c1 There are two differing interpretations and implementations of the running balances for assertions: Beancount first sorts all the directives and verifies the balance at the beginning of the date of the directive. In the previous example, that is \u201c the balance before any transactions on 2012-02-04 are applied.\u201d We will call this date assertions or date-based assertions . Ledger keeps a running balance of each account\u2019s inventory during its parsing phase and performs the check at the site of the assertion in the file. We will call this file assertions or file-order, or file-based assertions . This kind of assertion has no date associated with it (this is slightly misleading in Ledger because of the way assertions are specified, as attached to a transaction\u2019s posting, which appears to imply that they occur on the transaction date, but they don\u2019t, they strictly apply to the file location). Ordering & Ambiguity \uf0c1 An important difference between those two types of assertions is that file-based assertions are not order-independent. For example, take the following input file: ;; Credit card account 2014/05/01 opening Liabilities:CreditCard $-1000.00 Expenses:Opening-Balances 2014/05/12 dinner Liabilities:CreditCard $-74.20 Expenses:Restaurant ;; Checking account 2014/06/05 salary Assets:Checking $4082.87 Income:Salary 2014/06/05 cc payment Assets:Checking $-1074.20 = $3008.67 Liabilities:CreditCard = $0 If you move the credit card payment up to the credit card account section, the same set of transactions fails: ;; Credit card account 2014/05/01 opening Liabilities:CreditCard $-1000.00 Expenses:Opening-Balances 2014/05/12 dinner Liabilities:CreditCard $-74.20 Expenses:Restaurant 2014/06/05 cc payment Assets:Checking $-1074.20 = $3008.67 Liabilities:CreditCard = $0 ;; Checking account 2014/06/05 salary Assets:Checking $4082.87 Income:Salary This subtle problem could be difficult for beginners to understand. Moving the file assertion to its own, undated directive might be more indicative syntax of its semantics, something that would look like this: balance Assets:Checking = $3008.67 The absence of a date indicates that the check is not applied at a particular point in time. Intra-Day Assertions \uf0c1 On the other hand, date-based assertions, because of their order-independence, preclude intra-day assertions, that is, a balance assertion that occurs between two postings on the same account during the same day. Beancount does not support intra-day assertions at the moment. Note despite this shortcoming, this has not been much of a problem, because it is often possible to fudge the date where necessary by adding or removing a day, or even just skipping an assertion where it would be impossible (skipping assertions is not a big deal, as they are optional and their purpose is just to provide certainty. As long as you have one that appears some date after the skipped one, it isn\u2019t much of an issue). It would be nice to find a solution to intra-day assertions for date-based assertions, however. One interesting idea would be to extend the semantics to apply the balance in file order within the set of all transactions directly before and after the balance check that occur on the same date as the balance check, for example, this would balance: 2013-05-05 balance Assets:Checking 100 USD 2013-05-20 * \"Interest payment\" Assets:Checking 12.01 USD Income:Interest 2013-05-20 balance Assets:Checking 112.01 USD 2013-05-20 * \"Check deposit\" Assets:Checking 731.73 USD Assets:Receivable The spell would be broken as soon as a directive would appear at a different date. Another idea would be to always sort the balance assertions in file-order as the second sort key (after the date) and apply them as such. I\u2019m not sure this would be easy to understand though. Beginning vs. End of Day \uf0c1 Finally, just for completeness, it is worth mentioning that date assertions have to have well-defined semantics regarding when they apply during the day. In Beancount, they currently apply at the beginning of the day. It might be worthwhile to provide an alternate version of date-based assertions that applies at the end of the day, e.g. \u201cbalance_end\u201d. Beancount v1 used to have this (\u201ccheck_end\u201d) but it was removed in the v2 rewrite, as it wasn\u2019t clear it would be really needed. The simplicity of a single meaning for balance assertions is nice too. Status \uf0c1 Ledger 3.0 currently supports only partial file-order assertions, on transactions. Beancount 2.0 currently supports only partial date-based assertions at the beginning of the day. Proposal \uf0c1 I propose the following improvements to Beancount\u2019s balance assertions. File Assertions \uf0c1 File assertions should be provided as a plugin. They would look like this: 2012-02-03 file_balance Assets:CA:Bank:Checking 417.61 CAD Ideally, in order to make it clear that they apply strictly in file order, they would not have a date, something like this: file_balance Assets:CA:Bank:Checking 417.61 CAD But this breaks the regularity of the syntax for all other directives. It also complicates an otherwise very regular and simple parser just that much more\u2026 all other directives begin with a date and a word, and all other lines are pretty much ignored. It would be a bit of a departure from this. Finally, it would still be nice to have a date just to insert those directives somewhere in the rendered journal. So I\u2019m considering keeping a date for it. If you decide to use those odd assertions, you should know what they mean. I also don\u2019t like the idea of attaching assertions to transactions; transaction syntax is already busy enough, it is calling to remain simple. This should be a standalone directive that few people use. In order to implement this, the plugin would simply resort all the directives according to their file location only (using the fileloc attribute of entries), disregarding the date, and recompute the running balances top to bottom while applying the checks. This can entirely be done via post-processing, like date-based assertions, without disturbing any of the other processing. Moreover, another advantage of doing this in a plugin is that people who don\u2019t use this directive won\u2019t have to pay the cost of calculating these inventories. Complete Assertions \uf0c1 Complete assertions should be supported in Beancount by the current balance assertion directive. They aren\u2019t very important but are potentially useful. Possible syntax ideas: 2012-02-03 balance* Assets:CA:Bank:Checking 417.61 CAD, 162 USD 2012-02-03 balance Assets:CA:Bank:Checking = 417.61 CAD, 162 USD 2012-02-03 balance full Assets:CA:Bank:Checking 417.61 CAD, 162 USD 2012-02-03 balance Assets:CA:Bank:Checking 417.61 CAD Assets:CA:Bank:Checking 162 USD I\u2019m still undecided which is best. So far it seems a matter of taste. As far as we know, the notion of inputting an explicit expected amount is unique to command-line accounting systems. Other systems \u201creconcile\u201d by freezing changes in the past. \u21a9 There are multiple reasons for this. First, in pre-computer times, accounting was done using books, and recomputing running balances manually would have involved making multiple annoying corrections to a book. This must have been incredibly inconvenient, and inserting correcting entries at the current time is a lot easier. Secondly, if your accounting balances are used to file taxes, changing some of the balances retroactively makes it difficult to go back and check the detail of reported amounts in case of an audit. This problem also applies to our context, but whether a past correction should be allowed is a choice that depends on the context and the particular account, and we leave it up to the user to decide whether it should be allowed. \u21a9","title":"Balance Assertions in Beancount"},{"location":"balance_assertions_in_beancount.html#balance-assertions-in-beancount","text":"Martin Blais, July 2014 http://furius.ca/beancount/doc/proposal-balance This document summarizes the different kinds of semantics for balance assertions in all command-line accounting systems and proposes new syntax for total and file-order running assertions in Beancount. Motivation Partial vs. Complete Assertions File vs. Date Assertions Ordering & Ambiguity Intra-Day Assertions Beginning vs. End of Day Status Proposal File Assertions Complete Assertions","title":"Balance Assertions in Beancount"},{"location":"balance_assertions_in_beancount.html#motivation","text":"Both Beancount and Ledger implement balance assertions. These provide the system with checkpoints it can use to verify the integrity of the data entry 1 . Traditional accounting principles state that a user may never change the past\u2014correcting the past involves inserting new entries to undo past mistakes 2 \u2014but we in the command-line accounting community take issue with that: we want to remain able to reconstruct the past and more importantly, to correct past mistakes at the site of their original data entry. Yes, we are dilettantes but we are bent on simplifying the process of bookkeeping by challenging existing concepts and think that this is perfectly reasonable, as long as it does not break known balances. These known balances are what we provide via balance assertions. Another way to look at these balance assertions, is that they are simply the bottom line amounts reported on various account statements, as exemplified in the figure below. Following this thread , we established that there were differing interpretations of balance assertions in the current versions of Ledger (3.0) and Beancount (2.0b).","title":"Motivation"},{"location":"balance_assertions_in_beancount.html#partial-vs-complete-assertions","text":"An assertion in Beancount currently looks like this: 2012-02-04 balance Assets:CA:Bank:Checking 417.61 CAD The meaning of this directive is: \u201cplease assert that the inventory of account \u2018Assets:CA:Bank:Checking\u2019 contains exactly 417.61 units of CAD at the end of February 3, 2012\u201d (so it\u2019s dated on the 4th because in Beancount balance assertions are specified at the beginning of the date). It says nothing about other commodities in the account\u2019s inventory. For example, if the account contains units of \u201cUSD\u201d, those are unchecked. We will call this interpretation a partial balance assertion or single-commodity assertion . An alternative assertion would be exhaustive: \u201cplease assert that the inventory of account \u2018Assets:CA:Bank:Checking\u2019 contains only 417.61 units of CAD at the end of February 3, 2012.\u201d We call this a complete balance assertion . In order to work, this second type of assertion would have to support the specification of the full contents of an inventory. This is currently not supported. Further note that we are not asserting the cost basis of an inventory, just the number of units.","title":"Partial vs. Complete Assertions"},{"location":"balance_assertions_in_beancount.html#file-vs-date-assertions","text":"There are two differing interpretations and implementations of the running balances for assertions: Beancount first sorts all the directives and verifies the balance at the beginning of the date of the directive. In the previous example, that is \u201c the balance before any transactions on 2012-02-04 are applied.\u201d We will call this date assertions or date-based assertions . Ledger keeps a running balance of each account\u2019s inventory during its parsing phase and performs the check at the site of the assertion in the file. We will call this file assertions or file-order, or file-based assertions . This kind of assertion has no date associated with it (this is slightly misleading in Ledger because of the way assertions are specified, as attached to a transaction\u2019s posting, which appears to imply that they occur on the transaction date, but they don\u2019t, they strictly apply to the file location).","title":"File vs. Date Assertions"},{"location":"balance_assertions_in_beancount.html#ordering-ambiguity","text":"An important difference between those two types of assertions is that file-based assertions are not order-independent. For example, take the following input file: ;; Credit card account 2014/05/01 opening Liabilities:CreditCard $-1000.00 Expenses:Opening-Balances 2014/05/12 dinner Liabilities:CreditCard $-74.20 Expenses:Restaurant ;; Checking account 2014/06/05 salary Assets:Checking $4082.87 Income:Salary 2014/06/05 cc payment Assets:Checking $-1074.20 = $3008.67 Liabilities:CreditCard = $0 If you move the credit card payment up to the credit card account section, the same set of transactions fails: ;; Credit card account 2014/05/01 opening Liabilities:CreditCard $-1000.00 Expenses:Opening-Balances 2014/05/12 dinner Liabilities:CreditCard $-74.20 Expenses:Restaurant 2014/06/05 cc payment Assets:Checking $-1074.20 = $3008.67 Liabilities:CreditCard = $0 ;; Checking account 2014/06/05 salary Assets:Checking $4082.87 Income:Salary This subtle problem could be difficult for beginners to understand. Moving the file assertion to its own, undated directive might be more indicative syntax of its semantics, something that would look like this: balance Assets:Checking = $3008.67 The absence of a date indicates that the check is not applied at a particular point in time.","title":"Ordering & Ambiguity"},{"location":"balance_assertions_in_beancount.html#intra-day-assertions","text":"On the other hand, date-based assertions, because of their order-independence, preclude intra-day assertions, that is, a balance assertion that occurs between two postings on the same account during the same day. Beancount does not support intra-day assertions at the moment. Note despite this shortcoming, this has not been much of a problem, because it is often possible to fudge the date where necessary by adding or removing a day, or even just skipping an assertion where it would be impossible (skipping assertions is not a big deal, as they are optional and their purpose is just to provide certainty. As long as you have one that appears some date after the skipped one, it isn\u2019t much of an issue). It would be nice to find a solution to intra-day assertions for date-based assertions, however. One interesting idea would be to extend the semantics to apply the balance in file order within the set of all transactions directly before and after the balance check that occur on the same date as the balance check, for example, this would balance: 2013-05-05 balance Assets:Checking 100 USD 2013-05-20 * \"Interest payment\" Assets:Checking 12.01 USD Income:Interest 2013-05-20 balance Assets:Checking 112.01 USD 2013-05-20 * \"Check deposit\" Assets:Checking 731.73 USD Assets:Receivable The spell would be broken as soon as a directive would appear at a different date. Another idea would be to always sort the balance assertions in file-order as the second sort key (after the date) and apply them as such. I\u2019m not sure this would be easy to understand though.","title":"Intra-Day Assertions"},{"location":"balance_assertions_in_beancount.html#beginning-vs-end-of-day","text":"Finally, just for completeness, it is worth mentioning that date assertions have to have well-defined semantics regarding when they apply during the day. In Beancount, they currently apply at the beginning of the day. It might be worthwhile to provide an alternate version of date-based assertions that applies at the end of the day, e.g. \u201cbalance_end\u201d. Beancount v1 used to have this (\u201ccheck_end\u201d) but it was removed in the v2 rewrite, as it wasn\u2019t clear it would be really needed. The simplicity of a single meaning for balance assertions is nice too.","title":"Beginning vs. End of Day"},{"location":"balance_assertions_in_beancount.html#status","text":"Ledger 3.0 currently supports only partial file-order assertions, on transactions. Beancount 2.0 currently supports only partial date-based assertions at the beginning of the day.","title":"Status"},{"location":"balance_assertions_in_beancount.html#proposal","text":"I propose the following improvements to Beancount\u2019s balance assertions.","title":"Proposal"},{"location":"balance_assertions_in_beancount.html#file-assertions","text":"File assertions should be provided as a plugin. They would look like this: 2012-02-03 file_balance Assets:CA:Bank:Checking 417.61 CAD Ideally, in order to make it clear that they apply strictly in file order, they would not have a date, something like this: file_balance Assets:CA:Bank:Checking 417.61 CAD But this breaks the regularity of the syntax for all other directives. It also complicates an otherwise very regular and simple parser just that much more\u2026 all other directives begin with a date and a word, and all other lines are pretty much ignored. It would be a bit of a departure from this. Finally, it would still be nice to have a date just to insert those directives somewhere in the rendered journal. So I\u2019m considering keeping a date for it. If you decide to use those odd assertions, you should know what they mean. I also don\u2019t like the idea of attaching assertions to transactions; transaction syntax is already busy enough, it is calling to remain simple. This should be a standalone directive that few people use. In order to implement this, the plugin would simply resort all the directives according to their file location only (using the fileloc attribute of entries), disregarding the date, and recompute the running balances top to bottom while applying the checks. This can entirely be done via post-processing, like date-based assertions, without disturbing any of the other processing. Moreover, another advantage of doing this in a plugin is that people who don\u2019t use this directive won\u2019t have to pay the cost of calculating these inventories.","title":"File Assertions"},{"location":"balance_assertions_in_beancount.html#complete-assertions","text":"Complete assertions should be supported in Beancount by the current balance assertion directive. They aren\u2019t very important but are potentially useful. Possible syntax ideas: 2012-02-03 balance* Assets:CA:Bank:Checking 417.61 CAD, 162 USD 2012-02-03 balance Assets:CA:Bank:Checking = 417.61 CAD, 162 USD 2012-02-03 balance full Assets:CA:Bank:Checking 417.61 CAD, 162 USD 2012-02-03 balance Assets:CA:Bank:Checking 417.61 CAD Assets:CA:Bank:Checking 162 USD I\u2019m still undecided which is best. So far it seems a matter of taste. As far as we know, the notion of inputting an explicit expected amount is unique to command-line accounting systems. Other systems \u201creconcile\u201d by freezing changes in the past. \u21a9 There are multiple reasons for this. First, in pre-computer times, accounting was done using books, and recomputing running balances manually would have involved making multiple annoying corrections to a book. This must have been incredibly inconvenient, and inserting correcting entries at the current time is a lot easier. Secondly, if your accounting balances are used to file taxes, changing some of the balances retroactively makes it difficult to go back and check the detail of reported amounts in case of an audit. This problem also applies to our context, but whether a past correction should be allowed is a choice that depends on the context and the particular account, and we leave it up to the user to decide whether it should be allowed. \u21a9","title":"Complete Assertions"},{"location":"beancount_cheat_sheet.html","text":"Beancount Syntax Cheat Sheet \uf0c1 Example Account Name: Assets:US:BofA:Checking Account Types Assets Liabilities Income Expenses Equity + - - + - Commodities All in CAPS: USD, EUR, CAD, AUD GOOG, AAPL, RBF1005 HOME_MAYST, AIRMILES HOURS Directives General syntax: YYYY-MM-DD Opening & Closing Accounts 2001-05-29 open Expenses:Restaurant 2001-05-29 open Assets:Checking USD,EUR ; Currency constraints 2015-04-23 close Assets:Checking Declaring Commodities This is optional; use this only if you want to attach metadata by currency. 1998-07-22 commodity AAPL name: \"Apple Computer Inc.\" Prices Use many times to fill historical price database: 2015-04-30 price AAPL 125.15 USD 2015-05-30 price AAPL 130.28 USD Notes 2013-03-20 note Assets:Checking \"Called to ask about rebate\" Documents 2013-03-20 document Assets:Checking \"path/to/statement.pdf\" Transactions 2015-05-30 * \"Some narration about this transaction\" Liabilities:CreditCard -101.23 USD Expenses:Restaurant 101.23 USD 2015-05-30 ! \"Cable Co\" \"Phone Bill\" #tag \u02c6link id: \"TW378743437\" ; Meta-data Expenses:Home:Phone 87.45 USD Assets:Checking ; You may leave one amount out Postings ... 123.45 USD Simple ... 10 GOOG {502.12 USD} With per-unit cost ... 10 GOOG {{5021.20 USD}} With total cost ... 10 GOOG {502.12 # 9.95 USD} With both costs ... 1000.00 USD @ 1.10 CAD With per-unit price ... 10 GOOG {502.12 USD} @ 1.10 CAD With cost & price ... 10 GOOG {502.12 USD, 2014-05-12 } With date ! ... 123.45 USD ... With flag Balance Assertions and Padding Asserts the amount for only the given currency: 2015-06-01 balance Liabilities:CreditCard -634.30 USD Automatic insertion of transaction to fulfill the following assertion: YYYY-MM-DD pad Assets:Checking Equity:Opening-Balances Events YYYY-MM-DD event \"location\" \"New York, USA\" YYYY-MM-DD event \"address\" \"123 May Street\" Options option \"title\" \"My Personal Ledger\" See this doc for the full list of supported options. Other pushtag #trip-to-peru ... poptag #trip-to-peru ; Comments begin with a semi-colon","title":"Beancount Cheat Sheet"},{"location":"beancount_cheat_sheet.html#beancount-syntax-cheat-sheet","text":"Example Account Name: Assets:US:BofA:Checking","title":"Beancount Syntax Cheat Sheet"},{"location":"beancount_design_doc.html","text":"Beancount Design Doc \uf0c1 Martin Blais ( blais@furius.ca ) http://furius.ca/beancount/doc/design-doc A guide for developers to understand Beancount\u2019s internals. I hope for this document to provide a map of the main objects used in the source code to make it easy to write scripts and plugins on top of Beancount or even extend it. Introduction Invariants Isolation of Inputs Order-Independence All Transactions Must Balance Account Have Types Account Lifetimes & Open Directives Supports Dates Only (and No Time) Metadata is for User Data Overview of the Codebase Core Data Structures Number Commodity Amount Lot Position Inventory Account Flag Posting About Tuples & Mutability Summary Directives Common Properties Transactions Flag Payee & Narration Tags Links Postings Balancing Postings Elision of Amounts Stream Processing Stream Invariants Loader & Processing Order Loader Output Parser Implementation Two Stages of Parsing: Incomplete Entries The Printer Uniqueness & Hashing Display Context Realization The Web Interface Reports vs. Web Client-Side JavaScript The Query Interface Design Principles Minimize Configurability Favor Code over DSLs File Format or Input Language? Grammar via Parser Generator Future Work Tagged Strings Errors Cleanup Types Conclusion Introduction \uf0c1 This document describes the principles behind the design of Beancount and a high-level overview of its codebase, data structures, algorithms, implementation and methodology. This is not a user's manual; if you are interested in just using Beancount, see the associated User's Manual and all the other documents available here . However, if you just want to understand more deeply how Beancount works this document should be very helpful. This should also be of interest to developers. This is a place where I wrote about a lot of the ideas behind Beancount that did not find any other venue. Expect some random thoughts that aren\u2019t important for users. Normally one writes a design document before writing the software. I did not do that. But I figured it would still be worthwhile to spell out some of the ideas that led to this design here. I hope someone finds this useful. Invariants \uf0c1 Isolation of Inputs \uf0c1 Beancount should accept input only from the text file you provide to its tools. In particular, it should not contact any external networked service or open any \u201cglobal\u201d files, even just a cache of historical prices or otherwise. This isolation is by design. Isolating the input to a single source makes it easier to debug, reproduce and isolate problems when they occur. This is a nice property for the tool. In addition, fetching and converting external data is very messy. External data formats can be notoriously bad, and they are too numerous to handle all of them (handling just the most common subset would beg the question of where to include the implementation of new converters). Most importantly, the problems of representing the data vs. that of fetching and converting it segment from each other very well naturally: Beancount provides a core which allows you to ingest all the transactional data and derive reports from it, and its syntax is the hinge that connects it to these external repositories of transactions or prices. It isolates itself from the ugly details of external sources of data in this way. Order-Independence \uf0c1 Beancount offers a guarantee that the ordering of its directives in an input file is irrelevant to the outcome of its computations. You should be able to organize your input text and reorder any declaration as is most convenient for you, without having to worry about how the software will make its calculations. This also makes it trivial to implement inclusions of multiple files: you can just concatenate the files if you want, and you can process them in any order. Not even directives that declare accounts or commodities are required to appear before these accounts get used in the file. All directives are parsed and then basically sorted before any calculation or validation occurs (a minor detail is that the line number is used as a secondary key in the sorting, so that the re-ordering is stable). Correspondingly, all the non-directive grammar that is in the language is limited to either setting values with global impact (\u201c option \u201d, \u201c plugin \u201d) or syntax-related conveniences (\u201c pushtag \u201d, \u201c poptag \u201d). There is an exception: Options may have effects during the processing of the file and should appear at the beginning. At the moment, Beancount does not enforce this, e.g., by performing multiple passes of parsing, but it probably should. What\u2019s more, options in included files are currently ignored. You should put all your options in the top-level ledger file. All Transactions Must Balance \uf0c1 All transactions must balance by \u201cweight.\u201d There is no exception. Beancount transactions are required to have balanced, period. In particular, there is no allowance for exceptions such as the \u201cvirtual postings\u201d that can be seen in Ledger. The first implementation of Beancount allowed such postings. As a result, I often used them to \u201cresolve\u201d issues that I did not know how to model well otherwise. When I rewrote Beancount, I set out to convert my entire input file to avoid using these, and over time I succeeded in doing this and learned a lot of new things in the process. I have since become convinced that virtual postings are wholly unnecessary, and that making them available only provides a crutch upon which a lot of users will latch instead of learning to model transfers using balanced transactions. I have yet to come across a sufficiently convincing case for them 1 . This provides the property that any subsets of transactions will sum up to zero. This is a nice property to have, it means we can generate balance sheets at any point in time. Even when we eventually support settlement dates or per-posting dates , we will split up transactions in a way that does not break this invariant. Accounts Have Types \uf0c1 Beancount accounts should be constrained to be of one of five types: Assets, Liabilities, Income, Expenses and Equity. The rationale behind this is to realize that all matters of counting can be characterized by being either permanent (Assets, Liabilities) or transitory (Income, Expenses), and that the great majority of accounts have a usual balance sign: positive (Assets, Expenses) or negative (Liabilities, Income). Given a quantity to accumulate, we can always select one of these four types of labels for it. Enforcing this makes it possible to implement reports of accumulated values for permanent accounts (balance sheet) and reports of changes of periods of time (income statement). Although it is technically possible to explicitly book postings against Equity accounts the usual purpose of items in that category is to account for past changes on the balance sheet (net/retained income or opening balances). These concepts of time and sign generalize beyond that of traditional accounting. Not having them raises difficult and unnecessary questions about how to handle calculating these types of reports. We simply require that you label your accounts into this model. I\u2019ll admit that this requires a bit of practice and forethought, but the end result is a structure that easily allows us to build commonly expected outputs. Account Lifetimes & Open Directives \uf0c1 Accounts have lifetimes; an account opens at a particular date and optionally closes at another. This is directed by the Open and Close directives. All accounts are required to have a corresponding Open directive in the stream. By default, this is enforced by spitting out an error when posting to an account whose Open directive hasn\u2019t been seen in the stream. You can automate the generation of these directives by using the \u201cauto_accounts\u201d plugin, but in any case, the stream of entries will always contain the Open directives. This is an assumption that one should be able to rely upon when writing scripts. (Eventually a similar constraint will be applied for Commodity directives so that the stream always includes one before it is used; and they should be auto-generated as well. This is not the case right now [June 2015].) Supports Dates Only (and No Time) \uf0c1 Beancount does not represent time, only dates. The minimal time interval is one day. This is for simplicity\u2019s sake. The situations where time would be necessary to disambiguate account balances exist, but in practice they are rare enough that their presence does not justify the added complexity of handling time values and providing syntax to deal with it. If you have a use case whereby times are required, you may use metadata to add it in, or more likely, you should probably write custom software for it. This is outside the scope of Beancount by choice. Metadata is for User Data \uf0c1 By default, Beancount will not make assumptions about metadata fields and values. Metadata is reserved for private usage by the user and for plugins. The Beancount core does not read metadata and change its processing based on it. However, plugins may define special metadata values that affect what they produce. That being said, there are a few special metadata fields produced by the Beancount processing: filename: A string, the name of the file the transaction was read from (or the plugin that created it) lineno: An integer, the line number from the file where the transaction was read from. tolerances : A dict of currency to Decimal, the tolerance values used in balancing the transaction. residual (on postings): A boolean, true if the posting has been added to automatically absorb rounding error. There may be a few more produced in the future but in any case, the core should not read any metadata and affect its behavior as a consequence. (I should probably create a central registry or location where all the special values can be found in one place.) Overview of the Codebase \uf0c1 All source code lives under a \u201c beancount \u201d Python package. It consists of several packages with well-defined roles, the dependencies between which are enforced strictly. Beancount source code packages and approximate dependencies between them. At the bottom, we have a beancount.core package which contains the data structures used to represent transactions and other directives in memory. This contains code for accounts, amounts, lots, positions, and inventories. It also contains commonly used routines for processing streams of directives. One level up, the beancount.parser package contains code to parse the Beancount language and a printer that can produce the corresponding text given a stream of directives. It should be possible to convert between text and data structure and round-trip this process without loss. At the root of the main package is a beancount.loader module that coordinates everything that has to be done to load a file in memory. This is the main entry point for anyone writing a script for Beancount, it always starts with loading a stream of entries. This calls the parser, runs some processing code, imports the plugins and runs them in order to produce the final stream of directives which is used to produce reports. The beancount.plugins package contains implementations of various plugins. Each of these plugin modules are independent from each other. Consult the docstrings in the source code to figure out what each of these does. Some of these are prototypes of ideas. There is a beancount.ops package which contains some high-level processing code and some of the default code that always runs by default that is implemented as plugins as well (like padding using the Pad directive). This package needs to be shuffled a bit in order to clarify its role. On top of this, reporting code calls modules from those packages. There are four packages which contain reporting code, corresponding to the Beancount reporting tools: beancount.reports : This package contains code specialized to produce different kinds of outputs (invoked by bean-report). In general I\u2019d like to avoid defining custom routines to produce desired outputs and use the SQL query tool to express grouping & aggregation instead, but I think there will always be a need for at least some of these. The reports are defined in a class hierarchy with a common interface and you should be able to extend them. Each report supports some subset of a number of output formats. beancount.query : This package contains the implementation of a query language and interactive shell (invoked by bean-query) that allows you to group and aggregate positions using an SQL-like DSL. This essentially implements processing over an in-memory table of Posting objects, with functions defined over Amount, Position and Inventory objects. beancount.web : This package contains all the source code for the default web interface (invoked by bean-web). This is a simple Bottle application that serves many of the reports from beancount.report to HTML format, running on your machine locally. The web interface provides simple views and access to your data. (It stands to be improved greatly, it\u2019s in no way perfect.) beancount.projects : This package contains various custom scripts for particular applications that I\u2019ve wanted to share and distribute. Wherever possible, I aim to fold these into reports. There are no scripts to invoke these; you should use \u201c python3 -m beancount.projects. \u201d to run them. There is a beancount.scripts package that contains the \u201cmain\u201d programs for all the scripts under the bin directory . Those scripts are simple launchers that import the corresponding file under beancount.scripts. This allows us to keep all the source code under a single directory and that makes it easier to run linters and other code health checkers on the code\u2014it\u2019s all in one place. Finally, there is a beancount.utils package which is there to contain generic reusable random bits of utility code. And there is a relatively unimportant beancount.docs package that contains code used by the author just to produce and maintain this documentation (code that connects to Google Drive). Enforcing the dependency relationships between those packages is done by a custom script . Core Data Structures \uf0c1 This section describes the basic data structures that are used as building blocks to represent directives. Where possible I will describe the data structure in conceptual terms. (Note for Ledger users : This section introduces some terminology for Beancount; look here if you\u2019re interested to contrast it with concepts and terminology found in Ledger.) Number \uf0c1 Numbers are represented using decimal objects, which are perfectly suited for this. The great majority of numbers entered in accounting systems are naturally decimal numbers and binary representations involve representational errors which cause many problems for display and in precision. Rational numbers avoid this problem, but they do not carry the limited amount of precision that the user intended in the input. We must deal with tolerances explicitly. Therefore, all numbers should use Python\u2019s \u201c decimal.Decimal \u201d objects. Conveniently, Python v3.x supports a C implementation of decimal types natively (in its standard library; this used to be an external \u201ccdecimal\u201d package to install but it has been integrated in the C/Python interpreter). The default constructor for decimal objects does not support some of the input syntax we want to allow, such as commas in the integer part of numbers (e.g., \u201c278 , 401.35 USD\u201d) or initialization from an empty string. These are important cases to handle. So I provide a special constructor to accommodate these inputs: beancount.core.number.D() . This is the only method that should be used to create decimal objects: from beancount.core.number import D \u2026 number = D(\"2,402.21\") I like to import the \u201c D \u201d symbol by itself (and not use number.D ). All the number-related code is located under beancount.core.number . Some number constants have been defined as well: ZERO , ONE , and HALF . Use those instead of explicitly constructed numbers (such as D(\"1\") ) as it makes it easier to grep for such initializations. Commodity \uf0c1 A commodity , or currency (I use both names interchangeably in the code and documentation) is a string that represents a kind of \u201cthing\u201d that can be stored in accounts. In the implementation, it is represented as a Python str object (there is no module with currency manipulation code). These strings may only contain capital letters and numbers and some special characters (see the lexer code). Beancount does not predefine any currency names or categories\u2014all currencies are created equal. Really. It genuinely does not know anything special about dollars or euros or yen or anything else. The only place in the source code where there is a reference to those is in the tests. There is no support for syntax using \u201c$\u201d or other currency symbols; I understand that some users may want this syntax, but in the name of keeping the parser very simple and consistent I choose not to provide that option. Moreover, Beancount does not make a distinction between commodities which represent \"money\" and others that do not (such as shares, hours, etc.). These are treated the same throughout the system. It also does not have the concept of a \"home\" currency 2 ; it's a genuinely multi-currency system. Currencies need not be defined explicitly in the input file format; you can just start using them in the file and they will be recognized as such by their unique syntax (the lexer recognizes and emits currency tokens). However, you may declare some using a Commodity directive. This is only provided so that a per-commodity entity exists upon which the user can attach some metadata, and some reports and plugins are able to find and use that metadata. Account \uf0c1 An account is basically just the name of a bucket associated with a posting and is represented as a simple string (a Python str object). Accounts are detected and tokenized by the lexer and have names with at least two words separated by a colon (\":\"). Accounts don\u2019t have a corresponding object type; we just refer to them by their unique name string (like filenames in Python). When per-account attributes are needed, we can extract the Open directives from the stream of entries and find the one corresponding to the particular account we\u2019re looking for. Similar to Python\u2019s os.path module, there are some routines to manipulate account names in beancount.core.account and the functions are named similarly to those of the os.path module. The first component of an account\u2019s name is limited to one of five types: Assets Liabilities Equity Income Expenses The names of the account types as read in the input syntax may be customized with some \"option\" directives, so that you can change those names to another language, or even just rename \"Income\" to \"Revenue\" if you prefer that. The beancount.core.account_types module contains some helpers to deal with these. Note that the set of account names forms an implicit hierarchy. For example, the names: Assets:US:TDBank:Checking Assets:US:TDBank:Savings implicitly defines a tree of nodes with parent nodes \"Assets\", \"US\", \"TDBank\" with two leaf nodes \"Checking\" and \"Savings\". This implicit tree is never realized during processing, but there is a Python module that allows one to do this easily (see beancount.core.realization ) and create linearized renderings of the tree. Flag \uf0c1 A \u201cflag\u201d is a single-character string that may be associated with Transactions and Postings to indicate whether they are assumed to be correct (\"reconciled\") or flagged as suspicious. The typical value used on transaction instances is the character \u201c*\u201d. On Postings, it is usually left absent (and set to a None). Amount \uf0c1 An Amount is the combination of a number and an associated currency, conceptually: Amount = (Number, Currency) Amount instances are used to represent a quantity of a particular currency (the \u201cunits\u201d) and the price on a posting. A class is defined in beancount.core.amount as a simple tuple-like object. Functions exist to perform simple math operations directly on instances of Amount. You can also create Amount instance using amount.from_string() , for example: value = amount.from_string(\"201.32 USD\") Cost \uf0c1 A Cost object represents the cost basis for a particular lot of a commodity. Conceptually, it is Cost = (Number, Currency, Date, Label) The number and currency is that of the cost itself, not of the commodity. For example, if you bought 40 shares of AAPL stock at 56.78 USD, the Number is a \u201c56.78\u201d decimal and the Currency is \u201cUSD.\u201d For example: Cost(Decimal(\"56.78\"), \"USD\", date(2012, 3, 5), \"lot15\") The Date is the acquisition date of the corresponding lot (a datetime.date object). This is automatically attached to the Cost object when a posting augments an inventory\u2014the Transaction\u2019s date is automatically attached to the Cost object\u2014or if the input syntax provides an explicit date override. The Label can be any string. It is provided as a convenience for a user to refer to a particular lot or disambiguate similar lots. On a Cost object, the number, currency and date attributes are always set. If the label is unset, it has a value of \u201cNone.\u201d CostSpec \uf0c1 In the input syntax, we allow users to provide as little information as necessary in order to disambiguate between the lots contained in the inventory prior to posting. The data provided filters the set of matching lots to an unambiguous choice, or to a subset from which an automated booking algorithm will apply (e.g., \u201cFIFO\u201d). In addition, we allow the user to provide either the per-unit cost and/or the total-cost. This convenience is useful to let Beancount automatically compute the per-unit cost from a total of proceeds. CostSpec = (Number-per-unit, Number-total, Currency, Date, Label, Merge) Since any of the input elements may be omitted, any of the attributes of a CostSpec may be left to None. If a number is missing and necessary to be filled in, the special value \u201cMISSING\u201d will be set on it. The Merge attribute it used to record a user request to merge all the input lots before applying the transaction and to merge them after. This is the method used for all transactions posted to an account with the \u201cAVERAGE\u201d booking method. Position \uf0c1 A position represents some units of a particular commodity held at cost. It consists simply in Position = (Units, Cost) Units is an instance of Amount , and Cost is an instance of Cost , or a null value if the commodity is not held at cost. Inventories contain lists of Position instances. See its definition in beancount.core.position . Posting \uf0c1 Each Transaction directive is composed of multiple Postings (I often informally refer to these in the code and on the mailing-list as the \u201clegs\u201d of a transaction). Each of these postings is associated with an account, a position and an optional price and flag: Posting = (Account, Units, Cost-or-CostSpec, Price, Flag, Metadata) As you can see, a Posting embeds its Position instance 3 . The Units is an Amount , and the \u2018cost\u2019 attribute refers to either a Cost or a CostSpec instance (the parser outputs Posting instances with an CostSpec attribute which is resolved to a Cost instance by the booking process; see How Inventories Work for details). The Price is an instance of Amount or a null value. It is used to declare a currency conversion for balancing the transaction, or the current price of a position held at cost. It is the Amount that appears next to a \u201c@\u201d in the input. Flags on postings are relatively rare; users will normally find it sufficient to flag an entire transaction instead of a specific posting. The flag is usually left to None; if set, it is a single-character string. The Posting type is defined in beancount.core.data , along with all the directive types. Inventory \uf0c1 An Inventory is a container for an account\u2019s balance in various lots of commodities. It is essentially a list of Position instances with suitable operations defined on it. Conceptually you may think of it as a mapping with unique keys: Inventory = [Position1, Position2, Position3, \u2026 , PositionN] Generally, the combination of a position\u2019s ( Units.Currency, Cost) is kept unique in the list, like the key of a mapping. Positions for equal values of currency and cost are merged together by summing up their Units.Number and keeping a single position for it. And simple positions are mixed in the list with positions held at cost. The Inventory is one of the most important and oft-used object in Beancount\u2019s implementation, because it is used to sum the balance of one or more accounts over time. It is also the place where the inventory reduction algorithms get applied to, and traces of that mechanism can be found there. The \u201c How Inventories Work \u201d document provides the full detail of that process. For testing, you can create initialized instances of Inventory using inventory.from_string() . All the inventory code is written in beancount.core.inventory . About Tuples & Mutability \uf0c1 Despite the program being written in a language which does not make mutability \u201cdifficult by default\u201d, I designed the software to avoid mutability in most places. Python provides a \u201c collections.namedtuple \u201d factory that makes up new record types whose attributes cannot be overridden. Well\u2026 this is only partially true: mutable attributes of immutable tuples can be modified. Python does not provide very strong mechanisms to enforce this property. Regardless, functional programming is not so much an attribute of the language used to implement our programs than of the guarantees we build into its structure. A language that supports strong guarantees helps to enforce this. But if, even by just using a set of conventions, we manage to mostly avoid mutability, we have a mostly functional program that avoids most of the pitfalls that occur from unpredictable mutations and our code is that much easier to maintain. Programs with no particular regard for where mutation occurs are most difficult to maintain. By avoiding most of the mutation in a functional approach, we avoid most of those problems. Most of the data structures used in Beancount are namedtuples , which make the modification of their attributes inconvenient on purpose. Most of the code will attempt to avoid mutation, except for local state (within a function) or in a narrow scope that we can easily wrap our head around. Where mutation is possible, by convention I try to avoid it by default. When we do mutation, I try to document the effects. I avoid the creation of classes which mutate their internal state, except for a few cases (e.g. the web application). I prefer functions to objects and where I define classes I mostly avoid inheritance. These properties are especially true for all the small objects: Amount, Lot, Position, Posting objects, and all the directives types from beancount.core.data . On the other hand, the Inventory object is nearly always used as an accumulator and does allow the modification of its internal state (it would require a special, persistent data structure to avoid this). You have to be careful how you share access to Inventory objects\u2026 and modify them, if you ever do. Finally, the loader produces lists of directives which are all simple namedtuple objects. These lists form the main application state. I\u2019ve avoided placing these inside some special container and instead pass them around explicitly, on purpose. Instead of having some sort of big \u201capplication\u201d object, I\u2019ve trimmed down all the fat and all your state is represented in two things: A dated and sorted list of directives which can be the subject of stream processing and a list of constant read-only option values. I think this is simpler. I credit my ability to make wide-ranging changes to a mid-size Python codebase to the adoption of these principles. I would love to have types in order to safeguard against another class of potential errors, and I plan to experiment with Python 3.5\u2019s upcoming typing module . Summary \uf0c1 The following diagram explains how these objects relate to each other, starting from a Posting. For example, to access the number of units of a postings you could use posting.units.number For the cost currency: posting.cost.currency You can print out the tuples in Python to figure out their structure. Previous Design \uf0c1 For the sake of preservation, if you go back in time in the repository, the structure of postings was deeper and more complex. The new design reflects a flatter and simpler version of it. Here is what the old design used to look like: Directives \uf0c1 The main production from loading a Beancount input file is a list of directives . I also call these entries interchangeably in the codebase and in the documents. There are directives of various types: Transaction Balance & Pad Open & Close Commodity Note Event Price Document In a typical Beancount input file, the great majority of entries will be Transactions and some Balance assertions. There will also be Open & perhaps some Close entries. Everything else is optional. Since these combined with a map of option values form the entire application state, you should be able to feed those entries to functions that will produce reports. The system is built around this idea of processing a stream of directives to extract all contents and produce reports, which are essentially different flavors of filtering and aggregations of values attached to this stream of directives. Common Properties \uf0c1 Some properties are in common to all the directives: Date. A datetime.date object. This is useful and consistent. For a Transaction, that\u2019s the date at which it occurs. For an Open or Close directive, that\u2019s the date at which the account was opened or closed. For a Pad directive, that\u2019s the date at which to insert a transaction. For a Note or Document, it is the date at which to insert the note in the stream of postings of that account. For an Event, it is the date at which it occurs. For a Commodity directive which essentially provides a per-commodity hanging point for commodity-related metadata, the date isn\u2019t as meaningful; I choose to date those on the day the commodity was created. Meta . All the directives have metadata attribute (a Python dict object). The purpose of metadata is to allow the user to hang any kind of ancillary data they want and then use this in scripts or queries. Posting instances also have a metadata attribute. Transactions \uf0c1 The Transaction directive is the subject of Beancount and is by far the most common directive found in input files and is worth of special attention. The function of a bookkeeping system is to organize the Postings attached to Transactions in various ways. All the other types of entries occupy supporting roles in our system. A Transaction has the following additional fields. Flag \uf0c1 The single-character flag is usually there to replace the \u201ctxn\u201d keyword (Transaction is the only directive which allows being entered without entering its keyword). I\u2019ve been considering changing the syntax definition somewhat to allow not entering the flag nor the keyword, because I\u2019d like to eventually support that. Right now, either the flag or keyword is required. The flag attribute may be set to None. Payee & Narration \uf0c1 The narration field is a user-provided description of the transaction, such as \"Dinner with Mary-Ann.\" You can put any information in this string. It shows up in the journal report. Oftentimes it is used to enrich the transaction with context that cannot be imported automatically, such as \"transfer from savings account to pay for car repairs.\" The payee name is optional, and exists to describe the entity with which the transaction is conducted, such as \"Whole Foods Market\" or \"Shell.\" Note that I want to be able to produce reports for all transactions associated with a particular payee, so it would be nice to enter consistent payee names. The problem with this is that the effort to do this right is sometimes too great. Either better tools or looser matching over names is required in order to make this work. The input syntax also allows only a single string to be provided. By default this becomes the narration, but I\u2019ve found that in practice it can be useful to have just the payee. It\u2019s just a matter of convenience. At the moment, if you want to enter just the payee string you need to append an empty narration string. This should be revisited at some point. Tags \uf0c1 Tags are sets of strings that can be used to group sets of transactions (or set to None if there are no tags). A view of this subset of transactions can then be generated, including all the usual reports (balance sheet, income statement, journals, etc.). You can tag transactions for a variety of purposes; here are some examples: All transactions during a particular travel might be tagged to later summarize your trip expenses. Those transactions will usually occur around the same date and therefore there is convenient syntax used to automatically tag many transactions that are declared together in a file. Transactions related to a particular project to be accomplished. I took some classes in an online program once, and tagged all related expenses to it. I use this to track all my immigration-related expenses for a particular stage (e.g. green card). Declaring a group of expenses to be paid back by an employer (or your own company) later on. Expenses related to moving between locations. Typical tag names that I use for tags look like #trip-israel-2012 , #conference-siggraph , and #course-cfa . In general, tags are useful where adding a sub-accounts won't do. This is often the case where a group of related expenses are of differing types, and so they would not belong to a single account. Given the right support from the query tools, they could eventually be subsumed by metadata\u2014I have been considering converting tags into metadata keys with a boolean value of True, in order to remove unnecessary complexity. Links \uf0c1 Links are a unique set of strings or None , and in practice will be usually empty for most transactions. They differ from tags only in purpose. Links are there to chain together a list of related transactions and there are tools used to list, navigate and balance a subset of transactions linked together. They are a way for a transaction to refer to other transactions. They are not meant to be used for summarizing. Examples include: transaction-ids from trading accounts (these often provide an id to a \"related\" or \"other\" transaction); account entries associated with related corrections, for example a reversed fee following a phone call could be linked to the original invalid fee from several weeks before; the purchase and sale of a home, and related acquisition expenses. In contrast to tags, their strings are most often unique numbers produced by the importers. No views are produced for links; only a journal of a particular links transactions can be produced and a rendered transaction should be accompanied by an actual \"link\" icon you can click on to view all the other related transactions. Postings \uf0c1 A list of Postings are attached to the Transaction object. Technically this list object is mutable but in practice we try not to modify it. A Posting can ever only be part of a single Transaction. Sometimes different postings of the same transaction will settle at different dates in their respective accounts, so eventually we may allow a posting to have its own date to override the transaction's date, to be used as documentation; in the simplest version, we enforce all transactions to occur punctually, which is simple to understand and was not found to be a significant problem in practice. Eventually we might implement this by implicitly converting Transactions with multiple dates into multiple Transactions and using some sort of transfer account to hold the pending balance in-between dates. See the associated proposal for details. Balancing Postings \uf0c1 The fundamental principle of double-entry book-keeping is enforced in each of the Transaction entries: the sum of all postings must be zero. This section describes the specific way in which we do this, and is the key piece of logic that allow the entire system to balance nicely. This is also one of the few places in this system where calculations go beyond simple filtering and aggregation. As we saw earlier, postings are associated with A position, which is a number of units of a lot (which itself may or may not have a cost) An optional conversion price Given this, how do we balance a transaction? Some terminology is in order: for this example posting: Assets:Investment:HOOL -50 HOOL {700 USD} @ 920 USD Units. The number of units is the number of the position, or \u201c50\u201d. Currency. The commodity of the lot, that is, \u201cHOOL\u201d. Cost. The cost of this position is the number of units times the per-unit cost in the cost currency, that is 50 x 700 = 35000 USD. (Total) Price. The price of a unit is the attached price amount, 920 USD. The total price of a position is its number of units times the conversion price, in this example 50 x 920 = 46000 USD. Weight. The amount used to balance each posting in the transaction: If the posting has an associated cost, we calculate the cost of the position (regardless of whether a price is present or not); If the posting has no associated cost but has a conversion price, we convert the position into its total price; Finally, if the posting has no associated cost nor conversion price, the number of units of the lot are used directly. Balancing is really simple: each of the Posting's positions are first converted into their weight. These amounts are then grouped together by currency, and the final sum for each currency is asserted to be close to zero, that is, within a small amount of tolerance (as inferred by a combination of by the numbers in the input and the options ). The following example is one of case (2), with a price conversion and a regular leg with neither a cost nor a price (case 3): 2013-07-22 * \"Wired money to foreign account\" Assets:Investment:HOOL -35350 CAD @ 1.01 USD ;; -35000 USD (2) Assets:Investment:Cash 35000 USD ;; 35000 USD (3) ;;------------------ ;; 0 USD In the next example, the posting for the first leg has a cost, the second posting has neither: 2013-07-22 * \"Bought some investment\" Assets:Investment:HOOL 50 HOOL {700 USD} ;; 35000 USD (1) Assets:Investment:Cash -35000 USD ;; -35000 USD (3) ;;------------------ ;; 0 USD Here's a more complex example with both a price and a cost; the price here is ignored for the purpose of balance and is used only to enter a data-point in the internal price database: 2013-07-22 * \"Sold some investment\" Assets:Investment:HOOL -50 HOOL {700 USD} @ 920 USD ;; -35000 USD (1) Assets:Investment:Cash 46000 USD ;; 46000 USD (3) Income:CapitalGains -11000 USD ;; -11000 USD (3) ;;------------------ ;; 0 USD Finally, here's an example with multiple commodities: 2013-07-05 * \"COMPANY INC PAYROLL\" Assets:US:TD:Checking 4585.38 USD Income:US:Company:GroupTermLife -25.38 USD Income:US:Company:Salary -5000.00 USD Assets:US:Vanguard:Cash 540.00 USD Assets:US:Federal:IRAContrib -540.00 IRAUSD Expenses:Taxes:US:Federal:IRAContrib 540.00 IRAUSD Assets:US:Company:Vacation 4.62 VACHR Income:US:Company:Vacation -4.62 VACHR Here there are three groups of weights to balance: USD: (4485.38) + (-25.38) + (-5000) + (540) ~= 0 USD IRAUSD: (540.00) + (-540.00) ~= 0 IRAUSD VACHR: (4.62) + (-4.62) ~= 0 VACHR Elision of Amounts \uf0c1 Transactions allow for at most one posting to be elided and automatically set; if an amount was elided, the final balance of all the other postings is attributed to the balance. With the inventory booking proposal , it should be extended to be able to handle more cases of elision. An interesting idea would be to allow the elided postings to at least specify the currency they're using. This would allow the user to elide multiple postings, like this: 2013-07-05 * \"COMPANY INC PAYROLL\" Assets:US:TD:Checking USD Income:US:Company:GroupTermLife -25.38 USD Income:US:Company:Salary -5000.00 USD Assets:US:Vanguard:Cash 540.00 USD Assets:US:Federal:IRAContrib -540.00 IRAUSD Expenses:Taxes:US:Federal:IRAContrib IRAUSD Assets:US:Company:Vacation 4.62 VACHR Income:US:Company:Vacation VACHR Stream Processing \uf0c1 An important by-product of representing all state using a single stream of directives is that most of the operations in Beancount can be implemented by simple functions that accept the list of directives as input and output a modified list of directives. For example, The \u201cPad\u201d directive is implemented by processing the stream of transactions, accumulating balances, inserting a new padding transaction after the Pad directive when a balance assertion fails. Summarization operations (open books, close books, clear net income to equity) are all implemented this way, as functional operators on the list of streams. For example, opening the books at a particular date is implemented by summing all balances before a certain date and replacing those transactions by new transactions that pull the final balance from an Equity account. Closing the books only involves moving the balances of income statement account to Equity and truncation the list of transactions to that date. This is very elegant\u2014the reporting code can be oblivious to the fact that summarization has occurred. Prices on postings held with a cost are normally ignored. The \u201cimplicit prices\u201d option is implemented using a plugin which works its magic by processing the stream of operations and inserting otherwise banal Price directives automatically when such a posting is found. Many types of verifications are also implemented this way; the sellgains plugin verifies that non-income balances check against the converted price of postings disregarding the cost. This one does not insert any new directives, but may generate new errors. These are just some examples. I\u2019m aiming to make most operations work this way. This design has proved to be elegant, but also surprisingly flexible and it has given birth to the plugins system available in Beancount; you might be surprised to learn that I had not originally thought to provide a plugins system... it just emerged over time teasing abstractions and trying to avoid state in my program. I am considering experimenting with weaving the errors within the stream of directives by providing a new \u201cError\u201d directive that could be inserted by stream processing functions. I\u2019m not sure whether this would make anything simpler yet, it\u2019s just an idea at this point [July 2015]. Stream Invariants \uf0c1 The stream of directives comes with a few guarantees you can rest upon: All the directives are ordered by date. Because many dates have multiple directives, in order to have a stable sorting order the line number in the file is used as a secondary sort key. All uses of an account is preceded in the stream by a corresponding Open entry. If the input did not provide one, an Open directive is automatically synthesized. All Balance directives precede any other directive on a particular day. This is enforced in order to make the processing of balance assertions straightforward and to emphasize that their semantics is to occur at the beginning of the day. Loader & Processing Order \uf0c1 The process of loading a list of entries from an input file is the heart of the project. It is important to understand it in order to understand how Beancount works. Refer to the diagram below. It consists of A parsing step that reads in all the input files and produces a stream of potentially incomplete directives. The processing of this stream of entries by built-in and user-specified plugins. This step potentially produces new error objects. A final validation step that ensures the invariants have not been broken by the plugins. The beancount.loader module orchestrates this processing. In the code and documents, I\u2019m careful to distinguish between the terms \u201cparsing\u201d and \u201cloading\u201d carefully. These two concepts are distinct. \u201cParsing\u201d is only a part of \u201cloading.\u201d The user-specified plugins are run in the order they are provided in the input files. Raw mode. Some users have expressed a desire for more control over which built-in plugins are run and in which order they are to be run, and so enabling the \u201craw\u201d option disables the automatic loading of all built-in plugins (you have to replace those with explicit \u201cplugin\u201d directives if you want to do that). I\u2019m not sure if this is going to be useful yet, but it allows for tighter control over the loading process for experimentation. Loader Output \uf0c1 The parser and the loader produce three lists: entries: A list of directive tuples from beancount.core.data. This is the stream of data which should consists mostly of Transaction and Balance instances. As much as possible, all data transformations are performed by inserting or removing entries from this list. Throughout the code and documents, I refer to these as entries or directives interchangeably. errors: A list of error objects. In Beancount I don\u2019t use exceptions to report errors; rather, all the functions produce error objects and they are displayed at the most appropriate time. (These deserve a base common type but for now the convention they respect is that they all provide a source (filename, line-no), message and entry attributes.) options_map: A dict of the options provided in the file and derived during parsing. Though this is a mutable object, we never modify it once produced by the parser. Parser Implementation \uf0c1 The Beancount parser is a mix of C and Python 3 code. This is the reason you need to compile anything in the first place. The parser is implemented in C using a flex tokenizer and a Bison parser generator, mainly for performance reasons. I chose the C language and these crufty old GNU tools because they are portable and will work everywhere. There are better parser generators out there, but they would either require my users to install more compiler tools or exotic languages. The C language is a great common denominator. I want Beancount to work everywhere and to be easy to install. I want this to make it easy on others, but I also want this for myself, because at some point I\u2019ll be working on other projects and the less dependencies I have the lighter it will be to maintain aging software. Just looking ahead. (That\u2019s also why I chose to use Python instead of some of the other languages I\u2019m more naturally attracted to these days (Clojure, Go, ML); I need Beancount to work... when I\u2019m counting the beans I don\u2019t have time to debug problems. Python is careful about its changes and it will be around for a long long time as is.) There is a lexer file lexer.l written in flex and a Bison grammar in grammar.y. These tools are used to generate the corresponding C source code (lexer.h/.c and grammar.h/.c). These, along with some hand-written C code that defines some interface functions for the module (parser.h/.c), are compiled into an extension module (_parser.so). Eventually we could consider creating a small dependency rule in setup.py to invoke flex and Bison automatically but at the moment, in order to minimize the installation burden, I check the generated source code in the repository (lexer.h/c and grammar.h/c). The interaction between the Python and C code works like this: You import beancount.parser.parser and invoke either parse_file() or parse_string(). This uses code in grammar.py to create a Builder object which essentially provides callbacks in Python to process grammar rules. parser.py calls a C function in the extension module with the Builder object. That C code sets up flex to be able to read the input file or string then hands over control to the parser by calling the generated C function \u201cyyparse()\u201d. yyparse() is the parser and will fetch input tokens from the lexer C code using successive calls to \u201cyylex()\u201d and attempt to reduce rules. When a rule or token is successfully reduced, the C code invokes callback methods on the Builder object you provided. The parser\u2019s rules communicate between each other by passing around \u201cPyObject*\u201d instances, so that code has to be a little careful about correctly handling reference counting. This allows me to run Python code on rule reduction, which makes it really easy to customize parser behaviour yet maintain the performance of the grammar rule processing in C. Note that the grammar.py Builder derives from a similar Builder class defined in lexer.py, so that lexer tokens recognized calls methods defined in the lexer.py file to create them and parser rule correspondingly calls methods from grammar.py\u2019s Builder. This isolation is not only clean, it also makes it possible to just use the lexer to tokenize a file from Python (without parsing) for testing the tokenizer; see the tests where this is used if you\u2019re curious how this works. I just came up with this; I haven\u2019t seen this done anywhere else. I tried it and the faster parser made a huge difference compared to using PLY so I stuck with that. I quite like the pattern, it\u2019s a good compromise that offers the flexibility of a parser generator yet allows me to handle the rules using Python. Eventually I\u2019d like to move just some of the most important callbacks to C code in order to make this a great deal faster (I haven\u2019t done any real performance optimizations yet). This has worked well so far but for one thing: There are several limitations inherent in the flex tokenizer that have proved problematic. In particular, in order to recognize transaction postings using indent I have had to tokenize the whitespace that occurs at the beginning of a line. Also, single-characters in the input should be parsed as flags and at the moment this is limited to a small subset of characters. I\u2019d like to eventually write a custom lexer with some lookahead capabilities to better deal with these problems (this is easy to do). Two Stages of Parsing: Incomplete Entries \uf0c1 At the moment, the parser produces transactions which may or may not balance. The validation stage which runs after the plugins carries out the balance assertions. This degree of freedom is allowed to provide the opportunity for users to enter incomplete lists of postings and for corresponding plugins to automate some data entry and insert completing postings. However, the postings on the transactions are always complete objects, with all their expected attributes set. For example, a posting which has its amount elided in the input syntax will have a filled in position by the time it comes out of the parser. We will have to loosen this property when we implement the inventory booking proposal , because we want to support the elision of numbers whose values depend on the accumulated state of previous transactions. Here is an obvious example. A posting like this 2015-04-03 * \"Sell stock\" Assets:Investments:AAPL -10 AAPL {} ... should succeed if at the beginning of April 3rd the account contains exactly 10 units of AAPL, in either a single or multiple lots. There is no ambiguity: \u201csell all the AAPL,\u201d this says. However, the fact of its unambiguity assumes that we\u2019ve computed the inventory balance of that account up until April 3rd\u2026 So the parser will need to be split into two phases: A simple parsing step that produces potentially incomplete entries with missing amounts A separate step for the interpolation which will have available the inventory balances of each account as inputs. This second step is where the booking algorithms (e.g., FIFO) will be invoked from. See the diagram above for reference. Once implemented, everything else should be the same. The Printer \uf0c1 In the same package as the parser lives a printer. This isolates all the functionality that deals with the Beancount \u201clanguage\u201d in the beancount.parser package: the parser converts from input syntax to data structure and the printer does the reverse. No code outside this package should concern itself with the Beancount syntax. At some point I decided to make sure that the printer was able to round-trip through the parser, that is, given a stream of entries produced by the loader, you should be able to convert those to text input and parse them back in and the resulting set of entries should be the same (outputting the re-read input back to text should produce the same text), e.g., Note that the reverse isn\u2019t necessarily true: reading an input file and processing it through the loader potentially synthesizes a lot of entries (thanks to the plugins), so printing it back may not produce the same input file (even ignoring reordering and white space concerns). This is a nice property to have. Among other things, it allows us to use the parser to easily create tests with expected outputs. While there is a test that attempts to protect this property, some of the recent changes break it partially: Metadata round-tripping hasn\u2019t been tested super well. In particular, some of the metadata fields need to be ignored (e.g., filename and lineno). Some directives contain derived data, e.g. the Balance directive has a \u201cdiff_amount\u201d field that contains the difference when there is a failure in asserting a balance. This is used to report errors more easily. I probably need to remove these exceptions at some point because it is the only one of its kind (I could replace it with an inserted \u201cError\u201d directive). This probably needs to get refined a bit at some point with a more complete test (it\u2019s not far as it is). Uniqueness & Hashing \uf0c1 In order to make it possible to compare directives quickly, we support unique hashing of all directives, that is, from each directive we should be able to produce a short and unique id. We can then use these ids to perform set inclusion/exclusion/comparison tests for our unit tests. We provide a base test case class with assertion methods that use this capability . This feature is used liberally throughout our test suites. This is also used to detect and remove duplicates. This feature is optional and enabled by the beancount.plugins.noduplicates plugin. Note that the hashing of directives currently does not include user meta-data . Display Context \uf0c1 Number are input with different numbers of fractional digits: 500 -> 0 fractional digits 520.23 -> 2 fractional digits 1.2357 -> 4 fractional digits 31.462 -> 3 fractional digits Often, a number used for the same currency is input with a different number of digits. Given this variation in the input, a question that arises is how to render the numbers consistently in reports? Consider that: We would like that the user not to have to specify the number of fractional digits to use for rendering by default. Rendering numbers may differ depending on the context: most of the time we want to render numbers using their most commonly seen number of digits, rounding if necessary, but when we are rendering conversion rates we would like to render numbers using the maximum number of digits ever seen. The number of digits used tends to vary with the commodity they represent. For example, US dollars will usually render with two digits of precision; the number of units of a mutual fund such as RGAGX might be recorded by your broker with 3 digits of precision. Japanese Yen will usually be specified in integer units. We want to render text report which need to align numbers with varying number of fractional digits together, aligning them by their period or rightmost digits, and possibly rendering a currency thereafter. In order to deal with this thorny problem, I built a kind of accumulator which is used to record all numbers seen from some input and tally statistics about the precisions witnessed for each currency. I call this a DisplayContext . From this object one can request to build a DisplayFormatter object which can be used to render numbers in a particular way. I refer to those objects using the variable names dcontext and dformat throughout the code. The parser automatically creates a DisplayContext object and feeds it all the numbers it sees during parsing. The object is available in the options_map produced by the loader. Realization \uf0c1 It occurs often that one needs to compute the final balances of a list of filtered transactions, and to report on them in a hierarchical account structure. See diagram below. \uf0c1 For the purpose of creating hierarchical renderings, I provide a process called a \u201c realization .\u201d A realization is a tree of nodes, each of which contains An account name, A list of postings and other entries to that account, and The final balance for that account, A mapping of sub-account names to child nodes. The nodes are of type \u201cRealAccount,\u201d which is also a dict of its children. All variables of type RealAccount in the codebase are by convention prefixed by \u201creal_*\u201d. The realization module provides functions to iterate and produce hierarchical reports on these trees of balances, and the reporting routines all use these. For example, here is a bit of code that will dump a tree of balances of your accounts to the console: import sys from beancount import loader from beancount.core import realization entries, errors, options_map = loader.load_file(\"filename.beancount\") real_root = realization.realize(entries) realization.dump_balances(real_root, file=sys.stdout) The Web Interface \uf0c1 Before the appearance of the SQL syntax to filter and aggregate postings, the only report produced by Beancount was the web interface provided by bean-web. As such, bean-web evolved to be good enough for most usage and general reports. Reports vs. Web \uf0c1 One of the important characteristics of bean-web is that it should just be a thin dispatching shell that serves reports generated from the beancount.reports layer. It used to contain the report rendering code itself, but at some point I began to factor out all the reporting code to a separate package in order to produce reports to other formats, such as text reports and output to CSV. This is mostly finished, but at this time [July 2015] some reports remain that only support HTML output. This is why. Client-Side JavaScript \uf0c1 I would like to eventually include a lot more client-side scripting in the web interface. However, I don\u2019t think I\u2019ll be able to work on this for a while, at least not until all the proposals for changes to the core are complete (e.g., inventory booking improvements, settlement splitting and merging, etc.). If you\u2019d like to contribute to Beancount, improving bean-web or creating your own visualizations would be a great venue. The Query Interface \uf0c1 The current query interface is the result of a prototype . It has not yet been subjected to the amount of testing and refinement as the rest of the codebase. I\u2019ve been experimenting with it and have a lot of ideas for improving the SQL language and the kinds of outputs that can be produced from it. I think of it as \u201c70% done\u201d. However, it works, and though some of the reports can be a little clunky to specify, it produces useful results. It will be the subject of a complete rewrite at some point and when I do, I will keep the current implementation for a little while so that existing scripts don\u2019t just break; I\u2019ll implement a v2 of the shell. Design Principles \uf0c1 Minimize Configurability \uf0c1 First, Beancount should have as few options as possible. Second, command-line programs should have no options that pertain to the processing and semantic of the input file (options that affect the output or that pertain to the script itself are fine). The goal is to avoid feature creep. Just a few options opens the possibility of them interacting in complex ways and raises a lot of questions. In this project, instead of providing options to customize every possible thing, I choose to bias the design towards providing the best behavior by default, even if that means less behavior. This is the Apple approach to design. I don\u2019t usually favor this approach for my projects, but I chose it for this particular one. What I\u2019ve learned in the process is how far you can get and still provide much of the functionality you originally set out for. Too many command-line options makes a program difficult to use. I think Ledger suffers from this, for instance. I can never remember options that I don\u2019t use regularly, so I prefer to just design without them. Options that affect semantics should live in the file itself (and should be minimized as well). Options provided when running a process should be only to affect this process. Having less options also makes the software much easier to refactor. The main obstacle to evolving a library of software is the number of complex interactions between the codes. Guaranteeing a well-defined set of invariants makes it much easier to later on split up functionality or group it differently. So.. by default I will resist making changes that aren't generic or that would not work for most users. On the other hand, large-scale changes that would generalize well and that require little configurability are more likely to see implementation. Favor Code over DSLs \uf0c1 Beancount provides a simple syntax, a parser and printer for it, and a library of very simple data record types to allow a user to easily write their scripts to process their data, taking advantage of the multitude of libraries available from the Python environment. Should the built-in querying tools fail to suffice, I want it to be trivially easy for someone to build what they need on top of Beancount. This also means that the tools provided by Beancount don\u2019t have to support all the features everyone might ever possibly want. Beancount\u2019s strength should be representational coherence and simplicity rather than the richness of its reporting features. Creating new kinds of reports should be easy; changing the internals should be hard. (That said, it does provide an evolving palette of querying utilities that should be sufficient for most users.) In contrast, the implementation of the Ledger system provides high-level operations that are invoked as \"expressions\" defined in a domain-specific language, expressions which are either provided on the command-line or in the input file itself. For the user, this expression language is yet another thing to learn, and it\u2019s yet another thing that might involve limitations requiring expansion and work\u2026 I prefer to avoid DSLs if possible and use Python\u2019s well understood semantics instead, though this is not always sensible. File Format or Input Language? \uf0c1 One may wonder whether Beancount\u2019s input syntax should be a computer language or just a data format. The problem we're trying to solve is essentially that of making it easy for a human being to create a transactions-to-postings-to-accounts-&-positions data structure. What is the difference between a data file format and a simple declarative computer language? One distinction is the intended writer of the file. Is it a human? Or is it a computer? The input files are meant to be manicured by a human, at the very least briefly eyeballed. While we are trying to automate as much of this process as possible via importing code\u2014well, the unpleasant bits, anyway\u2014we do want to ensure that we review all the new transactions that we're adding to the ledger in order to ensure correctness. If the intended writer is a human one could file the input under the computer language rubric (most data formats are designed to be written by computers). We can also judge it by the power of its semantics. Beancount\u2019s language is not one designed to be used to perform computation, that is, you cannot define new \u201cBeancount functions\u201d in it. But it has data types. In this regard, it is more like a file format. Just something to think about, especially in the context of adding new semantics. Grammar via Parser Generator \uf0c1 The grammar of its language should be compatible with the use of commonly used parser generator tools. It should be possible to parse the language with a simple lexer and some grammar input file. Beancount\u2019s original syntax was modeled after the syntax of Ledger. However, in its attempt to make the amount of markup minimal, that syntax is difficult to parse with regular tools. By making simple changes to the grammar, e.g. adding tokens for strings and accounts and commodities, the Beancount v2 reimplementation made it possible to use a flex/bison parser generator. This has important advantages: It makes it really easy to make incremental changes to the grammar. Using a custom parser without a well-defined grammar can make it quite difficult to prototype syntax changes. It makes it much easier to implement a parser in another computer language. I\u2019d like to eventually be able to parse Beancount input files in other languages. The data structures are simple enough that it should be easy to reimplement the core in a different language. It\u2019s just a lot less code to write and maintain. (Eventually I\u2019d like to create a file format to generate parsers in multiple languages from a single input. That will be done later, once all the major features are implemented.) Future Work \uf0c1 Tagged Strings \uf0c1 At the moment, account names, tags, links and commodities are represented as simple Python strings. I\u2019ve tried to keep things simple. At some point I\u2019d like to refine this a bit and create a specialization of strings for each of these types. I\u2019d need to assess the performance impact of doing that beforehand. I\u2019m not entirely sure how it could benefit functionality yet. Errors Cleanup \uf0c1 I\u2019ve been thinking about removing the separate \u201cerrors\u201d list and integrating it into the stream of entries as automatically produced \u201cError\u201d entries. This has the advantage that the errors could be rendered as part of the rest of the stream, but it means we have to process the whole list of entries any time we need to print and find errors (probably not a big deal, given how rarely we output errors). Conclusion \uf0c1 This document is bound to evolve. If you have questions about the internals, please post them to the mailing-list . References \uf0c1 Nothing in Beancount is inspired from the following docs, but you may find them interesting as well: Accounting for Computer Scientists I have been considering the addition of a very constrained form of non-balancing postings that would preserve the property of transactions otherwise being balanced, whereby the extra postings would not be able to interact (or sum with) regular balancing postings. \u21a9 There is an option called operating_currency but it is only used to provide good defaults for building reports, never in the processing of transactions. It is used to tell the reporting code which commodities should be broken out to have their own columns of balances, for example. \u21a9 In previous version of Beancount this was not true, the Posting used to have a \u2018position\u2019 attribute and composed it. I prefer the flattened design, as many of the functions apply to either a Position or a Posting. Think of a Posting as deriving from a Position, though, technically it does not. \u21a9","title":"Beancount Design Doc"},{"location":"beancount_design_doc.html#beancount-design-doc","text":"Martin Blais ( blais@furius.ca ) http://furius.ca/beancount/doc/design-doc A guide for developers to understand Beancount\u2019s internals. I hope for this document to provide a map of the main objects used in the source code to make it easy to write scripts and plugins on top of Beancount or even extend it. Introduction Invariants Isolation of Inputs Order-Independence All Transactions Must Balance Account Have Types Account Lifetimes & Open Directives Supports Dates Only (and No Time) Metadata is for User Data Overview of the Codebase Core Data Structures Number Commodity Amount Lot Position Inventory Account Flag Posting About Tuples & Mutability Summary Directives Common Properties Transactions Flag Payee & Narration Tags Links Postings Balancing Postings Elision of Amounts Stream Processing Stream Invariants Loader & Processing Order Loader Output Parser Implementation Two Stages of Parsing: Incomplete Entries The Printer Uniqueness & Hashing Display Context Realization The Web Interface Reports vs. Web Client-Side JavaScript The Query Interface Design Principles Minimize Configurability Favor Code over DSLs File Format or Input Language? Grammar via Parser Generator Future Work Tagged Strings Errors Cleanup Types Conclusion","title":"Beancount Design Doc"},{"location":"beancount_design_doc.html#introduction","text":"This document describes the principles behind the design of Beancount and a high-level overview of its codebase, data structures, algorithms, implementation and methodology. This is not a user's manual; if you are interested in just using Beancount, see the associated User's Manual and all the other documents available here . However, if you just want to understand more deeply how Beancount works this document should be very helpful. This should also be of interest to developers. This is a place where I wrote about a lot of the ideas behind Beancount that did not find any other venue. Expect some random thoughts that aren\u2019t important for users. Normally one writes a design document before writing the software. I did not do that. But I figured it would still be worthwhile to spell out some of the ideas that led to this design here. I hope someone finds this useful.","title":"Introduction"},{"location":"beancount_design_doc.html#invariants","text":"","title":"Invariants"},{"location":"beancount_design_doc.html#isolation-of-inputs","text":"Beancount should accept input only from the text file you provide to its tools. In particular, it should not contact any external networked service or open any \u201cglobal\u201d files, even just a cache of historical prices or otherwise. This isolation is by design. Isolating the input to a single source makes it easier to debug, reproduce and isolate problems when they occur. This is a nice property for the tool. In addition, fetching and converting external data is very messy. External data formats can be notoriously bad, and they are too numerous to handle all of them (handling just the most common subset would beg the question of where to include the implementation of new converters). Most importantly, the problems of representing the data vs. that of fetching and converting it segment from each other very well naturally: Beancount provides a core which allows you to ingest all the transactional data and derive reports from it, and its syntax is the hinge that connects it to these external repositories of transactions or prices. It isolates itself from the ugly details of external sources of data in this way.","title":"Isolation of Inputs"},{"location":"beancount_design_doc.html#order-independence","text":"Beancount offers a guarantee that the ordering of its directives in an input file is irrelevant to the outcome of its computations. You should be able to organize your input text and reorder any declaration as is most convenient for you, without having to worry about how the software will make its calculations. This also makes it trivial to implement inclusions of multiple files: you can just concatenate the files if you want, and you can process them in any order. Not even directives that declare accounts or commodities are required to appear before these accounts get used in the file. All directives are parsed and then basically sorted before any calculation or validation occurs (a minor detail is that the line number is used as a secondary key in the sorting, so that the re-ordering is stable). Correspondingly, all the non-directive grammar that is in the language is limited to either setting values with global impact (\u201c option \u201d, \u201c plugin \u201d) or syntax-related conveniences (\u201c pushtag \u201d, \u201c poptag \u201d). There is an exception: Options may have effects during the processing of the file and should appear at the beginning. At the moment, Beancount does not enforce this, e.g., by performing multiple passes of parsing, but it probably should. What\u2019s more, options in included files are currently ignored. You should put all your options in the top-level ledger file.","title":"Order-Independence"},{"location":"beancount_design_doc.html#all-transactions-must-balance","text":"All transactions must balance by \u201cweight.\u201d There is no exception. Beancount transactions are required to have balanced, period. In particular, there is no allowance for exceptions such as the \u201cvirtual postings\u201d that can be seen in Ledger. The first implementation of Beancount allowed such postings. As a result, I often used them to \u201cresolve\u201d issues that I did not know how to model well otherwise. When I rewrote Beancount, I set out to convert my entire input file to avoid using these, and over time I succeeded in doing this and learned a lot of new things in the process. I have since become convinced that virtual postings are wholly unnecessary, and that making them available only provides a crutch upon which a lot of users will latch instead of learning to model transfers using balanced transactions. I have yet to come across a sufficiently convincing case for them 1 . This provides the property that any subsets of transactions will sum up to zero. This is a nice property to have, it means we can generate balance sheets at any point in time. Even when we eventually support settlement dates or per-posting dates , we will split up transactions in a way that does not break this invariant.","title":"All Transactions Must Balance"},{"location":"beancount_design_doc.html#accounts-have-types","text":"Beancount accounts should be constrained to be of one of five types: Assets, Liabilities, Income, Expenses and Equity. The rationale behind this is to realize that all matters of counting can be characterized by being either permanent (Assets, Liabilities) or transitory (Income, Expenses), and that the great majority of accounts have a usual balance sign: positive (Assets, Expenses) or negative (Liabilities, Income). Given a quantity to accumulate, we can always select one of these four types of labels for it. Enforcing this makes it possible to implement reports of accumulated values for permanent accounts (balance sheet) and reports of changes of periods of time (income statement). Although it is technically possible to explicitly book postings against Equity accounts the usual purpose of items in that category is to account for past changes on the balance sheet (net/retained income or opening balances). These concepts of time and sign generalize beyond that of traditional accounting. Not having them raises difficult and unnecessary questions about how to handle calculating these types of reports. We simply require that you label your accounts into this model. I\u2019ll admit that this requires a bit of practice and forethought, but the end result is a structure that easily allows us to build commonly expected outputs.","title":"Accounts Have Types"},{"location":"beancount_design_doc.html#account-lifetimes-open-directives","text":"Accounts have lifetimes; an account opens at a particular date and optionally closes at another. This is directed by the Open and Close directives. All accounts are required to have a corresponding Open directive in the stream. By default, this is enforced by spitting out an error when posting to an account whose Open directive hasn\u2019t been seen in the stream. You can automate the generation of these directives by using the \u201cauto_accounts\u201d plugin, but in any case, the stream of entries will always contain the Open directives. This is an assumption that one should be able to rely upon when writing scripts. (Eventually a similar constraint will be applied for Commodity directives so that the stream always includes one before it is used; and they should be auto-generated as well. This is not the case right now [June 2015].)","title":"Account Lifetimes & Open Directives"},{"location":"beancount_design_doc.html#supports-dates-only-and-no-time","text":"Beancount does not represent time, only dates. The minimal time interval is one day. This is for simplicity\u2019s sake. The situations where time would be necessary to disambiguate account balances exist, but in practice they are rare enough that their presence does not justify the added complexity of handling time values and providing syntax to deal with it. If you have a use case whereby times are required, you may use metadata to add it in, or more likely, you should probably write custom software for it. This is outside the scope of Beancount by choice.","title":"Supports Dates Only (and No Time)"},{"location":"beancount_design_doc.html#metadata-is-for-user-data","text":"By default, Beancount will not make assumptions about metadata fields and values. Metadata is reserved for private usage by the user and for plugins. The Beancount core does not read metadata and change its processing based on it. However, plugins may define special metadata values that affect what they produce. That being said, there are a few special metadata fields produced by the Beancount processing: filename: A string, the name of the file the transaction was read from (or the plugin that created it) lineno: An integer, the line number from the file where the transaction was read from. tolerances : A dict of currency to Decimal, the tolerance values used in balancing the transaction. residual (on postings): A boolean, true if the posting has been added to automatically absorb rounding error. There may be a few more produced in the future but in any case, the core should not read any metadata and affect its behavior as a consequence. (I should probably create a central registry or location where all the special values can be found in one place.)","title":"Metadata is for User Data"},{"location":"beancount_design_doc.html#overview-of-the-codebase","text":"All source code lives under a \u201c beancount \u201d Python package. It consists of several packages with well-defined roles, the dependencies between which are enforced strictly. Beancount source code packages and approximate dependencies between them. At the bottom, we have a beancount.core package which contains the data structures used to represent transactions and other directives in memory. This contains code for accounts, amounts, lots, positions, and inventories. It also contains commonly used routines for processing streams of directives. One level up, the beancount.parser package contains code to parse the Beancount language and a printer that can produce the corresponding text given a stream of directives. It should be possible to convert between text and data structure and round-trip this process without loss. At the root of the main package is a beancount.loader module that coordinates everything that has to be done to load a file in memory. This is the main entry point for anyone writing a script for Beancount, it always starts with loading a stream of entries. This calls the parser, runs some processing code, imports the plugins and runs them in order to produce the final stream of directives which is used to produce reports. The beancount.plugins package contains implementations of various plugins. Each of these plugin modules are independent from each other. Consult the docstrings in the source code to figure out what each of these does. Some of these are prototypes of ideas. There is a beancount.ops package which contains some high-level processing code and some of the default code that always runs by default that is implemented as plugins as well (like padding using the Pad directive). This package needs to be shuffled a bit in order to clarify its role. On top of this, reporting code calls modules from those packages. There are four packages which contain reporting code, corresponding to the Beancount reporting tools: beancount.reports : This package contains code specialized to produce different kinds of outputs (invoked by bean-report). In general I\u2019d like to avoid defining custom routines to produce desired outputs and use the SQL query tool to express grouping & aggregation instead, but I think there will always be a need for at least some of these. The reports are defined in a class hierarchy with a common interface and you should be able to extend them. Each report supports some subset of a number of output formats. beancount.query : This package contains the implementation of a query language and interactive shell (invoked by bean-query) that allows you to group and aggregate positions using an SQL-like DSL. This essentially implements processing over an in-memory table of Posting objects, with functions defined over Amount, Position and Inventory objects. beancount.web : This package contains all the source code for the default web interface (invoked by bean-web). This is a simple Bottle application that serves many of the reports from beancount.report to HTML format, running on your machine locally. The web interface provides simple views and access to your data. (It stands to be improved greatly, it\u2019s in no way perfect.) beancount.projects : This package contains various custom scripts for particular applications that I\u2019ve wanted to share and distribute. Wherever possible, I aim to fold these into reports. There are no scripts to invoke these; you should use \u201c python3 -m beancount.projects. \u201d to run them. There is a beancount.scripts package that contains the \u201cmain\u201d programs for all the scripts under the bin directory . Those scripts are simple launchers that import the corresponding file under beancount.scripts. This allows us to keep all the source code under a single directory and that makes it easier to run linters and other code health checkers on the code\u2014it\u2019s all in one place. Finally, there is a beancount.utils package which is there to contain generic reusable random bits of utility code. And there is a relatively unimportant beancount.docs package that contains code used by the author just to produce and maintain this documentation (code that connects to Google Drive). Enforcing the dependency relationships between those packages is done by a custom script .","title":"Overview of the Codebase"},{"location":"beancount_design_doc.html#core-data-structures","text":"This section describes the basic data structures that are used as building blocks to represent directives. Where possible I will describe the data structure in conceptual terms. (Note for Ledger users : This section introduces some terminology for Beancount; look here if you\u2019re interested to contrast it with concepts and terminology found in Ledger.)","title":"Core Data Structures"},{"location":"beancount_design_doc.html#number","text":"Numbers are represented using decimal objects, which are perfectly suited for this. The great majority of numbers entered in accounting systems are naturally decimal numbers and binary representations involve representational errors which cause many problems for display and in precision. Rational numbers avoid this problem, but they do not carry the limited amount of precision that the user intended in the input. We must deal with tolerances explicitly. Therefore, all numbers should use Python\u2019s \u201c decimal.Decimal \u201d objects. Conveniently, Python v3.x supports a C implementation of decimal types natively (in its standard library; this used to be an external \u201ccdecimal\u201d package to install but it has been integrated in the C/Python interpreter). The default constructor for decimal objects does not support some of the input syntax we want to allow, such as commas in the integer part of numbers (e.g., \u201c278 , 401.35 USD\u201d) or initialization from an empty string. These are important cases to handle. So I provide a special constructor to accommodate these inputs: beancount.core.number.D() . This is the only method that should be used to create decimal objects: from beancount.core.number import D \u2026 number = D(\"2,402.21\") I like to import the \u201c D \u201d symbol by itself (and not use number.D ). All the number-related code is located under beancount.core.number . Some number constants have been defined as well: ZERO , ONE , and HALF . Use those instead of explicitly constructed numbers (such as D(\"1\") ) as it makes it easier to grep for such initializations.","title":"Number"},{"location":"beancount_design_doc.html#commodity","text":"A commodity , or currency (I use both names interchangeably in the code and documentation) is a string that represents a kind of \u201cthing\u201d that can be stored in accounts. In the implementation, it is represented as a Python str object (there is no module with currency manipulation code). These strings may only contain capital letters and numbers and some special characters (see the lexer code). Beancount does not predefine any currency names or categories\u2014all currencies are created equal. Really. It genuinely does not know anything special about dollars or euros or yen or anything else. The only place in the source code where there is a reference to those is in the tests. There is no support for syntax using \u201c$\u201d or other currency symbols; I understand that some users may want this syntax, but in the name of keeping the parser very simple and consistent I choose not to provide that option. Moreover, Beancount does not make a distinction between commodities which represent \"money\" and others that do not (such as shares, hours, etc.). These are treated the same throughout the system. It also does not have the concept of a \"home\" currency 2 ; it's a genuinely multi-currency system. Currencies need not be defined explicitly in the input file format; you can just start using them in the file and they will be recognized as such by their unique syntax (the lexer recognizes and emits currency tokens). However, you may declare some using a Commodity directive. This is only provided so that a per-commodity entity exists upon which the user can attach some metadata, and some reports and plugins are able to find and use that metadata.","title":"Commodity"},{"location":"beancount_design_doc.html#account","text":"An account is basically just the name of a bucket associated with a posting and is represented as a simple string (a Python str object). Accounts are detected and tokenized by the lexer and have names with at least two words separated by a colon (\":\"). Accounts don\u2019t have a corresponding object type; we just refer to them by their unique name string (like filenames in Python). When per-account attributes are needed, we can extract the Open directives from the stream of entries and find the one corresponding to the particular account we\u2019re looking for. Similar to Python\u2019s os.path module, there are some routines to manipulate account names in beancount.core.account and the functions are named similarly to those of the os.path module. The first component of an account\u2019s name is limited to one of five types: Assets Liabilities Equity Income Expenses The names of the account types as read in the input syntax may be customized with some \"option\" directives, so that you can change those names to another language, or even just rename \"Income\" to \"Revenue\" if you prefer that. The beancount.core.account_types module contains some helpers to deal with these. Note that the set of account names forms an implicit hierarchy. For example, the names: Assets:US:TDBank:Checking Assets:US:TDBank:Savings implicitly defines a tree of nodes with parent nodes \"Assets\", \"US\", \"TDBank\" with two leaf nodes \"Checking\" and \"Savings\". This implicit tree is never realized during processing, but there is a Python module that allows one to do this easily (see beancount.core.realization ) and create linearized renderings of the tree.","title":"Account"},{"location":"beancount_design_doc.html#flag","text":"A \u201cflag\u201d is a single-character string that may be associated with Transactions and Postings to indicate whether they are assumed to be correct (\"reconciled\") or flagged as suspicious. The typical value used on transaction instances is the character \u201c*\u201d. On Postings, it is usually left absent (and set to a None).","title":"Flag"},{"location":"beancount_design_doc.html#amount","text":"An Amount is the combination of a number and an associated currency, conceptually: Amount = (Number, Currency) Amount instances are used to represent a quantity of a particular currency (the \u201cunits\u201d) and the price on a posting. A class is defined in beancount.core.amount as a simple tuple-like object. Functions exist to perform simple math operations directly on instances of Amount. You can also create Amount instance using amount.from_string() , for example: value = amount.from_string(\"201.32 USD\")","title":"Amount"},{"location":"beancount_design_doc.html#cost","text":"A Cost object represents the cost basis for a particular lot of a commodity. Conceptually, it is Cost = (Number, Currency, Date, Label) The number and currency is that of the cost itself, not of the commodity. For example, if you bought 40 shares of AAPL stock at 56.78 USD, the Number is a \u201c56.78\u201d decimal and the Currency is \u201cUSD.\u201d For example: Cost(Decimal(\"56.78\"), \"USD\", date(2012, 3, 5), \"lot15\") The Date is the acquisition date of the corresponding lot (a datetime.date object). This is automatically attached to the Cost object when a posting augments an inventory\u2014the Transaction\u2019s date is automatically attached to the Cost object\u2014or if the input syntax provides an explicit date override. The Label can be any string. It is provided as a convenience for a user to refer to a particular lot or disambiguate similar lots. On a Cost object, the number, currency and date attributes are always set. If the label is unset, it has a value of \u201cNone.\u201d","title":"Cost"},{"location":"beancount_design_doc.html#costspec","text":"In the input syntax, we allow users to provide as little information as necessary in order to disambiguate between the lots contained in the inventory prior to posting. The data provided filters the set of matching lots to an unambiguous choice, or to a subset from which an automated booking algorithm will apply (e.g., \u201cFIFO\u201d). In addition, we allow the user to provide either the per-unit cost and/or the total-cost. This convenience is useful to let Beancount automatically compute the per-unit cost from a total of proceeds. CostSpec = (Number-per-unit, Number-total, Currency, Date, Label, Merge) Since any of the input elements may be omitted, any of the attributes of a CostSpec may be left to None. If a number is missing and necessary to be filled in, the special value \u201cMISSING\u201d will be set on it. The Merge attribute it used to record a user request to merge all the input lots before applying the transaction and to merge them after. This is the method used for all transactions posted to an account with the \u201cAVERAGE\u201d booking method.","title":"CostSpec"},{"location":"beancount_design_doc.html#position","text":"A position represents some units of a particular commodity held at cost. It consists simply in Position = (Units, Cost) Units is an instance of Amount , and Cost is an instance of Cost , or a null value if the commodity is not held at cost. Inventories contain lists of Position instances. See its definition in beancount.core.position .","title":"Position"},{"location":"beancount_design_doc.html#posting","text":"Each Transaction directive is composed of multiple Postings (I often informally refer to these in the code and on the mailing-list as the \u201clegs\u201d of a transaction). Each of these postings is associated with an account, a position and an optional price and flag: Posting = (Account, Units, Cost-or-CostSpec, Price, Flag, Metadata) As you can see, a Posting embeds its Position instance 3 . The Units is an Amount , and the \u2018cost\u2019 attribute refers to either a Cost or a CostSpec instance (the parser outputs Posting instances with an CostSpec attribute which is resolved to a Cost instance by the booking process; see How Inventories Work for details). The Price is an instance of Amount or a null value. It is used to declare a currency conversion for balancing the transaction, or the current price of a position held at cost. It is the Amount that appears next to a \u201c@\u201d in the input. Flags on postings are relatively rare; users will normally find it sufficient to flag an entire transaction instead of a specific posting. The flag is usually left to None; if set, it is a single-character string. The Posting type is defined in beancount.core.data , along with all the directive types.","title":"Posting"},{"location":"beancount_design_doc.html#inventory","text":"An Inventory is a container for an account\u2019s balance in various lots of commodities. It is essentially a list of Position instances with suitable operations defined on it. Conceptually you may think of it as a mapping with unique keys: Inventory = [Position1, Position2, Position3, \u2026 , PositionN] Generally, the combination of a position\u2019s ( Units.Currency, Cost) is kept unique in the list, like the key of a mapping. Positions for equal values of currency and cost are merged together by summing up their Units.Number and keeping a single position for it. And simple positions are mixed in the list with positions held at cost. The Inventory is one of the most important and oft-used object in Beancount\u2019s implementation, because it is used to sum the balance of one or more accounts over time. It is also the place where the inventory reduction algorithms get applied to, and traces of that mechanism can be found there. The \u201c How Inventories Work \u201d document provides the full detail of that process. For testing, you can create initialized instances of Inventory using inventory.from_string() . All the inventory code is written in beancount.core.inventory .","title":"Inventory"},{"location":"beancount_design_doc.html#about-tuples-mutability","text":"Despite the program being written in a language which does not make mutability \u201cdifficult by default\u201d, I designed the software to avoid mutability in most places. Python provides a \u201c collections.namedtuple \u201d factory that makes up new record types whose attributes cannot be overridden. Well\u2026 this is only partially true: mutable attributes of immutable tuples can be modified. Python does not provide very strong mechanisms to enforce this property. Regardless, functional programming is not so much an attribute of the language used to implement our programs than of the guarantees we build into its structure. A language that supports strong guarantees helps to enforce this. But if, even by just using a set of conventions, we manage to mostly avoid mutability, we have a mostly functional program that avoids most of the pitfalls that occur from unpredictable mutations and our code is that much easier to maintain. Programs with no particular regard for where mutation occurs are most difficult to maintain. By avoiding most of the mutation in a functional approach, we avoid most of those problems. Most of the data structures used in Beancount are namedtuples , which make the modification of their attributes inconvenient on purpose. Most of the code will attempt to avoid mutation, except for local state (within a function) or in a narrow scope that we can easily wrap our head around. Where mutation is possible, by convention I try to avoid it by default. When we do mutation, I try to document the effects. I avoid the creation of classes which mutate their internal state, except for a few cases (e.g. the web application). I prefer functions to objects and where I define classes I mostly avoid inheritance. These properties are especially true for all the small objects: Amount, Lot, Position, Posting objects, and all the directives types from beancount.core.data . On the other hand, the Inventory object is nearly always used as an accumulator and does allow the modification of its internal state (it would require a special, persistent data structure to avoid this). You have to be careful how you share access to Inventory objects\u2026 and modify them, if you ever do. Finally, the loader produces lists of directives which are all simple namedtuple objects. These lists form the main application state. I\u2019ve avoided placing these inside some special container and instead pass them around explicitly, on purpose. Instead of having some sort of big \u201capplication\u201d object, I\u2019ve trimmed down all the fat and all your state is represented in two things: A dated and sorted list of directives which can be the subject of stream processing and a list of constant read-only option values. I think this is simpler. I credit my ability to make wide-ranging changes to a mid-size Python codebase to the adoption of these principles. I would love to have types in order to safeguard against another class of potential errors, and I plan to experiment with Python 3.5\u2019s upcoming typing module .","title":"About Tuples & Mutability"},{"location":"beancount_design_doc.html#summary","text":"The following diagram explains how these objects relate to each other, starting from a Posting. For example, to access the number of units of a postings you could use posting.units.number For the cost currency: posting.cost.currency You can print out the tuples in Python to figure out their structure.","title":"Summary"},{"location":"beancount_design_doc.html#previous-design","text":"For the sake of preservation, if you go back in time in the repository, the structure of postings was deeper and more complex. The new design reflects a flatter and simpler version of it. Here is what the old design used to look like:","title":"Previous Design"},{"location":"beancount_design_doc.html#directives","text":"The main production from loading a Beancount input file is a list of directives . I also call these entries interchangeably in the codebase and in the documents. There are directives of various types: Transaction Balance & Pad Open & Close Commodity Note Event Price Document In a typical Beancount input file, the great majority of entries will be Transactions and some Balance assertions. There will also be Open & perhaps some Close entries. Everything else is optional. Since these combined with a map of option values form the entire application state, you should be able to feed those entries to functions that will produce reports. The system is built around this idea of processing a stream of directives to extract all contents and produce reports, which are essentially different flavors of filtering and aggregations of values attached to this stream of directives.","title":"Directives"},{"location":"beancount_design_doc.html#common-properties","text":"Some properties are in common to all the directives: Date. A datetime.date object. This is useful and consistent. For a Transaction, that\u2019s the date at which it occurs. For an Open or Close directive, that\u2019s the date at which the account was opened or closed. For a Pad directive, that\u2019s the date at which to insert a transaction. For a Note or Document, it is the date at which to insert the note in the stream of postings of that account. For an Event, it is the date at which it occurs. For a Commodity directive which essentially provides a per-commodity hanging point for commodity-related metadata, the date isn\u2019t as meaningful; I choose to date those on the day the commodity was created. Meta . All the directives have metadata attribute (a Python dict object). The purpose of metadata is to allow the user to hang any kind of ancillary data they want and then use this in scripts or queries. Posting instances also have a metadata attribute.","title":"Common Properties"},{"location":"beancount_design_doc.html#transactions","text":"The Transaction directive is the subject of Beancount and is by far the most common directive found in input files and is worth of special attention. The function of a bookkeeping system is to organize the Postings attached to Transactions in various ways. All the other types of entries occupy supporting roles in our system. A Transaction has the following additional fields.","title":"Transactions"},{"location":"beancount_design_doc.html#flag_1","text":"The single-character flag is usually there to replace the \u201ctxn\u201d keyword (Transaction is the only directive which allows being entered without entering its keyword). I\u2019ve been considering changing the syntax definition somewhat to allow not entering the flag nor the keyword, because I\u2019d like to eventually support that. Right now, either the flag or keyword is required. The flag attribute may be set to None.","title":"Flag"},{"location":"beancount_design_doc.html#payee-narration","text":"The narration field is a user-provided description of the transaction, such as \"Dinner with Mary-Ann.\" You can put any information in this string. It shows up in the journal report. Oftentimes it is used to enrich the transaction with context that cannot be imported automatically, such as \"transfer from savings account to pay for car repairs.\" The payee name is optional, and exists to describe the entity with which the transaction is conducted, such as \"Whole Foods Market\" or \"Shell.\" Note that I want to be able to produce reports for all transactions associated with a particular payee, so it would be nice to enter consistent payee names. The problem with this is that the effort to do this right is sometimes too great. Either better tools or looser matching over names is required in order to make this work. The input syntax also allows only a single string to be provided. By default this becomes the narration, but I\u2019ve found that in practice it can be useful to have just the payee. It\u2019s just a matter of convenience. At the moment, if you want to enter just the payee string you need to append an empty narration string. This should be revisited at some point.","title":"Payee & Narration"},{"location":"beancount_design_doc.html#tags","text":"Tags are sets of strings that can be used to group sets of transactions (or set to None if there are no tags). A view of this subset of transactions can then be generated, including all the usual reports (balance sheet, income statement, journals, etc.). You can tag transactions for a variety of purposes; here are some examples: All transactions during a particular travel might be tagged to later summarize your trip expenses. Those transactions will usually occur around the same date and therefore there is convenient syntax used to automatically tag many transactions that are declared together in a file. Transactions related to a particular project to be accomplished. I took some classes in an online program once, and tagged all related expenses to it. I use this to track all my immigration-related expenses for a particular stage (e.g. green card). Declaring a group of expenses to be paid back by an employer (or your own company) later on. Expenses related to moving between locations. Typical tag names that I use for tags look like #trip-israel-2012 , #conference-siggraph , and #course-cfa . In general, tags are useful where adding a sub-accounts won't do. This is often the case where a group of related expenses are of differing types, and so they would not belong to a single account. Given the right support from the query tools, they could eventually be subsumed by metadata\u2014I have been considering converting tags into metadata keys with a boolean value of True, in order to remove unnecessary complexity.","title":"Tags"},{"location":"beancount_design_doc.html#links","text":"Links are a unique set of strings or None , and in practice will be usually empty for most transactions. They differ from tags only in purpose. Links are there to chain together a list of related transactions and there are tools used to list, navigate and balance a subset of transactions linked together. They are a way for a transaction to refer to other transactions. They are not meant to be used for summarizing. Examples include: transaction-ids from trading accounts (these often provide an id to a \"related\" or \"other\" transaction); account entries associated with related corrections, for example a reversed fee following a phone call could be linked to the original invalid fee from several weeks before; the purchase and sale of a home, and related acquisition expenses. In contrast to tags, their strings are most often unique numbers produced by the importers. No views are produced for links; only a journal of a particular links transactions can be produced and a rendered transaction should be accompanied by an actual \"link\" icon you can click on to view all the other related transactions.","title":"Links"},{"location":"beancount_design_doc.html#postings","text":"A list of Postings are attached to the Transaction object. Technically this list object is mutable but in practice we try not to modify it. A Posting can ever only be part of a single Transaction. Sometimes different postings of the same transaction will settle at different dates in their respective accounts, so eventually we may allow a posting to have its own date to override the transaction's date, to be used as documentation; in the simplest version, we enforce all transactions to occur punctually, which is simple to understand and was not found to be a significant problem in practice. Eventually we might implement this by implicitly converting Transactions with multiple dates into multiple Transactions and using some sort of transfer account to hold the pending balance in-between dates. See the associated proposal for details.","title":"Postings"},{"location":"beancount_design_doc.html#balancing-postings","text":"The fundamental principle of double-entry book-keeping is enforced in each of the Transaction entries: the sum of all postings must be zero. This section describes the specific way in which we do this, and is the key piece of logic that allow the entire system to balance nicely. This is also one of the few places in this system where calculations go beyond simple filtering and aggregation. As we saw earlier, postings are associated with A position, which is a number of units of a lot (which itself may or may not have a cost) An optional conversion price Given this, how do we balance a transaction? Some terminology is in order: for this example posting: Assets:Investment:HOOL -50 HOOL {700 USD} @ 920 USD Units. The number of units is the number of the position, or \u201c50\u201d. Currency. The commodity of the lot, that is, \u201cHOOL\u201d. Cost. The cost of this position is the number of units times the per-unit cost in the cost currency, that is 50 x 700 = 35000 USD. (Total) Price. The price of a unit is the attached price amount, 920 USD. The total price of a position is its number of units times the conversion price, in this example 50 x 920 = 46000 USD. Weight. The amount used to balance each posting in the transaction: If the posting has an associated cost, we calculate the cost of the position (regardless of whether a price is present or not); If the posting has no associated cost but has a conversion price, we convert the position into its total price; Finally, if the posting has no associated cost nor conversion price, the number of units of the lot are used directly. Balancing is really simple: each of the Posting's positions are first converted into their weight. These amounts are then grouped together by currency, and the final sum for each currency is asserted to be close to zero, that is, within a small amount of tolerance (as inferred by a combination of by the numbers in the input and the options ). The following example is one of case (2), with a price conversion and a regular leg with neither a cost nor a price (case 3): 2013-07-22 * \"Wired money to foreign account\" Assets:Investment:HOOL -35350 CAD @ 1.01 USD ;; -35000 USD (2) Assets:Investment:Cash 35000 USD ;; 35000 USD (3) ;;------------------ ;; 0 USD In the next example, the posting for the first leg has a cost, the second posting has neither: 2013-07-22 * \"Bought some investment\" Assets:Investment:HOOL 50 HOOL {700 USD} ;; 35000 USD (1) Assets:Investment:Cash -35000 USD ;; -35000 USD (3) ;;------------------ ;; 0 USD Here's a more complex example with both a price and a cost; the price here is ignored for the purpose of balance and is used only to enter a data-point in the internal price database: 2013-07-22 * \"Sold some investment\" Assets:Investment:HOOL -50 HOOL {700 USD} @ 920 USD ;; -35000 USD (1) Assets:Investment:Cash 46000 USD ;; 46000 USD (3) Income:CapitalGains -11000 USD ;; -11000 USD (3) ;;------------------ ;; 0 USD Finally, here's an example with multiple commodities: 2013-07-05 * \"COMPANY INC PAYROLL\" Assets:US:TD:Checking 4585.38 USD Income:US:Company:GroupTermLife -25.38 USD Income:US:Company:Salary -5000.00 USD Assets:US:Vanguard:Cash 540.00 USD Assets:US:Federal:IRAContrib -540.00 IRAUSD Expenses:Taxes:US:Federal:IRAContrib 540.00 IRAUSD Assets:US:Company:Vacation 4.62 VACHR Income:US:Company:Vacation -4.62 VACHR Here there are three groups of weights to balance: USD: (4485.38) + (-25.38) + (-5000) + (540) ~= 0 USD IRAUSD: (540.00) + (-540.00) ~= 0 IRAUSD VACHR: (4.62) + (-4.62) ~= 0 VACHR","title":"Balancing Postings"},{"location":"beancount_design_doc.html#elision-of-amounts","text":"Transactions allow for at most one posting to be elided and automatically set; if an amount was elided, the final balance of all the other postings is attributed to the balance. With the inventory booking proposal , it should be extended to be able to handle more cases of elision. An interesting idea would be to allow the elided postings to at least specify the currency they're using. This would allow the user to elide multiple postings, like this: 2013-07-05 * \"COMPANY INC PAYROLL\" Assets:US:TD:Checking USD Income:US:Company:GroupTermLife -25.38 USD Income:US:Company:Salary -5000.00 USD Assets:US:Vanguard:Cash 540.00 USD Assets:US:Federal:IRAContrib -540.00 IRAUSD Expenses:Taxes:US:Federal:IRAContrib IRAUSD Assets:US:Company:Vacation 4.62 VACHR Income:US:Company:Vacation VACHR","title":"Elision of Amounts"},{"location":"beancount_design_doc.html#stream-processing","text":"An important by-product of representing all state using a single stream of directives is that most of the operations in Beancount can be implemented by simple functions that accept the list of directives as input and output a modified list of directives. For example, The \u201cPad\u201d directive is implemented by processing the stream of transactions, accumulating balances, inserting a new padding transaction after the Pad directive when a balance assertion fails. Summarization operations (open books, close books, clear net income to equity) are all implemented this way, as functional operators on the list of streams. For example, opening the books at a particular date is implemented by summing all balances before a certain date and replacing those transactions by new transactions that pull the final balance from an Equity account. Closing the books only involves moving the balances of income statement account to Equity and truncation the list of transactions to that date. This is very elegant\u2014the reporting code can be oblivious to the fact that summarization has occurred. Prices on postings held with a cost are normally ignored. The \u201cimplicit prices\u201d option is implemented using a plugin which works its magic by processing the stream of operations and inserting otherwise banal Price directives automatically when such a posting is found. Many types of verifications are also implemented this way; the sellgains plugin verifies that non-income balances check against the converted price of postings disregarding the cost. This one does not insert any new directives, but may generate new errors. These are just some examples. I\u2019m aiming to make most operations work this way. This design has proved to be elegant, but also surprisingly flexible and it has given birth to the plugins system available in Beancount; you might be surprised to learn that I had not originally thought to provide a plugins system... it just emerged over time teasing abstractions and trying to avoid state in my program. I am considering experimenting with weaving the errors within the stream of directives by providing a new \u201cError\u201d directive that could be inserted by stream processing functions. I\u2019m not sure whether this would make anything simpler yet, it\u2019s just an idea at this point [July 2015].","title":"Stream Processing"},{"location":"beancount_design_doc.html#stream-invariants","text":"The stream of directives comes with a few guarantees you can rest upon: All the directives are ordered by date. Because many dates have multiple directives, in order to have a stable sorting order the line number in the file is used as a secondary sort key. All uses of an account is preceded in the stream by a corresponding Open entry. If the input did not provide one, an Open directive is automatically synthesized. All Balance directives precede any other directive on a particular day. This is enforced in order to make the processing of balance assertions straightforward and to emphasize that their semantics is to occur at the beginning of the day.","title":"Stream Invariants"},{"location":"beancount_design_doc.html#loader-processing-order","text":"The process of loading a list of entries from an input file is the heart of the project. It is important to understand it in order to understand how Beancount works. Refer to the diagram below. It consists of A parsing step that reads in all the input files and produces a stream of potentially incomplete directives. The processing of this stream of entries by built-in and user-specified plugins. This step potentially produces new error objects. A final validation step that ensures the invariants have not been broken by the plugins. The beancount.loader module orchestrates this processing. In the code and documents, I\u2019m careful to distinguish between the terms \u201cparsing\u201d and \u201cloading\u201d carefully. These two concepts are distinct. \u201cParsing\u201d is only a part of \u201cloading.\u201d The user-specified plugins are run in the order they are provided in the input files. Raw mode. Some users have expressed a desire for more control over which built-in plugins are run and in which order they are to be run, and so enabling the \u201craw\u201d option disables the automatic loading of all built-in plugins (you have to replace those with explicit \u201cplugin\u201d directives if you want to do that). I\u2019m not sure if this is going to be useful yet, but it allows for tighter control over the loading process for experimentation.","title":"Loader & Processing Order"},{"location":"beancount_design_doc.html#loader-output","text":"The parser and the loader produce three lists: entries: A list of directive tuples from beancount.core.data. This is the stream of data which should consists mostly of Transaction and Balance instances. As much as possible, all data transformations are performed by inserting or removing entries from this list. Throughout the code and documents, I refer to these as entries or directives interchangeably. errors: A list of error objects. In Beancount I don\u2019t use exceptions to report errors; rather, all the functions produce error objects and they are displayed at the most appropriate time. (These deserve a base common type but for now the convention they respect is that they all provide a source (filename, line-no), message and entry attributes.) options_map: A dict of the options provided in the file and derived during parsing. Though this is a mutable object, we never modify it once produced by the parser.","title":"Loader Output"},{"location":"beancount_design_doc.html#parser-implementation","text":"The Beancount parser is a mix of C and Python 3 code. This is the reason you need to compile anything in the first place. The parser is implemented in C using a flex tokenizer and a Bison parser generator, mainly for performance reasons. I chose the C language and these crufty old GNU tools because they are portable and will work everywhere. There are better parser generators out there, but they would either require my users to install more compiler tools or exotic languages. The C language is a great common denominator. I want Beancount to work everywhere and to be easy to install. I want this to make it easy on others, but I also want this for myself, because at some point I\u2019ll be working on other projects and the less dependencies I have the lighter it will be to maintain aging software. Just looking ahead. (That\u2019s also why I chose to use Python instead of some of the other languages I\u2019m more naturally attracted to these days (Clojure, Go, ML); I need Beancount to work... when I\u2019m counting the beans I don\u2019t have time to debug problems. Python is careful about its changes and it will be around for a long long time as is.) There is a lexer file lexer.l written in flex and a Bison grammar in grammar.y. These tools are used to generate the corresponding C source code (lexer.h/.c and grammar.h/.c). These, along with some hand-written C code that defines some interface functions for the module (parser.h/.c), are compiled into an extension module (_parser.so). Eventually we could consider creating a small dependency rule in setup.py to invoke flex and Bison automatically but at the moment, in order to minimize the installation burden, I check the generated source code in the repository (lexer.h/c and grammar.h/c). The interaction between the Python and C code works like this: You import beancount.parser.parser and invoke either parse_file() or parse_string(). This uses code in grammar.py to create a Builder object which essentially provides callbacks in Python to process grammar rules. parser.py calls a C function in the extension module with the Builder object. That C code sets up flex to be able to read the input file or string then hands over control to the parser by calling the generated C function \u201cyyparse()\u201d. yyparse() is the parser and will fetch input tokens from the lexer C code using successive calls to \u201cyylex()\u201d and attempt to reduce rules. When a rule or token is successfully reduced, the C code invokes callback methods on the Builder object you provided. The parser\u2019s rules communicate between each other by passing around \u201cPyObject*\u201d instances, so that code has to be a little careful about correctly handling reference counting. This allows me to run Python code on rule reduction, which makes it really easy to customize parser behaviour yet maintain the performance of the grammar rule processing in C. Note that the grammar.py Builder derives from a similar Builder class defined in lexer.py, so that lexer tokens recognized calls methods defined in the lexer.py file to create them and parser rule correspondingly calls methods from grammar.py\u2019s Builder. This isolation is not only clean, it also makes it possible to just use the lexer to tokenize a file from Python (without parsing) for testing the tokenizer; see the tests where this is used if you\u2019re curious how this works. I just came up with this; I haven\u2019t seen this done anywhere else. I tried it and the faster parser made a huge difference compared to using PLY so I stuck with that. I quite like the pattern, it\u2019s a good compromise that offers the flexibility of a parser generator yet allows me to handle the rules using Python. Eventually I\u2019d like to move just some of the most important callbacks to C code in order to make this a great deal faster (I haven\u2019t done any real performance optimizations yet). This has worked well so far but for one thing: There are several limitations inherent in the flex tokenizer that have proved problematic. In particular, in order to recognize transaction postings using indent I have had to tokenize the whitespace that occurs at the beginning of a line. Also, single-characters in the input should be parsed as flags and at the moment this is limited to a small subset of characters. I\u2019d like to eventually write a custom lexer with some lookahead capabilities to better deal with these problems (this is easy to do).","title":"Parser Implementation"},{"location":"beancount_design_doc.html#two-stages-of-parsing-incomplete-entries","text":"At the moment, the parser produces transactions which may or may not balance. The validation stage which runs after the plugins carries out the balance assertions. This degree of freedom is allowed to provide the opportunity for users to enter incomplete lists of postings and for corresponding plugins to automate some data entry and insert completing postings. However, the postings on the transactions are always complete objects, with all their expected attributes set. For example, a posting which has its amount elided in the input syntax will have a filled in position by the time it comes out of the parser. We will have to loosen this property when we implement the inventory booking proposal , because we want to support the elision of numbers whose values depend on the accumulated state of previous transactions. Here is an obvious example. A posting like this 2015-04-03 * \"Sell stock\" Assets:Investments:AAPL -10 AAPL {} ... should succeed if at the beginning of April 3rd the account contains exactly 10 units of AAPL, in either a single or multiple lots. There is no ambiguity: \u201csell all the AAPL,\u201d this says. However, the fact of its unambiguity assumes that we\u2019ve computed the inventory balance of that account up until April 3rd\u2026 So the parser will need to be split into two phases: A simple parsing step that produces potentially incomplete entries with missing amounts A separate step for the interpolation which will have available the inventory balances of each account as inputs. This second step is where the booking algorithms (e.g., FIFO) will be invoked from. See the diagram above for reference. Once implemented, everything else should be the same.","title":"Two Stages of Parsing: Incomplete Entries"},{"location":"beancount_design_doc.html#the-printer","text":"In the same package as the parser lives a printer. This isolates all the functionality that deals with the Beancount \u201clanguage\u201d in the beancount.parser package: the parser converts from input syntax to data structure and the printer does the reverse. No code outside this package should concern itself with the Beancount syntax. At some point I decided to make sure that the printer was able to round-trip through the parser, that is, given a stream of entries produced by the loader, you should be able to convert those to text input and parse them back in and the resulting set of entries should be the same (outputting the re-read input back to text should produce the same text), e.g., Note that the reverse isn\u2019t necessarily true: reading an input file and processing it through the loader potentially synthesizes a lot of entries (thanks to the plugins), so printing it back may not produce the same input file (even ignoring reordering and white space concerns). This is a nice property to have. Among other things, it allows us to use the parser to easily create tests with expected outputs. While there is a test that attempts to protect this property, some of the recent changes break it partially: Metadata round-tripping hasn\u2019t been tested super well. In particular, some of the metadata fields need to be ignored (e.g., filename and lineno). Some directives contain derived data, e.g. the Balance directive has a \u201cdiff_amount\u201d field that contains the difference when there is a failure in asserting a balance. This is used to report errors more easily. I probably need to remove these exceptions at some point because it is the only one of its kind (I could replace it with an inserted \u201cError\u201d directive). This probably needs to get refined a bit at some point with a more complete test (it\u2019s not far as it is).","title":"The Printer"},{"location":"beancount_design_doc.html#uniqueness-hashing","text":"In order to make it possible to compare directives quickly, we support unique hashing of all directives, that is, from each directive we should be able to produce a short and unique id. We can then use these ids to perform set inclusion/exclusion/comparison tests for our unit tests. We provide a base test case class with assertion methods that use this capability . This feature is used liberally throughout our test suites. This is also used to detect and remove duplicates. This feature is optional and enabled by the beancount.plugins.noduplicates plugin. Note that the hashing of directives currently does not include user meta-data .","title":"Uniqueness & Hashing"},{"location":"beancount_design_doc.html#display-context","text":"Number are input with different numbers of fractional digits: 500 -> 0 fractional digits 520.23 -> 2 fractional digits 1.2357 -> 4 fractional digits 31.462 -> 3 fractional digits Often, a number used for the same currency is input with a different number of digits. Given this variation in the input, a question that arises is how to render the numbers consistently in reports? Consider that: We would like that the user not to have to specify the number of fractional digits to use for rendering by default. Rendering numbers may differ depending on the context: most of the time we want to render numbers using their most commonly seen number of digits, rounding if necessary, but when we are rendering conversion rates we would like to render numbers using the maximum number of digits ever seen. The number of digits used tends to vary with the commodity they represent. For example, US dollars will usually render with two digits of precision; the number of units of a mutual fund such as RGAGX might be recorded by your broker with 3 digits of precision. Japanese Yen will usually be specified in integer units. We want to render text report which need to align numbers with varying number of fractional digits together, aligning them by their period or rightmost digits, and possibly rendering a currency thereafter. In order to deal with this thorny problem, I built a kind of accumulator which is used to record all numbers seen from some input and tally statistics about the precisions witnessed for each currency. I call this a DisplayContext . From this object one can request to build a DisplayFormatter object which can be used to render numbers in a particular way. I refer to those objects using the variable names dcontext and dformat throughout the code. The parser automatically creates a DisplayContext object and feeds it all the numbers it sees during parsing. The object is available in the options_map produced by the loader.","title":"Display Context"},{"location":"beancount_design_doc.html#realization","text":"It occurs often that one needs to compute the final balances of a list of filtered transactions, and to report on them in a hierarchical account structure. See diagram below.","title":"Realization"},{"location":"beancount_design_doc.html#_1","text":"For the purpose of creating hierarchical renderings, I provide a process called a \u201c realization .\u201d A realization is a tree of nodes, each of which contains An account name, A list of postings and other entries to that account, and The final balance for that account, A mapping of sub-account names to child nodes. The nodes are of type \u201cRealAccount,\u201d which is also a dict of its children. All variables of type RealAccount in the codebase are by convention prefixed by \u201creal_*\u201d. The realization module provides functions to iterate and produce hierarchical reports on these trees of balances, and the reporting routines all use these. For example, here is a bit of code that will dump a tree of balances of your accounts to the console: import sys from beancount import loader from beancount.core import realization entries, errors, options_map = loader.load_file(\"filename.beancount\") real_root = realization.realize(entries) realization.dump_balances(real_root, file=sys.stdout)","title":""},{"location":"beancount_design_doc.html#the-web-interface","text":"Before the appearance of the SQL syntax to filter and aggregate postings, the only report produced by Beancount was the web interface provided by bean-web. As such, bean-web evolved to be good enough for most usage and general reports.","title":"The Web Interface"},{"location":"beancount_design_doc.html#reports-vs-web","text":"One of the important characteristics of bean-web is that it should just be a thin dispatching shell that serves reports generated from the beancount.reports layer. It used to contain the report rendering code itself, but at some point I began to factor out all the reporting code to a separate package in order to produce reports to other formats, such as text reports and output to CSV. This is mostly finished, but at this time [July 2015] some reports remain that only support HTML output. This is why.","title":"Reports vs. Web"},{"location":"beancount_design_doc.html#client-side-javascript","text":"I would like to eventually include a lot more client-side scripting in the web interface. However, I don\u2019t think I\u2019ll be able to work on this for a while, at least not until all the proposals for changes to the core are complete (e.g., inventory booking improvements, settlement splitting and merging, etc.). If you\u2019d like to contribute to Beancount, improving bean-web or creating your own visualizations would be a great venue.","title":"Client-Side JavaScript"},{"location":"beancount_design_doc.html#the-query-interface","text":"The current query interface is the result of a prototype . It has not yet been subjected to the amount of testing and refinement as the rest of the codebase. I\u2019ve been experimenting with it and have a lot of ideas for improving the SQL language and the kinds of outputs that can be produced from it. I think of it as \u201c70% done\u201d. However, it works, and though some of the reports can be a little clunky to specify, it produces useful results. It will be the subject of a complete rewrite at some point and when I do, I will keep the current implementation for a little while so that existing scripts don\u2019t just break; I\u2019ll implement a v2 of the shell.","title":"The Query Interface"},{"location":"beancount_design_doc.html#design-principles","text":"","title":"Design Principles"},{"location":"beancount_design_doc.html#minimize-configurability","text":"First, Beancount should have as few options as possible. Second, command-line programs should have no options that pertain to the processing and semantic of the input file (options that affect the output or that pertain to the script itself are fine). The goal is to avoid feature creep. Just a few options opens the possibility of them interacting in complex ways and raises a lot of questions. In this project, instead of providing options to customize every possible thing, I choose to bias the design towards providing the best behavior by default, even if that means less behavior. This is the Apple approach to design. I don\u2019t usually favor this approach for my projects, but I chose it for this particular one. What I\u2019ve learned in the process is how far you can get and still provide much of the functionality you originally set out for. Too many command-line options makes a program difficult to use. I think Ledger suffers from this, for instance. I can never remember options that I don\u2019t use regularly, so I prefer to just design without them. Options that affect semantics should live in the file itself (and should be minimized as well). Options provided when running a process should be only to affect this process. Having less options also makes the software much easier to refactor. The main obstacle to evolving a library of software is the number of complex interactions between the codes. Guaranteeing a well-defined set of invariants makes it much easier to later on split up functionality or group it differently. So.. by default I will resist making changes that aren't generic or that would not work for most users. On the other hand, large-scale changes that would generalize well and that require little configurability are more likely to see implementation.","title":"Minimize Configurability"},{"location":"beancount_design_doc.html#favor-code-over-dsls","text":"Beancount provides a simple syntax, a parser and printer for it, and a library of very simple data record types to allow a user to easily write their scripts to process their data, taking advantage of the multitude of libraries available from the Python environment. Should the built-in querying tools fail to suffice, I want it to be trivially easy for someone to build what they need on top of Beancount. This also means that the tools provided by Beancount don\u2019t have to support all the features everyone might ever possibly want. Beancount\u2019s strength should be representational coherence and simplicity rather than the richness of its reporting features. Creating new kinds of reports should be easy; changing the internals should be hard. (That said, it does provide an evolving palette of querying utilities that should be sufficient for most users.) In contrast, the implementation of the Ledger system provides high-level operations that are invoked as \"expressions\" defined in a domain-specific language, expressions which are either provided on the command-line or in the input file itself. For the user, this expression language is yet another thing to learn, and it\u2019s yet another thing that might involve limitations requiring expansion and work\u2026 I prefer to avoid DSLs if possible and use Python\u2019s well understood semantics instead, though this is not always sensible.","title":"Favor Code over DSLs"},{"location":"beancount_design_doc.html#file-format-or-input-language","text":"One may wonder whether Beancount\u2019s input syntax should be a computer language or just a data format. The problem we're trying to solve is essentially that of making it easy for a human being to create a transactions-to-postings-to-accounts-&-positions data structure. What is the difference between a data file format and a simple declarative computer language? One distinction is the intended writer of the file. Is it a human? Or is it a computer? The input files are meant to be manicured by a human, at the very least briefly eyeballed. While we are trying to automate as much of this process as possible via importing code\u2014well, the unpleasant bits, anyway\u2014we do want to ensure that we review all the new transactions that we're adding to the ledger in order to ensure correctness. If the intended writer is a human one could file the input under the computer language rubric (most data formats are designed to be written by computers). We can also judge it by the power of its semantics. Beancount\u2019s language is not one designed to be used to perform computation, that is, you cannot define new \u201cBeancount functions\u201d in it. But it has data types. In this regard, it is more like a file format. Just something to think about, especially in the context of adding new semantics.","title":"File Format or Input Language?"},{"location":"beancount_design_doc.html#grammar-via-parser-generator","text":"The grammar of its language should be compatible with the use of commonly used parser generator tools. It should be possible to parse the language with a simple lexer and some grammar input file. Beancount\u2019s original syntax was modeled after the syntax of Ledger. However, in its attempt to make the amount of markup minimal, that syntax is difficult to parse with regular tools. By making simple changes to the grammar, e.g. adding tokens for strings and accounts and commodities, the Beancount v2 reimplementation made it possible to use a flex/bison parser generator. This has important advantages: It makes it really easy to make incremental changes to the grammar. Using a custom parser without a well-defined grammar can make it quite difficult to prototype syntax changes. It makes it much easier to implement a parser in another computer language. I\u2019d like to eventually be able to parse Beancount input files in other languages. The data structures are simple enough that it should be easy to reimplement the core in a different language. It\u2019s just a lot less code to write and maintain. (Eventually I\u2019d like to create a file format to generate parsers in multiple languages from a single input. That will be done later, once all the major features are implemented.)","title":"Grammar via Parser Generator"},{"location":"beancount_design_doc.html#future-work","text":"","title":"Future Work"},{"location":"beancount_design_doc.html#tagged-strings","text":"At the moment, account names, tags, links and commodities are represented as simple Python strings. I\u2019ve tried to keep things simple. At some point I\u2019d like to refine this a bit and create a specialization of strings for each of these types. I\u2019d need to assess the performance impact of doing that beforehand. I\u2019m not entirely sure how it could benefit functionality yet.","title":"Tagged Strings"},{"location":"beancount_design_doc.html#errors-cleanup","text":"I\u2019ve been thinking about removing the separate \u201cerrors\u201d list and integrating it into the stream of entries as automatically produced \u201cError\u201d entries. This has the advantage that the errors could be rendered as part of the rest of the stream, but it means we have to process the whole list of entries any time we need to print and find errors (probably not a big deal, given how rarely we output errors).","title":"Errors Cleanup"},{"location":"beancount_design_doc.html#conclusion","text":"This document is bound to evolve. If you have questions about the internals, please post them to the mailing-list .","title":"Conclusion"},{"location":"beancount_design_doc.html#references","text":"Nothing in Beancount is inspired from the following docs, but you may find them interesting as well: Accounting for Computer Scientists I have been considering the addition of a very constrained form of non-balancing postings that would preserve the property of transactions otherwise being balanced, whereby the extra postings would not be able to interact (or sum with) regular balancing postings. \u21a9 There is an option called operating_currency but it is only used to provide good defaults for building reports, never in the processing of transactions. It is used to tell the reporting code which commodities should be broken out to have their own columns of balances, for example. \u21a9 In previous version of Beancount this was not true, the Posting used to have a \u2018position\u2019 attribute and composed it. I prefer the flattened design, as many of the functions apply to either a Position or a Posting. Think of a Posting as deriving from a Position, though, technically it does not. \u21a9","title":"References"},{"location":"beancount_history_and_credits.html","text":"Beancount History and Credits \uf0c1 Martin Blais , July 2014 http://furius.ca/beancount/doc/history A history of the development of Beancount and credits for contributors.. History of Beancount \uf0c1 John Wiegley's Ledger was the inspiration for the first version of Beancount. His system is where much of the original ideas for this system came from. When I first learned about double-entry bookkeeping and realized that it could be the perfect method to solve many of the tracking problems I was having in counting various installments for my company, and after a quick disappointment in the solutions that were available at the time (including GnuCash, which I could break very easily), I was quickly led to the Ledger website. There, John laid out his vision for a text-based system, in particular, the idea of doing away with credits and debits and just the signs, and the basics of a convenient input syntax which allows you to omit the amount of one of the postings. I got really excited and had various enthusiastic discussions with him about Ledger and how I wanted to use it. There was some cross-pollination of ideas and John was very receptive to proposals for adding new features. I was so intensely curious about bookkeeping that I began writing a Python interface to Ledger. But in the end I found myself rewriting the entire thing in Python\u2013not for dislike of Ledger but rather because it was simple enough that I could do most of it in little time, and immediately add some features I thought would be useful. One reason for doing so was that instead of parsing the input file every time and generating one report to the console, I would parse it once and then serve the various reports from the in-memory database of transactions, requested via a web page. Therefore, I did not need processing speed, so having to use C++ for performance reasons was not necessary anymore, and I chose to just stick with a dynamic language, which allowed me to add many features quickly. This became Beancount version 1, which stood on its own and evolved its own set of experimental features. My dream was to be able to quickly and easily manipulate these transaction objects to get various views and breakdowns of the data. I don't think the first implementation pushed the limits far enough, however; the code was substandard, to be honest\u2014I wrote it really quickly\u2014and making modifications to the system was awkward. In particular, the way I originally implemented the tracking of capital gains was inelegant and involved some manual counting. I was unhappy with this, but it worked. It was also using ugly ad-hoc parser code in order to remain reasonably compatible with Ledger syntax\u2014I thought it would be interesting to be able to share some common input syntax and use either system for validation and maybe even to convert between them\u2013and that made me wary of making modifications to the syntax to evolve new features, so it stabilized for a few years and I lost interest in adding new features. But it was correct and it worked, mostly, so I used the system continuously from 2008 to 2012 to manage my own personal finances, my company's finances, and joint property with my wife, with detailed transactions; this was great. I learned a lot about how to keep books during that time (the cookbook document is meant to help you do the same). In the summer of 2013, I had an epiphany and realized a correct and generalizable way to implement capital gains, how to merge the tracking of positions held at a cost and regular positions, and a set of simple rules for carrying out operations on them sensibly (the design of how inventories work). I also saw a better way to factor out the internal data structures, and decided to break from the constraint of compatibility with Ledger and redesign the input syntax in order to parse the input language using a lex/yacc generator, which would allow me to easily evolve the input syntax without having to deal with parsing issues, and to create ports to other languages more easily. In the process, a much simpler and more consistent syntax emerged, and in a fit of sweat and a few intense weekends I re-implemented the entire thing from scratch, without even looking at the previous version, clean-room. Beancount version 2 was born, much better than the last, modular, and easy to extend with plugins. The result is what I think is an elegant design involving a small set of objects, a design that could easily be a basis for reimplementation in other computer languages. This is described in the accompanying design doc, for those who would have an interest in having a go at it (this would be welcome and I'm expecting this will happen). While Ledger remains an interesting project bubbling with ideas for expressing the problem of bookkeeping, the second version of Beancount proposes a simpler design that leaves out features that are not strictly necessary and aims at maximum usability through a simple web interface and a very small set of command-line options. Since I had more than 5 years worth of real-world usage experience with the first version, I set a goal for myself to remove all the features that I thought weren't actually useful and introduced unnecessary complexity (like virtual transactions, allowing accounts not in the five types, etc.), and to simplify the system as much as possible without compromising on its usability. The resulting language and software were much simpler to use and understand, the resulting data structures are much simpler to use, the processing more \u201cfunctional\u201d in nature, and the internals of Beancount are very modular. I converted all my 6 years worth of input data\u2013thanks to some very simple Python scripts to manipulate the file\u2013and began using the new version exclusively. It is now in a fully functional state. Ledger's syntax implements many features that trigger a lot of implicitly-defined behaviour; I find this confusing and some of the reasons for this are documented in the many improvement proposals. I don\u2019t like command-line options. In contrast, Beancount's design provides a less expressive, lower-level syntax but one that closely matches the generated in-memory data structure, and that is hopefully more explicit in that way. I think both projects have strengths and weaknesses. Despite its popularity, the latest version of Ledger remains with a number of shortcomings in my view. In particular, reductions in positions are not booked at the moment they occur, but rather they appear to simply accumulate and get matched only at display time, using different methods depending on the command-line options. Therefore, it is possible in Ledger to hold positive and negative lots of the same commodity in an account simultaneously. I believe that this can lead to confusing and even incorrect accounting for trading lots. I think this is still being figured out by the Ledger look-alikes community and that they will eventually converge to the same solution I have, or perhaps even figure out a better solution. In the redesign, I separated out configuration directives that I had used for importing and over many iterations eventually figured out an elegant way to mostly automate imports and automatically detect the various input files and convert them into my input syntax. The result is the LedgerHub design doc . LedgerHub is currently implemented and I\u2019m using it exclusively, but has insufficient testing for me to stamp a public release [July 2014]. You are welcome to try it out for yourself. In June and July 2014, I decided to dump seven years\u2019 worth of thinking about command-line accounting in a set of Google Docs and this now forms the basis for the current documentation of Beancount. I hope to be evolving it gradually from here on. Chronology \uf0c1 Ledger was begun in August 2003 http://web.archive.org/web/*/http://www.newartisans.com/ledger/ Beancount was begun in 2008 http://web.archive.org/web/*/furius.ca/beancount HLedger was also begun in 2008 https://github.com/simonmichael/hledger/graphs/contributors?from=2007-01-21&to=2014-09-08&type=c Credits \uf0c1 So far I\u2019ve been contributing all the code to Beancount. Some users have made significant contributions in other ways: Daniel Clemente has been reporting all the issues he came across while using Beancount to manage his company and personal finances. His relentless perseverance and attention to detail has helped me put a focus on fixing the rough corners of Beancount that I knew to avoid myself. After many years of prodding, my old friend Filippo Tampieri has finally decided to convert his trading history in Beancount format. He has contributed a number of sophisticated reviews of my documentation and is working on adding various methods for evaluating returns on assets.","title":"Beancount History and Credits"},{"location":"beancount_history_and_credits.html#beancount-history-and-credits","text":"Martin Blais , July 2014 http://furius.ca/beancount/doc/history A history of the development of Beancount and credits for contributors..","title":"Beancount History and Credits"},{"location":"beancount_history_and_credits.html#history-of-beancount","text":"John Wiegley's Ledger was the inspiration for the first version of Beancount. His system is where much of the original ideas for this system came from. When I first learned about double-entry bookkeeping and realized that it could be the perfect method to solve many of the tracking problems I was having in counting various installments for my company, and after a quick disappointment in the solutions that were available at the time (including GnuCash, which I could break very easily), I was quickly led to the Ledger website. There, John laid out his vision for a text-based system, in particular, the idea of doing away with credits and debits and just the signs, and the basics of a convenient input syntax which allows you to omit the amount of one of the postings. I got really excited and had various enthusiastic discussions with him about Ledger and how I wanted to use it. There was some cross-pollination of ideas and John was very receptive to proposals for adding new features. I was so intensely curious about bookkeeping that I began writing a Python interface to Ledger. But in the end I found myself rewriting the entire thing in Python\u2013not for dislike of Ledger but rather because it was simple enough that I could do most of it in little time, and immediately add some features I thought would be useful. One reason for doing so was that instead of parsing the input file every time and generating one report to the console, I would parse it once and then serve the various reports from the in-memory database of transactions, requested via a web page. Therefore, I did not need processing speed, so having to use C++ for performance reasons was not necessary anymore, and I chose to just stick with a dynamic language, which allowed me to add many features quickly. This became Beancount version 1, which stood on its own and evolved its own set of experimental features. My dream was to be able to quickly and easily manipulate these transaction objects to get various views and breakdowns of the data. I don't think the first implementation pushed the limits far enough, however; the code was substandard, to be honest\u2014I wrote it really quickly\u2014and making modifications to the system was awkward. In particular, the way I originally implemented the tracking of capital gains was inelegant and involved some manual counting. I was unhappy with this, but it worked. It was also using ugly ad-hoc parser code in order to remain reasonably compatible with Ledger syntax\u2014I thought it would be interesting to be able to share some common input syntax and use either system for validation and maybe even to convert between them\u2013and that made me wary of making modifications to the syntax to evolve new features, so it stabilized for a few years and I lost interest in adding new features. But it was correct and it worked, mostly, so I used the system continuously from 2008 to 2012 to manage my own personal finances, my company's finances, and joint property with my wife, with detailed transactions; this was great. I learned a lot about how to keep books during that time (the cookbook document is meant to help you do the same). In the summer of 2013, I had an epiphany and realized a correct and generalizable way to implement capital gains, how to merge the tracking of positions held at a cost and regular positions, and a set of simple rules for carrying out operations on them sensibly (the design of how inventories work). I also saw a better way to factor out the internal data structures, and decided to break from the constraint of compatibility with Ledger and redesign the input syntax in order to parse the input language using a lex/yacc generator, which would allow me to easily evolve the input syntax without having to deal with parsing issues, and to create ports to other languages more easily. In the process, a much simpler and more consistent syntax emerged, and in a fit of sweat and a few intense weekends I re-implemented the entire thing from scratch, without even looking at the previous version, clean-room. Beancount version 2 was born, much better than the last, modular, and easy to extend with plugins. The result is what I think is an elegant design involving a small set of objects, a design that could easily be a basis for reimplementation in other computer languages. This is described in the accompanying design doc, for those who would have an interest in having a go at it (this would be welcome and I'm expecting this will happen). While Ledger remains an interesting project bubbling with ideas for expressing the problem of bookkeeping, the second version of Beancount proposes a simpler design that leaves out features that are not strictly necessary and aims at maximum usability through a simple web interface and a very small set of command-line options. Since I had more than 5 years worth of real-world usage experience with the first version, I set a goal for myself to remove all the features that I thought weren't actually useful and introduced unnecessary complexity (like virtual transactions, allowing accounts not in the five types, etc.), and to simplify the system as much as possible without compromising on its usability. The resulting language and software were much simpler to use and understand, the resulting data structures are much simpler to use, the processing more \u201cfunctional\u201d in nature, and the internals of Beancount are very modular. I converted all my 6 years worth of input data\u2013thanks to some very simple Python scripts to manipulate the file\u2013and began using the new version exclusively. It is now in a fully functional state. Ledger's syntax implements many features that trigger a lot of implicitly-defined behaviour; I find this confusing and some of the reasons for this are documented in the many improvement proposals. I don\u2019t like command-line options. In contrast, Beancount's design provides a less expressive, lower-level syntax but one that closely matches the generated in-memory data structure, and that is hopefully more explicit in that way. I think both projects have strengths and weaknesses. Despite its popularity, the latest version of Ledger remains with a number of shortcomings in my view. In particular, reductions in positions are not booked at the moment they occur, but rather they appear to simply accumulate and get matched only at display time, using different methods depending on the command-line options. Therefore, it is possible in Ledger to hold positive and negative lots of the same commodity in an account simultaneously. I believe that this can lead to confusing and even incorrect accounting for trading lots. I think this is still being figured out by the Ledger look-alikes community and that they will eventually converge to the same solution I have, or perhaps even figure out a better solution. In the redesign, I separated out configuration directives that I had used for importing and over many iterations eventually figured out an elegant way to mostly automate imports and automatically detect the various input files and convert them into my input syntax. The result is the LedgerHub design doc . LedgerHub is currently implemented and I\u2019m using it exclusively, but has insufficient testing for me to stamp a public release [July 2014]. You are welcome to try it out for yourself. In June and July 2014, I decided to dump seven years\u2019 worth of thinking about command-line accounting in a set of Google Docs and this now forms the basis for the current documentation of Beancount. I hope to be evolving it gradually from here on.","title":"History of Beancount"},{"location":"beancount_history_and_credits.html#chronology","text":"Ledger was begun in August 2003 http://web.archive.org/web/*/http://www.newartisans.com/ledger/ Beancount was begun in 2008 http://web.archive.org/web/*/furius.ca/beancount HLedger was also begun in 2008 https://github.com/simonmichael/hledger/graphs/contributors?from=2007-01-21&to=2014-09-08&type=c","title":"Chronology"},{"location":"beancount_history_and_credits.html#credits","text":"So far I\u2019ve been contributing all the code to Beancount. Some users have made significant contributions in other ways: Daniel Clemente has been reporting all the issues he came across while using Beancount to manage his company and personal finances. His relentless perseverance and attention to detail has helped me put a focus on fixing the rough corners of Beancount that I knew to avoid myself. After many years of prodding, my old friend Filippo Tampieri has finally decided to convert his trading history in Beancount format. He has contributed a number of sophisticated reviews of my documentation and is working on adding various methods for evaluating returns on assets.","title":"Credits"},{"location":"beancount_language_syntax.html","text":"Beancount Language Syntax \uf0c1 Martin Blais , Updated: April 2016 http://furius.ca/beancount/doc/syntax Introduction Syntax Overview Directives Ordering of Directives Accounts Commodities / Currencies Strings Comments Directives Open Close Commodity Transactions Metadata Payee & Narration Costs and Prices Balancing Rule - The \u201cweight\u201d of postings Reducing Positions Amount Interpolation Tags The Tag Stack Links Balance Assertions Multiple Commodities Lots Are Aggregated Checks on Parent Accounts Before Close Pad Unused Pad Directives Commodities Cost Basis Multiple Paddings Notes Documents Documents from a Directory Prices Prices from Postings Prices on the Same Day Events Query Custom Metadata Options Operating Currencies Plugins Includes What\u2019s Next? Introduction \uf0c1 This is a user's manual to the language of Beancount, the command-line double-entry bookkeeping system. Beancount defines a computer language that allows you to enter financial transactions in a text file and extract various reports from it. It is a generic counting tool that works with multiple currencies, commodities held at cost (e.g., stocks), and even allows you to track unusual things, like vacation hours, air miles and rewards points, and anything else you might want to count, even beans. This document provides an introduction to Beancount\u2019s syntax and some of the technical details needed for one to understand how it carries out its calculations. This document does not provide an introduction to the double-entry method , a motivation , nor examples and guidelines for entering transactions in your input file, nor how to run the tools . These subjects have their own dedicated documents , and it is recommended that you have had a look at those before diving into this user\u2019s manual. This manual covers the technical details for using Beancount. Syntax Overview \uf0c1 Directives \uf0c1 Beancount is a declarative language. The input consists of a text file containing mainly a list of directives , or entries (we use these terms interchangeably in the code and documentation); there is also syntax for defining various options . Each directive begins with an associated date , which determines the point in time at which the directive will apply, and its type , which defines which kind of event this directive represents. All the directives begin with a syntax that looks like this: YYYY-MM-DD \u2026 where YYYY is the year, MM is the numerical month, and DD the numerical date. All digits are required, for example, the 7th of May 2007 should be \u201c2007-05-07\u201d, including its zeros. Beancount supports the international ISO 8601 standard format for dates, with dashes (e.g., \u201c2014-02-03\u201d), or the same ordering with slashes (e.g., \u201c2014/02/03\u201d). Here are some example directives, just to give you an idea of the aesthetics: 2014-02-03 open Assets:US:BofA:Checking 2014-04-10 note Assets:US:BofA:Checking \"Called to confirm wire transfer.\" 2014-05-02 balance Assets:US:BofA:Checking 154.20 USD The end product of a parsed input file is a simple list of these entries, in a data structure. All operations in Beancount are performed on these entries. Each particular directive type is documented in a section below. Ordering of Directives \uf0c1 The order of declaration of the directives is not important. In fact, the entries are re-sorted chronologically after parsing and before being processed. This is an important feature of the language, because it makes it possible for you to organize your input file any way you like without having to worry about affecting the meaning of the directives. Except for transactions, each directive is assumed to occur at the beginning of each day. For example, you could declare an account being opened on the same day as its first transaction: 2014-02-03 open Assets:US:BofA:Checking 2014-02-03 * \"Initial deposit\" Assets:US:BofA:Checking 100 USD Assets:Cash -100 USD However, if you hypothetically closed that account immediately, you could not declare it closed on the same day, you would have to fudge the date forward by declaring the close on 2/4: 2014-02-04 close Assets:US:BofA:Checking This also explains why balance assertions are verified before any transactions that occur on the same date. This is for consistency. Accounts \uf0c1 Beancount accumulates commodities in accounts. The names of these accounts do not have to be declared before being used in the file, they are recognized as \u201caccounts\u201d by virtue of their syntax alone 1 . An account name is a colon-separated list of capitalized words which begin with a letter, and whose first word must be one of five account types: Assets Liabilities Equity Income Expenses Each component of the account names begin with a capital letter or a number and are followed by letters, numbers or dash (-) characters. All other characters are disallowed. Here are some realistic example account names: Assets:US:BofA:Checking Liabilities:CA:RBC:CreditCard Equity:Retained-Earnings Income:US:Acme:Salary Expenses:Food:Groceries The set of all names of accounts seen in an input file implicitly define a hierarchy of accounts (sometimes called a chart-of-accounts ), similarly to how files are organized in a file system. For example, the following account names: Assets:US:BofA:Checking Assets:US:BofA:Savings Assets:US:Vanguard:Cash Assets:US:Vanguard:RGAGX Assets:Receivables implicitly declare a tree of accounts that looks like this: `-- Assets |-- Receivables `-- US |-- BofA | |-- Checking | `-- Savings `-- Vanguard |-- Cash `-- RGAGX We would say that \u201c Assets:US:BofA \u201d is the parent account of \u201c Assets:US:BofA:Checking \u201d, and that the latter is a child account of the former. Commodities / Currencies \uf0c1 Accounts contain currencies , which we sometimes also call commodities (we use both terms interchangeably). Like account names, currency names are recognized by their syntax, though, unlike account names, they need not be declared before being used). The syntax for a currency is a word all in capital letters, like these: USD CAD EUR MSFT IBM AIRMILE (Technically, a currency name may be up to 24 characters long, and it must start with a capital letter, must end with with a capital letter or number, and its other characters must only be capital letters, numbers, or punctuation limited to these characters: \u201c'._-\u201d (apostrophe, period, underscore, dash.) The first three might evoke real world currencies to you (US dollars, Canadian dollars, Euros); the next two, stock ticker names (Microsoft and IBM). And the last: rewards points (airmiles). Beancount knows of no such thing; from its perspective all of these instruments are treated similarly. There is no built-in notion of any previously existing currency. These currency names are just names of \u201cthings\u201d that can be put in accounts and accumulated in inventories associated with these accounts. There is something elegant about the fact that there is no \u201cspecial\u201d currency unit, that all commodities are treated equally the same: Beancount is inherently a multi-currency system. You will appreciate this if, like many of us, you are an expat and your life is divided between two or three continents. You can handle an international ledger of accounts without any problems. And your use of currencies can get quite creative: you can create a currency for your home, for example (e.g. MYLOFT ), a currency to count accumulated vacation hours ( VACHR ), or a currency to count potential contributions to your retirement accounts allowed annually ( IRAUSD ). You can actually solve many problems this way. The cookbook describes many such concrete examples. Beancount does not support the dollar sign syntax, e.g., \u201c$120.00\u201d. You should always use names for currencies in your input file. This makes the input more regular and is a design choice. For monetary units, I suggest that you use the standard ISO 4217 currency code as a guideline; these quickly become familiar. However, as detailed above, you may include some other characters in currency names, like underscores (_), dashes (-), periods (.), or apostrophes (\u2018), but no spaces. Finally, you will notice that there exists a \u201c commodity \u201d directive that can be used to declare currencies. It is entirely optional: currencies come into being as you use them. The purpose of the directive is simply to attach metadata to it. Strings \uf0c1 Whenever we need to insert some free text as part of an entry, it should be surrounded by double-quotes. This applies to the payee and narration fields, mainly; basically anything that\u2019s not a date, a number, a currency, an account name. Strings may be split over multiple lines. (Strings with multiple lines will include their newline characters and those need to be handled accordingly when rendering.) Comments \uf0c1 The Beancount input file isn\u2019t intended to contain only your directives: you can be liberal in placing comments and headers in it to organize your file. Any text on a line after the character \u201c;\u201d is ignored, text like this: ; I paid and left the taxi, forgot to take change, it was cold. 2015-01-01 * \"Taxi home from concert in Brooklyn\" Assets:Cash -20 USD ; inline comment Expenses:Taxi You can use one or more \u201c;\u201d characters if you like. Prepend on all lines if you want to enter a larger comment text. If you prefer to have the comment text parsed in and rendered in your journals, see the Note directive elsewhere in this document. Any line that does not begin as a valid Beancount syntax directive (e.g. with a date) is silently ignored. This way you can insert markup to organize your file for various outline modes, such as org-mode in Emacs. For example, you could organize your input file by institution like this and fold & unfold each of the sections independently,: * Banking ** Bank of America 2003-01-05 open Assets:US:BofA:Checking 2003-01-05 open Assets:US:BofA:Savings ;; Transactions follow \u2026 ** TD Bank 2006-03-15 open Assets:US:TD:Cash ;; More transactions follow \u2026 The unmatching lines are simply ignored. Note to visiting Ledger users: In Ledger, \u201c;\u201d is used both for marking comments and for attaching \u201cLedger tags\u201d (Beancount metadata) to postings. This is not the case in Beancount. In Beancount comments are always just comments. Metadata has its own separate syntax. Directives \uf0c1 For a quick reference & overview of directive syntax, please consult the Syntax Cheat Sheet . Open \uf0c1 All accounts need to be declared \u201copen\u201d in order to accept amounts posted to them. You do this by writing a directive that looks like this: 2014-05-01 open Liabilities:CreditCard:CapitalOne USD The general format of the Open directive is: YYYY-MM-DD open Account [ConstraintCurrency,...] [\"BookingMethod\"] The comma-separated optional list of constraint currencies enforces that all changes posted to this account are in units of one of the declared currencies. Specifying a currency constraint is recommended: the more constraints you provide Beancount with, the less likely you will be to make data entry mistakes because if you do, it will warn you about it. Each account should be declared \u201copened\u201d at a particular date that precedes (or is the same as) the date of the first transaction that posts an amount to that account. Just to be clear: an Open directive does not have to appear before transactions in the file , but rather, the date of the Open directive must precede the date of postings to that account. The order of declarations in the file is not important. So for example, this is a legal input file: 2014-05-05 * \"Using my new credit card\" Liabilities:CreditCard:CapitalOne -37.45 USD Expenses:Restaurant 2014-05-01 open Liabilities:CreditCard:CapitalOne USD 1990-01-01 open Expenses:Restaurant Another optional declaration for opening accounts is the \u201cbooking method\u201d, which is the algorithm that will be invoked if a reducing lot leads to an ambiguous choice of matching lots from the inventory (0, 2 or more lots match). Currently, the possible values it can take are: STRICT : The lot specification has to match exactly one lot. This is the default method. If this booking method is invoked, it will simply raise an error. This ensures that your input file explicitly selects all matching lots. NONE : No lot matching is performed. Lots of any price will be accepted. A mix of positive and negative numbers of lots for the same currency is allowed. (This is similar to how Ledger treats matching\u2026 it ignores it.) Close \uf0c1 Similarly to the Open directive, there is a Close directive that can be used to tell Beancount that an account has become inactive, for example: ; Closing credit card after fraud was detected. 2016-11-28 close Liabilities:CreditCard:CapitalOne The general format of the Close directive is: YYYY-MM-DD close Account This directive is used in a couple of ways: An error message is raised if you post amounts to that account after its closing date (it's a sanity check). This helps avoid mistakes in data entry. It helps the reporting code figure out which accounts are still active and filter out closed accounts outside of the reporting period. This is especially useful as your ledger accumulates much data over time, as there will be accounts that stop existing and which you just don't want to see in reports for years that follow their closure. Note that a Close directive does not currently generate an implicit zero balance check. You may want to add one just before the closing date to ensure that the account is correctly closed with empty contents. At the moment, once an account is closed, you cannot reopen it after that date. (Though you can, of course, delete or comment-out the directive that closed it.) Finally, there are utility functions in the code that allow you to establish which accounts are open on a particular date. I strongly recommend that you close accounts when they actually close in reality, it will keep your ledger more tidy. Commodity \uf0c1 There is a \u201cCommodity\u201d directive that can be used to declare currencies, financial instruments, commodities (different names for the same thing in Beancount): 1867-07-01 commodity CAD The general format of the Commodity directive is: YYYY-MM-DD commodity Currency This directive is a late arrival, and is entirely optional: you can use commodities without having to really declare them this way. The purpose of this directive is to attach commodity-specific metadata fields on it, so that it can be gathered by plugins later on. For example, you might want to provide a long descriptive name for each commodity, such as \u201cSwiss Franc\u201d for commodity \u201cCHF\u201d, or \u201cHooli Corporation Class C Shares\u201d for \u201cHOOL\u201d, like this: 1867-07-01 commodity CAD name: \"Canadian Dollar\" asset-class: \"cash\" 2012-01-01 commodity HOOL name: \"Hooli Corporation Class C Shares\" asset-class: \"stock\" For example, a plugin could then gather the metadata attribute names and perform some aggregations per asset class. You can use any date for a commodity\u2026 but a relevant date is the date at which it was created or introduced, e.g. Canadian dollars were first introduced in 1867, ILS (new Israeli Shekels) are in use since 1986-01-01. For a company, the date the corporation was formed and shares created could be a good date. Since the main purpose of this directive is to collect per-commodity information, the particular date you choose doesn\u2019t matter all that much. It is an error to declare the same commodity twice. Transactions \uf0c1 Transactions are the most common type of directives that occur in a ledger. They are slightly different to the other ones, because they can be followed by a list of postings. Here is an example: 2014-05-05 txn \"Cafe Mogador\" \"Lamb tagine with wine\" Liabilities:CreditCard:CapitalOne -37.45 USD Expenses:Restaurant As for all the other directives, a transaction directive begins with a date in the YYYY-MM-DD format and is followed by the directive name, in this case, \u201c txn \u201d. However, because transactions are the raison d\u2019\u00eatre for our double-entry system and as such are by far the most common type of directive that should appear in a Beancount input file, we make a special case and allow the user to elide the \u201ctxn\u201d keyword and just use a flag instead of it: 2014-05-05 * \"Cafe Mogador\" \"Lamb tagine with wine\" Liabilities:CreditCard:CapitalOne -37.45 USD Expenses:Restaurant A flag is used to indicate the status of a transaction, and the particular meaning of the flag is yours to define. We recommend using the following interpretation for them: *: Completed transaction, known amounts, \u201cthis looks correct.\u201d !: Incomplete transaction, needs confirmation or revision, \u201cthis looks incorrect.\u201d In the case of the first example using \u201ctxn\u201d to leave the transaction unflagged, the default flag (\u201c*\u201d) will be set on the transaction object. (I nearly always use the \u201c*\u201d variant and never the keyword one, it is mainly there for consistency with all the other directive formats.) You can also attach flags to the postings themselves, if you want to flag one of the transaction\u2019s legs in particular: 2014-05-05 * \"Transfer from Savings account\" Assets:MyBank:Checking -400.00 USD ! Assets:MyBank:Savings This is useful in the intermediate stage of de-duping transactions (see Getting Started document for more details). The general format of a Transaction directive is: YYYY-MM-DD [txn|Flag] [[Payee] Narration] [Flag] Account Amount [{Cost}] [@ Price] [Flag] Account Amount [{Cost}] [@ Price] ... The lines that follow the first line are for \u201cPostings.\u201d You can attach as many postings as you want to a transaction. For example, a salary entry might look like this: 2014-03-19 * \"Acme Corp\" \"Bi-monthly salary payment\" Assets:MyBank:Checking 3062.68 USD ; Direct deposit Income:AcmeCorp:Salary -4615.38 USD ; Gross salary Expenses:Taxes:TY2014:Federal 920.53 USD ; Federal taxes Expenses:Taxes:TY2014:SocSec 286.15 USD ; Social security Expenses:Taxes:TY2014:Medicare 66.92 USD ; Medicare Expenses:Taxes:TY2014:StateNY 277.90 USD ; New York taxes Expenses:Taxes:TY2014:SDI 1.20 USD ; Disability insurance The Amount in \u201cPostings\u201d can also be an arithmetic expression using ( ) * / - + . For example, 2014-10-05 * \"Costco\" \"Shopping for birthday\" Liabilities:CreditCard:CapitalOne -45.00 USD Assets:AccountsReceivable:John ((40.00/3) + 5) USD Assets:AccountsReceivable:Michael 40.00/3 USD Expenses:Shopping The crucial and only constraint on postings is that the sum of their balance amounts must be zero. This is explained in full detail below. Metadata \uf0c1 It\u2019s also possible to attach metadata to the transaction and/or any of its postings, so the fully general format is: YYYY-MM-DD [txn|Flag] [[Payee] Narration] [Key: Value] ... [Flag] Account Amount [{Cost}] [@ Price] [Key: Value] ... [Flag] Account Amount [{Cost}] [@ Price] [Key: Value] ... ... See the dedicated section on metadata below. Payee & Narration \uf0c1 A transaction may have an optional \u201cpayee\u201d and/or a \u201cnarration.\u201d In the first example above, the payee is \u201cCafe Mogador\u201d and the narration is \u201cLamb tagine with wine\u201d. The payee is a string that represents an external entity that is involved in the transaction. Payees are sometimes useful on transactions that post amounts to Expense accounts, whereby the account accumulates a category of expenses from multiple businesses. A good example is \u201c Expenses:Restaurant \u201d, which will include all postings for the various restaurants one might visit. A narration is a description of the transaction that you write. It can be a comment about the context, the person who accompanied you, some note about the product you bought... whatever you want it to be. It\u2019s for you to insert whatever you like. I like to insert notes when I reconcile, it\u2019s quick and I can refer to my notes later on, for example, to answer the question \u201cWhat was the name of that great little sushi place I visited with Andreas on the West side last winter?\u201d If you place a single string on a transaction line, it becomes its narration: 2014-05-05 * \"Lamb tagine with wine\" \u2026 If you want to set just a payee, put an empty narration string: 2014-05-05 * \"Cafe Mogador\" \"\" \u2026 For legacy reasons, a pipe symbol (\u201c|\u201d) is accepted between those strings (but this will be removed at some point in the future): 2014-05-05 * \"Cafe Mogador\" | \"\" \u2026 You may also leave out either (but you must provide a flag): 2014-05-05 * \u2026 Note for Ledger users. Ledger does not have separate narration and payee fields, it has only one field, which is referred to by the \u201cPayee\u201d metadata tag, and the narration ends up in a saved comment (a \u201cpersistent note\u201d). In Beancount, a Transaction object simply has two fields: payee and narration, where payee just happens to have an empty value a lot of the time. For a deeper discussion of how and when to use payees or not, see Payees, Subaccounts, and Assets . Costs and Prices \uf0c1 Postings represent a single amount being deposited to or withdrawn from an account. The simplest type of posting includes only its amount: 2012-11-03 * \"Transfer to pay credit card\" Assets:MyBank:Checking -400.00 USD Liabilities:CreditCard 400.00 USD If you converted the amount from another currency, you must provide a conversion rate to balance the transaction (see next section). This is done by attaching a \u201cprice\u201d to the posting, which is the rate of conversion: 2012-11-03 * \"Transfer to account in Canada\" Assets:MyBank:Checking -400.00 USD @ 1.09 CAD Assets:FR:SocGen:Checking 436.01 CAD You could also use the \u201c@@\u201d syntax to specify the total cost: 2012-11-03 * \"Transfer to account in Canada\" Assets:MyBank:Checking -400.00 USD @@ 436.01 CAD Assets:FR:SocGen:Checking 436.01 CAD Beancount will automatically compute the per-unit price, that is 1.090025 CAD (note that the precision will differ between the last two examples). After the transaction, we are not interested in keeping track of the exchange rate for the units of USD deposited into the account; those units of \u201cUSD\u201d are simply deposited. In a sense, the rate at which they were converted at has been forgotten. However, some commodities that you deposit in accounts must be \u201cheld at cost.\u201d This happens when you want to keep track of the cost basis of those commodities. The typical use case is investing, for example when you deposit shares of a stock in an account. What you want in that circumstance is for the acquisition cost of the commodities you deposit to be attached to the units, such that when you remove those units later on (when you sell), you should be able to identify by cost which of those units to remove, in order to control the effect of taxes (and avoid errors). You can imagine that for each of the units in the account there is an attached cost basis. This will allow us to automatically compute capital gains later. In order to specify that a posting to an account is to be held at a specific cost, include the cost in curly brackets: 2014-02-11 * \"Bought shares of S&P 500\" Assets:ETrade:IVV 10 IVV {183.07 USD} Assets:ETrade:Cash -1830.70 USD This is the subject of a deeper discussion. Refer to \u201c How Inventories Work \u201d and \u201c Trading with Beancount \u201d documents for an in-depth discussion of these topics. Finally, you can include both a cost and a price on a posting: 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 The price will only be used to insert a price entry in the prices database (see Price section below). This is discussed in more details in the Balancing Postings section of this document. Important Note. Amounts specified as either per-share or total prices or costs are always unsigned . It is an error to use a negative sign or a negative cost and Beancount will raise an error if you attempt to do so. Balancing Rule - The \u201cweight\u201d of postings \uf0c1 A crucial aspect of the double-entry method is ensuring that the sum of all the amounts on its postings equals ZERO, in all currencies. This is the central, non-negotiable condition that engenders the accounting equation, and makes it possible to filter any subset of transactions and drawing balance sheets that balance to zero. But with different types of units, the previously introduced price conversions and units \u201cheld at cost\u201d, what does it all mean? It\u2019s simple: we have devised a simple and clear rule for obtaining an amount and a currency from each posting, to be used for balancing them together. We call this the \u201cweight\u201d of a posting, or the balancing amount. Here is a short example of weights derived from postings using the four possible types of cost/price combinations: YYYY-MM-DD Account 10.00 USD -> 10.00 USD Account 10.00 CAD @ 1.01 USD -> 10.10 USD Account 10 SOME {2.02 USD} -> 20.20 USD Account 10 SOME {2.02 USD} @ 2.50 USD -> 20.20 USD Here is the explanation of how it is calculated: If the posting has only an amount and no cost, the balance amount is just the amount and currency on that posting. Using the first example from the previous section, the amount is -400.00 USD, and that is balanced against the 400.00 USD of the second leg. If the posting has only a price , the price is multiplied by the number of units and the price currency is used. In the second example from the preceding section, that is -400.00 USD x 1.09 CAD(/USD) = -436.00 CAD, and that is balanced against the other posting of 436.00 CAD 2 . If the posting has a cost , the cost is multiplied by the number of units and the cost currency is used. In the third example from the preceding section, that is 10 IVV x 183.08 USD(/IVV) = 1830.70 USD. That is balanced against the cash leg of -1830.70 USD, so all is good. Finally, if a posting has both a cost and a price , we simply ignore the price. This optional price is used later on to generate an entry in the in-memory prices database, but it is not used in balancing at all. With this rule, you should be able to easily balance all your transactions. Moreover, this rule makes it possible to let Beancount automatically calculate capital gains for you (see Trading with Beancount for details). Reducing Positions \uf0c1 When you post a reduction to a position in an account, the reduction must always match an existing lot. For example, if an account holds 3200 USD and a transaction posts a -1200 USD change to that account, the 1200 USD match against the existing 3200 USD, and the result is a single position of 2000 USD. This also works for negative values. For example, if an account has a -1300 USD balance and you post a +2000 USD change to it, you obtain a 700 USD balance. A change posted to an account, regardless of the account type, can result in a positive or negative balance; there are no limitations on the balances of simple commodity amounts (that is, those with no cost associated to them). For example, while Assets accounts normally have a positive balance and Liabilities accounts usually a negative one, you can legally credit an Assets account to a negative balance, or debit a Liabilities account to a positive balance. This is because in the real world these things do happen: you might write a check too many and obtain temporary credit from your bank\u2019s checking account (usually along with an outrageous \u201coverdraft\u201d fee), or pay that credit card balance twice by mistake. For commodities held at cost, the cost specification of the posting must match one of the lots held in the inventory before the transaction is applied. The list of lots is gathered, and matched against the specification in the {...} part of the posting. For example, if you provide a cost, only those lots whose cost match that will remain. If you provide a date, only those lots which match that date will remain. And you can use a label as well. If you provide a cost and a date, both of these are matched against the list of candidate lots to reduce. This is essentially a filter on the list of lots. If the filtered list results in a single lot, that lot is chosen to be reduced. If the list results in multiple lots, but the total amount being reduced equals the total amount in the lots, all those lots are reduced by that posting. For example, if in the past you had the following transactions: 2014-02-11 * \"Bought shares of S&P 500\" Assets:ETrade:IVV 20 IVV {183.07 USD, \"ref-001\"} \u2026 2014-03-22 * \"Bought shares of S&P 500\" Assets:ETrade:IVV 15 IVV {187.12 USD} \u2026 Each of the following reductions would be unambiguous: 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {183.07 USD} \u2026 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {2014-02-11} \u2026 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {\"ref-001\"} \u2026 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -35 IVV {} \u2026 However, the following would be ambiguous: 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {} \u2026 If multiple lots match against the reducing posting and their number is not the total number, we are in a situation of ambiguous matches . What happens then, is that the account\u2019s booking method is invoked. There are multiple booking methods, but by default, all accounts are set to use the \u201cSTRICT\u201d booking method. This method simply issues an error in an ambiguous situation. You may set the account\u2019s booking method to \u201cFIFO\u201d to instruct Beancount to select the oldest of the lots. Or \u201cLIFO\u201d for the latest (youngest) of the lots. This will automatically select all the necessary matching lots to fulfill the reduction. PLEASE NOTE! Requiring the dates to match will be dealt with more sensibly in the near future. See A Proposal for an Improvement on Inventory Booking for details of this upcoming change. For such postings, a change that results in a negative number of units is usually impossible. Beancount does not currently allow holding a negative number of a commodity held at cost. For example, an input with just this transaction will fail: 2014-05-23 * Assets:Investments:MSFT -10 MSFT {43.40 USD} Assets:Investments:Cash 434.00 USD If it did not, this would result in a balance of -10 units of MSFT. On the other hand, if the account had a balance of 12 units of MSFT held at 43.40 USD on 5/23, the transaction would book just fine, reducing the existing 12 units to 2. Most often, the error that will occur is that the account will be holding a balance of 10 or more units of MSFT at a different cost , and the user will specify an incorrect value for the cost. For instance, if the account had a positive balance of 20 MSFT {42.10 USD}, the transaction above would still fail, because there aren\u2019t 10 or more units of MSFT at 43.40 USD to remove from. This constraint is enforced for a few reasons: Mistakes in data entry for stock units are not uncommon, they have an important negative impact on the correctness of your Ledger\u2014the amounts are usually large\u2014and they will typically trigger an error from this constraint. Therefore, the error check is a useful way to detect these types of errors. Negative numbers of units held at cost are fairly rare. Chances are you don\u2019t need them at all. Exceptions include: short sales of stock, holding spreads on futures contracts, and depending on how you account for them, short positions in currency trading. This is why this check is enabled by default. PLEASE NOTE! In a future version of Beancount, we will relax this constraint somewhat. We will allow an account to hold a negative number of units of a commodity if and only if there are no other units of that commodity held in the account. Either that, or we will allow you to mark an account as having no such constraints at all. The purpose is to allow the account of short positions in commodities. The only blocking factor is this constraint. For more details of the inventory booking algorithm, see the How Inventories Work document. Amount Interpolation \uf0c1 Beancount is able to fill in some of the details of a transaction automatically. You can currently elide the amount of at most one posting within a transaction: 2012-11-03 * \"Transfer to pay credit card\" Assets:MyBank:Checking -400.00 USD Liabilities:CreditCard In the example above, the amount of the credit card posting has been elided. It is automatically calculated by Beancount at 400.00 USD to balance the transaction. This also works with multiple postings, and with postings with costs: 2014-07-11 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -10 IVV {183.07 USD} Assets:ETrade:Cash 1979.90 USD Income:ETrade:CapitalGains In this case, the units of IVV are sold at a higher price ($197.90) than they were bought for ($183.07). The cash first posting has a weight of -10 x 183.07 = -1830.70 and the second posting a straightforward $1979.90. The last posting will be ascribed the difference, that is, a balance of -149.20 USD , which is to say, a gain of $149.20. When calculating the amount to be balanced, the same balance amounts that are used to check that the transaction balances to zero are used to fill in the missing amounts. For example, the following would not trigger an error: 2014-07-11 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -10 IVV {183.07 USD} @ 197.90 USD Assets:ETrade:Cash The cash account would receive 1830.70 USD automatically, because the balance amount of the IVV posting is -1830.70 USD (if a posting has both a cost and a price, the cost basis is always used and the optional price ignored). While this is accepted and correct from a balancing perspective, this would be incomplete from an accounting perspective: the capital gain on a sale needs to be accounted for separately and besides, the amount deposited to the cash account if you did as above would fall short of the real deposit (1979.00 USD) and hopefully a subsequent balance assertion in the cash account would indicate this oversight by triggering an error. Finally, this also works when the balance includes multiple commodities: 2014-07-12 * \"Uncle Bob gave me his foreign currency collection!\" Income:Gifts -117.00 ILS Income:Gifts -3000.00 INR Income:Gifts -800.00 JPY Assets:ForeignCash Multiple postings (one for each commodity required to balance) will be inserted to replace the elided one. Tags \uf0c1 Transactions can be tagged with arbitrary strings: 2014-04-23 * \"Flight to Berlin\" #berlin-trip-2014 Expenses:Flights -1230.27 USD Liabilities:CreditCard This is similar to the popular idea of \u201chash tagging\u201d on Twitter and such. These tags essentially allow you to mark a subset of transactions. They can then be used as a filter to generate reports on only this subset of transactions. They have numerous uses. I like to use them to mark all my trips. Multiple tags can be specified as well: 2014-04-23 * \"Flight to Berlin\" #berlin-trip-2014 #germany Expenses:Flights -1230.27 USD Liabilities:CreditCard (If you want to store key-value pairs on directives, see the section on metadata below.) The Tag Stack \uf0c1 Oftentimes multiple transactions related to a single tag will be entered consecutively in a file. As a convenience, the parser can automatically tag transactions within a block of text. How this works is simple: the parser has a \u201cstack\u201d of current tags which it applies to all transactions as it reads them one-by-one. You can push and pop tags onto/from this stack, like this: pushtag #berlin-trip-2014 2014-04-23 * \"Flight to Berlin\" Expenses:Flights -1230.27 USD Liabilities:CreditCard poptag #berlin-trip-2014 This way, you can also push multiple tags onto a long, consecutive set of transactions without having to type them all in. Links \uf0c1 Transactions can also be linked together. You may think of the link as a special kind of tag that can be used to group together a set of financially related transactions over time. For example you may use links to group together transactions that are each related with a specific invoice. This allows to track payments (or write-offs) associated with the invoice: 2014-02-05 * \"Invoice for January\" ^invoice-pepe-studios-jan14 Income:Clients:PepeStudios -8450.00 USD Assets:AccountsReceivable 2014-02-20 * \"Check deposit - payment from Pepe\" ^invoice-pepe-studios-jan14 Assets:BofA:Checking 8450.00 USD Assets:AccountsReceivable Or track multiple transfers related to a single nefarious purpose: 2014-02-05 * \"Moving money to Isle of Man\" ^transfers-offshore-17 Assets:WellsFargo:Savings -40000.00 USD Assets:WellsFargo:Checking 40000.00 USD 2014-02-09 * \"Wire to FX broker\" ^transfers-offshore-17 Assets:WellsFargo:Checking -40025.00 USD Expenses:Fees:WireTransfers 25.00 USD Assets:OANDA:USDollar 40000.00 2014-03-16 * \"Conversion to offshore beans\" ^transfers-offshore-17 Assets:OANDA:USDollar -40000.00 USD Assets:OANDA:GBPounds 23391.81 GBP @ 1.71 USD 2014-03-16 * \"God save the Queen (and taxes)\" ^transfers-offshore-17 Assets:OANDA:GBPounds -23391.81 GBP Expenses:Fees:WireTransfers 15.00 GBP Assets:Brittania:PrivateBanking 23376.81 GBP Linked transactions can be rendered by the web interface in their own dedicated journal, regardless of the current view/filtered set of transactions (the list of links is a global page). Balance Assertions \uf0c1 A balance assertion is a way for you to input your statement balance into the flow of transactions. It tells Beancount to verify that the number of units of a particular commodity in some account should equal some expected value at some point in time. For instance, this 2014-12-26 balance Liabilities:US:CreditCard -3492.02 USD says \u201cCheck that the number of USD units in account \u201c Liabilities:US:CreditCard \u201d on the morning of December 26th, 2014 is -3492.02 USD.\u201d When processing the list of entries, if Beancount encounters a different balance than this for USD it will report an error. If no error is reported, you should have some confidence that the list of transactions that precedes it in this account is highly likely to be correct. This is useful in practice, because in many cases some transactions can get imported separately from the accounts of each of their postings (see the de-duping problem ). This can result in you booking the same transaction twice without noticing, and regularly inserting a balance assertion will catch that problem every time. The general format of the Balance directive is: YYYY-MM-DD balance Account Amount Note that a balance assertion, like all other non-transaction directives, applies at the beginning of its date (i.e., midnight at the start of day). Just imagine that the balance check occurs right after midnight on that day. Balance assertions only make sense on balance sheet accounts (Assets and Liabilities). Because the postings on Income and Expenses accounts are only interesting because of their transient value, i.e., for these accounts we\u2019re interested in sums of changes over a period of time (not the absolute value), it makes little sense to use a balance assertion on income statement accounts. Also, take note that each account benefits from an implicit balance assertion that the account is empty after it is opened at the date of its Open directive. You do not need to explicitly assert a zero balance when opening accounts. Multiple Commodities \uf0c1 A Beancount account may contain more than one commodity (although in practice, you will find that this does not occur often, it is sensible to create dedicated sub-accounts to hold each commodity, for example, holding a portfolio of stocks). A balance assertion applies only to the commodity of the assertion; it leaves the other commodities in the balance unchecked. If you want to check multiple commodities, use multiple balance assertions, like this: ; Check cash balances from wallet 2014-08-09 balance Assets:Cash 562.00 USD 2014-08-09 balance Assets:Cash 210.00 CAD 2014-08-09 balance Assets:Cash 60.00 EUR There is currently no way to exhaustively check the full list of commodities in an account ( a proposal is underway ). Note that in this example if an exhaustive check really matters to you, you could circumvent by defining a subaccount of the cash account to segregate each commodity separately, like this Assets:Cash:USD , Assets:Cash:CAD . Lots Are Aggregated \uf0c1 The balance assertion applies to the sum of units of a particular commody, irrespective of their cost. For example, if you hold three lots of the same commodity in an account, for example, 5 HOOL {500 USD} and 6 HOOL {510 USD}, the following balance check should succeed: 2014-08-09 balance Assets:Investing:HOOL 11 HOOL All the lots are aggregated together and you can verify their number of units. Checks on Parent Accounts \uf0c1 Balance assertions may be performed on parent accounts, and will include the balances of theirs and their sub-accounts: 2014-01-01 open Assets:Investing 2014-01-01 open Assets:Investing:Apple AAPL 2014-01-01 open Assets:Investing:Amazon AMZN 2014-01-01 open Assets:Investing:Microsoft MSFT 2014-01-01 open Equity:Opening-Balances 2014-06-01 * Assets:Investing:Apple 5 AAPL {578.23 USD} Assets:Investing:Amazon 5 AMZN {346.20 USD} Assets:Investing:Microsoft 5 MSFT {42.09 USD} Equity:Opening-Balances 2014-07-13 balance Assets:Investing 5 AAPL 2014-07-13 balance Assets:Investing 5 AMZN 2014-07-13 balance Assets:Investing 5 MSFT Note that this does require that a parent account have been declared as Open, in order to be legitimately used in the balance assertions directive. Before Close \uf0c1 It is useful to insert a balance assertion for 0 units just before closing an account, just to make sure its contents are empty as you close it. The Close directive does not insert that for you automatically (we may eventually build a plug-in for it). Local Tolerance \uf0c1 It's pretty common that sometimes one needs to override the tolerance on the balance check to loosen it on that balance assertion. This can be done using a local tolerance amount off of the balance amount, like this: 2013-09-20 balance Assets:Investing:Funds 319.020 ~ 0.002 RGAGX Pad \uf0c1 A padding directive automatically inserts a transaction that will make the subsequent balance assertion succeed, if it is needed. It inserts the difference needed to fulfill that balance assertion. (What \u201crubber space\u201d is in LaTeX, Pad directives are to balances in Beancount.) Note that by \u201csubsequent,\u201d I mean in date order , not in the order of the declarations in the file. This is the conceptual equivalent of a transaction that will automatically expand or contract to fill the difference between two balance assertions over time. It looks like this: 2014-06-01 pad Assets:BofA:Checking Equity:Opening-Balances The general format of the Pad directive is: YYYY-MM-DD pad Account AccountPad The first account is the account to credit the automatically calculated amount to. This is the account that should have a balance assertion following it (if the account does not have a balance assertion, the pad entry is benign and does nothing). The second account is for the other leg of the transaction, it is the source where the funds will come from, and this is almost always some Equity account. The reason for this is that this directive is generally used for initializing the balances of new accounts, to save us from having to either insert such a directive manually, or from having to enter the full past history of transactions that will bring the account to its current balance. Here is a realistic example usage scenario: ; Account was opened way back in the past. 2002-01-17 open Assets:US:BofA:Checking 2002-01-17 pad Assets:US:BofA:Checking Equity:Opening-Balances 2014-07-09 balance Assets:US:BofA:Checking 987.34 USD This will result in the following transaction being inserted right after the Pad directive, on the same date: 2002-01-17 P \"(Padding inserted for balance of 987.34 USD)\" Assets:US:BofA:Checking 987.34 USD Equity:Opening-Balances -987.34 USD This is a normal transaction\u2014you will see it appear in the rendered journals along with the other ones. (Note the special \u201cP\u201d flag, which can be used by scripts to find these.) Observe that without balance assertions, Pad directives make no sense. Therefore, like balance assertions, they are normally only used on balance sheet accounts (Assets and Liabilities). You could also insert Pad entries between balance assertions, it works too. For example: 2014-07-09 balance Assets:US:BofA:Checking 987.34 USD \u2026 more transactions\u2026 2014-08-08 pad Assets:US:BofA:Checking Equity:Opening-Balances 2014-08-09 balance Assets:US:BofA:Checking 1137.23 USD Without intervening transactions, this would insert the following padding transaction: 2014-08-08 P \"(Padding inserted for balance of 1137.23 USD)\" Assets:US:BofA:Checking 149.89 USD Equity:Opening-Balances -149.89 USD In case that\u2019s not obvious, 149.89 USD is the difference between 1137.23 USD and 987.34 USD. If there were more intervening transactions posting amounts to the checking account, the amount would automatically have been adjusted to make the second assertion pass. Unused Pad Directives \uf0c1 You may not currently leave unused Pad directives in your input file. They will trigger an error: 2014-01-01 open Assets:US:BofA:Checking 2014-02-01 pad Assets:US:BofA:Checking Equity:Opening-Balances 2014-06-01 * \"Initializing account\" Assets:US:BofA:Checking 212.00 USD Equity:Opening-Balances 2014-07-09 balance Assets:US:BofA:Checking 212.00 USD (Being this strict is a matter for debate, I suppose, and it could eventually be moved to an optional plugin.) Commodities \uf0c1 Note that the Pad directive does not specify any commodities at all. All commodities with corresponding balance assertions in the account are affected. For instance, the following code would have a padding directive insert a transaction with separate postings for USD and CAD: 2002-01-17 open Assets:Cash 2002-01-17 pad Assets:Cash Equity:Opening-Balances 2014-07-09 balance Assets:Cash 987.34 USD 2014-07-09 balance Assets:Cash 236.24 CAD If the account contained other commodities that aren\u2019t balance asserted, no posting would be inserted for those. Cost Basis \uf0c1 At the moment, Pad directives do not work with accounts holding positions held at cost. The directive is really only useful for cash accounts. (This is mainly because balance assertions do not yet allow specifying a cost basis to assert. It is possible that in the future we decide to support asserting the total cost basis, and that point we could consider supporting padding with cost basis.) Multiple Paddings \uf0c1 You cannot currently insert multiple padding entries for the same account and commodity: 2002-01-17 open Assets:Cash 2002-02-01 pad Assets:Cash Equity:Opening-Balances 2002-03-01 pad Assets:Cash Equity:Opening-Balances 2014-04-19 balance Assets:Cash 987.34 USD (There is a proposal pending to allow this and spread the padding amount evenly among all the intervening Pad directives, but it is as of yet unimplemented.) Notes \uf0c1 A Note directive is simply used to attach a dated comment to the journal of a particular account, like this: 2013-11-03 note Liabilities:CreditCard \"Called about fraudulent card.\" When you render the journal, the note should be rendered in context. This can be useful to record facts and claims associated with a financial event. I often use this to record snippets of information that would otherwise not make their way to a transaction. The general format of the Note directive is: YYYY-MM-DD note Account Description The description string may be split among multiple lines. Documents \uf0c1 A Document directive can be used to attach an external file to the journal of an account: 2013-11-03 document Liabilities:CreditCard \"/home/joe/stmts/apr-2014.pdf\" The filename gets rendered as a browser link in the journals of the web interface for the corresponding account and you should be able to click on it to view the contents of the file itself. This is useful to integrate account statements and other downloads into the flow of accounts, so that they\u2019re easily accessible from a few clicks. Scripts could also be written to obtain this list of documents from an account name and do something with them. The general format of the Document directive is: YYYY-MM-DD document Account PathToDocument Documents from a Directory \uf0c1 A more convenient way to create these entries is to use a special option to specify directories that contain a hierarchy of sub-directories that mirrors that of the chart of accounts. For example, the Document directive shown in the previous section could have been created from a directory hierarchy that looks like this: stmts `-- Liabilities `-- CreditCard `-- 2014-04-27.apr-2014.pdf By simply specifying the root directory as an option (note the absence of a trailing slash): option \"documents\" \"/home/joe/stmts\" The files that will be picked up are those that begin with a date as above, in the YYYY-MM-DD format. You may specify this option multiple times if you have many such document archives. In the past I have used one directory for each year (if you scan all your documents, the directory can grow to a large size, scanned documents tend to be large files). Organizing your electronic document statements and scans using the hierarchy of your ledger\u2019s accounts is a fantastic way to organize them and establish a clear, unambiguous place to find these documents later on, when they\u2019re needed. Prices \uf0c1 Beancount sometimes creates an in-memory data store of prices for each commodity, that is used for various reasons. In particular, it is used to report unrealized gains on account holdings. Price directives can be used to provide data points for this database. A Price directive establishes the rate of exchange between one commodity (the base currency) and another (the quote currency): 2014-07-09 price HOOL 579.18 USD This directive says: \u201cThe price of one unit of HOOL on July 9th, 2014 was 579.18 USD.\u201d Price entries for currency exchange rates work the same way: 2014-07-09 price USD 1.08 CAD The general format of the Price directive is: YYYY-MM-DD price Commodity Price Remember that Beancount knows nothing about what HOOL, USD or CAD are. They are opaque \u201cthings.\u201d You attach meaning to them. So setting a price per hour for your vacation hours is perfectly valid and useful too, if you account for your unused vacations on such terms: 2014-07-09 price VACHR 38.46 USD ; Equiv. $80,000 year Prices from Postings \uf0c1 If you use the beancount.plugins.implicit_prices plugin, every time a Posting appears that has a cost or an optional price declared, it will use that cost or price to automatically synthesize a Price directive. For example, this transaction: 2014-05-23 * Assets:Investments:MSFT -10 MSFT {43.40 USD} Assets:Investments:Cash 434.00 USD automatically becomes this after parsing: 2014-05-23 * Assets:Investments:MSFT -10 MSFT {43.40 USD} Assets:Investments:Cash 434.00 USD 2014-05-23 price MSFT 43.40 USD This is convenient and if you enable it, will probably be where most of the price points in your ledger\u2019s price database will come from. You can print a table of the parsed prices from a ledger (it is just another type of report). Prices on the Same Day \uf0c1 Notice that there is no notion of time; Beancount is not designed to solve problems for intra-day traders, though of course, it certainly is able to handle multiple trades per day. It just stores its prices per day. (Generally, if you need many features that require a notion of intra-day time, you\u2019re better off using another system, this is not the scope of a bookkeeping system.) When multiple Price directives do exist for the same day, the last one to appear in the file will be selected for inclusion in the Price database. Events \uf0c1 Event directives 3 are used to track the value of some variable of your choice over time. For example, your location: 2014-07-09 event \"location\" \"Paris, France\" The above directive says: \u201cChange the value of the \u2018location\u2019 event to \u2018Paris, France\u2019 as of the 9th of July 2014 and onwards.\u201d A particular event type only has a single value per day. The general format of the Event directive is: YYYY-MM-DD event Name Value The event\u2019s Name string does not have to be declared anywhere, it just begins to exist the first time you use the directive. The Value string can be anything you like; it has no prescribed structure. How to use these is best explained by providing examples: Location : You can use events for tracking the city or country you\u2019re in. This is sensible usage within a ledger because the nature of your expenses are heavily correlated to where you are. It\u2019s convenient: if you use Beancount regularly, it is very little effort to add this bit of information to your file. I usually have a \u201ccash daybook\u201d section where I put miscellaneous cash expenses, and that\u2019s where I enter those directives. Address : If you move around a lot, it\u2019s useful to keep a record of your past home addresses. This is sometimes requested on government forms for immigration. The Green Card application process in the US, for instance, Employer : You can record your date of employment and departure for each job this way. Then you can count the number of days you worked there. Trading window : If you\u2019re an employee of a public company, you can record the dates that you\u2019re allowed to trade its stock. This can then be used to ensure you did not trade that stock when the window is closed. Events are often used to report on numbers of days. For example, in the province of Quebec (Canada) you are insured for free health care coverage if \u201c you spend 183 days of the calendar year or more, excluding trips of 21 days or less .\u201d If you travel abroad a lot, you could easily write a script to warn you of remaining day allocations to avoid losing your coverage. Many expats are in similar situations. In the same vein, the IRS recognizes US immigrants as \u201cresident alien\u201d\u2014and thus subject to filing taxes, yay!\u2014if you pass the Substantial Presence Test , which is defined as \u201cbeing physically present in the US on at least 31 days during the current year, and 193 days during the 3 year period that includes the current year and the 2 years immediately before that, counting: all the days you were present in the current year, and \u2153 of the days of the year before that, and \u2159 of the year preceding that one.\u201d Ouch\u2026 my head hurts. This gets complicated. Armed with your location over time, you could report it automatically. Events will also be usable in the filtering language, to specify non-contiguous periods of time. For example, if you are tracking your location using Event directives, you could produce reports for transactions that occur only when you are in a specific location, e.g., \u201cShow me my expenses on all my trips to Germany,\u201d or \u201cGive me a list of payees for restaurant transactions when I\u2019m in Montreal.\u201d PLEASE NOTE! Filters haven\u2019t been implemented yet. Also, reports on events have not yet been re-implemented in Beancount 2.0. They will be reintroduced again soon, as well as filtering. It is worth noticing that the Price and Event directives are the only ones not associated to an account. Query \uf0c1 It can be convenient to be able to associate SQL queries in a Beancount file to be able to run these as a report automatically. This is still an early development / experimental directive. In any case, you can insert queries in the stream of transactions like this: 2014-07-09 query \"france-balances\" \" SELECT account, sum(position) WHERE \u2018trip-france-2014\u2019 in tags\" The grammar is YYYY-MM-DD query Name SqlContents Each query has a name, which will probably be used to invoke its running as a report type. Also, the date of the query is intended to be the date at which the query is intended to be run for, that is, transactions following it should be ignored. If you\u2019re familiar with the SQL syntax, it\u2019s an implicit CLOSE. Custom \uf0c1 The long-term plan for Beancount is to allow plugins and external clients to define their own directive types, to be declared and validated by the Beancount input language parser. In the meantime, a generic directive is provided for clients to prototype new features, e.g., budgeting. 2014-07-09 custom \"budget\" \"...\" TRUE 45.30 USD The grammar for this directive is flexible: YYYY-MM-DD custom TypeName Value1 ... The first argument is a string and is intended to be unique to your directive. Think of this as the type of your directive. Following it, you can put an arbitrary list of strings, dates, booleans, amounts, and numbers. Note that there is no validation that checks that the number and types of arguments following the TypeName is consistent for a particular type. (See this thread for the origin story around this feature.) Metadata \uf0c1 You may attach arbitrary data to each of your entries and postings. The syntax for it looks like this: 2013-03-14 open Assets:BTrade:HOOLI category: \"taxable\" 2013-08-26 * \"Buying some shares of Hooli\" statement: \"confirmation-826453.pdf\" Assets:BTrade:HOOLI 10 HOOL @ {498.45 USD} decision: \"scheduled\" Assets:BTrade:Cash In this example, a \u201ccategory\u201d attribute is attached to an account\u2019s Open directive, a \u201cstatement\u201d attribute is attached to a Transaction directive (with a string value that represents a filename) and a \u201cdecision\u201d attribute has been attached to the first posting of the transaction (the additional indentation from the posting is not strictly necessary but it helps with readability). Metadata can be attached to any directive type. Keys must begin with a lowercase character from a-z and may contain (uppercase or lowercase) letters, numbers, dashes and underscores. Moreover, the values can be any of the following data types: Strings Accounts Currency Dates (datetime.date) Tags Numbers (Decimal) Amount (beancount.core.amount.Amount) There are two ways in which this data can be used: Query tools that come with Beancount (such as bean-query) will allow you to make use of the metadata values for filtering and aggregation. You can access and use the metadata in custom scripts. The metadata values are accessible as a \u201c .meta \u201d attribute on all directives and is a Python dict. There are no special meanings attached to particular attributes, these are intended for users to define. However, all directives are guaranteed to contain a \u2018 filename \u2019 (string) and a \u2018 lineno \u2019 (integer) attribute which reflect the location they were created from. Finally, attributes without a value will be parsed and have a value of 'None'. If an attribute is repeated multiple times, only the first value for this attribute will be parsed and retained and the following values ignored. Options \uf0c1 The great majority of a Beancount input file consists in directives, as seen in the previous section. However, there are a few global options that can be set in the input file as well, by using a special undated \u201coption\u201d directive: option \"title\" \"Ed\u2019s Personal Ledger\" The general format of the Option directive is: option Name Value where Name and Value are both strings. Note that depending the option, the effect may be to set a single value, or add to a list of existing values. In other words, some options are lists. There are three ways to view the list of options: In this document , which I update regularly. To view the definitive list of options supported by your installed version, use the following command: bean-doctor list-options Finally, you can peek at the source code as well. Operating Currencies \uf0c1 One notable option is \u201c operating_currency \u201d. By default Beancount does not treat any of the commodities any different from each other. In particular, it doesn\u2019t know that there\u2019s anything special about the most common of commodities one uses: their currencies. For example, if you live in New Zealand, you\u2019re going to have an overwhelming number of NZD commodities in your transactions. But useful reports try to reduce all non-currency commodities into one of the main currencies used. Also, it\u2019s useful to break out the currency units into their own dedicated columns. This may also be useful for exporting in order to avoid having to specify the units for that column and import to a spreadsheet with numbers you can process. For this reason, you are able to declare the most common currencies you use in an option: option \"operating_currency\" \"USD\" You may declare more than one. In any case, this option is only ever used by reporting code, it never changes the behavior of Beancount\u2019s processing or semantics. Plugins \uf0c1 In order to load plugin Python modules, use the dedicated \u201cplugin\u201d directive: plugin \"beancount.plugins.module_name\" The name of a plugin should be the name of a Python module in your PYTHONPATH. Those modules will be imported by the Beancount loader and run on the list of parsed entries in order for the plugins to transform the entries or output errors. This allows you to integrate some of your code within Beancount, making arbitrary transformations on the entries. See Scripting & Plugins for details. Plugins also optionally accept some configuration parameters. These can be provided by an optional final string argument, like this: plugin \"beancount.plugins.module_name\" \"configuration data\" The general format of the Option directive is: plugin ModuleName StringConfig The format of the configuration data is plugin-dependent. At the moment, an arbitrary string is passed provided to the plugin. See the plugins\u2019 documentation for specific detail on what it can accept. Also see the \u201cplugin processing mode\u201d option which affects the list of built-in plugins that get run. Includes \uf0c1 Include directives are supported. This allows you to split up large input files into multiple files. The syntax looks like this: include \"path/to/include/file.beancount\" The general format is include Filename The specified path can be an absolute or a relative filename. If the filename is relative, it is relative to the including filename\u2019s directory. This makes it easy to put relative includes in a hierarchy of directories that can be placed under source control and checked out anywhere. Include directives are not processed strictly (as in C, for example). The include directives are accumulated by the Beancount parser and processed separately by the loader. This is possible because the order of declarations of a Beancount input file is not relevant. However, for the moment, options are parsed per-file. The options-map that is kept for post-parse processing is the options-map returned for the top-level file. This is probably subject to review in the future. What\u2019s Next? \uf0c1 This document described all the possible syntax of the Beancount language. If you haven\u2019t written any Beancount input yet, you can head to the Getting Started guide, or browse through a list of practical use cases in the Command-line Accounting Cookbook . Note that there exists an \u201cOpen\u201d directive that is used to provide the start date of each account. That can be located anywhere in the file, it does not have to appear in the file somewhere before you use an account name. You can just start using account names in transactions right away, though all account names that receive postings to them will eventually have to have a corresponding Open directive with a date that precedes all transactions posted to the account in the input file. \u21a9 Note that this is valid whether the price is specified as a per-unit price with the @ syntax or as a total price using the @@ syntax. \u21a9 I really dislike the name \u201cevent\u201d for this directive. I\u2019ve been trying to find a better alternative, so far without success. The name \u201cregister\u201d might be more appropriate, as it resembles that of a processor\u2019s register, but that could be confused with an account register report. \u201cvariable\u201d might work, but somehow that just sounds too computer-sciency and out of context. If you can think of something better, please make a suggestion and I\u2019ll seriously entertain a complete rename (with legacy support for \u201cevent\u201d). \u21a9","title":"Beancount Language Syntax"},{"location":"beancount_language_syntax.html#beancount-language-syntax","text":"Martin Blais , Updated: April 2016 http://furius.ca/beancount/doc/syntax Introduction Syntax Overview Directives Ordering of Directives Accounts Commodities / Currencies Strings Comments Directives Open Close Commodity Transactions Metadata Payee & Narration Costs and Prices Balancing Rule - The \u201cweight\u201d of postings Reducing Positions Amount Interpolation Tags The Tag Stack Links Balance Assertions Multiple Commodities Lots Are Aggregated Checks on Parent Accounts Before Close Pad Unused Pad Directives Commodities Cost Basis Multiple Paddings Notes Documents Documents from a Directory Prices Prices from Postings Prices on the Same Day Events Query Custom Metadata Options Operating Currencies Plugins Includes What\u2019s Next?","title":"Beancount Language Syntax"},{"location":"beancount_language_syntax.html#introduction","text":"This is a user's manual to the language of Beancount, the command-line double-entry bookkeeping system. Beancount defines a computer language that allows you to enter financial transactions in a text file and extract various reports from it. It is a generic counting tool that works with multiple currencies, commodities held at cost (e.g., stocks), and even allows you to track unusual things, like vacation hours, air miles and rewards points, and anything else you might want to count, even beans. This document provides an introduction to Beancount\u2019s syntax and some of the technical details needed for one to understand how it carries out its calculations. This document does not provide an introduction to the double-entry method , a motivation , nor examples and guidelines for entering transactions in your input file, nor how to run the tools . These subjects have their own dedicated documents , and it is recommended that you have had a look at those before diving into this user\u2019s manual. This manual covers the technical details for using Beancount.","title":"Introduction"},{"location":"beancount_language_syntax.html#syntax-overview","text":"","title":"Syntax Overview"},{"location":"beancount_language_syntax.html#directives","text":"Beancount is a declarative language. The input consists of a text file containing mainly a list of directives , or entries (we use these terms interchangeably in the code and documentation); there is also syntax for defining various options . Each directive begins with an associated date , which determines the point in time at which the directive will apply, and its type , which defines which kind of event this directive represents. All the directives begin with a syntax that looks like this: YYYY-MM-DD \u2026 where YYYY is the year, MM is the numerical month, and DD the numerical date. All digits are required, for example, the 7th of May 2007 should be \u201c2007-05-07\u201d, including its zeros. Beancount supports the international ISO 8601 standard format for dates, with dashes (e.g., \u201c2014-02-03\u201d), or the same ordering with slashes (e.g., \u201c2014/02/03\u201d). Here are some example directives, just to give you an idea of the aesthetics: 2014-02-03 open Assets:US:BofA:Checking 2014-04-10 note Assets:US:BofA:Checking \"Called to confirm wire transfer.\" 2014-05-02 balance Assets:US:BofA:Checking 154.20 USD The end product of a parsed input file is a simple list of these entries, in a data structure. All operations in Beancount are performed on these entries. Each particular directive type is documented in a section below.","title":"Directives"},{"location":"beancount_language_syntax.html#ordering-of-directives","text":"The order of declaration of the directives is not important. In fact, the entries are re-sorted chronologically after parsing and before being processed. This is an important feature of the language, because it makes it possible for you to organize your input file any way you like without having to worry about affecting the meaning of the directives. Except for transactions, each directive is assumed to occur at the beginning of each day. For example, you could declare an account being opened on the same day as its first transaction: 2014-02-03 open Assets:US:BofA:Checking 2014-02-03 * \"Initial deposit\" Assets:US:BofA:Checking 100 USD Assets:Cash -100 USD However, if you hypothetically closed that account immediately, you could not declare it closed on the same day, you would have to fudge the date forward by declaring the close on 2/4: 2014-02-04 close Assets:US:BofA:Checking This also explains why balance assertions are verified before any transactions that occur on the same date. This is for consistency.","title":"Ordering of Directives"},{"location":"beancount_language_syntax.html#accounts","text":"Beancount accumulates commodities in accounts. The names of these accounts do not have to be declared before being used in the file, they are recognized as \u201caccounts\u201d by virtue of their syntax alone 1 . An account name is a colon-separated list of capitalized words which begin with a letter, and whose first word must be one of five account types: Assets Liabilities Equity Income Expenses Each component of the account names begin with a capital letter or a number and are followed by letters, numbers or dash (-) characters. All other characters are disallowed. Here are some realistic example account names: Assets:US:BofA:Checking Liabilities:CA:RBC:CreditCard Equity:Retained-Earnings Income:US:Acme:Salary Expenses:Food:Groceries The set of all names of accounts seen in an input file implicitly define a hierarchy of accounts (sometimes called a chart-of-accounts ), similarly to how files are organized in a file system. For example, the following account names: Assets:US:BofA:Checking Assets:US:BofA:Savings Assets:US:Vanguard:Cash Assets:US:Vanguard:RGAGX Assets:Receivables implicitly declare a tree of accounts that looks like this: `-- Assets |-- Receivables `-- US |-- BofA | |-- Checking | `-- Savings `-- Vanguard |-- Cash `-- RGAGX We would say that \u201c Assets:US:BofA \u201d is the parent account of \u201c Assets:US:BofA:Checking \u201d, and that the latter is a child account of the former.","title":"Accounts"},{"location":"beancount_language_syntax.html#commodities-currencies","text":"Accounts contain currencies , which we sometimes also call commodities (we use both terms interchangeably). Like account names, currency names are recognized by their syntax, though, unlike account names, they need not be declared before being used). The syntax for a currency is a word all in capital letters, like these: USD CAD EUR MSFT IBM AIRMILE (Technically, a currency name may be up to 24 characters long, and it must start with a capital letter, must end with with a capital letter or number, and its other characters must only be capital letters, numbers, or punctuation limited to these characters: \u201c'._-\u201d (apostrophe, period, underscore, dash.) The first three might evoke real world currencies to you (US dollars, Canadian dollars, Euros); the next two, stock ticker names (Microsoft and IBM). And the last: rewards points (airmiles). Beancount knows of no such thing; from its perspective all of these instruments are treated similarly. There is no built-in notion of any previously existing currency. These currency names are just names of \u201cthings\u201d that can be put in accounts and accumulated in inventories associated with these accounts. There is something elegant about the fact that there is no \u201cspecial\u201d currency unit, that all commodities are treated equally the same: Beancount is inherently a multi-currency system. You will appreciate this if, like many of us, you are an expat and your life is divided between two or three continents. You can handle an international ledger of accounts without any problems. And your use of currencies can get quite creative: you can create a currency for your home, for example (e.g. MYLOFT ), a currency to count accumulated vacation hours ( VACHR ), or a currency to count potential contributions to your retirement accounts allowed annually ( IRAUSD ). You can actually solve many problems this way. The cookbook describes many such concrete examples. Beancount does not support the dollar sign syntax, e.g., \u201c$120.00\u201d. You should always use names for currencies in your input file. This makes the input more regular and is a design choice. For monetary units, I suggest that you use the standard ISO 4217 currency code as a guideline; these quickly become familiar. However, as detailed above, you may include some other characters in currency names, like underscores (_), dashes (-), periods (.), or apostrophes (\u2018), but no spaces. Finally, you will notice that there exists a \u201c commodity \u201d directive that can be used to declare currencies. It is entirely optional: currencies come into being as you use them. The purpose of the directive is simply to attach metadata to it.","title":"Commodities / Currencies"},{"location":"beancount_language_syntax.html#strings","text":"Whenever we need to insert some free text as part of an entry, it should be surrounded by double-quotes. This applies to the payee and narration fields, mainly; basically anything that\u2019s not a date, a number, a currency, an account name. Strings may be split over multiple lines. (Strings with multiple lines will include their newline characters and those need to be handled accordingly when rendering.)","title":"Strings"},{"location":"beancount_language_syntax.html#comments","text":"The Beancount input file isn\u2019t intended to contain only your directives: you can be liberal in placing comments and headers in it to organize your file. Any text on a line after the character \u201c;\u201d is ignored, text like this: ; I paid and left the taxi, forgot to take change, it was cold. 2015-01-01 * \"Taxi home from concert in Brooklyn\" Assets:Cash -20 USD ; inline comment Expenses:Taxi You can use one or more \u201c;\u201d characters if you like. Prepend on all lines if you want to enter a larger comment text. If you prefer to have the comment text parsed in and rendered in your journals, see the Note directive elsewhere in this document. Any line that does not begin as a valid Beancount syntax directive (e.g. with a date) is silently ignored. This way you can insert markup to organize your file for various outline modes, such as org-mode in Emacs. For example, you could organize your input file by institution like this and fold & unfold each of the sections independently,: * Banking ** Bank of America 2003-01-05 open Assets:US:BofA:Checking 2003-01-05 open Assets:US:BofA:Savings ;; Transactions follow \u2026 ** TD Bank 2006-03-15 open Assets:US:TD:Cash ;; More transactions follow \u2026 The unmatching lines are simply ignored. Note to visiting Ledger users: In Ledger, \u201c;\u201d is used both for marking comments and for attaching \u201cLedger tags\u201d (Beancount metadata) to postings. This is not the case in Beancount. In Beancount comments are always just comments. Metadata has its own separate syntax.","title":"Comments"},{"location":"beancount_language_syntax.html#directives_1","text":"For a quick reference & overview of directive syntax, please consult the Syntax Cheat Sheet .","title":"Directives"},{"location":"beancount_language_syntax.html#open","text":"All accounts need to be declared \u201copen\u201d in order to accept amounts posted to them. You do this by writing a directive that looks like this: 2014-05-01 open Liabilities:CreditCard:CapitalOne USD The general format of the Open directive is: YYYY-MM-DD open Account [ConstraintCurrency,...] [\"BookingMethod\"] The comma-separated optional list of constraint currencies enforces that all changes posted to this account are in units of one of the declared currencies. Specifying a currency constraint is recommended: the more constraints you provide Beancount with, the less likely you will be to make data entry mistakes because if you do, it will warn you about it. Each account should be declared \u201copened\u201d at a particular date that precedes (or is the same as) the date of the first transaction that posts an amount to that account. Just to be clear: an Open directive does not have to appear before transactions in the file , but rather, the date of the Open directive must precede the date of postings to that account. The order of declarations in the file is not important. So for example, this is a legal input file: 2014-05-05 * \"Using my new credit card\" Liabilities:CreditCard:CapitalOne -37.45 USD Expenses:Restaurant 2014-05-01 open Liabilities:CreditCard:CapitalOne USD 1990-01-01 open Expenses:Restaurant Another optional declaration for opening accounts is the \u201cbooking method\u201d, which is the algorithm that will be invoked if a reducing lot leads to an ambiguous choice of matching lots from the inventory (0, 2 or more lots match). Currently, the possible values it can take are: STRICT : The lot specification has to match exactly one lot. This is the default method. If this booking method is invoked, it will simply raise an error. This ensures that your input file explicitly selects all matching lots. NONE : No lot matching is performed. Lots of any price will be accepted. A mix of positive and negative numbers of lots for the same currency is allowed. (This is similar to how Ledger treats matching\u2026 it ignores it.)","title":"Open"},{"location":"beancount_language_syntax.html#close","text":"Similarly to the Open directive, there is a Close directive that can be used to tell Beancount that an account has become inactive, for example: ; Closing credit card after fraud was detected. 2016-11-28 close Liabilities:CreditCard:CapitalOne The general format of the Close directive is: YYYY-MM-DD close Account This directive is used in a couple of ways: An error message is raised if you post amounts to that account after its closing date (it's a sanity check). This helps avoid mistakes in data entry. It helps the reporting code figure out which accounts are still active and filter out closed accounts outside of the reporting period. This is especially useful as your ledger accumulates much data over time, as there will be accounts that stop existing and which you just don't want to see in reports for years that follow their closure. Note that a Close directive does not currently generate an implicit zero balance check. You may want to add one just before the closing date to ensure that the account is correctly closed with empty contents. At the moment, once an account is closed, you cannot reopen it after that date. (Though you can, of course, delete or comment-out the directive that closed it.) Finally, there are utility functions in the code that allow you to establish which accounts are open on a particular date. I strongly recommend that you close accounts when they actually close in reality, it will keep your ledger more tidy.","title":"Close"},{"location":"beancount_language_syntax.html#commodity","text":"There is a \u201cCommodity\u201d directive that can be used to declare currencies, financial instruments, commodities (different names for the same thing in Beancount): 1867-07-01 commodity CAD The general format of the Commodity directive is: YYYY-MM-DD commodity Currency This directive is a late arrival, and is entirely optional: you can use commodities without having to really declare them this way. The purpose of this directive is to attach commodity-specific metadata fields on it, so that it can be gathered by plugins later on. For example, you might want to provide a long descriptive name for each commodity, such as \u201cSwiss Franc\u201d for commodity \u201cCHF\u201d, or \u201cHooli Corporation Class C Shares\u201d for \u201cHOOL\u201d, like this: 1867-07-01 commodity CAD name: \"Canadian Dollar\" asset-class: \"cash\" 2012-01-01 commodity HOOL name: \"Hooli Corporation Class C Shares\" asset-class: \"stock\" For example, a plugin could then gather the metadata attribute names and perform some aggregations per asset class. You can use any date for a commodity\u2026 but a relevant date is the date at which it was created or introduced, e.g. Canadian dollars were first introduced in 1867, ILS (new Israeli Shekels) are in use since 1986-01-01. For a company, the date the corporation was formed and shares created could be a good date. Since the main purpose of this directive is to collect per-commodity information, the particular date you choose doesn\u2019t matter all that much. It is an error to declare the same commodity twice.","title":"Commodity"},{"location":"beancount_language_syntax.html#transactions","text":"Transactions are the most common type of directives that occur in a ledger. They are slightly different to the other ones, because they can be followed by a list of postings. Here is an example: 2014-05-05 txn \"Cafe Mogador\" \"Lamb tagine with wine\" Liabilities:CreditCard:CapitalOne -37.45 USD Expenses:Restaurant As for all the other directives, a transaction directive begins with a date in the YYYY-MM-DD format and is followed by the directive name, in this case, \u201c txn \u201d. However, because transactions are the raison d\u2019\u00eatre for our double-entry system and as such are by far the most common type of directive that should appear in a Beancount input file, we make a special case and allow the user to elide the \u201ctxn\u201d keyword and just use a flag instead of it: 2014-05-05 * \"Cafe Mogador\" \"Lamb tagine with wine\" Liabilities:CreditCard:CapitalOne -37.45 USD Expenses:Restaurant A flag is used to indicate the status of a transaction, and the particular meaning of the flag is yours to define. We recommend using the following interpretation for them: *: Completed transaction, known amounts, \u201cthis looks correct.\u201d !: Incomplete transaction, needs confirmation or revision, \u201cthis looks incorrect.\u201d In the case of the first example using \u201ctxn\u201d to leave the transaction unflagged, the default flag (\u201c*\u201d) will be set on the transaction object. (I nearly always use the \u201c*\u201d variant and never the keyword one, it is mainly there for consistency with all the other directive formats.) You can also attach flags to the postings themselves, if you want to flag one of the transaction\u2019s legs in particular: 2014-05-05 * \"Transfer from Savings account\" Assets:MyBank:Checking -400.00 USD ! Assets:MyBank:Savings This is useful in the intermediate stage of de-duping transactions (see Getting Started document for more details). The general format of a Transaction directive is: YYYY-MM-DD [txn|Flag] [[Payee] Narration] [Flag] Account Amount [{Cost}] [@ Price] [Flag] Account Amount [{Cost}] [@ Price] ... The lines that follow the first line are for \u201cPostings.\u201d You can attach as many postings as you want to a transaction. For example, a salary entry might look like this: 2014-03-19 * \"Acme Corp\" \"Bi-monthly salary payment\" Assets:MyBank:Checking 3062.68 USD ; Direct deposit Income:AcmeCorp:Salary -4615.38 USD ; Gross salary Expenses:Taxes:TY2014:Federal 920.53 USD ; Federal taxes Expenses:Taxes:TY2014:SocSec 286.15 USD ; Social security Expenses:Taxes:TY2014:Medicare 66.92 USD ; Medicare Expenses:Taxes:TY2014:StateNY 277.90 USD ; New York taxes Expenses:Taxes:TY2014:SDI 1.20 USD ; Disability insurance The Amount in \u201cPostings\u201d can also be an arithmetic expression using ( ) * / - + . For example, 2014-10-05 * \"Costco\" \"Shopping for birthday\" Liabilities:CreditCard:CapitalOne -45.00 USD Assets:AccountsReceivable:John ((40.00/3) + 5) USD Assets:AccountsReceivable:Michael 40.00/3 USD Expenses:Shopping The crucial and only constraint on postings is that the sum of their balance amounts must be zero. This is explained in full detail below.","title":"Transactions"},{"location":"beancount_language_syntax.html#metadata","text":"It\u2019s also possible to attach metadata to the transaction and/or any of its postings, so the fully general format is: YYYY-MM-DD [txn|Flag] [[Payee] Narration] [Key: Value] ... [Flag] Account Amount [{Cost}] [@ Price] [Key: Value] ... [Flag] Account Amount [{Cost}] [@ Price] [Key: Value] ... ... See the dedicated section on metadata below.","title":"Metadata"},{"location":"beancount_language_syntax.html#payee-narration","text":"A transaction may have an optional \u201cpayee\u201d and/or a \u201cnarration.\u201d In the first example above, the payee is \u201cCafe Mogador\u201d and the narration is \u201cLamb tagine with wine\u201d. The payee is a string that represents an external entity that is involved in the transaction. Payees are sometimes useful on transactions that post amounts to Expense accounts, whereby the account accumulates a category of expenses from multiple businesses. A good example is \u201c Expenses:Restaurant \u201d, which will include all postings for the various restaurants one might visit. A narration is a description of the transaction that you write. It can be a comment about the context, the person who accompanied you, some note about the product you bought... whatever you want it to be. It\u2019s for you to insert whatever you like. I like to insert notes when I reconcile, it\u2019s quick and I can refer to my notes later on, for example, to answer the question \u201cWhat was the name of that great little sushi place I visited with Andreas on the West side last winter?\u201d If you place a single string on a transaction line, it becomes its narration: 2014-05-05 * \"Lamb tagine with wine\" \u2026 If you want to set just a payee, put an empty narration string: 2014-05-05 * \"Cafe Mogador\" \"\" \u2026 For legacy reasons, a pipe symbol (\u201c|\u201d) is accepted between those strings (but this will be removed at some point in the future): 2014-05-05 * \"Cafe Mogador\" | \"\" \u2026 You may also leave out either (but you must provide a flag): 2014-05-05 * \u2026 Note for Ledger users. Ledger does not have separate narration and payee fields, it has only one field, which is referred to by the \u201cPayee\u201d metadata tag, and the narration ends up in a saved comment (a \u201cpersistent note\u201d). In Beancount, a Transaction object simply has two fields: payee and narration, where payee just happens to have an empty value a lot of the time. For a deeper discussion of how and when to use payees or not, see Payees, Subaccounts, and Assets .","title":"Payee & Narration"},{"location":"beancount_language_syntax.html#costs-and-prices","text":"Postings represent a single amount being deposited to or withdrawn from an account. The simplest type of posting includes only its amount: 2012-11-03 * \"Transfer to pay credit card\" Assets:MyBank:Checking -400.00 USD Liabilities:CreditCard 400.00 USD If you converted the amount from another currency, you must provide a conversion rate to balance the transaction (see next section). This is done by attaching a \u201cprice\u201d to the posting, which is the rate of conversion: 2012-11-03 * \"Transfer to account in Canada\" Assets:MyBank:Checking -400.00 USD @ 1.09 CAD Assets:FR:SocGen:Checking 436.01 CAD You could also use the \u201c@@\u201d syntax to specify the total cost: 2012-11-03 * \"Transfer to account in Canada\" Assets:MyBank:Checking -400.00 USD @@ 436.01 CAD Assets:FR:SocGen:Checking 436.01 CAD Beancount will automatically compute the per-unit price, that is 1.090025 CAD (note that the precision will differ between the last two examples). After the transaction, we are not interested in keeping track of the exchange rate for the units of USD deposited into the account; those units of \u201cUSD\u201d are simply deposited. In a sense, the rate at which they were converted at has been forgotten. However, some commodities that you deposit in accounts must be \u201cheld at cost.\u201d This happens when you want to keep track of the cost basis of those commodities. The typical use case is investing, for example when you deposit shares of a stock in an account. What you want in that circumstance is for the acquisition cost of the commodities you deposit to be attached to the units, such that when you remove those units later on (when you sell), you should be able to identify by cost which of those units to remove, in order to control the effect of taxes (and avoid errors). You can imagine that for each of the units in the account there is an attached cost basis. This will allow us to automatically compute capital gains later. In order to specify that a posting to an account is to be held at a specific cost, include the cost in curly brackets: 2014-02-11 * \"Bought shares of S&P 500\" Assets:ETrade:IVV 10 IVV {183.07 USD} Assets:ETrade:Cash -1830.70 USD This is the subject of a deeper discussion. Refer to \u201c How Inventories Work \u201d and \u201c Trading with Beancount \u201d documents for an in-depth discussion of these topics. Finally, you can include both a cost and a price on a posting: 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 The price will only be used to insert a price entry in the prices database (see Price section below). This is discussed in more details in the Balancing Postings section of this document. Important Note. Amounts specified as either per-share or total prices or costs are always unsigned . It is an error to use a negative sign or a negative cost and Beancount will raise an error if you attempt to do so.","title":"Costs and Prices"},{"location":"beancount_language_syntax.html#balancing-rule-the-weight-of-postings","text":"A crucial aspect of the double-entry method is ensuring that the sum of all the amounts on its postings equals ZERO, in all currencies. This is the central, non-negotiable condition that engenders the accounting equation, and makes it possible to filter any subset of transactions and drawing balance sheets that balance to zero. But with different types of units, the previously introduced price conversions and units \u201cheld at cost\u201d, what does it all mean? It\u2019s simple: we have devised a simple and clear rule for obtaining an amount and a currency from each posting, to be used for balancing them together. We call this the \u201cweight\u201d of a posting, or the balancing amount. Here is a short example of weights derived from postings using the four possible types of cost/price combinations: YYYY-MM-DD Account 10.00 USD -> 10.00 USD Account 10.00 CAD @ 1.01 USD -> 10.10 USD Account 10 SOME {2.02 USD} -> 20.20 USD Account 10 SOME {2.02 USD} @ 2.50 USD -> 20.20 USD Here is the explanation of how it is calculated: If the posting has only an amount and no cost, the balance amount is just the amount and currency on that posting. Using the first example from the previous section, the amount is -400.00 USD, and that is balanced against the 400.00 USD of the second leg. If the posting has only a price , the price is multiplied by the number of units and the price currency is used. In the second example from the preceding section, that is -400.00 USD x 1.09 CAD(/USD) = -436.00 CAD, and that is balanced against the other posting of 436.00 CAD 2 . If the posting has a cost , the cost is multiplied by the number of units and the cost currency is used. In the third example from the preceding section, that is 10 IVV x 183.08 USD(/IVV) = 1830.70 USD. That is balanced against the cash leg of -1830.70 USD, so all is good. Finally, if a posting has both a cost and a price , we simply ignore the price. This optional price is used later on to generate an entry in the in-memory prices database, but it is not used in balancing at all. With this rule, you should be able to easily balance all your transactions. Moreover, this rule makes it possible to let Beancount automatically calculate capital gains for you (see Trading with Beancount for details).","title":"Balancing Rule - The \u201cweight\u201d of postings"},{"location":"beancount_language_syntax.html#reducing-positions","text":"When you post a reduction to a position in an account, the reduction must always match an existing lot. For example, if an account holds 3200 USD and a transaction posts a -1200 USD change to that account, the 1200 USD match against the existing 3200 USD, and the result is a single position of 2000 USD. This also works for negative values. For example, if an account has a -1300 USD balance and you post a +2000 USD change to it, you obtain a 700 USD balance. A change posted to an account, regardless of the account type, can result in a positive or negative balance; there are no limitations on the balances of simple commodity amounts (that is, those with no cost associated to them). For example, while Assets accounts normally have a positive balance and Liabilities accounts usually a negative one, you can legally credit an Assets account to a negative balance, or debit a Liabilities account to a positive balance. This is because in the real world these things do happen: you might write a check too many and obtain temporary credit from your bank\u2019s checking account (usually along with an outrageous \u201coverdraft\u201d fee), or pay that credit card balance twice by mistake. For commodities held at cost, the cost specification of the posting must match one of the lots held in the inventory before the transaction is applied. The list of lots is gathered, and matched against the specification in the {...} part of the posting. For example, if you provide a cost, only those lots whose cost match that will remain. If you provide a date, only those lots which match that date will remain. And you can use a label as well. If you provide a cost and a date, both of these are matched against the list of candidate lots to reduce. This is essentially a filter on the list of lots. If the filtered list results in a single lot, that lot is chosen to be reduced. If the list results in multiple lots, but the total amount being reduced equals the total amount in the lots, all those lots are reduced by that posting. For example, if in the past you had the following transactions: 2014-02-11 * \"Bought shares of S&P 500\" Assets:ETrade:IVV 20 IVV {183.07 USD, \"ref-001\"} \u2026 2014-03-22 * \"Bought shares of S&P 500\" Assets:ETrade:IVV 15 IVV {187.12 USD} \u2026 Each of the following reductions would be unambiguous: 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {183.07 USD} \u2026 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {2014-02-11} \u2026 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {\"ref-001\"} \u2026 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -35 IVV {} \u2026 However, the following would be ambiguous: 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {} \u2026 If multiple lots match against the reducing posting and their number is not the total number, we are in a situation of ambiguous matches . What happens then, is that the account\u2019s booking method is invoked. There are multiple booking methods, but by default, all accounts are set to use the \u201cSTRICT\u201d booking method. This method simply issues an error in an ambiguous situation. You may set the account\u2019s booking method to \u201cFIFO\u201d to instruct Beancount to select the oldest of the lots. Or \u201cLIFO\u201d for the latest (youngest) of the lots. This will automatically select all the necessary matching lots to fulfill the reduction. PLEASE NOTE! Requiring the dates to match will be dealt with more sensibly in the near future. See A Proposal for an Improvement on Inventory Booking for details of this upcoming change. For such postings, a change that results in a negative number of units is usually impossible. Beancount does not currently allow holding a negative number of a commodity held at cost. For example, an input with just this transaction will fail: 2014-05-23 * Assets:Investments:MSFT -10 MSFT {43.40 USD} Assets:Investments:Cash 434.00 USD If it did not, this would result in a balance of -10 units of MSFT. On the other hand, if the account had a balance of 12 units of MSFT held at 43.40 USD on 5/23, the transaction would book just fine, reducing the existing 12 units to 2. Most often, the error that will occur is that the account will be holding a balance of 10 or more units of MSFT at a different cost , and the user will specify an incorrect value for the cost. For instance, if the account had a positive balance of 20 MSFT {42.10 USD}, the transaction above would still fail, because there aren\u2019t 10 or more units of MSFT at 43.40 USD to remove from. This constraint is enforced for a few reasons: Mistakes in data entry for stock units are not uncommon, they have an important negative impact on the correctness of your Ledger\u2014the amounts are usually large\u2014and they will typically trigger an error from this constraint. Therefore, the error check is a useful way to detect these types of errors. Negative numbers of units held at cost are fairly rare. Chances are you don\u2019t need them at all. Exceptions include: short sales of stock, holding spreads on futures contracts, and depending on how you account for them, short positions in currency trading. This is why this check is enabled by default. PLEASE NOTE! In a future version of Beancount, we will relax this constraint somewhat. We will allow an account to hold a negative number of units of a commodity if and only if there are no other units of that commodity held in the account. Either that, or we will allow you to mark an account as having no such constraints at all. The purpose is to allow the account of short positions in commodities. The only blocking factor is this constraint. For more details of the inventory booking algorithm, see the How Inventories Work document.","title":"Reducing Positions"},{"location":"beancount_language_syntax.html#amount-interpolation","text":"Beancount is able to fill in some of the details of a transaction automatically. You can currently elide the amount of at most one posting within a transaction: 2012-11-03 * \"Transfer to pay credit card\" Assets:MyBank:Checking -400.00 USD Liabilities:CreditCard In the example above, the amount of the credit card posting has been elided. It is automatically calculated by Beancount at 400.00 USD to balance the transaction. This also works with multiple postings, and with postings with costs: 2014-07-11 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -10 IVV {183.07 USD} Assets:ETrade:Cash 1979.90 USD Income:ETrade:CapitalGains In this case, the units of IVV are sold at a higher price ($197.90) than they were bought for ($183.07). The cash first posting has a weight of -10 x 183.07 = -1830.70 and the second posting a straightforward $1979.90. The last posting will be ascribed the difference, that is, a balance of -149.20 USD , which is to say, a gain of $149.20. When calculating the amount to be balanced, the same balance amounts that are used to check that the transaction balances to zero are used to fill in the missing amounts. For example, the following would not trigger an error: 2014-07-11 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -10 IVV {183.07 USD} @ 197.90 USD Assets:ETrade:Cash The cash account would receive 1830.70 USD automatically, because the balance amount of the IVV posting is -1830.70 USD (if a posting has both a cost and a price, the cost basis is always used and the optional price ignored). While this is accepted and correct from a balancing perspective, this would be incomplete from an accounting perspective: the capital gain on a sale needs to be accounted for separately and besides, the amount deposited to the cash account if you did as above would fall short of the real deposit (1979.00 USD) and hopefully a subsequent balance assertion in the cash account would indicate this oversight by triggering an error. Finally, this also works when the balance includes multiple commodities: 2014-07-12 * \"Uncle Bob gave me his foreign currency collection!\" Income:Gifts -117.00 ILS Income:Gifts -3000.00 INR Income:Gifts -800.00 JPY Assets:ForeignCash Multiple postings (one for each commodity required to balance) will be inserted to replace the elided one.","title":"Amount Interpolation"},{"location":"beancount_language_syntax.html#tags","text":"Transactions can be tagged with arbitrary strings: 2014-04-23 * \"Flight to Berlin\" #berlin-trip-2014 Expenses:Flights -1230.27 USD Liabilities:CreditCard This is similar to the popular idea of \u201chash tagging\u201d on Twitter and such. These tags essentially allow you to mark a subset of transactions. They can then be used as a filter to generate reports on only this subset of transactions. They have numerous uses. I like to use them to mark all my trips. Multiple tags can be specified as well: 2014-04-23 * \"Flight to Berlin\" #berlin-trip-2014 #germany Expenses:Flights -1230.27 USD Liabilities:CreditCard (If you want to store key-value pairs on directives, see the section on metadata below.)","title":"Tags"},{"location":"beancount_language_syntax.html#the-tag-stack","text":"Oftentimes multiple transactions related to a single tag will be entered consecutively in a file. As a convenience, the parser can automatically tag transactions within a block of text. How this works is simple: the parser has a \u201cstack\u201d of current tags which it applies to all transactions as it reads them one-by-one. You can push and pop tags onto/from this stack, like this: pushtag #berlin-trip-2014 2014-04-23 * \"Flight to Berlin\" Expenses:Flights -1230.27 USD Liabilities:CreditCard poptag #berlin-trip-2014 This way, you can also push multiple tags onto a long, consecutive set of transactions without having to type them all in.","title":"The Tag Stack"},{"location":"beancount_language_syntax.html#links","text":"Transactions can also be linked together. You may think of the link as a special kind of tag that can be used to group together a set of financially related transactions over time. For example you may use links to group together transactions that are each related with a specific invoice. This allows to track payments (or write-offs) associated with the invoice: 2014-02-05 * \"Invoice for January\" ^invoice-pepe-studios-jan14 Income:Clients:PepeStudios -8450.00 USD Assets:AccountsReceivable 2014-02-20 * \"Check deposit - payment from Pepe\" ^invoice-pepe-studios-jan14 Assets:BofA:Checking 8450.00 USD Assets:AccountsReceivable Or track multiple transfers related to a single nefarious purpose: 2014-02-05 * \"Moving money to Isle of Man\" ^transfers-offshore-17 Assets:WellsFargo:Savings -40000.00 USD Assets:WellsFargo:Checking 40000.00 USD 2014-02-09 * \"Wire to FX broker\" ^transfers-offshore-17 Assets:WellsFargo:Checking -40025.00 USD Expenses:Fees:WireTransfers 25.00 USD Assets:OANDA:USDollar 40000.00 2014-03-16 * \"Conversion to offshore beans\" ^transfers-offshore-17 Assets:OANDA:USDollar -40000.00 USD Assets:OANDA:GBPounds 23391.81 GBP @ 1.71 USD 2014-03-16 * \"God save the Queen (and taxes)\" ^transfers-offshore-17 Assets:OANDA:GBPounds -23391.81 GBP Expenses:Fees:WireTransfers 15.00 GBP Assets:Brittania:PrivateBanking 23376.81 GBP Linked transactions can be rendered by the web interface in their own dedicated journal, regardless of the current view/filtered set of transactions (the list of links is a global page).","title":"Links"},{"location":"beancount_language_syntax.html#balance-assertions","text":"A balance assertion is a way for you to input your statement balance into the flow of transactions. It tells Beancount to verify that the number of units of a particular commodity in some account should equal some expected value at some point in time. For instance, this 2014-12-26 balance Liabilities:US:CreditCard -3492.02 USD says \u201cCheck that the number of USD units in account \u201c Liabilities:US:CreditCard \u201d on the morning of December 26th, 2014 is -3492.02 USD.\u201d When processing the list of entries, if Beancount encounters a different balance than this for USD it will report an error. If no error is reported, you should have some confidence that the list of transactions that precedes it in this account is highly likely to be correct. This is useful in practice, because in many cases some transactions can get imported separately from the accounts of each of their postings (see the de-duping problem ). This can result in you booking the same transaction twice without noticing, and regularly inserting a balance assertion will catch that problem every time. The general format of the Balance directive is: YYYY-MM-DD balance Account Amount Note that a balance assertion, like all other non-transaction directives, applies at the beginning of its date (i.e., midnight at the start of day). Just imagine that the balance check occurs right after midnight on that day. Balance assertions only make sense on balance sheet accounts (Assets and Liabilities). Because the postings on Income and Expenses accounts are only interesting because of their transient value, i.e., for these accounts we\u2019re interested in sums of changes over a period of time (not the absolute value), it makes little sense to use a balance assertion on income statement accounts. Also, take note that each account benefits from an implicit balance assertion that the account is empty after it is opened at the date of its Open directive. You do not need to explicitly assert a zero balance when opening accounts.","title":"Balance Assertions"},{"location":"beancount_language_syntax.html#multiple-commodities","text":"A Beancount account may contain more than one commodity (although in practice, you will find that this does not occur often, it is sensible to create dedicated sub-accounts to hold each commodity, for example, holding a portfolio of stocks). A balance assertion applies only to the commodity of the assertion; it leaves the other commodities in the balance unchecked. If you want to check multiple commodities, use multiple balance assertions, like this: ; Check cash balances from wallet 2014-08-09 balance Assets:Cash 562.00 USD 2014-08-09 balance Assets:Cash 210.00 CAD 2014-08-09 balance Assets:Cash 60.00 EUR There is currently no way to exhaustively check the full list of commodities in an account ( a proposal is underway ). Note that in this example if an exhaustive check really matters to you, you could circumvent by defining a subaccount of the cash account to segregate each commodity separately, like this Assets:Cash:USD , Assets:Cash:CAD .","title":"Multiple Commodities"},{"location":"beancount_language_syntax.html#lots-are-aggregated","text":"The balance assertion applies to the sum of units of a particular commody, irrespective of their cost. For example, if you hold three lots of the same commodity in an account, for example, 5 HOOL {500 USD} and 6 HOOL {510 USD}, the following balance check should succeed: 2014-08-09 balance Assets:Investing:HOOL 11 HOOL All the lots are aggregated together and you can verify their number of units.","title":"Lots Are Aggregated"},{"location":"beancount_language_syntax.html#checks-on-parent-accounts","text":"Balance assertions may be performed on parent accounts, and will include the balances of theirs and their sub-accounts: 2014-01-01 open Assets:Investing 2014-01-01 open Assets:Investing:Apple AAPL 2014-01-01 open Assets:Investing:Amazon AMZN 2014-01-01 open Assets:Investing:Microsoft MSFT 2014-01-01 open Equity:Opening-Balances 2014-06-01 * Assets:Investing:Apple 5 AAPL {578.23 USD} Assets:Investing:Amazon 5 AMZN {346.20 USD} Assets:Investing:Microsoft 5 MSFT {42.09 USD} Equity:Opening-Balances 2014-07-13 balance Assets:Investing 5 AAPL 2014-07-13 balance Assets:Investing 5 AMZN 2014-07-13 balance Assets:Investing 5 MSFT Note that this does require that a parent account have been declared as Open, in order to be legitimately used in the balance assertions directive.","title":"Checks on Parent Accounts"},{"location":"beancount_language_syntax.html#before-close","text":"It is useful to insert a balance assertion for 0 units just before closing an account, just to make sure its contents are empty as you close it. The Close directive does not insert that for you automatically (we may eventually build a plug-in for it).","title":"Before Close"},{"location":"beancount_language_syntax.html#local-tolerance","text":"It's pretty common that sometimes one needs to override the tolerance on the balance check to loosen it on that balance assertion. This can be done using a local tolerance amount off of the balance amount, like this: 2013-09-20 balance Assets:Investing:Funds 319.020 ~ 0.002 RGAGX","title":"Local Tolerance"},{"location":"beancount_language_syntax.html#pad","text":"A padding directive automatically inserts a transaction that will make the subsequent balance assertion succeed, if it is needed. It inserts the difference needed to fulfill that balance assertion. (What \u201crubber space\u201d is in LaTeX, Pad directives are to balances in Beancount.) Note that by \u201csubsequent,\u201d I mean in date order , not in the order of the declarations in the file. This is the conceptual equivalent of a transaction that will automatically expand or contract to fill the difference between two balance assertions over time. It looks like this: 2014-06-01 pad Assets:BofA:Checking Equity:Opening-Balances The general format of the Pad directive is: YYYY-MM-DD pad Account AccountPad The first account is the account to credit the automatically calculated amount to. This is the account that should have a balance assertion following it (if the account does not have a balance assertion, the pad entry is benign and does nothing). The second account is for the other leg of the transaction, it is the source where the funds will come from, and this is almost always some Equity account. The reason for this is that this directive is generally used for initializing the balances of new accounts, to save us from having to either insert such a directive manually, or from having to enter the full past history of transactions that will bring the account to its current balance. Here is a realistic example usage scenario: ; Account was opened way back in the past. 2002-01-17 open Assets:US:BofA:Checking 2002-01-17 pad Assets:US:BofA:Checking Equity:Opening-Balances 2014-07-09 balance Assets:US:BofA:Checking 987.34 USD This will result in the following transaction being inserted right after the Pad directive, on the same date: 2002-01-17 P \"(Padding inserted for balance of 987.34 USD)\" Assets:US:BofA:Checking 987.34 USD Equity:Opening-Balances -987.34 USD This is a normal transaction\u2014you will see it appear in the rendered journals along with the other ones. (Note the special \u201cP\u201d flag, which can be used by scripts to find these.) Observe that without balance assertions, Pad directives make no sense. Therefore, like balance assertions, they are normally only used on balance sheet accounts (Assets and Liabilities). You could also insert Pad entries between balance assertions, it works too. For example: 2014-07-09 balance Assets:US:BofA:Checking 987.34 USD \u2026 more transactions\u2026 2014-08-08 pad Assets:US:BofA:Checking Equity:Opening-Balances 2014-08-09 balance Assets:US:BofA:Checking 1137.23 USD Without intervening transactions, this would insert the following padding transaction: 2014-08-08 P \"(Padding inserted for balance of 1137.23 USD)\" Assets:US:BofA:Checking 149.89 USD Equity:Opening-Balances -149.89 USD In case that\u2019s not obvious, 149.89 USD is the difference between 1137.23 USD and 987.34 USD. If there were more intervening transactions posting amounts to the checking account, the amount would automatically have been adjusted to make the second assertion pass.","title":"Pad"},{"location":"beancount_language_syntax.html#unused-pad-directives","text":"You may not currently leave unused Pad directives in your input file. They will trigger an error: 2014-01-01 open Assets:US:BofA:Checking 2014-02-01 pad Assets:US:BofA:Checking Equity:Opening-Balances 2014-06-01 * \"Initializing account\" Assets:US:BofA:Checking 212.00 USD Equity:Opening-Balances 2014-07-09 balance Assets:US:BofA:Checking 212.00 USD (Being this strict is a matter for debate, I suppose, and it could eventually be moved to an optional plugin.)","title":"Unused Pad Directives"},{"location":"beancount_language_syntax.html#commodities","text":"Note that the Pad directive does not specify any commodities at all. All commodities with corresponding balance assertions in the account are affected. For instance, the following code would have a padding directive insert a transaction with separate postings for USD and CAD: 2002-01-17 open Assets:Cash 2002-01-17 pad Assets:Cash Equity:Opening-Balances 2014-07-09 balance Assets:Cash 987.34 USD 2014-07-09 balance Assets:Cash 236.24 CAD If the account contained other commodities that aren\u2019t balance asserted, no posting would be inserted for those.","title":"Commodities"},{"location":"beancount_language_syntax.html#cost-basis","text":"At the moment, Pad directives do not work with accounts holding positions held at cost. The directive is really only useful for cash accounts. (This is mainly because balance assertions do not yet allow specifying a cost basis to assert. It is possible that in the future we decide to support asserting the total cost basis, and that point we could consider supporting padding with cost basis.)","title":"Cost Basis"},{"location":"beancount_language_syntax.html#multiple-paddings","text":"You cannot currently insert multiple padding entries for the same account and commodity: 2002-01-17 open Assets:Cash 2002-02-01 pad Assets:Cash Equity:Opening-Balances 2002-03-01 pad Assets:Cash Equity:Opening-Balances 2014-04-19 balance Assets:Cash 987.34 USD (There is a proposal pending to allow this and spread the padding amount evenly among all the intervening Pad directives, but it is as of yet unimplemented.)","title":"Multiple Paddings"},{"location":"beancount_language_syntax.html#notes","text":"A Note directive is simply used to attach a dated comment to the journal of a particular account, like this: 2013-11-03 note Liabilities:CreditCard \"Called about fraudulent card.\" When you render the journal, the note should be rendered in context. This can be useful to record facts and claims associated with a financial event. I often use this to record snippets of information that would otherwise not make their way to a transaction. The general format of the Note directive is: YYYY-MM-DD note Account Description The description string may be split among multiple lines.","title":"Notes"},{"location":"beancount_language_syntax.html#documents","text":"A Document directive can be used to attach an external file to the journal of an account: 2013-11-03 document Liabilities:CreditCard \"/home/joe/stmts/apr-2014.pdf\" The filename gets rendered as a browser link in the journals of the web interface for the corresponding account and you should be able to click on it to view the contents of the file itself. This is useful to integrate account statements and other downloads into the flow of accounts, so that they\u2019re easily accessible from a few clicks. Scripts could also be written to obtain this list of documents from an account name and do something with them. The general format of the Document directive is: YYYY-MM-DD document Account PathToDocument","title":"Documents"},{"location":"beancount_language_syntax.html#documents-from-a-directory","text":"A more convenient way to create these entries is to use a special option to specify directories that contain a hierarchy of sub-directories that mirrors that of the chart of accounts. For example, the Document directive shown in the previous section could have been created from a directory hierarchy that looks like this: stmts `-- Liabilities `-- CreditCard `-- 2014-04-27.apr-2014.pdf By simply specifying the root directory as an option (note the absence of a trailing slash): option \"documents\" \"/home/joe/stmts\" The files that will be picked up are those that begin with a date as above, in the YYYY-MM-DD format. You may specify this option multiple times if you have many such document archives. In the past I have used one directory for each year (if you scan all your documents, the directory can grow to a large size, scanned documents tend to be large files). Organizing your electronic document statements and scans using the hierarchy of your ledger\u2019s accounts is a fantastic way to organize them and establish a clear, unambiguous place to find these documents later on, when they\u2019re needed.","title":"Documents from a Directory"},{"location":"beancount_language_syntax.html#prices","text":"Beancount sometimes creates an in-memory data store of prices for each commodity, that is used for various reasons. In particular, it is used to report unrealized gains on account holdings. Price directives can be used to provide data points for this database. A Price directive establishes the rate of exchange between one commodity (the base currency) and another (the quote currency): 2014-07-09 price HOOL 579.18 USD This directive says: \u201cThe price of one unit of HOOL on July 9th, 2014 was 579.18 USD.\u201d Price entries for currency exchange rates work the same way: 2014-07-09 price USD 1.08 CAD The general format of the Price directive is: YYYY-MM-DD price Commodity Price Remember that Beancount knows nothing about what HOOL, USD or CAD are. They are opaque \u201cthings.\u201d You attach meaning to them. So setting a price per hour for your vacation hours is perfectly valid and useful too, if you account for your unused vacations on such terms: 2014-07-09 price VACHR 38.46 USD ; Equiv. $80,000 year","title":"Prices"},{"location":"beancount_language_syntax.html#prices-from-postings","text":"If you use the beancount.plugins.implicit_prices plugin, every time a Posting appears that has a cost or an optional price declared, it will use that cost or price to automatically synthesize a Price directive. For example, this transaction: 2014-05-23 * Assets:Investments:MSFT -10 MSFT {43.40 USD} Assets:Investments:Cash 434.00 USD automatically becomes this after parsing: 2014-05-23 * Assets:Investments:MSFT -10 MSFT {43.40 USD} Assets:Investments:Cash 434.00 USD 2014-05-23 price MSFT 43.40 USD This is convenient and if you enable it, will probably be where most of the price points in your ledger\u2019s price database will come from. You can print a table of the parsed prices from a ledger (it is just another type of report).","title":"Prices from Postings"},{"location":"beancount_language_syntax.html#prices-on-the-same-day","text":"Notice that there is no notion of time; Beancount is not designed to solve problems for intra-day traders, though of course, it certainly is able to handle multiple trades per day. It just stores its prices per day. (Generally, if you need many features that require a notion of intra-day time, you\u2019re better off using another system, this is not the scope of a bookkeeping system.) When multiple Price directives do exist for the same day, the last one to appear in the file will be selected for inclusion in the Price database.","title":"Prices on the Same Day"},{"location":"beancount_language_syntax.html#events","text":"Event directives 3 are used to track the value of some variable of your choice over time. For example, your location: 2014-07-09 event \"location\" \"Paris, France\" The above directive says: \u201cChange the value of the \u2018location\u2019 event to \u2018Paris, France\u2019 as of the 9th of July 2014 and onwards.\u201d A particular event type only has a single value per day. The general format of the Event directive is: YYYY-MM-DD event Name Value The event\u2019s Name string does not have to be declared anywhere, it just begins to exist the first time you use the directive. The Value string can be anything you like; it has no prescribed structure. How to use these is best explained by providing examples: Location : You can use events for tracking the city or country you\u2019re in. This is sensible usage within a ledger because the nature of your expenses are heavily correlated to where you are. It\u2019s convenient: if you use Beancount regularly, it is very little effort to add this bit of information to your file. I usually have a \u201ccash daybook\u201d section where I put miscellaneous cash expenses, and that\u2019s where I enter those directives. Address : If you move around a lot, it\u2019s useful to keep a record of your past home addresses. This is sometimes requested on government forms for immigration. The Green Card application process in the US, for instance, Employer : You can record your date of employment and departure for each job this way. Then you can count the number of days you worked there. Trading window : If you\u2019re an employee of a public company, you can record the dates that you\u2019re allowed to trade its stock. This can then be used to ensure you did not trade that stock when the window is closed. Events are often used to report on numbers of days. For example, in the province of Quebec (Canada) you are insured for free health care coverage if \u201c you spend 183 days of the calendar year or more, excluding trips of 21 days or less .\u201d If you travel abroad a lot, you could easily write a script to warn you of remaining day allocations to avoid losing your coverage. Many expats are in similar situations. In the same vein, the IRS recognizes US immigrants as \u201cresident alien\u201d\u2014and thus subject to filing taxes, yay!\u2014if you pass the Substantial Presence Test , which is defined as \u201cbeing physically present in the US on at least 31 days during the current year, and 193 days during the 3 year period that includes the current year and the 2 years immediately before that, counting: all the days you were present in the current year, and \u2153 of the days of the year before that, and \u2159 of the year preceding that one.\u201d Ouch\u2026 my head hurts. This gets complicated. Armed with your location over time, you could report it automatically. Events will also be usable in the filtering language, to specify non-contiguous periods of time. For example, if you are tracking your location using Event directives, you could produce reports for transactions that occur only when you are in a specific location, e.g., \u201cShow me my expenses on all my trips to Germany,\u201d or \u201cGive me a list of payees for restaurant transactions when I\u2019m in Montreal.\u201d PLEASE NOTE! Filters haven\u2019t been implemented yet. Also, reports on events have not yet been re-implemented in Beancount 2.0. They will be reintroduced again soon, as well as filtering. It is worth noticing that the Price and Event directives are the only ones not associated to an account.","title":"Events"},{"location":"beancount_language_syntax.html#query","text":"It can be convenient to be able to associate SQL queries in a Beancount file to be able to run these as a report automatically. This is still an early development / experimental directive. In any case, you can insert queries in the stream of transactions like this: 2014-07-09 query \"france-balances\" \" SELECT account, sum(position) WHERE \u2018trip-france-2014\u2019 in tags\" The grammar is YYYY-MM-DD query Name SqlContents Each query has a name, which will probably be used to invoke its running as a report type. Also, the date of the query is intended to be the date at which the query is intended to be run for, that is, transactions following it should be ignored. If you\u2019re familiar with the SQL syntax, it\u2019s an implicit CLOSE.","title":"Query"},{"location":"beancount_language_syntax.html#custom","text":"The long-term plan for Beancount is to allow plugins and external clients to define their own directive types, to be declared and validated by the Beancount input language parser. In the meantime, a generic directive is provided for clients to prototype new features, e.g., budgeting. 2014-07-09 custom \"budget\" \"...\" TRUE 45.30 USD The grammar for this directive is flexible: YYYY-MM-DD custom TypeName Value1 ... The first argument is a string and is intended to be unique to your directive. Think of this as the type of your directive. Following it, you can put an arbitrary list of strings, dates, booleans, amounts, and numbers. Note that there is no validation that checks that the number and types of arguments following the TypeName is consistent for a particular type. (See this thread for the origin story around this feature.)","title":"Custom"},{"location":"beancount_language_syntax.html#metadata_1","text":"You may attach arbitrary data to each of your entries and postings. The syntax for it looks like this: 2013-03-14 open Assets:BTrade:HOOLI category: \"taxable\" 2013-08-26 * \"Buying some shares of Hooli\" statement: \"confirmation-826453.pdf\" Assets:BTrade:HOOLI 10 HOOL @ {498.45 USD} decision: \"scheduled\" Assets:BTrade:Cash In this example, a \u201ccategory\u201d attribute is attached to an account\u2019s Open directive, a \u201cstatement\u201d attribute is attached to a Transaction directive (with a string value that represents a filename) and a \u201cdecision\u201d attribute has been attached to the first posting of the transaction (the additional indentation from the posting is not strictly necessary but it helps with readability). Metadata can be attached to any directive type. Keys must begin with a lowercase character from a-z and may contain (uppercase or lowercase) letters, numbers, dashes and underscores. Moreover, the values can be any of the following data types: Strings Accounts Currency Dates (datetime.date) Tags Numbers (Decimal) Amount (beancount.core.amount.Amount) There are two ways in which this data can be used: Query tools that come with Beancount (such as bean-query) will allow you to make use of the metadata values for filtering and aggregation. You can access and use the metadata in custom scripts. The metadata values are accessible as a \u201c .meta \u201d attribute on all directives and is a Python dict. There are no special meanings attached to particular attributes, these are intended for users to define. However, all directives are guaranteed to contain a \u2018 filename \u2019 (string) and a \u2018 lineno \u2019 (integer) attribute which reflect the location they were created from. Finally, attributes without a value will be parsed and have a value of 'None'. If an attribute is repeated multiple times, only the first value for this attribute will be parsed and retained and the following values ignored.","title":"Metadata"},{"location":"beancount_language_syntax.html#options","text":"The great majority of a Beancount input file consists in directives, as seen in the previous section. However, there are a few global options that can be set in the input file as well, by using a special undated \u201coption\u201d directive: option \"title\" \"Ed\u2019s Personal Ledger\" The general format of the Option directive is: option Name Value where Name and Value are both strings. Note that depending the option, the effect may be to set a single value, or add to a list of existing values. In other words, some options are lists. There are three ways to view the list of options: In this document , which I update regularly. To view the definitive list of options supported by your installed version, use the following command: bean-doctor list-options Finally, you can peek at the source code as well.","title":"Options"},{"location":"beancount_language_syntax.html#operating-currencies","text":"One notable option is \u201c operating_currency \u201d. By default Beancount does not treat any of the commodities any different from each other. In particular, it doesn\u2019t know that there\u2019s anything special about the most common of commodities one uses: their currencies. For example, if you live in New Zealand, you\u2019re going to have an overwhelming number of NZD commodities in your transactions. But useful reports try to reduce all non-currency commodities into one of the main currencies used. Also, it\u2019s useful to break out the currency units into their own dedicated columns. This may also be useful for exporting in order to avoid having to specify the units for that column and import to a spreadsheet with numbers you can process. For this reason, you are able to declare the most common currencies you use in an option: option \"operating_currency\" \"USD\" You may declare more than one. In any case, this option is only ever used by reporting code, it never changes the behavior of Beancount\u2019s processing or semantics.","title":"Operating Currencies"},{"location":"beancount_language_syntax.html#plugins","text":"In order to load plugin Python modules, use the dedicated \u201cplugin\u201d directive: plugin \"beancount.plugins.module_name\" The name of a plugin should be the name of a Python module in your PYTHONPATH. Those modules will be imported by the Beancount loader and run on the list of parsed entries in order for the plugins to transform the entries or output errors. This allows you to integrate some of your code within Beancount, making arbitrary transformations on the entries. See Scripting & Plugins for details. Plugins also optionally accept some configuration parameters. These can be provided by an optional final string argument, like this: plugin \"beancount.plugins.module_name\" \"configuration data\" The general format of the Option directive is: plugin ModuleName StringConfig The format of the configuration data is plugin-dependent. At the moment, an arbitrary string is passed provided to the plugin. See the plugins\u2019 documentation for specific detail on what it can accept. Also see the \u201cplugin processing mode\u201d option which affects the list of built-in plugins that get run.","title":"Plugins"},{"location":"beancount_language_syntax.html#includes","text":"Include directives are supported. This allows you to split up large input files into multiple files. The syntax looks like this: include \"path/to/include/file.beancount\" The general format is include Filename The specified path can be an absolute or a relative filename. If the filename is relative, it is relative to the including filename\u2019s directory. This makes it easy to put relative includes in a hierarchy of directories that can be placed under source control and checked out anywhere. Include directives are not processed strictly (as in C, for example). The include directives are accumulated by the Beancount parser and processed separately by the loader. This is possible because the order of declarations of a Beancount input file is not relevant. However, for the moment, options are parsed per-file. The options-map that is kept for post-parse processing is the options-map returned for the top-level file. This is probably subject to review in the future.","title":"Includes"},{"location":"beancount_language_syntax.html#whats-next","text":"This document described all the possible syntax of the Beancount language. If you haven\u2019t written any Beancount input yet, you can head to the Getting Started guide, or browse through a list of practical use cases in the Command-line Accounting Cookbook . Note that there exists an \u201cOpen\u201d directive that is used to provide the start date of each account. That can be located anywhere in the file, it does not have to appear in the file somewhere before you use an account name. You can just start using account names in transactions right away, though all account names that receive postings to them will eventually have to have a corresponding Open directive with a date that precedes all transactions posted to the account in the input file. \u21a9 Note that this is valid whether the price is specified as a per-unit price with the @ syntax or as a total price using the @@ syntax. \u21a9 I really dislike the name \u201cevent\u201d for this directive. I\u2019ve been trying to find a better alternative, so far without success. The name \u201cregister\u201d might be more appropriate, as it resembles that of a processor\u2019s register, but that could be confused with an account register report. \u201cvariable\u201d might work, but somehow that just sounds too computer-sciency and out of context. If you can think of something better, please make a suggestion and I\u2019ll seriously entertain a complete rename (with legacy support for \u201cevent\u201d). \u21a9","title":"What\u2019s Next?"},{"location":"beancount_options_reference.html","text":"option \"title\" \"Joe Smith's Personal Ledger\" The title of this ledger / input file. This shows up at the top of every page. option \"name_assets\" \"Assets\" option \"name_liabilities\" \"Liabilities\" option \"name_equity\" \"Equity\" option \"name_income\" \"Income\" option \"name_expenses\" \"Expenses\" Root names of every account. This can be used to customize your category names, so that if you prefer \"Revenue\" over \"Income\" or \"Capital\" over \"Equity\", you can set them here. The account names in your input files must match, and the parser will validate these. You should place these options at the beginning of your file, because they affect how the parser recognizes account names. option \"account_previous_balances\" \"Opening-Balances\" Leaf name of the equity account used for summarizing previous transactions into opening balances. option \"account_previous_earnings\" \"Earnings:Previous\" Leaf name of the equity account used for transferring previous retained earnings from income and expenses accrued before the beginning of the exercise into the balance sheet. option \"account_previous_conversions\" \"Conversions:Previous\" Leaf name of the equity account used for inserting conversions that will zero out remaining amounts due to transfers before the opening date. This will essentially \"fixup\" the basic accounting equation due to the errors that priced conversions introduce. option \"account_current_earnings\" \"Earnings:Current\" Leaf name of the equity account used for transferring current retained earnings from income and expenses accrued during the current exercise into the balance sheet. This is most often called \"Net Income\". option \"account_current_conversions\" \"Conversions:Current\" Leaf name of the equity account used for inserting conversions that will zero out remaining amounts due to transfers during the exercise period. option \"account_rounding\" \"Rounding\" The name of an account to be used to post to and accumulate rounding error. This is unset and this feature is disabled by default; setting this value to an account name will automatically enable the addition of postings on all transactions that have a residual amount. option \"conversion_currency\" \"NOTHING\" The imaginary currency used to convert all units for conversions at a degenerate rate of zero. This can be any currency name that isn't used in the rest of the ledger. Choose something unique that makes sense in your language. option \"inferred_tolerance_default\" \"CHF:0.01\" option \"default_tolerance\" \"CHF:0.01\" THIS OPTION IS DEPRECATED: This option has been renamed to 'inferred_tolerance_default' Mappings of currency to the tolerance used when it cannot be inferred automatically. The tolerance at hand is the one used for verifying (1) that transactions balance, (2) explicit balance checks from 'balance' directives balance, and (3) in the precision used for padding (from the 'pad' directive). The values must be strings in the following format: : for example, 'USD:0.005'. By default, the tolerance used for currencies without an inferred value is zero (which means infinite precision). As a special case, this value, that is, the fallabck value used for all currencies without an explicit default can be overridden using the '*' currency, like this: '*:0.5'. Used by itself, this last example sets the fallabck tolerance as '0.5' for all currencies. (Note: The new value of this option is \"inferred_tolerance_default\"; it renames the option which used to be called \"default_tolerance\". The latter name was confusing.) For detailed documentation about how precision is handled, see this doc: http://furius.ca/beancount/doc/tolerances (This option may be supplied multiple times.) option \"inferred_tolerance_multiplier\" \"1.1\" A multiplier for inferred tolerance values. When the tolerance values aren't specified explicitly via the 'inferred_tolerance_default' option, the tolerance is inferred from the numbers in the input file. For example, if a transaction has posting with a value like '32.424 CAD', the tolerance for CAD will be inferred to be 0.001 times some multiplier. This is the muliplier value. We normally assume that the institution we're reproducing this posting from applies rounding, and so the default value for the multiplier is 0.5, that is, half of the smallest digit encountered. You can customize this multiplier by changing this option, typically expanding it to account for amounts slightly beyond the usual tolerance, for example, if you deal with institutions with bad of unexpected rounding behaviour. For detailed documentation about how precision is handled, see this doc: http://furius.ca/beancount/doc/tolerances option \"infer_tolerance_from_cost\" \"True\" Enable a feature that expands the maximum tolerance inferred on transactions to include values on cost currencies inferred by postings held at-cost or converted at price. Those postings can imply a tolerance value by multiplying the smallest digit of the unit by the cost or price value and taking half of that value. For example, if a posting has an amount of \"2.345 RGAGX {45.00 USD}\" attached to it, it implies a tolerance of 0.001 x 45.00 * M = 0.045 USD (where M is the inferred_tolerance_multiplier) and this is added to the mix to enlarge the tolerance allowed for units of USD on that transaction. All the normally inferred tolerances (see http://furius.ca/beancount/doc/tolerances) are still taken into account. Enabling this flag only makes the tolerances potentially wider. option \"tolerance\" \"0.015\" THIS OPTION IS DEPRECATED: The 'tolerance' option has been deprecated and has no effect. The tolerance allowed for balance checks and padding directives. In the real world, rounding occurs in various places, and we need to allow a small (but very small) amount of tolerance in checking the balance of transactions and in requiring padding entries to be auto- inserted. This is the tolerance amount, which you can override. option \"use_legacy_fixed_tolerances\" \"True\" Restore the legacy fixed handling of tolerances. Balance and Pad directives have a fixed tolerance of 0.015 units, and Transactions balance at 0.005 units. For any units. This is intended as a way for people to revert the behavior of Beancount to ease the transition to the new inferred tolerance logic. See http://furius.ca/beancount/doc/tolerances for more details. option \"documents\" \"/path/to/your/documents/archive\" A list of directory roots, relative to the CWD, which should be searched for document files. For the document files to be automatically found they must have the following filename format: YYYY-MM-DD.(.*) (This option may be supplied multiple times.) option \"operating_currency\" \"USD\" A list of currencies that we single out during reporting and create dedicated columns for. This is used to indicate the main currencies that you work with in real life. (Refrain from listing all the possible currencies here, this is not what it is made for; just list the very principal currencies you use daily only.) Because our system is agnostic to any unit definition that occurs in the input file, we use this to display these values in table cells without their associated unit strings. This allows you to import the numbers in a spreadsheet (e.g, \"101.00 USD\" does not get parsed by a spreadsheet import, but \"101.00\" does). If you need to enter a list of operating currencies, you may input this option multiple times, that is, you repeat the entire directive once for each desired operating currency. (This option may be supplied multiple times.) option \"render_commas\" \"TRUE\" A boolean, true if the number formatting routines should output commas as thousand separators in numbers. option \"plugin_processing_mode\" \"raw\" A string that defines which set of plugins is to be run by the loader: if the mode is \"default\", a preset list of plugins are automatically run before any user plugin. If the mode is \"raw\", no preset plugins are run at all, only user plugins are run (the user should explicitly load the desired list of plugins by using the 'plugin' option. This is useful in case the user wants full control over the ordering in which the plugins are run). option \"plugin\" \"beancount.plugins.module_name\" THIS OPTION IS DEPRECATED: The 'plugin' option is deprecated; it should be replaced by the 'plugin' directive A list of Python modules containing transformation functions to run the entries through after parsing. The parser reads the entries as they are, transforms them through a list of standard functions, such as balance checks and inserting padding entries, and then hands the entries over to those plugins to add more auto-generated goodies. The list is a list of pairs/tuples, in the format (plugin-name, plugin-configuration). The plugin-name should be the name of a Python module to import, and within the module we expect a special '__plugins__' attribute that should list the name of transform functions to run the entries through. The plugin-configuration argument is an optional string to be provided by the user. Each function accepts a pair of (entries, options_map) and should return a pair of (new entries, error instances). If a plugin configuration is provided, it is provided as an extra argument to the plugin function. Errors should not be printed out the output, they will be converted to strings by the loader and displayed as dictated by the output medium. (This option may be supplied multiple times.) option \"long_string_maxlines\" \"64\" The number of lines beyond which a multi-line string will trigger a overly long line warning. This warning is meant to help detect a dangling quote by warning users of unexpectedly long strings. option \"experiment_explicit_tolerances\" \"True\" Enable an EXPERIMENTAL feature that supports an explicit tolerance value on Balance assertions. If enabled, the balance amount supports a tolerance in the input, with this syntax: ~ , for example, \"532.23 ~ 0.001 USD\". See the document on tolerances for more details: http://furius.ca/beancount/doc/tolerances WARNING: This feature may go away at any time. It is an exploration to see if it is truly useful. We may be able to do without. option \"booking_method\" \"SIMPLE\" The booking method to apply, for interpolation and for matching lot specifications to the available lots in an inventory at the moment of the transaction. Values may be 'SIMPLE' for the original method used in Beancount, or 'FULL' for the newer method that does fuzzy matching against the inventory and allows multiple amounts to be interpolated (see http://furius.ca/beancount/doc/proposal-booking for details).","title":"Beancount Options Reference"},{"location":"beancount_query_language.html","text":"Beancount Query Language \uf0c1 Martin Blais, January 2015 http://furius.ca/beancount/doc/query Introduction \uf0c1 The purpose of Beancount is to allow the user to create an accurate and error-free representation of financial transactions, typically those occurring in a user or in an institution\u2019s associated set of accounts, to then extract various reports from this list of transactions. Beancount provides a few tools to extract reports from the corpus of transactions: custom reports (using the Beancount bean-report tool), a web interface (using the bean-web tool) and the ability for the user to write their own scripts to output anything they want. The repository of financial transactions is always read from the text file input, but once parsed and loaded in memory, extracting information from Beancount could be carried out much like you would another database, that is, instead of using custom code to generate output from the data structures, a query language could be compiled and run over the relatively regular list of transactions. In practice, you could flatten out the list of postings to an external SQL database and make queries using that database\u2019s own tools, but the results of this approach are quite disappointing, mainly due to the lack of operations on inventories which is the basis of balancing rules in Beancount. By providing a slightly specialized query engine that takes advantage of the structure of the double-entry transactions we can easily generate custom reports specific to accounting purposes. This document describes our specialized SQL-like query client. It assumes you have at least a passing knowledge of SQL syntax. If not, you may want to first read something about it. Motivation \uf0c1 So one might ask: Why create another SQL client? Why not output the data to an SQLite database and allow the user to use that SQL client? Well, we have done that (see the bean-sql script which converts your Beancount ledger into an SQLite database) and the results are not great. Writing queries is painful and carrying out operations on lots that are held at cost is difficult. By taking advantage of a few aspects of our in-memory data structures, we can do better. So Beancount comes with its own SQL-like query client called \u201c bean-query \u201d. The clients implements the following \u201cextras\u201d that are essential to Beancount: It allows to easily filter at two levels simultaneously: You can filter whole transactions, which has the benefit of respecting the accounting equation, and then, usually for presentation purposes, you can also filter at the postings level. The client supports the semantics of inventory booking implemented in Beancount. It also supports aggregation functions on inventory objects and rendering functions (e.g., COST() to render the cost of an inventory instead of its contents). The client allows you to flatten multiple lots into separate postings to produce lists of holdings each with their associated cost basis. Transactions can be summarized in a manner useful to produce balance sheets and income statements. For example, our SQL variant explicitly supports a \u201cclose\u201d operation with an effect similar to closing the year, which inserts transactions to clear income statement accounts to equity and removes past history. See this post as well for a similar answer. Warning & Caveat \uf0c1 Approximately 70% of the features desired in the original design doc were implemented in the query language in late 2014. More work will be needed to cover the full feature set, but the current iteration supports most of the use cases covered by Ledger, and I suspect by Beancount users. More feedback is desired on the current version before moving on, and I would like to move forward improving some of the more fundamental aspects of Beancount (namely, inventory booking) before spending more time on the query language. It is functional as it is, but a second revision will be made later on, informed by user feedback and prolonged use. Therefore, a first release of the query language has been merged in the default stable branch. This document presents this first iteration on the Beancount query language. Making Queries \uf0c1 The custom query client that we provide is called bean-query . Run it on your ledger file, like this: $ bean-query myfile.beancount Input file: \"My Ledger\u2019s Title\" Ready with 13996 directives (21112 postings in 8833 transactions). beancount> _ This launches the query tool in interactive mode, where you can enter multiple commands on the dataset loaded in memory. bean-query parses the input file, spits out a few basic statistics about your ledger, and provides a command prompt for you to enter query commands. You can type \u201c help \u201d here to view the list of available commands. If any errors in your ledger are incurred, they are printed before the prompt. To suppress error printing, run the tool with the \u201cno-errors\u201d option: $ bean-query -q myfile.beancount Batch Mode Queries \uf0c1 If you\u2019d like to run queries directly from the command-line, without an interactive prompt, you can provide the query directly following your filename: $ bean-query myfile.beancount 'balances from year = 2014' account balance ---------------------------------------------------------------------- \u2026 \u2026 All the interactive commands are supported. \uf0c1 Shell Variables \uf0c1 The interactive shell has a few \u201c set \u201d variables that you can customize to change some of the behavior of the shell. These are like environment variables. Type the \u201c set \u201d command to see the list of available variables and their current value. The variables are: format (string): The output format. Currently, only \u201ctext\u201d is supported. boxed (boolean): Whether we should draw a box around the output table. spaced (boolean): Whether to insert an empty line between every result row. This is only relevant because postings with multiple lots may require multiple lines to be rendered, and inserting an empty line helps delineate those as separate. pager (string): The name of the pager program to pipe multi-page output to when the output is larger than the screen. The initial value is copied from the PAGER environment variable. expand (boolean): If true, expand columns that render to lists on multiple rows. Transactions and Postings \uf0c1 The structure of transactions and entries can be explained by the following simplified diagram: \uf0c1 The contents of a ledger is parsed into a list of directives, most of which are \u201cTransaction\u201d objects which contain two or more \u201cPosting\u201d objects. Postings are always linked only to a single transaction (they are never shared between transactions). Each posting refers to its parent transaction but has a unique account name, amount and associated lot (possibly with a cost), a price and some other attributes. The parent transaction itself contains a few useful attributes as well, such as a date, the name of a payee, a narration string, a flag, links, tags, etc. If we ignore the list of directives other than transactions, you can view the dataset as a single table of all postings joined with their parent transaction. It is mainly on this joined table of postings that we want to perform filtering and aggregation operations. However, because of the double-entry bookkeeping constraint, that is, the sum of amounts on postings attached to a transaction is zero, it is also quite useful to perform filtering operations at the transaction level. Because any isolated transaction has a total impact of zero on the global balance, any subset of transactions will also respect the accounting equation (Assets + Liabilities + Equity + Income + Expenses = 0), and producing balance sheets and income statements on subset of transactions provides meaningful views, for example, \u201call asset changes and expenses incurred during a trip to the Bahamas\u201d which could be selected by a tag. For this reason, we modify the SQL SELECT syntax to provide a two-level filtering syntax: since we have a single table of data, we replace the table name in FROM by a filtering expression which applies over transactions, and the WHERE clause applies to data pulled from the resulting list of postings: SELECT , , \u2026 FROM WHERE ; Both filtering expressions are optional. If no filtering expressions are provided, all postings will be enumerated over. Note that since the transactions are always filtered in date order, the results will be processed and returned in this order by default. Posting Data Columns \uf0c1 The list of targets refers to attributes of postings or of their parent transaction. The same list of \u201ccolumns\u201d is made available in the , to filter by posting attributes. For example, you could write the following query: SELECT date, narration, account, position WHERE account ~ \u201c.*:Vacation\u201d AND year >= 2014; Here, the \u201cdate\u201d, \u201cyear\u201d and \u201cnarration\u201d columns refer to attributes of the parent transaction, the \u201caccount\u201d and \u201cposition\u201d columns refer to attributes of the posting itself. You may name targets explicitly with the familiar AS operator: SELECT last(date) as last_date, cost(sum(position)) as cost; The full list of posting columns and functions available on them is best viewed by querying your actual client using \u201c help targets \u201c or \u201c help where \u201d, which prints out the list and data type of each available data column. You may also refer to the following diagram of the structure of a Posting object for the correspondence between the columns and the data structure attributes. Entry Data Columns \uf0c1 A different list of column names is available on the of the FROM clause. These columns refer to attributes of the Transaction objects. This clause is intended to filter whole transactions (i.e., all their postings or none at all). Available attributes include the date, transaction flag, the optional payee, narration, set of tags and links. Use the \u201c help from \u201d command to find the complete list of columns and functions available in this clause. A Beancount input file consists of many different types of entries, not just transactions. Some of these other types of entries (such as Open, Close, Balance, etc.) may also provide attributes that can be accessed from the FROM clause. This is embryonic at this point. (It\u2019s unclear yet how these will be used in the future, but I suspect we will find some interesting applications for them eventually. The FROM clause provides access to the type of the data entry via column \u201c type \u201d. It\u2019s still an exploration how much we can make pragmatic use of the SQL language for other types of directives.) The \u201cid\u201d Column A special column exists that identifies each transaction uniquely: \u201c id \u201d. It is a unique hash automatically computed from the transaction and should be stable between runs. SELECT DISTINCT id; This hash is derived from the contents of the transaction object itself (if you change something about the transaction, e.g. you edit the narration, the id will change). You can print and select using this column. It can be used for debugging, e.g. PRINT FROM id = '8e7c47250d040ae2b85de580dd4f5c2a'; The \u201cbalance\u201d Column One common desired output is a journal of entries over time (also called a \u201cregister\u201d in Ledger): SELECT date, account, position WHERE account ~ \"Chase:Slate\"; For this type of report, it is convenient to also render a column of the cumulative balance of the selected postings rows. Access to the previous row is not a standard SQL feature, so we get a little creative and provide a special column called \u201c balance \u201d which is automatically calculated based on the previous selected rows: SELECT date, account, position, balance WHERE account ~ \"Chase:Slate\"; This provides the ability to render typical account statements such as those mailed to you by a bank. Output might look like this: $ bean-query $T \"select date, account, position, balance where account ~ 'Expenses:Food:Restaurant';\" date account position balance ---------- ------------------------ --------- ---------- 2012-01-02 Expenses:Food:Restaurant 31.02 USD 31.02 USD 2012-01-04 Expenses:Food:Restaurant 25.33 USD 56.35 USD 2012-01-08 Expenses:Food:Restaurant 67.88 USD 124.23 USD 2012-01-09 Expenses:Food:Restaurant 35.28 USD 159.51 USD 2012-01-14 Expenses:Food:Restaurant 25.84 USD 185.35 USD 2012-01-17 Expenses:Food:Restaurant 36.73 USD 222.08 USD 2012-01-21 Expenses:Food:Restaurant 28.11 USD 250.19 USD 2012-01-22 Expenses:Food:Restaurant 21.12 USD 271.31 USD Wildcard Targets \uf0c1 Using a wildcard as the target list (\u201c*\u201d) select a good default list of columns: SELECT * FROM year = 2014; To view the actual list of columns selected, you can use the EXPLAIN prefix: EXPLAIN SELECT * FROM year = 2014; Data Types \uf0c1 The data attributes extracted from the postings or transactions have particular types. Most of the data types are regular types as provided by the underlying Python implementation language, types such as String (Python str) Date (a datetime.date instance). You can parse a date with the #\"...\" syntax; this uses Python\u2019s dateutil module and is pretty liberal in the formats it accepts. Integer (Python int) Boolean (Python bool object), as TRUE , FALSE Number (a decimal.Decimal object) Set of Strings (a Python set of str objects) Null objects ( NULL ) Positions and Inventories \uf0c1 However, one reason that our SQL-like client exists in the first place is for its ability to carry out aggregation operations on inventories of positions, the data structures at the core of Beancount, that implements its balancing semantics. Internally, Beancount defines Position and Inventory objects and is able to aggregate them together in an instance of Inventory. On each Posting, the \u201cposition\u201d column extracts an object of type Position, which when summed over produces an instance of Inventory. The shell is able to display those appropriately. More specifically, Inventory objects can contain multiple different lots of holdings, and each of these will get rendered on a separate line. Quantities of Positions and Inventories \uf0c1 Objects of type Position are rendered in their full detail by default, including not just their number and currency, but the details of their lot. Inventories consist of a list of lots, and as such are rendered similarly, as a list of positions (one per line) each with their full detail by default. This is generally too much detail. The shell provides functions that allow the user to summarize the positions into one of the various derived quantities. The types of derived quantities are: \u201c raw \u201d: render the position in its full detail, including cost and lot date \u201c units \u201d: render just the number and currency of the position \u201c cost \u201d: render the total cost of the position, that is the number of units x the per-unit cost \u201c weight \u201d: render the amount that is used to balance the postings of a transaction. The main distinction between cost and weight is for postings with a price conversion. \u201c value \u201d: render the amount at the market value of the last entry rendered. Functions with the same names are available to operate on position or inventory objects. For example, one could generate a table of final balances for each account like this: SELECT account, units(sum(position)), cost(sum(position)) GROUP BY 1; Refer to the table below for explicit examples of each type of posting and how it would get converted and rendered. posting raw (full detail) units cost weight market Simple 50.00 USD 50.00 USD 50.00 USD 50.00 USD 50.00 USD With Price Conversion 50.00 USD @ 1.35 CAD 50.00 USD 50.00 USD 67.50 CAD 50.00 USD Held at Cost 50 VEA {1.35 CAD} 50 VEA 67.50 CAD 67.50 CAD 67.50 CAD Held at Cost with Price 50 VEA {1.35 CAD} @ 1.45 CAD 50 VEA 67.50 CAD 67.50 CAD 72.50 CAD Operators \uf0c1 Common comparison and logical operators are provided to operate on the available data columns: = (equality), != (inequality) < (less than), <= (less than or equal) (greater than), >= (greater than or equal) AND (logical conjunction) OR (logical disjunction) NOT (logical negation) IN (set membership) We also provide a regular expression search operator into a string object: ~ (search regexp) At the moment, matching groups are ignored. You can use string, number and integer constants with those operators, and parentheses to explicitly state precedence. You can use the #\u201d...\u201d literal syntax to input dates (valid contents for the string are pretty liberal, it supports anything Python\u2019s dateutil.parser supports). Here is an example query that uses a few of these: SELECT date, payee WHERE account ~ 'Expenses:Food:Restaurant' AND 'trip-new-york' IN tags AND NOT payee = 'Uncle Boons' Unlike SQL, bean-query does not implement three-valued logic for NULL . This means that e.g. the expression NULL = NULL yields TRUE instead of NULL , which simplifies things, but may come as a surprise to veteran SQL users. Simple Functions \uf0c1 The shell provides a list of simple function that operate on a single data column and return a new value. These functions operate on particular types. The shell implements rudimentary type verification and should be able to warn you on incompatible types. Some example functions follow: COST(Inventory), COST(Position): Return an Amount, the cost of the position or inventory. UNITS(Inventory), UNITS(Position): Return the units of the position or inventory. DAY(date), MONTH(date), YEAR(date): Return an integer, the day, month or year of the posting or entry\u2019s date. LENGTH(list): Computes the length of a list or a set, e.g. on tags. PARENT(account-string): Returns the name of the parent account. These are just examples; for the complete list, see \u201c help targets \u201d, \u201c help where \u201d, \u201c help from \u201d. Note that it is exceedingly easy to add new functions to this list. As of December 2014, we are just beginning using the shell widely and we expect to be adding new functions as needed. If you need a function, please add a comment here or log a ticket and we will consider adding it to the list (we understand that the current list is limited). I intend to be liberal about adding new functions; as long as they have generic application, I don\u2019t think it should be a problem. Otherwise, I may be able to provide a mechanism for user to register new functions as part of Python plugins that could live outside the Beancount codebase. Aggregate Functions \uf0c1 Some functions operate on more than a single row. These functions aggregate and summarize the multiple values for the data column that they operate on. A prototypical usage of such a function is to sum the positions in an inventory: SELECT account, sum(position) WHERE account ~ 'Income' GROUP BY account; If a query target has at least one aggregating function, the query becomes an aggregated query (see relevant section for details). Note that you cannot use aggregation functions in the FROM or WHERE clauses. Examples of aggregate functions include: COUNT(...): Computes the number of postings selected (an integer). FIRST(...), LAST(...): Returns first or last value seen. MIN(...), MAX(...): Computes the minimum or maximum value seen. SUM(...): Sums up the values of each set. This works on amounts, positions, inventories, numbers, etc. As for simple functions, this is just a starting list. We will be adding more as needed. Use \u201c help targets \u201d to access the full list of available aggregate functions. Note: You cannot filter (using a WHERE clause) the results of aggregation functions; this requires the implementation offering a HAVING clause, and at the moment, HAVING filtering is not yet implemented. Simple vs. Aggregated Queries \uf0c1 There are two types of queries: Simple queries , which produce a row of results for each posting that matches the restricts in the WHERE clause. Aggregate queries , which produce a row of results for each group of postings that match the restricts in the WHERE clause. A query is \u201caggregate\u201d if it has at least one aggregate function in its list of targets. In order to identify the aggregation keys, all the non-aggregate columns have to be flagged using the GROUP BY clause, like this: SELECT payee, account, COST(SUM(position)), LAST(date) GROUP BY payee, account; You may also use the positional order of the targets to declare the group key, like this: SELECT payee, account, COST(SUM(position)), LAST(date) GROUP BY 1, 2; Furthermore, if you name your targets, you can use the explicit target names: SELECT payee, account as acc, COST(SUM(position)), LAST(date) GROUP BY 1, acc; This should all feel familiar if you have preliminary knowledge of SQL. Finally, because we implement a limited version of SQL, and that the simple columns must always be specified, omitting the GROUP BY clause should also eventually work and we should group by those columns implicitly, as a convenience. Distinct \uf0c1 There is a post-filtering phase that supports uniquifying result rows. You can trigger this unique filter with the DISTINCT flag after SELECT , as is common in SQL, e.g. SELECT DISTINCT account; Controlling Results \uf0c1 Order By \uf0c1 Analogous to the GROUP BY clause is an ORDER BY clause that controls the final ordering of the result rows: SELECT \u2026 GROUP BY account, payee ORDER BY payee, date; The clause is optional. If you do not specify it, the default order of iteration of selected postings is used to output the results (that is, the order of transactions-sorted by date- and then their postings). As in SQL, you may reverse the order of sorting by a DESC suffix (the default is the same as specifying ASC ): SELECT \u2026 GROUP BY account, payee ORDER BY payee, date DESC; Limit \uf0c1 Our query language also supports a LIMIT clause to interrupt output row generation: SELECT \u2026 LIMIT 100; This would output the first 100 result rows and then stop. While this is a common clause present in the SQL language, in the context of double-entry bookkeeping it is not very useful: we always have relatively small datasets to work from. Nevertheless, we provide it for completeness. Format \uf0c1 For SELECT , JOURNAL and BALANCES queries, the output format is a table of text by default. We support CSV output. ( We could easily add support for XLS or Google Sheets output.) However, for PRINT queries, the output format is Beancount input text format. Statement Operators \uf0c1 The shell provides a few operators designed to facilitate the generation of balance sheets and income statements. The particular methodology used to define these operations should be described in detail in the \u201c introduction to double-entry bookkeeping \u201d document that accompanies Beancount and is mostly located in the source code in the summarize module. These special operators are provided on the FROM clause that is made available on the various forms of query commands in the shell. These further transform the set of entries selected by the FROM expression at the transaction levels (not postings). Please note that these are not from standard SQL; these are extensions provided by this shell language only. Opening a Period \uf0c1 Opening an exercise period at a particular date replaces all entries before that date by summarization entries that book the expected balance against an Equity \u201copening balances\u201d account and implicitly clears the income and expenses to zero by transferring their balances to an Equity \u201cprevious earnings\u201d account (see beancount.ops.summarize.open() for implementation details). It is invoked like this: SELECT \u2026 FROM OPEN ON \u2026 For example: SELECT * FROM has_account(\"Invest\") OPEN ON 2014-01-01; If you want, you can view just the inserted summarization entries like this: PRINT FROM flag = \"S\" AND account ~ \"Invest\" OPEN ON 2014-01-01; Closing a Period \uf0c1 Closing an exercise period involves mainly truncating all entries that come after the given date and ensuring that currency conversions are correctly corrected for (see beancount.ops.summarize.close() for implementation details). It is invoked like this: SELECT \u2026 FROM CLOSE [ON ] \u2026 For example: SELECT * FROM has_account(\"Invest\") CLOSE ON 2015-04-01; Note that the closing date should be one day after the last transaction you would like to include (this is in line with the convention we use everywhere in Beancount whereby starting dates are inclusive and ending dates exclusive). The closing date is optional. If the date is not specified, the date one day beyond the date of the last entry is used. Closing a period leaves the Income and Expenses accounts as they are, that is, their balances are not cleared to zero to Equity. This is because closing is also used to produce final balances for income statements. \u201cClearing\u201d, as described in the next section, is only needed for balance sheets. Clearing Income & Expenses \uf0c1 In order to produce a balance sheet, we need to transfer final balances of the Income and Expenses to an Equity \u201ccurrent earnings\u201d account (sometimes called \u201cretained earnings\u201d or \u201cnet income\u201d; you can select the specific account name to use using options in the input file). The resulting balances of income statement accounts should be zero (see beancount.ops.summarize.clear() for implementation details). You can clear like this: SELECT \u2026 FROM CLEAR \u2026 For example: SELECT * FROM has_account(\"Invest\") CLOSE ON 2015-04-01 CLEAR; This is a statement suitable to produce a list of accounts to build a balance sheet. The \u201c Equity:Earnings:Current \u201d (by default) will contain the net income accumulated during the preceding period. No balances for the Income nor Expenses accounts should appear in the output. Example Statements \uf0c1 The statement operators of course may be combined. For instance, if you wanted to output data for an income statement for year 2013, you could issue the following statement: SELECT account, sum(position) FROM OPEN ON 2013-01-01 CLOSE ON 2014-01-01 WHERE account ~ \"Income|Expenses\" GROUP BY 1 ORDER BY 1; This would produce a list of balances for Income and Expenses accounts. To generate a balance sheet, you would add the CLEAR option and select the other accounts: SELECT account, sum(position) FROM OPEN ON 2013-01-01 CLOSE ON 2014-01-01 CLEAR WHERE not account ~ \"Income|Expenses\" GROUP BY 1 ORDER BY 1; Note that if you added the CLEAR operator to the statement of income statement accounts, all the balances would show at zero because of the inserted transactions that move those balances to the Equity net income account at the end of the period. It is relevant to notice that the examples above do not filter the transactions any further. If you are selecting a subset of transactions you may want to leave the accounts unopened, unclosed and uncleared because applying only some of the transactions on top of the opening balances of Assets and Liabilities accounts will not produce correct balances for those accounts. It would be more useful to leave them all opened, and to interpret the balances of the balance sheet accounts as the changes in those accounts for the subset of transactions selected. For example, if you selected all transactions from a trip (using a tag), you would obtain a list of changes in Expenses (and possibly Income) tagged as being included in this trip, and the Assets and Liabilities accounts would show where the funds for those Expenses came from. Consult the \u201cintroduction to double-entry method\u201d document for a pictorial representation of this. (Granted, this is probably worth a dedicated document and I might produce one at some point.) Example Fetching Cost Basis \uf0c1 \u201c... is there currently an easy way to determine what my cost basis is for an account on a given date (other than manually adding up UNITS * COST for every contribution, which is kind of a pain)? I'm trying to estimate the tax implications of potential stock sales.\u201d [Question from Matthew Harris] For a detailed report of share movements: SELECT account, currency, position, COST(position) WHERE year <= 2015 AND account ~ \"Assets:US:Schwab\" AND currency != \"USD\" Or this for the sum total of the cost bases: SELECT sum(cost(position)) WHERE year <= 2015 AND account ~ \"Assets:US:Schwab\" AND currency != \"USD\" High-Level Shortcuts \uf0c1 There are two types of queries that are very common for accounting applications: journals and balances reports. While we have explicit implementations of such reports that can be produced using the bean-report tool, we are also able to synthesize good approximations of such reports using SELECT statements. This section describes a few additional selection commands that translate directly into SELECT statements and which are then run with the same query code. These are intended as convenient shortcuts. Selecting Journals \uf0c1 A common type of query is one that generates a linear journal of entries (Ledger calls this a \u201cregister\u201d). This roughly corresponds to an account statement, but with our language, such a statement can be generated for any subset of postings. You can generate a journal with the following syntax: JOURNAL [AT ] [FROM \u2026] The regular expression account-regexp is used to select which subset of accounts to generate a journal for. The optional \u201cAT \u201d clause is used to specify an aggregation function for the amounts rendered (typically UNITS or COST ). The FROM clause follows the same rules as for the SELECT statement and is optional. Here is an example journal-generating query: JOURNAL \"Invest\" AT COST FROM HAS_ACCOUNT(\"Assets:US\"); Selecting Balances \uf0c1 The other most common type of report is a table of the balances of various accounts at a particular date. This can be viewed as a SELECT query aggregating positions grouping by account. You can generate a balances report with the following syntax: BALANCES [AT ] [FROM \u2026] The optional \u201cAT \u201d clause is used to specify an aggregation function for the balances rendered (usually UNITS or COST ). The FROM clause follows the same rules as for the SELECT statement and is optional. To generate your balances at a particular date, close your set of entries using the \u201c FROM\u2026 CLOSE ON \u201d form described above. Observe that typical balance sheets and income statements seen in an accounting context are subsets of tables of balances such as reported by this query. An income statement reports on just the transactions that appears during a period of time, and a balance sheet summarizes transactions before its reporting before and clears the income & expenses accumulated during the period to an equity account. Then some minor reformatting is carried out. Please consult the introduction document on double-entry bookkeeping for more details, and the section above that discusses the \u201copen\u201d, \u201cclose\u201d and \u201cclear\u201d operations. We will also be providing a separate text processing tool that can accept balance reports and reformat them in a two-column format similar to that you would see balance sheets and income statements. Print \uf0c1 It can be useful to generate output in Beancount format, so that subsets of transactions can be saved to files, for example. The shell provides that ability via the PRINT command: PRINT [FROM \u2026] The FROM clause obeys the usual semantics as described elsewhere in this document. The resulting filtered stream of Beancount entries is then printed out on the output in Beancount syntax. In particular, just running the \u201c PRINT \u201d command will spit out the parsed and loaded contents of a Beancount file. You can use this for troubleshooting if needed, or to expand transactions generated from a plugin you may be in the process of developing. Debugging / Explain \uf0c1 If you\u2019re having trouble getting a particular statement to compile and run,. you can prefix any query statement with the EXPLAIN modifier, e.g.: EXPLAIN SELECT \u2026 This will not run the statement, but rather print out the intermediate AST and compiled representation as well as the list of computed statements. This can be useful to report bugs on the mailing-list. Also, this shows you the translated form of the JOURNAL and BALANCES statements. Future Features \uf0c1 The following list of features were planned for the first release but I\u2019ve decided to make a first cut without them. I\u2019ll be adding those during a revision. Flattening Inventories \uf0c1 If you provide the FLATTEN option after a query, it tells the query engine to flatten inventories with multiple lots into separate rows for each lot. For example, if you have an inventory balance with the following contents: 3 AAPL {102.34 USD} 4 AAPL {104.53 USD} 5 AAPL {106.23 USD} Using the following query: SELECT account, sum(position) GROUP BY account; It should return a single row of results, rendered over three lines. However, adding the option: SELECT account, sum(position) GROUP BY account FLATTEN; This should return three separate rows, with all the selected attributes, as if there were that many postings. Sub-Selects \uf0c1 The ability to select from the result of another SELECT is not currently supported, but the internals of the query code are prepared to do so. More Information \uf0c1 This document attempts to provide a good high-level summary of the features supported in our query language. However, should you find you need more information, you may take a look at the original proposal , or consult the source code under the beancount.query directory. In particular, the parser will provide insight into the specifics of the syntax, and the environments will shed some light on the supported data columns and functions. Feel free to rummage in the source code and ask questions on the mailing-list. Appendix \uf0c1 Future Features \uf0c1 This section documents ideas for features to be implemented in a future version. Pivot By \uf0c1 A special PIVOT BY clause will eventually be usable to convert the output from a one-dimensional list of results to a two-dimensional table. For example, the following query: SELECT account, YEAR(date) AS year, SUM(COST(position)) AS balance WHERE account ~ 'Expenses:Food' AND currency = 'USD' AND year >= 2012 GROUP BY 1,2 ORDER BY 1,2; Might generate the following table of results: account year balance ------------------------ ---- ----------- Expenses:Food:Alcohol 2012 57.91 USD Expenses:Food:Alcohol 2013 33.45 USD Expenses:Food:Coffee 2012 42.07 USD Expenses:Food:Coffee 2013 124.69 USD Expenses:Food:Coffee 2014 38.74 USD Expenses:Food:Groceries 2012 2172.97 USD Expenses:Food:Groceries 2013 2161.90 USD Expenses:Food:Groceries 2014 2072.36 USD Expenses:Food:Restaurant 2012 4310.60 USD Expenses:Food:Restaurant 2013 5053.61 USD Expenses:Food:Restaurant 2014 4209.06 USD If you add a PIVOT clause to the query, like this: \u2026 PIVOT BY account, year; You would get a table like this: account/year 2012 2013 2014 ------------------------ ----------- ----------- ----------- Expenses:Food:Alcohol 57.91 USD 33.45 USD Expenses:Food:Coffee 42.07 USD 124.69 USD 38.74 USD Expenses:Food:Groceries 2172.97 USD 2161.90 USD 2072.36 USD Expenses:Food:Restaurant 4310.60 USD 5053.61 USD 4209.06 USD If your table has more than three columns, the non-pivoted column would get expanded horizontally, like this: SELECT account, YEAR(date), SUM(COST(position)) AS balance, LAST(date) AS updated WHERE account ~ 'Expenses:Food' AND currency = 'USD' AND year >= 2012 GROUP BY 1,2 ORDER BY 1, 2; You would get a table like this: account year balance updated ------------------------ ---- ----------- ---------- Expenses:Food:Alcohol 2012 57.91 USD 2012-07-17 Expenses:Food:Alcohol 2013 33.45 USD 2013-12-13 Expenses:Food:Coffee 2012 42.07 USD 2012-07-19 Expenses:Food:Coffee 2013 124.69 USD 2013-12-16 Expenses:Food:Coffee 2014 38.74 USD 2014-09-21 Expenses:Food:Groceries 2012 2172.97 USD 2012-12-30 Expenses:Food:Groceries 2013 2161.90 USD 2013-12-31 Expenses:Food:Groceries 2014 2072.36 USD 2014-11-20 Expenses:Food:Restaurant 2012 4310.60 USD 2012-12-30 Expenses:Food:Restaurant 2013 5053.61 USD 2013-12-29 Expenses:Food:Restaurant 2014 4209.06 USD 2014-11-28 Pivoting, this would generate this table: account/balance,updated 2012/balanc 2012/updat 2013/balanc 2013/updat 2014/balanc 2014/updat ------------------------ ----------- ---------- ----------- ---------- ----------- ---------- Expenses:Food:Alcohol 57.91 USD 2012-07-17 33.45 USD 2013-12-13 Expenses:Food:Coffee 42.07 USD 2012-07-19 124.69 USD 2013-12-16 38.74 USD 2014-09-21 Expenses:Food:Groceries 2172.97 USD 2012-12-30 2161.90 USD 2013-12-31 2072.36 USD 2014-11-20 Expenses:Food:Restaurant 4310.60 USD 2012-12-30 5053.61 USD 2013-12-29 4209.06 USD 2014-11-28","title":"Beancount Query Language"},{"location":"beancount_query_language.html#beancount-query-language","text":"Martin Blais, January 2015 http://furius.ca/beancount/doc/query","title":"Beancount Query Language"},{"location":"beancount_query_language.html#introduction","text":"The purpose of Beancount is to allow the user to create an accurate and error-free representation of financial transactions, typically those occurring in a user or in an institution\u2019s associated set of accounts, to then extract various reports from this list of transactions. Beancount provides a few tools to extract reports from the corpus of transactions: custom reports (using the Beancount bean-report tool), a web interface (using the bean-web tool) and the ability for the user to write their own scripts to output anything they want. The repository of financial transactions is always read from the text file input, but once parsed and loaded in memory, extracting information from Beancount could be carried out much like you would another database, that is, instead of using custom code to generate output from the data structures, a query language could be compiled and run over the relatively regular list of transactions. In practice, you could flatten out the list of postings to an external SQL database and make queries using that database\u2019s own tools, but the results of this approach are quite disappointing, mainly due to the lack of operations on inventories which is the basis of balancing rules in Beancount. By providing a slightly specialized query engine that takes advantage of the structure of the double-entry transactions we can easily generate custom reports specific to accounting purposes. This document describes our specialized SQL-like query client. It assumes you have at least a passing knowledge of SQL syntax. If not, you may want to first read something about it.","title":"Introduction"},{"location":"beancount_query_language.html#motivation","text":"So one might ask: Why create another SQL client? Why not output the data to an SQLite database and allow the user to use that SQL client? Well, we have done that (see the bean-sql script which converts your Beancount ledger into an SQLite database) and the results are not great. Writing queries is painful and carrying out operations on lots that are held at cost is difficult. By taking advantage of a few aspects of our in-memory data structures, we can do better. So Beancount comes with its own SQL-like query client called \u201c bean-query \u201d. The clients implements the following \u201cextras\u201d that are essential to Beancount: It allows to easily filter at two levels simultaneously: You can filter whole transactions, which has the benefit of respecting the accounting equation, and then, usually for presentation purposes, you can also filter at the postings level. The client supports the semantics of inventory booking implemented in Beancount. It also supports aggregation functions on inventory objects and rendering functions (e.g., COST() to render the cost of an inventory instead of its contents). The client allows you to flatten multiple lots into separate postings to produce lists of holdings each with their associated cost basis. Transactions can be summarized in a manner useful to produce balance sheets and income statements. For example, our SQL variant explicitly supports a \u201cclose\u201d operation with an effect similar to closing the year, which inserts transactions to clear income statement accounts to equity and removes past history. See this post as well for a similar answer.","title":"Motivation"},{"location":"beancount_query_language.html#warning-caveat","text":"Approximately 70% of the features desired in the original design doc were implemented in the query language in late 2014. More work will be needed to cover the full feature set, but the current iteration supports most of the use cases covered by Ledger, and I suspect by Beancount users. More feedback is desired on the current version before moving on, and I would like to move forward improving some of the more fundamental aspects of Beancount (namely, inventory booking) before spending more time on the query language. It is functional as it is, but a second revision will be made later on, informed by user feedback and prolonged use. Therefore, a first release of the query language has been merged in the default stable branch. This document presents this first iteration on the Beancount query language.","title":"Warning & Caveat"},{"location":"beancount_query_language.html#making-queries","text":"The custom query client that we provide is called bean-query . Run it on your ledger file, like this: $ bean-query myfile.beancount Input file: \"My Ledger\u2019s Title\" Ready with 13996 directives (21112 postings in 8833 transactions). beancount> _ This launches the query tool in interactive mode, where you can enter multiple commands on the dataset loaded in memory. bean-query parses the input file, spits out a few basic statistics about your ledger, and provides a command prompt for you to enter query commands. You can type \u201c help \u201d here to view the list of available commands. If any errors in your ledger are incurred, they are printed before the prompt. To suppress error printing, run the tool with the \u201cno-errors\u201d option: $ bean-query -q myfile.beancount","title":"Making Queries"},{"location":"beancount_query_language.html#batch-mode-queries","text":"If you\u2019d like to run queries directly from the command-line, without an interactive prompt, you can provide the query directly following your filename: $ bean-query myfile.beancount 'balances from year = 2014' account balance ---------------------------------------------------------------------- \u2026 \u2026","title":"Batch Mode Queries"},{"location":"beancount_query_language.html#all-the-interactive-commands-are-supported","text":"","title":"All the interactive commands are supported."},{"location":"beancount_query_language.html#shell-variables","text":"The interactive shell has a few \u201c set \u201d variables that you can customize to change some of the behavior of the shell. These are like environment variables. Type the \u201c set \u201d command to see the list of available variables and their current value. The variables are: format (string): The output format. Currently, only \u201ctext\u201d is supported. boxed (boolean): Whether we should draw a box around the output table. spaced (boolean): Whether to insert an empty line between every result row. This is only relevant because postings with multiple lots may require multiple lines to be rendered, and inserting an empty line helps delineate those as separate. pager (string): The name of the pager program to pipe multi-page output to when the output is larger than the screen. The initial value is copied from the PAGER environment variable. expand (boolean): If true, expand columns that render to lists on multiple rows.","title":"Shell Variables"},{"location":"beancount_query_language.html#transactions-and-postings","text":"The structure of transactions and entries can be explained by the following simplified diagram:","title":"Transactions and Postings"},{"location":"beancount_query_language.html#_1","text":"The contents of a ledger is parsed into a list of directives, most of which are \u201cTransaction\u201d objects which contain two or more \u201cPosting\u201d objects. Postings are always linked only to a single transaction (they are never shared between transactions). Each posting refers to its parent transaction but has a unique account name, amount and associated lot (possibly with a cost), a price and some other attributes. The parent transaction itself contains a few useful attributes as well, such as a date, the name of a payee, a narration string, a flag, links, tags, etc. If we ignore the list of directives other than transactions, you can view the dataset as a single table of all postings joined with their parent transaction. It is mainly on this joined table of postings that we want to perform filtering and aggregation operations. However, because of the double-entry bookkeeping constraint, that is, the sum of amounts on postings attached to a transaction is zero, it is also quite useful to perform filtering operations at the transaction level. Because any isolated transaction has a total impact of zero on the global balance, any subset of transactions will also respect the accounting equation (Assets + Liabilities + Equity + Income + Expenses = 0), and producing balance sheets and income statements on subset of transactions provides meaningful views, for example, \u201call asset changes and expenses incurred during a trip to the Bahamas\u201d which could be selected by a tag. For this reason, we modify the SQL SELECT syntax to provide a two-level filtering syntax: since we have a single table of data, we replace the table name in FROM by a filtering expression which applies over transactions, and the WHERE clause applies to data pulled from the resulting list of postings: SELECT , , \u2026 FROM WHERE ; Both filtering expressions are optional. If no filtering expressions are provided, all postings will be enumerated over. Note that since the transactions are always filtered in date order, the results will be processed and returned in this order by default.","title":""},{"location":"beancount_query_language.html#posting-data-columns","text":"The list of targets refers to attributes of postings or of their parent transaction. The same list of \u201ccolumns\u201d is made available in the , to filter by posting attributes. For example, you could write the following query: SELECT date, narration, account, position WHERE account ~ \u201c.*:Vacation\u201d AND year >= 2014; Here, the \u201cdate\u201d, \u201cyear\u201d and \u201cnarration\u201d columns refer to attributes of the parent transaction, the \u201caccount\u201d and \u201cposition\u201d columns refer to attributes of the posting itself. You may name targets explicitly with the familiar AS operator: SELECT last(date) as last_date, cost(sum(position)) as cost; The full list of posting columns and functions available on them is best viewed by querying your actual client using \u201c help targets \u201c or \u201c help where \u201d, which prints out the list and data type of each available data column. You may also refer to the following diagram of the structure of a Posting object for the correspondence between the columns and the data structure attributes.","title":"Posting Data Columns"},{"location":"beancount_query_language.html#entry-data-columns","text":"A different list of column names is available on the of the FROM clause. These columns refer to attributes of the Transaction objects. This clause is intended to filter whole transactions (i.e., all their postings or none at all). Available attributes include the date, transaction flag, the optional payee, narration, set of tags and links. Use the \u201c help from \u201d command to find the complete list of columns and functions available in this clause. A Beancount input file consists of many different types of entries, not just transactions. Some of these other types of entries (such as Open, Close, Balance, etc.) may also provide attributes that can be accessed from the FROM clause. This is embryonic at this point. (It\u2019s unclear yet how these will be used in the future, but I suspect we will find some interesting applications for them eventually. The FROM clause provides access to the type of the data entry via column \u201c type \u201d. It\u2019s still an exploration how much we can make pragmatic use of the SQL language for other types of directives.) The \u201cid\u201d Column A special column exists that identifies each transaction uniquely: \u201c id \u201d. It is a unique hash automatically computed from the transaction and should be stable between runs. SELECT DISTINCT id; This hash is derived from the contents of the transaction object itself (if you change something about the transaction, e.g. you edit the narration, the id will change). You can print and select using this column. It can be used for debugging, e.g. PRINT FROM id = '8e7c47250d040ae2b85de580dd4f5c2a'; The \u201cbalance\u201d Column One common desired output is a journal of entries over time (also called a \u201cregister\u201d in Ledger): SELECT date, account, position WHERE account ~ \"Chase:Slate\"; For this type of report, it is convenient to also render a column of the cumulative balance of the selected postings rows. Access to the previous row is not a standard SQL feature, so we get a little creative and provide a special column called \u201c balance \u201d which is automatically calculated based on the previous selected rows: SELECT date, account, position, balance WHERE account ~ \"Chase:Slate\"; This provides the ability to render typical account statements such as those mailed to you by a bank. Output might look like this: $ bean-query $T \"select date, account, position, balance where account ~ 'Expenses:Food:Restaurant';\" date account position balance ---------- ------------------------ --------- ---------- 2012-01-02 Expenses:Food:Restaurant 31.02 USD 31.02 USD 2012-01-04 Expenses:Food:Restaurant 25.33 USD 56.35 USD 2012-01-08 Expenses:Food:Restaurant 67.88 USD 124.23 USD 2012-01-09 Expenses:Food:Restaurant 35.28 USD 159.51 USD 2012-01-14 Expenses:Food:Restaurant 25.84 USD 185.35 USD 2012-01-17 Expenses:Food:Restaurant 36.73 USD 222.08 USD 2012-01-21 Expenses:Food:Restaurant 28.11 USD 250.19 USD 2012-01-22 Expenses:Food:Restaurant 21.12 USD 271.31 USD","title":"Entry Data Columns"},{"location":"beancount_query_language.html#wildcard-targets","text":"Using a wildcard as the target list (\u201c*\u201d) select a good default list of columns: SELECT * FROM year = 2014; To view the actual list of columns selected, you can use the EXPLAIN prefix: EXPLAIN SELECT * FROM year = 2014;","title":"Wildcard Targets"},{"location":"beancount_query_language.html#data-types","text":"The data attributes extracted from the postings or transactions have particular types. Most of the data types are regular types as provided by the underlying Python implementation language, types such as String (Python str) Date (a datetime.date instance). You can parse a date with the #\"...\" syntax; this uses Python\u2019s dateutil module and is pretty liberal in the formats it accepts. Integer (Python int) Boolean (Python bool object), as TRUE , FALSE Number (a decimal.Decimal object) Set of Strings (a Python set of str objects) Null objects ( NULL )","title":"Data Types"},{"location":"beancount_query_language.html#positions-and-inventories","text":"However, one reason that our SQL-like client exists in the first place is for its ability to carry out aggregation operations on inventories of positions, the data structures at the core of Beancount, that implements its balancing semantics. Internally, Beancount defines Position and Inventory objects and is able to aggregate them together in an instance of Inventory. On each Posting, the \u201cposition\u201d column extracts an object of type Position, which when summed over produces an instance of Inventory. The shell is able to display those appropriately. More specifically, Inventory objects can contain multiple different lots of holdings, and each of these will get rendered on a separate line.","title":"Positions and Inventories"},{"location":"beancount_query_language.html#quantities-of-positions-and-inventories","text":"Objects of type Position are rendered in their full detail by default, including not just their number and currency, but the details of their lot. Inventories consist of a list of lots, and as such are rendered similarly, as a list of positions (one per line) each with their full detail by default. This is generally too much detail. The shell provides functions that allow the user to summarize the positions into one of the various derived quantities. The types of derived quantities are: \u201c raw \u201d: render the position in its full detail, including cost and lot date \u201c units \u201d: render just the number and currency of the position \u201c cost \u201d: render the total cost of the position, that is the number of units x the per-unit cost \u201c weight \u201d: render the amount that is used to balance the postings of a transaction. The main distinction between cost and weight is for postings with a price conversion. \u201c value \u201d: render the amount at the market value of the last entry rendered. Functions with the same names are available to operate on position or inventory objects. For example, one could generate a table of final balances for each account like this: SELECT account, units(sum(position)), cost(sum(position)) GROUP BY 1; Refer to the table below for explicit examples of each type of posting and how it would get converted and rendered. posting raw (full detail) units cost weight market Simple 50.00 USD 50.00 USD 50.00 USD 50.00 USD 50.00 USD With Price Conversion 50.00 USD @ 1.35 CAD 50.00 USD 50.00 USD 67.50 CAD 50.00 USD Held at Cost 50 VEA {1.35 CAD} 50 VEA 67.50 CAD 67.50 CAD 67.50 CAD Held at Cost with Price 50 VEA {1.35 CAD} @ 1.45 CAD 50 VEA 67.50 CAD 67.50 CAD 72.50 CAD","title":"Quantities of Positions and Inventories"},{"location":"beancount_query_language.html#operators","text":"Common comparison and logical operators are provided to operate on the available data columns: = (equality), != (inequality) < (less than), <= (less than or equal) (greater than), >= (greater than or equal) AND (logical conjunction) OR (logical disjunction) NOT (logical negation) IN (set membership) We also provide a regular expression search operator into a string object: ~ (search regexp) At the moment, matching groups are ignored. You can use string, number and integer constants with those operators, and parentheses to explicitly state precedence. You can use the #\u201d...\u201d literal syntax to input dates (valid contents for the string are pretty liberal, it supports anything Python\u2019s dateutil.parser supports). Here is an example query that uses a few of these: SELECT date, payee WHERE account ~ 'Expenses:Food:Restaurant' AND 'trip-new-york' IN tags AND NOT payee = 'Uncle Boons' Unlike SQL, bean-query does not implement three-valued logic for NULL . This means that e.g. the expression NULL = NULL yields TRUE instead of NULL , which simplifies things, but may come as a surprise to veteran SQL users.","title":"Operators"},{"location":"beancount_query_language.html#simple-functions","text":"The shell provides a list of simple function that operate on a single data column and return a new value. These functions operate on particular types. The shell implements rudimentary type verification and should be able to warn you on incompatible types. Some example functions follow: COST(Inventory), COST(Position): Return an Amount, the cost of the position or inventory. UNITS(Inventory), UNITS(Position): Return the units of the position or inventory. DAY(date), MONTH(date), YEAR(date): Return an integer, the day, month or year of the posting or entry\u2019s date. LENGTH(list): Computes the length of a list or a set, e.g. on tags. PARENT(account-string): Returns the name of the parent account. These are just examples; for the complete list, see \u201c help targets \u201d, \u201c help where \u201d, \u201c help from \u201d. Note that it is exceedingly easy to add new functions to this list. As of December 2014, we are just beginning using the shell widely and we expect to be adding new functions as needed. If you need a function, please add a comment here or log a ticket and we will consider adding it to the list (we understand that the current list is limited). I intend to be liberal about adding new functions; as long as they have generic application, I don\u2019t think it should be a problem. Otherwise, I may be able to provide a mechanism for user to register new functions as part of Python plugins that could live outside the Beancount codebase.","title":"Simple Functions"},{"location":"beancount_query_language.html#aggregate-functions","text":"Some functions operate on more than a single row. These functions aggregate and summarize the multiple values for the data column that they operate on. A prototypical usage of such a function is to sum the positions in an inventory: SELECT account, sum(position) WHERE account ~ 'Income' GROUP BY account; If a query target has at least one aggregating function, the query becomes an aggregated query (see relevant section for details). Note that you cannot use aggregation functions in the FROM or WHERE clauses. Examples of aggregate functions include: COUNT(...): Computes the number of postings selected (an integer). FIRST(...), LAST(...): Returns first or last value seen. MIN(...), MAX(...): Computes the minimum or maximum value seen. SUM(...): Sums up the values of each set. This works on amounts, positions, inventories, numbers, etc. As for simple functions, this is just a starting list. We will be adding more as needed. Use \u201c help targets \u201d to access the full list of available aggregate functions. Note: You cannot filter (using a WHERE clause) the results of aggregation functions; this requires the implementation offering a HAVING clause, and at the moment, HAVING filtering is not yet implemented.","title":"Aggregate Functions"},{"location":"beancount_query_language.html#simple-vs-aggregated-queries","text":"There are two types of queries: Simple queries , which produce a row of results for each posting that matches the restricts in the WHERE clause. Aggregate queries , which produce a row of results for each group of postings that match the restricts in the WHERE clause. A query is \u201caggregate\u201d if it has at least one aggregate function in its list of targets. In order to identify the aggregation keys, all the non-aggregate columns have to be flagged using the GROUP BY clause, like this: SELECT payee, account, COST(SUM(position)), LAST(date) GROUP BY payee, account; You may also use the positional order of the targets to declare the group key, like this: SELECT payee, account, COST(SUM(position)), LAST(date) GROUP BY 1, 2; Furthermore, if you name your targets, you can use the explicit target names: SELECT payee, account as acc, COST(SUM(position)), LAST(date) GROUP BY 1, acc; This should all feel familiar if you have preliminary knowledge of SQL. Finally, because we implement a limited version of SQL, and that the simple columns must always be specified, omitting the GROUP BY clause should also eventually work and we should group by those columns implicitly, as a convenience.","title":"Simple vs. Aggregated Queries"},{"location":"beancount_query_language.html#distinct","text":"There is a post-filtering phase that supports uniquifying result rows. You can trigger this unique filter with the DISTINCT flag after SELECT , as is common in SQL, e.g. SELECT DISTINCT account;","title":"Distinct"},{"location":"beancount_query_language.html#controlling-results","text":"","title":"Controlling Results"},{"location":"beancount_query_language.html#order-by","text":"Analogous to the GROUP BY clause is an ORDER BY clause that controls the final ordering of the result rows: SELECT \u2026 GROUP BY account, payee ORDER BY payee, date; The clause is optional. If you do not specify it, the default order of iteration of selected postings is used to output the results (that is, the order of transactions-sorted by date- and then their postings). As in SQL, you may reverse the order of sorting by a DESC suffix (the default is the same as specifying ASC ): SELECT \u2026 GROUP BY account, payee ORDER BY payee, date DESC;","title":"Order By"},{"location":"beancount_query_language.html#limit","text":"Our query language also supports a LIMIT clause to interrupt output row generation: SELECT \u2026 LIMIT 100; This would output the first 100 result rows and then stop. While this is a common clause present in the SQL language, in the context of double-entry bookkeeping it is not very useful: we always have relatively small datasets to work from. Nevertheless, we provide it for completeness.","title":"Limit"},{"location":"beancount_query_language.html#format","text":"For SELECT , JOURNAL and BALANCES queries, the output format is a table of text by default. We support CSV output. ( We could easily add support for XLS or Google Sheets output.) However, for PRINT queries, the output format is Beancount input text format.","title":"Format"},{"location":"beancount_query_language.html#statement-operators","text":"The shell provides a few operators designed to facilitate the generation of balance sheets and income statements. The particular methodology used to define these operations should be described in detail in the \u201c introduction to double-entry bookkeeping \u201d document that accompanies Beancount and is mostly located in the source code in the summarize module. These special operators are provided on the FROM clause that is made available on the various forms of query commands in the shell. These further transform the set of entries selected by the FROM expression at the transaction levels (not postings). Please note that these are not from standard SQL; these are extensions provided by this shell language only.","title":"Statement Operators"},{"location":"beancount_query_language.html#opening-a-period","text":"Opening an exercise period at a particular date replaces all entries before that date by summarization entries that book the expected balance against an Equity \u201copening balances\u201d account and implicitly clears the income and expenses to zero by transferring their balances to an Equity \u201cprevious earnings\u201d account (see beancount.ops.summarize.open() for implementation details). It is invoked like this: SELECT \u2026 FROM OPEN ON \u2026 For example: SELECT * FROM has_account(\"Invest\") OPEN ON 2014-01-01; If you want, you can view just the inserted summarization entries like this: PRINT FROM flag = \"S\" AND account ~ \"Invest\" OPEN ON 2014-01-01;","title":"Opening a Period"},{"location":"beancount_query_language.html#closing-a-period","text":"Closing an exercise period involves mainly truncating all entries that come after the given date and ensuring that currency conversions are correctly corrected for (see beancount.ops.summarize.close() for implementation details). It is invoked like this: SELECT \u2026 FROM CLOSE [ON ] \u2026 For example: SELECT * FROM has_account(\"Invest\") CLOSE ON 2015-04-01; Note that the closing date should be one day after the last transaction you would like to include (this is in line with the convention we use everywhere in Beancount whereby starting dates are inclusive and ending dates exclusive). The closing date is optional. If the date is not specified, the date one day beyond the date of the last entry is used. Closing a period leaves the Income and Expenses accounts as they are, that is, their balances are not cleared to zero to Equity. This is because closing is also used to produce final balances for income statements. \u201cClearing\u201d, as described in the next section, is only needed for balance sheets.","title":"Closing a Period"},{"location":"beancount_query_language.html#clearing-income-expenses","text":"In order to produce a balance sheet, we need to transfer final balances of the Income and Expenses to an Equity \u201ccurrent earnings\u201d account (sometimes called \u201cretained earnings\u201d or \u201cnet income\u201d; you can select the specific account name to use using options in the input file). The resulting balances of income statement accounts should be zero (see beancount.ops.summarize.clear() for implementation details). You can clear like this: SELECT \u2026 FROM CLEAR \u2026 For example: SELECT * FROM has_account(\"Invest\") CLOSE ON 2015-04-01 CLEAR; This is a statement suitable to produce a list of accounts to build a balance sheet. The \u201c Equity:Earnings:Current \u201d (by default) will contain the net income accumulated during the preceding period. No balances for the Income nor Expenses accounts should appear in the output.","title":"Clearing Income & Expenses"},{"location":"beancount_query_language.html#example-statements","text":"The statement operators of course may be combined. For instance, if you wanted to output data for an income statement for year 2013, you could issue the following statement: SELECT account, sum(position) FROM OPEN ON 2013-01-01 CLOSE ON 2014-01-01 WHERE account ~ \"Income|Expenses\" GROUP BY 1 ORDER BY 1; This would produce a list of balances for Income and Expenses accounts. To generate a balance sheet, you would add the CLEAR option and select the other accounts: SELECT account, sum(position) FROM OPEN ON 2013-01-01 CLOSE ON 2014-01-01 CLEAR WHERE not account ~ \"Income|Expenses\" GROUP BY 1 ORDER BY 1; Note that if you added the CLEAR operator to the statement of income statement accounts, all the balances would show at zero because of the inserted transactions that move those balances to the Equity net income account at the end of the period. It is relevant to notice that the examples above do not filter the transactions any further. If you are selecting a subset of transactions you may want to leave the accounts unopened, unclosed and uncleared because applying only some of the transactions on top of the opening balances of Assets and Liabilities accounts will not produce correct balances for those accounts. It would be more useful to leave them all opened, and to interpret the balances of the balance sheet accounts as the changes in those accounts for the subset of transactions selected. For example, if you selected all transactions from a trip (using a tag), you would obtain a list of changes in Expenses (and possibly Income) tagged as being included in this trip, and the Assets and Liabilities accounts would show where the funds for those Expenses came from. Consult the \u201cintroduction to double-entry method\u201d document for a pictorial representation of this. (Granted, this is probably worth a dedicated document and I might produce one at some point.)","title":"Example Statements"},{"location":"beancount_query_language.html#example-fetching-cost-basis","text":"\u201c... is there currently an easy way to determine what my cost basis is for an account on a given date (other than manually adding up UNITS * COST for every contribution, which is kind of a pain)? I'm trying to estimate the tax implications of potential stock sales.\u201d [Question from Matthew Harris] For a detailed report of share movements: SELECT account, currency, position, COST(position) WHERE year <= 2015 AND account ~ \"Assets:US:Schwab\" AND currency != \"USD\" Or this for the sum total of the cost bases: SELECT sum(cost(position)) WHERE year <= 2015 AND account ~ \"Assets:US:Schwab\" AND currency != \"USD\"","title":"Example Fetching Cost Basis"},{"location":"beancount_query_language.html#high-level-shortcuts","text":"There are two types of queries that are very common for accounting applications: journals and balances reports. While we have explicit implementations of such reports that can be produced using the bean-report tool, we are also able to synthesize good approximations of such reports using SELECT statements. This section describes a few additional selection commands that translate directly into SELECT statements and which are then run with the same query code. These are intended as convenient shortcuts.","title":"High-Level Shortcuts"},{"location":"beancount_query_language.html#selecting-journals","text":"A common type of query is one that generates a linear journal of entries (Ledger calls this a \u201cregister\u201d). This roughly corresponds to an account statement, but with our language, such a statement can be generated for any subset of postings. You can generate a journal with the following syntax: JOURNAL [AT ] [FROM \u2026] The regular expression account-regexp is used to select which subset of accounts to generate a journal for. The optional \u201cAT \u201d clause is used to specify an aggregation function for the amounts rendered (typically UNITS or COST ). The FROM clause follows the same rules as for the SELECT statement and is optional. Here is an example journal-generating query: JOURNAL \"Invest\" AT COST FROM HAS_ACCOUNT(\"Assets:US\");","title":"Selecting Journals"},{"location":"beancount_query_language.html#selecting-balances","text":"The other most common type of report is a table of the balances of various accounts at a particular date. This can be viewed as a SELECT query aggregating positions grouping by account. You can generate a balances report with the following syntax: BALANCES [AT ] [FROM \u2026] The optional \u201cAT \u201d clause is used to specify an aggregation function for the balances rendered (usually UNITS or COST ). The FROM clause follows the same rules as for the SELECT statement and is optional. To generate your balances at a particular date, close your set of entries using the \u201c FROM\u2026 CLOSE ON \u201d form described above. Observe that typical balance sheets and income statements seen in an accounting context are subsets of tables of balances such as reported by this query. An income statement reports on just the transactions that appears during a period of time, and a balance sheet summarizes transactions before its reporting before and clears the income & expenses accumulated during the period to an equity account. Then some minor reformatting is carried out. Please consult the introduction document on double-entry bookkeeping for more details, and the section above that discusses the \u201copen\u201d, \u201cclose\u201d and \u201cclear\u201d operations. We will also be providing a separate text processing tool that can accept balance reports and reformat them in a two-column format similar to that you would see balance sheets and income statements.","title":"Selecting Balances"},{"location":"beancount_query_language.html#print","text":"It can be useful to generate output in Beancount format, so that subsets of transactions can be saved to files, for example. The shell provides that ability via the PRINT command: PRINT [FROM \u2026] The FROM clause obeys the usual semantics as described elsewhere in this document. The resulting filtered stream of Beancount entries is then printed out on the output in Beancount syntax. In particular, just running the \u201c PRINT \u201d command will spit out the parsed and loaded contents of a Beancount file. You can use this for troubleshooting if needed, or to expand transactions generated from a plugin you may be in the process of developing.","title":"Print"},{"location":"beancount_query_language.html#debugging-explain","text":"If you\u2019re having trouble getting a particular statement to compile and run,. you can prefix any query statement with the EXPLAIN modifier, e.g.: EXPLAIN SELECT \u2026 This will not run the statement, but rather print out the intermediate AST and compiled representation as well as the list of computed statements. This can be useful to report bugs on the mailing-list. Also, this shows you the translated form of the JOURNAL and BALANCES statements.","title":"Debugging / Explain"},{"location":"beancount_query_language.html#future-features","text":"The following list of features were planned for the first release but I\u2019ve decided to make a first cut without them. I\u2019ll be adding those during a revision.","title":"Future Features"},{"location":"beancount_query_language.html#flattening-inventories","text":"If you provide the FLATTEN option after a query, it tells the query engine to flatten inventories with multiple lots into separate rows for each lot. For example, if you have an inventory balance with the following contents: 3 AAPL {102.34 USD} 4 AAPL {104.53 USD} 5 AAPL {106.23 USD} Using the following query: SELECT account, sum(position) GROUP BY account; It should return a single row of results, rendered over three lines. However, adding the option: SELECT account, sum(position) GROUP BY account FLATTEN; This should return three separate rows, with all the selected attributes, as if there were that many postings.","title":"Flattening Inventories"},{"location":"beancount_query_language.html#sub-selects","text":"The ability to select from the result of another SELECT is not currently supported, but the internals of the query code are prepared to do so.","title":"Sub-Selects"},{"location":"beancount_query_language.html#more-information","text":"This document attempts to provide a good high-level summary of the features supported in our query language. However, should you find you need more information, you may take a look at the original proposal , or consult the source code under the beancount.query directory. In particular, the parser will provide insight into the specifics of the syntax, and the environments will shed some light on the supported data columns and functions. Feel free to rummage in the source code and ask questions on the mailing-list.","title":"More Information"},{"location":"beancount_query_language.html#appendix","text":"","title":"Appendix"},{"location":"beancount_query_language.html#future-features_1","text":"This section documents ideas for features to be implemented in a future version.","title":"Future Features"},{"location":"beancount_query_language.html#pivot-by","text":"A special PIVOT BY clause will eventually be usable to convert the output from a one-dimensional list of results to a two-dimensional table. For example, the following query: SELECT account, YEAR(date) AS year, SUM(COST(position)) AS balance WHERE account ~ 'Expenses:Food' AND currency = 'USD' AND year >= 2012 GROUP BY 1,2 ORDER BY 1,2; Might generate the following table of results: account year balance ------------------------ ---- ----------- Expenses:Food:Alcohol 2012 57.91 USD Expenses:Food:Alcohol 2013 33.45 USD Expenses:Food:Coffee 2012 42.07 USD Expenses:Food:Coffee 2013 124.69 USD Expenses:Food:Coffee 2014 38.74 USD Expenses:Food:Groceries 2012 2172.97 USD Expenses:Food:Groceries 2013 2161.90 USD Expenses:Food:Groceries 2014 2072.36 USD Expenses:Food:Restaurant 2012 4310.60 USD Expenses:Food:Restaurant 2013 5053.61 USD Expenses:Food:Restaurant 2014 4209.06 USD If you add a PIVOT clause to the query, like this: \u2026 PIVOT BY account, year; You would get a table like this: account/year 2012 2013 2014 ------------------------ ----------- ----------- ----------- Expenses:Food:Alcohol 57.91 USD 33.45 USD Expenses:Food:Coffee 42.07 USD 124.69 USD 38.74 USD Expenses:Food:Groceries 2172.97 USD 2161.90 USD 2072.36 USD Expenses:Food:Restaurant 4310.60 USD 5053.61 USD 4209.06 USD If your table has more than three columns, the non-pivoted column would get expanded horizontally, like this: SELECT account, YEAR(date), SUM(COST(position)) AS balance, LAST(date) AS updated WHERE account ~ 'Expenses:Food' AND currency = 'USD' AND year >= 2012 GROUP BY 1,2 ORDER BY 1, 2; You would get a table like this: account year balance updated ------------------------ ---- ----------- ---------- Expenses:Food:Alcohol 2012 57.91 USD 2012-07-17 Expenses:Food:Alcohol 2013 33.45 USD 2013-12-13 Expenses:Food:Coffee 2012 42.07 USD 2012-07-19 Expenses:Food:Coffee 2013 124.69 USD 2013-12-16 Expenses:Food:Coffee 2014 38.74 USD 2014-09-21 Expenses:Food:Groceries 2012 2172.97 USD 2012-12-30 Expenses:Food:Groceries 2013 2161.90 USD 2013-12-31 Expenses:Food:Groceries 2014 2072.36 USD 2014-11-20 Expenses:Food:Restaurant 2012 4310.60 USD 2012-12-30 Expenses:Food:Restaurant 2013 5053.61 USD 2013-12-29 Expenses:Food:Restaurant 2014 4209.06 USD 2014-11-28 Pivoting, this would generate this table: account/balance,updated 2012/balanc 2012/updat 2013/balanc 2013/updat 2014/balanc 2014/updat ------------------------ ----------- ---------- ----------- ---------- ----------- ---------- Expenses:Food:Alcohol 57.91 USD 2012-07-17 33.45 USD 2013-12-13 Expenses:Food:Coffee 42.07 USD 2012-07-19 124.69 USD 2013-12-16 38.74 USD 2014-09-21 Expenses:Food:Groceries 2172.97 USD 2012-12-30 2161.90 USD 2013-12-31 2072.36 USD 2014-11-20 Expenses:Food:Restaurant 4310.60 USD 2012-12-30 5053.61 USD 2013-12-29 4209.06 USD 2014-11-28","title":"Pivot By"},{"location":"beancount_scripting_plugins.html","text":"Beancount Scripting & Plugins \uf0c1 Martin Blais , July 2014 http://furius.ca/beancount/doc/scripting Introduction Load Pipeline Writing Plug-ins Writing Scripts Loading from File Loading from String Printing Errors Printing Entries & Round-Tripping Going Further Introduction \uf0c1 This document provides examples and guidelines on how to write scripts that use the contents of your ledger. It also provides information on how to write your own \u201cplugins,\u201d which are just Python functions that you can configure to transform your transactions or synthesize ones programmatically. These are the main two methods for extending Beancount\u2019s features and for writing your own custom reports. You simply use Python to do this. Load Pipeline \uf0c1 You need to know a little bit about how Beancount processes its input files. Internally, the single point of entry to load an input file is the beancount.loader.load_file() function, which accepts an input file and carries out a list of transformation steps, as in this diagram: The stages of loading are as follows: Parser. Run the input file through the parser. The output of this stage is entries: A list of tuples (defined in beancount.core.data) corresponding to each directive exactly as it appeared in the file, and sorted by date and line number. Moreover, Transaction directives that occur on the same date as other directives are always guaranteed to be sorted after them. This prepares the entries for processing. This list of entries will get transformed and refined by the various subsequent stages. options_map: A Python dict of the option values from the input file. See beancount.parser.options for details. Once created, this will never be modified thereafter. errors: A list of error objects, if any occurred. At every stage, new errors generated are collected. Process plugins. For each plugin, load the plugin module and call its functions with the list of entries and the options_map from the previous stage, replacing the current list by the ones returned by the plugin. This effectively allows the plugin to filter the entries. The list of plugins to run is composed of a set of default plugin modules that implement some of the built-in features of Beancount, followed by the list provided by the user from the \u201cplugin\u201d options in the input file. Validation. Run the resulting entries through a validation stage, to ensure that directives synthesized or modified by the plugins conform to some invariants that the codebase depends on. This mainly exists to generate errors. The list of entries generated by this pipeline are of the various types defined in beancount.core.data , and in a typical input file, most of them will be of type Transaction . Beancount\u2019s own filtering and reporting programs directly process those, and so can you too. These entries are dumb read-only objects (Python namedtuples ) and have no methods that modify their contents explicitly. All processing within Beancount is performed functional-style by processing lists of entries that are assumed immutable 1 . The list of user plugins to run is part of the load stage because that allows programs that monitor the file for changes to reload it and reapply the same list of plugins. It also allows the author of the input file to selectively enable various optional features that way. Writing Plug-ins \uf0c1 As you saw in the previous section, loading a Beancount file essentially produces a list of directives. Many syntax extensions can be carried out by transforming the list of directives into a new list in the plug-ins processing stage. Here are some examples of transformations that you might want to carry out on some of the directives: Add some postings automatically Link some transactions with a common tag Synthesize new transactions Remove or replace some sets of transactions Modify the various fields There is no limit to what you can do, as long as the entries your plugin produces fulfill certain constraints (all postings balance, all data types are as expected). A plugin is added to the input file via the option syntax , for example, like this: plugin \"accounting.wash_sales\" With this directive, the loader will attempt to import the accounting.wash_sales Python module (the code must be Python-3.3 or above), look for a special __plugins__ attribute which should be a sequence of functions to run, and then run those functions. For running the plugins, see the Executing Plugins section below. As an example, you would place code like this in a \u201c accounting/wash_sales.py \u201d file: __plugins__ = ['wash_sales'] def wash_sales(entries, options_map): errors = [] for entry in entries: print(type(entry)) return entries, errors This is a minimal example which does not modify the entries and prints them on the console. In practice, to do something useful, you would modify some of the entries in the list and output them. You then invoke the usual tools provided by Beancount on your input file. The various filters and reports will then operate on the list of entries output by your plugin. Refer to the source code in beancount.core for details and examples of how to manipulate entries. Plugin Configuration \uf0c1 Some plugins will require configuration. In order to provide a plugin some data specific to your file, you can provide a configuration string: plugin \"accounting.wash_sales\" \"days=31\" The plugin function will then receive an extra parameter, the configuration string. It is up to the plugin itself to define how it gets interpreted. Writing Scripts \uf0c1 If you need to produce some custom analysis or visualization that cannot be achieved using the built-in filtering and reporting capabilities, you can just write a script that loads the directives explicitly. This gives you control over the flow of the program and you can do anything you want. Loading from File \uf0c1 You can simply call the beancount.loader.load_file() loader function yourself. Here is an example minimal script: #!/usr/bin/env python3 from beancount import loader filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename) \u2026 At this point you can process the entries as you like, print them out, generate HTML, call out to Python libraries, etc. (I recommend that you use best programming practices and use docstrings on your script and a main function; the script above is meant to be minimal). Once again, refer to the source code in beancount.core for details and examples of how to manipulate entries. Loading from String \uf0c1 You can also parse a string directly. Use beancount.loader.load_string() : #!/usr/bin/env python3 from beancount import loader entries, errors, options = loader.load_string(\"\"\" 2014-02-02 open Assets:TestAccount USD \u2026 \"\"\") The stdlib textwrap.dedent function comes in handy if you want to indent the Beancount directives and have it automatically remove indentation. For a source of many examples, see the various tests in the Beancount source code. Printing Errors \uf0c1 By default, the loader will not print any errors upon loading; we prefer loading not to have any side-effect by default. You can provide an optional argument to print errors, which is the function to call to write error strings: #!/usr/bin/env python3 import sys from beancount import loader filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename, log_errors=sys.stderr) \u2026 Or if you prefer to do it yourself explicitly, you can call the beancount.parser.printer.print_errors() helper function: #!/usr/bin/env python3 from beancount import loader from beancount.parser import printer filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename) printer.print_errors(errors) \u2026 Printing Entries & Round-Tripping \uf0c1 Printing namedtuple entries directly will output some readable though relatively poorly formatted output. It\u2019s best to use the beancount.parser.printer.print_entry() utility function to print out an entry in a readable way: #!/usr/bin/env python3 from beancount import loader from beancount.parser import printer filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename) for entry in entries: printer.print_entry(entry) In particular, Beancount offers the guarantee that the output of the printer should always be parseable and should result in the same data structure when read back in. (It should be considered a bug if that is not the case.) See the beancount.parser.printer module source code for more utility functions. Executing Plugins \uf0c1 All that is required for the plug-in module to be found, is that it must be present in your PYTHONPATH environment variable (you need to make sure that the relevant __init__.py files exist for import). It can live in your own code: you don\u2019t have to modify Beancount itself. There is also an option, which can be added to your beancount file: option \"insert_pythonpath\" \"True\" This will add the folder which contains the beancount file to the PYTHONPATH . The result is that you can place the plugins along the beancount file and have them execute when you use this file. Here is a brief example, using the wash_sales.py plugin we wrote above. Your beancount file would include the following lines: option \"insert_pythonpath\" \"True\" plugin \"wash_sales\" The Python file wash_sales.py would be stored in the same folder as the .beancount file. Going Further \uf0c1 To understand how to manipulate entries, you should refer to the source code, and probably learn more about the following modules: beancount.core.data beancount.core.account beancount.core.number beancount.core.amount beancount.core.position beancount.core.inventory Refer to the Design Doc for more details. Enjoy! Technically, Python does not prevent the modifications of namedtuple attributes that are themselves mutable such as lists and sets, but in practice, by convention, once an entry is created we never modify it in any way. Avoiding side-effects and using a functional style provides benefits in any language. \u21a9","title":"Beancount Scripting Plugins"},{"location":"beancount_scripting_plugins.html#beancount-scripting-plugins","text":"Martin Blais , July 2014 http://furius.ca/beancount/doc/scripting Introduction Load Pipeline Writing Plug-ins Writing Scripts Loading from File Loading from String Printing Errors Printing Entries & Round-Tripping Going Further","title":"Beancount Scripting & Plugins"},{"location":"beancount_scripting_plugins.html#introduction","text":"This document provides examples and guidelines on how to write scripts that use the contents of your ledger. It also provides information on how to write your own \u201cplugins,\u201d which are just Python functions that you can configure to transform your transactions or synthesize ones programmatically. These are the main two methods for extending Beancount\u2019s features and for writing your own custom reports. You simply use Python to do this.","title":"Introduction"},{"location":"beancount_scripting_plugins.html#load-pipeline","text":"You need to know a little bit about how Beancount processes its input files. Internally, the single point of entry to load an input file is the beancount.loader.load_file() function, which accepts an input file and carries out a list of transformation steps, as in this diagram: The stages of loading are as follows: Parser. Run the input file through the parser. The output of this stage is entries: A list of tuples (defined in beancount.core.data) corresponding to each directive exactly as it appeared in the file, and sorted by date and line number. Moreover, Transaction directives that occur on the same date as other directives are always guaranteed to be sorted after them. This prepares the entries for processing. This list of entries will get transformed and refined by the various subsequent stages. options_map: A Python dict of the option values from the input file. See beancount.parser.options for details. Once created, this will never be modified thereafter. errors: A list of error objects, if any occurred. At every stage, new errors generated are collected. Process plugins. For each plugin, load the plugin module and call its functions with the list of entries and the options_map from the previous stage, replacing the current list by the ones returned by the plugin. This effectively allows the plugin to filter the entries. The list of plugins to run is composed of a set of default plugin modules that implement some of the built-in features of Beancount, followed by the list provided by the user from the \u201cplugin\u201d options in the input file. Validation. Run the resulting entries through a validation stage, to ensure that directives synthesized or modified by the plugins conform to some invariants that the codebase depends on. This mainly exists to generate errors. The list of entries generated by this pipeline are of the various types defined in beancount.core.data , and in a typical input file, most of them will be of type Transaction . Beancount\u2019s own filtering and reporting programs directly process those, and so can you too. These entries are dumb read-only objects (Python namedtuples ) and have no methods that modify their contents explicitly. All processing within Beancount is performed functional-style by processing lists of entries that are assumed immutable 1 . The list of user plugins to run is part of the load stage because that allows programs that monitor the file for changes to reload it and reapply the same list of plugins. It also allows the author of the input file to selectively enable various optional features that way.","title":"Load Pipeline"},{"location":"beancount_scripting_plugins.html#writing-plug-ins","text":"As you saw in the previous section, loading a Beancount file essentially produces a list of directives. Many syntax extensions can be carried out by transforming the list of directives into a new list in the plug-ins processing stage. Here are some examples of transformations that you might want to carry out on some of the directives: Add some postings automatically Link some transactions with a common tag Synthesize new transactions Remove or replace some sets of transactions Modify the various fields There is no limit to what you can do, as long as the entries your plugin produces fulfill certain constraints (all postings balance, all data types are as expected). A plugin is added to the input file via the option syntax , for example, like this: plugin \"accounting.wash_sales\" With this directive, the loader will attempt to import the accounting.wash_sales Python module (the code must be Python-3.3 or above), look for a special __plugins__ attribute which should be a sequence of functions to run, and then run those functions. For running the plugins, see the Executing Plugins section below. As an example, you would place code like this in a \u201c accounting/wash_sales.py \u201d file: __plugins__ = ['wash_sales'] def wash_sales(entries, options_map): errors = [] for entry in entries: print(type(entry)) return entries, errors This is a minimal example which does not modify the entries and prints them on the console. In practice, to do something useful, you would modify some of the entries in the list and output them. You then invoke the usual tools provided by Beancount on your input file. The various filters and reports will then operate on the list of entries output by your plugin. Refer to the source code in beancount.core for details and examples of how to manipulate entries.","title":"Writing Plug-ins"},{"location":"beancount_scripting_plugins.html#plugin-configuration","text":"Some plugins will require configuration. In order to provide a plugin some data specific to your file, you can provide a configuration string: plugin \"accounting.wash_sales\" \"days=31\" The plugin function will then receive an extra parameter, the configuration string. It is up to the plugin itself to define how it gets interpreted.","title":"Plugin Configuration"},{"location":"beancount_scripting_plugins.html#writing-scripts","text":"If you need to produce some custom analysis or visualization that cannot be achieved using the built-in filtering and reporting capabilities, you can just write a script that loads the directives explicitly. This gives you control over the flow of the program and you can do anything you want.","title":"Writing Scripts"},{"location":"beancount_scripting_plugins.html#loading-from-file","text":"You can simply call the beancount.loader.load_file() loader function yourself. Here is an example minimal script: #!/usr/bin/env python3 from beancount import loader filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename) \u2026 At this point you can process the entries as you like, print them out, generate HTML, call out to Python libraries, etc. (I recommend that you use best programming practices and use docstrings on your script and a main function; the script above is meant to be minimal). Once again, refer to the source code in beancount.core for details and examples of how to manipulate entries.","title":"Loading from File"},{"location":"beancount_scripting_plugins.html#loading-from-string","text":"You can also parse a string directly. Use beancount.loader.load_string() : #!/usr/bin/env python3 from beancount import loader entries, errors, options = loader.load_string(\"\"\" 2014-02-02 open Assets:TestAccount USD \u2026 \"\"\") The stdlib textwrap.dedent function comes in handy if you want to indent the Beancount directives and have it automatically remove indentation. For a source of many examples, see the various tests in the Beancount source code.","title":"Loading from String"},{"location":"beancount_scripting_plugins.html#printing-errors","text":"By default, the loader will not print any errors upon loading; we prefer loading not to have any side-effect by default. You can provide an optional argument to print errors, which is the function to call to write error strings: #!/usr/bin/env python3 import sys from beancount import loader filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename, log_errors=sys.stderr) \u2026 Or if you prefer to do it yourself explicitly, you can call the beancount.parser.printer.print_errors() helper function: #!/usr/bin/env python3 from beancount import loader from beancount.parser import printer filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename) printer.print_errors(errors) \u2026","title":"Printing Errors"},{"location":"beancount_scripting_plugins.html#printing-entries-round-tripping","text":"Printing namedtuple entries directly will output some readable though relatively poorly formatted output. It\u2019s best to use the beancount.parser.printer.print_entry() utility function to print out an entry in a readable way: #!/usr/bin/env python3 from beancount import loader from beancount.parser import printer filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename) for entry in entries: printer.print_entry(entry) In particular, Beancount offers the guarantee that the output of the printer should always be parseable and should result in the same data structure when read back in. (It should be considered a bug if that is not the case.) See the beancount.parser.printer module source code for more utility functions.","title":"Printing Entries & Round-Tripping"},{"location":"beancount_scripting_plugins.html#executing-plugins","text":"All that is required for the plug-in module to be found, is that it must be present in your PYTHONPATH environment variable (you need to make sure that the relevant __init__.py files exist for import). It can live in your own code: you don\u2019t have to modify Beancount itself. There is also an option, which can be added to your beancount file: option \"insert_pythonpath\" \"True\" This will add the folder which contains the beancount file to the PYTHONPATH . The result is that you can place the plugins along the beancount file and have them execute when you use this file. Here is a brief example, using the wash_sales.py plugin we wrote above. Your beancount file would include the following lines: option \"insert_pythonpath\" \"True\" plugin \"wash_sales\" The Python file wash_sales.py would be stored in the same folder as the .beancount file.","title":"Executing Plugins"},{"location":"beancount_scripting_plugins.html#going-further","text":"To understand how to manipulate entries, you should refer to the source code, and probably learn more about the following modules: beancount.core.data beancount.core.account beancount.core.number beancount.core.amount beancount.core.position beancount.core.inventory Refer to the Design Doc for more details. Enjoy! Technically, Python does not prevent the modifications of namedtuple attributes that are themselves mutable such as lists and sets, but in practice, by convention, once an entry is created we never modify it in any way. Avoiding side-effects and using a functional style provides benefits in any language. \u21a9","title":"Going Further"},{"location":"beancount_v3.html","text":"Beancount Vnext: Goals & Design \uf0c1 Martin Blais , July 2020 http://furius.ca/beancount/doc/Vnext Motivation \uf0c1 It's time to give Beancount a refresh and to put down a concrete plan for what the next iteration of it ought to be. I've had these thoughts in the back of my mind for a long while\u2014at least a year\u2014and I'm putting these in writing partly to share a vision of what the product we all use to organize our finances can become, partly to solicit feedback, and partly to organize my thoughts and to prioritize well on the stuff that matters. Current status. The current state of Beancount is that development has been static for a while now, for a number of reasons. The software is in a state that's far from perfect (and I'll be enumerating the main problems in this document) but I've been resisting making too many changes in order to provide myself and others a really stable base to work from. More importantly, while I used to be able to spend a significant amount of weekend time on its development, life changes and a focus on my career of late has made it difficult for me to justify or find the extra time (it has been 10 years after all). A multitude of ideas have aged to this TODO file but it's too detailed to grok and a bit of a dump, this document should be more useful. Why rewrites happen. When I wrote version 2 of Beancount (a full rewrite of the first version), it was because of a confluence of ideas for improving my first draft; I resisted for a while, but eventually it made so much sense to me that it became simply impossible not to write it. Many of the ideas driving the redesign at the time are still axioms in today's design: removing order dependence, normalizing the syntax to be well-defined with a BNF grammar, converting custom processing to a sequence of plugins off of a simple stream of directives, the current design of booking selection and how cost basis works, and all the directives beyond \"Transaction\". These ideas largely shape what a lot of people like about using Beancount today. Goals. Now is the time for yet another wave of evolution for Beancount, and similarly, a set of new ideas I'm going to lay down in this document form as potent a change as the v1 to v2 transition. The vision I have for Vnext will simplify Beancount, by factoring into simpler, more isolated, more reusable, better defined parts, and not merely by adding new features on top of what's there. In many ways, Vnext will be a distillation of the current system. It will also make space to finally implement some of the core features most often desired by users. And those changes will enhance some organizational aspects: allow for more contributions, and also trim down the part that I'm handling myself to less code, so I can more effectively focus on just the core features. Current Problems \uf0c1 Performance \uf0c1 My personal ledger, and I know that the ledgers of many users, are simply too large to process instantly. My current file takes 6 seconds on the souped-up NUC I use for a desktop at home\u2014but that's just too long. I'm really quite attached to the idea of processing the entire set of inputs every time, instead of forcing users to cut-up their ledgers into multiple files with \"closing\" transitions at arbitrary points in the year, but I really do want that \"instant\" feeling that you get when you run two-letter UNIX programs, that it runs in well under half a second . It makes it a lot more interactive and fun to use. C++ rewrite. One of the reasons for the slow performance right now is the fact that Beancount is implemented in Python, even at the level of the parser (C code calling back into a Python driver). An obvious solution is to rewrite the core of the software in a language closer to the metal, and that will be C++. I'm selecting C++ for its control and because the current slate of tools around it is mature and widespread enough that it should be easy for most to build without too many problems, and I can leverage C libraries that I will need. Using a functional language could have been fun but many of the libraries I want simply would not be available or it would be too difficult for mere mortals to build. Simple, portable C++. It's important to mention that the C++ code I have in mind is not in the style of template-heavy modern C++ code you'd find in something like Boost. Rather, it's a lot more like the conservative \"almost C without exceptions\" subset of C++ that Google uses , with a base on Abseil-Cpp (for example and flavor, see tips ). The reasons for this are stability and portability, and while this rewrite is for faster performance, I believe that it will not be necessary to pull template tricks to make it run fast enough; just a straightforward port to avoid the Python runtime will likely be sufficient. Above all I want to keep the new code simple and \"functional-ish\" as much as possible (no classes if I can avoid it), relying on a trusted set of stable dependencies , built hermetically using the Bazel build tool. Python API. It's also important that the Python API remains for plugins and scripts, and that the full suite of unit tests be carried over to the newer version of the code. After all, the ability to write custom scripts using all that personal finance data is one of the most attractive features of the text-based approach. Code beyond the new core implementation will remain in Python, and existing code built on top of the Python API should be very easily portable to Vnext. This can be achieved by exposing the directives with wrappers written in pybind11. Other languages. The resolved output of the Beancount core will be a stream of protocol buffer objects, so processing from other languages (e.g., Go) will have first-class support. Processing model. An additional algorithmic improvement to performance, should it be necessary, would be to define plugins processing in terms of iterator functions that cascade and interleave the processing of the directive stream without making entirely disjoint passes over the full list of directives. While the freedom to have each plugin process all the directives on their own has been instrumental in keeping the system free of synchronized application state and has allowed me to isolate behavior of the plugins from each other, there are opportunities to join together multiple transformations in a single pass. Fewer passes = faster. Intermediate Parsed Data vs. Final List of Directives \uf0c1 In Beancount v2, I was never too careful to clearly distinguish between The list of directives coming out of the parser , missing interpolation and booking, using position.CostSpec instead of position.Cost on each lot, and The resolved and booked list of directives with booking algorithms applied to select matching lots, and interpolated values filled in, as well as transformations having been applied by the various plugins. These two lists of directives are really quite distinct in purpose, though they share many common data structures, and for the most part, the first list appears mostly in the parser module. There have been cases where it was confusing, even to me, which of the lists I was manipulating. Part of the reason is due to how I'm using mostly the same Python data structures for both, that allow me to bend the rules on typing. Perhaps more importantly is that because plugins run after booking and interpolation, and are required to put out fully interpolated and booked transactions, a plugin that wants to extend transactions that would run as invalid in the input syntax is difficult. See #541 for an example. The next iteration will see both the intermediate parser production and final resolved list of directives implemented as protocol buffer messages, with strictly distinct data types. This will replace beancount.core.data . The distinction between these two streams will be made very clear, and I will try to hide the former as much as possible. The goal is to avoid plugin writers to ever even see the intermediate list. It should become a hidden detail of the implementation of the core. Furthermore, there may be two types of plugins: a plugin that runs on the uninterpolated, unbooked output of the parser, and a plugin that runs on the resolved and booked stream. This would allow more creative use of partial input that might be invalid under the limitations of interpolation and booking. Updates: We could convert the plugin system to one that runs at booking/interpolation time. We should attempt to make the booking/interpolation atomic, in that one could write a Python loop with an accumulator and invoke it independently, so that in theory, booking could be used in the importers (like I do for Ameritrade). Make Rewriting the Input First Class \uf0c1 Added in Dec 2020 after comments and #586 . A number of frequently asked questions have to do with how to process the input data itself. Usually, a new user will attempt to load the contents of the ledger, modify the data structures, and print to update their file, not realizing that the printer includes all the interpolations, booking data, and modifications from plugins, so this cannot work. However, since we're rewriting the parser and ensuring a clean separation between intermediate ASI-like data and processed and finalized directives, we can implement a special printer for the AST intermediate data, so that users could run just the parser, modify the intermediate directives, and print them back out, perhaps losing just some of the formatting and whitespace. This formatting loss can be leveraged to reimplement bean-format more naturally: the output of that printer should always be formatted neatly. This would avoid users having to write ad-hoc parsers on their input file, sed-like conversions, and so on. They could do it properly by modifying the data structure instead. What's more, in order for this to work accurately, we'd have to delay processing of the arithmetic operations post-parsing, so that we can render them back out. This offers another advantage: if we process the calculations after parsing, we can afford to provide an option to let the user specify the precision configuration to use for mpdecimal. I really like that idea, because it avoids hard-coding calculation precision and better defines the outcome of these options, potentially opening the door to a more rational way to remove extra digits that often get rendered out. Finally, if a nice library function can be made to process transactions in-place and output them back out, preserving all comments around them, this can become another way\u2014perhaps the preferential way\u2014for us to clean payees and somesuch. At the moment, the solution is to write a plugin that will clean up the data, but the input file remains a bit of a mess. Making it easy to automatically clean up the input file is an appealing alternative and potentially will add an important new dimension to the Beancount workflow. I want to make all changes necessary to make this possible and well supported (I'm imagining my ledger file all cleaned up right now and it's appealing). I think it's not very much work, it involves: Storing begin/end line information on everything. Adding AST constructs for representing arithmetic calculations. Adding comments parsing to the renderer. Implementing a new renderer that can reproduce the AST, including handling missing data. Implementing a library to make modification of a file in-place as easy as writing plugins, while preserving all non-directive data in the file as is. Contributions \uf0c1 For most of the development of Beancount, I've been pretty reluctant to accept contributions. It has been a closely held pet project of mine since it has so much impact on my personal financial arrangements and I dread unplanned breakage. The main reservations I've had over contributions are two-fold: Not enough testing. Proposed changes that did not include enough testing, or none at all, sometimes even the kind testing that would prevent basic breakage. When I'd accept some proposals and commit to writing the tests myself it could sometimes take me down the rabbit hole for hours (if not days). This wasn't practical. Cascading design impact. Some of the proposals did not take into account broader design considerations that would affect other parts of the code, which I may not have communicated or documented well. I've had to reject some ideas in the interest of keeping things coherent and \"tight.\" Part of this is my fault: putting a high bar on contributions hasn't allowed potential contributors to acquire enough context and familiarity with the codebase to make changes compatible with its design. Allowing more contributions. Starting in Vnext I'd like to restructure the project so that more people are able to get involved directly, and to closely work 1-on-1 with some contributors on particular features. Clearly Beancount benefits from direct input from more people. The recent move to GitHub only compounds the urgency for making that easier. To this effect, I'd like to implement a few strategies: Break down the code in parts. This is a natural evolution that has been seen in many other projects; I'm no Linus nor Guido, but like their respective projects, as they grew in popularity, the original authors narrowed their focus on the core part and let other people expand on adjacent but also important functionality. I'd like to focus more of my time only on core functionality that will impact things like support for settlement dates, currency accounts, split transactions, trade reporting, etc. Letting other people deal with adding or updating price sources, making improvements to the ingestion framework, and making beautiful renderings and presentations of the data would be ideal. In time, I may eventually break down these libraries to separate repositories with looser contribution guidelines and/or add ACLs for others to push directly to those repos. Acquire \"lieutenants.\" I need to leave more space for trusted and frequent contributors to chip in more liberally. For example, Martin Michlmayr now has direct edit access to most documents and has been making numerous helpful contributions and updates to the docs. Kirill Goncharov's conversion of the documentation out of Google Docs is simply beautiful. RedStreet and many others are regularly pitching in answers on the mailing-list. Stefano Zacchiroli and Martin have built a standalone conversion tool from Ledger. Daniele Nicolodi is proposing some low-level changes to the scanner and parser. And of course, Dominik Aumayr and Jakob Schnitzer continue developing the Fava project adjacent to Beancount. There are many more people, there is a slowly-but-surely growing list of familiar recurring names. The question in my mind is: Is there a way to communicate with regular faces so that we're aligned in terms of design and can coordinate our efforts in the same direction? Does the newly acquired familiarity with video-conference meetings (thanks to the coronavirus crisis) afford us a new channel for coordination that wasn't there before? The Python community has thrived in no small part due to the annual face-to-face interactions between its participants and a number of core developers being in the same place for some time. Can we achieve the same thing online, as we are a smaller community? If so, better coordination may make it easier to accept proposed changes. I wonder if I should propose a monthly \"team\" meeting. 20% projects. I should provide a list of \"20% projects\" that are well aligned with the direction of the project for others to take up if they want to, and add profuse guidance and detail of downstream side-effects from those proposed features. The idea is to make it possible for newcomers to contribute changes that are likely to fit well and be easily integrated and accepted on the codebase.: Proposals. Beancount's equivalents of Python's \" PEPs \" are essentially the Google Docs proposal documents I started from threads where many others comment and add suggestions. A central list of those should be shared to a folder, identified as such, and allow others to write similar proposals. Maybe a little more structure and discipline around those would be useful. Restructuring the Code \uf0c1 At the very coarse level, the code restructuring for Vnext looks like this: C++ core, parser, and built-in plugins. The Beancount core, parser, booking algorithm and plugins get rewritten in simple C++, outputting its parsed and booked contents as a stream of protobuf objects. Query engine. Beancount query/SQL gets forked to a separate project operating on arbitrary data schemas, with a much broader scope than Beancount. See section below. The rest. Most of the rest gets cut up into separate projects or at least, at first, in a distinct place within the repository (until the core is rewritten). Note that because the core outputs the stream of directives as proto objects, any language supported by protobufs should be able to read those. This extends the reach of Beancount. Here's a simplified diagram showing how this might look: Here is a detailed breakdown of the various parts of the codebase today and what I think will happen to them: Core. This is the part of Beancount's code which will get rewritten in C++ and output a sequence of messages to a stream of directives. I'll continue keeping a tight focus on that part with a conservative eye toward stability, but in Vnext will be adding desired new capabilities that have been lacking so far as described in the next section of this document. The core will include the following packages: beancount/core beancount/ops beancount/parser beancount/utils beancount/loader.py beancount/plugins (some, see below) beancount/utils (most) Query. The query language will be factored out into a completely separate repo with a broader application domain (and hooks for customizing for Beancount). I suspect that over time that project will acquire a much broader range of contributors, many of which will not even be Beancount users. This includes the code from these packages: beancount/query beancount/tools Prices. This is a simple library and tool that helps users fetch prices from external sources. This should definitely move to another repo and I'd welcome a new owner building a competing solution. People are sending me patches for new price sources and I have too little time to maintain them over time, as the upstream sources change or even disappear. This requires very little from Beancount itself (in theory you could just print() the directives for output, without even loading library code) but I think the Beancount core should include and functions to enumerate a list of required date/instrument pairs at a particular date from a given ledger (and I'm happy to support that). Note that the internal price database core will remain in the core, because it's needed there. The affected packages are: beancount/prices Improvements should be made to this library after it moves out of the Beancount repository: we should isolate the Beancount code to just a few modules, and turn the scope of this project to something larger than Beancount: it's three things, really: a) an up-to-date Python library of price fetchers with unit tests and maintained by the community (i.e., when sources break, we update the library) and a common API interface (needs to be improved from what's there TBH, the API should support fetching time series in a single call); b) an accompanying command-line tool (currency \"bean-price\") for fetching those prices from the command-line. This requires the specification of \"a price in a particular currency from a particular source\" as a string. I'd like to improve that spec to make the USD: prefix optional, and maybe eliminate the chain of prices in the spec, which hasn't found much use in practice and move that upstream. c) Make the interfaces to fetch ledger-related information (e.g., list of missing/required prices and lists of instruments) onto modules: beancount v2, beancount Vnext, ledger, hledger, and rendering output formats to any of these. In other words, this library should be able to fetch prices even if Beancount isn't installed. To turn this project into something that can run independent of beancount. Ingest. The importers library will probably move to another repo and eventually could even find another owner. I think the most interesting part of it has been the establishment of clear phases: the identify, extract and file tasks, and a regression testing framework which works on real input files checking against expected converted outputs, which has worked well to minimize the pain of upgrading importers when they break (as they do break regularly, is a SNAFU). In the past I've had to pull some tricks to make command-line tools provided by the project support an input configuration as Python code but also possible to integrate in a script; I will remove the generic programs and users will be required to turn their configuration itself into a script that will just provide subcommands when run; the change will be very easy for existing users: it will require only a single line-of-code at the bottom of their existing files. The Bazel build may add some minor difficulties in loading a Python extension module built from within a Bazel workspace from an otherwise machine-wide Python installation, but I'm confident we'll figure it out. I'd also be happy for someone else to eventually take ownership of this framework, as long as the basic functionality and API remains stable. The example csv and ofx importers should be removed from it and live in their own repos, perhaps: ofx. the OFX importer should be replaced by something using ofxtools (the one I built is pretty bad), and the CSV importer really needs a thorough rewrite with lots of unit testing for the large list of options it now supports (these tests are sorely missing). csv. Furthermore, I think the CSV importer could be enhanced to be smarter and more flexible, to automatically detect from the column headers and inferred data types in the files which column should convert into which field. I'm not going to do that (I don't have time). Someone with the urge to make the ultimate automatic CSV parser ought to create a separate repository for that. The affected packages are: beancount/ingest : could eventually move to another repo. beancount/ingest/importers: someone could revive a repository of importer implementations, like what LedgerHub once aimed to become, and swallow those codes. See this document for details on what's to happen with the ingestion code. Custom reports and bean-web should be removed: the underlying bottle library seems unmaintained at this point, Fava subsumes bean-web, and I never liked the custom reports code anyway (they're a pain to modify). I never use them myself anymore (other than through bean-web). I really think it's possible to replace those with filters on top enhanced SQL query results. The conversion to Ledger and HLedger from Beancount now seems largely useless, I'm not sure anyone's using those. I'll probably move these to another repo, where they would eventually rot, or if someone cares, adopt them and maintain or evolve them. beancount/web : will be deleted or moved to another repo. beancount/reports : will be deleted or moved to another repo. Note that this includes deprecating beancount/scripts/bake , which depends heavily on bean-web. I have no substitute for bean-bake, but I think I'd like to eventually build something better, a tool that would directly render a user-provided list of specific SQL queries to PDF files and collate them, something you can print. Jupyter notebook support. A replacement for the lightweight interface bean-web used to provide could be Jupyter Notebook integration of the query engine, so that users can run SQL queries from cells and have them rendered as tables, or perhaps a super light web application which only supports rendering general SQL queries to tables. Built-in Plugins. Beancount provides a list of internal plugins under beancount/plugins . It's not indicated clearly, but there have evolved two groups of plugins in there: stable plugins used by the core, and experimental plugins showcasing ideas, which are often incomplete implementations of something that was proposed from a thread on the mailing-list. The former group will be ported to C++, and the latter group should probably move to another location with much looser acceptance constraints. First, there are \"meta-plugins\" which only include groups of other plugins: Only one of those should remain, and maybe be enabled by default (making Beancount pedantic by default): auto pedantic The following plugins should remain in the core and be ported to C++: auto_accounts check_closing check_commodity close_tree commodity_attr check_average_cost coherent_cost currency_accounts implicit_prices leafonly noduplicates nounused onecommodity sellgains unique_prices The following are the experimental implementations of ideas that should move to a dedicated repo where other people can chip in other plugin implementations: book_conversions divert_expenses exclude_tag fill_account fix_payees forecast ira_contribs mark_unverified merge_meta split_expenses tag_pending unrealized Because it's a really common occurrence, the new transfer_lots plugin should be part of the built-in ones. Projects. The beancount/projects directory contains the export script and a project to produce data for a will. The will script will be moved outside the core of Beancount, I'm not sure anyone's using that. Maybe the new external plugins repo could include that script and other scripts I shared under /experimental. The export script should be grouped together with beancount/scripts/sql and other methods to send / share data outside of a ledger; these could remain in the core (I'm using the export script regularly to sync my aggregates and stock exposure to a Google Sheets doc which reflects intraday changes). Scripts. Some of the scripts are completely unrelated to Beancount, they are companions. The scrape validator. The sheets upload. The treeify tool. These should be moved elsewhere. One of the advantages of having all the code in the same repo is that it makes it possible to synchronize API changes across the entire codebase with a single commit. As such, I may keep some of the codes in the same repo until the new C++ core has stabilized, and properly separate them only when Vnext releases. Universal Lightweight Query Engine (ulque) \uf0c1 The SQL query engine for Beancount was initially a prototype but has grown to become the main way to get data out of it. I've been pretty liberal about adding functionality to it when needed and it's time to clean this up and consider a more polished solution. In Vnext, the query/SQL code gets eventually forked to a separate project (and repo) operating on arbitrary data schemas (via protobufs as a common description for various sources of data) and has support for Beancount integration. Imagine if you could automatically infer a schema from an arbitrary CSV file, and run operations on it, either as a Python library function or as a standalone tool. Furthermore, this tool will support sources and/or sinks to/from Google Sheets, XLS spreadsheets, containers of binary streams of serialized protos, tables from HTML web pages, PDF files, directories of files, and many more. This is going to be a data analysis tool with a scope closer to that of the Pandas library rather than an accounting-focused project, but also a universal converter tool, that will include the functionality of the upload-to-sheets script (which will get removed). One of the lessons from the SQL query engine in Beancount is that with just a little bit of post-processing (such as treeify ), we can do most of the operations in Beancount (journals, balance sheet & income statements) as queries with filters and aggregations. The tool will be made extensible in the ways required to add some of the idiosyncrasies required by Beancount, which are: Native support for a Decimal type . The addition of custom types for aggregators with the semantics of beancount.core.Inventory/Position/Amount . The ability to automatically generate a dynamic column rendering a line-by-line aggregation of another column (or set thereof), that is, a \"balance\" column . The ability to render a \" bottom line \" of aggregates at the end of the results table. Functions for splitting of aggregated columns , for amounts and inventories into multiple columns (e..g, \"123.00 USD\" becomes two columns: (123.00, \"USD\") to be processable in spreadsheets, and also for splitting debits and credits to their own columns. In particular, printing multiple lots accumulated in an account should be made natural from the SQL query, replacing the \"flatten\" feature by a more standard splitting off an array type. Moreover, broadening the focus with a new project definition will make a change to testing it thoroughly (the current one is still in a bit of a prototype stage and does not have nearly the amount of required tests), and also include data type validation (no more exceptions at runtime), by implementing a typed SQL translator. I'll document this elsewhere. This is a much bigger project, but I suspect with the broader scope, it will be easier to test and take on a life of its own. I'm preparing a design doc on this. API Rework \uf0c1 I write a lot of custom scripts, and there are a number of things that bother me about today's Beancount API, which I want to radically improve: Consolidate symbols under \"bn\". The internal API calls for importing the symbols from each package separately, but now that I'll have split off the ingestion and reporting code, all of the public API, or at least the majority of the commonly used objects in the core should be available from a single package, a bit like numpy: import beancount as bn \u2026 bn.Inventory(...) bn.Amount(...) bn.Transaction(...) # etc. I'd like for \"bn\" to become the de-facto two-letter import on top of which we write all the scripts. Default values in constructors. The namedtuple containers are mighty fine, but their constructors never had optional arguments, and it's always a bit of a dance to create those containers with a ton of \"None\" options. I never liked it. We'll make this tidy in the next iteration. No API documentation. While there is a substantial amount of documentation around the project, there is no documentation showing people how to use the Python API, e.g. how to accumulate balances, how to create and use a realization tree, how to pull information out of an accumulated inventory object, etc. I think that documenting some of the most common operations will go a long way towards empowering people to make the most out of Beancount. Some of these operations include: Accumulating lots of an inventory and printing them. Converting to market value, and making corresponding account adjustments. \u2026. add more \u2026 Exposed, usable booking. Booking will be a simple loop that can be invoked from Python with an entry and some accumulated state. Moreover, the Inventory object should begin to implement some of the lower-level operations required for booking, such that iterating over a set of postings and doing e.g., average booking, can be done via method calls on an Inventory object. Inventory should take a more prominent place in the API. Data types. Well defined data types should be provided for all objects to make liberal use of the typing module over all new code. Maybe create a module called \"bn.types\" but they should be available directly from \"bn.*\" so that there is a single short-named import. Terminology. I'd like to stop using \"entries\" and consolidate over the name \"directives\" in Vnext. Realization. I've been using a collections.defaultdict(Inventory) and a \"realization\" interchangeably. Both of these are mappings from an account name (or some other key) to an Inventory state object. I'd like to unify both of these constructs into the realization and make it into a commonly used object, with some helper methods. Parser Rewrite \uf0c1 Since we will now depend on C++, the parser will get to be rewritten. Worry not: the input syntax will remain the same or at least compatible with the existing v2 parser. What will change is: Unicode UTF-8 support. The lexer will get rewritten with RE/flex instead of GNU flex. This scanner generator supports Unicode natively and any of the input tokens will support UTF-8 syntax. This should include account names, an oft-requested feature. Flags. The current scanner limits our ability to support any flag and supports only a small list of them. I think the list has proved sufficient for use, but since I'll be putting some work into a new scanner I'm hoping to clean up that story and support a broader, better defined subset of single-letter flags for transactions. Time. The parser will parse and provide a time field, in addition to the date. The time may be used as an extra key in sorting directives. The details for this are yet to be determined, but this is requested often enough at the very minimum the parser will output it as metadata, and at best, it may become a first-class feature. Caching. The pickle cache will be removed. Until very recently , there weren't great options for disabling it (env vars) and I'd rather remove the only two environment variables that Beancount honors as a side-effect. Since the C++ code should be fast enough, hopefully a cache will not be needed. Tags & links. In practice, those two features occupy a very similar role as that of metadata (used to filter transactions). I'm contemplating unseating the special place taken by tags and links in the favor of turning those into metadata; the input syntax would not be removed, but instead the values would be merged into the metadata fields. I'm not 100% sure yet about doing this and open for discussion. Furthermore, the parser should be made to accept #tag and ^link where metadata is declared today, which would be convenient syntax. Finally, users have expressed a desire for tags on postings. We should contemplate that. Plugins configuration as protos. The options for the various plugins have been loosely defined as eval'ed Python code. This is pretty loose and doesn't provide a great opportunity for plugins to do validation nor document their expected inputs. I'd like to formalize plugin configuration syntax a bit, by supporting text-formatted protos in the input syntax (for a message type which would be provided by the plugins themselves). Parser in C++. The parser will be rewritten in C++. In the process of writing Vnext, I'll try to maintain a single grammar for both for as long as possible by calling out to a C++ driver interface, which will have two distinct implementations: one for the V2 version calling into Python, and one for the Vnext parser generating protos. In the process I may be porting the lexer and grammar Python implementation to C, as discussed in this ticket . Better includes. Current includes fail to recognize options that aren't in the top-level file. This caused many surprises in the past and should be fixed. At the minimum, an error should be raised. Code Quality Improvements \uf0c1 Rename \"augmentation\" and \"reduction\" to \"opening\" and \"closing\" everywhere. This is just more common terminology and will be more familiar and understandable to people outside of our context. Type annotations. The use of mypy or pytype with type annotations in Python 3 is by now a very common sight, and works quite well. As part of Vnext, all of the core libraries will be modified to include type annotations and the build should be running pytype automatically. I'll need to add this to our Bazel rules (Google doesn't currently provide external support for this). While doing this, I may relax some of the Args/Returns documentation convention, because in many cases (but not all) the type annotations are sufficient to get a good interpretation of a function's API. PyLint in build. Similarly, the linter should be run as an integral part of the build. I'd like to find a way to selectively and explicitly have to disable it during development, but otherwise be set up such that lint errors would be equivalent to build failures. Flexible constructors for Python API. The types generated by collections.namedtuple() or typing.NamedTuple don't offer flexible constructors with named parameters. I think all codes that create transaction objects today would benefit from having constructors with default values, and I'll be providing those to create corresponding proto objects. Tolerances & Precision \uf0c1 The story around how precision and tolerances are dealt with hasn't been great, for two reasons: Explicit tolerance option. I've tried to design the tolerance (used for balancing transactions) to be automatic and automatically inferred from statistics from the numbers in the input. The results aren't great. In Vnext I aim to provide an explicit option for setting the tolerance per currency. Precision. There are various places where numbers get rendered in v2: the reports code, the SQL query, and debugging scripts, and the way precision is set hasn't been used consistently. The precision also needs to be explicitly settable by the user. Rounding. There is another quantity that's used during interpolation: the precision used to round calculated numbers. Moreover, there is a need to distinguish between the precision and tolerances for numbers when used as prices vs. when used as units (see here ). One way is to store the display context per currency PAIR, not per currency itself. The distinction between these quantities hasn't been documented well; I'll keep in mind to clearly annotate those codes in Vnext and add suitable docs for this. Mostly the precision will be a rendering concern and a quantity that will be relevant for the new universal SQL query tool. Some prior design documentation exists here . Core Improvements \uf0c1 Some desiderata of new features are discussed below. These are all relevant to the core. Note that the changes should not interfere with current usage much, if at all. I expect that v2 users will be largely unaffected and won't have to change their ledger files. Booking Rules Redesign \uf0c1 Main document One of the current problems with booking is that entering an augmenting leg and a reducing leg have to be different by nature. The augmentation leg has to provide the cost basis via {...} syntax, and the reducing leg has to enter the price in the annotation and not in the cost basis. For example: 2021-02-24 * \"BOT +1 /NQH21:XCME 1/20 FEB 21 (EOM) /QNEG21:XCME 13100 CALL @143.75\" Assets:US:Ameritrade:Futures:Options 1 QNEG21C13100 {2875.00 USD} contract: 143.75 USD ... 2021-02-24 * \"SOLD -1 /NQH21:XCME 1/20 FEB 21 (EOM) /QNEG21:XCME 13100 CALL @149.00\" Assets:US:Ameritrade:Futures:Options -1 QNEG21C13100 {} @ 2980.00 USD contract: 149.00 USD ... Notice how the selling transaction has to be written down differently from the perspective of the user. The thing is, this makes it difficult from the perspective of the importer writer. It also ties the required syntax with the state of the inventory it's applied to, as it assumes something about this inventory. Moreover, this makes it difficult to write an importer that would handle a crossing of the absolute position, like this: 2021-02-19 * \"BOT +1 /NQH21:XCME @13593.00\" Assets:US:Ameritrade:Futures:Contracts 1 NQH21 {271860.00 USD} contract: 13593.00 USD Assets:US:Ameritrade:Futures:Margin -271860.00 USD Expenses:Financial:Commissions 2.25 USD Expenses:Financial:Fees 1.25 USD Assets:US:Ameritrade:Futures:Cash -3.50 USD 2021-02-19 * \"SOLD -2 /NQH21:XCME @13590.75\" Assets:US:Ameritrade:Futures:Contracts -2 NQH21 {271815.00 USD} contract: 13590.75 USD Assets:US:Ameritrade:Futures:Margin 543630.00 USD Income:US:Ameritrade:Futures:PnL 45.00 USD Expenses:Financial:Commissions 4.50 USD Expenses:Financial:Fees 2.50 USD Assets:US:Ameritrade:Futures:Cash -52.00 USD The issue here is that we're crossing the flat line, in other words, we go from long one to short one. There are only two ways to do that properly right now: Disable booking and use the cost only, as per above. This is not great \u2014 booking is terribly useful. Track the position in your importer and separate the reducing and augmenting legs: 2021-02-19 * \"SOLD -2 /NQH21:XCME @13590.75\" Assets:US:Ameritrade:Futures:Contracts -1 NQH21 {} @ 271815.00 USD Assets:US:Ameritrade:Futures:Contracts -1 NQH21 {271815.00 USD} Both solutions aren't great. So I came up with something new: a complete reevaluation of how the syntax is to be interpreted. In fact, it's a simplification. What we can do is the following: use only the price annotation syntax for both augmentation and reduction and currency conversions, with a new booking rule \u2014 Match lots without cost basis in priority. If the lots have no cost basis, the weight of this posting is simply the converted amount, as before. If a match has been made against a lot with cost basis, the weight of this posting is that implied by the matched lots. Make the {...} used solely for disambiguating lots to match, and nothing else. If you have unambiguous matches, or a flexible booking strategy, e.g. FIFO, you'd pretty much never have to use the cost matching reduction. With this, the futures transaction above would simply use the @ price annotation syntax for both transactions. It would Make importers substantially simpler to write Supports futures naturally Be backward compatible with existing inputs for both currency conversions and investments. It is also more generally consistent and friendlier to Ledger users, without sacrificing any of the tighter constraints Beancount provides. I think it's even simpler to think about. Furthermore, this: Assets:US:Ameritrade:Futures:Contracts -1 NQH21 {} @ 271815.00 USD would be interpreted as \"match this lot, but only those with a cost basis attached to them.\" One question that remains is to decide whether an augmentation \u2014 now written down simply with @ price annotation \u2014 would store the cost basis in the inventory or not. I think we could make this determination per-commodity, or per-account. This would impose a new constraint: a commodity (or \"in an account\") would always be stored with cost basis, or not. Posting vs. Settlement Dates \uf0c1 When you import a transaction between multiple accounts within a single ledger, e.g. a credit card payment from one's checking account, the dates at which the transaction posts in each account may differ. One side is called the \"transaction date\" or \"posting date\" and the other side the \"settlement date.\" Where the money lives in between is somewhere in limbo (well in practice there is no money at all, just differences in accounting between institutions, things are never reflected instantly). One of the major shortcomings of the current core code is that the ability to insert a single transaction with postings at different dates is missing. Users are recommended to select a single date and fudge the other one. Some prior discussion on this topic exists here . Unfortunately, this method makes it impossible to represent the precise posting history on at least one of the two accounts. A good solution needs to be provided in Vnext, because this is a very common problem and I'd like to provide a system that allows you to precisely mirror your actual account history. The automatic insertion of transfer accounts to hold the commodities can be implemented as a feature, and it should live in the core. One possible idea would be to allow optional posting dates, like this: 2020-01-19 * \"ONLINE PAYMENT - THANK YOU\" \"\" Assets:US:BofA:Checking -2397.72 USD 2020-01-21 Liabilities:US:Amex:BlueCash 2397.72 USD which would result in two transactions behind the scenes, like this: 2020-01-19 * \"ONLINE PAYMENT - THANK YOU\" \"\" Assets:US:BofA:Checking -2397.72 USD Equity:Transfer 2020-01-21 * \"ONLINE PAYMENT - THANK YOU\" \"\" Liabilities:US:Amex:BlueCash 2397.72 USD Equity:Transfer The lack of symmetry here raises the question of whether we should allow a transaction without a date or not: * \"ONLINE PAYMENT - THANK YOU\" \"\" 2020-01-19 Assets:US:BofA:Checking -2397.72 USD 2020-01-21 Liabilities:US:Amex:BlueCash 2397.72 USD I think we can figure this out and the first solution is very doable. Input Split Transactions Some users like to organize their inputs in different files, or in different sections that strictly contain all of an account's transactions in order. This is related in spirit to the posting and settlement dates problem: at the moment the user is required to choose one of the two locations to insert their transaction. This should not be necessary. We should provide a mechanism that would allow users to insert the halves of a transaction into two different locations in their file, and a robust merging mechanism that would ensure that the two related transactions have been matched and merged (so that no unmerged half remains) and otherwise report errors clearly. The two halves could look like this: 2020-01-19 * \"ONLINE PAYMENT - THANK YOU\" \"\" Assets:US:BofA:Checking -2397.72 USD \u2026 2020-01-21 * \"AMEX EPAYMENT ACH PMT; DEBIT\" Liabilities:US:Amex:BlueCash 2397.72 USD The matching could be done via explicit insertion of special links, or by heuristics to match all such related transactions (perhaps declaring valid account pairs, thresholding on date differences and exactly matching amounts). When impossible to match, an error should be raised. Those merged transactions should be checked for balancing. Note how each of the transactions has a differing date; this would integrate with the transfer account solution proposed in the previous section. I haven't designed something yet, but this should be easy to implement and should be provided as a core feature, since it's so closely related to the input syntax. Currency Accounts instead of a Single Conversion \uf0c1 The current implementation of multiple currency transactions relies on a special \"conversion transaction\" that is automatically inserted at reporting time (when closing the year) to account for the sum total of imbalances between currencies. The goal of this transaction is to ensure that if you just sum up all the postings in the book, the result is purely an empty inventory (and not some residual amount of profit or loss incurred during currency exchange across different rates \u2014 note that we're talking only of the @price syntax, not investments). This is a bit of a kludge (the transaction itself does not balance, it converts to zero amounts of a fictional currency in order to keep itself quietly passing the balance test). What's more, its actual value is dependent on a subset of filtered transactions being summed up so it's a reporting-level construct, see here . There exists a method for dealing with multiple currencies without compromising on the hermeticity of individual transactions, described online, here . Using that method, you can filter any subset of transactions and summing them up will cleanly cancel out all lots. You don't need to insert any extra weight to fix up the balance. Also, you can explicitly book profits against the accrued gains in the currency accounts and zero them out and take advantage of this when you report them (and track them over time). The downside is that any currency conversion would see extra postings being inserted, etc. 2020-06-02 * \"Bought document camera\" Expenses:Work:Conference 59.98 EUR @ USD Liabilities:CreditCard -87.54 USD Equity:CurrencyAccounts:EUR -59.98 EUR Equity:CurrencyAccounts:USD 87.54 USD The problem is that it's a pain to use this method manually, it requires too much extra input. It's possible to have Beancount do that for us behind the scenes, completely automatically. I coded a proof-of-concept implementation here , but it's incomplete . In Vnext: The insertion of the kludgey conversions transactions should be removed. The currency accounts should become the norm. The fact that the two streams of directives will be very clearly separated should help, by distinguishing even more clearly between the parsing representation and the fully booked one, which will show these extra legs on transactions The prototype should be completed and issues fixed completely (not that much work involved). Strict Payees \uf0c1 I'm not sure if this makes sense yet, but I'd like to clean up the mess that payee strings are today. Payees are free-form, and if the user does not take care to clean them up\u2014and I'm one of those who doesn't\u2014the memos from imported sources are messy. It could be interesting to create a new directive to declare payee names ahead of time and an optional model that would require payees to be found in the list of declared payee names. Payees would have to have open and close dates, dates which would define the valid duration of the relationship with that payee (thereby adding more error verification capability). Price Inference from Database \uf0c1 Interpolation from price database. One of the oft-requested features is the ability to automatically interpolate prices from the internal price database history. I think that this should be doable unambiguously and deterministically and added to the interpolation algorithm. Price validation. Since a lot of the conversions at price (i.e., using \"@\") are inferred by leaving out one number, we should validate that the effective price is within some tolerance of a pre-existing price point near the date. This would provide yet another level of checking. Quantizing Operators \uf0c1 Another useful addition to the syntax would be operators that automatically quantize their result to a precision that depends on the particular target currency. For example, 1970-01-01 * \"coffee\" Expenses:Food:Net 2.13 / 1.19 EUR Expenses:Food:Taxes 2.13 / 1.19 * 0.19 EUR ; for example to calculate tax Assets:Cash That would become: 1970-01-01 * \"coffee\" Expenses:Food:Net 1.789915966386555 EUR Expenses:Food:Taxes 0.340084033613445 EUR Assets:Cash If instead an operator like this were provided, it would fix the issue: 1970-01-01 * \"coffee\" Expenses:Food:Net 2.13 /. 1.19 EUR Expenses:Food:Taxes (2.13 / 1.19 * 0.19). EUR Assets:Cash Or somesuch. Or maybe we'll want to add an option such that every evaluation of an arithmetic expression is automatically quantized as such. Constraints System & Budgeting \uf0c1 Beancount does not support budgeting constraints explicitly, but I think it would be possible to extend the balance assertion semantics to cover this. The current balance assertions check (a) a single commodity, and (b) that the amount is precisely equal to an expected one. Balance assertions should be extended to support inequalities, e.g., 2020-06-02 balance Liabilities:CreditCard > 1000.00 USD and perhaps we could check for the total inventory like this 2020-06-02 balanceall Assets:Cash 200.00 USD, 300.00 CAD I'd be curious to hear what would work best from users who do budgeting and design a minimalistic expression language to support that use case (though I'd try to keep it as simple as possible to avoid feature creep). Also, if the syntax is getting changed, adding a syntax that allows checking multiple currencies at once, and possibly a complete assertion that checks there aren't other commodities in the account could also make sense. Average Cost Booking \uf0c1 Average cost booking has been discussed and a good solution sketched out a very long time ago. Vnext should sport that method natively; a lot of users want to have this feature for dealing with their tax-deferred accounts. It takes a bit of work to handle the precision of the various automated conversions right. The way it would work is by automatically merging all related lots of the same commodity on a reduction, and optionally on an augmentation. Some constraints may be required (e.g. only a single commodity in that account). Trade Matching & Reporting \uf0c1 A few core tasks related to P/L and trading still need to be implemented. Trade list. A problem that I've really been wanting to solve for a very long time but never found the time for is to save crumbs from the booking process so that a correct list of trade pairs could be easily extracted from the list of directives. I wrote some notes here and here a long time ago. Essentially, for each booked reduction, insert a reference to the corresponding augmenting posting. I've prototyped this as metadata but it should be made something more official. A single linear scan can pick up these references, build a mapping and recover the (buy, sell) pairs to produce a table of trades. There's a precedent I once wrote in a plugin . Needless to say, producing a list of trades is a pretty basic function that Beancount does not provide out of the box today; it really should. Right now users write their own scripts. This needs to be supported out-of-the-box. Harmonize balance and gains validation. Checking that transactions balance and that income from gains balance with a transaction's prices (the sellgains plugin) are done in completely separate places. Those two codes occupy similar roles, and should be implemented next to each other. Commissions in P/L. Properly counting profits & losses by taking off the fraction of buying commission of an original lot and the selling commission into account is not possible at the moment. I think it could be done with a plugin that moves some of the (computed) income leg into a separate negative income account to do this properly for reporting purposes. Self-Reductions \uf0c1 Currently the application of reductions operates on the inventory preceding the transaction. This prevents the common case of self-reductions, and both I and some users have come across this problem before, e.g. this recent thread ( ticket ). This comes off as unintuitive to some users and ought to have a better solution than requiring splitting of transactions. Since we're rewriting the booking code entirely in Vnext, contemplate a new definition that would provide a well-defined behavior in this case. I remember from prior experiments attempting to implement this that it wasn't a trivial thing to define. Revisit. This would be a nice improvement. Stock Splits \uf0c1 Some discussion and perhaps a strategy for handling stock splits should be devised in Vnext. Right now, Beancount ignores the issue. At the minimum this could be just adding the information to the price database. See this document for more details. Multipliers \uf0c1 Options have a standard contract size of 100. Futures have a contract size that depends on the particular instrument (e.g., /NQ with a multiplier of 20). I've been handling this for options by multiplying the units by 100, and for futures by multiplying the contract size by the per-contract multipliers (ditto for options on futures). I do this in the importers. For options, it works and it isn't too bad (e.g. positions of -300 instead of -3), but for futures, it's ugly. The result is not representative of the actual transaction. I'd like to add a per-currency multiplier, as well as a global dictionary of regexps-to-multiplier to apply, and for this to be applied everywhere consistently. One challenge is that everywhere there's a cost or price calculation, this has to be applied. In the current version, those are just multiplications so in many parts of the codebase these haven't been wrapped up in a function that could easily be modified. This needs to happen in a big rewrite \u2014 this is the opportunity to do this. Example here. Returns Calculations \uf0c1 If you look at investment brokers out there, no one calculates returns correctly. Brokers provide one of two features: No cash transfers. Total value of account today vs. total value of account at some point in the past (i.e., account inception or beginning of the year). This isn't very useful because they never account for the addition or removal of cash to the account. For example, say you open an account with $100,000 and invest, and mid-year add another $20,000, say the original investments are now worth $95,000, the report would show a gain of $15,000, whereas you really incurred a loss. Better brokers like Vanguard will show a plot that includes two overlaid bars, one with cash added and profit overlaid, like this: Lack of interest or dividends. Other brokers will report P/L over time from the investments, but they fail to account for actual interest or dividends received (they only look at the price of the underlying) so that's not useful for bonds or for stocks with significant dividends, or when grouping them, they fail to account for the addition of new positions over time. Counterfactual performance. Finally, all of them fail to compare your actual annualized performance with that of a benchmark portfolio with equivalent cash infusions. For example, instead of your actual investments made, compare with the performance you would have obtained if you had invested in some standardized portfolio of investments over that particular time period, given the actual historical prices of those instruments. Ideally one should be able to define any alternative portfolio to compare against using their particular cash transfers. More fancy analyses aren't even contemplated, e.g., what would have been the impact of changing my rebalancing strategy (or actually implementing a more strict one)? There are well known methods for both time-based and value-based returns reporting. The right thing to do is to extract a time-series of cash flows and compute the annualized or IRR returns, or value-weights. I started this work at some point and ran against some difficulties and eventually removed it. The results remain here. I'd really love to build this, and eventually perhaps this could grow into its own project, with associated support functions in the Beancount core. This will possibly be a project of its own, but this requires similar support for enumerating instruments and price sources as that which is needed for fetching prices, as well as functions for isolating cash flows for specific subsets of accounts; these should probably live in the core. UPDATE September 2020: This has mostly been implemented. See this document for details. Unsigned Debits and Credits \uf0c1 A useful idea that's nearly trivial to implement is to allow users to input all positive unit numbers and to automatically flip the signs on input, and to output them all as positive numbers as well splitting them up between Debit and Credit columns. This would make Beancount's rendering a lot easier to grok for people with an existing background in accounting. This feature will introduce no complexity, easy to add for free. See TODO here . Holdings \uf0c1 One of the libraries I built at some point was this notion of a \"holding\", here . At the time I wasn't sure if that data would contain much more than what's in a posting, but as it turns out, representing holdings as just the posting is good enough, all you need is the market value, and compute the total values from the units is trivial. In fact, I've haven't been using that code for years, I've been using the export script instead, which writes out a table that gets uploaded to a Google Sheets doc. This proves to me aggregating the positions in an Inventory is plenty sufficient in practice, along with a mapping of latest market prices. I'm going to delete that code. It's only been used in the reports code anyway, which will be removed anyway, and in the experimental \" unrealized gains \" plug, which was only a proof-of-concept that convinced me booking such gains as transactions is not a good idea and which will get removed and live in a separate experimental repository anyway. Tooling for Debugging \uf0c1 Context should recover erroneous transactions. One of the major annoyances of error recovery is that if a transaction involves some types of errors, the postings aren't produced in the stream of directives. This problem is related to the lack of clarity between the merely parsed data structure and the fully resolved one. In particular, this means that the \"bean-doctor context\" debugging command often cannot provide useful context around a failing transaction. This really needs to be fixed, to improve debuggability. Document debugging tools. In general, I should write a better exposition of how to use the various transaction debugging tools; a lot of the questions on the mailing-list would disappear if users knew better how to leverage those. Interactive context in Emacs. If the performance allows it, we could build an Emacs mode which renders the context around a partially written transaction, including inventories of the affected accounts before and after the transaction, as well as interpolated values, to a different buffer updated interactively. This would make it much more fun to input data and provide immediate feedback about the newly inserted transaction. Documentation Improvements \uf0c1 Remove dependency on furius.ca. The current Google Docs based documentation links to other documents via a global redirect (the definition is found here ). While it does not happen often that my web server goes down (perhaps a few times per year), when it does it takes a few days to rectify the situation. That server is hosted graciously in the company of some friends of mine. Kirill has proved that it would be possible to replace all the links to redirects on github, that would look like this: beancount.github.io/ instead of furius.ca/beancount/doc/ . In order to do this, I'll need to run a script using the Docs API on all the Google Docs to change them automatically. Conclusion \uf0c1 There are other items in the TODO file . These are just the main, big issues that I think matter the most and I'd like to address them in a Vnext rewrite. Development branches will look like this: v2 : Current master will be branched to \"v2\", which will track the stable current version. That branch will build with both the current setup.py system and Bazel. Fixes will be implemented on that branch where possible, and merged to Vnext. master : Current master will become Vnext. Only the Bazel build will be supported on that branch. Any comments appreciated. Appendix \uf0c1 More core ideas for Vnext that came about during discussions after the fact. Customizable Booking \uf0c1 For transfer lots with cost basis\u2026 an idea would be to create a new kind of hook, one that is registered from a plugin, e.g. a callback of yours invoked by the booking code itself, and whose results applied to a transaction are immediately reflected on the state of the affected inventories. Maybe this is the right place to provide custom algorithms so that their impact is affecting the subsequent inventories correctly and immediately. Now, imagine generalizing this further to provide and implement all of the current booking mechanisms that are currently built in the core. Call this \"customizable booking.\" ( thread ). Ugly Little Things \uf0c1 print_entry() uses buffering that makes it impossible to use regular print() interspersed with the regular stdout without providing file= option. Fix this, make this regular instead, that's just annoying, just print to regular stdout. The default format for __str__ for inventories puts () around the rendering. When there's a single position, that looks like a negative number. That's dumb. Use {} instead, or something else. Add a flag to bean-check to make it run --auto plugins by default. This is great for imported files, which may not have a complete ledger to feed in. Incremental Booking/ Beancount Server / Emacs Companion \uf0c1 In order to make recomputation fast, the idea of creating a standalone \"Beancount server\" starts to make sense. The expensive part of the Beancout calculations on a large file is the booking and interpolation. The key to making things fast is thus to keep all the original unprocessed transactions in memory along with the booked and interpolated ones, and on a change, reparse the modified files and scan all the transactions, updating only the ones whose accounts have been affected. This could be problematic in theory: some plugins may rely on non-local effects in a way that affects what they output. I believe in practice it would work 99% of the time. But I think it may be worth a prototype. On the other hand, Vnext may turn out to be fast enough recomputing everything from scratch every single time (my own file went from 4s -> 0.3ms for the parsing stage of the largest file), so maybe this is irrelevant overall. Such a server would be a perfect companion to a running Emacs. We could build an Emacs mode which communicates with the server. Tags & Links Merge with MetaData \uf0c1 TODO(blais): Add colon syntax","title":"Goals & Design"},{"location":"beancount_v3.html#beancount-vnext-goals-design","text":"Martin Blais , July 2020 http://furius.ca/beancount/doc/Vnext","title":"Beancount Vnext: Goals & Design"},{"location":"beancount_v3.html#motivation","text":"It's time to give Beancount a refresh and to put down a concrete plan for what the next iteration of it ought to be. I've had these thoughts in the back of my mind for a long while\u2014at least a year\u2014and I'm putting these in writing partly to share a vision of what the product we all use to organize our finances can become, partly to solicit feedback, and partly to organize my thoughts and to prioritize well on the stuff that matters. Current status. The current state of Beancount is that development has been static for a while now, for a number of reasons. The software is in a state that's far from perfect (and I'll be enumerating the main problems in this document) but I've been resisting making too many changes in order to provide myself and others a really stable base to work from. More importantly, while I used to be able to spend a significant amount of weekend time on its development, life changes and a focus on my career of late has made it difficult for me to justify or find the extra time (it has been 10 years after all). A multitude of ideas have aged to this TODO file but it's too detailed to grok and a bit of a dump, this document should be more useful. Why rewrites happen. When I wrote version 2 of Beancount (a full rewrite of the first version), it was because of a confluence of ideas for improving my first draft; I resisted for a while, but eventually it made so much sense to me that it became simply impossible not to write it. Many of the ideas driving the redesign at the time are still axioms in today's design: removing order dependence, normalizing the syntax to be well-defined with a BNF grammar, converting custom processing to a sequence of plugins off of a simple stream of directives, the current design of booking selection and how cost basis works, and all the directives beyond \"Transaction\". These ideas largely shape what a lot of people like about using Beancount today. Goals. Now is the time for yet another wave of evolution for Beancount, and similarly, a set of new ideas I'm going to lay down in this document form as potent a change as the v1 to v2 transition. The vision I have for Vnext will simplify Beancount, by factoring into simpler, more isolated, more reusable, better defined parts, and not merely by adding new features on top of what's there. In many ways, Vnext will be a distillation of the current system. It will also make space to finally implement some of the core features most often desired by users. And those changes will enhance some organizational aspects: allow for more contributions, and also trim down the part that I'm handling myself to less code, so I can more effectively focus on just the core features.","title":"Motivation"},{"location":"beancount_v3.html#current-problems","text":"","title":"Current Problems"},{"location":"beancount_v3.html#performance","text":"My personal ledger, and I know that the ledgers of many users, are simply too large to process instantly. My current file takes 6 seconds on the souped-up NUC I use for a desktop at home\u2014but that's just too long. I'm really quite attached to the idea of processing the entire set of inputs every time, instead of forcing users to cut-up their ledgers into multiple files with \"closing\" transitions at arbitrary points in the year, but I really do want that \"instant\" feeling that you get when you run two-letter UNIX programs, that it runs in well under half a second . It makes it a lot more interactive and fun to use. C++ rewrite. One of the reasons for the slow performance right now is the fact that Beancount is implemented in Python, even at the level of the parser (C code calling back into a Python driver). An obvious solution is to rewrite the core of the software in a language closer to the metal, and that will be C++. I'm selecting C++ for its control and because the current slate of tools around it is mature and widespread enough that it should be easy for most to build without too many problems, and I can leverage C libraries that I will need. Using a functional language could have been fun but many of the libraries I want simply would not be available or it would be too difficult for mere mortals to build. Simple, portable C++. It's important to mention that the C++ code I have in mind is not in the style of template-heavy modern C++ code you'd find in something like Boost. Rather, it's a lot more like the conservative \"almost C without exceptions\" subset of C++ that Google uses , with a base on Abseil-Cpp (for example and flavor, see tips ). The reasons for this are stability and portability, and while this rewrite is for faster performance, I believe that it will not be necessary to pull template tricks to make it run fast enough; just a straightforward port to avoid the Python runtime will likely be sufficient. Above all I want to keep the new code simple and \"functional-ish\" as much as possible (no classes if I can avoid it), relying on a trusted set of stable dependencies , built hermetically using the Bazel build tool. Python API. It's also important that the Python API remains for plugins and scripts, and that the full suite of unit tests be carried over to the newer version of the code. After all, the ability to write custom scripts using all that personal finance data is one of the most attractive features of the text-based approach. Code beyond the new core implementation will remain in Python, and existing code built on top of the Python API should be very easily portable to Vnext. This can be achieved by exposing the directives with wrappers written in pybind11. Other languages. The resolved output of the Beancount core will be a stream of protocol buffer objects, so processing from other languages (e.g., Go) will have first-class support. Processing model. An additional algorithmic improvement to performance, should it be necessary, would be to define plugins processing in terms of iterator functions that cascade and interleave the processing of the directive stream without making entirely disjoint passes over the full list of directives. While the freedom to have each plugin process all the directives on their own has been instrumental in keeping the system free of synchronized application state and has allowed me to isolate behavior of the plugins from each other, there are opportunities to join together multiple transformations in a single pass. Fewer passes = faster.","title":"Performance"},{"location":"beancount_v3.html#intermediate-parsed-data-vs-final-list-of-directives","text":"In Beancount v2, I was never too careful to clearly distinguish between The list of directives coming out of the parser , missing interpolation and booking, using position.CostSpec instead of position.Cost on each lot, and The resolved and booked list of directives with booking algorithms applied to select matching lots, and interpolated values filled in, as well as transformations having been applied by the various plugins. These two lists of directives are really quite distinct in purpose, though they share many common data structures, and for the most part, the first list appears mostly in the parser module. There have been cases where it was confusing, even to me, which of the lists I was manipulating. Part of the reason is due to how I'm using mostly the same Python data structures for both, that allow me to bend the rules on typing. Perhaps more importantly is that because plugins run after booking and interpolation, and are required to put out fully interpolated and booked transactions, a plugin that wants to extend transactions that would run as invalid in the input syntax is difficult. See #541 for an example. The next iteration will see both the intermediate parser production and final resolved list of directives implemented as protocol buffer messages, with strictly distinct data types. This will replace beancount.core.data . The distinction between these two streams will be made very clear, and I will try to hide the former as much as possible. The goal is to avoid plugin writers to ever even see the intermediate list. It should become a hidden detail of the implementation of the core. Furthermore, there may be two types of plugins: a plugin that runs on the uninterpolated, unbooked output of the parser, and a plugin that runs on the resolved and booked stream. This would allow more creative use of partial input that might be invalid under the limitations of interpolation and booking. Updates: We could convert the plugin system to one that runs at booking/interpolation time. We should attempt to make the booking/interpolation atomic, in that one could write a Python loop with an accumulator and invoke it independently, so that in theory, booking could be used in the importers (like I do for Ameritrade).","title":"Intermediate Parsed Data vs. Final List of Directives"},{"location":"beancount_v3.html#make-rewriting-the-input-first-class","text":"Added in Dec 2020 after comments and #586 . A number of frequently asked questions have to do with how to process the input data itself. Usually, a new user will attempt to load the contents of the ledger, modify the data structures, and print to update their file, not realizing that the printer includes all the interpolations, booking data, and modifications from plugins, so this cannot work. However, since we're rewriting the parser and ensuring a clean separation between intermediate ASI-like data and processed and finalized directives, we can implement a special printer for the AST intermediate data, so that users could run just the parser, modify the intermediate directives, and print them back out, perhaps losing just some of the formatting and whitespace. This formatting loss can be leveraged to reimplement bean-format more naturally: the output of that printer should always be formatted neatly. This would avoid users having to write ad-hoc parsers on their input file, sed-like conversions, and so on. They could do it properly by modifying the data structure instead. What's more, in order for this to work accurately, we'd have to delay processing of the arithmetic operations post-parsing, so that we can render them back out. This offers another advantage: if we process the calculations after parsing, we can afford to provide an option to let the user specify the precision configuration to use for mpdecimal. I really like that idea, because it avoids hard-coding calculation precision and better defines the outcome of these options, potentially opening the door to a more rational way to remove extra digits that often get rendered out. Finally, if a nice library function can be made to process transactions in-place and output them back out, preserving all comments around them, this can become another way\u2014perhaps the preferential way\u2014for us to clean payees and somesuch. At the moment, the solution is to write a plugin that will clean up the data, but the input file remains a bit of a mess. Making it easy to automatically clean up the input file is an appealing alternative and potentially will add an important new dimension to the Beancount workflow. I want to make all changes necessary to make this possible and well supported (I'm imagining my ledger file all cleaned up right now and it's appealing). I think it's not very much work, it involves: Storing begin/end line information on everything. Adding AST constructs for representing arithmetic calculations. Adding comments parsing to the renderer. Implementing a new renderer that can reproduce the AST, including handling missing data. Implementing a library to make modification of a file in-place as easy as writing plugins, while preserving all non-directive data in the file as is.","title":"Make Rewriting the Input First Class"},{"location":"beancount_v3.html#contributions","text":"For most of the development of Beancount, I've been pretty reluctant to accept contributions. It has been a closely held pet project of mine since it has so much impact on my personal financial arrangements and I dread unplanned breakage. The main reservations I've had over contributions are two-fold: Not enough testing. Proposed changes that did not include enough testing, or none at all, sometimes even the kind testing that would prevent basic breakage. When I'd accept some proposals and commit to writing the tests myself it could sometimes take me down the rabbit hole for hours (if not days). This wasn't practical. Cascading design impact. Some of the proposals did not take into account broader design considerations that would affect other parts of the code, which I may not have communicated or documented well. I've had to reject some ideas in the interest of keeping things coherent and \"tight.\" Part of this is my fault: putting a high bar on contributions hasn't allowed potential contributors to acquire enough context and familiarity with the codebase to make changes compatible with its design. Allowing more contributions. Starting in Vnext I'd like to restructure the project so that more people are able to get involved directly, and to closely work 1-on-1 with some contributors on particular features. Clearly Beancount benefits from direct input from more people. The recent move to GitHub only compounds the urgency for making that easier. To this effect, I'd like to implement a few strategies: Break down the code in parts. This is a natural evolution that has been seen in many other projects; I'm no Linus nor Guido, but like their respective projects, as they grew in popularity, the original authors narrowed their focus on the core part and let other people expand on adjacent but also important functionality. I'd like to focus more of my time only on core functionality that will impact things like support for settlement dates, currency accounts, split transactions, trade reporting, etc. Letting other people deal with adding or updating price sources, making improvements to the ingestion framework, and making beautiful renderings and presentations of the data would be ideal. In time, I may eventually break down these libraries to separate repositories with looser contribution guidelines and/or add ACLs for others to push directly to those repos. Acquire \"lieutenants.\" I need to leave more space for trusted and frequent contributors to chip in more liberally. For example, Martin Michlmayr now has direct edit access to most documents and has been making numerous helpful contributions and updates to the docs. Kirill Goncharov's conversion of the documentation out of Google Docs is simply beautiful. RedStreet and many others are regularly pitching in answers on the mailing-list. Stefano Zacchiroli and Martin have built a standalone conversion tool from Ledger. Daniele Nicolodi is proposing some low-level changes to the scanner and parser. And of course, Dominik Aumayr and Jakob Schnitzer continue developing the Fava project adjacent to Beancount. There are many more people, there is a slowly-but-surely growing list of familiar recurring names. The question in my mind is: Is there a way to communicate with regular faces so that we're aligned in terms of design and can coordinate our efforts in the same direction? Does the newly acquired familiarity with video-conference meetings (thanks to the coronavirus crisis) afford us a new channel for coordination that wasn't there before? The Python community has thrived in no small part due to the annual face-to-face interactions between its participants and a number of core developers being in the same place for some time. Can we achieve the same thing online, as we are a smaller community? If so, better coordination may make it easier to accept proposed changes. I wonder if I should propose a monthly \"team\" meeting. 20% projects. I should provide a list of \"20% projects\" that are well aligned with the direction of the project for others to take up if they want to, and add profuse guidance and detail of downstream side-effects from those proposed features. The idea is to make it possible for newcomers to contribute changes that are likely to fit well and be easily integrated and accepted on the codebase.: Proposals. Beancount's equivalents of Python's \" PEPs \" are essentially the Google Docs proposal documents I started from threads where many others comment and add suggestions. A central list of those should be shared to a folder, identified as such, and allow others to write similar proposals. Maybe a little more structure and discipline around those would be useful.","title":"Contributions"},{"location":"beancount_v3.html#restructuring-the-code","text":"At the very coarse level, the code restructuring for Vnext looks like this: C++ core, parser, and built-in plugins. The Beancount core, parser, booking algorithm and plugins get rewritten in simple C++, outputting its parsed and booked contents as a stream of protobuf objects. Query engine. Beancount query/SQL gets forked to a separate project operating on arbitrary data schemas, with a much broader scope than Beancount. See section below. The rest. Most of the rest gets cut up into separate projects or at least, at first, in a distinct place within the repository (until the core is rewritten). Note that because the core outputs the stream of directives as proto objects, any language supported by protobufs should be able to read those. This extends the reach of Beancount. Here's a simplified diagram showing how this might look: Here is a detailed breakdown of the various parts of the codebase today and what I think will happen to them: Core. This is the part of Beancount's code which will get rewritten in C++ and output a sequence of messages to a stream of directives. I'll continue keeping a tight focus on that part with a conservative eye toward stability, but in Vnext will be adding desired new capabilities that have been lacking so far as described in the next section of this document. The core will include the following packages: beancount/core beancount/ops beancount/parser beancount/utils beancount/loader.py beancount/plugins (some, see below) beancount/utils (most) Query. The query language will be factored out into a completely separate repo with a broader application domain (and hooks for customizing for Beancount). I suspect that over time that project will acquire a much broader range of contributors, many of which will not even be Beancount users. This includes the code from these packages: beancount/query beancount/tools Prices. This is a simple library and tool that helps users fetch prices from external sources. This should definitely move to another repo and I'd welcome a new owner building a competing solution. People are sending me patches for new price sources and I have too little time to maintain them over time, as the upstream sources change or even disappear. This requires very little from Beancount itself (in theory you could just print() the directives for output, without even loading library code) but I think the Beancount core should include and functions to enumerate a list of required date/instrument pairs at a particular date from a given ledger (and I'm happy to support that). Note that the internal price database core will remain in the core, because it's needed there. The affected packages are: beancount/prices Improvements should be made to this library after it moves out of the Beancount repository: we should isolate the Beancount code to just a few modules, and turn the scope of this project to something larger than Beancount: it's three things, really: a) an up-to-date Python library of price fetchers with unit tests and maintained by the community (i.e., when sources break, we update the library) and a common API interface (needs to be improved from what's there TBH, the API should support fetching time series in a single call); b) an accompanying command-line tool (currency \"bean-price\") for fetching those prices from the command-line. This requires the specification of \"a price in a particular currency from a particular source\" as a string. I'd like to improve that spec to make the USD: prefix optional, and maybe eliminate the chain of prices in the spec, which hasn't found much use in practice and move that upstream. c) Make the interfaces to fetch ledger-related information (e.g., list of missing/required prices and lists of instruments) onto modules: beancount v2, beancount Vnext, ledger, hledger, and rendering output formats to any of these. In other words, this library should be able to fetch prices even if Beancount isn't installed. To turn this project into something that can run independent of beancount. Ingest. The importers library will probably move to another repo and eventually could even find another owner. I think the most interesting part of it has been the establishment of clear phases: the identify, extract and file tasks, and a regression testing framework which works on real input files checking against expected converted outputs, which has worked well to minimize the pain of upgrading importers when they break (as they do break regularly, is a SNAFU). In the past I've had to pull some tricks to make command-line tools provided by the project support an input configuration as Python code but also possible to integrate in a script; I will remove the generic programs and users will be required to turn their configuration itself into a script that will just provide subcommands when run; the change will be very easy for existing users: it will require only a single line-of-code at the bottom of their existing files. The Bazel build may add some minor difficulties in loading a Python extension module built from within a Bazel workspace from an otherwise machine-wide Python installation, but I'm confident we'll figure it out. I'd also be happy for someone else to eventually take ownership of this framework, as long as the basic functionality and API remains stable. The example csv and ofx importers should be removed from it and live in their own repos, perhaps: ofx. the OFX importer should be replaced by something using ofxtools (the one I built is pretty bad), and the CSV importer really needs a thorough rewrite with lots of unit testing for the large list of options it now supports (these tests are sorely missing). csv. Furthermore, I think the CSV importer could be enhanced to be smarter and more flexible, to automatically detect from the column headers and inferred data types in the files which column should convert into which field. I'm not going to do that (I don't have time). Someone with the urge to make the ultimate automatic CSV parser ought to create a separate repository for that. The affected packages are: beancount/ingest : could eventually move to another repo. beancount/ingest/importers: someone could revive a repository of importer implementations, like what LedgerHub once aimed to become, and swallow those codes. See this document for details on what's to happen with the ingestion code. Custom reports and bean-web should be removed: the underlying bottle library seems unmaintained at this point, Fava subsumes bean-web, and I never liked the custom reports code anyway (they're a pain to modify). I never use them myself anymore (other than through bean-web). I really think it's possible to replace those with filters on top enhanced SQL query results. The conversion to Ledger and HLedger from Beancount now seems largely useless, I'm not sure anyone's using those. I'll probably move these to another repo, where they would eventually rot, or if someone cares, adopt them and maintain or evolve them. beancount/web : will be deleted or moved to another repo. beancount/reports : will be deleted or moved to another repo. Note that this includes deprecating beancount/scripts/bake , which depends heavily on bean-web. I have no substitute for bean-bake, but I think I'd like to eventually build something better, a tool that would directly render a user-provided list of specific SQL queries to PDF files and collate them, something you can print. Jupyter notebook support. A replacement for the lightweight interface bean-web used to provide could be Jupyter Notebook integration of the query engine, so that users can run SQL queries from cells and have them rendered as tables, or perhaps a super light web application which only supports rendering general SQL queries to tables. Built-in Plugins. Beancount provides a list of internal plugins under beancount/plugins . It's not indicated clearly, but there have evolved two groups of plugins in there: stable plugins used by the core, and experimental plugins showcasing ideas, which are often incomplete implementations of something that was proposed from a thread on the mailing-list. The former group will be ported to C++, and the latter group should probably move to another location with much looser acceptance constraints. First, there are \"meta-plugins\" which only include groups of other plugins: Only one of those should remain, and maybe be enabled by default (making Beancount pedantic by default): auto pedantic The following plugins should remain in the core and be ported to C++: auto_accounts check_closing check_commodity close_tree commodity_attr check_average_cost coherent_cost currency_accounts implicit_prices leafonly noduplicates nounused onecommodity sellgains unique_prices The following are the experimental implementations of ideas that should move to a dedicated repo where other people can chip in other plugin implementations: book_conversions divert_expenses exclude_tag fill_account fix_payees forecast ira_contribs mark_unverified merge_meta split_expenses tag_pending unrealized Because it's a really common occurrence, the new transfer_lots plugin should be part of the built-in ones. Projects. The beancount/projects directory contains the export script and a project to produce data for a will. The will script will be moved outside the core of Beancount, I'm not sure anyone's using that. Maybe the new external plugins repo could include that script and other scripts I shared under /experimental. The export script should be grouped together with beancount/scripts/sql and other methods to send / share data outside of a ledger; these could remain in the core (I'm using the export script regularly to sync my aggregates and stock exposure to a Google Sheets doc which reflects intraday changes). Scripts. Some of the scripts are completely unrelated to Beancount, they are companions. The scrape validator. The sheets upload. The treeify tool. These should be moved elsewhere. One of the advantages of having all the code in the same repo is that it makes it possible to synchronize API changes across the entire codebase with a single commit. As such, I may keep some of the codes in the same repo until the new C++ core has stabilized, and properly separate them only when Vnext releases.","title":"Restructuring the Code"},{"location":"beancount_v3.html#universal-lightweight-query-engine-ulque","text":"The SQL query engine for Beancount was initially a prototype but has grown to become the main way to get data out of it. I've been pretty liberal about adding functionality to it when needed and it's time to clean this up and consider a more polished solution. In Vnext, the query/SQL code gets eventually forked to a separate project (and repo) operating on arbitrary data schemas (via protobufs as a common description for various sources of data) and has support for Beancount integration. Imagine if you could automatically infer a schema from an arbitrary CSV file, and run operations on it, either as a Python library function or as a standalone tool. Furthermore, this tool will support sources and/or sinks to/from Google Sheets, XLS spreadsheets, containers of binary streams of serialized protos, tables from HTML web pages, PDF files, directories of files, and many more. This is going to be a data analysis tool with a scope closer to that of the Pandas library rather than an accounting-focused project, but also a universal converter tool, that will include the functionality of the upload-to-sheets script (which will get removed). One of the lessons from the SQL query engine in Beancount is that with just a little bit of post-processing (such as treeify ), we can do most of the operations in Beancount (journals, balance sheet & income statements) as queries with filters and aggregations. The tool will be made extensible in the ways required to add some of the idiosyncrasies required by Beancount, which are: Native support for a Decimal type . The addition of custom types for aggregators with the semantics of beancount.core.Inventory/Position/Amount . The ability to automatically generate a dynamic column rendering a line-by-line aggregation of another column (or set thereof), that is, a \"balance\" column . The ability to render a \" bottom line \" of aggregates at the end of the results table. Functions for splitting of aggregated columns , for amounts and inventories into multiple columns (e..g, \"123.00 USD\" becomes two columns: (123.00, \"USD\") to be processable in spreadsheets, and also for splitting debits and credits to their own columns. In particular, printing multiple lots accumulated in an account should be made natural from the SQL query, replacing the \"flatten\" feature by a more standard splitting off an array type. Moreover, broadening the focus with a new project definition will make a change to testing it thoroughly (the current one is still in a bit of a prototype stage and does not have nearly the amount of required tests), and also include data type validation (no more exceptions at runtime), by implementing a typed SQL translator. I'll document this elsewhere. This is a much bigger project, but I suspect with the broader scope, it will be easier to test and take on a life of its own. I'm preparing a design doc on this.","title":"Universal Lightweight Query Engine (ulque)"},{"location":"beancount_v3.html#api-rework","text":"I write a lot of custom scripts, and there are a number of things that bother me about today's Beancount API, which I want to radically improve: Consolidate symbols under \"bn\". The internal API calls for importing the symbols from each package separately, but now that I'll have split off the ingestion and reporting code, all of the public API, or at least the majority of the commonly used objects in the core should be available from a single package, a bit like numpy: import beancount as bn \u2026 bn.Inventory(...) bn.Amount(...) bn.Transaction(...) # etc. I'd like for \"bn\" to become the de-facto two-letter import on top of which we write all the scripts. Default values in constructors. The namedtuple containers are mighty fine, but their constructors never had optional arguments, and it's always a bit of a dance to create those containers with a ton of \"None\" options. I never liked it. We'll make this tidy in the next iteration. No API documentation. While there is a substantial amount of documentation around the project, there is no documentation showing people how to use the Python API, e.g. how to accumulate balances, how to create and use a realization tree, how to pull information out of an accumulated inventory object, etc. I think that documenting some of the most common operations will go a long way towards empowering people to make the most out of Beancount. Some of these operations include: Accumulating lots of an inventory and printing them. Converting to market value, and making corresponding account adjustments. \u2026. add more \u2026 Exposed, usable booking. Booking will be a simple loop that can be invoked from Python with an entry and some accumulated state. Moreover, the Inventory object should begin to implement some of the lower-level operations required for booking, such that iterating over a set of postings and doing e.g., average booking, can be done via method calls on an Inventory object. Inventory should take a more prominent place in the API. Data types. Well defined data types should be provided for all objects to make liberal use of the typing module over all new code. Maybe create a module called \"bn.types\" but they should be available directly from \"bn.*\" so that there is a single short-named import. Terminology. I'd like to stop using \"entries\" and consolidate over the name \"directives\" in Vnext. Realization. I've been using a collections.defaultdict(Inventory) and a \"realization\" interchangeably. Both of these are mappings from an account name (or some other key) to an Inventory state object. I'd like to unify both of these constructs into the realization and make it into a commonly used object, with some helper methods.","title":"API Rework"},{"location":"beancount_v3.html#parser-rewrite","text":"Since we will now depend on C++, the parser will get to be rewritten. Worry not: the input syntax will remain the same or at least compatible with the existing v2 parser. What will change is: Unicode UTF-8 support. The lexer will get rewritten with RE/flex instead of GNU flex. This scanner generator supports Unicode natively and any of the input tokens will support UTF-8 syntax. This should include account names, an oft-requested feature. Flags. The current scanner limits our ability to support any flag and supports only a small list of them. I think the list has proved sufficient for use, but since I'll be putting some work into a new scanner I'm hoping to clean up that story and support a broader, better defined subset of single-letter flags for transactions. Time. The parser will parse and provide a time field, in addition to the date. The time may be used as an extra key in sorting directives. The details for this are yet to be determined, but this is requested often enough at the very minimum the parser will output it as metadata, and at best, it may become a first-class feature. Caching. The pickle cache will be removed. Until very recently , there weren't great options for disabling it (env vars) and I'd rather remove the only two environment variables that Beancount honors as a side-effect. Since the C++ code should be fast enough, hopefully a cache will not be needed. Tags & links. In practice, those two features occupy a very similar role as that of metadata (used to filter transactions). I'm contemplating unseating the special place taken by tags and links in the favor of turning those into metadata; the input syntax would not be removed, but instead the values would be merged into the metadata fields. I'm not 100% sure yet about doing this and open for discussion. Furthermore, the parser should be made to accept #tag and ^link where metadata is declared today, which would be convenient syntax. Finally, users have expressed a desire for tags on postings. We should contemplate that. Plugins configuration as protos. The options for the various plugins have been loosely defined as eval'ed Python code. This is pretty loose and doesn't provide a great opportunity for plugins to do validation nor document their expected inputs. I'd like to formalize plugin configuration syntax a bit, by supporting text-formatted protos in the input syntax (for a message type which would be provided by the plugins themselves). Parser in C++. The parser will be rewritten in C++. In the process of writing Vnext, I'll try to maintain a single grammar for both for as long as possible by calling out to a C++ driver interface, which will have two distinct implementations: one for the V2 version calling into Python, and one for the Vnext parser generating protos. In the process I may be porting the lexer and grammar Python implementation to C, as discussed in this ticket . Better includes. Current includes fail to recognize options that aren't in the top-level file. This caused many surprises in the past and should be fixed. At the minimum, an error should be raised.","title":"Parser Rewrite"},{"location":"beancount_v3.html#code-quality-improvements","text":"Rename \"augmentation\" and \"reduction\" to \"opening\" and \"closing\" everywhere. This is just more common terminology and will be more familiar and understandable to people outside of our context. Type annotations. The use of mypy or pytype with type annotations in Python 3 is by now a very common sight, and works quite well. As part of Vnext, all of the core libraries will be modified to include type annotations and the build should be running pytype automatically. I'll need to add this to our Bazel rules (Google doesn't currently provide external support for this). While doing this, I may relax some of the Args/Returns documentation convention, because in many cases (but not all) the type annotations are sufficient to get a good interpretation of a function's API. PyLint in build. Similarly, the linter should be run as an integral part of the build. I'd like to find a way to selectively and explicitly have to disable it during development, but otherwise be set up such that lint errors would be equivalent to build failures. Flexible constructors for Python API. The types generated by collections.namedtuple() or typing.NamedTuple don't offer flexible constructors with named parameters. I think all codes that create transaction objects today would benefit from having constructors with default values, and I'll be providing those to create corresponding proto objects.","title":"Code Quality Improvements"},{"location":"beancount_v3.html#tolerances-precision","text":"The story around how precision and tolerances are dealt with hasn't been great, for two reasons: Explicit tolerance option. I've tried to design the tolerance (used for balancing transactions) to be automatic and automatically inferred from statistics from the numbers in the input. The results aren't great. In Vnext I aim to provide an explicit option for setting the tolerance per currency. Precision. There are various places where numbers get rendered in v2: the reports code, the SQL query, and debugging scripts, and the way precision is set hasn't been used consistently. The precision also needs to be explicitly settable by the user. Rounding. There is another quantity that's used during interpolation: the precision used to round calculated numbers. Moreover, there is a need to distinguish between the precision and tolerances for numbers when used as prices vs. when used as units (see here ). One way is to store the display context per currency PAIR, not per currency itself. The distinction between these quantities hasn't been documented well; I'll keep in mind to clearly annotate those codes in Vnext and add suitable docs for this. Mostly the precision will be a rendering concern and a quantity that will be relevant for the new universal SQL query tool. Some prior design documentation exists here .","title":"Tolerances & Precision"},{"location":"beancount_v3.html#core-improvements","text":"Some desiderata of new features are discussed below. These are all relevant to the core. Note that the changes should not interfere with current usage much, if at all. I expect that v2 users will be largely unaffected and won't have to change their ledger files.","title":"Core Improvements"},{"location":"beancount_v3.html#booking-rules-redesign","text":"Main document One of the current problems with booking is that entering an augmenting leg and a reducing leg have to be different by nature. The augmentation leg has to provide the cost basis via {...} syntax, and the reducing leg has to enter the price in the annotation and not in the cost basis. For example: 2021-02-24 * \"BOT +1 /NQH21:XCME 1/20 FEB 21 (EOM) /QNEG21:XCME 13100 CALL @143.75\" Assets:US:Ameritrade:Futures:Options 1 QNEG21C13100 {2875.00 USD} contract: 143.75 USD ... 2021-02-24 * \"SOLD -1 /NQH21:XCME 1/20 FEB 21 (EOM) /QNEG21:XCME 13100 CALL @149.00\" Assets:US:Ameritrade:Futures:Options -1 QNEG21C13100 {} @ 2980.00 USD contract: 149.00 USD ... Notice how the selling transaction has to be written down differently from the perspective of the user. The thing is, this makes it difficult from the perspective of the importer writer. It also ties the required syntax with the state of the inventory it's applied to, as it assumes something about this inventory. Moreover, this makes it difficult to write an importer that would handle a crossing of the absolute position, like this: 2021-02-19 * \"BOT +1 /NQH21:XCME @13593.00\" Assets:US:Ameritrade:Futures:Contracts 1 NQH21 {271860.00 USD} contract: 13593.00 USD Assets:US:Ameritrade:Futures:Margin -271860.00 USD Expenses:Financial:Commissions 2.25 USD Expenses:Financial:Fees 1.25 USD Assets:US:Ameritrade:Futures:Cash -3.50 USD 2021-02-19 * \"SOLD -2 /NQH21:XCME @13590.75\" Assets:US:Ameritrade:Futures:Contracts -2 NQH21 {271815.00 USD} contract: 13590.75 USD Assets:US:Ameritrade:Futures:Margin 543630.00 USD Income:US:Ameritrade:Futures:PnL 45.00 USD Expenses:Financial:Commissions 4.50 USD Expenses:Financial:Fees 2.50 USD Assets:US:Ameritrade:Futures:Cash -52.00 USD The issue here is that we're crossing the flat line, in other words, we go from long one to short one. There are only two ways to do that properly right now: Disable booking and use the cost only, as per above. This is not great \u2014 booking is terribly useful. Track the position in your importer and separate the reducing and augmenting legs: 2021-02-19 * \"SOLD -2 /NQH21:XCME @13590.75\" Assets:US:Ameritrade:Futures:Contracts -1 NQH21 {} @ 271815.00 USD Assets:US:Ameritrade:Futures:Contracts -1 NQH21 {271815.00 USD} Both solutions aren't great. So I came up with something new: a complete reevaluation of how the syntax is to be interpreted. In fact, it's a simplification. What we can do is the following: use only the price annotation syntax for both augmentation and reduction and currency conversions, with a new booking rule \u2014 Match lots without cost basis in priority. If the lots have no cost basis, the weight of this posting is simply the converted amount, as before. If a match has been made against a lot with cost basis, the weight of this posting is that implied by the matched lots. Make the {...} used solely for disambiguating lots to match, and nothing else. If you have unambiguous matches, or a flexible booking strategy, e.g. FIFO, you'd pretty much never have to use the cost matching reduction. With this, the futures transaction above would simply use the @ price annotation syntax for both transactions. It would Make importers substantially simpler to write Supports futures naturally Be backward compatible with existing inputs for both currency conversions and investments. It is also more generally consistent and friendlier to Ledger users, without sacrificing any of the tighter constraints Beancount provides. I think it's even simpler to think about. Furthermore, this: Assets:US:Ameritrade:Futures:Contracts -1 NQH21 {} @ 271815.00 USD would be interpreted as \"match this lot, but only those with a cost basis attached to them.\" One question that remains is to decide whether an augmentation \u2014 now written down simply with @ price annotation \u2014 would store the cost basis in the inventory or not. I think we could make this determination per-commodity, or per-account. This would impose a new constraint: a commodity (or \"in an account\") would always be stored with cost basis, or not.","title":"Booking Rules Redesign"},{"location":"beancount_v3.html#posting-vs-settlement-dates","text":"When you import a transaction between multiple accounts within a single ledger, e.g. a credit card payment from one's checking account, the dates at which the transaction posts in each account may differ. One side is called the \"transaction date\" or \"posting date\" and the other side the \"settlement date.\" Where the money lives in between is somewhere in limbo (well in practice there is no money at all, just differences in accounting between institutions, things are never reflected instantly). One of the major shortcomings of the current core code is that the ability to insert a single transaction with postings at different dates is missing. Users are recommended to select a single date and fudge the other one. Some prior discussion on this topic exists here . Unfortunately, this method makes it impossible to represent the precise posting history on at least one of the two accounts. A good solution needs to be provided in Vnext, because this is a very common problem and I'd like to provide a system that allows you to precisely mirror your actual account history. The automatic insertion of transfer accounts to hold the commodities can be implemented as a feature, and it should live in the core. One possible idea would be to allow optional posting dates, like this: 2020-01-19 * \"ONLINE PAYMENT - THANK YOU\" \"\" Assets:US:BofA:Checking -2397.72 USD 2020-01-21 Liabilities:US:Amex:BlueCash 2397.72 USD which would result in two transactions behind the scenes, like this: 2020-01-19 * \"ONLINE PAYMENT - THANK YOU\" \"\" Assets:US:BofA:Checking -2397.72 USD Equity:Transfer 2020-01-21 * \"ONLINE PAYMENT - THANK YOU\" \"\" Liabilities:US:Amex:BlueCash 2397.72 USD Equity:Transfer The lack of symmetry here raises the question of whether we should allow a transaction without a date or not: * \"ONLINE PAYMENT - THANK YOU\" \"\" 2020-01-19 Assets:US:BofA:Checking -2397.72 USD 2020-01-21 Liabilities:US:Amex:BlueCash 2397.72 USD I think we can figure this out and the first solution is very doable. Input Split Transactions Some users like to organize their inputs in different files, or in different sections that strictly contain all of an account's transactions in order. This is related in spirit to the posting and settlement dates problem: at the moment the user is required to choose one of the two locations to insert their transaction. This should not be necessary. We should provide a mechanism that would allow users to insert the halves of a transaction into two different locations in their file, and a robust merging mechanism that would ensure that the two related transactions have been matched and merged (so that no unmerged half remains) and otherwise report errors clearly. The two halves could look like this: 2020-01-19 * \"ONLINE PAYMENT - THANK YOU\" \"\" Assets:US:BofA:Checking -2397.72 USD \u2026 2020-01-21 * \"AMEX EPAYMENT ACH PMT; DEBIT\" Liabilities:US:Amex:BlueCash 2397.72 USD The matching could be done via explicit insertion of special links, or by heuristics to match all such related transactions (perhaps declaring valid account pairs, thresholding on date differences and exactly matching amounts). When impossible to match, an error should be raised. Those merged transactions should be checked for balancing. Note how each of the transactions has a differing date; this would integrate with the transfer account solution proposed in the previous section. I haven't designed something yet, but this should be easy to implement and should be provided as a core feature, since it's so closely related to the input syntax.","title":"Posting vs. Settlement Dates"},{"location":"beancount_v3.html#currency-accounts-instead-of-a-single-conversion","text":"The current implementation of multiple currency transactions relies on a special \"conversion transaction\" that is automatically inserted at reporting time (when closing the year) to account for the sum total of imbalances between currencies. The goal of this transaction is to ensure that if you just sum up all the postings in the book, the result is purely an empty inventory (and not some residual amount of profit or loss incurred during currency exchange across different rates \u2014 note that we're talking only of the @price syntax, not investments). This is a bit of a kludge (the transaction itself does not balance, it converts to zero amounts of a fictional currency in order to keep itself quietly passing the balance test). What's more, its actual value is dependent on a subset of filtered transactions being summed up so it's a reporting-level construct, see here . There exists a method for dealing with multiple currencies without compromising on the hermeticity of individual transactions, described online, here . Using that method, you can filter any subset of transactions and summing them up will cleanly cancel out all lots. You don't need to insert any extra weight to fix up the balance. Also, you can explicitly book profits against the accrued gains in the currency accounts and zero them out and take advantage of this when you report them (and track them over time). The downside is that any currency conversion would see extra postings being inserted, etc. 2020-06-02 * \"Bought document camera\" Expenses:Work:Conference 59.98 EUR @ USD Liabilities:CreditCard -87.54 USD Equity:CurrencyAccounts:EUR -59.98 EUR Equity:CurrencyAccounts:USD 87.54 USD The problem is that it's a pain to use this method manually, it requires too much extra input. It's possible to have Beancount do that for us behind the scenes, completely automatically. I coded a proof-of-concept implementation here , but it's incomplete . In Vnext: The insertion of the kludgey conversions transactions should be removed. The currency accounts should become the norm. The fact that the two streams of directives will be very clearly separated should help, by distinguishing even more clearly between the parsing representation and the fully booked one, which will show these extra legs on transactions The prototype should be completed and issues fixed completely (not that much work involved).","title":"Currency Accounts instead of a Single Conversion"},{"location":"beancount_v3.html#strict-payees","text":"I'm not sure if this makes sense yet, but I'd like to clean up the mess that payee strings are today. Payees are free-form, and if the user does not take care to clean them up\u2014and I'm one of those who doesn't\u2014the memos from imported sources are messy. It could be interesting to create a new directive to declare payee names ahead of time and an optional model that would require payees to be found in the list of declared payee names. Payees would have to have open and close dates, dates which would define the valid duration of the relationship with that payee (thereby adding more error verification capability).","title":"Strict Payees"},{"location":"beancount_v3.html#price-inference-from-database","text":"Interpolation from price database. One of the oft-requested features is the ability to automatically interpolate prices from the internal price database history. I think that this should be doable unambiguously and deterministically and added to the interpolation algorithm. Price validation. Since a lot of the conversions at price (i.e., using \"@\") are inferred by leaving out one number, we should validate that the effective price is within some tolerance of a pre-existing price point near the date. This would provide yet another level of checking.","title":"Price Inference from Database"},{"location":"beancount_v3.html#quantizing-operators","text":"Another useful addition to the syntax would be operators that automatically quantize their result to a precision that depends on the particular target currency. For example, 1970-01-01 * \"coffee\" Expenses:Food:Net 2.13 / 1.19 EUR Expenses:Food:Taxes 2.13 / 1.19 * 0.19 EUR ; for example to calculate tax Assets:Cash That would become: 1970-01-01 * \"coffee\" Expenses:Food:Net 1.789915966386555 EUR Expenses:Food:Taxes 0.340084033613445 EUR Assets:Cash If instead an operator like this were provided, it would fix the issue: 1970-01-01 * \"coffee\" Expenses:Food:Net 2.13 /. 1.19 EUR Expenses:Food:Taxes (2.13 / 1.19 * 0.19). EUR Assets:Cash Or somesuch. Or maybe we'll want to add an option such that every evaluation of an arithmetic expression is automatically quantized as such.","title":"Quantizing Operators"},{"location":"beancount_v3.html#constraints-system-budgeting","text":"Beancount does not support budgeting constraints explicitly, but I think it would be possible to extend the balance assertion semantics to cover this. The current balance assertions check (a) a single commodity, and (b) that the amount is precisely equal to an expected one. Balance assertions should be extended to support inequalities, e.g., 2020-06-02 balance Liabilities:CreditCard > 1000.00 USD and perhaps we could check for the total inventory like this 2020-06-02 balanceall Assets:Cash 200.00 USD, 300.00 CAD I'd be curious to hear what would work best from users who do budgeting and design a minimalistic expression language to support that use case (though I'd try to keep it as simple as possible to avoid feature creep). Also, if the syntax is getting changed, adding a syntax that allows checking multiple currencies at once, and possibly a complete assertion that checks there aren't other commodities in the account could also make sense.","title":"Constraints System & Budgeting"},{"location":"beancount_v3.html#average-cost-booking","text":"Average cost booking has been discussed and a good solution sketched out a very long time ago. Vnext should sport that method natively; a lot of users want to have this feature for dealing with their tax-deferred accounts. It takes a bit of work to handle the precision of the various automated conversions right. The way it would work is by automatically merging all related lots of the same commodity on a reduction, and optionally on an augmentation. Some constraints may be required (e.g. only a single commodity in that account).","title":"Average Cost Booking"},{"location":"beancount_v3.html#trade-matching-reporting","text":"A few core tasks related to P/L and trading still need to be implemented. Trade list. A problem that I've really been wanting to solve for a very long time but never found the time for is to save crumbs from the booking process so that a correct list of trade pairs could be easily extracted from the list of directives. I wrote some notes here and here a long time ago. Essentially, for each booked reduction, insert a reference to the corresponding augmenting posting. I've prototyped this as metadata but it should be made something more official. A single linear scan can pick up these references, build a mapping and recover the (buy, sell) pairs to produce a table of trades. There's a precedent I once wrote in a plugin . Needless to say, producing a list of trades is a pretty basic function that Beancount does not provide out of the box today; it really should. Right now users write their own scripts. This needs to be supported out-of-the-box. Harmonize balance and gains validation. Checking that transactions balance and that income from gains balance with a transaction's prices (the sellgains plugin) are done in completely separate places. Those two codes occupy similar roles, and should be implemented next to each other. Commissions in P/L. Properly counting profits & losses by taking off the fraction of buying commission of an original lot and the selling commission into account is not possible at the moment. I think it could be done with a plugin that moves some of the (computed) income leg into a separate negative income account to do this properly for reporting purposes.","title":"Trade Matching & Reporting"},{"location":"beancount_v3.html#self-reductions","text":"Currently the application of reductions operates on the inventory preceding the transaction. This prevents the common case of self-reductions, and both I and some users have come across this problem before, e.g. this recent thread ( ticket ). This comes off as unintuitive to some users and ought to have a better solution than requiring splitting of transactions. Since we're rewriting the booking code entirely in Vnext, contemplate a new definition that would provide a well-defined behavior in this case. I remember from prior experiments attempting to implement this that it wasn't a trivial thing to define. Revisit. This would be a nice improvement.","title":"Self-Reductions"},{"location":"beancount_v3.html#stock-splits","text":"Some discussion and perhaps a strategy for handling stock splits should be devised in Vnext. Right now, Beancount ignores the issue. At the minimum this could be just adding the information to the price database. See this document for more details.","title":"Stock Splits"},{"location":"beancount_v3.html#multipliers","text":"Options have a standard contract size of 100. Futures have a contract size that depends on the particular instrument (e.g., /NQ with a multiplier of 20). I've been handling this for options by multiplying the units by 100, and for futures by multiplying the contract size by the per-contract multipliers (ditto for options on futures). I do this in the importers. For options, it works and it isn't too bad (e.g. positions of -300 instead of -3), but for futures, it's ugly. The result is not representative of the actual transaction. I'd like to add a per-currency multiplier, as well as a global dictionary of regexps-to-multiplier to apply, and for this to be applied everywhere consistently. One challenge is that everywhere there's a cost or price calculation, this has to be applied. In the current version, those are just multiplications so in many parts of the codebase these haven't been wrapped up in a function that could easily be modified. This needs to happen in a big rewrite \u2014 this is the opportunity to do this. Example here.","title":"Multipliers"},{"location":"beancount_v3.html#returns-calculations","text":"If you look at investment brokers out there, no one calculates returns correctly. Brokers provide one of two features: No cash transfers. Total value of account today vs. total value of account at some point in the past (i.e., account inception or beginning of the year). This isn't very useful because they never account for the addition or removal of cash to the account. For example, say you open an account with $100,000 and invest, and mid-year add another $20,000, say the original investments are now worth $95,000, the report would show a gain of $15,000, whereas you really incurred a loss. Better brokers like Vanguard will show a plot that includes two overlaid bars, one with cash added and profit overlaid, like this: Lack of interest or dividends. Other brokers will report P/L over time from the investments, but they fail to account for actual interest or dividends received (they only look at the price of the underlying) so that's not useful for bonds or for stocks with significant dividends, or when grouping them, they fail to account for the addition of new positions over time. Counterfactual performance. Finally, all of them fail to compare your actual annualized performance with that of a benchmark portfolio with equivalent cash infusions. For example, instead of your actual investments made, compare with the performance you would have obtained if you had invested in some standardized portfolio of investments over that particular time period, given the actual historical prices of those instruments. Ideally one should be able to define any alternative portfolio to compare against using their particular cash transfers. More fancy analyses aren't even contemplated, e.g., what would have been the impact of changing my rebalancing strategy (or actually implementing a more strict one)? There are well known methods for both time-based and value-based returns reporting. The right thing to do is to extract a time-series of cash flows and compute the annualized or IRR returns, or value-weights. I started this work at some point and ran against some difficulties and eventually removed it. The results remain here. I'd really love to build this, and eventually perhaps this could grow into its own project, with associated support functions in the Beancount core. This will possibly be a project of its own, but this requires similar support for enumerating instruments and price sources as that which is needed for fetching prices, as well as functions for isolating cash flows for specific subsets of accounts; these should probably live in the core. UPDATE September 2020: This has mostly been implemented. See this document for details.","title":"Returns Calculations"},{"location":"beancount_v3.html#unsigned-debits-and-credits","text":"A useful idea that's nearly trivial to implement is to allow users to input all positive unit numbers and to automatically flip the signs on input, and to output them all as positive numbers as well splitting them up between Debit and Credit columns. This would make Beancount's rendering a lot easier to grok for people with an existing background in accounting. This feature will introduce no complexity, easy to add for free. See TODO here .","title":"Unsigned Debits and Credits"},{"location":"beancount_v3.html#holdings","text":"One of the libraries I built at some point was this notion of a \"holding\", here . At the time I wasn't sure if that data would contain much more than what's in a posting, but as it turns out, representing holdings as just the posting is good enough, all you need is the market value, and compute the total values from the units is trivial. In fact, I've haven't been using that code for years, I've been using the export script instead, which writes out a table that gets uploaded to a Google Sheets doc. This proves to me aggregating the positions in an Inventory is plenty sufficient in practice, along with a mapping of latest market prices. I'm going to delete that code. It's only been used in the reports code anyway, which will be removed anyway, and in the experimental \" unrealized gains \" plug, which was only a proof-of-concept that convinced me booking such gains as transactions is not a good idea and which will get removed and live in a separate experimental repository anyway.","title":"Holdings"},{"location":"beancount_v3.html#tooling-for-debugging","text":"Context should recover erroneous transactions. One of the major annoyances of error recovery is that if a transaction involves some types of errors, the postings aren't produced in the stream of directives. This problem is related to the lack of clarity between the merely parsed data structure and the fully resolved one. In particular, this means that the \"bean-doctor context\" debugging command often cannot provide useful context around a failing transaction. This really needs to be fixed, to improve debuggability. Document debugging tools. In general, I should write a better exposition of how to use the various transaction debugging tools; a lot of the questions on the mailing-list would disappear if users knew better how to leverage those. Interactive context in Emacs. If the performance allows it, we could build an Emacs mode which renders the context around a partially written transaction, including inventories of the affected accounts before and after the transaction, as well as interpolated values, to a different buffer updated interactively. This would make it much more fun to input data and provide immediate feedback about the newly inserted transaction.","title":"Tooling for Debugging"},{"location":"beancount_v3.html#documentation-improvements","text":"Remove dependency on furius.ca. The current Google Docs based documentation links to other documents via a global redirect (the definition is found here ). While it does not happen often that my web server goes down (perhaps a few times per year), when it does it takes a few days to rectify the situation. That server is hosted graciously in the company of some friends of mine. Kirill has proved that it would be possible to replace all the links to redirects on github, that would look like this: beancount.github.io/ instead of furius.ca/beancount/doc/ . In order to do this, I'll need to run a script using the Docs API on all the Google Docs to change them automatically.","title":"Documentation Improvements"},{"location":"beancount_v3.html#conclusion","text":"There are other items in the TODO file . These are just the main, big issues that I think matter the most and I'd like to address them in a Vnext rewrite. Development branches will look like this: v2 : Current master will be branched to \"v2\", which will track the stable current version. That branch will build with both the current setup.py system and Bazel. Fixes will be implemented on that branch where possible, and merged to Vnext. master : Current master will become Vnext. Only the Bazel build will be supported on that branch. Any comments appreciated.","title":"Conclusion"},{"location":"beancount_v3.html#appendix","text":"More core ideas for Vnext that came about during discussions after the fact.","title":"Appendix"},{"location":"beancount_v3.html#customizable-booking","text":"For transfer lots with cost basis\u2026 an idea would be to create a new kind of hook, one that is registered from a plugin, e.g. a callback of yours invoked by the booking code itself, and whose results applied to a transaction are immediately reflected on the state of the affected inventories. Maybe this is the right place to provide custom algorithms so that their impact is affecting the subsequent inventories correctly and immediately. Now, imagine generalizing this further to provide and implement all of the current booking mechanisms that are currently built in the core. Call this \"customizable booking.\" ( thread ).","title":"Customizable Booking"},{"location":"beancount_v3.html#ugly-little-things","text":"print_entry() uses buffering that makes it impossible to use regular print() interspersed with the regular stdout without providing file= option. Fix this, make this regular instead, that's just annoying, just print to regular stdout. The default format for __str__ for inventories puts () around the rendering. When there's a single position, that looks like a negative number. That's dumb. Use {} instead, or something else. Add a flag to bean-check to make it run --auto plugins by default. This is great for imported files, which may not have a complete ledger to feed in.","title":"Ugly Little Things"},{"location":"beancount_v3.html#incremental-booking-beancount-server-emacs-companion","text":"In order to make recomputation fast, the idea of creating a standalone \"Beancount server\" starts to make sense. The expensive part of the Beancout calculations on a large file is the booking and interpolation. The key to making things fast is thus to keep all the original unprocessed transactions in memory along with the booked and interpolated ones, and on a change, reparse the modified files and scan all the transactions, updating only the ones whose accounts have been affected. This could be problematic in theory: some plugins may rely on non-local effects in a way that affects what they output. I believe in practice it would work 99% of the time. But I think it may be worth a prototype. On the other hand, Vnext may turn out to be fast enough recomputing everything from scratch every single time (my own file went from 4s -> 0.3ms for the parsing stage of the largest file), so maybe this is irrelevant overall. Such a server would be a perfect companion to a running Emacs. We could build an Emacs mode which communicates with the server.","title":"Incremental Booking/ Beancount Server / Emacs Companion"},{"location":"beancount_v3.html#tags-links-merge-with-metadata","text":"TODO(blais): Add colon syntax","title":"Tags & Links Merge with MetaData"},{"location":"beancount_v3_dependencies.html","text":"Beancount C++ version: Dependencies \uf0c1 Martin Blais , June 2020 Beancount is going to get rewritten in C++, here is the set of dependencies I've tested and that I'm comfortable maintaining in the long run: Base environment \uf0c1 Bazel build ( https://github.com/bazelbuild/bazel ): The Google build system is the most stable approach to build that I know of, much better than SCons and certainly much better than CMake. It allows you to pin down a specific set of dependencies by explicitly referencing other repositories and git repos at specific commit revisions (including non-released ones), and the sometimes annoying constraints it imposes results in hermetically reproducible builds like no other build system can do. This minimizes surprises and hopefully the number of platform-dependent portability issues. It also minimizes the amount of pre-installed packages we assume your system has (e.g. it'll download and compile its own Bison, for example). It runs fast, computes the minimal set of tests and targets to rebuild, and is highly configurable. The downside of choosing Bazel is the same of other Google-issued open source projects: the original version of that product is internal and as a result there are a lot of strange idiosyncrasies to deal with (e.g. //external, the @bazel_tools repo, etc.), many of which are poorly documented outside the company and with a good number of unresolved tickets. However, at this stage I've already managed to create a working build with most of the dependencies described in this section. C++14 with GCC and Clang/LLVM : Both compilers will be supported. Clang provides a much better front-end and stdlib implementation but is a little slower to build. GCC is more commonly present in the wild but the error messages are\u2026 well, we all got used to this I suppose. Note that despite requiring C++14, I will refrain from using exotic features of the language (including classes). There may be questions about Windows support. Abseil-Cpp base library ( https://github.com/abseil/abseil-cpp ): The base library of functions is issued from Google's own gigantic codebase and has been battle-hardened and tested like no other\u2014this is what the Google products run on. This provides a most stable API to work with (it's unlikely to change much given how much code depends on it), one which complements stdc++ well, and whose existing contact surfaces are bound to remain pretty static. It's simpler and more stable than Boost, and doesn't offer a myriad of libraries we're not going to need anyway (plus, I love Titus' approach to C++).. This fills in a lot of the basic string manipulation functions you get for free in Python but crave in C++ (e.g. absl::StrCat). Google Test ( https://github.com/google/googletest ): This is the widely used C++ testing framework I'm already familiar with, which supports matchers and mocking. Data representation \uf0c1 Protocol Buffers ( https://github.com/protocolbuffers/protobuf ): I will maintain a functional style in this C++ rewrite and I need a replacement for Python's nametuples to represent directives. This means creating a lot of simple naked structured data that will need to be created dynamically from within tests (there's a good text-format parser) and also serialized to disk as the boundary between the core and query language will become a file of protobuf messages. Protobuf provides a good hierarchical data structure with repeated fields that is supported in many languages (this opens the door potentially to plugins written in e.g., Go), and it's possible to provide Python bindings for them. It will also become the interface between the Beancount's core and the input to the query language. We will be using proto3 with version >=3.12 in order to have support for optional presence fields (null values). Riegeli (https://github.com/google/riegeli) : An efficient and compressed binary format for storing sequences of protobuf messages to files. I think the Beancount core will output this; it's compact and reads fast. It's also another Googlething that ought to receive more attention than it does and supports both C++ and Python and protobufs. mpdecimal ( https://www.bytereef.org/mpdecimal/ ) : This is the same C-level library used by Python's implementation of Decimal numbers. Using this library will allow to easily operate between the C++ core and Python's runtime. I need to represent decimal numbers in C++ memory with minimal functionality and reasonably small range (BigNum classes are typically more than what we need). We don't need much of the scope for decimal\u2026. basic arithmetic operations + quantizing, mainly. There are other libraries out there: GMP , decNumber . There is some information on this thread: ( https://stackoverflow.com/questions/14096026/c-decimal-data-types . For on-disk representation, I will need a protobuf message definition for those, and I'm thinking of defining a union of string (nice to read but lots of conversions from string to decimal) with some more efficient exponent + mantissa decimal equivalent. Parser \uf0c1 RE/flex lexer ( https://github.com/Genivia/RE-flex ): This modern regexp-based scanner generator supports Unicode natively and is very fast and well documented. It provides a great alternative to the aging GNU flex which made it difficult to support non-ASCII characters outside of string literals (i.e.., for account names). I've had success using it on other projects. Many users want account names in their home language; this will make it easy to provide a UTF-8 parser for the entire file. GNU Bison ( https://git.savannah.gnu.org/git/bison.git ): We will stick with GNU Bison, but instead use the C++ complete modes it supports. I'm hesitating continuing with this parser generator as it's showing its age but it's pretty stable and I can't quite justify the extra work to upgrade to ANTLR. We will have to pull some tricks to support the same grammar for generating C code for v2 and C++ code for the next version; the parser code could be provided with a dispatch table of functions, which would be static C functions in v2, and methods in a C++ version. Some of the generation parameters (% directives) will be different (see here for an example). I nternational Components for Unicode (ICU) ( https://github.com/unicode-org/icu.git ): This is the standard library to depend on for Unicode support. Our C++ will not use std::wstring/std::wchar, but rather regular std::string and function calls to this library where necessary. Python \uf0c1 Python3 (https://www.python.org/) : Not much to say. I will keep using the latest version. Python is a tank of an extension language and no plans to change that. pybind11 ( https://github.com/pybind/pybind11 ): I want to provide a Python API nearly identical to the current one in Beancount, or better (which means simpler). One of the requirements I've had is to make it cheap to pass a list of protobuf objects for the directives to a Python callback, without copying (serializing and deserializing) between C++ and Python\u2014for plugins. I've investigated multiple libraries to interoperate between Python and C++: Cython, CLIF, SWIG, etc. and serialization is a problem (see this partial solution ). The one that seems to have the most momentum at the moment is pybind11, a pure header library which is an evolution from Boost::Python, that offers the most control over the generated API. It also works well with protocol buffer targets built with fast_cpp_protos: only pointers are passed through, so plugins passing in and out the full list of directives should be possible. I also happen to be familiar with Boost::Python having used it 20 years ago, it's really quite similar actually (but does away with the rest of Boost). Type annotations , PyType (or MyPy ?): I've already been compliant to a custom configuration of PyLint for Python but the codebase does not use the increasingly ubiquitous type annotations . In the rewritten subset of the code that will remain, I'd like to have all functions annotated and to replace the sometimes redundant Args/Returns docstrings with a more free-form documentation (the types may be sufficient to avoid the formalism of Args/Returns blocks). I'll have to see how this affects the auto-generated docs . An important addition is that I want to start not only annotating, but running one of the type checkers automatically as part of the build. I'm already familiar with Google's pytype, but perhaps mypy is a good alternative. In any case, the only hurdle for that is to craft Bazel rules that invoke these automatically across the entire codebase, as part of py_library() and py_binary() rules. I'll also attempt to make pylint run in the same way (as part of the build) with a custom flag to disable it during development, instead of having a separate lint target. Subpar ( https://github.com/google/subpar ): It's not clear to me yet how to perform a pip-compatible setup.py for a Bazel build, but surely we can find a way to build wheels for PyPI using the binaries built by Bazel. For packaging a self-contained binary of Python + extensions, the \"subpar\" Bazel rules is supposed to handle that. However, at the moment it does not support C extensions .","title":"Dependencies"},{"location":"beancount_v3_dependencies.html#beancount-c-version-dependencies","text":"Martin Blais , June 2020 Beancount is going to get rewritten in C++, here is the set of dependencies I've tested and that I'm comfortable maintaining in the long run:","title":"Beancount C++ version: Dependencies"},{"location":"beancount_v3_dependencies.html#base-environment","text":"Bazel build ( https://github.com/bazelbuild/bazel ): The Google build system is the most stable approach to build that I know of, much better than SCons and certainly much better than CMake. It allows you to pin down a specific set of dependencies by explicitly referencing other repositories and git repos at specific commit revisions (including non-released ones), and the sometimes annoying constraints it imposes results in hermetically reproducible builds like no other build system can do. This minimizes surprises and hopefully the number of platform-dependent portability issues. It also minimizes the amount of pre-installed packages we assume your system has (e.g. it'll download and compile its own Bison, for example). It runs fast, computes the minimal set of tests and targets to rebuild, and is highly configurable. The downside of choosing Bazel is the same of other Google-issued open source projects: the original version of that product is internal and as a result there are a lot of strange idiosyncrasies to deal with (e.g. //external, the @bazel_tools repo, etc.), many of which are poorly documented outside the company and with a good number of unresolved tickets. However, at this stage I've already managed to create a working build with most of the dependencies described in this section. C++14 with GCC and Clang/LLVM : Both compilers will be supported. Clang provides a much better front-end and stdlib implementation but is a little slower to build. GCC is more commonly present in the wild but the error messages are\u2026 well, we all got used to this I suppose. Note that despite requiring C++14, I will refrain from using exotic features of the language (including classes). There may be questions about Windows support. Abseil-Cpp base library ( https://github.com/abseil/abseil-cpp ): The base library of functions is issued from Google's own gigantic codebase and has been battle-hardened and tested like no other\u2014this is what the Google products run on. This provides a most stable API to work with (it's unlikely to change much given how much code depends on it), one which complements stdc++ well, and whose existing contact surfaces are bound to remain pretty static. It's simpler and more stable than Boost, and doesn't offer a myriad of libraries we're not going to need anyway (plus, I love Titus' approach to C++).. This fills in a lot of the basic string manipulation functions you get for free in Python but crave in C++ (e.g. absl::StrCat). Google Test ( https://github.com/google/googletest ): This is the widely used C++ testing framework I'm already familiar with, which supports matchers and mocking.","title":"Base environment"},{"location":"beancount_v3_dependencies.html#data-representation","text":"Protocol Buffers ( https://github.com/protocolbuffers/protobuf ): I will maintain a functional style in this C++ rewrite and I need a replacement for Python's nametuples to represent directives. This means creating a lot of simple naked structured data that will need to be created dynamically from within tests (there's a good text-format parser) and also serialized to disk as the boundary between the core and query language will become a file of protobuf messages. Protobuf provides a good hierarchical data structure with repeated fields that is supported in many languages (this opens the door potentially to plugins written in e.g., Go), and it's possible to provide Python bindings for them. It will also become the interface between the Beancount's core and the input to the query language. We will be using proto3 with version >=3.12 in order to have support for optional presence fields (null values). Riegeli (https://github.com/google/riegeli) : An efficient and compressed binary format for storing sequences of protobuf messages to files. I think the Beancount core will output this; it's compact and reads fast. It's also another Googlething that ought to receive more attention than it does and supports both C++ and Python and protobufs. mpdecimal ( https://www.bytereef.org/mpdecimal/ ) : This is the same C-level library used by Python's implementation of Decimal numbers. Using this library will allow to easily operate between the C++ core and Python's runtime. I need to represent decimal numbers in C++ memory with minimal functionality and reasonably small range (BigNum classes are typically more than what we need). We don't need much of the scope for decimal\u2026. basic arithmetic operations + quantizing, mainly. There are other libraries out there: GMP , decNumber . There is some information on this thread: ( https://stackoverflow.com/questions/14096026/c-decimal-data-types . For on-disk representation, I will need a protobuf message definition for those, and I'm thinking of defining a union of string (nice to read but lots of conversions from string to decimal) with some more efficient exponent + mantissa decimal equivalent.","title":"Data representation"},{"location":"beancount_v3_dependencies.html#parser","text":"RE/flex lexer ( https://github.com/Genivia/RE-flex ): This modern regexp-based scanner generator supports Unicode natively and is very fast and well documented. It provides a great alternative to the aging GNU flex which made it difficult to support non-ASCII characters outside of string literals (i.e.., for account names). I've had success using it on other projects. Many users want account names in their home language; this will make it easy to provide a UTF-8 parser for the entire file. GNU Bison ( https://git.savannah.gnu.org/git/bison.git ): We will stick with GNU Bison, but instead use the C++ complete modes it supports. I'm hesitating continuing with this parser generator as it's showing its age but it's pretty stable and I can't quite justify the extra work to upgrade to ANTLR. We will have to pull some tricks to support the same grammar for generating C code for v2 and C++ code for the next version; the parser code could be provided with a dispatch table of functions, which would be static C functions in v2, and methods in a C++ version. Some of the generation parameters (% directives) will be different (see here for an example). I nternational Components for Unicode (ICU) ( https://github.com/unicode-org/icu.git ): This is the standard library to depend on for Unicode support. Our C++ will not use std::wstring/std::wchar, but rather regular std::string and function calls to this library where necessary.","title":"Parser"},{"location":"beancount_v3_dependencies.html#python","text":"Python3 (https://www.python.org/) : Not much to say. I will keep using the latest version. Python is a tank of an extension language and no plans to change that. pybind11 ( https://github.com/pybind/pybind11 ): I want to provide a Python API nearly identical to the current one in Beancount, or better (which means simpler). One of the requirements I've had is to make it cheap to pass a list of protobuf objects for the directives to a Python callback, without copying (serializing and deserializing) between C++ and Python\u2014for plugins. I've investigated multiple libraries to interoperate between Python and C++: Cython, CLIF, SWIG, etc. and serialization is a problem (see this partial solution ). The one that seems to have the most momentum at the moment is pybind11, a pure header library which is an evolution from Boost::Python, that offers the most control over the generated API. It also works well with protocol buffer targets built with fast_cpp_protos: only pointers are passed through, so plugins passing in and out the full list of directives should be possible. I also happen to be familiar with Boost::Python having used it 20 years ago, it's really quite similar actually (but does away with the rest of Boost). Type annotations , PyType (or MyPy ?): I've already been compliant to a custom configuration of PyLint for Python but the codebase does not use the increasingly ubiquitous type annotations . In the rewritten subset of the code that will remain, I'd like to have all functions annotated and to replace the sometimes redundant Args/Returns docstrings with a more free-form documentation (the types may be sufficient to avoid the formalism of Args/Returns blocks). I'll have to see how this affects the auto-generated docs . An important addition is that I want to start not only annotating, but running one of the type checkers automatically as part of the build. I'm already familiar with Google's pytype, but perhaps mypy is a good alternative. In any case, the only hurdle for that is to craft Bazel rules that invoke these automatically across the entire codebase, as part of py_library() and py_binary() rules. I'll also attempt to make pylint run in the same way (as part of the build) with a custom flag to disable it during development, instead of having a separate lint target. Subpar ( https://github.com/google/subpar ): It's not clear to me yet how to perform a pip-compatible setup.py for a Bazel build, but surely we can find a way to build wheels for PyPI using the binaries built by Bazel. For packaging a self-contained binary of Python + extensions, the \"subpar\" Bazel rules is supposed to handle that. However, at the moment it does not support C extensions .","title":"Python"},{"location":"beangulp.html","text":"Beangulp \uf0c1 Martin Blais , Jan 2021 Importing data for Beancount has been supported originally by the LedgerHub project along with a library of importers, then reintegrated as a pure framework library (with some examples) in the Beancount repo as beancount.ingest, and now we're splitting up that repository again and will factor out the importing framework to another repo for easier maintenance and evolution. This document lays out some of the changes desired in this new version. New Repo \uf0c1 The new repository will be located at http://github.com/beancount/beangulp Beangulp will target compatibility with the latest beancount release from the v3 branch only. Beancount v3 is expected to evolve rapidly at the beginning, thus, to make the life of early adopters less painful, careful use of version number increments and versioned dependencies should be employed. Ideally, Beangulp should depend on the current minor version of Beancount only, for example, if Beancount 3.0.0 is released, Beangulp should declare install_requires: beancount >3.0, <3.1 See setuptools doc and PEP-440 . Status \uf0c1 As of Jan 2022, most of this proposal is complete and implemented. (Thanks to Daniele Nicolodi for doing most of the work.) Changes \uf0c1 Library Only \uf0c1 The current implementation allows one to use the bean-identify, bean-extract and bean-file tools on a \"config file\" which is evaluated Python, or create a script and call a single endpoint that will implement the subcommands. In order to make this work, a really convoluted trampoline is used to bounce the evaluation to the same code. I'll admit it trumps even me who wrote it whenever I have to go in there and edit that code. It also makes it inconvenient to let users add custom before/after customizations to their import process. The next version will support only (2). The bean-identify, bean-extract, bean-file programs will all be removed. The user will be expected to write their own script. One File \uf0c1 Right now, each importer consists of two files: the implementation file and an associated test file, e.g. soandso_bank.py soandso_bank_test.py The test file is small and usually calls out to a library function to find model files and expected outputs. Since there's hardly any real test code, we'd like to be able to have a single Python file that contains its test invocation. A new endpoint will be added to define the tests in the importer implementation. Self-Running \uf0c1 Moreover, that new function should also be the same one as that which is used to put together the configuration script. In other words, an importer's main() function should be equivalent to an importer's invocation with a single configured importer, one that is configured with the configuration used for testing. This will allow users to just \"run the importer\" on a specific set of files without having to define a configuration, by using the test configuration, like this: soandso_bank.py extract ~/Downloads/transactions.csv Having this option makes it super convenient for people to share the one file and test it out immediately, without having to create a configuration nor a main program. I don\u2019t think there is an easy clean way to implement this other than having something like if __name__ == \u2018__main__\u2019: main = Ingest([SoAndSoBankImporter()]) main() in the importer definition file. This should work right now without changes. Although, it is often the case now that importers require a bit of configuration to work (I am not sure, I don\u2019t use any of the importers distributed with Beancount or widely used). Test S ubcommand & Generate \uf0c1 Since the importers are runnable and define their test cases for pytest to run over, we should also add a subcommand \"test\" to complete \"identify\", \"extract\" and \"file\". That venue is also a great place to replace the --generate option which required ugly injection of the pytestconfig, and instead, implement it ourselves and add a second subcommand: \"genexpect\" to generate the expected file for testing. The interface becomes: soandso_bank.py identify ~/Downloads/ soandso_bank.py extract ~/Downloads/ soandso_bank.py file ~/Downloads/ ~/documents soandso_bank.py test ~/documents/testdocs soandso_bank.py generate ~/Downloads/transactions.csv This way we can remove the pytestconfig dep injection and also simplify the logic of unit tests, which had to handle both the check vs. generate scenarios. This should result in simpler code. One Expected File \uf0c1 Expected outputs from an importer are stored in multiple files with suffixes .extract, .file_name, .file_date, etc. If we had all the output in one file, the \"genexpect\" subcommand could generate everything to stdout. This is convenient. myimporter.py myimporter.beancount Leverage the Beancount syntax to store the expected values for \"file_name()\", \"file_date()\" and other outputs. Store those in Event or Custom directives and have the testing code assert on them. The new contents of a test directory should be simple pairs of (a) original downloaded file and (b) expected output, containing the transactions but also all the other method outputs. Duplicates Identification \uf0c1 This has never really worked properly. I think if this was implemented separately based on each importer \u2014 in other words, letting each importer define how to identify duplicates, e.g., if a unique transaction ID can be assumed having been inserted as a link to disambiguate \u2014 we could do this a lot cleaner. It would be ideal if each importer could specify duplicate id detection, in the importer. It could call on a more general but less reliable method, and that code should live in Beangulp. CSV Utils \uf0c1 I have a lot of really convenient utilities for slicing and dicing CSV files from ugly CSV downloads. CSV downloads often are used to house multiple tables and types of data, and a higher-level of processing is often needed on top of these files. I have code like this spread all over. This deserves a nice library. What's more, I have a nice table processing library under the Baskets project, which hasn't been given the proper quality treatment yet. Clean up my little table library and merge it with all the CSV utils. Put this in beangulp, or even contemplate making this its own project, including a CSV importer. \"CSV world.\" Not sure this matters as much after discovering petl. We could depend on petl. Caching \uf0c1 When running conversion jobs on large files, it would be nice to have a cache and avoid running those more than once. The (small) converted data could be cached and loaded back up in order to avoid running the expensive conversion more than once. One difficulty is that the conversions required to be run depend on the importers configuration, and each importer is unaware of the other ones. All the command-line arguments and at least the head of the file contents should be hashed in. This library could be pretty independent from Beancount. API Changes \uf0c1 \"file_date()\" is not clear; \"get_filing_date()\" would be. The extra argument on extract() is irregular compared to all the other methods. Find a better way? I'm not 100% sure I like my little memoizing file wrapper (\"cache\") with cache. Replace it with a disk-only one. Automatic Insertion \uf0c1 A really convenient and easily built feature that the new code should have is the automatic insertions of the extracted output to an existing ledger, before the point of a special string token in the file. Make this part of the library, as an alternative for storing the output of the importer, e.g. $ ./myimport.py extract | bean-insert ledger.beancount Amex This could also be a flag to \"extract\" $ ./myimport.py extract -f ledger.beancount -F Amex","title":"Beangulp"},{"location":"beangulp.html#beangulp","text":"Martin Blais , Jan 2021 Importing data for Beancount has been supported originally by the LedgerHub project along with a library of importers, then reintegrated as a pure framework library (with some examples) in the Beancount repo as beancount.ingest, and now we're splitting up that repository again and will factor out the importing framework to another repo for easier maintenance and evolution. This document lays out some of the changes desired in this new version.","title":"Beangulp"},{"location":"beangulp.html#new-repo","text":"The new repository will be located at http://github.com/beancount/beangulp Beangulp will target compatibility with the latest beancount release from the v3 branch only. Beancount v3 is expected to evolve rapidly at the beginning, thus, to make the life of early adopters less painful, careful use of version number increments and versioned dependencies should be employed. Ideally, Beangulp should depend on the current minor version of Beancount only, for example, if Beancount 3.0.0 is released, Beangulp should declare install_requires: beancount >3.0, <3.1 See setuptools doc and PEP-440 .","title":"New Repo"},{"location":"beangulp.html#status","text":"As of Jan 2022, most of this proposal is complete and implemented. (Thanks to Daniele Nicolodi for doing most of the work.)","title":"Status"},{"location":"beangulp.html#changes","text":"","title":"Changes"},{"location":"beangulp.html#library-only","text":"The current implementation allows one to use the bean-identify, bean-extract and bean-file tools on a \"config file\" which is evaluated Python, or create a script and call a single endpoint that will implement the subcommands. In order to make this work, a really convoluted trampoline is used to bounce the evaluation to the same code. I'll admit it trumps even me who wrote it whenever I have to go in there and edit that code. It also makes it inconvenient to let users add custom before/after customizations to their import process. The next version will support only (2). The bean-identify, bean-extract, bean-file programs will all be removed. The user will be expected to write their own script.","title":"Library Only"},{"location":"beangulp.html#one-file","text":"Right now, each importer consists of two files: the implementation file and an associated test file, e.g. soandso_bank.py soandso_bank_test.py The test file is small and usually calls out to a library function to find model files and expected outputs. Since there's hardly any real test code, we'd like to be able to have a single Python file that contains its test invocation. A new endpoint will be added to define the tests in the importer implementation.","title":"One File"},{"location":"beangulp.html#self-running","text":"Moreover, that new function should also be the same one as that which is used to put together the configuration script. In other words, an importer's main() function should be equivalent to an importer's invocation with a single configured importer, one that is configured with the configuration used for testing. This will allow users to just \"run the importer\" on a specific set of files without having to define a configuration, by using the test configuration, like this: soandso_bank.py extract ~/Downloads/transactions.csv Having this option makes it super convenient for people to share the one file and test it out immediately, without having to create a configuration nor a main program. I don\u2019t think there is an easy clean way to implement this other than having something like if __name__ == \u2018__main__\u2019: main = Ingest([SoAndSoBankImporter()]) main() in the importer definition file. This should work right now without changes. Although, it is often the case now that importers require a bit of configuration to work (I am not sure, I don\u2019t use any of the importers distributed with Beancount or widely used).","title":"Self-Running"},{"location":"beangulp.html#test-subcommand-generate","text":"Since the importers are runnable and define their test cases for pytest to run over, we should also add a subcommand \"test\" to complete \"identify\", \"extract\" and \"file\". That venue is also a great place to replace the --generate option which required ugly injection of the pytestconfig, and instead, implement it ourselves and add a second subcommand: \"genexpect\" to generate the expected file for testing. The interface becomes: soandso_bank.py identify ~/Downloads/ soandso_bank.py extract ~/Downloads/ soandso_bank.py file ~/Downloads/ ~/documents soandso_bank.py test ~/documents/testdocs soandso_bank.py generate ~/Downloads/transactions.csv This way we can remove the pytestconfig dep injection and also simplify the logic of unit tests, which had to handle both the check vs. generate scenarios. This should result in simpler code.","title":"Test Subcommand & Generate"},{"location":"beangulp.html#one-expected-file","text":"Expected outputs from an importer are stored in multiple files with suffixes .extract, .file_name, .file_date, etc. If we had all the output in one file, the \"genexpect\" subcommand could generate everything to stdout. This is convenient. myimporter.py myimporter.beancount Leverage the Beancount syntax to store the expected values for \"file_name()\", \"file_date()\" and other outputs. Store those in Event or Custom directives and have the testing code assert on them. The new contents of a test directory should be simple pairs of (a) original downloaded file and (b) expected output, containing the transactions but also all the other method outputs.","title":"One Expected File"},{"location":"beangulp.html#duplicates-identification","text":"This has never really worked properly. I think if this was implemented separately based on each importer \u2014 in other words, letting each importer define how to identify duplicates, e.g., if a unique transaction ID can be assumed having been inserted as a link to disambiguate \u2014 we could do this a lot cleaner. It would be ideal if each importer could specify duplicate id detection, in the importer. It could call on a more general but less reliable method, and that code should live in Beangulp.","title":"Duplicates Identification"},{"location":"beangulp.html#csv-utils","text":"I have a lot of really convenient utilities for slicing and dicing CSV files from ugly CSV downloads. CSV downloads often are used to house multiple tables and types of data, and a higher-level of processing is often needed on top of these files. I have code like this spread all over. This deserves a nice library. What's more, I have a nice table processing library under the Baskets project, which hasn't been given the proper quality treatment yet. Clean up my little table library and merge it with all the CSV utils. Put this in beangulp, or even contemplate making this its own project, including a CSV importer. \"CSV world.\" Not sure this matters as much after discovering petl. We could depend on petl.","title":"CSV Utils"},{"location":"beangulp.html#caching","text":"When running conversion jobs on large files, it would be nice to have a cache and avoid running those more than once. The (small) converted data could be cached and loaded back up in order to avoid running the expensive conversion more than once. One difficulty is that the conversions required to be run depend on the importers configuration, and each importer is unaware of the other ones. All the command-line arguments and at least the head of the file contents should be hashed in. This library could be pretty independent from Beancount.","title":"Caching"},{"location":"beangulp.html#api-changes","text":"\"file_date()\" is not clear; \"get_filing_date()\" would be. The extra argument on extract() is irregular compared to all the other methods. Find a better way? I'm not 100% sure I like my little memoizing file wrapper (\"cache\") with cache. Replace it with a disk-only one.","title":"API Changes"},{"location":"beangulp.html#automatic-insertion","text":"A really convenient and easily built feature that the new code should have is the automatic insertions of the extracted output to an existing ledger, before the point of a special string token in the file. Make this part of the library, as an alternative for storing the output of the importer, e.g. $ ./myimport.py extract | bean-insert ledger.beancount Amex This could also be a flag to \"extract\" $ ./myimport.py extract -f ledger.beancount -F Amex","title":"Automatic Insertion"},{"location":"calculating_portolio_returns.html","text":"Calculating Portfolio Returns \uf0c1 Martin Blais , Sept 2020 http://furius.ca/beancount/doc/returns This document describes how to compute portfolio returns from a Beancount ledger. Motivation \uf0c1 You will be surprised to find that discount brokers typically do not provide accurate and complete returns calculations for your investments based on your specific cash flows. They tend to report other measures of performance: Change in value. The simplest they provide is a snapshot of the account value at the beginning and end of the period (or year). The problem with this method is that it does not reflect your infusions or removal of cash as such, nor your changes in positions. For example, if you had an account with $50,000 at the beginning of the year and you've added in $30,000 in August, reporting a difference of $37,000 at the end of December is just not useful (you'd have to mentally discount for new invested cash during the year, and what if you want to break it down and compare the returns from different instruments in the account?). Underlying performance. They might report the growth of the underlying asset in isolation, disregarding your specific positions. It's not very useful to say \"HDV grew by 8.2% over the last year\" if you invested only (or even mostly) in the second half of the year. What I'd like is to know how much my specific investments grew, given my specific changes in positions and timings. In other words, how did I do? No dividends. Another issue is that performance is typically only reporting capital appreciation due to the change in price of the investment. Ideally one would like to break down the performance between capital appreciation and dividends and returns for each of those components, as well as overall performance, so you can compare returns from stocks and bonds to each other. You can sometimes find out that information from the Yield field in the summary, but for uneven distributions, this won't help me find out what my actual returns from dividends were, over any period. Commissions and management expenses. Some brokers charge management expenses on a monthly basis, based on the value of the account. I would like to know what my portfolio return is net of those expenses. I want accurate returns reporting, based on my actual cash flows and changes of positions over time. If I maintain your investment information in a Beancount ledger, in theory it contains all the data I need in order to compute your true returns, based on the specific timings of my own savings (cash infusions) and which positions I held at which time. It's just not in the simplest format required to do it\u2014 Beancount transactions are much more flexible than one might want and a simpler series of cash flows needs to be extracted from it. This document explains how I finally did this from my own Ledger. And how we might generalize this to yours, based on some simple rules. Most of this text is dedicated to the pedestrian details of extracting the right data. The source code can be found here . In addition, a fair and honest comparison to other investments scenarios should be possible, based on those same cash flows. For instance, you should be able to produce data that looks like \"My investments in ZZZ have returned 8.2%, 1.1% of which were from dividends, and if I'd invested in a 60/40 portfolio of broad stocks and bonds it would have returned 7.2% instead.\" In other words, I want to assess my performance relative to a number of common alternatives. (Finally, note that if all you need is a snapshot of your current positions, that's already handled by the export script .) History \uf0c1 In 2014, I made a brief attempt to break down information from my ledger to do this. At the time I got bogged down in details when some of the time-series I was extracting weren't producing what looked like sensible results (some with outliers). I got too detailed too fast. Sometimes it's better to just get the whole job done and come back for the details. I hadn't logged enough debugging information and I didn't have enough confidence in its output to use it. I never actually finished the work at the time, and eventually moved the scripts to experiments and shrugged. \"Later.\" In August 2020, I sat down to do this again, this time with a less ambitious goal of just getting a good approximation and producing lots of debug output, boiling down extraction to pull out just the cash flows and to get the job complete over all my accounts, even if it meant making some adjustments to my input file. It turned out to be the right decision: I managed to complete the task, and this document presents my journey, the method, its assumptions, quirks, and some results. Overview of Method \uf0c1 The method I'll use is to extract cash flow information for each investment. What I call \" an investment \" in this context is a kind of financial instrument , e.g., shares of \"VTI\" for the Vanguard Total Stock Market ETF, invested in a particular account , e.g. a Vanguard 401k. A list of cash flows is a dated list of positive or negative currency being invested or withdrawn as proceeds to/from that investment. So I have a list of cash flows per account, and each account records only one financial instrument. For reporting I'll group those per-account series in logical units of \" reports \", for example lumping together ones for the same instrument bought in different accounts / brokers, or different instruments that represent the same underlying by merging their cash flows. Or even to group them by \"strategy\". Basically, by merging the cash flows of multiple accounts, I have a generic way to group any subsets of accounts and compute their returns. Using the cash flows, I will then run a simple root finding routine to calculate the average annual rate those flows would have to grow in order to result in their final market value. This provides me with overall returns. This is similar to calculating the Internal Rate of Return . I will do that for the entire time series, but also for sub-intervals within the time series to compute, e.g. calendar returns (i.e., each year or quarter) or cumulative returns for trailing periods. Since cash flows are flagged as dividends or not, I can separate the returns from appreciation from the returns from dividends. Reports with plots are produced for each of the groups. Here's a diagram that shows how the \"configure\", \"compute_returns\" and \"download_prices\" scripts work together: These will be further detailed below. Configuration \uf0c1 First, a configuration needs to be created to define a list of investments, and groups of those, for which reports will be produced. This configuration is provided as a text-formatted protocol buffer message. Investment. An investment corresponds to a particular instrument stored in a particular account. It also involves other transactions that don't directly involve that particular account. We want to provide a few set of account names: Asset account. The name of the account holding the commodities for that investment. Matching accounts. A list of additional accounts which will select transactions with postings to them. Cash accounts. A list of accounts external to the investment, which will be used to flag inflows and outflows to and from the investment. Report. While we can compute returns for each investment, the more general and useful case is to compute returns on groups of them. A report is simply a list of investment names. A PDF file will be produced for each report. Here's an example of 6 investments and 3 reports: investments { # Accounts at BTrade. investment { currency: \"GLD\" asset_account: \"Assets:US:BTrade:GLD\" cash_accounts: \"Assets:US:BTrade:Cash\" } investment { currency: \"AMZN\" asset_account: \"Assets:US:BTrade:AMZN\" cash_accounts: \"Assets:US:BTrade:Cash\" } investment { currency: \"QQQ\" asset_account: \"Assets:US:BTrade:QQQ\" dividend_accounts: \"Income:US:BTrade:QQQ:Dividend\" cash_accounts: \"Assets:US:BTrade:Cash\" } # Accounts at IBKR. investment { currency: \"IAU\" asset_account: \"Assets:US:IBKR:IAU\" cash_accounts: \"Assets:US:IBKR:Cash\" } investment { currency: \"SLV\" asset_account: \"Assets:US:IBKR:SLV\" cash_accounts: \"Assets:US:IBKR:Cash\" } # Accounts at Schwab. investment { currency: \"GOOG\" asset_account: \"Assets:US:Schwab:GOOG\" cash_accounts: \"Assets:US:Schwab:Cash\" cash_accounts: \"Assets:AccountsReceivable\" cash_accounts: \"Assets:US:GoogleInc:GSURefund\" } } groups { group { name: \"strategy.gold\" investment: \"Assets:US:BTrade:GLD\" investment: \"Assets:US:IBKR:IAU\" } group { name: \"strategy.tech\" investment: \"Assets:US:BTrade:QQQ\" investment: \"Assets:US:BTrade:FB\" investment: \"Assets:US:Schwab:GOOG\" } group { name: \"all\" investment: \"Assets:US:*\" currency: \"USD\" } group { name: \"accounts.BTrade\" investment: \"Assets:US:BTrade:*\" currency: \"USD\" } } Different reports can include the same investment. References to accounts and investment names support simple UNIX-style globbing patterns in the configuration. These are expanded to full account names at runtime and stored in the output. A \" configure.py \" script can automatically infer a working basic configuration from an existing Beancount ledger. A report will be generated for each unique instrument and the same metadata fields honored by the \"export\" script (\"assetcls\", \"strategy\") will also generate reports. I recommend you run this on your ledger and then custom tailor the configuration manually. Finding Accounts \uf0c1 This script needs to figure out the list of available investments to report on. By convention I keep pairs of dedicated leaf accounts for each commodity type in my ledger, one to contain the actual positions (assets) and one to receive dividends, e.g., for \"VTI\" held in broker \"BTrade\", accounts like 2012-03-01 open Assets:US:BTrade:VTI VTI 2012-03-01 open Income:US:BTrade:VTI:Dividend USD This has two consequences: (a) it makes it easy to find the list of accounts that contain investments (any account with a leaf account name that is also one of the commodities found in the ledger), and (b) it nicely isolates all the activity related to each of those investments by finding all the transactions with a posting to that account . I recommend you follow the same convention in your chart of accounts. The result is a list of accounts like \"Assets:US:BTrade:VTI\", specific to each (instrument, institution). I further filter down this list to the subset of accounts which were still open up to 15 years ago (I close my accounts\u2014using Beancount's Close directive\u2014when they're done). In my particular case, I didn't have much savings back then, and there's no point in bothering to do the work to normalize those crumbs in my investing history for that far back. Extracting Cash Flow Data \uf0c1 This section describes the various steps I took to extract relevant data from my ledger. Extracting Relevant Transactions \uf0c1 For each of the identified asset accounts, we want to pull out from the ledger's transactions the list of transactions affecting that account. We simply run through the entire ledger's transactions keeping transactions with at least one posting to the investment's asset account, dividend income accounts, or to any of the other \"match accounts\" defined for it. For instance, transactions for cash dividend payments will not show an asset posting so if dividends are paid; a typical dividend payment would contain only the dividend income posting and a cash posting (for the deposit): 2019-11-27 * \"Dividend\" Income:US:BTrade:VTI:Dividend -123.45 USD Assets:US:BTrade:Cash 123.45 USD So it is necessary to include the dividend account in the configuration to include those transactions, because they typically do not involve the asset account. Account Categorization \uf0c1 The next step is to generalize transactions to templates : we record the full set of accounts involved in the extracted transactions of each investment, and assign them to general categories based on their role in the transaction. For example, if I inspect my \"VTI\" trades, I will encounter the following accounts: Assets:US:BTrade:Cash Assets:US:BTrade:VTI Expenses:Financial:Commissions Income:US:BTrade:VTI:Dividend I map each account to one of several generic categories (I could probably simplify this now): ASSET # The account holding the commodity. CASH # Cash accounts, employer matches, contributions. DIVIDEND # Dividend income account. EXPENSES # Commissions, fees and other expenses. INCOME # Non-dividend income, P/L, gains, or other. OTHERASSET # Other assets than the primary asset for this investment. OTHER # Any other account. Like this: 'Assets:US:BTrade:Cash': CASH 'Assets:US:BTrade:VTI': ASSET 'Expenses:Financial:Commissions': EXPENSES 'Income:US:BTrade:VTI:Dividend': DIVIDEND In this way, I can compare similar transactions to each other across instruments and extract information from them with the same code. For example, a transaction that involves a dividend account and a cash account is a cash dividend payment, and I can write a generic handler to extract the cash flows for this. The categorization was originally prototyped with a set of ad-hoc rules, but the configuration now serves to provide the categorization. Note: In the process of doing this, I noticed many irregularities in how I named my accounts. For example, I used \":Dividend\" and \":Dividends\" sometimes. I went through my ledger and had to make some changes to name accounts coherently, and iterated until all my accounts were categorized correctly. You may have to review some of your data entry as well. Handling Transactions using the Signature \uf0c1 Using the account-category mappings from the previous section, I was able to derive a unique \"signature\" for each transaction. For example, a transaction like this: 2020-03-12 * \"(DOI) ORDINARY DIVIDEND\" Income:US:BTrade:VTI:Dividend -1312.31 USD Assets:US:BTrade:Cash 1312.31 USD would have a signature of CASH_DIVIDEND which is hopefully always a dividend payment. Beancount has a pretty flexible syntax and does not enforce that your transactions follow particular templates like this, so it wasn't clear to me when I started this project what patterns I would find in my ledger of 12 years of ad-hoc data entry\u2026 I wasn't certain that this categorization and these signatures would be sufficient to correctly handle a correct conversion to cash flows. So I had my script produce two sets of files for debugging: Investment details. A file for each investment , rendering a list of all the transactions that were extracted for it, decorated with metadata showing the categorizations inferred on each posting, as well as a categorization map of all the accounts encountered. I inspected these files visually to ensure that the account/patterns from the configuration were extracting the full and correct set of transactions involved in that investment. Signature transactions. A file for each unique signature , with the full list of transactions matching that signature across all investments. By inspecting these files, I made sure that all the transactions matching the same signature were indeed playing the same role, so that a single handler per signature is sufficient. At this point, I had a limited list of unique signatures, each with clear unique roles: ASSET_CASH : Purchase or sale ASSET_CASH_EXPENSES : Purchase or sale with commission ASSET_CASH_INCOME : Purchase or sale with profit ASSET_CASH_INCOME_EXPENSES : Purchase or sale with commission and profit ASSET_EXPENSES : Fee paid from liquidation ASSET_INCOME : Cost basis adjustment (with P/L) ASSET_INCOME_EXPENSES : Fee from liquidation (with P/L) ASSET : Stock splits ASSET_DIVIDEND : Dividend reinvested CASH_DIVIDEND : Dividend payment CASH_INCOME_DIVIDEND : Dividend payment and gains distribution ASSET_OTHERASSET : Exchange of stock/symbol \u2026 Note that the specific list really depends on the particular contents of your ledger and you should inspect the files produced for correctness. I then wrote specific handlers to produce the cash flows corresponding to each of those transaction signatures, reasoning about each of those cases in isolation. This allowed me to correctly produce a full list of cash flows per investment. Note: In practice I encountered 3 or 4 more signatures types that were a bit exotic and by fixing up my ledger I managed to either correct or break apart these transactions to equivalent but simpler ones. In particular, one of my importers was lumping together trades occurring on the same day, and I went back and fixed it and those transactions manually. The ASSET_OTHERASSET signature, in particular, is an exchange of stock (Google -> GOOG,GOOGL). Doing something like this brings up idiosyncrasies in your bookkeeping technique. Being consistent and using fewer templates is helpful. It would be a valuable idea for an accompanying plugin to restrict the possible set of templates to a select few, so that data entry is constrained to work well with this returns production code. Generalizing Production of Cash Flows \uf0c1 After inspecting each of my signature handlers, I tried to generalize them to a single unified handler that would work across all transactions. It turns out that, at least with my existing ledger's transactions, it's possible. Essentially, recording inflows or outflows to cash accounts or other assets is sufficient. In a transaction like this: 2013-09-18 * \"Buy shares of HOOL\" Assets:US:BTrade:Cash -818.55 USD flow: CASH Assets:US:BTrade:HOOL 8 HOOL {101.20 USD} flow: ASSET Expenses:Financial:Commissions 8.95 USD flow: EXPENSES The \"CASH\" posting is a sufficient incoming cash flow, so we record -818.55 USD. In a cash dividend payment: 2013-12-17 * \"Cash Dividend payment\" Assets:US:BTrade:Cash 38.50 USD flow: CASH Income:US:BTrade:HOOL:Dividends -38.50 USD flow: DIVIDEND Similarly the 38.50 is a sufficient outgoing cash flow, so we record +38.50 USD. On the other hand a reinvested asset dividend, as you would find in some mutual funds, does not generate any cash flow; it simply remains in the investment and increases its total value: 2013-12-30 * \"Reinvested dividend\" Assets:US:BTrade:HOOL 0.356 {103.41} USD flow: ASSET Income:US:BTrade:HOOL:Dividends -36.81 USD flow: DIVIDEND This rule seems sufficient to handle all the contents of my ledger correctly. In the end, I implemented both methods: I use the general rule to produce the list of cash flows, but I also call out to the explicit handlers and cross-check that the extracted cash flows are identical, just to be sure. This is enabled by a flag in compute_returns.py ( --check-explicit-flows ). This forces me to ensure that I've analyzed all the possible transaction templates. Note: If in using this script you find cases from your ledger that aren't handled by using a series of cash accounts, please let me know (on the mailing-list). Cash Flows \uf0c1 The handlers described in the previous section each produced a list of cash flows per transaction, and together for the account, they are essentially a list of: (Date, Amount, IsDividend) Now, this is a simpler model to work from. For each account, we now have a sorted series of dated cash flows. Note that Amount includes its cost currency (I have both USD and CAD), IsDividend is a flag identifying the cash flow as being a dividend payment or not (to compute returns without the dividends). These series of cash flows can be easily merged between accounts, and truncated over time by inserting initial or final cash flows corresponding to the market value at those dates. Rendered, they might look like this (because of the scale, rendering the log brings up detail that is otherwise difficult to see; dividends are rendered in green): Note that since many transactions do not generate cash flows, the list of cash flows of an investment is insufficient by itself to compute the value of the investment over time. When truncating for a time interval, the market value of the investment is derived using the list of transactions. Finally, the list of cash flows for each group of investments reported can be trivially merged by concatenating them. Computing Returns \uf0c1 Calculating the Average Growth Rate \uf0c1 For each series of cash flows, the cash flows are merged together. I use scipy.optimize.fsolve to calculate the rate that satisfies net present value: c f i /(1\u2005+\u2005 r ) t i = 0 where cf i are the signed cash flow amounts and t i are the times from today for each cash flow (in years). We solve for r . To compute the returns without dividends, we just exclude cash flows returned from dividends. The difference tells us how much of our returns was due solely to dividend income. It's important to note that if the corresponding positions are still invested, you have to insert a final negative cash flow for the market value at the latest date to zero it out. You're essentially simulating a sale. If significant transaction costs are going to be involved, you might want to simulate those as well (e.g. if you're doing this for a home, in particular). Here's the beauty in this: nowhere was the underlying price used , except for marking the current position value to market value. We did not read any external measure of returns. These returns are computed from cash going in and coming out . These are actual realized returns. They do not lie. Intervals. To compute calendar returns, e.g., returns for years 2016, 2017, 2018, 2019, 2020, I truncate the cash flows to keep only those inside the interval, e.g. 2018-01-01 to 2018-12-31 for year 2018, and if there was an existing position at the beginning of the interval, insert a negative cash flow at the start of the series equivalent to the market value at those dates. I do the same thing at the end of the interval, with a positive cash flow, as described previously. Ideally I'd like to look at different sets of intervals: Lifetime of investment. Total returns over the entire lifetime of the positions, to boil it all down to a single number. Calendar. Annual or quarterly returns over the last 10 or 15 years, to witness variation in returns over time. Cumulative. Cumulative returns over the last 10 or 15 years, aligned on calendar periods, to get a sense of whether things are improving or worsening, and how well my strategies are doing in more recent periods (e.g. last 3 years). High frequency cumulative. Cumulative returns over the last 12 months, aligned with monthly or weekly dates, to assess the impact of decisions in the short-term. These can be rendered in tables, and investments can be compared to each other in that way. Filling in Missing Price Points \uf0c1 The calculations of returns over various intervals require marking positions to market value at the beginning and end of the interval dates. Beancount being designed hermetically, i.e., it does not fetch any price externally, only uses what's in the ledger, its price database lookup will automatically produce the last available price point and its recording date before the requested date. Depending on your recording discipline, some of those prices might be out-of-date and introduce inaccuracies. This is especially important since the amounts converted at the end of periods (i.e. to estimate the value of current positions) can be large and meaningfully influence even the lifetime returns number. So it's important to have relatively fresh price points in the ledger's price database. Now the question is, given a changing set of positions over time, for a given set of interval dates, which price entries are required to produce accurate results? Because this depends heavily on the particular inputs of the returns script, in order to solve this problem I simply wrapped the price database with a facade that collects all the (instrument, date) pairs for requested conversions during the production of the reports, and filter those down by some age threshold (e.g., price not older than 3 days). These are essentially the price points missing from the file. At the end of the script, I output these to a file with Price directives, and another program (download_prices.py) can read that file and fetch historical rates for those. It produces updated rates which you can paste to your ledger file as a one-off adjustment and then recompute more accurate returns. Pulling data from Yahoo! Finance worked for 90% of my positions, but some of my older instruments were quite old or even retired, or not available (e.g., some retirement funds), so I had to find them by browsing and manually entering some of these price points (I had something like 30\u2026 no big deal). Rolling windows. One important point is that going forward, it will be easier to align reporting intervals to some calendar-based interval (e.g., monthly), so that I don't have to regenerate price data every time I produce my returns. Aligning to months is probably good enough for my time horizon. Stock Splits. Beancount does not explicitly make adjustments of prices for stocks that split, so your price source should return the pre-split price of the stock at the time if you are recording it as such. You should be able to check your price time series for errors using Fava. Currency Conversion \uf0c1 Another important detail is that each investment has its own quote currency. I used to live in Canada, and some of my older investments are denominated in CAD. So the question arises: do I compute the returns in local (CAD) currency, or in terms of my reference currency (USD)? It's convenient that Beancount's Inventory object has functions that can easily perform those conversions where needed. And since the cash flows I extracted are stored using Beancount's Amount object, I already have the quote currencies correctly in my extracted dataset. In any group, if all the instruments have the same quote currency, I report returns in that currency. If the group includes a mix of quote currencies, I further convert everything to USD (so I get USD returns). Reporting \uf0c1 Grouping Accounts \uf0c1 Returns are calculated jointly for each group of accounts in each \"report\", as defined in the configuration. Here are some example groupings that are sensible to define: Same instrument in different accounts. If you buy the same stock in different accounts, it makes sense to report on the returns for that stock jointly, across all your accounts. Same underlying. Some instruments represent the same stock, e.g. GOOG and GOOGL (different share class, same company). Also, IAU and GLD (Gold) are different ETFs whose values are both derived from physical gold reserves (located in bank basements in London). Same asset class. Instruments from the same asset class, e.g. \"metals\", which would include IAU, GLD, SLV, COPX, etc., or \"REITs\", which would include VNQ, VGSLX, etc. Or \"all stocks\" vs. \"all bonds\". By Strategy. In my portfolio investment method, I have a multi-headed approach where I define specific broad strategies and then select a list of instruments to implement it. For instance, I have a \"tech sector\" strategy, which includes FAANG companies. Or a \"growth stock\" strategy, which might include different indexes like VUG, IWO and RGAGX. I can report how well those strategies are doing, across brokers. Or geographically, \"developed APAC\", including EWY, EWT, EWS, EWA. By Broker. I can report returns by broker or broker account. In particular, this can be an easy way to separate realized profits by tax treatment (e.g., 401k is tax-deferred). Asset type. Comparing all index funds to all managed funds (e.g. mutual funds). Note that different reports can include the same investments. Groupings aren't exclusive. You define what makes most sense for your situation. For reference, I use more than 20 reporting groups. Running the Code \uf0c1 Simply call ./experiments/returns/compute_returns.py to generate all reports and debugging files, where is in the format shown in \u201cConfiguration\u201d . It's a little slow \u2014 some performance improvements are possible \u2014 but if you supply a list of report names after the final argument, only those investments and reports will be processed, so you can iterate faster that way. See flags with --help for details, and config.proto for the configuration input documentation. Results Rendered \uf0c1 For each reporting group, I currently produce: A plot of the cash flows over time , and a smaller plot of log (cash flows). Dividend payments are typically dwarfed by cash flows for the principal, so the log chart allows me to see timings. This is mainly used to get an overview of activity over time, and for debugging. A plot of cumulative value, where I render two curves: Cumulative cash flows , with a growth curve matching that returns I regressed over. The result should be a plot with gentle slope between cash flows (corresponding to the total returns growth), with a final drop to zero. Market value over time : A curve of the mark-to-market value of the portfolio over time. This allows me to make some sense of calendar returns, by witnessing how the asset value moves based on price. A table of total returns , returns without dividends, and returns from dividends only. A table of calendar returns for each year, also broken down by total, ex-dividends, dividends-only. (I'll probably render this as a plot in the future.) A table of trailing cumulative returns . This is going to get refined and augmented as I'm actively working on this code [September 2020]. Example \uf0c1 Here's an example report, for a subset of accounts with a \"growth\" focus, held at different brokers. I produce one of these for each reporting group. (I greyed out parts for privacy.) Interpretation Gotchas \uf0c1 A few notes are in order: All rates are annualized . This makes it easy to compare numbers to each other, but it also means that positions held for a short amount of time will produce numbers that are unrealistic for long term extrapolation. In particular, new positions entered only a few months ago may be subject to high growth or a big drop, both of which when extrapolated to an entire year may show eye-popping percentages. Do keep this in mind, especially when looking at recent positions added to your portfolio. Taxes aren't factored in. Returns from taxable accounts and tax-deferred accounts should be evaluated differently and if the difference in tax is large, they can't be compared so readily. Do keep in mind that in most countries gains are only taxed on realization (sale) so in effect, long held investments behave more or less like tax-deferred ones. Just don't sell so much. This is a great advantage in holding broadly diversified ETFs (and usually unquantified, as people's attention is still overly focused on those benefits from registered accounts, e.g., 401k plan). Cost basis. Note that nowhere in our calculations was the cost basis used or factored in, so don't confuse it with market value. The cost basis is only useful for tax-related effects. Other Instruments Types \uf0c1 Note that computing returns in this manner isn't limited to only stocks and bonds. Using the same methodology, we can include other types of instruments: P2P Lending \uf0c1 I've used LendingClub before all the hedge funds got involved to pick up the low-hanging fruit, and eventually let all the bonds I'd invested in expire. It was easy to apply the same methodology to compute returns from those investments. Originally, I had the discipline to record this investment from monthly PDF statements using transactions like this: 2016-10-31 * \"2016-10-31.Monthly_Statement.pdf\" Assets:US:LendingClub:FundsLent -451.52 LENCLUB {1 USD} Assets:US:LendingClub:Cash 451.52 USD Income:US:LendingClub:LoanInterest -21.68 USD Income:US:LendingClub:Recoveries -5.92 USD Expenses:Financial:Fees 1.08 USD ;; Recovery fees Expenses:Financial:Fees 4.71 USD ;; Service fees Expenses:Financial:Fees 0.45 USD ;; Collection fees Assets:US:LendingClub:Cash Assets:US:LendingClub:FundsLent -23.05 LENCLUB {1 USD} Income:US:LendingClub:ChargedOff 23.05 USD And later on, after the principal bonds expired, transactions like this: 2018-11-30 * \"2018-11-30.Monthly_Statement.pdf\" Income:US:LendingClub:Recoveries -2.73 USD Expenses:Financial:Fees 0.49 USD ;; Recovery fees Assets:US:LendingClub:Cash All it took to compute returns is this configuration for the investment: investment { currency: \"LENCLUB\" asset_account: \"Assets:US:LendingClub:FundsLent\" match_accounts: \"Income:US:LendingClub:Recoveries\" cash_accounts: \"Assets:US:LendingClub:Cash\" } Note that the match account configuration was necessary to pick up crumbs from later transactions with only recoveries (and no posting to the asset account). For what it's worth, I was able to generate a 6.75% return over this investment. Meh. Real Estate \uf0c1 Cash flows can be extracted from all transactions for a home, so you can compute the returns for all the money poured into your home, as if it was done purely for investment purposes. Usually buying a home is done for other reasons \u2014 stability for children, the ability to make your own improvements, the forced savings involved in regular principal payments, and oftentimes just a sense of \"having a home\" \u2014 but in the vast majority of the cases, home ownership is more of a cost center and returns would have been better invested elsewhere (see this book for a great expos\u00e9 of the pros & cons). Personally, I have better things to do than fix toilets and worry about leaky windows in the winter so I went back to renting, but I went through the experience once and it was quite worthwhile, as a learning experience but also to experience the \"joy\" of having my own place. Through the exercise is useful to calculate how much having your own home is actually costing you, and how much you might have made by putting the very same cash flows into the market instead. It's a little more involved, because, You will have to have discipline to segregate expenses you wouldn't have had if you'd have rented to accounts specific for your home. You will need to account for equivalent rent received that you didn't have to pay because you lived in the home. If you still own the home, you will have to simulate a substantial agent fee and other closing costs (by inserting a transaction). Most places have a large tax break when selling a property (sometimes the full capital appreciation), so that really needs to be factored in. It's not always simple to calculate, especially if you rented your property for some of its ownership lifetime, in which case you might only be able to deduct a part of the capital gains. For some, there is great value in the optionality of being able to move easily and at low cost (e..g, accepting a new job at the other side of the country) and that personal value will be difficult to estimate. This will require me to do more categorization work, and it will be best documented as a separate project, though using the same code. I did manage to create a simple configuration and extract a 5%-ish number out of it, but I think I'll need a bit more work to declare victory. More to come. Options \uf0c1 I sometimes hedge some of my portfolio downrisk with long-dated OTM PUT positions. I've sold OTM PUTs and CALLs in the near term to finance it. The method I described for stocks in this document works equally well for options, that is, extracting cash flows from cash. The main differences are that: Currency names. Instrument names are specific to each contract\u2014they include the expiration date and strike price\u2014so I don't store the options to an account whose name includes the instrument name at the leaf. I just use a generic leaf account name, such as \"Options\" or \"Hedging\" which I select when I enter/exit positions. Prices for options. Prices for options aren't as easily fetchable programmatically. I'm having to use a private API for this. Perhaps more importantly, declining theta, varying vol and non-linearity near the strike means I do need to have to have pretty recent price estimates in my ledger in order to compute returns on unrealized gains/losses. I think the effect is strong enough that I'd want to eventually have some code to always update the prices just before generating the reports. Future Work \uf0c1 This section describes desired future improvements on the returns code, which I'm likely to implement, and the corresponding challenges. Note: As I'm writing this in September 2020, I'm actively working on this code and probably will continue over the next few months' weekends. It's possible this document falls slightly out-of-date and that some of the proposals below become implemented. Refer to the source code for the latest. Relative Size over Time \uf0c1 Another useful bit of data I could easily add to reports is a stack plot of market values over time, as well as relative fractions of each investment in a report group. This would be useful for monitoring growth of particular instruments in a group to help with rebalancing. Comparison Against a Benchmark \uf0c1 One of the important barometers of success in running your own portfolio is a comparison to an equivalent investment of capital in a simple portfolio of diversified stocks and bonds rebalanced regularly. After all, if my returns aren't beating that, in a normal environment, one could make the argument I shouldn't bother with a more complicated allocation. Assuming that my access to capital is the same (which it is, because in this project I'm looking at savings from income), I could simply replace my cash flows by cash flows to this simulated portfolio, in other words, use the same timings to simulate buying other assets. I would have to exclude the dividend payments because those are specific to the particular instruments I used, and at the same time generate simulated dividends from similarly sized positions in the benchmark portfolio. It should be possible to do this without modifying my ledger. One issue is that I will require a set of accurate prices for the benchmark, at the dates of my historical cash flows. Like my script already does for aging price points, this could easily be stored in a file for fetching. Perhaps more complicated is the fact that Beancount does not currently support a database of per-share dividend distributions. This could be added without introducing new syntax by attaching and honoring metadata on Price directives, such as 2020-09-01 price LQD 136.16 USD distribution: 0.295 USD Overall, it wouldn't be too difficult to implement this. Including Uninvested Cash \uf0c1 One of the problems I'm facing in investing my own portfolio is the lack of discipline around timely investing of available cash, especially when times are uncertain and difficult decisions have to be made. In order to account for the drag on performance from that, I should include an \"investment\" that's purely reflecting the total amount of uninvested cash over time. Because this varies a lot, a good approximation can be obtained by sampling total amount of investable cash every month and synthesizing cash flows from differences. If that cash flow series gets included as part of a portfolio, it'll suitably drag its returns by diluting them. After-Tax Value \uf0c1 At the moment I export all my holdings to a Google Sheets doc using the export script , and from that, break it down between long-term and short-term positions vs. pre-tax, after-tax, Roth, and taxable buckets. From those 8 aggregates, I remove estimated taxes and report a single \"estimated after-tax net worth\" and corresponding tax liability. This is a rough estimate. The returns report is however much more detailed, and I could simulate tax payments not only on liquidation, but also at the end of each year (from dividends and sales). I have all the lot dates on each position held so I can automatically figure out short-term vs. long-term lots. Inflation Adjustments \uf0c1 The market value amounts reported on the returns charts and calendar returns do not account for inflation. Over long periods of time, those can make an important difference in returns. It would be useful to discount the returns over time using annual estimates for the CPI (or some other estimate of inflation; you could even make up your own, from the expenses in your ledger), so that we can look at a curve of real growth and not just nominal growth. Sales Commission \uf0c1 The configuration could easily be improved to let the user specify expected commissions on sales of investments, either in absolute or relative (%) amounts. This would be used to mark positions with more realistic liquidation values. This could make a difference on investments with either small amounts or large commissions (i.e., working through a regular broker, or on real estate). Risk Estimation & Beta \uf0c1 A perhaps more advanced topic would be to compute an estimate of the variance from the specific portfolio composition in order to calculate and report some measurement of risk, such as Sharpe Ratio . This would require a sufficient number of regularly spaced price points. Variation of the measures over time could be fun too, as well as calculating the current portfolio's specific sensitivity to market as a whole (beta). Conclusion \uf0c1 I had expected it would be possible to produce a clear picture of returns from Beancount data, and having done it I'm more satisfied with the level of detail and clarity I was to produce from my ledger than I had expected. This goes well beyond plotting of net worth over time, this actually works really well, and I can use it to compare the performance of different investments fairly. I hope at least some Beancount users will be able to run it on their ledgers and I'm looking forward to hearing some feedback from those who set it up. Perhaps most importantly, I was very surprised to see results on my own portfolio returns. I'm one of those people who would normally shrug and guess an underwhelming ballpark number if asked what I thought my returns were, such as \"meh, 6% or something, not super happy.\" Doing this work was originally driven by not having the right answer to this question. It turns out, over the last 15 years, that I've generated an almost 12% average annual return, and 14% annual return in the last 5 years. I haven't yet made the benchmark comparison, and for sure these numbers should be put side-by-side with the market for a fair assessment. Nevertheless, going through the exercise has provided me with renewed confidence and hope about the future, and I hope the clarity it will bring to other Beancount users' own investments will be similarly enlightening.","title":"Calculating Portfolio Returns"},{"location":"calculating_portolio_returns.html#calculating-portfolio-returns","text":"Martin Blais , Sept 2020 http://furius.ca/beancount/doc/returns This document describes how to compute portfolio returns from a Beancount ledger.","title":"Calculating Portfolio Returns"},{"location":"calculating_portolio_returns.html#motivation","text":"You will be surprised to find that discount brokers typically do not provide accurate and complete returns calculations for your investments based on your specific cash flows. They tend to report other measures of performance: Change in value. The simplest they provide is a snapshot of the account value at the beginning and end of the period (or year). The problem with this method is that it does not reflect your infusions or removal of cash as such, nor your changes in positions. For example, if you had an account with $50,000 at the beginning of the year and you've added in $30,000 in August, reporting a difference of $37,000 at the end of December is just not useful (you'd have to mentally discount for new invested cash during the year, and what if you want to break it down and compare the returns from different instruments in the account?). Underlying performance. They might report the growth of the underlying asset in isolation, disregarding your specific positions. It's not very useful to say \"HDV grew by 8.2% over the last year\" if you invested only (or even mostly) in the second half of the year. What I'd like is to know how much my specific investments grew, given my specific changes in positions and timings. In other words, how did I do? No dividends. Another issue is that performance is typically only reporting capital appreciation due to the change in price of the investment. Ideally one would like to break down the performance between capital appreciation and dividends and returns for each of those components, as well as overall performance, so you can compare returns from stocks and bonds to each other. You can sometimes find out that information from the Yield field in the summary, but for uneven distributions, this won't help me find out what my actual returns from dividends were, over any period. Commissions and management expenses. Some brokers charge management expenses on a monthly basis, based on the value of the account. I would like to know what my portfolio return is net of those expenses. I want accurate returns reporting, based on my actual cash flows and changes of positions over time. If I maintain your investment information in a Beancount ledger, in theory it contains all the data I need in order to compute your true returns, based on the specific timings of my own savings (cash infusions) and which positions I held at which time. It's just not in the simplest format required to do it\u2014 Beancount transactions are much more flexible than one might want and a simpler series of cash flows needs to be extracted from it. This document explains how I finally did this from my own Ledger. And how we might generalize this to yours, based on some simple rules. Most of this text is dedicated to the pedestrian details of extracting the right data. The source code can be found here . In addition, a fair and honest comparison to other investments scenarios should be possible, based on those same cash flows. For instance, you should be able to produce data that looks like \"My investments in ZZZ have returned 8.2%, 1.1% of which were from dividends, and if I'd invested in a 60/40 portfolio of broad stocks and bonds it would have returned 7.2% instead.\" In other words, I want to assess my performance relative to a number of common alternatives. (Finally, note that if all you need is a snapshot of your current positions, that's already handled by the export script .)","title":"Motivation"},{"location":"calculating_portolio_returns.html#history","text":"In 2014, I made a brief attempt to break down information from my ledger to do this. At the time I got bogged down in details when some of the time-series I was extracting weren't producing what looked like sensible results (some with outliers). I got too detailed too fast. Sometimes it's better to just get the whole job done and come back for the details. I hadn't logged enough debugging information and I didn't have enough confidence in its output to use it. I never actually finished the work at the time, and eventually moved the scripts to experiments and shrugged. \"Later.\" In August 2020, I sat down to do this again, this time with a less ambitious goal of just getting a good approximation and producing lots of debug output, boiling down extraction to pull out just the cash flows and to get the job complete over all my accounts, even if it meant making some adjustments to my input file. It turned out to be the right decision: I managed to complete the task, and this document presents my journey, the method, its assumptions, quirks, and some results.","title":"History"},{"location":"calculating_portolio_returns.html#overview-of-method","text":"The method I'll use is to extract cash flow information for each investment. What I call \" an investment \" in this context is a kind of financial instrument , e.g., shares of \"VTI\" for the Vanguard Total Stock Market ETF, invested in a particular account , e.g. a Vanguard 401k. A list of cash flows is a dated list of positive or negative currency being invested or withdrawn as proceeds to/from that investment. So I have a list of cash flows per account, and each account records only one financial instrument. For reporting I'll group those per-account series in logical units of \" reports \", for example lumping together ones for the same instrument bought in different accounts / brokers, or different instruments that represent the same underlying by merging their cash flows. Or even to group them by \"strategy\". Basically, by merging the cash flows of multiple accounts, I have a generic way to group any subsets of accounts and compute their returns. Using the cash flows, I will then run a simple root finding routine to calculate the average annual rate those flows would have to grow in order to result in their final market value. This provides me with overall returns. This is similar to calculating the Internal Rate of Return . I will do that for the entire time series, but also for sub-intervals within the time series to compute, e.g. calendar returns (i.e., each year or quarter) or cumulative returns for trailing periods. Since cash flows are flagged as dividends or not, I can separate the returns from appreciation from the returns from dividends. Reports with plots are produced for each of the groups. Here's a diagram that shows how the \"configure\", \"compute_returns\" and \"download_prices\" scripts work together: These will be further detailed below.","title":"Overview of Method"},{"location":"calculating_portolio_returns.html#configuration","text":"First, a configuration needs to be created to define a list of investments, and groups of those, for which reports will be produced. This configuration is provided as a text-formatted protocol buffer message. Investment. An investment corresponds to a particular instrument stored in a particular account. It also involves other transactions that don't directly involve that particular account. We want to provide a few set of account names: Asset account. The name of the account holding the commodities for that investment. Matching accounts. A list of additional accounts which will select transactions with postings to them. Cash accounts. A list of accounts external to the investment, which will be used to flag inflows and outflows to and from the investment. Report. While we can compute returns for each investment, the more general and useful case is to compute returns on groups of them. A report is simply a list of investment names. A PDF file will be produced for each report. Here's an example of 6 investments and 3 reports: investments { # Accounts at BTrade. investment { currency: \"GLD\" asset_account: \"Assets:US:BTrade:GLD\" cash_accounts: \"Assets:US:BTrade:Cash\" } investment { currency: \"AMZN\" asset_account: \"Assets:US:BTrade:AMZN\" cash_accounts: \"Assets:US:BTrade:Cash\" } investment { currency: \"QQQ\" asset_account: \"Assets:US:BTrade:QQQ\" dividend_accounts: \"Income:US:BTrade:QQQ:Dividend\" cash_accounts: \"Assets:US:BTrade:Cash\" } # Accounts at IBKR. investment { currency: \"IAU\" asset_account: \"Assets:US:IBKR:IAU\" cash_accounts: \"Assets:US:IBKR:Cash\" } investment { currency: \"SLV\" asset_account: \"Assets:US:IBKR:SLV\" cash_accounts: \"Assets:US:IBKR:Cash\" } # Accounts at Schwab. investment { currency: \"GOOG\" asset_account: \"Assets:US:Schwab:GOOG\" cash_accounts: \"Assets:US:Schwab:Cash\" cash_accounts: \"Assets:AccountsReceivable\" cash_accounts: \"Assets:US:GoogleInc:GSURefund\" } } groups { group { name: \"strategy.gold\" investment: \"Assets:US:BTrade:GLD\" investment: \"Assets:US:IBKR:IAU\" } group { name: \"strategy.tech\" investment: \"Assets:US:BTrade:QQQ\" investment: \"Assets:US:BTrade:FB\" investment: \"Assets:US:Schwab:GOOG\" } group { name: \"all\" investment: \"Assets:US:*\" currency: \"USD\" } group { name: \"accounts.BTrade\" investment: \"Assets:US:BTrade:*\" currency: \"USD\" } } Different reports can include the same investment. References to accounts and investment names support simple UNIX-style globbing patterns in the configuration. These are expanded to full account names at runtime and stored in the output. A \" configure.py \" script can automatically infer a working basic configuration from an existing Beancount ledger. A report will be generated for each unique instrument and the same metadata fields honored by the \"export\" script (\"assetcls\", \"strategy\") will also generate reports. I recommend you run this on your ledger and then custom tailor the configuration manually.","title":"Configuration"},{"location":"calculating_portolio_returns.html#finding-accounts","text":"This script needs to figure out the list of available investments to report on. By convention I keep pairs of dedicated leaf accounts for each commodity type in my ledger, one to contain the actual positions (assets) and one to receive dividends, e.g., for \"VTI\" held in broker \"BTrade\", accounts like 2012-03-01 open Assets:US:BTrade:VTI VTI 2012-03-01 open Income:US:BTrade:VTI:Dividend USD This has two consequences: (a) it makes it easy to find the list of accounts that contain investments (any account with a leaf account name that is also one of the commodities found in the ledger), and (b) it nicely isolates all the activity related to each of those investments by finding all the transactions with a posting to that account . I recommend you follow the same convention in your chart of accounts. The result is a list of accounts like \"Assets:US:BTrade:VTI\", specific to each (instrument, institution). I further filter down this list to the subset of accounts which were still open up to 15 years ago (I close my accounts\u2014using Beancount's Close directive\u2014when they're done). In my particular case, I didn't have much savings back then, and there's no point in bothering to do the work to normalize those crumbs in my investing history for that far back.","title":"Finding Accounts"},{"location":"calculating_portolio_returns.html#extracting-cash-flow-data","text":"This section describes the various steps I took to extract relevant data from my ledger.","title":"Extracting Cash Flow Data"},{"location":"calculating_portolio_returns.html#extracting-relevant-transactions","text":"For each of the identified asset accounts, we want to pull out from the ledger's transactions the list of transactions affecting that account. We simply run through the entire ledger's transactions keeping transactions with at least one posting to the investment's asset account, dividend income accounts, or to any of the other \"match accounts\" defined for it. For instance, transactions for cash dividend payments will not show an asset posting so if dividends are paid; a typical dividend payment would contain only the dividend income posting and a cash posting (for the deposit): 2019-11-27 * \"Dividend\" Income:US:BTrade:VTI:Dividend -123.45 USD Assets:US:BTrade:Cash 123.45 USD So it is necessary to include the dividend account in the configuration to include those transactions, because they typically do not involve the asset account.","title":"Extracting Relevant Transactions"},{"location":"calculating_portolio_returns.html#account-categorization","text":"The next step is to generalize transactions to templates : we record the full set of accounts involved in the extracted transactions of each investment, and assign them to general categories based on their role in the transaction. For example, if I inspect my \"VTI\" trades, I will encounter the following accounts: Assets:US:BTrade:Cash Assets:US:BTrade:VTI Expenses:Financial:Commissions Income:US:BTrade:VTI:Dividend I map each account to one of several generic categories (I could probably simplify this now): ASSET # The account holding the commodity. CASH # Cash accounts, employer matches, contributions. DIVIDEND # Dividend income account. EXPENSES # Commissions, fees and other expenses. INCOME # Non-dividend income, P/L, gains, or other. OTHERASSET # Other assets than the primary asset for this investment. OTHER # Any other account. Like this: 'Assets:US:BTrade:Cash': CASH 'Assets:US:BTrade:VTI': ASSET 'Expenses:Financial:Commissions': EXPENSES 'Income:US:BTrade:VTI:Dividend': DIVIDEND In this way, I can compare similar transactions to each other across instruments and extract information from them with the same code. For example, a transaction that involves a dividend account and a cash account is a cash dividend payment, and I can write a generic handler to extract the cash flows for this. The categorization was originally prototyped with a set of ad-hoc rules, but the configuration now serves to provide the categorization. Note: In the process of doing this, I noticed many irregularities in how I named my accounts. For example, I used \":Dividend\" and \":Dividends\" sometimes. I went through my ledger and had to make some changes to name accounts coherently, and iterated until all my accounts were categorized correctly. You may have to review some of your data entry as well.","title":"Account Categorization"},{"location":"calculating_portolio_returns.html#handling-transactions-using-the-signature","text":"Using the account-category mappings from the previous section, I was able to derive a unique \"signature\" for each transaction. For example, a transaction like this: 2020-03-12 * \"(DOI) ORDINARY DIVIDEND\" Income:US:BTrade:VTI:Dividend -1312.31 USD Assets:US:BTrade:Cash 1312.31 USD would have a signature of CASH_DIVIDEND which is hopefully always a dividend payment. Beancount has a pretty flexible syntax and does not enforce that your transactions follow particular templates like this, so it wasn't clear to me when I started this project what patterns I would find in my ledger of 12 years of ad-hoc data entry\u2026 I wasn't certain that this categorization and these signatures would be sufficient to correctly handle a correct conversion to cash flows. So I had my script produce two sets of files for debugging: Investment details. A file for each investment , rendering a list of all the transactions that were extracted for it, decorated with metadata showing the categorizations inferred on each posting, as well as a categorization map of all the accounts encountered. I inspected these files visually to ensure that the account/patterns from the configuration were extracting the full and correct set of transactions involved in that investment. Signature transactions. A file for each unique signature , with the full list of transactions matching that signature across all investments. By inspecting these files, I made sure that all the transactions matching the same signature were indeed playing the same role, so that a single handler per signature is sufficient. At this point, I had a limited list of unique signatures, each with clear unique roles: ASSET_CASH : Purchase or sale ASSET_CASH_EXPENSES : Purchase or sale with commission ASSET_CASH_INCOME : Purchase or sale with profit ASSET_CASH_INCOME_EXPENSES : Purchase or sale with commission and profit ASSET_EXPENSES : Fee paid from liquidation ASSET_INCOME : Cost basis adjustment (with P/L) ASSET_INCOME_EXPENSES : Fee from liquidation (with P/L) ASSET : Stock splits ASSET_DIVIDEND : Dividend reinvested CASH_DIVIDEND : Dividend payment CASH_INCOME_DIVIDEND : Dividend payment and gains distribution ASSET_OTHERASSET : Exchange of stock/symbol \u2026 Note that the specific list really depends on the particular contents of your ledger and you should inspect the files produced for correctness. I then wrote specific handlers to produce the cash flows corresponding to each of those transaction signatures, reasoning about each of those cases in isolation. This allowed me to correctly produce a full list of cash flows per investment. Note: In practice I encountered 3 or 4 more signatures types that were a bit exotic and by fixing up my ledger I managed to either correct or break apart these transactions to equivalent but simpler ones. In particular, one of my importers was lumping together trades occurring on the same day, and I went back and fixed it and those transactions manually. The ASSET_OTHERASSET signature, in particular, is an exchange of stock (Google -> GOOG,GOOGL). Doing something like this brings up idiosyncrasies in your bookkeeping technique. Being consistent and using fewer templates is helpful. It would be a valuable idea for an accompanying plugin to restrict the possible set of templates to a select few, so that data entry is constrained to work well with this returns production code.","title":"Handling Transactions using the Signature"},{"location":"calculating_portolio_returns.html#generalizing-production-of-cash-flows","text":"After inspecting each of my signature handlers, I tried to generalize them to a single unified handler that would work across all transactions. It turns out that, at least with my existing ledger's transactions, it's possible. Essentially, recording inflows or outflows to cash accounts or other assets is sufficient. In a transaction like this: 2013-09-18 * \"Buy shares of HOOL\" Assets:US:BTrade:Cash -818.55 USD flow: CASH Assets:US:BTrade:HOOL 8 HOOL {101.20 USD} flow: ASSET Expenses:Financial:Commissions 8.95 USD flow: EXPENSES The \"CASH\" posting is a sufficient incoming cash flow, so we record -818.55 USD. In a cash dividend payment: 2013-12-17 * \"Cash Dividend payment\" Assets:US:BTrade:Cash 38.50 USD flow: CASH Income:US:BTrade:HOOL:Dividends -38.50 USD flow: DIVIDEND Similarly the 38.50 is a sufficient outgoing cash flow, so we record +38.50 USD. On the other hand a reinvested asset dividend, as you would find in some mutual funds, does not generate any cash flow; it simply remains in the investment and increases its total value: 2013-12-30 * \"Reinvested dividend\" Assets:US:BTrade:HOOL 0.356 {103.41} USD flow: ASSET Income:US:BTrade:HOOL:Dividends -36.81 USD flow: DIVIDEND This rule seems sufficient to handle all the contents of my ledger correctly. In the end, I implemented both methods: I use the general rule to produce the list of cash flows, but I also call out to the explicit handlers and cross-check that the extracted cash flows are identical, just to be sure. This is enabled by a flag in compute_returns.py ( --check-explicit-flows ). This forces me to ensure that I've analyzed all the possible transaction templates. Note: If in using this script you find cases from your ledger that aren't handled by using a series of cash accounts, please let me know (on the mailing-list).","title":"Generalizing Production of Cash Flows"},{"location":"calculating_portolio_returns.html#cash-flows","text":"The handlers described in the previous section each produced a list of cash flows per transaction, and together for the account, they are essentially a list of: (Date, Amount, IsDividend) Now, this is a simpler model to work from. For each account, we now have a sorted series of dated cash flows. Note that Amount includes its cost currency (I have both USD and CAD), IsDividend is a flag identifying the cash flow as being a dividend payment or not (to compute returns without the dividends). These series of cash flows can be easily merged between accounts, and truncated over time by inserting initial or final cash flows corresponding to the market value at those dates. Rendered, they might look like this (because of the scale, rendering the log brings up detail that is otherwise difficult to see; dividends are rendered in green): Note that since many transactions do not generate cash flows, the list of cash flows of an investment is insufficient by itself to compute the value of the investment over time. When truncating for a time interval, the market value of the investment is derived using the list of transactions. Finally, the list of cash flows for each group of investments reported can be trivially merged by concatenating them.","title":"Cash Flows"},{"location":"calculating_portolio_returns.html#computing-returns","text":"","title":"Computing Returns"},{"location":"calculating_portolio_returns.html#calculating-the-average-growth-rate","text":"For each series of cash flows, the cash flows are merged together. I use scipy.optimize.fsolve to calculate the rate that satisfies net present value: c f i /(1\u2005+\u2005 r ) t i = 0 where cf i are the signed cash flow amounts and t i are the times from today for each cash flow (in years). We solve for r . To compute the returns without dividends, we just exclude cash flows returned from dividends. The difference tells us how much of our returns was due solely to dividend income. It's important to note that if the corresponding positions are still invested, you have to insert a final negative cash flow for the market value at the latest date to zero it out. You're essentially simulating a sale. If significant transaction costs are going to be involved, you might want to simulate those as well (e.g. if you're doing this for a home, in particular). Here's the beauty in this: nowhere was the underlying price used , except for marking the current position value to market value. We did not read any external measure of returns. These returns are computed from cash going in and coming out . These are actual realized returns. They do not lie. Intervals. To compute calendar returns, e.g., returns for years 2016, 2017, 2018, 2019, 2020, I truncate the cash flows to keep only those inside the interval, e.g. 2018-01-01 to 2018-12-31 for year 2018, and if there was an existing position at the beginning of the interval, insert a negative cash flow at the start of the series equivalent to the market value at those dates. I do the same thing at the end of the interval, with a positive cash flow, as described previously. Ideally I'd like to look at different sets of intervals: Lifetime of investment. Total returns over the entire lifetime of the positions, to boil it all down to a single number. Calendar. Annual or quarterly returns over the last 10 or 15 years, to witness variation in returns over time. Cumulative. Cumulative returns over the last 10 or 15 years, aligned on calendar periods, to get a sense of whether things are improving or worsening, and how well my strategies are doing in more recent periods (e.g. last 3 years). High frequency cumulative. Cumulative returns over the last 12 months, aligned with monthly or weekly dates, to assess the impact of decisions in the short-term. These can be rendered in tables, and investments can be compared to each other in that way.","title":"Calculating the Average Growth Rate"},{"location":"calculating_portolio_returns.html#filling-in-missing-price-points","text":"The calculations of returns over various intervals require marking positions to market value at the beginning and end of the interval dates. Beancount being designed hermetically, i.e., it does not fetch any price externally, only uses what's in the ledger, its price database lookup will automatically produce the last available price point and its recording date before the requested date. Depending on your recording discipline, some of those prices might be out-of-date and introduce inaccuracies. This is especially important since the amounts converted at the end of periods (i.e. to estimate the value of current positions) can be large and meaningfully influence even the lifetime returns number. So it's important to have relatively fresh price points in the ledger's price database. Now the question is, given a changing set of positions over time, for a given set of interval dates, which price entries are required to produce accurate results? Because this depends heavily on the particular inputs of the returns script, in order to solve this problem I simply wrapped the price database with a facade that collects all the (instrument, date) pairs for requested conversions during the production of the reports, and filter those down by some age threshold (e.g., price not older than 3 days). These are essentially the price points missing from the file. At the end of the script, I output these to a file with Price directives, and another program (download_prices.py) can read that file and fetch historical rates for those. It produces updated rates which you can paste to your ledger file as a one-off adjustment and then recompute more accurate returns. Pulling data from Yahoo! Finance worked for 90% of my positions, but some of my older instruments were quite old or even retired, or not available (e.g., some retirement funds), so I had to find them by browsing and manually entering some of these price points (I had something like 30\u2026 no big deal). Rolling windows. One important point is that going forward, it will be easier to align reporting intervals to some calendar-based interval (e.g., monthly), so that I don't have to regenerate price data every time I produce my returns. Aligning to months is probably good enough for my time horizon. Stock Splits. Beancount does not explicitly make adjustments of prices for stocks that split, so your price source should return the pre-split price of the stock at the time if you are recording it as such. You should be able to check your price time series for errors using Fava.","title":"Filling in Missing Price Points"},{"location":"calculating_portolio_returns.html#currency-conversion","text":"Another important detail is that each investment has its own quote currency. I used to live in Canada, and some of my older investments are denominated in CAD. So the question arises: do I compute the returns in local (CAD) currency, or in terms of my reference currency (USD)? It's convenient that Beancount's Inventory object has functions that can easily perform those conversions where needed. And since the cash flows I extracted are stored using Beancount's Amount object, I already have the quote currencies correctly in my extracted dataset. In any group, if all the instruments have the same quote currency, I report returns in that currency. If the group includes a mix of quote currencies, I further convert everything to USD (so I get USD returns).","title":"Currency Conversion"},{"location":"calculating_portolio_returns.html#reporting","text":"","title":"Reporting"},{"location":"calculating_portolio_returns.html#grouping-accounts","text":"Returns are calculated jointly for each group of accounts in each \"report\", as defined in the configuration. Here are some example groupings that are sensible to define: Same instrument in different accounts. If you buy the same stock in different accounts, it makes sense to report on the returns for that stock jointly, across all your accounts. Same underlying. Some instruments represent the same stock, e.g. GOOG and GOOGL (different share class, same company). Also, IAU and GLD (Gold) are different ETFs whose values are both derived from physical gold reserves (located in bank basements in London). Same asset class. Instruments from the same asset class, e.g. \"metals\", which would include IAU, GLD, SLV, COPX, etc., or \"REITs\", which would include VNQ, VGSLX, etc. Or \"all stocks\" vs. \"all bonds\". By Strategy. In my portfolio investment method, I have a multi-headed approach where I define specific broad strategies and then select a list of instruments to implement it. For instance, I have a \"tech sector\" strategy, which includes FAANG companies. Or a \"growth stock\" strategy, which might include different indexes like VUG, IWO and RGAGX. I can report how well those strategies are doing, across brokers. Or geographically, \"developed APAC\", including EWY, EWT, EWS, EWA. By Broker. I can report returns by broker or broker account. In particular, this can be an easy way to separate realized profits by tax treatment (e.g., 401k is tax-deferred). Asset type. Comparing all index funds to all managed funds (e.g. mutual funds). Note that different reports can include the same investments. Groupings aren't exclusive. You define what makes most sense for your situation. For reference, I use more than 20 reporting groups.","title":"Grouping Accounts"},{"location":"calculating_portolio_returns.html#running-the-code","text":"Simply call ./experiments/returns/compute_returns.py to generate all reports and debugging files, where is in the format shown in \u201cConfiguration\u201d . It's a little slow \u2014 some performance improvements are possible \u2014 but if you supply a list of report names after the final argument, only those investments and reports will be processed, so you can iterate faster that way. See flags with --help for details, and config.proto for the configuration input documentation.","title":"Running the Code"},{"location":"calculating_portolio_returns.html#results-rendered","text":"For each reporting group, I currently produce: A plot of the cash flows over time , and a smaller plot of log (cash flows). Dividend payments are typically dwarfed by cash flows for the principal, so the log chart allows me to see timings. This is mainly used to get an overview of activity over time, and for debugging. A plot of cumulative value, where I render two curves: Cumulative cash flows , with a growth curve matching that returns I regressed over. The result should be a plot with gentle slope between cash flows (corresponding to the total returns growth), with a final drop to zero. Market value over time : A curve of the mark-to-market value of the portfolio over time. This allows me to make some sense of calendar returns, by witnessing how the asset value moves based on price. A table of total returns , returns without dividends, and returns from dividends only. A table of calendar returns for each year, also broken down by total, ex-dividends, dividends-only. (I'll probably render this as a plot in the future.) A table of trailing cumulative returns . This is going to get refined and augmented as I'm actively working on this code [September 2020].","title":"Results Rendered"},{"location":"calculating_portolio_returns.html#example","text":"Here's an example report, for a subset of accounts with a \"growth\" focus, held at different brokers. I produce one of these for each reporting group. (I greyed out parts for privacy.)","title":"Example"},{"location":"calculating_portolio_returns.html#interpretation-gotchas","text":"A few notes are in order: All rates are annualized . This makes it easy to compare numbers to each other, but it also means that positions held for a short amount of time will produce numbers that are unrealistic for long term extrapolation. In particular, new positions entered only a few months ago may be subject to high growth or a big drop, both of which when extrapolated to an entire year may show eye-popping percentages. Do keep this in mind, especially when looking at recent positions added to your portfolio. Taxes aren't factored in. Returns from taxable accounts and tax-deferred accounts should be evaluated differently and if the difference in tax is large, they can't be compared so readily. Do keep in mind that in most countries gains are only taxed on realization (sale) so in effect, long held investments behave more or less like tax-deferred ones. Just don't sell so much. This is a great advantage in holding broadly diversified ETFs (and usually unquantified, as people's attention is still overly focused on those benefits from registered accounts, e.g., 401k plan). Cost basis. Note that nowhere in our calculations was the cost basis used or factored in, so don't confuse it with market value. The cost basis is only useful for tax-related effects.","title":"Interpretation Gotchas"},{"location":"calculating_portolio_returns.html#other-instruments-types","text":"Note that computing returns in this manner isn't limited to only stocks and bonds. Using the same methodology, we can include other types of instruments:","title":"Other Instruments Types"},{"location":"calculating_portolio_returns.html#p2p-lending","text":"I've used LendingClub before all the hedge funds got involved to pick up the low-hanging fruit, and eventually let all the bonds I'd invested in expire. It was easy to apply the same methodology to compute returns from those investments. Originally, I had the discipline to record this investment from monthly PDF statements using transactions like this: 2016-10-31 * \"2016-10-31.Monthly_Statement.pdf\" Assets:US:LendingClub:FundsLent -451.52 LENCLUB {1 USD} Assets:US:LendingClub:Cash 451.52 USD Income:US:LendingClub:LoanInterest -21.68 USD Income:US:LendingClub:Recoveries -5.92 USD Expenses:Financial:Fees 1.08 USD ;; Recovery fees Expenses:Financial:Fees 4.71 USD ;; Service fees Expenses:Financial:Fees 0.45 USD ;; Collection fees Assets:US:LendingClub:Cash Assets:US:LendingClub:FundsLent -23.05 LENCLUB {1 USD} Income:US:LendingClub:ChargedOff 23.05 USD And later on, after the principal bonds expired, transactions like this: 2018-11-30 * \"2018-11-30.Monthly_Statement.pdf\" Income:US:LendingClub:Recoveries -2.73 USD Expenses:Financial:Fees 0.49 USD ;; Recovery fees Assets:US:LendingClub:Cash All it took to compute returns is this configuration for the investment: investment { currency: \"LENCLUB\" asset_account: \"Assets:US:LendingClub:FundsLent\" match_accounts: \"Income:US:LendingClub:Recoveries\" cash_accounts: \"Assets:US:LendingClub:Cash\" } Note that the match account configuration was necessary to pick up crumbs from later transactions with only recoveries (and no posting to the asset account). For what it's worth, I was able to generate a 6.75% return over this investment. Meh.","title":"P2P Lending"},{"location":"calculating_portolio_returns.html#real-estate","text":"Cash flows can be extracted from all transactions for a home, so you can compute the returns for all the money poured into your home, as if it was done purely for investment purposes. Usually buying a home is done for other reasons \u2014 stability for children, the ability to make your own improvements, the forced savings involved in regular principal payments, and oftentimes just a sense of \"having a home\" \u2014 but in the vast majority of the cases, home ownership is more of a cost center and returns would have been better invested elsewhere (see this book for a great expos\u00e9 of the pros & cons). Personally, I have better things to do than fix toilets and worry about leaky windows in the winter so I went back to renting, but I went through the experience once and it was quite worthwhile, as a learning experience but also to experience the \"joy\" of having my own place. Through the exercise is useful to calculate how much having your own home is actually costing you, and how much you might have made by putting the very same cash flows into the market instead. It's a little more involved, because, You will have to have discipline to segregate expenses you wouldn't have had if you'd have rented to accounts specific for your home. You will need to account for equivalent rent received that you didn't have to pay because you lived in the home. If you still own the home, you will have to simulate a substantial agent fee and other closing costs (by inserting a transaction). Most places have a large tax break when selling a property (sometimes the full capital appreciation), so that really needs to be factored in. It's not always simple to calculate, especially if you rented your property for some of its ownership lifetime, in which case you might only be able to deduct a part of the capital gains. For some, there is great value in the optionality of being able to move easily and at low cost (e..g, accepting a new job at the other side of the country) and that personal value will be difficult to estimate. This will require me to do more categorization work, and it will be best documented as a separate project, though using the same code. I did manage to create a simple configuration and extract a 5%-ish number out of it, but I think I'll need a bit more work to declare victory. More to come.","title":"Real Estate"},{"location":"calculating_portolio_returns.html#options","text":"I sometimes hedge some of my portfolio downrisk with long-dated OTM PUT positions. I've sold OTM PUTs and CALLs in the near term to finance it. The method I described for stocks in this document works equally well for options, that is, extracting cash flows from cash. The main differences are that: Currency names. Instrument names are specific to each contract\u2014they include the expiration date and strike price\u2014so I don't store the options to an account whose name includes the instrument name at the leaf. I just use a generic leaf account name, such as \"Options\" or \"Hedging\" which I select when I enter/exit positions. Prices for options. Prices for options aren't as easily fetchable programmatically. I'm having to use a private API for this. Perhaps more importantly, declining theta, varying vol and non-linearity near the strike means I do need to have to have pretty recent price estimates in my ledger in order to compute returns on unrealized gains/losses. I think the effect is strong enough that I'd want to eventually have some code to always update the prices just before generating the reports.","title":"Options"},{"location":"calculating_portolio_returns.html#future-work","text":"This section describes desired future improvements on the returns code, which I'm likely to implement, and the corresponding challenges. Note: As I'm writing this in September 2020, I'm actively working on this code and probably will continue over the next few months' weekends. It's possible this document falls slightly out-of-date and that some of the proposals below become implemented. Refer to the source code for the latest.","title":"Future Work"},{"location":"calculating_portolio_returns.html#relative-size-over-time","text":"Another useful bit of data I could easily add to reports is a stack plot of market values over time, as well as relative fractions of each investment in a report group. This would be useful for monitoring growth of particular instruments in a group to help with rebalancing.","title":"Relative Size over Time"},{"location":"calculating_portolio_returns.html#comparison-against-a-benchmark","text":"One of the important barometers of success in running your own portfolio is a comparison to an equivalent investment of capital in a simple portfolio of diversified stocks and bonds rebalanced regularly. After all, if my returns aren't beating that, in a normal environment, one could make the argument I shouldn't bother with a more complicated allocation. Assuming that my access to capital is the same (which it is, because in this project I'm looking at savings from income), I could simply replace my cash flows by cash flows to this simulated portfolio, in other words, use the same timings to simulate buying other assets. I would have to exclude the dividend payments because those are specific to the particular instruments I used, and at the same time generate simulated dividends from similarly sized positions in the benchmark portfolio. It should be possible to do this without modifying my ledger. One issue is that I will require a set of accurate prices for the benchmark, at the dates of my historical cash flows. Like my script already does for aging price points, this could easily be stored in a file for fetching. Perhaps more complicated is the fact that Beancount does not currently support a database of per-share dividend distributions. This could be added without introducing new syntax by attaching and honoring metadata on Price directives, such as 2020-09-01 price LQD 136.16 USD distribution: 0.295 USD Overall, it wouldn't be too difficult to implement this.","title":"Comparison Against a Benchmark"},{"location":"calculating_portolio_returns.html#including-uninvested-cash","text":"One of the problems I'm facing in investing my own portfolio is the lack of discipline around timely investing of available cash, especially when times are uncertain and difficult decisions have to be made. In order to account for the drag on performance from that, I should include an \"investment\" that's purely reflecting the total amount of uninvested cash over time. Because this varies a lot, a good approximation can be obtained by sampling total amount of investable cash every month and synthesizing cash flows from differences. If that cash flow series gets included as part of a portfolio, it'll suitably drag its returns by diluting them.","title":"Including Uninvested Cash"},{"location":"calculating_portolio_returns.html#after-tax-value","text":"At the moment I export all my holdings to a Google Sheets doc using the export script , and from that, break it down between long-term and short-term positions vs. pre-tax, after-tax, Roth, and taxable buckets. From those 8 aggregates, I remove estimated taxes and report a single \"estimated after-tax net worth\" and corresponding tax liability. This is a rough estimate. The returns report is however much more detailed, and I could simulate tax payments not only on liquidation, but also at the end of each year (from dividends and sales). I have all the lot dates on each position held so I can automatically figure out short-term vs. long-term lots.","title":"After-Tax Value"},{"location":"calculating_portolio_returns.html#inflation-adjustments","text":"The market value amounts reported on the returns charts and calendar returns do not account for inflation. Over long periods of time, those can make an important difference in returns. It would be useful to discount the returns over time using annual estimates for the CPI (or some other estimate of inflation; you could even make up your own, from the expenses in your ledger), so that we can look at a curve of real growth and not just nominal growth.","title":"Inflation Adjustments"},{"location":"calculating_portolio_returns.html#sales-commission","text":"The configuration could easily be improved to let the user specify expected commissions on sales of investments, either in absolute or relative (%) amounts. This would be used to mark positions with more realistic liquidation values. This could make a difference on investments with either small amounts or large commissions (i.e., working through a regular broker, or on real estate).","title":"Sales Commission"},{"location":"calculating_portolio_returns.html#risk-estimation-beta","text":"A perhaps more advanced topic would be to compute an estimate of the variance from the specific portfolio composition in order to calculate and report some measurement of risk, such as Sharpe Ratio . This would require a sufficient number of regularly spaced price points. Variation of the measures over time could be fun too, as well as calculating the current portfolio's specific sensitivity to market as a whole (beta).","title":"Risk Estimation & Beta"},{"location":"calculating_portolio_returns.html#conclusion","text":"I had expected it would be possible to produce a clear picture of returns from Beancount data, and having done it I'm more satisfied with the level of detail and clarity I was to produce from my ledger than I had expected. This goes well beyond plotting of net worth over time, this actually works really well, and I can use it to compare the performance of different investments fairly. I hope at least some Beancount users will be able to run it on their ledgers and I'm looking forward to hearing some feedback from those who set it up. Perhaps most importantly, I was very surprised to see results on my own portfolio returns. I'm one of those people who would normally shrug and guess an underwhelming ballpark number if asked what I thought my returns were, such as \"meh, 6% or something, not super happy.\" Doing this work was originally driven by not having the right answer to this question. It turns out, over the last 15 years, that I've generated an almost 12% average annual return, and 14% annual return in the last 5 years. I haven't yet made the benchmark comparison, and for sure these numbers should be put side-by-side with the market for a fair assessment. Nevertheless, going through the exercise has provided me with renewed confidence and hope about the future, and I hope the clarity it will bring to other Beancount users' own investments will be similarly enlightening.","title":"Conclusion"},{"location":"command_line_accounting_cookbook.html","text":"Command-line Accounting Cookbook \uf0c1 Martin Blais , July 2014 http://furius.ca/beancount/doc/cookbook Introduction A Note of Caution Account Naming Conventions Choosing an Account Type Choosing Opening Dates How to Deal with Cash Cash Withdrawals Tracking Cash Expenses Salary Income Employment Income Accounts Booking Salary Deposits Vacation Hours 401k Contributions Vesting Stock Grants Other Benefits Points Food Benefits Currency Transfers & Conversions Investing and Trading Accounts Setup Funds Transfers Making a Trade Receiving Dividends Conclusion Introduction \uf0c1 The best way to learn the double-entry method is to look at real-world examples. The method is elegant, but it can seem unintuitive to the newcomer how transactions have to be posted in order to perform the various operations that one needs to do in counting for different types financial events. This is why I wrote this cookbook. It is not meant to be a comprehensive description of all the features supported, but rather a set of practical guidelines to help you solve problems. I think this will likely be the most useful document in the Beancount documentation set! All the examples here apply to any double-entry accounting system: Ledger, GnuCash, or even commercial systems. Some of the details may differ only slightly. This cookbook is written using the syntax and calculation method of the Beancount software. This document also assumes that you are already familiar with the general balancing concepts of the double-entry method and with at least some of the syntax of Beancount which is available from its user\u2019s manual or its cheat sheet . If you haven\u2019t begun writing down your first file, you will want to read Getting Started with Beancount and do that first. Command-line accounting systems are agnostic about the types of things they can count and allow you to get creative with the kinds of units that you can invent to track various kinds of things. For instance, you can count \u201cIRA contribution dollars,\u201d which are not real dollars, but which correspond to \u201cpossible contributions in real dollars,\u201d and you obtain accounts of assets, income and expenses types for them - it works. Please do realize that some of those clever tricks may not be possible in more traditional accounting systems. In addition, some of the operations that would normally require a manual process in these systems can be automated away for us, e.g., \u201cclosing a year\u201d is entirely done by the software at any point in time, and balance assertions provide a safeguard that allow us to change the details of past transactions with little risk, so there is no need to \u201creconcile\u201d by baking the past into a frozen state. More flexibility is at hand. Finally, if you have a transaction entry problem that is not covered in this document, please do leave a comment in the margin, or write up your problem to the Ledger mailing-list . I would like for this document to cover as many realistic scenarios as possible. A Note of Caution \uf0c1 While reading this, please take note that the author is a dilettante: I am a computer scientist, not an accountant. In fact, apart from a general course I took in college and having completed the first year of a CFA program, I have no real training in accounting. Despite this, I do have some practical experience in maintaining three set of books using this software: my personal ledger (8 years worth of full financial data for all accounts), a joint ledger with my spouse, and the books of a contracting and consulting company I used to own. I also used my double-entry system to communicate with my accountant for many years and he made suggestions. Nevertheless\u2026 I may be making fundamental mistakes here and there, and I would appreciate you leaving a comment in the margin if you find anything dubious. Account Naming Conventions \uf0c1 You can define any account name you like, as long as it begins with one of the five categories: Assets, Liabilities, Income, Expenses, or Equity (note that you can customize those names with options - see the Language Syntax document for details). The accounts names are generally defined to have multiple name components, separated by a colon (:), which imply an accounts hierarchy, or \u201c chart of accounts \u201d: Assets:Component1:Component2:Component3:... Over time, I\u2019ve iterated over many ways of defining my account names and I have converged to the following convention for Assets, Liabilities, and Income accounts: Type : Country : Institution : Account : SubAccount What I like about this is that when you render a balance sheet, the tree that gets rendered nicely displays accounts by country first, then by institution. Some example account names: Assets:US:BofA:Savings ; Bank of America \u201cSavings\u201d account Assets:CA:RBC:Checking ; Royal Bank of Canada \u201cChecking\u201d account Liabilities:US:Amex:Platinum ; American Express Platinum credit card Liabilities:CA:RBC:Mortgage ; Mortgage loan account at RBC Income:US:ETrade:Interest ; Interest payments in E*Trade account Income:US:Acme:Salary ; Salary income from ACME corp. Sometimes I use a further sub-account or two, when it makes sense. For example, Vanguard internally keeps separate accounts depending on whether the contributions were from the employee or the employer\u2019s matching amount: Assets:US:Vanguard:Contrib401k:RGAGX ; My contributions to this fund Assets:US:Vanguard:Match401k:RGAGX ; Employer contributions For investment accounts, I tend organize all their contents by storing each particular type of stock in its own sub-account: Assets:US:ETrade:Cash ; The cash contents of the account Assets:US:ETrade:FB ; Shares of Facebook Assets:US:ETrade:AAPL ; Shares of Apple Assets:US:ETrade:MSFT ; Shares of Microsoft \u2026 This automatically organizes the balance sheet by types of shares, which I find really nice. Another convention that I like is to use the same institution component name when I have different related types of accounts. For instance, the E*Trade assets account above has associated income streams that would be booked under similarly named accounts: Income:US:ETrade:Interest ; Interest income from cash deposits Income:US:ETrade:Dividends ; Dividends received in this account Income:US:ETrade:PnL ; Capital gains or losses from trades \u2026 For \u201cExpenses\u201d accounts, I find that there are generally no relevant institutions. For those it makes more sense to treat them as categories and just have a simple hierarchy that corresponds to the kinds of expenses they count, some examples: Expenses:Sports:Scuba ; All matters of diving expenses Expenses:Transport:Train ; Train (mostly Amtrak, but not always) Expenses:Transport:Bus ; Various \u201cchinese bus\u201d companies Expenses:Transport:Flights ; Flights (various airlines) \u2026 I have a lot of these, like 250 or more. It is really up to you to decide how many to define and how finely to aggregate or \u201ccategorize\u201d your expenses this way. But of course, you should only define them as you need them; don\u2019t bother defining a huge list ahead of time. It\u2019s always easy to add new ones. It is worth noting that the institution does not have to be a \u201creal\u201d institution. For instance, I owned a condo unit in a building, and I used the Loft4530 \u201cinstitution\u201d for all its related accounts: Assets:CA:Loft4530:Property Assets:CA:Loft4530:Association Income:CA:Loft4530:Rental Expenses:Loft4530:Acquisition:Legal Expenses:Loft4530:Acquisition:SaleAgent Expenses:Loft4530:Loan-Interest Expenses:Loft4530:Electricity Expenses:Loft4530:Insurance Expenses:Loft4530:Taxes:Municipal Expenses:Loft4530:Taxes:School If you have all of your business in a single country and have no plans to move to another, you might want to skip the country component for brevity. Finally, for \u201cEquity\u201d accounts, well, \u2026. normally you don\u2019t end up defining many of them, because these are mostly created to report the net income and currency conversions from previous years or the current exercise period on the balance sheet. Typically you will need at least one, and it doesn\u2019t matter much what you name it: Equity:Opening-Balances ; Balances used to open accounts You can customize the name of the other Equity accounts that get automatically created for the balance sheet. Choosing an Account Type \uf0c1 Part of the art of learning what accounts to book transactions to is to come up with relevant account names and design a scheme for how money will flow between those accounts, by jotting down some example transactions. It can get a bit creative. As you\u2019re working out how to \u201ccount\u201d all the financial events in your life, you will often end up wondering what account type to select for some of the accounts. Should this be an \u201cAssets\u201d account? Or an \u201cIncome\u201d account? After all, other than for creating reports, Beancount doesn\u2019t treat any of these account types differently\u2026 But this does not mean that you can just use any type willy nilly. Whether an account appears in the balance sheet or income statement does matter\u2014there is usually a correct choice. When in doubt, here are some guidelines to choose an account type. First, if the amounts to be posted to the account are only relevant to be reported for a period of time , they should be one of the income statement accounts: Income or Expenses. On the other hand, if the amount always needs to be included in the total balance of an account, then it should be a balance sheet account: Assets or Liabilities. Second, if the amounts are generally 1 positive, or \u201cgood from your perspective,\u201d the account should be either an Assets or an Expenses account. If the amounts are generally negative, or \u201cbad from your perspective,\u201d the account should be either a Liabilities or an Income account. Based on these two indicators, you should be able to figure out any case. Let\u2019s work through some examples: A restaurant meal represents something that you obtained in exchange for some assets (cash) or a liability (paid by credit card). Nobody ever cares what the \u201csum total of all food since you were born\u201d amounts to. Only the transitional value matters: \u201cHow much did I spend in restaurants this month ?\u201d Or, since the beginning of the year ? Or, during this trip? This clearly points to an Expenses account. But you might wonder\u2026 this is a positive number, but it is money I spent? Yes, the account that you spent from was subtracted from (debited) in exchange for the expense you received . Think of the numbers in the expenses account as things you received that vanish into the ether right after you receive them. These meals are consumed.. and then they go somewhere. Okay, we\u2019ll stop the analogy here. You own some shares of a bond, and receive an interest payment. This interest is cash deposited in an Assets account, for example, a trading account. What is the other leg to be booked to? Choosing Opening Dates \uf0c1 Some of the accounts you need to define don\u2019t correspond to real world accounts. The Expenses:Groceries account represents the sum total of grocery expenses since you started counting. Personally, I like to use my birth date on those. There\u2019s a rationale to it: it sums all the groceries you\u2019ve ever spent money on, and this started only when you came to this world. You can use this rationale on other accounts. For example, all the income accounts associated with an employer should probably be opened at the date you began the job, and end on the date you left. Makes sense. How to Deal with Cash \uf0c1 Let\u2019s start with cash. I typically define two accounts at my birth date: 1973-04-27 open Assets:Cash 1973-04-27 open Assets:ForeignCash The first account is for active use, this represents my wallet, and usually contains only units of my operating currencies, that is, the commodities I usually think of as \u201ccash.\u201d For me, they are USD and CAD commodities. The second account is meant to hold all the paper bills that I keep stashed in a pocket from trips around the world, so they\u2019re out of the way in some other account and I don\u2019t see them in my cash balance. I transfer those to the main account when I do travel to such places, e.g., if I return to Japan, I\u2019ll move my JPY from Assets:ForeignCash to Assets:Cash right before the trip and use them during that time. Cash Withdrawals \uf0c1 An ATM withdrawal from a checking account to cash will typically look like this: 2014-06-28 * \"DDA WITHDRAW 0609C\" Assets:CA:BofA:Checking -700.00 USD Assets:Cash You would see this transaction be imported in your checking account transactions download. Tracking Cash Expenses \uf0c1 One mistake people make when you tell them you\u2019re tracking all of your financial accounts is to assume that you have to book every single little irrelevant cash transaction to a notebook. Not so! It is your choice to decide how many of these cash transactions to take down (or not). Personally, I try to minimize the amount of manual effort I put into updating my Ledger. My rule for dealing with cash is this: If it is for food or alcohol, I don\u2019t track it. If it is for something else, I keep the receipt and enter it later. This works for me, because the great majority of my cash expenses tend to be food (or maybe I just make it that way by paying for everything else with credit cards). Only a few receipts pile up somewhere on my desk for a couple of months before I bother to type them in. However, you will need to make occasional adjustments to your cash account to account for these expenses. I don\u2019t actually bother doing this very often\u2026 maybe once every three months, when I feel like it. The method I use is to take a snapshot of my wallet (manually, by counting the bills) and enter a corresponding balance assertion: 2014-05-12 balance Assets:Cash 234.13 USD Every time I do this I\u2019ll also add a cash distribution adjusted to balance the account: 2014-06-19 * \"Cash distribution\" Expenses:Food:Restaurant 402.30 USD Expenses:Food:Alcohol 100.00 USD Assets:Cash ; -502.30 USD 2014-06-20 balance Assets:Cash 194.34 USD If you wonder why the amounts in the cash account don\u2019t add up (234.13 -502.30 \u2260 194.34), it is because between the two assertions I added to the cash account by doing some ATM withdrawals against the checking account, and those appear somewhere else (in the checking account section). The withdrawal increased the balance of the cash account. It would appear if I rendered a journal for Assets:Cash . I could have made my life simpler and used a Pad directive if I had booked everything to food\u2014pad entries don\u2019t work solely at the beginning of an account\u2019s history, but also between any two balance assertions on the same account\u2014but I want to book 80% of it to food and 20% alcohol, to more accurately represent my real usage of cash 2 . Finally, if you end up with a long time period between the times that you do this, you may want to \u201cspread out\u201d your expenses by adding more than one cash distribution 3 manually, so that if you generate a monthly report, a large cash expense does not appear as a single lump in or outside that month. Salary Income \uf0c1 Accounting for your salary is rewarding: you will be able to obtain a summary of income earned during the year as well as the detail of where the various deductions are going, and you will enjoy the satisfaction of seeing matching numbers from your Beancount reports when you receive your W-2 form from your employer (or on your T4 if you\u2019re located in Canada). I put all entries related to an employer in their own dedicated section. I start it by setting an event to the date I began working there, for example, using the hypothetical company \u201cHooli\u201d (from the Silicon Valley show): 2012-12-13 event \"employer\" \"Hooli Inc.\" This allows me to automatically calculate the number of days I\u2019ve been working there. When I leave a job, I\u2019ll change it to the new one, or an empty string, if I don\u2019t leave for another job: 2019-03-02 event \"employer\" \"\" This section will make several assumptions. The goal is to expose you to the various ideas you can use to account for your income correctly. You will almost certainly end up having to adapt these ideas to your specific situation. Employment Income Accounts \uf0c1 Then you define accounts for your pay stubs. You need to make sure that you have an account corresponding to each line of your pay stub. For example, here are some of the income accounts I define for this employment income at Hooli Inc.: 2012-12-13 open Income:US:Hooli:Salary USD ; \"Regular Pay\" 2012-12-13 open Income:US:Hooli:ReferralBonus USD ; \"Referral bonuses\" 2012-12-13 open Income:US:Hooli:AnnualBonus USD ; \"Annual bonus\" 2012-12-13 open Income:US:Hooli:Match401k USD ; \"Employer 401k match\" 2012-12-13 open Income:US:Hooli:GroupTermLife USD ; \"Group Term Life\" These correspond to regular salary pay, bonuses received for employee referrals, annual bonus, receipts for 401k (Hooli in this example will match some percentage of your contributions to your retirement account), receipts for life insurance (it appears both as an income and an expense), and benefits paid for your gym subscription. There are more, but this is a good example. (In this example I wrote down the names used in the stub as a comment, but you could insert them as metadata instead if you prefer.) You will need to book taxes withheld at the source to accounts for that year (see the tax section for details on this): 2014-01-01 open Expenses:Taxes:TY2014:US:Medicare USD 2014-01-01 open Expenses:Taxes:TY2014:US:Federal USD 2014-01-01 open Expenses:Taxes:TY2014:US:StateNY USD 2014-01-01 open Expenses:Taxes:TY2014:US:CityNYC USD 2014-01-01 open Expenses:Taxes:TY2014:US:SDI USD 2014-01-01 open Expenses:Taxes:TY2014:US:SocSec USD These accounts are for Medicare taxes, Federal, New York State and NYC taxes (yes, New York City residents have an additional tax on top of state tax), state disability insurance (SDI) payments, and finally, taxes to pay for social security. You will also need to have some accounts defined elsewhere for the various expenses that are paid automatically from your pay: 2012-12-13 open Expenses:Health:Life:GroupTermLife USD ; \"Life Ins.\" 2012-12-13 open Expenses:Health:Dental:Insurance USD ; \"Dental\" 2012-12-13 open Expenses:Health:Medical:Insurance USD ; \"Medical\" 2012-12-13 open Expenses:Health:Vision:Insurance USD ; \"Vision\" 2012-12-13 open Expenses:Internet:Reimbursement USD ; \"Internet Reim\" 2012-12-13 open Expenses:Transportation:PreTax USD ; \"Transit PreTax\" These correspond to typical company group plan life insurance payments, premiums for dental, medical and vision insurances, reimbursements for home internet usage, and pre-tax payments for public transit (the city of New York allows you to pay for your MetroCard with pre-tax money through your employer). Booking Salary Deposits \uf0c1 Then, when I import details for a payment via direct deposit to my checking account, it will look like this: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD If I haven\u2019t received my pay stub yet, I might book it temporarily to the salary account until I do: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD ! Income:US:Hooli:Salary When I receive or fetch my pay stub, I remove this and complete the rest of the postings. A realistic entry for a gross salary of $140,000 would look something like this: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD Income:US:Hooli:GroupTermLife -25.38 USD Income:US:Hooli:Salary -5384.62 USD Expenses:Health:Dental:Insurance 2.88 USD Expenses:Health:Life:GroupTermLife 25.38 USD Expenses:Internet:Reimbursement -34.65 USD Expenses:Health:Medical:Insurance 36.33 USD Expenses:Transportation:PreTax 56.00 USD Expenses:Health:Vision:Insurance 0.69 USD Expenses:Taxes:TY2014:US:Medicare 78.08 USD Expenses:Taxes:TY2014:US:Federal 1135.91 USD Expenses:Taxes:TY2014:US:CityNYC 75.03 USD Expenses:Taxes:TY2014:US:SDI 1.20 USD Expenses:Taxes:TY2014:US:StateNY 340.06 USD Expenses:Taxes:TY2014:US:SocSec 328.42 USD It\u2019s quite unusual for a salary payment to have no variation at all from its previous one: rounding up or down from the payroll processor will often result in a difference of a penny, social security payments will cap to their maximum, and there are various deductions that will occur from time to time, e.g., deductions on taxable benefits received. Moreover, contributions to a 401k will affect that amounts of taxes withheld at the source. Therefore, you end up having to look at each pay stub individually to enter its information correctly. But this is not as time-consuming as it sounds! Here\u2019s a trick: it\u2019s a lot easier to update your transactions if you list your postings in the same order as they appear on your pay stub. You just copy-paste the previous entry, read the pay stub from top to bottom and adjust the numbers accordingly. It takes a minute for each. It\u2019s worth noting some unusual things about the previous entry. The \u201cgroup term life\u201d entry has both a $25.38 income leg and an expense one. This is because Hooli pays for the premium (it reads exactly like that on the stubs.) Hooli also reimburses some of home internet, because I use it to deal with production issues. This appears as a negative posting to reduce the amount of my expense Expenses:Internet account. Vacation Hours \uf0c1 Our pay stubs also include accrued vacation and the total vacation balance, in vacation hours. You can also track these amounts on the same transactions. You need to declare corresponding accounts: 2012-12-13 open Income:US:Hooli:Vacation VACHR 2012-12-13 open Assets:US:Hooli:Vacation VACHR 2012-12-13 open Expenses:Vacation:Hooli VACHR Vacation that accrues is something you receive and thus is treated as Income in units of \u201cVACHR\u201d, and accumulates in an Assets account, which holds how many of these hours you currently have available to \u201cspend\u201d as time off. Updating the previous salary income transaction entry: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD \u2026 Assets:US:Hooli:Vacation 4.62 VACHR Income:US:Hooli:Vacation -4.62 VACHR 4.62 VACHR on a bi-weekly paycheck 26 times per year is 26 x 4.62 ~= 120 hours. At 8 hours per day, that is 15 work days, or 3 weeks, which is a standard vacation package for new US Hooligans in this example. When you do take time off, you book an expense against your accumulated vacation time: 2014-06-17 * \"Going to the beach today\" Assets:US:Hooli:Vacation -8 VACHR Expenses:Vacation:Hooli The Expenses account tracks how much vacation you\u2019ve used. From time to time you can check that the balance reported on your pay stub\u2014the amount of vacation left that your employer thinks you have\u2014is the same as that which you have accounted for: 2014-02-29 balance Assets:US:Hooli:Vacation 112.3400 VACHR You can \u201cprice\u201d your vacation hour units to your hourly rate, so that your vacations Assets account shows how much the company would pay you if you decided to quit. Assuming that $140,000/year salary, 40 hour weeks and 50 weeks of work, which is 2000 hours per year, we obtain a rate of $70/hour, which you enter like this: 2012-12-13 price VACHR 70.00 USD Similarly, if your vacation hours expires or caps, you can calculate how much dollar-equivalent you\u2019re forfeiting by working too much and giving up your vacation time. You would write off some of the VACHR from your Assets account into an income account (representing losses). 401k Contributions \uf0c1 The 401k plan allows you to make contributions to a tax-deferred retirement account using pre-tax dollars. This is carried out via withholdings from your pay. To account for those, you simply include a posting with the corresponding contribution towards your retirement account: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD \u2026 Assets:US:Vanguard:Cash 1000.00 USD \u2026 If you\u2019re accounting for your available contributions (see the tax section of this document), you will want to reduce your \u201c401k contribution\u201d Assets account at the same time. You would add two more postings to the transaction: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD \u2026 Assets:US:Vanguard:Cash 1000.00 USD Assets:US:Federal:PreTax401k -1000.00 US401K Expenses:Taxes:TY2014:US:Federal:PreTax401k 1000.00 US401K \u2026 If your employer matches your contributions, this may not show on your pay stubs. Because these contributions are not taxable\u2014they are deposited directly to a tax-deferred account\u2014your employer does not have to include them in the withholding statement. You will see them appear directly in your investment account as deposits. You can book them like this to the retirement account\u2019s tax balance: 2013-03-16 * \"BUYMF - MATCH\" Income:US:Hooli:Match401k -1173.08 USD Assets:US:Vanguard:Cash 1173.08 USD And then insert a second transaction when you invest this case, or directly purchasing assets from the contribution if you have specified an asset allocation and this is automated by the broker: 2013-03-16 * \"BUYMF - MATCH\" Income:US:Hooli:Match401k -1173.08 USD Assets:US:Vanguard:VMBPX 106.741 VMBPX {10.99 USD} Note that the fund that manages your 401k accounts may be tracking your contributions and your employer\u2019s contributions in separate buckets. You would declare sub-accounts for this and make the corresponding changes: 2012-12-13 open Assets:US:Vanguard:PreTax401k:VMBPX VMBPX 2012-12-13 open Assets:US:Vanguard:Match401k:VMBPX VMBPX It is common for them to do this in order to track each source of contribution separately, because there are several constraints on rollovers to other accounts that depend on it. Vesting Stock Grants \uf0c1 See the dedicated document on this topic for more details. Other Benefits \uf0c1 You can go crazy with tracking benefits if you want. Here are a few wild ideas. Points \uf0c1 If your employer offers a sponsored massage therapy program on-site, you could presumably book a massage out of your paycheck or even from some internal website (if the company is modern), and you could pay for them using some sort of internal points system, say, \u201cHooli points\u201d. You could track those using a made-up currency, e.g., \u201cMASSA\u2019s\u201d and which could be priced at 0.50 USD, the price at which you could purchase them: 2012-12-13 open Assets:US:Hooli:Massage MASSA 2012-12-13 price MASSA 0.50 USD When I purchase new massage points, I 2013-03-15 * \"Buying points for future massages\" Liabilities:US:BofA:CreditCard -45.00 USD Assets:US:Hooli:Massage 90 MASSA {0.50 USD} If you\u2019re occasionally awarded some of these points, and you can track that in an Income account. Food Benefits \uf0c1 Like many of the larger technology companies, Hooli presumably provides free food for its employees. This saves time and encourages people to eat healthy. This is a bit of a trend in the tech world right now. This benefit does not show up anywhere, but if you want to price it as part of your compensation package, you can track it using an Income account: 2012-12-13 open Income:US:Hooli:Food Depending on how often you end up eating at work, you could guesstimate some monthly allowance per month: 2013-06-30 * \"Distribution for food eaten at Hooli\" Income:US:Hooli:Food -350 USD Expenses:Food:Restaurant Currency Transfers & Conversions \uf0c1 If you convert between currencies, such as when performing an international transfer between banks, you need to provide the exchange rate to Beancount. It looks like this: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9000.00 CHF Assets:US:BofA:Checking 10000.00 USD @ 0.90 CHF The balance amount of the second posting is calculated as 10,000.00 USD x 0.90 CHF/USD = 9,000 CHF, and the transaction balances. Depending on your preference, you could have placed the rate on the other posting, like this: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9000.00 CHF @ 1.11111 USD Assets:US:BofA:Checking 10000.00 USD The balance amount of the first posting is calculated as -9000.00 CHF x 1.11111 USD/CHF = 10000.00 USD 4 . Typically I will choose the rate that was reported to me and put it on the corresponding side. You may also want to use the direction that F/X markets use for trading the rate, for example, the Swiss franc trades as USD/CHF, so I would prefer the first transaction. The price database converts the rates in both directions, so it is not that important 5 . If you use wire transfers, which is typical for this type of money transfer, you might incur a fee: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9025.00 CHF Assets:US:BofA:Checking 10000.00 USD @ 0.90 CHF Expenses:Fees:Wires 25.00 CHF If you convert cash at one of these shady-looking currency exchange parlors found in tourist locations, it might look like this: 2014-03-03 * \"Changed some cash at the airport in Lausanne\" Assets:Cash -400.00 USD @ 0.90 CHF Assets:Cash 355.00 CHF Expenses:Fees:Services 5.00 CHF In any case, you should never convert currency units using the cost basis syntax, because the original conversion rate needs to be forgotten after depositing the units, and not kept around attached to simple currency. For example, this would be incorrect usage: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9000.00 CHF Assets:US:BofA:Checking 10000.00 USD {0.90 CHF} ; <-bad! If you did that by mistake, you would incur errors when you attempted to use the newly USD deposited: Beancount would require that you specify the cost of these \u201cUSD\u201d in CHF, e.g., \u201cdebit from my USD that I changed at 0.90 USD/CHF\u201d. Nobody does this in the real world, and neither should you when you represent your transactions: once the money has converted, it\u2019s just money in a different currency, with no associated cost. Finally, a rather subtle problem is that using these price conversions back and forth at different rates over time breaks the accounting equation to some extent: changes in exchange rate may create small amounts of money out of thin air and all the balances don\u2019t end up summing up to zero. However, this is not a problem, because Beancount implements an elegant solution to automatically correct for this problem, so you can use these conversions freely without having to worry about this: it inserts a special conversions entry on the balance sheet to invert the cumulative effect of conversions for the report and obtain a clean balance of zero. (A discussion of the conversions problem is beyond the scope of this cookbook; please refer to Solving the Conversions Problem if you\u2019d like to know more.) Investing and Trading \uf0c1 Tracking trades and associated gains is a fairly involved topic. You will find a more complete introduction to profit and loss and a detailed discussion of various scenarios in the Trading with Beancount document, which is dedicated to this topic. Here we will discuss how to setup your account and provide simple example transactions to get you started. Accounts Setup \uf0c1 You should create an account prefix to root various sub-accounts associated with your investment account. Say you have an account at ETrade, this could be \u201c Assets:US:ETrade \u201d. Choose an appropriate institution name. Your investment account will have a cash component. You should create a dedicated sub-account will represent uninvested cash deposits: 2013-02-01 open Assets:US:ETrade:Cash USD I recommend that you further define a sub-account for each of the commodity types that you will invest in. Although this is not strictly necessary\u2014Beancount accounts may contain any number of commodities\u2014it is a nice way to aggregate all the positions in that commodity together for reporting. Say you will buy shares of LQD and BND, two popular bond ETFs: 2013-02-01 open Assets:US:ETrade:LQD LQD 2013-02-01 open Assets:US:ETrade:BND BND This also helps produce nicer reports: balances are often shown at cost and it\u2019s nice to see the total cost aggregated by commodity for various reasons (i.e., each commodity provides exposure to different market characteristics). Using a dedicated sub-account for each commodity held within an institution is a good way to do that. Unless you have specific reasons not to do so, I highly suggest sticking with this by default (you can always change it later by renaming accounts). Specifying commodity constraints on your accounts will help you detect data entry mistakes. Stock trades tend to be a bit more involved than regular transactions, and this is certainly going to be helpful. Then, you will hopefully receive income in this account, in two forms: capital gains, or \u201cP&L\u201d, and dividends. I like to account for these by institution, because this is how they have to be declared for taxes. You may also receive interest income. Define these: 2013-02-01 open Income:US:ETrade:PnL USD 2013-02-01 open Income:US:ETrade:Dividends USD 2013-02-01 open Income:US:ETrade:Interest USD Finally, to account for transaction fees and commissions, you will need some general accounts to receive these: 1973-04-27 open Expenses:Financial:Fees 1973-04-27 open Expenses:Financial:Commissions Funds Transfers \uf0c1 You will normally add some initial money in this account by making a transfer from an external account, say, a checking account: 2014-02-04 * \"Transferring money for investing\" Assets:US:BofA:Checking -2000.00 USD Assets:US:ETrade:Cash 2000.00 USD Making a Trade \uf0c1 Buying stock should have a posting that deposits the new commodity in the commodity\u2019s sub-account, and debits the cash account to the corresponding amounts plus commissions: 2014-02-16 * \"Buying some LQD\" Assets:US:ETrade:LQD 10 LQD {119.24 USD} Assets:US:ETrade:Cash -1199.35 USD Expenses:Financial:Commissions 6.95 USD Note that when you\u2019re buying units of a commodity, you are establishing a new trade lot in the account\u2019s inventory and it is necessary that you provide the cost of each unit (in this example, 119.24 USD per share of LQD). This allows us to account for capital gains correctly. Selling some of the same stock work similarly, except that an extra posting is added to absorb the capital gain or loss: 2014-02-16 * \"Selling some LQD\" Assets:US:ETrade:LQD -5 LQD {119.24 USD} @ 123.40 USD Assets:US:ETrade:Cash 610.05 USD Expenses:Financial:Commissions 6.95 USD Income:US:Etrade:PnL Note that the postings of shares removed from the Assets:US:ETrade:LQD account is a lot reduction and you must provide information to identify which lot you\u2019re reducing, in this case, by providing the per-share cost basis of 119.24 USD. I normally let Beancount calculate the capital gain or loss for me, which is why I don\u2019t specify it in the last posting. Beancount will automatically balance the transaction by setting the amount of this posting to -20.80 USD, which is a gain of 20.80 USD (remember that the signs are inverted for income accounts). Specifying the sale price of 123.40 USD is optional, and it is ignored for the purpose of balancing the transaction, the cash deposit and commissions legs determine the profit. Receiving Dividends \uf0c1 Receiving dividends takes on two forms. First, you can receive dividends in cash, which will go into the cash account: 2014-02-16 * \"Dividends from LQD\" Income:US:ETrade:Dividends -87.45 USD Assets:US:ETrade:Cash 87.45 USD Note that the source of the dividends isn\u2019t specified here. You could use a sub-account of the income account to count it separately. Or you can receive dividends in shares reinvested: 2014-06-27 * \"Dividends reinvested\" Assets:US:Vanguard:VBMPX 1.77400 VBMPX {10.83 USD} Income:US:Vanguard:Dividends -19.21 USD This is booked similarly to a stock purchase, and you also have to provide the cost basis of the received units. This would typically happen in a non-taxable retirement account. Refer to the Trading with Beancount document for a more thorough discussion and numerous and more complex examples. Choosing a Date \uf0c1 Buying or selling a single lot of stock typically involves multiple events over time: the trade is placed, the trade is filled (usually on the same day), the trade is settled. Settlement usually occurs 2 or 3 business days after the trade is filled. For simplicity, I recommend using the trade date as the date of your transaction. In the US, this is the date that is recognized for tax purposes, and settlement has no impact on your account (brokers typically won\u2019t allow you to trade without the corresponding cash or margin anyhow). So normally I don\u2019t bother creating separate entries for settlement, it\u2019s not very useful. More complex schemes can be envisioned, e.g. you could store the settlement date as a metadata field and then use it in scripts later on, but that\u2019s beyond the scope of this document. Conclusion \uf0c1 This document is incomplete. I have many more example use cases that I\u2019m planning to add here as I complete them. I will be announcing those on the mailing-list as they materialize. In particular, the following topics will be discussed: Health Care Expenses, e.g., insurance premiums and rebates Taxes IRAs, 401k and other tax-deferred accounts Real Estate Options This is not strictly always true: in accounting for companies, some account types are held at their opposite value for reasons, usually to offset the value of another account of the same type. These are called \u201ccontra\u201d accounts. But as an individual, you\u2019re quite unlikely to have one of those. If you\u2019re setting up a chart of accounts for a company, Beancount doesn\u2019t actually care whether the balance is of one sign or other. You declare contra-accounts just like regular accounts. \u21a9 I am considering supporting an extended version of the Pad directive that can take a percentage value and make it possible to pad only a percentage of the full amount, to automate this. \u21a9 Yet another extension to Beancount involves support multiple Pad directives between two balance assertions and automatically support this spreading out of padding directives. \u21a9 If you\u2019re concerned about the issue of precision or rounding in balancing, see this document . \u21a9 Note that if the price database needs to invert the date its calculation may result in a price with a large number of digits. Beancount uses IEEE decimal objects and the default context of the Python implementation is 28 digits, so inverting 0.9 will result in 1.111111\u2026.111 with 28 digits. \u21a9","title":"Command Line Accounting Cookbook"},{"location":"command_line_accounting_cookbook.html#command-line-accounting-cookbook","text":"Martin Blais , July 2014 http://furius.ca/beancount/doc/cookbook Introduction A Note of Caution Account Naming Conventions Choosing an Account Type Choosing Opening Dates How to Deal with Cash Cash Withdrawals Tracking Cash Expenses Salary Income Employment Income Accounts Booking Salary Deposits Vacation Hours 401k Contributions Vesting Stock Grants Other Benefits Points Food Benefits Currency Transfers & Conversions Investing and Trading Accounts Setup Funds Transfers Making a Trade Receiving Dividends Conclusion","title":"Command-line Accounting Cookbook"},{"location":"command_line_accounting_cookbook.html#introduction","text":"The best way to learn the double-entry method is to look at real-world examples. The method is elegant, but it can seem unintuitive to the newcomer how transactions have to be posted in order to perform the various operations that one needs to do in counting for different types financial events. This is why I wrote this cookbook. It is not meant to be a comprehensive description of all the features supported, but rather a set of practical guidelines to help you solve problems. I think this will likely be the most useful document in the Beancount documentation set! All the examples here apply to any double-entry accounting system: Ledger, GnuCash, or even commercial systems. Some of the details may differ only slightly. This cookbook is written using the syntax and calculation method of the Beancount software. This document also assumes that you are already familiar with the general balancing concepts of the double-entry method and with at least some of the syntax of Beancount which is available from its user\u2019s manual or its cheat sheet . If you haven\u2019t begun writing down your first file, you will want to read Getting Started with Beancount and do that first. Command-line accounting systems are agnostic about the types of things they can count and allow you to get creative with the kinds of units that you can invent to track various kinds of things. For instance, you can count \u201cIRA contribution dollars,\u201d which are not real dollars, but which correspond to \u201cpossible contributions in real dollars,\u201d and you obtain accounts of assets, income and expenses types for them - it works. Please do realize that some of those clever tricks may not be possible in more traditional accounting systems. In addition, some of the operations that would normally require a manual process in these systems can be automated away for us, e.g., \u201cclosing a year\u201d is entirely done by the software at any point in time, and balance assertions provide a safeguard that allow us to change the details of past transactions with little risk, so there is no need to \u201creconcile\u201d by baking the past into a frozen state. More flexibility is at hand. Finally, if you have a transaction entry problem that is not covered in this document, please do leave a comment in the margin, or write up your problem to the Ledger mailing-list . I would like for this document to cover as many realistic scenarios as possible.","title":"Introduction"},{"location":"command_line_accounting_cookbook.html#a-note-of-caution","text":"While reading this, please take note that the author is a dilettante: I am a computer scientist, not an accountant. In fact, apart from a general course I took in college and having completed the first year of a CFA program, I have no real training in accounting. Despite this, I do have some practical experience in maintaining three set of books using this software: my personal ledger (8 years worth of full financial data for all accounts), a joint ledger with my spouse, and the books of a contracting and consulting company I used to own. I also used my double-entry system to communicate with my accountant for many years and he made suggestions. Nevertheless\u2026 I may be making fundamental mistakes here and there, and I would appreciate you leaving a comment in the margin if you find anything dubious.","title":"A Note of Caution"},{"location":"command_line_accounting_cookbook.html#account-naming-conventions","text":"You can define any account name you like, as long as it begins with one of the five categories: Assets, Liabilities, Income, Expenses, or Equity (note that you can customize those names with options - see the Language Syntax document for details). The accounts names are generally defined to have multiple name components, separated by a colon (:), which imply an accounts hierarchy, or \u201c chart of accounts \u201d: Assets:Component1:Component2:Component3:... Over time, I\u2019ve iterated over many ways of defining my account names and I have converged to the following convention for Assets, Liabilities, and Income accounts: Type : Country : Institution : Account : SubAccount What I like about this is that when you render a balance sheet, the tree that gets rendered nicely displays accounts by country first, then by institution. Some example account names: Assets:US:BofA:Savings ; Bank of America \u201cSavings\u201d account Assets:CA:RBC:Checking ; Royal Bank of Canada \u201cChecking\u201d account Liabilities:US:Amex:Platinum ; American Express Platinum credit card Liabilities:CA:RBC:Mortgage ; Mortgage loan account at RBC Income:US:ETrade:Interest ; Interest payments in E*Trade account Income:US:Acme:Salary ; Salary income from ACME corp. Sometimes I use a further sub-account or two, when it makes sense. For example, Vanguard internally keeps separate accounts depending on whether the contributions were from the employee or the employer\u2019s matching amount: Assets:US:Vanguard:Contrib401k:RGAGX ; My contributions to this fund Assets:US:Vanguard:Match401k:RGAGX ; Employer contributions For investment accounts, I tend organize all their contents by storing each particular type of stock in its own sub-account: Assets:US:ETrade:Cash ; The cash contents of the account Assets:US:ETrade:FB ; Shares of Facebook Assets:US:ETrade:AAPL ; Shares of Apple Assets:US:ETrade:MSFT ; Shares of Microsoft \u2026 This automatically organizes the balance sheet by types of shares, which I find really nice. Another convention that I like is to use the same institution component name when I have different related types of accounts. For instance, the E*Trade assets account above has associated income streams that would be booked under similarly named accounts: Income:US:ETrade:Interest ; Interest income from cash deposits Income:US:ETrade:Dividends ; Dividends received in this account Income:US:ETrade:PnL ; Capital gains or losses from trades \u2026 For \u201cExpenses\u201d accounts, I find that there are generally no relevant institutions. For those it makes more sense to treat them as categories and just have a simple hierarchy that corresponds to the kinds of expenses they count, some examples: Expenses:Sports:Scuba ; All matters of diving expenses Expenses:Transport:Train ; Train (mostly Amtrak, but not always) Expenses:Transport:Bus ; Various \u201cchinese bus\u201d companies Expenses:Transport:Flights ; Flights (various airlines) \u2026 I have a lot of these, like 250 or more. It is really up to you to decide how many to define and how finely to aggregate or \u201ccategorize\u201d your expenses this way. But of course, you should only define them as you need them; don\u2019t bother defining a huge list ahead of time. It\u2019s always easy to add new ones. It is worth noting that the institution does not have to be a \u201creal\u201d institution. For instance, I owned a condo unit in a building, and I used the Loft4530 \u201cinstitution\u201d for all its related accounts: Assets:CA:Loft4530:Property Assets:CA:Loft4530:Association Income:CA:Loft4530:Rental Expenses:Loft4530:Acquisition:Legal Expenses:Loft4530:Acquisition:SaleAgent Expenses:Loft4530:Loan-Interest Expenses:Loft4530:Electricity Expenses:Loft4530:Insurance Expenses:Loft4530:Taxes:Municipal Expenses:Loft4530:Taxes:School If you have all of your business in a single country and have no plans to move to another, you might want to skip the country component for brevity. Finally, for \u201cEquity\u201d accounts, well, \u2026. normally you don\u2019t end up defining many of them, because these are mostly created to report the net income and currency conversions from previous years or the current exercise period on the balance sheet. Typically you will need at least one, and it doesn\u2019t matter much what you name it: Equity:Opening-Balances ; Balances used to open accounts You can customize the name of the other Equity accounts that get automatically created for the balance sheet.","title":"Account Naming Conventions"},{"location":"command_line_accounting_cookbook.html#choosing-an-account-type","text":"Part of the art of learning what accounts to book transactions to is to come up with relevant account names and design a scheme for how money will flow between those accounts, by jotting down some example transactions. It can get a bit creative. As you\u2019re working out how to \u201ccount\u201d all the financial events in your life, you will often end up wondering what account type to select for some of the accounts. Should this be an \u201cAssets\u201d account? Or an \u201cIncome\u201d account? After all, other than for creating reports, Beancount doesn\u2019t treat any of these account types differently\u2026 But this does not mean that you can just use any type willy nilly. Whether an account appears in the balance sheet or income statement does matter\u2014there is usually a correct choice. When in doubt, here are some guidelines to choose an account type. First, if the amounts to be posted to the account are only relevant to be reported for a period of time , they should be one of the income statement accounts: Income or Expenses. On the other hand, if the amount always needs to be included in the total balance of an account, then it should be a balance sheet account: Assets or Liabilities. Second, if the amounts are generally 1 positive, or \u201cgood from your perspective,\u201d the account should be either an Assets or an Expenses account. If the amounts are generally negative, or \u201cbad from your perspective,\u201d the account should be either a Liabilities or an Income account. Based on these two indicators, you should be able to figure out any case. Let\u2019s work through some examples: A restaurant meal represents something that you obtained in exchange for some assets (cash) or a liability (paid by credit card). Nobody ever cares what the \u201csum total of all food since you were born\u201d amounts to. Only the transitional value matters: \u201cHow much did I spend in restaurants this month ?\u201d Or, since the beginning of the year ? Or, during this trip? This clearly points to an Expenses account. But you might wonder\u2026 this is a positive number, but it is money I spent? Yes, the account that you spent from was subtracted from (debited) in exchange for the expense you received . Think of the numbers in the expenses account as things you received that vanish into the ether right after you receive them. These meals are consumed.. and then they go somewhere. Okay, we\u2019ll stop the analogy here. You own some shares of a bond, and receive an interest payment. This interest is cash deposited in an Assets account, for example, a trading account. What is the other leg to be booked to?","title":"Choosing an Account Type"},{"location":"command_line_accounting_cookbook.html#choosing-opening-dates","text":"Some of the accounts you need to define don\u2019t correspond to real world accounts. The Expenses:Groceries account represents the sum total of grocery expenses since you started counting. Personally, I like to use my birth date on those. There\u2019s a rationale to it: it sums all the groceries you\u2019ve ever spent money on, and this started only when you came to this world. You can use this rationale on other accounts. For example, all the income accounts associated with an employer should probably be opened at the date you began the job, and end on the date you left. Makes sense.","title":"Choosing Opening Dates"},{"location":"command_line_accounting_cookbook.html#how-to-deal-with-cash","text":"Let\u2019s start with cash. I typically define two accounts at my birth date: 1973-04-27 open Assets:Cash 1973-04-27 open Assets:ForeignCash The first account is for active use, this represents my wallet, and usually contains only units of my operating currencies, that is, the commodities I usually think of as \u201ccash.\u201d For me, they are USD and CAD commodities. The second account is meant to hold all the paper bills that I keep stashed in a pocket from trips around the world, so they\u2019re out of the way in some other account and I don\u2019t see them in my cash balance. I transfer those to the main account when I do travel to such places, e.g., if I return to Japan, I\u2019ll move my JPY from Assets:ForeignCash to Assets:Cash right before the trip and use them during that time.","title":"How to Deal with Cash"},{"location":"command_line_accounting_cookbook.html#cash-withdrawals","text":"An ATM withdrawal from a checking account to cash will typically look like this: 2014-06-28 * \"DDA WITHDRAW 0609C\" Assets:CA:BofA:Checking -700.00 USD Assets:Cash You would see this transaction be imported in your checking account transactions download.","title":"Cash Withdrawals"},{"location":"command_line_accounting_cookbook.html#tracking-cash-expenses","text":"One mistake people make when you tell them you\u2019re tracking all of your financial accounts is to assume that you have to book every single little irrelevant cash transaction to a notebook. Not so! It is your choice to decide how many of these cash transactions to take down (or not). Personally, I try to minimize the amount of manual effort I put into updating my Ledger. My rule for dealing with cash is this: If it is for food or alcohol, I don\u2019t track it. If it is for something else, I keep the receipt and enter it later. This works for me, because the great majority of my cash expenses tend to be food (or maybe I just make it that way by paying for everything else with credit cards). Only a few receipts pile up somewhere on my desk for a couple of months before I bother to type them in. However, you will need to make occasional adjustments to your cash account to account for these expenses. I don\u2019t actually bother doing this very often\u2026 maybe once every three months, when I feel like it. The method I use is to take a snapshot of my wallet (manually, by counting the bills) and enter a corresponding balance assertion: 2014-05-12 balance Assets:Cash 234.13 USD Every time I do this I\u2019ll also add a cash distribution adjusted to balance the account: 2014-06-19 * \"Cash distribution\" Expenses:Food:Restaurant 402.30 USD Expenses:Food:Alcohol 100.00 USD Assets:Cash ; -502.30 USD 2014-06-20 balance Assets:Cash 194.34 USD If you wonder why the amounts in the cash account don\u2019t add up (234.13 -502.30 \u2260 194.34), it is because between the two assertions I added to the cash account by doing some ATM withdrawals against the checking account, and those appear somewhere else (in the checking account section). The withdrawal increased the balance of the cash account. It would appear if I rendered a journal for Assets:Cash . I could have made my life simpler and used a Pad directive if I had booked everything to food\u2014pad entries don\u2019t work solely at the beginning of an account\u2019s history, but also between any two balance assertions on the same account\u2014but I want to book 80% of it to food and 20% alcohol, to more accurately represent my real usage of cash 2 . Finally, if you end up with a long time period between the times that you do this, you may want to \u201cspread out\u201d your expenses by adding more than one cash distribution 3 manually, so that if you generate a monthly report, a large cash expense does not appear as a single lump in or outside that month.","title":"Tracking Cash Expenses"},{"location":"command_line_accounting_cookbook.html#salary-income","text":"Accounting for your salary is rewarding: you will be able to obtain a summary of income earned during the year as well as the detail of where the various deductions are going, and you will enjoy the satisfaction of seeing matching numbers from your Beancount reports when you receive your W-2 form from your employer (or on your T4 if you\u2019re located in Canada). I put all entries related to an employer in their own dedicated section. I start it by setting an event to the date I began working there, for example, using the hypothetical company \u201cHooli\u201d (from the Silicon Valley show): 2012-12-13 event \"employer\" \"Hooli Inc.\" This allows me to automatically calculate the number of days I\u2019ve been working there. When I leave a job, I\u2019ll change it to the new one, or an empty string, if I don\u2019t leave for another job: 2019-03-02 event \"employer\" \"\" This section will make several assumptions. The goal is to expose you to the various ideas you can use to account for your income correctly. You will almost certainly end up having to adapt these ideas to your specific situation.","title":"Salary Income"},{"location":"command_line_accounting_cookbook.html#employment-income-accounts","text":"Then you define accounts for your pay stubs. You need to make sure that you have an account corresponding to each line of your pay stub. For example, here are some of the income accounts I define for this employment income at Hooli Inc.: 2012-12-13 open Income:US:Hooli:Salary USD ; \"Regular Pay\" 2012-12-13 open Income:US:Hooli:ReferralBonus USD ; \"Referral bonuses\" 2012-12-13 open Income:US:Hooli:AnnualBonus USD ; \"Annual bonus\" 2012-12-13 open Income:US:Hooli:Match401k USD ; \"Employer 401k match\" 2012-12-13 open Income:US:Hooli:GroupTermLife USD ; \"Group Term Life\" These correspond to regular salary pay, bonuses received for employee referrals, annual bonus, receipts for 401k (Hooli in this example will match some percentage of your contributions to your retirement account), receipts for life insurance (it appears both as an income and an expense), and benefits paid for your gym subscription. There are more, but this is a good example. (In this example I wrote down the names used in the stub as a comment, but you could insert them as metadata instead if you prefer.) You will need to book taxes withheld at the source to accounts for that year (see the tax section for details on this): 2014-01-01 open Expenses:Taxes:TY2014:US:Medicare USD 2014-01-01 open Expenses:Taxes:TY2014:US:Federal USD 2014-01-01 open Expenses:Taxes:TY2014:US:StateNY USD 2014-01-01 open Expenses:Taxes:TY2014:US:CityNYC USD 2014-01-01 open Expenses:Taxes:TY2014:US:SDI USD 2014-01-01 open Expenses:Taxes:TY2014:US:SocSec USD These accounts are for Medicare taxes, Federal, New York State and NYC taxes (yes, New York City residents have an additional tax on top of state tax), state disability insurance (SDI) payments, and finally, taxes to pay for social security. You will also need to have some accounts defined elsewhere for the various expenses that are paid automatically from your pay: 2012-12-13 open Expenses:Health:Life:GroupTermLife USD ; \"Life Ins.\" 2012-12-13 open Expenses:Health:Dental:Insurance USD ; \"Dental\" 2012-12-13 open Expenses:Health:Medical:Insurance USD ; \"Medical\" 2012-12-13 open Expenses:Health:Vision:Insurance USD ; \"Vision\" 2012-12-13 open Expenses:Internet:Reimbursement USD ; \"Internet Reim\" 2012-12-13 open Expenses:Transportation:PreTax USD ; \"Transit PreTax\" These correspond to typical company group plan life insurance payments, premiums for dental, medical and vision insurances, reimbursements for home internet usage, and pre-tax payments for public transit (the city of New York allows you to pay for your MetroCard with pre-tax money through your employer).","title":"Employment Income Accounts"},{"location":"command_line_accounting_cookbook.html#booking-salary-deposits","text":"Then, when I import details for a payment via direct deposit to my checking account, it will look like this: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD If I haven\u2019t received my pay stub yet, I might book it temporarily to the salary account until I do: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD ! Income:US:Hooli:Salary When I receive or fetch my pay stub, I remove this and complete the rest of the postings. A realistic entry for a gross salary of $140,000 would look something like this: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD Income:US:Hooli:GroupTermLife -25.38 USD Income:US:Hooli:Salary -5384.62 USD Expenses:Health:Dental:Insurance 2.88 USD Expenses:Health:Life:GroupTermLife 25.38 USD Expenses:Internet:Reimbursement -34.65 USD Expenses:Health:Medical:Insurance 36.33 USD Expenses:Transportation:PreTax 56.00 USD Expenses:Health:Vision:Insurance 0.69 USD Expenses:Taxes:TY2014:US:Medicare 78.08 USD Expenses:Taxes:TY2014:US:Federal 1135.91 USD Expenses:Taxes:TY2014:US:CityNYC 75.03 USD Expenses:Taxes:TY2014:US:SDI 1.20 USD Expenses:Taxes:TY2014:US:StateNY 340.06 USD Expenses:Taxes:TY2014:US:SocSec 328.42 USD It\u2019s quite unusual for a salary payment to have no variation at all from its previous one: rounding up or down from the payroll processor will often result in a difference of a penny, social security payments will cap to their maximum, and there are various deductions that will occur from time to time, e.g., deductions on taxable benefits received. Moreover, contributions to a 401k will affect that amounts of taxes withheld at the source. Therefore, you end up having to look at each pay stub individually to enter its information correctly. But this is not as time-consuming as it sounds! Here\u2019s a trick: it\u2019s a lot easier to update your transactions if you list your postings in the same order as they appear on your pay stub. You just copy-paste the previous entry, read the pay stub from top to bottom and adjust the numbers accordingly. It takes a minute for each. It\u2019s worth noting some unusual things about the previous entry. The \u201cgroup term life\u201d entry has both a $25.38 income leg and an expense one. This is because Hooli pays for the premium (it reads exactly like that on the stubs.) Hooli also reimburses some of home internet, because I use it to deal with production issues. This appears as a negative posting to reduce the amount of my expense Expenses:Internet account.","title":"Booking Salary Deposits"},{"location":"command_line_accounting_cookbook.html#vacation-hours","text":"Our pay stubs also include accrued vacation and the total vacation balance, in vacation hours. You can also track these amounts on the same transactions. You need to declare corresponding accounts: 2012-12-13 open Income:US:Hooli:Vacation VACHR 2012-12-13 open Assets:US:Hooli:Vacation VACHR 2012-12-13 open Expenses:Vacation:Hooli VACHR Vacation that accrues is something you receive and thus is treated as Income in units of \u201cVACHR\u201d, and accumulates in an Assets account, which holds how many of these hours you currently have available to \u201cspend\u201d as time off. Updating the previous salary income transaction entry: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD \u2026 Assets:US:Hooli:Vacation 4.62 VACHR Income:US:Hooli:Vacation -4.62 VACHR 4.62 VACHR on a bi-weekly paycheck 26 times per year is 26 x 4.62 ~= 120 hours. At 8 hours per day, that is 15 work days, or 3 weeks, which is a standard vacation package for new US Hooligans in this example. When you do take time off, you book an expense against your accumulated vacation time: 2014-06-17 * \"Going to the beach today\" Assets:US:Hooli:Vacation -8 VACHR Expenses:Vacation:Hooli The Expenses account tracks how much vacation you\u2019ve used. From time to time you can check that the balance reported on your pay stub\u2014the amount of vacation left that your employer thinks you have\u2014is the same as that which you have accounted for: 2014-02-29 balance Assets:US:Hooli:Vacation 112.3400 VACHR You can \u201cprice\u201d your vacation hour units to your hourly rate, so that your vacations Assets account shows how much the company would pay you if you decided to quit. Assuming that $140,000/year salary, 40 hour weeks and 50 weeks of work, which is 2000 hours per year, we obtain a rate of $70/hour, which you enter like this: 2012-12-13 price VACHR 70.00 USD Similarly, if your vacation hours expires or caps, you can calculate how much dollar-equivalent you\u2019re forfeiting by working too much and giving up your vacation time. You would write off some of the VACHR from your Assets account into an income account (representing losses).","title":"Vacation Hours"},{"location":"command_line_accounting_cookbook.html#401k-contributions","text":"The 401k plan allows you to make contributions to a tax-deferred retirement account using pre-tax dollars. This is carried out via withholdings from your pay. To account for those, you simply include a posting with the corresponding contribution towards your retirement account: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD \u2026 Assets:US:Vanguard:Cash 1000.00 USD \u2026 If you\u2019re accounting for your available contributions (see the tax section of this document), you will want to reduce your \u201c401k contribution\u201d Assets account at the same time. You would add two more postings to the transaction: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD \u2026 Assets:US:Vanguard:Cash 1000.00 USD Assets:US:Federal:PreTax401k -1000.00 US401K Expenses:Taxes:TY2014:US:Federal:PreTax401k 1000.00 US401K \u2026 If your employer matches your contributions, this may not show on your pay stubs. Because these contributions are not taxable\u2014they are deposited directly to a tax-deferred account\u2014your employer does not have to include them in the withholding statement. You will see them appear directly in your investment account as deposits. You can book them like this to the retirement account\u2019s tax balance: 2013-03-16 * \"BUYMF - MATCH\" Income:US:Hooli:Match401k -1173.08 USD Assets:US:Vanguard:Cash 1173.08 USD And then insert a second transaction when you invest this case, or directly purchasing assets from the contribution if you have specified an asset allocation and this is automated by the broker: 2013-03-16 * \"BUYMF - MATCH\" Income:US:Hooli:Match401k -1173.08 USD Assets:US:Vanguard:VMBPX 106.741 VMBPX {10.99 USD} Note that the fund that manages your 401k accounts may be tracking your contributions and your employer\u2019s contributions in separate buckets. You would declare sub-accounts for this and make the corresponding changes: 2012-12-13 open Assets:US:Vanguard:PreTax401k:VMBPX VMBPX 2012-12-13 open Assets:US:Vanguard:Match401k:VMBPX VMBPX It is common for them to do this in order to track each source of contribution separately, because there are several constraints on rollovers to other accounts that depend on it.","title":"401k Contributions"},{"location":"command_line_accounting_cookbook.html#vesting-stock-grants","text":"See the dedicated document on this topic for more details.","title":"Vesting Stock Grants"},{"location":"command_line_accounting_cookbook.html#other-benefits","text":"You can go crazy with tracking benefits if you want. Here are a few wild ideas.","title":"Other Benefits"},{"location":"command_line_accounting_cookbook.html#points","text":"If your employer offers a sponsored massage therapy program on-site, you could presumably book a massage out of your paycheck or even from some internal website (if the company is modern), and you could pay for them using some sort of internal points system, say, \u201cHooli points\u201d. You could track those using a made-up currency, e.g., \u201cMASSA\u2019s\u201d and which could be priced at 0.50 USD, the price at which you could purchase them: 2012-12-13 open Assets:US:Hooli:Massage MASSA 2012-12-13 price MASSA 0.50 USD When I purchase new massage points, I 2013-03-15 * \"Buying points for future massages\" Liabilities:US:BofA:CreditCard -45.00 USD Assets:US:Hooli:Massage 90 MASSA {0.50 USD} If you\u2019re occasionally awarded some of these points, and you can track that in an Income account.","title":"Points"},{"location":"command_line_accounting_cookbook.html#food-benefits","text":"Like many of the larger technology companies, Hooli presumably provides free food for its employees. This saves time and encourages people to eat healthy. This is a bit of a trend in the tech world right now. This benefit does not show up anywhere, but if you want to price it as part of your compensation package, you can track it using an Income account: 2012-12-13 open Income:US:Hooli:Food Depending on how often you end up eating at work, you could guesstimate some monthly allowance per month: 2013-06-30 * \"Distribution for food eaten at Hooli\" Income:US:Hooli:Food -350 USD Expenses:Food:Restaurant","title":"Food Benefits"},{"location":"command_line_accounting_cookbook.html#currency-transfers-conversions","text":"If you convert between currencies, such as when performing an international transfer between banks, you need to provide the exchange rate to Beancount. It looks like this: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9000.00 CHF Assets:US:BofA:Checking 10000.00 USD @ 0.90 CHF The balance amount of the second posting is calculated as 10,000.00 USD x 0.90 CHF/USD = 9,000 CHF, and the transaction balances. Depending on your preference, you could have placed the rate on the other posting, like this: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9000.00 CHF @ 1.11111 USD Assets:US:BofA:Checking 10000.00 USD The balance amount of the first posting is calculated as -9000.00 CHF x 1.11111 USD/CHF = 10000.00 USD 4 . Typically I will choose the rate that was reported to me and put it on the corresponding side. You may also want to use the direction that F/X markets use for trading the rate, for example, the Swiss franc trades as USD/CHF, so I would prefer the first transaction. The price database converts the rates in both directions, so it is not that important 5 . If you use wire transfers, which is typical for this type of money transfer, you might incur a fee: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9025.00 CHF Assets:US:BofA:Checking 10000.00 USD @ 0.90 CHF Expenses:Fees:Wires 25.00 CHF If you convert cash at one of these shady-looking currency exchange parlors found in tourist locations, it might look like this: 2014-03-03 * \"Changed some cash at the airport in Lausanne\" Assets:Cash -400.00 USD @ 0.90 CHF Assets:Cash 355.00 CHF Expenses:Fees:Services 5.00 CHF In any case, you should never convert currency units using the cost basis syntax, because the original conversion rate needs to be forgotten after depositing the units, and not kept around attached to simple currency. For example, this would be incorrect usage: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9000.00 CHF Assets:US:BofA:Checking 10000.00 USD {0.90 CHF} ; <-bad! If you did that by mistake, you would incur errors when you attempted to use the newly USD deposited: Beancount would require that you specify the cost of these \u201cUSD\u201d in CHF, e.g., \u201cdebit from my USD that I changed at 0.90 USD/CHF\u201d. Nobody does this in the real world, and neither should you when you represent your transactions: once the money has converted, it\u2019s just money in a different currency, with no associated cost. Finally, a rather subtle problem is that using these price conversions back and forth at different rates over time breaks the accounting equation to some extent: changes in exchange rate may create small amounts of money out of thin air and all the balances don\u2019t end up summing up to zero. However, this is not a problem, because Beancount implements an elegant solution to automatically correct for this problem, so you can use these conversions freely without having to worry about this: it inserts a special conversions entry on the balance sheet to invert the cumulative effect of conversions for the report and obtain a clean balance of zero. (A discussion of the conversions problem is beyond the scope of this cookbook; please refer to Solving the Conversions Problem if you\u2019d like to know more.)","title":"Currency Transfers & Conversions"},{"location":"command_line_accounting_cookbook.html#investing-and-trading","text":"Tracking trades and associated gains is a fairly involved topic. You will find a more complete introduction to profit and loss and a detailed discussion of various scenarios in the Trading with Beancount document, which is dedicated to this topic. Here we will discuss how to setup your account and provide simple example transactions to get you started.","title":"Investing and Trading"},{"location":"command_line_accounting_cookbook.html#accounts-setup","text":"You should create an account prefix to root various sub-accounts associated with your investment account. Say you have an account at ETrade, this could be \u201c Assets:US:ETrade \u201d. Choose an appropriate institution name. Your investment account will have a cash component. You should create a dedicated sub-account will represent uninvested cash deposits: 2013-02-01 open Assets:US:ETrade:Cash USD I recommend that you further define a sub-account for each of the commodity types that you will invest in. Although this is not strictly necessary\u2014Beancount accounts may contain any number of commodities\u2014it is a nice way to aggregate all the positions in that commodity together for reporting. Say you will buy shares of LQD and BND, two popular bond ETFs: 2013-02-01 open Assets:US:ETrade:LQD LQD 2013-02-01 open Assets:US:ETrade:BND BND This also helps produce nicer reports: balances are often shown at cost and it\u2019s nice to see the total cost aggregated by commodity for various reasons (i.e., each commodity provides exposure to different market characteristics). Using a dedicated sub-account for each commodity held within an institution is a good way to do that. Unless you have specific reasons not to do so, I highly suggest sticking with this by default (you can always change it later by renaming accounts). Specifying commodity constraints on your accounts will help you detect data entry mistakes. Stock trades tend to be a bit more involved than regular transactions, and this is certainly going to be helpful. Then, you will hopefully receive income in this account, in two forms: capital gains, or \u201cP&L\u201d, and dividends. I like to account for these by institution, because this is how they have to be declared for taxes. You may also receive interest income. Define these: 2013-02-01 open Income:US:ETrade:PnL USD 2013-02-01 open Income:US:ETrade:Dividends USD 2013-02-01 open Income:US:ETrade:Interest USD Finally, to account for transaction fees and commissions, you will need some general accounts to receive these: 1973-04-27 open Expenses:Financial:Fees 1973-04-27 open Expenses:Financial:Commissions","title":"Accounts Setup"},{"location":"command_line_accounting_cookbook.html#funds-transfers","text":"You will normally add some initial money in this account by making a transfer from an external account, say, a checking account: 2014-02-04 * \"Transferring money for investing\" Assets:US:BofA:Checking -2000.00 USD Assets:US:ETrade:Cash 2000.00 USD","title":"Funds Transfers"},{"location":"command_line_accounting_cookbook.html#making-a-trade","text":"Buying stock should have a posting that deposits the new commodity in the commodity\u2019s sub-account, and debits the cash account to the corresponding amounts plus commissions: 2014-02-16 * \"Buying some LQD\" Assets:US:ETrade:LQD 10 LQD {119.24 USD} Assets:US:ETrade:Cash -1199.35 USD Expenses:Financial:Commissions 6.95 USD Note that when you\u2019re buying units of a commodity, you are establishing a new trade lot in the account\u2019s inventory and it is necessary that you provide the cost of each unit (in this example, 119.24 USD per share of LQD). This allows us to account for capital gains correctly. Selling some of the same stock work similarly, except that an extra posting is added to absorb the capital gain or loss: 2014-02-16 * \"Selling some LQD\" Assets:US:ETrade:LQD -5 LQD {119.24 USD} @ 123.40 USD Assets:US:ETrade:Cash 610.05 USD Expenses:Financial:Commissions 6.95 USD Income:US:Etrade:PnL Note that the postings of shares removed from the Assets:US:ETrade:LQD account is a lot reduction and you must provide information to identify which lot you\u2019re reducing, in this case, by providing the per-share cost basis of 119.24 USD. I normally let Beancount calculate the capital gain or loss for me, which is why I don\u2019t specify it in the last posting. Beancount will automatically balance the transaction by setting the amount of this posting to -20.80 USD, which is a gain of 20.80 USD (remember that the signs are inverted for income accounts). Specifying the sale price of 123.40 USD is optional, and it is ignored for the purpose of balancing the transaction, the cash deposit and commissions legs determine the profit.","title":"Making a Trade"},{"location":"command_line_accounting_cookbook.html#receiving-dividends","text":"Receiving dividends takes on two forms. First, you can receive dividends in cash, which will go into the cash account: 2014-02-16 * \"Dividends from LQD\" Income:US:ETrade:Dividends -87.45 USD Assets:US:ETrade:Cash 87.45 USD Note that the source of the dividends isn\u2019t specified here. You could use a sub-account of the income account to count it separately. Or you can receive dividends in shares reinvested: 2014-06-27 * \"Dividends reinvested\" Assets:US:Vanguard:VBMPX 1.77400 VBMPX {10.83 USD} Income:US:Vanguard:Dividends -19.21 USD This is booked similarly to a stock purchase, and you also have to provide the cost basis of the received units. This would typically happen in a non-taxable retirement account. Refer to the Trading with Beancount document for a more thorough discussion and numerous and more complex examples.","title":"Receiving Dividends"},{"location":"command_line_accounting_cookbook.html#choosing-a-date","text":"Buying or selling a single lot of stock typically involves multiple events over time: the trade is placed, the trade is filled (usually on the same day), the trade is settled. Settlement usually occurs 2 or 3 business days after the trade is filled. For simplicity, I recommend using the trade date as the date of your transaction. In the US, this is the date that is recognized for tax purposes, and settlement has no impact on your account (brokers typically won\u2019t allow you to trade without the corresponding cash or margin anyhow). So normally I don\u2019t bother creating separate entries for settlement, it\u2019s not very useful. More complex schemes can be envisioned, e.g. you could store the settlement date as a metadata field and then use it in scripts later on, but that\u2019s beyond the scope of this document.","title":"Choosing a Date"},{"location":"command_line_accounting_cookbook.html#conclusion","text":"This document is incomplete. I have many more example use cases that I\u2019m planning to add here as I complete them. I will be announcing those on the mailing-list as they materialize. In particular, the following topics will be discussed: Health Care Expenses, e.g., insurance premiums and rebates Taxes IRAs, 401k and other tax-deferred accounts Real Estate Options This is not strictly always true: in accounting for companies, some account types are held at their opposite value for reasons, usually to offset the value of another account of the same type. These are called \u201ccontra\u201d accounts. But as an individual, you\u2019re quite unlikely to have one of those. If you\u2019re setting up a chart of accounts for a company, Beancount doesn\u2019t actually care whether the balance is of one sign or other. You declare contra-accounts just like regular accounts. \u21a9 I am considering supporting an extended version of the Pad directive that can take a percentage value and make it possible to pad only a percentage of the full amount, to automate this. \u21a9 Yet another extension to Beancount involves support multiple Pad directives between two balance assertions and automatically support this spreading out of padding directives. \u21a9 If you\u2019re concerned about the issue of precision or rounding in balancing, see this document . \u21a9 Note that if the price database needs to invert the date its calculation may result in a price with a large number of digits. Beancount uses IEEE decimal objects and the default context of the Python implementation is 28 digits, so inverting 0.9 will result in 1.111111\u2026.111 with 28 digits. \u21a9","title":"Conclusion"},{"location":"command_line_accounting_in_context.html","text":"Command-line Accounting in Context \uf0c1 Martin Blais , July 2014 http://furius.ca/beancount/doc/motivation This text provides context and motivation for the usage of double-entry command-line accounting systems to manage personal finances. Motivation What exactly is \u201cAccounting\u201d? What can it do for me? Keeping Books Generating Reports Custom Scripting Filing Documents How the Pieces Fit Together Why not just use a spreadsheet? Why not just use a commercial app? How about Mint.com? How about Quicken? How about Quickbooks? How about GnuCash? Why build a computer language? Advantages of Command-Line Bookkeeping Why not just use an SQL database? But... I just want to do X? Why am I so Excited? Motivation \uf0c1 When I tell people about my command-line accounting hacking activities, I get a variety of reactions. Sometimes I hear echoes of desperation about their own finances: many people wish they had a better handle and understanding of their finances, they sigh and wish they were better organized. Other times, after I describe my process I get incredulous reactions: \u201cWhy do you even bother?\u201d Those are the people who feel that their financial life is so simple that it can be summarized in 5 minutes. These are most often blissfully unaware how many accounts they have and have only an imprecise idea of where they stand. They have at least 20 accounts to their name; it\u2019s only when we go through the details that they realize that their financial ecosystem is more complex than they thought. Finally, there are those who get really excited about the idea of using a powerful accounting system but who don\u2019t actually understand what it is that I\u2019m talking about. They might think it is a system to support investment portfolios, or something that I use to curate a budget like a stickler. Usually they end up thinking it is too complicated to actually get started with. This document attempts to explain what command-line bookkeeping is about in concrete terms, what you will get out of doing it, how the various software pieces fit together, and what kinds of results you can expect from this method. The fact is, the double-entry method is a basic technique that everyone should have been taught in high school. And using it for yourself is a simple and powerful process you can use to drive your entire financial life. What exactly is \u201cAccounting\u201d? \uf0c1 When one talks about \u201caccounting,\u201d they often implicitly refer to one or more of various distinct financial processes: Bookkeeping. Recording past transactions in a single place, a \u201cledger,\u201d also called \u201cthe books,\u201d as in, \u201cthe books of this company.\u201d Essentially, this means copying the amounts and category of financial transactions that occur in external account statements to a single system that includes all of the \u201caccounts\u201d relating to an entity and links them together. This is colloquially called \u201ckeeping books,\u201d or the activity of \u201cbookkeeping.\u201d Invoices. Preparing invoices and tracking payments. Contractors will often bring this up because it is a major concern of their activity: issuing requests to clients for payments for services rendered, and checking whether the corresponding payments have actually been received later on (and taking collection actions in case they haven\u2019t). If you\u2019re managing a company\u2019s finances, processing payroll is another aspect of this which is heavy in bookkeeping. Taxes. Finding or calculating taxable income, filling out tax forms and filing taxes to the various governmental authorities the entity is subject to. This process can be arduous, and for people who are beginning to see an increase in the complexity of their personal assets (many accounts of different types, new reporting requirements), it can be quite stressful. This is often the moment that they start thinking about organizing themselves. Expenses. Analyzing expenses, basically answering the question: \u201cWhere is my money going?\u201d A person with a small income and many credit cards may want to precisely track and calculate how much they\u2019re spending every month. This just develops awareness. Even in the presence of abundant resources, it is interesting to look at how much one is spending regularly, and where most of one\u2019s regular income is going, it often brings up surprises. Budgeting. Forecasting future expenses, allocating limited amounts to spend in various categories, and tracking how close one\u2019s actual spending is to those allocations. For individuals, this is usually in the context of trying to pay down debt or finding ways to save more. For companies, this occurs when planning for various projects. Investing. Summarizing and computing investment returns and capital gains. Many of us now manage our own savings via discount brokers, investing via ETFs and individually selected mutual funds. It is useful to be able to view asset distribution and risk exposure, as well as compute capital gains. Reporting. Public companies have regulatory requirements to provide transparency to their investors. As such, they usually report an annual income statement and a beginning and ending balance sheet . For an individual, those same reports are useful when applying for a personal loan or a mortgage at a bank, as they provide a window to someone\u2019s financial health situation. In this document, I will describe how command-line accounting can provide help and support for these activities. What can it do for me? \uf0c1 You might legitimately ask: sure, but why bother? We can look at the uses of accounting in terms of the questions it can answer for you: Where\u2019s my money, Lebowski? If you don\u2019t keep track of stuff, use cash a lot, have too many credit cards or if you are simply a disorganized and brilliant artist with his head in the clouds, you might wonder why there aren\u2019t as many beans left at the end of the month as you would like. An income statement will answer this question. I\u2019d like to be like Warren Buffet. How much do I have to save every month? I personally don\u2019t do a budget, but I know many people who do. If you set specific financial goals for a project or to save for later, the first thing you need to do is allocate a budget and set limits on your spending. Tracking your money is the first step towards doing that. You could even compute your returns in a way that can be compared against the market. I have some cash to invest. Given my portfolio, where should I invest it? Being able to report on the totality of your holdings, you can determine your asset class and currency exposures. This can help you decide where to place new savings in order to match a target portfolio allocation. How much am I worth? You have a 401k, an IRA and taxable accounts. A remaining student loan, or perhaps you\u2019re sitting on real-estate with two mortgages. Whatever. What\u2019s the total? It\u2019s really nice to obtain a single number that tells you how far you are from your retirement goals. Beancount can easily compute your net worth (to the cent). I still hurt from 2008. Are my investments safe? For example, I\u2019m managing my own money with various discount brokers, and many people are. I\u2019m basically running a miniature hedge fund using ETFs, where my goal is to be as diversified as possible between asset classes, sector exposure, currency exposure, etc. Plus, because of tax-deferred accounts I cannot do that in a single place. With Beancount you can generate a list of holdings and aggregate them by category. Taxes suck. Well, enough said. Why do they suck? Mostly because of uncertainty and doubt. It\u2019s not fun to not know what\u2019s going on. If you had all the data at your fingertips instantly it wouldn\u2019t be nearly as bad. What do you have to report? For some people there\u2019s a single stream of income, but for many others, it gets complicated (you might even be in that situation and you don\u2019t know it). Qualified vs. ordinary dividends, long-term vs. short-term capital gains, income from secondary sources, wash sales, etc. When it\u2019s time to do my taxes, I bring up the year\u2019s income statement, and I have a clear list of items to put in and deductions I can make. I want to buy a home. How much can I gather for a down payment? If you needed a lot of cash all of a sudden, how much could you afford? Well, if you can produce a list of your holdings, you can aggregate them \u201cby liquidity.\u201d This gives you an idea of available cash in case of an urgent need. Mr. Banker, please lend me some money. Impress the banker geek: just bring your balance sheet. Ideally the one generated fresh from that morning. You should have your complete balance sheet at any point in time. Instructions for your eventual passing. Making a will involves listing your assets. Make your children\u2019s lives easier should you pass by having a complete list of all the beans to be passed on or collected. The amounts are just part of the story: being able to list all the accounts and institutions will make it easier for someone to clear your assets. I can\u2019t remember if they paid me. If you send out invoices and have receivables, it\u2019s nice to have a method to track who has paid and who just says they\u2019ll pay. I\u2019m sure there is a lot more you can think of. Let me know. Keeping Books \uf0c1 Alright, so here\u2019s the part where I give you the insight of the method. The central and most basic activity that provides support for all the other ones is bookkeeping . A most important and fundamental realization is that this relatively simple act\u2014that of copying all of the financial transactions into a single integrated system\u2014allows you to produce reports that solve all of the other problems. You are building a corpus of data, a list of dated transaction objects that each represents movements of money between some of the accounts that you own. This data provides a full timeline of your financial activity. All you have to do is enter each transaction while respecting a simple constraint, that of the double-entry method , which is this: Each time an amount is posted to an account, it must have a corresponding inverse amount posted to some other account(s), and the sum of these amounts must be zero. That\u2019s it. This is the essence of the method, and an ensemble of transactions that respects this constraint acquires nice properties which are discussed in detail in some of my other documents. In order to generate sensible reports, all accounts are further labeled with one of four categories: Assets , Liabilities , Income and Expenses . A fifth category, Equity , exists only to summarize the history of all previous income and expense transactions. Command-line accounting systems are just simplistic computer languages for conveniently entering, reading and processing this transaction data, and ensure that the constraint of the double-entry method is respected. They\u2019re simple counting systems, that can add any kind of \u201cthing\u201d in dedicated counters (\u201caccounts\u201d). They\u2019re basically calculators with multiple buckets. Beancount and Ledger may differ slightly in their syntax and on some of their semantics, but the general principle is the same, and the simplifying assumptions are very similar. Here is a concrete example of what a transaction looks like, just so you can get a feeling for it. In a text file\u2014which is the input to a command-line accounting system\u2014something like the following is written and expresses a credit card transaction: 2014-05-23 * \"CAFE MOGADOR NEW YO\" \"Dinner with Caroline\" Liabilities:US:BofA:CreditCard -98.32 USD Expenses:Restaurant This is an example transaction with two \u201cpostings,\u201d or \u201clegs.\u201d The \u201c Expenses:Restaurant \u201d line is an account, not a category, though accounts often act like categories (there is no distinction between these two concepts). The amount on the expenses leg is left unspecified, and this is a convenience allowed by the input language. The software determines its amount automatically with the remainder of the balance which in this case will be 98.32 USD, so that -98.32 USD + 98.32 USD = 0 (remember that the sum of all postings must balance to zero\u2014this is enforced by the language). Most financial institutions provide some way for you to download a summary of account activity in some format or other. Much of the details of your transactions are automatically pulled in from such a downloaded file, using some custom script you write that converts it into the above syntax: A transaction date is always available from the downloadable files. The \u201c CAFE MOGADOR NEW YO \u201d bit is the \u201cmemo,\u201d also provided by the downloaded file, and those names are often good enough for you to figure out what the business you spent at was. Those memos are the same ugly names you would see appear on your credit card statements. This is attached to a transaction as a \u201cpayee\u201d attribute. I manually added \u201c Dinner with Caroline \u201d as a comment. I don\u2019t have to do this (it\u2019s optional), but I like to do it when I reconcile new transactions, it takes me only a minute and it helps me remember past events if I look for them. The importer brought in the Liabilities:US:BofA:CreditCard posting with its amount automatically, but I\u2019ve had to insert the Expenses:Restaurant account myself: I typed it in. I have shortcuts in my text editor that allow me to do that using account name completion, it takes a second and it\u2019s incredibly easy. Furthermore, pre-selecting this account could be automated as well, by running a simple learning algorithm on the previous history contained in the same input file (we tend to go to the same places all the time). The syntax gets a little bit more complicated, for example it allows you to represent stock purchases and sales, tracking the cost basis of your assets, and there are many other types of conveniences, like being able to define and count any kind of \u201cthing\u201d in an account (e.g., \u201cvacation hours accumulated\u201d), but this example captures the essence of what I do. I replicate all the transactions from all of the accounts that I own in this way, for the most part in an automated fashion. I spend about 1-2 hours every couple of weeks in order to update this input file, and only for the most used accounts (credit card and checking accounts). Other accounts I\u2019ll update every couple of months, or when I need to generate some reports. Because I have a solid understanding of my finances, this is not a burden anymore\u2026 it has become fun . What you obtain is a full history, a complete timeline of all the financial transactions from all the accounts over time, often connected together, your financial life, in a single text file . Generating Reports \uf0c1 So what\u2019s the point of manicuring this file? I have code that reads it, parses it, and that can serve various views and aggregations of it on a local web server running on my machine, or produce custom reports from this stream of data. The most useful views are undeniably the balance sheet and income statement , but there are others: Balance Sheet. A balance sheet lists the final balance of all of your assets and liabilities accounts on a single page. This is a snapshot of all your accounts at a single point in time, a well-understood overview of your financial situation. This shows your net worth (very precisely, if you update all your accounts) and is also what a banker would be interested in if you were to apply for a loan. Beancount can produce my balance sheet at any point in time, but most often I\u2019m interested in the \u201ccurrent\u201d or \u201clatest\u201d balance sheet. Income Statement. An income statement is a summary of all income and expenses that occur between two points in time. It renders the final balance for these accounts in format familiar to any accountant: income accounts on the left, expense accounts on the right. This provides insights on the changes that occurred during a time period, a delta of changes. This tells you how much you\u2019re earning and where your money is going, in detail. The difference between the two is how much you saved. Beancount can render such a statement for any arbitrary period of time, e.g., this year, or month by month. Journals. For each account (or category, if you like to think of them that way), I can render a list of all the transactions that posted changes to that account. I call this a journal (Ledger calls this a \u201cregister\u201d). If you\u2019re looking at your credit card account\u2019s journal, for instance, it should match that of your bank statement. On the other hand, if you look at your \u201crestaurants expense\u201d account, it should show all of your restaurant outings, across all methods of payment (including cash, if you choose to enter that). You can easily fetch any detail of your financial history, like \u201cWhere was that place I had dinner with Arthur in March three years ago?\u201d or \u201cI want to sell that couch\u2026 how much did I pay for it again?\u201d Payables and Receivables. If you have amounts known to be received, for example, you filed your taxes or sent an invoice and are expecting a payment, or you mailed a check and you are expecting it to be cashed at some point in the future, you can track this with dedicated accounts. Transactions have syntax that allows you to link many of them together and figure out what has been paid or received. Calculating taxes. If you enter your salary deposits with the detail from your pay stub, you can calculate exactly how much taxes you\u2019ve paid, and the amounts should match exactly those that will be reported on your employer\u2019s annual income reporting form (e.g., W2 form in the USA, T4 if in Canada, Form P60 in the UK, or otherwise if you live elsewhere). This is particularly convenient if you have to make tax installments, for instance. If you\u2019re a contractor, you will have to track many such things, including company expensable expenses. It is also useful to count dividends that you have to report as taxable income. Investing. I\u2019m not quite rich, but I manage my own assets using discount brokers and mainly ETFs. In addition, I take advantage of a variety of tax sheltered accounts, with limited choices in assets. This is pretty common now. You could say that I\u2019m managing a miniature hedge fund and you wouldn\u2019t be too far from reality. Using Beancount, I can produce a detailed list of all holdings, with reports on daily market value changes, capital gains, dividends, asset type distribution (e.g. stocks vs. fixed income), currency exposure, and I can figure out how much of my assets are liquid, e.g., how much I have available towards a downpayment on a house. Precisely, and at any point in time. This is nice. I can also produce various aggregations of the holdings, i.e., value by account, by type of instrument, by currency. Forecasting. The company I work for has an employee stock plan, with a vesting schedule. I can forecast my approximate expected income including the vesting shares under reasonable assumptions. I can answer the question: \u201cAt this rate, at which date do I reach a net worth of X?\u201d Say, if X is the amount which you require for retirement. Sharing Expenses. If you share expenses with others, like when you go on a trip with friends, or have a roommate, the double-entry method provides a natural and easy solution to reconciling expenses together. You can also produce reports of the various expenses incurred for clarity. No uncertainty. Budgeting. I make enough money that I don\u2019t particularly set specific goals myself, but I plan to support features to do this. You basically end up managing your personal finances like a company would\u2026 but it\u2019s very easy because you\u2019re using a simple and cleverly designed computer language that makes a lot of simplifications (doing away with the concepts of \u201ccredit and debits\u201d for example), reducing the process to its essential minimum. Custom Scripting \uf0c1 The applications are endless. I have all sorts of wild ideas for generating reports for custom projects. These are useful and fun experiments, \u201cchallenges\u201d as I call them. Some examples: I once owned a condo unit and I\u2019ve been doing double-entry bookkeeping throughout the period I owned it, through selling it. All of the corresponding accounts share the Loft4530 name in them. This means that I could potentially compute the precise internal rate of return on all of the cash flows related to it, including such petty things as replacement light bulbs expenses. To consider it as a pure investment. Just for fun. I can render a tree-map of my annual expenses and assets. This is a good visualization of these categories, that preserve their relative importance. I could look at average expenses with a correction for the time-value of money. This would be fun, tell me how my cost of living has changed over time. The beauty of it is that once you have the corpus of data, which is relatively easy to create if you maintain it incrementally, you can do all sorts of fun things by writing a little bit of Python code. I built Beancount to be able to do that in two ways: By providing a \u201cplugins\u201d system that allows you to filter a parsed set of transactions. This makes it possible for you to hack the syntax of Beancount and prototype new conveniences in data entry. Plugins provided by default provide extended features just by filtering and transforming the list of parsed transactions. And it\u2019s simple: all you have to do is implement a callback function with a particular signature and add a line to your input file. By writing scripts. You can parse and obtain the contents of a ledger with very little code. In Python, this looks no more complicated than this: import beancount.loader \u2026 entries, errors, options = beancount.loader.load_file('myfile.ledger') for entry in entries: \u2026 Voila. You\u2019re on your way to spitting out whatever output you want. You have access to all the libraries in the Python world, and my code is mostly functional, heavily documented and thoroughly unit-tested. You should be able to find your way easily. Moreover, if you\u2019re uncertain about using this system, you could just use it to begin entering your data and later write a script that converts it into something else. Filing Documents \uf0c1 If you have defined accounts for each of your real-world accounts, you have also created a natural method for organizing your statements and other paper documents. As we are communicating more and more by email with our accountants and institutions, it is becoming increasingly common to scan letters to PDFs and have those available as computer files. All the banks have now gone paperless, and you can download these statements if you care (I tend to do this once at the end of the year, for preservation, just in case). It\u2019s nice to be able to organize these nicely and retrieve those documents easily. What I do is keep a directory hierarchy mirroring the account names that I\u2019ve defined, something that looks like this: .../documents/ Assets/ US/ TDBank/ Checking/ 2014-04-08.statement.march.pdf 2014-05-07.statement.april.pdf \u2026 Liabilities/ US/ Amex/ Platinum/ 2014-04-17.March.pdf 2014-04-19.download.ofx \u2026 Expenses/ Health/ Medical/ 2014-04-02.anthem.eob-physiotherapy.pdf \u2026 These example files would correspond to accounts with names Assets:US:TDBank:Checking , Liabilities:US:Amex:Platinum , and Expenses:Health:Medical . I keep this directory under version control. As long as a file name begins with a date, such as \u201c 2014-04-02 \u201d, Beancount is able to find the files automatically and insert directives that are rendered in its web interface as part of an account\u2019s journal, which you can click on to view the document itself. This allows me to find all my statements in one place, and if I\u2019m searching for a document, it has a well-defined place where I know to find it. Moreover, the importing software I wrote is able to identify downloaded files and automatically move them into the corresponding account\u2019s directory. How the Pieces Fit Together \uf0c1 So I\u2019ve described what I do to organize my finances. Here I\u2019ll tell you about the various software pieces and how they fit together: Beancount is the core of the system. It reads the text file I update, parses the computer language I\u2019ve defined and produces reports from the resulting data structures. This software only reads the input file and does not communicate with other programs on purpose. It runs in isolation. Beancount\u2019s ingest package and tools help automate the updating of account data by extracting transactions from file downloads from banks and other institutions. These tools orchestrate the running of importers which you implement (this is where all the messy importing code lives, the code you need to make it easier to keep your text file up-to-date, which can parse OFX and CSV files, for instance). See bean-extract, bean-identify tools. The ingest package also helps with filing documents (see bean-file tool). Because it is able to identify which document belongs to which account, it can move the downloaded file to my documents archive automatically. This saves me time. Finally, in order to provide market values, a Beancount input file should have suitable price directives. Beancount also contains code to fetch latest or historical prices for the various commodities present in one\u2019s ledger file (see the bean-price tool). Like the extraction of transactions from OFX downloads, it also spits out Beancount input syntax used to define prices. See the diagram below for a pretty picture that illustrates how these work together. Why not just use a spreadsheet? \uf0c1 This is indeed a good question, and spreadsheets are incredibly useful for sure. I certainly would not be writing my own software if I could track my finances with a spreadsheet. The problem is that the intrinsic structure of the double-entry transactional data does not lend itself to a tabular representation. Each transaction has multiple attributes (date, narration, tags), and two or more legs, each of which has an associated amount and possibly a cost. If you put the dates on one axis and the accounts on the other, you would end up with a very sparse and very large table; that would not be very useful, and it would be incredibly difficult to edit. If on the other hand, you had a column dedicated to account names for each row, all of your computations would have to take the account cell into the calculation logic. It would be very difficult to deal with, if not impossible. All matters of data aggregations are performed relative to account names. Moreover, dealing with the accumulation of units with a cost basis would require difficult gymnastics. I don\u2019t even know how I would proceed forward to do this in a spreadsheet. A core part of command-line accounting systems is the inventory logic that allows you to track a cost basis for every unit held in an account and book reductions of positions against existing lots only. This allows the system to compute capital gains automatically. And this is related to the codes that enforce the constraint that all postings on a transaction balance out to zero. I believe that the \u201ctransaction <-> postings\u201d data representation combined with a sensible method for updating inventories is the essence of this system. The need for these is the justification to create a dedicated method to build this data, such as a computer language. Finally, having our own syntax offers the opportunity to provide other types of useful directives such as balance assertions, open/close dates, and sophisticated filtering of subsets of transactions. You just cannot do what we\u2019re doing in a spreadsheet. Why not just use a commercial app? \uf0c1 How about Mint.com? \uf0c1 Oftentimes people tell me they\u2019re using Mint.com to track their finances, usually with some amount of specific complaints following close behind. Online services such as Mint provide a subset of the functionality of double-entry accounting. The main focus of these services is the reporting of expenses by category, and the maintenance of a budget. As such, they do a great job at automating the download of your credit card and bank transactions. I think that if you want a low-maintenance option to tracking your finances and you don\u2019t have a problem with the obvious privacy risks, this is a fantastic option: it comes with a web interface, mobile apps, and updating your accounts probably requires clicking some buttons, providing some passwords, and a small amount of manual corrections every couple of weeks. I have several reservations about using it for myself, however: Passwords. I\u2019m just not comfortable enough with any commercial company to share the passwords to all my bank, credit card, and investment accounts. This sounds like an insane idea to me, a very scary one, and I\u2019m just not willing to put that much trust in any one company\u2019s hands. I don\u2019t care what they say: I worked for several software companies and I\u2019m aware of the disconnect between promises made by salespeople, website PR, and engineering reality. I also know the power of determined computer hackers. Insufficient reporting. Reporting is probably beautiful and colorful\u2014it\u2019s gorgeous on the website\u2014but certainly insufficient for all the reporting I want to do on my data. Reporting that doesn\u2019t do what they want it to is a common complaint I hear about these systems. With my system, I can always write a script and produce any kind of report I might possibly want in the future. For instance, spitting out a treemap visualization of my expenses instead of a useless pie chart. I can slice and dice my transactions in all kinds of unique ways. I can tag subsets of transactions for projects or trips and report on them. It\u2019s more powerful than generic reporting. Perennity. What if the company is not in business in 5 years? Where will my data be? If I spend any time at all curating my financial data, I want to ensure that it will be available to me forever, in an open format. In their favor, some of these sites probably have a downloadable option, but who uses it? Does it even work well? Would it include all of the transactions that they downloaded? I don\u2019t know. Not international. It probably does not work well with an international, multi-currency situation. These services target a \u201cmajority\u201d of users, most of which have all their affairs in a single country. I live in the USA, have a past history in Canada, which involves remaining tax-sheltered investment accounts, occasional expenses during visits which justify maintaining a credit card there, and a future history which might involve some years spent in another country such as Hong Kong or Australia. Will this work in Mint? Probably not. They might support Canada, but will they support accounts in both places? How about some other country I might want to move to? I want a single integrated view of all my accounts across all countries in all currencies forever, nothing less. My system supports that very well. (I\u2019m not aware of any double-entry system that deals with the international problem in a currency agnostic way as well as Beancount does.) Inability to deal with particular situations. What if I own real estate? Will I be able to price the value of my home at a reasonable amount, so it creates an accurate balance sheet? Regularly obtaining \u201ccomparables\u201d for my property from an eager real estate agent will tell me much more precisely how much my home is worth than services like Zillow ever could. I need to be able to input that for my balance sheet. What about those stock options from that privately held Australian company I used to work for? How do I price that? How about other intangible things, such as receivables from a personal loan I made to a friend? I doubt online services are able to provide you with the ability to enter those. If you want the whole picture with precision, you need to be able to make these adjustments. Custom tracking. Using \u201cimaginary currencies\u201d, I\u2019m able to track all kinds of other things than currencies and stocks. For example, by using an \u201c IRAUSD \u201d commodity in Beancount I\u2019m able to track how many 401k contributions I\u2019ve made at any point during the year. I can count the after-tax basis of a traditional IRA account similarly. I can even count my vacation hours using an imaginary \u201c VACHR \u201d currency and verify it against my pay stubs. Capital gains reporting. I haven\u2019t tried it myself, but I\u2019ve heard some discontent about Mint from others about its limited capabilities for capital gains reporting. Will it maintain trade lots? Will it handle average cost booking? How about PnL on FOREX gains? What about revenue received for that book I\u2019m selling via PayPal? I want to be able to use my accounting system as an input for my tax reporting. Cash transactions. How difficult is it to enter cash transactions? Do I have to log in, or start a slow, heavy program that will only work under Windows? With Beancount I bring up a file in a text editor, this is instant and easy. Is it even possible to enter custom cash entries in online services? In other words, I\u2019m a very sophisticated user and yes, a bit of a control freak . I\u2019m not lying about it. I don\u2019t have any ideological objections about using a commercial service, it is probably not for me. If it\u2019s good enough for you, suit yourself. Despite their beautiful and promising websites, I haven\u2019t heard of anyone being completely satisfied with these services. I hear a fair amount of complaining, some mild satisfaction, but I have yet to meet anyone who raves about it. How about Quicken? \uf0c1 Quicken is a single-entry system, that is, it replicates the transactions of a remote account locally, and allows you to add a label to each transaction to place it in a category. I believe it also has support for synchronizing investment accounts. This is not enough for me, I want to track all kinds of things, and I want to use the double-entry method, which provides an intrinsic check that I\u2019ve entered my data correctly. Single-entry accounting is just not good enough if you\u2019ve already crossed the bridge of understanding the double-entry method. How about Quickbooks? \uf0c1 So let\u2019s talk about sophisticated software that is good enough for a company. Why wouldn\u2019t I use that instead? If it\u2019s good enough for small businesses, it should be good enough for me, no? There\u2019s Quickbooks and other ones. Why don\u2019t I use them: It costs money. Commercial software comes at a price. Ok, I probably could afford to pay a few hundred dollars per year (Quickbooks 2014 looks like around 300$/year for the full set of features), but I don\u2019t really want to. Platform. These softwares usually run on Microsoft Windows and sometimes on Mac OS X . I\u2019m a software developer, I mostly use Linux , and a Macbook Air for a laptop, on which I get annoyed running anything other than tmux and a web browser . I\u2019m not going to reboot just to enter a quick cash transaction. Slow startup. I cannot speak specifically to Quickbooks\u2019 implementation, but virtually every software suite of commercial scope I\u2019ve had to use had a splash screen and a slow, slow startup that involved initializing tons of plugins. They assume you\u2019re going to spend hours in it, which is reasonable for commercial users, but not for me, if I want to do a quick update of my ledger. UIs are inadequate. I haven\u2019t seen their UI but given the nature of transactions and my desire to input precisely and search quickly and organize things, I want to be able to edit in as text. I imagine it would be inconvenient for me. With Emacs and org-mode , I can easily i-search my way to any transaction within seconds after opening my ledger file. Inflexible. How would I go about re-organizing all my account names? I think I\u2019m still learning about the double-entry bookkeeping method and I have made mistakes in the past, mistakes where I desire to revisit the way I organize my accounts in a hierarchy. With my text file, I was able to safely rename a large number of accounts several times, and evolve my chart-of-accounts to reflect my improving understanding of how to organize my financial tracking system. Text is powerful! How about GnuCash? \uf0c1 I don\u2019t like UIs; they\u2019re inconvenient. There\u2019s nothing quite like editing a text file if you are a programmer. Moreover, GnuCash does not deal with multiple currencies well. I find bugs in it within the first hour every time I kick the tires on it, which I do every couple of years, out of curiosity. Other programs, such as Skrooge, also take the heavy-handed big UI approach. Why build a computer language? \uf0c1 A bookkeeping system provides conditions for a solution that involves a simple computer language for many reasons. Single-entry bookkeeping is largely insufficient if you're trying to track everything holistically. Existing systems either limit themselves to expense categories with little checking beyond \"reconciling\" which sometimes involves freezing the past. If you're not doing the bookkeeping for a company, sometimes just changing the past and fixing the mistakes where they occurred makes more sense. More importantly, the single-entry method leaves us wanting for the natural error-checking mechanism involved in the double-entry system. The problem is also not solvable elegantly by using spreadsheets; the simple data structure that forms the basis of the double-entry system infers either a very sparse spreadsheet with accounts on one dimension and transactions on the other. For real-world usage, this is impractical. Another iteration on this theme would involve inserting the postings with two columns, one with the account and one with the amount, but the varying number of columns and the lookup code makes this inelegant as well. Plus, it's not obvious how you would deal with a large number of currencies. Programs that provide fancy graphical or web-based user interfaces are inevitably awkward, due to the nature of the problem: each transaction is organized by viewing it through the lens of one account's journal, but any of the accounts present in its postings provide equally valid views. Ideally, what you want, is just to look at the transaction. Organizing them for most convenient input has little to do with the order in which they are to be presented. Using text has a lot of advantages: You can easily used search-and-replace and/or sed to make global changes, for example, rename or reorganize your accounts; You can organize the transactions in the order that is most convenient for data entry; There are a number of existing tools to search the text; You can easily write various little tools to spit out the data syntax, i.e., for importing from other file types, or converting from other systems. Text is inherently open , that is the file format is one that you can read your data from and store anywhere else, not a blob of incomprehensible data that becomes unusable when the company that makes the software stops supporting it. Finally, systems that attempt to automate the process of importing all your data from automated sources (e.g., mint.com) have one major shortfall: most often it's not very easy or even possible to add information for accounts that aren't automated. It is my experience that in practice you will have some entries and accounts to track that will not have a nice downloadable file format, or that simply don't have a real-world counterpart. In order to produce a complete view of one's balance sheet, it is important to be able to enter all of an individual's account within a single system. In other words, custom accounts and manually entered transactions do matter a lot. For all the reasons mentioned above, I feel that a computer language is more appropriate to express this data structure than a heavy piece of software with a customized interface. Being able to easily bring up a text file and quickly type in a few lines of text to add a transaction is great\u2013it's fast and easy. The ledger file provides a naturally open way to express one's data, and can be source-controlled or shared between people as well. Multiple files can be merged together, and scripted manipulations on a source file can be used to reorganize one's data history. Furthermore, a read-only web interface that presents the various reports one expects and allows the user to explore different views on the dataset is sufficient for the purpose of viewing the data. Advantages of Command-Line Bookkeeping \uf0c1 In summary, there are many advantages to using a command-line accounting system over a commercial package that provides a user-interface: Fast. You don\u2019t have to fire up a slow program with a splash screen that will take a while to initialize in order to add something to your books. Bringing up a text file from a bookmark in your favorite editing program (I use Emacs) is easy and quick. And if you\u2019re normally using a text editor all day long, as many programmers do, you won\u2019t even blink before the file is in front of your eyes. It\u2019s quick and easy. Portable. It will work on all platforms. Beancount is written in Python 3 with some C extensions, and as such will work on Mac, Linux and Windows platforms. I am very careful and determined to keep external dependencies on third-party packages to an absolute minimum in order to avoid installation problems. It should be easy to install and update, and work everywhere the same. Openness. Your data is open, and will remain open forever . I plan on having my corpus of data until the day I die. With an open format you will never end up in a situation where your transactional data is sitting in a binary blob with an unknown format and the software goes unsupported. Furthermore, your data can be converted to other languages easily. You can easily invoke the parser I provide and write a script that spits it out in another program\u2019s input syntax. You can place it under version control. You can entirely reorganize the structure of your accounts by renaming strings with sed. Text is empowering. Customized. You can produce very customized reports, that address exactly the kinds of problems you are having. One complaint I often hear from people about other financial software is that it doesn\u2019t quite do what they want. With command-line accounting systems you can at least write it yourself, as a small extension that uses your corpus of data. Why not just use an SQL database? \uf0c1 I don\u2019t like to reinvent the wheel. If this problem could be solved by filling up an SQL database and then making queries on it, that\u2019s exactly what I would do. Creating a language is a large overhead, it needs to be maintained and evolved, it\u2019s not an easy task, but as it turns out, necessary and justified to solve this problem. The problem is due to a few reasons: Filtering occurs on a two-level data structure of transactions vs. their children postings, and it is inconvenient to represent the data in a single table upon which we could then make manipulations. You cannot simply work only with postings: in order to ensure that the reports balance, you need to be selecting complete transactions. When you select transactions, the semantics is \u201cselect all the postings for which the transaction has this or that property.\u201d One such property is \u201call transactions that have some posting with account X .\u201d These semantics are not obvious to implement with a database. The nature of the data structure makes it inconvenient. The wide variety of directives makes it difficult to design a single elegant table that can accommodate all of their data. Ideally we would want to define two tables for each type of directive: a table that holds all common data (date, source filename & lineno, directive type) and a table to hold the type-specific data. While this is possible, it steps away from the neat structure of a single table of data. Operations on inventories\u2014the data structure that holds the incrementally changing contents of accounts\u2014require special treatment that would be difficult to implement in a database. Lot reductions are constrained against a running inventory of lots, each of which has a specific cost basis. Some lot reductions trigger the merging of lots (for average cost booking). This requires some custom operations on these inventory objects. Aggregations (breakdowns) are hierarchical in nature. The tree-like structure of accounts allows us to perform operations on subtrees of postings. This would also not be easy to implement with tables. Finally, input would be difficult. By defining a language, we side-step the problem of having to build a custom user interface that would allow us to create the data. Nevertheless, I really like the idea of working with databases. A script (bean-sql) is provided to convert the contents of a Beancount file to an SQL database; it\u2019s not super elegant to carry out computations on those tables, but it\u2019s provided nonetheless. You should be able to play around with it, even if operations are difficult. I might even pre-compute some of the operation\u2019s outputs to make it easier to run SQL queries. But... I just want to do X ? \uf0c1 Some may infer that what we\u2019re doing must be terribly complicated given that they envision they might want to use such a system only for a single purpose. But the fact is, how many accounts you decide to track is a personal choice. You can choose to track as little as you want in as little detail as you deem sufficient. For example, if all that you\u2019re interested in is investing, then you can keep books on only your investment accounts. If you\u2019re interested in replacing your usage of Mint.com or Quicken , you can simply just replicate the statements for your credit cards and see your corresponding expenses. The simplicity of having to just replicate the transactions in all your accounts over doing a painful annual \u201cstarting from scratch\u201d evaluation of all your assets and expenses for the purpose of looking at your finances will convince you. Looking at your finances with a spreadsheet will require you to at least copy values from your portfolio. Every time you want to generate the report you\u2019ll have to update the values\u2026 with my method, you just update each account\u2019s full activity, and you can obtain the complete list holdings as a by-product. It\u2019s easy to bring everything up-to-date if you have a systematic method. To those I say: try it out, accounting just for the bits that you\u2019re interested in. Once you\u2019ll have a taste of how the double-entry method works and have learned a little bit about the language syntax, you will want to get a fuller picture. You will get sucked in. Why am I so Excited? \uf0c1 Okay, so I\u2019ve become an accounting nerd. I did not ask for it. Just kind-of happened while I wasn\u2019t looking (I still don\u2019t wear brown socks though). Why is it I can\u2019t stop talking about this stuff? I used to have a company. It was an umbrella for doing contract work (yes, I originally had bigger aspirations for it, but it ended up being just that. Bleh.) As a company owner in Canada, I had to periodically make five different kinds of tax installments to the Federal and Provincial governments, some every month, some every three months, and then it varied. An accountant was feeding me the \u201cmagic numbers\u201d to put in the forms, numbers which I was copying like a monkey, without fully understanding how they were going to get used later on. I had to count separately the various expenses I would incur in relation to my work (for deductions), and those which were only personal \u2014I did not have the enlightened view of getting separate credit cards so I was trying to track personal vs. company expenses from the same accounts. I also had to track transfers between company and personal accounts that would later on get reported as dividends or salary income. I often had multiple pending invoices that would take more than two months to get paid, from different clients. It was a demi-hell. You get the idea. I used to try to track all these things manually, using systems composed of little text files and spreadsheets and doing things very carefully and in a particular way. The thing is, although I am by nature quite methodical, I would sometimes, just sometimes forget to update one of the files. Things would fall out of sync. When this happened it was incredibly frustrating, I had to spend time digging around various statements and figure out where I had gone wrong. This was time-consuming and unpleasant. And of course, when something you have to do is unpleasant, you tend not to do it so well. I did not have a solid idea of the current state of affairs of my company during the year, I just worked and earned money for it. My accountant would draw a balance sheet once a year after doing taxes in July, and it was always a tiny bit of a surprise. I felt permanently somewhat confused, and frankly annoyed every time I had to deal with \u201cmoney things.\u201d It wasn\u2019t quite a nightmare, but it was definitely making me tired. I felt my life was so much simpler when I just had a job. But one day\u2026 one day\u2026 I saw the light: I discovered the double-entry method. I don\u2019t recall exactly how. I think it was on a website, yes\u2026 this site: dwmbeancounter.com (it\u2019s a wonderful site). I realized that using a single system , I could account for all of these problems at the same time, and in a way that would impose an inherent error-checking mechanism. I was blown away! I think I printed the whole thing on paper at the time and worked my way through every tutorial. This simple counting trick is exactly what I was in dire need for. But all the software I tried was either disappointing, broken, or too complicated. So I read up on Ledger. And I got in touch with its author . And then shortly after I started on Beancount 1 . I\u2019ll admit that going through the effort of designing my own system just to solve my accounting problems is a bit overkill, but I\u2019ve been known to be a little more than extreme about certain things , and I\u2019ve really enjoyed solving this problem. My life is fulfilled when I maintain a good balance of \u201clearning\u201d and \u201cdoing,\u201d and this falls squarely in the \u201cdoing\u201d domain. Ever since, I feel so excited about anything related to personal finance. Probably because it makes me so happy to have such a level of awareness about what\u2019s going on with mine. I even sometimes find myself loving spending money in a new way, just from knowing that I\u2019ll have to figure out how to account for it later. I feel so elated by the ability to solve these financial puzzles that I have had bouts of engaging complete weekends in building this software. They are small challenges with a truly practical application and a tangible consequence. Instant gratification. I get a feeling of empowerment . And I wish the same for you. For a full list of differences, refer to this document . \u21a9","title":"Command Line Accounting in Context"},{"location":"command_line_accounting_in_context.html#command-line-accounting-in-context","text":"Martin Blais , July 2014 http://furius.ca/beancount/doc/motivation This text provides context and motivation for the usage of double-entry command-line accounting systems to manage personal finances. Motivation What exactly is \u201cAccounting\u201d? What can it do for me? Keeping Books Generating Reports Custom Scripting Filing Documents How the Pieces Fit Together Why not just use a spreadsheet? Why not just use a commercial app? How about Mint.com? How about Quicken? How about Quickbooks? How about GnuCash? Why build a computer language? Advantages of Command-Line Bookkeeping Why not just use an SQL database? But... I just want to do X? Why am I so Excited?","title":"Command-line Accounting in Context"},{"location":"command_line_accounting_in_context.html#motivation","text":"When I tell people about my command-line accounting hacking activities, I get a variety of reactions. Sometimes I hear echoes of desperation about their own finances: many people wish they had a better handle and understanding of their finances, they sigh and wish they were better organized. Other times, after I describe my process I get incredulous reactions: \u201cWhy do you even bother?\u201d Those are the people who feel that their financial life is so simple that it can be summarized in 5 minutes. These are most often blissfully unaware how many accounts they have and have only an imprecise idea of where they stand. They have at least 20 accounts to their name; it\u2019s only when we go through the details that they realize that their financial ecosystem is more complex than they thought. Finally, there are those who get really excited about the idea of using a powerful accounting system but who don\u2019t actually understand what it is that I\u2019m talking about. They might think it is a system to support investment portfolios, or something that I use to curate a budget like a stickler. Usually they end up thinking it is too complicated to actually get started with. This document attempts to explain what command-line bookkeeping is about in concrete terms, what you will get out of doing it, how the various software pieces fit together, and what kinds of results you can expect from this method. The fact is, the double-entry method is a basic technique that everyone should have been taught in high school. And using it for yourself is a simple and powerful process you can use to drive your entire financial life.","title":"Motivation"},{"location":"command_line_accounting_in_context.html#what-exactly-is-accounting","text":"When one talks about \u201caccounting,\u201d they often implicitly refer to one or more of various distinct financial processes: Bookkeeping. Recording past transactions in a single place, a \u201cledger,\u201d also called \u201cthe books,\u201d as in, \u201cthe books of this company.\u201d Essentially, this means copying the amounts and category of financial transactions that occur in external account statements to a single system that includes all of the \u201caccounts\u201d relating to an entity and links them together. This is colloquially called \u201ckeeping books,\u201d or the activity of \u201cbookkeeping.\u201d Invoices. Preparing invoices and tracking payments. Contractors will often bring this up because it is a major concern of their activity: issuing requests to clients for payments for services rendered, and checking whether the corresponding payments have actually been received later on (and taking collection actions in case they haven\u2019t). If you\u2019re managing a company\u2019s finances, processing payroll is another aspect of this which is heavy in bookkeeping. Taxes. Finding or calculating taxable income, filling out tax forms and filing taxes to the various governmental authorities the entity is subject to. This process can be arduous, and for people who are beginning to see an increase in the complexity of their personal assets (many accounts of different types, new reporting requirements), it can be quite stressful. This is often the moment that they start thinking about organizing themselves. Expenses. Analyzing expenses, basically answering the question: \u201cWhere is my money going?\u201d A person with a small income and many credit cards may want to precisely track and calculate how much they\u2019re spending every month. This just develops awareness. Even in the presence of abundant resources, it is interesting to look at how much one is spending regularly, and where most of one\u2019s regular income is going, it often brings up surprises. Budgeting. Forecasting future expenses, allocating limited amounts to spend in various categories, and tracking how close one\u2019s actual spending is to those allocations. For individuals, this is usually in the context of trying to pay down debt or finding ways to save more. For companies, this occurs when planning for various projects. Investing. Summarizing and computing investment returns and capital gains. Many of us now manage our own savings via discount brokers, investing via ETFs and individually selected mutual funds. It is useful to be able to view asset distribution and risk exposure, as well as compute capital gains. Reporting. Public companies have regulatory requirements to provide transparency to their investors. As such, they usually report an annual income statement and a beginning and ending balance sheet . For an individual, those same reports are useful when applying for a personal loan or a mortgage at a bank, as they provide a window to someone\u2019s financial health situation. In this document, I will describe how command-line accounting can provide help and support for these activities.","title":"What exactly is \u201cAccounting\u201d?"},{"location":"command_line_accounting_in_context.html#what-can-it-do-for-me","text":"You might legitimately ask: sure, but why bother? We can look at the uses of accounting in terms of the questions it can answer for you: Where\u2019s my money, Lebowski? If you don\u2019t keep track of stuff, use cash a lot, have too many credit cards or if you are simply a disorganized and brilliant artist with his head in the clouds, you might wonder why there aren\u2019t as many beans left at the end of the month as you would like. An income statement will answer this question. I\u2019d like to be like Warren Buffet. How much do I have to save every month? I personally don\u2019t do a budget, but I know many people who do. If you set specific financial goals for a project or to save for later, the first thing you need to do is allocate a budget and set limits on your spending. Tracking your money is the first step towards doing that. You could even compute your returns in a way that can be compared against the market. I have some cash to invest. Given my portfolio, where should I invest it? Being able to report on the totality of your holdings, you can determine your asset class and currency exposures. This can help you decide where to place new savings in order to match a target portfolio allocation. How much am I worth? You have a 401k, an IRA and taxable accounts. A remaining student loan, or perhaps you\u2019re sitting on real-estate with two mortgages. Whatever. What\u2019s the total? It\u2019s really nice to obtain a single number that tells you how far you are from your retirement goals. Beancount can easily compute your net worth (to the cent). I still hurt from 2008. Are my investments safe? For example, I\u2019m managing my own money with various discount brokers, and many people are. I\u2019m basically running a miniature hedge fund using ETFs, where my goal is to be as diversified as possible between asset classes, sector exposure, currency exposure, etc. Plus, because of tax-deferred accounts I cannot do that in a single place. With Beancount you can generate a list of holdings and aggregate them by category. Taxes suck. Well, enough said. Why do they suck? Mostly because of uncertainty and doubt. It\u2019s not fun to not know what\u2019s going on. If you had all the data at your fingertips instantly it wouldn\u2019t be nearly as bad. What do you have to report? For some people there\u2019s a single stream of income, but for many others, it gets complicated (you might even be in that situation and you don\u2019t know it). Qualified vs. ordinary dividends, long-term vs. short-term capital gains, income from secondary sources, wash sales, etc. When it\u2019s time to do my taxes, I bring up the year\u2019s income statement, and I have a clear list of items to put in and deductions I can make. I want to buy a home. How much can I gather for a down payment? If you needed a lot of cash all of a sudden, how much could you afford? Well, if you can produce a list of your holdings, you can aggregate them \u201cby liquidity.\u201d This gives you an idea of available cash in case of an urgent need. Mr. Banker, please lend me some money. Impress the banker geek: just bring your balance sheet. Ideally the one generated fresh from that morning. You should have your complete balance sheet at any point in time. Instructions for your eventual passing. Making a will involves listing your assets. Make your children\u2019s lives easier should you pass by having a complete list of all the beans to be passed on or collected. The amounts are just part of the story: being able to list all the accounts and institutions will make it easier for someone to clear your assets. I can\u2019t remember if they paid me. If you send out invoices and have receivables, it\u2019s nice to have a method to track who has paid and who just says they\u2019ll pay. I\u2019m sure there is a lot more you can think of. Let me know.","title":"What can it do for me?"},{"location":"command_line_accounting_in_context.html#keeping-books","text":"Alright, so here\u2019s the part where I give you the insight of the method. The central and most basic activity that provides support for all the other ones is bookkeeping . A most important and fundamental realization is that this relatively simple act\u2014that of copying all of the financial transactions into a single integrated system\u2014allows you to produce reports that solve all of the other problems. You are building a corpus of data, a list of dated transaction objects that each represents movements of money between some of the accounts that you own. This data provides a full timeline of your financial activity. All you have to do is enter each transaction while respecting a simple constraint, that of the double-entry method , which is this: Each time an amount is posted to an account, it must have a corresponding inverse amount posted to some other account(s), and the sum of these amounts must be zero. That\u2019s it. This is the essence of the method, and an ensemble of transactions that respects this constraint acquires nice properties which are discussed in detail in some of my other documents. In order to generate sensible reports, all accounts are further labeled with one of four categories: Assets , Liabilities , Income and Expenses . A fifth category, Equity , exists only to summarize the history of all previous income and expense transactions. Command-line accounting systems are just simplistic computer languages for conveniently entering, reading and processing this transaction data, and ensure that the constraint of the double-entry method is respected. They\u2019re simple counting systems, that can add any kind of \u201cthing\u201d in dedicated counters (\u201caccounts\u201d). They\u2019re basically calculators with multiple buckets. Beancount and Ledger may differ slightly in their syntax and on some of their semantics, but the general principle is the same, and the simplifying assumptions are very similar. Here is a concrete example of what a transaction looks like, just so you can get a feeling for it. In a text file\u2014which is the input to a command-line accounting system\u2014something like the following is written and expresses a credit card transaction: 2014-05-23 * \"CAFE MOGADOR NEW YO\" \"Dinner with Caroline\" Liabilities:US:BofA:CreditCard -98.32 USD Expenses:Restaurant This is an example transaction with two \u201cpostings,\u201d or \u201clegs.\u201d The \u201c Expenses:Restaurant \u201d line is an account, not a category, though accounts often act like categories (there is no distinction between these two concepts). The amount on the expenses leg is left unspecified, and this is a convenience allowed by the input language. The software determines its amount automatically with the remainder of the balance which in this case will be 98.32 USD, so that -98.32 USD + 98.32 USD = 0 (remember that the sum of all postings must balance to zero\u2014this is enforced by the language). Most financial institutions provide some way for you to download a summary of account activity in some format or other. Much of the details of your transactions are automatically pulled in from such a downloaded file, using some custom script you write that converts it into the above syntax: A transaction date is always available from the downloadable files. The \u201c CAFE MOGADOR NEW YO \u201d bit is the \u201cmemo,\u201d also provided by the downloaded file, and those names are often good enough for you to figure out what the business you spent at was. Those memos are the same ugly names you would see appear on your credit card statements. This is attached to a transaction as a \u201cpayee\u201d attribute. I manually added \u201c Dinner with Caroline \u201d as a comment. I don\u2019t have to do this (it\u2019s optional), but I like to do it when I reconcile new transactions, it takes me only a minute and it helps me remember past events if I look for them. The importer brought in the Liabilities:US:BofA:CreditCard posting with its amount automatically, but I\u2019ve had to insert the Expenses:Restaurant account myself: I typed it in. I have shortcuts in my text editor that allow me to do that using account name completion, it takes a second and it\u2019s incredibly easy. Furthermore, pre-selecting this account could be automated as well, by running a simple learning algorithm on the previous history contained in the same input file (we tend to go to the same places all the time). The syntax gets a little bit more complicated, for example it allows you to represent stock purchases and sales, tracking the cost basis of your assets, and there are many other types of conveniences, like being able to define and count any kind of \u201cthing\u201d in an account (e.g., \u201cvacation hours accumulated\u201d), but this example captures the essence of what I do. I replicate all the transactions from all of the accounts that I own in this way, for the most part in an automated fashion. I spend about 1-2 hours every couple of weeks in order to update this input file, and only for the most used accounts (credit card and checking accounts). Other accounts I\u2019ll update every couple of months, or when I need to generate some reports. Because I have a solid understanding of my finances, this is not a burden anymore\u2026 it has become fun . What you obtain is a full history, a complete timeline of all the financial transactions from all the accounts over time, often connected together, your financial life, in a single text file .","title":"Keeping Books"},{"location":"command_line_accounting_in_context.html#generating-reports","text":"So what\u2019s the point of manicuring this file? I have code that reads it, parses it, and that can serve various views and aggregations of it on a local web server running on my machine, or produce custom reports from this stream of data. The most useful views are undeniably the balance sheet and income statement , but there are others: Balance Sheet. A balance sheet lists the final balance of all of your assets and liabilities accounts on a single page. This is a snapshot of all your accounts at a single point in time, a well-understood overview of your financial situation. This shows your net worth (very precisely, if you update all your accounts) and is also what a banker would be interested in if you were to apply for a loan. Beancount can produce my balance sheet at any point in time, but most often I\u2019m interested in the \u201ccurrent\u201d or \u201clatest\u201d balance sheet. Income Statement. An income statement is a summary of all income and expenses that occur between two points in time. It renders the final balance for these accounts in format familiar to any accountant: income accounts on the left, expense accounts on the right. This provides insights on the changes that occurred during a time period, a delta of changes. This tells you how much you\u2019re earning and where your money is going, in detail. The difference between the two is how much you saved. Beancount can render such a statement for any arbitrary period of time, e.g., this year, or month by month. Journals. For each account (or category, if you like to think of them that way), I can render a list of all the transactions that posted changes to that account. I call this a journal (Ledger calls this a \u201cregister\u201d). If you\u2019re looking at your credit card account\u2019s journal, for instance, it should match that of your bank statement. On the other hand, if you look at your \u201crestaurants expense\u201d account, it should show all of your restaurant outings, across all methods of payment (including cash, if you choose to enter that). You can easily fetch any detail of your financial history, like \u201cWhere was that place I had dinner with Arthur in March three years ago?\u201d or \u201cI want to sell that couch\u2026 how much did I pay for it again?\u201d Payables and Receivables. If you have amounts known to be received, for example, you filed your taxes or sent an invoice and are expecting a payment, or you mailed a check and you are expecting it to be cashed at some point in the future, you can track this with dedicated accounts. Transactions have syntax that allows you to link many of them together and figure out what has been paid or received. Calculating taxes. If you enter your salary deposits with the detail from your pay stub, you can calculate exactly how much taxes you\u2019ve paid, and the amounts should match exactly those that will be reported on your employer\u2019s annual income reporting form (e.g., W2 form in the USA, T4 if in Canada, Form P60 in the UK, or otherwise if you live elsewhere). This is particularly convenient if you have to make tax installments, for instance. If you\u2019re a contractor, you will have to track many such things, including company expensable expenses. It is also useful to count dividends that you have to report as taxable income. Investing. I\u2019m not quite rich, but I manage my own assets using discount brokers and mainly ETFs. In addition, I take advantage of a variety of tax sheltered accounts, with limited choices in assets. This is pretty common now. You could say that I\u2019m managing a miniature hedge fund and you wouldn\u2019t be too far from reality. Using Beancount, I can produce a detailed list of all holdings, with reports on daily market value changes, capital gains, dividends, asset type distribution (e.g. stocks vs. fixed income), currency exposure, and I can figure out how much of my assets are liquid, e.g., how much I have available towards a downpayment on a house. Precisely, and at any point in time. This is nice. I can also produce various aggregations of the holdings, i.e., value by account, by type of instrument, by currency. Forecasting. The company I work for has an employee stock plan, with a vesting schedule. I can forecast my approximate expected income including the vesting shares under reasonable assumptions. I can answer the question: \u201cAt this rate, at which date do I reach a net worth of X?\u201d Say, if X is the amount which you require for retirement. Sharing Expenses. If you share expenses with others, like when you go on a trip with friends, or have a roommate, the double-entry method provides a natural and easy solution to reconciling expenses together. You can also produce reports of the various expenses incurred for clarity. No uncertainty. Budgeting. I make enough money that I don\u2019t particularly set specific goals myself, but I plan to support features to do this. You basically end up managing your personal finances like a company would\u2026 but it\u2019s very easy because you\u2019re using a simple and cleverly designed computer language that makes a lot of simplifications (doing away with the concepts of \u201ccredit and debits\u201d for example), reducing the process to its essential minimum.","title":"Generating Reports"},{"location":"command_line_accounting_in_context.html#custom-scripting","text":"The applications are endless. I have all sorts of wild ideas for generating reports for custom projects. These are useful and fun experiments, \u201cchallenges\u201d as I call them. Some examples: I once owned a condo unit and I\u2019ve been doing double-entry bookkeeping throughout the period I owned it, through selling it. All of the corresponding accounts share the Loft4530 name in them. This means that I could potentially compute the precise internal rate of return on all of the cash flows related to it, including such petty things as replacement light bulbs expenses. To consider it as a pure investment. Just for fun. I can render a tree-map of my annual expenses and assets. This is a good visualization of these categories, that preserve their relative importance. I could look at average expenses with a correction for the time-value of money. This would be fun, tell me how my cost of living has changed over time. The beauty of it is that once you have the corpus of data, which is relatively easy to create if you maintain it incrementally, you can do all sorts of fun things by writing a little bit of Python code. I built Beancount to be able to do that in two ways: By providing a \u201cplugins\u201d system that allows you to filter a parsed set of transactions. This makes it possible for you to hack the syntax of Beancount and prototype new conveniences in data entry. Plugins provided by default provide extended features just by filtering and transforming the list of parsed transactions. And it\u2019s simple: all you have to do is implement a callback function with a particular signature and add a line to your input file. By writing scripts. You can parse and obtain the contents of a ledger with very little code. In Python, this looks no more complicated than this: import beancount.loader \u2026 entries, errors, options = beancount.loader.load_file('myfile.ledger') for entry in entries: \u2026 Voila. You\u2019re on your way to spitting out whatever output you want. You have access to all the libraries in the Python world, and my code is mostly functional, heavily documented and thoroughly unit-tested. You should be able to find your way easily. Moreover, if you\u2019re uncertain about using this system, you could just use it to begin entering your data and later write a script that converts it into something else.","title":"Custom Scripting"},{"location":"command_line_accounting_in_context.html#filing-documents","text":"If you have defined accounts for each of your real-world accounts, you have also created a natural method for organizing your statements and other paper documents. As we are communicating more and more by email with our accountants and institutions, it is becoming increasingly common to scan letters to PDFs and have those available as computer files. All the banks have now gone paperless, and you can download these statements if you care (I tend to do this once at the end of the year, for preservation, just in case). It\u2019s nice to be able to organize these nicely and retrieve those documents easily. What I do is keep a directory hierarchy mirroring the account names that I\u2019ve defined, something that looks like this: .../documents/ Assets/ US/ TDBank/ Checking/ 2014-04-08.statement.march.pdf 2014-05-07.statement.april.pdf \u2026 Liabilities/ US/ Amex/ Platinum/ 2014-04-17.March.pdf 2014-04-19.download.ofx \u2026 Expenses/ Health/ Medical/ 2014-04-02.anthem.eob-physiotherapy.pdf \u2026 These example files would correspond to accounts with names Assets:US:TDBank:Checking , Liabilities:US:Amex:Platinum , and Expenses:Health:Medical . I keep this directory under version control. As long as a file name begins with a date, such as \u201c 2014-04-02 \u201d, Beancount is able to find the files automatically and insert directives that are rendered in its web interface as part of an account\u2019s journal, which you can click on to view the document itself. This allows me to find all my statements in one place, and if I\u2019m searching for a document, it has a well-defined place where I know to find it. Moreover, the importing software I wrote is able to identify downloaded files and automatically move them into the corresponding account\u2019s directory.","title":"Filing Documents"},{"location":"command_line_accounting_in_context.html#how-the-pieces-fit-together","text":"So I\u2019ve described what I do to organize my finances. Here I\u2019ll tell you about the various software pieces and how they fit together: Beancount is the core of the system. It reads the text file I update, parses the computer language I\u2019ve defined and produces reports from the resulting data structures. This software only reads the input file and does not communicate with other programs on purpose. It runs in isolation. Beancount\u2019s ingest package and tools help automate the updating of account data by extracting transactions from file downloads from banks and other institutions. These tools orchestrate the running of importers which you implement (this is where all the messy importing code lives, the code you need to make it easier to keep your text file up-to-date, which can parse OFX and CSV files, for instance). See bean-extract, bean-identify tools. The ingest package also helps with filing documents (see bean-file tool). Because it is able to identify which document belongs to which account, it can move the downloaded file to my documents archive automatically. This saves me time. Finally, in order to provide market values, a Beancount input file should have suitable price directives. Beancount also contains code to fetch latest or historical prices for the various commodities present in one\u2019s ledger file (see the bean-price tool). Like the extraction of transactions from OFX downloads, it also spits out Beancount input syntax used to define prices. See the diagram below for a pretty picture that illustrates how these work together.","title":"How the Pieces Fit Together"},{"location":"command_line_accounting_in_context.html#why-not-just-use-a-spreadsheet","text":"This is indeed a good question, and spreadsheets are incredibly useful for sure. I certainly would not be writing my own software if I could track my finances with a spreadsheet. The problem is that the intrinsic structure of the double-entry transactional data does not lend itself to a tabular representation. Each transaction has multiple attributes (date, narration, tags), and two or more legs, each of which has an associated amount and possibly a cost. If you put the dates on one axis and the accounts on the other, you would end up with a very sparse and very large table; that would not be very useful, and it would be incredibly difficult to edit. If on the other hand, you had a column dedicated to account names for each row, all of your computations would have to take the account cell into the calculation logic. It would be very difficult to deal with, if not impossible. All matters of data aggregations are performed relative to account names. Moreover, dealing with the accumulation of units with a cost basis would require difficult gymnastics. I don\u2019t even know how I would proceed forward to do this in a spreadsheet. A core part of command-line accounting systems is the inventory logic that allows you to track a cost basis for every unit held in an account and book reductions of positions against existing lots only. This allows the system to compute capital gains automatically. And this is related to the codes that enforce the constraint that all postings on a transaction balance out to zero. I believe that the \u201ctransaction <-> postings\u201d data representation combined with a sensible method for updating inventories is the essence of this system. The need for these is the justification to create a dedicated method to build this data, such as a computer language. Finally, having our own syntax offers the opportunity to provide other types of useful directives such as balance assertions, open/close dates, and sophisticated filtering of subsets of transactions. You just cannot do what we\u2019re doing in a spreadsheet.","title":"Why not just use a spreadsheet?"},{"location":"command_line_accounting_in_context.html#why-not-just-use-a-commercial-app","text":"","title":"Why not just use a commercial app?"},{"location":"command_line_accounting_in_context.html#how-about-mintcom","text":"Oftentimes people tell me they\u2019re using Mint.com to track their finances, usually with some amount of specific complaints following close behind. Online services such as Mint provide a subset of the functionality of double-entry accounting. The main focus of these services is the reporting of expenses by category, and the maintenance of a budget. As such, they do a great job at automating the download of your credit card and bank transactions. I think that if you want a low-maintenance option to tracking your finances and you don\u2019t have a problem with the obvious privacy risks, this is a fantastic option: it comes with a web interface, mobile apps, and updating your accounts probably requires clicking some buttons, providing some passwords, and a small amount of manual corrections every couple of weeks. I have several reservations about using it for myself, however: Passwords. I\u2019m just not comfortable enough with any commercial company to share the passwords to all my bank, credit card, and investment accounts. This sounds like an insane idea to me, a very scary one, and I\u2019m just not willing to put that much trust in any one company\u2019s hands. I don\u2019t care what they say: I worked for several software companies and I\u2019m aware of the disconnect between promises made by salespeople, website PR, and engineering reality. I also know the power of determined computer hackers. Insufficient reporting. Reporting is probably beautiful and colorful\u2014it\u2019s gorgeous on the website\u2014but certainly insufficient for all the reporting I want to do on my data. Reporting that doesn\u2019t do what they want it to is a common complaint I hear about these systems. With my system, I can always write a script and produce any kind of report I might possibly want in the future. For instance, spitting out a treemap visualization of my expenses instead of a useless pie chart. I can slice and dice my transactions in all kinds of unique ways. I can tag subsets of transactions for projects or trips and report on them. It\u2019s more powerful than generic reporting. Perennity. What if the company is not in business in 5 years? Where will my data be? If I spend any time at all curating my financial data, I want to ensure that it will be available to me forever, in an open format. In their favor, some of these sites probably have a downloadable option, but who uses it? Does it even work well? Would it include all of the transactions that they downloaded? I don\u2019t know. Not international. It probably does not work well with an international, multi-currency situation. These services target a \u201cmajority\u201d of users, most of which have all their affairs in a single country. I live in the USA, have a past history in Canada, which involves remaining tax-sheltered investment accounts, occasional expenses during visits which justify maintaining a credit card there, and a future history which might involve some years spent in another country such as Hong Kong or Australia. Will this work in Mint? Probably not. They might support Canada, but will they support accounts in both places? How about some other country I might want to move to? I want a single integrated view of all my accounts across all countries in all currencies forever, nothing less. My system supports that very well. (I\u2019m not aware of any double-entry system that deals with the international problem in a currency agnostic way as well as Beancount does.) Inability to deal with particular situations. What if I own real estate? Will I be able to price the value of my home at a reasonable amount, so it creates an accurate balance sheet? Regularly obtaining \u201ccomparables\u201d for my property from an eager real estate agent will tell me much more precisely how much my home is worth than services like Zillow ever could. I need to be able to input that for my balance sheet. What about those stock options from that privately held Australian company I used to work for? How do I price that? How about other intangible things, such as receivables from a personal loan I made to a friend? I doubt online services are able to provide you with the ability to enter those. If you want the whole picture with precision, you need to be able to make these adjustments. Custom tracking. Using \u201cimaginary currencies\u201d, I\u2019m able to track all kinds of other things than currencies and stocks. For example, by using an \u201c IRAUSD \u201d commodity in Beancount I\u2019m able to track how many 401k contributions I\u2019ve made at any point during the year. I can count the after-tax basis of a traditional IRA account similarly. I can even count my vacation hours using an imaginary \u201c VACHR \u201d currency and verify it against my pay stubs. Capital gains reporting. I haven\u2019t tried it myself, but I\u2019ve heard some discontent about Mint from others about its limited capabilities for capital gains reporting. Will it maintain trade lots? Will it handle average cost booking? How about PnL on FOREX gains? What about revenue received for that book I\u2019m selling via PayPal? I want to be able to use my accounting system as an input for my tax reporting. Cash transactions. How difficult is it to enter cash transactions? Do I have to log in, or start a slow, heavy program that will only work under Windows? With Beancount I bring up a file in a text editor, this is instant and easy. Is it even possible to enter custom cash entries in online services? In other words, I\u2019m a very sophisticated user and yes, a bit of a control freak . I\u2019m not lying about it. I don\u2019t have any ideological objections about using a commercial service, it is probably not for me. If it\u2019s good enough for you, suit yourself. Despite their beautiful and promising websites, I haven\u2019t heard of anyone being completely satisfied with these services. I hear a fair amount of complaining, some mild satisfaction, but I have yet to meet anyone who raves about it.","title":"How about Mint.com?"},{"location":"command_line_accounting_in_context.html#how-about-quicken","text":"Quicken is a single-entry system, that is, it replicates the transactions of a remote account locally, and allows you to add a label to each transaction to place it in a category. I believe it also has support for synchronizing investment accounts. This is not enough for me, I want to track all kinds of things, and I want to use the double-entry method, which provides an intrinsic check that I\u2019ve entered my data correctly. Single-entry accounting is just not good enough if you\u2019ve already crossed the bridge of understanding the double-entry method.","title":"How about Quicken?"},{"location":"command_line_accounting_in_context.html#how-about-quickbooks","text":"So let\u2019s talk about sophisticated software that is good enough for a company. Why wouldn\u2019t I use that instead? If it\u2019s good enough for small businesses, it should be good enough for me, no? There\u2019s Quickbooks and other ones. Why don\u2019t I use them: It costs money. Commercial software comes at a price. Ok, I probably could afford to pay a few hundred dollars per year (Quickbooks 2014 looks like around 300$/year for the full set of features), but I don\u2019t really want to. Platform. These softwares usually run on Microsoft Windows and sometimes on Mac OS X . I\u2019m a software developer, I mostly use Linux , and a Macbook Air for a laptop, on which I get annoyed running anything other than tmux and a web browser . I\u2019m not going to reboot just to enter a quick cash transaction. Slow startup. I cannot speak specifically to Quickbooks\u2019 implementation, but virtually every software suite of commercial scope I\u2019ve had to use had a splash screen and a slow, slow startup that involved initializing tons of plugins. They assume you\u2019re going to spend hours in it, which is reasonable for commercial users, but not for me, if I want to do a quick update of my ledger. UIs are inadequate. I haven\u2019t seen their UI but given the nature of transactions and my desire to input precisely and search quickly and organize things, I want to be able to edit in as text. I imagine it would be inconvenient for me. With Emacs and org-mode , I can easily i-search my way to any transaction within seconds after opening my ledger file. Inflexible. How would I go about re-organizing all my account names? I think I\u2019m still learning about the double-entry bookkeeping method and I have made mistakes in the past, mistakes where I desire to revisit the way I organize my accounts in a hierarchy. With my text file, I was able to safely rename a large number of accounts several times, and evolve my chart-of-accounts to reflect my improving understanding of how to organize my financial tracking system. Text is powerful!","title":"How about Quickbooks?"},{"location":"command_line_accounting_in_context.html#how-about-gnucash","text":"I don\u2019t like UIs; they\u2019re inconvenient. There\u2019s nothing quite like editing a text file if you are a programmer. Moreover, GnuCash does not deal with multiple currencies well. I find bugs in it within the first hour every time I kick the tires on it, which I do every couple of years, out of curiosity. Other programs, such as Skrooge, also take the heavy-handed big UI approach.","title":"How about GnuCash?"},{"location":"command_line_accounting_in_context.html#why-build-a-computer-language","text":"A bookkeeping system provides conditions for a solution that involves a simple computer language for many reasons. Single-entry bookkeeping is largely insufficient if you're trying to track everything holistically. Existing systems either limit themselves to expense categories with little checking beyond \"reconciling\" which sometimes involves freezing the past. If you're not doing the bookkeeping for a company, sometimes just changing the past and fixing the mistakes where they occurred makes more sense. More importantly, the single-entry method leaves us wanting for the natural error-checking mechanism involved in the double-entry system. The problem is also not solvable elegantly by using spreadsheets; the simple data structure that forms the basis of the double-entry system infers either a very sparse spreadsheet with accounts on one dimension and transactions on the other. For real-world usage, this is impractical. Another iteration on this theme would involve inserting the postings with two columns, one with the account and one with the amount, but the varying number of columns and the lookup code makes this inelegant as well. Plus, it's not obvious how you would deal with a large number of currencies. Programs that provide fancy graphical or web-based user interfaces are inevitably awkward, due to the nature of the problem: each transaction is organized by viewing it through the lens of one account's journal, but any of the accounts present in its postings provide equally valid views. Ideally, what you want, is just to look at the transaction. Organizing them for most convenient input has little to do with the order in which they are to be presented. Using text has a lot of advantages: You can easily used search-and-replace and/or sed to make global changes, for example, rename or reorganize your accounts; You can organize the transactions in the order that is most convenient for data entry; There are a number of existing tools to search the text; You can easily write various little tools to spit out the data syntax, i.e., for importing from other file types, or converting from other systems. Text is inherently open , that is the file format is one that you can read your data from and store anywhere else, not a blob of incomprehensible data that becomes unusable when the company that makes the software stops supporting it. Finally, systems that attempt to automate the process of importing all your data from automated sources (e.g., mint.com) have one major shortfall: most often it's not very easy or even possible to add information for accounts that aren't automated. It is my experience that in practice you will have some entries and accounts to track that will not have a nice downloadable file format, or that simply don't have a real-world counterpart. In order to produce a complete view of one's balance sheet, it is important to be able to enter all of an individual's account within a single system. In other words, custom accounts and manually entered transactions do matter a lot. For all the reasons mentioned above, I feel that a computer language is more appropriate to express this data structure than a heavy piece of software with a customized interface. Being able to easily bring up a text file and quickly type in a few lines of text to add a transaction is great\u2013it's fast and easy. The ledger file provides a naturally open way to express one's data, and can be source-controlled or shared between people as well. Multiple files can be merged together, and scripted manipulations on a source file can be used to reorganize one's data history. Furthermore, a read-only web interface that presents the various reports one expects and allows the user to explore different views on the dataset is sufficient for the purpose of viewing the data.","title":"Why build a computer language?"},{"location":"command_line_accounting_in_context.html#advantages-of-command-line-bookkeeping","text":"In summary, there are many advantages to using a command-line accounting system over a commercial package that provides a user-interface: Fast. You don\u2019t have to fire up a slow program with a splash screen that will take a while to initialize in order to add something to your books. Bringing up a text file from a bookmark in your favorite editing program (I use Emacs) is easy and quick. And if you\u2019re normally using a text editor all day long, as many programmers do, you won\u2019t even blink before the file is in front of your eyes. It\u2019s quick and easy. Portable. It will work on all platforms. Beancount is written in Python 3 with some C extensions, and as such will work on Mac, Linux and Windows platforms. I am very careful and determined to keep external dependencies on third-party packages to an absolute minimum in order to avoid installation problems. It should be easy to install and update, and work everywhere the same. Openness. Your data is open, and will remain open forever . I plan on having my corpus of data until the day I die. With an open format you will never end up in a situation where your transactional data is sitting in a binary blob with an unknown format and the software goes unsupported. Furthermore, your data can be converted to other languages easily. You can easily invoke the parser I provide and write a script that spits it out in another program\u2019s input syntax. You can place it under version control. You can entirely reorganize the structure of your accounts by renaming strings with sed. Text is empowering. Customized. You can produce very customized reports, that address exactly the kinds of problems you are having. One complaint I often hear from people about other financial software is that it doesn\u2019t quite do what they want. With command-line accounting systems you can at least write it yourself, as a small extension that uses your corpus of data.","title":"Advantages of Command-Line Bookkeeping"},{"location":"command_line_accounting_in_context.html#why-not-just-use-an-sql-database","text":"I don\u2019t like to reinvent the wheel. If this problem could be solved by filling up an SQL database and then making queries on it, that\u2019s exactly what I would do. Creating a language is a large overhead, it needs to be maintained and evolved, it\u2019s not an easy task, but as it turns out, necessary and justified to solve this problem. The problem is due to a few reasons: Filtering occurs on a two-level data structure of transactions vs. their children postings, and it is inconvenient to represent the data in a single table upon which we could then make manipulations. You cannot simply work only with postings: in order to ensure that the reports balance, you need to be selecting complete transactions. When you select transactions, the semantics is \u201cselect all the postings for which the transaction has this or that property.\u201d One such property is \u201call transactions that have some posting with account X .\u201d These semantics are not obvious to implement with a database. The nature of the data structure makes it inconvenient. The wide variety of directives makes it difficult to design a single elegant table that can accommodate all of their data. Ideally we would want to define two tables for each type of directive: a table that holds all common data (date, source filename & lineno, directive type) and a table to hold the type-specific data. While this is possible, it steps away from the neat structure of a single table of data. Operations on inventories\u2014the data structure that holds the incrementally changing contents of accounts\u2014require special treatment that would be difficult to implement in a database. Lot reductions are constrained against a running inventory of lots, each of which has a specific cost basis. Some lot reductions trigger the merging of lots (for average cost booking). This requires some custom operations on these inventory objects. Aggregations (breakdowns) are hierarchical in nature. The tree-like structure of accounts allows us to perform operations on subtrees of postings. This would also not be easy to implement with tables. Finally, input would be difficult. By defining a language, we side-step the problem of having to build a custom user interface that would allow us to create the data. Nevertheless, I really like the idea of working with databases. A script (bean-sql) is provided to convert the contents of a Beancount file to an SQL database; it\u2019s not super elegant to carry out computations on those tables, but it\u2019s provided nonetheless. You should be able to play around with it, even if operations are difficult. I might even pre-compute some of the operation\u2019s outputs to make it easier to run SQL queries.","title":"Why not just use an SQL database?"},{"location":"command_line_accounting_in_context.html#but-i-just-want-to-do-x","text":"Some may infer that what we\u2019re doing must be terribly complicated given that they envision they might want to use such a system only for a single purpose. But the fact is, how many accounts you decide to track is a personal choice. You can choose to track as little as you want in as little detail as you deem sufficient. For example, if all that you\u2019re interested in is investing, then you can keep books on only your investment accounts. If you\u2019re interested in replacing your usage of Mint.com or Quicken , you can simply just replicate the statements for your credit cards and see your corresponding expenses. The simplicity of having to just replicate the transactions in all your accounts over doing a painful annual \u201cstarting from scratch\u201d evaluation of all your assets and expenses for the purpose of looking at your finances will convince you. Looking at your finances with a spreadsheet will require you to at least copy values from your portfolio. Every time you want to generate the report you\u2019ll have to update the values\u2026 with my method, you just update each account\u2019s full activity, and you can obtain the complete list holdings as a by-product. It\u2019s easy to bring everything up-to-date if you have a systematic method. To those I say: try it out, accounting just for the bits that you\u2019re interested in. Once you\u2019ll have a taste of how the double-entry method works and have learned a little bit about the language syntax, you will want to get a fuller picture. You will get sucked in.","title":"But... I just want to do X?"},{"location":"command_line_accounting_in_context.html#why-am-i-so-excited","text":"Okay, so I\u2019ve become an accounting nerd. I did not ask for it. Just kind-of happened while I wasn\u2019t looking (I still don\u2019t wear brown socks though). Why is it I can\u2019t stop talking about this stuff? I used to have a company. It was an umbrella for doing contract work (yes, I originally had bigger aspirations for it, but it ended up being just that. Bleh.) As a company owner in Canada, I had to periodically make five different kinds of tax installments to the Federal and Provincial governments, some every month, some every three months, and then it varied. An accountant was feeding me the \u201cmagic numbers\u201d to put in the forms, numbers which I was copying like a monkey, without fully understanding how they were going to get used later on. I had to count separately the various expenses I would incur in relation to my work (for deductions), and those which were only personal \u2014I did not have the enlightened view of getting separate credit cards so I was trying to track personal vs. company expenses from the same accounts. I also had to track transfers between company and personal accounts that would later on get reported as dividends or salary income. I often had multiple pending invoices that would take more than two months to get paid, from different clients. It was a demi-hell. You get the idea. I used to try to track all these things manually, using systems composed of little text files and spreadsheets and doing things very carefully and in a particular way. The thing is, although I am by nature quite methodical, I would sometimes, just sometimes forget to update one of the files. Things would fall out of sync. When this happened it was incredibly frustrating, I had to spend time digging around various statements and figure out where I had gone wrong. This was time-consuming and unpleasant. And of course, when something you have to do is unpleasant, you tend not to do it so well. I did not have a solid idea of the current state of affairs of my company during the year, I just worked and earned money for it. My accountant would draw a balance sheet once a year after doing taxes in July, and it was always a tiny bit of a surprise. I felt permanently somewhat confused, and frankly annoyed every time I had to deal with \u201cmoney things.\u201d It wasn\u2019t quite a nightmare, but it was definitely making me tired. I felt my life was so much simpler when I just had a job. But one day\u2026 one day\u2026 I saw the light: I discovered the double-entry method. I don\u2019t recall exactly how. I think it was on a website, yes\u2026 this site: dwmbeancounter.com (it\u2019s a wonderful site). I realized that using a single system , I could account for all of these problems at the same time, and in a way that would impose an inherent error-checking mechanism. I was blown away! I think I printed the whole thing on paper at the time and worked my way through every tutorial. This simple counting trick is exactly what I was in dire need for. But all the software I tried was either disappointing, broken, or too complicated. So I read up on Ledger. And I got in touch with its author . And then shortly after I started on Beancount 1 . I\u2019ll admit that going through the effort of designing my own system just to solve my accounting problems is a bit overkill, but I\u2019ve been known to be a little more than extreme about certain things , and I\u2019ve really enjoyed solving this problem. My life is fulfilled when I maintain a good balance of \u201clearning\u201d and \u201cdoing,\u201d and this falls squarely in the \u201cdoing\u201d domain. Ever since, I feel so excited about anything related to personal finance. Probably because it makes me so happy to have such a level of awareness about what\u2019s going on with mine. I even sometimes find myself loving spending money in a new way, just from knowing that I\u2019ll have to figure out how to account for it later. I feel so elated by the ability to solve these financial puzzles that I have had bouts of engaging complete weekends in building this software. They are small challenges with a truly practical application and a tangible consequence. Instant gratification. I get a feeling of empowerment . And I wish the same for you. For a full list of differences, refer to this document . \u21a9","title":"Why am I so Excited?"},{"location":"exporting_your_portfolio.html","text":"Exporting Your Portfolio \uf0c1 Martin Blais , December 2015 (v2) http://furius.ca/beancount/doc/export Overview \uf0c1 This document explains how to export your portfolio of holdings from Beancount to a Google Finance portfolio (and eventually to other portfolio tracking websites). Note: This is the second version of this document, rewritten in Dec 2015, after greatly simplifying the process of exporting portfolios and completely separating the specification of stock tickers for price downloads. This new, simplified version only uses a single metadata field name: \u201cexport\u201d. The previous document can be found here . Portfolio Tracking Tools \uf0c1 There are multiple websites on the internet that allow someone to create a portfolio of investments (or upload a list of transactions to create such a portfolio) and that reports on the changes in the portfolio due to price movements, shows you unrealized capital gains, etc. One such website is the Google Finance portal. Another example is the Yahoo Finance one. These are convenient because they allow you to monitor the impact of price changes on your entire portfolio of assets, across all accounts, during the day or otherwise. However, each of these sites expects their users to use their interfaces and workflows to painfully enter each of the positions one-by-one. A great advantage of using Beancount is that you should never have to enter this type of information manually; instead, you should be able to extract it and upload it to one of these sites. You can be independent of the particular portfolio tracking service you use and should be able to switch between them without losing any data; Beancount can serve as your pristine source for your list of holdings as your needs evolve. Google Finance supports an \u201cimport\u201d feature to create portfolio data which supports the Microsoft OFX financial interchange data format. In this document, we show how we built a Beancount report that exports the portfolio of holdings to OFX for creating a Google Finance portfolio. Exporting to Google Finance \uf0c1 Exporting your Holdings to OFX \uf0c1 First, create an OFX file corresponding to your Beancount holdings. You can use this command to do this: bean-report file.beancount export_portfolio > portfolio.ofx See the report\u2019s own help for options: bean-report file.beancount export_portfolio --help Importing the OFX File in Google Finance \uf0c1 Then we have to import that OFX file in a web-based portfolio. Visit http://finance.google.com and click on \u201cPortfolios\u201d on the left (or simply visit https://www.google.com/finance/portfolio , this works as of Jan 2015) If you have an existing, previously imported portfolio, click on \u201cDelete Portfolio\u201d to get rid of it. Click on \u201cImport Transactions\u201d, then \u201cChoose File\u201d and select the portfolio.ofx file you exported to, then click on \u201cPreview Import\u201d. You should see a list of imported lots, with familiar stock symbols and names, and Type \u201cBuy\u201d with realistic Shares and Price columns. If not, see the note below. Otherwise, scroll to the bottom of the page and click \u201cImport\u201d. Your portfolio should now appear. You are done. You should never bother updating this portfolio directly using the website\u2026 instead, update your Beancount ledger file, re-export to a new OFX file, delete the previous portfolio and re-import a brand new one over it. Your pristine source is always your Beancount file, ideally you should never have to be worried about corrupting or deleting the portfolio data in any external website. Controlling Exported Commodities \uf0c1 Declaring Your Commodities \uf0c1 Generally, we recommend that you explicitly declare each of the commodities used in your input file. It is a neat place to attach information about those commodities, metadata that you should be able to use later on from bean-query or in scripts that you make. For example, you could declare a human-readable description of the commodity, and some other attributes, like this: 2001-09-06 commodity XIN name: \"iShares MSCI EAFE Index ETF (CAD-Hedged)\" class: \"Stock\" type: \"ETF\" ... Beancount will work with or without these declarations (it automatically generates Commodity directives if you haven\u2019t provided them). If you like to be strict and have a bit of discipline, you can require that each commodity be declared by using a plugin that will issue an error when an undeclared commodity appears: plugin \"beancount.plugins.check_commodity\" You can use any date for that Commodity directive. I recommend using the date of the commodity\u2019s inception, or perhaps when it was first introduced by the issuing country, if it is a currency. You can find a suitable date on Wikipedia or on the issuer\u2019s websites. Google Finance may have the date itself. What Happens by Default \uf0c1 By default, all holdings are exported as positions with a ticker symbol named the same as the Beancount commodity that you used to define them. If you have a holding of \u201cAAPL\u201d units, it will create an export entry for \u201cAAPL\u201d. The export code attempts to export all holdings by default. However, in any but the simplest unambiguous cases, this is probably not good enough to produce a working Google Finance portfolio. The name for each commodity that you use in your Beancount input file may or may not correspond to a financial instrument in the Google Finance database; due to the very large number of symbols supported in its database, just specifying the ticker symbol is often ambiguous. Google Finance attempts to resolve an ambiguous symbol string to the most likely instrument in its database. It is possible that it resolves it to a different financial instrument from the one you intended. So even if you use the same basic symbol that is used by the exchange, you often still need to disambiguate the symbol by specifying which exchange or symbology it lives in. Google provides a list of these symbol spaces . Here is a real-life example. The symbol for the \u201c CAD-Hedged MSCI EAFE Index \u201d ETF product issued by iShares/Blackrock is \u201c XIN \u201d on the Toronto Stock Exchange ( TSE ). If you just looked up \u201cXIN\u201d on Google Finance , it would choose to resolve it by default to the more likely \u201c NYSE:XIN \u201d symbol ( Xinyuan Real Estate Co. on the New York Stock Exchange ). So you need to disambiguate it by specifying that the desired ETF ticker for this instrument is \u201c TSE:XIN \u201d. Explicitly Specifying Exported Symbols \uf0c1 You can specify which exchange-specific symbol is used to export a commodity by attaching an \u201c export \u201d metadata field to each of your Commodity directives, like this: 2001-09-06 commodity XIN ... export: \"TSE:XIN\" The \u201c export \u201d field is used to map your commodity name to the corresponding instrument in the Google Finance system. If a holding in that commodity needs to be exported, this code is used instead of the Beancount currency name. The symbology used by Google Finance appears to follow the following syntax: Exchange:Symbol \uf0c1 where Exchange is a code either for the exchange where the stock trades, or for another source of financial data, e.g. \u201c MUTF \u201d for \u201cmutual funds in the US\u201d, and more . Symbol is a name that is unique within that exchange. I recommend searching for each of your financial instruments in Google Finance, confirming that the instrument corresponds to your instrument (by inspecting the full name, description and price), and inserting the corresponding code like this. Exporting to a Cash Equivalent \uf0c1 To account for positions that aren\u2019t supported in Google Finance, the export report can convert a holding to its cash-equivalent value. This is also useful for cash positions (e.g., cash sitting idle in a savings or checking account). For example, I hold units of an insurance policy investment vehicle (this is common in Canada, for example, with London Life). This is a financial instrument, but each particular policy issuance has its own associated value\u2014there is no public source of data for each of those products, it\u2019s rather opaque, I can obtain its value with my annual statement, but definitely not in Google Finance. But I\u2019d still like for the asset\u2019s value to be reflected in my portfolio. The way you tell the export code to make this conversion is to specify a special value of \u201cCASH\u201d for the \u201cexport\u201d field, like this: 1878-01-01 commodity LDNLIFE export: \"CASH\" This would convert holdings in LDNLIFE commodities to their corresponding quoted value before exporting, using the price nearest to the date of exporting. Note that attempting to convert to cash a commodity that does not have a corresponding cost or price available for us to determine its value will generate an error. A price must be present to make the conversion. Simple currencies should also be marked as cash in order to be exported: 1999-01-01 commodity EUR name: \"European Union Euro currency\" export: \"CASH\" Finally, all converted holdings are agglomerated into a single cash position. There is no point in exporting these cash entries to separate OFX entries because the Google Finance code will agglomerate them to a single one anyhow. Declaring Money Instruments \uf0c1 There is a small hiccup in this cash conversion story: the Google Finance importer does not appear to correctly grok an OFX position in \u201ccash\u201d amounts in the importer; I think this is probably just a bug in Google Finance\u2019s import code (or perhaps I haven\u2019t found the correct OFX field values to make this work). Instead, in order to insert a cash position the exporter uses a cash-equivalent commodity which always prices at 1 unit of the currency, e.g. $1.00 for US dollars. For example, for US dollars I I use VMMXX which is a Vanguard Prime Money Market Fund, and for Canadian dollars I use IGI806 . A good type of commodity for this is some sort of Money Market fund. It doesn\u2019t matter so much which one you use, as long as it prices very close to 1. Find one. If you want to include cash commodities, you need to find such a commodity for each of the cash currencies you have on your books and tell Beancount about them. Typically that will be only one or two currencies. You declare them by append the special value \u201c MONEY \u201d for the \u201c export \u201d field, specifying which currency this commodity represents, like this: 1900-01-01 commodity VMMXX export: \"MUTF:VMMXX (MONEY:USD)\" 1900-01-01 commodity IGI806 export: \"MUTF_CA:IGI806 (MONEY:CAD)\" Ignoring Commodities \uf0c1 Finally, some commodities held in a ledger should be ignored. This is the case for the imaginary commodities used in mirror accounting, for example, to track unvested shares of an employment stock plan, or commodities used to track amounts contributed to a retirement account, like this: 1996-01-01 commodity RSPCAD name: \"Canada Registered Savings Plan Contributions\" You tell the export code to ignore a commodity specifying the special value \u201c IGNORE \u201d for the \u201c export \u201d field, like this: 1996-01-01 commodity RSPCAD name: \"Canada Registered Savings Plan Contributions\" export: \"IGNORE\" All holdings in units of RSPCAD will thus not be exported. The question of whether some commodities should be exported or not sometimes presents interesting choices. Here is an example: I track my accumulated vacation hours in an asset account. The units are \u201c VACHR \u201d. I associate with this commodity a price that is roughly equivalent to my net hourly salary. This gives me a rough idea how much vacation time money is accumulated on the books, e.g. if I quit my job, how much I\u2019d get paid. Do I want to them included in my total net worth? Should the value from those hours be reflected in the value of my exported portfolio? I think that largely depends on whether I plan to use up those vacations before I leave this job or not, whether I want to have this accumulated value show up on my balance sheet. Comparing with Net Worth \uf0c1 The end result is that the sum total of all your exported positions plus the cash position should approximate the value of all your assets, and the total value calculated by the Google Finance website should be very close to the one reported by this report: bean-report file.beancount networth As a point of comparison, the value of my own portfolio is usually close to within a few hundred US dollars. Details of the OFX Export \uf0c1 Import Failures \uf0c1 Exporting a portfolio with symbols that Google Finance does not recognize fatally trips up Google\u2019s import feature. Google Finance then proceeds to fail to recognize your entire file. I recommend that you use explicit exchange:symbol names on all commodities that get exported in order to avoid this problem, as is described further in this document. Google Finance can also be a little bit finicky about the format of the particular OFX file you give it to import. The export_portfolio command attempts to avoid OFX features that would break it but it\u2019s fragile, and it\u2019s possible that the particulars of your portfolio\u2019s contents triggers output that fails to import. If this is the case, at step (4) above, instead of a list of stock symbols you would see a long list of positions that look like XML tags (this is how failure manifests itself). If that is the case, send email to the mailing-list (best if you can isolate the positions that trigger breakage and have the capability to diff files and do some troubleshooting). Mutual Funds vs. Stocks \uf0c1 The OFX format distinguishes between stocks and mutual funds. In practice, the Google Finance importer does not appear to distinguish between these two (at least it appears to behave the same way), so this is likely an irrelevant implementation detail. Nevertheless, the export code is able to honor the OFX convention of distinguishing between \u201cBUYMF\u201d vs. \u201cBUYSTOCK\u201d XML elements. To this effect, the export code attempts to classify which commodities represent mutual funds by inspecting whether the ticker associated with the commodity begins with the letters \u201cMUTF\u201d and is followed by a colon. For example, \u201c MUTF:RGAGX \u201d and \u201c MUTF_CA:RBF1005 \" will both be detected as mutual funds, for example. Debugging the Export \uf0c1 In order to debug how each of your holdings gets exported, use the --debug flag, which will print a detailed account of how each holding is handled by the export script to stderr : bean-report file.beancount export_portfolio --debug 2>&1 >/dev/null | more The script should print the list of exported positions and their corresponding holdings, then the list of converted positions and their corresponding holdings (usually many cash positions are aggregated together) and finally, the list of ignored holdings. This should be enough to explain the entire contents of the exported portfolio. Purchase Dates \uf0c1 Beancount does not currently have all the lot purchase dates, so the purchase dates are exported as if purchased the day before the export. Eventually, when the purchase date is available in Beancount (pending the inventory booking changes ) the actual lot purchase date will probably be used in the export format. However, it\u2019s not yet clear that using the correct date is the right thing to do, because Google Finance might insist on inserting cash for dividends since the reported purchase date\u2026 but Beancount already takes care of inserting a special lot for cash that should already include this. We shall see when we get there. Disable Dividends \uf0c1 Under the \u201cEdit Portfolio\u201d option there is a checkbox that appears to disable the calculation of dividends offered. It would be nice to find a way to automatically disable this checkbox upon import. Automate Upload \uf0c1 It would be nice to automate the replacement of the portfolio with a Python script. Unfortunately, the Google Finance API has been deprecated. Maybe someone can write a screen-scraping routine to do this. Summary \uf0c1 Each holding\u2019s export can be controlled by how its commodity is treated, in one of the following ways: Exported to a portfolio position. This is the default, but you should specify the ticker symbol using the \u201c ticker \u201d or \u201c export \u201d metadata fields, in \u201c ExchangeCode:Symbol \u201d format. Converted to cash and exported to a money market cash-equivalent position, by setting the value of the \u201c export \u201d metadata field to the special value \u201c CASH \u201d. Ignored by specifying the \u201c export \u201d metadata field to the special value \u201c IGNORE \u201d. Provided as Money Instrument , to be used for cash-equivalent value of each holding intended to be converted to cash and included in the portfolio. These are identified by a special value \u201c( MONEY:) \u201d in the \u201c export \u201d metadata field.","title":"Exporting Your Portfolio"},{"location":"exporting_your_portfolio.html#exporting-your-portfolio","text":"Martin Blais , December 2015 (v2) http://furius.ca/beancount/doc/export","title":"Exporting Your Portfolio"},{"location":"exporting_your_portfolio.html#overview","text":"This document explains how to export your portfolio of holdings from Beancount to a Google Finance portfolio (and eventually to other portfolio tracking websites). Note: This is the second version of this document, rewritten in Dec 2015, after greatly simplifying the process of exporting portfolios and completely separating the specification of stock tickers for price downloads. This new, simplified version only uses a single metadata field name: \u201cexport\u201d. The previous document can be found here .","title":"Overview"},{"location":"exporting_your_portfolio.html#portfolio-tracking-tools","text":"There are multiple websites on the internet that allow someone to create a portfolio of investments (or upload a list of transactions to create such a portfolio) and that reports on the changes in the portfolio due to price movements, shows you unrealized capital gains, etc. One such website is the Google Finance portal. Another example is the Yahoo Finance one. These are convenient because they allow you to monitor the impact of price changes on your entire portfolio of assets, across all accounts, during the day or otherwise. However, each of these sites expects their users to use their interfaces and workflows to painfully enter each of the positions one-by-one. A great advantage of using Beancount is that you should never have to enter this type of information manually; instead, you should be able to extract it and upload it to one of these sites. You can be independent of the particular portfolio tracking service you use and should be able to switch between them without losing any data; Beancount can serve as your pristine source for your list of holdings as your needs evolve. Google Finance supports an \u201cimport\u201d feature to create portfolio data which supports the Microsoft OFX financial interchange data format. In this document, we show how we built a Beancount report that exports the portfolio of holdings to OFX for creating a Google Finance portfolio.","title":"Portfolio Tracking Tools"},{"location":"exporting_your_portfolio.html#exporting-to-google-finance","text":"","title":"Exporting to Google Finance"},{"location":"exporting_your_portfolio.html#exporting-your-holdings-to-ofx","text":"First, create an OFX file corresponding to your Beancount holdings. You can use this command to do this: bean-report file.beancount export_portfolio > portfolio.ofx See the report\u2019s own help for options: bean-report file.beancount export_portfolio --help","title":"Exporting your Holdings to OFX"},{"location":"exporting_your_portfolio.html#importing-the-ofx-file-in-google-finance","text":"Then we have to import that OFX file in a web-based portfolio. Visit http://finance.google.com and click on \u201cPortfolios\u201d on the left (or simply visit https://www.google.com/finance/portfolio , this works as of Jan 2015) If you have an existing, previously imported portfolio, click on \u201cDelete Portfolio\u201d to get rid of it. Click on \u201cImport Transactions\u201d, then \u201cChoose File\u201d and select the portfolio.ofx file you exported to, then click on \u201cPreview Import\u201d. You should see a list of imported lots, with familiar stock symbols and names, and Type \u201cBuy\u201d with realistic Shares and Price columns. If not, see the note below. Otherwise, scroll to the bottom of the page and click \u201cImport\u201d. Your portfolio should now appear. You are done. You should never bother updating this portfolio directly using the website\u2026 instead, update your Beancount ledger file, re-export to a new OFX file, delete the previous portfolio and re-import a brand new one over it. Your pristine source is always your Beancount file, ideally you should never have to be worried about corrupting or deleting the portfolio data in any external website.","title":"Importing the OFX File in Google Finance"},{"location":"exporting_your_portfolio.html#controlling-exported-commodities","text":"","title":"Controlling Exported Commodities"},{"location":"exporting_your_portfolio.html#declaring-your-commodities","text":"Generally, we recommend that you explicitly declare each of the commodities used in your input file. It is a neat place to attach information about those commodities, metadata that you should be able to use later on from bean-query or in scripts that you make. For example, you could declare a human-readable description of the commodity, and some other attributes, like this: 2001-09-06 commodity XIN name: \"iShares MSCI EAFE Index ETF (CAD-Hedged)\" class: \"Stock\" type: \"ETF\" ... Beancount will work with or without these declarations (it automatically generates Commodity directives if you haven\u2019t provided them). If you like to be strict and have a bit of discipline, you can require that each commodity be declared by using a plugin that will issue an error when an undeclared commodity appears: plugin \"beancount.plugins.check_commodity\" You can use any date for that Commodity directive. I recommend using the date of the commodity\u2019s inception, or perhaps when it was first introduced by the issuing country, if it is a currency. You can find a suitable date on Wikipedia or on the issuer\u2019s websites. Google Finance may have the date itself.","title":"Declaring Your Commodities"},{"location":"exporting_your_portfolio.html#what-happens-by-default","text":"By default, all holdings are exported as positions with a ticker symbol named the same as the Beancount commodity that you used to define them. If you have a holding of \u201cAAPL\u201d units, it will create an export entry for \u201cAAPL\u201d. The export code attempts to export all holdings by default. However, in any but the simplest unambiguous cases, this is probably not good enough to produce a working Google Finance portfolio. The name for each commodity that you use in your Beancount input file may or may not correspond to a financial instrument in the Google Finance database; due to the very large number of symbols supported in its database, just specifying the ticker symbol is often ambiguous. Google Finance attempts to resolve an ambiguous symbol string to the most likely instrument in its database. It is possible that it resolves it to a different financial instrument from the one you intended. So even if you use the same basic symbol that is used by the exchange, you often still need to disambiguate the symbol by specifying which exchange or symbology it lives in. Google provides a list of these symbol spaces . Here is a real-life example. The symbol for the \u201c CAD-Hedged MSCI EAFE Index \u201d ETF product issued by iShares/Blackrock is \u201c XIN \u201d on the Toronto Stock Exchange ( TSE ). If you just looked up \u201cXIN\u201d on Google Finance , it would choose to resolve it by default to the more likely \u201c NYSE:XIN \u201d symbol ( Xinyuan Real Estate Co. on the New York Stock Exchange ). So you need to disambiguate it by specifying that the desired ETF ticker for this instrument is \u201c TSE:XIN \u201d.","title":"What Happens by Default"},{"location":"exporting_your_portfolio.html#explicitly-specifying-exported-symbols","text":"You can specify which exchange-specific symbol is used to export a commodity by attaching an \u201c export \u201d metadata field to each of your Commodity directives, like this: 2001-09-06 commodity XIN ... export: \"TSE:XIN\" The \u201c export \u201d field is used to map your commodity name to the corresponding instrument in the Google Finance system. If a holding in that commodity needs to be exported, this code is used instead of the Beancount currency name. The symbology used by Google Finance appears to follow the following syntax:","title":"Explicitly Specifying Exported Symbols"},{"location":"exporting_your_portfolio.html#exchangesymbol","text":"where Exchange is a code either for the exchange where the stock trades, or for another source of financial data, e.g. \u201c MUTF \u201d for \u201cmutual funds in the US\u201d, and more . Symbol is a name that is unique within that exchange. I recommend searching for each of your financial instruments in Google Finance, confirming that the instrument corresponds to your instrument (by inspecting the full name, description and price), and inserting the corresponding code like this.","title":"Exchange:Symbol"},{"location":"exporting_your_portfolio.html#exporting-to-a-cash-equivalent","text":"To account for positions that aren\u2019t supported in Google Finance, the export report can convert a holding to its cash-equivalent value. This is also useful for cash positions (e.g., cash sitting idle in a savings or checking account). For example, I hold units of an insurance policy investment vehicle (this is common in Canada, for example, with London Life). This is a financial instrument, but each particular policy issuance has its own associated value\u2014there is no public source of data for each of those products, it\u2019s rather opaque, I can obtain its value with my annual statement, but definitely not in Google Finance. But I\u2019d still like for the asset\u2019s value to be reflected in my portfolio. The way you tell the export code to make this conversion is to specify a special value of \u201cCASH\u201d for the \u201cexport\u201d field, like this: 1878-01-01 commodity LDNLIFE export: \"CASH\" This would convert holdings in LDNLIFE commodities to their corresponding quoted value before exporting, using the price nearest to the date of exporting. Note that attempting to convert to cash a commodity that does not have a corresponding cost or price available for us to determine its value will generate an error. A price must be present to make the conversion. Simple currencies should also be marked as cash in order to be exported: 1999-01-01 commodity EUR name: \"European Union Euro currency\" export: \"CASH\" Finally, all converted holdings are agglomerated into a single cash position. There is no point in exporting these cash entries to separate OFX entries because the Google Finance code will agglomerate them to a single one anyhow.","title":"Exporting to a Cash Equivalent"},{"location":"exporting_your_portfolio.html#declaring-money-instruments","text":"There is a small hiccup in this cash conversion story: the Google Finance importer does not appear to correctly grok an OFX position in \u201ccash\u201d amounts in the importer; I think this is probably just a bug in Google Finance\u2019s import code (or perhaps I haven\u2019t found the correct OFX field values to make this work). Instead, in order to insert a cash position the exporter uses a cash-equivalent commodity which always prices at 1 unit of the currency, e.g. $1.00 for US dollars. For example, for US dollars I I use VMMXX which is a Vanguard Prime Money Market Fund, and for Canadian dollars I use IGI806 . A good type of commodity for this is some sort of Money Market fund. It doesn\u2019t matter so much which one you use, as long as it prices very close to 1. Find one. If you want to include cash commodities, you need to find such a commodity for each of the cash currencies you have on your books and tell Beancount about them. Typically that will be only one or two currencies. You declare them by append the special value \u201c MONEY \u201d for the \u201c export \u201d field, specifying which currency this commodity represents, like this: 1900-01-01 commodity VMMXX export: \"MUTF:VMMXX (MONEY:USD)\" 1900-01-01 commodity IGI806 export: \"MUTF_CA:IGI806 (MONEY:CAD)\"","title":"Declaring Money Instruments"},{"location":"exporting_your_portfolio.html#ignoring-commodities","text":"Finally, some commodities held in a ledger should be ignored. This is the case for the imaginary commodities used in mirror accounting, for example, to track unvested shares of an employment stock plan, or commodities used to track amounts contributed to a retirement account, like this: 1996-01-01 commodity RSPCAD name: \"Canada Registered Savings Plan Contributions\" You tell the export code to ignore a commodity specifying the special value \u201c IGNORE \u201d for the \u201c export \u201d field, like this: 1996-01-01 commodity RSPCAD name: \"Canada Registered Savings Plan Contributions\" export: \"IGNORE\" All holdings in units of RSPCAD will thus not be exported. The question of whether some commodities should be exported or not sometimes presents interesting choices. Here is an example: I track my accumulated vacation hours in an asset account. The units are \u201c VACHR \u201d. I associate with this commodity a price that is roughly equivalent to my net hourly salary. This gives me a rough idea how much vacation time money is accumulated on the books, e.g. if I quit my job, how much I\u2019d get paid. Do I want to them included in my total net worth? Should the value from those hours be reflected in the value of my exported portfolio? I think that largely depends on whether I plan to use up those vacations before I leave this job or not, whether I want to have this accumulated value show up on my balance sheet.","title":"Ignoring Commodities"},{"location":"exporting_your_portfolio.html#comparing-with-net-worth","text":"The end result is that the sum total of all your exported positions plus the cash position should approximate the value of all your assets, and the total value calculated by the Google Finance website should be very close to the one reported by this report: bean-report file.beancount networth As a point of comparison, the value of my own portfolio is usually close to within a few hundred US dollars.","title":"Comparing with Net Worth"},{"location":"exporting_your_portfolio.html#details-of-the-ofx-export","text":"","title":"Details of the OFX Export"},{"location":"exporting_your_portfolio.html#import-failures","text":"Exporting a portfolio with symbols that Google Finance does not recognize fatally trips up Google\u2019s import feature. Google Finance then proceeds to fail to recognize your entire file. I recommend that you use explicit exchange:symbol names on all commodities that get exported in order to avoid this problem, as is described further in this document. Google Finance can also be a little bit finicky about the format of the particular OFX file you give it to import. The export_portfolio command attempts to avoid OFX features that would break it but it\u2019s fragile, and it\u2019s possible that the particulars of your portfolio\u2019s contents triggers output that fails to import. If this is the case, at step (4) above, instead of a list of stock symbols you would see a long list of positions that look like XML tags (this is how failure manifests itself). If that is the case, send email to the mailing-list (best if you can isolate the positions that trigger breakage and have the capability to diff files and do some troubleshooting).","title":"Import Failures"},{"location":"exporting_your_portfolio.html#mutual-funds-vs-stocks","text":"The OFX format distinguishes between stocks and mutual funds. In practice, the Google Finance importer does not appear to distinguish between these two (at least it appears to behave the same way), so this is likely an irrelevant implementation detail. Nevertheless, the export code is able to honor the OFX convention of distinguishing between \u201cBUYMF\u201d vs. \u201cBUYSTOCK\u201d XML elements. To this effect, the export code attempts to classify which commodities represent mutual funds by inspecting whether the ticker associated with the commodity begins with the letters \u201cMUTF\u201d and is followed by a colon. For example, \u201c MUTF:RGAGX \u201d and \u201c MUTF_CA:RBF1005 \" will both be detected as mutual funds, for example.","title":"Mutual Funds vs. Stocks"},{"location":"exporting_your_portfolio.html#debugging-the-export","text":"In order to debug how each of your holdings gets exported, use the --debug flag, which will print a detailed account of how each holding is handled by the export script to stderr : bean-report file.beancount export_portfolio --debug 2>&1 >/dev/null | more The script should print the list of exported positions and their corresponding holdings, then the list of converted positions and their corresponding holdings (usually many cash positions are aggregated together) and finally, the list of ignored holdings. This should be enough to explain the entire contents of the exported portfolio.","title":"Debugging the Export"},{"location":"exporting_your_portfolio.html#purchase-dates","text":"Beancount does not currently have all the lot purchase dates, so the purchase dates are exported as if purchased the day before the export. Eventually, when the purchase date is available in Beancount (pending the inventory booking changes ) the actual lot purchase date will probably be used in the export format. However, it\u2019s not yet clear that using the correct date is the right thing to do, because Google Finance might insist on inserting cash for dividends since the reported purchase date\u2026 but Beancount already takes care of inserting a special lot for cash that should already include this. We shall see when we get there.","title":"Purchase Dates"},{"location":"exporting_your_portfolio.html#disable-dividends","text":"Under the \u201cEdit Portfolio\u201d option there is a checkbox that appears to disable the calculation of dividends offered. It would be nice to find a way to automatically disable this checkbox upon import.","title":"Disable Dividends"},{"location":"exporting_your_portfolio.html#automate-upload","text":"It would be nice to automate the replacement of the portfolio with a Python script. Unfortunately, the Google Finance API has been deprecated. Maybe someone can write a screen-scraping routine to do this.","title":"Automate Upload"},{"location":"exporting_your_portfolio.html#summary","text":"Each holding\u2019s export can be controlled by how its commodity is treated, in one of the following ways: Exported to a portfolio position. This is the default, but you should specify the ticker symbol using the \u201c ticker \u201d or \u201c export \u201d metadata fields, in \u201c ExchangeCode:Symbol \u201d format. Converted to cash and exported to a money market cash-equivalent position, by setting the value of the \u201c export \u201d metadata field to the special value \u201c CASH \u201d. Ignored by specifying the \u201c export \u201d metadata field to the special value \u201c IGNORE \u201d. Provided as Money Instrument , to be used for cash-equivalent value of each holding intended to be converted to cash and included in the portfolio. These are identified by a special value \u201c( MONEY:) \u201d in the \u201c export \u201d metadata field.","title":"Summary"},{"location":"external_contributions.html","text":"External Contributions to Beancount \uf0c1 Martin Blais - Updated: June 2024 http://furius.ca/beancount/doc/contrib Links to codes written by other people that build on top of or that are related to Beancount and/or Ledgerhub. Indexes \uf0c1 This document contains only packages that were discussed or have had an announcement sent to the mailing-list. You will be able to find other packages on public indices: PyPI: You can find a lot of other Beancount-related projects at PyPI . GitHub: A search for \"beancount\" as of September 2020 brings up 318 projects. Books and Articles \uf0c1 Managing Personal Finances using Python (Siddhant Goel): a 2020 book on plain-text accounting, and Beancount. The Five Minute Ledger Update (RedStreet) A series of articles showing how to automate downloading data from institutions (banks, credit cards, brokerages, etc.) so that ledger updates can be done in under five minutes. Mailing list thread . Tax Loss Harvesting with Beancount (RedStreet): An article about TLH from a US perspective, includes requirements, wash sale subtleties and safe to sell/buy dates, and comparisons to robo-advisors. (Related: fava_investor TLH module . for fava and plain-beancount command line version). Scaled Estimates of Mutual Fund NAVs (RedStreet) : Problem: Mutual fund NAVs (at least in the US) are updated exactly once per day, at the end of the day. When needing to make financial decisions when the trading window is still open (eg: when tax loss harvesting), and the end-of-day NAV is not yet available, it is sometimes useful to make a trivial estimate of that NAV, especially on days when there are huge changes in the market. A Shortcut to Scrape Trade History from Fidelity (David Avraamides) I wrote up a description of how I use a Shortcut to scrape trade history from Fidelity\u2019s website, run it through a Python script to convert to Beancount\u2019s ledger format, and then save it in the clipboard so I can paste it into a ledger file. Plugins \uf0c1 split_transactions : Johann Kl\u00e4hn wrote a plugin that can split a single transaction into many against a limbo account, as would be done for depreciation. zerosum : Red S wrote a plugin to match up transactions that when taken together should sum up to zero and move them to a separate account. effective_dates : Red S wrote a plugin to book different legs of a transaction to different dates beancount-plugins : Dave Stephens created a repository to share various of his plugins related to depreciation. beancount-plugins-zack : Stefano Zacchiroli created this repository to share his plugins. Contains sorting of directives and more. b eancount-oneliner : Akuukis created a plugin to write an entry in one line ( PyPi ). beancount-interpolate : Akuukis created plugins for Beancount to interpolate transactions (recur, split, depr, spread) ( PyPi ). metadata-spray : Add metadata across entries by regex expression rather than having explicit entries (by Vivek Gani). Akuukis/beancount_share : A beancount plugin to share expenses among multiple partners within one ledger. This plugin is very powerful and most probably can deal with all of your sharing needs. w1ndy/beancount_balexpr (Di Weng): A plugin that provides \"balance expressions\" to be run against the Beancount entries, as a Custom directive. See this thread . autobean.narration (Archimedes Smith): Allows to annotate each posting in a concise way by populating per-posting narration metadata from inline comments. autobean.sorted : Checks that transactions are in non-descending order in each file. Helps identifying misplaced or misdated directives, by warning on those directives not following a non-descending order by date in the file. hoostus/beancount-asset-transfer-plugin : A plugin to automatically generate in-kind transfers between two beancount accounts, while preserving the cost basis and acquisition date. PhracturedBlue/fava-portfolio-summary (Phractured Blue): Fava Plugin to show portfolio summaries with rate of return. rename_accounts : Plugin from Red S to rename accounts. E.g.: rename \u201cExpenses:Taxes\u201d to \u201cIncome:Taxes\u201d is helpful for expense analysis. More here . Long_short capital gains classifier : Plugin from Red S to classify capital gains into long and short based on duration the assets were held, and into gains and losses based on value. Autoclose_tree : Automatically closes all of an account's descendants when an account is closed. Tools \uf0c1 alfred-beancount (Yue Wu): An add-on to the \u201cAlfred\u201d macOS tool to quickly enter transactions in one\u2019s Beancount file. Supports full account names and payees match. bean-add (Simon Volpert): A Beancount transaction entry assistant. hoostus/fincen_114 (Justus Pendleton): An FBAR / FinCEN 114 report generator. ghislainbourgeois/beancount_portfolio_allocation ( Ghislain Bourgeois ): A quick way to figure out the asset allocations in different portfolios. hoostus/portfolio-returns (Justus Pendleton): portfolio returns calculator costflow/syntax (Leplay Li): A product that allows users to keep plain text accounting from their favorite messaging apps. A syntax for converting one-line message to beancount/*ledger format. process control chart (Justus Pendleton): Spending relative to portfolio size. Thread. Pinto (Sean Leavey): Supercharged command line interface for Beancount. Supports automatic insertions of transactions in ledger file. PhracturedBlue/fava-encrypt : A docker-base solution for keeping Fava online while keeping beancount data encrypted at rest. See this thread for context. kubauk/beancount-import-gmail : beancount-import-gmail uses the gmail API and OAuth to log into your mailbox and download order details which are then used to augment your transactions for easier classification. sulemankm/budget_report : A very simple command-line budget tracking tool for beancount ledger files. dyumnin/dyu_accounting : Accounting setup to automate generation of various financial statements for Compliance with Indian Govt. Gains Minimizer (RedStreet): Automatically determine lots to sell to minimize capital gains taxes. Live example. beanahead (Marcus Read): Adds the ability to include future transactions (automatically generates regular transactions, provides for ad hoc expected transactions, expected transactions are reconciled against imported transactions; all functionality accessible via cli). autobean-format (Archimedes Smith): Yet another formatter for beancount,, powered by earlier project autobean-refactor, a library for parsing and programmatically manipulating beancount files. based on a proper parser, allowing it to format every corner of your ledger, including arithmetic expressions. akirak/flymake-bean-check (Akira Komamura): flymake support for Emacs. bean-download (Red Street): A downloader that ships with beancount-reds-importers that you can configure to run arbitrary commands to download your account statements. It now has a new feature: the needs-update subcommand. gerdemb/beanpost (Ben Gerdemann): Beanpost consists of a PostgreSQL schema and import/export commands that let you transfer data between a beancount file and a PostgreSQL database. Much of Beancount's functionality is implemented using custom PostgreSQL functions, allowing for complex queries and data manipulation. This setup provides a flexible backend that can integrate with other tools like web apps or reporting systems LaunchPlatform/beanhub-cli (Fang-Pen Lin): Command line tools for BeanHub or Beancount users. zacchiro/beangrep : Beangrep is a grep-like filter for the Beancount plain text accounting system. Alternative Parsers \uf0c1 Bison \uf0c1 The Beancount v2 parser uses GNU flex + GNU bison (for maximum portability). The Beancount v3 parser uses RE/flex + GNU bison (for Unicode and C++). Using Antlr \uf0c1 jord1e/jbeancount (Jordie Biemold) / using Antlr: An alternative parser for Beancount input syntax in Java (using the Antlr4 parser generator). This provides access to parsed Beancount data - without the effect of plugins - from JVM languages. See this post for details. Using Tree-sitter \uf0c1 polarmutex/tree-sitter-beancount (Bryan Ryall): A tree-sitter parser for the Beancount syntax. https://github.com/dnicolodi/tree-sitter-beancount (Daniele Nicolodi): Another tree-sitter based parser for the Beancount syntax. In Rust \uf0c1 jcornaz/beancount-parser (Jonathan Cornaz): A beancount file parser library for Rust. Uses nom. beancount_parser_lima (Simon Guest): A zero-copy parser for Beancount in Rust. It is intended to be a complete implementation of the Beancount file format, except for those parts which are deprecated and other features as documented here (in a list which may not be comprehensive). Uses Logos , Chumsky , and Ariadne . Importers \uf0c1 reds importers : Simple importers and tools for several US based institutions, and various file types. Emphasizes ease of writing your own importers by providing well maintained common libraries for banks, credit cards, and investment houses, and for various file types, which minimizes the institution specific code you need to write. This is a reference implementation of the principles expressed in The Five Minute Ledger Update . Contributions welcome. By RedStreet plaid2text : An importer from Plaid which stores the transactions to a Mongo DB and is able to render it to Beancount syntax. By Micah Duke. jbms/beancount-import : A tool for semi-automatically importing transactions from external data sources, with support for merging and reconciling imported transactions with each other and with existing transactions in the beancount journal. The UI is web based. ( Announcement , link to previous version ). By Jeremy Maitin-Shepard. awesome-beancount : A collection of importers for Chinese banks + tips and tricks. By Zhuoyun Wei . beansoup : Filippo Tampieri is sharing some of his Beancount importers and auto-completer in this project. montaropdf/beancount-importers : An importer to extract overtime and vacation from a timesheet format for invoicing customers. siddhantgoel/beancount-dkb (Siddhant Goel): importer for DKB CSV files. prabusw/beancount-importer-zerodha : Importer for the Indian broker Zerodha. swapi/beancount-utils : Another importer for Zerodha. Dr-Nuke/drnuke-bean (Dr Nuke): An importer for IBKR, based on the flex query (API-like) and one for Swiss PostFinance. Beanborg (Luciano Fiandesio): Beanborg automatically imports financial transactions from external CSV files into the Beancount bookkeeping system. szabootibor/beancount-degiro ( PyPI ): Importer for the trading accounts of the Dutch broker Degiro. siddhantgoel/beancount-ing-diba ( PyPI ): ING account importer (NL). PaulsTek/csv2bean : Asimple application to preprocess csv files using google sheets in Go. ericaltendorf/magicbeans (Eric Altendorf): Beancount importers for crypto data. Detailed lot tracking and capital gains/losses reporting for crypto assets. \" I wrote it because I was not satisfied with the accuracy or transparency of existing commercial services for crypto tax reporting.\" OSadovy/uabean (Oleksii Sadovyi): A set of Beancount importers and scripts for popular Ukrainian banks and more. fdavies93/seneca (Frank Davies): Importer for Wise. Multi-currency transfers. LaunchPlatform/beanhub-import : New beancount importer with a UI. rlan/beancount-multitool (Rick Lan): Beancount Multitool is a command-line-interface (CLI) tool that converts financial data from financial institutions to Beancount files (supported: JA Bank \uff2a\uff21\u30cd\u30c3\u30c8\u30d0\u30f3\u30af, Rakuten Card \u697d\u5929\u30ab\u30fc\u30c9, Rakuten Bank \u697d\u5929\u9280\u884c, SBI Shinsei Bank \u65b0\u751f\u9280\u884c). Associated post: https://www.linkedin.com/feed/update/urn:li:activity:7198125470662500352/ Converters \uf0c1 plaid2text : Python Scripts to export Plaid transactions and transform them into Ledger or Beancount syntax formatted files. gnucash-to-beancount : A script from Henrique Bastos to convert a GNUcash SQLite database into an equivalent Beancount input file. debanjum/gnucash-to-beancount : A fork of the above. andrewStein/gnucash-to-beancount : A further fork from the above two, which fixes a lot of issues (see this thread ). hoostus/beancount-ynab : A converter from YNAB to Beancount. hoostus/beancount-ynab5 : Same convert for YNAB from the same author, but for the more recent version 5. ledger2beancount : A script to convert ledger files to beancount. It was developed by Stefano Zacchiroli and Martin Michlmayr. smart_importer : A smart importer for beancount and fava, with intelligent suggestions for account names. By Johannes Harms. beancount-export-patreon.js : JavaScript that will export your Patreon transactions so you can see details of exactly who you've been giving money to. By kanepyork@gmail. alensiljak/pta-converters (Alen \u0160iljak): GnuCash -> Beancount converter (2019). grostim/Beancount-myTools (Timothee Gros): Personal importer tools of the author for French banks. Downloaders \uf0c1 bean-download (RedStreet): bean-download is a tool to conveniently download your transactions from supporting institutions. You configure it with a list of your institutions and arbitrary commands to download them, typically via ofxget . It downloads all of them in parallel, names them appropriately and puts them in the directory of your choice, from which you can then import. The tool is installed as a part of beancount-reds-importers . See accompanying article . ofx-summarize (RedStreet): When building importers, it helps to be able to peek into a .ofx or a .qfx file that you are trying to import. The ofx-summarize command does just that. It ships with beancount-reds-importers , and should be available by simply invoking the command. Running the command on a file shows you a few transactions in the file. What is very useful is to be able to explore your .ofx file via the python debugger or interpreter. Price Sources \uf0c1 hoostus/beancount-price-sources : A Morningstar price fetcher which aggregates multiple exchanges, including non-US ones. andyjscott/beancount-financequote : Finance::Quote support for bean-price. aamerabbas/beancount-coinmarketcap : Price fetcher for coinmarketcap ( see post ). grostim/Beancount-myTools/.../iexcloud.py : Price fetcher for iexcloud by Timothee Gros. xuhcc/beancount-cryptoassets (Kirill Goncharov): Price sources for cryptocurrencies. xuhcc/beancount-ethereum-importer (Kirill Goncharov): Ethereum transaction importer for Beancount. Includes a script that downloads transactions from Etherscan and an importer for downloaded transactions. xuhcc/beancount-exchangerates (Kirill Goncharov): Price source for http://exchangeratesapi.io . tarioch/beancounttools (Patrick Ruckstuhl): Price sources and importers. https://gitlab.com/chrisberkhout/pricehist (Chris Berkhout): A command-line tool that can fetch daily historical prices from multiple sources and output them in several formats. Supports some sources for CoinDesk, European Central Bank, Alpha Vantage, CoinMarketCap. The user can request a specific price type such as high, low, open, close or adjusted close. It can also be used through bean-price. Development \uf0c1 Py3k type annotations : Yuchen Ying is implementing python3 type annotations for Beancount. bryall/tree-sitter-beancount (Bryan Ryall): A tree-sitter parser for the beancount syntax. jmgilman/beancount-stubs : Typing .pyi stubs for some of the Beancount source. Documentation \uf0c1 Beancount Documentation ( Kirill Goncharov ): Official conversion of the Beancount documentation from Google Docs source to Markdown and HTML. This includes most of the Google Docs documents and is maintained in a Beancount org repo here by Kirill Goncharov. Beancount Source Code Documentation ( Dominik Aumayr ): A Sphinx-generated source code documentation of the Beancount codebase. The code to produce this is located here . SQL queries for Beancount (Dominik Aumayr): Example SQL queries. Beancount \u2014\u2014 \u547d\u4ee4\u884c\u590d\u5f0f\u7c3f\u8bb0 (Zhuoyun Wei): A tutorial (blog post) in Chinese on how to use Beancount. Managing my personal finances with Beancount (Alex Johnstone) Counting beans\u2014and more\u2014with Beancount (LWN) Interfaces / Web \uf0c1 fava: A web interface for Beancount (Dominik Aumayr, Jakob Schnitzer): Beancount comes with its own simple web front-end (\u201cbean-web\u201d) intended merely as a thin shell to invoke and display HTML versions of its reports. \u201cFava\u201d is an alternative web application front-end with more & different features, intended initially as a playground and proof-of-concept to explore a newer, better design for presenting the contents of a Beancount file. Fava Classy Portfolio (Vivek Gani): Classy Portfolio is an Extension for Fava, a web interface for the Beancount plaintext accounting software. The extension displays a list of different portfolios (e.g. 'taxable' vs. 'retirement'), with breakdowns using 'asset-class' and 'asset-subclass' metadata labels on commodities. Fava Investor project (RedStreet): Fava_investor aims to be a comprehensive set of reports, analyses, and tools for investments, for Beancount and Fava. It is a collection of modules, with each module offering a Fava plugin, a Beancount library, and a Beancount based CLI (command line interface). Current modules include: Visual, tree structured asset allocation by class, asset allocation by account, tax loss harvester, cash drag analysis. Fava Miler (RedStreet): Airline miles and rewards points: expiration and value reporting. Fava Envelope (Brian Ryall): A beancount fava extension to add an envelope budgeting capability to fava and beancount. It is developed as a Fava plugin and CLI. scauligi/refried (Sunjay Cauligi): An envelope budgeting plugin for Fava, inspired by YNAB: all expense accounts become individual budgeting categories, budgeting is carried out using transactions to these accounts, and the plugin automaticallyapplies a tag to all rebudget transactions so they can easily be filtered out. Provides budget and account views like YNAB. BeanHub.io : A web front-end for Beancount content. \" Since I started using Beancount, I have dreamed of making it fully automatic. For a few years now, I've been building tools for that goal. Connecting to the bank and fetching data directly from there is one of the goals I want to achieve. I built this feature and have been testing it for a while for my accounting book. Now my Beancount books are 80% fully automatic. I can open my repository, and the transactions from the bank will automatically appear as a new commit like this without me lifting a finger. The whole import system is based on our open-source beanhub-import and beanhub-extract. The only proprietary part in the import flow is the Plaid integration. So, suppose you don't trust me and still want to import transactions automatically. As long as you connect to Plaid and write CSV files based on the transactions you fetched from Plaid, you should be able to have the same automatic transaction importing system without using the BeanHub service. \" Blog posts: https://beanhub.io/blog/2024/06/24/introduction-of-beanhub-connect/ https://beanhub.io/blog/2024/04/23/how-beanhub-works-part1-sandboxing/ https://beanhub.io/blog/2024/06/26/how-beanhub-works-part2-layer-based-git-repos/ jmgilman/bdantic : A package for extending beancount with pydantic . With this package you can convert your ledger to JSON, and more. autobean/refactor (Archimedes Smith): Tooling to programmatically edit one's ledger, including formatting, sorting, refactoring, rearranging accounts, optimizing via plugins, migration from v2, inserting transactions in a ledger on import, and more. seltzered/beancolage (Vivek Gani): An Eclipse Theia (vendor-agnostic vscode) app that tries to bundle existing beancount-based packages such as vscode-beancount and Fava. aaronstacy.com/personal-finances-dashboard : HTML + D3.js visualization dashboard for Beancount data. Mobile/Phone Data Entry \uf0c1 Beancount Mobile App (Kirill Goncharov): A mobile data entry app for Beancount. (Currently only Android is supported.) Repo: https://github.com/xuhcc/beancount-mobile ( Announcement ). http://costflow.io : Plain Text Accounting in WeChat. \" Send a message to our bot in Telegram, Facebook Messenger, Whatsapp, LINE, WeChat, etc. Costflow will transform your message into Beancount / Ledger / hledger format transaction magically. Append the transaction to the file in your Dropbox / Google Drive. With the help of their apps, the file will be synced to your computer. \"","title":"External Contributions"},{"location":"external_contributions.html#external-contributions-to-beancount","text":"Martin Blais - Updated: June 2024 http://furius.ca/beancount/doc/contrib Links to codes written by other people that build on top of or that are related to Beancount and/or Ledgerhub.","title":"External Contributions to Beancount"},{"location":"external_contributions.html#indexes","text":"This document contains only packages that were discussed or have had an announcement sent to the mailing-list. You will be able to find other packages on public indices: PyPI: You can find a lot of other Beancount-related projects at PyPI . GitHub: A search for \"beancount\" as of September 2020 brings up 318 projects.","title":"Indexes"},{"location":"external_contributions.html#books-and-articles","text":"Managing Personal Finances using Python (Siddhant Goel): a 2020 book on plain-text accounting, and Beancount. The Five Minute Ledger Update (RedStreet) A series of articles showing how to automate downloading data from institutions (banks, credit cards, brokerages, etc.) so that ledger updates can be done in under five minutes. Mailing list thread . Tax Loss Harvesting with Beancount (RedStreet): An article about TLH from a US perspective, includes requirements, wash sale subtleties and safe to sell/buy dates, and comparisons to robo-advisors. (Related: fava_investor TLH module . for fava and plain-beancount command line version). Scaled Estimates of Mutual Fund NAVs (RedStreet) : Problem: Mutual fund NAVs (at least in the US) are updated exactly once per day, at the end of the day. When needing to make financial decisions when the trading window is still open (eg: when tax loss harvesting), and the end-of-day NAV is not yet available, it is sometimes useful to make a trivial estimate of that NAV, especially on days when there are huge changes in the market. A Shortcut to Scrape Trade History from Fidelity (David Avraamides) I wrote up a description of how I use a Shortcut to scrape trade history from Fidelity\u2019s website, run it through a Python script to convert to Beancount\u2019s ledger format, and then save it in the clipboard so I can paste it into a ledger file.","title":"Books and Articles"},{"location":"external_contributions.html#plugins","text":"split_transactions : Johann Kl\u00e4hn wrote a plugin that can split a single transaction into many against a limbo account, as would be done for depreciation. zerosum : Red S wrote a plugin to match up transactions that when taken together should sum up to zero and move them to a separate account. effective_dates : Red S wrote a plugin to book different legs of a transaction to different dates beancount-plugins : Dave Stephens created a repository to share various of his plugins related to depreciation. beancount-plugins-zack : Stefano Zacchiroli created this repository to share his plugins. Contains sorting of directives and more. b eancount-oneliner : Akuukis created a plugin to write an entry in one line ( PyPi ). beancount-interpolate : Akuukis created plugins for Beancount to interpolate transactions (recur, split, depr, spread) ( PyPi ). metadata-spray : Add metadata across entries by regex expression rather than having explicit entries (by Vivek Gani). Akuukis/beancount_share : A beancount plugin to share expenses among multiple partners within one ledger. This plugin is very powerful and most probably can deal with all of your sharing needs. w1ndy/beancount_balexpr (Di Weng): A plugin that provides \"balance expressions\" to be run against the Beancount entries, as a Custom directive. See this thread . autobean.narration (Archimedes Smith): Allows to annotate each posting in a concise way by populating per-posting narration metadata from inline comments. autobean.sorted : Checks that transactions are in non-descending order in each file. Helps identifying misplaced or misdated directives, by warning on those directives not following a non-descending order by date in the file. hoostus/beancount-asset-transfer-plugin : A plugin to automatically generate in-kind transfers between two beancount accounts, while preserving the cost basis and acquisition date. PhracturedBlue/fava-portfolio-summary (Phractured Blue): Fava Plugin to show portfolio summaries with rate of return. rename_accounts : Plugin from Red S to rename accounts. E.g.: rename \u201cExpenses:Taxes\u201d to \u201cIncome:Taxes\u201d is helpful for expense analysis. More here . Long_short capital gains classifier : Plugin from Red S to classify capital gains into long and short based on duration the assets were held, and into gains and losses based on value. Autoclose_tree : Automatically closes all of an account's descendants when an account is closed.","title":"Plugins"},{"location":"external_contributions.html#tools","text":"alfred-beancount (Yue Wu): An add-on to the \u201cAlfred\u201d macOS tool to quickly enter transactions in one\u2019s Beancount file. Supports full account names and payees match. bean-add (Simon Volpert): A Beancount transaction entry assistant. hoostus/fincen_114 (Justus Pendleton): An FBAR / FinCEN 114 report generator. ghislainbourgeois/beancount_portfolio_allocation ( Ghislain Bourgeois ): A quick way to figure out the asset allocations in different portfolios. hoostus/portfolio-returns (Justus Pendleton): portfolio returns calculator costflow/syntax (Leplay Li): A product that allows users to keep plain text accounting from their favorite messaging apps. A syntax for converting one-line message to beancount/*ledger format. process control chart (Justus Pendleton): Spending relative to portfolio size. Thread. Pinto (Sean Leavey): Supercharged command line interface for Beancount. Supports automatic insertions of transactions in ledger file. PhracturedBlue/fava-encrypt : A docker-base solution for keeping Fava online while keeping beancount data encrypted at rest. See this thread for context. kubauk/beancount-import-gmail : beancount-import-gmail uses the gmail API and OAuth to log into your mailbox and download order details which are then used to augment your transactions for easier classification. sulemankm/budget_report : A very simple command-line budget tracking tool for beancount ledger files. dyumnin/dyu_accounting : Accounting setup to automate generation of various financial statements for Compliance with Indian Govt. Gains Minimizer (RedStreet): Automatically determine lots to sell to minimize capital gains taxes. Live example. beanahead (Marcus Read): Adds the ability to include future transactions (automatically generates regular transactions, provides for ad hoc expected transactions, expected transactions are reconciled against imported transactions; all functionality accessible via cli). autobean-format (Archimedes Smith): Yet another formatter for beancount,, powered by earlier project autobean-refactor, a library for parsing and programmatically manipulating beancount files. based on a proper parser, allowing it to format every corner of your ledger, including arithmetic expressions. akirak/flymake-bean-check (Akira Komamura): flymake support for Emacs. bean-download (Red Street): A downloader that ships with beancount-reds-importers that you can configure to run arbitrary commands to download your account statements. It now has a new feature: the needs-update subcommand. gerdemb/beanpost (Ben Gerdemann): Beanpost consists of a PostgreSQL schema and import/export commands that let you transfer data between a beancount file and a PostgreSQL database. Much of Beancount's functionality is implemented using custom PostgreSQL functions, allowing for complex queries and data manipulation. This setup provides a flexible backend that can integrate with other tools like web apps or reporting systems LaunchPlatform/beanhub-cli (Fang-Pen Lin): Command line tools for BeanHub or Beancount users. zacchiro/beangrep : Beangrep is a grep-like filter for the Beancount plain text accounting system.","title":"Tools"},{"location":"external_contributions.html#alternative-parsers","text":"","title":"Alternative Parsers"},{"location":"external_contributions.html#bison","text":"The Beancount v2 parser uses GNU flex + GNU bison (for maximum portability). The Beancount v3 parser uses RE/flex + GNU bison (for Unicode and C++).","title":"Bison"},{"location":"external_contributions.html#using-antlr","text":"jord1e/jbeancount (Jordie Biemold) / using Antlr: An alternative parser for Beancount input syntax in Java (using the Antlr4 parser generator). This provides access to parsed Beancount data - without the effect of plugins - from JVM languages. See this post for details.","title":"Using Antlr"},{"location":"external_contributions.html#using-tree-sitter","text":"polarmutex/tree-sitter-beancount (Bryan Ryall): A tree-sitter parser for the Beancount syntax. https://github.com/dnicolodi/tree-sitter-beancount (Daniele Nicolodi): Another tree-sitter based parser for the Beancount syntax.","title":"Using Tree-sitter"},{"location":"external_contributions.html#in-rust","text":"jcornaz/beancount-parser (Jonathan Cornaz): A beancount file parser library for Rust. Uses nom. beancount_parser_lima (Simon Guest): A zero-copy parser for Beancount in Rust. It is intended to be a complete implementation of the Beancount file format, except for those parts which are deprecated and other features as documented here (in a list which may not be comprehensive). Uses Logos , Chumsky , and Ariadne .","title":"In Rust"},{"location":"external_contributions.html#importers","text":"reds importers : Simple importers and tools for several US based institutions, and various file types. Emphasizes ease of writing your own importers by providing well maintained common libraries for banks, credit cards, and investment houses, and for various file types, which minimizes the institution specific code you need to write. This is a reference implementation of the principles expressed in The Five Minute Ledger Update . Contributions welcome. By RedStreet plaid2text : An importer from Plaid which stores the transactions to a Mongo DB and is able to render it to Beancount syntax. By Micah Duke. jbms/beancount-import : A tool for semi-automatically importing transactions from external data sources, with support for merging and reconciling imported transactions with each other and with existing transactions in the beancount journal. The UI is web based. ( Announcement , link to previous version ). By Jeremy Maitin-Shepard. awesome-beancount : A collection of importers for Chinese banks + tips and tricks. By Zhuoyun Wei . beansoup : Filippo Tampieri is sharing some of his Beancount importers and auto-completer in this project. montaropdf/beancount-importers : An importer to extract overtime and vacation from a timesheet format for invoicing customers. siddhantgoel/beancount-dkb (Siddhant Goel): importer for DKB CSV files. prabusw/beancount-importer-zerodha : Importer for the Indian broker Zerodha. swapi/beancount-utils : Another importer for Zerodha. Dr-Nuke/drnuke-bean (Dr Nuke): An importer for IBKR, based on the flex query (API-like) and one for Swiss PostFinance. Beanborg (Luciano Fiandesio): Beanborg automatically imports financial transactions from external CSV files into the Beancount bookkeeping system. szabootibor/beancount-degiro ( PyPI ): Importer for the trading accounts of the Dutch broker Degiro. siddhantgoel/beancount-ing-diba ( PyPI ): ING account importer (NL). PaulsTek/csv2bean : Asimple application to preprocess csv files using google sheets in Go. ericaltendorf/magicbeans (Eric Altendorf): Beancount importers for crypto data. Detailed lot tracking and capital gains/losses reporting for crypto assets. \" I wrote it because I was not satisfied with the accuracy or transparency of existing commercial services for crypto tax reporting.\" OSadovy/uabean (Oleksii Sadovyi): A set of Beancount importers and scripts for popular Ukrainian banks and more. fdavies93/seneca (Frank Davies): Importer for Wise. Multi-currency transfers. LaunchPlatform/beanhub-import : New beancount importer with a UI. rlan/beancount-multitool (Rick Lan): Beancount Multitool is a command-line-interface (CLI) tool that converts financial data from financial institutions to Beancount files (supported: JA Bank \uff2a\uff21\u30cd\u30c3\u30c8\u30d0\u30f3\u30af, Rakuten Card \u697d\u5929\u30ab\u30fc\u30c9, Rakuten Bank \u697d\u5929\u9280\u884c, SBI Shinsei Bank \u65b0\u751f\u9280\u884c). Associated post: https://www.linkedin.com/feed/update/urn:li:activity:7198125470662500352/","title":"Importers"},{"location":"external_contributions.html#converters","text":"plaid2text : Python Scripts to export Plaid transactions and transform them into Ledger or Beancount syntax formatted files. gnucash-to-beancount : A script from Henrique Bastos to convert a GNUcash SQLite database into an equivalent Beancount input file. debanjum/gnucash-to-beancount : A fork of the above. andrewStein/gnucash-to-beancount : A further fork from the above two, which fixes a lot of issues (see this thread ). hoostus/beancount-ynab : A converter from YNAB to Beancount. hoostus/beancount-ynab5 : Same convert for YNAB from the same author, but for the more recent version 5. ledger2beancount : A script to convert ledger files to beancount. It was developed by Stefano Zacchiroli and Martin Michlmayr. smart_importer : A smart importer for beancount and fava, with intelligent suggestions for account names. By Johannes Harms. beancount-export-patreon.js : JavaScript that will export your Patreon transactions so you can see details of exactly who you've been giving money to. By kanepyork@gmail. alensiljak/pta-converters (Alen \u0160iljak): GnuCash -> Beancount converter (2019). grostim/Beancount-myTools (Timothee Gros): Personal importer tools of the author for French banks.","title":"Converters"},{"location":"external_contributions.html#downloaders","text":"bean-download (RedStreet): bean-download is a tool to conveniently download your transactions from supporting institutions. You configure it with a list of your institutions and arbitrary commands to download them, typically via ofxget . It downloads all of them in parallel, names them appropriately and puts them in the directory of your choice, from which you can then import. The tool is installed as a part of beancount-reds-importers . See accompanying article . ofx-summarize (RedStreet): When building importers, it helps to be able to peek into a .ofx or a .qfx file that you are trying to import. The ofx-summarize command does just that. It ships with beancount-reds-importers , and should be available by simply invoking the command. Running the command on a file shows you a few transactions in the file. What is very useful is to be able to explore your .ofx file via the python debugger or interpreter.","title":"Downloaders"},{"location":"external_contributions.html#price-sources","text":"hoostus/beancount-price-sources : A Morningstar price fetcher which aggregates multiple exchanges, including non-US ones. andyjscott/beancount-financequote : Finance::Quote support for bean-price. aamerabbas/beancount-coinmarketcap : Price fetcher for coinmarketcap ( see post ). grostim/Beancount-myTools/.../iexcloud.py : Price fetcher for iexcloud by Timothee Gros. xuhcc/beancount-cryptoassets (Kirill Goncharov): Price sources for cryptocurrencies. xuhcc/beancount-ethereum-importer (Kirill Goncharov): Ethereum transaction importer for Beancount. Includes a script that downloads transactions from Etherscan and an importer for downloaded transactions. xuhcc/beancount-exchangerates (Kirill Goncharov): Price source for http://exchangeratesapi.io . tarioch/beancounttools (Patrick Ruckstuhl): Price sources and importers. https://gitlab.com/chrisberkhout/pricehist (Chris Berkhout): A command-line tool that can fetch daily historical prices from multiple sources and output them in several formats. Supports some sources for CoinDesk, European Central Bank, Alpha Vantage, CoinMarketCap. The user can request a specific price type such as high, low, open, close or adjusted close. It can also be used through bean-price.","title":"Price Sources"},{"location":"external_contributions.html#development","text":"Py3k type annotations : Yuchen Ying is implementing python3 type annotations for Beancount. bryall/tree-sitter-beancount (Bryan Ryall): A tree-sitter parser for the beancount syntax. jmgilman/beancount-stubs : Typing .pyi stubs for some of the Beancount source.","title":"Development"},{"location":"external_contributions.html#documentation","text":"Beancount Documentation ( Kirill Goncharov ): Official conversion of the Beancount documentation from Google Docs source to Markdown and HTML. This includes most of the Google Docs documents and is maintained in a Beancount org repo here by Kirill Goncharov. Beancount Source Code Documentation ( Dominik Aumayr ): A Sphinx-generated source code documentation of the Beancount codebase. The code to produce this is located here . SQL queries for Beancount (Dominik Aumayr): Example SQL queries. Beancount \u2014\u2014 \u547d\u4ee4\u884c\u590d\u5f0f\u7c3f\u8bb0 (Zhuoyun Wei): A tutorial (blog post) in Chinese on how to use Beancount. Managing my personal finances with Beancount (Alex Johnstone) Counting beans\u2014and more\u2014with Beancount (LWN)","title":"Documentation"},{"location":"external_contributions.html#interfaces-web","text":"fava: A web interface for Beancount (Dominik Aumayr, Jakob Schnitzer): Beancount comes with its own simple web front-end (\u201cbean-web\u201d) intended merely as a thin shell to invoke and display HTML versions of its reports. \u201cFava\u201d is an alternative web application front-end with more & different features, intended initially as a playground and proof-of-concept to explore a newer, better design for presenting the contents of a Beancount file. Fava Classy Portfolio (Vivek Gani): Classy Portfolio is an Extension for Fava, a web interface for the Beancount plaintext accounting software. The extension displays a list of different portfolios (e.g. 'taxable' vs. 'retirement'), with breakdowns using 'asset-class' and 'asset-subclass' metadata labels on commodities. Fava Investor project (RedStreet): Fava_investor aims to be a comprehensive set of reports, analyses, and tools for investments, for Beancount and Fava. It is a collection of modules, with each module offering a Fava plugin, a Beancount library, and a Beancount based CLI (command line interface). Current modules include: Visual, tree structured asset allocation by class, asset allocation by account, tax loss harvester, cash drag analysis. Fava Miler (RedStreet): Airline miles and rewards points: expiration and value reporting. Fava Envelope (Brian Ryall): A beancount fava extension to add an envelope budgeting capability to fava and beancount. It is developed as a Fava plugin and CLI. scauligi/refried (Sunjay Cauligi): An envelope budgeting plugin for Fava, inspired by YNAB: all expense accounts become individual budgeting categories, budgeting is carried out using transactions to these accounts, and the plugin automaticallyapplies a tag to all rebudget transactions so they can easily be filtered out. Provides budget and account views like YNAB. BeanHub.io : A web front-end for Beancount content. \" Since I started using Beancount, I have dreamed of making it fully automatic. For a few years now, I've been building tools for that goal. Connecting to the bank and fetching data directly from there is one of the goals I want to achieve. I built this feature and have been testing it for a while for my accounting book. Now my Beancount books are 80% fully automatic. I can open my repository, and the transactions from the bank will automatically appear as a new commit like this without me lifting a finger. The whole import system is based on our open-source beanhub-import and beanhub-extract. The only proprietary part in the import flow is the Plaid integration. So, suppose you don't trust me and still want to import transactions automatically. As long as you connect to Plaid and write CSV files based on the transactions you fetched from Plaid, you should be able to have the same automatic transaction importing system without using the BeanHub service. \" Blog posts: https://beanhub.io/blog/2024/06/24/introduction-of-beanhub-connect/ https://beanhub.io/blog/2024/04/23/how-beanhub-works-part1-sandboxing/ https://beanhub.io/blog/2024/06/26/how-beanhub-works-part2-layer-based-git-repos/ jmgilman/bdantic : A package for extending beancount with pydantic . With this package you can convert your ledger to JSON, and more. autobean/refactor (Archimedes Smith): Tooling to programmatically edit one's ledger, including formatting, sorting, refactoring, rearranging accounts, optimizing via plugins, migration from v2, inserting transactions in a ledger on import, and more. seltzered/beancolage (Vivek Gani): An Eclipse Theia (vendor-agnostic vscode) app that tries to bundle existing beancount-based packages such as vscode-beancount and Fava. aaronstacy.com/personal-finances-dashboard : HTML + D3.js visualization dashboard for Beancount data.","title":"Interfaces / Web"},{"location":"external_contributions.html#mobilephone-data-entry","text":"Beancount Mobile App (Kirill Goncharov): A mobile data entry app for Beancount. (Currently only Android is supported.) Repo: https://github.com/xuhcc/beancount-mobile ( Announcement ). http://costflow.io : Plain Text Accounting in WeChat. \" Send a message to our bot in Telegram, Facebook Messenger, Whatsapp, LINE, WeChat, etc. Costflow will transform your message into Beancount / Ledger / hledger format transaction magically. Append the transaction to the file in your Dropbox / Google Drive. With the help of their apps, the file will be synced to your computer. \"","title":"Mobile/Phone Data Entry"},{"location":"fetching_prices_in_beancount.html","text":"Prices in Beancount \uf0c1 Martin Blais , December 2015 http://furius.ca/beancount/doc/prices Introduction \uf0c1 Processing a Beancount file is, by definition, constrained to the contents of the file itself. In particular, the latest prices of commodities are never fetched automatically from the internet. This is by design, so that any run of a report is deterministic and can also be run offline. No surprises. However, we do need access to price information in order to compute market values of assets. To this end, Beancount provides a Price directive which can be used to fill its in-memory price database by inserting these price points inline in the input file: 2015-11-20 price ITOT 95.46 USD 2015-11-20 price LQD 115.63 USD 2015-11-21 price USD 1.33495 CAD \u2026 Of course, you could do this manually, looking up the prices online and writing the directives yourself. But for assets which are traded publicly you can automate it, by invoking some code that will download prices and write out the directives for you. Beancount comes with some tools to help you do this. This document describes these tools. The Problem \uf0c1 In the context of maintaining a Beancount file, we have a few particular needs to address. First, we cannot expect the user to always update the input file in a timely manner. This means that we have to be able to fetch not only the latest prices, but also historical prices, from the past. Beancount provides an interface to provide these prices and a few implementations (fetching from Yahoo! Finance and from Google Finance). Second, we don't want to store too many prices for holdings which aren't relevant to us at a particular point in time. The list of assets held in a file varies over time. Ideally we would want to include the list of prices for just those assets, while they are held. The Beancount price maintenance tool is able to figure out which commodities it needs to fetch prices for at a particular date. Third, we don't want to fetch the same price if it already appears in the input file. The tools detect this and skip those prices. There's also a caching mechanism that avoids redundant network calls. Finally, while we provide some basic implementations of price sources, we cannot provide such codes for all possible online sources and websites. The problem is analogous to that of importing and extracting data from various institutions. In order to address that, we provide a mechanism for extensibility , a way that you can implement your own price source fetcher in a Python module and point to it from your input file by specifying the module's name as the source for that commodity. The \u201cbean-price\u201d Tool \uf0c1 Beancount comes with a \u201cbean-price\u201d command-line tool that integrates the ideas above. By default, this script accepts a list of Beancount input filenames, and fetches prices required to compute latest market values for current positions held in accounts: bean-price /home/joe/finances/joe.beancount It is also possible to provide a list of specific price fetching jobs to run, e.g., bean-price -e USD:google/TSE:XUS CAD:mysources.morningstar/RBF1005 These jobs are run concurrently so it should be fairly fast. Source Strings \uf0c1 The general format of each of these \"job source strings\" is :/[^] For example: USD:beancount.prices.sources.google/NASDAQ:AAPL The \u201cquote-currency\u201d is the currency the Commodity is quoted in. For example, shares of Apple are quoted in US dollars. The \"module\" is the name of a Python module that contains a Source class which can be instantiated and which connect to a data source to extract price data. These modules are automatically imported and a Source class therein instantiated in order to pull the price from the particular online source they support. This allows you to write your own fetcher codes without having to modify this script. Your code can be placed anywhere on your Python PYTHONPATH and you should not have to modify Beancount itself for this to work. The \u201csymbol\u201d is a string that is fed to the price fetcher to lookup the currency. For example, Apple shares trade on the Nasdaq, and the corresponding symbol in the Google Finance source is \u201cNASDAQ:AAPL\u201d. Other price sources may have a different symbology, e.g., some may require the asset's CUSIP. Default implementations of price sources are provided; we provide fetchers for Yahoo! Finance or Google Finance, which cover a large universe of common public investment types (e.g. stock and some mutual funds). As a convenience, the module name is always first searched under the \" beancount.prices.sources \" package, where those implementations live. This is how, for example, in order to use the provided Yahoo! Finance data fetcher you don't have to write all of \" beancount.prices.sources.yahoo/AAPL \" but you can simply use \" yahoo/AAPL \". Fallback Sources \uf0c1 In practice, fetching prices online often fails. Data sources typically only support some limited number of assets and even then, the support may vary between assets. As an example, Google Finance supports historical prices for stocks, but does not return historical prices for currency instruments (these restrictions may be more related to contractual arrangements between them and the data providers upstream than with technical limitations). To this extent, a source string may provide multiple sources for the data, separated with commas. For example: USD:google/CURRENCY:GBPUSD,yahoo/GBPUSD Each source is tried in turn, and if one fails to return a valid price, the next source is tried as a fallback. The hope is that at least one of the specified sources will work out. Inverted Prices \uf0c1 Sometimes, prices are only available for the inverse of an instrument. This is often the case for currencies. For example, the price of Canadian dollars quoted in US dollars is provided by the USD/CAD market, which gives the price of a US dollar in Canadian dollars (the inverse). In order use this, you can prepend \" ^ \" to the instrument name to instruct the tool to compute the inverse of the fetched price: USD:google/^CURRENCY:USDCAD If a source price is to be inverted, like this, the precision could be different than what is fetched. For instance, if the price of USD/CAD is 1.32759, for the above directive to price the \u201cCAD\u201d instrument it would output this: 2015-10-28 price CAD 0.753244601119 USD By default, inverted rates will be rounded similarly to how other Price directives were rounding those numbers. As you may now, Beancount's in-memory price database works in both directions (the reciprocals of all rates are stored automatically). So if you prefer to have the output Price entries with swapped currencies instead of inverting the rate number itself, you can use the --swap-inverted option. In the previous example for the price of CAD, it would output this: 2015-10-28 price USD 1.32759 CAD Date \uf0c1 By default, the latest prices for the assets are pulled in. You can use an option to fetch prices for a desired date in the past instead: bean-price --date=2015-02-03 \u2026 If you are using an input file to specify the list of prices to be fetched, the tool will figure out the list of assets held on the books at that time and fetch historical prices for those assets only. Caching \uf0c1 Prices are automatically cached (if current and latest, prices are cached for only a short period of time, about half an hour). This is convenient when you're having to run the script multiple times in a row for troubleshooting. You can disable the cache with an option: bean-price --no-cache You can also instruct the script to clear the cache before fetching its prices: bean-price --clear-cache Prices from a Beancount Input File \uf0c1 Generally, one uses a Beancount input file to specify the list of currencies to fetch. In order to do that, you should have Commodity directives in your input file for each of the currencies you mean to fetch prices for, like this: 2007-07-20 commodity VEA price: \"USD:google/NYSEARCA:VEA\" The \"price\" metadata should contain a list of price source strings. For example, a stock product might look like this: 2007-07-20 commodity CAD price: \"USD:google/CURRENCY:USDCAD,yahoo/USDCAD\" While a currency may have multiple target currencies it needs to get converted to: 1990-01-01 commodity GBP price: \"USD:yahoo/GBPUSD CAD:yahoo/GBPCAD CHF:yahoo/GBPCHF\" Which Assets are Fetched \uf0c1 There are many ways to compute a list of commodities with needed prices from a Beancount input file: Commodity directives. The list of all Commodity directives with \u201cprice\u201d metadata present in the file. For each of those holdings, the directive is consulted and its \" price \" metadata field is used to specify where to fetch prices from. Commodities of assets held at cost. Prices for all the holdings that were seen held at cost at a particular date. Because these holdings are held at cost, we can assume there is a corresponding time-varying price for their commodity. Converted commodities. Prices for holdings which were price-converted from some other commodity in the past (i.e., converting some cash in a currency from another). By default, the list of tickers to be fetched includes only the intersection of these three lists. This is because the most common usage of this script is to fetch missing prices for a particular date, and only the needed ones. Inactive commodities. You can use the \u201c --inactive \u201d option to fetch the entire set of prices from (1), regardless of asset holdings determined in (2) and (3). Undeclared commodities. Commodities without a corresponding \u201cCommodity\u201d directive will be ignored by default. To include the full list of commodities seen in an input file, use the \u201c --undeclared \u201d option. Clobber. Existing price directives for the same data are excluded by default, since the price is already in the file. You can use \u201c --clobber \u201d to ignore existing price directives and avoid filtering out what is fetched. Finally, you can use \u201c--all\u201d to include inactive and undeclared commodities and allow clobbering existing ones. You probably don't want to use that other than for testing. If you'd like to do some troubleshooting and print out the list of seen commodities, use the \u201c --verbose \u201d option twice, i.e., \u201c -vv \u201d. You can also just print the list of prices to be fetched with the \u201c --dry-run \u201d option, which stops short of actually fetching the missing prices. Conclusion \uf0c1 Writing Your Own Script \uf0c1 If the workflow defined by this tool does not fit your needs and you would like to cook up your own script, you should not have to start from scratch; you should be able to reuse the existing price fetching code to do that. I'm hoping to provide a few examples of such scripts in the experiments directory. For example, given an existing file it would be convenient to fetch all prices of assets every Friday in order to fill up a history of missing prices. Another example would be to fetch all price directives required in order to correctly compute investment returns in the presence of contributions and distributions. Contributions \uf0c1 If this workflow suits your needs well and you'd like to contribute some price source fetcher to Beancount, please contact the mailing-list. I'm open to including very general fetchers that have been very carefully unit-tested and used for a while.","title":"Fetching Prices in Beancount"},{"location":"fetching_prices_in_beancount.html#prices-in-beancount","text":"Martin Blais , December 2015 http://furius.ca/beancount/doc/prices","title":"Prices in Beancount"},{"location":"fetching_prices_in_beancount.html#introduction","text":"Processing a Beancount file is, by definition, constrained to the contents of the file itself. In particular, the latest prices of commodities are never fetched automatically from the internet. This is by design, so that any run of a report is deterministic and can also be run offline. No surprises. However, we do need access to price information in order to compute market values of assets. To this end, Beancount provides a Price directive which can be used to fill its in-memory price database by inserting these price points inline in the input file: 2015-11-20 price ITOT 95.46 USD 2015-11-20 price LQD 115.63 USD 2015-11-21 price USD 1.33495 CAD \u2026 Of course, you could do this manually, looking up the prices online and writing the directives yourself. But for assets which are traded publicly you can automate it, by invoking some code that will download prices and write out the directives for you. Beancount comes with some tools to help you do this. This document describes these tools.","title":"Introduction"},{"location":"fetching_prices_in_beancount.html#the-problem","text":"In the context of maintaining a Beancount file, we have a few particular needs to address. First, we cannot expect the user to always update the input file in a timely manner. This means that we have to be able to fetch not only the latest prices, but also historical prices, from the past. Beancount provides an interface to provide these prices and a few implementations (fetching from Yahoo! Finance and from Google Finance). Second, we don't want to store too many prices for holdings which aren't relevant to us at a particular point in time. The list of assets held in a file varies over time. Ideally we would want to include the list of prices for just those assets, while they are held. The Beancount price maintenance tool is able to figure out which commodities it needs to fetch prices for at a particular date. Third, we don't want to fetch the same price if it already appears in the input file. The tools detect this and skip those prices. There's also a caching mechanism that avoids redundant network calls. Finally, while we provide some basic implementations of price sources, we cannot provide such codes for all possible online sources and websites. The problem is analogous to that of importing and extracting data from various institutions. In order to address that, we provide a mechanism for extensibility , a way that you can implement your own price source fetcher in a Python module and point to it from your input file by specifying the module's name as the source for that commodity.","title":"The Problem"},{"location":"fetching_prices_in_beancount.html#the-bean-price-tool","text":"Beancount comes with a \u201cbean-price\u201d command-line tool that integrates the ideas above. By default, this script accepts a list of Beancount input filenames, and fetches prices required to compute latest market values for current positions held in accounts: bean-price /home/joe/finances/joe.beancount It is also possible to provide a list of specific price fetching jobs to run, e.g., bean-price -e USD:google/TSE:XUS CAD:mysources.morningstar/RBF1005 These jobs are run concurrently so it should be fairly fast.","title":"The \u201cbean-price\u201d Tool"},{"location":"fetching_prices_in_beancount.html#source-strings","text":"The general format of each of these \"job source strings\" is :/[^] For example: USD:beancount.prices.sources.google/NASDAQ:AAPL The \u201cquote-currency\u201d is the currency the Commodity is quoted in. For example, shares of Apple are quoted in US dollars. The \"module\" is the name of a Python module that contains a Source class which can be instantiated and which connect to a data source to extract price data. These modules are automatically imported and a Source class therein instantiated in order to pull the price from the particular online source they support. This allows you to write your own fetcher codes without having to modify this script. Your code can be placed anywhere on your Python PYTHONPATH and you should not have to modify Beancount itself for this to work. The \u201csymbol\u201d is a string that is fed to the price fetcher to lookup the currency. For example, Apple shares trade on the Nasdaq, and the corresponding symbol in the Google Finance source is \u201cNASDAQ:AAPL\u201d. Other price sources may have a different symbology, e.g., some may require the asset's CUSIP. Default implementations of price sources are provided; we provide fetchers for Yahoo! Finance or Google Finance, which cover a large universe of common public investment types (e.g. stock and some mutual funds). As a convenience, the module name is always first searched under the \" beancount.prices.sources \" package, where those implementations live. This is how, for example, in order to use the provided Yahoo! Finance data fetcher you don't have to write all of \" beancount.prices.sources.yahoo/AAPL \" but you can simply use \" yahoo/AAPL \".","title":"Source Strings"},{"location":"fetching_prices_in_beancount.html#fallback-sources","text":"In practice, fetching prices online often fails. Data sources typically only support some limited number of assets and even then, the support may vary between assets. As an example, Google Finance supports historical prices for stocks, but does not return historical prices for currency instruments (these restrictions may be more related to contractual arrangements between them and the data providers upstream than with technical limitations). To this extent, a source string may provide multiple sources for the data, separated with commas. For example: USD:google/CURRENCY:GBPUSD,yahoo/GBPUSD Each source is tried in turn, and if one fails to return a valid price, the next source is tried as a fallback. The hope is that at least one of the specified sources will work out.","title":"Fallback Sources"},{"location":"fetching_prices_in_beancount.html#inverted-prices","text":"Sometimes, prices are only available for the inverse of an instrument. This is often the case for currencies. For example, the price of Canadian dollars quoted in US dollars is provided by the USD/CAD market, which gives the price of a US dollar in Canadian dollars (the inverse). In order use this, you can prepend \" ^ \" to the instrument name to instruct the tool to compute the inverse of the fetched price: USD:google/^CURRENCY:USDCAD If a source price is to be inverted, like this, the precision could be different than what is fetched. For instance, if the price of USD/CAD is 1.32759, for the above directive to price the \u201cCAD\u201d instrument it would output this: 2015-10-28 price CAD 0.753244601119 USD By default, inverted rates will be rounded similarly to how other Price directives were rounding those numbers. As you may now, Beancount's in-memory price database works in both directions (the reciprocals of all rates are stored automatically). So if you prefer to have the output Price entries with swapped currencies instead of inverting the rate number itself, you can use the --swap-inverted option. In the previous example for the price of CAD, it would output this: 2015-10-28 price USD 1.32759 CAD","title":"Inverted Prices"},{"location":"fetching_prices_in_beancount.html#date","text":"By default, the latest prices for the assets are pulled in. You can use an option to fetch prices for a desired date in the past instead: bean-price --date=2015-02-03 \u2026 If you are using an input file to specify the list of prices to be fetched, the tool will figure out the list of assets held on the books at that time and fetch historical prices for those assets only.","title":"Date"},{"location":"fetching_prices_in_beancount.html#caching","text":"Prices are automatically cached (if current and latest, prices are cached for only a short period of time, about half an hour). This is convenient when you're having to run the script multiple times in a row for troubleshooting. You can disable the cache with an option: bean-price --no-cache You can also instruct the script to clear the cache before fetching its prices: bean-price --clear-cache","title":"Caching"},{"location":"fetching_prices_in_beancount.html#prices-from-a-beancount-input-file","text":"Generally, one uses a Beancount input file to specify the list of currencies to fetch. In order to do that, you should have Commodity directives in your input file for each of the currencies you mean to fetch prices for, like this: 2007-07-20 commodity VEA price: \"USD:google/NYSEARCA:VEA\" The \"price\" metadata should contain a list of price source strings. For example, a stock product might look like this: 2007-07-20 commodity CAD price: \"USD:google/CURRENCY:USDCAD,yahoo/USDCAD\" While a currency may have multiple target currencies it needs to get converted to: 1990-01-01 commodity GBP price: \"USD:yahoo/GBPUSD CAD:yahoo/GBPCAD CHF:yahoo/GBPCHF\"","title":"Prices from a Beancount Input File"},{"location":"fetching_prices_in_beancount.html#which-assets-are-fetched","text":"There are many ways to compute a list of commodities with needed prices from a Beancount input file: Commodity directives. The list of all Commodity directives with \u201cprice\u201d metadata present in the file. For each of those holdings, the directive is consulted and its \" price \" metadata field is used to specify where to fetch prices from. Commodities of assets held at cost. Prices for all the holdings that were seen held at cost at a particular date. Because these holdings are held at cost, we can assume there is a corresponding time-varying price for their commodity. Converted commodities. Prices for holdings which were price-converted from some other commodity in the past (i.e., converting some cash in a currency from another). By default, the list of tickers to be fetched includes only the intersection of these three lists. This is because the most common usage of this script is to fetch missing prices for a particular date, and only the needed ones. Inactive commodities. You can use the \u201c --inactive \u201d option to fetch the entire set of prices from (1), regardless of asset holdings determined in (2) and (3). Undeclared commodities. Commodities without a corresponding \u201cCommodity\u201d directive will be ignored by default. To include the full list of commodities seen in an input file, use the \u201c --undeclared \u201d option. Clobber. Existing price directives for the same data are excluded by default, since the price is already in the file. You can use \u201c --clobber \u201d to ignore existing price directives and avoid filtering out what is fetched. Finally, you can use \u201c--all\u201d to include inactive and undeclared commodities and allow clobbering existing ones. You probably don't want to use that other than for testing. If you'd like to do some troubleshooting and print out the list of seen commodities, use the \u201c --verbose \u201d option twice, i.e., \u201c -vv \u201d. You can also just print the list of prices to be fetched with the \u201c --dry-run \u201d option, which stops short of actually fetching the missing prices.","title":"Which Assets are Fetched"},{"location":"fetching_prices_in_beancount.html#conclusion","text":"","title":"Conclusion"},{"location":"fetching_prices_in_beancount.html#writing-your-own-script","text":"If the workflow defined by this tool does not fit your needs and you would like to cook up your own script, you should not have to start from scratch; you should be able to reuse the existing price fetching code to do that. I'm hoping to provide a few examples of such scripts in the experiments directory. For example, given an existing file it would be convenient to fetch all prices of assets every Friday in order to fill up a history of missing prices. Another example would be to fetch all price directives required in order to correctly compute investment returns in the presence of contributions and distributions.","title":"Writing Your Own Script"},{"location":"fetching_prices_in_beancount.html#contributions","text":"If this workflow suits your needs well and you'd like to contribute some price source fetcher to Beancount, please contact the mailing-list. I'm open to including very general fetchers that have been very carefully unit-tested and used for a while.","title":"Contributions"},{"location":"fund_accounting_with_beancount.html","text":"Fund Accounting with Beancount \uf0c1 Martin Blais , Carl Hauser, August 2014 http://furius.ca/beancount/doc/proposal-funds A discussion about how to carry out fund accounting within Beancount, various approaches, solutions and possible extensions. Motivation \uf0c1 Multiple users are attempting to solve the problem of fund accounting using command-line accounting systems, partially because this type of accounting occurs in the context of non-profit organizations that have small budgets and would prefer to use free software, and partially because the flexibility and customization required appear to be a good fit for command-line bookkeeping systems. What is Fund Accounting? \uf0c1 For example, see this thread : \u201cAnother religious duty I compute is effectively tithing (we call it Huq\u00faqu'll\u00e1h, and it's computed differently, but that's another story). In order to compute the tithe owed, I accrue 19% of every deposit to a virtual account, and then subtract from that account 19% of every needful expenditure. The total remaining at the end of the year is what I owe in tithe. This tithing account is not a real account, as it exists in no financial institution; but it is real enough as a personal duty. By using virtual account, I can track this \"side-band\" Liability, and then pay it off from an assets account when the time comes. If I report with --real I will simply see how much I've paid to this Liability; and if I report without --real I see how much Huq\u00faqu'll\u00e1h is presently owed.\u201d \u2014 John Wiegley Here\u2019s another description, as a comment from another user: [...] basically the idea that you split your financial life into separate pots called \"funds\". Each fund has its own chart of accounts (to a certain extent) and each fund obeys Assets+Liabilities+Equities == 0. This is often needed in non-profits where money given for specific purposes has to be accounted separately. The one area of non-separation is that actual asset accounts (e.g. bank accounts) and actual liability accounts (credit cards) may hold or owe money on behalf of multiple funds, so you can't use entirely separate ledger files. At our church we use a program called PowerChurchPlus for this and it works really well. My wife is now treasurer for a community music organization and facing the same problem but on such a small scale that the cost of a commercial program isn't warranted. I've seen what was posted in the ledger-cli list about non-profit accounting using C++ ledger and it just looks like it requires way too much discipline to use the tags correctly and consistently. The fund idea is much easier to explain and use (and the balance account invariant naturally enforced). So I was looking at the Beancount code to see whether I could adapt it to support fund accounting. I think the answer is \"yes and relatively easily so\". Furthermore, I think this idea has uses even for individuals: a couple of scenarios present themselves. First, I live in a community property state. My wife and I are each likely to inherit property which will be ours individually, but we also have the community property. These can each be treated as a separate fund and we will be able to report on them separately but also together for understanding our overall financial situation. Similarly, it potentially makes sense to account for retirement money with a separate fund for each person. \u2014 Carl Hauser From the PowerChurchPlus 11.5 Manual (PowerChurch, Inc. 2013): \u201cIn accounting, the term fund has a very specific meaning. An Accounting Fund is a self-balancing Chart of Accounts. [...] In accounting we need to think of a fund as a division, or sub-group of your church. Each Accounting Fund has its own assets, liabilities, equity, income, transfer and expense accounts. So when would you use an additional fund? If you have an area of your church that needs to produce its own reporting, you would need to use an additional fund. For example, if your church operates a preschool or play school, you might want to set up an additional fund to keep their finances separate from the church's. Depending on your needs, you might want to setup a separate fund for the men's or women's group. You might even setup a new fund to keep track of all fixed assets separately from the daily operating accounts.\u201d This is an interesting and apparently common problem. We will describe use cases in the rest of this section. Joint Account Management \uf0c1 I have personally used this \u201cfund accounting\u201d idea to manage a joint account that I had with my ex-wife, where we would both hold individual accounts\u2014we were both working professionals\u2014 and chip in to the joint account as needed. This section describes how I did this 1 . The accounting for the joint account was held in a separate file. Two sub-accounts were created to hold each of our \u201cportions\u201d: 2010-01-01 open Assets:BofA:Joint 2010-01-01 open Assets:BofA:Joint:Martin 2010-01-01 open Assets:BofA:Joint:Judie Transfers to the joint account were directly booked into one of the two sub-account: 2012-09-07 * \"Online Xfer Transfer from CK 345\" Assets:BofA:Joint:Judie 1000.00 USD Income:Contributions:Judie When we would incur expenses, we would reduce the asset account with two legs, one for each subaccount. We often booked them 2:1 to account for difference in income, or I just booked many of the transactions to myself (the fact that it was precisely accounted for does not imply that we weren\u2019t being generous to each other in that way): 2013-04-27 * \"Urban Vets for Grumpy\" Expenses:Medical:Cat 100.00 USD Assets:BofA:Joint:Martin -50 USD Assets:BofA:Joint:Judie -50 USD 2013-05-30 * \"Takahachi\" \"Dinner\" Expenses:Food:Restaurant 65.80 USD Assets:BofA:Joint:Judie -25.00 USD Assets:BofA:Joint:Martin It was convenient to elide one of the two amounts, as we weren\u2019t being very precise about this. Handling Multiple Funds \uf0c1 (Contributed from Carl Hauser) Here\u2019s the model used in the PowerChurchPlus system that is mentioned above (replacing the account numbers it uses with Beancount-style names). \u201cFund\u201d names are prefixed to the account names. Operations:Assets:Bank:... Endowment:Assets:Bank:... Operations:Liabilities:CreditCard:... Endowment:Liabilities:CreditCard:... Operations:Income:Pledges:2014 Operations:Expenses:Salaries:... Operations:Expenses:BuildingImprovement:... Endowment:Income:Investments:... Endowment:Expenses:BuildingImprovement:... \u2026 It is required that any transaction be balanced in every fund that it uses. For example, our Endowment fund often helps pay for things that are beyond the reach of current donations income. 2014-07-25 * \"Bill\u2019s Audio\" \"Sound system upgrade\" Endowment:Assets:Bank1:Checking 800.00 USD Operations:Assets:Bank1:Checking 200.00 USD Endowment:Expenses:BuildingImprovement:Sound -800.00 USD Operations:Expenses:BuildingImprovement:Sound -200.00 USD This represents a single check to Bill\u2019s Audio paid from assets of both the Endowment and Operations funds that are kept in the single external assets account Assets:Bank1:Checking. Note 1: An empty fund name could be allowed and the following \u201c:\u201d omitted, and in fact could be the default for people who don\u2019t want to use these features. (i.e., nothing changes if you don\u2019t use these features.) The Fund with the empty string for its name is, of course, distinct from all other Funds. Note 2: balance and pad directives are not very useful with accounts that participate in more than one Fund. Their use would require knowing the allocation of the account between the different funds and account statements from external holders (banks, e.g.) will not have this information. It might be useful to allow something like 2014-07-31 balance *:Assets:Bank1:Checking 579.39 USD as a check that things were right, but automatically correcting it with pad entries seems impossible. A balance sheet report can be run on any Fund or any combination of Funds and it will balance. You can keep track of what is owned for each different purpose easily. Transfers between funds are booked as expenses and decreases in the assets of one fund and income and increases in assets of the other. The income and expense accounts used for transfers may be generic ( Operations:Income:Transfer ) or you can use accounts set up for a particular kind of income or expense ( Endowment:Expense:BuildingImprovement:Sound ) would be fine as one leg of a transfer transaction. The account name syntax here is just one way it might work and relies on Beancount\u2019s use of five specifically-named top-level accounts. Anything to the left of the first of those could be treated as a fund name, or a different separator could be used between the fund part and the account name part. Similarly, I\u2019ve only shown single-level fund names but they might be hierarchical as well. I\u2019m not sure of the value of that, but observe that if transactions balance at the leaf-level funds they will also balance for funds higher in the hierarchy and there might be some mileage there. For John W.\u2019s Huq\u00faqu'll\u00e1h example one might set up a Fund whose liabilities were \u201cmoral obligations\u201d rather than legal ones (that seems to be the objection to simply tracking the tithes in an ordinary liability account). As income comes in (say, direct deposited in a real bank checking account), book 19% of it to the \u201cmoral obligation\u201d fund\u2019s checking account with a matching liability. When needful expenses are made, take back 19% from the \u201cmoral obligation\u201d fund\u2019s checking account and reduce the liability. No virtual postings or transactions -- everything must balance. This would work well if for example we were to have a HisRetirement fund and a HerRetirement fund -- useful to have separate for estate planning purposes -- but more commonly we want to know about our combined retirement which could be defined to be a virtual fund OurRetirement equal to the sum of HisRetirement and HerRetirement . Note that this only matters when creating reports: there is no need to do anything except normal, double-entry booking with balanced transactions in each real fund. When I say the \u201csum\u201d of two funds I mean a combination of taking the union of the contained account names, after stripping off the fund names, then summing the balances of the common accounts and keeping the balances of the others. (Balance Sheet) For reporting, one wants the capability for balance by fund and balance summed over a set of funds. I also use a report that shows a subset of funds, one per column, with corresponding account names lined up horizontally and a column at the right that is the \u201csum\u201d. When all funds are included in this latter report you get a complete picture of what you own and owe and what is set aside for different purposes, or restricted in different ways. Here\u2019s a small sample of a balance sheet for a subset of the church Funds. The terminology is a little different: what Beancount calls Equity is here Net Assets. And because using large numbers of Funds in PowerChurchPlus is not so easy, the NetAssets are actually categorized for different purposes -- this is where I think the ideas we\u2019re exploring here can really shine: if Funds are really easy to create and combine for reporting then some of the mechanisms that PCP has for divvying up assets within a fund become unnecessary. (Income Statement) For the Income and Expense report, usually I only care about expenses in one Fund at a time, but adding two funds together makes perfect sense if that\u2019s what you need to do. For me, this approach to fund accounting is appealing because it relies on and preserves the fundamental principles of double-entry bookkeeping: when transactions sum to 0 the balance sheet equation is always true. Out of this we automatically get the ability to combine any set of funds (we don\u2019t have to do anything special when entering transactions or organizing the deep structure of the accounts) and have it make at least arithmetical sense, and we don\u2019t rely on any \u201cmagic\u201d associated with renaming or tagging. I don\u2019t see how this can be so easily or neatly achieved by pushing the idea of the \u201cfunds\u201d down into the account hierarchy: funds belong above the five root accounts (Assets, Liabilities, Equity, Income and Expenses), not below them. Ideas for Implementation \uf0c1 Some random ideas for now. This needs a bit more work. If multiple redundant postings are required, the generation of these can be automated using a plugin . For instance, if a technique similar to mirror accounting is used in order to \u201csend the same dollars to multiple accounts\u201d, at least the user should not have to do this manually, which would be both tedious and prone to errors. A procedure to rename accounts upon parsing could be used, in order to merge multiple files into one. (Allowing the user to install such a mapping is an idea I\u2019ve had for a while but never implemented, though it could be implemented by a plugin filter.) We can rely on the fact that the transactions of subaccounts may be joined and summed in a parent account (despite the fact that reporting is lagging behind in that matter at the moment. It will be implemented eventually). Building off the earlier remark about doing something similar to the tag stack for Funds. What if the current tag architecture were extended to allow tags to have a value, #fund=Operations, or #fund=Endowment . Call them value-tags. You would also need to allow postings to have tags. Semantics of value-tags would be that they could only occur once for any posting, that a tag explicitly on a posting overrides the value-tag inherited from the transaction, and that an explicit tag on a transaction overrides a value from the tag-stack, and that only the last value-tag (with a particular key) in the tag-stack is applied to a transaction. This makes Funds a little less first-class than the earlier proposal to stick them in front of account names, but gets around the minor parsing difficulty previously mentioned. It suggests that opening of accounts within funds is not necessary where the previous syntax suggests that it is. The strict balancing rule for each fund in a transaction can still be implemented as a plugin. And reporting for a fund (or sum of funds) looks like: select transactions with any posting matching the desired fund (or funds) collect (and sum if necessary in the obvious way) postings associated with the fund (or funds) being reported on (A) collect (and sum in the obvious way) postings from the selected transactions not associated with the desired fund (B) Format a report with (A) as the information for the desired fund or funds and (B) as OTHER. OTHER is needed to make sure that the report balances, but could be omitted by choice. From an implementation perspective this seems more orthogonal to the current status quo, requiring even less change to existing code. It adds a new feature -- value tags and that can then be used by plugins and new reports to do what we want for fund accounting. Examples \uf0c1 (Carl) Here is an example of how I might try to handle things associated with my paycheck, which involves deferred compensation (403(b) -- extremely similar to a 401(k)) and a Flexible Spending Account (somewhat similar to an HSA which has been discussed previously on the ledger-cli group). Without Funds \uf0c1 (Carl) First, without Funds (this passes a bean-check): This is how I might set up my pay stub with health insurance, an FSA and 403b contributions in the absence of Funds. One problem is that it distributes Gross Income directly into the 403b and FSA accounts even though it recognizes that the health insurance contribution is a reduction in salary which the 403b and FSA ought to be as well. So tracking contributions to both of those is made more difficult as well as tracking taxable income. By thinking hard we could fix this -- we would come up with Income and Expense type accounts to represent the contributions, but they would end up looking kind of silly (in my opinion) because they are entirely internal to the accounts system. See the next example for how it would look using Funds. If you stick out your tongue, rub your tummy and stand on your head you will see that the Funds-based solution is equivalent to what we would have come up with in the paragraph above in terms of the complexity of its transactions -- just as many lines are required. The advantage is primarily a mental one -- it is much easier to see what to do to be both consistent and useful. option \u201ctitle\u201d \u201cPaystub - no funds\u201d 2014-07-15 open Assets:Bank:Checking 2014-07-15 open Assets:CreditUnion:Saving 2014-07-15 open Assets:FedIncTaxDeposits 2014-07-15 open Assets:Deferred:R-403b 2014-07-15 open Expenses:OASI 2014-07-15 open Expenses:Medicare 2014-07-15 open Expenses:MedicalAid 2014-07-15 open Expenses:SalReduction:HealthInsurance 2014-07-15 open Income:Gross:Emp1 2014-07-15 open Income:EmplContrib:Emp1:Retirement 2014-01-01 open Assets:FSA ; This way of setting up an FSA looks pretty good. It recognizes the ; rule that the designated amount for the year is immediately available ; (in the Asset account), and that we are obliged to make contributions ; to fund it over the course of the year (the Liability account). 2014-01-01 open Liabilities:FSA 2014-01-01 ! \"Set up FSA for 2014\" Assets:FSA 2000 USD Liabilities:FSA -2000 USD 2014-07-15 ! \"Emp1 Paystub\" Income:Gross:Emp1 -6000 USD Assets:Bank:Checking 3000 USD Assets:CreditUnion:Saving 1000 USD Assets:FedIncTaxDeposits 750 USD Expenses:OASI 375 USD Expenses:Medicare 100 USD Expenses:MedicalAid 10 USD Assets:Deferred:R-403b 600 USD Liabilities:FSA 75 USD Expenses:SalReduction:HealthInsurance 90 USD Income:EmplContrib:Emp1:Retirement -600 USD Assets:Deferred:R-403b 2014-01-01 open Expenses:Medical 2014-07-20 ! \"Direct expense from FSA\" Expenses:Medical 25 USD Assets:FSA 2014-07-20 ! \"Medical expense from checking\" Expenses:Medical 25 USD Assets:Bank:Checking 2014-07-20 ! \"Medical expense reimbursed from FSA\" Assets:Bank:Checking 25 USD Assets:FSA Using Funds \uf0c1 (Carl) And now using Funds (uses proposed features and hence can\u2019t be checked by bean-check): This is how I might set up my pay stub with health insurance, an FSA and 403b contributions using Funds. I can straightforwardly arrange things so that contributions to the FSA and 403b are recognized as salary reductions for income tax purposes. And I can easily see how much I have contributed to the 403b and how much my employer has contributed. See the previous example for how it would look without using Funds and which is not as accurate. What this does NOT do is track taxes ultimately owed on the 403b money. I think that is a matter of one's decision about whether to do cash-basis or accrual-basis accounting. If cash basis those taxes are not a current liability and cannot be reported as such. If accrual basis, they are a current liability and need to be recorded as such when the income is booked. For cash-basis books, we'd want the ability to report the state of affairs as if taxes were owed, but that is a matter for reporting rather than booking. We need to make sure we have enough identifiable information to automate creating those reports. I believe that taking a Fund-based accounting perspective easily does this. A problem not solved: what if your basis is different for Federal and State purposes, or even for Federal and multiple different states. Yikes! I've used the convention that the Fund name precedes the root account name. Note that with appropriate standing on one's head along with pivoting rules you can put the Fund name anywhere. Putting it first emphasizes that it identifies a set of accounts that must balance, and it makes it easy for the txn processor to guarantee this property. Accounts without a fund name in front belong to the Fund whose name is the empty string. option \"title\" \"Paystub - no Funds\" 2014-07-15 open Assets:Bank:Checking 2014-07-15 open Assets:CreditUnion:Saving 2014-07-15 open Assets:FedIncTaxDeposits 2014-07-15 open Expenses:OASI 2014-07-15 open Expenses:Medicare 2014-07-15 open Expenses:MedicalAid 2014-07-15 open Expenses:SalReduction:HealthInsurance 2014-07-15 open Expenses:SalReduction:FSA 2014-07-15 open Expenses:SalReduction:R-403b 2014-07-15 open Income:Gross:Emp1 2014-07-15 open Income:EmplContrib:Emp1:Retirement 2014-01-01 open FSA:Assets ; FSA fund accounts 2014-01-01 open FSA:Income:Contributions 2014-01-01 open FSA:Expenses:Medical 2014-01-01 open FSA:Expenses:ReimburseMedical 2014-01-01 open FSA:Liabilities 2014-07-15 open Retirement403b:Assets:CREF ; Retirement fund accounts 2014-07-15 open Retirement403b:Income:EmployeeContrib 2014-07-15 open Retirement403b:Income:EmployerContrib 2014-07-15 open Retirement403b:Income:EarningsGainsAndLosses ; This implements the same idea as above for the FSA, of balancing ; Assets and Liabilities at the opening, but now does it using a ; separate Fund. 2014-01-01 ! \"Set up FSA for 2014\" FSA:Assets 2000 USD FSA:Liabilities -2000 USD 2014-07-15 ! \"Emp1 Paystub\" Income:Gross:Emp1 -6000 USD Assets:Bank:Checking 3000 USD Assets:CreditUnion:Saving 1000 USD Assets:FedIncTaxDeposits 750 USD Expenses:OASI 375 USD Expenses:Medicare 100 USD Expenses:MedicalAid 10 USD Expenses:SalReduction:R-403b 600 USD Retirement403b:Income:EmployeeContrib -600 USD Retirement403b:Assets:CREF 600 USD Expenses:SalReduction:FSA 75 USD FSA:Income:Contributions -75 USD FSA:Liabilities Expenses:SalReduction:HealthInsurance Retirement403b:Income:EmployerContrib -600 USD Retirement403b:Assets:CREF 2014-01-01 open Expenses:Medical 2014-07-20 ! \"Direct expense from FSA\" FSA:Expenses:Medical 25 USD FSA:Assets 2014-07-20 ! \"Medical expense from checking\" Expenses:Medical 25 USD Assets:Bank:Checking 2014-07-20 ! \"Medical expense reimbursed from FSA\" Assets:Bank:Checking 25 USD Income:ReimburseMedical FSA:Assets -25 USD FSA:Expenses:ReimburseMedical Transfer Accounts Proposal \uf0c1 By Carl Hauser One problem that I\u2019ve experienced using the Fund approach is that it\u2019s a bit too easy to make mistakes when transferring money between funds, such as in the very last transaction above. Formalizing the idea of Transfer accounts can help with this. The most common mistake is to end up with something that moves assets in both accounts in the same direction -- both up or both down as in this mistaken version of the transaction in question: 2014-07-20 ! \"Medical expense reimbursed from FSA - with mistake\" Assets:Bank:Checking 25 USD Income:ReimburseMedical FSA:Assets 25 USD FSA:Expenses:ReimburseMedical This balances but isn\u2019t what we intended. Suppose we add the idea of Transfer accounts. They live at the same place in the hierarchy as Income and Expenses and like those are non-balance-sheet accounts. But they really come into play only for transactions that involve multiple funds. There is an additional rule for transactions containing Transfer accounts: the sum of the transfers must also be zero (additional in the sense that the rule about transactions balancing within each fund is still there). So to use this we set things up a little differently: 2014-07-15 open FSA:Transfer:Incoming:Contribution 2014-07-15 open FSA:Transfer:Outgoing:ReimburseMedical 2014-07-15 open Transfer:Outgoing:FSAContribution 2014-07-15 open Transfer:Incoming:ReimburseMedical The incorrect transaction is now flagged because sum of the transfers is -50 USD , not zero. 2014-07-20 ! \"Medical expense reimbursed from FSA - with mistake\" Assets:Bank:Checking 25 USD Transfer:Incoming:ReimburseMedical FSA:Assets 25 USD FSA:Transfer:Outgoing:ReimburseMedical The paycheck transaction using transfer accounts for the FSA and the retirement account amounts might look like this (after appropriate open s of course): 2014-07-15 ! \"Emp1 Paystub - using transfer accounts\" Income:Gross:Emp1 -6000 USD Assets:Bank:Checking 3000 USD Assets:CreditUnion:Saving 1000 USD Assets:FedIncTaxDeposits 750 USD Expenses:OASI 375 USD Expenses:Medicare 100 USD Expenses:MedicalAid 10 USD Transfer:Outgoing:Retirement403b:SalReduction 600 USD Retirement403b:Transfer:Incoming:EmployeeContrib Retirement403b:Assets:CREF 600 USD Transfer:Outgoing:FSA:SalReduction 75 USD FSA:Transfer:Incoming:Contributions FSA:Liabilities Expenses:SalReduction:HealthInsurance Retirement403b:Income:EmployerContrib -600 USD Retirement403b:Assets:CREF Some might think that this is too complicated. Without changing the Transfer accounts idea or rule, you can simplify booking to just a single account per fund, Fund:Transfer , losing some ability for precision in reporting but without losing the ability to check correctness of transfer transactions. Account Aliases \uf0c1 Simon Michael mentions that this is related to HLedger account aliases : \u201cI think this is related to the situation where you want to view entities' finances both separately and merged. Account aliases can be another way to approximate this, as in http://hledger.org/how-to-use-account-aliases .\u201d If you find yourself culturally challenged by our modern lifestyle, perhaps you can imagine the case of roommates, although I don\u2019t like the reductionist view this association brings to my mind. \u21a9","title":"Fund Accounting with Beancount"},{"location":"fund_accounting_with_beancount.html#fund-accounting-with-beancount","text":"Martin Blais , Carl Hauser, August 2014 http://furius.ca/beancount/doc/proposal-funds A discussion about how to carry out fund accounting within Beancount, various approaches, solutions and possible extensions.","title":"Fund Accounting with Beancount"},{"location":"fund_accounting_with_beancount.html#motivation","text":"Multiple users are attempting to solve the problem of fund accounting using command-line accounting systems, partially because this type of accounting occurs in the context of non-profit organizations that have small budgets and would prefer to use free software, and partially because the flexibility and customization required appear to be a good fit for command-line bookkeeping systems.","title":"Motivation"},{"location":"fund_accounting_with_beancount.html#what-is-fund-accounting","text":"For example, see this thread : \u201cAnother religious duty I compute is effectively tithing (we call it Huq\u00faqu'll\u00e1h, and it's computed differently, but that's another story). In order to compute the tithe owed, I accrue 19% of every deposit to a virtual account, and then subtract from that account 19% of every needful expenditure. The total remaining at the end of the year is what I owe in tithe. This tithing account is not a real account, as it exists in no financial institution; but it is real enough as a personal duty. By using virtual account, I can track this \"side-band\" Liability, and then pay it off from an assets account when the time comes. If I report with --real I will simply see how much I've paid to this Liability; and if I report without --real I see how much Huq\u00faqu'll\u00e1h is presently owed.\u201d \u2014 John Wiegley Here\u2019s another description, as a comment from another user: [...] basically the idea that you split your financial life into separate pots called \"funds\". Each fund has its own chart of accounts (to a certain extent) and each fund obeys Assets+Liabilities+Equities == 0. This is often needed in non-profits where money given for specific purposes has to be accounted separately. The one area of non-separation is that actual asset accounts (e.g. bank accounts) and actual liability accounts (credit cards) may hold or owe money on behalf of multiple funds, so you can't use entirely separate ledger files. At our church we use a program called PowerChurchPlus for this and it works really well. My wife is now treasurer for a community music organization and facing the same problem but on such a small scale that the cost of a commercial program isn't warranted. I've seen what was posted in the ledger-cli list about non-profit accounting using C++ ledger and it just looks like it requires way too much discipline to use the tags correctly and consistently. The fund idea is much easier to explain and use (and the balance account invariant naturally enforced). So I was looking at the Beancount code to see whether I could adapt it to support fund accounting. I think the answer is \"yes and relatively easily so\". Furthermore, I think this idea has uses even for individuals: a couple of scenarios present themselves. First, I live in a community property state. My wife and I are each likely to inherit property which will be ours individually, but we also have the community property. These can each be treated as a separate fund and we will be able to report on them separately but also together for understanding our overall financial situation. Similarly, it potentially makes sense to account for retirement money with a separate fund for each person. \u2014 Carl Hauser From the PowerChurchPlus 11.5 Manual (PowerChurch, Inc. 2013): \u201cIn accounting, the term fund has a very specific meaning. An Accounting Fund is a self-balancing Chart of Accounts. [...] In accounting we need to think of a fund as a division, or sub-group of your church. Each Accounting Fund has its own assets, liabilities, equity, income, transfer and expense accounts. So when would you use an additional fund? If you have an area of your church that needs to produce its own reporting, you would need to use an additional fund. For example, if your church operates a preschool or play school, you might want to set up an additional fund to keep their finances separate from the church's. Depending on your needs, you might want to setup a separate fund for the men's or women's group. You might even setup a new fund to keep track of all fixed assets separately from the daily operating accounts.\u201d This is an interesting and apparently common problem. We will describe use cases in the rest of this section.","title":"What is Fund Accounting?"},{"location":"fund_accounting_with_beancount.html#joint-account-management","text":"I have personally used this \u201cfund accounting\u201d idea to manage a joint account that I had with my ex-wife, where we would both hold individual accounts\u2014we were both working professionals\u2014 and chip in to the joint account as needed. This section describes how I did this 1 . The accounting for the joint account was held in a separate file. Two sub-accounts were created to hold each of our \u201cportions\u201d: 2010-01-01 open Assets:BofA:Joint 2010-01-01 open Assets:BofA:Joint:Martin 2010-01-01 open Assets:BofA:Joint:Judie Transfers to the joint account were directly booked into one of the two sub-account: 2012-09-07 * \"Online Xfer Transfer from CK 345\" Assets:BofA:Joint:Judie 1000.00 USD Income:Contributions:Judie When we would incur expenses, we would reduce the asset account with two legs, one for each subaccount. We often booked them 2:1 to account for difference in income, or I just booked many of the transactions to myself (the fact that it was precisely accounted for does not imply that we weren\u2019t being generous to each other in that way): 2013-04-27 * \"Urban Vets for Grumpy\" Expenses:Medical:Cat 100.00 USD Assets:BofA:Joint:Martin -50 USD Assets:BofA:Joint:Judie -50 USD 2013-05-30 * \"Takahachi\" \"Dinner\" Expenses:Food:Restaurant 65.80 USD Assets:BofA:Joint:Judie -25.00 USD Assets:BofA:Joint:Martin It was convenient to elide one of the two amounts, as we weren\u2019t being very precise about this.","title":"Joint Account Management"},{"location":"fund_accounting_with_beancount.html#handling-multiple-funds","text":"(Contributed from Carl Hauser) Here\u2019s the model used in the PowerChurchPlus system that is mentioned above (replacing the account numbers it uses with Beancount-style names). \u201cFund\u201d names are prefixed to the account names. Operations:Assets:Bank:... Endowment:Assets:Bank:... Operations:Liabilities:CreditCard:... Endowment:Liabilities:CreditCard:... Operations:Income:Pledges:2014 Operations:Expenses:Salaries:... Operations:Expenses:BuildingImprovement:... Endowment:Income:Investments:... Endowment:Expenses:BuildingImprovement:... \u2026 It is required that any transaction be balanced in every fund that it uses. For example, our Endowment fund often helps pay for things that are beyond the reach of current donations income. 2014-07-25 * \"Bill\u2019s Audio\" \"Sound system upgrade\" Endowment:Assets:Bank1:Checking 800.00 USD Operations:Assets:Bank1:Checking 200.00 USD Endowment:Expenses:BuildingImprovement:Sound -800.00 USD Operations:Expenses:BuildingImprovement:Sound -200.00 USD This represents a single check to Bill\u2019s Audio paid from assets of both the Endowment and Operations funds that are kept in the single external assets account Assets:Bank1:Checking. Note 1: An empty fund name could be allowed and the following \u201c:\u201d omitted, and in fact could be the default for people who don\u2019t want to use these features. (i.e., nothing changes if you don\u2019t use these features.) The Fund with the empty string for its name is, of course, distinct from all other Funds. Note 2: balance and pad directives are not very useful with accounts that participate in more than one Fund. Their use would require knowing the allocation of the account between the different funds and account statements from external holders (banks, e.g.) will not have this information. It might be useful to allow something like 2014-07-31 balance *:Assets:Bank1:Checking 579.39 USD as a check that things were right, but automatically correcting it with pad entries seems impossible. A balance sheet report can be run on any Fund or any combination of Funds and it will balance. You can keep track of what is owned for each different purpose easily. Transfers between funds are booked as expenses and decreases in the assets of one fund and income and increases in assets of the other. The income and expense accounts used for transfers may be generic ( Operations:Income:Transfer ) or you can use accounts set up for a particular kind of income or expense ( Endowment:Expense:BuildingImprovement:Sound ) would be fine as one leg of a transfer transaction. The account name syntax here is just one way it might work and relies on Beancount\u2019s use of five specifically-named top-level accounts. Anything to the left of the first of those could be treated as a fund name, or a different separator could be used between the fund part and the account name part. Similarly, I\u2019ve only shown single-level fund names but they might be hierarchical as well. I\u2019m not sure of the value of that, but observe that if transactions balance at the leaf-level funds they will also balance for funds higher in the hierarchy and there might be some mileage there. For John W.\u2019s Huq\u00faqu'll\u00e1h example one might set up a Fund whose liabilities were \u201cmoral obligations\u201d rather than legal ones (that seems to be the objection to simply tracking the tithes in an ordinary liability account). As income comes in (say, direct deposited in a real bank checking account), book 19% of it to the \u201cmoral obligation\u201d fund\u2019s checking account with a matching liability. When needful expenses are made, take back 19% from the \u201cmoral obligation\u201d fund\u2019s checking account and reduce the liability. No virtual postings or transactions -- everything must balance. This would work well if for example we were to have a HisRetirement fund and a HerRetirement fund -- useful to have separate for estate planning purposes -- but more commonly we want to know about our combined retirement which could be defined to be a virtual fund OurRetirement equal to the sum of HisRetirement and HerRetirement . Note that this only matters when creating reports: there is no need to do anything except normal, double-entry booking with balanced transactions in each real fund. When I say the \u201csum\u201d of two funds I mean a combination of taking the union of the contained account names, after stripping off the fund names, then summing the balances of the common accounts and keeping the balances of the others. (Balance Sheet) For reporting, one wants the capability for balance by fund and balance summed over a set of funds. I also use a report that shows a subset of funds, one per column, with corresponding account names lined up horizontally and a column at the right that is the \u201csum\u201d. When all funds are included in this latter report you get a complete picture of what you own and owe and what is set aside for different purposes, or restricted in different ways. Here\u2019s a small sample of a balance sheet for a subset of the church Funds. The terminology is a little different: what Beancount calls Equity is here Net Assets. And because using large numbers of Funds in PowerChurchPlus is not so easy, the NetAssets are actually categorized for different purposes -- this is where I think the ideas we\u2019re exploring here can really shine: if Funds are really easy to create and combine for reporting then some of the mechanisms that PCP has for divvying up assets within a fund become unnecessary. (Income Statement) For the Income and Expense report, usually I only care about expenses in one Fund at a time, but adding two funds together makes perfect sense if that\u2019s what you need to do. For me, this approach to fund accounting is appealing because it relies on and preserves the fundamental principles of double-entry bookkeeping: when transactions sum to 0 the balance sheet equation is always true. Out of this we automatically get the ability to combine any set of funds (we don\u2019t have to do anything special when entering transactions or organizing the deep structure of the accounts) and have it make at least arithmetical sense, and we don\u2019t rely on any \u201cmagic\u201d associated with renaming or tagging. I don\u2019t see how this can be so easily or neatly achieved by pushing the idea of the \u201cfunds\u201d down into the account hierarchy: funds belong above the five root accounts (Assets, Liabilities, Equity, Income and Expenses), not below them.","title":"Handling Multiple Funds"},{"location":"fund_accounting_with_beancount.html#ideas-for-implementation","text":"Some random ideas for now. This needs a bit more work. If multiple redundant postings are required, the generation of these can be automated using a plugin . For instance, if a technique similar to mirror accounting is used in order to \u201csend the same dollars to multiple accounts\u201d, at least the user should not have to do this manually, which would be both tedious and prone to errors. A procedure to rename accounts upon parsing could be used, in order to merge multiple files into one. (Allowing the user to install such a mapping is an idea I\u2019ve had for a while but never implemented, though it could be implemented by a plugin filter.) We can rely on the fact that the transactions of subaccounts may be joined and summed in a parent account (despite the fact that reporting is lagging behind in that matter at the moment. It will be implemented eventually). Building off the earlier remark about doing something similar to the tag stack for Funds. What if the current tag architecture were extended to allow tags to have a value, #fund=Operations, or #fund=Endowment . Call them value-tags. You would also need to allow postings to have tags. Semantics of value-tags would be that they could only occur once for any posting, that a tag explicitly on a posting overrides the value-tag inherited from the transaction, and that an explicit tag on a transaction overrides a value from the tag-stack, and that only the last value-tag (with a particular key) in the tag-stack is applied to a transaction. This makes Funds a little less first-class than the earlier proposal to stick them in front of account names, but gets around the minor parsing difficulty previously mentioned. It suggests that opening of accounts within funds is not necessary where the previous syntax suggests that it is. The strict balancing rule for each fund in a transaction can still be implemented as a plugin. And reporting for a fund (or sum of funds) looks like: select transactions with any posting matching the desired fund (or funds) collect (and sum if necessary in the obvious way) postings associated with the fund (or funds) being reported on (A) collect (and sum in the obvious way) postings from the selected transactions not associated with the desired fund (B) Format a report with (A) as the information for the desired fund or funds and (B) as OTHER. OTHER is needed to make sure that the report balances, but could be omitted by choice. From an implementation perspective this seems more orthogonal to the current status quo, requiring even less change to existing code. It adds a new feature -- value tags and that can then be used by plugins and new reports to do what we want for fund accounting.","title":"Ideas for Implementation"},{"location":"fund_accounting_with_beancount.html#examples","text":"(Carl) Here is an example of how I might try to handle things associated with my paycheck, which involves deferred compensation (403(b) -- extremely similar to a 401(k)) and a Flexible Spending Account (somewhat similar to an HSA which has been discussed previously on the ledger-cli group).","title":"Examples"},{"location":"fund_accounting_with_beancount.html#without-funds","text":"(Carl) First, without Funds (this passes a bean-check): This is how I might set up my pay stub with health insurance, an FSA and 403b contributions in the absence of Funds. One problem is that it distributes Gross Income directly into the 403b and FSA accounts even though it recognizes that the health insurance contribution is a reduction in salary which the 403b and FSA ought to be as well. So tracking contributions to both of those is made more difficult as well as tracking taxable income. By thinking hard we could fix this -- we would come up with Income and Expense type accounts to represent the contributions, but they would end up looking kind of silly (in my opinion) because they are entirely internal to the accounts system. See the next example for how it would look using Funds. If you stick out your tongue, rub your tummy and stand on your head you will see that the Funds-based solution is equivalent to what we would have come up with in the paragraph above in terms of the complexity of its transactions -- just as many lines are required. The advantage is primarily a mental one -- it is much easier to see what to do to be both consistent and useful. option \u201ctitle\u201d \u201cPaystub - no funds\u201d 2014-07-15 open Assets:Bank:Checking 2014-07-15 open Assets:CreditUnion:Saving 2014-07-15 open Assets:FedIncTaxDeposits 2014-07-15 open Assets:Deferred:R-403b 2014-07-15 open Expenses:OASI 2014-07-15 open Expenses:Medicare 2014-07-15 open Expenses:MedicalAid 2014-07-15 open Expenses:SalReduction:HealthInsurance 2014-07-15 open Income:Gross:Emp1 2014-07-15 open Income:EmplContrib:Emp1:Retirement 2014-01-01 open Assets:FSA ; This way of setting up an FSA looks pretty good. It recognizes the ; rule that the designated amount for the year is immediately available ; (in the Asset account), and that we are obliged to make contributions ; to fund it over the course of the year (the Liability account). 2014-01-01 open Liabilities:FSA 2014-01-01 ! \"Set up FSA for 2014\" Assets:FSA 2000 USD Liabilities:FSA -2000 USD 2014-07-15 ! \"Emp1 Paystub\" Income:Gross:Emp1 -6000 USD Assets:Bank:Checking 3000 USD Assets:CreditUnion:Saving 1000 USD Assets:FedIncTaxDeposits 750 USD Expenses:OASI 375 USD Expenses:Medicare 100 USD Expenses:MedicalAid 10 USD Assets:Deferred:R-403b 600 USD Liabilities:FSA 75 USD Expenses:SalReduction:HealthInsurance 90 USD Income:EmplContrib:Emp1:Retirement -600 USD Assets:Deferred:R-403b 2014-01-01 open Expenses:Medical 2014-07-20 ! \"Direct expense from FSA\" Expenses:Medical 25 USD Assets:FSA 2014-07-20 ! \"Medical expense from checking\" Expenses:Medical 25 USD Assets:Bank:Checking 2014-07-20 ! \"Medical expense reimbursed from FSA\" Assets:Bank:Checking 25 USD Assets:FSA","title":"Without Funds"},{"location":"fund_accounting_with_beancount.html#using-funds","text":"(Carl) And now using Funds (uses proposed features and hence can\u2019t be checked by bean-check): This is how I might set up my pay stub with health insurance, an FSA and 403b contributions using Funds. I can straightforwardly arrange things so that contributions to the FSA and 403b are recognized as salary reductions for income tax purposes. And I can easily see how much I have contributed to the 403b and how much my employer has contributed. See the previous example for how it would look without using Funds and which is not as accurate. What this does NOT do is track taxes ultimately owed on the 403b money. I think that is a matter of one's decision about whether to do cash-basis or accrual-basis accounting. If cash basis those taxes are not a current liability and cannot be reported as such. If accrual basis, they are a current liability and need to be recorded as such when the income is booked. For cash-basis books, we'd want the ability to report the state of affairs as if taxes were owed, but that is a matter for reporting rather than booking. We need to make sure we have enough identifiable information to automate creating those reports. I believe that taking a Fund-based accounting perspective easily does this. A problem not solved: what if your basis is different for Federal and State purposes, or even for Federal and multiple different states. Yikes! I've used the convention that the Fund name precedes the root account name. Note that with appropriate standing on one's head along with pivoting rules you can put the Fund name anywhere. Putting it first emphasizes that it identifies a set of accounts that must balance, and it makes it easy for the txn processor to guarantee this property. Accounts without a fund name in front belong to the Fund whose name is the empty string. option \"title\" \"Paystub - no Funds\" 2014-07-15 open Assets:Bank:Checking 2014-07-15 open Assets:CreditUnion:Saving 2014-07-15 open Assets:FedIncTaxDeposits 2014-07-15 open Expenses:OASI 2014-07-15 open Expenses:Medicare 2014-07-15 open Expenses:MedicalAid 2014-07-15 open Expenses:SalReduction:HealthInsurance 2014-07-15 open Expenses:SalReduction:FSA 2014-07-15 open Expenses:SalReduction:R-403b 2014-07-15 open Income:Gross:Emp1 2014-07-15 open Income:EmplContrib:Emp1:Retirement 2014-01-01 open FSA:Assets ; FSA fund accounts 2014-01-01 open FSA:Income:Contributions 2014-01-01 open FSA:Expenses:Medical 2014-01-01 open FSA:Expenses:ReimburseMedical 2014-01-01 open FSA:Liabilities 2014-07-15 open Retirement403b:Assets:CREF ; Retirement fund accounts 2014-07-15 open Retirement403b:Income:EmployeeContrib 2014-07-15 open Retirement403b:Income:EmployerContrib 2014-07-15 open Retirement403b:Income:EarningsGainsAndLosses ; This implements the same idea as above for the FSA, of balancing ; Assets and Liabilities at the opening, but now does it using a ; separate Fund. 2014-01-01 ! \"Set up FSA for 2014\" FSA:Assets 2000 USD FSA:Liabilities -2000 USD 2014-07-15 ! \"Emp1 Paystub\" Income:Gross:Emp1 -6000 USD Assets:Bank:Checking 3000 USD Assets:CreditUnion:Saving 1000 USD Assets:FedIncTaxDeposits 750 USD Expenses:OASI 375 USD Expenses:Medicare 100 USD Expenses:MedicalAid 10 USD Expenses:SalReduction:R-403b 600 USD Retirement403b:Income:EmployeeContrib -600 USD Retirement403b:Assets:CREF 600 USD Expenses:SalReduction:FSA 75 USD FSA:Income:Contributions -75 USD FSA:Liabilities Expenses:SalReduction:HealthInsurance Retirement403b:Income:EmployerContrib -600 USD Retirement403b:Assets:CREF 2014-01-01 open Expenses:Medical 2014-07-20 ! \"Direct expense from FSA\" FSA:Expenses:Medical 25 USD FSA:Assets 2014-07-20 ! \"Medical expense from checking\" Expenses:Medical 25 USD Assets:Bank:Checking 2014-07-20 ! \"Medical expense reimbursed from FSA\" Assets:Bank:Checking 25 USD Income:ReimburseMedical FSA:Assets -25 USD FSA:Expenses:ReimburseMedical","title":"Using Funds"},{"location":"fund_accounting_with_beancount.html#transfer-accounts-proposal","text":"By Carl Hauser One problem that I\u2019ve experienced using the Fund approach is that it\u2019s a bit too easy to make mistakes when transferring money between funds, such as in the very last transaction above. Formalizing the idea of Transfer accounts can help with this. The most common mistake is to end up with something that moves assets in both accounts in the same direction -- both up or both down as in this mistaken version of the transaction in question: 2014-07-20 ! \"Medical expense reimbursed from FSA - with mistake\" Assets:Bank:Checking 25 USD Income:ReimburseMedical FSA:Assets 25 USD FSA:Expenses:ReimburseMedical This balances but isn\u2019t what we intended. Suppose we add the idea of Transfer accounts. They live at the same place in the hierarchy as Income and Expenses and like those are non-balance-sheet accounts. But they really come into play only for transactions that involve multiple funds. There is an additional rule for transactions containing Transfer accounts: the sum of the transfers must also be zero (additional in the sense that the rule about transactions balancing within each fund is still there). So to use this we set things up a little differently: 2014-07-15 open FSA:Transfer:Incoming:Contribution 2014-07-15 open FSA:Transfer:Outgoing:ReimburseMedical 2014-07-15 open Transfer:Outgoing:FSAContribution 2014-07-15 open Transfer:Incoming:ReimburseMedical The incorrect transaction is now flagged because sum of the transfers is -50 USD , not zero. 2014-07-20 ! \"Medical expense reimbursed from FSA - with mistake\" Assets:Bank:Checking 25 USD Transfer:Incoming:ReimburseMedical FSA:Assets 25 USD FSA:Transfer:Outgoing:ReimburseMedical The paycheck transaction using transfer accounts for the FSA and the retirement account amounts might look like this (after appropriate open s of course): 2014-07-15 ! \"Emp1 Paystub - using transfer accounts\" Income:Gross:Emp1 -6000 USD Assets:Bank:Checking 3000 USD Assets:CreditUnion:Saving 1000 USD Assets:FedIncTaxDeposits 750 USD Expenses:OASI 375 USD Expenses:Medicare 100 USD Expenses:MedicalAid 10 USD Transfer:Outgoing:Retirement403b:SalReduction 600 USD Retirement403b:Transfer:Incoming:EmployeeContrib Retirement403b:Assets:CREF 600 USD Transfer:Outgoing:FSA:SalReduction 75 USD FSA:Transfer:Incoming:Contributions FSA:Liabilities Expenses:SalReduction:HealthInsurance Retirement403b:Income:EmployerContrib -600 USD Retirement403b:Assets:CREF Some might think that this is too complicated. Without changing the Transfer accounts idea or rule, you can simplify booking to just a single account per fund, Fund:Transfer , losing some ability for precision in reporting but without losing the ability to check correctness of transfer transactions.","title":"Transfer Accounts Proposal"},{"location":"fund_accounting_with_beancount.html#account-aliases","text":"Simon Michael mentions that this is related to HLedger account aliases : \u201cI think this is related to the situation where you want to view entities' finances both separately and merged. Account aliases can be another way to approximate this, as in http://hledger.org/how-to-use-account-aliases .\u201d If you find yourself culturally challenged by our modern lifestyle, perhaps you can imagine the case of roommates, although I don\u2019t like the reductionist view this association brings to my mind. \u21a9","title":"Account Aliases"},{"location":"getting_started_with_beancount.html","text":"Getting Started with Beancount \uf0c1 Martin Blais , July 2014 http://furius.ca/beancount/doc/getting-started Introduction \uf0c1 This document is a gentle guide to creating your first Beancount file, initializing it with some options, some guidelines for how to organize your file, and instructions for declaring accounts and making sure their initial balance does not raise errors. It also contains some material on configuring the Emacs text editor, if you use that. You will probably want to have read some of the User\u2019s Manual first in order to familiarize yourself with the syntax and kinds of available directives, or move on to the Cookbook if you\u2019ve already setup a file or know how to do that. If you\u2019re familiar with Ledger, you may want to read up on the differences between Ledger and Beancount first. Editor Support \uf0c1 Beancount ledgers are simple text files. You can use any text editor to compose your input file. However, a good text editor which understands enough of the Beancount syntax to offer focused facilities like syntax highlighting, autocompletion, and automatic indentation highly has the potential to greatly increase productivity in compiling and maintaining your ledger. Emacs \uf0c1 Support for editing Beancount ledger files in Emacs was traditionally distributed with Beancount. It now lives as its own project in this Github repository . Vim \uf0c1 Support for editing Beancount ledger files in Vim has been implemented by Nathan Grigg and is available in this Github repository . Sublime \uf0c1 Support for editing with Sublime has been contributed by Martin Andreas Andersen and is available in this github repository or as a Sublime package here . VSCode \uf0c1 There are a number of plugins for working with Beancount text files including VSCode-Beancount by Lencerf. Creating your First Input File \uf0c1 To get started, let\u2019s create a minimal input file with two accounts and a single transaction. Enter or copy the following input to a text file: 2014-01-01 open Assets:Checking 2014-01-01 open Equity:Opening-Balances 2014-01-02 * \"Deposit\" Assets:Checking 100.00 USD Equity:Opening-Balances Brief Syntax Overview \uf0c1 A few notes and an ultra brief overview of the Beancount syntax: Currencies must be entirely in capital letters (allowing numbers and some special characters, like \u201c_\u201d or \u201c-\u201d). Currency symbols (such as $ or \u20ac) are not supported. Account names do not admit spaces (though you can use dashes), and must have at least two components, separated by colons. Each component of an account name must begin with a capital letter or number. Description strings must be quoted, like this: \"AMEX PMNT\" . Dates are only parsed in ISO8601 format, that is, YYYY-MM-DD. Tags must begin with \u201c#\u201d, and links with \u201c^\u201d. For a complete description of the syntax, visit the User\u2019s Manual . Validating your File \uf0c1 The purpose of Beancount is to produce reports from your input file (either on the console or serve via its web interface). However, there is a tool that you can use to simply load its contents and make some validation checks on it, to ensure that your input does not contain errors. Beancount can be quite strict; this is a tool that you use while you\u2019re entering your data to ensure that your input file is legal. The tool is called \u201cbean-check\u201d and you invoke it like this: bean-check /path/to/your/file.beancount Try it now on the file you just created from the previous section. It should exit with no output. If there are errors, they will be printed on the console. The errors are printed out in a format that Emacs recognizes by default, so you can use Emacs\u2019 next-error and previous-error built-in functions to move the cursor to the location of the problem. Viewing the Web Interface \uf0c1 A convenient way to view reports is to bring up the \u201cbean-web\u201d tool on your input file. Try it: bean-web /path/to/your/file.beancount You can then point a web browser to http://localhost:8080 and click your way around the various reports generated by Beancount. You can then modify the input file and reload the web page your browser is pointing to\u2014bean-web will automatically reload the file contents. At this point, you should probably read some of the Language Syntax document. How to Organize your File \uf0c1 In this section we provide general guidelines for how to organize your file. This assumes you\u2019ve read the Language Syntax document. Preamble to your Input File \uf0c1 I recommend that you begin with just a single file 1 . My file has a header that tells Emacs what mode to open the file with, followed by some common options: ;; -*- mode: beancount; coding: utf-8; fill-column: 400; -*- option \"title\" \"My Personal Ledger\" option \"operating_currency\" \"USD\" option \"operating_currency\" \"CAD\" The title option is used in reports. The list of \u201coperating currencies\u201d identify those commodities which you use most commonly as \u201ccurrencies\u201d and which warrant rendering in their own dedicated columns in reports (this declaration has no other effect on the behavior of any of the calculations). Sections & Declaring Accounts \uf0c1 I like to organize my input file in sections that correspond to each real-world account. Each section defines all the accounts related to this real-world account by using an Open directive. For example, this is a checking account: 2007-02-01 open Assets:US:BofA:Savings USD 2007-02-01 open Income:US:BofA:Savings:Interest USD I like to declare the currency constraints as much as possible, to avoid mistakes. Also, note how I declare an income account specific to this account. This helps break down income in reporting for taxes, as you will likely receive a tax document in relation to that specific account\u2019s income (in the US this would be a 1099-INT form produced by your bank). Here\u2019s what the opening accounts might look like for an investment account: 2012-03-01 open Assets:US:Etrade:Main:Cash USD 2012-03-01 open Assets:US:Etrade:Main:ITOT ITOT 2012-03-01 open Assets:US:Etrade:Main:IXUS IXUS 2012-03-01 open Assets:US:Etrade:Main:IEFA IEFA 2012-03-01 open Income:US:Etrade:Main:Interest USD 2012-03-01 open Income:US:Etrade:Main:PnL USD 2012-03-01 open Income:US:Etrade:Main:Dividend USD 2012-03-01 open Income:US:Etrade:Main:DividendNoTax USD The point is that all these accounts are related somehow. The various sections of the cookbook will describe the set of accounts suggested to create for each section. Not all sections have to be that way. For example, I have the following sections: Eternal accounts. I have a section at the top dedicated to contain special and \u201ceternal\u201d accounts, such as payables and receivables. Daybook. I have a \u201cdaybook\u201d section at the bottom that contains all cash expenses, in chronological order. Expense accounts. All my expenses accounts (categories) are defined in their own section. Employers. For each employer I\u2019ve defined a section where I put the entries for their direct deposits, and track vacations, stock vesting and other job-related transactions. Taxes. I have a section for taxes, organized by taxation year. You can organize it any way you like, because Beancount doesn\u2019t care about the ordering of declarations. Closing Accounts \uf0c1 If a real-world account has closed, or is never going to have any more transactions posted to it, you can declare it \u201cclosed\u201d at a particular date by using a Close directive: ; Moving to another bank. 2013-06-13 close Assets:US:BofA:Savings This tells Beancount not to show the account in reports that don\u2019t include any date where it was active. It also avoids errors by triggering an error if you do try to post to it at a later date. De-duping \uf0c1 One problem that will occur frequently is that once you have some sort of code or process set up to automatically extract postings from downloaded files, you will end up importing postings which provide two separate sides of the same transaction. An example is the payment of a credit card balance via a transfer from a checking account. If you download the transactions for your checking account, you will extract something like this: 2014-06-08 * \"ONLINE PAYMENT - THANK YOU\" Assets:CA:BofA:Checking -923.24 USD The credit card download will yield you this: 2014-06-10 * \"AMEX EPAYMENT ACH PMT\" Liabilities:US:Amex:Platinum 923.24 USD Many times, transactions from these accounts need to be booked to an expense account, but in this case, these are two separate legs of the same transaction: a transfer. When you import one of these, you normally look for the other side and merge them together: ;2014-06-08 * \"ONLINE PAYMENT - THANK YOU\" 2014-06-10 * \"AMEX EPAYMENT ACH PMT\" Liabilities:US:Amex:Platinum 923.24 USD Assets:CA:BofA:Checking -923.24 USD I often leave one of the description lines in comments\u2014just my choice, Beancount ignores it. Also note that I had to choose one of the two dates. I just choose the one I prefer, as long as it does not break any balance assertion. In the case that you would forget to merge those two imported transactions, worry not! That\u2019s what balance assertions are for. Regularly place a balance assertion in either of these accounts, e.g., every time you import, and you will get a nice error if you end up entering the transaction twice. This is pretty common and after a while it becomes second nature to interpret that compiler error and fix it in seconds. Finally, when I know I import just one side of these, I select the other account manually and I mark the posting I know will be imported later with a flag, which tells me I haven\u2019t de-duped this transaction yet: 2014-06-10 * \"AMEX EPAYMENT ACH PMT\" Liabilities:US:Amex:Platinum 923.24 USD ! Assets:CA:BofA:Checking Later on, when I import the checking account\u2019s transactions and go fishing for the other side of this payment, I will find this and get a good feeling that the world is operating as it should. (If you\u2019re interested in more of a discussion around de-duplicating and merging transactions, see this feature proposal . Also, you might be interested in the \u201ceffective_date\u201d plugin external contribution, which splits transactions in two.) Which Side? \uf0c1 So if you organize your account in sections the way I suggest above, which section of the file should you leave such \u201cmerged\u201d transactions in, that is, transactions that involve two separate accounts? Well, it\u2019s your call. For example, in the case of a transfer between two accounts organized such that they have their own dedicated sections, it would be nice to be able to leave both transactions there so that when you edit your input file you see them in either section, but unfortunately, the transaction must occur in only one place in your document. You have to choose one. Personally I\u2019m a little careless about being consistent which of the section I choose to leave the transaction in; sometimes I choose one section of my input file, or that of the other account, for the same pair of accounts. It hasn\u2019t been a problem, as I use Emacs and i-search liberally which makes it easy to dig around my gigantic input file. If you want to keep your input more tidy and organized, you could come up with a rule for yourself, e.g. \u201ccredit card payments are always left in the paying account\u2019s section, not in the credit card account\u2019s section\u201d, or perhaps you could leave the transaction in both sections and comment one out 2 . Padding \uf0c1 If you\u2019re just starting out\u2014and you probably are if you\u2019re reading this\u2014you will have no historical data. This means that the balances of your Assets and Liabilities accounts in Beancount will all be zero. But the first thing you should want to do after defining some accounts is establish a balance sheet and bring those amounts to their actual current value. Let\u2019s take your checking account as an example, say you opened it a while back. You don\u2019t remember exactly when, so let\u2019s use an approximate date: 2000-05-28 open Assets:CA:BofA:Checking USD The next thing you do is look up your current balance and put a balance assertion for the corresponding amount: 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Running Beancount on just this will correctly produce an error because Beancount assumes an implicit balance assertion of \u201cempty\u201d at the time you open an account. You will have to \u201cpad\u201d your account to today\u2019s balance by inserting a balance adjustment at some point in time between the opening and the balance, against some equity account, which is an arbitrary place to book \u201cwhere you received the initial balance from.\u201d For this purpose, this is usually the \u201c Equity:Opening-Balances \u201d account. So let\u2019s include this padding transaction and recap what we have so far: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 * \"Initialize account\" Equity:Opening-Balances -1256.35 USD Assets:CA:BofA:Checking 1256.35 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD From here onwards, you would start adding entries reflecting everything that happened after 7/1. However, what if you wanted to go back in time? It is perfectly reasonable that once you\u2019ve got your chart-of-accounts set up you might want to fill in the missing history until at least the beginning of this year. Let\u2019s assume you had a single transaction in June 2014, and let\u2019s add it: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 * \"Initialize account\" Equity:Opening-Balances -1256.35 USD Assets:CA:BofA:Checking 1256.35 USD 2014-06-28 * \"Paid credit card bill\" Assets:CA:BofA:Checking -700.00 USD Liabilities:US:Amex:Platinum 700.00 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Now the balance assertion fails! You would need to adjust the initialization entry to fix this: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 * \"Initialize account\" Equity:Opening-Balances -1956.35 USD Assets:CA:BofA:Checking 1956.35 USD 2014-06-28 * \"Paid credit card bill\" Assets:CA:BofA:Checking -700.00 USD Liabilities:US:Amex:Platinum 700.00 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Now this works. So basically, every single time you insert an entry in the past, you would have to adjust the balance. Isn\u2019t this annoying? Well, yes. Fortunately, we can provide some help: you can use a Pad directive to replace and automatically synthesize the balance adjustment to match the next balance check, like this: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 pad Assets:CA:BofA:Checking Equity:Opening-Balances 2014-06-28 * \"Paid credit card bill\" Assets:CA:BofA:Checking -700.00 USD Liabilities:US:Amex:Platinum 700.00 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Note that this is only needed for balance sheet accounts (Assets and Liabilities) because we don\u2019t care about the initial balances of the Income and Expenses accounts, we only care about their transitional value (the changes they post during a period). For example, it makes no sense to bring up the Expenses:Restaurant account to the sum total value of all the costs of the meals you consumed since you were born. So you will probably want to get started with Open & Pad directives for each Assets and Liabilities accounts. What\u2019s Next? \uf0c1 At this point you will probably move onwards to the Cookbook , or read the User\u2019s Manual if you haven\u2019t already done that. It is tempting to want to break down a large file into many smaller ones, but especially at first, the convenience of having everything in a single place is great. \u21a9 Some people have suggested that Beancount automatically detect duplicated transactions based on a heuristic and automatically ignore (remove) one of the two, but this has not been tried out yet. In particular, this would lend itself well to organizing transactions not just per section, but in separate files, i.e., all files would contain all the transactions for the accounts they represent. If you\u2019re interested in adding this feature, you could easily implement this as a plugin, without disrupting the rest of the system. \u21a9","title":"Getting Started with Beancount"},{"location":"getting_started_with_beancount.html#getting-started-with-beancount","text":"Martin Blais , July 2014 http://furius.ca/beancount/doc/getting-started","title":"Getting Started with Beancount"},{"location":"getting_started_with_beancount.html#introduction","text":"This document is a gentle guide to creating your first Beancount file, initializing it with some options, some guidelines for how to organize your file, and instructions for declaring accounts and making sure their initial balance does not raise errors. It also contains some material on configuring the Emacs text editor, if you use that. You will probably want to have read some of the User\u2019s Manual first in order to familiarize yourself with the syntax and kinds of available directives, or move on to the Cookbook if you\u2019ve already setup a file or know how to do that. If you\u2019re familiar with Ledger, you may want to read up on the differences between Ledger and Beancount first.","title":"Introduction"},{"location":"getting_started_with_beancount.html#editor-support","text":"Beancount ledgers are simple text files. You can use any text editor to compose your input file. However, a good text editor which understands enough of the Beancount syntax to offer focused facilities like syntax highlighting, autocompletion, and automatic indentation highly has the potential to greatly increase productivity in compiling and maintaining your ledger.","title":"Editor Support"},{"location":"getting_started_with_beancount.html#emacs","text":"Support for editing Beancount ledger files in Emacs was traditionally distributed with Beancount. It now lives as its own project in this Github repository .","title":"Emacs"},{"location":"getting_started_with_beancount.html#vim","text":"Support for editing Beancount ledger files in Vim has been implemented by Nathan Grigg and is available in this Github repository .","title":"Vim"},{"location":"getting_started_with_beancount.html#sublime","text":"Support for editing with Sublime has been contributed by Martin Andreas Andersen and is available in this github repository or as a Sublime package here .","title":"Sublime"},{"location":"getting_started_with_beancount.html#vscode","text":"There are a number of plugins for working with Beancount text files including VSCode-Beancount by Lencerf.","title":"VSCode"},{"location":"getting_started_with_beancount.html#creating-your-first-input-file","text":"To get started, let\u2019s create a minimal input file with two accounts and a single transaction. Enter or copy the following input to a text file: 2014-01-01 open Assets:Checking 2014-01-01 open Equity:Opening-Balances 2014-01-02 * \"Deposit\" Assets:Checking 100.00 USD Equity:Opening-Balances","title":"Creating your First Input File"},{"location":"getting_started_with_beancount.html#brief-syntax-overview","text":"A few notes and an ultra brief overview of the Beancount syntax: Currencies must be entirely in capital letters (allowing numbers and some special characters, like \u201c_\u201d or \u201c-\u201d). Currency symbols (such as $ or \u20ac) are not supported. Account names do not admit spaces (though you can use dashes), and must have at least two components, separated by colons. Each component of an account name must begin with a capital letter or number. Description strings must be quoted, like this: \"AMEX PMNT\" . Dates are only parsed in ISO8601 format, that is, YYYY-MM-DD. Tags must begin with \u201c#\u201d, and links with \u201c^\u201d. For a complete description of the syntax, visit the User\u2019s Manual .","title":"Brief Syntax Overview"},{"location":"getting_started_with_beancount.html#validating-your-file","text":"The purpose of Beancount is to produce reports from your input file (either on the console or serve via its web interface). However, there is a tool that you can use to simply load its contents and make some validation checks on it, to ensure that your input does not contain errors. Beancount can be quite strict; this is a tool that you use while you\u2019re entering your data to ensure that your input file is legal. The tool is called \u201cbean-check\u201d and you invoke it like this: bean-check /path/to/your/file.beancount Try it now on the file you just created from the previous section. It should exit with no output. If there are errors, they will be printed on the console. The errors are printed out in a format that Emacs recognizes by default, so you can use Emacs\u2019 next-error and previous-error built-in functions to move the cursor to the location of the problem.","title":"Validating your File"},{"location":"getting_started_with_beancount.html#viewing-the-web-interface","text":"A convenient way to view reports is to bring up the \u201cbean-web\u201d tool on your input file. Try it: bean-web /path/to/your/file.beancount You can then point a web browser to http://localhost:8080 and click your way around the various reports generated by Beancount. You can then modify the input file and reload the web page your browser is pointing to\u2014bean-web will automatically reload the file contents. At this point, you should probably read some of the Language Syntax document.","title":"Viewing the Web Interface"},{"location":"getting_started_with_beancount.html#how-to-organize-your-file","text":"In this section we provide general guidelines for how to organize your file. This assumes you\u2019ve read the Language Syntax document.","title":"How to Organize your File"},{"location":"getting_started_with_beancount.html#preamble-to-your-input-file","text":"I recommend that you begin with just a single file 1 . My file has a header that tells Emacs what mode to open the file with, followed by some common options: ;; -*- mode: beancount; coding: utf-8; fill-column: 400; -*- option \"title\" \"My Personal Ledger\" option \"operating_currency\" \"USD\" option \"operating_currency\" \"CAD\" The title option is used in reports. The list of \u201coperating currencies\u201d identify those commodities which you use most commonly as \u201ccurrencies\u201d and which warrant rendering in their own dedicated columns in reports (this declaration has no other effect on the behavior of any of the calculations).","title":"Preamble to your Input File"},{"location":"getting_started_with_beancount.html#sections-declaring-accounts","text":"I like to organize my input file in sections that correspond to each real-world account. Each section defines all the accounts related to this real-world account by using an Open directive. For example, this is a checking account: 2007-02-01 open Assets:US:BofA:Savings USD 2007-02-01 open Income:US:BofA:Savings:Interest USD I like to declare the currency constraints as much as possible, to avoid mistakes. Also, note how I declare an income account specific to this account. This helps break down income in reporting for taxes, as you will likely receive a tax document in relation to that specific account\u2019s income (in the US this would be a 1099-INT form produced by your bank). Here\u2019s what the opening accounts might look like for an investment account: 2012-03-01 open Assets:US:Etrade:Main:Cash USD 2012-03-01 open Assets:US:Etrade:Main:ITOT ITOT 2012-03-01 open Assets:US:Etrade:Main:IXUS IXUS 2012-03-01 open Assets:US:Etrade:Main:IEFA IEFA 2012-03-01 open Income:US:Etrade:Main:Interest USD 2012-03-01 open Income:US:Etrade:Main:PnL USD 2012-03-01 open Income:US:Etrade:Main:Dividend USD 2012-03-01 open Income:US:Etrade:Main:DividendNoTax USD The point is that all these accounts are related somehow. The various sections of the cookbook will describe the set of accounts suggested to create for each section. Not all sections have to be that way. For example, I have the following sections: Eternal accounts. I have a section at the top dedicated to contain special and \u201ceternal\u201d accounts, such as payables and receivables. Daybook. I have a \u201cdaybook\u201d section at the bottom that contains all cash expenses, in chronological order. Expense accounts. All my expenses accounts (categories) are defined in their own section. Employers. For each employer I\u2019ve defined a section where I put the entries for their direct deposits, and track vacations, stock vesting and other job-related transactions. Taxes. I have a section for taxes, organized by taxation year. You can organize it any way you like, because Beancount doesn\u2019t care about the ordering of declarations.","title":"Sections & Declaring Accounts"},{"location":"getting_started_with_beancount.html#closing-accounts","text":"If a real-world account has closed, or is never going to have any more transactions posted to it, you can declare it \u201cclosed\u201d at a particular date by using a Close directive: ; Moving to another bank. 2013-06-13 close Assets:US:BofA:Savings This tells Beancount not to show the account in reports that don\u2019t include any date where it was active. It also avoids errors by triggering an error if you do try to post to it at a later date.","title":"Closing Accounts"},{"location":"getting_started_with_beancount.html#de-duping","text":"One problem that will occur frequently is that once you have some sort of code or process set up to automatically extract postings from downloaded files, you will end up importing postings which provide two separate sides of the same transaction. An example is the payment of a credit card balance via a transfer from a checking account. If you download the transactions for your checking account, you will extract something like this: 2014-06-08 * \"ONLINE PAYMENT - THANK YOU\" Assets:CA:BofA:Checking -923.24 USD The credit card download will yield you this: 2014-06-10 * \"AMEX EPAYMENT ACH PMT\" Liabilities:US:Amex:Platinum 923.24 USD Many times, transactions from these accounts need to be booked to an expense account, but in this case, these are two separate legs of the same transaction: a transfer. When you import one of these, you normally look for the other side and merge them together: ;2014-06-08 * \"ONLINE PAYMENT - THANK YOU\" 2014-06-10 * \"AMEX EPAYMENT ACH PMT\" Liabilities:US:Amex:Platinum 923.24 USD Assets:CA:BofA:Checking -923.24 USD I often leave one of the description lines in comments\u2014just my choice, Beancount ignores it. Also note that I had to choose one of the two dates. I just choose the one I prefer, as long as it does not break any balance assertion. In the case that you would forget to merge those two imported transactions, worry not! That\u2019s what balance assertions are for. Regularly place a balance assertion in either of these accounts, e.g., every time you import, and you will get a nice error if you end up entering the transaction twice. This is pretty common and after a while it becomes second nature to interpret that compiler error and fix it in seconds. Finally, when I know I import just one side of these, I select the other account manually and I mark the posting I know will be imported later with a flag, which tells me I haven\u2019t de-duped this transaction yet: 2014-06-10 * \"AMEX EPAYMENT ACH PMT\" Liabilities:US:Amex:Platinum 923.24 USD ! Assets:CA:BofA:Checking Later on, when I import the checking account\u2019s transactions and go fishing for the other side of this payment, I will find this and get a good feeling that the world is operating as it should. (If you\u2019re interested in more of a discussion around de-duplicating and merging transactions, see this feature proposal . Also, you might be interested in the \u201ceffective_date\u201d plugin external contribution, which splits transactions in two.)","title":"De-duping"},{"location":"getting_started_with_beancount.html#which-side","text":"So if you organize your account in sections the way I suggest above, which section of the file should you leave such \u201cmerged\u201d transactions in, that is, transactions that involve two separate accounts? Well, it\u2019s your call. For example, in the case of a transfer between two accounts organized such that they have their own dedicated sections, it would be nice to be able to leave both transactions there so that when you edit your input file you see them in either section, but unfortunately, the transaction must occur in only one place in your document. You have to choose one. Personally I\u2019m a little careless about being consistent which of the section I choose to leave the transaction in; sometimes I choose one section of my input file, or that of the other account, for the same pair of accounts. It hasn\u2019t been a problem, as I use Emacs and i-search liberally which makes it easy to dig around my gigantic input file. If you want to keep your input more tidy and organized, you could come up with a rule for yourself, e.g. \u201ccredit card payments are always left in the paying account\u2019s section, not in the credit card account\u2019s section\u201d, or perhaps you could leave the transaction in both sections and comment one out 2 .","title":"Which Side?"},{"location":"getting_started_with_beancount.html#padding","text":"If you\u2019re just starting out\u2014and you probably are if you\u2019re reading this\u2014you will have no historical data. This means that the balances of your Assets and Liabilities accounts in Beancount will all be zero. But the first thing you should want to do after defining some accounts is establish a balance sheet and bring those amounts to their actual current value. Let\u2019s take your checking account as an example, say you opened it a while back. You don\u2019t remember exactly when, so let\u2019s use an approximate date: 2000-05-28 open Assets:CA:BofA:Checking USD The next thing you do is look up your current balance and put a balance assertion for the corresponding amount: 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Running Beancount on just this will correctly produce an error because Beancount assumes an implicit balance assertion of \u201cempty\u201d at the time you open an account. You will have to \u201cpad\u201d your account to today\u2019s balance by inserting a balance adjustment at some point in time between the opening and the balance, against some equity account, which is an arbitrary place to book \u201cwhere you received the initial balance from.\u201d For this purpose, this is usually the \u201c Equity:Opening-Balances \u201d account. So let\u2019s include this padding transaction and recap what we have so far: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 * \"Initialize account\" Equity:Opening-Balances -1256.35 USD Assets:CA:BofA:Checking 1256.35 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD From here onwards, you would start adding entries reflecting everything that happened after 7/1. However, what if you wanted to go back in time? It is perfectly reasonable that once you\u2019ve got your chart-of-accounts set up you might want to fill in the missing history until at least the beginning of this year. Let\u2019s assume you had a single transaction in June 2014, and let\u2019s add it: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 * \"Initialize account\" Equity:Opening-Balances -1256.35 USD Assets:CA:BofA:Checking 1256.35 USD 2014-06-28 * \"Paid credit card bill\" Assets:CA:BofA:Checking -700.00 USD Liabilities:US:Amex:Platinum 700.00 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Now the balance assertion fails! You would need to adjust the initialization entry to fix this: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 * \"Initialize account\" Equity:Opening-Balances -1956.35 USD Assets:CA:BofA:Checking 1956.35 USD 2014-06-28 * \"Paid credit card bill\" Assets:CA:BofA:Checking -700.00 USD Liabilities:US:Amex:Platinum 700.00 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Now this works. So basically, every single time you insert an entry in the past, you would have to adjust the balance. Isn\u2019t this annoying? Well, yes. Fortunately, we can provide some help: you can use a Pad directive to replace and automatically synthesize the balance adjustment to match the next balance check, like this: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 pad Assets:CA:BofA:Checking Equity:Opening-Balances 2014-06-28 * \"Paid credit card bill\" Assets:CA:BofA:Checking -700.00 USD Liabilities:US:Amex:Platinum 700.00 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Note that this is only needed for balance sheet accounts (Assets and Liabilities) because we don\u2019t care about the initial balances of the Income and Expenses accounts, we only care about their transitional value (the changes they post during a period). For example, it makes no sense to bring up the Expenses:Restaurant account to the sum total value of all the costs of the meals you consumed since you were born. So you will probably want to get started with Open & Pad directives for each Assets and Liabilities accounts.","title":"Padding"},{"location":"getting_started_with_beancount.html#whats-next","text":"At this point you will probably move onwards to the Cookbook , or read the User\u2019s Manual if you haven\u2019t already done that. It is tempting to want to break down a large file into many smaller ones, but especially at first, the convenience of having everything in a single place is great. \u21a9 Some people have suggested that Beancount automatically detect duplicated transactions based on a heuristic and automatically ignore (remove) one of the two, but this has not been tried out yet. In particular, this would lend itself well to organizing transactions not just per section, but in separate files, i.e., all files would contain all the transactions for the accounts they represent. If you\u2019re interested in adding this feature, you could easily implement this as a plugin, without disrupting the rest of the system. \u21a9","title":"What\u2019s Next?"},{"location":"health_care_expenses.html","text":"Health Care Expenses \uf0c1 Martin Blais , July 2014 This is incomplete, work-in-progress, not released yet. Accounting for your health care expenses is a little different than regular expenses because of the various maximums imposed by your insurance plan. For the purpose of this section, we will assume a context of privatized health care system as is present in the USA, but the same principles may apply to other countries (if anything, it will be simpler). Accounting With No Insurance Plan - The Naive Way \uf0c1 So what\u2019s so different about health care expenses? You might argue that they should be treated the same as other expenses. Say, we could define a few categories like these: 1973-04-27 open Expenses:Health:Medical 1973-04-27 open Expenses:Health:Dental 1973-04-27 open Expenses:Health:Vision 1973-04-27 open Expenses:Health:Drugs 1973-04-27 open Expenses:Health:Acupuncture 1973-04-27 open Expenses:Health:Massage-Therapy 1973-04-27 open Expenses:Health:Physical-Therapy And simply book outflows of moneys to them when you spend on one of these categories: 2014-01-08 * \"CityPT\" \"Copay for PT session w/ Rob on 12/28\" Liabilities:US:BofA:Credit-Card -25 USD Expenses:Health:Physical-Therapy 25 USD In fact, this would be work just fine if you paid for all of your health care costs out-of-pocket. If that is your situation, this is what you should do. It\u2019s straightforward. The problem is that in practice, the great majority of people don\u2019t pay for health care costs themselves. The great majority of your health-related costs are paid for by your insurance. This does not mean that there are no costs: your insurance usually only pays for a portion of the actual expenses, depending on the service. In particular, depending on your insurance plan, in any calendar year you pay for 100% of your health care costs up to a fixed amount (usually a few hundred dollars). This is called the deductible amount. Heretofore, you pay a percentage of the service (the copayment amounts ) , and after a higher limit amount (the out-of-pocket maximum amount), you don\u2019t pay anything anymore; plans with such limits guarantee that you will never pay more than this amount. If you booked the amounts that you pay for your deductible, the scale of the expenses that would get reflected on your income statement would depend mostly on which type of service you happened to use first after January 1st. It would not accurately reflect the cost of each service that you use. It would not be that useful. Counting the Cash Payments - The Incorrect Way \uf0c1 So you might argue that you should instead book the same outflows of money to categories that reflect their true nature: 1973-04-27 open Expenses:Health:Medical:Deductible 1973-04-27 open Expenses:Health:Medical:Copayments 1973-04-27 open Expenses:Health:Vision:Deductible 1973-04-27 open Expenses:Health:Vision:Copayments 1973-04-27 open Expenses:Health:Dental:Deductible 1973-04-27 open Expenses:Health:Dental:Copayments Then booking the payments to those categories: 2014-01-08 * \"CityPT\" \"Copay for PT session w/ Rob on 12/28\" Liabilities:US:BofA:Credit-Card -25 USD Expenses:Health:Medical:Copayments 25 USD But this is largely unsatisfying, for a few reasons: while this tracks how much of was paid in total in deductible and copayments, it does not reflect anything about how much of a particular health provider services was used. More importantly, it is incorrect, because deductible and copayment limits apply to each calendar year for dates when the service was provided, not when it was paid for. In other words, if you received the service on December 28th, 2013 but paid for it the year following on January 8th, 2014, the copayment accrual is going towards the wrong year. In order to do this correctly, you must accrue the deductible and copayment amounts on the date of service , which is always provided on the documents your insurance company provides (the Explanation of Benefits , more later about this). The date of service is the date you actually were seen by the provider. Another solution would be to define accounts for each year, similarly to tax accounts (see chapter on Taxes): 2012-01-01 open Expenses:Health:Medical:Y2012:Deductible 2012-01-01 open Expenses:Health:Medical:Y2012:Copayments 2013-01-01 open Expenses:Health:Medical:Y2013:Deductible 2013-01-01 open Expenses:Health:Medical:Y2013:Copayments 2014-01-01 open Expenses:Health:Medical:Y2014:Deductible 2014-01-91 open Expenses:Health:Medical:Y2014:Copayments 2014-01-01 open Expenses:Health:Dental:Y2012:Deductible 2014-01-91 open Expenses:Health:Dental:Y2012:Copayments \u2026 Then when you book your expense, you could use the account corresponding to the year the service was received: 2014-01-08 * \"CityPT\" \"Copay for PT session w/ Rob on 12/28\" Liabilities:US:BofA:Credit-Card -25 USD Expenses:Health:Medical:Y2013:Copayments 25 USD This is not a nice solution however, due to the proliferation of accounts that need to get created for each year, and it still does not tell us how much of each service we\u2019ve consumed. We clearly need a better solution. How Health Care Insurance Payments Work \uf0c1 Let\u2019s first review how insurance payments work, as it will be needed to have a basic understanding of claims payments to correctly account for these costs. For each service, your insurance covers a portion of it and you normally pay the difference. The way that this works is that your doctor sends a bill to the insurance company and the insurance company either (a) responds to them directly with payment for the covered part of the service, or (b) the insurance company sends you a check and you are meant to sign it over to your doctor (usually after some annoying reminders for you to do so). Sometimes, but not always, your insurance company sends you a report of the doctor\u2019s claim. This document is called an explanation of benefits or \u201cEOB\u201d, and it details the portion of the service that the insurance paid for, the portion that you\u2019re responsible to pay for, and other amounts. Service providers that have no direct relationship with your insurance company will need this detail. This is a useful document for the purpose of accounting: make sure to keep a copy for yourself 1 . Here is a sample from an EOB, with the important areas highlighted: The example above exhibits the following amounts: Claims/Billed. How much the doctor asked the insurance for the service. Patient Savings. How much the service was reduced as a result of pre-established negotiations with the provider (for in-network providers). Applied to Deductible. How much of this service you need to cover yourself as part of this calendar year\u2019s deductible amount. You need to pay this. Copayments. The portion of the service not covered by the insurance. You have to pay this. Claims Payments. How much the insurance actually sent a check to the provider for. The terminology used on the EOB provided by your insurance company may vary slightly, but you should be able to find corresponding amounts easily. The doctor\u2019s billing dept. then sends you a bill for the uncovered portion 2 and you issue them a payment, by credit card, check or otherwise. Consider this a payable when the bill or EOB arrives at your door. All in all, there are four dates relevant to a claim: Date of Service. That is the date you visited the doctor\u2019s office, the date you received the service. This is the date relevant for accounting for deductibles and copayments. Date of Issue. The date that the claim was processed by the insurance. You may ignore this. Billing Date. The date the provider receive payments for its claim and issue you a bill for the remaining portion. We don\u2019t care much about this either. Date of Payment. The date you made the payment. This will automatically appear on your credit card or checking account statement, or if you paid cash, you need to enter this manually (as you do all cash transactions you care about). The most important date when you refer to any claim is the date of service . If you scan and file your EOBs to documents, it is wise to rename them to include this date in the filename. Provider Networks \uf0c1 In the USA, each medical professional (or \u201cservice provider\u201d) decides whether to maintain an established arrangement with each insurance company, depending on their rates of payment and on how much of a pain they are about paying up bills. When they do, they are considered an \u201cin-network provider\u201d by the insurance company. Otherwise they are considered an \u201cout-of-network provider\u201d and the proportion of the services that the insurance covers is much smaller. That\u2019s the only difference. The list of providers that an insurance has in their network\u2014and how difficult they are about refusing to pay bills\u2014is usually a major consideration in the selection of an insurance plan, for someone who has a choice. But because most employers compete on compensation benefits by paying for their employees\u2019 health care insurance costs, as well as the fact that they are usually able to negotiate better rates from insurance companies than an individual can because they represent a large pool of customers (the employees), the great majority of people with jobs end up choosing their employer\u2019s plan and then try to go to in-network providers. This is not always possible, however: your family doctor may decide to stop accepting your insurance as in-network during the course of the year, and you might prefer to maintain an established relationship with your doctor rather than switch, so you end up having to pay a bit more. The bottom line is that in a typical year, you usually use some services of both in-network and out-of-network professionals and you have to account for both of these costs. Accruing on Service Date - The Correct Way \uf0c1 Ideally, we would like to obtain balances for the following amounts: The amount of money that each service cost, regardless of who paid The amount of deductible and copayments that are used in each calendar year What we will do, is enter two entries: An entry for each EOB, at the date of service, for which we obtain a payable. An entry for each payments, that comes out of an Asset or Liability account. In-Network Providers \uf0c1 For in-network providers a typical entry looks like this: 2013-04-01 * \"DR RAPPOPORT\" \"Office visit\" ^anthem-claim-8765937424 Expenses:Health:Medical:Claims 225.00 USD Expenses:Health:Medical:PatientSavings -151.53 USD Liabilities:US:Accounts-Payable -10.00 USD ; Copay Expenses:Health:Medical:ClaimsPayment -63.47 USD Once you\u2019ve figured out which numbers to pull out of of these EOBs, it becomes simpler to enter others, because they are very regular and all look the same. Note that an in-network EOB will include a \u201cPatient Savings\u201d number. This is meant to reflect how much lower you\u2019re paying due to your insurance plan selection. (In practice, this is a bit of smoke and mirrors, because all the claims made by doctors are inflated to reflect the amount they actually get paid for, but I think the insurance likes to show these to you anyway.) I like to immediately reflect the missing portion to an \u201caccounts payable\u201d liability, which tells me I have this registered as an upcoming payment: 1973-04-27 open Liabilities:US:Accounts-Payable You can then eliminate this payable as the payment entry comes in through your credit card import: 2014-01-08 * \"Rappoport Medical Office\" ^anthem-claim-8765937424 Liabilities:US:BofA:Credit-Card -10.00 USD Liabilities:US:Accounts-Payable 10.00 USD Note how I \u201clink\u201d both of the transactions to each other with \u201c ^anthem-claim-8765937424 \u201d so they can easily be found and verified later on. For in-network doctors, because the payments from the insurance are generally predictable, the doctor\u2019s office will sometimes require the copayment on the same day you visit. Just book this as a cash entry against the same liability account. If you have a doctor that you visit regularly, you may create dedicated Liabilities accounts for them: 2009-09-17 open Liabilities:US:Accounts-Payable:DrRappoport Out-of-Network Providers \uf0c1 For out-of-network providers, the EOBs are slightly different: What we noticed is that there is an \u201cother amount\u201d section, which is essentially what will be written off by the provider, and the Patient Savings section is empty. A typical entry corresponding to this EOB would look like this: 2014-02-13 * \"ROB BOONAN\" | \"Physical Therapy\" ^anthem-claim-17646398 Expenses:Health:PhysicalTherapy:Claims 320.00 USD Expenses:Health:PhysicalTherapy:ClaimsPayment -115.14 USD Expenses:Health:PhysicalTherapy:Uncovered -155.52 USD Liabilities:US:Accounts-Payable:CityPT -49.34 USD You will typically receive the check for your service provider\u2019s payment (in the example, that is a check for $115.14) and have to send it over to them yourself. You can either deposit the check right away and make the full payment with your credit card, or send the check and just pay the difference (this is what most people do): 2014-01-08 * \"City PhysioTherapy\" ^anthem-claim-17646398 Liabilities:US:BofA:Credit-Card -49.34 USD Liabilities:US:Accounts-Payable:CityPT 49.34 USD One interesting twist is that many out-of-network providers will accept the lower amount that insurance companies pay for out-of-network services and write off the excess amount, or charge you nominal amount only. For example, the \u201cdeal\u201d with my favorite physical therapy place is that they charge me 25$ per session. Once again, the claim is inflated, of course, and the apparent amount the insurance says you have to pay has to be largely written off. This is how you\u2019d book such an entry\u2019s EOB: 2014-02-13 * \"ROB BOONAN\" | \"Physical Therapy\" ^anthem-claim-17646398 Expenses:Health:PhysicalTherapy:Claims 320.00 USD Expenses:Health:PhysicalTherapy:ClaimsPayment -115.14 USD Liabilities:US:Accounts-Payable:CityPT -25.00 USD Expenses:Health:PhysicalTherapy:WriteOffs The provider\u2019s administrator does not charge me on every visit. She accrues the amounts and whenever decides to process her accounting, she makes a charge for the total, which clears the account to zero. This might look like this: 2014-02-26 * \"Payment for 3 Physical Therapy treatments\" Liabilities:US:BofA:CreditCard -75.00 USD Liabilities:US:Accounts-Payable:CityPT Tracking Deductible and Copayment Limits \uf0c1 As we\u2019ve seen previously, there are limits on how much you end up paying for health care services. These limits are applied by calendar year, so they need to be applied at the date of service. For this reasons, we can attach corresponding entries to the EOB entries, and then filter transactions by year to calculate the balances for that year. I like to keep tabs on these limits and ensure that they are applied properly. According to my insurance plan, the limits are defined like this: Deductible. \u201cThe amount you pay out-of-pocket within a calendar year before your health insurance begins to pay for covered service.\u201d Out-of-pocket maximum. \u201cThe maximum amount of money you could pay out-of-pocket in a calendar year for covered health care services. There are exclusions, e.g. for prescription drugs and some services.\u201d Co-payment. \u201cA fixed fee that you pay out-of-pocket for a service. This fee does not vary according to the actual cost of the service.\u201d It is not straightforward, however, because there are distinct limits for in-network and out-of-network providers, both for deductible amounts and copayment amounts: Amounts paid for in-network deductibles count towards your limit for out-of-network deductibles. Amounts paid for in-network copayments count towards your limit for out-of-network copayments. Amounts paid for deductibles count towards your limit for copayments. I carry this out using the \u201cmirror accounting\u201d technique I describe in another document. The idea is to use an alternative currency to count for these amounts. TODO: complete this Insurance Premiums \uf0c1 Then you have to track how much you spend on your insurance premiums. These are the fixed semi-monthly payments you make\u2014usually directly processed out of your paycheck\u2014for getting the insurance policy itself. As for most people with jobs in the US, my employer offers a few health coverage plans and pays for most of it, but there is always a portion I have to pay myself: 1973-04-27 open Expenses:Health:Insurance A fixed premium is automatically deducted from my paycheck entries, as it appears on my pay stub: 2014-02-08 * \"Acme Corp - Salary\" \u2026 Expenses:Health:Insurance 42.45 USD \u2026 On your pay stub, be careful not to confuse this with the \u201cMedicare tax,\u201d which is a tax used to pay for the bit of socialized medicine costs the US has for older people. This is just a tax and has little to do with your own health care expenses. Drugs \uf0c1 Drugs are accounted for separately. TODO: complete this You do not always receive these through the mail, but insurance companies are now finally coming up with websites where you can download all the claims that were made on your behalf (and account for them), even those which weren\u2019t mailed to you. \u21a9 This is a tremendous annoyance because each doctor has a different billing department, they are typically antiquated and their online payment options are almost always broken (you end up having to call a phone number and convince a grumpy administrator to pay by credit card because they are still used to receiving checks ), and all the different service providers that collaborate together to offer you a health service will bill you separately (with different billing departments as well). For example, if you undergo surgery, over the following 6 months you can expect to get different bills from your surgeon, his assistant, the anesthesiologist, the person who monitored your brain activity while asleep, the lab who did the blood work, the person who read an X-ray, and so on. And the bills may be sent more than 6 months after the operation took place, well into the following year. It\u2019s completely Kafkaesque. You have to really wonder why the insurance company does not always pay the full service to the providers and then bill you , just once, for the deductibles. That would make the process a lot simpler and reduce the exhorbitant cost of administering health care. \u21a9","title":"Health Care Expenses"},{"location":"health_care_expenses.html#health-care-expenses","text":"Martin Blais , July 2014 This is incomplete, work-in-progress, not released yet. Accounting for your health care expenses is a little different than regular expenses because of the various maximums imposed by your insurance plan. For the purpose of this section, we will assume a context of privatized health care system as is present in the USA, but the same principles may apply to other countries (if anything, it will be simpler).","title":"Health Care Expenses"},{"location":"health_care_expenses.html#accounting-with-no-insurance-plan-the-naive-way","text":"So what\u2019s so different about health care expenses? You might argue that they should be treated the same as other expenses. Say, we could define a few categories like these: 1973-04-27 open Expenses:Health:Medical 1973-04-27 open Expenses:Health:Dental 1973-04-27 open Expenses:Health:Vision 1973-04-27 open Expenses:Health:Drugs 1973-04-27 open Expenses:Health:Acupuncture 1973-04-27 open Expenses:Health:Massage-Therapy 1973-04-27 open Expenses:Health:Physical-Therapy And simply book outflows of moneys to them when you spend on one of these categories: 2014-01-08 * \"CityPT\" \"Copay for PT session w/ Rob on 12/28\" Liabilities:US:BofA:Credit-Card -25 USD Expenses:Health:Physical-Therapy 25 USD In fact, this would be work just fine if you paid for all of your health care costs out-of-pocket. If that is your situation, this is what you should do. It\u2019s straightforward. The problem is that in practice, the great majority of people don\u2019t pay for health care costs themselves. The great majority of your health-related costs are paid for by your insurance. This does not mean that there are no costs: your insurance usually only pays for a portion of the actual expenses, depending on the service. In particular, depending on your insurance plan, in any calendar year you pay for 100% of your health care costs up to a fixed amount (usually a few hundred dollars). This is called the deductible amount. Heretofore, you pay a percentage of the service (the copayment amounts ) , and after a higher limit amount (the out-of-pocket maximum amount), you don\u2019t pay anything anymore; plans with such limits guarantee that you will never pay more than this amount. If you booked the amounts that you pay for your deductible, the scale of the expenses that would get reflected on your income statement would depend mostly on which type of service you happened to use first after January 1st. It would not accurately reflect the cost of each service that you use. It would not be that useful.","title":"Accounting With No Insurance Plan - The Naive Way"},{"location":"health_care_expenses.html#counting-the-cash-payments-the-incorrect-way","text":"So you might argue that you should instead book the same outflows of money to categories that reflect their true nature: 1973-04-27 open Expenses:Health:Medical:Deductible 1973-04-27 open Expenses:Health:Medical:Copayments 1973-04-27 open Expenses:Health:Vision:Deductible 1973-04-27 open Expenses:Health:Vision:Copayments 1973-04-27 open Expenses:Health:Dental:Deductible 1973-04-27 open Expenses:Health:Dental:Copayments Then booking the payments to those categories: 2014-01-08 * \"CityPT\" \"Copay for PT session w/ Rob on 12/28\" Liabilities:US:BofA:Credit-Card -25 USD Expenses:Health:Medical:Copayments 25 USD But this is largely unsatisfying, for a few reasons: while this tracks how much of was paid in total in deductible and copayments, it does not reflect anything about how much of a particular health provider services was used. More importantly, it is incorrect, because deductible and copayment limits apply to each calendar year for dates when the service was provided, not when it was paid for. In other words, if you received the service on December 28th, 2013 but paid for it the year following on January 8th, 2014, the copayment accrual is going towards the wrong year. In order to do this correctly, you must accrue the deductible and copayment amounts on the date of service , which is always provided on the documents your insurance company provides (the Explanation of Benefits , more later about this). The date of service is the date you actually were seen by the provider. Another solution would be to define accounts for each year, similarly to tax accounts (see chapter on Taxes): 2012-01-01 open Expenses:Health:Medical:Y2012:Deductible 2012-01-01 open Expenses:Health:Medical:Y2012:Copayments 2013-01-01 open Expenses:Health:Medical:Y2013:Deductible 2013-01-01 open Expenses:Health:Medical:Y2013:Copayments 2014-01-01 open Expenses:Health:Medical:Y2014:Deductible 2014-01-91 open Expenses:Health:Medical:Y2014:Copayments 2014-01-01 open Expenses:Health:Dental:Y2012:Deductible 2014-01-91 open Expenses:Health:Dental:Y2012:Copayments \u2026 Then when you book your expense, you could use the account corresponding to the year the service was received: 2014-01-08 * \"CityPT\" \"Copay for PT session w/ Rob on 12/28\" Liabilities:US:BofA:Credit-Card -25 USD Expenses:Health:Medical:Y2013:Copayments 25 USD This is not a nice solution however, due to the proliferation of accounts that need to get created for each year, and it still does not tell us how much of each service we\u2019ve consumed. We clearly need a better solution.","title":"Counting the Cash Payments - The Incorrect Way"},{"location":"health_care_expenses.html#how-health-care-insurance-payments-work","text":"Let\u2019s first review how insurance payments work, as it will be needed to have a basic understanding of claims payments to correctly account for these costs. For each service, your insurance covers a portion of it and you normally pay the difference. The way that this works is that your doctor sends a bill to the insurance company and the insurance company either (a) responds to them directly with payment for the covered part of the service, or (b) the insurance company sends you a check and you are meant to sign it over to your doctor (usually after some annoying reminders for you to do so). Sometimes, but not always, your insurance company sends you a report of the doctor\u2019s claim. This document is called an explanation of benefits or \u201cEOB\u201d, and it details the portion of the service that the insurance paid for, the portion that you\u2019re responsible to pay for, and other amounts. Service providers that have no direct relationship with your insurance company will need this detail. This is a useful document for the purpose of accounting: make sure to keep a copy for yourself 1 . Here is a sample from an EOB, with the important areas highlighted: The example above exhibits the following amounts: Claims/Billed. How much the doctor asked the insurance for the service. Patient Savings. How much the service was reduced as a result of pre-established negotiations with the provider (for in-network providers). Applied to Deductible. How much of this service you need to cover yourself as part of this calendar year\u2019s deductible amount. You need to pay this. Copayments. The portion of the service not covered by the insurance. You have to pay this. Claims Payments. How much the insurance actually sent a check to the provider for. The terminology used on the EOB provided by your insurance company may vary slightly, but you should be able to find corresponding amounts easily. The doctor\u2019s billing dept. then sends you a bill for the uncovered portion 2 and you issue them a payment, by credit card, check or otherwise. Consider this a payable when the bill or EOB arrives at your door. All in all, there are four dates relevant to a claim: Date of Service. That is the date you visited the doctor\u2019s office, the date you received the service. This is the date relevant for accounting for deductibles and copayments. Date of Issue. The date that the claim was processed by the insurance. You may ignore this. Billing Date. The date the provider receive payments for its claim and issue you a bill for the remaining portion. We don\u2019t care much about this either. Date of Payment. The date you made the payment. This will automatically appear on your credit card or checking account statement, or if you paid cash, you need to enter this manually (as you do all cash transactions you care about). The most important date when you refer to any claim is the date of service . If you scan and file your EOBs to documents, it is wise to rename them to include this date in the filename.","title":"How Health Care Insurance Payments Work"},{"location":"health_care_expenses.html#provider-networks","text":"In the USA, each medical professional (or \u201cservice provider\u201d) decides whether to maintain an established arrangement with each insurance company, depending on their rates of payment and on how much of a pain they are about paying up bills. When they do, they are considered an \u201cin-network provider\u201d by the insurance company. Otherwise they are considered an \u201cout-of-network provider\u201d and the proportion of the services that the insurance covers is much smaller. That\u2019s the only difference. The list of providers that an insurance has in their network\u2014and how difficult they are about refusing to pay bills\u2014is usually a major consideration in the selection of an insurance plan, for someone who has a choice. But because most employers compete on compensation benefits by paying for their employees\u2019 health care insurance costs, as well as the fact that they are usually able to negotiate better rates from insurance companies than an individual can because they represent a large pool of customers (the employees), the great majority of people with jobs end up choosing their employer\u2019s plan and then try to go to in-network providers. This is not always possible, however: your family doctor may decide to stop accepting your insurance as in-network during the course of the year, and you might prefer to maintain an established relationship with your doctor rather than switch, so you end up having to pay a bit more. The bottom line is that in a typical year, you usually use some services of both in-network and out-of-network professionals and you have to account for both of these costs.","title":"Provider Networks"},{"location":"health_care_expenses.html#accruing-on-service-date-the-correct-way","text":"Ideally, we would like to obtain balances for the following amounts: The amount of money that each service cost, regardless of who paid The amount of deductible and copayments that are used in each calendar year What we will do, is enter two entries: An entry for each EOB, at the date of service, for which we obtain a payable. An entry for each payments, that comes out of an Asset or Liability account.","title":"Accruing on Service Date - The Correct Way"},{"location":"health_care_expenses.html#in-network-providers","text":"For in-network providers a typical entry looks like this: 2013-04-01 * \"DR RAPPOPORT\" \"Office visit\" ^anthem-claim-8765937424 Expenses:Health:Medical:Claims 225.00 USD Expenses:Health:Medical:PatientSavings -151.53 USD Liabilities:US:Accounts-Payable -10.00 USD ; Copay Expenses:Health:Medical:ClaimsPayment -63.47 USD Once you\u2019ve figured out which numbers to pull out of of these EOBs, it becomes simpler to enter others, because they are very regular and all look the same. Note that an in-network EOB will include a \u201cPatient Savings\u201d number. This is meant to reflect how much lower you\u2019re paying due to your insurance plan selection. (In practice, this is a bit of smoke and mirrors, because all the claims made by doctors are inflated to reflect the amount they actually get paid for, but I think the insurance likes to show these to you anyway.) I like to immediately reflect the missing portion to an \u201caccounts payable\u201d liability, which tells me I have this registered as an upcoming payment: 1973-04-27 open Liabilities:US:Accounts-Payable You can then eliminate this payable as the payment entry comes in through your credit card import: 2014-01-08 * \"Rappoport Medical Office\" ^anthem-claim-8765937424 Liabilities:US:BofA:Credit-Card -10.00 USD Liabilities:US:Accounts-Payable 10.00 USD Note how I \u201clink\u201d both of the transactions to each other with \u201c ^anthem-claim-8765937424 \u201d so they can easily be found and verified later on. For in-network doctors, because the payments from the insurance are generally predictable, the doctor\u2019s office will sometimes require the copayment on the same day you visit. Just book this as a cash entry against the same liability account. If you have a doctor that you visit regularly, you may create dedicated Liabilities accounts for them: 2009-09-17 open Liabilities:US:Accounts-Payable:DrRappoport","title":"In-Network Providers"},{"location":"health_care_expenses.html#out-of-network-providers","text":"For out-of-network providers, the EOBs are slightly different: What we noticed is that there is an \u201cother amount\u201d section, which is essentially what will be written off by the provider, and the Patient Savings section is empty. A typical entry corresponding to this EOB would look like this: 2014-02-13 * \"ROB BOONAN\" | \"Physical Therapy\" ^anthem-claim-17646398 Expenses:Health:PhysicalTherapy:Claims 320.00 USD Expenses:Health:PhysicalTherapy:ClaimsPayment -115.14 USD Expenses:Health:PhysicalTherapy:Uncovered -155.52 USD Liabilities:US:Accounts-Payable:CityPT -49.34 USD You will typically receive the check for your service provider\u2019s payment (in the example, that is a check for $115.14) and have to send it over to them yourself. You can either deposit the check right away and make the full payment with your credit card, or send the check and just pay the difference (this is what most people do): 2014-01-08 * \"City PhysioTherapy\" ^anthem-claim-17646398 Liabilities:US:BofA:Credit-Card -49.34 USD Liabilities:US:Accounts-Payable:CityPT 49.34 USD One interesting twist is that many out-of-network providers will accept the lower amount that insurance companies pay for out-of-network services and write off the excess amount, or charge you nominal amount only. For example, the \u201cdeal\u201d with my favorite physical therapy place is that they charge me 25$ per session. Once again, the claim is inflated, of course, and the apparent amount the insurance says you have to pay has to be largely written off. This is how you\u2019d book such an entry\u2019s EOB: 2014-02-13 * \"ROB BOONAN\" | \"Physical Therapy\" ^anthem-claim-17646398 Expenses:Health:PhysicalTherapy:Claims 320.00 USD Expenses:Health:PhysicalTherapy:ClaimsPayment -115.14 USD Liabilities:US:Accounts-Payable:CityPT -25.00 USD Expenses:Health:PhysicalTherapy:WriteOffs The provider\u2019s administrator does not charge me on every visit. She accrues the amounts and whenever decides to process her accounting, she makes a charge for the total, which clears the account to zero. This might look like this: 2014-02-26 * \"Payment for 3 Physical Therapy treatments\" Liabilities:US:BofA:CreditCard -75.00 USD Liabilities:US:Accounts-Payable:CityPT","title":"Out-of-Network Providers"},{"location":"health_care_expenses.html#tracking-deductible-and-copayment-limits","text":"As we\u2019ve seen previously, there are limits on how much you end up paying for health care services. These limits are applied by calendar year, so they need to be applied at the date of service. For this reasons, we can attach corresponding entries to the EOB entries, and then filter transactions by year to calculate the balances for that year. I like to keep tabs on these limits and ensure that they are applied properly. According to my insurance plan, the limits are defined like this: Deductible. \u201cThe amount you pay out-of-pocket within a calendar year before your health insurance begins to pay for covered service.\u201d Out-of-pocket maximum. \u201cThe maximum amount of money you could pay out-of-pocket in a calendar year for covered health care services. There are exclusions, e.g. for prescription drugs and some services.\u201d Co-payment. \u201cA fixed fee that you pay out-of-pocket for a service. This fee does not vary according to the actual cost of the service.\u201d It is not straightforward, however, because there are distinct limits for in-network and out-of-network providers, both for deductible amounts and copayment amounts: Amounts paid for in-network deductibles count towards your limit for out-of-network deductibles. Amounts paid for in-network copayments count towards your limit for out-of-network copayments. Amounts paid for deductibles count towards your limit for copayments. I carry this out using the \u201cmirror accounting\u201d technique I describe in another document. The idea is to use an alternative currency to count for these amounts. TODO: complete this","title":"Tracking Deductible and Copayment Limits"},{"location":"health_care_expenses.html#insurance-premiums","text":"Then you have to track how much you spend on your insurance premiums. These are the fixed semi-monthly payments you make\u2014usually directly processed out of your paycheck\u2014for getting the insurance policy itself. As for most people with jobs in the US, my employer offers a few health coverage plans and pays for most of it, but there is always a portion I have to pay myself: 1973-04-27 open Expenses:Health:Insurance A fixed premium is automatically deducted from my paycheck entries, as it appears on my pay stub: 2014-02-08 * \"Acme Corp - Salary\" \u2026 Expenses:Health:Insurance 42.45 USD \u2026 On your pay stub, be careful not to confuse this with the \u201cMedicare tax,\u201d which is a tax used to pay for the bit of socialized medicine costs the US has for older people. This is just a tax and has little to do with your own health care expenses.","title":"Insurance Premiums"},{"location":"health_care_expenses.html#drugs","text":"Drugs are accounted for separately. TODO: complete this You do not always receive these through the mail, but insurance companies are now finally coming up with websites where you can download all the claims that were made on your behalf (and account for them), even those which weren\u2019t mailed to you. \u21a9 This is a tremendous annoyance because each doctor has a different billing department, they are typically antiquated and their online payment options are almost always broken (you end up having to call a phone number and convince a grumpy administrator to pay by credit card because they are still used to receiving checks ), and all the different service providers that collaborate together to offer you a health service will bill you separately (with different billing departments as well). For example, if you undergo surgery, over the following 6 months you can expect to get different bills from your surgeon, his assistant, the anesthesiologist, the person who monitored your brain activity while asleep, the lab who did the blood work, the person who read an X-ray, and so on. And the bills may be sent more than 6 months after the operation took place, well into the following year. It\u2019s completely Kafkaesque. You have to really wonder why the insurance company does not always pay the full service to the providers and then bill you , just once, for the deductibles. That would make the process a lot simpler and reduce the exhorbitant cost of administering health care. \u21a9","title":"Drugs"},{"location":"how_inventories_work.html","text":"How Inventories Work \uf0c1 Martin Blais , December 2016 http://furius.ca/beancount/doc/booking This document explains how we accumulate commodities and match sales (reductions) against accumulated inventory contents. Introduction \uf0c1 Beyond the ability to track and list the postings made to each of the accounts (an operation that produces a journal of entries), one of the most common and useful operations of Beancount is to sum up the positions of arbitrary sets of postings. These aggregations are at the heart of how Beancount works, and are implemented in an object called \u201cinventory.\u201d This document explains how this aggregation process works. If you\u2019re going to track investments, it\u2019s necessary to understand what follows. Matches & Booking Methods \uf0c1 In order to get the big picture, let\u2019s walk through the various booking features by way of a simple examples. This should expose you to all the main ideas in one go. Simple Postings \u2014 No Cost \uf0c1 Consider a set of simple postings to an account, e.g., 2016-04-24 * \"Deposit check\" Assets:Bank:Checking 221.23 USD \u2026 2016-04-29 * \"ATM Withdrawal\" Assets:Bank:Checking -100.00 USD \u2026 2016-04-29 * \"Debit card payment\" Assets:Bank:Checking -45.67 USD \u2026 The inventory of the Checking account begins empty. After the first transaction, its contents are 221.23 USD. After the second, 121.23 USD. Finally, the third transaction brings this balance to 75.56 USD. This seems very natural; the numbers simply add to. It might be obvious, but note also that the numbers are allowed to change the sign (go negative). Multiple Commodities \uf0c1 An inventory may contain more than one type of commodity. It is equivalent to a mapping from commodity to some number of units. For example, 2016-07-24 * \"Dinner before leaving NYC\" Expenses:Restaurants 34.58 USD \u2026 2016-07-26 * \"Food with friends after landing\" Expenses:Restaurants 62.11 CAD \u2026 After those two transactions, the Restaurants account contains 34.58 USD and 62.11 CAD. Its contents are said to be of mixed commodities. And naturally, postings are applied to just the currencies they affect. For instance, the following transaction 2016-07-27 * \"Brunch\" Expenses:Restaurants 23.91 CAD \u2026 brings the balance of that account to 34.58 USD and 86.02 CAD. The number of units USD hasn\u2019t changed. Note that accounts may contain any number of commodities, and this is also true for commodities held at cost, which we\u2019ll see shortly. While this is made possible, I recommend that you define enough accounts to keep a single commodity in each; this can be enforced with the \u201c onecommodity \u201d plugin. Cost Basis \uf0c1 Things get a little more hairy when we consider the tracking of investments with a cost basis. Beancount allows you to associate a cost basis and an optional label with a particular lot acquired. Consider these two purchases to an investment account: 2015-04-01 * \"Buy some shares of Hooli in April\" Assets:Invest 25 HOOL {23.00 USD, \"first-lot\"} \u2026 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest 35 HOOL {27.00 USD} \u2026 So now, the investment account\u2019s inventory contains units ccy cost cost-ccy lot-date label 25 HOOL {23.00 USD, 2015-04-01, \"first-lot\"} 35 HOOL {27.00 USD, 2015-05-01, None} Those two lots were not merged, they are still two distinct positions in the inventory. Inventories merge lots together and adjust the units only if the commodity and all of its cost attributes exactly match. (In practice, it\u2019s pretty rare that two augmentations will have the same cost and date attributes.) Note how Beancount automatically associated the acquisition date to each lot; you can override it if desired, by adding the date similar to the optional label. this is useful for making cost basis adjustments). Postings that add to the content of an inventory are called augmentations . Reductions \uf0c1 But how do we remove commodities from an inventory? You could eat away at an existing lot by selling some of it. You do this by posting a reduction to the account, like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {23.00 USD} \u2026 Just to be clear, what makes this posting a reduction is the mere fact that the sign (-) is opposite that of the balance of the account (+25) for that commodity. This posting tells Beancount to find all lots with a cost basis of 23.00 USD and remove 12 units from it. The resulting inventory will be units ccy cost cost-ccy lot-date label 13 HOOL {23.00 USD, 2015-04-01, \"first-lot\"} 35 HOOL {27.00 USD, 2015-05-01, None} Note how the first posting was reduced by 12 units. We didn\u2019t have to specify all of the lot\u2019s attributes, just the cost. We could have equivalently used the date to specify which lot to reduce: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {2015-04-01} \u2026 Or the label: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {\"first-lot\"} \u2026 Or a combination of these. Any combination of attributes will be matched against the inventory contents to find which lot to reduce. In fact, if the inventory happened to have just a single lot in it, you could reduce it like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {} \u2026 Ambiguous Matches \uf0c1 But what happens if multiple lots match the reduction? For example, with the previous inventory containing two lots, if you wrote your sale like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {} \u2026 Beancount wouldn\u2019t be able to figure out which lot needs to get reduced. We have an ambiguous match. Partially ambiguous matches are also possible. For example, if you have the following inventory to reduce from: units ccy cost cost-ccy lot-date label 25 HOOL {23.00 USD, 2015-04-01, None} 30 HOOL {25.00 USD, 2015-04-01, None} 35 HOOL {27.00 USD, 2015-05-01, None} And you attempted to reduce like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {2015-04-01} \u2026 The first two lots are selected as matches. Strict Booking \uf0c1 What does Beancount do with ambiguous matches? By default, it issues an error. More precisely, what happens is that Beancount invokes the booking method and it handles the ambiguous match depending on what it is set. The default booking method is \u201c STRICT \u201d and it just gives up and spits out an error, telling you you need to refine your input in order to disambiguate your inventory reduction. FIFO and LIFO Booking \uf0c1 Other booking methods are available. They can be configured using options, like this: option \"booking_method\" \"FIFO\" The \u201c FIFO \u201d method automatically selects the oldest of the matching lots up to the requested size of the reduction. For example, given our previous inventory: units ccy cost cost-ccy lot-date label 25 HOOL {23.00 USD, 2015-04-01, \"first-lot\"} 35 HOOL {27.00 USD, 2015-05-01, None} Attempting to reduce 28 shares like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -28 HOOL {} \u2026 would match both lots, completely reduce the first one to zero units and remove the remaining 3 units from the second lot, to result in the following inventory: units ccy cost cost-ccy lot-date label 32 HOOL {27.00 USD, 2015-05-01, None} The \u201cLIFO\u201d method works similarly, but consumes the youngest (latest) lots first, working its way backward in time to remove the volume. Per-account Booking Method \uf0c1 You don\u2019t have to make all accounts follow the same booking method; the option in the previous section sets the default method for all accounts. In order to override the booking method for a particular account, you can use an optional string option on the account\u2019s Open directive, like this: 2014-01-01 open Assets:Invest \"FIFO\" This allows you to treat different accounts with a different booking resolution. Total Matches \uf0c1 There is an exception to strict booking: if the entire inventory is being reduced by exactly the total number of units, it\u2019s clear that all the matching lots are to be selected and this is considered unambiguous, even under \u201cSTRICT\u201d booking. For example, under \u201cSTRICT\u201d booking, this reduction would empty up the previous inventory without raising an error, because there are 25 + 35 shares matching: 2015-05-15 * \"Sell all my shares\" Assets:Invest -60 HOOL {} \u2026 Average Booking \uf0c1 Retirement accounts created by government incentive programs (such as the 401k plan in the US or the RRSP in Canada) typically consist in pre-tax money. For these types of accounts, brokers usually disregard the calculation of cost basis because the taxation is to be made upon distributing money outside the account. These accounts are often managed to the extent that they are fully invested; therefore, fees are often taken as shares of the investment portfolio, priced on the day the fee is paid out. This makes it awkward to track the cost basis of individual lots. The correct way to deal with this is to treat the number of units and the cost basis of each commodity\u2019s entire set of lots separately. For example, the following two transactions: 2016-07-28 * \"Buy some shares of retirement fund\" Assets:Invest 45.0045 VBMPX {11.11 USD} \u2026 2016-10-12 * \"Buy some shares of retirement fund\" Assets:Invest 54.5951 VBMPX {10.99 USD} \u2026 Should result in a single lot with the total number of units and the averaged cost: units ccy cost cost-ccy lot-date label 99.5996 VBMPX {11.0442 USD, 2016-07-28, None} A fee taken in this account might look like this: 2016-12-30 * \"Quarterly management fee\" Assets:Invest -1.4154 VBMPX {10.59 USD} Expenses:Fees Even with negative units the number and cost get aggregated separately: units ccy cost cost-ccy lot-date label 98.1842 VBMPX {11.0508 USD, 2016-07-28, None} This feature isn\u2019t yet supported in Beancount; it\u2019s fairly tricky to implement, and will be the subject in a minor release in the future. No Booking \uf0c1 However, there is another way to deal with non-taxable accounts in the meantime: you can simply disable the booking. There is a booking method called \u201c NONE \u201d which implements a very liberal strategy which accepts any new lot.. New lots are always appended unconditionally to the inventory. Using this strategy on the transactions from the previous section would result in this inventory: units ccy cost cost-ccy lot-date label 45.0045 VBMPX {11.11 USD, 2016-07-28, None} 54.5951 VBMPX {10.99 USD, 2016-10-12, None} -1.4154 VBMPX {10.59 USD, 2016-12-30, None} Observe how the resulting inventory has a mix of signs; normally this is not allowed, but it is tolerated under this degenerate booking method. Note that under this method, the only meaningful values are the total number of units and the total or average cost amounts. The individual lots aren\u2019t really lots, they only represent the list of all postings made to that account. Note: If you are familiar with Ledger, this is the default and only booking method that it supports. Summary \uf0c1 In summary, here\u2019s what we presented in the walkthrough. Augmentations are never problematic; they always add a new position to an existing inventory. On the other hand, reductions may result in a few outcomes: Single match. Only one position matches the reduction; it is reduced. Total match. The total number of units requested matches the total number of units of the positions matched. These positions are reduced away. No match. None of the positions matches the reducing posting. An error is raised. Ambiguous matches. More than one position in the inventory matches the reducing posting; the booking method is invoked to handle this . There are a few booking methods available to handle the last case: STRICT. An error is raised. FIFO. Units from oldest (earliest) lots are selected until the reduction is complete. LIFO. Units from youngest (latest) lots are selected until the reduction is complete. AVERAGE. After every reduction, all the units of the affected commodity are merged and their new average cost is recalculated. NONE. Booking is disabled; the reducing lots is simply added to the inventory. This results in an inventory with mixed signs and only the total number of units and total cost basis are sensible numbers. Beancount has a default booking method for all accounts, which can be overridden with an option: option \"booking_method\" \"FIFO\" The default value for the booking method is \u201cSTRICT\u201d. I recommend that you leave it that way and override the booking method for specific accounts. The method can be specified for each account by adding a string to its Open directive: 2016-05-01 open Assets:Vanguard:RGAGX \"AVERAGE\" How Prices are Used \uf0c1 The short answer is that prices aren\u2019t used nor affect the booking algorithm at all. However, it is relevant to discuss what they do in this context because users invariably get confused about their interpretation. There are two use cases for prices: making conversions between commodities and tagging a reducing lot with its sale price in order to record it and optionally balance the proceeds. Commodity Conversions \uf0c1 Conversions are used to exchange one currency for another. They look like this: 2016-04-24 * \"Deposit check\" Assets:Bank:Checking 220.00 USD @ 1.3 CAD Income:Payment -286.00 CAD For the purpose of booking it against the Checking account\u2019s inventory, the posting with the price attached to it is treated just the same as if there was no price: the Checking account simply receives a deposit of 220.00 units of USD and will match against positions of commodity \u201cUSD\u201d. The price is used only to verify that the transaction balances and ensure the double-entry accounting rule is respected (220.00 x 1.3 CAD + -286.00 CAD = 0.00). It is otherwise ignored for the purpose of modifying the inventory contents. In a sense, after the postings have been applied to the account inventories, the price is forgotten and the inventory balance retains no memory of the deposit having occurred from a conversion. Price vs. Cost Basis \uf0c1 One might wonder how the price is used if there is a cost basis specification, like this: 2015-05-15 * \"Sell some shares\" Assets:Invest:HOOL -12 HOOL {23.00 USD} @ 24.70 USD Assets:Invest:Cash 296.40 USD Income:Invest:Gains The answer is often surprising to many users: the price is not used by the balancing algorithm if there is a cost basis; the cost basis is the number used to balance the postings. This is a useful property that allows us to compute capital gains automatically. In the previous example, the balance algorithm would sum up -12 x 23.00 + 296.40 = -20.40 USD, which is the capital gain, (24.70 - 23.00) * 12. It would complete the last posting and assign it this value. In general, the way that profits on sales are calculated is by weighing the proceedings, i.e., the cash deposits, against the cost basis of the sold lots, and this is sufficient to establish the gain difference. Also, if an augmenting posting happens to have a price annotation on it, it is also unused. The price is an annotation for your records. It remains attached to the Posting objects and if you want to make use of it somehow, you can always do that by writing some Python code. There are already two plugins which make use of this annotation: beancount.plugins.implicit_prices : This plugin takes the prices attached to the postings and automatically creates and inserts Price directives for each of them, in order to feed the global price database. beancount.plugins.sellgains : This plugin implements an additional balancing check: it uses the prices to compute the expected proceeds and weighs them against all the other postings of the transaction excluding any postings to Income accounts. In our example, it would check that (-12 x 24.70 + 296.40) = 0. This provides yet another means of verifying the correctness of your input. See the Trading with Beancount document for more details on this topic. Trades \uf0c1 The combination of acquiring some asset and selling it back is what we call a \u201ctrade.\u201d In Beancount we consider only assets with a cost basis to be the subject of trades. Since booking reductions against accumulated inventory contents happens during the booking process, this is where trades should be identified and recorded. As of now [Dec 2016], trade recording has not been implemented. Some prototypes for it have been tested previously and I believe it will be very easy to add in the near future. This will be documented here. Watch this space. The way trades will be implemented is by allowing the booking process to insert matching metadata with unique UUIDs on both the augmenting and reducing postings, in the stream of transactions. Functions and reports will be provided that are able to easily extract the pairs of postings for each reducing postings and filter those out in different ways. Ultimately, one should be able to extract a list of all trades to a table, with the acquisition and sale price, as well as other fees. Debugging Booking Issues \uf0c1 If you\u2019re experiencing difficulties in recording your sales due to the matching process, there are tools you can use to view an account\u2019s detailed inventory contents before and after applying a Transaction to it. To do this, you can use the bean-doctor command. You invoke the program providing it with the file and line number close to the Transaction you want to select, like this: bean-doctor context The resulting output will show the list of inventory contents of all affected accounts prior to the transaction being applied, including cost basis, acquisition date, and optional label fully rendered. Note that some failures are typically handled by throwing away an invalid Transaction\u2019s effects (but never quietly). From Emacs or VI, placing the cursor near a transaction and invoking the corresponding command is the easiest way to invoke the command, as it inserts the line number automatically. Appendix \uf0c1 The rest of this document delves into more technical details. You should feel free to ignore this entirely, it\u2019s not necessary reading to understand how Beancount works. Only bother if you\u2019re interested in the details. Data Representation \uf0c1 It is useful to know how positions are represented in an inventory object. A Position is essentially some number of units of a commodity with some optional information about its acquisition: Cost. Its per-unit acquisition cost (the \u201ccost basis\u201d). Date. The date at which the units were acquired. Label. Some user-specified label which can be used to refer to the lot). We often refer to these position objects as \u201clots\u201d or \u201clegs.\u201d Schematically, a position object looks like this: The structure of a Position object. There are two different types of positions, discussed in detail in the sections that follow: Simple positions. These are positions with no cost basis. The \u201ccost\u201d attribute is set to a null value. (\u201c None \u201d in Python.) Positions held at cost. These are positions with an associated cost basis and acquisition details. An Inventory is simply an accumulation of such positions, represented as a list. We sometimes talk of the ante-inventory to refer to the contents of the inventory before a transaction\u2019s postings have been applied to it, and the ex-inventory to the resulting inventory after they have been applied. A Posting is an object which is a superset of a position: in addition to units and cost, it has an associated account and an optional price attributes. If present, the price has the same type as units. It represents one of the legs of a transaction in the input. Postings imply positions, and these positions are added to inventories. We can say that a position is posted to an account. For more details on the internal data structures used in Beancount, please refer to the Design Doc which expands on this topic further. Why Booking is Not Simple \uf0c1 The complexity of the reduction process shows itself when we consider how to keep track of the cost basis of various lots. To demonstrate how this works, let us consider a simple example that we shall reuse in the different sections below: 25 shares are bought on 4/1 at $23/share. 35 shares are bought on 5/1 at $27/share. 30 shares are sold on 5/15; at that time, the price is $26/share. We\u2019ll ignore commissions for now, they don\u2019t introduce any additional complexity. In Beancount, this scenario would be recorded like this: 2015-04-01 * \"Buy some shares of Hooli in April\" Assets:Invest:HOOL 25 HOOL {23.00 USD} Assets:Invest:Cash -575.00 USD 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest:HOOL 35 HOOL {27.00 USD} Assets:Invest:Cash -945.00 USD 2015-05-15 * \"Sell 30 shares\" Assets:Invest:HOOL -30 HOOL {...} @ 26.00 USD Assets:Invest:Cash 780.00 USD Income:Invest:HOOL:Gains Now, the entire question revolves around which of the shares are selected to be sold. I\u2019ve rendered this input as a red ellipsis (\u201c\u2026\u201d). Whatever the user puts in that spot will be used to determine which lot we want to use. Whichever lot(s) we elect to be the ones sold will determine the amount of gains, because that is a function of the cost basis of those shares. This is why this matters. Augmentations vs. Reductions \uf0c1 The most important observation is that there are two distinct kinds of lot specifications which look very similar in the input but which are processed very differently. When we buy, as in the first transaction above, the {...} cost basis syntax provides Beancount with information about a new lot: 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest:HOOL 35 HOOL {27.00 USD} Assets:Invest:Cash -945.00 USD We call this an \u201caugmentation\u201d to the inventory, because it will simply add a new position to it. The cost basis that you provide is attached to this position and preserved through time, in the inventory. In addition, there are a few other pieces of data you can provide for an augmenting lot. Let\u2019s have a look at all the data that can be provided in the cost spec: Cost basis. This consists in per-unit and total cost numbers\u2014which are combined into a single per-unit number\u2014and a currency. Acquisition date. A lot has an acquisition date. By default, the date attached of its parent transaction will be set as its acquisition date automatically. You may override this date by providing one. This comes in handy to handle stock splits or wash sales and preserve the original acquisition date of the replacement shares, as we\u2019ll see later. Label. You can provide a unique label for it, so that you can more easily refer to it later on, when you sell some or all of it. Merge. An indicator (a flag) that the lot should be merged (this will be useful for average cost booking which will be implemented later). For an augmenting postings , these informations must be either provided or inferred automatically. They can be provided in any order: 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest:HOOL 35 HOOL {2015-04-25, 27.00 USD, \"hooli-123\"} Assets:Invest:Cash -945.00 USD If you omit the date, the date of the Transaction is attached to it. If you omit the cost, the rest of the postings must be filled in such that the cost amount can be inferred from them. Since the label is optional anyway, an unspecified label field is left as a null value. You might wonder why it is allowed to override the date of an augmentation; it is useful when making cost basis adjustments to preserve the original acquisition date of a posting: You remove the posting, and then replace it with its original date and a new cost basis. Now, when we sell those shares, we will refer to the posting as a \u201creducing\u201d posting , a \u201c reduction \u201d. Note that the terms \u201caugmenting\u201d and \u201creducing\u201d are just terminology I\u2019m came up with in the context of designing how Beancount processes inventories; they\u2019re not general accounting terms. It\u2019s a \u201creduction\u201d because we\u2019re removing shares from the inventory that has been accumulated up to the date of its transaction. For example, if we were to sell 30 shares from that lot of 35 shares, the input might look like this: 2015-05-15 * \"Sell 30 shares\" Assets:Invest:HOOL -30 HOOL {27.00 USD} @ 26.00 USD Assets:Invest:Cash 780.00 USD Income:Invest:HOOL:Gains While the input looks the same as on the augmenting posting, Beancount handles this quite differently: it looks at the state of the account\u2019s inventory before applying the transaction and finds all the positions that match the lot data you provided. It then uses the details of the matched lots as the cost basis information for the reducing posting. In this example, it would simply match all the positions which have a cost basis of $27.00. This example\u2019s inventory before the sale contains a single lot of 35 shares at $27.00, so there is a single position matching it and that lot is reduced by 30 shares and 5 shares remain. We\u2019ll see later what happens in the case of multiple lots matching the specification. Note that you could have provide other subsets of lot information to match against, like just providing the label, for example: 2015-05-15 * \"Sell 30 shares\" Assets:Invest:HOOL -30 HOOL {\"hooli-123\"} @ 26.00 USD Assets:Invest:Cash 780.00 USD Income:Invest:HOOL:Gains This is also a valid way to identify the particular lot you wish to reduce. If you had provided a date here, it would also only be used to match against the inventory contents, to disambiguate between lots acquired at different dates, not to attach the date anywhere. And furthermore, if there was a single lot in the inventory you could have also just provided just an empty cost basis spec like this: \u201c {} \u201d. The Booking Methods section below will delve into the detail of what happens when the matches are ambiguous. In summary: When you\u2019re adding something to an account\u2019s inventory (augmenting), the information you provide is used to create a new lot and is attached to it. When you\u2019re removing from an account\u2019s inventory (reducing), the information you provide is used to filter the inventory contents to select which of the lot(s) to reduce, and information from the selected lots is filled in. Homogeneous and Mixed Inventories \uf0c1 So far in the example and in the vast majority of the examples in the documentation, \u201caugmenting\u201d means adding a positive number of shares. But in Beancount many of the accounts normally have a negative balance, e.g., liabilities accounts. It\u2019s fair to ask if it makes sense to hold a negative balance of commodities held at cost. The answer is yes. These would correspond to \u201cshort\u201d positions. Most people are unlikely to be selling short, but Beancount inventories support it. How we define \u201caugmenting\u201d is in relation to the existing balance of lots of a particular commodity. For example, if an account\u2019s inventory contains the following positions: 25 HOOL {23.00 USD, 2016-04-01} 35 HOOL {27.00 USD, 2016-05-01} Then \u201cadding\u201d means a positive number of shares. On the other hand, if the account contains only short positions, like this: -20 HOOL {23.00 USD, 2016-04-15} -10 HOOL {27.00 USD, 2016-05-15} Then \u201cadding\u201d means a negative number of shares, and \u201creducing\u201d would be carried out by matching a positive number of shares against it. The two inventories portrayed above are homogeneous in units of HOOL, that is, all of the positions have the same sign. With of the most booking methods we will see further, Beancount makes it impossible to create a non-homogeneous, or \u201cmixed,\u201d inventory. But the \u201cNONE\u201d method allows it. A mixed inventory might have the following contents, for example: 25 HOOL {23.00 USD, 2016-04-01} -20 HOOL {23.00 USD, 2016-04-15} As you may intuit, the notion of \u201caugmenting\u201d or \u201creducing\u201d only makes sense for homogeneous inventories. Original Proposal \uf0c1 If you\u2019re interested in the design doc that led to this implementation, you can find the document here . I hope the resulting implementation is simple enough yet general.","title":"How Inventories Work"},{"location":"how_inventories_work.html#how-inventories-work","text":"Martin Blais , December 2016 http://furius.ca/beancount/doc/booking This document explains how we accumulate commodities and match sales (reductions) against accumulated inventory contents.","title":"How Inventories Work"},{"location":"how_inventories_work.html#introduction","text":"Beyond the ability to track and list the postings made to each of the accounts (an operation that produces a journal of entries), one of the most common and useful operations of Beancount is to sum up the positions of arbitrary sets of postings. These aggregations are at the heart of how Beancount works, and are implemented in an object called \u201cinventory.\u201d This document explains how this aggregation process works. If you\u2019re going to track investments, it\u2019s necessary to understand what follows.","title":"Introduction"},{"location":"how_inventories_work.html#matches-booking-methods","text":"In order to get the big picture, let\u2019s walk through the various booking features by way of a simple examples. This should expose you to all the main ideas in one go.","title":"Matches & Booking Methods"},{"location":"how_inventories_work.html#simple-postings-no-cost","text":"Consider a set of simple postings to an account, e.g., 2016-04-24 * \"Deposit check\" Assets:Bank:Checking 221.23 USD \u2026 2016-04-29 * \"ATM Withdrawal\" Assets:Bank:Checking -100.00 USD \u2026 2016-04-29 * \"Debit card payment\" Assets:Bank:Checking -45.67 USD \u2026 The inventory of the Checking account begins empty. After the first transaction, its contents are 221.23 USD. After the second, 121.23 USD. Finally, the third transaction brings this balance to 75.56 USD. This seems very natural; the numbers simply add to. It might be obvious, but note also that the numbers are allowed to change the sign (go negative).","title":"Simple Postings \u2014 No Cost"},{"location":"how_inventories_work.html#multiple-commodities","text":"An inventory may contain more than one type of commodity. It is equivalent to a mapping from commodity to some number of units. For example, 2016-07-24 * \"Dinner before leaving NYC\" Expenses:Restaurants 34.58 USD \u2026 2016-07-26 * \"Food with friends after landing\" Expenses:Restaurants 62.11 CAD \u2026 After those two transactions, the Restaurants account contains 34.58 USD and 62.11 CAD. Its contents are said to be of mixed commodities. And naturally, postings are applied to just the currencies they affect. For instance, the following transaction 2016-07-27 * \"Brunch\" Expenses:Restaurants 23.91 CAD \u2026 brings the balance of that account to 34.58 USD and 86.02 CAD. The number of units USD hasn\u2019t changed. Note that accounts may contain any number of commodities, and this is also true for commodities held at cost, which we\u2019ll see shortly. While this is made possible, I recommend that you define enough accounts to keep a single commodity in each; this can be enforced with the \u201c onecommodity \u201d plugin.","title":"Multiple Commodities"},{"location":"how_inventories_work.html#cost-basis","text":"Things get a little more hairy when we consider the tracking of investments with a cost basis. Beancount allows you to associate a cost basis and an optional label with a particular lot acquired. Consider these two purchases to an investment account: 2015-04-01 * \"Buy some shares of Hooli in April\" Assets:Invest 25 HOOL {23.00 USD, \"first-lot\"} \u2026 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest 35 HOOL {27.00 USD} \u2026 So now, the investment account\u2019s inventory contains units ccy cost cost-ccy lot-date label 25 HOOL {23.00 USD, 2015-04-01, \"first-lot\"} 35 HOOL {27.00 USD, 2015-05-01, None} Those two lots were not merged, they are still two distinct positions in the inventory. Inventories merge lots together and adjust the units only if the commodity and all of its cost attributes exactly match. (In practice, it\u2019s pretty rare that two augmentations will have the same cost and date attributes.) Note how Beancount automatically associated the acquisition date to each lot; you can override it if desired, by adding the date similar to the optional label. this is useful for making cost basis adjustments). Postings that add to the content of an inventory are called augmentations .","title":"Cost Basis"},{"location":"how_inventories_work.html#reductions","text":"But how do we remove commodities from an inventory? You could eat away at an existing lot by selling some of it. You do this by posting a reduction to the account, like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {23.00 USD} \u2026 Just to be clear, what makes this posting a reduction is the mere fact that the sign (-) is opposite that of the balance of the account (+25) for that commodity. This posting tells Beancount to find all lots with a cost basis of 23.00 USD and remove 12 units from it. The resulting inventory will be units ccy cost cost-ccy lot-date label 13 HOOL {23.00 USD, 2015-04-01, \"first-lot\"} 35 HOOL {27.00 USD, 2015-05-01, None} Note how the first posting was reduced by 12 units. We didn\u2019t have to specify all of the lot\u2019s attributes, just the cost. We could have equivalently used the date to specify which lot to reduce: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {2015-04-01} \u2026 Or the label: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {\"first-lot\"} \u2026 Or a combination of these. Any combination of attributes will be matched against the inventory contents to find which lot to reduce. In fact, if the inventory happened to have just a single lot in it, you could reduce it like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {} \u2026","title":"Reductions"},{"location":"how_inventories_work.html#ambiguous-matches","text":"But what happens if multiple lots match the reduction? For example, with the previous inventory containing two lots, if you wrote your sale like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {} \u2026 Beancount wouldn\u2019t be able to figure out which lot needs to get reduced. We have an ambiguous match. Partially ambiguous matches are also possible. For example, if you have the following inventory to reduce from: units ccy cost cost-ccy lot-date label 25 HOOL {23.00 USD, 2015-04-01, None} 30 HOOL {25.00 USD, 2015-04-01, None} 35 HOOL {27.00 USD, 2015-05-01, None} And you attempted to reduce like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {2015-04-01} \u2026 The first two lots are selected as matches.","title":"Ambiguous Matches"},{"location":"how_inventories_work.html#strict-booking","text":"What does Beancount do with ambiguous matches? By default, it issues an error. More precisely, what happens is that Beancount invokes the booking method and it handles the ambiguous match depending on what it is set. The default booking method is \u201c STRICT \u201d and it just gives up and spits out an error, telling you you need to refine your input in order to disambiguate your inventory reduction.","title":"Strict Booking"},{"location":"how_inventories_work.html#fifo-and-lifo-booking","text":"Other booking methods are available. They can be configured using options, like this: option \"booking_method\" \"FIFO\" The \u201c FIFO \u201d method automatically selects the oldest of the matching lots up to the requested size of the reduction. For example, given our previous inventory: units ccy cost cost-ccy lot-date label 25 HOOL {23.00 USD, 2015-04-01, \"first-lot\"} 35 HOOL {27.00 USD, 2015-05-01, None} Attempting to reduce 28 shares like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -28 HOOL {} \u2026 would match both lots, completely reduce the first one to zero units and remove the remaining 3 units from the second lot, to result in the following inventory: units ccy cost cost-ccy lot-date label 32 HOOL {27.00 USD, 2015-05-01, None} The \u201cLIFO\u201d method works similarly, but consumes the youngest (latest) lots first, working its way backward in time to remove the volume.","title":"FIFO and LIFO Booking"},{"location":"how_inventories_work.html#per-account-booking-method","text":"You don\u2019t have to make all accounts follow the same booking method; the option in the previous section sets the default method for all accounts. In order to override the booking method for a particular account, you can use an optional string option on the account\u2019s Open directive, like this: 2014-01-01 open Assets:Invest \"FIFO\" This allows you to treat different accounts with a different booking resolution.","title":"Per-account Booking Method"},{"location":"how_inventories_work.html#total-matches","text":"There is an exception to strict booking: if the entire inventory is being reduced by exactly the total number of units, it\u2019s clear that all the matching lots are to be selected and this is considered unambiguous, even under \u201cSTRICT\u201d booking. For example, under \u201cSTRICT\u201d booking, this reduction would empty up the previous inventory without raising an error, because there are 25 + 35 shares matching: 2015-05-15 * \"Sell all my shares\" Assets:Invest -60 HOOL {} \u2026","title":"Total Matches"},{"location":"how_inventories_work.html#average-booking","text":"Retirement accounts created by government incentive programs (such as the 401k plan in the US or the RRSP in Canada) typically consist in pre-tax money. For these types of accounts, brokers usually disregard the calculation of cost basis because the taxation is to be made upon distributing money outside the account. These accounts are often managed to the extent that they are fully invested; therefore, fees are often taken as shares of the investment portfolio, priced on the day the fee is paid out. This makes it awkward to track the cost basis of individual lots. The correct way to deal with this is to treat the number of units and the cost basis of each commodity\u2019s entire set of lots separately. For example, the following two transactions: 2016-07-28 * \"Buy some shares of retirement fund\" Assets:Invest 45.0045 VBMPX {11.11 USD} \u2026 2016-10-12 * \"Buy some shares of retirement fund\" Assets:Invest 54.5951 VBMPX {10.99 USD} \u2026 Should result in a single lot with the total number of units and the averaged cost: units ccy cost cost-ccy lot-date label 99.5996 VBMPX {11.0442 USD, 2016-07-28, None} A fee taken in this account might look like this: 2016-12-30 * \"Quarterly management fee\" Assets:Invest -1.4154 VBMPX {10.59 USD} Expenses:Fees Even with negative units the number and cost get aggregated separately: units ccy cost cost-ccy lot-date label 98.1842 VBMPX {11.0508 USD, 2016-07-28, None} This feature isn\u2019t yet supported in Beancount; it\u2019s fairly tricky to implement, and will be the subject in a minor release in the future.","title":"Average Booking"},{"location":"how_inventories_work.html#no-booking","text":"However, there is another way to deal with non-taxable accounts in the meantime: you can simply disable the booking. There is a booking method called \u201c NONE \u201d which implements a very liberal strategy which accepts any new lot.. New lots are always appended unconditionally to the inventory. Using this strategy on the transactions from the previous section would result in this inventory: units ccy cost cost-ccy lot-date label 45.0045 VBMPX {11.11 USD, 2016-07-28, None} 54.5951 VBMPX {10.99 USD, 2016-10-12, None} -1.4154 VBMPX {10.59 USD, 2016-12-30, None} Observe how the resulting inventory has a mix of signs; normally this is not allowed, but it is tolerated under this degenerate booking method. Note that under this method, the only meaningful values are the total number of units and the total or average cost amounts. The individual lots aren\u2019t really lots, they only represent the list of all postings made to that account. Note: If you are familiar with Ledger, this is the default and only booking method that it supports.","title":"No Booking"},{"location":"how_inventories_work.html#summary","text":"In summary, here\u2019s what we presented in the walkthrough. Augmentations are never problematic; they always add a new position to an existing inventory. On the other hand, reductions may result in a few outcomes: Single match. Only one position matches the reduction; it is reduced. Total match. The total number of units requested matches the total number of units of the positions matched. These positions are reduced away. No match. None of the positions matches the reducing posting. An error is raised. Ambiguous matches. More than one position in the inventory matches the reducing posting; the booking method is invoked to handle this . There are a few booking methods available to handle the last case: STRICT. An error is raised. FIFO. Units from oldest (earliest) lots are selected until the reduction is complete. LIFO. Units from youngest (latest) lots are selected until the reduction is complete. AVERAGE. After every reduction, all the units of the affected commodity are merged and their new average cost is recalculated. NONE. Booking is disabled; the reducing lots is simply added to the inventory. This results in an inventory with mixed signs and only the total number of units and total cost basis are sensible numbers. Beancount has a default booking method for all accounts, which can be overridden with an option: option \"booking_method\" \"FIFO\" The default value for the booking method is \u201cSTRICT\u201d. I recommend that you leave it that way and override the booking method for specific accounts. The method can be specified for each account by adding a string to its Open directive: 2016-05-01 open Assets:Vanguard:RGAGX \"AVERAGE\"","title":"Summary"},{"location":"how_inventories_work.html#how-prices-are-used","text":"The short answer is that prices aren\u2019t used nor affect the booking algorithm at all. However, it is relevant to discuss what they do in this context because users invariably get confused about their interpretation. There are two use cases for prices: making conversions between commodities and tagging a reducing lot with its sale price in order to record it and optionally balance the proceeds.","title":"How Prices are Used"},{"location":"how_inventories_work.html#commodity-conversions","text":"Conversions are used to exchange one currency for another. They look like this: 2016-04-24 * \"Deposit check\" Assets:Bank:Checking 220.00 USD @ 1.3 CAD Income:Payment -286.00 CAD For the purpose of booking it against the Checking account\u2019s inventory, the posting with the price attached to it is treated just the same as if there was no price: the Checking account simply receives a deposit of 220.00 units of USD and will match against positions of commodity \u201cUSD\u201d. The price is used only to verify that the transaction balances and ensure the double-entry accounting rule is respected (220.00 x 1.3 CAD + -286.00 CAD = 0.00). It is otherwise ignored for the purpose of modifying the inventory contents. In a sense, after the postings have been applied to the account inventories, the price is forgotten and the inventory balance retains no memory of the deposit having occurred from a conversion.","title":"Commodity Conversions"},{"location":"how_inventories_work.html#price-vs-cost-basis","text":"One might wonder how the price is used if there is a cost basis specification, like this: 2015-05-15 * \"Sell some shares\" Assets:Invest:HOOL -12 HOOL {23.00 USD} @ 24.70 USD Assets:Invest:Cash 296.40 USD Income:Invest:Gains The answer is often surprising to many users: the price is not used by the balancing algorithm if there is a cost basis; the cost basis is the number used to balance the postings. This is a useful property that allows us to compute capital gains automatically. In the previous example, the balance algorithm would sum up -12 x 23.00 + 296.40 = -20.40 USD, which is the capital gain, (24.70 - 23.00) * 12. It would complete the last posting and assign it this value. In general, the way that profits on sales are calculated is by weighing the proceedings, i.e., the cash deposits, against the cost basis of the sold lots, and this is sufficient to establish the gain difference. Also, if an augmenting posting happens to have a price annotation on it, it is also unused. The price is an annotation for your records. It remains attached to the Posting objects and if you want to make use of it somehow, you can always do that by writing some Python code. There are already two plugins which make use of this annotation: beancount.plugins.implicit_prices : This plugin takes the prices attached to the postings and automatically creates and inserts Price directives for each of them, in order to feed the global price database. beancount.plugins.sellgains : This plugin implements an additional balancing check: it uses the prices to compute the expected proceeds and weighs them against all the other postings of the transaction excluding any postings to Income accounts. In our example, it would check that (-12 x 24.70 + 296.40) = 0. This provides yet another means of verifying the correctness of your input. See the Trading with Beancount document for more details on this topic.","title":"Price vs. Cost Basis"},{"location":"how_inventories_work.html#trades","text":"The combination of acquiring some asset and selling it back is what we call a \u201ctrade.\u201d In Beancount we consider only assets with a cost basis to be the subject of trades. Since booking reductions against accumulated inventory contents happens during the booking process, this is where trades should be identified and recorded. As of now [Dec 2016], trade recording has not been implemented. Some prototypes for it have been tested previously and I believe it will be very easy to add in the near future. This will be documented here. Watch this space. The way trades will be implemented is by allowing the booking process to insert matching metadata with unique UUIDs on both the augmenting and reducing postings, in the stream of transactions. Functions and reports will be provided that are able to easily extract the pairs of postings for each reducing postings and filter those out in different ways. Ultimately, one should be able to extract a list of all trades to a table, with the acquisition and sale price, as well as other fees.","title":"Trades"},{"location":"how_inventories_work.html#debugging-booking-issues","text":"If you\u2019re experiencing difficulties in recording your sales due to the matching process, there are tools you can use to view an account\u2019s detailed inventory contents before and after applying a Transaction to it. To do this, you can use the bean-doctor command. You invoke the program providing it with the file and line number close to the Transaction you want to select, like this: bean-doctor context The resulting output will show the list of inventory contents of all affected accounts prior to the transaction being applied, including cost basis, acquisition date, and optional label fully rendered. Note that some failures are typically handled by throwing away an invalid Transaction\u2019s effects (but never quietly). From Emacs or VI, placing the cursor near a transaction and invoking the corresponding command is the easiest way to invoke the command, as it inserts the line number automatically.","title":"Debugging Booking Issues"},{"location":"how_inventories_work.html#appendix","text":"The rest of this document delves into more technical details. You should feel free to ignore this entirely, it\u2019s not necessary reading to understand how Beancount works. Only bother if you\u2019re interested in the details.","title":"Appendix"},{"location":"how_inventories_work.html#data-representation","text":"It is useful to know how positions are represented in an inventory object. A Position is essentially some number of units of a commodity with some optional information about its acquisition: Cost. Its per-unit acquisition cost (the \u201ccost basis\u201d). Date. The date at which the units were acquired. Label. Some user-specified label which can be used to refer to the lot). We often refer to these position objects as \u201clots\u201d or \u201clegs.\u201d Schematically, a position object looks like this: The structure of a Position object. There are two different types of positions, discussed in detail in the sections that follow: Simple positions. These are positions with no cost basis. The \u201ccost\u201d attribute is set to a null value. (\u201c None \u201d in Python.) Positions held at cost. These are positions with an associated cost basis and acquisition details. An Inventory is simply an accumulation of such positions, represented as a list. We sometimes talk of the ante-inventory to refer to the contents of the inventory before a transaction\u2019s postings have been applied to it, and the ex-inventory to the resulting inventory after they have been applied. A Posting is an object which is a superset of a position: in addition to units and cost, it has an associated account and an optional price attributes. If present, the price has the same type as units. It represents one of the legs of a transaction in the input. Postings imply positions, and these positions are added to inventories. We can say that a position is posted to an account. For more details on the internal data structures used in Beancount, please refer to the Design Doc which expands on this topic further.","title":"Data Representation"},{"location":"how_inventories_work.html#why-booking-is-not-simple","text":"The complexity of the reduction process shows itself when we consider how to keep track of the cost basis of various lots. To demonstrate how this works, let us consider a simple example that we shall reuse in the different sections below: 25 shares are bought on 4/1 at $23/share. 35 shares are bought on 5/1 at $27/share. 30 shares are sold on 5/15; at that time, the price is $26/share. We\u2019ll ignore commissions for now, they don\u2019t introduce any additional complexity. In Beancount, this scenario would be recorded like this: 2015-04-01 * \"Buy some shares of Hooli in April\" Assets:Invest:HOOL 25 HOOL {23.00 USD} Assets:Invest:Cash -575.00 USD 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest:HOOL 35 HOOL {27.00 USD} Assets:Invest:Cash -945.00 USD 2015-05-15 * \"Sell 30 shares\" Assets:Invest:HOOL -30 HOOL {...} @ 26.00 USD Assets:Invest:Cash 780.00 USD Income:Invest:HOOL:Gains Now, the entire question revolves around which of the shares are selected to be sold. I\u2019ve rendered this input as a red ellipsis (\u201c\u2026\u201d). Whatever the user puts in that spot will be used to determine which lot we want to use. Whichever lot(s) we elect to be the ones sold will determine the amount of gains, because that is a function of the cost basis of those shares. This is why this matters.","title":"Why Booking is Not Simple"},{"location":"how_inventories_work.html#augmentations-vs-reductions","text":"The most important observation is that there are two distinct kinds of lot specifications which look very similar in the input but which are processed very differently. When we buy, as in the first transaction above, the {...} cost basis syntax provides Beancount with information about a new lot: 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest:HOOL 35 HOOL {27.00 USD} Assets:Invest:Cash -945.00 USD We call this an \u201caugmentation\u201d to the inventory, because it will simply add a new position to it. The cost basis that you provide is attached to this position and preserved through time, in the inventory. In addition, there are a few other pieces of data you can provide for an augmenting lot. Let\u2019s have a look at all the data that can be provided in the cost spec: Cost basis. This consists in per-unit and total cost numbers\u2014which are combined into a single per-unit number\u2014and a currency. Acquisition date. A lot has an acquisition date. By default, the date attached of its parent transaction will be set as its acquisition date automatically. You may override this date by providing one. This comes in handy to handle stock splits or wash sales and preserve the original acquisition date of the replacement shares, as we\u2019ll see later. Label. You can provide a unique label for it, so that you can more easily refer to it later on, when you sell some or all of it. Merge. An indicator (a flag) that the lot should be merged (this will be useful for average cost booking which will be implemented later). For an augmenting postings , these informations must be either provided or inferred automatically. They can be provided in any order: 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest:HOOL 35 HOOL {2015-04-25, 27.00 USD, \"hooli-123\"} Assets:Invest:Cash -945.00 USD If you omit the date, the date of the Transaction is attached to it. If you omit the cost, the rest of the postings must be filled in such that the cost amount can be inferred from them. Since the label is optional anyway, an unspecified label field is left as a null value. You might wonder why it is allowed to override the date of an augmentation; it is useful when making cost basis adjustments to preserve the original acquisition date of a posting: You remove the posting, and then replace it with its original date and a new cost basis. Now, when we sell those shares, we will refer to the posting as a \u201creducing\u201d posting , a \u201c reduction \u201d. Note that the terms \u201caugmenting\u201d and \u201creducing\u201d are just terminology I\u2019m came up with in the context of designing how Beancount processes inventories; they\u2019re not general accounting terms. It\u2019s a \u201creduction\u201d because we\u2019re removing shares from the inventory that has been accumulated up to the date of its transaction. For example, if we were to sell 30 shares from that lot of 35 shares, the input might look like this: 2015-05-15 * \"Sell 30 shares\" Assets:Invest:HOOL -30 HOOL {27.00 USD} @ 26.00 USD Assets:Invest:Cash 780.00 USD Income:Invest:HOOL:Gains While the input looks the same as on the augmenting posting, Beancount handles this quite differently: it looks at the state of the account\u2019s inventory before applying the transaction and finds all the positions that match the lot data you provided. It then uses the details of the matched lots as the cost basis information for the reducing posting. In this example, it would simply match all the positions which have a cost basis of $27.00. This example\u2019s inventory before the sale contains a single lot of 35 shares at $27.00, so there is a single position matching it and that lot is reduced by 30 shares and 5 shares remain. We\u2019ll see later what happens in the case of multiple lots matching the specification. Note that you could have provide other subsets of lot information to match against, like just providing the label, for example: 2015-05-15 * \"Sell 30 shares\" Assets:Invest:HOOL -30 HOOL {\"hooli-123\"} @ 26.00 USD Assets:Invest:Cash 780.00 USD Income:Invest:HOOL:Gains This is also a valid way to identify the particular lot you wish to reduce. If you had provided a date here, it would also only be used to match against the inventory contents, to disambiguate between lots acquired at different dates, not to attach the date anywhere. And furthermore, if there was a single lot in the inventory you could have also just provided just an empty cost basis spec like this: \u201c {} \u201d. The Booking Methods section below will delve into the detail of what happens when the matches are ambiguous. In summary: When you\u2019re adding something to an account\u2019s inventory (augmenting), the information you provide is used to create a new lot and is attached to it. When you\u2019re removing from an account\u2019s inventory (reducing), the information you provide is used to filter the inventory contents to select which of the lot(s) to reduce, and information from the selected lots is filled in.","title":"Augmentations vs. Reductions"},{"location":"how_inventories_work.html#homogeneous-and-mixed-inventories","text":"So far in the example and in the vast majority of the examples in the documentation, \u201caugmenting\u201d means adding a positive number of shares. But in Beancount many of the accounts normally have a negative balance, e.g., liabilities accounts. It\u2019s fair to ask if it makes sense to hold a negative balance of commodities held at cost. The answer is yes. These would correspond to \u201cshort\u201d positions. Most people are unlikely to be selling short, but Beancount inventories support it. How we define \u201caugmenting\u201d is in relation to the existing balance of lots of a particular commodity. For example, if an account\u2019s inventory contains the following positions: 25 HOOL {23.00 USD, 2016-04-01} 35 HOOL {27.00 USD, 2016-05-01} Then \u201cadding\u201d means a positive number of shares. On the other hand, if the account contains only short positions, like this: -20 HOOL {23.00 USD, 2016-04-15} -10 HOOL {27.00 USD, 2016-05-15} Then \u201cadding\u201d means a negative number of shares, and \u201creducing\u201d would be carried out by matching a positive number of shares against it. The two inventories portrayed above are homogeneous in units of HOOL, that is, all of the positions have the same sign. With of the most booking methods we will see further, Beancount makes it impossible to create a non-homogeneous, or \u201cmixed,\u201d inventory. But the \u201cNONE\u201d method allows it. A mixed inventory might have the following contents, for example: 25 HOOL {23.00 USD, 2016-04-01} -20 HOOL {23.00 USD, 2016-04-15} As you may intuit, the notion of \u201caugmenting\u201d or \u201creducing\u201d only makes sense for homogeneous inventories.","title":"Homogeneous and Mixed Inventories"},{"location":"how_inventories_work.html#original-proposal","text":"If you\u2019re interested in the design doc that led to this implementation, you can find the document here . I hope the resulting implementation is simple enough yet general.","title":"Original Proposal"},{"location":"how_we_share_expenses.html","text":"How We Share Expenses \uf0c1 This document explains how I share expenses with my wife. This is a bit involved and I\u2019ve developed a good working system, but it\u2019s not so simple for most people to do that, so I figured I would take the time to describe it to help others with designing similar processes for themselves. Context \uf0c1 We\u2019re both working professionals and have decided to share all expenses roughly 70%/30%. This is what we shoot for, this is not a hard rule, but we track it as if it were rigid, and accept it as a good approximation. We have two types of shared expenses: Shared. Common expenses between us, e.g., rent, a dinner out with friends, etc. Kyle. Expenses for our child, e.g., daycare, diapers, nanny, baby food. These get handled very differently, because we book our child's expenses as if it were a separate project on its own. (If you prefer pictures, there\u2019s a diagram at the end of this document that provides an overview of the system.) Shared Expenses \uf0c1 For our shared expenses, I maintain an account for her on my personal ledger file. This is simple and I\u2019m not too interested in maintaining a separate ledger for the totality of our common shared expenses. It\u2019s sufficient for me to just keep track of her balance on that one account. You can imagine that account like a credit card (I\u2019m the credit provider) that she pays off with transfers and also by making her own shared expenses. I just declare an Assets:US:Share:Carolyn account on my personal ledger ( blais.beancount ). My Shared Expenses \uf0c1 Whenever I incur an expense that is for both of us, I book it normally but I will tag it with #carolyn: 2018-12-23 * \"WHISK\" \"Water refill\" #carolyn Liabilities:US:Amex:BlueCash -32.66 USD Expenses:Food:Grocery This gets automatically converted to: 2018-12-23 * \"WHISK\" \"Water refill\" #carolyn Liabilities:US:Amex:BlueCash -32.66 USD Expenses:Food:Grocery 19.60 USD Assets:US:Share:Carolyn 13.06 USD share: TRUE This is done by a custom plugin I built that splits the expenses according to some rules that we have between us (see also this plugin by Akkukis ). In this example, 40% of 32.66 (13.06) gets rerouted to her account. Note that this is an asset account for me, because she owes this. Her Shared Expenses \uf0c1 We also have to keep track of the money she spends on her own for shared expenses. Since she\u2019s not a Beancount user, I\u2019ve set up a Google Sheets doc in which she can add rows to a particular sheet. This sheet has fields: Date, Description, Account, Amount. I try to keep it simple. Then, I built an extract_sheets.py script that can pull down this data automatically and it writes it to a dedicated file for this, overwriting the entire contents each time. The contents of this ledger ( carolyn.beancount ) look like this: pushtag #carolyn ... 2017-05-21 * \"Amazon\" \"Cotton Mattress Protector in White\" Expenses:Home:Furniture 199.99 USD Assets:US:Share:Carolyn -199.99 USD ... poptag #carolyn All of those transactions are tagged as #carolyn from the pushtag/poptag directive. They get translated by the plugin to reduce the amount by the portion I\u2019m supposed to pay: 2017-05-21 * \"Amazon\" \"Cotton Mattress Protector in White\" #carolyn Expenses:Home:Furniture 119.99 USD Assets:US:Share:Carolyn -119.99 USD share: TRUE These postings typically reduce her asset account by that much, and thus remove the portion she paid for me on these shared expenses. I generate the file with a single command like this: extract_sheets.py --tag='carolyn' --sheet='Shared Expenses' '' 'Assets:US:Share:Carolyn' > carolyn.beancount In my personal ledger, I include this file to merge those expenses with my own directives flow. I also define a query to generate a statement for her account. In blais.beancount : include \"carolyn.beancount\" 2020-01-01 query \"carolyn\" \" select date, description, position, balance from open on 2017-01-01 where account ~ 'Assets:US:Share:Carolyn' \" Reviewing & Statement \uf0c1 In order to produce a statement for her to review (and spot the occasional mistake in data entry), I simply produce a journal of that account to a CSV file and upload that to another sheet in the same Google Sheets doc she inputs her expenses: bean-query -f csv -o carolyn.csv --numberify $L run carolyn upload-to-sheets -v --docid=\"\" carolyn.csv:\"Shared Account (Read-Only)\" This makes it easy for her to eyeball all the amounts posted to her balance with me and point out errors if they occur (some always appear). Moreover, all the information is in one place\u2014it\u2019s important to keep it simple. Finally, we can use the collaborative features of Sheets to communicate, e.g. comments, highlighting text, etc. Note that this system has the benefit of correctly accruing my expenses, by reducing my portion on categories for the stuff I pay and by including her portion on categories for the stuff she pays for. Reconciling our Shared Expenses \uf0c1 Finally, in order to reconcile this account, my wife (or I, but usually she\u2019s behind) just makes a bank transfer to my account, which I book to reduce her running account balance: 2019-01-30 * \"HERBANK EXT TRNSFR; DIRECTDEP\" Assets:US:MyBank:Checking 3000 USD Assets:US:Share:Carolyn Typically she'll do this every month or two. She'll be fiddling on her laptop and ask casually \"Hey, what's my balance I can do a transfer now?\" It\u2019s all fine to relax about the particulars since the system is keeping track of everything precisely, so she can send some approximate amount, it doesn't matter, it'll post to her account. Child Expenses \uf0c1 I designed a very different system to track our child\u2019s expenses. For Kyle, I\u2019m definitely interested in tracking the total cash flows and expenses related to him, regardless of who paid for them. It\u2019s interesting to be able to ask (our ledger) a question like: \u201cHow much did his schooling cost?\u201d, for example, or \u201cHow much did we pay in diapers, in total?\u201d. Furthermore, we tend to pay for different things for Kyle, e.g. I deal with the daycare expenses (I\u2019m the accounting nerd after all, so this shouldn\u2019t be surprising), and his mother tends to buy all the clothing and prepare his food. To have a global picture of all costs related to him, we need to account for these things correctly. One interesting detail is that it would be difficult to do this with the previously described method, because I\u2019d have to have a mirror of all expense accounts I\u2019d use for him. This would make my personal ledger really ugly. For example, I want to book diapers to Expenses:Pharmacy , but I also have my own personal pharmacy expenses. So in theory, to do that I\u2019d like to have separate accounts for him and me, e.g., Expenses:Kyle:Pharmacy and Expenses:Pharmacy . This would have to be done for all the accounts we use for him. I don't do that. My Child Expenses on my Personal Ledger \uf0c1 Instead of doing that, what I want is for my personal ledger to book all the expenses I make for him to a single category: Expenses:Kyle , and to track all the detail in a shared ledger. But I still have to book all the expenses to some category, and these appear on my personal ledger, there\u2019s no option (I won\u2019t maintain a separate credit card to pay for his expenses, that would be overkill, so I have to find a way). I accomplish this by booking the expenses to my own expenses account, as usual, but tagging the transaction with #kyle: 2019-02-01 * \"AMAZON.COM\" \"MERCHANDISE - Diapers size 4 for Kyle\" #kyle Liabilities:US:Amex:BlueCash -49.99 USD Expenses:Pharmacy And I have a different plugin that automatically makes the conversion of those transactions to: 2019-02-01 * \"AMAZON.COM\" \"MERCHANDISE - Diapers size 4 for Kyle\" #kyle Liabilities:US:Amex:BlueCash -49.99 USD Expenses:Kyle 49.99 USD diverted_account: \"Expenses:Pharmacy\" So from my personal side, all those expenses get booked to my \u201cKyle project\u201d account. This is accomplished by the divert_expenses plugin, with this configuration: plugin \"beancount.plugins.divert_expenses\" \"{ 'tag': 'kyle', 'account': 'Expenses:Kyle' }\" The \u201cdiverted_account\u201d metadata is used to keep track of the original account, and this is used later by another script that generates a ledger file decided to my expenses for him (more below). My Child Expenses in Kyle\u2019s own Ledger \uf0c1 Now, because we\u2019re considering Kyle\u2019s expenses a project of his own, I have to maintain a set of ledgers for him. I automatically pull the transactions I described in the previous section from my personal ledger and automatically convert them to a file dedicated to his dad (me). This is done by calling the extract_tagged script: extract_tagged.py blais.beancount '#kyle' 'Income:Dad' --translate \"Expenses:Kyle:Mom=Income:Mom\" > dad.beancount The matching transaction from the previous section would look like this in it: 2019-02-01 * \"AMAZON.COM\" \"MERCHANDISE - Diapers size 4 for Kyle\" #kyle Income:Dad -49.99 USD Expenses:Pharmacy 49.99 USD As you can see, the script was able to reconstruct the original account name in Kyle\u2019s ledger by using the metadata saved by the divert_expenses plugin in the previous section. It also books the source of the payment to a single account showing it came from his father ( Income:Dad ). There\u2019s no need for me to keep track of which payment method I used on Kyle\u2019s side (e.g. by credit card), that\u2019s not important to him. This ledger contains the sum total of all expenses I\u2019ve made for him to date. The file gets entirely overwritten each time I run this (this is a purely generated output, no hand editing is ever done here, if I change anything I change it in my personal ledger file). Her Child Expenses \uf0c1 In order to keep track of her expenses for Kyle, we use the same method (and programs) as we use for our shared accounts in order to pull a set of \u201cCarolyn\u2019s expenses for Kyle\u201d from another sheet in the same Google Sheets doc: extract_sheets.py --sheet='Kyle Expenses (Regular)' '' 'Income:Mom' > mom.beancount This pulls in transactions that look like this: 2018-09-23 * \"SPROUT SAN FRANCISCO\" \"Clothing for Kyle 9-12 months\" Expenses:Clothing 118.30 USD Income:Mom -118.30 USD The expenses accounts are pulled from the sheet\u2014sometimes I have to go fix that by hand a bit, as they may not track precisely those from our ledger\u2014and the income shows that the contribution was made by his mother. Putting it All Together \uf0c1 Finally, we need to put together all these files. I created a top-level kyle.beancount file that simple declares all of his account categories and includes his mom and dad files. We have three files: dad.beancount mom.beancount kyle.beancount -> includes transactions from dad.beancount and mom.beancount I can then run bean-web or bean-query on kyle.beancount. There are two things which are interesting to see on that ledger: The distribution of Kyle\u2019s expenses, in other words, what\u2019s it costing us to raise a child (regardless of who pays). The difference between our contributions. In order to reconcile (2), we basically compare the balances of the Income:Mom and Income:Dad accounts. This can be done \u201cby hand\u201d, visually (using a calculator), but since Beancount\u2019s API make it so easy to pull any number from a ledger, I wrote a simple script which does that, computes the total amount of Income contributions, breaks it down to the expected numbers based on our chosen breakdown, compares it to each parent\u2019s actual contribution, and simply prints out the difference: $ reconcile_shared.py kyle.beancount Total contributions: 71009.30 Dad expected contribution: 42605.58 Dad actual contribution: 42906.58 Mom expected contribution: 28403.72 Mom actual contribution: 28102.72 Mom OWES Dad: 301.00 After reconciling, the final number should be zero. Reconciling the Child Ledger \uf0c1 In order to account for the difference and make the contributions to Kyle\u2019s upbringing in line with our mutual arrangement of 60%/40%, in the previous section, my wife would need to transfer $301 to me. Of course, we don\u2019t actually transfer anything in practice, I just add a virtual transfer to book the difference to her shared account on my ledger. To do this, all I have to do is insert a transaction in blais.beancount that looks like this: 2019-02-09 * \"Transfer to reconcile Kyle's expenses\" #kyle Assets:US:Share:Carolyn 301.00 USD Expenses:Kyle:Mom When this gets pulled into dad.beancount (as explained previously), it looks like this: 2019-02-09 * \"Transfer to reconcile Kyle's expenses\" #kyle Income:Mom -301.00 USD Income:Dad 301.00 USD After that, going back to the Kyle ledger, pulling in all the transactions again (this is done by using a Makefile) would show an updated balance of 0: $ reconcile_shared.py kyle.beancount Total contributions: 71009.30 Dad expected contribution: 42605.58 Dad actual contribution: 42605.58 Mom expected contribution: 28403.72 Mom actual contribution: 28403.72 Mom OWES Dad: 0.00 Summary of the System \uf0c1 Here\u2019s a diagram that puts in perspective the entire system together: I (\u201cDad\u201d) use Beancount via Emacs, exclusively. Carolyn (\u201cMom\u201d) only interacts with a single Google Sheets doc with three sheets in it. I pull in Carolyn\u2019s shared expenses from a sheet that she fills in to a ledger which gets included in my personal ledger. I also pull in her expenses for Kyle in a similar document, and from my personal ledger I generate my own expenses for Kyle. Both of these documents are merged in a top-level ledger dedicated to Kyle\u2019s expenses. I\u2019m not going to claim it\u2019s simple and that it\u2019s always up-to-date. I tend to update all of these things once/quarter and fix a few input errors each time we review it. I automate much of it via Makefiles, but frankly I don\u2019t update it frequently enough to just remember where all the moving pieces are, so every time, there\u2019s a bit of scrambling and reading my makefiles and figuring out what\u2019s what (in fact, I was motivated to write this to cement my understanding solidly for the future). Amazingly enough, it hasn\u2019t broken at all and I find all the pieces every time and make it work. And we have enough loose change between the two of us that it\u2019s never that urgent to reconcile every week. My wife tends to pay more shared expenses and I tend to pay more Kyle\u2019s expenses, so I often end up doing a transfer from one to the other to even things out and it\u2019s relatively close\u2014the shortfall in Kyle expenses makes up for my shortfall on the shared expenses. Conclusion \uf0c1 I hope this was useful to some of you trying to solve problems using the double-entry bookkeeping method. Please write questions and comments on the Beancount mailing-list, or as comments on this doc.","title":"How We Share Expenses"},{"location":"how_we_share_expenses.html#how-we-share-expenses","text":"This document explains how I share expenses with my wife. This is a bit involved and I\u2019ve developed a good working system, but it\u2019s not so simple for most people to do that, so I figured I would take the time to describe it to help others with designing similar processes for themselves.","title":"How We Share Expenses"},{"location":"how_we_share_expenses.html#context","text":"We\u2019re both working professionals and have decided to share all expenses roughly 70%/30%. This is what we shoot for, this is not a hard rule, but we track it as if it were rigid, and accept it as a good approximation. We have two types of shared expenses: Shared. Common expenses between us, e.g., rent, a dinner out with friends, etc. Kyle. Expenses for our child, e.g., daycare, diapers, nanny, baby food. These get handled very differently, because we book our child's expenses as if it were a separate project on its own. (If you prefer pictures, there\u2019s a diagram at the end of this document that provides an overview of the system.)","title":"Context"},{"location":"how_we_share_expenses.html#shared-expenses","text":"For our shared expenses, I maintain an account for her on my personal ledger file. This is simple and I\u2019m not too interested in maintaining a separate ledger for the totality of our common shared expenses. It\u2019s sufficient for me to just keep track of her balance on that one account. You can imagine that account like a credit card (I\u2019m the credit provider) that she pays off with transfers and also by making her own shared expenses. I just declare an Assets:US:Share:Carolyn account on my personal ledger ( blais.beancount ).","title":"Shared Expenses"},{"location":"how_we_share_expenses.html#my-shared-expenses","text":"Whenever I incur an expense that is for both of us, I book it normally but I will tag it with #carolyn: 2018-12-23 * \"WHISK\" \"Water refill\" #carolyn Liabilities:US:Amex:BlueCash -32.66 USD Expenses:Food:Grocery This gets automatically converted to: 2018-12-23 * \"WHISK\" \"Water refill\" #carolyn Liabilities:US:Amex:BlueCash -32.66 USD Expenses:Food:Grocery 19.60 USD Assets:US:Share:Carolyn 13.06 USD share: TRUE This is done by a custom plugin I built that splits the expenses according to some rules that we have between us (see also this plugin by Akkukis ). In this example, 40% of 32.66 (13.06) gets rerouted to her account. Note that this is an asset account for me, because she owes this.","title":"My Shared Expenses"},{"location":"how_we_share_expenses.html#her-shared-expenses","text":"We also have to keep track of the money she spends on her own for shared expenses. Since she\u2019s not a Beancount user, I\u2019ve set up a Google Sheets doc in which she can add rows to a particular sheet. This sheet has fields: Date, Description, Account, Amount. I try to keep it simple. Then, I built an extract_sheets.py script that can pull down this data automatically and it writes it to a dedicated file for this, overwriting the entire contents each time. The contents of this ledger ( carolyn.beancount ) look like this: pushtag #carolyn ... 2017-05-21 * \"Amazon\" \"Cotton Mattress Protector in White\" Expenses:Home:Furniture 199.99 USD Assets:US:Share:Carolyn -199.99 USD ... poptag #carolyn All of those transactions are tagged as #carolyn from the pushtag/poptag directive. They get translated by the plugin to reduce the amount by the portion I\u2019m supposed to pay: 2017-05-21 * \"Amazon\" \"Cotton Mattress Protector in White\" #carolyn Expenses:Home:Furniture 119.99 USD Assets:US:Share:Carolyn -119.99 USD share: TRUE These postings typically reduce her asset account by that much, and thus remove the portion she paid for me on these shared expenses. I generate the file with a single command like this: extract_sheets.py --tag='carolyn' --sheet='Shared Expenses' '' 'Assets:US:Share:Carolyn' > carolyn.beancount In my personal ledger, I include this file to merge those expenses with my own directives flow. I also define a query to generate a statement for her account. In blais.beancount : include \"carolyn.beancount\" 2020-01-01 query \"carolyn\" \" select date, description, position, balance from open on 2017-01-01 where account ~ 'Assets:US:Share:Carolyn' \"","title":"Her Shared Expenses"},{"location":"how_we_share_expenses.html#reviewing-statement","text":"In order to produce a statement for her to review (and spot the occasional mistake in data entry), I simply produce a journal of that account to a CSV file and upload that to another sheet in the same Google Sheets doc she inputs her expenses: bean-query -f csv -o carolyn.csv --numberify $L run carolyn upload-to-sheets -v --docid=\"\" carolyn.csv:\"Shared Account (Read-Only)\" This makes it easy for her to eyeball all the amounts posted to her balance with me and point out errors if they occur (some always appear). Moreover, all the information is in one place\u2014it\u2019s important to keep it simple. Finally, we can use the collaborative features of Sheets to communicate, e.g. comments, highlighting text, etc. Note that this system has the benefit of correctly accruing my expenses, by reducing my portion on categories for the stuff I pay and by including her portion on categories for the stuff she pays for.","title":"Reviewing & Statement"},{"location":"how_we_share_expenses.html#reconciling-our-shared-expenses","text":"Finally, in order to reconcile this account, my wife (or I, but usually she\u2019s behind) just makes a bank transfer to my account, which I book to reduce her running account balance: 2019-01-30 * \"HERBANK EXT TRNSFR; DIRECTDEP\" Assets:US:MyBank:Checking 3000 USD Assets:US:Share:Carolyn Typically she'll do this every month or two. She'll be fiddling on her laptop and ask casually \"Hey, what's my balance I can do a transfer now?\" It\u2019s all fine to relax about the particulars since the system is keeping track of everything precisely, so she can send some approximate amount, it doesn't matter, it'll post to her account.","title":"Reconciling our Shared Expenses"},{"location":"how_we_share_expenses.html#child-expenses","text":"I designed a very different system to track our child\u2019s expenses. For Kyle, I\u2019m definitely interested in tracking the total cash flows and expenses related to him, regardless of who paid for them. It\u2019s interesting to be able to ask (our ledger) a question like: \u201cHow much did his schooling cost?\u201d, for example, or \u201cHow much did we pay in diapers, in total?\u201d. Furthermore, we tend to pay for different things for Kyle, e.g. I deal with the daycare expenses (I\u2019m the accounting nerd after all, so this shouldn\u2019t be surprising), and his mother tends to buy all the clothing and prepare his food. To have a global picture of all costs related to him, we need to account for these things correctly. One interesting detail is that it would be difficult to do this with the previously described method, because I\u2019d have to have a mirror of all expense accounts I\u2019d use for him. This would make my personal ledger really ugly. For example, I want to book diapers to Expenses:Pharmacy , but I also have my own personal pharmacy expenses. So in theory, to do that I\u2019d like to have separate accounts for him and me, e.g., Expenses:Kyle:Pharmacy and Expenses:Pharmacy . This would have to be done for all the accounts we use for him. I don't do that.","title":"Child Expenses"},{"location":"how_we_share_expenses.html#my-child-expenses-on-my-personal-ledger","text":"Instead of doing that, what I want is for my personal ledger to book all the expenses I make for him to a single category: Expenses:Kyle , and to track all the detail in a shared ledger. But I still have to book all the expenses to some category, and these appear on my personal ledger, there\u2019s no option (I won\u2019t maintain a separate credit card to pay for his expenses, that would be overkill, so I have to find a way). I accomplish this by booking the expenses to my own expenses account, as usual, but tagging the transaction with #kyle: 2019-02-01 * \"AMAZON.COM\" \"MERCHANDISE - Diapers size 4 for Kyle\" #kyle Liabilities:US:Amex:BlueCash -49.99 USD Expenses:Pharmacy And I have a different plugin that automatically makes the conversion of those transactions to: 2019-02-01 * \"AMAZON.COM\" \"MERCHANDISE - Diapers size 4 for Kyle\" #kyle Liabilities:US:Amex:BlueCash -49.99 USD Expenses:Kyle 49.99 USD diverted_account: \"Expenses:Pharmacy\" So from my personal side, all those expenses get booked to my \u201cKyle project\u201d account. This is accomplished by the divert_expenses plugin, with this configuration: plugin \"beancount.plugins.divert_expenses\" \"{ 'tag': 'kyle', 'account': 'Expenses:Kyle' }\" The \u201cdiverted_account\u201d metadata is used to keep track of the original account, and this is used later by another script that generates a ledger file decided to my expenses for him (more below).","title":"My Child Expenses on my Personal Ledger"},{"location":"how_we_share_expenses.html#my-child-expenses-in-kyles-own-ledger","text":"Now, because we\u2019re considering Kyle\u2019s expenses a project of his own, I have to maintain a set of ledgers for him. I automatically pull the transactions I described in the previous section from my personal ledger and automatically convert them to a file dedicated to his dad (me). This is done by calling the extract_tagged script: extract_tagged.py blais.beancount '#kyle' 'Income:Dad' --translate \"Expenses:Kyle:Mom=Income:Mom\" > dad.beancount The matching transaction from the previous section would look like this in it: 2019-02-01 * \"AMAZON.COM\" \"MERCHANDISE - Diapers size 4 for Kyle\" #kyle Income:Dad -49.99 USD Expenses:Pharmacy 49.99 USD As you can see, the script was able to reconstruct the original account name in Kyle\u2019s ledger by using the metadata saved by the divert_expenses plugin in the previous section. It also books the source of the payment to a single account showing it came from his father ( Income:Dad ). There\u2019s no need for me to keep track of which payment method I used on Kyle\u2019s side (e.g. by credit card), that\u2019s not important to him. This ledger contains the sum total of all expenses I\u2019ve made for him to date. The file gets entirely overwritten each time I run this (this is a purely generated output, no hand editing is ever done here, if I change anything I change it in my personal ledger file).","title":"My Child Expenses in Kyle\u2019s own Ledger"},{"location":"how_we_share_expenses.html#her-child-expenses","text":"In order to keep track of her expenses for Kyle, we use the same method (and programs) as we use for our shared accounts in order to pull a set of \u201cCarolyn\u2019s expenses for Kyle\u201d from another sheet in the same Google Sheets doc: extract_sheets.py --sheet='Kyle Expenses (Regular)' '' 'Income:Mom' > mom.beancount This pulls in transactions that look like this: 2018-09-23 * \"SPROUT SAN FRANCISCO\" \"Clothing for Kyle 9-12 months\" Expenses:Clothing 118.30 USD Income:Mom -118.30 USD The expenses accounts are pulled from the sheet\u2014sometimes I have to go fix that by hand a bit, as they may not track precisely those from our ledger\u2014and the income shows that the contribution was made by his mother.","title":"Her Child Expenses"},{"location":"how_we_share_expenses.html#putting-it-all-together","text":"Finally, we need to put together all these files. I created a top-level kyle.beancount file that simple declares all of his account categories and includes his mom and dad files. We have three files: dad.beancount mom.beancount kyle.beancount -> includes transactions from dad.beancount and mom.beancount I can then run bean-web or bean-query on kyle.beancount. There are two things which are interesting to see on that ledger: The distribution of Kyle\u2019s expenses, in other words, what\u2019s it costing us to raise a child (regardless of who pays). The difference between our contributions. In order to reconcile (2), we basically compare the balances of the Income:Mom and Income:Dad accounts. This can be done \u201cby hand\u201d, visually (using a calculator), but since Beancount\u2019s API make it so easy to pull any number from a ledger, I wrote a simple script which does that, computes the total amount of Income contributions, breaks it down to the expected numbers based on our chosen breakdown, compares it to each parent\u2019s actual contribution, and simply prints out the difference: $ reconcile_shared.py kyle.beancount Total contributions: 71009.30 Dad expected contribution: 42605.58 Dad actual contribution: 42906.58 Mom expected contribution: 28403.72 Mom actual contribution: 28102.72 Mom OWES Dad: 301.00 After reconciling, the final number should be zero.","title":"Putting it All Together"},{"location":"how_we_share_expenses.html#reconciling-the-child-ledger","text":"In order to account for the difference and make the contributions to Kyle\u2019s upbringing in line with our mutual arrangement of 60%/40%, in the previous section, my wife would need to transfer $301 to me. Of course, we don\u2019t actually transfer anything in practice, I just add a virtual transfer to book the difference to her shared account on my ledger. To do this, all I have to do is insert a transaction in blais.beancount that looks like this: 2019-02-09 * \"Transfer to reconcile Kyle's expenses\" #kyle Assets:US:Share:Carolyn 301.00 USD Expenses:Kyle:Mom When this gets pulled into dad.beancount (as explained previously), it looks like this: 2019-02-09 * \"Transfer to reconcile Kyle's expenses\" #kyle Income:Mom -301.00 USD Income:Dad 301.00 USD After that, going back to the Kyle ledger, pulling in all the transactions again (this is done by using a Makefile) would show an updated balance of 0: $ reconcile_shared.py kyle.beancount Total contributions: 71009.30 Dad expected contribution: 42605.58 Dad actual contribution: 42605.58 Mom expected contribution: 28403.72 Mom actual contribution: 28403.72 Mom OWES Dad: 0.00","title":"Reconciling the Child Ledger"},{"location":"how_we_share_expenses.html#summary-of-the-system","text":"Here\u2019s a diagram that puts in perspective the entire system together: I (\u201cDad\u201d) use Beancount via Emacs, exclusively. Carolyn (\u201cMom\u201d) only interacts with a single Google Sheets doc with three sheets in it. I pull in Carolyn\u2019s shared expenses from a sheet that she fills in to a ledger which gets included in my personal ledger. I also pull in her expenses for Kyle in a similar document, and from my personal ledger I generate my own expenses for Kyle. Both of these documents are merged in a top-level ledger dedicated to Kyle\u2019s expenses. I\u2019m not going to claim it\u2019s simple and that it\u2019s always up-to-date. I tend to update all of these things once/quarter and fix a few input errors each time we review it. I automate much of it via Makefiles, but frankly I don\u2019t update it frequently enough to just remember where all the moving pieces are, so every time, there\u2019s a bit of scrambling and reading my makefiles and figuring out what\u2019s what (in fact, I was motivated to write this to cement my understanding solidly for the future). Amazingly enough, it hasn\u2019t broken at all and I find all the pieces every time and make it work. And we have enough loose change between the two of us that it\u2019s never that urgent to reconcile every week. My wife tends to pay more shared expenses and I tend to pay more Kyle\u2019s expenses, so I often end up doing a transfer from one to the other to even things out and it\u2019s relatively close\u2014the shortfall in Kyle expenses makes up for my shortfall on the shared expenses.","title":"Summary of the System"},{"location":"how_we_share_expenses.html#conclusion","text":"I hope this was useful to some of you trying to solve problems using the double-entry bookkeeping method. Please write questions and comments on the Beancount mailing-list, or as comments on this doc.","title":"Conclusion"},{"location":"importing_external_data.html","text":"Importing External Data in Beancount \uf0c1 Martin Blais , March 2016 http://furius.ca/beancount/doc/ingest This document is about Beancount v2; Beancount v3 is in development and uses a completely different build and installation system. For instructions on importing v3, see this document (Beangulp). Introduction The Importing Process Automating Network Downloads Typical Downloads Extracting Data from PDF Files Tools Invocation Configuration Configuring from an Input File Writing an Importer Regression Testing your Importers Generating Test Input Making Incremental Improvements Running the Tests Caching Data In-Memory Caching On-Disk Caching Organizing your Files Example Importers Cleaning Up Automatic Categorization Cleaning up Payees Future Work Related Discussion Threads Historical Note Introduction \uf0c1 This is the user\u2019s manual for the library and tools in Beancount which can help you automate the importing of external transaction data into your Beancount input file and manage the documents you download from your financial institutions\u2019 websites. The Importing Process \uf0c1 People often wonder how we do this, so let me describe candidly and in more detail what we\u2019re talking about doing here. The essence of the task at hand is to transcribe the transactions that occur in a person\u2019s entire set of accounts to a single text file: the Beancount input file. Having the entire set of transactions ingested in a single system is what we need to do in order to generate comprehensive reports about one\u2019s wealth and expenses. Some people call this \u201creconciling\u201d. We could transcribe all the transactions manually from paper statements by typing them in. However nowadays most financial institutions have a website where you can download a statement of historical transactions in a number of data formats which you can parse to output Beancount syntax for them. Importing transactions from these documents involves: Manually reviewing the transactions for correctness or even fraud; Merging new transactions with previous transactions imported from another account. For example, a payment from a bank account to pay off one\u2019s credit card will typically be imported from both the bank AND the credit card account. You must manually merge the corresponding transactions together 1 . Assigning the right category to an expense transaction Organizing your file by moving the resulting directives to the right place in your file. Verifying balances either visually or inserting a Balance directive which asserts what the final account balance should be after the new transactions are inserted. If my importers work without bugs, this is a process that takes me 30-60 minutes to update the majority of my active accounts. Less active accounts are updated every quarter or when I feel like it. I tend to do this on Saturday morning maybe twice per month, or sometimes weekly. If you maintain a well-organized input file with lots of assertions, mismatches are easily found, it\u2019s a pleasant and easy process, and after you\u2019re done generating an updated balance sheet is rewarding (I typically re-export to a Google Finance portfolio). Automating Network Downloads \uf0c1 The downloading of files is not something I automate, and Beancount provides no tools to connect to the network and fetch your files. There is simply too great a variety of protocols out there to make a meaningful contribution to this problem 2 . Given the nature of today's secure websites and the castles of JavaScript used to implement them, it would be a nightmare to implement. Web scraping is probably too much to be a worthwhile, viable solution. I manually log into the various websites with my usernames & passwords and click the right buttons to generate the downloaded files I need. These files are recognized automatically by the importers and extracting transactions and filing the documents in a well-organized directory hierarchy is automated using the tools described in this document. While I\u2019m not scripting the fetching, I think it\u2019s possible to do so on some sites. That work is left for you to implement where you think it\u2019s worth the time. Typical Downloads \uf0c1 Here\u2019s a description of the typical kinds of files involved; this describes my use case and what I\u2019ve managed to do. This should give you a qualitative sense of what\u2019s involved. Credit cards and banks provide fairly good quality historical statement downloads in OFX or CSV file formats but I need to categorize the other side of those transactions manually and merge some of the transactions together. Investment accounts provide me with great quality of processable statements and the extraction of purchase transactions is fully automated, but I need to manually edit sales transactions in order to associate the correct cost basis. Some institutions for specialized products (e.g., P2P lending) provide only PDF files and those are translated manually. Payroll stubs and vesting events are usually provided only as PDFs and I don't bother trying to extract data automatically; I transcribe those manually, keeping the input very regular and with postings in the same order as they appear on the statements. This makes it easier. Cash transactions : I have to enter those by hand. I only book non-food expenses as individual transactions directly, and for food maybe once every six months I'll count my wallet balance and insert a summarizing transaction for each month to debit away the cash account towards food to make it balance. If you do this, you end up with surprisingly little transactions to type manually, maybe just a few each week (it depends on lifestyle choices, for me this works). When I\u2019m on the go, I just note those on my phone in Google Keep and eventually transcribe them after they accumulate. Extracting Data from PDF Files \uf0c1 I've made some headway toward converting data from PDF files, which is a common need, but it's incomplete; it turns out that fully automating table extraction from PDF isn't easy in the general case. I have some code that is close to working and will release it when the time is right. Otherwise, the best FOSS solution I\u2019ve found for this is a tool called TabulaPDF but you still need to manually identify where the tables of data are located on the page; you may be able to automate some fetching using its sister project tabula-java . Nevertheless, I usually have good success with my importers grepping around PDF statements converted to ugly text in order to identify what institution they are for and extracting the date of issuance of the document. Finally, there are a number of different tools used to extract text from PDF documents, such as PDFMiner , LibreOffice , the xpdf library, the poppler library 3 and more... but none of them works consistently on all input documents; you will likely end up installing many and relying on different ones for different input files. For this reason, I\u2019m not requiring a dependency on PDF conversion tools from within Beancount. You should test what works on your specific documents and invoke those tools from your importer implementations. Tools \uf0c1 There are three Beancount tools provided to orchestrate the three stages of importing: bean-identify : Given a messy list of downloaded files (e.g. in ~/Downloads), automatically identify which of your configured importers is able to handle them and print them out. This is to be used for debugging and figuring out if your configuration is properly associating a suitable importer for each of the files you downloaded; bean-extract : Extracting transactions and statement date from each file, if at all possible. This produces some Beancount input text to be moved to your input file; bean-file : Filing away the downloaded files to a directory hierarchy which mirrors the chart of accounts, for preservation, e.g. in a personal git repo. The filenames are cleaned, the files are moved and an appropriate statement date is prepended to each of them so that Beancount may produce corresponding Document directives. Invocation \uf0c1 All tools accept the same input parameters: bean- For example, bean-extract blais.config ~/Downloads The filing tool accepts an extra option that lets the user decide where to move the files, e.g., bean-file -o ~/accounting/documents blais.config ~/Downloads Its default behavior is to move the files to the same directory as that of the configuration file. Configuration \uf0c1 The tools introduced previously orchestrate the processes, but they don\u2019t do all that much of the concrete work of groking the individual downloads themselves. They call methods on importer objects. You must provide a list of such importers; this list is the configuration for the importing process (without it, those tools don\u2019t do anything useful). For each file found, each of the importers is called to assert whether it can or cannot handle that file. If it deems that it can, methods can be called to produce a list of transactions, extract a date, or produce a cleaned up filename for the downloaded file. The configuration should be a Python3 module in which you instantiate the importers and assign the list to the module-level \u201c CONFIG \u201d variable, like this: #!/usr/bin/env python3 from myimporters.bank import acmebank from myimporters.bank import chase \u2026 CONFIG = [ acmebank.Importer(), chase.Importer(), \u2026 ] Of course, since you\u2019re crafting a Python script, you can insert whatever other code in there you like. All that matters is that this \u201c CONFIG \u201d variable refers to a list of objects which comply with the importer protocol (described in the next section). Their order does not matter. In particular, it\u2019s a good idea to write your importers as generically as possible and to parameterize them with the particular account names you use in your input file. This helps keep your code independent of the particular accounts and forces you to define logical accounts, and I\u2019ve found that this helps with clarity. Or not\u2026 At the end of the day, these importer codes live in some of your own personal place, not with Beancount. If you so desire, you can keep them as messy and unshareable as you like. Configuring from an Input File \uf0c1 An interesting idea that I haven\u2019t tested yet is to use one\u2019s Beancount input file to infer the configuration of importers. If you want to try this out and hack something, you can load your input file from the import configuration Python config, by using the API\u2019s beancount.loader.load_file() function. Writing an Importer \uf0c1 Each of the importers must comply with a particular protocol and implement at least some of its methods. The full detail of this protocol is best found in the source code itself: importer.py . The tools above will take care of finding the downloads and invoking the appropriate methods on your importer objects. Here\u2019s a brief summary of the methods you need to, or may want to, implement: name(): This method provides a unique id for each importer instance. It\u2019s convenient to be able to refer to your importers with a unique name; it gets printed out by the identification process, for instance. identify(): This method just returns true if this importer can handle the given file. You must implement this method, and all the tools invoke it to figure out the list of (file, importer) pairs. extract(): This is called to attempt to extract some Beancount directives from the file contents. It must create the directives by instantiating the objects defined in beancount.core.data and return them. file_account(): This method returns the root account associated with this importer. This is where the downloaded file will be moved by the filing script file_date(): If a date can be extracted from the statement\u2019s contents, return it here. This is useful for dated PDF statements\u2026 it\u2019s often possible using regular expressions to grep out the date from a PDF converted to text. This allows the filing script to prepend a relevant date instead of using the date when the file was downloaded (the default). file_name(): It\u2019s most convenient not to bother renaming downloaded files. Oftentimes, the files generated from your bank all have a unique name and they end up getting renamed by your browser when you download multiple ones and the names collide. This function is used for the importer to provide a \u201cnice\u201d name to file the download under. So basically, you create some module somewhere on your PYTHONPATH\u2014anywhere you like, somewhere private\u2014and you implement a class, something like this: from beancount.ingest import importer class Importer(importer.ImporterProtocol): def identify(self, file): \u2026 # Override other methods\u2026 Typically I create my importer module files in directories dedicated to each importer, so that I can place example input files all in that directory for regression testing. Regression Testing your Importers \uf0c1 I've found over time that regression testing is key to maintaining your importer code working. Importers are often written against file formats with no official spec and unexpected surprises routinely occur. For example, I have XML files with some unescaped \"&\" characters, which require a custom fix just for that bank 4 . I\u2019ve also witnessed a discount brokerage switching its dates format between MM/DD/YY and DD/MM/YY; that importer now needs to be able to handle both types. So you make the necessary adjustment, and eventually you find out that something else breaks; this isn\u2019t great. And the timing is particularly annoying: usually things break when you\u2019re trying to update your ledger: you have other things to do. The easiest, laziest and most relevant way to test those importers is to use some real data files and compare what your importer extracts from them to expected outputs. For the importers to be at least somewhat reliable, you really need to be able to reproduce the extractions on a number of real inputs. And since the inputs are so unpredictable and poorly defined, it\u2019s not practical to write exhaustive tests on what they could be. In practice, I have to make at least some fix to some of my importers every couple of months, and with this process, it only sinks about a half-hour of my time: I add the new downloaded file which causes breakage to the importer directory, I fix the code by running it there locally as a test. And I also run the tests over all the previously downloaded test inputs in that directory (old and new) to ensure my importer is still working as intended on the older files. There is some support for automating this process in beancount.ingest.regression . What we want is some routine that will list the importer\u2019s package directory, identify the input files which are to be used for testing, and generate a suite of unit tests which compares the output produced by importer methods to the contents of \u201cexpected files\u201d placed next to the test file. For example, given a package with an implementation of an importer and two sample input files: /home/joe/importers/acmebank/__init__.py <- code goes here /home/joe/importers/acmebank/sample1.csv /home/joe/importers/acmebank/sample2.csv You can place this code in the Python module (the __init__.py file): from beancount.ingest import regression \u2026 def test(): importer = Importer(...) yield from regression.compare_sample_files(importer) If your importer overrides the extract() and file_date() methods, this will generate four unit tests which get run automatically by pytest : A test which calls extract() on sample1.csv , prints the extracted entries to a string, and compares this string with the contents of sample1.csv.extract A test which calls file_date() on sample1.csv and compares the date with the one found in the sample1.csv.file_date file. A test like (1) but on sample2.csv A test like (2) but on sample2.csv Generating Test Input \uf0c1 At first, the files containing the expected outputs do not exist. When an expected output file is absent like this, the regression tests automatically generate those files from the extracted output. This would result in the following list of files: /home/joe/importers/acmebank/__init__.py <- code goes here /home/joe/importers/acmebank/sample1.csv /home/joe/importers/acmebank/sample1.csv.extract /home/joe/importers/acmebank/sample1.csv.file_date /home/joe/importers/acmebank/sample2.csv /home/joe/importers/acmebank/sample2.csv.extract /home/joe/importers/acmebank/sample2.csv.file_date You should inspect the contents of the expected output files to visually assert that they represent the contents of the downloaded files. If you run the tests again with those files present, the expected output files will be used as inputs to the tests. If the contents differ in the future, the test will fail and an error will be generated. (You can test this out now if you want, by manually editing and inserting some unexpected data in one of those files.) When you edit your source code, you can always re-run the tests to make sure it still works on those older files. When a newly downloaded file fails, you repeat the process above: You make a copy of it in that directory, fix the importer, run it, check the expected files. That\u2019s it 5 . Making Incremental Improvements \uf0c1 Sometimes I make improvements to the importers that result in more or better output being generated even in the older files, so that all the old tests will now fail. A good way to deal with this is to keep all of these files under source control, locally delete all the expected files, run the tests to regenerate new ones, and then diff against the most recent commit to check that the changes are as expected. Caching Data \uf0c1 Some of the data conversions for binary files can be costly and slow. This is usually the case for converting PDF files to text 6 . This is particularly painful, since in the process of ingesting our downloaded data we\u2019re typically going to run the tools multiple times\u2014at least twice if everything works without flaw: once to extract, twice to file\u2014and usually many more times if there are problems. For this reason, we want to cache these conversions, so that a painful 40 second PDF-to-text conversion doesn\u2019t have to be run twice, for example. Beancount aims to provide two levels of caching for conversions on downloaded files: An in-memory caching of conversions so that multiple importers requesting the same conversion runs them only once, and An on-disk caching of conversions so that multiple invocations of the tools get reused. In-Memory Caching \uf0c1 In-memory caching works like this: Your methods receive a wrapper object for a given file and invoke the wrapper\u2019s convert() method, providing a converter callable/function. class MyImporter(ImporterProtocol): ... def extract(self, file): text = file.convert(slow_convert_pdf_to_text) match = re.search(..., text) This conversion is automatically memoized: if two importers or two different methods use the same converter on the file, the conversion is only run once. This is a simple way of handling redundant conversions in-memory. Make sure to always call those through the .convert() method and share the converter functions to take advantage of this. On-Disk Caching \uf0c1 At the moment. Beancount only implements (1). On-disk caching will be implemented later. Track this ticket for status updates. Organizing your Files \uf0c1 The tools described in this document are pretty flexible in terms of letting you specify Import configuration : The Python file which provides the list of importer objects as a configuration; Importers implementation : The Python modules which implement the individual importers and their regression testing files; Downloads directory : Which directory the downloaded files are to be found in; Filing directory : Which directory the downloaded files are intended to be filed to. You can specify these from any location you want. Despite this, some people are often asking how to organize their files, so I provide a template example under beancount/examples/ingest/office , and I describe this here. I recommend that you create a Git or Mercurial 7 source-controlled repository following this structure: office \u251c\u2500\u2500 documents \u2502 \u251c\u2500\u2500 Assets \u2502 \u251c\u2500\u2500 Liabilities \u2502 \u251c\u2500\u2500 Income \u2502 \u2514\u2500\u2500 Expenses \u251c\u2500\u2500 importers \u2502 \u251c\u2500\u2500 __init__.py \u2502 \u2514\u2500\u2500 \u2026 \u2502 \u251c\u2500\u2500 __init__.py \u2502 \u251c\u2500\u2500 sample-download-1.csv \u2502 \u251c\u2500\u2500 sample-download-1.extract \u2502 \u251c\u2500\u2500 sample-download-1.file_date \u2502 \u2514\u2500\u2500 sample-download-1.file_name \u251c\u2500\u2500 personal.beancount \u2514\u2500\u2500 personal.import The root \u201coffice\u201d directory is your repository. It contains your ledger file (\u201c personal.beancount \u201d), your importer configuration (\u201c personal.import \u201d), your custom importers source code (\u201c importers/ \u201d) and your history of documents (\u201c documents/ \u201d), which should be well-organized by bean-file. You always run the commands from this root directory. An advantage of storing your documents in the same repository as your importers source code is that you can just symlink your regression tests to some files under the documents/ directory. You can check your configuration by running identify: bean-identify example.import ~/Downloads If it works, you can extract transactions from your downloaded files at once: bean-extract -e example.beancount example.import ~/Downloads > tmp.beancount You then open tmp.beancount and move its contents to your personal.beancount file. Once you\u2019re finished, you can stash away the downloaded files for posterity like this: bean-file example.import ~/Downloads -o documents If my importers work, I usually don\u2019t even bother opening those files. You can use the --dry-run option to test moving destinations before doing so. To run the regression tests of the custom importers, use the following command: pytest -v importers Personally, I have a Makefile in my root directory with these targets to make my life easier. Note that you will have to install \u201cpytest\u201d, which is a test runner; it is often packaged as \u201cpython3-pytest\u201d or \u201cpytest\u201d. Example Importers \uf0c1 Beyond the documentation above, I cooked up an example importer for a made-up CSV file format for a made-up investment account. See this directory . There\u2019s also an example of an importer which uses an external tool (PDFMiner2) to convert a PDF file to text to identify it and to extract the statement date from it. See this directory . Beancount also comes with some very basic generic importers. See this directory . There is a simple OFX importer that has worked for me for a long time. Though it\u2019s pretty simple, I\u2019ve used it for years, it\u2019s good enough to pull info out of most credit card accounts. There are also a couple of mixin classes you can mix into your importer implementation to make it more convenient; these are relics from the LedgerHub project\u2014you don\u2019t really need to use them\u2014which can help in the transition to it. Eventually I plan to build and provide a generic CSV file parser in this framework, as well as a parser for QIF files which should allow one to transition from Quicken to Beancount. (I need example inputs to do this; if you\u2019re comfortable sharing your file I could use it to build this, as I don\u2019t have any real input, I don\u2019t use Quicken.) It would also be nice to build a converter from GnuCash at some point; this would go here as well. Cleaning Up \uf0c1 Automatic Categorization \uf0c1 A frequently asked question and common idea from first-time users is \u201cHow do I automatically assign a category to transactions I\u2019ve imported which have only one side?\u201d For example, importing transactions from a credit card account usually provides only one posting, like this: 2016-03-18 * \"UNION MARKET\" Liabilities:US:CreditCard -12.99 USD For which you must manually insert an Expenses posting, like this: 2016-03-18 * \"UNION MARKET\" Liabilities:US:CreditCard -12.99 USD Expenses:Food:Grocery People often have the impression that it is time-consuming to do this. My standard answer is that while it would be fun to have, if you have a text editor with account name completion configured properly, it\u2019s a breeze to do this manually and you don\u2019t really need it. You wouldn\u2019t save much time by automating this away. And personally I like to go over each of the transactions to check what they are and sometimes add comments (e.g., who I had dinner with, what that Amazon charge was for, etc.) and that\u2019s when I categorize. It\u2019s something that could eventually be solved by letting the user provide some simple rules, or by using the history of past transactions to feed into a simple learning classifier. Beancount does not currently provide a mechanism to automatically categorize transactions. You can build this into your importer code. I want to provide a hook for the user to register a completion function that could run across all the importers where you could hook that code in. Cleaning up Payees \uf0c1 The payees that one can find in the downloads are usually ugly names: They are sometimes the legal names of the business, which often does not reflect the street name of the place you went, for various reasons. For example, I recently ate at a restaurant called the \u201cLucky Bee\u201d in New York, and the memo from the OFX file was \u201cKING BEE\u201d. The names are sometimes abbreviated or contain some crud. In the previous example, the actual memo was \u201cKING BEE NEW YO\u201d, where \u201cNEW YO\u201d is a truncated location string. The amount of ugliness is inconsistent between data sources. It would be nice to be able to normalize the payee names by translating them at import time. I think you can do most of it using some simple rules mapping regular expressions to names provided by the user. There\u2019s really no good automated way to obtain the \u201cclean name\u201d of the payee. Beancount does not provide a hook for letting you do this this yet. It will eventually. You could also build a plugin to rename those accounts when loading your ledger. I\u2019ll build that too\u2014it\u2019s easy and would result in much nicer output. Future Work \uf0c1 A list of things I\u2019d really want to add, beyond fortifying what\u2019s already there: A generic, configurable CSV importer which you can instantiate. I plan to play with this a bit and build a sniffer that could automatically figure out the role of each column. A hook to allow you to register a callback for post-processing transactions that works across all importers. Related Discussion Threads \uf0c1 Getting started; assigning accounts to bank .csv data Status of LedgerHub\u2026 how can I get started? Rekon wants your CSV files Historical Note \uf0c1 There once was a first implementation of the process described in this document. The project was called LedgerHub and has been decommissioned in February 2016, rewritten and the resulting code integrated in Beancount itself, into this beancount.ingest library. The original project was intended to include the implementation of various importers to share them with other people, but this sharing was not very successful, and so the rewrite includes only the scaffolding for building your own importers and invoking them, and only a very limited number of example importer implementations. Documents about LedgerHub are preserved, and can help you understand the origins and design choices for Beancount\u2019s importer support. They can be found here: Original design Original instructions & final status (the old version of this doc) An analysis of the reasons why it the project was terminated (post-mortem) There are essentially three conceptual modes of entering such transactions: (1) a user crafts a single transaction manually, (2) another where a user inputs the two sides as a single transaction to transfer accounts, and (3) the two separate transactions get merged into a single one automatically. These are dual modes of each other. The twist in this story is that the same transaction often posts at different dates in each of its accounts. Beancount currently [March 2016] does not support multiple dates for a single transaction\u2019s postings, but a discussion is ongoing to implement support for these input modes. See this document for more details. \u21a9 The closest to universal downloader you will find in the free software world is ofxclient for OFX files, and in the commercial world, Yodlee provides a service that connects to many financial institutions. \u21a9 The \u2018pdftotext\u2019 utility in poppler provides the useful \u2018-layout\u2019 flag which outputs a text file without mangling tables, which can be helpful in the normal case of \u2018transaction-per-row\u2019 \u21a9 After sending them a few detailed emails about this and getting no response nor seeing any change in the downloaded files, I have given up on them fixing the issue. \u21a9 As you can see, this process is partly why I don\u2019t share my importers code. It requires the storage of way too much personal data in order to keep them working. \u21a9 I don\u2019t really understand why, since opening them up for viewing is almost instant, but nearly all the tools to convert them to other formats are vastly slower. \u21a9 I personally much prefer Mercurial for the clarity of its commands and output and its extensibility, but an advantage of Git\u2019s storage model is that moving files within it comes for free (no extra copy is stored). Moving files in a Mercurial repository costs you a bit in storage space. And if you rename accounts or change how you organize your files you will end up potentially copying many large files. \u21a9","title":"Importing External Data"},{"location":"importing_external_data.html#importing-external-data-in-beancount","text":"Martin Blais , March 2016 http://furius.ca/beancount/doc/ingest This document is about Beancount v2; Beancount v3 is in development and uses a completely different build and installation system. For instructions on importing v3, see this document (Beangulp). Introduction The Importing Process Automating Network Downloads Typical Downloads Extracting Data from PDF Files Tools Invocation Configuration Configuring from an Input File Writing an Importer Regression Testing your Importers Generating Test Input Making Incremental Improvements Running the Tests Caching Data In-Memory Caching On-Disk Caching Organizing your Files Example Importers Cleaning Up Automatic Categorization Cleaning up Payees Future Work Related Discussion Threads Historical Note","title":"Importing External Data in Beancount"},{"location":"importing_external_data.html#introduction","text":"This is the user\u2019s manual for the library and tools in Beancount which can help you automate the importing of external transaction data into your Beancount input file and manage the documents you download from your financial institutions\u2019 websites.","title":"Introduction"},{"location":"importing_external_data.html#the-importing-process","text":"People often wonder how we do this, so let me describe candidly and in more detail what we\u2019re talking about doing here. The essence of the task at hand is to transcribe the transactions that occur in a person\u2019s entire set of accounts to a single text file: the Beancount input file. Having the entire set of transactions ingested in a single system is what we need to do in order to generate comprehensive reports about one\u2019s wealth and expenses. Some people call this \u201creconciling\u201d. We could transcribe all the transactions manually from paper statements by typing them in. However nowadays most financial institutions have a website where you can download a statement of historical transactions in a number of data formats which you can parse to output Beancount syntax for them. Importing transactions from these documents involves: Manually reviewing the transactions for correctness or even fraud; Merging new transactions with previous transactions imported from another account. For example, a payment from a bank account to pay off one\u2019s credit card will typically be imported from both the bank AND the credit card account. You must manually merge the corresponding transactions together 1 . Assigning the right category to an expense transaction Organizing your file by moving the resulting directives to the right place in your file. Verifying balances either visually or inserting a Balance directive which asserts what the final account balance should be after the new transactions are inserted. If my importers work without bugs, this is a process that takes me 30-60 minutes to update the majority of my active accounts. Less active accounts are updated every quarter or when I feel like it. I tend to do this on Saturday morning maybe twice per month, or sometimes weekly. If you maintain a well-organized input file with lots of assertions, mismatches are easily found, it\u2019s a pleasant and easy process, and after you\u2019re done generating an updated balance sheet is rewarding (I typically re-export to a Google Finance portfolio).","title":"The Importing Process"},{"location":"importing_external_data.html#automating-network-downloads","text":"The downloading of files is not something I automate, and Beancount provides no tools to connect to the network and fetch your files. There is simply too great a variety of protocols out there to make a meaningful contribution to this problem 2 . Given the nature of today's secure websites and the castles of JavaScript used to implement them, it would be a nightmare to implement. Web scraping is probably too much to be a worthwhile, viable solution. I manually log into the various websites with my usernames & passwords and click the right buttons to generate the downloaded files I need. These files are recognized automatically by the importers and extracting transactions and filing the documents in a well-organized directory hierarchy is automated using the tools described in this document. While I\u2019m not scripting the fetching, I think it\u2019s possible to do so on some sites. That work is left for you to implement where you think it\u2019s worth the time.","title":"Automating Network Downloads"},{"location":"importing_external_data.html#typical-downloads","text":"Here\u2019s a description of the typical kinds of files involved; this describes my use case and what I\u2019ve managed to do. This should give you a qualitative sense of what\u2019s involved. Credit cards and banks provide fairly good quality historical statement downloads in OFX or CSV file formats but I need to categorize the other side of those transactions manually and merge some of the transactions together. Investment accounts provide me with great quality of processable statements and the extraction of purchase transactions is fully automated, but I need to manually edit sales transactions in order to associate the correct cost basis. Some institutions for specialized products (e.g., P2P lending) provide only PDF files and those are translated manually. Payroll stubs and vesting events are usually provided only as PDFs and I don't bother trying to extract data automatically; I transcribe those manually, keeping the input very regular and with postings in the same order as they appear on the statements. This makes it easier. Cash transactions : I have to enter those by hand. I only book non-food expenses as individual transactions directly, and for food maybe once every six months I'll count my wallet balance and insert a summarizing transaction for each month to debit away the cash account towards food to make it balance. If you do this, you end up with surprisingly little transactions to type manually, maybe just a few each week (it depends on lifestyle choices, for me this works). When I\u2019m on the go, I just note those on my phone in Google Keep and eventually transcribe them after they accumulate.","title":"Typical Downloads"},{"location":"importing_external_data.html#extracting-data-from-pdf-files","text":"I've made some headway toward converting data from PDF files, which is a common need, but it's incomplete; it turns out that fully automating table extraction from PDF isn't easy in the general case. I have some code that is close to working and will release it when the time is right. Otherwise, the best FOSS solution I\u2019ve found for this is a tool called TabulaPDF but you still need to manually identify where the tables of data are located on the page; you may be able to automate some fetching using its sister project tabula-java . Nevertheless, I usually have good success with my importers grepping around PDF statements converted to ugly text in order to identify what institution they are for and extracting the date of issuance of the document. Finally, there are a number of different tools used to extract text from PDF documents, such as PDFMiner , LibreOffice , the xpdf library, the poppler library 3 and more... but none of them works consistently on all input documents; you will likely end up installing many and relying on different ones for different input files. For this reason, I\u2019m not requiring a dependency on PDF conversion tools from within Beancount. You should test what works on your specific documents and invoke those tools from your importer implementations.","title":"Extracting Data from PDF Files"},{"location":"importing_external_data.html#tools","text":"There are three Beancount tools provided to orchestrate the three stages of importing: bean-identify : Given a messy list of downloaded files (e.g. in ~/Downloads), automatically identify which of your configured importers is able to handle them and print them out. This is to be used for debugging and figuring out if your configuration is properly associating a suitable importer for each of the files you downloaded; bean-extract : Extracting transactions and statement date from each file, if at all possible. This produces some Beancount input text to be moved to your input file; bean-file : Filing away the downloaded files to a directory hierarchy which mirrors the chart of accounts, for preservation, e.g. in a personal git repo. The filenames are cleaned, the files are moved and an appropriate statement date is prepended to each of them so that Beancount may produce corresponding Document directives.","title":"Tools"},{"location":"importing_external_data.html#invocation","text":"All tools accept the same input parameters: bean- For example, bean-extract blais.config ~/Downloads The filing tool accepts an extra option that lets the user decide where to move the files, e.g., bean-file -o ~/accounting/documents blais.config ~/Downloads Its default behavior is to move the files to the same directory as that of the configuration file.","title":"Invocation"},{"location":"importing_external_data.html#configuration","text":"The tools introduced previously orchestrate the processes, but they don\u2019t do all that much of the concrete work of groking the individual downloads themselves. They call methods on importer objects. You must provide a list of such importers; this list is the configuration for the importing process (without it, those tools don\u2019t do anything useful). For each file found, each of the importers is called to assert whether it can or cannot handle that file. If it deems that it can, methods can be called to produce a list of transactions, extract a date, or produce a cleaned up filename for the downloaded file. The configuration should be a Python3 module in which you instantiate the importers and assign the list to the module-level \u201c CONFIG \u201d variable, like this: #!/usr/bin/env python3 from myimporters.bank import acmebank from myimporters.bank import chase \u2026 CONFIG = [ acmebank.Importer(), chase.Importer(), \u2026 ] Of course, since you\u2019re crafting a Python script, you can insert whatever other code in there you like. All that matters is that this \u201c CONFIG \u201d variable refers to a list of objects which comply with the importer protocol (described in the next section). Their order does not matter. In particular, it\u2019s a good idea to write your importers as generically as possible and to parameterize them with the particular account names you use in your input file. This helps keep your code independent of the particular accounts and forces you to define logical accounts, and I\u2019ve found that this helps with clarity. Or not\u2026 At the end of the day, these importer codes live in some of your own personal place, not with Beancount. If you so desire, you can keep them as messy and unshareable as you like.","title":"Configuration"},{"location":"importing_external_data.html#configuring-from-an-input-file","text":"An interesting idea that I haven\u2019t tested yet is to use one\u2019s Beancount input file to infer the configuration of importers. If you want to try this out and hack something, you can load your input file from the import configuration Python config, by using the API\u2019s beancount.loader.load_file() function.","title":"Configuring from an Input File"},{"location":"importing_external_data.html#writing-an-importer","text":"Each of the importers must comply with a particular protocol and implement at least some of its methods. The full detail of this protocol is best found in the source code itself: importer.py . The tools above will take care of finding the downloads and invoking the appropriate methods on your importer objects. Here\u2019s a brief summary of the methods you need to, or may want to, implement: name(): This method provides a unique id for each importer instance. It\u2019s convenient to be able to refer to your importers with a unique name; it gets printed out by the identification process, for instance. identify(): This method just returns true if this importer can handle the given file. You must implement this method, and all the tools invoke it to figure out the list of (file, importer) pairs. extract(): This is called to attempt to extract some Beancount directives from the file contents. It must create the directives by instantiating the objects defined in beancount.core.data and return them. file_account(): This method returns the root account associated with this importer. This is where the downloaded file will be moved by the filing script file_date(): If a date can be extracted from the statement\u2019s contents, return it here. This is useful for dated PDF statements\u2026 it\u2019s often possible using regular expressions to grep out the date from a PDF converted to text. This allows the filing script to prepend a relevant date instead of using the date when the file was downloaded (the default). file_name(): It\u2019s most convenient not to bother renaming downloaded files. Oftentimes, the files generated from your bank all have a unique name and they end up getting renamed by your browser when you download multiple ones and the names collide. This function is used for the importer to provide a \u201cnice\u201d name to file the download under. So basically, you create some module somewhere on your PYTHONPATH\u2014anywhere you like, somewhere private\u2014and you implement a class, something like this: from beancount.ingest import importer class Importer(importer.ImporterProtocol): def identify(self, file): \u2026 # Override other methods\u2026 Typically I create my importer module files in directories dedicated to each importer, so that I can place example input files all in that directory for regression testing.","title":"Writing an Importer"},{"location":"importing_external_data.html#regression-testing-your-importers","text":"I've found over time that regression testing is key to maintaining your importer code working. Importers are often written against file formats with no official spec and unexpected surprises routinely occur. For example, I have XML files with some unescaped \"&\" characters, which require a custom fix just for that bank 4 . I\u2019ve also witnessed a discount brokerage switching its dates format between MM/DD/YY and DD/MM/YY; that importer now needs to be able to handle both types. So you make the necessary adjustment, and eventually you find out that something else breaks; this isn\u2019t great. And the timing is particularly annoying: usually things break when you\u2019re trying to update your ledger: you have other things to do. The easiest, laziest and most relevant way to test those importers is to use some real data files and compare what your importer extracts from them to expected outputs. For the importers to be at least somewhat reliable, you really need to be able to reproduce the extractions on a number of real inputs. And since the inputs are so unpredictable and poorly defined, it\u2019s not practical to write exhaustive tests on what they could be. In practice, I have to make at least some fix to some of my importers every couple of months, and with this process, it only sinks about a half-hour of my time: I add the new downloaded file which causes breakage to the importer directory, I fix the code by running it there locally as a test. And I also run the tests over all the previously downloaded test inputs in that directory (old and new) to ensure my importer is still working as intended on the older files. There is some support for automating this process in beancount.ingest.regression . What we want is some routine that will list the importer\u2019s package directory, identify the input files which are to be used for testing, and generate a suite of unit tests which compares the output produced by importer methods to the contents of \u201cexpected files\u201d placed next to the test file. For example, given a package with an implementation of an importer and two sample input files: /home/joe/importers/acmebank/__init__.py <- code goes here /home/joe/importers/acmebank/sample1.csv /home/joe/importers/acmebank/sample2.csv You can place this code in the Python module (the __init__.py file): from beancount.ingest import regression \u2026 def test(): importer = Importer(...) yield from regression.compare_sample_files(importer) If your importer overrides the extract() and file_date() methods, this will generate four unit tests which get run automatically by pytest : A test which calls extract() on sample1.csv , prints the extracted entries to a string, and compares this string with the contents of sample1.csv.extract A test which calls file_date() on sample1.csv and compares the date with the one found in the sample1.csv.file_date file. A test like (1) but on sample2.csv A test like (2) but on sample2.csv","title":"Regression Testing your Importers"},{"location":"importing_external_data.html#generating-test-input","text":"At first, the files containing the expected outputs do not exist. When an expected output file is absent like this, the regression tests automatically generate those files from the extracted output. This would result in the following list of files: /home/joe/importers/acmebank/__init__.py <- code goes here /home/joe/importers/acmebank/sample1.csv /home/joe/importers/acmebank/sample1.csv.extract /home/joe/importers/acmebank/sample1.csv.file_date /home/joe/importers/acmebank/sample2.csv /home/joe/importers/acmebank/sample2.csv.extract /home/joe/importers/acmebank/sample2.csv.file_date You should inspect the contents of the expected output files to visually assert that they represent the contents of the downloaded files. If you run the tests again with those files present, the expected output files will be used as inputs to the tests. If the contents differ in the future, the test will fail and an error will be generated. (You can test this out now if you want, by manually editing and inserting some unexpected data in one of those files.) When you edit your source code, you can always re-run the tests to make sure it still works on those older files. When a newly downloaded file fails, you repeat the process above: You make a copy of it in that directory, fix the importer, run it, check the expected files. That\u2019s it 5 .","title":"Generating Test Input"},{"location":"importing_external_data.html#making-incremental-improvements","text":"Sometimes I make improvements to the importers that result in more or better output being generated even in the older files, so that all the old tests will now fail. A good way to deal with this is to keep all of these files under source control, locally delete all the expected files, run the tests to regenerate new ones, and then diff against the most recent commit to check that the changes are as expected.","title":"Making Incremental Improvements"},{"location":"importing_external_data.html#caching-data","text":"Some of the data conversions for binary files can be costly and slow. This is usually the case for converting PDF files to text 6 . This is particularly painful, since in the process of ingesting our downloaded data we\u2019re typically going to run the tools multiple times\u2014at least twice if everything works without flaw: once to extract, twice to file\u2014and usually many more times if there are problems. For this reason, we want to cache these conversions, so that a painful 40 second PDF-to-text conversion doesn\u2019t have to be run twice, for example. Beancount aims to provide two levels of caching for conversions on downloaded files: An in-memory caching of conversions so that multiple importers requesting the same conversion runs them only once, and An on-disk caching of conversions so that multiple invocations of the tools get reused.","title":"Caching Data"},{"location":"importing_external_data.html#in-memory-caching","text":"In-memory caching works like this: Your methods receive a wrapper object for a given file and invoke the wrapper\u2019s convert() method, providing a converter callable/function. class MyImporter(ImporterProtocol): ... def extract(self, file): text = file.convert(slow_convert_pdf_to_text) match = re.search(..., text) This conversion is automatically memoized: if two importers or two different methods use the same converter on the file, the conversion is only run once. This is a simple way of handling redundant conversions in-memory. Make sure to always call those through the .convert() method and share the converter functions to take advantage of this.","title":"In-Memory Caching"},{"location":"importing_external_data.html#on-disk-caching","text":"At the moment. Beancount only implements (1). On-disk caching will be implemented later. Track this ticket for status updates.","title":"On-Disk Caching"},{"location":"importing_external_data.html#organizing-your-files","text":"The tools described in this document are pretty flexible in terms of letting you specify Import configuration : The Python file which provides the list of importer objects as a configuration; Importers implementation : The Python modules which implement the individual importers and their regression testing files; Downloads directory : Which directory the downloaded files are to be found in; Filing directory : Which directory the downloaded files are intended to be filed to. You can specify these from any location you want. Despite this, some people are often asking how to organize their files, so I provide a template example under beancount/examples/ingest/office , and I describe this here. I recommend that you create a Git or Mercurial 7 source-controlled repository following this structure: office \u251c\u2500\u2500 documents \u2502 \u251c\u2500\u2500 Assets \u2502 \u251c\u2500\u2500 Liabilities \u2502 \u251c\u2500\u2500 Income \u2502 \u2514\u2500\u2500 Expenses \u251c\u2500\u2500 importers \u2502 \u251c\u2500\u2500 __init__.py \u2502 \u2514\u2500\u2500 \u2026 \u2502 \u251c\u2500\u2500 __init__.py \u2502 \u251c\u2500\u2500 sample-download-1.csv \u2502 \u251c\u2500\u2500 sample-download-1.extract \u2502 \u251c\u2500\u2500 sample-download-1.file_date \u2502 \u2514\u2500\u2500 sample-download-1.file_name \u251c\u2500\u2500 personal.beancount \u2514\u2500\u2500 personal.import The root \u201coffice\u201d directory is your repository. It contains your ledger file (\u201c personal.beancount \u201d), your importer configuration (\u201c personal.import \u201d), your custom importers source code (\u201c importers/ \u201d) and your history of documents (\u201c documents/ \u201d), which should be well-organized by bean-file. You always run the commands from this root directory. An advantage of storing your documents in the same repository as your importers source code is that you can just symlink your regression tests to some files under the documents/ directory. You can check your configuration by running identify: bean-identify example.import ~/Downloads If it works, you can extract transactions from your downloaded files at once: bean-extract -e example.beancount example.import ~/Downloads > tmp.beancount You then open tmp.beancount and move its contents to your personal.beancount file. Once you\u2019re finished, you can stash away the downloaded files for posterity like this: bean-file example.import ~/Downloads -o documents If my importers work, I usually don\u2019t even bother opening those files. You can use the --dry-run option to test moving destinations before doing so. To run the regression tests of the custom importers, use the following command: pytest -v importers Personally, I have a Makefile in my root directory with these targets to make my life easier. Note that you will have to install \u201cpytest\u201d, which is a test runner; it is often packaged as \u201cpython3-pytest\u201d or \u201cpytest\u201d.","title":"Organizing your Files"},{"location":"importing_external_data.html#example-importers","text":"Beyond the documentation above, I cooked up an example importer for a made-up CSV file format for a made-up investment account. See this directory . There\u2019s also an example of an importer which uses an external tool (PDFMiner2) to convert a PDF file to text to identify it and to extract the statement date from it. See this directory . Beancount also comes with some very basic generic importers. See this directory . There is a simple OFX importer that has worked for me for a long time. Though it\u2019s pretty simple, I\u2019ve used it for years, it\u2019s good enough to pull info out of most credit card accounts. There are also a couple of mixin classes you can mix into your importer implementation to make it more convenient; these are relics from the LedgerHub project\u2014you don\u2019t really need to use them\u2014which can help in the transition to it. Eventually I plan to build and provide a generic CSV file parser in this framework, as well as a parser for QIF files which should allow one to transition from Quicken to Beancount. (I need example inputs to do this; if you\u2019re comfortable sharing your file I could use it to build this, as I don\u2019t have any real input, I don\u2019t use Quicken.) It would also be nice to build a converter from GnuCash at some point; this would go here as well.","title":"Example Importers"},{"location":"importing_external_data.html#cleaning-up","text":"","title":"Cleaning Up"},{"location":"importing_external_data.html#automatic-categorization","text":"A frequently asked question and common idea from first-time users is \u201cHow do I automatically assign a category to transactions I\u2019ve imported which have only one side?\u201d For example, importing transactions from a credit card account usually provides only one posting, like this: 2016-03-18 * \"UNION MARKET\" Liabilities:US:CreditCard -12.99 USD For which you must manually insert an Expenses posting, like this: 2016-03-18 * \"UNION MARKET\" Liabilities:US:CreditCard -12.99 USD Expenses:Food:Grocery People often have the impression that it is time-consuming to do this. My standard answer is that while it would be fun to have, if you have a text editor with account name completion configured properly, it\u2019s a breeze to do this manually and you don\u2019t really need it. You wouldn\u2019t save much time by automating this away. And personally I like to go over each of the transactions to check what they are and sometimes add comments (e.g., who I had dinner with, what that Amazon charge was for, etc.) and that\u2019s when I categorize. It\u2019s something that could eventually be solved by letting the user provide some simple rules, or by using the history of past transactions to feed into a simple learning classifier. Beancount does not currently provide a mechanism to automatically categorize transactions. You can build this into your importer code. I want to provide a hook for the user to register a completion function that could run across all the importers where you could hook that code in.","title":"Automatic Categorization"},{"location":"importing_external_data.html#cleaning-up-payees","text":"The payees that one can find in the downloads are usually ugly names: They are sometimes the legal names of the business, which often does not reflect the street name of the place you went, for various reasons. For example, I recently ate at a restaurant called the \u201cLucky Bee\u201d in New York, and the memo from the OFX file was \u201cKING BEE\u201d. The names are sometimes abbreviated or contain some crud. In the previous example, the actual memo was \u201cKING BEE NEW YO\u201d, where \u201cNEW YO\u201d is a truncated location string. The amount of ugliness is inconsistent between data sources. It would be nice to be able to normalize the payee names by translating them at import time. I think you can do most of it using some simple rules mapping regular expressions to names provided by the user. There\u2019s really no good automated way to obtain the \u201cclean name\u201d of the payee. Beancount does not provide a hook for letting you do this this yet. It will eventually. You could also build a plugin to rename those accounts when loading your ledger. I\u2019ll build that too\u2014it\u2019s easy and would result in much nicer output.","title":"Cleaning up Payees"},{"location":"importing_external_data.html#future-work","text":"A list of things I\u2019d really want to add, beyond fortifying what\u2019s already there: A generic, configurable CSV importer which you can instantiate. I plan to play with this a bit and build a sniffer that could automatically figure out the role of each column. A hook to allow you to register a callback for post-processing transactions that works across all importers.","title":"Future Work"},{"location":"importing_external_data.html#related-discussion-threads","text":"Getting started; assigning accounts to bank .csv data Status of LedgerHub\u2026 how can I get started? Rekon wants your CSV files","title":"Related Discussion Threads"},{"location":"importing_external_data.html#historical-note","text":"There once was a first implementation of the process described in this document. The project was called LedgerHub and has been decommissioned in February 2016, rewritten and the resulting code integrated in Beancount itself, into this beancount.ingest library. The original project was intended to include the implementation of various importers to share them with other people, but this sharing was not very successful, and so the rewrite includes only the scaffolding for building your own importers and invoking them, and only a very limited number of example importer implementations. Documents about LedgerHub are preserved, and can help you understand the origins and design choices for Beancount\u2019s importer support. They can be found here: Original design Original instructions & final status (the old version of this doc) An analysis of the reasons why it the project was terminated (post-mortem) There are essentially three conceptual modes of entering such transactions: (1) a user crafts a single transaction manually, (2) another where a user inputs the two sides as a single transaction to transfer accounts, and (3) the two separate transactions get merged into a single one automatically. These are dual modes of each other. The twist in this story is that the same transaction often posts at different dates in each of its accounts. Beancount currently [March 2016] does not support multiple dates for a single transaction\u2019s postings, but a discussion is ongoing to implement support for these input modes. See this document for more details. \u21a9 The closest to universal downloader you will find in the free software world is ofxclient for OFX files, and in the commercial world, Yodlee provides a service that connects to many financial institutions. \u21a9 The \u2018pdftotext\u2019 utility in poppler provides the useful \u2018-layout\u2019 flag which outputs a text file without mangling tables, which can be helpful in the normal case of \u2018transaction-per-row\u2019 \u21a9 After sending them a few detailed emails about this and getting no response nor seeing any change in the downloaded files, I have given up on them fixing the issue. \u21a9 As you can see, this process is partly why I don\u2019t share my importers code. It requires the storage of way too much personal data in order to keep them working. \u21a9 I don\u2019t really understand why, since opening them up for viewing is almost instant, but nearly all the tools to convert them to other formats are vastly slower. \u21a9 I personally much prefer Mercurial for the clarity of its commands and output and its extensibility, but an advantage of Git\u2019s storage model is that moving files within it comes for free (no extra copy is stored). Moving files in a Mercurial repository costs you a bit in storage space. And if you rename accounts or change how you organize your files you will end up potentially copying many large files. \u21a9","title":"Historical Note"},{"location":"installing_beancount.html","text":"Installing Beancount (v2) \uf0c1 Martin Blais - Updated: June 2015 http://furius.ca/beancount/doc/install Instructions for downloading and installing Beancount on your computer. This document is about Beancount v2; Beancount v3 is in development and uses a completely different build and installation system. For instructions on building v3, see this document . Releases \uf0c1 Beancount is a mature project: the first version was written in 2008. The current rewrite of Beancount is stable. (Technically, this is what I call version 2.x beta). I\u2019m still working on this Beancount code every weekend these days, so it is very much in active development and evolving, though the great majority of the basic features are basically unchanging. I\u2019ve built an extensive suite of tests so you can consider the \u201cdefault\u201d branch of the repository as stable. New features are developed in branches and only merged in the \u201cdefault\u201d branch when fully stable (the entire battery of tests passes without failures). Changes to \u201cdefault\u201d are posted to the CHANGES file and a corresponding email is sent to the mailing-list . There's a PyPI page. Where to Get It \uf0c1 This is the official location for the source code: https://github.com/beancount/beancount Download it like this, by using Git to make a clone on your machine: git clone https://github.com/beancount/beancount How to Install \uf0c1 Installing Python \uf0c1 Beancount uses Python 3.5 1 or above, which is a pretty recent version of Python (as of this writing), and a few common library dependencies. I try to minimize dependencies, but you do have to install a few. This is very easy. First, you should have a working Python install. Install the latest stable version >=3.5 using the download from python.org . Make sure you have the development headers and libraries installed as well (e.g., the \u201cPython.h\u201d header file). For example, on a Debian/Ubuntu system you would install the python3-dev package. Beancount supports setuptools since Feb 2016, and you will need to install dependencies. You will want to have the \u201cpip3\u201d tool installed. It\u2019s probably installed by default along with Python3\u2014test this out by invoking \u201cpip3\u201d command. In any case, under a Debian/Ubuntu system you would simply install the python3-pip package. Python Dependencies \uf0c1 Note that in order to build a full working Python install from source, you will probably need to install a host of other development libraries and their corresponding header files, e.g., libxml2, libxslt1, libgdbm, libmp, libssl, etc. Installing those is dependent on your particular distribution and/or OS. Just make sure that your Python installation has all the basic modules compiled for its default configuration. Installing Beancount using pip \uf0c1 This is the easiest way to install Beancount. You just install Beancount using sudo -H python3 -m pip install beancount This should automatically download and install all the dependencies. Note, however, that this will install the latest version that was pushed to the PyPI repository and not the very latest version available from source. Releases to PyPI are made sporadically but frequently enough not to be too far behind. Consult the CHANGES file if you\u2019d like to find out what is not included since the release date. Installing Beancount using pip from Repository \uf0c1 You can also use pip to install Beancount from its source code repository directly: sudo -H python3 -m pip install git+https://github.com/beancount/beancount#egg=beancount Installing Beancount from Source \uf0c1 Installing from source offers the advantage of providing you with the very latest version of the stable branch (\u201cdefault\u201d). The default branch is as stable as the released version. Obtain the Source Code \uf0c1 Get the source code from the official repository: git clone https://github.com/beancount/beancount Install third-party dependencies \uf0c1 You might need to install some non-Python library dependencies, such as libxml2-dev, libxslt1-dev, and perhaps a few more. Try to build, it should be obvious what\u2019s missing. If on Ubuntu, use apt-get. If installing on Windows, see the Windows section below. Install Beancount from source using pip3 \uf0c1 You can then install all the dependencies and Beancount itself using pip: cd beancount sudo -H python3 -m pip install . Install Beancount from source using setup.py \uf0c1 First you\u2019ll need to install dependent libraries. You can do this using pip: sudo -H python3 -m pip install python-dateutil bottle ply lxml python-magic beautifulsoup4 Or equivalently, you may be able to do that using your distribution, e.g., on Ubuntu/Debian: sudo apt-get install python3-dateutil python3-bottle python3-ply python3-lxml python3-bs4 \u2026 Then, you can install the package in your Python library using the usual setup.py invocation: cd beancount sudo python3 setup.py install Or you can install the package in your user-local Python library using this: sudo python3 setup.py install --user Remember to add ~/.local/bin to your path to access the local install. Installing for Development \uf0c1 If you want to execute the source in-place for making changes to it, you can use the setuptools \u201cdevelop\u201d command to point to it: cd beancount sudo python3 setup.py develop Warning: This modifies a .pth file in your Python installation to point to the path to your clone. You may or may not want this. The way I work myself is old-school ; I build it locally and setup my environment to find its libraries. You build it like this: cd beancount python3 setup.py build_ext -i Finally, both the PATH and PYTHONPATH environment variables need to be updated for it: export PATH=$PATH:/path/to/beancount/bin export PYTHONPATH=$PYTHONPATH:/path/to/beancount Installing from Packages \uf0c1 Various distributions may package Beancount. Here are links to those known to exist: Arch: https://aur.archlinux.org/packages/beancount/ Windows Installation \uf0c1 Native \uf0c1 Installing this package by pip requires compiling some C++ code during the installation procedure which is only possible if an appropriate compiler is available on the computer, otherwise you will receive an error message about missing vsvarsall.bat or cl.exe . To be able to compile C++ code for Python you will need to install the same major version of the C++ compiler as your Python installation was compiled with. By running python in a console and looking for a text similar to [MSC v.1900 64 bit (AMD64)] you can determine which compiler was used for your particular Python distribution. In this example it is v.1900 . Using this number find the required Visual C++ version here . Since different versions seem to be compatible as long as the first two digits are the same you can in theory use any Visual C++ compiler between 1900 and 1999. According to my experience both Python 3.5 and 3.6 was compiled with MSC v.1900 so you can do either of the following to satisfy this requirement: Install the standalone Build Tools for Visual Studio 2017 or Install the standalone Visual C++ Build Tools 2015 or Modify an existing Visual Studio 2017 installation Start the Visual Studio 2017 installer from Add or remove programs Select Individual components Check VC++ 2017 version 15.9 v14.16 latest v141 tools or newer under Compilers, build tools, and runtimes Install Visual Studio 2019 add C++ build tools: C++ core features, MSVC v142 build tools If cl.exe is not in your path after installation, run Developer Command Prompt for Visual Studio and run the commands there. With Cygwin \uf0c1 Windows installation is, of course, a bit different. It\u2019s a breeze if you use Cygwin. You just have to prep your machine first. Here\u2019s how. Install the latest Cygwin . This may take a while (it downloads a lot of stuff), but it is well worth it in any case. But before you kick off the install, make sure the following packages are all manually enabled in the interface provided by setup.exe (they\u2019re not selected by default): python3 python3-devel python3-setuptools git make gcc-core flex bison lxml ply Start a new Cygwin bash shell (there should be a new icon on your desktop) and install the pip3 installer tool by running this command: easy_install-3.4 pip Make sure you invoke the version of easy_install which matches your Python version, e.g. easy_install-3.5 if you have Python 3.5 installed, or more. At this point, you should be able to follow the instructions from the previous sections as is, starting from \u201cInstall Beancount using pip\u201d. With WSL \uf0c1 The newly released Windows 10 Anniversary Update brings WSL 'Windows Subsystem for Linux' with bash on Ubuntu on Windows (installation instructions and more at https://msdn.microsoft.com/commandline/wsl/about ). This makes beancount installation easy, from bash: sudo apt-get install python3-pip sudo apt-get install python3-lxml sudo pip3 install m3-cdecimal sudo pip3 install beancount --pre This is not totally \u201cWindows compatible\u201d, as it is running in a pico-process, but provides a convenient way to get the Linux command-line experience on Windows. (Contrib: willwray) Notes on lxml \uf0c1 Some users have reported problems installing lxml, and a solution: when installing lxml with pip (under Cygwin), using this may help: STATIC_DEPS=true pip install lxml Checking your Install \uf0c1 You should be able to run the binaries from this document . For example, running bean-check should produce something like this: $ bean-check usage: bean-check [-h] [-v] filename bean-check: error: the following arguments are required: filename If this works, you can now go to the tutorial and begin learning how Beancount works. Reporting Problems \uf0c1 If you need to report a problem, either send email on the mailing-list or file a ticket on Github. Running the following command lists the presence and versions of dependencies installed on your computer and it might be useful to include the output of this command in your bug report: python3 -m beancount.scripts.deps Editor Support \uf0c1 There is support for some editors available: Emacs support is provided in a separate repo . See the Getting Started text for installation instruction. Support for editing with Sublime has been contributed by Martin Andreas Andersen . See his github repo . Support for editing with Vim has been contributed by Nathan Grigg . See his GitHub repo . Visual Studio Code currently has two extensions available. Both have been tested on Linux. Beancount , with syntax checking (bean-check) and support for accounts, currencies, etc. It not only allows selecting existing open accounts but also displays their balance and other metadata. Quite helpful! Beancount Formatter , which can format the whole document, aligning the numbers, etc. using bean-format. If You Have Problems \uf0c1 If you run into any installation problems, file a ticket , email me , or hit the mailing-list . Post-Installation Usage \uf0c1 Normally the scripts located under beancount/bin should be automatically installed somewhere in your executable path. For example, you should be able to just run \u201cbean-check\u201d on the command-line. Appendix \uf0c1 If everything works, you can stop reading here. Here I just discuss the various dependencies and why you need them (or why you don\u2019t, some of them are optional). This is of interest to developers and some of this info might help troubleshoot problems if you encounter any. Notes on Dependencies \uf0c1 Python 3.5 or greater \uf0c1 Python 3.5 is widely available at this point, released more than a year ago. However, my experience with open source distribution tells me that a lot of users are running on old machines that won\u2019t be upgraded for a while. So for those users, you might have to install your own Python\u2026 don\u2019t worry, installing Python manually is pretty straightforward. On Mac, it can also be installed with one of the various package management suits, like Brew, e.g., with \u201c brew install python3 \u201d. On an old Linux, download the source from python.org , and then build it like this: tar zxcf Python-3.5.2.tgz cd Python-3.5.2 ./configure make sudo make install This should just work. I recommend you install the latest 3.x release (3.5.2 at the time of this writing). Note: The reason I require at least version 3.5 is because the cdecimal library which supplies Beancount with its implementation of a Decimal representation has been added to the standard library. We need this library to represent all our numbers. Also, Beancount uses the \u201ctyping\u201d type annotations which are included in 3.5 (note: You may be able to install \u201ctyping\u201d explicitly if you use an older version; no guarantees however). Some users have reported that there are distributions which package the cdecimal support for Python3 separately. This is the case for Arch, and I\u2019ve witnessed missing cdecimal support in some Ubuntu installs as well. A check has been inserted into Beancount in December 2015 for this, and a warning should be issued if your Python installation does not have fast decimal numbers. If you are in such a situation, you can try to install cdecimal explicitly, like this: sudo pip3 install m3-cdecimal and Beancount will then use it. Python Libraries \uf0c1 If you need to install Python libraries, there are a few different ways. First, there is the easy way: there is a package management tool called \u201cpip3\u201d (or just \u201cpip\u201d for version 2 of Python) and you install libraries like this: sudo pip3 install Installing libraries from their source code is also pretty easy: download and unzip the source code, and then run this command from its root directory: sudo python3 setup.py install Just make sure the \u201cpython3\u201d executable you run when you do that is the same one you will use to run Beancount. Here are the libraries Beancount depends on and a short discussion of why. python-dateutil \uf0c1 This library provides Beancount with the ability to parse dates in various formats, it is very convenient for users to have flexible options on the command-line and in the SQL shell. bottle \uf0c1 The Beancount web interface (invoked via bean-web ) runs a little self-contained web server accessible locally on your machine. This is implemented using a tiny library that makes it easy to implement such web applications: bottle.py . ply \uf0c1 The query client ( bean-query ) which is used to extract data tables from a ledger file depends on a parser generator: I use Dave Beazley\u2019s popular PLY library (version 3.4 or above) because it makes it really easy to implement a custom SQL language parser. lxml \uf0c1 A tool is provided to bake a static HTML version of the web interface to a zip file ( bean-bake ). This is convenient to share files with an accountant who may not have your software installed. The web scraping code that is used to do that used the lxml HTML parsing library. Python Libraries for Export (optional) \uf0c1 google-api-python-client \uf0c1 Some of the scripts I use to export outputs to Google Drive (as well as scripts to maintain and download documentation from it) are checked into the codebase. You don\u2019t have to install these libraries if you\u2019re not exporting to Google Drive; everything else should work fine without them. These are thus optional. If you do install this library, it require recent (as of June 2015) installs of google-api-python-client : A Python client library for Google's discovery based APIs. oauth2client : This is a client library for accessing resources protected by OAuth 2.0, used by the Google API Python client library. httplib2 : A comprehensive HTTP client library. This is used by oauth2client. These are best installed using the pip3 tool. Update: as of 2016, you should be able to install all of the above like this: pip3 install google-api-python-client IMPORTANT: The support for Python3 is fairly recent, and if you have an old install you might experience some failures, e.g. with missing dependencies, such as a missing \u201cgflags\u201d import. Please install those from recent releases and you should be fine. You can install them manually. Python Libraries for Imports (Optional) \uf0c1 Support for importing identifying, extracting and filing transactions from externally downloaded files is built into Beancount. (This used to be in the LedgerHub project, which has been deprecated.) If you don\u2019t take advantage of those importing tools and libraries, you don\u2019t need these imports. python-magic (optional) \uf0c1 Beancount attempt to identify the types of files using the stdlib \u201cmimetypes\u201d module and some local heuristics for types which aren\u2019t supported, but in addition, it is also useful to install libmagic, which it will use if it is not present: pip3 install python-magic Note that there exists another, older library which provides libmagic support called \u201cfilemagic\u201d. You need \u201cpython-magic\u201d and not \u201cfilemagic.\u201d More confusingly, under Debian the \u201cpython-magic\u201d library is called \u201cfilemagic.\u201d Other Tools \uf0c1 It is expected that the user will build their own importers. However, Beancount will provide some modules to help you invoke various external tools. The dependencies you need for these depends on which tool you\u2019ve configured your importer to use. Virtualenv Installation \uf0c1 If you\u2019d like to use virtualenv, you can try this (suggestion by Remy X). First install Python 3.5 or beyond 2 : sudo add-apt-repository ppa:fkrull/deadsnakes sudo apt-get update sudo apt-get install python3.5 sudo apt-get install python3.5-dev sudo apt-get install libncurses5-dev Then install and activate virtualenv: sudo apt-get install virtualenv virtualenv -p /usr/bin/python3.5 bean source bean/bin/activate pip install package-name sudo apt-get install libz-dev # For lxml to build pip install lxml pip install beancount Development Setup \uf0c1 Installation for Development \uf0c1 For development, you will want to avoid installing Beancount in your Python distribution and instead just modify your PYTHONPATH environment variable to run it from source: export PYTHONPATH=/path/to/install/of/beancount Beancount has a few compiled C extension modules. This is just portable C code and should work everywhere. For development, you want to compile the C modules in-place, within the source code, and load them from there. Build the C extension module in-place like this: python3 setup.py build_ext -i Or equivalently, the root directory has a Makefile that does the same thing and that will rebuild the C code for the lexer and parser if needed (you need flex and bison installed for this): make build With this, you should be able to run the executables under the bin/ subdirectory. You may want to add this to your PATH as well. Dependencies for Development \uf0c1 Beancount needs a few more tools for development. If you\u2019re reading this, you\u2019re a developer, so I\u2019ll assume you can figure out how to install packages, skip the detail, and just list what you might need: nosetests ( nose , or python3-nose ): This is the test driver to use for running all the unit tests. You definitely need this. GNU flex : This lexer generator is needed if you intend to modify the lexer. It generated C code that chops up the input files into tokens for the parser to consume. GNU bison : This old-school parser generator is needed if you intend to modify the grammar. It generates C code that parses the tokens generated by flex. (I like using old tools like this because they are pretty low on dependencies, just C code. It should work everywhere.) GNU tar : You need this to test the archival capabilities of bean-bake. InfoZIP zip (comes with Ubuntu): You need this to test the archival capabilities of bean-bake. lxml : This XML parsing library is used in the web tests. (Unfortunately, the built-in one fails to parse large XML files.) pylint >= 1.2: I\u2019m running all the source code through this linter. If you contribute code with the intent of it being integrated and released, it has to pass the linter tests (or I\u2019ll have to make it pass myself). pyflakes : I\u2019m running all the source code through this logical error detector. If you contribute code with the intent of it being integrated and released, it has to pass those tests (or I\u2019ll have to make it pass myself). snakefood : I use snakefood to analyze the dependencies between the modules and enforce some ordering, e.g. core cannot import from plugins, for example. There\u2019s a target to run it from the root Makefile. graphviz : The dependency tree of all the modules is generated by snakefood but graphed by the graphviz tool. You need to install it if you want to look at dependencies. I think that\u2019s about it. You certainly don\u2019t need everything above, but that\u2019s the list of tools I use. If you find anything missing, please leave a comment, I may have missed something. Some people have reported bugs with the cdecimal library in 3.4. I would recommend actually installing 3.5, which appears to have fixed the problem. Technically, 3.3 will still run, but I\u2019ll deprecate it for 3.5 at some point, probably when the Ubuntu and Mac OS X distributions have it installed by default. \u21a9 Installing py3.5: http://www.jianshu.com/p/4f4b2ed568f4 \u21a9","title":"Installing Beancount"},{"location":"installing_beancount.html#installing-beancount-v2","text":"Martin Blais - Updated: June 2015 http://furius.ca/beancount/doc/install Instructions for downloading and installing Beancount on your computer. This document is about Beancount v2; Beancount v3 is in development and uses a completely different build and installation system. For instructions on building v3, see this document .","title":"Installing Beancount (v2)"},{"location":"installing_beancount.html#releases","text":"Beancount is a mature project: the first version was written in 2008. The current rewrite of Beancount is stable. (Technically, this is what I call version 2.x beta). I\u2019m still working on this Beancount code every weekend these days, so it is very much in active development and evolving, though the great majority of the basic features are basically unchanging. I\u2019ve built an extensive suite of tests so you can consider the \u201cdefault\u201d branch of the repository as stable. New features are developed in branches and only merged in the \u201cdefault\u201d branch when fully stable (the entire battery of tests passes without failures). Changes to \u201cdefault\u201d are posted to the CHANGES file and a corresponding email is sent to the mailing-list . There's a PyPI page.","title":"Releases"},{"location":"installing_beancount.html#where-to-get-it","text":"This is the official location for the source code: https://github.com/beancount/beancount Download it like this, by using Git to make a clone on your machine: git clone https://github.com/beancount/beancount","title":"Where to Get It"},{"location":"installing_beancount.html#how-to-install","text":"","title":"How to Install"},{"location":"installing_beancount.html#installing-python","text":"Beancount uses Python 3.5 1 or above, which is a pretty recent version of Python (as of this writing), and a few common library dependencies. I try to minimize dependencies, but you do have to install a few. This is very easy. First, you should have a working Python install. Install the latest stable version >=3.5 using the download from python.org . Make sure you have the development headers and libraries installed as well (e.g., the \u201cPython.h\u201d header file). For example, on a Debian/Ubuntu system you would install the python3-dev package. Beancount supports setuptools since Feb 2016, and you will need to install dependencies. You will want to have the \u201cpip3\u201d tool installed. It\u2019s probably installed by default along with Python3\u2014test this out by invoking \u201cpip3\u201d command. In any case, under a Debian/Ubuntu system you would simply install the python3-pip package.","title":"Installing Python"},{"location":"installing_beancount.html#python-dependencies","text":"Note that in order to build a full working Python install from source, you will probably need to install a host of other development libraries and their corresponding header files, e.g., libxml2, libxslt1, libgdbm, libmp, libssl, etc. Installing those is dependent on your particular distribution and/or OS. Just make sure that your Python installation has all the basic modules compiled for its default configuration.","title":"Python Dependencies"},{"location":"installing_beancount.html#installing-beancount-using-pip","text":"This is the easiest way to install Beancount. You just install Beancount using sudo -H python3 -m pip install beancount This should automatically download and install all the dependencies. Note, however, that this will install the latest version that was pushed to the PyPI repository and not the very latest version available from source. Releases to PyPI are made sporadically but frequently enough not to be too far behind. Consult the CHANGES file if you\u2019d like to find out what is not included since the release date.","title":"Installing Beancount using pip"},{"location":"installing_beancount.html#installing-beancount-using-pip-from-repository","text":"You can also use pip to install Beancount from its source code repository directly: sudo -H python3 -m pip install git+https://github.com/beancount/beancount#egg=beancount","title":"Installing Beancount using pip from Repository"},{"location":"installing_beancount.html#installing-beancount-from-source","text":"Installing from source offers the advantage of providing you with the very latest version of the stable branch (\u201cdefault\u201d). The default branch is as stable as the released version.","title":"Installing Beancount from Source"},{"location":"installing_beancount.html#obtain-the-source-code","text":"Get the source code from the official repository: git clone https://github.com/beancount/beancount","title":"Obtain the Source Code"},{"location":"installing_beancount.html#install-third-party-dependencies","text":"You might need to install some non-Python library dependencies, such as libxml2-dev, libxslt1-dev, and perhaps a few more. Try to build, it should be obvious what\u2019s missing. If on Ubuntu, use apt-get. If installing on Windows, see the Windows section below.","title":"Install third-party dependencies"},{"location":"installing_beancount.html#install-beancount-from-source-using-pip3","text":"You can then install all the dependencies and Beancount itself using pip: cd beancount sudo -H python3 -m pip install .","title":"Install Beancount from source using pip3"},{"location":"installing_beancount.html#install-beancount-from-source-using-setuppy","text":"First you\u2019ll need to install dependent libraries. You can do this using pip: sudo -H python3 -m pip install python-dateutil bottle ply lxml python-magic beautifulsoup4 Or equivalently, you may be able to do that using your distribution, e.g., on Ubuntu/Debian: sudo apt-get install python3-dateutil python3-bottle python3-ply python3-lxml python3-bs4 \u2026 Then, you can install the package in your Python library using the usual setup.py invocation: cd beancount sudo python3 setup.py install Or you can install the package in your user-local Python library using this: sudo python3 setup.py install --user Remember to add ~/.local/bin to your path to access the local install.","title":"Install Beancount from source using setup.py"},{"location":"installing_beancount.html#installing-for-development","text":"If you want to execute the source in-place for making changes to it, you can use the setuptools \u201cdevelop\u201d command to point to it: cd beancount sudo python3 setup.py develop Warning: This modifies a .pth file in your Python installation to point to the path to your clone. You may or may not want this. The way I work myself is old-school ; I build it locally and setup my environment to find its libraries. You build it like this: cd beancount python3 setup.py build_ext -i Finally, both the PATH and PYTHONPATH environment variables need to be updated for it: export PATH=$PATH:/path/to/beancount/bin export PYTHONPATH=$PYTHONPATH:/path/to/beancount","title":"Installing for Development"},{"location":"installing_beancount.html#installing-from-packages","text":"Various distributions may package Beancount. Here are links to those known to exist: Arch: https://aur.archlinux.org/packages/beancount/","title":"Installing from Packages"},{"location":"installing_beancount.html#windows-installation","text":"","title":"Windows Installation"},{"location":"installing_beancount.html#native","text":"Installing this package by pip requires compiling some C++ code during the installation procedure which is only possible if an appropriate compiler is available on the computer, otherwise you will receive an error message about missing vsvarsall.bat or cl.exe . To be able to compile C++ code for Python you will need to install the same major version of the C++ compiler as your Python installation was compiled with. By running python in a console and looking for a text similar to [MSC v.1900 64 bit (AMD64)] you can determine which compiler was used for your particular Python distribution. In this example it is v.1900 . Using this number find the required Visual C++ version here . Since different versions seem to be compatible as long as the first two digits are the same you can in theory use any Visual C++ compiler between 1900 and 1999. According to my experience both Python 3.5 and 3.6 was compiled with MSC v.1900 so you can do either of the following to satisfy this requirement: Install the standalone Build Tools for Visual Studio 2017 or Install the standalone Visual C++ Build Tools 2015 or Modify an existing Visual Studio 2017 installation Start the Visual Studio 2017 installer from Add or remove programs Select Individual components Check VC++ 2017 version 15.9 v14.16 latest v141 tools or newer under Compilers, build tools, and runtimes Install Visual Studio 2019 add C++ build tools: C++ core features, MSVC v142 build tools If cl.exe is not in your path after installation, run Developer Command Prompt for Visual Studio and run the commands there.","title":"Native"},{"location":"installing_beancount.html#with-cygwin","text":"Windows installation is, of course, a bit different. It\u2019s a breeze if you use Cygwin. You just have to prep your machine first. Here\u2019s how. Install the latest Cygwin . This may take a while (it downloads a lot of stuff), but it is well worth it in any case. But before you kick off the install, make sure the following packages are all manually enabled in the interface provided by setup.exe (they\u2019re not selected by default): python3 python3-devel python3-setuptools git make gcc-core flex bison lxml ply Start a new Cygwin bash shell (there should be a new icon on your desktop) and install the pip3 installer tool by running this command: easy_install-3.4 pip Make sure you invoke the version of easy_install which matches your Python version, e.g. easy_install-3.5 if you have Python 3.5 installed, or more. At this point, you should be able to follow the instructions from the previous sections as is, starting from \u201cInstall Beancount using pip\u201d.","title":"With Cygwin"},{"location":"installing_beancount.html#with-wsl","text":"The newly released Windows 10 Anniversary Update brings WSL 'Windows Subsystem for Linux' with bash on Ubuntu on Windows (installation instructions and more at https://msdn.microsoft.com/commandline/wsl/about ). This makes beancount installation easy, from bash: sudo apt-get install python3-pip sudo apt-get install python3-lxml sudo pip3 install m3-cdecimal sudo pip3 install beancount --pre This is not totally \u201cWindows compatible\u201d, as it is running in a pico-process, but provides a convenient way to get the Linux command-line experience on Windows. (Contrib: willwray)","title":"With WSL"},{"location":"installing_beancount.html#notes-on-lxml","text":"Some users have reported problems installing lxml, and a solution: when installing lxml with pip (under Cygwin), using this may help: STATIC_DEPS=true pip install lxml","title":"Notes on lxml"},{"location":"installing_beancount.html#checking-your-install","text":"You should be able to run the binaries from this document . For example, running bean-check should produce something like this: $ bean-check usage: bean-check [-h] [-v] filename bean-check: error: the following arguments are required: filename If this works, you can now go to the tutorial and begin learning how Beancount works.","title":"Checking your Install"},{"location":"installing_beancount.html#reporting-problems","text":"If you need to report a problem, either send email on the mailing-list or file a ticket on Github. Running the following command lists the presence and versions of dependencies installed on your computer and it might be useful to include the output of this command in your bug report: python3 -m beancount.scripts.deps","title":"Reporting Problems"},{"location":"installing_beancount.html#editor-support","text":"There is support for some editors available: Emacs support is provided in a separate repo . See the Getting Started text for installation instruction. Support for editing with Sublime has been contributed by Martin Andreas Andersen . See his github repo . Support for editing with Vim has been contributed by Nathan Grigg . See his GitHub repo . Visual Studio Code currently has two extensions available. Both have been tested on Linux. Beancount , with syntax checking (bean-check) and support for accounts, currencies, etc. It not only allows selecting existing open accounts but also displays their balance and other metadata. Quite helpful! Beancount Formatter , which can format the whole document, aligning the numbers, etc. using bean-format.","title":"Editor Support"},{"location":"installing_beancount.html#if-you-have-problems","text":"If you run into any installation problems, file a ticket , email me , or hit the mailing-list .","title":"If You Have Problems"},{"location":"installing_beancount.html#post-installation-usage","text":"Normally the scripts located under beancount/bin should be automatically installed somewhere in your executable path. For example, you should be able to just run \u201cbean-check\u201d on the command-line.","title":"Post-Installation Usage"},{"location":"installing_beancount.html#appendix","text":"If everything works, you can stop reading here. Here I just discuss the various dependencies and why you need them (or why you don\u2019t, some of them are optional). This is of interest to developers and some of this info might help troubleshoot problems if you encounter any.","title":"Appendix"},{"location":"installing_beancount.html#notes-on-dependencies","text":"","title":"Notes on Dependencies"},{"location":"installing_beancount.html#python-35-or-greater","text":"Python 3.5 is widely available at this point, released more than a year ago. However, my experience with open source distribution tells me that a lot of users are running on old machines that won\u2019t be upgraded for a while. So for those users, you might have to install your own Python\u2026 don\u2019t worry, installing Python manually is pretty straightforward. On Mac, it can also be installed with one of the various package management suits, like Brew, e.g., with \u201c brew install python3 \u201d. On an old Linux, download the source from python.org , and then build it like this: tar zxcf Python-3.5.2.tgz cd Python-3.5.2 ./configure make sudo make install This should just work. I recommend you install the latest 3.x release (3.5.2 at the time of this writing). Note: The reason I require at least version 3.5 is because the cdecimal library which supplies Beancount with its implementation of a Decimal representation has been added to the standard library. We need this library to represent all our numbers. Also, Beancount uses the \u201ctyping\u201d type annotations which are included in 3.5 (note: You may be able to install \u201ctyping\u201d explicitly if you use an older version; no guarantees however). Some users have reported that there are distributions which package the cdecimal support for Python3 separately. This is the case for Arch, and I\u2019ve witnessed missing cdecimal support in some Ubuntu installs as well. A check has been inserted into Beancount in December 2015 for this, and a warning should be issued if your Python installation does not have fast decimal numbers. If you are in such a situation, you can try to install cdecimal explicitly, like this: sudo pip3 install m3-cdecimal and Beancount will then use it.","title":"Python 3.5 or greater"},{"location":"installing_beancount.html#python-libraries","text":"If you need to install Python libraries, there are a few different ways. First, there is the easy way: there is a package management tool called \u201cpip3\u201d (or just \u201cpip\u201d for version 2 of Python) and you install libraries like this: sudo pip3 install Installing libraries from their source code is also pretty easy: download and unzip the source code, and then run this command from its root directory: sudo python3 setup.py install Just make sure the \u201cpython3\u201d executable you run when you do that is the same one you will use to run Beancount. Here are the libraries Beancount depends on and a short discussion of why.","title":"Python Libraries"},{"location":"installing_beancount.html#python-dateutil","text":"This library provides Beancount with the ability to parse dates in various formats, it is very convenient for users to have flexible options on the command-line and in the SQL shell.","title":"python-dateutil"},{"location":"installing_beancount.html#bottle","text":"The Beancount web interface (invoked via bean-web ) runs a little self-contained web server accessible locally on your machine. This is implemented using a tiny library that makes it easy to implement such web applications: bottle.py .","title":"bottle"},{"location":"installing_beancount.html#ply","text":"The query client ( bean-query ) which is used to extract data tables from a ledger file depends on a parser generator: I use Dave Beazley\u2019s popular PLY library (version 3.4 or above) because it makes it really easy to implement a custom SQL language parser.","title":"ply"},{"location":"installing_beancount.html#lxml","text":"A tool is provided to bake a static HTML version of the web interface to a zip file ( bean-bake ). This is convenient to share files with an accountant who may not have your software installed. The web scraping code that is used to do that used the lxml HTML parsing library.","title":"lxml"},{"location":"installing_beancount.html#python-libraries-for-export-optional","text":"","title":"Python Libraries for Export (optional)"},{"location":"installing_beancount.html#google-api-python-client","text":"Some of the scripts I use to export outputs to Google Drive (as well as scripts to maintain and download documentation from it) are checked into the codebase. You don\u2019t have to install these libraries if you\u2019re not exporting to Google Drive; everything else should work fine without them. These are thus optional. If you do install this library, it require recent (as of June 2015) installs of google-api-python-client : A Python client library for Google's discovery based APIs. oauth2client : This is a client library for accessing resources protected by OAuth 2.0, used by the Google API Python client library. httplib2 : A comprehensive HTTP client library. This is used by oauth2client. These are best installed using the pip3 tool. Update: as of 2016, you should be able to install all of the above like this: pip3 install google-api-python-client IMPORTANT: The support for Python3 is fairly recent, and if you have an old install you might experience some failures, e.g. with missing dependencies, such as a missing \u201cgflags\u201d import. Please install those from recent releases and you should be fine. You can install them manually.","title":"google-api-python-client"},{"location":"installing_beancount.html#python-libraries-for-imports-optional","text":"Support for importing identifying, extracting and filing transactions from externally downloaded files is built into Beancount. (This used to be in the LedgerHub project, which has been deprecated.) If you don\u2019t take advantage of those importing tools and libraries, you don\u2019t need these imports.","title":"Python Libraries for Imports (Optional)"},{"location":"installing_beancount.html#python-magic-optional","text":"Beancount attempt to identify the types of files using the stdlib \u201cmimetypes\u201d module and some local heuristics for types which aren\u2019t supported, but in addition, it is also useful to install libmagic, which it will use if it is not present: pip3 install python-magic Note that there exists another, older library which provides libmagic support called \u201cfilemagic\u201d. You need \u201cpython-magic\u201d and not \u201cfilemagic.\u201d More confusingly, under Debian the \u201cpython-magic\u201d library is called \u201cfilemagic.\u201d","title":"python-magic (optional)"},{"location":"installing_beancount.html#other-tools","text":"It is expected that the user will build their own importers. However, Beancount will provide some modules to help you invoke various external tools. The dependencies you need for these depends on which tool you\u2019ve configured your importer to use.","title":"Other Tools"},{"location":"installing_beancount.html#virtualenv-installation","text":"If you\u2019d like to use virtualenv, you can try this (suggestion by Remy X). First install Python 3.5 or beyond 2 : sudo add-apt-repository ppa:fkrull/deadsnakes sudo apt-get update sudo apt-get install python3.5 sudo apt-get install python3.5-dev sudo apt-get install libncurses5-dev Then install and activate virtualenv: sudo apt-get install virtualenv virtualenv -p /usr/bin/python3.5 bean source bean/bin/activate pip install package-name sudo apt-get install libz-dev # For lxml to build pip install lxml pip install beancount","title":"Virtualenv Installation"},{"location":"installing_beancount.html#development-setup","text":"","title":"Development Setup"},{"location":"installing_beancount.html#installation-for-development","text":"For development, you will want to avoid installing Beancount in your Python distribution and instead just modify your PYTHONPATH environment variable to run it from source: export PYTHONPATH=/path/to/install/of/beancount Beancount has a few compiled C extension modules. This is just portable C code and should work everywhere. For development, you want to compile the C modules in-place, within the source code, and load them from there. Build the C extension module in-place like this: python3 setup.py build_ext -i Or equivalently, the root directory has a Makefile that does the same thing and that will rebuild the C code for the lexer and parser if needed (you need flex and bison installed for this): make build With this, you should be able to run the executables under the bin/ subdirectory. You may want to add this to your PATH as well.","title":"Installation for Development"},{"location":"installing_beancount.html#dependencies-for-development","text":"Beancount needs a few more tools for development. If you\u2019re reading this, you\u2019re a developer, so I\u2019ll assume you can figure out how to install packages, skip the detail, and just list what you might need: nosetests ( nose , or python3-nose ): This is the test driver to use for running all the unit tests. You definitely need this. GNU flex : This lexer generator is needed if you intend to modify the lexer. It generated C code that chops up the input files into tokens for the parser to consume. GNU bison : This old-school parser generator is needed if you intend to modify the grammar. It generates C code that parses the tokens generated by flex. (I like using old tools like this because they are pretty low on dependencies, just C code. It should work everywhere.) GNU tar : You need this to test the archival capabilities of bean-bake. InfoZIP zip (comes with Ubuntu): You need this to test the archival capabilities of bean-bake. lxml : This XML parsing library is used in the web tests. (Unfortunately, the built-in one fails to parse large XML files.) pylint >= 1.2: I\u2019m running all the source code through this linter. If you contribute code with the intent of it being integrated and released, it has to pass the linter tests (or I\u2019ll have to make it pass myself). pyflakes : I\u2019m running all the source code through this logical error detector. If you contribute code with the intent of it being integrated and released, it has to pass those tests (or I\u2019ll have to make it pass myself). snakefood : I use snakefood to analyze the dependencies between the modules and enforce some ordering, e.g. core cannot import from plugins, for example. There\u2019s a target to run it from the root Makefile. graphviz : The dependency tree of all the modules is generated by snakefood but graphed by the graphviz tool. You need to install it if you want to look at dependencies. I think that\u2019s about it. You certainly don\u2019t need everything above, but that\u2019s the list of tools I use. If you find anything missing, please leave a comment, I may have missed something. Some people have reported bugs with the cdecimal library in 3.4. I would recommend actually installing 3.5, which appears to have fixed the problem. Technically, 3.3 will still run, but I\u2019ll deprecate it for 3.5 at some point, probably when the Ubuntu and Mac OS X distributions have it installed by default. \u21a9 Installing py3.5: http://www.jianshu.com/p/4f4b2ed568f4 \u21a9","title":"Dependencies for Development"},{"location":"installing_beancount_v3.html","text":"Installing Beancount (C++ version) \uf0c1 Martin Blais - July 2020 http://furius.ca/beancount/doc/v3-install Instructions for downloading and running Beancount v3 (in development) on your computer. For v2, see this document instead: Beancount - Install (v2) This document is about Beancount v3, an experimental in-development version (as of July 2020); Instructions for building the stable version (Beancount v2) can be found in this other document . Setup Python \uf0c1 Python dependencies are still required to run programs. pip install \u2013r requirements/dev.txt Building with Bazel \uf0c1 Warning: This is an experimental development branch. Do not expect everything to be polished perfectly. Bazel Dependencies \uf0c1 Beancount v3 uses the Bazel build system, which for the most part insulates you from local installs and dependencies from your computer. The dependencies to install are: Bazel itself. Follow instructions on https://bazel.build/ A C++ compiler. Either g++ or clang works. I'm using clang-11. A Python runtime (version 3.8 or above). Install from your distribution. Bazel will download and compile all the libraries it requires itself (even the code generators, e.g., Bison), building them at their precise versions as specified in the build, so you will not have to worry about them. Building & Testing \uf0c1 Simply run the following command: bazel build //bin:all There is currently no installation script, you have to run from source. You can run individual programs (e.g. bean-check) with this command: bazel run //bin:bean_check -- /path/to/ledger.beancount Or if you don't care to automatically rebuild modified code, like this: ./bazel-bin/bin/bean_check /path/to/ledger.beancount Development \uf0c1 You can run all the unit tests like this: bazel test //... Because Bazel has a detailed account of all dependencies, re-running this command after modifying code will result in only the touched targets being re-tested; this makes iterative development with testing a bit more fun. Another advantage is that since all the libraries the build depends on are downloaded and built, while this can be slow on the first build, it allows us to use very recently released versions of the code we depend on. Targets are defined in BUILD files local to their directories. All the build rules can be found under //third_party. Ingestion \uf0c1 The ingestion code involves importing code that lives outside the repository. Bazel binaries are self-contained and will fail to import modules that haven't been declared as dependencies, so running the //bin:bean_extract target, for example, probably won't work. This does not work yet (short of building your import configuration as a py_binary() target that would explicitly link to Beancount). This is doable without writing much Bazel code by defining a suitable WORKSPACE file that fetches the rules from it. I haven't produced an example of this yet (TBD). As a workaround, you can set your PYTHONPATH to import from the source location and create a symlink to the parser .so file beforehand. You can do it like this: make bazel-link TBD \uf0c1 A few build integration tasks remain to be done: pytype is not supported yet. pylint is not integrated in the build either. Installation for development with meson \uf0c1 Note : this section is updated base on the following discussion: https://groups.google.com/g/beancount/c/7ppbyz_5B5w The Bazel build for Beancount v3 builds some experimental C++ code that is at the time of writing (9 March 2024) not yet used for anything else than a technology demonstration. In \u201cproduction\u201d v3 uses meson-python to build the extension modules and pack them up in a Python wheel. This is what is used by pip during installation. This section describes installation processes for the purposes of development beancount v3 python code, rather than experimenting with C++ code. On Linux \uf0c1 Tested with python3.12 on Ubuntu Related links https://mesonbuild.com/meson-python/how-to-guides/editable-installs.html git clone https://github.com/beancount/beancount.git Create and activate virtual environment python3.12 -m venv beancount/venv . beancount/venv/bin/activate cd beancount Install required packages. python -m pip install meson-python meson ninja Install beancount in editable mode with no build isolation python -m pip install --no-build-isolation --editable . Note: There is an opinion that --no-build-isolation option is not needed, but it was also mentioned that the installation wasn\u2019t working without this option. Also the documentation suggests that this option is needed. This may depend on the type of Linux environment Install pytest python -m pip install pytest Run the tests and make sure everything is ok: pytest --import-mode=importlib beancount/ On Windows \uf0c1 Tested on 64 bit Windows 10 Pro prerequisites Install Microsoft Visual Studio (tested on v 2022) Procedure git clone https://github.com/beancount/beancount.git cd beancount If running x64 bit Windows, start the \"x64 Native Tools Command Prompt for VS 20XX\". To do this press and release the Windows keys and type x64 Open this prompt Go to the directory, where beancount is installed C:\\Program Files\\Microsoft Visual Studio\\2022\\Community>cd C:\\_code\\t\\beancount C:\\_code\\t\\beancount> Activate the virtual environment C:\\_code\\t\\beancount>venv\\Scripts\\Activate (venv) C:\\_code\\t\\beancount> Instal required packages (venv) C:\\_code\\t\\beancount>py -m pip install meson-python meson ninja Install beancount in editable mode. Unlike on Linux the --no-build-isolation on Windows from one side throws some errors from the other side does not seem to be needed (venv) C:\\_code\\t\\beancount> py -m pip install --editable . Install pytest (venv) C:\\_code\\t\\beancount>py -m pip install pytest Run tests (venv) C:\\_code\\t\\beancount>pytest --import-mode=importlib beancount Note: some of the tests on Windows fail, but this is due to general portability issue","title":"Installing Beancount"},{"location":"installing_beancount_v3.html#installing-beancount-c-version","text":"Martin Blais - July 2020 http://furius.ca/beancount/doc/v3-install Instructions for downloading and running Beancount v3 (in development) on your computer. For v2, see this document instead: Beancount - Install (v2) This document is about Beancount v3, an experimental in-development version (as of July 2020); Instructions for building the stable version (Beancount v2) can be found in this other document .","title":"Installing Beancount (C++ version)"},{"location":"installing_beancount_v3.html#setup-python","text":"Python dependencies are still required to run programs. pip install \u2013r requirements/dev.txt","title":"Setup Python"},{"location":"installing_beancount_v3.html#building-with-bazel","text":"Warning: This is an experimental development branch. Do not expect everything to be polished perfectly.","title":"Building with Bazel"},{"location":"installing_beancount_v3.html#bazel-dependencies","text":"Beancount v3 uses the Bazel build system, which for the most part insulates you from local installs and dependencies from your computer. The dependencies to install are: Bazel itself. Follow instructions on https://bazel.build/ A C++ compiler. Either g++ or clang works. I'm using clang-11. A Python runtime (version 3.8 or above). Install from your distribution. Bazel will download and compile all the libraries it requires itself (even the code generators, e.g., Bison), building them at their precise versions as specified in the build, so you will not have to worry about them.","title":"Bazel Dependencies"},{"location":"installing_beancount_v3.html#building-testing","text":"Simply run the following command: bazel build //bin:all There is currently no installation script, you have to run from source. You can run individual programs (e.g. bean-check) with this command: bazel run //bin:bean_check -- /path/to/ledger.beancount Or if you don't care to automatically rebuild modified code, like this: ./bazel-bin/bin/bean_check /path/to/ledger.beancount","title":"Building & Testing"},{"location":"installing_beancount_v3.html#development","text":"You can run all the unit tests like this: bazel test //... Because Bazel has a detailed account of all dependencies, re-running this command after modifying code will result in only the touched targets being re-tested; this makes iterative development with testing a bit more fun. Another advantage is that since all the libraries the build depends on are downloaded and built, while this can be slow on the first build, it allows us to use very recently released versions of the code we depend on. Targets are defined in BUILD files local to their directories. All the build rules can be found under //third_party.","title":"Development"},{"location":"installing_beancount_v3.html#ingestion","text":"The ingestion code involves importing code that lives outside the repository. Bazel binaries are self-contained and will fail to import modules that haven't been declared as dependencies, so running the //bin:bean_extract target, for example, probably won't work. This does not work yet (short of building your import configuration as a py_binary() target that would explicitly link to Beancount). This is doable without writing much Bazel code by defining a suitable WORKSPACE file that fetches the rules from it. I haven't produced an example of this yet (TBD). As a workaround, you can set your PYTHONPATH to import from the source location and create a symlink to the parser .so file beforehand. You can do it like this: make bazel-link","title":"Ingestion"},{"location":"installing_beancount_v3.html#tbd","text":"A few build integration tasks remain to be done: pytype is not supported yet. pylint is not integrated in the build either.","title":"TBD"},{"location":"installing_beancount_v3.html#installation-for-development-with-meson","text":"Note : this section is updated base on the following discussion: https://groups.google.com/g/beancount/c/7ppbyz_5B5w The Bazel build for Beancount v3 builds some experimental C++ code that is at the time of writing (9 March 2024) not yet used for anything else than a technology demonstration. In \u201cproduction\u201d v3 uses meson-python to build the extension modules and pack them up in a Python wheel. This is what is used by pip during installation. This section describes installation processes for the purposes of development beancount v3 python code, rather than experimenting with C++ code.","title":"Installation for development with meson"},{"location":"installing_beancount_v3.html#on-linux","text":"Tested with python3.12 on Ubuntu Related links https://mesonbuild.com/meson-python/how-to-guides/editable-installs.html git clone https://github.com/beancount/beancount.git Create and activate virtual environment python3.12 -m venv beancount/venv . beancount/venv/bin/activate cd beancount Install required packages. python -m pip install meson-python meson ninja Install beancount in editable mode with no build isolation python -m pip install --no-build-isolation --editable . Note: There is an opinion that --no-build-isolation option is not needed, but it was also mentioned that the installation wasn\u2019t working without this option. Also the documentation suggests that this option is needed. This may depend on the type of Linux environment Install pytest python -m pip install pytest Run the tests and make sure everything is ok: pytest --import-mode=importlib beancount/","title":"On Linux"},{"location":"installing_beancount_v3.html#on-windows","text":"Tested on 64 bit Windows 10 Pro prerequisites Install Microsoft Visual Studio (tested on v 2022) Procedure git clone https://github.com/beancount/beancount.git cd beancount If running x64 bit Windows, start the \"x64 Native Tools Command Prompt for VS 20XX\". To do this press and release the Windows keys and type x64 Open this prompt Go to the directory, where beancount is installed C:\\Program Files\\Microsoft Visual Studio\\2022\\Community>cd C:\\_code\\t\\beancount C:\\_code\\t\\beancount> Activate the virtual environment C:\\_code\\t\\beancount>venv\\Scripts\\Activate (venv) C:\\_code\\t\\beancount> Instal required packages (venv) C:\\_code\\t\\beancount>py -m pip install meson-python meson ninja Install beancount in editable mode. Unlike on Linux the --no-build-isolation on Windows from one side throws some errors from the other side does not seem to be needed (venv) C:\\_code\\t\\beancount> py -m pip install --editable . Install pytest (venv) C:\\_code\\t\\beancount>py -m pip install pytest Run tests (venv) C:\\_code\\t\\beancount>pytest --import-mode=importlib beancount Note: some of the tests on Windows fail, but this is due to general portability issue","title":"On Windows"},{"location":"ledgerhub_design_doc.html","text":"Design Doc for Ledgerhub \uf0c1 Martin Blais , February 2014 http://furius.ca/beancount/doc/ledgerhub-design-doc Motivation Goals & Stages Details of Stages Fetching Identification Extraction Transform Rendering Filing Implementation Details Importers Interface References Please note that this document is the original design doc for LedgerHub. LedgerHub is being transitioned back to Beancount. See this postmortem document for details [blais, 2015-12]. Motivation \uf0c1 Several open source projects currently exist that provide the capability to create double-entry transactions for bookkeeping from a text file input. These various double-entry bookkeeping projects include Beancount , Ledger , HLedger , Abandon , and they are independent implementations of a similar goal: the creation of an in-memory representation for double-entry accounting transactions from a text file, and the production of various reports from it, such as balance sheets, income statements, journals, and others. Each implementation explores slightly different feature sets, but essentially all work by reading their input from a file whose format is custom declarative language that describe the transactions, a language which is meant to be written by humans and whose syntax is designed with that goal in mind. While the languages do vary somewhat, the underlying data structures that they define are fairly similar. An essential part of the process of regularly updating one\u2019s journal files is the replication of a real-world account\u2019s transaction detail to a single input file in a consistent data format. This is essentially a translation step, meant to bring the transaction details of many institutions\u2019 accounts into a single system. Various banks and credit card companies provide downloadable transaction data in either Quicken or Microsoft Money (OFX) formats, and many institutions provide custom CSV files with transaction detail. Moreover, many of these institutions also make regular statements available for download as PDF files, and these can be associated with one\u2019s ledger accounts. The process of translating these external data formats can be automated to some extent. These various files can be translated to output text that can then be massaged by the user to be integrated into input file formats accepted by a double-entry bookkeeping package. Several projects have begun to make inroads in that domain: Ledger-autosync aims at fetching transactions automatically from OFX servers for and translating them for Ledger and HLedger, and Reckon converts CSV files for Ledger. Beancount includes code that can automate the identification of downloaded files to the accounts from a ledger, extract their transaction detail, and automatically file them to a directory hierarchy that mirrors the ledger\u2019s chart of accounts. This code should probably live outside of Beancount. Ledger also sports a \u201cconvert\u201d command that attempts to do similar things and a CSV2Ledger Perl script is available that can convert CSV files. HLedger also had a convert command which translated CSV files with optional conversion hints defined in a separate file; HLedger now does the same conversion on-the-fly when the input file is CSV (i.e., CSV is considered a first-class input format). The programs that fetch and convert external data files do not have to be tied to a single system. Moreover, this is often cumbersome code that would benefit greatly from having a large number of contributors, which could each benefit each other from having common parsers ready and working for the various institutions that they\u2019re using or likely to use in the future. I - the author of Beancount - have decided to move Beancount\u2019s importing and filing source code outside of its home project and to decouple it from the Beancount source code, so that others can contribute to it, with the intent of providing project-agnostic functionality. This document describes the goals and design of this project. Goals & Stages \uf0c1 This new project should address the following aspects in a project-agnostic manner: Fetching : Automate obtaining the external data files by connecting to the data sources directly. External tools and libraries such as ofxclient for OFX sources can be leveraged for this purpose. Web scraping could be used to fetch downloadable files where possible. The output of this stage is a list of institution-specific files downloaded to a directory. Note that fetching does not just apply to transaction data here; we will also support fetching prices . A list of (date, price) entries may be created from this data. We will likely want to support an intermediate format for expressing a list of positions (and appropriate support in the ledgerhub-Ledger/Beancount/HLedger interface to obtain it). Identification : Given a filename and its contents, automatically guess which institution and account configuration the file is for, and ideally be able to extract the date from the file or statement. This should also work with PDF files. The output of this stage is an association of each input file to a particular extractor and configuration (e.g. a particular account name). Extraction : Parse each file (if possible) and extract a list of information required to generate double-entry transactions data structures from it, in some sort of generic data structure, such as dicts of strings and numbers, independent of the underlying project\u2019s desired output. If possible, a verbatim snippet of the original text that generated the transaction should be attached to the output data structure. The output of this stage is a data structure, e.g., a list of Python dictionaries in some defined format. Transform : Given some information from the past transaction history contained in a journal, using simple learning algorithms, a program should be able to apply transformations on the transactional information extracted from the previous step. The most common use case for this is to automatically add a categorization posting to transactions that have a single posting only. For example, transactions from credit card statements typically include the changes in balance of the credit card account but all transactions are left to be manually associated with a particular expense account. Some of this process can be automated at this stage. Rendering : Convert the internal transactions data structures to the particular syntax of a double-entry bookkeeping project implementation and to the particular desired syntax variants (e.g. currency formatting, comma vs. dot decimal separator, localized input date format). This steps spits out text to be inserted into an input file compatible with the ledger software of choice. Filing : Sanitize the downloaded files\u2019 filenames and move them into a well organized and structured directory hierarchy corresponding to the identified account. This can run from the same associations derived in the identification step. Apart from the Render stage, all the other stages should be implemented without regard for a particular project, this should work across all ledger implementations. The Rendering code, however, should specialize, import source code, and attempt to add as many of the particular features provided by each project to its output text. Where necessary, interfaces to obtain particular data sets from each ledger implementation\u2019s input files should be provided to shield the common code from the particular implementation details of that project. For instance, a categorization Transform step would need to train its algorithm on some of the transaction data (i.e., the narration fields and perhaps some of the amounts, account names, and dates). Each project should provide a way to obtain the necessary data from its input data file, in the same format. Details of Stages \uf0c1 Fetching \uf0c1 By default, a user should be able to click their way to their institution\u2019s website and download documents to their ~/Downloads directory. A directory with some files in it should be the reasonable default input to the identification stage. This directory should be allowed to have other/garbage files in it, the identification step should be able to skip those automatically. A module that can automatically fetch the data needs to be implemented. Ideally this would not require an external tool. The data extracted should also have a copy saved in some Downloads directory. This is the domain of the ledger-autosync project. Perhaps we should coordinate input/outputs or even integrate call some of its library code at this stage. The author notes that fetching data from OFX servers is pretty easy, though the begin/end dates will have to get processed and filtered. Automatic fetching support will vary widely depending on where the institutions are located. Some places have solid support, some less. Use the data from ofxhome.com to configure. Fetching Prices \uf0c1 For fetching prices, there are many libraries out there. Initially we will port Beancount\u2019s bean-prices to ledgerhub. Identification \uf0c1 The identification stage consists in running a driver program that Searches for files in a directory hierarchy (typically your ~/Downloads folder) If necessary, converts the files into some text/ascii format, so that regular expressions can be matched against it (even if the output is messy, e.g., with PDF files converted to ASCII). This works well for PDF files: despite the fact that we cannot typically extract transactional data from them, we can generally pretty reliably identify which account they\u2019re for and almost always extract the statement date as well. Check a list of regular expressions against the ASCII\u2019fied contents. If the regular expressions all match, the configuration is associated to the filename. Note that more than one configuration may be associated to the same file because some files contain many sections, sections for which different importers may be called on to extract their data (e.g., OFX banking + OFX credit card can be mixed in the same file, and some institutions do). The net result of this process is an association of each filename with the a specific importer object instantiated in the configuration file. These importer objects are created with a set of required account names which they use to produce the Ledger-like syntax from the downloaded file that was associated with it. Here is an example configuration for two importers: from ledgerhub.sources.rbc import rbcinvesting, rbcpdf CONFIG = [ ... (('FileType: application/vnd.ms-excel', r'Filename: .*Activity-123456789-', ), rbcinvesting.Importer({ 'FILE' : 'Assets:CA:RBC-Investing:Taxable', 'cash' : 'Assets:CA:RBC-Investing:Taxable:Cash', 'positions' : 'Assets:CA:RBC-Investing:Taxable', 'interest' : 'Income:CA:RBC-Investing:Taxable:Interest', 'dividend' : 'Income:CA:RBC-Investing:Taxable:Dividends', 'fees' : 'Expenses:Financial:Fees', 'commission' : 'Expenses:Financial:Commissions', 'transfer' : 'Assets:CA:RBC:Checking', })), (('FileType: application/pdf', 'Filename:.*/123456789-\\d\\d\\d\\d[A-Z][a-z][a-z]\\d\\d-\\d\\d\\d\\d[A-Z][a-z][a-z]\\d\\d.pdf'), rbcpdf.Importer({ 'FILE': 'Assets:CA:RBC-Investing:RRSP', })), The configuration consists in a list, for each possible importer, of a pair of 1) a list of regular expressions which all should match against a \u201cmatch text\u201d, which is a \u201ctextified\u201d version of the contents of a file to be imported, and 2) an importer object, configured with a specific set of accounts to use for producing transactions. Each importer requires a particular set of output accounts which it uses to create its transactions and postings. The ledger\u2019s filename, and a list of these (regexps, importer) pairs is all that is necessary for the driver to carry out all of its work. The textification consists in a simple and imperfect conversion of downloaded file that are in binary format to something that we can run regular expressions against. For an OFX file or CSV file there is no conversion required for textification, we can just match against the text contents of those files; for an Excel/XLS file, we need to convert that to a CSV file, which can then be searched; for a PDF file, a number of different pdf-to-text converters are attempted until one succeeds (the tools for this are notoriously unreliable, so we have to try various ones). Note that this converted \"match text\" is only created temporarily and only for the purpose of identification; the importer will get the original binary file to do its work. It is not entirely clear whether the regular expressions can be standardized to avoid having the user configure them manually. In practice, I have found it often necessary, or at least very convenient, to place an account id in my import configuration. It is true that configuring each of the possible downloads can be a hassle that requires the user to do a bit of guesswork while looking at the contents of each file, but this has been much more reliable in practice than attempts at normalizing this process, likely because it is a much easier problem to uniquely distinguish between all the files of a particular user than to distinguish between all the types of files. Using an account id in one of the regular expressions is the easy way to do that, and it works well. This also provides a clear place to attach the list of accounts to a particular importer, something that necessarily requires user input anyway. Extraction \uf0c1 Once the association is made, we run the importers on each of the files. Some data structure is produced. The importers each do what they do - this is where the ugly tricks go. Ideally, we should build a library of common utilities to help parsing similar file types. Though each of the importer modules should be pretty much independent, some common functionality can be imagined, for example, how one deals with different stocks held in a single investment account, could be configured outside of each importer (e.g., preferred method could be to create a subaccount of that account, with the symbol of the stock, or otherwise). Note [AMaffei]: This could output a generic and well-defined CSV file format if you want to have the option of running the various steps as separate UNIX-style tools and/or process the intermediate files with regular text processing tools. Transform \uf0c1 Some transformations should be independent of importers. In particular, automatically categorizing incomplete transactions is not dependent on which importer created the transaction. I\u2019d like to keep this step as general as possible so that other embellishment steps can be inserted here in the future. Right now, I can only think of the following uses: Auto-categorization of transactions with only a single leg Detection of duplicate transactions: imported files often contain transactions which are already in the ledger; those should be either ignored or marked as such. In practice, this is not as easy as it sounds, because a straightforward date + narration comparison will fail: if the same transaction comes from two input data files, one side always ends up getting merged to the other, and sometimes even the date differs a bit. Some amount of fuzzy matching is required. Normalization of payee names: the imported names of payees are often cut short or include some irrelevant words, such as \u201cLLC\u201d, city names, and/or number codes. It may be desirable to somehow clean those up automatically. This step involves a bootstrapping phase, where we will extract some data from the actual ledger that the transactions are meant to be imported into. We will implement a generic interface that should allow each ledger language implementation to provide relevant data for training. The output data here should be in the same format as its input, so that we can optionally skip this phase. Rendering \uf0c1 An output renderer should be selected by the driver. This is where we convert the extracted data structures to the particular flavor of ledger implementation you\u2019re using. Each of the renderer implementations should be free to import modules from its particular implementation, and we should be careful to constraint these import dependencies to only these modules, to make sure that only a single ledger implementation is required in order for the code to run. Options for rendering style could be defined here, for each renderer, because each of the languages have particularies. [AMaffei] Also, it should be possible to provide a generic renderer that takes printf-style format strings to output in any desired format. Filing \uf0c1 Importers should be able to look at the textified contents of the files and find the file/statement date. This is useful, because we can rename the file by prepending the date of the statement, and the date at which we download the statement or transaction files is rarely the same date at which it was generated. In the case where we are not able to extract a date from the file, we fall back on the filename\u2019s last modified time. A target directory should be provided and we should move each file to the account with which it is associated. For example, a file like this: ~/Downloads/ofx32755.qbo should be moved to a directory .../Assets/US/RBC/Checking/2013-11-27.ofx32755.qbo if it is associated by the identification step with an importer for the Assets:US:RBC:Checking account. For this purpose, all the importers should have a required \u201cfiling\u201d account associated with them. As far as I know only Beancount implements this at the moment, but I suspect this convenient mechanism of organizing and preserving your imported files will be found useful by others. Given a list of directories, Beancount automatically finds those files and using the date in the filename, is able to render links to the files as line items in the journal web pages, and serve their contents when the user clicks on the links. Even without this capability, it can be used to maintain a cache of your documents (I maintain mine in a repository which I sync to an external drive for backup). Implementation Details \uf0c1 Notes about the initial implementation: The implementation of this project will be carried out in Python3. Why Python? The performance of importing and extracting is largely irrelevant, a dynamic language works well for this type of task Parsing in a dynamic language works great, there are many available libraries Python3 is now widely distributed and all desired parsing libraries are commonly available for it at this point All modules should be tested, including testing with sample input. If you want to add a new module, you should need to provide an anonymized sample file for it. We will have to have an automated test suite, because past experience has shown this type of code to be quite brittle and fragile to new and unexpected inputs. It\u2019s easy to write, but it\u2019s also easy to break. In order to test binary files that cannot be anonymized, we will provide the ability to test from match-text instead of from original binary statement PDF. Those files are generally not extractable anyhow and are only there for identification and filing (e.g. a PDF statement, we can\u2019t extract any meaningful data out of those except perhaps for the statement date). There should be a quick way to test a particular importer with a particular downloaded file with zero configuration, even if the output account names are a little wonky. There needs to be clean and readable tracing for what the importers are doing, including a debugging/verbose option. We provide a single function to call as the driver for your own import script. Your configuration is a script / your script is the configuration. You call a function at the end. We will also provide a script that imports a filename and fetches an attribute from it, for those who want a more traditional invocation. We should keep types simples, but use the standard datetime types for dates, decimal.Decimal for numbers, and strings for currencies/commodities. This is obviously based on my current importers code in Beancount. I\u2019m very open to new ideas and suggestions for this project. Collaborations will be most welcome. The more importers we can support, the better. Importers Interface \uf0c1 Each importer should be implemented as a class that derives from this one: class ImporterBase: \"Base class/interface for all source importers.\" # A dict of required configuration variables to their docstring. # This declares the list of options required for the importer # to be provided with, and their meaning. REQUIRED_CONFIG = {} def __init__(self, config): \"\"\"Create an importer. Most concrete implementations can just use this without overriding. Args: config: A dict of configuration accounts, that must match the REQUIRED_CONFIG values. \"\"\" # a dict of Configuration values. This can be accessed publicly. assert isinstance(config, dict) self.config = config # Check that the config has just the required configuration values. if not verify_config(self, config, self.REQUIRED_CONFIG): raise ValueError(\"Invalid config {}, requires {}\".format( config, self.REQUIRED_CONFIG)) def get_filing_account(self): \"\"\"Return the account for moving the input file to. Returns: The name of the account that corresponds to this importer. \"\"\" return self.config['FILE'] def import_file (self, filename): \"\"\"Attempt to import a file. Args: filename: the name of the file to be imported. Returns: A list of new, imported entries extracted from the file. \"\"\" raise NotImplementedError def import_date (self, filename, text_contents): \"\"\"Attempt to obtain a date that corresponds to the given file. Args: filename: the name of the file to extract the date from text_contents: an ASCII text version of the file contents, whatever format it is originally in. Returns: A date object, if successful, or None. \"\"\" raise NotImplementedError For each importer, a detailed explanation of how the original input file on the institution\u2019s website is to be found and downloaded should be provided, to help those find the correct download when adding this importer (some institutions provide a variety of download formats). In addition, a one-line description of the input file support should be provided, so that we can render at runtime a list of the supported file types. References \uf0c1 Other projects with the same goal as importing account data into Ledger are listed here. Ledger\u2019s \u201cconvert\u201d command HLedger with its built-in readers Reckon OFXmate (GUI for ledger-autosync) CSV2Ledger icsv2ledger csv2ledger (seems to lack active maintainers) Update (Nov 2015): This design doc has been implemented and the project is being transitioned back to Beancount. Read the details here .","title":"Ledgerhub Design Doc"},{"location":"ledgerhub_design_doc.html#design-doc-for-ledgerhub","text":"Martin Blais , February 2014 http://furius.ca/beancount/doc/ledgerhub-design-doc Motivation Goals & Stages Details of Stages Fetching Identification Extraction Transform Rendering Filing Implementation Details Importers Interface References Please note that this document is the original design doc for LedgerHub. LedgerHub is being transitioned back to Beancount. See this postmortem document for details [blais, 2015-12].","title":"Design Doc for Ledgerhub"},{"location":"ledgerhub_design_doc.html#motivation","text":"Several open source projects currently exist that provide the capability to create double-entry transactions for bookkeeping from a text file input. These various double-entry bookkeeping projects include Beancount , Ledger , HLedger , Abandon , and they are independent implementations of a similar goal: the creation of an in-memory representation for double-entry accounting transactions from a text file, and the production of various reports from it, such as balance sheets, income statements, journals, and others. Each implementation explores slightly different feature sets, but essentially all work by reading their input from a file whose format is custom declarative language that describe the transactions, a language which is meant to be written by humans and whose syntax is designed with that goal in mind. While the languages do vary somewhat, the underlying data structures that they define are fairly similar. An essential part of the process of regularly updating one\u2019s journal files is the replication of a real-world account\u2019s transaction detail to a single input file in a consistent data format. This is essentially a translation step, meant to bring the transaction details of many institutions\u2019 accounts into a single system. Various banks and credit card companies provide downloadable transaction data in either Quicken or Microsoft Money (OFX) formats, and many institutions provide custom CSV files with transaction detail. Moreover, many of these institutions also make regular statements available for download as PDF files, and these can be associated with one\u2019s ledger accounts. The process of translating these external data formats can be automated to some extent. These various files can be translated to output text that can then be massaged by the user to be integrated into input file formats accepted by a double-entry bookkeeping package. Several projects have begun to make inroads in that domain: Ledger-autosync aims at fetching transactions automatically from OFX servers for and translating them for Ledger and HLedger, and Reckon converts CSV files for Ledger. Beancount includes code that can automate the identification of downloaded files to the accounts from a ledger, extract their transaction detail, and automatically file them to a directory hierarchy that mirrors the ledger\u2019s chart of accounts. This code should probably live outside of Beancount. Ledger also sports a \u201cconvert\u201d command that attempts to do similar things and a CSV2Ledger Perl script is available that can convert CSV files. HLedger also had a convert command which translated CSV files with optional conversion hints defined in a separate file; HLedger now does the same conversion on-the-fly when the input file is CSV (i.e., CSV is considered a first-class input format). The programs that fetch and convert external data files do not have to be tied to a single system. Moreover, this is often cumbersome code that would benefit greatly from having a large number of contributors, which could each benefit each other from having common parsers ready and working for the various institutions that they\u2019re using or likely to use in the future. I - the author of Beancount - have decided to move Beancount\u2019s importing and filing source code outside of its home project and to decouple it from the Beancount source code, so that others can contribute to it, with the intent of providing project-agnostic functionality. This document describes the goals and design of this project.","title":"Motivation"},{"location":"ledgerhub_design_doc.html#goals-stages","text":"This new project should address the following aspects in a project-agnostic manner: Fetching : Automate obtaining the external data files by connecting to the data sources directly. External tools and libraries such as ofxclient for OFX sources can be leveraged for this purpose. Web scraping could be used to fetch downloadable files where possible. The output of this stage is a list of institution-specific files downloaded to a directory. Note that fetching does not just apply to transaction data here; we will also support fetching prices . A list of (date, price) entries may be created from this data. We will likely want to support an intermediate format for expressing a list of positions (and appropriate support in the ledgerhub-Ledger/Beancount/HLedger interface to obtain it). Identification : Given a filename and its contents, automatically guess which institution and account configuration the file is for, and ideally be able to extract the date from the file or statement. This should also work with PDF files. The output of this stage is an association of each input file to a particular extractor and configuration (e.g. a particular account name). Extraction : Parse each file (if possible) and extract a list of information required to generate double-entry transactions data structures from it, in some sort of generic data structure, such as dicts of strings and numbers, independent of the underlying project\u2019s desired output. If possible, a verbatim snippet of the original text that generated the transaction should be attached to the output data structure. The output of this stage is a data structure, e.g., a list of Python dictionaries in some defined format. Transform : Given some information from the past transaction history contained in a journal, using simple learning algorithms, a program should be able to apply transformations on the transactional information extracted from the previous step. The most common use case for this is to automatically add a categorization posting to transactions that have a single posting only. For example, transactions from credit card statements typically include the changes in balance of the credit card account but all transactions are left to be manually associated with a particular expense account. Some of this process can be automated at this stage. Rendering : Convert the internal transactions data structures to the particular syntax of a double-entry bookkeeping project implementation and to the particular desired syntax variants (e.g. currency formatting, comma vs. dot decimal separator, localized input date format). This steps spits out text to be inserted into an input file compatible with the ledger software of choice. Filing : Sanitize the downloaded files\u2019 filenames and move them into a well organized and structured directory hierarchy corresponding to the identified account. This can run from the same associations derived in the identification step. Apart from the Render stage, all the other stages should be implemented without regard for a particular project, this should work across all ledger implementations. The Rendering code, however, should specialize, import source code, and attempt to add as many of the particular features provided by each project to its output text. Where necessary, interfaces to obtain particular data sets from each ledger implementation\u2019s input files should be provided to shield the common code from the particular implementation details of that project. For instance, a categorization Transform step would need to train its algorithm on some of the transaction data (i.e., the narration fields and perhaps some of the amounts, account names, and dates). Each project should provide a way to obtain the necessary data from its input data file, in the same format.","title":"Goals & Stages"},{"location":"ledgerhub_design_doc.html#details-of-stages","text":"","title":"Details of Stages"},{"location":"ledgerhub_design_doc.html#fetching","text":"By default, a user should be able to click their way to their institution\u2019s website and download documents to their ~/Downloads directory. A directory with some files in it should be the reasonable default input to the identification stage. This directory should be allowed to have other/garbage files in it, the identification step should be able to skip those automatically. A module that can automatically fetch the data needs to be implemented. Ideally this would not require an external tool. The data extracted should also have a copy saved in some Downloads directory. This is the domain of the ledger-autosync project. Perhaps we should coordinate input/outputs or even integrate call some of its library code at this stage. The author notes that fetching data from OFX servers is pretty easy, though the begin/end dates will have to get processed and filtered. Automatic fetching support will vary widely depending on where the institutions are located. Some places have solid support, some less. Use the data from ofxhome.com to configure.","title":"Fetching"},{"location":"ledgerhub_design_doc.html#fetching-prices","text":"For fetching prices, there are many libraries out there. Initially we will port Beancount\u2019s bean-prices to ledgerhub.","title":"Fetching Prices"},{"location":"ledgerhub_design_doc.html#identification","text":"The identification stage consists in running a driver program that Searches for files in a directory hierarchy (typically your ~/Downloads folder) If necessary, converts the files into some text/ascii format, so that regular expressions can be matched against it (even if the output is messy, e.g., with PDF files converted to ASCII). This works well for PDF files: despite the fact that we cannot typically extract transactional data from them, we can generally pretty reliably identify which account they\u2019re for and almost always extract the statement date as well. Check a list of regular expressions against the ASCII\u2019fied contents. If the regular expressions all match, the configuration is associated to the filename. Note that more than one configuration may be associated to the same file because some files contain many sections, sections for which different importers may be called on to extract their data (e.g., OFX banking + OFX credit card can be mixed in the same file, and some institutions do). The net result of this process is an association of each filename with the a specific importer object instantiated in the configuration file. These importer objects are created with a set of required account names which they use to produce the Ledger-like syntax from the downloaded file that was associated with it. Here is an example configuration for two importers: from ledgerhub.sources.rbc import rbcinvesting, rbcpdf CONFIG = [ ... (('FileType: application/vnd.ms-excel', r'Filename: .*Activity-123456789-', ), rbcinvesting.Importer({ 'FILE' : 'Assets:CA:RBC-Investing:Taxable', 'cash' : 'Assets:CA:RBC-Investing:Taxable:Cash', 'positions' : 'Assets:CA:RBC-Investing:Taxable', 'interest' : 'Income:CA:RBC-Investing:Taxable:Interest', 'dividend' : 'Income:CA:RBC-Investing:Taxable:Dividends', 'fees' : 'Expenses:Financial:Fees', 'commission' : 'Expenses:Financial:Commissions', 'transfer' : 'Assets:CA:RBC:Checking', })), (('FileType: application/pdf', 'Filename:.*/123456789-\\d\\d\\d\\d[A-Z][a-z][a-z]\\d\\d-\\d\\d\\d\\d[A-Z][a-z][a-z]\\d\\d.pdf'), rbcpdf.Importer({ 'FILE': 'Assets:CA:RBC-Investing:RRSP', })), The configuration consists in a list, for each possible importer, of a pair of 1) a list of regular expressions which all should match against a \u201cmatch text\u201d, which is a \u201ctextified\u201d version of the contents of a file to be imported, and 2) an importer object, configured with a specific set of accounts to use for producing transactions. Each importer requires a particular set of output accounts which it uses to create its transactions and postings. The ledger\u2019s filename, and a list of these (regexps, importer) pairs is all that is necessary for the driver to carry out all of its work. The textification consists in a simple and imperfect conversion of downloaded file that are in binary format to something that we can run regular expressions against. For an OFX file or CSV file there is no conversion required for textification, we can just match against the text contents of those files; for an Excel/XLS file, we need to convert that to a CSV file, which can then be searched; for a PDF file, a number of different pdf-to-text converters are attempted until one succeeds (the tools for this are notoriously unreliable, so we have to try various ones). Note that this converted \"match text\" is only created temporarily and only for the purpose of identification; the importer will get the original binary file to do its work. It is not entirely clear whether the regular expressions can be standardized to avoid having the user configure them manually. In practice, I have found it often necessary, or at least very convenient, to place an account id in my import configuration. It is true that configuring each of the possible downloads can be a hassle that requires the user to do a bit of guesswork while looking at the contents of each file, but this has been much more reliable in practice than attempts at normalizing this process, likely because it is a much easier problem to uniquely distinguish between all the files of a particular user than to distinguish between all the types of files. Using an account id in one of the regular expressions is the easy way to do that, and it works well. This also provides a clear place to attach the list of accounts to a particular importer, something that necessarily requires user input anyway.","title":"Identification"},{"location":"ledgerhub_design_doc.html#extraction","text":"Once the association is made, we run the importers on each of the files. Some data structure is produced. The importers each do what they do - this is where the ugly tricks go. Ideally, we should build a library of common utilities to help parsing similar file types. Though each of the importer modules should be pretty much independent, some common functionality can be imagined, for example, how one deals with different stocks held in a single investment account, could be configured outside of each importer (e.g., preferred method could be to create a subaccount of that account, with the symbol of the stock, or otherwise). Note [AMaffei]: This could output a generic and well-defined CSV file format if you want to have the option of running the various steps as separate UNIX-style tools and/or process the intermediate files with regular text processing tools.","title":"Extraction"},{"location":"ledgerhub_design_doc.html#transform","text":"Some transformations should be independent of importers. In particular, automatically categorizing incomplete transactions is not dependent on which importer created the transaction. I\u2019d like to keep this step as general as possible so that other embellishment steps can be inserted here in the future. Right now, I can only think of the following uses: Auto-categorization of transactions with only a single leg Detection of duplicate transactions: imported files often contain transactions which are already in the ledger; those should be either ignored or marked as such. In practice, this is not as easy as it sounds, because a straightforward date + narration comparison will fail: if the same transaction comes from two input data files, one side always ends up getting merged to the other, and sometimes even the date differs a bit. Some amount of fuzzy matching is required. Normalization of payee names: the imported names of payees are often cut short or include some irrelevant words, such as \u201cLLC\u201d, city names, and/or number codes. It may be desirable to somehow clean those up automatically. This step involves a bootstrapping phase, where we will extract some data from the actual ledger that the transactions are meant to be imported into. We will implement a generic interface that should allow each ledger language implementation to provide relevant data for training. The output data here should be in the same format as its input, so that we can optionally skip this phase.","title":"Transform"},{"location":"ledgerhub_design_doc.html#rendering","text":"An output renderer should be selected by the driver. This is where we convert the extracted data structures to the particular flavor of ledger implementation you\u2019re using. Each of the renderer implementations should be free to import modules from its particular implementation, and we should be careful to constraint these import dependencies to only these modules, to make sure that only a single ledger implementation is required in order for the code to run. Options for rendering style could be defined here, for each renderer, because each of the languages have particularies. [AMaffei] Also, it should be possible to provide a generic renderer that takes printf-style format strings to output in any desired format.","title":"Rendering"},{"location":"ledgerhub_design_doc.html#filing","text":"Importers should be able to look at the textified contents of the files and find the file/statement date. This is useful, because we can rename the file by prepending the date of the statement, and the date at which we download the statement or transaction files is rarely the same date at which it was generated. In the case where we are not able to extract a date from the file, we fall back on the filename\u2019s last modified time. A target directory should be provided and we should move each file to the account with which it is associated. For example, a file like this: ~/Downloads/ofx32755.qbo should be moved to a directory .../Assets/US/RBC/Checking/2013-11-27.ofx32755.qbo if it is associated by the identification step with an importer for the Assets:US:RBC:Checking account. For this purpose, all the importers should have a required \u201cfiling\u201d account associated with them. As far as I know only Beancount implements this at the moment, but I suspect this convenient mechanism of organizing and preserving your imported files will be found useful by others. Given a list of directories, Beancount automatically finds those files and using the date in the filename, is able to render links to the files as line items in the journal web pages, and serve their contents when the user clicks on the links. Even without this capability, it can be used to maintain a cache of your documents (I maintain mine in a repository which I sync to an external drive for backup).","title":"Filing"},{"location":"ledgerhub_design_doc.html#implementation-details","text":"Notes about the initial implementation: The implementation of this project will be carried out in Python3. Why Python? The performance of importing and extracting is largely irrelevant, a dynamic language works well for this type of task Parsing in a dynamic language works great, there are many available libraries Python3 is now widely distributed and all desired parsing libraries are commonly available for it at this point All modules should be tested, including testing with sample input. If you want to add a new module, you should need to provide an anonymized sample file for it. We will have to have an automated test suite, because past experience has shown this type of code to be quite brittle and fragile to new and unexpected inputs. It\u2019s easy to write, but it\u2019s also easy to break. In order to test binary files that cannot be anonymized, we will provide the ability to test from match-text instead of from original binary statement PDF. Those files are generally not extractable anyhow and are only there for identification and filing (e.g. a PDF statement, we can\u2019t extract any meaningful data out of those except perhaps for the statement date). There should be a quick way to test a particular importer with a particular downloaded file with zero configuration, even if the output account names are a little wonky. There needs to be clean and readable tracing for what the importers are doing, including a debugging/verbose option. We provide a single function to call as the driver for your own import script. Your configuration is a script / your script is the configuration. You call a function at the end. We will also provide a script that imports a filename and fetches an attribute from it, for those who want a more traditional invocation. We should keep types simples, but use the standard datetime types for dates, decimal.Decimal for numbers, and strings for currencies/commodities. This is obviously based on my current importers code in Beancount. I\u2019m very open to new ideas and suggestions for this project. Collaborations will be most welcome. The more importers we can support, the better.","title":"Implementation Details"},{"location":"ledgerhub_design_doc.html#importers-interface","text":"Each importer should be implemented as a class that derives from this one: class ImporterBase: \"Base class/interface for all source importers.\" # A dict of required configuration variables to their docstring. # This declares the list of options required for the importer # to be provided with, and their meaning. REQUIRED_CONFIG = {} def __init__(self, config): \"\"\"Create an importer. Most concrete implementations can just use this without overriding. Args: config: A dict of configuration accounts, that must match the REQUIRED_CONFIG values. \"\"\" # a dict of Configuration values. This can be accessed publicly. assert isinstance(config, dict) self.config = config # Check that the config has just the required configuration values. if not verify_config(self, config, self.REQUIRED_CONFIG): raise ValueError(\"Invalid config {}, requires {}\".format( config, self.REQUIRED_CONFIG)) def get_filing_account(self): \"\"\"Return the account for moving the input file to. Returns: The name of the account that corresponds to this importer. \"\"\" return self.config['FILE'] def import_file (self, filename): \"\"\"Attempt to import a file. Args: filename: the name of the file to be imported. Returns: A list of new, imported entries extracted from the file. \"\"\" raise NotImplementedError def import_date (self, filename, text_contents): \"\"\"Attempt to obtain a date that corresponds to the given file. Args: filename: the name of the file to extract the date from text_contents: an ASCII text version of the file contents, whatever format it is originally in. Returns: A date object, if successful, or None. \"\"\" raise NotImplementedError For each importer, a detailed explanation of how the original input file on the institution\u2019s website is to be found and downloaded should be provided, to help those find the correct download when adding this importer (some institutions provide a variety of download formats). In addition, a one-line description of the input file support should be provided, so that we can render at runtime a list of the supported file types.","title":"Importers Interface"},{"location":"ledgerhub_design_doc.html#references","text":"Other projects with the same goal as importing account data into Ledger are listed here. Ledger\u2019s \u201cconvert\u201d command HLedger with its built-in readers Reckon OFXmate (GUI for ledger-autosync) CSV2Ledger icsv2ledger csv2ledger (seems to lack active maintainers) Update (Nov 2015): This design doc has been implemented and the project is being transitioned back to Beancount. Read the details here .","title":"References"},{"location":"precision_tolerances.html","text":"Beancount Precision & Tolerances \uf0c1 Martin Blais , May 2015 http://furius.ca/beancount/doc/tolerances This document describes how Beancount handles the limited precision of numbers in transaction balance checks and balance assertions. It also documents rounding that may occur in inferring numbers automatically. Motivation \uf0c1 Beancount automatically enforces that the amounts on the Postings of Transactions entered in an input file sum up to zero. In order for Beancount to verify this in a realistic way, it must tolerate a small amount of imprecision. This is because Beancount lets you replicate what happens in real world account transactions , and in the real world, institutions round amounts up or down for practical reasons. Here\u2019s an example: Consider the following transaction which consists in a transfer between two accounts denominated in different currencies (US dollars and Euros): 2015-05-01 * \"Transfer from secret Swiss bank account\" Assets:CH:SBS:Checking -9000.00 CHF Assets:US:BofA:Checking 9643.82 USD @ 0.93324 CHF In this example, the exchange rate used was 0.93324 USD/CHF, that is, 0.93324 Swiss Francs per US dollar. This rate was quoted to 5 digits of precision by the bank. A full-precision conversion of 9000.00 CHF / 0.93324 CHF yields 9643.82152501... USD. Similarly, converting the US dollars to Francs using the given rate yields an imprecise result as well: 9643.82 x 0.93324 = 8999.9985768\u2026 . Here is another example where this type of rounding may occur: A transaction for a fractional number of shares of a mutual fund: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.22626 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384.61 USD Once again, rounding occurs in this transaction: not only the Net Asset Value of the fund is rounded to its nearest penny value ($37.61), but the number of units is also rounded and accounted for by Vanguard with a fixed number of digits (10.22626 units of VPMBX). And the balance of the entire transaction needs to tolerate some imprecision, whether you compute the value of the shares (10.22626 x $37.61 = $384.6096386 ) or whether you compute the number of shares from the desired dollar amount of the contribution ($384.61 / $37.61 = 10.2262696091 ). From Beancount\u2019s point-of-view, both of the examples above are balancing transactions. Clearly, if we are to try to represent and reproduce the transactions of external accounts to our input file, there needs to be some tolerance in the balance verification algorithm. How Precision is Determined \uf0c1 Beancount attempts to derive the precision from each transaction automatically , from the input, for each Transaction in isolation 1 . Let us inspect our last example again: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.22626 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384.61 USD In this transaction, Beancount will infer the tolerance of RGAGX at 5 fractional digits, that is, 0.000005 RGAGX , and USD at 2 fractional digits, that is, 0.005 USD . Note that the tolerance used is half of the last digit of precision provided by the user. This is entirely inferred from the input, without having to fetch any global tolerance declaration. Also note how the precision is calculated separately for each currency . Observe that although we are inferring a tolerance for units of RGAGX, it is actually not used in the balancing of this transaction, because the \u201cweight\u201d of the first posting is in USD (10.22626 x 37.61 = 384.6096386 USD). So what happens here? The weights of each postings are calculated: 384.6096386 USD for the first posting -384.61 USD for the second These are summed together, by currency (there is only USD in the weights of this transaction) which results in a residual value of -0.0003614 USD. This value is compared to the tolerance for units of USD: |-0.0003614| < 0.005, and this transaction balances. Prices and Costs \uf0c1 For the purpose of inferring the tolerance to be used, the price and cost amounts declared on a transaction\u2019s Postings are ignored . This makes sense if you consider that these are usually specified at a higher precision than the base amounts of the postings\u2014and sometimes this extra precision is necessary to make the transaction balance. These should not be used in setting the precision of the whole transaction. For example, in the following transaction: 1999-09-30 * \"Vest ESPP - Bought at discount: 18.5980 USD\" Assets:US:Schwab:ESPP 54 HOOL {21.8800 USD} Income:CA:ESPP:PayContrib -1467.84 CAD @ 0.6842 USD Income:CA:ESPP:Discount -259.03 CAD @ 0.6842 USD The only tolerance inferred here is 0.005 for CAD. (54 HOOL does not yield anything in this case because it is integral; the next section explains this). There is no tolerance inferred for USD, neither from the cost from the first posting (21.8800 USD), nor from the prices of the remaining postings (0.6842 USD). Integer Amounts \uf0c1 For integer amounts in the input, the precision is not inferred to 0.5, that is, this should fail to balance: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.21005 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384 USD In other words, integer amounts do not contribute a number of digits to the determination of the tolerance for their currency. By default, the tolerance used on amounts without an inferred precision is zero . So in this example, because we cannot infer the precision of USD (recall that the cost is ignored), this transaction will fail to balance, because its residual is non-zero (|-0.0003614| > 0). You can customize what the default tolerance should be for each currency separately and for any currency as well (see section below on how to do this). This treatment of integer amounts implies that the maximum amount of precision that one can specify just by inputting numbers is 0.05 units of the currency, for example, by providing a number such as 10.7 as input 2 . On the other hand, the settings for the default tolerance to use allows specifying arbitrary numbers. Resolving Ambiguities \uf0c1 A case that presents itself rarely is one where multiple different precisions are being input for the same currency. In this case, the largest (coarsest) of the inferred input tolerances is used. For example, if we wanted to track income to more than pennies, we might write this: 1999-08-20 * \"Sell\" Assets:US:BRS:ESPP -81 HOOL {26.3125 USD} Assets:US:BRS:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:CA:ESPP:PnL -10.125 USD The amounts we have for USD in this case are 2141.36, 0.08 and -10.125, which infer tolerances of either 0.005 or 0.0005. We select the coarsest amount: this transaction tolerates an imprecision of 0.005 USD. Default Tolerances \uf0c1 When a transaction\u2019s numbers do not provide enough information to infer a tolerance locally , we fall back to some default tolerance value. As seen in previous examples, this may occur either because (a) the numbers associated with the currency we need it for are integral, or (b) sufficient numbers are simply absent from the input. By default, this default tolerance is zero for all currencies. This can be specified with an option, like this: option \"inferred_tolerance_default\" \"*:0.001\" The default tolerance can be further refined for each currency involved, by providing the currency to the option, like this: option \"inferred_tolerance_default\" \"USD:0.003\" If provided, the currency-specific tolerance will be used over the global value. The general form for this option is: option \"inferred_tolerance_default\" \":\" Just to be clear: this option is only used when the tolerance cannot be inferred. If you have overly large rounding errors and the numbers in your transactions do infer some tolerance value, this value will be ignored (e.g., setting it to a larger number to try to address that fix will not work). If you need to loosen up the tolerance, see the \u201c inferred_tolerance_multiplier \u201d in the next section. (Note: I\u2019ve been considering dedicating a special meta-data field to the Commodity directive for this, but this would break from the invariant that meta-data is only there to be used by users and plugins, so I\u2019ve refrained so far.) Tolerance Multiplier \uf0c1 We\u2019re shown previously that when the tolerance value isn\u2019t provided explicitly, that it is inferred from the numbers on the postings. By default, the smallest digit found on those numbers is divided by half to obtain the tolerance because we assume that the institutions which we\u2019re reproducing the transactions apply rounding and so the error should never be more than half. But in reality, you may find that the rounding errors sometime exceed this value. For this reason, we provide an option to set the multiplier for the inferred tolerance: option \"inferred_tolerance_multiplier\" \"1.2\" This value overrides the default multiplier. In this example, for a transaction with postings only with values such as 24.45 CHF, the inferred tolerance for CHF would be +/- 0.012 CHF. Inferring Tolerances from Cost \uf0c1 There is also a feature that expands the maximum tolerance inferred on transactions to include values on cost currencies inferred by postings held at-cost or converted at price. Those postings can imply a tolerance value by multiplying the smallest digit of the unit by the cost or price value and taking half of that value. For example, if a posting has an amount of \"2.345 RGAGX {45.00 USD}\" attached to it, it implies a tolerance of 0.001 x 45.00 / 2 = 0.045 USD and the sum of all such possible rounding errors is calculate for all postings held at cost or converted from a price, and the resulting tolerance is added to the list of candidates used to figure out the tolerance we should use for the given commodity (we use the maximum value of all the inferred tolerances). You turn on the feature like this: option \"infer_tolerance_from_cost\" \"TRUE\" Enabling this flag only makes the tolerances potentially wider, never smaller. Balance Assertions & Padding \uf0c1 There are a few other places where approximate comparisons are needed. Balance assertions also compare two numbers: 2015-05-08 balance Assets:Investments:RGAGX 4.271 RGAGX This asserts that the accumulated balance for this account has 4.271 units of RGAGX, plus or minus 0.001 RGAGX. So accumulated values of 4.270 RGAGX up to 4.272 RGAGX will check as asserted. The tolerance is inferred automatically to be 1 unit of the least significant digit of the number on the balance assertion. If you wanted a looser assertion, you could have declared: 2015-05-08 balance Assets:Investments:RGAGX 4.27 RGAGX This assertion would accept values from 4.26 RGAGX to 4.28 RGAGX. Note that the inferred tolerances are also expanded by the inferred tolerance multiplier discussed above. Tolerances that Trigger Padding \uf0c1 Pad directives automatically insert transactions to bring account balances in-line with a subsequent balance assertion. The insertion only triggers if the balance differs from the expected value, and the tolerance for this to occur behaves exactly the same as for balance assertions. Explicit Tolerances on Balance Assertions \uf0c1 Beancount supports the specification of an explicit tolerance amount, like this: 2015-05-08 balance Assets:Investments:RGAGX 4.271 ~ 0.01 RGAGX This feature was added because of some observed peculiarities in Vanguard investment accounts whereby rounding appears to follow odd rules and balances don\u2019t match. Saving Rounding Error \uf0c1 As we saw previously, transactions don\u2019t have to balance exactly, they allow for a small amount of imprecision. This bothers some people. If you would like to track and measure the residual amounts allowed by the tolerances, Beancount offers an option to automatically insert postings that will make each transaction balance exactly. You enable the feature like this: option \"account_rounding\" \"Equity:RoundingError\" This tells Beancount to insert postings to compensate for the rounding error to an \u201c Equity:RoundingError \u201d account. For example, with the feature enabled, the following transaction: 2013-02-23 * \"Buying something\" Assets:Invest 1.245 RGAGX {43.23 USD} Assets:Cash -53.82 USD will be automatically transformed into this: 2013-02-23 * \"Buying something\" Assets:Invest 1.245 RGAGX {43.23 USD} Assets:Cash -53.82 USD Equity:RoundingError -0.00135 USD You can verify that this transaction balances exactly. If the transaction already balances exactly (this is the case for most transactions) no posting is inserted. Finally, if you require that all accounts be opened explicitly, you should remember to declare the rounding account in your file at an appropriate date, like this: 2000-01-01 open Equity:RoundingError Precision of Inferred Numbers \uf0c1 Beancount is able to infer some missing numbers in the input. For example, the second posting in this transaction is \u201cinterpolated\u201d automatically by Beancount: 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash The calculated amount to be inserted from the first posting is -227.2067 USD. Now, you might ask, to which precision is it inserted at? Does it insert 227.2067 USD at the full precision or does the number get rounded to a penny, e.g. 227.21 USD? It depends on the tolerance inferred for that currency. In this example, no tolerance is able to get inferred (there is no USD amount provided other than the cost amount, which is ignored for the purpose of inferring the tolerance), so we have to defer to the default tolerance. If the default tolerance is not overridden in the input file\u2014and therefore is zero\u2014the full precision will be used; no rounding occurs. This will result in the following transaction: 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.2067 USD Note that if a tolerance could be inferred from other numbers on that transaction, it would be used for rounding, such as in this example where the Cash posting is rounded to two digits because of the 9.95 USD number on the Commissions posting: 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Expenses:Commissions 9.95 USD Assets:Investments:Cash -237.16 USD However, if no inference is possible, and the default tolerance for USD is set to 0.001, the number will be quantized to 0.001 before insertion, that is, 227.207 USD will be stored: option \"default_tolerance\" \"USD:0.001\" 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.207 USD Finally, if you enabled the accumulation of rounding error, the posting\u2019s amount will reflect the correct residual, taking into account the rounded amount that was automatically inserted: option \"default_tolerance\" \"USD:0.01\" option \"account_rounding\" \"Equity:RoundingError\" 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.207 USD Equity:RoundingError 0.0003 USD Porting Existing Input \uf0c1 The inference of tolerance values from the transaction\u2019s numbers is generally good enough to keep existing files working without changes. There may be new errors appearing in older files once we process them with the method described in this document, but they should either point to previously undetected errors in the input, or be fixable with simple addition of a suitable number of digits. As a testimony, porting the author\u2019s very large input file has been a relatively painless process that took less than 1 hour. In order to ease the transition, you will probably want to change the default tolerance for all currencies to match the previous value that Beancount had been using, like this: option \"inferred_tolerance_default\" \"*:0.005\" I would recommend you start with this and fix all errors in your file, then proceed to removing this and fix the rest of errors. This should make it easier to adapt your file to this new behavior. As an example of how to fix a new error\u2026 converting this newly failing transaction from the Integer Amounts section: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.21005 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384 USD by inserting zero\u2019s to provide a locally inferred value like this: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.21005 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384.00 USD is sufficient to silence the balance check. Representational Issues \uf0c1 Internally, Beancount uses a decimal number representation (not a binary/float representation, neither rational numbers). Calculations that result in a large number of fractional digits are carried out to 28 decimal places (the default precision from the context of Python\u2019s IEEE decimal implementation). This is plenty sufficient, because the method we propose above rarely trickles these types of numbers throughout the system: the tolerances allows us to post the precise amounts declared by users, and only automatically derived prices and costs will possibly result in precisions calculated to an unrealistic number of digits that could creep into aggregations in the rest of the system. References \uf0c1 The original proposal that led to this implementation can be found here . In particular, the proposal highlights on the other systems have attempted to deal with this issue. There are also some discussions on the mailing-list dedicated to this topic. Note that for the longest time, Beancount used a fixed precision of 0.005 across all currencies. This was eliminated once the method described in this document was implemented. Also, for Balance and Pad directives, there used to be a \u201ctolerance\u201d option that was set by default to 0.015 of any units. This option has been deprecated with the merging of the changes described in this document. Historical Notes \uf0c1 Here\u2019s an overview of the status of numbers rendering in Beancount as of March 2016, from the mailing-list : First, it's important to realize how these numbers are represented in memory. They are using the Decimal representation which beyond being able to accurately representing decimal numbers (as opposed to the approximation that binary floats provides) also contains a specific precision. That is, the number 2.00 is represented differently than the numbers 2.0 and 2.000. The numbers \"remember\" which precision they are represented up to. This is important. When I say rendering the numbers to their \"natural precision\" I mean the precision with which they are represented, i.e., 2.0 renders as \"2.0\", 2.000 renders as \"2.000\". Then, there are two DISTINCT topics: (1) tolerances, and (2) precision. \"Tolerances\" are values used to determine how much imprecision is acceptable in balancing transactions. This is used in the verification stage, to determine how much looseness to allow. It should not affect how numbers are rendered. \"Precision\" is perhaps a bit of a misnomer: By that I'm referring to is how many digits the numbers are to be rendered with. Once upon a time - after the shell was already written - these concepts weren't well defined in Beancount and I wasn't dealing with these things consistently. At some point it became clear what I needed to do and I created a class called \"DisplayContext\" which could contain appropriate settings for rendering the precision of numbers for each currency (each currency tends to have its own most common rendering precision, e.g. two digits for USD, one digit for MXN, no digits for JPY and in reports we're typically fine rounding the actual numbers to that precision). So an instance of this DisplayContext is automatically instantiated in the parser and in order to avoid the user having to set these values manually - for Beancount to \"do the right thing\" by default - it is able to accumulate the numbers seen and to deduce the most common and maximum number of digits used from the input, and to use that as the default number of digits for rendering numbers. The most common format/number of digits is used to render the number of units, and the maximum number of digits seen is used to render costs and prices. In addition, this class also has capabilities for aligning to the decimal dot and to insert commas on thousands as well. It separates the control of the formatting from the numbers themselves. MOST of the code that renders numbers uses the DisplayContext (via the to_string() methods) to convert the numbers into strings, such as the web interface and explicit text reports. But NOT ALL... there's a bit of HISTORY here... the SQL shell uses some old special-purpose code to render numbers that I never bothered to convert to the DisplayContext class. There's a TODO item for it. It needs to get converted at some point, but I've neglected doing this so far because I have much bigger plans for the SQL query engine that involve a full rewrite of it with many improvements and I figured I'd do that then. If you recall, the SQL query engine was a prototype, and actually it works, but it is not well covered by unit tests. My purpose with it was to discover through usage what would be useful and to then write a v2 of it that would be much better. Now, about that PRINT command... this is not intended as a reporting tool. The printer's purpose is to print input that accurately represents the content of the transactions. In order to do this, it needs to render the numbers at their \"natural\" precision, so that when they get read back in, they parse into the very same number, that is, with the same number of digits (even if zeros). For this reason, the PRINT command does not attempt to render using the DisplayContext instance derived from the input file - this is on purpose. I could change that, but then round-trip would break: the rounding resulting from formatting using the display context may output transactions which don't balance anymore. As you can see, it's not an obvious topic... Hopefully this should allow you to understand what comes out of Beancount in terms of the precision of the numbers it renders. Note: \"default_tolerances\" has been renamed to \"inferred_tolerance_default\" recently because the name was too general and confusing. Old name will work but generate a warning. I just noticed from your comments and some grepping around that the \"render_commas\" option is not used anymore. I'm not sure how that happened, but I'll go ad fix that right away and set the default value of the DisplayContext derived from the input. I should probably also convert the SQL shell rendering to use the display context regardless of future plans, so that it renders consistently with all the rest. Not sure I can do that this weekend, but I'll log a ticket, here . I hope this helps. You're welcome to ask questions if the above isn't clear. I'm sorry if this isn't entirely obvious... there's been a fair bit of history there and there's a lot of code. I should review the naming of options, I think the tolerance options all have \"tolerance\" in their name, but there aren't options to override the rendering and when I add them they should all have a common name as well. Further Reading \uf0c1 What Every Computer Scientist Should Know About Floating-Point Arithmetic This stands in contrast to Ledger which attempts to infer the precision based on other transactions recently parsed in the file, in file order. This has the unfortunate effect of creating \u201ccross-talk\u201d between the transactions in terms of what precision can be used. \u21a9 Note that due to the way Beancount represents numbers internally, it is also not able to distinguish between \u201c230\u201d and \u201c230.\u201d; these parse into the same representation for Beancount. Therefore, we are not able to use that distinction in the input to support a precision of 0.5. \u21a9","title":"Precision Tolerances"},{"location":"precision_tolerances.html#beancount-precision-tolerances","text":"Martin Blais , May 2015 http://furius.ca/beancount/doc/tolerances This document describes how Beancount handles the limited precision of numbers in transaction balance checks and balance assertions. It also documents rounding that may occur in inferring numbers automatically.","title":"Beancount Precision & Tolerances"},{"location":"precision_tolerances.html#motivation","text":"Beancount automatically enforces that the amounts on the Postings of Transactions entered in an input file sum up to zero. In order for Beancount to verify this in a realistic way, it must tolerate a small amount of imprecision. This is because Beancount lets you replicate what happens in real world account transactions , and in the real world, institutions round amounts up or down for practical reasons. Here\u2019s an example: Consider the following transaction which consists in a transfer between two accounts denominated in different currencies (US dollars and Euros): 2015-05-01 * \"Transfer from secret Swiss bank account\" Assets:CH:SBS:Checking -9000.00 CHF Assets:US:BofA:Checking 9643.82 USD @ 0.93324 CHF In this example, the exchange rate used was 0.93324 USD/CHF, that is, 0.93324 Swiss Francs per US dollar. This rate was quoted to 5 digits of precision by the bank. A full-precision conversion of 9000.00 CHF / 0.93324 CHF yields 9643.82152501... USD. Similarly, converting the US dollars to Francs using the given rate yields an imprecise result as well: 9643.82 x 0.93324 = 8999.9985768\u2026 . Here is another example where this type of rounding may occur: A transaction for a fractional number of shares of a mutual fund: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.22626 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384.61 USD Once again, rounding occurs in this transaction: not only the Net Asset Value of the fund is rounded to its nearest penny value ($37.61), but the number of units is also rounded and accounted for by Vanguard with a fixed number of digits (10.22626 units of VPMBX). And the balance of the entire transaction needs to tolerate some imprecision, whether you compute the value of the shares (10.22626 x $37.61 = $384.6096386 ) or whether you compute the number of shares from the desired dollar amount of the contribution ($384.61 / $37.61 = 10.2262696091 ). From Beancount\u2019s point-of-view, both of the examples above are balancing transactions. Clearly, if we are to try to represent and reproduce the transactions of external accounts to our input file, there needs to be some tolerance in the balance verification algorithm.","title":"Motivation"},{"location":"precision_tolerances.html#how-precision-is-determined","text":"Beancount attempts to derive the precision from each transaction automatically , from the input, for each Transaction in isolation 1 . Let us inspect our last example again: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.22626 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384.61 USD In this transaction, Beancount will infer the tolerance of RGAGX at 5 fractional digits, that is, 0.000005 RGAGX , and USD at 2 fractional digits, that is, 0.005 USD . Note that the tolerance used is half of the last digit of precision provided by the user. This is entirely inferred from the input, without having to fetch any global tolerance declaration. Also note how the precision is calculated separately for each currency . Observe that although we are inferring a tolerance for units of RGAGX, it is actually not used in the balancing of this transaction, because the \u201cweight\u201d of the first posting is in USD (10.22626 x 37.61 = 384.6096386 USD). So what happens here? The weights of each postings are calculated: 384.6096386 USD for the first posting -384.61 USD for the second These are summed together, by currency (there is only USD in the weights of this transaction) which results in a residual value of -0.0003614 USD. This value is compared to the tolerance for units of USD: |-0.0003614| < 0.005, and this transaction balances.","title":"How Precision is Determined"},{"location":"precision_tolerances.html#prices-and-costs","text":"For the purpose of inferring the tolerance to be used, the price and cost amounts declared on a transaction\u2019s Postings are ignored . This makes sense if you consider that these are usually specified at a higher precision than the base amounts of the postings\u2014and sometimes this extra precision is necessary to make the transaction balance. These should not be used in setting the precision of the whole transaction. For example, in the following transaction: 1999-09-30 * \"Vest ESPP - Bought at discount: 18.5980 USD\" Assets:US:Schwab:ESPP 54 HOOL {21.8800 USD} Income:CA:ESPP:PayContrib -1467.84 CAD @ 0.6842 USD Income:CA:ESPP:Discount -259.03 CAD @ 0.6842 USD The only tolerance inferred here is 0.005 for CAD. (54 HOOL does not yield anything in this case because it is integral; the next section explains this). There is no tolerance inferred for USD, neither from the cost from the first posting (21.8800 USD), nor from the prices of the remaining postings (0.6842 USD).","title":"Prices and Costs"},{"location":"precision_tolerances.html#integer-amounts","text":"For integer amounts in the input, the precision is not inferred to 0.5, that is, this should fail to balance: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.21005 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384 USD In other words, integer amounts do not contribute a number of digits to the determination of the tolerance for their currency. By default, the tolerance used on amounts without an inferred precision is zero . So in this example, because we cannot infer the precision of USD (recall that the cost is ignored), this transaction will fail to balance, because its residual is non-zero (|-0.0003614| > 0). You can customize what the default tolerance should be for each currency separately and for any currency as well (see section below on how to do this). This treatment of integer amounts implies that the maximum amount of precision that one can specify just by inputting numbers is 0.05 units of the currency, for example, by providing a number such as 10.7 as input 2 . On the other hand, the settings for the default tolerance to use allows specifying arbitrary numbers.","title":"Integer Amounts"},{"location":"precision_tolerances.html#resolving-ambiguities","text":"A case that presents itself rarely is one where multiple different precisions are being input for the same currency. In this case, the largest (coarsest) of the inferred input tolerances is used. For example, if we wanted to track income to more than pennies, we might write this: 1999-08-20 * \"Sell\" Assets:US:BRS:ESPP -81 HOOL {26.3125 USD} Assets:US:BRS:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:CA:ESPP:PnL -10.125 USD The amounts we have for USD in this case are 2141.36, 0.08 and -10.125, which infer tolerances of either 0.005 or 0.0005. We select the coarsest amount: this transaction tolerates an imprecision of 0.005 USD.","title":"Resolving Ambiguities"},{"location":"precision_tolerances.html#default-tolerances","text":"When a transaction\u2019s numbers do not provide enough information to infer a tolerance locally , we fall back to some default tolerance value. As seen in previous examples, this may occur either because (a) the numbers associated with the currency we need it for are integral, or (b) sufficient numbers are simply absent from the input. By default, this default tolerance is zero for all currencies. This can be specified with an option, like this: option \"inferred_tolerance_default\" \"*:0.001\" The default tolerance can be further refined for each currency involved, by providing the currency to the option, like this: option \"inferred_tolerance_default\" \"USD:0.003\" If provided, the currency-specific tolerance will be used over the global value. The general form for this option is: option \"inferred_tolerance_default\" \":\" Just to be clear: this option is only used when the tolerance cannot be inferred. If you have overly large rounding errors and the numbers in your transactions do infer some tolerance value, this value will be ignored (e.g., setting it to a larger number to try to address that fix will not work). If you need to loosen up the tolerance, see the \u201c inferred_tolerance_multiplier \u201d in the next section. (Note: I\u2019ve been considering dedicating a special meta-data field to the Commodity directive for this, but this would break from the invariant that meta-data is only there to be used by users and plugins, so I\u2019ve refrained so far.)","title":"Default Tolerances"},{"location":"precision_tolerances.html#tolerance-multiplier","text":"We\u2019re shown previously that when the tolerance value isn\u2019t provided explicitly, that it is inferred from the numbers on the postings. By default, the smallest digit found on those numbers is divided by half to obtain the tolerance because we assume that the institutions which we\u2019re reproducing the transactions apply rounding and so the error should never be more than half. But in reality, you may find that the rounding errors sometime exceed this value. For this reason, we provide an option to set the multiplier for the inferred tolerance: option \"inferred_tolerance_multiplier\" \"1.2\" This value overrides the default multiplier. In this example, for a transaction with postings only with values such as 24.45 CHF, the inferred tolerance for CHF would be +/- 0.012 CHF.","title":"Tolerance Multiplier"},{"location":"precision_tolerances.html#inferring-tolerances-from-cost","text":"There is also a feature that expands the maximum tolerance inferred on transactions to include values on cost currencies inferred by postings held at-cost or converted at price. Those postings can imply a tolerance value by multiplying the smallest digit of the unit by the cost or price value and taking half of that value. For example, if a posting has an amount of \"2.345 RGAGX {45.00 USD}\" attached to it, it implies a tolerance of 0.001 x 45.00 / 2 = 0.045 USD and the sum of all such possible rounding errors is calculate for all postings held at cost or converted from a price, and the resulting tolerance is added to the list of candidates used to figure out the tolerance we should use for the given commodity (we use the maximum value of all the inferred tolerances). You turn on the feature like this: option \"infer_tolerance_from_cost\" \"TRUE\" Enabling this flag only makes the tolerances potentially wider, never smaller.","title":"Inferring Tolerances from Cost"},{"location":"precision_tolerances.html#balance-assertions-padding","text":"There are a few other places where approximate comparisons are needed. Balance assertions also compare two numbers: 2015-05-08 balance Assets:Investments:RGAGX 4.271 RGAGX This asserts that the accumulated balance for this account has 4.271 units of RGAGX, plus or minus 0.001 RGAGX. So accumulated values of 4.270 RGAGX up to 4.272 RGAGX will check as asserted. The tolerance is inferred automatically to be 1 unit of the least significant digit of the number on the balance assertion. If you wanted a looser assertion, you could have declared: 2015-05-08 balance Assets:Investments:RGAGX 4.27 RGAGX This assertion would accept values from 4.26 RGAGX to 4.28 RGAGX. Note that the inferred tolerances are also expanded by the inferred tolerance multiplier discussed above.","title":"Balance Assertions & Padding"},{"location":"precision_tolerances.html#tolerances-that-trigger-padding","text":"Pad directives automatically insert transactions to bring account balances in-line with a subsequent balance assertion. The insertion only triggers if the balance differs from the expected value, and the tolerance for this to occur behaves exactly the same as for balance assertions.","title":"Tolerances that Trigger Padding"},{"location":"precision_tolerances.html#explicit-tolerances-on-balance-assertions","text":"Beancount supports the specification of an explicit tolerance amount, like this: 2015-05-08 balance Assets:Investments:RGAGX 4.271 ~ 0.01 RGAGX This feature was added because of some observed peculiarities in Vanguard investment accounts whereby rounding appears to follow odd rules and balances don\u2019t match.","title":"Explicit Tolerances on Balance Assertions"},{"location":"precision_tolerances.html#saving-rounding-error","text":"As we saw previously, transactions don\u2019t have to balance exactly, they allow for a small amount of imprecision. This bothers some people. If you would like to track and measure the residual amounts allowed by the tolerances, Beancount offers an option to automatically insert postings that will make each transaction balance exactly. You enable the feature like this: option \"account_rounding\" \"Equity:RoundingError\" This tells Beancount to insert postings to compensate for the rounding error to an \u201c Equity:RoundingError \u201d account. For example, with the feature enabled, the following transaction: 2013-02-23 * \"Buying something\" Assets:Invest 1.245 RGAGX {43.23 USD} Assets:Cash -53.82 USD will be automatically transformed into this: 2013-02-23 * \"Buying something\" Assets:Invest 1.245 RGAGX {43.23 USD} Assets:Cash -53.82 USD Equity:RoundingError -0.00135 USD You can verify that this transaction balances exactly. If the transaction already balances exactly (this is the case for most transactions) no posting is inserted. Finally, if you require that all accounts be opened explicitly, you should remember to declare the rounding account in your file at an appropriate date, like this: 2000-01-01 open Equity:RoundingError","title":"Saving Rounding Error"},{"location":"precision_tolerances.html#precision-of-inferred-numbers","text":"Beancount is able to infer some missing numbers in the input. For example, the second posting in this transaction is \u201cinterpolated\u201d automatically by Beancount: 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash The calculated amount to be inserted from the first posting is -227.2067 USD. Now, you might ask, to which precision is it inserted at? Does it insert 227.2067 USD at the full precision or does the number get rounded to a penny, e.g. 227.21 USD? It depends on the tolerance inferred for that currency. In this example, no tolerance is able to get inferred (there is no USD amount provided other than the cost amount, which is ignored for the purpose of inferring the tolerance), so we have to defer to the default tolerance. If the default tolerance is not overridden in the input file\u2014and therefore is zero\u2014the full precision will be used; no rounding occurs. This will result in the following transaction: 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.2067 USD Note that if a tolerance could be inferred from other numbers on that transaction, it would be used for rounding, such as in this example where the Cash posting is rounded to two digits because of the 9.95 USD number on the Commissions posting: 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Expenses:Commissions 9.95 USD Assets:Investments:Cash -237.16 USD However, if no inference is possible, and the default tolerance for USD is set to 0.001, the number will be quantized to 0.001 before insertion, that is, 227.207 USD will be stored: option \"default_tolerance\" \"USD:0.001\" 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.207 USD Finally, if you enabled the accumulation of rounding error, the posting\u2019s amount will reflect the correct residual, taking into account the rounded amount that was automatically inserted: option \"default_tolerance\" \"USD:0.01\" option \"account_rounding\" \"Equity:RoundingError\" 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.207 USD Equity:RoundingError 0.0003 USD","title":"Precision of Inferred Numbers"},{"location":"precision_tolerances.html#porting-existing-input","text":"The inference of tolerance values from the transaction\u2019s numbers is generally good enough to keep existing files working without changes. There may be new errors appearing in older files once we process them with the method described in this document, but they should either point to previously undetected errors in the input, or be fixable with simple addition of a suitable number of digits. As a testimony, porting the author\u2019s very large input file has been a relatively painless process that took less than 1 hour. In order to ease the transition, you will probably want to change the default tolerance for all currencies to match the previous value that Beancount had been using, like this: option \"inferred_tolerance_default\" \"*:0.005\" I would recommend you start with this and fix all errors in your file, then proceed to removing this and fix the rest of errors. This should make it easier to adapt your file to this new behavior. As an example of how to fix a new error\u2026 converting this newly failing transaction from the Integer Amounts section: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.21005 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384 USD by inserting zero\u2019s to provide a locally inferred value like this: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.21005 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384.00 USD is sufficient to silence the balance check.","title":"Porting Existing Input"},{"location":"precision_tolerances.html#representational-issues","text":"Internally, Beancount uses a decimal number representation (not a binary/float representation, neither rational numbers). Calculations that result in a large number of fractional digits are carried out to 28 decimal places (the default precision from the context of Python\u2019s IEEE decimal implementation). This is plenty sufficient, because the method we propose above rarely trickles these types of numbers throughout the system: the tolerances allows us to post the precise amounts declared by users, and only automatically derived prices and costs will possibly result in precisions calculated to an unrealistic number of digits that could creep into aggregations in the rest of the system.","title":"Representational Issues"},{"location":"precision_tolerances.html#references","text":"The original proposal that led to this implementation can be found here . In particular, the proposal highlights on the other systems have attempted to deal with this issue. There are also some discussions on the mailing-list dedicated to this topic. Note that for the longest time, Beancount used a fixed precision of 0.005 across all currencies. This was eliminated once the method described in this document was implemented. Also, for Balance and Pad directives, there used to be a \u201ctolerance\u201d option that was set by default to 0.015 of any units. This option has been deprecated with the merging of the changes described in this document.","title":"References"},{"location":"precision_tolerances.html#historical-notes","text":"Here\u2019s an overview of the status of numbers rendering in Beancount as of March 2016, from the mailing-list : First, it's important to realize how these numbers are represented in memory. They are using the Decimal representation which beyond being able to accurately representing decimal numbers (as opposed to the approximation that binary floats provides) also contains a specific precision. That is, the number 2.00 is represented differently than the numbers 2.0 and 2.000. The numbers \"remember\" which precision they are represented up to. This is important. When I say rendering the numbers to their \"natural precision\" I mean the precision with which they are represented, i.e., 2.0 renders as \"2.0\", 2.000 renders as \"2.000\". Then, there are two DISTINCT topics: (1) tolerances, and (2) precision. \"Tolerances\" are values used to determine how much imprecision is acceptable in balancing transactions. This is used in the verification stage, to determine how much looseness to allow. It should not affect how numbers are rendered. \"Precision\" is perhaps a bit of a misnomer: By that I'm referring to is how many digits the numbers are to be rendered with. Once upon a time - after the shell was already written - these concepts weren't well defined in Beancount and I wasn't dealing with these things consistently. At some point it became clear what I needed to do and I created a class called \"DisplayContext\" which could contain appropriate settings for rendering the precision of numbers for each currency (each currency tends to have its own most common rendering precision, e.g. two digits for USD, one digit for MXN, no digits for JPY and in reports we're typically fine rounding the actual numbers to that precision). So an instance of this DisplayContext is automatically instantiated in the parser and in order to avoid the user having to set these values manually - for Beancount to \"do the right thing\" by default - it is able to accumulate the numbers seen and to deduce the most common and maximum number of digits used from the input, and to use that as the default number of digits for rendering numbers. The most common format/number of digits is used to render the number of units, and the maximum number of digits seen is used to render costs and prices. In addition, this class also has capabilities for aligning to the decimal dot and to insert commas on thousands as well. It separates the control of the formatting from the numbers themselves. MOST of the code that renders numbers uses the DisplayContext (via the to_string() methods) to convert the numbers into strings, such as the web interface and explicit text reports. But NOT ALL... there's a bit of HISTORY here... the SQL shell uses some old special-purpose code to render numbers that I never bothered to convert to the DisplayContext class. There's a TODO item for it. It needs to get converted at some point, but I've neglected doing this so far because I have much bigger plans for the SQL query engine that involve a full rewrite of it with many improvements and I figured I'd do that then. If you recall, the SQL query engine was a prototype, and actually it works, but it is not well covered by unit tests. My purpose with it was to discover through usage what would be useful and to then write a v2 of it that would be much better. Now, about that PRINT command... this is not intended as a reporting tool. The printer's purpose is to print input that accurately represents the content of the transactions. In order to do this, it needs to render the numbers at their \"natural\" precision, so that when they get read back in, they parse into the very same number, that is, with the same number of digits (even if zeros). For this reason, the PRINT command does not attempt to render using the DisplayContext instance derived from the input file - this is on purpose. I could change that, but then round-trip would break: the rounding resulting from formatting using the display context may output transactions which don't balance anymore. As you can see, it's not an obvious topic... Hopefully this should allow you to understand what comes out of Beancount in terms of the precision of the numbers it renders. Note: \"default_tolerances\" has been renamed to \"inferred_tolerance_default\" recently because the name was too general and confusing. Old name will work but generate a warning. I just noticed from your comments and some grepping around that the \"render_commas\" option is not used anymore. I'm not sure how that happened, but I'll go ad fix that right away and set the default value of the DisplayContext derived from the input. I should probably also convert the SQL shell rendering to use the display context regardless of future plans, so that it renders consistently with all the rest. Not sure I can do that this weekend, but I'll log a ticket, here . I hope this helps. You're welcome to ask questions if the above isn't clear. I'm sorry if this isn't entirely obvious... there's been a fair bit of history there and there's a lot of code. I should review the naming of options, I think the tolerance options all have \"tolerance\" in their name, but there aren't options to override the rendering and when I add them they should all have a common name as well.","title":"Historical Notes"},{"location":"precision_tolerances.html#further-reading","text":"What Every Computer Scientist Should Know About Floating-Point Arithmetic This stands in contrast to Ledger which attempts to infer the precision based on other transactions recently parsed in the file, in file order. This has the unfortunate effect of creating \u201ccross-talk\u201d between the transactions in terms of what precision can be used. \u21a9 Note that due to the way Beancount represents numbers internally, it is also not able to distinguish between \u201c230\u201d and \u201c230.\u201d; these parse into the same representation for Beancount. Therefore, we are not able to use that distinction in the input to support a precision of 0.5. \u21a9","title":"Further Reading"},{"location":"rounding_precision_in_beancount.html","text":"Proposal: Rounding & Precision in Beancount \uf0c1 Martin Blais , October 2014 This document describes the problem of rounding errors on Beancount transactions and how they are handled. It also includes a proposal for better handling precision issues in Beancount. Motivation \uf0c1 Balancing Precision \uf0c1 Balancing transactions cannot be done precisely. This has been discussed on the Ledger mailing-list before . It is necessary to allow for some tolerance on the amounts used to balance a transaction. This need is clear when you consider that inputting numbers in a text file implies a limited decimal representation. For example, if you\u2019re going to multiply a number of units and a cost, say both written down with 2 fractional digits, you might end up with a number that has 4 fractional digits, and then you need to compare that result with a cash amount that would typically be entered with only 2 fractional digits, something like this example: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.21 USD If you calculate it, the first posting\u2019s precise balance amount is 227.2067 USD, not 227.21 USD. However, the broker company managing the investment account will apply rounding to the closest cent for the cash withdrawal, and the rounded amount is the correct one to be used. This transaction has to balance; we need to allow for some looseness somehow. The great majority of the cases where mathematical operations occur involve the conversion from a number of units and a price or a cost to a corresponding cash value (e.g., units x cost = total cost). Our task in representing transactional information is the replication of operations that take place mostly in institutions. These operations always involve the rounding of numbers for units and currencies (banks do apply stochastic rounding), and the correct numbers to be used from the perspective of these institutions, and from the perspective of the government, are indeed the rounded numbers themselves. It is a not a question of mathematical purity, but one of practicality, and our system should do the same that banks do. Therefore, I think that we should always post the rounded numbers to accounts. Using rational numbers is not a limitation in that sense, but we must be careful to store rounded numbers where it matters. Automatic Rounding \uf0c1 Another related issue is that of automatically rounding amounts for interpolated numbers. Let\u2019s take our original problematic example again: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash ;; Interpolated posting = -227.2067 USD Here the amount from the second posting is interpolated from the balance amount for the first posting. Ideally, we should find a way to specify how it should round to 2 fractional digits of precision. Note that this affects interpolated prices and costs too: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.27 RGAGX {USD} Assets:Investments:Cash -227.21 USD Here, the cost is intended to be automatically calculated from the cash legs: 227.21 / 4.27 = 53.2107728337\u2026 USD. The correct cost to be inferred is also the rounded amount of 53.21 USD. We would like a mechanism to allow us to infer the desired precision. This mechanism cannot unfortunately be solely based on commodity: different accounts may track currencies with different precisions. As a real-world example, I have a retail FOREX trading account that really uses 4 digits of precision for its prices and deposits. Precision of Balance Assertions \uf0c1 The precision of a balance assertions is also subject to this problem, assertions like this one: 2014-04-01 balance Assets:Investments:Cash 4526.77 USD The user does not intend for this balance check to precisely sum up to 4526.77000000\u2026 USD. However, it this cash account previously received a deposit with a greater precision as in the previous section\u2019s example, then we have a problem. Now the cash amount contains some of the crumbs deposited from the interpolation (0.0067 USD). If we were able to find a good solution for the automatic rounding of postings in the previous section, this would not be a problem. But in the meantime, we must find a solution. Beancount\u2019s current approach is a kludge: it uses a user-configurable tolerance of 0.0150 (in any unit). We\u2019d like to change this so that the tolerance used is able to depend on the commodity, the account, or even the particular directive in use. Other Systems \uf0c1 Other command-line accounting systems differ in how they choose that tolerance: Ledger attempts to automatically derive the precision to use for its balance checks by using recently parsed context (in file order). The precision to be used is that of the last value parsed for the particular commodity under consideration. This can be problematic: it can lead to unnecessary side-effects between transactions which can be difficult to debug . HLedger, on the other hand, uses global precision settings. The whole file is processed first, then the precisions are derived from the most precise numbers seen in the entire input file. At the moment, Beancount uses a constant value for the tolerance used in its balance checking algorithm (0.005 of any unit). This is weak and should, at the very least, be commodity-dependent, if not also dependent on the particular account in which the commodity is used. Proposal \uf0c1 Automatically Inferring Tolerance \uf0c1 Beancount should derive its precision using a method entirely local to each transaction, perhaps with a global value for defaults. That is, for each transaction, it will inspect postings with simple amounts (no cost, no price) and infer the precision to be used for tolerance as half of that of the most precise amount entered by the user on this transaction. For example: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.278 RGAGX {53.21 USD} Assets:Investments:Cash -227.6324 USD Expenses:Commissions 9.95 USD The number of digits for the precision to be used here is the maximum of the 2nd and 3rd postings, that is, max(4, 2) = 4. The first postings is ignored because its amount is the result of a mathematical operation. The tolerance value should be half of the most precise digit, that is 0.00005 USD. This should allow the user to use an arbitrary precision, simply by inserting more digits in the input. Only fractional digits will be used to derive precision\u2026 an integer should imply exact matching. The user could specify a single trailing period to imply a sub-dollar precision. For example, the following transaction should fail to balance because the calculated amount is 999.999455 USD: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 23.45 RGAGX {42.6439 USD} Assets:Investments:Cash -1000 USD Instead, the user should explicitly allow for some tolerance to be used: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 23.45 RGAGX {42.6439 USD} Assets:Investments:Cash -1000. USD Or better, use 1000.00 USD. This has the disadvantage that is prevents the user from specifying the simpler integer amount. I\u2019m not sure if this is a big deal. Finally, no global effect implied by transactions will be applied. No transaction should ever affect any other transaction\u2019s balancing context. Inference on Amounts Held at Cost \uf0c1 An idea from by Matthew Harris ( here ) is that we could also use the value of the to the smallest decimal of the number of units times the cost as a number to use in establishing the tolerance for balancing transactions. For example, in the following transaction: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 23.45 RGAGX {42.6439 USD} ... The tolerance value that could be derived is 0.01 RGAGX x 42.6439 USD = 0.426439 USD The original use case presented by Matthew was of a transaction that did not contain a simple amount, just a conversion with both legs held at cost: 2011-01-25 * \"Transfer of Assets, 3467.90 USD\" * Assets:RothIRA:Vanguard:VTIVX 250.752 VTIVX {18.35 USD} @ 13.83 USD * Assets:RothIRA:DodgeCox:DODGX -30.892 DODGX {148.93 USD} @ 112.26 USD I\u2019ve actually tried to implement this and the resulting tolerances are either unacceptably wide or unacceptably small. It does not work well in practice so I\u2019ve abandoned the idea. Automated Rounding \uf0c1 For values that are automatically calculated, for example, on auto-postings where the remaining value is derived automatically, we should consider rounding the values. No work has been done on this yet; these values are currently not rounded. Fixing Balance Assertions \uf0c1 To fix balance assertions, we will derive the required precision by the number of digits used in the balance amount itself, by looking at the most precision fractional digit and using half of that digit\u2019s value to compute the tolerance: 2014-04-01 balance Assets:Investments:Cash 4526.7702 USD This balance check implies a precision of 0.00005 USD. If you use an integer number of units, no tolerance is allowed. The precise number should match: 2014-04-01 balance Assets:Investments:Cash 4526 USD If you want to allow for sub-dollar variance, use a single comma: 2014-04-01 balance Assets:Investments:Cash 4526. USD This balance check implies a precision of 0.50 USD. Approximate Assertions \uf0c1 Another idea, proposed in this ticket on Ledger , proposes an explicitly approximate assertion. We could implement it this way (just an idea): 2014-04-01 balance Assets:Investments:Cash 4526.00 +/- 0.05 USD Accumulating & Reporting Residuals \uf0c1 In order to explicitly render and monitor the amount of rounding errors that occur in a Ledger, we should accumulate it to an Equity account , such as \u201cEquity:Rounding\u201d. This should be turned on optionally. It should be possible for the user to specify an account to be used to accumulate the error. Whenever a transaction does not balance exactly, the residual, or rounding error, will be inserted as a posting of the transaction to the equity account. By default, this accumulation should be turned off. It\u2019s not clear whether the extra postings will be disruptive yet (if they\u2019re not, maybe this should be turned on by default; practice will inform us). Implementation \uf0c1 The implementation of this proposal is documented here .","title":"Rounding Precision in Beancount"},{"location":"rounding_precision_in_beancount.html#proposal-rounding-precision-in-beancount","text":"Martin Blais , October 2014 This document describes the problem of rounding errors on Beancount transactions and how they are handled. It also includes a proposal for better handling precision issues in Beancount.","title":"Proposal: Rounding & Precision in Beancount"},{"location":"rounding_precision_in_beancount.html#motivation","text":"","title":"Motivation"},{"location":"rounding_precision_in_beancount.html#balancing-precision","text":"Balancing transactions cannot be done precisely. This has been discussed on the Ledger mailing-list before . It is necessary to allow for some tolerance on the amounts used to balance a transaction. This need is clear when you consider that inputting numbers in a text file implies a limited decimal representation. For example, if you\u2019re going to multiply a number of units and a cost, say both written down with 2 fractional digits, you might end up with a number that has 4 fractional digits, and then you need to compare that result with a cash amount that would typically be entered with only 2 fractional digits, something like this example: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.21 USD If you calculate it, the first posting\u2019s precise balance amount is 227.2067 USD, not 227.21 USD. However, the broker company managing the investment account will apply rounding to the closest cent for the cash withdrawal, and the rounded amount is the correct one to be used. This transaction has to balance; we need to allow for some looseness somehow. The great majority of the cases where mathematical operations occur involve the conversion from a number of units and a price or a cost to a corresponding cash value (e.g., units x cost = total cost). Our task in representing transactional information is the replication of operations that take place mostly in institutions. These operations always involve the rounding of numbers for units and currencies (banks do apply stochastic rounding), and the correct numbers to be used from the perspective of these institutions, and from the perspective of the government, are indeed the rounded numbers themselves. It is a not a question of mathematical purity, but one of practicality, and our system should do the same that banks do. Therefore, I think that we should always post the rounded numbers to accounts. Using rational numbers is not a limitation in that sense, but we must be careful to store rounded numbers where it matters.","title":"Balancing Precision"},{"location":"rounding_precision_in_beancount.html#automatic-rounding","text":"Another related issue is that of automatically rounding amounts for interpolated numbers. Let\u2019s take our original problematic example again: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash ;; Interpolated posting = -227.2067 USD Here the amount from the second posting is interpolated from the balance amount for the first posting. Ideally, we should find a way to specify how it should round to 2 fractional digits of precision. Note that this affects interpolated prices and costs too: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.27 RGAGX {USD} Assets:Investments:Cash -227.21 USD Here, the cost is intended to be automatically calculated from the cash legs: 227.21 / 4.27 = 53.2107728337\u2026 USD. The correct cost to be inferred is also the rounded amount of 53.21 USD. We would like a mechanism to allow us to infer the desired precision. This mechanism cannot unfortunately be solely based on commodity: different accounts may track currencies with different precisions. As a real-world example, I have a retail FOREX trading account that really uses 4 digits of precision for its prices and deposits.","title":"Automatic Rounding"},{"location":"rounding_precision_in_beancount.html#precision-of-balance-assertions","text":"The precision of a balance assertions is also subject to this problem, assertions like this one: 2014-04-01 balance Assets:Investments:Cash 4526.77 USD The user does not intend for this balance check to precisely sum up to 4526.77000000\u2026 USD. However, it this cash account previously received a deposit with a greater precision as in the previous section\u2019s example, then we have a problem. Now the cash amount contains some of the crumbs deposited from the interpolation (0.0067 USD). If we were able to find a good solution for the automatic rounding of postings in the previous section, this would not be a problem. But in the meantime, we must find a solution. Beancount\u2019s current approach is a kludge: it uses a user-configurable tolerance of 0.0150 (in any unit). We\u2019d like to change this so that the tolerance used is able to depend on the commodity, the account, or even the particular directive in use.","title":"Precision of Balance Assertions"},{"location":"rounding_precision_in_beancount.html#other-systems","text":"Other command-line accounting systems differ in how they choose that tolerance: Ledger attempts to automatically derive the precision to use for its balance checks by using recently parsed context (in file order). The precision to be used is that of the last value parsed for the particular commodity under consideration. This can be problematic: it can lead to unnecessary side-effects between transactions which can be difficult to debug . HLedger, on the other hand, uses global precision settings. The whole file is processed first, then the precisions are derived from the most precise numbers seen in the entire input file. At the moment, Beancount uses a constant value for the tolerance used in its balance checking algorithm (0.005 of any unit). This is weak and should, at the very least, be commodity-dependent, if not also dependent on the particular account in which the commodity is used.","title":"Other Systems"},{"location":"rounding_precision_in_beancount.html#proposal","text":"","title":"Proposal"},{"location":"rounding_precision_in_beancount.html#automatically-inferring-tolerance","text":"Beancount should derive its precision using a method entirely local to each transaction, perhaps with a global value for defaults. That is, for each transaction, it will inspect postings with simple amounts (no cost, no price) and infer the precision to be used for tolerance as half of that of the most precise amount entered by the user on this transaction. For example: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.278 RGAGX {53.21 USD} Assets:Investments:Cash -227.6324 USD Expenses:Commissions 9.95 USD The number of digits for the precision to be used here is the maximum of the 2nd and 3rd postings, that is, max(4, 2) = 4. The first postings is ignored because its amount is the result of a mathematical operation. The tolerance value should be half of the most precise digit, that is 0.00005 USD. This should allow the user to use an arbitrary precision, simply by inserting more digits in the input. Only fractional digits will be used to derive precision\u2026 an integer should imply exact matching. The user could specify a single trailing period to imply a sub-dollar precision. For example, the following transaction should fail to balance because the calculated amount is 999.999455 USD: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 23.45 RGAGX {42.6439 USD} Assets:Investments:Cash -1000 USD Instead, the user should explicitly allow for some tolerance to be used: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 23.45 RGAGX {42.6439 USD} Assets:Investments:Cash -1000. USD Or better, use 1000.00 USD. This has the disadvantage that is prevents the user from specifying the simpler integer amount. I\u2019m not sure if this is a big deal. Finally, no global effect implied by transactions will be applied. No transaction should ever affect any other transaction\u2019s balancing context.","title":"Automatically Inferring Tolerance"},{"location":"rounding_precision_in_beancount.html#inference-on-amounts-held-at-cost","text":"An idea from by Matthew Harris ( here ) is that we could also use the value of the to the smallest decimal of the number of units times the cost as a number to use in establishing the tolerance for balancing transactions. For example, in the following transaction: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 23.45 RGAGX {42.6439 USD} ... The tolerance value that could be derived is 0.01 RGAGX x 42.6439 USD = 0.426439 USD The original use case presented by Matthew was of a transaction that did not contain a simple amount, just a conversion with both legs held at cost: 2011-01-25 * \"Transfer of Assets, 3467.90 USD\" * Assets:RothIRA:Vanguard:VTIVX 250.752 VTIVX {18.35 USD} @ 13.83 USD * Assets:RothIRA:DodgeCox:DODGX -30.892 DODGX {148.93 USD} @ 112.26 USD I\u2019ve actually tried to implement this and the resulting tolerances are either unacceptably wide or unacceptably small. It does not work well in practice so I\u2019ve abandoned the idea.","title":"Inference on Amounts Held at Cost"},{"location":"rounding_precision_in_beancount.html#automated-rounding","text":"For values that are automatically calculated, for example, on auto-postings where the remaining value is derived automatically, we should consider rounding the values. No work has been done on this yet; these values are currently not rounded.","title":"Automated Rounding"},{"location":"rounding_precision_in_beancount.html#fixing-balance-assertions","text":"To fix balance assertions, we will derive the required precision by the number of digits used in the balance amount itself, by looking at the most precision fractional digit and using half of that digit\u2019s value to compute the tolerance: 2014-04-01 balance Assets:Investments:Cash 4526.7702 USD This balance check implies a precision of 0.00005 USD. If you use an integer number of units, no tolerance is allowed. The precise number should match: 2014-04-01 balance Assets:Investments:Cash 4526 USD If you want to allow for sub-dollar variance, use a single comma: 2014-04-01 balance Assets:Investments:Cash 4526. USD This balance check implies a precision of 0.50 USD.","title":"Fixing Balance Assertions"},{"location":"rounding_precision_in_beancount.html#approximate-assertions","text":"Another idea, proposed in this ticket on Ledger , proposes an explicitly approximate assertion. We could implement it this way (just an idea): 2014-04-01 balance Assets:Investments:Cash 4526.00 +/- 0.05 USD","title":"Approximate Assertions"},{"location":"rounding_precision_in_beancount.html#accumulating-reporting-residuals","text":"In order to explicitly render and monitor the amount of rounding errors that occur in a Ledger, we should accumulate it to an Equity account , such as \u201cEquity:Rounding\u201d. This should be turned on optionally. It should be possible for the user to specify an account to be used to accumulate the error. Whenever a transaction does not balance exactly, the residual, or rounding error, will be inserted as a posting of the transaction to the equity account. By default, this accumulation should be turned off. It\u2019s not clear whether the extra postings will be disruptive yet (if they\u2019re not, maybe this should be turned on by default; practice will inform us).","title":"Accumulating & Reporting Residuals"},{"location":"rounding_precision_in_beancount.html#implementation","text":"The implementation of this proposal is documented here .","title":"Implementation"},{"location":"running_beancount_and_generating_reports.html","text":"Running Beancount & Generating Reports \uf0c1 Martin Blais , July-Sep 2014 http://furius.ca/beancount/doc/tools Introduction Tools bean-check bean-report bean-query bean-web Global Pages View Reports Pages bean-bake bean-doctor Context bean-format bean-example Filtering Transactions Reports Balance Reports Trial Balance (balances) Balance Sheet (balsheet) Opening Balances (openbal) Income Statement (income) Journal Reports Journal (journal) Rendering at Cost Adding a Balance Column Character Width Precision Compact, Normal or Verbose Summary Equivalent SQL Query Conversions (conversions) Documents (documents) Holdings Reports Holdings & Aggregations (holdings*) Net Worth (networth) Other Report Types Cash Prices (prices) Statistics (stats) Update Activity (activity) Introduction \uf0c1 This document describes the tools you use to process Beancount input files, and many of the reports available from it. The syntax of the language is described in the Beancount Language Syntax document. This manual only covers the technical details for using Beancount from the command-line. Tools \uf0c1 bean-check \uf0c1 bean-check is the program you use to verify that your input syntax and transactions work correctly. All it does is load your input file and run the various plugins you configured in it, plus some extra validation checks. It report errors (if any), and then exits. You run it on your input file, like this: bean-check /path/to/my/file.beancount If there are no errors, there should be no output, it should exit quietly. If there were errors, they will be printed to stderr with the filename, line number and error description (in a format that is understood by Emacs, so you can just use next-error and previous-error to navigate to the file if you want): /home/user/myledger.beancount:44381: Transaction does not balance: 34.46 USD 2014-07-12 * \"Black Iron Burger\" \"\" Expenses:Food:Restaurant 17.23 USD Assets:Cash 17.23 USD You should always fix all the errors before producing reports. bean-report \uf0c1 This is the main tool used to extract specialized reports to the console in text or one of the various other formats. You invoke it like this: bean-report /path/to/my/file.beancount For example: bean-report /path/to/my/file.beancount balances There are many reports available. See the section on reports for a description of the main ones. If you want to produce the full list of reports, ask it for help: bean-report --help-reports Report names can sometimes accept arguments. At the moment the arguments are specified as part of the report name itself, often separated by a colon (:), like this: bean-report /path/to/my/file.beancount balances:Vanguard There are a few special reports you should know about: check, or validate: This is the same as running the bean-check command. print: This simply prints out the entries that Beancount has parsed, in Beancount syntax. This can be used to confirm that Beancount has read and interpreted your input data correctly (if you\u2019re debugging something difficult). The other reports are what you\u2019d expect: they print out various tables of aggregations of amounts. The reports you can generate are described in a dedicated section below. PLEASE NOTE! At the moment of release, the list of reports available from the web page will differ from the list available from the console. In a future release, I will consolidate those two lists and all the reports that are available from the web pages will also be available from the console, and in many different formats. Stay tuned. bean-query \uf0c1 Beancount\u2019s parsed list of transactions and postings is like an in-memory database. bean-query is a command-line tool that acts like a client to that in-memory database in which you can type queries in a variant of SQL. You invoke it like this: bean-query /path/to/my/file.beancount Input file: \"/path/to/my/file.beancount\" Ready with 14212 directives (21284 postings in 8879 transactions). beancount> _ More details are available in its own document . bean-web \uf0c1 bean-web serves all the reports on a web server that runs on your computer. You run it like this: bean-web /path/to/my/file.beancount It will serve all pages on port 8080 on your machine. Navigate to http://localhost:8080 with a web browser. You should be able to click your way through all the reports easily. The web interface provides a set of global pages and a set of report pages for each \u201cview.\u201d Global Pages \uf0c1 The top-level table of contents page provides links to all the global pages at the top: The table of contents (the page you\u2019re looking at) A list of the errors that occurred in your ledger file A view of the source code of your Ledger file (this is used by various other links when referring to a location in your input file.) The table of contents provides a convenient list of links to all the common views, such as \u201cview by year\u201d, \u201cview by tag\u201d, and of course, \u201cview all transactions.\u201d There are a few more. View Reports Pages \uf0c1 When you click on a view report page, you enter a set of pages for the subset of transactions for that view. Various reports about those transactions are available from here: Opening balances (a balance sheet at the beginning of the view) Balance sheet (a balance sheet at the end of the view) Income statement (for the period of the view) Various journals for each account (just click on an account name) Various reports of holdings at the end of the view Lists of documents and prices included in the view\u2019s entries Some statistics about the view data \u2026 and much more. There should be an index of all the available view reports. bean-bake \uf0c1 bean-bake runs a bean-web instance and bakes all the pages to a directory: bean-bake /path/to/my/file.beancount myfinances It also support baking directly to an archive file: bean-bake /path/to/my/file.beancount myfinances.zip Various compression methods are supported, e.g. .tar.gz. This is useful to share the web interface with your accountant or other people, who usually don\u2019t have the ability to run Beancount. The page links have all been converted to relative links, and they should be able to extract the archive to a directory and browse all the reports the same way you do with bean-web . bean-doctor \uf0c1 This is a debugging tool used to perform various diagnostics and run debugging commands, and to help provide information for reporting bugs. For example, it can do the following: List the Beancount dependencies that are installed on your machine, and the ones that are missing. It can tell you what you\u2019re missing that you should be installing. Print a dump of the parser\u2019s lexer tokens for your input file. If you report a parsing bug, it can be useful to look at the lexer output (if you know what you\u2019re doing). It can run your input file through a parsing round-trip, that is, print out the file and re-read it again and compare it. This is a useful parser and printer test. It can check that a directory hierarchy corresponds to a Beancount input file\u2019s chart-of-accounts, and reporting directories that do not comply. This is useful in case you decide to change some account names and are maintaining a corresponding archive of documents which needs to be adjusted accordingly. Context \uf0c1 It can list the context upon which a transaction is applied, i.e., the inventory balances of each account the transaction postings modify, before and after it is applied. Use it like this: $ bean-doctor context /home/blais/accounting/blais.beancount 28514 /home/blais/accounting/blais.beancount:28513: ; Assets:US:ETrade:BND 100.00 BND {81.968730000000 USD} ; Assets:US:ETrade:Cash 8143.97 USD 2014-07-25 * \"(TRD) BOT +50 BND @82.10\" ^273755872 Assets:US:ETrade:BND 50.00 BND {82.10 USD} Assets:US:ETrade:Cash -4105.00 USD ; Assets:US:ETrade:BND 100.00 BND {81.968730000000 USD} ; ! Assets:US:ETrade:BND 50.00 BND {82.10 USD} ; ! Assets:US:ETrade:Cash 4038.97 USD There is a corresponding Emacs binding (C-c x) to invoke this around the cursor. bean-format \uf0c1 This pure text processing tool will reformat Beancount input to right-align all the numbers at the same, minimal column. It left-aligns all the currencies. It only modifies whitespace. This tool accepts a filename as arguments and outputs the aligned file on stdout (similar to UNIX cat ). bean-example \uf0c1 This program generates an example Beancount input file. See the Tutorial for more details about the contents of this example file. Filtering Transactions \uf0c1 In order to produce different views of your financial transactions, we select a subset of full list of parsed transactions, for example, \u201call the transactions that occurred in year 2013\u201d, and then use that to produce the various available reports that Beancount provides. At the moment, only a preset list of filters are available as \u201cviews\u201d from the web interface. These views include: All transactions Transactions that occur in a particular year Transactions with a particular tag Transactions with a particular payee Transactions that involve at least one account with a particular name component At the moment, in order to access reports from these subsets of transactions, you need to use the bean-web web interface, and click on the related keyword in the root global page, which enters you into a set of reports for that view. Reports \uf0c1 The whole point of entering your transactions in a single input file in the first place is that it allows you to sum, filter, aggregate and arrange various subsets of your data into well-known reports. There are three distinct ways to produce reports from Beancount: by using bean-web and browsing to a view and then to a specific report (this is the easy way), by using bean-report and providing the name of a desired report (and possibly some report-specific arguments), and by using bean-query and requesting data by specifying an SQL statement. Reports can sometimes be rendered in different file formats. Each report type will support being rendered in a list of common ones, such as console text, HTML and CSV. Some reports render in Beancount syntax itself, and we simply call this format name \u201cbeancount.\u201d There are many types of reports available, and there will be many more in the future, as many of the features on the roadmap involve new types of output. This section provides an overview of the most common ones. Use bean-report to inquire about the full list supported by your installed version: bean-report --help-reports PLEASE NOTE! At the moment, the sets of reports that are available from the web interface and from the console are different, though there is some overlap. In a subsequent version, the list of reports will be reconciled and all reports will be made available via both the web interface and the console, in a variety of data formats (text, CSV, HTML and maybe others.) For now, we will document these in the sections below. Balance Reports \uf0c1 All the balance reports are similar in that they produce tables of some set of accounts and their associated balances: [output] |-- Assets | `-- US | |-- BofA | | `-- Checking 596.05 USD | |-- ETrade | | |-- Cash 5,120.50 USD | | |-- GLD 70.00 GLD | | |-- ITOT 17.00 ITOT | | |-- VEA 36.00 VEA | | `-- VHT 294.00 VHT | |-- Federal | | `-- PreTax401k | |-- Hoogle | | `-- Vacation 337.26 VACHR | `-- Vanguard ... Balances for commodities held \u201cat cost\u201d are rendered at their book value. (Unrealized gains, if any, are inserted as separate transactions by an optional plugin and the result amounts get mixed in with the cost basis if rendered in the same account.) If an account\u2019s balance contains many different types of currencies (commodities not held \u201cat cost\u201d, such as dollars, euros, yen), each gets printed on its own line. This can render the balance column a bit too busy and messes with the vertical regularity of account names. This is to some extent an unavoidable compromise, but in practice, there are only a small number of commodities that form the large majority of a user\u2019s ledger: the home country\u2019s currency units. To this extent, amounts in common currencies can be broken out into their own column using the \u201c operating_currency \u201d option: option \"operating_currency\" \"USD\" option \"operating_currency\" \"CAD\" You may use this option multiple times if you have many of them (this is my case, for instance, because I am an expat and hold assets in both my host and home countries). Declaring operating currencies also hold the advantage that the name of the currency need not be rendered, and it can thus more easily be imported into a spreadsheet. Finally, some accounts are deemed \u201cactive\u201d if they have not been closed. A closed account with no transactions in the filtered set of transactions will not be rendered. Trial Balance ( balances ) \uf0c1 A trial balance report simply produces a table of final balances of all the active accounts, with all the accounts rendered vertically. The sum total of all balances is reported at the bottom. Unlike a balance sheet, it may not always balance to zero because of currency conversions. (This is the equivalent of Ledger\u2019s bal report.) The equivalent bean-query command is: SELECT account, sum(position) GROUP BY account ORDER BY account; Balance Sheet ( balsheet ) \uf0c1 A balance sheet is a snapshot of the balances of the Assets, Liabilities and Equity accounts at a particular point in time. In order to build such a report, we have to move balances from the other accounts to it: compute the balances of the Income and Expenses account at that point in time, insert transactions that will zero out balances from these accounts by transferring them to an equity account ( Equity:Earnings:Current ), render a tree of the balances of the Assets accounts on the left side, render a tree of the Liabilities accounts on the right side, render a tree of the Equity accounts below the Liabilities accounts. See the introduction document for an example. Note that the Equity accounts include the amounts reported from the Income and Expenses accounts, also often called \u201cNet Income.\u201d Also, in practice, we make two transfers because we\u2019re typically looking at a reporting period, and we want to differentiate between Net Income amounts transferred before the beginning of the period ( Equity:Earnings:Previous ) and during the period itself ( Equity:Earnings:Current ). And similar pair of transfers is carried out in order to handle currency conversions (this is a bit of a hairy topic, but one with a great solution; refer to the dedicated document if you want all the details). The equivalent bean-query command is: SELECT account, sum(position) FROM CLOSE ON 2016-01-01 GROUP BY account ORDER BY account; Opening Balances ( openbal ) \uf0c1 The opening balances report is simply a balance sheet drawn at the beginning of the reporting period. This report only makes sense for a list of filtered entries that represents a period of time, such as \u201cyear 2014.\u201d The balance sheet is generated using only the summarization entries that were synthesized when the transactions were filtered (see the double-entry method document ). Income Statement ( income ) \uf0c1 An income statement lists the final balances of the Income and Expenses accounts. It represents a summary of the transient activity within these accounts. If the balance sheet is the snapshot at a particular point in time, this is the difference between the beginning and the end of a period (in our case: of a filtered set of transactions). The balances of the active Income accounts are rendered on the left, and those of the active Expenses accounts on the right. See the introduction document for an example. The difference between the total of Income and Expenses balances is the Net Income. Note that the initial balances of the Income and Expenses accounts should have been zero\u2019ed out by summarization transactions that occur at the beginning of the period, because we\u2019re only interested in the changes in these accounts. Journal Reports \uf0c1 The reports in this section render lists of transactions and other directives in a linear fashion. Journal ( journal ) \uf0c1 This report is the equivalent of an \u201caccount statement\u201d from an institution, a list of transactions with at least one posting in that account. This is the equivalent of Ledger\u2019s register report ( reg ). You generate a journal report like this: bean-report myfile.beancount journal \u2026 By default, this renders a journal of all the transactions, which is unlikely to be what you want. Select a particular account to render like this: bean-report myfile.beancount journal -a Expenses:Restaurant At the moment, the \u201c -a \u201d option accepts only a complete account name, or the name of one of the parent accounts. Eventually we will extend it to handle expressions. Rendering at Cost \uf0c1 The numbers column on the right displays the changes from the postings of the selected account. Notice that only the balances for the postings affecting the given account are rendered. The change column renders the changes in the units affected. For example, if this posting is selected: Assets:Investments:Apple 2 AAPL {402.00 USD} The value reported for the change will be \u201c 2 AAPL \u201d. If you would like to render the values at cost, use the \u201c --at-cost \u201d or \u201c -c \u201d option, which will in this case render \u201c 804.00 USD \u201d instead. There is no \u201cmarket value\u201d option. Unrealized gains are automatically inserted at the end of the history by the \u201c beancount.plugins.unrealized \u201d plugin. See options for that plugin to insert its unrealized gains. Note that if the sum of the selected postings is zero, no amount is rendered in the change column. Adding a Balance Column \uf0c1 If you want to add a column that sums up the running balance for the reported changes, use the \u201c --render-balance \u201d or \u201c -b \u201d option. This does not always make sense to report, so it is up to you to decide whether you want a running balance. Character Width \uf0c1 By default, the report will be as wide as your terminal allows. Restrict the width to a set number of characters with the \u201c-w\u201d option. Precision \uf0c1 The number of fractional digits for the number rendering can be specified via \u201c --precision \u201d or \u201c -k \u201d. Compact, Normal or Verbose \uf0c1 In its normal operation, Beancount renders an empty line between transactions. This helps delineate transactions where there are multiple currencies affected, as they render on separate lines. If you want a more compact rendering, use the \u201c --compact \u201d or \u201c -x \u201d option. On the other hand, if you want to render the affected postings under the transaction line, use the \u201c --verbose \u201d or \u201c -X \u201d option. Summary \uf0c1 Here is a summary of its arguments: optional arguments: -h, --help show this help message and exit -a ACCOUNT, --account ACCOUNT Account to render -w WIDTH, --width WIDTH The number of characters wide to render the report to -k PRECISION, --precision PRECISION The number of digits to render after the period -b, --render-balance, --balance If true, render a running balance -c, --at-cost, --cost If true, render values at cost -x, --compact Rendering compactly -X, --verbose Rendering verbosely Equivalent SQL Query \uf0c1 The equivalent bean-query command is: SELECT date, flag, description, account, cost(position), cost(balance); Conversions ( conversions ) \uf0c1 This report lists the total of currency conversions that result from the selected transactions. (Most people won\u2019t need this.) Documents ( documents ) \uf0c1 This report produces an HTML list of all the external documents found in the ledger, either from explicit directives or from plugins that automatically find the documents and add them to the stream of transactions. Holdings Reports \uf0c1 These reports produces aggregations for assets held at cost. Holdings & Aggregations ( holdings* ) \uf0c1 This report produces a detailed list of all holdings found in the ledger. You can produce aggregates by commodity and accounts using the \u201c-g\u201d option. Net Worth ( networth ) \uf0c1 This report produces a short summary of the net worth (equity) of the ledger, in each of the operating currencies. Other Report Types \uf0c1 Cash \uf0c1 This report renders balances in commodities not held at cost, in other words, cash: bean-report example.beancount cash -c USD Account Units Currency Cost Currency Average Cost Price Book Value Market Value -------------------------- --------- -------- ------------- ------------ ----- ---------- ------------ Assets:US:BofA:Checking 596.05 USD USD 596.05 596.05 Assets:US:ETrade:Cash 5,120.50 USD USD 5,120.50 5,120.50 Assets:US:Hoogle:Vacation 337.26 VACHR Assets:US:Vanguard:Cash -0.02 USD USD -0.02 -0.02 Liabilities:US:Chase:Slate -2,891.85 USD USD -2,891.85 -2,891.85 -------------------------- --------- -------- ------------- ------------ ----- ---------- ------------ The report allows you to convert all currencies to a common currency (in the example above, \"convert everything to USD\"). There's also an option to report only on the operating currencies. I use this to get an overview of all uninvested cash. Prices ( prices ) \uf0c1 This report renders a list of price points for a base currency in terms of a quote currency. The list is sorted by date. You can output this table in beancount format as well. This is convenient to save a price database to a file, that can then be combined and loaded into another input file. Statistics ( stats ) \uf0c1 This report simply provides various statistics on the parsed entries. Update Activity ( activity ) \uf0c1 This table renders for each account the date of the last entry.","title":"Running Beancount and Generating Reports"},{"location":"running_beancount_and_generating_reports.html#running-beancount-generating-reports","text":"Martin Blais , July-Sep 2014 http://furius.ca/beancount/doc/tools Introduction Tools bean-check bean-report bean-query bean-web Global Pages View Reports Pages bean-bake bean-doctor Context bean-format bean-example Filtering Transactions Reports Balance Reports Trial Balance (balances) Balance Sheet (balsheet) Opening Balances (openbal) Income Statement (income) Journal Reports Journal (journal) Rendering at Cost Adding a Balance Column Character Width Precision Compact, Normal or Verbose Summary Equivalent SQL Query Conversions (conversions) Documents (documents) Holdings Reports Holdings & Aggregations (holdings*) Net Worth (networth) Other Report Types Cash Prices (prices) Statistics (stats) Update Activity (activity)","title":"Running Beancount & Generating Reports"},{"location":"running_beancount_and_generating_reports.html#introduction","text":"This document describes the tools you use to process Beancount input files, and many of the reports available from it. The syntax of the language is described in the Beancount Language Syntax document. This manual only covers the technical details for using Beancount from the command-line.","title":"Introduction"},{"location":"running_beancount_and_generating_reports.html#tools","text":"","title":"Tools"},{"location":"running_beancount_and_generating_reports.html#bean-check","text":"bean-check is the program you use to verify that your input syntax and transactions work correctly. All it does is load your input file and run the various plugins you configured in it, plus some extra validation checks. It report errors (if any), and then exits. You run it on your input file, like this: bean-check /path/to/my/file.beancount If there are no errors, there should be no output, it should exit quietly. If there were errors, they will be printed to stderr with the filename, line number and error description (in a format that is understood by Emacs, so you can just use next-error and previous-error to navigate to the file if you want): /home/user/myledger.beancount:44381: Transaction does not balance: 34.46 USD 2014-07-12 * \"Black Iron Burger\" \"\" Expenses:Food:Restaurant 17.23 USD Assets:Cash 17.23 USD You should always fix all the errors before producing reports.","title":"bean-check"},{"location":"running_beancount_and_generating_reports.html#bean-report","text":"This is the main tool used to extract specialized reports to the console in text or one of the various other formats. You invoke it like this: bean-report /path/to/my/file.beancount For example: bean-report /path/to/my/file.beancount balances There are many reports available. See the section on reports for a description of the main ones. If you want to produce the full list of reports, ask it for help: bean-report --help-reports Report names can sometimes accept arguments. At the moment the arguments are specified as part of the report name itself, often separated by a colon (:), like this: bean-report /path/to/my/file.beancount balances:Vanguard There are a few special reports you should know about: check, or validate: This is the same as running the bean-check command. print: This simply prints out the entries that Beancount has parsed, in Beancount syntax. This can be used to confirm that Beancount has read and interpreted your input data correctly (if you\u2019re debugging something difficult). The other reports are what you\u2019d expect: they print out various tables of aggregations of amounts. The reports you can generate are described in a dedicated section below. PLEASE NOTE! At the moment of release, the list of reports available from the web page will differ from the list available from the console. In a future release, I will consolidate those two lists and all the reports that are available from the web pages will also be available from the console, and in many different formats. Stay tuned.","title":"bean-report"},{"location":"running_beancount_and_generating_reports.html#bean-query","text":"Beancount\u2019s parsed list of transactions and postings is like an in-memory database. bean-query is a command-line tool that acts like a client to that in-memory database in which you can type queries in a variant of SQL. You invoke it like this: bean-query /path/to/my/file.beancount Input file: \"/path/to/my/file.beancount\" Ready with 14212 directives (21284 postings in 8879 transactions). beancount> _ More details are available in its own document .","title":"bean-query"},{"location":"running_beancount_and_generating_reports.html#bean-web","text":"bean-web serves all the reports on a web server that runs on your computer. You run it like this: bean-web /path/to/my/file.beancount It will serve all pages on port 8080 on your machine. Navigate to http://localhost:8080 with a web browser. You should be able to click your way through all the reports easily. The web interface provides a set of global pages and a set of report pages for each \u201cview.\u201d","title":"bean-web"},{"location":"running_beancount_and_generating_reports.html#global-pages","text":"The top-level table of contents page provides links to all the global pages at the top: The table of contents (the page you\u2019re looking at) A list of the errors that occurred in your ledger file A view of the source code of your Ledger file (this is used by various other links when referring to a location in your input file.) The table of contents provides a convenient list of links to all the common views, such as \u201cview by year\u201d, \u201cview by tag\u201d, and of course, \u201cview all transactions.\u201d There are a few more.","title":"Global Pages"},{"location":"running_beancount_and_generating_reports.html#view-reports-pages","text":"When you click on a view report page, you enter a set of pages for the subset of transactions for that view. Various reports about those transactions are available from here: Opening balances (a balance sheet at the beginning of the view) Balance sheet (a balance sheet at the end of the view) Income statement (for the period of the view) Various journals for each account (just click on an account name) Various reports of holdings at the end of the view Lists of documents and prices included in the view\u2019s entries Some statistics about the view data \u2026 and much more. There should be an index of all the available view reports.","title":"View Reports Pages"},{"location":"running_beancount_and_generating_reports.html#bean-bake","text":"bean-bake runs a bean-web instance and bakes all the pages to a directory: bean-bake /path/to/my/file.beancount myfinances It also support baking directly to an archive file: bean-bake /path/to/my/file.beancount myfinances.zip Various compression methods are supported, e.g. .tar.gz. This is useful to share the web interface with your accountant or other people, who usually don\u2019t have the ability to run Beancount. The page links have all been converted to relative links, and they should be able to extract the archive to a directory and browse all the reports the same way you do with bean-web .","title":"bean-bake"},{"location":"running_beancount_and_generating_reports.html#bean-doctor","text":"This is a debugging tool used to perform various diagnostics and run debugging commands, and to help provide information for reporting bugs. For example, it can do the following: List the Beancount dependencies that are installed on your machine, and the ones that are missing. It can tell you what you\u2019re missing that you should be installing. Print a dump of the parser\u2019s lexer tokens for your input file. If you report a parsing bug, it can be useful to look at the lexer output (if you know what you\u2019re doing). It can run your input file through a parsing round-trip, that is, print out the file and re-read it again and compare it. This is a useful parser and printer test. It can check that a directory hierarchy corresponds to a Beancount input file\u2019s chart-of-accounts, and reporting directories that do not comply. This is useful in case you decide to change some account names and are maintaining a corresponding archive of documents which needs to be adjusted accordingly.","title":"bean-doctor"},{"location":"running_beancount_and_generating_reports.html#context","text":"It can list the context upon which a transaction is applied, i.e., the inventory balances of each account the transaction postings modify, before and after it is applied. Use it like this: $ bean-doctor context /home/blais/accounting/blais.beancount 28514 /home/blais/accounting/blais.beancount:28513: ; Assets:US:ETrade:BND 100.00 BND {81.968730000000 USD} ; Assets:US:ETrade:Cash 8143.97 USD 2014-07-25 * \"(TRD) BOT +50 BND @82.10\" ^273755872 Assets:US:ETrade:BND 50.00 BND {82.10 USD} Assets:US:ETrade:Cash -4105.00 USD ; Assets:US:ETrade:BND 100.00 BND {81.968730000000 USD} ; ! Assets:US:ETrade:BND 50.00 BND {82.10 USD} ; ! Assets:US:ETrade:Cash 4038.97 USD There is a corresponding Emacs binding (C-c x) to invoke this around the cursor.","title":"Context"},{"location":"running_beancount_and_generating_reports.html#bean-format","text":"This pure text processing tool will reformat Beancount input to right-align all the numbers at the same, minimal column. It left-aligns all the currencies. It only modifies whitespace. This tool accepts a filename as arguments and outputs the aligned file on stdout (similar to UNIX cat ).","title":"bean-format"},{"location":"running_beancount_and_generating_reports.html#bean-example","text":"This program generates an example Beancount input file. See the Tutorial for more details about the contents of this example file.","title":"bean-example"},{"location":"running_beancount_and_generating_reports.html#filtering-transactions","text":"In order to produce different views of your financial transactions, we select a subset of full list of parsed transactions, for example, \u201call the transactions that occurred in year 2013\u201d, and then use that to produce the various available reports that Beancount provides. At the moment, only a preset list of filters are available as \u201cviews\u201d from the web interface. These views include: All transactions Transactions that occur in a particular year Transactions with a particular tag Transactions with a particular payee Transactions that involve at least one account with a particular name component At the moment, in order to access reports from these subsets of transactions, you need to use the bean-web web interface, and click on the related keyword in the root global page, which enters you into a set of reports for that view.","title":"Filtering Transactions"},{"location":"running_beancount_and_generating_reports.html#reports","text":"The whole point of entering your transactions in a single input file in the first place is that it allows you to sum, filter, aggregate and arrange various subsets of your data into well-known reports. There are three distinct ways to produce reports from Beancount: by using bean-web and browsing to a view and then to a specific report (this is the easy way), by using bean-report and providing the name of a desired report (and possibly some report-specific arguments), and by using bean-query and requesting data by specifying an SQL statement. Reports can sometimes be rendered in different file formats. Each report type will support being rendered in a list of common ones, such as console text, HTML and CSV. Some reports render in Beancount syntax itself, and we simply call this format name \u201cbeancount.\u201d There are many types of reports available, and there will be many more in the future, as many of the features on the roadmap involve new types of output. This section provides an overview of the most common ones. Use bean-report to inquire about the full list supported by your installed version: bean-report --help-reports PLEASE NOTE! At the moment, the sets of reports that are available from the web interface and from the console are different, though there is some overlap. In a subsequent version, the list of reports will be reconciled and all reports will be made available via both the web interface and the console, in a variety of data formats (text, CSV, HTML and maybe others.) For now, we will document these in the sections below.","title":"Reports"},{"location":"running_beancount_and_generating_reports.html#balance-reports","text":"All the balance reports are similar in that they produce tables of some set of accounts and their associated balances: [output] |-- Assets | `-- US | |-- BofA | | `-- Checking 596.05 USD | |-- ETrade | | |-- Cash 5,120.50 USD | | |-- GLD 70.00 GLD | | |-- ITOT 17.00 ITOT | | |-- VEA 36.00 VEA | | `-- VHT 294.00 VHT | |-- Federal | | `-- PreTax401k | |-- Hoogle | | `-- Vacation 337.26 VACHR | `-- Vanguard ... Balances for commodities held \u201cat cost\u201d are rendered at their book value. (Unrealized gains, if any, are inserted as separate transactions by an optional plugin and the result amounts get mixed in with the cost basis if rendered in the same account.) If an account\u2019s balance contains many different types of currencies (commodities not held \u201cat cost\u201d, such as dollars, euros, yen), each gets printed on its own line. This can render the balance column a bit too busy and messes with the vertical regularity of account names. This is to some extent an unavoidable compromise, but in practice, there are only a small number of commodities that form the large majority of a user\u2019s ledger: the home country\u2019s currency units. To this extent, amounts in common currencies can be broken out into their own column using the \u201c operating_currency \u201d option: option \"operating_currency\" \"USD\" option \"operating_currency\" \"CAD\" You may use this option multiple times if you have many of them (this is my case, for instance, because I am an expat and hold assets in both my host and home countries). Declaring operating currencies also hold the advantage that the name of the currency need not be rendered, and it can thus more easily be imported into a spreadsheet. Finally, some accounts are deemed \u201cactive\u201d if they have not been closed. A closed account with no transactions in the filtered set of transactions will not be rendered.","title":"Balance Reports"},{"location":"running_beancount_and_generating_reports.html#trial-balance-balances","text":"A trial balance report simply produces a table of final balances of all the active accounts, with all the accounts rendered vertically. The sum total of all balances is reported at the bottom. Unlike a balance sheet, it may not always balance to zero because of currency conversions. (This is the equivalent of Ledger\u2019s bal report.) The equivalent bean-query command is: SELECT account, sum(position) GROUP BY account ORDER BY account;","title":"Trial Balance (balances)"},{"location":"running_beancount_and_generating_reports.html#balance-sheet-balsheet","text":"A balance sheet is a snapshot of the balances of the Assets, Liabilities and Equity accounts at a particular point in time. In order to build such a report, we have to move balances from the other accounts to it: compute the balances of the Income and Expenses account at that point in time, insert transactions that will zero out balances from these accounts by transferring them to an equity account ( Equity:Earnings:Current ), render a tree of the balances of the Assets accounts on the left side, render a tree of the Liabilities accounts on the right side, render a tree of the Equity accounts below the Liabilities accounts. See the introduction document for an example. Note that the Equity accounts include the amounts reported from the Income and Expenses accounts, also often called \u201cNet Income.\u201d Also, in practice, we make two transfers because we\u2019re typically looking at a reporting period, and we want to differentiate between Net Income amounts transferred before the beginning of the period ( Equity:Earnings:Previous ) and during the period itself ( Equity:Earnings:Current ). And similar pair of transfers is carried out in order to handle currency conversions (this is a bit of a hairy topic, but one with a great solution; refer to the dedicated document if you want all the details). The equivalent bean-query command is: SELECT account, sum(position) FROM CLOSE ON 2016-01-01 GROUP BY account ORDER BY account;","title":"Balance Sheet (balsheet)"},{"location":"running_beancount_and_generating_reports.html#opening-balances-openbal","text":"The opening balances report is simply a balance sheet drawn at the beginning of the reporting period. This report only makes sense for a list of filtered entries that represents a period of time, such as \u201cyear 2014.\u201d The balance sheet is generated using only the summarization entries that were synthesized when the transactions were filtered (see the double-entry method document ).","title":"Opening Balances (openbal)"},{"location":"running_beancount_and_generating_reports.html#income-statement-income","text":"An income statement lists the final balances of the Income and Expenses accounts. It represents a summary of the transient activity within these accounts. If the balance sheet is the snapshot at a particular point in time, this is the difference between the beginning and the end of a period (in our case: of a filtered set of transactions). The balances of the active Income accounts are rendered on the left, and those of the active Expenses accounts on the right. See the introduction document for an example. The difference between the total of Income and Expenses balances is the Net Income. Note that the initial balances of the Income and Expenses accounts should have been zero\u2019ed out by summarization transactions that occur at the beginning of the period, because we\u2019re only interested in the changes in these accounts.","title":"Income Statement (income)"},{"location":"running_beancount_and_generating_reports.html#journal-reports","text":"The reports in this section render lists of transactions and other directives in a linear fashion.","title":"Journal Reports"},{"location":"running_beancount_and_generating_reports.html#journal-journal","text":"This report is the equivalent of an \u201caccount statement\u201d from an institution, a list of transactions with at least one posting in that account. This is the equivalent of Ledger\u2019s register report ( reg ). You generate a journal report like this: bean-report myfile.beancount journal \u2026 By default, this renders a journal of all the transactions, which is unlikely to be what you want. Select a particular account to render like this: bean-report myfile.beancount journal -a Expenses:Restaurant At the moment, the \u201c -a \u201d option accepts only a complete account name, or the name of one of the parent accounts. Eventually we will extend it to handle expressions.","title":"Journal (journal)"},{"location":"running_beancount_and_generating_reports.html#rendering-at-cost","text":"The numbers column on the right displays the changes from the postings of the selected account. Notice that only the balances for the postings affecting the given account are rendered. The change column renders the changes in the units affected. For example, if this posting is selected: Assets:Investments:Apple 2 AAPL {402.00 USD} The value reported for the change will be \u201c 2 AAPL \u201d. If you would like to render the values at cost, use the \u201c --at-cost \u201d or \u201c -c \u201d option, which will in this case render \u201c 804.00 USD \u201d instead. There is no \u201cmarket value\u201d option. Unrealized gains are automatically inserted at the end of the history by the \u201c beancount.plugins.unrealized \u201d plugin. See options for that plugin to insert its unrealized gains. Note that if the sum of the selected postings is zero, no amount is rendered in the change column.","title":"Rendering at Cost"},{"location":"running_beancount_and_generating_reports.html#adding-a-balance-column","text":"If you want to add a column that sums up the running balance for the reported changes, use the \u201c --render-balance \u201d or \u201c -b \u201d option. This does not always make sense to report, so it is up to you to decide whether you want a running balance.","title":"Adding a Balance Column"},{"location":"running_beancount_and_generating_reports.html#character-width","text":"By default, the report will be as wide as your terminal allows. Restrict the width to a set number of characters with the \u201c-w\u201d option.","title":"Character Width"},{"location":"running_beancount_and_generating_reports.html#precision","text":"The number of fractional digits for the number rendering can be specified via \u201c --precision \u201d or \u201c -k \u201d.","title":"Precision"},{"location":"running_beancount_and_generating_reports.html#compact-normal-or-verbose","text":"In its normal operation, Beancount renders an empty line between transactions. This helps delineate transactions where there are multiple currencies affected, as they render on separate lines. If you want a more compact rendering, use the \u201c --compact \u201d or \u201c -x \u201d option. On the other hand, if you want to render the affected postings under the transaction line, use the \u201c --verbose \u201d or \u201c -X \u201d option.","title":"Compact, Normal or Verbose"},{"location":"running_beancount_and_generating_reports.html#summary","text":"Here is a summary of its arguments: optional arguments: -h, --help show this help message and exit -a ACCOUNT, --account ACCOUNT Account to render -w WIDTH, --width WIDTH The number of characters wide to render the report to -k PRECISION, --precision PRECISION The number of digits to render after the period -b, --render-balance, --balance If true, render a running balance -c, --at-cost, --cost If true, render values at cost -x, --compact Rendering compactly -X, --verbose Rendering verbosely","title":"Summary"},{"location":"running_beancount_and_generating_reports.html#equivalent-sql-query","text":"The equivalent bean-query command is: SELECT date, flag, description, account, cost(position), cost(balance);","title":"Equivalent SQL Query"},{"location":"running_beancount_and_generating_reports.html#conversions-conversions","text":"This report lists the total of currency conversions that result from the selected transactions. (Most people won\u2019t need this.)","title":"Conversions (conversions)"},{"location":"running_beancount_and_generating_reports.html#documents-documents","text":"This report produces an HTML list of all the external documents found in the ledger, either from explicit directives or from plugins that automatically find the documents and add them to the stream of transactions.","title":"Documents (documents)"},{"location":"running_beancount_and_generating_reports.html#holdings-reports","text":"These reports produces aggregations for assets held at cost.","title":"Holdings Reports"},{"location":"running_beancount_and_generating_reports.html#holdings-aggregations-holdings","text":"This report produces a detailed list of all holdings found in the ledger. You can produce aggregates by commodity and accounts using the \u201c-g\u201d option.","title":"Holdings & Aggregations (holdings*)"},{"location":"running_beancount_and_generating_reports.html#net-worth-networth","text":"This report produces a short summary of the net worth (equity) of the ledger, in each of the operating currencies.","title":"Net Worth (networth)"},{"location":"running_beancount_and_generating_reports.html#other-report-types","text":"","title":"Other Report Types"},{"location":"running_beancount_and_generating_reports.html#cash","text":"This report renders balances in commodities not held at cost, in other words, cash: bean-report example.beancount cash -c USD Account Units Currency Cost Currency Average Cost Price Book Value Market Value -------------------------- --------- -------- ------------- ------------ ----- ---------- ------------ Assets:US:BofA:Checking 596.05 USD USD 596.05 596.05 Assets:US:ETrade:Cash 5,120.50 USD USD 5,120.50 5,120.50 Assets:US:Hoogle:Vacation 337.26 VACHR Assets:US:Vanguard:Cash -0.02 USD USD -0.02 -0.02 Liabilities:US:Chase:Slate -2,891.85 USD USD -2,891.85 -2,891.85 -------------------------- --------- -------- ------------- ------------ ----- ---------- ------------ The report allows you to convert all currencies to a common currency (in the example above, \"convert everything to USD\"). There's also an option to report only on the operating currencies. I use this to get an overview of all uninvested cash.","title":"Cash"},{"location":"running_beancount_and_generating_reports.html#prices-prices","text":"This report renders a list of price points for a base currency in terms of a quote currency. The list is sorted by date. You can output this table in beancount format as well. This is convenient to save a price database to a file, that can then be combined and loaded into another input file.","title":"Prices (prices)"},{"location":"running_beancount_and_generating_reports.html#statistics-stats","text":"This report simply provides various statistics on the parsed entries.","title":"Statistics (stats)"},{"location":"running_beancount_and_generating_reports.html#update-activity-activity","text":"This table renders for each account the date of the last entry.","title":"Update Activity (activity)"},{"location":"settlement_dates_in_beancount.html","text":"Settlement Dates & Transfer Accounts in Beancount \uf0c1 Martin Blais, July 2014 http://furius.ca/beancount/doc/proposal-dates Motivation Proposal Description Remaining Questions Previous Work Ledger Effective and Auxiliary Dates References Motivation \uf0c1 When a trade executes in an investment account, there is most often a delay between the date that the transaction is carried out (the \u201ctransaction date\u201d) and the date that the funds are deposited in an associated cash account (the \u201csettlement date\u201d). This makes imported balance assertions sometimes requiring the fudging of their dates, and sometimes they can even be impossible. This document proposes the addition of an optional \u201csettlement date\u201d to be attached to a transaction or a posting, and associated semantics for how to deal with the problem. Proposal Description \uf0c1 Settlement Dates \uf0c1 In the first implementation of Beancount I used to have two dates attached to a transaction, but I never did anything with them. The alternate date would get attached but was ignored thereafter. The meaning of it is that it should have split the transaction into two, with some sort of transfer account, that might have been useful semantics, I never developed it. Something like this as input: 2014-06-23=2014-06-28 * \"Sale\" Assets:ScottTrade:GOOG -10 GOOG {589.00 USD} S Assets:ScottTrade:Cash where the \u201cS\u201d posting flag marks the leg \u201cto be postponed to the settlement date.\u201d Alternatively, you could attach the date to a posting: 2014-06-23 * \"Sale\" Assets:ScottTrade:GOOG -10 GOOG {589.00 USD} 2014-06-28 Assets:ScottTrade:Cash Both of the above syntax proposals allow you to specify which postings are meant to be postponed to settlement. The second one is more flexible, as each posting could potentially have a different date, but the more constrained syntax of the first would create less complications. Either of these could get translated to multiple transactions with a transfer account to absorb the pending amount: 2014-06-23 * \"Sale\" ^settlement-3463873948 Assets:ScottTrade:GOOG -10 GOOG {589.00 USD} Assets:ScottTrade:Transfer 2014-06-28 * \"Sale\" ^settlement-3463873948 Assets:ScottTrade:Transfer Assets:ScottTrade:Cash 5890.00 USD So far, I\u2019ve been getting away with fudging the dates on balance assertions where necessary. I have relatively few sales, so this hasn\u2019t been a big problem so far. I\u2019m not convinced it needs a solution yet, maybe the best thing to do is just to document how to deal with the issue when it occurs. Maybe someone can convince me otherwise. Transfer Accounts \uf0c1 In the previous section, we discuss a style whereby a single entry moving money between two accounts contains two dates and results in two separate entries. An auxiliary problem, which is related in its solution, is how to carry out the reverse operation, that is, how to merge two separate entries posting to a common transfer account (sometimes called a \u201c suspense account \u201d). For example, a user may want to input the two sides of a transaction separately, e.g. by running import scripts on separate input files, and instead of having to reconcile and merge those by hand, we would want to explicitly support this by identifying matching transactions to these transfer accounts and creating a common link between them. Most importantly, we want to be able to easily identify which of the transactions is not matched on the other side, which indicates missing data. There is a prototype of this under beancount.plugins.tag_pending . Also see redstreet0\u2019s \u201c zerosum \u201d plugin from this thread . Remaining Questions \uf0c1 How do we determine a proper transfer account name? Is a subaccount a reasonable approach? What if a user would like to have a single global limbo account? TODO Does this property solve the problem of making balance assertions between trade and settlement? TODO [Write out a detailed example] Any drawbacks? TODO How does this affect the balance sheet and income statement, if any? Is it going to be obvious to users what the amounts in these limbo/transfer accounts are? TODO Unrooting Transactions \uf0c1 A wilder idea would be to add an extra level in the transaction-posting hierarchy, adding the capability to group multiple partial transactions, and move the balancing rule to that level. Basically, two transactions input separately and then grouped - by some rule, or trivially by themselves - could form a new unit of balance rule. That would be a much more demanding change on the schema and on the Beancount design but would allow to natively support partial transactions, keeping their individual dates, descriptions, etc. Maybe that's a better model? Consider the advantages. Previous Work \uf0c1 Ledger Effective and Auxiliary Dates \uf0c1 Ledger has the concept of \u201c auxiliary dates \u201d. The way these work is straightforward: any transaction may have a second date, and the user can select at runtime (with --aux-date ) whether the main date or the auxiliary dates are meant to be used. It is unclear to me how this is meant to be used in practice, in the presence of balance assertions. Without balance assertions, I can see how it would just work: you\u2019d render everything with settlement dates only. This would probably only make sense for specific reports. I would much rather keep a single semantic for the set of transactions that gets parsed in; the idea that the meaning of the transactions varies depending on the invocation conditions would set a precedent in Beancount, I\u2019d prefer not to break this nice property, so by default I\u2019d prefer to avoid implementing this solution. Auxiliary dates are also known as \u201c effective dates \u201d and can be associated with each individual posting. Auxiliary dates are secondary to the the \u201cprimary date\u201d or the \u201cactual date\u201d, being the posting date of the record): 2008/10/16 * (2090) Bountiful Blessings Farm Expenses:Food:Groceries $ 37.50 ; [=2008/10/01] Expenses:Food:Groceries $ 37.50 ; [=2008/11/01] Expenses:Food:Groceries $ 37.50 ; [=2008/12/01] Expenses:Food:Groceries $ 37.50 ; [=2009/01/01] Expenses:Food:Groceries $ 37.50 ; [=2009/02/01] Expenses:Food:Groceries $ 37.50 ; [=2009/03/01] Assets:Checking The original motivation for this was for budgeting, allow one to move accounting of expenses to neighboring budget periods in order to carry over actual paid amounts to those periods. Bank amounts in one month could be set against a budget from the immediately preceding or following month, as needed. (Note: John Wiegley) This is similar to one of the syntaxes I\u2019m suggesting above\u2014letting the user specify a date for each posting\u2014but the other postings are not split as independent transactions. The usage of those dates is similarly triggered by a command-line option ( --effective ). I\u2019m assuming that the posting on the checking account above occurs at once at 2008/10/16, regardless of reporting date. Let\u2019s verify this: $ ledger -f settlement1.lgr reg checking --effective 08-Oct-16 Bountiful Blessings.. Assets:Checking $ -225.00 $ -225.00 That\u2019s what I thought. This works, but a problem with this approach is that any balance sheet drawn between 2008/10/01 (the earliest effective date) and 2009/03/01 (the latest effective date) would not balance. Between those dates, some amounts are \u201cin limbo\u201d and drawing up a balance sheet at one of those dates would not balance. This would break an invariant in Beancount: we require that you should always be able to draw a balance sheet at any point in time, and any subset of transactions should balance. I would rather implement this by splitting this example transaction into many other ones, as in the proposal above, moving those temporary amounts living in limbo in an explicit \u201climbo\u201d or \u201ctransfer\u201d account, where each transaction balances. Moreover, this step can be implemented as a transformation stage, replacing the transaction with effective dates by one transaction for each posting where the effective date differs from the transaction\u2019s date (this could be enabled on demand via a plugin). GnuCash \uf0c1 TODO(blais) - How is this handled in GnuCash and other GUI systems? Is there a standard account method? References \uf0c1 The IRS requires you to use the trade date and NOT the settlement date for tax reporting; from the IRS Publication 17: Securities traded on established market. For securities traded on an established securities market, your holding period begins the day after the trade date you bought the securities, and ends on the trade date you sold them. Do not confuse the trade date with the settlement date, which is the date by which the stock must be delivered and payment must be made. Example. You are a cash method, calendar year taxpayer. You sold stock at a gain on December 30, 2013. According to the rules of the stock exchange, the sale was closed by delivery of the stock 4 trading days after the sale, on January 6, 2014. You received payment of the sales price on that same day. Report your gain on your 2013 return, even though you received the payment in 2014. The gain is long term or short term depending on whether you held the stock more than 1 year. Your holding period ended on December 30. If you had sold the stock at a loss, you would also report it on your 2013 return. Threads \uf0c1 An interesting \"feature by coincidence\" First Opinions, Coming from Ledger","title":"Settlement Dates in Beancount"},{"location":"settlement_dates_in_beancount.html#settlement-dates-transfer-accounts-in-beancount","text":"Martin Blais, July 2014 http://furius.ca/beancount/doc/proposal-dates Motivation Proposal Description Remaining Questions Previous Work Ledger Effective and Auxiliary Dates References","title":"Settlement Dates & Transfer Accounts in Beancount"},{"location":"settlement_dates_in_beancount.html#motivation","text":"When a trade executes in an investment account, there is most often a delay between the date that the transaction is carried out (the \u201ctransaction date\u201d) and the date that the funds are deposited in an associated cash account (the \u201csettlement date\u201d). This makes imported balance assertions sometimes requiring the fudging of their dates, and sometimes they can even be impossible. This document proposes the addition of an optional \u201csettlement date\u201d to be attached to a transaction or a posting, and associated semantics for how to deal with the problem.","title":"Motivation"},{"location":"settlement_dates_in_beancount.html#proposal-description","text":"","title":"Proposal Description"},{"location":"settlement_dates_in_beancount.html#settlement-dates","text":"In the first implementation of Beancount I used to have two dates attached to a transaction, but I never did anything with them. The alternate date would get attached but was ignored thereafter. The meaning of it is that it should have split the transaction into two, with some sort of transfer account, that might have been useful semantics, I never developed it. Something like this as input: 2014-06-23=2014-06-28 * \"Sale\" Assets:ScottTrade:GOOG -10 GOOG {589.00 USD} S Assets:ScottTrade:Cash where the \u201cS\u201d posting flag marks the leg \u201cto be postponed to the settlement date.\u201d Alternatively, you could attach the date to a posting: 2014-06-23 * \"Sale\" Assets:ScottTrade:GOOG -10 GOOG {589.00 USD} 2014-06-28 Assets:ScottTrade:Cash Both of the above syntax proposals allow you to specify which postings are meant to be postponed to settlement. The second one is more flexible, as each posting could potentially have a different date, but the more constrained syntax of the first would create less complications. Either of these could get translated to multiple transactions with a transfer account to absorb the pending amount: 2014-06-23 * \"Sale\" ^settlement-3463873948 Assets:ScottTrade:GOOG -10 GOOG {589.00 USD} Assets:ScottTrade:Transfer 2014-06-28 * \"Sale\" ^settlement-3463873948 Assets:ScottTrade:Transfer Assets:ScottTrade:Cash 5890.00 USD So far, I\u2019ve been getting away with fudging the dates on balance assertions where necessary. I have relatively few sales, so this hasn\u2019t been a big problem so far. I\u2019m not convinced it needs a solution yet, maybe the best thing to do is just to document how to deal with the issue when it occurs. Maybe someone can convince me otherwise.","title":"Settlement Dates"},{"location":"settlement_dates_in_beancount.html#transfer-accounts","text":"In the previous section, we discuss a style whereby a single entry moving money between two accounts contains two dates and results in two separate entries. An auxiliary problem, which is related in its solution, is how to carry out the reverse operation, that is, how to merge two separate entries posting to a common transfer account (sometimes called a \u201c suspense account \u201d). For example, a user may want to input the two sides of a transaction separately, e.g. by running import scripts on separate input files, and instead of having to reconcile and merge those by hand, we would want to explicitly support this by identifying matching transactions to these transfer accounts and creating a common link between them. Most importantly, we want to be able to easily identify which of the transactions is not matched on the other side, which indicates missing data. There is a prototype of this under beancount.plugins.tag_pending . Also see redstreet0\u2019s \u201c zerosum \u201d plugin from this thread .","title":"Transfer Accounts"},{"location":"settlement_dates_in_beancount.html#remaining-questions","text":"How do we determine a proper transfer account name? Is a subaccount a reasonable approach? What if a user would like to have a single global limbo account? TODO Does this property solve the problem of making balance assertions between trade and settlement? TODO [Write out a detailed example] Any drawbacks? TODO How does this affect the balance sheet and income statement, if any? Is it going to be obvious to users what the amounts in these limbo/transfer accounts are? TODO","title":"Remaining Questions"},{"location":"settlement_dates_in_beancount.html#unrooting-transactions","text":"A wilder idea would be to add an extra level in the transaction-posting hierarchy, adding the capability to group multiple partial transactions, and move the balancing rule to that level. Basically, two transactions input separately and then grouped - by some rule, or trivially by themselves - could form a new unit of balance rule. That would be a much more demanding change on the schema and on the Beancount design but would allow to natively support partial transactions, keeping their individual dates, descriptions, etc. Maybe that's a better model? Consider the advantages.","title":"Unrooting Transactions"},{"location":"settlement_dates_in_beancount.html#previous-work","text":"","title":"Previous Work"},{"location":"settlement_dates_in_beancount.html#ledger-effective-and-auxiliary-dates","text":"Ledger has the concept of \u201c auxiliary dates \u201d. The way these work is straightforward: any transaction may have a second date, and the user can select at runtime (with --aux-date ) whether the main date or the auxiliary dates are meant to be used. It is unclear to me how this is meant to be used in practice, in the presence of balance assertions. Without balance assertions, I can see how it would just work: you\u2019d render everything with settlement dates only. This would probably only make sense for specific reports. I would much rather keep a single semantic for the set of transactions that gets parsed in; the idea that the meaning of the transactions varies depending on the invocation conditions would set a precedent in Beancount, I\u2019d prefer not to break this nice property, so by default I\u2019d prefer to avoid implementing this solution. Auxiliary dates are also known as \u201c effective dates \u201d and can be associated with each individual posting. Auxiliary dates are secondary to the the \u201cprimary date\u201d or the \u201cactual date\u201d, being the posting date of the record): 2008/10/16 * (2090) Bountiful Blessings Farm Expenses:Food:Groceries $ 37.50 ; [=2008/10/01] Expenses:Food:Groceries $ 37.50 ; [=2008/11/01] Expenses:Food:Groceries $ 37.50 ; [=2008/12/01] Expenses:Food:Groceries $ 37.50 ; [=2009/01/01] Expenses:Food:Groceries $ 37.50 ; [=2009/02/01] Expenses:Food:Groceries $ 37.50 ; [=2009/03/01] Assets:Checking The original motivation for this was for budgeting, allow one to move accounting of expenses to neighboring budget periods in order to carry over actual paid amounts to those periods. Bank amounts in one month could be set against a budget from the immediately preceding or following month, as needed. (Note: John Wiegley) This is similar to one of the syntaxes I\u2019m suggesting above\u2014letting the user specify a date for each posting\u2014but the other postings are not split as independent transactions. The usage of those dates is similarly triggered by a command-line option ( --effective ). I\u2019m assuming that the posting on the checking account above occurs at once at 2008/10/16, regardless of reporting date. Let\u2019s verify this: $ ledger -f settlement1.lgr reg checking --effective 08-Oct-16 Bountiful Blessings.. Assets:Checking $ -225.00 $ -225.00 That\u2019s what I thought. This works, but a problem with this approach is that any balance sheet drawn between 2008/10/01 (the earliest effective date) and 2009/03/01 (the latest effective date) would not balance. Between those dates, some amounts are \u201cin limbo\u201d and drawing up a balance sheet at one of those dates would not balance. This would break an invariant in Beancount: we require that you should always be able to draw a balance sheet at any point in time, and any subset of transactions should balance. I would rather implement this by splitting this example transaction into many other ones, as in the proposal above, moving those temporary amounts living in limbo in an explicit \u201climbo\u201d or \u201ctransfer\u201d account, where each transaction balances. Moreover, this step can be implemented as a transformation stage, replacing the transaction with effective dates by one transaction for each posting where the effective date differs from the transaction\u2019s date (this could be enabled on demand via a plugin).","title":"Ledger Effective and Auxiliary Dates"},{"location":"settlement_dates_in_beancount.html#gnucash","text":"TODO(blais) - How is this handled in GnuCash and other GUI systems? Is there a standard account method?","title":"GnuCash"},{"location":"settlement_dates_in_beancount.html#references","text":"The IRS requires you to use the trade date and NOT the settlement date for tax reporting; from the IRS Publication 17: Securities traded on established market. For securities traded on an established securities market, your holding period begins the day after the trade date you bought the securities, and ends on the trade date you sold them. Do not confuse the trade date with the settlement date, which is the date by which the stock must be delivered and payment must be made. Example. You are a cash method, calendar year taxpayer. You sold stock at a gain on December 30, 2013. According to the rules of the stock exchange, the sale was closed by delivery of the stock 4 trading days after the sale, on January 6, 2014. You received payment of the sales price on that same day. Report your gain on your 2013 return, even though you received the payment in 2014. The gain is long term or short term depending on whether you held the stock more than 1 year. Your holding period ended on December 30. If you had sold the stock at a loss, you would also report it on your 2013 return.","title":"References"},{"location":"settlement_dates_in_beancount.html#threads","text":"An interesting \"feature by coincidence\" First Opinions, Coming from Ledger","title":"Threads"},{"location":"sharing_expenses_with_beancount.html","text":"Sharing Expenses in Beancount \uf0c1 Martin Blais , May 2015 http://furius.ca/beancount/doc/shared Introduction \uf0c1 This document presents a method for precisely and easily accounting for shared expenses in complicated group situations. For example, traveling with a group of friends where people pay for different things. We show it is possible to deal with expenses to be split evenly among the group as well as group expenses that should be allocated to specific persons. The method uses the double-entry accounting system. The essence of the method consists in separating the accounting of expenses and their associated payments. This makes it much easier to deal with shared costs, because using this method it doesn\u2019t matter who pays for what: we reconcile the precise amounts owed for each individual at the end and automatically compute final correcting transfers for each. This article is structured around a simple trip with expenses shared equally by two people. However, the same method generalizes to any type of project with incomes and expenses to be shared among multiple participants, and splitting expenses evenly or not. A Travel Example \uf0c1 Martin (the author) and Caroline (his girlfriend) visited Mexico in March 2015. We visited the island of Cozumel for a SCUBA diving trip for three days and then headed to Tulum for two days to relax and dive in the cenotes . Our assumptions are: Chaotic payments . We lead very busy lives\u2026 both of us are going to make payments ahead of time and during the trip, without any forethought regarding who will pay for what, though we will carefully record every payment. Each of us just pays for whatever preparation costs as needed to arrange this trip. For example, Caroline selected and booked flights early while I paid for the resort and booked the rental and activities at the local dive shop one week before departure. Use of shared and individual assets. Both of us are bringing cash, and are going to make payments during the trip from our respective wallets as well as from a shared pool of cash converted to local currency (Mexican pesos, for which I will use the \u201cMXN\u201d symbol), and we will use credit cards as necessary before and during the trip. Multiple currencies. Some of our expenses will be denominated in US dollars and some in Mexican pesos. For example, flights were paid in US dollars, local meals and the accommodation were paid in pesos, but the local dive shop charged us dollars. Converted amounts will come from both cash and credit cards sources. Individual expenses in a shared pool. While most of the expenses are to be shared equally, some of the expenses will apply to only one of us, and we want to account for those explicitly. For example, Caroline took a SCUBA certification course ( PADI Open Water ) and will pay for that on her own; similarly, she should not be paying for Martin\u2019s expensive boat diving costs. To complicate matters, the dive shop issued us a single joint bill for everything at the end of our stay. A Note About Sharing \uf0c1 I feel that something should be said about the \u201csharing\u201d aspect of our expenses, as this topic has come up on previous discussions on the mailing-lists involving sharing examples. We are nitpicking on purpose. For the purpose of this exercise, we are accounting for every little penny spent in an incredibly detailed manner. The point of this document is specifically to show how a complex set of transactions well accounted for can be efficiently and simply disentangled to a precise accounting of expenses for each participant, regardless of who actually makes the payments. We are not cheapskates. We will assume that we\u2019ve decided to split expenses evenly. Our \u201cgenerosity\u201d to each other is not a topic relevant to this document. We\u2019re both well compensated working professionals and you can assume that we\u2019ve agreed to split the common costs for this trip evenly (50/50). One of the attributes of the method we show here is that the decision of how we choose to split expenses can be made separately from the actual payment of costs. For example, we may decide in the end that I pay for \u2154rd of the trip, but that can be decided precisely rather than in an ad-hoc \u201coh, I think I remember I paid for this\u201d manner. This is especially useful in a larger group of people because when expenses aren\u2019t tracked accurately, usually everyone is left with a feeling that they paid more than the others. The sum of the perceived fractions of expenses paid in a group is always greater than 100%... Overview of the Method \uf0c1 In this section we present a brief illustrated overview of the method. A set of common Assets accounts that belong to the project, and book all our individual expenses and transfer for the trip as coming from external Income accounts: During the trip, we use the common Assets to make expenses. Most of the expenses are attributed to both of us (and to be shared eventually), but some of the expenses are intended to be attributed to each of us individually: After the trip, remaining Assets (like cash we walked home with) gets distributed back to ourselves to zero out the balances of the Assets accounts and we record this through contra postings to Income accounts: Finally, the list of shared Expenses are split between each other\u2014using a plugin that forks every posting that is intended to be a shared expense\u2014and the final amount is used to make a final transfer between each other so that we\u2019ve each paid for our respective expenses and we\u2019re square: Note that the final balance of expenses for each participant may differ, and these are due to particular expenses that were attributed separately, or if we decide to split the total unevenly. How to Track Expenses \uf0c1 In order to write down all expenses for this trip we will open a brand new Beancount input file. Despite the fact that the expenses will come from each person\u2019s personal accounts, it is useful to think of the trip as a special project, as if it were a separate entity, e.g., a company that exists only for the duration of the trip. The example file we wrote for our trip can be found here and should help you follow along this text. Accounts \uf0c1 The set of accounts in the input file should not have to match your personal Beancount file\u2019s account names. The accounts we will use include accounts that correspond to Martin and Caroline\u2019s personal accounts but with generic names (e.g., Income:Martin:CreditCard instead of Liabilities:US:Chase ), and expense accounts may not match any regular expenses in my personal Beancount file\u2014it\u2019s not important. As a convention, any account that pertains specifically to one of the travelers will include that person\u2019s name in the account name. For example, Caroline\u2019s credit card will be named \u201c Income:Caroline:CreditCard \u201d. This is important, as we will use this later on to split contributions and expenses. Let\u2019s examine the different types of accounts we will need to carry this out. External Income Accounts \uf0c1 The \u201cproject\u201d will receive income in the form of transfers from personal accounts of either traveler. These are accounts we will consider external to the project and so will define them as Income accounts: ;; External accounts for Martin. 2015-02-01 open Income:Martin:Cash 2015-02-01 open Income:Martin:Cash:Foreign 2015-02-01 open Income:Martin:Wallet 2015-02-01 open Income:Martin:CreditCard ;; External accounts for Caroline. 2015-02-01 open Income:Caroline:Cash 2015-02-01 open Income:Caroline:Wallet 2015-02-01 open Income:Caroline:MetroCard 2015-02-01 open Income:Caroline:CreditCard Transactions carried out from these accounts must be copied from your personal Beancount file. Obviously, you must be careful to include all the transactions pertaining to the trip. I used a tag to do this in my personal file. Assets & Liabilities Accounts \uf0c1 There will be a few Asset accounts that will be active and exist for the duration of the trip. These temporary accounts will be zero\u2019ed out at the end of it. One example is a pool of petty cash in local currency: 2015-02-01 open Assets:Cash:Pesos description: \"A shared account to contain our pocket of pesos\" We also carried cash in each of our pockets while traveling, so I created two separate accounts for that: 2015-02-01 open Assets:Cash:Martin description: \"Cash for the trip held by Martin\" 2015-02-01 open Assets:Cash:Caroline description: \"Cash for the trip held by Caroline\" Note however that despite their individual names, those accounts are considered as part of the project. It was just convenient to separately track balances for the cash we each held during the trip. Expenses Accounts \uf0c1 We will define various accounts to book our Expenses to. For example, \u201c Expenses:Flights \u201d will contain our costs associated with flight travel. For convenience, and because there are many types of expenses in this file, we chose to leverage the \u201cauto-accounts\u201d plugin and let Beancount automatically open these accounts: plugin \"beancount.ops.auto_accounts\" The great majority of these accounts are for shared expenses to be split between us. For example, shared SCUBA diving expenses will be booked to \u201c Expenses:Scuba \u201d. However, for expenses that are intended to be covered by one of us only, we simply include the name of the traveler in the account name. For example, Martin\u2019s extra costs for boat diving will be booked to the \u201c Expenses:Scuba:Martin \u201d account. Example Transactions \uf0c1 Let\u2019s turn our attention to the different types of transactions present in the file. In this section I will walk you through some representative transactions (the names I give to these are arbitrary). Contributing Transactions \uf0c1 Contributions to the project\u2019s costs are usually done in the form of expenses paid from external accounts. For example, Caroline paying for flights with her credit card looks like this: 2015-02-01 * \"Flights to Cancun\" Income:Caroline:CreditCard -976.00 USD Expenses:Flights Martin paying for the taxi to the airport looks like this: 2015-02-25 * \"Taxi to airport\" ^433f66ea0e4e Expenses:Transport:Taxi 62.80 USD Income:Martin:CreditCard Bringing Cash \uf0c1 We both brought some cash on us, as we were warned it might be difficult to use credit cards in Mexico. In my personal Beancount file, the cash account is \u201c Assets:Cash \u201d but here it must get booked as an external contribution with my name on it: ;; Initial cash on us. 2015-02-24 * \"Getting cash for travel\" Income:Martin:Cash -1200 USD Assets:Cash:Martin 1200 USD Caroline\u2019s cash is similar: 2015-02-24 * \"Getting cash for travel\" Income:Caroline:Cash -300 USD Assets:Cash:Caroline 300 USD Once again, note that the Assets:Cash:Martin and Assets:Cash:Caroline accounts are considered a part of the project, in this case it just refers to who is carrying it. (These accounts are cleared at the end, so it does not matter that our names are in them.) Transfers \uf0c1 Before the trip, I was really busy. It looked like Caroline was going to make most of arrangements and upfront payments, so I made a transfer to her Google Wallet to help her cover for some expenses ahead of time: 2015-02-01 * \"Transfer Martin -> Caroline on Google Wallet\" Income:Martin:Wallet -1000 USD Income:Caroline:Wallet 1000 USD Cash Conversions \uf0c1 Obtaining local currency was done by changing a small amount of cash at the airport (at a very bad rate): 2015-02-25 * \"Exchanged cash at XIC at CUN airport\" Assets:Cash:Caroline -100.00 USD @ 12.00 MXN Assets:Cash:Pesos 1200 MXN The \u201c Assets:Cash:Pesos \u201d account tracks our common pool of local currency that we use for various small expenses. Cash Expenses in US Dollars \uf0c1 Some local expenses will call for US money, which in this example I paid from my cash pocket: 2015-03-01 * \"Motmot Diving\" | \"Deposit for cenote diving\" Expenses:Scuba 50.00 USD Assets:Cash:Martin Cash Expenses in Local Currency \uf0c1 Paying cash using pesos from our shared stash of pesos looks like this: 2015-02-25 * \"UltraMar Ferry across to Cozumel\" Expenses:Transport:Bus 326 MXN Assets:Cash:Pesos Sometimes we even had to pay with a mix of US dollars and pesos. In this example, we ran out of pesos, so we have to give them dollars and pesos (all the restaurants and hotels in the beach part of Tulum accept US currency): 2015-03-01 * \"Hartwood\" | \"Dinner - ran out of pesos\" Expenses:Restaurant 1880 MXN Assets:Cash:Pesos -1400 MXN Assets:Cash:Martin -40.00 USD @ 12.00 MXN I used the unfavorable rate the restaurant was offering to accept dollars at (the market rate was 14.5 at the time). Individual Expenses \uf0c1 Here is an example of booking individual expenses using shared money. In order for us to have access to the reef for diving, we had to pay a \u201cmarine park\u201d fee of $2.50 per day to the island. This was a short trip where I dove only three days there and Caroline\u2019s fee was included in her course except for one day: 2015-02-25 * \"Marine Park (3 days Martin, 1 day Caroline)\" Expenses:Scuba:ParkFees:Martin 7.50 USD Expenses:Scuba:ParkFees:Caroline 2.50 USD Assets:Cash:Martin All that needs to be done is to book these to expense accounts with our names in them, which will get separated out at the end. Here is a more complicated example: the dive shop at Scuba Club Cozumel charged us a single bill at the end of our stay for all our rental gear and extra dives. All I did here was translate the itemized bill into a single transaction, booking to each other: 2015-03-01 * \"Scuba Club Cozumel\" | \"Dive shop bill\" ^69b409189b37 Income:Martin:CreditCard -381.64 USD Expenses:Scuba:Martin 27 USD ;; Regulator w/ Gauge Expenses:Scuba:Caroline 9 USD ;; Regulator w/ Gauge Expenses:Scuba:Martin 27 USD ;; BCD Expenses:Scuba:Caroline 9 USD ;; BCD Expenses:Scuba:Martin 6 USD ;; Fins Expenses:Scuba:Martin 24 USD ;; Wetsuit Expenses:Scuba:Caroline 8 USD ;; Wetsuit Expenses:Scuba:Caroline 9 USD ;; Dive computer Expenses:Scuba:Martin 5 USD ;; U/W Light Expenses:Scuba:Caroline 70 USD ;; Dive trip (2 tank) Expenses:Scuba:Martin 45 USD ;; Wreck Dive w/ Lite Expenses:Scuba:Martin 45 USD ;; Afternoon dive Expenses:Scuba:Caroline 45 USD ;; Afternoon dive Expenses:Scuba:Martin 28.64 USD ;; Taxes Expenses:Scuba:Caroline 24.00 USD ;; Taxes Final Balances \uf0c1 Of course you can use balance assertions at any time during the trip. For example, just before leaving at the Cancun airport we knew we wouldn\u2019t spend any more Mexican pesos for a while, so I counted what we had left after Caroline decided to splurge the remainder of them into overpriced chocolate sold at the airport shop: 2015-03-04 balance Assets:Cash:Pesos 65 MXN Ideally the bookkeeper should want to do this at a quiet moment at the end of every day or couple of days, which makes it easier to triangulate an expense you might have forgotten to enter (we are on vacation after all, in our relaxed state of mind we forget to write down stuff here and there). Clearing Asset Accounts \uf0c1 At the end of the trip, you should clear the final balances of all Assets and Liabilities accounts by transferring the remaining funds out to the participants, i.e., to the Income accounts. This should leave all the balances of the trip at zero and ensures all the cash put forward for trip expenses has been moved out to travelers. By the way, it doesn\u2019t matter much who keeps this money, because at the end we will end up making a single correcting transfer that should account for the balance required to create an even split. You can transfer it to anyone; the end result will be the same. Our clearing transaction looked like this: 2015-03-06 * \"Final transfer to clear internal balances to external ones\" Assets:Cash:Pesos -65 MXN Income:Martin:Cash:Foreign 60 MXN Income:Caroline:Cash 5 MXN Assets:Cash:Martin -330 USD Income:Martin:Cash 330 USD Assets:Cash:Caroline -140 USD Income:Caroline:Cash 140 USD 2015-03-07 balance Assets:Cash:Pesos 0 MXN 2015-03-07 balance Assets:Cash:Pesos 0 USD 2015-03-07 balance Assets:Cash:Martin 0 USD 2015-03-07 balance Assets:Cash:Caroline 0 USD We had three 20 peso bills, and I kept the bills for future travels. Caroline kept the 5 peso coin (forgot to hand it over as a tip). We transferred out the respective cash amounts we had been carrying together during the trip. How to Take Notes \uf0c1 During this trip I did not carry a laptop\u2014this was vacation after all. I like to disconnect. I did not carry a notebook either. Instead, I just took notes at the end of the day on a few sheets of paper at the hotel. This process took about 5-10 minutes each night, just recalling from memory and writing things down. These notes look like this: I made a paper spreadsheet where each line had The narration for the transaction (a description that would allow me to select an Expenses account later on) Who paid (which Assets or Income account the money came from) The amount (either in USD or MXN) After our trip, I sat down at the computer and typed the corresponding Beancount file . If I had a computer during my vacation I probably would have typed it as we went along. Of course, I had to do a few adjustments here and there because of mistakes. The bottom line is: if you\u2019re organized well, the overhead of doing this is minimal. Reconciling Expenses \uf0c1 Running bean-web on the trip\u2019s file: bean-web beancount/examples/sharing/cozumel2015.beancount You can view the balances in the \u201cAll Transactions\u201d view (click on \u201cAll Transactions\u201d). The Balance Sheet should show empty balances for Assets accounts: The balances of the equity accounts should reflect the total amount of currency conversions made during the trip. You can verify this by calculating the amount-weight average rate like this: 7539.00 / 559.88 ~= 13.465 USD/MXN (which is about right). Reviewing Contributions \uf0c1 The Income Statement should show a summary of all expenses and contributions to the project: The Income account balances show the total amounts of contributions for each person. Note that in creating the Income accounts, I went through the extra step of creating some specific accounts for each source of payment, like \u201cCaroline\u2019s Credit Card\u201d, etc. From this view, we can see that we contributed a total of 4254.28 USD (and were left with 65 MXN in hand) for this trip. The expenses side should match, considering the currency exchanges: 3694.40 + 7474 / 13.465 ~= 4249 USD which is approximately right (the small difference can be explained by the varying currency conversions). If you want to view the list of contribution payments and the final balance, click on a particular traveler\u2019s root account, e.g., \u201cIncome:Caroline\u201d (click on \u201cCaroline\u201d) which should take you to the Journal for that root account: This journal includes all the transactions in its sub-accounts. The final value at the bottom should show the total balance of those accounts, and thus, the amount of money Caroline contributed to this trip: 415 USD, and kept 5 MXN (in coin). We can do the same for Martin and find the final balance of 3839.28 USD and kept 60 MXN (in bills). You can also get the same amounts by using bean-query to achieve the same thing: ~/p/.../examples/sharing$ bean-query cozumel2015.beancount Input file: \"Mexico Trip: Cozumel & Tulum SCUBA Diving\" Ready with 105 directives (160 postings in 45 transactions). beancount> SELECT sum(position) WHERE account ~ '^Income:.*Caroline' sum_positio ----------- -415.00 USD 5 MXN Splitting Expenses \uf0c1 The expenses side of the Income Statement shows the breakdown of expenses. Note how some of the expense accounts are explicitly booked to each member separately by their account name, e.g., \u201c Expenses:Scuba:Martin \u201d. The other accounts, e.g. \u201c Expenses:Groceries \u201d are intended to be split. How we\u2019re going to carry this out is by adding a plugin that will transform all the transactions to actually split the postings which are intended to be shared, that is, postings without any member\u2019s name as a component. For example, the following input transaction: 2015-02-28 * \"Scuba Club Cozumel\" | \"Room bill\" Income:Martin:CreditCard -1380.40 USD Expenses:Scuba:Martin 178.50 USD Expenses:Accommodation 1201.90 USD Will be transformed by the plugin into one like this: 2015-02-28 * \"Scuba Club Cozumel\" | \"Room bill\" Income:Martin:CreditCard -1380.40 USD Expenses:Scuba:Martin 178.50 USD Expenses:Accommodation:Martin 600.95 USD Expenses:Accommodation:Caroline 600.95 USD Note that: Only Expenses accounts get split into multiple postings. Accounts with the name of a member are assumed by convention to be already attributed to that person. In order to achieve this, the plugin has to be provided with the names of the members. All the resulting Income and Expenses accounts include the name of a member. The plugin is enabled like this: plugin \"beancount.plugins.split_expenses\" \"Martin Caroline\" Reloading the web page after uncommenting this line from the input file and visiting the Income Statement page should show a long list of Expenses accounts split by person. Now, in order to generate the total amount of expenses incurred for each person, we have to produce the balance of all Expenses accounts with a member\u2019s name in it, accounts like \u201c Expenses:.*Martin \u201d. The web tool does not provide such a filtering capability at the moment 1 , but we can use the bean-query tool to produce the total of expenses for each person: beancount> SELECT sum(position) WHERE account ~ '^Expenses:.*Martin' sum_positio ----------- 2007.43 USD 3837.0 MXN beancount> SELECT sum(position) WHERE account '^Expenses:.*Caroline' sum_positio ----------- 1686.97 USD 3637.0 MXN This says \u201cMartin accrued expenses of 2007.43 USD and 3837.0 MXN.\u201d You can manually convert this to a dollar amount: yuzu:~$ dc -e '2007.43 3837.0 13.465 / +p' 2291.43 Or you can use the recently introduced \u201c CONVERT \u201d function of bean-query to do this: beancount> SELECT convert(sum(position), 'USD') WHERE account ~ '^Expenses:.*Martin' convert_sum_position_c_ --------------------------------- 2288.528901098901098901098901 USD beancount> SELECT convert(sum(position), 'USD') WHERE account ~ '^Expenses:.*Caroline' convert_sum_position_c_ --------------------------------- 1953.416886446886446886446886 USD (The difference between the 2291.43 and 2288.53 amounts can be attributed to the usage of slightly different exchange rates used in the converting transactions.) Similarly, you can generate the list of payments made by each person, e.g.: beancount> SELECT sum(position) WHERE account ~ '^Income:.*Caroline' Final Transfer \uf0c1 In order to figure out the total amount owed by each member, the process is similar: Simply sum up the balances of all the accounts attributed to that particular member: beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Caroline' convert_sum_position_c_ --------------------------------- 1538.783186813186813186813187 USD beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Martin' convert_sum_position_c_ ---------------------------------- -1546.355494505494505494505494 USD Notice that this includes the Income and Expenses accounts for that person. It\u2019s as if we had two separate ledgers merged into one. (Once again, the small differences can be attributed to differences in exchange rate over time.) We can now make a final transfer amount in order to account for each of our expenses; we\u2019ve agreed to round this to 1500 USD: 2015-03-06 * \"Final transfer from Caroline to Martin to pay for difference\" Income:Caroline:Wallet -1500 USD Income:Martin:Wallet 1500 USD If you uncomment this transaction from the input file (near the end), you will find corrected balances: beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Martin' convert_sum_position_c_ --------------------------------- -46.3554945054945054945054945 USD beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Caroline' convert_sum_position_c_ -------------------------------- 38.7831868131868131868131868 USD Updating your Personal Ledger \uf0c1 So great! Now we\u2019ve reconciled each other for the trip. But you still need to reflect these expenses in your personal ledger (if you have one). In this section, I will explain how you should reflect these transactions in that file. Account Names \uf0c1 First, let us take note that the accounts in your personal ledger do not have to match the account names used in the trip\u2019s ledger file. I never process those files together. (A good argument for not attempting this is that each trip will include account names from other people and I prefer not to have those leak into my main ledger file.) Using a Tag \uf0c1 I like to use a tag to report on the entire set of transactions related to the trip. In this example, I used the tag #trip-cozumel2015 . Booking Contributions \uf0c1 Your contributions into the project should be booked to a temporary holding account. I call mine \u201c Assets:Travel:Pending \u201d. For example, this transaction from the trip\u2019s file: 2015-02-25 * \"Yellow Transfers\" | \"SuperShuttle to Playa del Carmen\" Expenses:Transport:Bus 656 MXN Income:Martin:CreditCard -44.12 USD @ 14.86854 MXN will eventually be imported in my personal file and categorized like this: 2015-02-25 * \"YELLOW TRANSFER MX CO\" #trip-cozumel2015 Liabilities:US:BofA:CreditCard -44.12 USD Assets:Travel:Pending 44.12 USD You can conceptualize this as contributing money to a pending pool of cash which you will eventually receive expenses from. The same goes for cash that I brought for the trip: 2015-02-24 * \"Taking cash with me on the trip\" Assets:Cash -1200.00 USD Assets:Travel:Pending 1200.00 USD Note that absolutely all of the transactions related to the trip should be booked to that one account, including the inter-account transfers: 2015-03-06 * \"Final transfer from Mexico trip\" #trip-cozumel2015 Assets:Google:Wallet 1500 USD Assets:Travel:Pending -1500 USD Booking Expenses \uf0c1 After the trip is concluded, we want to convert the balance in the pending account to a list of Expenses. To find the list of expenses for yourself, you can issue a query like this: beancount> SELECT account, sum(position) WHERE account ~ '^Expenses:.*:Martin' GROUP BY 1 ORDER BY 1 account sum_positio ------------------------------- ----------- Expenses:Accommodation:Martin 735.45 USD Expenses:Alcohol:Martin 483 MXN Expenses:Bicycles:Martin 69.5 MXN Expenses:Flights:Martin 488.00 USD Expenses:Groceries:Martin 197.0 MXN Expenses:Museum:Martin 64 MXN Expenses:Restaurant:Martin 22.28 USD 1795.5 MXN Expenses:Scuba:Martin 506.14 USD Expenses:Scuba:ParkFees:Martin 7.50 USD Expenses:Tips:Martin 225 MXN 189.16 USD Expenses:Transport:Bus:Martin 709 MXN Expenses:Transport:Taxi:Martin 53.90 USD 294 MXN Expenses:Transport:Train:Martin 5.00 USD I suppose you could script away this part to remove the member\u2019s name from the account names and have the script spit output already formatted as a nicely formatted Transaction, but because the account names will not match the personal ledger file\u2019s 1-to-1, you will have to perform a manual conversion anyway, so I did not bother automating this. Furthermore, you only have a single one of these to make after your trip, so it\u2019s not worth spending too much time making this part easier 2 . Here\u2019s what the final transaction looks like; I have a section in my input file with this: 2015-02-25 event \"location\" \"Cozumel, Mexico\" ;; (1) pushtag #trip-mexico-cozumel-2015 \u2026 other transactions related to the trip\u2026 2015-03-01 event \"location\" \"Tulum, Mexico\" ;; (1) 2015-03-07 * \"Final reconciliation - Booking pending expenses for trip\" Expenses:Travel:Accommodation 735.45 USD Expenses:Food:Alcohol 483.0 MXN Expenses:Sports:Velo 69.5 MXN Expenses:Transportation:Flights 488.00 USD Expenses:Food:Grocery 197.0 MXN Expenses:Fun:Museum 64.0 MXN Expenses:Food:Restaurant 22.28 USD Expenses:Food:Restaurant 1795.5 MXN Expenses:Scuba:Dives 506.14 USD Expenses:Scuba:Fees 7.50 USD Expenses:Scuba:Tips 225.0 MXN Expenses:Scuba:Tips 189.16 USD Expenses:Transportation:Bus 709.0 MXN Expenses:Transportation:Taxi 53.90 USD Expenses:Transportation:Taxi 294.0 MXN Expenses:Transportation:Train 5.00 USD Assets:Cash:Foreign 60.0 MXN ;; (2) Assets:Cash 330.00 USD ;; (3) Assets:Travel:Pending -2337.43 USD ;; (4) Assets:Travel:Pending -288.67 USD @@ 3897.0 MXN ;; (5) 2015-03-07 * \"Writing off difference as gift to Caroline\" ;; (6) Assets:Travel:Pending -43.18 USD Expenses:Gifts 2015-03-14 balance Assets:Travel:Pending 0 USD ;; (7) 2015-03-14 balance Assets:Travel:Pending 0 MXN poptag #trip-mexico-cozumel-2015 Observations: I used \u201cevent\u201d directives in order to track my location. I\u2019m doing this because I will eventually need it for immigration purposes (and just for fun, to track the days I spend in places). I moved the extra cash I kept to my \u201cforeign cash pocket\u201d account: Assets:Cash:Foreign I \u201cmoved\u201d back the cash I had with me after I returned from the trip. This leg removes the USD expenses from the Pending account. The leg removes the MXN expenses from the Pending account. I calculated its amount manually (3897 / 13.5 ~= 288.67 USD) and use the full amount as the exchange rate. I want to completely zero out the Pending account after the trip, and so I write off the excess amount we agreed not to pay (the difference between the imbalance and 1500.00 USD). Finally, I assert that the Pending account is empty of USD and of MXN. I found the missing amounts by running bean-check or using bean-doctor context on an incomplete transaction from Emacs. Generating Reports \uf0c1 If you want to automate the generation of reports for each of the participants in a trip, there is a script that generates text (and eventually CSV) reports for the queries mentioned in the previous sections. You can use this script to provide expenses status after or even during the trip or project, for each of the participants. The script lives in the split_expenses plugin itself, and you invoke it like this: python3 -m beancount.plugins.split_expenses --text= For each person, it will generate the following reports: A detailed journal of expenses A detailed journal of contributions A breakdown of expenses for each category (account type) Finally, it will also generate a final balance for each person, which you can use to send final reconciling transfers to each other. Try it now on one of the example files provided with Beancount. Other Examples \uf0c1 There is another example file that shows how to share expenses between three participants in duxbury2015.beancount . Look to more example files to be introduced over time. Conclusion \uf0c1 There is more than just one way to carry out the task we describe here. However, the method we present extends nicely to a larger group of participants, allows us to account for situations where particular expenses are incurred for one individual as part of a group, and finally, allows for a non-even split between the participants. This is pretty general. It\u2019s also a short step to accounting for an ongoing project with a longer term. The ideas presented in this document provide a nice use case for the usage of the double-entry method in a simple setting. I hope that working through this use case will help people develop the intuitions necessary in using the double-entry method. As our SQL-like query language matures, bean-web will eventually allow the user to create views from a set of transactions filtered from an expression. This will be implemented eventually. \u21a9 Note that we could implement a special \u201cbeancount\u201d supported format to the bean-query tool in order to write out balances in Transaction form. I\u2019m not sure how useful that would be but it\u2019s an idea worth considering for the future. \u21a9","title":"Sharing Expenses with Beancount"},{"location":"sharing_expenses_with_beancount.html#sharing-expenses-in-beancount","text":"Martin Blais , May 2015 http://furius.ca/beancount/doc/shared","title":"Sharing Expenses in Beancount"},{"location":"sharing_expenses_with_beancount.html#introduction","text":"This document presents a method for precisely and easily accounting for shared expenses in complicated group situations. For example, traveling with a group of friends where people pay for different things. We show it is possible to deal with expenses to be split evenly among the group as well as group expenses that should be allocated to specific persons. The method uses the double-entry accounting system. The essence of the method consists in separating the accounting of expenses and their associated payments. This makes it much easier to deal with shared costs, because using this method it doesn\u2019t matter who pays for what: we reconcile the precise amounts owed for each individual at the end and automatically compute final correcting transfers for each. This article is structured around a simple trip with expenses shared equally by two people. However, the same method generalizes to any type of project with incomes and expenses to be shared among multiple participants, and splitting expenses evenly or not.","title":"Introduction"},{"location":"sharing_expenses_with_beancount.html#a-travel-example","text":"Martin (the author) and Caroline (his girlfriend) visited Mexico in March 2015. We visited the island of Cozumel for a SCUBA diving trip for three days and then headed to Tulum for two days to relax and dive in the cenotes . Our assumptions are: Chaotic payments . We lead very busy lives\u2026 both of us are going to make payments ahead of time and during the trip, without any forethought regarding who will pay for what, though we will carefully record every payment. Each of us just pays for whatever preparation costs as needed to arrange this trip. For example, Caroline selected and booked flights early while I paid for the resort and booked the rental and activities at the local dive shop one week before departure. Use of shared and individual assets. Both of us are bringing cash, and are going to make payments during the trip from our respective wallets as well as from a shared pool of cash converted to local currency (Mexican pesos, for which I will use the \u201cMXN\u201d symbol), and we will use credit cards as necessary before and during the trip. Multiple currencies. Some of our expenses will be denominated in US dollars and some in Mexican pesos. For example, flights were paid in US dollars, local meals and the accommodation were paid in pesos, but the local dive shop charged us dollars. Converted amounts will come from both cash and credit cards sources. Individual expenses in a shared pool. While most of the expenses are to be shared equally, some of the expenses will apply to only one of us, and we want to account for those explicitly. For example, Caroline took a SCUBA certification course ( PADI Open Water ) and will pay for that on her own; similarly, she should not be paying for Martin\u2019s expensive boat diving costs. To complicate matters, the dive shop issued us a single joint bill for everything at the end of our stay.","title":"A Travel Example"},{"location":"sharing_expenses_with_beancount.html#a-note-about-sharing","text":"I feel that something should be said about the \u201csharing\u201d aspect of our expenses, as this topic has come up on previous discussions on the mailing-lists involving sharing examples. We are nitpicking on purpose. For the purpose of this exercise, we are accounting for every little penny spent in an incredibly detailed manner. The point of this document is specifically to show how a complex set of transactions well accounted for can be efficiently and simply disentangled to a precise accounting of expenses for each participant, regardless of who actually makes the payments. We are not cheapskates. We will assume that we\u2019ve decided to split expenses evenly. Our \u201cgenerosity\u201d to each other is not a topic relevant to this document. We\u2019re both well compensated working professionals and you can assume that we\u2019ve agreed to split the common costs for this trip evenly (50/50). One of the attributes of the method we show here is that the decision of how we choose to split expenses can be made separately from the actual payment of costs. For example, we may decide in the end that I pay for \u2154rd of the trip, but that can be decided precisely rather than in an ad-hoc \u201coh, I think I remember I paid for this\u201d manner. This is especially useful in a larger group of people because when expenses aren\u2019t tracked accurately, usually everyone is left with a feeling that they paid more than the others. The sum of the perceived fractions of expenses paid in a group is always greater than 100%...","title":"A Note About Sharing"},{"location":"sharing_expenses_with_beancount.html#overview-of-the-method","text":"In this section we present a brief illustrated overview of the method. A set of common Assets accounts that belong to the project, and book all our individual expenses and transfer for the trip as coming from external Income accounts: During the trip, we use the common Assets to make expenses. Most of the expenses are attributed to both of us (and to be shared eventually), but some of the expenses are intended to be attributed to each of us individually: After the trip, remaining Assets (like cash we walked home with) gets distributed back to ourselves to zero out the balances of the Assets accounts and we record this through contra postings to Income accounts: Finally, the list of shared Expenses are split between each other\u2014using a plugin that forks every posting that is intended to be a shared expense\u2014and the final amount is used to make a final transfer between each other so that we\u2019ve each paid for our respective expenses and we\u2019re square: Note that the final balance of expenses for each participant may differ, and these are due to particular expenses that were attributed separately, or if we decide to split the total unevenly.","title":"Overview of the Method"},{"location":"sharing_expenses_with_beancount.html#how-to-track-expenses","text":"In order to write down all expenses for this trip we will open a brand new Beancount input file. Despite the fact that the expenses will come from each person\u2019s personal accounts, it is useful to think of the trip as a special project, as if it were a separate entity, e.g., a company that exists only for the duration of the trip. The example file we wrote for our trip can be found here and should help you follow along this text.","title":"How to Track Expenses"},{"location":"sharing_expenses_with_beancount.html#accounts","text":"The set of accounts in the input file should not have to match your personal Beancount file\u2019s account names. The accounts we will use include accounts that correspond to Martin and Caroline\u2019s personal accounts but with generic names (e.g., Income:Martin:CreditCard instead of Liabilities:US:Chase ), and expense accounts may not match any regular expenses in my personal Beancount file\u2014it\u2019s not important. As a convention, any account that pertains specifically to one of the travelers will include that person\u2019s name in the account name. For example, Caroline\u2019s credit card will be named \u201c Income:Caroline:CreditCard \u201d. This is important, as we will use this later on to split contributions and expenses. Let\u2019s examine the different types of accounts we will need to carry this out.","title":"Accounts"},{"location":"sharing_expenses_with_beancount.html#external-income-accounts","text":"The \u201cproject\u201d will receive income in the form of transfers from personal accounts of either traveler. These are accounts we will consider external to the project and so will define them as Income accounts: ;; External accounts for Martin. 2015-02-01 open Income:Martin:Cash 2015-02-01 open Income:Martin:Cash:Foreign 2015-02-01 open Income:Martin:Wallet 2015-02-01 open Income:Martin:CreditCard ;; External accounts for Caroline. 2015-02-01 open Income:Caroline:Cash 2015-02-01 open Income:Caroline:Wallet 2015-02-01 open Income:Caroline:MetroCard 2015-02-01 open Income:Caroline:CreditCard Transactions carried out from these accounts must be copied from your personal Beancount file. Obviously, you must be careful to include all the transactions pertaining to the trip. I used a tag to do this in my personal file.","title":"External Income Accounts"},{"location":"sharing_expenses_with_beancount.html#assets-liabilities-accounts","text":"There will be a few Asset accounts that will be active and exist for the duration of the trip. These temporary accounts will be zero\u2019ed out at the end of it. One example is a pool of petty cash in local currency: 2015-02-01 open Assets:Cash:Pesos description: \"A shared account to contain our pocket of pesos\" We also carried cash in each of our pockets while traveling, so I created two separate accounts for that: 2015-02-01 open Assets:Cash:Martin description: \"Cash for the trip held by Martin\" 2015-02-01 open Assets:Cash:Caroline description: \"Cash for the trip held by Caroline\" Note however that despite their individual names, those accounts are considered as part of the project. It was just convenient to separately track balances for the cash we each held during the trip.","title":"Assets & Liabilities Accounts"},{"location":"sharing_expenses_with_beancount.html#expenses-accounts","text":"We will define various accounts to book our Expenses to. For example, \u201c Expenses:Flights \u201d will contain our costs associated with flight travel. For convenience, and because there are many types of expenses in this file, we chose to leverage the \u201cauto-accounts\u201d plugin and let Beancount automatically open these accounts: plugin \"beancount.ops.auto_accounts\" The great majority of these accounts are for shared expenses to be split between us. For example, shared SCUBA diving expenses will be booked to \u201c Expenses:Scuba \u201d. However, for expenses that are intended to be covered by one of us only, we simply include the name of the traveler in the account name. For example, Martin\u2019s extra costs for boat diving will be booked to the \u201c Expenses:Scuba:Martin \u201d account.","title":"Expenses Accounts"},{"location":"sharing_expenses_with_beancount.html#example-transactions","text":"Let\u2019s turn our attention to the different types of transactions present in the file. In this section I will walk you through some representative transactions (the names I give to these are arbitrary).","title":"Example Transactions"},{"location":"sharing_expenses_with_beancount.html#contributing-transactions","text":"Contributions to the project\u2019s costs are usually done in the form of expenses paid from external accounts. For example, Caroline paying for flights with her credit card looks like this: 2015-02-01 * \"Flights to Cancun\" Income:Caroline:CreditCard -976.00 USD Expenses:Flights Martin paying for the taxi to the airport looks like this: 2015-02-25 * \"Taxi to airport\" ^433f66ea0e4e Expenses:Transport:Taxi 62.80 USD Income:Martin:CreditCard","title":"Contributing Transactions"},{"location":"sharing_expenses_with_beancount.html#bringing-cash","text":"We both brought some cash on us, as we were warned it might be difficult to use credit cards in Mexico. In my personal Beancount file, the cash account is \u201c Assets:Cash \u201d but here it must get booked as an external contribution with my name on it: ;; Initial cash on us. 2015-02-24 * \"Getting cash for travel\" Income:Martin:Cash -1200 USD Assets:Cash:Martin 1200 USD Caroline\u2019s cash is similar: 2015-02-24 * \"Getting cash for travel\" Income:Caroline:Cash -300 USD Assets:Cash:Caroline 300 USD Once again, note that the Assets:Cash:Martin and Assets:Cash:Caroline accounts are considered a part of the project, in this case it just refers to who is carrying it. (These accounts are cleared at the end, so it does not matter that our names are in them.)","title":"Bringing Cash"},{"location":"sharing_expenses_with_beancount.html#transfers","text":"Before the trip, I was really busy. It looked like Caroline was going to make most of arrangements and upfront payments, so I made a transfer to her Google Wallet to help her cover for some expenses ahead of time: 2015-02-01 * \"Transfer Martin -> Caroline on Google Wallet\" Income:Martin:Wallet -1000 USD Income:Caroline:Wallet 1000 USD","title":"Transfers"},{"location":"sharing_expenses_with_beancount.html#cash-conversions","text":"Obtaining local currency was done by changing a small amount of cash at the airport (at a very bad rate): 2015-02-25 * \"Exchanged cash at XIC at CUN airport\" Assets:Cash:Caroline -100.00 USD @ 12.00 MXN Assets:Cash:Pesos 1200 MXN The \u201c Assets:Cash:Pesos \u201d account tracks our common pool of local currency that we use for various small expenses.","title":"Cash Conversions"},{"location":"sharing_expenses_with_beancount.html#cash-expenses-in-us-dollars","text":"Some local expenses will call for US money, which in this example I paid from my cash pocket: 2015-03-01 * \"Motmot Diving\" | \"Deposit for cenote diving\" Expenses:Scuba 50.00 USD Assets:Cash:Martin","title":"Cash Expenses in US Dollars"},{"location":"sharing_expenses_with_beancount.html#cash-expenses-in-local-currency","text":"Paying cash using pesos from our shared stash of pesos looks like this: 2015-02-25 * \"UltraMar Ferry across to Cozumel\" Expenses:Transport:Bus 326 MXN Assets:Cash:Pesos Sometimes we even had to pay with a mix of US dollars and pesos. In this example, we ran out of pesos, so we have to give them dollars and pesos (all the restaurants and hotels in the beach part of Tulum accept US currency): 2015-03-01 * \"Hartwood\" | \"Dinner - ran out of pesos\" Expenses:Restaurant 1880 MXN Assets:Cash:Pesos -1400 MXN Assets:Cash:Martin -40.00 USD @ 12.00 MXN I used the unfavorable rate the restaurant was offering to accept dollars at (the market rate was 14.5 at the time).","title":"Cash Expenses in Local Currency"},{"location":"sharing_expenses_with_beancount.html#individual-expenses","text":"Here is an example of booking individual expenses using shared money. In order for us to have access to the reef for diving, we had to pay a \u201cmarine park\u201d fee of $2.50 per day to the island. This was a short trip where I dove only three days there and Caroline\u2019s fee was included in her course except for one day: 2015-02-25 * \"Marine Park (3 days Martin, 1 day Caroline)\" Expenses:Scuba:ParkFees:Martin 7.50 USD Expenses:Scuba:ParkFees:Caroline 2.50 USD Assets:Cash:Martin All that needs to be done is to book these to expense accounts with our names in them, which will get separated out at the end. Here is a more complicated example: the dive shop at Scuba Club Cozumel charged us a single bill at the end of our stay for all our rental gear and extra dives. All I did here was translate the itemized bill into a single transaction, booking to each other: 2015-03-01 * \"Scuba Club Cozumel\" | \"Dive shop bill\" ^69b409189b37 Income:Martin:CreditCard -381.64 USD Expenses:Scuba:Martin 27 USD ;; Regulator w/ Gauge Expenses:Scuba:Caroline 9 USD ;; Regulator w/ Gauge Expenses:Scuba:Martin 27 USD ;; BCD Expenses:Scuba:Caroline 9 USD ;; BCD Expenses:Scuba:Martin 6 USD ;; Fins Expenses:Scuba:Martin 24 USD ;; Wetsuit Expenses:Scuba:Caroline 8 USD ;; Wetsuit Expenses:Scuba:Caroline 9 USD ;; Dive computer Expenses:Scuba:Martin 5 USD ;; U/W Light Expenses:Scuba:Caroline 70 USD ;; Dive trip (2 tank) Expenses:Scuba:Martin 45 USD ;; Wreck Dive w/ Lite Expenses:Scuba:Martin 45 USD ;; Afternoon dive Expenses:Scuba:Caroline 45 USD ;; Afternoon dive Expenses:Scuba:Martin 28.64 USD ;; Taxes Expenses:Scuba:Caroline 24.00 USD ;; Taxes","title":"Individual Expenses"},{"location":"sharing_expenses_with_beancount.html#final-balances","text":"Of course you can use balance assertions at any time during the trip. For example, just before leaving at the Cancun airport we knew we wouldn\u2019t spend any more Mexican pesos for a while, so I counted what we had left after Caroline decided to splurge the remainder of them into overpriced chocolate sold at the airport shop: 2015-03-04 balance Assets:Cash:Pesos 65 MXN Ideally the bookkeeper should want to do this at a quiet moment at the end of every day or couple of days, which makes it easier to triangulate an expense you might have forgotten to enter (we are on vacation after all, in our relaxed state of mind we forget to write down stuff here and there).","title":"Final Balances"},{"location":"sharing_expenses_with_beancount.html#clearing-asset-accounts","text":"At the end of the trip, you should clear the final balances of all Assets and Liabilities accounts by transferring the remaining funds out to the participants, i.e., to the Income accounts. This should leave all the balances of the trip at zero and ensures all the cash put forward for trip expenses has been moved out to travelers. By the way, it doesn\u2019t matter much who keeps this money, because at the end we will end up making a single correcting transfer that should account for the balance required to create an even split. You can transfer it to anyone; the end result will be the same. Our clearing transaction looked like this: 2015-03-06 * \"Final transfer to clear internal balances to external ones\" Assets:Cash:Pesos -65 MXN Income:Martin:Cash:Foreign 60 MXN Income:Caroline:Cash 5 MXN Assets:Cash:Martin -330 USD Income:Martin:Cash 330 USD Assets:Cash:Caroline -140 USD Income:Caroline:Cash 140 USD 2015-03-07 balance Assets:Cash:Pesos 0 MXN 2015-03-07 balance Assets:Cash:Pesos 0 USD 2015-03-07 balance Assets:Cash:Martin 0 USD 2015-03-07 balance Assets:Cash:Caroline 0 USD We had three 20 peso bills, and I kept the bills for future travels. Caroline kept the 5 peso coin (forgot to hand it over as a tip). We transferred out the respective cash amounts we had been carrying together during the trip.","title":"Clearing Asset Accounts"},{"location":"sharing_expenses_with_beancount.html#how-to-take-notes","text":"During this trip I did not carry a laptop\u2014this was vacation after all. I like to disconnect. I did not carry a notebook either. Instead, I just took notes at the end of the day on a few sheets of paper at the hotel. This process took about 5-10 minutes each night, just recalling from memory and writing things down. These notes look like this: I made a paper spreadsheet where each line had The narration for the transaction (a description that would allow me to select an Expenses account later on) Who paid (which Assets or Income account the money came from) The amount (either in USD or MXN) After our trip, I sat down at the computer and typed the corresponding Beancount file . If I had a computer during my vacation I probably would have typed it as we went along. Of course, I had to do a few adjustments here and there because of mistakes. The bottom line is: if you\u2019re organized well, the overhead of doing this is minimal.","title":"How to Take Notes"},{"location":"sharing_expenses_with_beancount.html#reconciling-expenses","text":"Running bean-web on the trip\u2019s file: bean-web beancount/examples/sharing/cozumel2015.beancount You can view the balances in the \u201cAll Transactions\u201d view (click on \u201cAll Transactions\u201d). The Balance Sheet should show empty balances for Assets accounts: The balances of the equity accounts should reflect the total amount of currency conversions made during the trip. You can verify this by calculating the amount-weight average rate like this: 7539.00 / 559.88 ~= 13.465 USD/MXN (which is about right).","title":"Reconciling Expenses"},{"location":"sharing_expenses_with_beancount.html#reviewing-contributions","text":"The Income Statement should show a summary of all expenses and contributions to the project: The Income account balances show the total amounts of contributions for each person. Note that in creating the Income accounts, I went through the extra step of creating some specific accounts for each source of payment, like \u201cCaroline\u2019s Credit Card\u201d, etc. From this view, we can see that we contributed a total of 4254.28 USD (and were left with 65 MXN in hand) for this trip. The expenses side should match, considering the currency exchanges: 3694.40 + 7474 / 13.465 ~= 4249 USD which is approximately right (the small difference can be explained by the varying currency conversions). If you want to view the list of contribution payments and the final balance, click on a particular traveler\u2019s root account, e.g., \u201cIncome:Caroline\u201d (click on \u201cCaroline\u201d) which should take you to the Journal for that root account: This journal includes all the transactions in its sub-accounts. The final value at the bottom should show the total balance of those accounts, and thus, the amount of money Caroline contributed to this trip: 415 USD, and kept 5 MXN (in coin). We can do the same for Martin and find the final balance of 3839.28 USD and kept 60 MXN (in bills). You can also get the same amounts by using bean-query to achieve the same thing: ~/p/.../examples/sharing$ bean-query cozumel2015.beancount Input file: \"Mexico Trip: Cozumel & Tulum SCUBA Diving\" Ready with 105 directives (160 postings in 45 transactions). beancount> SELECT sum(position) WHERE account ~ '^Income:.*Caroline' sum_positio ----------- -415.00 USD 5 MXN","title":"Reviewing Contributions"},{"location":"sharing_expenses_with_beancount.html#splitting-expenses","text":"The expenses side of the Income Statement shows the breakdown of expenses. Note how some of the expense accounts are explicitly booked to each member separately by their account name, e.g., \u201c Expenses:Scuba:Martin \u201d. The other accounts, e.g. \u201c Expenses:Groceries \u201d are intended to be split. How we\u2019re going to carry this out is by adding a plugin that will transform all the transactions to actually split the postings which are intended to be shared, that is, postings without any member\u2019s name as a component. For example, the following input transaction: 2015-02-28 * \"Scuba Club Cozumel\" | \"Room bill\" Income:Martin:CreditCard -1380.40 USD Expenses:Scuba:Martin 178.50 USD Expenses:Accommodation 1201.90 USD Will be transformed by the plugin into one like this: 2015-02-28 * \"Scuba Club Cozumel\" | \"Room bill\" Income:Martin:CreditCard -1380.40 USD Expenses:Scuba:Martin 178.50 USD Expenses:Accommodation:Martin 600.95 USD Expenses:Accommodation:Caroline 600.95 USD Note that: Only Expenses accounts get split into multiple postings. Accounts with the name of a member are assumed by convention to be already attributed to that person. In order to achieve this, the plugin has to be provided with the names of the members. All the resulting Income and Expenses accounts include the name of a member. The plugin is enabled like this: plugin \"beancount.plugins.split_expenses\" \"Martin Caroline\" Reloading the web page after uncommenting this line from the input file and visiting the Income Statement page should show a long list of Expenses accounts split by person. Now, in order to generate the total amount of expenses incurred for each person, we have to produce the balance of all Expenses accounts with a member\u2019s name in it, accounts like \u201c Expenses:.*Martin \u201d. The web tool does not provide such a filtering capability at the moment 1 , but we can use the bean-query tool to produce the total of expenses for each person: beancount> SELECT sum(position) WHERE account ~ '^Expenses:.*Martin' sum_positio ----------- 2007.43 USD 3837.0 MXN beancount> SELECT sum(position) WHERE account '^Expenses:.*Caroline' sum_positio ----------- 1686.97 USD 3637.0 MXN This says \u201cMartin accrued expenses of 2007.43 USD and 3837.0 MXN.\u201d You can manually convert this to a dollar amount: yuzu:~$ dc -e '2007.43 3837.0 13.465 / +p' 2291.43 Or you can use the recently introduced \u201c CONVERT \u201d function of bean-query to do this: beancount> SELECT convert(sum(position), 'USD') WHERE account ~ '^Expenses:.*Martin' convert_sum_position_c_ --------------------------------- 2288.528901098901098901098901 USD beancount> SELECT convert(sum(position), 'USD') WHERE account ~ '^Expenses:.*Caroline' convert_sum_position_c_ --------------------------------- 1953.416886446886446886446886 USD (The difference between the 2291.43 and 2288.53 amounts can be attributed to the usage of slightly different exchange rates used in the converting transactions.) Similarly, you can generate the list of payments made by each person, e.g.: beancount> SELECT sum(position) WHERE account ~ '^Income:.*Caroline'","title":"Splitting Expenses"},{"location":"sharing_expenses_with_beancount.html#final-transfer","text":"In order to figure out the total amount owed by each member, the process is similar: Simply sum up the balances of all the accounts attributed to that particular member: beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Caroline' convert_sum_position_c_ --------------------------------- 1538.783186813186813186813187 USD beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Martin' convert_sum_position_c_ ---------------------------------- -1546.355494505494505494505494 USD Notice that this includes the Income and Expenses accounts for that person. It\u2019s as if we had two separate ledgers merged into one. (Once again, the small differences can be attributed to differences in exchange rate over time.) We can now make a final transfer amount in order to account for each of our expenses; we\u2019ve agreed to round this to 1500 USD: 2015-03-06 * \"Final transfer from Caroline to Martin to pay for difference\" Income:Caroline:Wallet -1500 USD Income:Martin:Wallet 1500 USD If you uncomment this transaction from the input file (near the end), you will find corrected balances: beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Martin' convert_sum_position_c_ --------------------------------- -46.3554945054945054945054945 USD beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Caroline' convert_sum_position_c_ -------------------------------- 38.7831868131868131868131868 USD","title":"Final Transfer"},{"location":"sharing_expenses_with_beancount.html#updating-your-personal-ledger","text":"So great! Now we\u2019ve reconciled each other for the trip. But you still need to reflect these expenses in your personal ledger (if you have one). In this section, I will explain how you should reflect these transactions in that file.","title":"Updating your Personal Ledger"},{"location":"sharing_expenses_with_beancount.html#account-names","text":"First, let us take note that the accounts in your personal ledger do not have to match the account names used in the trip\u2019s ledger file. I never process those files together. (A good argument for not attempting this is that each trip will include account names from other people and I prefer not to have those leak into my main ledger file.)","title":"Account Names"},{"location":"sharing_expenses_with_beancount.html#using-a-tag","text":"I like to use a tag to report on the entire set of transactions related to the trip. In this example, I used the tag #trip-cozumel2015 .","title":"Using a Tag"},{"location":"sharing_expenses_with_beancount.html#booking-contributions","text":"Your contributions into the project should be booked to a temporary holding account. I call mine \u201c Assets:Travel:Pending \u201d. For example, this transaction from the trip\u2019s file: 2015-02-25 * \"Yellow Transfers\" | \"SuperShuttle to Playa del Carmen\" Expenses:Transport:Bus 656 MXN Income:Martin:CreditCard -44.12 USD @ 14.86854 MXN will eventually be imported in my personal file and categorized like this: 2015-02-25 * \"YELLOW TRANSFER MX CO\" #trip-cozumel2015 Liabilities:US:BofA:CreditCard -44.12 USD Assets:Travel:Pending 44.12 USD You can conceptualize this as contributing money to a pending pool of cash which you will eventually receive expenses from. The same goes for cash that I brought for the trip: 2015-02-24 * \"Taking cash with me on the trip\" Assets:Cash -1200.00 USD Assets:Travel:Pending 1200.00 USD Note that absolutely all of the transactions related to the trip should be booked to that one account, including the inter-account transfers: 2015-03-06 * \"Final transfer from Mexico trip\" #trip-cozumel2015 Assets:Google:Wallet 1500 USD Assets:Travel:Pending -1500 USD","title":"Booking Contributions"},{"location":"sharing_expenses_with_beancount.html#booking-expenses","text":"After the trip is concluded, we want to convert the balance in the pending account to a list of Expenses. To find the list of expenses for yourself, you can issue a query like this: beancount> SELECT account, sum(position) WHERE account ~ '^Expenses:.*:Martin' GROUP BY 1 ORDER BY 1 account sum_positio ------------------------------- ----------- Expenses:Accommodation:Martin 735.45 USD Expenses:Alcohol:Martin 483 MXN Expenses:Bicycles:Martin 69.5 MXN Expenses:Flights:Martin 488.00 USD Expenses:Groceries:Martin 197.0 MXN Expenses:Museum:Martin 64 MXN Expenses:Restaurant:Martin 22.28 USD 1795.5 MXN Expenses:Scuba:Martin 506.14 USD Expenses:Scuba:ParkFees:Martin 7.50 USD Expenses:Tips:Martin 225 MXN 189.16 USD Expenses:Transport:Bus:Martin 709 MXN Expenses:Transport:Taxi:Martin 53.90 USD 294 MXN Expenses:Transport:Train:Martin 5.00 USD I suppose you could script away this part to remove the member\u2019s name from the account names and have the script spit output already formatted as a nicely formatted Transaction, but because the account names will not match the personal ledger file\u2019s 1-to-1, you will have to perform a manual conversion anyway, so I did not bother automating this. Furthermore, you only have a single one of these to make after your trip, so it\u2019s not worth spending too much time making this part easier 2 . Here\u2019s what the final transaction looks like; I have a section in my input file with this: 2015-02-25 event \"location\" \"Cozumel, Mexico\" ;; (1) pushtag #trip-mexico-cozumel-2015 \u2026 other transactions related to the trip\u2026 2015-03-01 event \"location\" \"Tulum, Mexico\" ;; (1) 2015-03-07 * \"Final reconciliation - Booking pending expenses for trip\" Expenses:Travel:Accommodation 735.45 USD Expenses:Food:Alcohol 483.0 MXN Expenses:Sports:Velo 69.5 MXN Expenses:Transportation:Flights 488.00 USD Expenses:Food:Grocery 197.0 MXN Expenses:Fun:Museum 64.0 MXN Expenses:Food:Restaurant 22.28 USD Expenses:Food:Restaurant 1795.5 MXN Expenses:Scuba:Dives 506.14 USD Expenses:Scuba:Fees 7.50 USD Expenses:Scuba:Tips 225.0 MXN Expenses:Scuba:Tips 189.16 USD Expenses:Transportation:Bus 709.0 MXN Expenses:Transportation:Taxi 53.90 USD Expenses:Transportation:Taxi 294.0 MXN Expenses:Transportation:Train 5.00 USD Assets:Cash:Foreign 60.0 MXN ;; (2) Assets:Cash 330.00 USD ;; (3) Assets:Travel:Pending -2337.43 USD ;; (4) Assets:Travel:Pending -288.67 USD @@ 3897.0 MXN ;; (5) 2015-03-07 * \"Writing off difference as gift to Caroline\" ;; (6) Assets:Travel:Pending -43.18 USD Expenses:Gifts 2015-03-14 balance Assets:Travel:Pending 0 USD ;; (7) 2015-03-14 balance Assets:Travel:Pending 0 MXN poptag #trip-mexico-cozumel-2015 Observations: I used \u201cevent\u201d directives in order to track my location. I\u2019m doing this because I will eventually need it for immigration purposes (and just for fun, to track the days I spend in places). I moved the extra cash I kept to my \u201cforeign cash pocket\u201d account: Assets:Cash:Foreign I \u201cmoved\u201d back the cash I had with me after I returned from the trip. This leg removes the USD expenses from the Pending account. The leg removes the MXN expenses from the Pending account. I calculated its amount manually (3897 / 13.5 ~= 288.67 USD) and use the full amount as the exchange rate. I want to completely zero out the Pending account after the trip, and so I write off the excess amount we agreed not to pay (the difference between the imbalance and 1500.00 USD). Finally, I assert that the Pending account is empty of USD and of MXN. I found the missing amounts by running bean-check or using bean-doctor context on an incomplete transaction from Emacs.","title":"Booking Expenses"},{"location":"sharing_expenses_with_beancount.html#generating-reports","text":"If you want to automate the generation of reports for each of the participants in a trip, there is a script that generates text (and eventually CSV) reports for the queries mentioned in the previous sections. You can use this script to provide expenses status after or even during the trip or project, for each of the participants. The script lives in the split_expenses plugin itself, and you invoke it like this: python3 -m beancount.plugins.split_expenses --text= For each person, it will generate the following reports: A detailed journal of expenses A detailed journal of contributions A breakdown of expenses for each category (account type) Finally, it will also generate a final balance for each person, which you can use to send final reconciling transfers to each other. Try it now on one of the example files provided with Beancount.","title":"Generating Reports"},{"location":"sharing_expenses_with_beancount.html#other-examples","text":"There is another example file that shows how to share expenses between three participants in duxbury2015.beancount . Look to more example files to be introduced over time.","title":"Other Examples"},{"location":"sharing_expenses_with_beancount.html#conclusion","text":"There is more than just one way to carry out the task we describe here. However, the method we present extends nicely to a larger group of participants, allows us to account for situations where particular expenses are incurred for one individual as part of a group, and finally, allows for a non-even split between the participants. This is pretty general. It\u2019s also a short step to accounting for an ongoing project with a longer term. The ideas presented in this document provide a nice use case for the usage of the double-entry method in a simple setting. I hope that working through this use case will help people develop the intuitions necessary in using the double-entry method. As our SQL-like query language matures, bean-web will eventually allow the user to create views from a set of transactions filtered from an expression. This will be implemented eventually. \u21a9 Note that we could implement a special \u201cbeancount\u201d supported format to the bean-query tool in order to write out balances in Transaction form. I\u2019m not sure how useful that would be but it\u2019s an idea worth considering for the future. \u21a9","title":"Conclusion"},{"location":"stock_vesting_in_beancount.html","text":"Stock Vesting in Beancount \uf0c1 Martin Blais , June 2015 http://furius.ca/beancount/doc/vesting Introduction \uf0c1 This document explains the vesting of restricted stock units in Beancount, by way of an example. This example may not exactly match your situation, but enough detail is provided that you should be able to adapt it for your own particular differences. A working example file can be found here to follow along with this text. Restricted Stock Compensation \uf0c1 Many technology companies offer their employees incentive compensation in the form of \u201cgrants\u201d (or \u201cawards\u201d) of \u201crestricted stock units\u201d (RSU), which is essentially a promise for the \u201crelease\u201d to you of actual shares in the future. The stock is \u201crestricted\u201d in the sense that you cannot access it\u2014you only receive it when it \u201cvests\u201d, and this happens based on a schedule. Typically, you are promised a fixed number of shares that vest every quarter or every month over a period of 3 or 4 years. If you leave the company, your remaining unvested shares are lost. One way you can view these RSUs is as an asset, a receivable that arrives regularly over time. These RSUs are essentially compensation denominated in the currency of the company\u2019s shares itself. We want to track the unraveling of these unvested units, and correctly account for their conversion to real stock with a cost basis and including whatever taxes were paid upon vesting. Tracking Awards \uf0c1 Commodities \uf0c1 First we want to define some commodities. In this example, I work for \u201cHooli Inc.\u201d and will eventually receive shares of that company (valued in US dollars): 1990-12-02 commodity HOOL name: \"Common shares of Hooli Inc.\" quote: USD We will also want to track the amount of unvested shares: 2013-01-28 commodity HOOL.UNVEST name: \"Unvested shares of Hooli from awards.\" Accounts for Awards \uf0c1 Grants received is income. I use \u201cIncome:US:Hooli\u201d as the root for all income accounts from Hooli, but in particular, I define an account for the awards, which contains units of unvested stock: 2013-01-28 open Income:US:Hooli:Awards HOOL.UNVEST When the stock vests, we will need to book the other side of this income somewhere, so we define an expenses account to count how much stock has been vested over a period of time: 2014-01-28 open Expenses:Hooli:Vested HOOL.UNVEST Receiving Awards \uf0c1 When you receive a new award (this may occur every year, for example, some people call this a \u201cstock refresh\u201d), you receive it as income and deposit it into a fresh new account, used to track this particular award: 2014-04-02 * \"Award S0012345\" Income:US:Hooli:Awards -1680 HOOL.UNVEST Assets:US:Hooli:Unvested:S0012345 1680 HOOL.UNVEST 2014-04-02 open Assets:US:Hooli:Unvested:S0012345 You may have multiple active awards at the same time. It\u2019s nice to have a separate account per award, as it offers a natural way to list their contents and when the award expires, you can close the account\u2014the list of open award accounts gives you the list of outstanding & actively vesting awards. In this example I used the number of the award (#S0012345) as the sub-account name. It\u2019s useful to use the number as the statements typically include it. I like to keep all the awards in a small dedicated section. Vesting Events \uf0c1 Then I have a different section that contains all the transactions that follow a vesting event. Accounts \uf0c1 First, when we vest stock, it\u2019s a taxable income event. The cash value for the stock needs an Income account: 2013-04-04 open Income:US:Hooli:RSU Taxes paid should be on the annual expenses accounts you should have defined somewhere else to account for that year\u2019s taxes (this is covered elsewhere in the cookbook): 2015-01-01 open Expenses:Taxes:TY2015:US:StateNY 2015-01-01 open Expenses:Taxes:TY2015:US:Federal 2015-01-01 open Expenses:Taxes:TY2015:US:SocSec 2015-01-01 open Expenses:Taxes:TY2015:US:SDI 2015-01-01 open Expenses:Taxes:TY2015:US:Medicare 2015-01-01 open Expenses:Taxes:TY2015:US:CityNYC After paying taxes on the received income, the remaining cash is deposited in a limbo account before getting converted: 2013-01-28 open Assets:US:Hooli:RSURefund Also, in another section we should have an account for the brokerage which holds and manages the shares for you: 2013-04-04 open Assets:US:Schwab:HOOL Generally you don\u2019t have a choice for this broker because the company you work normally makes an arrangement with an external firm in order to administer the restricted stock program. Typical firms doing this type of administration are Morgan Stanley, Salomon Smith Barney, Schwab, even E*Trade. In the example we\u2019ll use a Schwab account. And we also need some sort of checking account to receive cash in lieu of fractional shares. I\u2019ll assume a Bank of America account in the example: 2001-01-01 open Assets:US:BofA:Checking Vesting \uf0c1 First, the vesting events themselves: 2015-05-27 * \"Vesting Event - S0012345 - HOOL\" #award-S0012345 ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Income:US:Hooli:RSU -4597.95 USD Expenses:Taxes:TY2015:US:Medicare 66.68 USD Expenses:Taxes:TY2015:US:Federal 1149.48 USD Expenses:Taxes:TY2015:US:CityNYC 195.42 USD Expenses:Taxes:TY2015:US:SDI 0.00 USD Expenses:Taxes:TY2015:US:StateNY 442.32 USD Expenses:Taxes:TY2015:US:SocSec 285.08 USD Assets:US:Hooli:RSURefund 2458.97 USD This corresponds line-by-line to a payroll stub that I receive each vesting event for each award. Since all the RSU awards at Hooli vest on the same day of the month, this means that I have clusters of these, one for each award (in the example file I show two awards). Some observations are in order: The value of the Income posting consists of the number of shares vested times the FMV of the stock. In this case, that is 35 shares \u2a09 $131.37/share = $4597.95. I just write the dollar amount because it is provided to me on the pay stub. Receiving vested stock is a taxable income event, and Hooli automatically withholds taxes and pays them to the government on its employees\u2019 behalf. These are the Expenses:Taxes accounts corresponding to your W-2. Finally, I don\u2019t directly deposit the converted shares to the brokerage account; this is because the remainder of cash after paying tax expenses is not necessarily a round number of shares. Therefore, I used a limbo account ( Assets:US:Hooli:RSURefund ) and decouple the receipt from the conversion. This way, each transaction corresponds exactly to one pay stub. It makes it easier to enter the data. Also note that I used a unique link ( ^392f97dd62d0 ) to group all the transactions for a particular vesting event. You could also use tags if you prefer. Conversion to Actual Stock \uf0c1 Now we\u2019re ready to convert the remaining cash to stock units. This happens in the brokerage firm and you should see the new shares in your brokerage account. The brokerage will typically issue a \u201cstock release report\u201d statement for each vesting event for each award, with the details necessary to make the conversion, namely, the actual number of shares converted from cash and the cost basis (the FMV on the vesting day): 2015-05-25 * \"Conversion into shares\" ^392f97dd62d0 Assets:US:Schwab:HOOL 18 HOOL {131.3700 USD} Assets:US:Hooli:RSURefund Assets:US:Hooli:Unvested:S0012345 -35 HOOL.UNVEST Expenses:Hooli:Vested 35 HOOL.UNVEST The first two postings deposit the shares and subtract the dollar value from the limbo account where the vesting transaction left the remaining cash. You should make sure to use the specific FMV price provided by the statement and not an approximate price, because this has tax consequences later on. Note that you cannot buy fractional shares, so the cost of the rounded amount of shares (18) will leave some remaining cash in the limbo account. The last two postings deduct from the balance of unvested shares and I \u201creceive\u201d an expenses; that expenses account basically counts how many shares were vested over a particular time period. Here again you will probably have one of these conversions for each stock grant you have. I enter them separately, so that one statement matches one transaction. This is a good rule to follow. Refund for Fractions \uf0c1 After all the conversion events have moved cash out of the limbo account, it is left the fractional remainders from all the conversions. In my case, this remainder is refunded by Hooli 3-4 weeks after vesting as a single separate pay stub that includes all the remainders (it even lists each of them separately). I enter this as a transaction as well: 2015-06-13 * \"HOOLI INC PAYROLL\" ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Assets:US:Hooli:RSURefund -94.31 USD Assets:US:Hooli:RSURefund -2.88 USD ; (For second award in example) Assets:US:BofA:Checking 97.19 USD After the fractions have been paid the limbo account should be empty. I verify this claim using a balance assertion: 2015-06-14 balance Assets:US:Hooli:RSURefund 0 USD This provides me with some sense that the numbers are right. Organizing your Input \uf0c1 I like to put all the vesting events together in my input file; this makes them much easier to update and reconcile, especially with multiple awards. For example, with two awards I would have multiple chunks of transactions like this, separated with 4-5 empty lines to delineate them: 2015-05-27 * \"Vesting Event - S0012345 - HOOL\" #award-S0012345 ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Income:US:Hooli:RSU -4597.95 USD Assets:US:Hooli:RSURefund 2458.97 USD Expenses:Taxes:TY2015:US:Medicare 66.68 USD Expenses:Taxes:TY2015:US:Federal 1149.48 USD Expenses:Taxes:TY2015:US:CityNYC 195.42 USD Expenses:Taxes:TY2015:US:SDI 0.00 USD Expenses:Taxes:TY2015:US:StateNY 442.32 USD Expenses:Taxes:TY2015:US:SocSec 285.08 USD 2015-05-27 * \"Vesting Event - C123456 - HOOL\" #award-C123456 ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Income:US:Hooli:RSU -1970.55 USD Assets:US:Hooli:RSURefund 1053.84 USD Expenses:Taxes:TY2015:US:Medicare 28.58 USD Expenses:Taxes:TY2015:US:Federal 492.63 USD Expenses:Taxes:TY2015:US:CityNYC 83.75 USD Expenses:Taxes:TY2015:US:SDI 0.00 USD Expenses:Taxes:TY2015:US:StateNY 189.57 USD Expenses:Taxes:TY2015:US:SocSec 122.18 USD 2015-05-25 * \"Conversion into shares\" ^392f97dd62d0 Assets:US:Schwab:HOOL 18 HOOL {131.3700 USD} Assets:US:Hooli:RSURefund Assets:US:Hooli:Unvested:S0012345 -35 HOOL.UNVEST Expenses:Hooli:Vested 35 HOOL.UNVEST 2015-05-25 * \"Conversion into shares\" ^392f97dd62d0 Assets:US:Schwab:HOOL 9 HOOL {131.3700 USD} Assets:US:Hooli:RSURefund Assets:US:Hooli:Unvested:C123456 -15 HOOL.UNVEST Expenses:Hooli:Vested 15 HOOL.UNVEST 2015-06-13 * \"HOOLI INC PAYROLL\" ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Assets:US:Hooli:RSURefund -94.30 USD Assets:US:Hooli:RSURefund -2.88 USD Assets:US:BofA:Checking 97.18 USD 2015-02-14 balance Assets:US:Hooli:RSURefund 0 USD Unvested Shares \uf0c1 Asserting Unvested Balances \uf0c1 Finally, you may occasionally want to assert the number of unvested shares. I like to do this semi-annually, for example. The brokerage company that handles the RSUs for Hooli should be able to list how many unvested shares of each award remain, so it\u2019s as simple as looking it up on a website: 2015-06-04 balance Assets:US:Hooli:Unvested:S0012345 1645 HOOL.UNVEST 2015-06-04 balance Assets:US:Hooli:Unvested:C123456 705 HOOL.UNVEST Pricing Unvested Shares \uf0c1 You can also put a price on the unvested shares in order to estimate the unvested dollar amount. You should use a fictional currency for this, because we want to avoid a situation where a balance sheet is produced that includes these unvested assets as regular dollars: 2015-06-02 price HOOL.UNVEST 132.4300 USD.UNVEST At the time of this writing, the bean-web interface does not convert the units if they are not held at cost, but using the SQL query interface or writing a custom script you should be able to produce those numbers: $ bean-query examples/vesting/vesting.beancount beancount> select account, sum(convert(position, 'USD.UNVEST')) as unvested where account ~ 'Unvested' group by account; account unvested --------------------------------- ---------------------- Assets:US:Hooli:Unvested:S0012345 217847.3500 USD.UNVEST Assets:US:Hooli:Unvested:C123456 93363.1500 USD.UNVEST Selling Vested Stock \uf0c1 After each vesting event, the stock is left in your brokerage account. Selling this stock proceeds just as in any other trading transaction (see Trading with Beancount for full details). For example, selling the shares from the example would look something like this: 2015-09-10 * \"Selling shares\" Assets:US:Schwab:HOOL -26 HOOL {131.3700 USD} @ 138.23 USD Assets:US:Schwab:Cash 3593.98 USD Income:US:Schwab:Gains Here you can see why it matters that the cost basis you used on the conversion event is the correct one: You will have to pay taxes on the difference (in Income:US:Schwab:Gains ). In this example the taxable difference is (138.23 - 131.37) dollars per share. I like to keep all the brokerage transactions in a separate section of my document, where other transactions related to the brokerage occur, such as fees, dividends and transfers. Conclusion \uf0c1 This is a simple example that is modeled after how technology companies deal with this type of compensation. It is by no means comprehensive, and some of the details will necessarily vary in your situation. In particular, it does not explain how to deal with options (ISOs). My hope is that there is enough meat in this document to allow you to extrapolate and adapt to your particular situation. If you get stuck, please reach out on the mailing-list .","title":"Stock Vesting in Beancount"},{"location":"stock_vesting_in_beancount.html#stock-vesting-in-beancount","text":"Martin Blais , June 2015 http://furius.ca/beancount/doc/vesting","title":"Stock Vesting in Beancount"},{"location":"stock_vesting_in_beancount.html#introduction","text":"This document explains the vesting of restricted stock units in Beancount, by way of an example. This example may not exactly match your situation, but enough detail is provided that you should be able to adapt it for your own particular differences. A working example file can be found here to follow along with this text.","title":"Introduction"},{"location":"stock_vesting_in_beancount.html#restricted-stock-compensation","text":"Many technology companies offer their employees incentive compensation in the form of \u201cgrants\u201d (or \u201cawards\u201d) of \u201crestricted stock units\u201d (RSU), which is essentially a promise for the \u201crelease\u201d to you of actual shares in the future. The stock is \u201crestricted\u201d in the sense that you cannot access it\u2014you only receive it when it \u201cvests\u201d, and this happens based on a schedule. Typically, you are promised a fixed number of shares that vest every quarter or every month over a period of 3 or 4 years. If you leave the company, your remaining unvested shares are lost. One way you can view these RSUs is as an asset, a receivable that arrives regularly over time. These RSUs are essentially compensation denominated in the currency of the company\u2019s shares itself. We want to track the unraveling of these unvested units, and correctly account for their conversion to real stock with a cost basis and including whatever taxes were paid upon vesting.","title":"Restricted Stock Compensation"},{"location":"stock_vesting_in_beancount.html#tracking-awards","text":"","title":"Tracking Awards"},{"location":"stock_vesting_in_beancount.html#commodities","text":"First we want to define some commodities. In this example, I work for \u201cHooli Inc.\u201d and will eventually receive shares of that company (valued in US dollars): 1990-12-02 commodity HOOL name: \"Common shares of Hooli Inc.\" quote: USD We will also want to track the amount of unvested shares: 2013-01-28 commodity HOOL.UNVEST name: \"Unvested shares of Hooli from awards.\"","title":"Commodities"},{"location":"stock_vesting_in_beancount.html#accounts-for-awards","text":"Grants received is income. I use \u201cIncome:US:Hooli\u201d as the root for all income accounts from Hooli, but in particular, I define an account for the awards, which contains units of unvested stock: 2013-01-28 open Income:US:Hooli:Awards HOOL.UNVEST When the stock vests, we will need to book the other side of this income somewhere, so we define an expenses account to count how much stock has been vested over a period of time: 2014-01-28 open Expenses:Hooli:Vested HOOL.UNVEST","title":"Accounts for Awards"},{"location":"stock_vesting_in_beancount.html#receiving-awards","text":"When you receive a new award (this may occur every year, for example, some people call this a \u201cstock refresh\u201d), you receive it as income and deposit it into a fresh new account, used to track this particular award: 2014-04-02 * \"Award S0012345\" Income:US:Hooli:Awards -1680 HOOL.UNVEST Assets:US:Hooli:Unvested:S0012345 1680 HOOL.UNVEST 2014-04-02 open Assets:US:Hooli:Unvested:S0012345 You may have multiple active awards at the same time. It\u2019s nice to have a separate account per award, as it offers a natural way to list their contents and when the award expires, you can close the account\u2014the list of open award accounts gives you the list of outstanding & actively vesting awards. In this example I used the number of the award (#S0012345) as the sub-account name. It\u2019s useful to use the number as the statements typically include it. I like to keep all the awards in a small dedicated section.","title":"Receiving Awards"},{"location":"stock_vesting_in_beancount.html#vesting-events","text":"Then I have a different section that contains all the transactions that follow a vesting event.","title":"Vesting Events"},{"location":"stock_vesting_in_beancount.html#accounts","text":"First, when we vest stock, it\u2019s a taxable income event. The cash value for the stock needs an Income account: 2013-04-04 open Income:US:Hooli:RSU Taxes paid should be on the annual expenses accounts you should have defined somewhere else to account for that year\u2019s taxes (this is covered elsewhere in the cookbook): 2015-01-01 open Expenses:Taxes:TY2015:US:StateNY 2015-01-01 open Expenses:Taxes:TY2015:US:Federal 2015-01-01 open Expenses:Taxes:TY2015:US:SocSec 2015-01-01 open Expenses:Taxes:TY2015:US:SDI 2015-01-01 open Expenses:Taxes:TY2015:US:Medicare 2015-01-01 open Expenses:Taxes:TY2015:US:CityNYC After paying taxes on the received income, the remaining cash is deposited in a limbo account before getting converted: 2013-01-28 open Assets:US:Hooli:RSURefund Also, in another section we should have an account for the brokerage which holds and manages the shares for you: 2013-04-04 open Assets:US:Schwab:HOOL Generally you don\u2019t have a choice for this broker because the company you work normally makes an arrangement with an external firm in order to administer the restricted stock program. Typical firms doing this type of administration are Morgan Stanley, Salomon Smith Barney, Schwab, even E*Trade. In the example we\u2019ll use a Schwab account. And we also need some sort of checking account to receive cash in lieu of fractional shares. I\u2019ll assume a Bank of America account in the example: 2001-01-01 open Assets:US:BofA:Checking","title":"Accounts"},{"location":"stock_vesting_in_beancount.html#vesting","text":"First, the vesting events themselves: 2015-05-27 * \"Vesting Event - S0012345 - HOOL\" #award-S0012345 ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Income:US:Hooli:RSU -4597.95 USD Expenses:Taxes:TY2015:US:Medicare 66.68 USD Expenses:Taxes:TY2015:US:Federal 1149.48 USD Expenses:Taxes:TY2015:US:CityNYC 195.42 USD Expenses:Taxes:TY2015:US:SDI 0.00 USD Expenses:Taxes:TY2015:US:StateNY 442.32 USD Expenses:Taxes:TY2015:US:SocSec 285.08 USD Assets:US:Hooli:RSURefund 2458.97 USD This corresponds line-by-line to a payroll stub that I receive each vesting event for each award. Since all the RSU awards at Hooli vest on the same day of the month, this means that I have clusters of these, one for each award (in the example file I show two awards). Some observations are in order: The value of the Income posting consists of the number of shares vested times the FMV of the stock. In this case, that is 35 shares \u2a09 $131.37/share = $4597.95. I just write the dollar amount because it is provided to me on the pay stub. Receiving vested stock is a taxable income event, and Hooli automatically withholds taxes and pays them to the government on its employees\u2019 behalf. These are the Expenses:Taxes accounts corresponding to your W-2. Finally, I don\u2019t directly deposit the converted shares to the brokerage account; this is because the remainder of cash after paying tax expenses is not necessarily a round number of shares. Therefore, I used a limbo account ( Assets:US:Hooli:RSURefund ) and decouple the receipt from the conversion. This way, each transaction corresponds exactly to one pay stub. It makes it easier to enter the data. Also note that I used a unique link ( ^392f97dd62d0 ) to group all the transactions for a particular vesting event. You could also use tags if you prefer.","title":"Vesting"},{"location":"stock_vesting_in_beancount.html#conversion-to-actual-stock","text":"Now we\u2019re ready to convert the remaining cash to stock units. This happens in the brokerage firm and you should see the new shares in your brokerage account. The brokerage will typically issue a \u201cstock release report\u201d statement for each vesting event for each award, with the details necessary to make the conversion, namely, the actual number of shares converted from cash and the cost basis (the FMV on the vesting day): 2015-05-25 * \"Conversion into shares\" ^392f97dd62d0 Assets:US:Schwab:HOOL 18 HOOL {131.3700 USD} Assets:US:Hooli:RSURefund Assets:US:Hooli:Unvested:S0012345 -35 HOOL.UNVEST Expenses:Hooli:Vested 35 HOOL.UNVEST The first two postings deposit the shares and subtract the dollar value from the limbo account where the vesting transaction left the remaining cash. You should make sure to use the specific FMV price provided by the statement and not an approximate price, because this has tax consequences later on. Note that you cannot buy fractional shares, so the cost of the rounded amount of shares (18) will leave some remaining cash in the limbo account. The last two postings deduct from the balance of unvested shares and I \u201creceive\u201d an expenses; that expenses account basically counts how many shares were vested over a particular time period. Here again you will probably have one of these conversions for each stock grant you have. I enter them separately, so that one statement matches one transaction. This is a good rule to follow.","title":"Conversion to Actual Stock"},{"location":"stock_vesting_in_beancount.html#refund-for-fractions","text":"After all the conversion events have moved cash out of the limbo account, it is left the fractional remainders from all the conversions. In my case, this remainder is refunded by Hooli 3-4 weeks after vesting as a single separate pay stub that includes all the remainders (it even lists each of them separately). I enter this as a transaction as well: 2015-06-13 * \"HOOLI INC PAYROLL\" ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Assets:US:Hooli:RSURefund -94.31 USD Assets:US:Hooli:RSURefund -2.88 USD ; (For second award in example) Assets:US:BofA:Checking 97.19 USD After the fractions have been paid the limbo account should be empty. I verify this claim using a balance assertion: 2015-06-14 balance Assets:US:Hooli:RSURefund 0 USD This provides me with some sense that the numbers are right.","title":"Refund for Fractions"},{"location":"stock_vesting_in_beancount.html#organizing-your-input","text":"I like to put all the vesting events together in my input file; this makes them much easier to update and reconcile, especially with multiple awards. For example, with two awards I would have multiple chunks of transactions like this, separated with 4-5 empty lines to delineate them: 2015-05-27 * \"Vesting Event - S0012345 - HOOL\" #award-S0012345 ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Income:US:Hooli:RSU -4597.95 USD Assets:US:Hooli:RSURefund 2458.97 USD Expenses:Taxes:TY2015:US:Medicare 66.68 USD Expenses:Taxes:TY2015:US:Federal 1149.48 USD Expenses:Taxes:TY2015:US:CityNYC 195.42 USD Expenses:Taxes:TY2015:US:SDI 0.00 USD Expenses:Taxes:TY2015:US:StateNY 442.32 USD Expenses:Taxes:TY2015:US:SocSec 285.08 USD 2015-05-27 * \"Vesting Event - C123456 - HOOL\" #award-C123456 ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Income:US:Hooli:RSU -1970.55 USD Assets:US:Hooli:RSURefund 1053.84 USD Expenses:Taxes:TY2015:US:Medicare 28.58 USD Expenses:Taxes:TY2015:US:Federal 492.63 USD Expenses:Taxes:TY2015:US:CityNYC 83.75 USD Expenses:Taxes:TY2015:US:SDI 0.00 USD Expenses:Taxes:TY2015:US:StateNY 189.57 USD Expenses:Taxes:TY2015:US:SocSec 122.18 USD 2015-05-25 * \"Conversion into shares\" ^392f97dd62d0 Assets:US:Schwab:HOOL 18 HOOL {131.3700 USD} Assets:US:Hooli:RSURefund Assets:US:Hooli:Unvested:S0012345 -35 HOOL.UNVEST Expenses:Hooli:Vested 35 HOOL.UNVEST 2015-05-25 * \"Conversion into shares\" ^392f97dd62d0 Assets:US:Schwab:HOOL 9 HOOL {131.3700 USD} Assets:US:Hooli:RSURefund Assets:US:Hooli:Unvested:C123456 -15 HOOL.UNVEST Expenses:Hooli:Vested 15 HOOL.UNVEST 2015-06-13 * \"HOOLI INC PAYROLL\" ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Assets:US:Hooli:RSURefund -94.30 USD Assets:US:Hooli:RSURefund -2.88 USD Assets:US:BofA:Checking 97.18 USD 2015-02-14 balance Assets:US:Hooli:RSURefund 0 USD","title":"Organizing your Input"},{"location":"stock_vesting_in_beancount.html#unvested-shares","text":"","title":"Unvested Shares"},{"location":"stock_vesting_in_beancount.html#asserting-unvested-balances","text":"Finally, you may occasionally want to assert the number of unvested shares. I like to do this semi-annually, for example. The brokerage company that handles the RSUs for Hooli should be able to list how many unvested shares of each award remain, so it\u2019s as simple as looking it up on a website: 2015-06-04 balance Assets:US:Hooli:Unvested:S0012345 1645 HOOL.UNVEST 2015-06-04 balance Assets:US:Hooli:Unvested:C123456 705 HOOL.UNVEST","title":"Asserting Unvested Balances"},{"location":"stock_vesting_in_beancount.html#pricing-unvested-shares","text":"You can also put a price on the unvested shares in order to estimate the unvested dollar amount. You should use a fictional currency for this, because we want to avoid a situation where a balance sheet is produced that includes these unvested assets as regular dollars: 2015-06-02 price HOOL.UNVEST 132.4300 USD.UNVEST At the time of this writing, the bean-web interface does not convert the units if they are not held at cost, but using the SQL query interface or writing a custom script you should be able to produce those numbers: $ bean-query examples/vesting/vesting.beancount beancount> select account, sum(convert(position, 'USD.UNVEST')) as unvested where account ~ 'Unvested' group by account; account unvested --------------------------------- ---------------------- Assets:US:Hooli:Unvested:S0012345 217847.3500 USD.UNVEST Assets:US:Hooli:Unvested:C123456 93363.1500 USD.UNVEST","title":"Pricing Unvested Shares"},{"location":"stock_vesting_in_beancount.html#selling-vested-stock","text":"After each vesting event, the stock is left in your brokerage account. Selling this stock proceeds just as in any other trading transaction (see Trading with Beancount for full details). For example, selling the shares from the example would look something like this: 2015-09-10 * \"Selling shares\" Assets:US:Schwab:HOOL -26 HOOL {131.3700 USD} @ 138.23 USD Assets:US:Schwab:Cash 3593.98 USD Income:US:Schwab:Gains Here you can see why it matters that the cost basis you used on the conversion event is the correct one: You will have to pay taxes on the difference (in Income:US:Schwab:Gains ). In this example the taxable difference is (138.23 - 131.37) dollars per share. I like to keep all the brokerage transactions in a separate section of my document, where other transactions related to the brokerage occur, such as fees, dividends and transfers.","title":"Selling Vested Stock"},{"location":"stock_vesting_in_beancount.html#conclusion","text":"This is a simple example that is modeled after how technology companies deal with this type of compensation. It is by no means comprehensive, and some of the details will necessarily vary in your situation. In particular, it does not explain how to deal with options (ISOs). My hope is that there is enough meat in this document to allow you to extrapolate and adapt to your particular situation. If you get stuck, please reach out on the mailing-list .","title":"Conclusion"},{"location":"the_double_entry_counting_method.html","text":"The Double-Entry Counting Method \uf0c1 Martin Blais, December 2016 http://furius.ca/beancount/doc/double-entry Introduction Basics of Double-Entry Bookkeeping Statements Single-Entry Bookkeeping Double-Entry Bookkeeping Many Accounts Multiple Postings Types of Accounts Trial Balance Income Statement Clearing Income Equity Balance Sheet Summarizing Period Reporting Chart of Accounts Country-Institution Convention Credits & Debits Accounting Equations Plain-Text Accounting The Table Perspective Introduction \uf0c1 This document is a gentle introduction to the double-entry counting method, as written from the perspective of a computer scientist. It is an attempt to explain basic bookkeeping using as simple an approach as possible, doing away with some of the idiosyncrasies normally involved in accounting. It is also representative of how Beancount works, and it should be useful to all users of plain-text accounting . Note that I am not an accountant, and in the process of writing this document I may have used terminology that is slightly different or unusual to that which is taught in perhaps more traditional training in accounting. I granted myself license to create something new and perhaps even unusual in order to explain those ideas as simply and clearly as possible to someone unfamiliar with them. I believe that the method of double-entry counting should be taught to everyone at the high school level everywhere as it is a tremendously useful organizational skill, and I hope that this text can help spread its knowledge beyond professional circles. Basics of Double-Entry Bookkeeping \uf0c1 The double-entry system is just a simple method of counting , with some simple rules. Let\u2019s begin by defining the notion of an account . An account is something that can contain things, like a bag. It is used to count things, to accumulate things. Let\u2019s draw a horizontal arrow to visually represent the evolving contents of an account over time: On the left, we have the past, and to the right, increasing time: the present, the future, etc. For now, let\u2019s assume that accounts can contain only one kind of thing, for example, dollars . All accounts begin with an empty content of zero dollars. We will call the number of units in the account the balance of an account. Note that it represents its contents at a particular point in time. I will draw the balance using a number above the account\u2019s timeline: The contents of accounts can change over time. In order to change the content of an account, we have to add something to it. We will call this addition a posting to an account, and I will draw this change as a circled number on the account\u2019s timeline, for example, adding $100 to the account: Now, we can draw the updated balance of the account after the posting with another little number right after it: The account\u2019s balance, after adding $100, is now $100. We can also remove from the contents of an account. For example, we could remove $25, and the resulting account balance is now $75: Account balances can also become negative , if we remove more dollars than there are in the account. For example, if we remove $200 from this account, the balance now becomes $-125: It\u2019s perfectly fine for accounts to contain a negative balance number. Remember that all we\u2019re doing is counting things. As we will see shortly, some accounts will remain with a negative balance for most of their timeline. Statements \uf0c1 Something worthy of notice is how the timeline notation I\u2019ve written in the previous section is analogous to paper account statements institutions maintain for each client and which you typically receive through the mail: Date Description Amount Balance 2016-10-02 ... 100.00 1100.00 2016-10-05 ... -25.00 1075.00 2016-10-06 ... -200.00 875.00 Final Balance 875.00 Sometimes the amount column is split into two, one showing the positive amounts and the other the negative ones: Date Description Debit Credit Balance 2016-10-02 ... 100.00 1100.00 2016-10-05 ... 25.00 1075.00 2016-10-06 ... 200.00 875.00 Final Balance 875.00 Here, \u201cdebit\u201d means \u201cremoved from your account\u201d and \u201ccredit\u201d means \u201cdeposited in your account.\u201d Sometimes the words \u201cwithdrawals\u201d and \u201cdeposits\u201d will be used. It all depends on context: for checking and savings accounts it is usual to have both types of postings, but for a credit card account typically it shows only positive numbers and then the occasional monthly payment so the single column format is used. In any case, the \u201cbalance\u201d column always shows the resulting balance after the amount has been posted to the account. And sometimes the statements are rendered in decreasing order of time. Single-Entry Bookkeeping \uf0c1 In this story, this account belongs to someone. We\u2019ll call this person the owner of the account. The account can be used to represent a real world account, for example, imagine that we use it to represent the content of the owner\u2019s checking account at a bank. So we\u2019re going to label the account by giving it a name, in this case \u201cChecking\u201d: Imagine that at some point, this account has a balance of $1000, like I\u2019ve drawn on the picture. Now, if the owner spends $79 of this account, we would represent it like this: Furthermore, if the expense was for a meal at a restaurant, we could flag the posting with a category to indicate what the change was used for. Let\u2019s say, \u201cRestaurant\u201d, like this: Now, if we have a lot of these, we could write a computer program to accumulate all the changes for each category and calculate the sums for each of them. That would tell us how much we spent in restaurants in total, for example. This is called the single-entry method of accounting. But we\u2019re not going to do it this way; we have a better way. Bear with me for a few more sections. Double-Entry Bookkeeping \uf0c1 An owner may have multiple accounts. I will represent this by drawing many similar account timelines on the same graphic. As before, these are labeled with unique names. Let\u2019s assume that the owner has the same \u201cChecking\u201d account as previously, but now also a \u201c Restaurant \u201d account as well, which can be used to accumulate all food expenses at restaurants. It looks like this: Now, instead of categorizing the posting to a \u201crestaurant category\u201d as we did previously, we could create a matching posting on the \u201cRestaurant\u201d account to record how much we spent for food, with the amount spent ($79): The \u201cRestaurant\u201d account, like all other accounts, also has an accumulated balance, so we can find out how much we spent in \u201cRestaurant\u201d in total. This is entirely symmetrical to counting changes in a checking account. Now, we can associate the two postings together, by creating a kind of \u201cparent\u201d box that refers to both of them. We will call this object a transaction : Notice here that we\u2019ve also associated a description to this transaction: \u201cDinner at Uncle Boons\u201d. A transaction also has a date , and all of its postings are recorded to occur on that date. We call this the transaction date. We can now introduce the fundamental rule of double-entry bookkeeping system: The sum of all the postings of a transaction must equal zero. Remember this, as this is the foundation of the double-entry method, and its most important characteristic. It has important consequences which I will discuss later in this document. In our example, we remove $79 from the \u201cChecking\u201d account and \u201cgive it\u201d to the \u201cRestaurant\u201d account. ($79) + ($-79) = $0. To emphasize this, I could draw a little summation line under the postings of the transaction, like this: Many Accounts \uf0c1 There may be many such transactions, over many different accounts. For example, if the owner of the accounts had a lunch the next day which she paid using a credit card, it could be represented by creating a \u201cCredit Card\u201d account dedicated to tracking the real world credit card balance, and with a corresponding transaction: In this example, the owner spent $35 at a restaurant called \u201cEataly.\u201d The previous balance of the owner\u2019s credit card was $-450; after the expense, the new balance is $-485. For each real world account, the owner can create a bookkeeping account like we did. Also, for each category of expenditure, the owner also creates a bookkeeping account. In this system, there are no limits to how many accounts can be created. Note that the balance in the example is a negative number; this is not an error. Balances for credit card accounts are normally negative: they represent an amount you owe , that the bank is lending you on credit . When your credit card company keeps track of your expenses, they write out your statement from their perspective, as positive numbers. For you, those are amounts you need to eventually pay. But here, in our accounting system, we\u2019re representing numbers from the owner\u2019s point-of-view, and from her perspective, this is money she owes, not something she has. What we have is a meal sitting in our stomach (a positive number of $ of \u201cRestaurant\u201d). Multiple Postings \uf0c1 Finally, transactions may have more than two postings; in fact, they may have any number of postings. The only thing that matters is that the sum of their amounts is zero (from the rule of double-entry bookkeeping above). For example, let\u2019s look at what would happen if the owner gets her salary paid for December: Her gross salary received in this example is recorded as $-2,905 (I\u2019ll explain the sign in a moment). $905 is set aside for taxes. Her \u201cnet\u201d salary of $2,000, the remainder, is deposited in her \u201cChecking\u201d account and the resulting balance of that account is $2,921 (the previous balance of $921 + $2,000 = $2,921). This transaction has three postings: (+2,000) + (-2,905) + (+905) = 0. The double-entry rule is respected. Now, you may ask: Why is her salary recorded as a negative number? The reasoning here is similar to that of the credit card above, though perhaps a bit more subtle. These accounts exist to track all the amounts from the owner\u2019s point-of-view. The owner gives out work, and receives money and taxes in exchange for it (positive amounts). The work given away is denominated in dollar units. It \u201cleaves\u201d the owner (imagine that the owner has potential work stored in her pocket and as she goes into work every day sprinkles that work potential giving it to the company). The owner gave $2,905\u2019s worth of work away. We want to track how much work was given, and it\u2019s done with the \u201cSalary\u201d account. That\u2019s her gross salary. Note also that we\u2019ve simplified this paycheck transaction a bit, for the sake of keeping things simple. A more realistic recording of one\u2019s pay stub would have many more accounts; we would separately account for state and federal tax amounts, as well as social security and medicare payments, deductions, insurance paid through work, and vacation time accrued during the period. But it wouldn\u2019t be much more complicated: the owner would simply translate all the amounts available from her pay stub into a single transaction with more postings. The structure remains similar. Types of Accounts \uf0c1 Let\u2019s now turn our attention to the different types of accounts an owner can have. Balance or Delta. First, the most important distinction between accounts is about whether we care about the balance at a particular point in time, or whether it only makes sense to care about differences over a period of time. For example, the balance of someone\u2019s Checking or Savings accounts is a meaningful number that both the owner and its corresponding bank will care about. Similarly, the total amount owed on someone\u2019s Credit Card account is also meaningful. The same goes with someone\u2019s remaining Mortgage amount to pay on a house. On the other hand, the total amount of Restaurant expenses since the beginning of somebody\u2019s life on earth is not particularly interesting. What we might care about for this account is the amount of Restaurant expenses incurred over a particular period of time . For example, \u201chow much did you spend in restaurants last month?\u201d Or last quarter. Or last year. Similarly, the total amount of gross salary since the beginning of someone\u2019s employment at a company a few years ago is not very important. But we would care about the total amount earned during a tax year, that is, for that time period, because it is used for reporting one\u2019s income to the tax man. Accounts whose balance at a point in time is meaningful are called balance sheet accounts . There are two types of such accounts: \u201c Assets \u201d and \u201c Liabilities .\u201d The other accounts, that is, those whose balance is not particularly meaningful but for which we are interested in calculating changes over a period of time are called income statement accounts . Again, there are two kinds: \u201c Income \u201d and \u201c Expenses .\u201d Normal sign. Secondly, we consider the usual sign of an account\u2019s balance . The great majority of accounts in the double-entry system tend to have a balance with always a positive sign, or always a negative sign (though as we\u2019ve seen previously, it is not impossible that an account\u2019s balance could change signs). This is how we will distinguish between the pairs of accounts mentioned before: For a balance sheet account, Assets normally have positive balances, and Liabilities normally have negative balances. For income statement accounts, Expenses normally have a positive balance, and Income accounts normally have a negative balance. This situation is summarized in the following table: Balance: Positive (+) Balance: Negative (-) Balance matters at a point in time (Balance Sheet) Assets Liabilities Change in balance matters over a period of time (Income Statement) Expenses Income Let\u2019s discuss each type of account and provide some examples, so that it doesn\u2019t remain too abstract. Assets. (+) Asset accounts represent something the owner has . A canonical example is banking accounts. Another one is a \u201ccash\u201d account, which counts how much money is in your wallet. Investments are also assets (their units aren\u2019t dollars in this case, but rather some number of shares of some mutual fund or stock). Finally, if you own a home, the home itself is considered an asset (and its market value fluctuates over time). Liabilities. (-) A liability account represents something the owner owes . The most common example is a credit card. Again, the statement provided by your bank will show positive numbers, but from your own perspective, they are negative numbers. A loan is also a liability account. For example, if you take out a mortgage on a home, this is money you owe, and will be tracked by an account with a negative amount. As you pay off the mortgage every month the negative number goes up, that is, its absolute value gets smaller and smaller over time (e.g., -120,000 -> -117,345). Expenses. (+) An expense account represents something you\u2019ve received , perhaps by exchanging something else to purchase it. This type of account will seem pretty natural: food, drinks, clothing, rent, flights, hotels and most other categories of things you typically spend your disposable income on. However, taxes are also typically tracked by an expense account: when you receive some salary income, the amount of taxes withheld at the source is recorded immediately as an expense. Think of it as paying for government services you receive throughout the year. Income. (-) An income account is used to count something you\u2019ve given away in order to receive something else (typically assets or expenses). For most people with jobs, that is the value of their time (a salary income). Specifically, here we\u2019re talking about the gross income. For example, if you\u2019re earning a salary of $120,000/year, that number is $120,000, not whatever amount remains after paying for taxes. Other types of income includes dividends received from investments, or interest paid from bonds held. There are also a number of oddball things received you might record as income, such the value of rewards received, e.g., cash back from a credit card, or monetary gifts from someone. In Beancount, all account names, without exception, must be associated to one of the types of accounts described previously. Since the type of an account never changes during its lifetime, we will make its type a part of an account\u2019s name, as a prefix , by convention. For example, the qualified account name for restaurant will be \u201cExpenses:Restaurant\u201d. For the bank checking account, the qualified account name will be \u201cAssets:Checking\u201d. Other than that, you can select any name you like for your accounts. You can create as many accounts as you like, and as we will see later, you can organize them in a hierarchy. As of the writing of this document, I\u2019m using more than 700 accounts to track my personal affairs. Let us now revisit our example and add some more accounts: And let\u2019s imagine there are more transactions: \u2026 and even more of them: Finally, we can label each of those accounts with one of the four types of accounts by prepending the type to their account names: A realistic book from someone tracking all of their personal affairs might easily contain thousands of transactions per year. But the principles remain simple and they remain the same: postings are applied to accounts over time, and must be parented to a transaction, and within this transaction the sum of all the postings is zero. When you do bookkeeping for a set of accounts, you are essentially describing all the postings that happen on all the accounts over time, subject to the constraint of the rule. You are creating a database of those postings in a book . You are \u201ckeeping the book,\u201d that is, traditionally, the book which contains all those transactions. Some people call this \u201cmaintaining a journal.\u201d We will now turn our attention to obtaining useful information from this data, summarizing information from the book. Trial Balance \uf0c1 Take our last example: we can easily reorder all the accounts such that all the Asset accounts appear together at the top, then all the Liabilities accounts, then Income, and finally Expenses accounts. We are simply changing the order without modifying the structure of transactions, in order to group each type of accounts together: We\u2019ve reordered the accounts with Assets accounts grouped at the top, then Liabilities, then some Equity accounts (which we have just introduced, more about them is discussed later), then Income and finally Expenses at the bottom. If we sum up the postings on all of the accounts and render just the account name and its final balance on the right, we obtain a report we call the \u201ctrial balance.\u201d This simply reflects the balance of each account at a particular point in time. And because each of the accounts began with a zero balance, and each transaction has itself a zero balance, we know that the sum of all those balances must equal zero. 1 This is a consequence of our constraining that each of the postings be part of a transaction, and that each transaction have postings that balance each other out. Income Statement \uf0c1 One kind of common information that is useful to extract from the list of transactions is a summary of changes in income statement accounts during a particular period of time. This tells us how much money was earned and spent during this period, and the difference tells us how much profit (or loss) was incurred. We call this the \u201cnet income.\u201d In order to generate this summary, we simply turn our attention to the balances of the accounts of types Income and Expenses, summing up just the transactions for a particular period, and we draw the Income balances on the left, and Expenses balances on the right: It is important to take note of the signs here: Income numbers are negative, and Expenses numbers are positive. So if you earned more than you spent (a good outcome), the final sum of Income + Expenses balances will be a negative number. Like any other income, a net income that has a negative number means that there is a corresponding amount of Assets and/or Liabilities with positive numbers (this is good for you). An Income Statement tells us what changed during a particular period of time. Companies typically report this information quarterly to investors and perhaps the public (if they are a publicly traded company) in order to share how much profit they were able to make. Individuals typically report this information on their annual tax returns. Clearing Income \uf0c1 Notice how in the income statement only the transactions within a particular interval of time are summed up. This allows one, for instance, to compute the sum of all income earned during a year. If we were to sum up all of the transactions of this account since its inception we would obtain the total amount of income earned since the account was created. A better way to achieve the same thing is to zero out the balances of the Income and Expenses accounts. Beancount calls this basic transformation \u201cclearing 2 .\u201d It is carried out by: Computing the balances of those accounts from the beginning of time to the start of the reporting period. For example, if you created your accounts in year 2000 and you wanted to generate an income statement for year 2016, you would sum up the balances from 2000 to Jan 1, 2016. Inserting transactions to empty those balances and transfer them to some other account that isn\u2019t Income nor Expenses. For instance, if the restaurant expense account for those 16 years amounts to $85,321 on Jan 1, 2016, it would insert a transaction of $-85,321 to restaurants and $+85,321 to \u201cprevious earnings\u201d. The transactions would be dated Jan 1, 2016. Including this transaction, the sum of that account would zero on that date. This is what we want. Those transactions inserted for all income statement accounts are pictured in green below. Now summing the entire set of transactions through the end of the ledger would yield only the changes during year 2016 because the balances were zero on that date: This is the semantics of the \u201cCLEAR\u201d operation of the bean-query shell. (Note that another way to achieve the same thing for income statement accounts would be to segregate and count amounts only for the transactions after the clearing date; however, jointly reporting on income statement accounts and balance sheet accounts would have incorrect balances for the balance sheet accounts.) Equity \uf0c1 The account that receives those previously accumulated incomes is called \u201cPrevious Earnings\u201d. It lives in a fifth and final type of accounts: Equity . We did not talk about this type of accounts earlier because they are most often only used to transfer amounts to build up reports, and the owner usually doesn\u2019t post changes to those types of accounts; the software does that automatically, e.g., when clearing net income. The account type \u201cequity\u201d is used for accounts that hold a summary of the net income implied by all the past activity. The point is that if we now list together the Assets, Liabilities and Equity accounts, because the Income and Expenses accounts have been zero\u2019ed out, the sum total of all these balances should equal exactly zero. And summing up all the Equity accounts clearly tells us what\u2019s our stake in the entity, in other words, if you used the assets to pay off all the liabilities, how much is left in the business\u2026 how much it\u2019s worth. Note that the normal sign of the Equity accounts is negative . There is no particular meaning to that, just that they are used to counterbalance Assets and Liabilities and if the owner has any value, that number should be negative. (A negative Equity means some positive net worth.) There are a few different Equity accounts in use in Beancount: Previous Earnings or Retained Earnings. An account used to hold the sum total of Income & Expenses balances from the beginning of time until the beginning of a reporting period. This is the account we were referring to in the previous section. Current Earnings , also called Net Income. An account used to contain the sum of Income & Expenses balances incurred during the reporting period. They are filled in by \u201cclearing\u201d the Income & Expenses accounts at the end of the reporting period. Opening Balances. An equity account used to counterbalance deposits used to initialize accounts. This type of account is used when we truncate the past history of transactions, but we also need to ensure that an account\u2019s balance begins its history with a particular amount. Once again: you don\u2019t need to define nor use these accounts yourself, as these are created for the purpose of summarizing transactions. Generally, the accounts are filled in by the clearing process described above, or filled in by Pad directives to \u201copening balances\u201d equity accounts, to account for summarized balances from the past. They are created and filled in automatically by the software. We\u2019ll see how these get used in the following sections. Balance Sheet \uf0c1 Another kind of summary is a listing of the owner\u2019s assets and debts, for each of the accounts. This answers the question: \u201c Where\u2019s the money? \u201d In theory, we could just restrict our focus to the Assets and Liabilities accounts and draw those up in a report: However, in practice, there is another closely related question that comes up and which is usually answered at the same time: \u201c Once all debts are paid off, how much are we left with? \u201d This is called the net worth . If the Income & Expenses accounts have been cleared to zero and all their balances have been transferred to Equity accounts, the net worth should be equal to the sum of all the Equity accounts. So in building up the Balance Sheet, it it customary to clear the net income and then display the balances of the Equity accounts. The report looks like this: Note that the balance sheet can be drawn for any point in time , simply by truncating the list of transactions following a particular date. A balance sheet displays a snapshot of balances at one date; an income statement displays the difference of those balances between two dates. Summarizing \uf0c1 It is useful to summarize a history of past transactions into a single equivalent deposit. For example, if we\u2019re interested in transactions for year 2016 for an account which has a balance of $450 on Jan 1, 2016, we can delete all the previous transactions and replace them with a single one that deposits $450 on Dec 31, 2015 and that takes it from somewhere else. That somewhere else will be the Equity account Opening Balances . First, we can do this for all Assets and Liabilities accounts (see transactions in blue): Then we delete all the transactions that precede the opening date, to obtain a truncated list of transactions: This is a useful operation when we\u2019re focused on the transactions for a particular interval of time. (This is a bit of an implementation detail: these operations are related to how Beancount is designed. Instead of making all the reporting operations with parameters, all of its reporting routines are simplified and instead operate on the entire stream of transactions; in this way, we convert the list of transactions to include only the data we want to report on. In this case, summarization is just a transformation which accepts the full set of transactions and returns an equivalent truncated stream. Then, from this stream, a journal can be produced that excludes the transactions from the past. From a program design perspective, this is appealing because the only state of the program is a stream of transactions, and it is never modified directly. It\u2019s simple and robust.) Period Reporting \uf0c1 Now we know we can produce a statement of changes over a period of time, by \u201cclearing\u201d and looking at just the Income & Expenses accounts (the Income Statement). We also know we can clear to produce a snapshot of Assets, Liabilities & Equity at any point in time (the Balance Sheet). More generally, we\u2019re interested in inspecting a particular period of time. That implies an income statement, but also two balance sheet statements: the balance sheet at the beginning of the period, and the balance sheet at the end of the period. In order to do this, we apply the following transformations: Open. We first clear net income at the beginning of the period, to move all previous income balances to the Equity Previous Earnings account. We then summarize up to the beginning of the period. We call the combination of clearing + summarizing: \u201cOpening.\u201d Close. We also truncate all the transactions following the end of the reporting period. We call this operation \u201cClosing.\u201d These are the meaning of the \u201cOPEN\u201d and \u201cCLOSE\u201d operations of the bean-query shell 3 . The resulting set of transactions should look like this. \u201cClosing\u201d involves two steps. First, we remove all transactions following the closing date: We can process this stream of transactions to produce an Income Statement for the period. Then we clear again at the end date of the desired report, but this time we clear the net income to \u201cEquity:Earnings:Current\u201d: From these transactions, we produce the Balance Sheet at the end of the period. This sums up the operations involved in preparing the streams of transactions to produce reports with Beancount, as well as a basic introduction to those types of reports. Chart of Accounts \uf0c1 New users are often wondering how much detail they should use in their account names. For example, should one include the payee in the account name itself, such as in these examples? Expenses:Phone:Mobile:VerizonWireless Assets:AccountsReceivable:Clients:AcmeInc Or should one use simpler names like the following, relying instead on the \u201cpayee\u201d, \u201ctags\u201d, or perhaps some other metadata in order to group the postings? Expenses:Phone Assets:AccountsReceivable The answer is that it depends on you . This is an arbitrary choice to make. It\u2019s a matter of taste. Personally I like to abuse the account names a bit and create long descriptive ones, other people prefer to keep them simple and use tags to group their postings. Sometimes one doesn\u2019t even need to filter subgroups of postings. There\u2019s no right answer, it depends on what you\u2019d like to do. One consideration to keep in mind is that account names implicitly define a hierarchy. The \u201c:\u201d separator is interpreted by some reporting code to create an in-memory tree and can allow you to collapse a node\u2019s children subaccounts and compute aggregates on the parent. Think of this as an additional way to group postings. Country-Institution Convention \uf0c1 One convention I\u2019ve come up with that works well for my assets, liabilities and income accounts is to root the tree with a code for the country the account lives in, followed by a short string for the institution it corresponds to. Underneath that, a unique name for the particular account in that institution. Like this: : : : For example, a checking account could be chosen to be \u201c Assets:US:BofA:Checking \u201d, where \u201cBofA\u201d stands for \u201cBank of America.\u201d A credit card account could include the name of the particular type of card as the account name, like \u201c Liabilities:US:Amex:Platinum \u201d, which can be useful if you have multiple cards. I\u2019ve found it doesn\u2019t make sense for me to use this scheme for expense accounts, since those tend to represent generic categories. For those, it seems to make more sense to group them by category, as in using \u201c Expenses:Food:Restaurant \u201d instead of just \u201c Expenses:Restaurant \u201d. In any case, Beancount doesn\u2019t enforce anything other than the root accounts; this is just a suggestion and this convention is not coded anywhere in the software. You have great freedom to experiment, and you can easily change all the names later by processing the text file. See the Cookbook for more practical guidance. Credits & Debits \uf0c1 At this point, we haven\u2019t discussed the concepts of \u201ccredits\u201d and \u201cdebits.\u201d This is on purpose: Beancount largely does away with these concepts because it makes everything else simpler. I believe that it is simpler to just learn that the signs of Income, Liabilities and Equity accounts are normally negative and to treat all accounts the same way than to deal with the debits and credits terminology and to treat different account categories differently. In any case, this section explains what these are. As I have pointed out in previous sections, we consider \u201cIncome\u201d, \u201cLiabilities\u201d and \u201cEquity\u201d accounts to normally have a negative balance. This may sound odd; after all, nobody thinks of their gross salary as a negative amount, and certainly your credit-card bill or mortgage loan statements report positive numbers. This is because in our double-entry accounting system we consider all accounts to be held from the perspective of the owner of the account . We use signs consistent from this perspective, because it makes all operations on account contents straightforward: they\u2019re all just simple additions and all the accounts are treated the same. In contrast, accountants traditionally keep all the balances of their accounts as positive numbers and then handle postings to those accounts differently depending on the account type upon which they are applied. The sign to apply to each account is entirely dictated by its type: Assets and Expenses accounts are debit accounts and Liabilities, Equity and Income accounts are credit accounts and require a sign adjustment. Moreover, posting a positive amount on an account is called \u201cdebiting\u201d and removing from an account is called \u201ccrediting.\u201d See this external document , for example, which nearly makes my head explode, and this recent thread has more detail. This way of handling postings makes everything much more complicated than it needs to be. The problem with this approach is that summing of amounts over the postings of a transaction is not a straightforward sum anymore. For example, let\u2019s say you\u2019re creating a new transaction with postings to two Asset accounts, an Expenses account and an Income account and the system tells you there is a $9.95 imbalance error somewhere. You\u2019re staring at the entry intently; which of the postings is too small? Or is one of the postings too large? Also, maybe a new posting needs to be added, but is it to a debit account or to a credit account? The mental gymnastics required to do this are taxing. Some double-entry accounting software tries to deal with this by creating separate columns for debits and credits and allowing the user enter an amount only in the column that corresponds to each posting account\u2019s type. This helps visually, but why not just use signs instead? Moreover, when you look at the accounting equations, you have to consider their signs as well. This makes it awkward to do transformations on them and make what is essentially a simple summation over postings into a convoluted mess that is difficult to understand. In plain-text accounting, we would rather just do away with this inconvenient baggage. We just use additions everywhere and learn to keep in mind that Liabilities, Equity and Income accounts normally have a negative balance. While this is unconventional, it\u2019s much easier to grok. And If there is a need to view a conventional report with positive numbers only, we will be able to trigger that in reporting code 4 , inverting the signs just to render them in the output. Save yourself some pain: Flush your brain from the \"debit\" and \"credit\" terminology. Accounting Equations \uf0c1 In light of the previous sections, we can easily express the accounting equations in signed terms. If, A = the sum of all Assets postings L = the sum of all Liabilities postings X = the sum of all Expenses postings I = the sum of all Income postings E = the sum of all Equity postings We can say that: A + L + E + X + I = 0 This follows from the fact that sum(all postings) = 0 Which follows from the fact that each transaction is guaranteed to sum up to zero (which is enforced by Beancount): for all transactions t, sum(postings of t) = 0 Moreover, the sum of postings from Income and Expenses is the Net Income (NI): NI = X + I If we adjust the equity to reflect the total Net Income effect by clearing the income to the Equity retained earnings account, we get an updated Equity value (E\u2019): E\u2019 = E + NI = E + X + I And we have a simplified accounting equation: A + L + E\u2019 = 0 If we were to adjust the signs for credits and debits (see previous section) and have sums that are all positive number, this becomes the familiar accounting equation: Assets - Liabilities = Equity As you can see, it\u2019s much easier to just always add up the numbers. Plain-Text Accounting \uf0c1 Ok, so now we understand the method and what it can do for us, at least in theory. The purpose of a double-entry bookkeeping system is to allow you to replicate the transactions that occur in various real world accounts into a single, unified system, in a common representation, and to extract various views and reports from this data. Let us now turn our attention to how we record this data in practice. This document talks about Beancount, whose purpose is \u201cdouble-entry bookkeeping using text files.\u201d Beancount implements a parser for a simple syntax that allows you to record transactions and postings. The syntax for an example transaction looks something like this: 2016-12-06 * \"Biang!\" \"Dinner\" Liabilities:CreditCard -47.23 USD Expenses:Restaurants You write many of declarations like these in a file, and Beancount will read it and create the corresponding data structures in memory. Verification. After parsing the transactions, Beancount also verifies the rule of the double-entry method: it checks that the sum of the postings on all your transactions is zero. If you make a mistake and record a transaction with a non-zero balance, an error will be displayed. Balance Assertions. Beancount allows you to replicate balances declared from external accounts, for example, a balance written on a monthly statement. It processes those and checks that the balances resulting from your input transactions match those declared balances. This helps you detect and find mistakes easily. Plugins. Beancount allows you to build programs which can automate and/or process the streams of transactions in your input files. You can build custom functionality by writing code which directly processes the transaction stream. Querying & Reporting. It provides tools to then process this stream of transactions to produce the kinds of reports we discussed earlier in this document. There are a few more details, for example, Beancount allows you to track cost basis and make currency conversions, but that\u2019s the essence of it. The Table Perspective \uf0c1 Almost always, questions asked by users on the mailing-list about how to calculate or track some value or other can be resolved easily simply by thinking of the data as a long list of rows, some of which need to be filtered and aggregated. If you consider that all that we\u2019re doing in the end is deriving \u201csums\u201d of these postings, and that the attributes of transactions and postings are what allows us to filter subsets of postings, it always becomes very simple. In almost all the cases, the answer is to find some way to disambiguate postings to select them, e.g. by account name, by attaching some tag, by using some metadata, etc. It can be illuminating to consider how this data can be represented as a table. Imagine that you have two tables: a table containing the fields of each Transaction such as date and description, and a table for the fields of each Posting, such as account, amount and currency, as well as a reference to its parent transaction. The simplest way to represent the data is to join those two tables, replicating values of the parent transaction across each of the postings. For example, this Beancount input: 2016-12-04 * \"Christmas gift\" Liabilities:CreditCard -153.45 USD Expenses:Gifts 2016-12-06 * \"Biang!\" \"Dinner\" Liabilities:CreditCard -47.23 USD Expenses:Restaurants 2016-12-07 * \"Pouring Ribbons\" \"Drinks with friends\" Assets:Cash -25.00 USD Expenses:Tips 4.00 USD Expenses:Alcohol could be rendered as a table like this: Date Fl Payee Narration Account Number Ccy 2016-12-04 * Christmas gift Liabilities:CreditCard -153.45 USD 2016-12-04 * Christmas gift Expenses:Gifts 153.45 USD 2016-12-06 * Biang! Dinner Liabilities:CreditCard -47.23 USD 2016-12-06 * Biang! Dinner Expenses:Restaurants 47.23 USD 2016-12-07 * Pouring Ribbons Drinks with friends Assets:Cash -25.00 USD 2016-12-07 * Pouring Ribbons Drinks with friends Expenses:Tips 4.00 USD 2016-12-07 * Pouring Ribbons Drinks with friends Expenses:Alcohol 21.00 USD Notice how the values of Transaction fields are replicated for each posting. This is exactly like a regular database join operation. The posting fields begin at column \u201cAccount.\u201d (Also note that this example table is simplified; in practice there are many more fields.) If you had a joined table just like this you could filter it and sum up amounts for arbitrary groups of postings. This is exactly what the bean-query tool allows you to do: You can run an SQL query on the data equivalent to this in-memory table and list values like this: SELECT date, payee, number WHERE account = \"Liabilities:CreditCard\"; Or sum up positions like this: SELECT account, sum(position) GROUP BY account; This simple last command generates the trial balance report. Note that the table representation does not inherently constrain the postings to sum to zero. If your selection criteria for the rows (in the WHERE clause) always selects all the postings for each of the matching transactions, you are ensured that the final sum of all the postings is zero. If not, the sum may be anything else. Just something to keep in mind. If you\u2019re familiar with SQL databases, you might ask why Beancount doesn\u2019t simply process its data in order to fill up an existing database system, so that the user could then use those database\u2019s tools. There are two main reasons for this: Reporting Operations. In order to generate income statements and balance sheets, the list of transactions needs to be preprocessed using the clear, open and close operations described previously. These operations are not trivial to implement in database queries and are dependent on just the report and ideally don\u2019t need to modify the input data. We\u2019d have to load up the posting data into memory and then run some code. We\u2019re already doing that by parsing the input file; the database step would be superfluous. Aggregating Positions. Though we haven\u2019t discussed it in this document so far, the contents of accounts may contain different types of commodities, as well as positions with an attached cost basis. The way that these positions are aggregated together requires the implementation of a custom data type because it obeys some rules about how positions are able to cancel each other out (see How Inventories Work for details). It would be very difficult to build these operations with an SQL database beyond the context of using just a single currency and ignoring cost basis. This is why Beancount provides a custom tool to directly process and query its data: It provides its own implementation of an SQL client that lets you specify open and close dates and leverages a custom \u201cInventory\u201d data structure to create sums of the positions of postings. This tools supports columns of Beancount\u2019s core types: Amount, Position and Inventory objects. (In any case, if you\u2019re not convinced, Beancount provides a tool to export its contents to a regular SQL database system. Feel free to experiment with it if you like, knock yourself out.) Please don\u2019t pay attention to the numbers in these large figures, they were randomly generated and don\u2019t reflect this. We\u2019re just interested in showing the structure, in these figures. \u21a9 Note that this is unrelated to the term \u201cclearing transactions\u201d which means acknowledging or marking that some transactions have been eyeballed by the bookkeeper and checked for correctness. \u21a9 Note that operations have nothing to do with the Open and Close directives Beancount provides. \u21a9 This is not provided yet in Beancount, but would be trivial to implement. All we'd need to do is invert the signs of balances from Liabilities, Income and Equity accounts. It's on the roadmap to provide this eventually. \u21a9","title":"The Double Entry Counting Method"},{"location":"the_double_entry_counting_method.html#the-double-entry-counting-method","text":"Martin Blais, December 2016 http://furius.ca/beancount/doc/double-entry Introduction Basics of Double-Entry Bookkeeping Statements Single-Entry Bookkeeping Double-Entry Bookkeeping Many Accounts Multiple Postings Types of Accounts Trial Balance Income Statement Clearing Income Equity Balance Sheet Summarizing Period Reporting Chart of Accounts Country-Institution Convention Credits & Debits Accounting Equations Plain-Text Accounting The Table Perspective","title":"The Double-Entry Counting Method"},{"location":"the_double_entry_counting_method.html#introduction","text":"This document is a gentle introduction to the double-entry counting method, as written from the perspective of a computer scientist. It is an attempt to explain basic bookkeeping using as simple an approach as possible, doing away with some of the idiosyncrasies normally involved in accounting. It is also representative of how Beancount works, and it should be useful to all users of plain-text accounting . Note that I am not an accountant, and in the process of writing this document I may have used terminology that is slightly different or unusual to that which is taught in perhaps more traditional training in accounting. I granted myself license to create something new and perhaps even unusual in order to explain those ideas as simply and clearly as possible to someone unfamiliar with them. I believe that the method of double-entry counting should be taught to everyone at the high school level everywhere as it is a tremendously useful organizational skill, and I hope that this text can help spread its knowledge beyond professional circles.","title":"Introduction"},{"location":"the_double_entry_counting_method.html#basics-of-double-entry-bookkeeping","text":"The double-entry system is just a simple method of counting , with some simple rules. Let\u2019s begin by defining the notion of an account . An account is something that can contain things, like a bag. It is used to count things, to accumulate things. Let\u2019s draw a horizontal arrow to visually represent the evolving contents of an account over time: On the left, we have the past, and to the right, increasing time: the present, the future, etc. For now, let\u2019s assume that accounts can contain only one kind of thing, for example, dollars . All accounts begin with an empty content of zero dollars. We will call the number of units in the account the balance of an account. Note that it represents its contents at a particular point in time. I will draw the balance using a number above the account\u2019s timeline: The contents of accounts can change over time. In order to change the content of an account, we have to add something to it. We will call this addition a posting to an account, and I will draw this change as a circled number on the account\u2019s timeline, for example, adding $100 to the account: Now, we can draw the updated balance of the account after the posting with another little number right after it: The account\u2019s balance, after adding $100, is now $100. We can also remove from the contents of an account. For example, we could remove $25, and the resulting account balance is now $75: Account balances can also become negative , if we remove more dollars than there are in the account. For example, if we remove $200 from this account, the balance now becomes $-125: It\u2019s perfectly fine for accounts to contain a negative balance number. Remember that all we\u2019re doing is counting things. As we will see shortly, some accounts will remain with a negative balance for most of their timeline.","title":"Basics of Double-Entry Bookkeeping"},{"location":"the_double_entry_counting_method.html#statements","text":"Something worthy of notice is how the timeline notation I\u2019ve written in the previous section is analogous to paper account statements institutions maintain for each client and which you typically receive through the mail: Date Description Amount Balance 2016-10-02 ... 100.00 1100.00 2016-10-05 ... -25.00 1075.00 2016-10-06 ... -200.00 875.00 Final Balance 875.00 Sometimes the amount column is split into two, one showing the positive amounts and the other the negative ones: Date Description Debit Credit Balance 2016-10-02 ... 100.00 1100.00 2016-10-05 ... 25.00 1075.00 2016-10-06 ... 200.00 875.00 Final Balance 875.00 Here, \u201cdebit\u201d means \u201cremoved from your account\u201d and \u201ccredit\u201d means \u201cdeposited in your account.\u201d Sometimes the words \u201cwithdrawals\u201d and \u201cdeposits\u201d will be used. It all depends on context: for checking and savings accounts it is usual to have both types of postings, but for a credit card account typically it shows only positive numbers and then the occasional monthly payment so the single column format is used. In any case, the \u201cbalance\u201d column always shows the resulting balance after the amount has been posted to the account. And sometimes the statements are rendered in decreasing order of time.","title":"Statements"},{"location":"the_double_entry_counting_method.html#single-entry-bookkeeping","text":"In this story, this account belongs to someone. We\u2019ll call this person the owner of the account. The account can be used to represent a real world account, for example, imagine that we use it to represent the content of the owner\u2019s checking account at a bank. So we\u2019re going to label the account by giving it a name, in this case \u201cChecking\u201d: Imagine that at some point, this account has a balance of $1000, like I\u2019ve drawn on the picture. Now, if the owner spends $79 of this account, we would represent it like this: Furthermore, if the expense was for a meal at a restaurant, we could flag the posting with a category to indicate what the change was used for. Let\u2019s say, \u201cRestaurant\u201d, like this: Now, if we have a lot of these, we could write a computer program to accumulate all the changes for each category and calculate the sums for each of them. That would tell us how much we spent in restaurants in total, for example. This is called the single-entry method of accounting. But we\u2019re not going to do it this way; we have a better way. Bear with me for a few more sections.","title":"Single-Entry Bookkeeping"},{"location":"the_double_entry_counting_method.html#double-entry-bookkeeping","text":"An owner may have multiple accounts. I will represent this by drawing many similar account timelines on the same graphic. As before, these are labeled with unique names. Let\u2019s assume that the owner has the same \u201cChecking\u201d account as previously, but now also a \u201c Restaurant \u201d account as well, which can be used to accumulate all food expenses at restaurants. It looks like this: Now, instead of categorizing the posting to a \u201crestaurant category\u201d as we did previously, we could create a matching posting on the \u201cRestaurant\u201d account to record how much we spent for food, with the amount spent ($79): The \u201cRestaurant\u201d account, like all other accounts, also has an accumulated balance, so we can find out how much we spent in \u201cRestaurant\u201d in total. This is entirely symmetrical to counting changes in a checking account. Now, we can associate the two postings together, by creating a kind of \u201cparent\u201d box that refers to both of them. We will call this object a transaction : Notice here that we\u2019ve also associated a description to this transaction: \u201cDinner at Uncle Boons\u201d. A transaction also has a date , and all of its postings are recorded to occur on that date. We call this the transaction date. We can now introduce the fundamental rule of double-entry bookkeeping system: The sum of all the postings of a transaction must equal zero. Remember this, as this is the foundation of the double-entry method, and its most important characteristic. It has important consequences which I will discuss later in this document. In our example, we remove $79 from the \u201cChecking\u201d account and \u201cgive it\u201d to the \u201cRestaurant\u201d account. ($79) + ($-79) = $0. To emphasize this, I could draw a little summation line under the postings of the transaction, like this:","title":"Double-Entry Bookkeeping"},{"location":"the_double_entry_counting_method.html#many-accounts","text":"There may be many such transactions, over many different accounts. For example, if the owner of the accounts had a lunch the next day which she paid using a credit card, it could be represented by creating a \u201cCredit Card\u201d account dedicated to tracking the real world credit card balance, and with a corresponding transaction: In this example, the owner spent $35 at a restaurant called \u201cEataly.\u201d The previous balance of the owner\u2019s credit card was $-450; after the expense, the new balance is $-485. For each real world account, the owner can create a bookkeeping account like we did. Also, for each category of expenditure, the owner also creates a bookkeeping account. In this system, there are no limits to how many accounts can be created. Note that the balance in the example is a negative number; this is not an error. Balances for credit card accounts are normally negative: they represent an amount you owe , that the bank is lending you on credit . When your credit card company keeps track of your expenses, they write out your statement from their perspective, as positive numbers. For you, those are amounts you need to eventually pay. But here, in our accounting system, we\u2019re representing numbers from the owner\u2019s point-of-view, and from her perspective, this is money she owes, not something she has. What we have is a meal sitting in our stomach (a positive number of $ of \u201cRestaurant\u201d).","title":"Many Accounts"},{"location":"the_double_entry_counting_method.html#multiple-postings","text":"Finally, transactions may have more than two postings; in fact, they may have any number of postings. The only thing that matters is that the sum of their amounts is zero (from the rule of double-entry bookkeeping above). For example, let\u2019s look at what would happen if the owner gets her salary paid for December: Her gross salary received in this example is recorded as $-2,905 (I\u2019ll explain the sign in a moment). $905 is set aside for taxes. Her \u201cnet\u201d salary of $2,000, the remainder, is deposited in her \u201cChecking\u201d account and the resulting balance of that account is $2,921 (the previous balance of $921 + $2,000 = $2,921). This transaction has three postings: (+2,000) + (-2,905) + (+905) = 0. The double-entry rule is respected. Now, you may ask: Why is her salary recorded as a negative number? The reasoning here is similar to that of the credit card above, though perhaps a bit more subtle. These accounts exist to track all the amounts from the owner\u2019s point-of-view. The owner gives out work, and receives money and taxes in exchange for it (positive amounts). The work given away is denominated in dollar units. It \u201cleaves\u201d the owner (imagine that the owner has potential work stored in her pocket and as she goes into work every day sprinkles that work potential giving it to the company). The owner gave $2,905\u2019s worth of work away. We want to track how much work was given, and it\u2019s done with the \u201cSalary\u201d account. That\u2019s her gross salary. Note also that we\u2019ve simplified this paycheck transaction a bit, for the sake of keeping things simple. A more realistic recording of one\u2019s pay stub would have many more accounts; we would separately account for state and federal tax amounts, as well as social security and medicare payments, deductions, insurance paid through work, and vacation time accrued during the period. But it wouldn\u2019t be much more complicated: the owner would simply translate all the amounts available from her pay stub into a single transaction with more postings. The structure remains similar.","title":"Multiple Postings"},{"location":"the_double_entry_counting_method.html#types-of-accounts","text":"Let\u2019s now turn our attention to the different types of accounts an owner can have. Balance or Delta. First, the most important distinction between accounts is about whether we care about the balance at a particular point in time, or whether it only makes sense to care about differences over a period of time. For example, the balance of someone\u2019s Checking or Savings accounts is a meaningful number that both the owner and its corresponding bank will care about. Similarly, the total amount owed on someone\u2019s Credit Card account is also meaningful. The same goes with someone\u2019s remaining Mortgage amount to pay on a house. On the other hand, the total amount of Restaurant expenses since the beginning of somebody\u2019s life on earth is not particularly interesting. What we might care about for this account is the amount of Restaurant expenses incurred over a particular period of time . For example, \u201chow much did you spend in restaurants last month?\u201d Or last quarter. Or last year. Similarly, the total amount of gross salary since the beginning of someone\u2019s employment at a company a few years ago is not very important. But we would care about the total amount earned during a tax year, that is, for that time period, because it is used for reporting one\u2019s income to the tax man. Accounts whose balance at a point in time is meaningful are called balance sheet accounts . There are two types of such accounts: \u201c Assets \u201d and \u201c Liabilities .\u201d The other accounts, that is, those whose balance is not particularly meaningful but for which we are interested in calculating changes over a period of time are called income statement accounts . Again, there are two kinds: \u201c Income \u201d and \u201c Expenses .\u201d Normal sign. Secondly, we consider the usual sign of an account\u2019s balance . The great majority of accounts in the double-entry system tend to have a balance with always a positive sign, or always a negative sign (though as we\u2019ve seen previously, it is not impossible that an account\u2019s balance could change signs). This is how we will distinguish between the pairs of accounts mentioned before: For a balance sheet account, Assets normally have positive balances, and Liabilities normally have negative balances. For income statement accounts, Expenses normally have a positive balance, and Income accounts normally have a negative balance. This situation is summarized in the following table: Balance: Positive (+) Balance: Negative (-) Balance matters at a point in time (Balance Sheet) Assets Liabilities Change in balance matters over a period of time (Income Statement) Expenses Income Let\u2019s discuss each type of account and provide some examples, so that it doesn\u2019t remain too abstract. Assets. (+) Asset accounts represent something the owner has . A canonical example is banking accounts. Another one is a \u201ccash\u201d account, which counts how much money is in your wallet. Investments are also assets (their units aren\u2019t dollars in this case, but rather some number of shares of some mutual fund or stock). Finally, if you own a home, the home itself is considered an asset (and its market value fluctuates over time). Liabilities. (-) A liability account represents something the owner owes . The most common example is a credit card. Again, the statement provided by your bank will show positive numbers, but from your own perspective, they are negative numbers. A loan is also a liability account. For example, if you take out a mortgage on a home, this is money you owe, and will be tracked by an account with a negative amount. As you pay off the mortgage every month the negative number goes up, that is, its absolute value gets smaller and smaller over time (e.g., -120,000 -> -117,345). Expenses. (+) An expense account represents something you\u2019ve received , perhaps by exchanging something else to purchase it. This type of account will seem pretty natural: food, drinks, clothing, rent, flights, hotels and most other categories of things you typically spend your disposable income on. However, taxes are also typically tracked by an expense account: when you receive some salary income, the amount of taxes withheld at the source is recorded immediately as an expense. Think of it as paying for government services you receive throughout the year. Income. (-) An income account is used to count something you\u2019ve given away in order to receive something else (typically assets or expenses). For most people with jobs, that is the value of their time (a salary income). Specifically, here we\u2019re talking about the gross income. For example, if you\u2019re earning a salary of $120,000/year, that number is $120,000, not whatever amount remains after paying for taxes. Other types of income includes dividends received from investments, or interest paid from bonds held. There are also a number of oddball things received you might record as income, such the value of rewards received, e.g., cash back from a credit card, or monetary gifts from someone. In Beancount, all account names, without exception, must be associated to one of the types of accounts described previously. Since the type of an account never changes during its lifetime, we will make its type a part of an account\u2019s name, as a prefix , by convention. For example, the qualified account name for restaurant will be \u201cExpenses:Restaurant\u201d. For the bank checking account, the qualified account name will be \u201cAssets:Checking\u201d. Other than that, you can select any name you like for your accounts. You can create as many accounts as you like, and as we will see later, you can organize them in a hierarchy. As of the writing of this document, I\u2019m using more than 700 accounts to track my personal affairs. Let us now revisit our example and add some more accounts: And let\u2019s imagine there are more transactions: \u2026 and even more of them: Finally, we can label each of those accounts with one of the four types of accounts by prepending the type to their account names: A realistic book from someone tracking all of their personal affairs might easily contain thousands of transactions per year. But the principles remain simple and they remain the same: postings are applied to accounts over time, and must be parented to a transaction, and within this transaction the sum of all the postings is zero. When you do bookkeeping for a set of accounts, you are essentially describing all the postings that happen on all the accounts over time, subject to the constraint of the rule. You are creating a database of those postings in a book . You are \u201ckeeping the book,\u201d that is, traditionally, the book which contains all those transactions. Some people call this \u201cmaintaining a journal.\u201d We will now turn our attention to obtaining useful information from this data, summarizing information from the book.","title":"Types of Accounts"},{"location":"the_double_entry_counting_method.html#trial-balance","text":"Take our last example: we can easily reorder all the accounts such that all the Asset accounts appear together at the top, then all the Liabilities accounts, then Income, and finally Expenses accounts. We are simply changing the order without modifying the structure of transactions, in order to group each type of accounts together: We\u2019ve reordered the accounts with Assets accounts grouped at the top, then Liabilities, then some Equity accounts (which we have just introduced, more about them is discussed later), then Income and finally Expenses at the bottom. If we sum up the postings on all of the accounts and render just the account name and its final balance on the right, we obtain a report we call the \u201ctrial balance.\u201d This simply reflects the balance of each account at a particular point in time. And because each of the accounts began with a zero balance, and each transaction has itself a zero balance, we know that the sum of all those balances must equal zero. 1 This is a consequence of our constraining that each of the postings be part of a transaction, and that each transaction have postings that balance each other out.","title":"Trial Balance"},{"location":"the_double_entry_counting_method.html#income-statement","text":"One kind of common information that is useful to extract from the list of transactions is a summary of changes in income statement accounts during a particular period of time. This tells us how much money was earned and spent during this period, and the difference tells us how much profit (or loss) was incurred. We call this the \u201cnet income.\u201d In order to generate this summary, we simply turn our attention to the balances of the accounts of types Income and Expenses, summing up just the transactions for a particular period, and we draw the Income balances on the left, and Expenses balances on the right: It is important to take note of the signs here: Income numbers are negative, and Expenses numbers are positive. So if you earned more than you spent (a good outcome), the final sum of Income + Expenses balances will be a negative number. Like any other income, a net income that has a negative number means that there is a corresponding amount of Assets and/or Liabilities with positive numbers (this is good for you). An Income Statement tells us what changed during a particular period of time. Companies typically report this information quarterly to investors and perhaps the public (if they are a publicly traded company) in order to share how much profit they were able to make. Individuals typically report this information on their annual tax returns.","title":"Income Statement"},{"location":"the_double_entry_counting_method.html#clearing-income","text":"Notice how in the income statement only the transactions within a particular interval of time are summed up. This allows one, for instance, to compute the sum of all income earned during a year. If we were to sum up all of the transactions of this account since its inception we would obtain the total amount of income earned since the account was created. A better way to achieve the same thing is to zero out the balances of the Income and Expenses accounts. Beancount calls this basic transformation \u201cclearing 2 .\u201d It is carried out by: Computing the balances of those accounts from the beginning of time to the start of the reporting period. For example, if you created your accounts in year 2000 and you wanted to generate an income statement for year 2016, you would sum up the balances from 2000 to Jan 1, 2016. Inserting transactions to empty those balances and transfer them to some other account that isn\u2019t Income nor Expenses. For instance, if the restaurant expense account for those 16 years amounts to $85,321 on Jan 1, 2016, it would insert a transaction of $-85,321 to restaurants and $+85,321 to \u201cprevious earnings\u201d. The transactions would be dated Jan 1, 2016. Including this transaction, the sum of that account would zero on that date. This is what we want. Those transactions inserted for all income statement accounts are pictured in green below. Now summing the entire set of transactions through the end of the ledger would yield only the changes during year 2016 because the balances were zero on that date: This is the semantics of the \u201cCLEAR\u201d operation of the bean-query shell. (Note that another way to achieve the same thing for income statement accounts would be to segregate and count amounts only for the transactions after the clearing date; however, jointly reporting on income statement accounts and balance sheet accounts would have incorrect balances for the balance sheet accounts.)","title":"Clearing Income"},{"location":"the_double_entry_counting_method.html#equity","text":"The account that receives those previously accumulated incomes is called \u201cPrevious Earnings\u201d. It lives in a fifth and final type of accounts: Equity . We did not talk about this type of accounts earlier because they are most often only used to transfer amounts to build up reports, and the owner usually doesn\u2019t post changes to those types of accounts; the software does that automatically, e.g., when clearing net income. The account type \u201cequity\u201d is used for accounts that hold a summary of the net income implied by all the past activity. The point is that if we now list together the Assets, Liabilities and Equity accounts, because the Income and Expenses accounts have been zero\u2019ed out, the sum total of all these balances should equal exactly zero. And summing up all the Equity accounts clearly tells us what\u2019s our stake in the entity, in other words, if you used the assets to pay off all the liabilities, how much is left in the business\u2026 how much it\u2019s worth. Note that the normal sign of the Equity accounts is negative . There is no particular meaning to that, just that they are used to counterbalance Assets and Liabilities and if the owner has any value, that number should be negative. (A negative Equity means some positive net worth.) There are a few different Equity accounts in use in Beancount: Previous Earnings or Retained Earnings. An account used to hold the sum total of Income & Expenses balances from the beginning of time until the beginning of a reporting period. This is the account we were referring to in the previous section. Current Earnings , also called Net Income. An account used to contain the sum of Income & Expenses balances incurred during the reporting period. They are filled in by \u201cclearing\u201d the Income & Expenses accounts at the end of the reporting period. Opening Balances. An equity account used to counterbalance deposits used to initialize accounts. This type of account is used when we truncate the past history of transactions, but we also need to ensure that an account\u2019s balance begins its history with a particular amount. Once again: you don\u2019t need to define nor use these accounts yourself, as these are created for the purpose of summarizing transactions. Generally, the accounts are filled in by the clearing process described above, or filled in by Pad directives to \u201copening balances\u201d equity accounts, to account for summarized balances from the past. They are created and filled in automatically by the software. We\u2019ll see how these get used in the following sections.","title":"Equity"},{"location":"the_double_entry_counting_method.html#balance-sheet","text":"Another kind of summary is a listing of the owner\u2019s assets and debts, for each of the accounts. This answers the question: \u201c Where\u2019s the money? \u201d In theory, we could just restrict our focus to the Assets and Liabilities accounts and draw those up in a report: However, in practice, there is another closely related question that comes up and which is usually answered at the same time: \u201c Once all debts are paid off, how much are we left with? \u201d This is called the net worth . If the Income & Expenses accounts have been cleared to zero and all their balances have been transferred to Equity accounts, the net worth should be equal to the sum of all the Equity accounts. So in building up the Balance Sheet, it it customary to clear the net income and then display the balances of the Equity accounts. The report looks like this: Note that the balance sheet can be drawn for any point in time , simply by truncating the list of transactions following a particular date. A balance sheet displays a snapshot of balances at one date; an income statement displays the difference of those balances between two dates.","title":"Balance Sheet"},{"location":"the_double_entry_counting_method.html#summarizing","text":"It is useful to summarize a history of past transactions into a single equivalent deposit. For example, if we\u2019re interested in transactions for year 2016 for an account which has a balance of $450 on Jan 1, 2016, we can delete all the previous transactions and replace them with a single one that deposits $450 on Dec 31, 2015 and that takes it from somewhere else. That somewhere else will be the Equity account Opening Balances . First, we can do this for all Assets and Liabilities accounts (see transactions in blue): Then we delete all the transactions that precede the opening date, to obtain a truncated list of transactions: This is a useful operation when we\u2019re focused on the transactions for a particular interval of time. (This is a bit of an implementation detail: these operations are related to how Beancount is designed. Instead of making all the reporting operations with parameters, all of its reporting routines are simplified and instead operate on the entire stream of transactions; in this way, we convert the list of transactions to include only the data we want to report on. In this case, summarization is just a transformation which accepts the full set of transactions and returns an equivalent truncated stream. Then, from this stream, a journal can be produced that excludes the transactions from the past. From a program design perspective, this is appealing because the only state of the program is a stream of transactions, and it is never modified directly. It\u2019s simple and robust.)","title":"Summarizing"},{"location":"the_double_entry_counting_method.html#period-reporting","text":"Now we know we can produce a statement of changes over a period of time, by \u201cclearing\u201d and looking at just the Income & Expenses accounts (the Income Statement). We also know we can clear to produce a snapshot of Assets, Liabilities & Equity at any point in time (the Balance Sheet). More generally, we\u2019re interested in inspecting a particular period of time. That implies an income statement, but also two balance sheet statements: the balance sheet at the beginning of the period, and the balance sheet at the end of the period. In order to do this, we apply the following transformations: Open. We first clear net income at the beginning of the period, to move all previous income balances to the Equity Previous Earnings account. We then summarize up to the beginning of the period. We call the combination of clearing + summarizing: \u201cOpening.\u201d Close. We also truncate all the transactions following the end of the reporting period. We call this operation \u201cClosing.\u201d These are the meaning of the \u201cOPEN\u201d and \u201cCLOSE\u201d operations of the bean-query shell 3 . The resulting set of transactions should look like this. \u201cClosing\u201d involves two steps. First, we remove all transactions following the closing date: We can process this stream of transactions to produce an Income Statement for the period. Then we clear again at the end date of the desired report, but this time we clear the net income to \u201cEquity:Earnings:Current\u201d: From these transactions, we produce the Balance Sheet at the end of the period. This sums up the operations involved in preparing the streams of transactions to produce reports with Beancount, as well as a basic introduction to those types of reports.","title":"Period Reporting"},{"location":"the_double_entry_counting_method.html#chart-of-accounts","text":"New users are often wondering how much detail they should use in their account names. For example, should one include the payee in the account name itself, such as in these examples? Expenses:Phone:Mobile:VerizonWireless Assets:AccountsReceivable:Clients:AcmeInc Or should one use simpler names like the following, relying instead on the \u201cpayee\u201d, \u201ctags\u201d, or perhaps some other metadata in order to group the postings? Expenses:Phone Assets:AccountsReceivable The answer is that it depends on you . This is an arbitrary choice to make. It\u2019s a matter of taste. Personally I like to abuse the account names a bit and create long descriptive ones, other people prefer to keep them simple and use tags to group their postings. Sometimes one doesn\u2019t even need to filter subgroups of postings. There\u2019s no right answer, it depends on what you\u2019d like to do. One consideration to keep in mind is that account names implicitly define a hierarchy. The \u201c:\u201d separator is interpreted by some reporting code to create an in-memory tree and can allow you to collapse a node\u2019s children subaccounts and compute aggregates on the parent. Think of this as an additional way to group postings.","title":"Chart of Accounts"},{"location":"the_double_entry_counting_method.html#country-institution-convention","text":"One convention I\u2019ve come up with that works well for my assets, liabilities and income accounts is to root the tree with a code for the country the account lives in, followed by a short string for the institution it corresponds to. Underneath that, a unique name for the particular account in that institution. Like this: : : : For example, a checking account could be chosen to be \u201c Assets:US:BofA:Checking \u201d, where \u201cBofA\u201d stands for \u201cBank of America.\u201d A credit card account could include the name of the particular type of card as the account name, like \u201c Liabilities:US:Amex:Platinum \u201d, which can be useful if you have multiple cards. I\u2019ve found it doesn\u2019t make sense for me to use this scheme for expense accounts, since those tend to represent generic categories. For those, it seems to make more sense to group them by category, as in using \u201c Expenses:Food:Restaurant \u201d instead of just \u201c Expenses:Restaurant \u201d. In any case, Beancount doesn\u2019t enforce anything other than the root accounts; this is just a suggestion and this convention is not coded anywhere in the software. You have great freedom to experiment, and you can easily change all the names later by processing the text file. See the Cookbook for more practical guidance.","title":"Country-Institution Convention"},{"location":"the_double_entry_counting_method.html#credits-debits","text":"At this point, we haven\u2019t discussed the concepts of \u201ccredits\u201d and \u201cdebits.\u201d This is on purpose: Beancount largely does away with these concepts because it makes everything else simpler. I believe that it is simpler to just learn that the signs of Income, Liabilities and Equity accounts are normally negative and to treat all accounts the same way than to deal with the debits and credits terminology and to treat different account categories differently. In any case, this section explains what these are. As I have pointed out in previous sections, we consider \u201cIncome\u201d, \u201cLiabilities\u201d and \u201cEquity\u201d accounts to normally have a negative balance. This may sound odd; after all, nobody thinks of their gross salary as a negative amount, and certainly your credit-card bill or mortgage loan statements report positive numbers. This is because in our double-entry accounting system we consider all accounts to be held from the perspective of the owner of the account . We use signs consistent from this perspective, because it makes all operations on account contents straightforward: they\u2019re all just simple additions and all the accounts are treated the same. In contrast, accountants traditionally keep all the balances of their accounts as positive numbers and then handle postings to those accounts differently depending on the account type upon which they are applied. The sign to apply to each account is entirely dictated by its type: Assets and Expenses accounts are debit accounts and Liabilities, Equity and Income accounts are credit accounts and require a sign adjustment. Moreover, posting a positive amount on an account is called \u201cdebiting\u201d and removing from an account is called \u201ccrediting.\u201d See this external document , for example, which nearly makes my head explode, and this recent thread has more detail. This way of handling postings makes everything much more complicated than it needs to be. The problem with this approach is that summing of amounts over the postings of a transaction is not a straightforward sum anymore. For example, let\u2019s say you\u2019re creating a new transaction with postings to two Asset accounts, an Expenses account and an Income account and the system tells you there is a $9.95 imbalance error somewhere. You\u2019re staring at the entry intently; which of the postings is too small? Or is one of the postings too large? Also, maybe a new posting needs to be added, but is it to a debit account or to a credit account? The mental gymnastics required to do this are taxing. Some double-entry accounting software tries to deal with this by creating separate columns for debits and credits and allowing the user enter an amount only in the column that corresponds to each posting account\u2019s type. This helps visually, but why not just use signs instead? Moreover, when you look at the accounting equations, you have to consider their signs as well. This makes it awkward to do transformations on them and make what is essentially a simple summation over postings into a convoluted mess that is difficult to understand. In plain-text accounting, we would rather just do away with this inconvenient baggage. We just use additions everywhere and learn to keep in mind that Liabilities, Equity and Income accounts normally have a negative balance. While this is unconventional, it\u2019s much easier to grok. And If there is a need to view a conventional report with positive numbers only, we will be able to trigger that in reporting code 4 , inverting the signs just to render them in the output. Save yourself some pain: Flush your brain from the \"debit\" and \"credit\" terminology.","title":"Credits & Debits"},{"location":"the_double_entry_counting_method.html#accounting-equations","text":"In light of the previous sections, we can easily express the accounting equations in signed terms. If, A = the sum of all Assets postings L = the sum of all Liabilities postings X = the sum of all Expenses postings I = the sum of all Income postings E = the sum of all Equity postings We can say that: A + L + E + X + I = 0 This follows from the fact that sum(all postings) = 0 Which follows from the fact that each transaction is guaranteed to sum up to zero (which is enforced by Beancount): for all transactions t, sum(postings of t) = 0 Moreover, the sum of postings from Income and Expenses is the Net Income (NI): NI = X + I If we adjust the equity to reflect the total Net Income effect by clearing the income to the Equity retained earnings account, we get an updated Equity value (E\u2019): E\u2019 = E + NI = E + X + I And we have a simplified accounting equation: A + L + E\u2019 = 0 If we were to adjust the signs for credits and debits (see previous section) and have sums that are all positive number, this becomes the familiar accounting equation: Assets - Liabilities = Equity As you can see, it\u2019s much easier to just always add up the numbers.","title":"Accounting Equations"},{"location":"the_double_entry_counting_method.html#plain-text-accounting","text":"Ok, so now we understand the method and what it can do for us, at least in theory. The purpose of a double-entry bookkeeping system is to allow you to replicate the transactions that occur in various real world accounts into a single, unified system, in a common representation, and to extract various views and reports from this data. Let us now turn our attention to how we record this data in practice. This document talks about Beancount, whose purpose is \u201cdouble-entry bookkeeping using text files.\u201d Beancount implements a parser for a simple syntax that allows you to record transactions and postings. The syntax for an example transaction looks something like this: 2016-12-06 * \"Biang!\" \"Dinner\" Liabilities:CreditCard -47.23 USD Expenses:Restaurants You write many of declarations like these in a file, and Beancount will read it and create the corresponding data structures in memory. Verification. After parsing the transactions, Beancount also verifies the rule of the double-entry method: it checks that the sum of the postings on all your transactions is zero. If you make a mistake and record a transaction with a non-zero balance, an error will be displayed. Balance Assertions. Beancount allows you to replicate balances declared from external accounts, for example, a balance written on a monthly statement. It processes those and checks that the balances resulting from your input transactions match those declared balances. This helps you detect and find mistakes easily. Plugins. Beancount allows you to build programs which can automate and/or process the streams of transactions in your input files. You can build custom functionality by writing code which directly processes the transaction stream. Querying & Reporting. It provides tools to then process this stream of transactions to produce the kinds of reports we discussed earlier in this document. There are a few more details, for example, Beancount allows you to track cost basis and make currency conversions, but that\u2019s the essence of it.","title":"Plain-Text Accounting"},{"location":"the_double_entry_counting_method.html#the-table-perspective","text":"Almost always, questions asked by users on the mailing-list about how to calculate or track some value or other can be resolved easily simply by thinking of the data as a long list of rows, some of which need to be filtered and aggregated. If you consider that all that we\u2019re doing in the end is deriving \u201csums\u201d of these postings, and that the attributes of transactions and postings are what allows us to filter subsets of postings, it always becomes very simple. In almost all the cases, the answer is to find some way to disambiguate postings to select them, e.g. by account name, by attaching some tag, by using some metadata, etc. It can be illuminating to consider how this data can be represented as a table. Imagine that you have two tables: a table containing the fields of each Transaction such as date and description, and a table for the fields of each Posting, such as account, amount and currency, as well as a reference to its parent transaction. The simplest way to represent the data is to join those two tables, replicating values of the parent transaction across each of the postings. For example, this Beancount input: 2016-12-04 * \"Christmas gift\" Liabilities:CreditCard -153.45 USD Expenses:Gifts 2016-12-06 * \"Biang!\" \"Dinner\" Liabilities:CreditCard -47.23 USD Expenses:Restaurants 2016-12-07 * \"Pouring Ribbons\" \"Drinks with friends\" Assets:Cash -25.00 USD Expenses:Tips 4.00 USD Expenses:Alcohol could be rendered as a table like this: Date Fl Payee Narration Account Number Ccy 2016-12-04 * Christmas gift Liabilities:CreditCard -153.45 USD 2016-12-04 * Christmas gift Expenses:Gifts 153.45 USD 2016-12-06 * Biang! Dinner Liabilities:CreditCard -47.23 USD 2016-12-06 * Biang! Dinner Expenses:Restaurants 47.23 USD 2016-12-07 * Pouring Ribbons Drinks with friends Assets:Cash -25.00 USD 2016-12-07 * Pouring Ribbons Drinks with friends Expenses:Tips 4.00 USD 2016-12-07 * Pouring Ribbons Drinks with friends Expenses:Alcohol 21.00 USD Notice how the values of Transaction fields are replicated for each posting. This is exactly like a regular database join operation. The posting fields begin at column \u201cAccount.\u201d (Also note that this example table is simplified; in practice there are many more fields.) If you had a joined table just like this you could filter it and sum up amounts for arbitrary groups of postings. This is exactly what the bean-query tool allows you to do: You can run an SQL query on the data equivalent to this in-memory table and list values like this: SELECT date, payee, number WHERE account = \"Liabilities:CreditCard\"; Or sum up positions like this: SELECT account, sum(position) GROUP BY account; This simple last command generates the trial balance report. Note that the table representation does not inherently constrain the postings to sum to zero. If your selection criteria for the rows (in the WHERE clause) always selects all the postings for each of the matching transactions, you are ensured that the final sum of all the postings is zero. If not, the sum may be anything else. Just something to keep in mind. If you\u2019re familiar with SQL databases, you might ask why Beancount doesn\u2019t simply process its data in order to fill up an existing database system, so that the user could then use those database\u2019s tools. There are two main reasons for this: Reporting Operations. In order to generate income statements and balance sheets, the list of transactions needs to be preprocessed using the clear, open and close operations described previously. These operations are not trivial to implement in database queries and are dependent on just the report and ideally don\u2019t need to modify the input data. We\u2019d have to load up the posting data into memory and then run some code. We\u2019re already doing that by parsing the input file; the database step would be superfluous. Aggregating Positions. Though we haven\u2019t discussed it in this document so far, the contents of accounts may contain different types of commodities, as well as positions with an attached cost basis. The way that these positions are aggregated together requires the implementation of a custom data type because it obeys some rules about how positions are able to cancel each other out (see How Inventories Work for details). It would be very difficult to build these operations with an SQL database beyond the context of using just a single currency and ignoring cost basis. This is why Beancount provides a custom tool to directly process and query its data: It provides its own implementation of an SQL client that lets you specify open and close dates and leverages a custom \u201cInventory\u201d data structure to create sums of the positions of postings. This tools supports columns of Beancount\u2019s core types: Amount, Position and Inventory objects. (In any case, if you\u2019re not convinced, Beancount provides a tool to export its contents to a regular SQL database system. Feel free to experiment with it if you like, knock yourself out.) Please don\u2019t pay attention to the numbers in these large figures, they were randomly generated and don\u2019t reflect this. We\u2019re just interested in showing the structure, in these figures. \u21a9 Note that this is unrelated to the term \u201cclearing transactions\u201d which means acknowledging or marking that some transactions have been eyeballed by the bookkeeper and checked for correctness. \u21a9 Note that operations have nothing to do with the Open and Close directives Beancount provides. \u21a9 This is not provided yet in Beancount, but would be trivial to implement. All we'd need to do is invert the signs of balances from Liabilities, Income and Equity accounts. It's on the roadmap to provide this eventually. \u21a9","title":"The Table Perspective"},{"location":"tracking_medical_claims.html","text":"Tracking Out-of-Network Medical Claims in Beancount \uf0c1 Martin Blais - Updated: November 2023 Let's illustrate how one might handle dealing with medical treatment with insurance and HSA claims. Let's use an example of psychotherapy sessions, received out-of-network and paid upfront out of pocket and reimbursed later. When a session is received, it is booked to receivables and payables: 2023-09-06 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD 2023-09-11 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD Later on, some payments are made for it, clearing the payables: 2023-09-12 * \"ZELLE SENT\" #freud-2023-09 Assets:US:BofA:Checking -520.00 USD Liabilities:AccountsPayable:Psychotherapy 520.00 USD And so on, for the entire month: 2023-09-20 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD 2023-09-23 * \"ZELLE SENT\" #freud-2023-09 Assets:US:BofA:Checking -260.00 USD Liabilities:AccountsPayable:Psychotherapy 260.00 USD 2023-09-27 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD 2023-09-28 * \"ZELLE SENT\" #freud-2023-09 Assets:US:BofA:Checking -260.00 USD Liabilities:AccountsPayable:Psychotherapy 260.00 USD At the end of the month, a claim form is produced by the therapist. We file the claim with the insurance company, clearing the receivable and shifting the remaining portion to a insurance company check to be issued: 2023-09-28 * \"Claim for September filed with insurance\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy -1040.00 USD Expenses:Mental:Psychotherapy 312.00 USD Assets:AccountsReceivable:Anthem 728.00 USD Eventually, an EOB is produced to confirm how much is covered (in this example, 70%, which was known from the terms) and a check is received and deposited at the bank: 2023-10-24 * \"ATM CHECK DEPOSIT\" #freud-2023-09 Assets:US:BofA:Checking 728.00 USD Assets:AccountsReceivable:Anthem -728.00 USD At this stage we know the amount of the remaining portion eligible to be paid from the HSA, so we file a claim to the HSA company, once again booking them to a receivable and a payable: 2023-10-25 * \"HealthEquity\" \"Filed for reimbursement\" #freud-2023-09 Liabilities:AccountsPayable:HealthEquity -312.00 USD Assets:AccountsReceivable:HealthEquity 312.00 USD The HSA company makes a direct deposit to our checking account: 2023-11-01 * \"BofA bank (Claim ID:1234567-890); EFT to bank\" #freud-2023-09 Assets:US:HealthEquity:Cash -312.00 USD Liabilities:AccountsPayable:HealthEquity 312.00 USD And on the bank side when we import this transaction we book it against the receivable: 2023-11-01 * \"HEALTHEQUITY INC\" #freud-2023-09 Assets:US:BofA:Checking 312.00 USD Assets:AccountsReceivable:HealthEquity -312.00 USD The final result is that the HSA was used to cover the uninsured portion of the cost. |-- Assets | |-- AccountsReceivable | | |-- Anthem | | |-- HealthEquity | | `-- Psychotherapy | `-- US | |-- HealthEquity | | `-- Cash -312.00 USD | `-- BofA | `-- Checking |-- Expenses | `-- Mental | `-- Psychotherapy 312.00 USD `-- Liabilities `-- AccountsPayable |-- HealthEquity `-- Psychotherapy Net Income: (-312.00 USD) There are some flaws with the approach above: The amount covered by insurance is known ahead of time; in many cases the amount is not known before the EOB is issued by the insurance (after all that's what it's for, it's the \"Explanation of Benefits\"). This would require modifying the above.","title":"Tracking Out-of-Network Medical Claims in Beancount"},{"location":"tracking_medical_claims.html#tracking-out-of-network-medical-claims-in-beancount","text":"Martin Blais - Updated: November 2023 Let's illustrate how one might handle dealing with medical treatment with insurance and HSA claims. Let's use an example of psychotherapy sessions, received out-of-network and paid upfront out of pocket and reimbursed later. When a session is received, it is booked to receivables and payables: 2023-09-06 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD 2023-09-11 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD Later on, some payments are made for it, clearing the payables: 2023-09-12 * \"ZELLE SENT\" #freud-2023-09 Assets:US:BofA:Checking -520.00 USD Liabilities:AccountsPayable:Psychotherapy 520.00 USD And so on, for the entire month: 2023-09-20 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD 2023-09-23 * \"ZELLE SENT\" #freud-2023-09 Assets:US:BofA:Checking -260.00 USD Liabilities:AccountsPayable:Psychotherapy 260.00 USD 2023-09-27 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD 2023-09-28 * \"ZELLE SENT\" #freud-2023-09 Assets:US:BofA:Checking -260.00 USD Liabilities:AccountsPayable:Psychotherapy 260.00 USD At the end of the month, a claim form is produced by the therapist. We file the claim with the insurance company, clearing the receivable and shifting the remaining portion to a insurance company check to be issued: 2023-09-28 * \"Claim for September filed with insurance\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy -1040.00 USD Expenses:Mental:Psychotherapy 312.00 USD Assets:AccountsReceivable:Anthem 728.00 USD Eventually, an EOB is produced to confirm how much is covered (in this example, 70%, which was known from the terms) and a check is received and deposited at the bank: 2023-10-24 * \"ATM CHECK DEPOSIT\" #freud-2023-09 Assets:US:BofA:Checking 728.00 USD Assets:AccountsReceivable:Anthem -728.00 USD At this stage we know the amount of the remaining portion eligible to be paid from the HSA, so we file a claim to the HSA company, once again booking them to a receivable and a payable: 2023-10-25 * \"HealthEquity\" \"Filed for reimbursement\" #freud-2023-09 Liabilities:AccountsPayable:HealthEquity -312.00 USD Assets:AccountsReceivable:HealthEquity 312.00 USD The HSA company makes a direct deposit to our checking account: 2023-11-01 * \"BofA bank (Claim ID:1234567-890); EFT to bank\" #freud-2023-09 Assets:US:HealthEquity:Cash -312.00 USD Liabilities:AccountsPayable:HealthEquity 312.00 USD And on the bank side when we import this transaction we book it against the receivable: 2023-11-01 * \"HEALTHEQUITY INC\" #freud-2023-09 Assets:US:BofA:Checking 312.00 USD Assets:AccountsReceivable:HealthEquity -312.00 USD The final result is that the HSA was used to cover the uninsured portion of the cost. |-- Assets | |-- AccountsReceivable | | |-- Anthem | | |-- HealthEquity | | `-- Psychotherapy | `-- US | |-- HealthEquity | | `-- Cash -312.00 USD | `-- BofA | `-- Checking |-- Expenses | `-- Mental | `-- Psychotherapy 312.00 USD `-- Liabilities `-- AccountsPayable |-- HealthEquity `-- Psychotherapy Net Income: (-312.00 USD) There are some flaws with the approach above: The amount covered by insurance is known ahead of time; in many cases the amount is not known before the EOB is issued by the insurance (after all that's what it's for, it's the \"Explanation of Benefits\"). This would require modifying the above.","title":"Tracking Out-of-Network Medical Claims in Beancount"},{"location":"trading_with_beancount.html","text":"Trading with Beancount \uf0c1 Martin Blais , July 2014 http://furius.ca/beancount/doc/trading Introduction What is Profit and Loss? Realized and Unrealized P/L Trade Lots Booking Methods Dated lots Reporting Unrealized P/L Commissions Stock Splits Cost Basis Adjustments Dividends Average Cost Booking Future Topics Introduction \uf0c1 This is a companion document for the Command-Line Accounting Cookbook that deals exclusively with the subject of trading and investments in Beancount. You probably should have read an introduction to the double-entry method before reading this document. The subject of stock trading needs to be preceded by a discussion of \u201cprofit and loss,\u201d or P/L, for short (pronounce: \u201cP and L\u201d), also called capital gains or losses. The notion of P/L against multiple trades can be difficult for a novice to understand, and I\u2019ve even seen professional traders lack sophistication in their understanding of P/L over varying time periods. It is worth spending a bit of time to explain this, and necessary to understand how to book your trades in a double-entry system. This discussion will be weaved with detailed examples of how to book these trades in Beancount, wherever possible. There is a related, active proposal for improving the booking methods in Beancount that you might also be interested in. Discussions of basis for tax-deferred accounts will not be treated here, but in the more general cookbook. What is Profit and Loss? \uf0c1 Let\u2019s imagine you have an account at the E*Trade discount broker and you buy some shares of a company, say IBM. If you buy 10 shares of IBM when its price is 160$/share, it will cost you 1600$. That value is what we will call the \u201cbook value\u201d, or equivalently, \u201cthe cost.\u201d This is how much money you had to spend in order to acquire the shares, also called \u201cthe position.\u201d This is how you would enter this transaction in Beancount: 2014-02-16 * \"Buying some IBM\" Assets:US:ETrade:IBM 10 IBM {160.00 USD} Assets:US:ETrade:Cash -1600.00 USD In practice you will probably pay some commission to E*Trade for this service, so let\u2019s put that in for completeness: 2014-02-16 * \"Buying some IBM\" Assets:US:ETrade:IBM 10 IBM {160.00 USD} Assets:US:ETrade:Cash -1609.95 USD Expenses:Financial:Commissions 9.95 USD This is how you tell Beancount to deposit some units \u201cat cost\u201d, in this case, units of \u201cIBM at 160 USD/share\u201d cost. This transaction balances because the sum of its legs is zero: 10 x 160 + -1609.95 + 9.95 = 0. Also note that we\u2019re choosing to use a subaccount dedicated to the shares of IBM; this is not strictly necessary but it is convenient for reporting in, for example, a balance sheet, because it will naturally aggregate all of your shares of each of your positions on their own line. Having a \u201ccash\u201d subaccount also emphasizes that uninvested funds you have there are not providing any return. The next day, the market opens and IBM shares are going for 170$/share. In this context, we will call this \u201cthe price.\u201d 1 The \u201cmarket value\u201d of your position, your shares, is the number of them x the market price, that is, 10 shares x 170$/share = 1700$. The difference between these two amounts is what we will call the P/L: market value - book value = P/L 10 x 170$ - 10 x 160$ = 1700$ - 1600$ = 100$ (profit) We will call a positive amount \u201ca profit\u201d and if the amount is negative, \u201ca loss.\u201d Realized and Unrealized P/L \uf0c1 The profit from the previous section is called an \u201cunrealized profit.\u201d That is because the shares have not actually been sold yet - this is a hypothetical profit: if I can sell those shares at the market value, this is how much I would pocket. The 100$ I mentioned in the previous section is actually an \u201cunrealized P/L.\u201d So let\u2019s say you like this unrealized profit and you feel that it\u2019s temporary luck that IBM went up. You decide to sell 3 of these 10 shares to the market at 170$/share. The profit on these share will now be \u201crealized\u201d: market value - book value = P/L 3 x 170$ - 3 x 160$ = 3 x (170 - 160) = 30$ (profit) This 30$ is a \u201crealized P/L.\u201d The remaining portion of your position is still showing an unrealized profit, that is, the price could fluctuate some more until you sell it: market value - book value = P/L 7 x 170$ - 7 x 160$ = 70$ This is how you would book this partial sale of your position in Beancount (again including a commission): 2014-02-17 * \"Selling some IBM\" Assets:US:ETrade:IBM -3 IBM {160.00 USD} Assets:US:ETrade:Cash 500.05 USD Expenses:Financial:Commissions 9.95 USD Do you notice something funny going on here? -3 x 160 = -480, -480 + 500.05 + 9.95 = 30\u2026 This transaction does not balance to zero! The problem is that we received 510$ in cash in exchange for the 3 shares we sold. This is because the actual price we sold them at was 170$: 3 x 170 = 510$. This is where we need to account for the profit, by adding another leg which will absorb this profit, and conveniently enough, automatically calculate and track our profits for us: 2014-02-17 * \"Selling some IBM\" Assets:US:ETrade:IBM -3 IBM {160.00 USD} Assets:US:ETrade:Cash 500.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL The last leg will be automatically filled in by Beancount to -30 USD , as we\u2019re allowed one posting without an amount (and remember that in the double-entry system without credits and debits, a profit is a negative number for \u201cIncome\u201d accounts). This is the number the government is interested in for your taxes. In summary, you now have: A position of 7 \u201cshares at book value of 160$\u201d = 1120$ (its book value) A realized P/L of 30$ An unrealized P/L of 70$ Now at this point, some of you will jump up and down and say: \u201cBut wait, waiiit! I sold at 170$/share, not 160$/share, why do you put 160$ here?\u201d The answer is that you did not have shares held at 170$ to sell. In order to explain this, I need to make a little detour to explain how we keep track of things in accounts... So how do we keep track of these shares? It\u2019s actually easy: when Beancount stores things in accounts, we use something called \u201can inventory.\u201d Imagine that an \u201cinventory\u201d is a bag with the name of that account on it. Each account has one such bag to hold the things in the account at a particular point in time, the \u201cbalance\u201d of this account at that time. Imagine that the things it contains have a little label attached to each of them, with their cost, that is, the price that was paid to acquire them. Whenever you put a thing in the bag, you attach a new label to the thing. For things to work right, all things need to be labeled 2 . In our example, the bag contained 10 items of \u201cshares of IBM bought at 160$/share\u201d. The syntax we used to put the IBM in the account can seem a little misleading; we wrote: Assets:US:ETrade:IBM 10 IBM {160.00 USD} but really, this is understood by Beancount closer to the following syntax: Assets:US:ETrade:IBM 10 {IBM 160.00 USD} But \u2026 it would be annoying to write this, so we use a syntax more intuitive to humans. So the thing is, you can\u2019t subtract units of {IBM at 170.00 USD} ... because there just aren\u2019t any in that bag. What you have in the bag are units of {IBM at 160.00 USD} . You can only take out these ones. Now that being said, do you see how it\u2019s the amount that was exchanged to us for the shares that really helps us track the P/L? Nowhere did we actually need to indicate the price at which we sold the shares. It\u2019s the fact that we received a certain amount of cash that is different than the cost of the position we\u2019re selling that triggers the imbalance, which we book to a capital gain. Hmmm\u2026 Beancount maintains a price database, wouldn\u2019t it be nice to at least record and attach that price to the transaction for documentation purposes? Indeed. Beancount allows you to also attach a price to that posting, but for the purpose of balancing the transaction, it ignores it completely. It is mainly there for documentation, and you can use it if you write scripts. And if you use the beancount.plugins.implicit_prices plugin, it will be used to automatically synthesize a price entry that will enrich our historical price database, which may be used in reporting the market value of the account contents (more details on this follow). So the complete and final transaction for selling those shares should be: 2014-02-17 * \"Selling some IBM\" Assets:US:ETrade:IBM -3 IBM {160.00 USD} @ 170.00 USD Assets:US:ETrade:Cash 500.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Trade Lots \uf0c1 In practice, the reality of trading gets a tiny bit more complicated than this. You might decide to buy some IBM multiple times, and each time, it is likely that you would buy them at a different price. Let\u2019s see how this works with another example trade. Given your previous position of 7 shares held at 160$ cost, the following day you see that the price went up some more, you change your mind on IBM and decide to \u201cgo long\u201d and buy 5 more shares. The price you get is 180$/share this time: 2014-02-18 * \"I put my chips on big blue!\" Assets:US:ETrade:IBM 5 IBM {180.00 USD} Assets:US:ETrade:Cash -909.95 USD Expenses:Financial:Commissions 9.95 USD Now, what do we have in the bag for Assets:US:ETrade:IBM ? We have two kinds of things: 7 shares of \u201cIBM held at 160 USD/share\u201d, from the first trade 5 shares of \u201cIBM held at 180 USD/share\u201d, from this last trade We will call these \u201clots,\u201d or \u201ctrade lots.\u201d In fact, if you were to sell this entire position, say, a month later, the way to legally sell it in Beancount (that is, without issuing an error), is by specifying both legs. Say the price is 172$/share at that moment: 2014-03-18 * \"Selling all my blue chips.\" Assets:US:ETrade:IBM -7 IBM {160.00 USD} @ 172.00 USD Assets:US:ETrade:IBM -5 IBM {180.00 USD} Assets:US:ETrade:Cash 2054.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Now your final position of IBM would be 0 shares. Alternatively, since you\u2019re selling the entire position, Beancount should be able to unambiguously match all the lots against an unspecified cost. This is equivalent: 2014-03-18 * \"Selling all my blue chips.\" Assets:US:ETrade:IBM -12 IBM {} @ 172.00 USD Assets:US:ETrade:Cash 2054.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Note that this won\u2019t work if the total amount of shares doesn\u2019t match all the lots (this would be ambiguous\u2026 which subset of the lots should be chosen isn\u2019t obvious). Booking Methods \uf0c1 But what if you decided to sell only some of those shares? Say you need some cash to buy a gift to your loved one and you want to sell 4 shares this time. Say the price is now 175$/share. Now you have a choice to make. You can choose to sell the older shares and realize a larger profit: 2014-03-18 * \"Selling my older blue chips.\" Assets:US:ETrade:IBM -4 IBM {160.00 USD} @ 175.00 USD Assets:US:ETrade:Cash 690.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL ;; -60.00 USD (profit) Or you may choose to sell the most recently acquired ones and realize a loss: 2014-03-18 * \"Selling my most recent blue chips.\" Assets:US:ETrade:IBM -4 IBM {180.00 USD} @ 175.00 USD Assets:US:ETrade:Cash 690.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL ;; 20.00 USD (loss) Or you can choose to sell a mix of both: just use two legs. Note that in practice this choice will depend on a number of factors: The tax law of the jurisdiction where you trade the shares may have a defined method for how to book the shares and you may not actually have a choice. For example, they may state that you must trade the oldest lot you bought, a method called \u201cfirst-in-first out.\u201d If you have a choice, the various lots you\u2019re holding may have different taxation characteristics because you\u2019ve held them for a different period of time. In the USA, for example, positions held for more than one year benefit from a lower taxation rate (the \u201clong-term\u201d capital gains rate). You may have other gains or losses that you want to offset in order to minimize your cash flow requirements on your tax liability. This is sometimes called \u201c tax loss harvesting .\u201d There are more\u2026 but I\u2019m not going to elaborate on them here. My goal is to show you how to book these things with the double-entry method. Dated lots \uf0c1 We\u2019ve almost completed the whole picture of how this works. There is one more rather technical detail to add and it begins with a question: What if I bought multiple lots of share at the same price? As we alluded to in the previous section, the duration for which you held a position may have an impact on your taxation, even if the P/L ends up being the same. How do we differentiate between these lots? Well\u2026 I had simplified things a tiny bit earlier, just to make it simpler to understand. When we put positions in an inventory, on the label that we attach to the things we put in it, we also mark down the date that lot was acquired if you supply it. This is how you would book entering the position this way: 2014-05-20 * \"First trade\" Assets:US:ETrade:IBM 5 IBM {180.00 USD, 2014-05-20} Assets:US:ETrade:Cash -909.95 USD Expenses:Financial:Commissions 9.95 USD 2014-05-21 * \"Second trade\" Assets:US:ETrade:IBM 3 IBM {180.00 USD, 2014-05-21} Assets:US:ETrade:Cash -549.95 USD Expenses:Financial:Commissions 9.95 USD Now when you sell, you can do the same thing to disambiguate which lot\u2019s position you want to reduce: 2014-08-04 * \"Selling off first trade\" Assets:US:ETrade:IBM -5 IBM {180.00 USD, 2014-05-20} Assets:US:ETrade:Cash 815.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Note that it\u2019s really unlikely that your broker will provide the information in the downloadable CSV or OFX files from their website\u2026 you probably won\u2019t be able to automate the lot detail of this transaction, you might have to pick up the PDF trade confirmations your broker provides to enter this manually, if it ever happens. But how often does it happen that you buy two lots at the same price? I trade relatively frequently - about every two weeks - and in 8 years worth of data I don\u2019t have a single occurrence of it. In practice, unless you do thousands of trades per day- and Beancount isn\u2019t really designed to handle that kind of activity, at least not in the most efficient way - it just won\u2019t happen very much. ( Technical Detail : that we\u2019re working on bettering the mechanism for lot selection so that you never have to insert the lot-date yourself, and so that you could disambiguate lot selection by supplying a name instead. See upcoming changes.) Reporting Unrealized P/L \uf0c1 Okay, so our account balances are holding the cost of each unit, and that provides us with the book value of these positions. Nice. But what about viewing the market value? The market value of the positions is simply the number of units of these instruments x the market price at the time we\u2019re interested in. This price fluctuates. So we need the price. Beancount supports a type of entry called a price entry that allows you to tell it what the price of an instrument was at a particular point in time, e.g. 2014-05-25 price IBM 182.27 USD In order to keep Beancount simple and with few dependencies, the software does not automatically fetch these prices (you can check out LedgerHub for this purpose, or write your own script that will insert the latest prices in your input file if so desired\u2026 there are many libraries to fetch prices from the internet online). It only knows about market prices from all these price entries. Using these, it builds an in-memory historical database of prices over time and can query it to obtain the most current values. Instead of supporting different reporting modes with options, you can trigger the insertion of unrealized gains by enabling a plugin: plugin \"beancount.plugins.unrealized\" \"Unrealized\" This will create a synthetic transaction at the date of the last of directives, that reflects the unrealized P/L. It books one side as Income and the other side as a change in Asset: 2014-05-25 U \"Unrealized gain for 7 units of IBM (price: 182.2700 USD as of 2014-05-25, average cost: 160.0000 USD)\" Assets:US:ETrade:IBM:Unrealized 155.89 USD Income:US:ETrade:IBM:Unrealized -155.89 USD Note that I used an option in this example to specify a sub-account to book the unrealized gains to. The unrealized P/L shows up on a separate line in the balance sheet and the parent account should show the market value on its balance (which includes that of its sub-accounts). Commissions \uf0c1 So far we have not discussed trading commissions. Depending on the tax law that applies to you, the costs associated with trading may be deductible from the raw capital gain as we\u2019ve calculated it in the previous examples. These are considered expenses by the government, and it is often the case that you can deduct those trading commissions (it\u2019s entirely reasonable from their part, you did not pocket that money after all). In the examples above, the capital gains and commission expenses get tracked into two separate accounts. For example, you could end up with reported balances that look like this: Income:US:ETrade:PnL -645.02 USD Expenses:Financial:Commissions 39.80 USD (Just to be clear, this is to be interpreted as a profit of $645.02 and an expense of $39.80.) You could subtract these numbers to obtain an approximation of the P/L without costs: 645.02 - 39.80 = $605.22. However, this is only an approximation of the correct P/L value. To understand why, we need to look at an example where a partial number of shares are sold across a reporting period. Imagine that we have an account with a commission rate of $10 per trade, 100 shares of ITOT were bought in 2013, 40 of those shares were later sold in that same year, and the remaining 60 were sold the year after, a scenario that looks like this: 2013-09-01 Buy 100 ITOT at $80, commission = 10$ 2013-11-01 Sell 40 ITOT at $82, commission = 10$ 2014-02-01 Sell 60 ITOT at $84, commission = 10$ If you computed the sum of commissions paid at the end of 2013, you would have $20, and using the approximate method outlined previously, for so 2013 and 2014 you would declare 2013: P/L of 40 x ($82 - $80) - ($10 + $10) = $60 2014: P/L of 60 x ($84 - $80) - $10 = $230 However, strictly speaking, this is incorrect. The $10 commission paid on acquiring the 100 shares has to be pro-rated with respect to the number of shares sold. This means that on that first sale of 40 shares only 4$ of the commission is deductible: $10 x (40 shares / 100 shares), and so we obtain: 2013: P/L of 40 x ($82 - $80) - $(4 + 10) = $66 2014: P/L of 60 x ($84 - $80) - $(6 + 10) = $224 As you can see, the P/L declared for each year differs, even if the sum of the P/L for both years is the same ($290). A convenient method to automatically allocate the acquisition costs to the pro-rata value of the number of shares sold is to add the acquisition trading cost to the total book value of the position. In this example, you would say that the position of 100 shares has a book value $8010 instead of $8000: 100 share x $80/share + $10, or equivalently, that the individual shares have a book value of $80.10 each. This would result in the following calculation: 2013: P/L of 40 x ($82 - $80.10) - $10 = $66 2014: P/L of 60 x ($84 - $80.10) - $10 = $224 You could even go one step further and fold the commission on sale into the price of each share sold as well: 2013: P/L of 40 x ($81.75 - $80.10) = $66 2014: P/L of 60 x ($83.8333 - $80.10) = $224 This may seem overkill, but imagine that those costs were much higher, as is the case on large commercial transactions; the details do begin to matter to the tax man. Accurate accounting is important, and we need to develop a method to do this more precisely. We don\u2019t currently have a good method of doing this with our input syntax. A suitable method is currently being developed and a proposal is on the table. Also see mailing-list for details. [June 2014] Stock Splits \uf0c1 Stock splits are currently dealt with by emptying an account\u2019s positions and recreating the positions at a different price: 2004-12-21 * \"Autodesk stock splits\" Assets:US:MSSB:ADSK -100 ADSK {66.30 USD} Assets:US:MSSB:ADSK 200 ADSK {33.15 USD} The postings balance each other, so the rule is respected. As you can see, this requires no special syntax feature. It also handles more general scenarios, such as the odd split of the Google company that occurred on the NASDAQ exchange in April 2014, into two different classes of stock (voting and non-voting shares, at 50.08% and 49.92%, respectively): 2014-04-07 * \"Stock splits into voting and non-voting shares\" Assets:US:MSSB:GOOG -25 GOOG {1212.51 USD} ; Old GOOG Assets:US:MSSB:GOOG 25 GOOG { 605.2850 USD} ; New GOOG Assets:US:MSSB:GOOGL 25 GOOG { 607.2250 USD} Ultimately, maybe a plug-in module should be provided to more easily create such stock split transactions, as there is some amount of redundancy involved. We need to figure out the most general way to do this. But the above will work for now. One problem with this approach is that the continuity of the trade lots is lost, that is, the purchase date of each lot has now been reset as a result of the transaction above, and it becomes impossible to automatically figure out the duration of the trade and its associated impact on taxation, i.e. long-term vs. short-term trade. Even without this the profit is still calculated correctly, but it is an annoying detail nonetheless. One way to handle this is by using the Dated Lots (see the appropriate section of this doc). That way, the original trade date can be preserved on the new lots. This provides accurate timing information in addition to the capital gain/loss based on the price. Another method for solving this and for easily propagating the lot trade date has been proposed and will be implemented in Beancount later on. A more important problem with the current implementation is that the meaning of a unit of ADSK before and after the stock split is different. The price graph for this commodity unit will show a radical discontinuity! This is a more general problem that has yet to be addressed in both Beancount and Ledger. The Commodity Definition Changes document has a discussion to address this topic. Cost Basis Adjustment and Return of Capital \uf0c1 Readjustment in cost basis may occur in managed funds, due to the fund\u2019s internal trading activities. This will typically occur in tax-sheltered accounts where the gain that occurs from such an adjustment has no impact on taxes, and where the cost basis is held at the average cost of all shares in each position. If we have the specific lot prices being adjusted, it is doable to book these in the same manner as we dealt with stock splits: 2014-04-07 * \"Cost basis adjustment for XSP\" Assets:CA:RRSP:XSP -100 ADSK {21.10 CAD} Assets:CA:RRSP:XSP 100 ADSK {23.40 CAD} Income:CA:RRSP:Gains -230.00 CAD However, this is really uncommon. The more common case of this is of an account using the average cost booking method, we don\u2019t currently have a way to deal with this. There is an active proposal in place to make this possible. The cost basis adjustment is commonly found in Return of Capital events. These happen, for example, when funds are returning capital to the shareholders. This can be caused by winding down the operation. From the taxation point of view, these are non-taxable events and affect the cost basis of the equity in the fund. The number of shares might stay the same, but their cost basis needs to be adjusted for potential Gain/Loss calculation at the point of sale in the future. Dividends \uf0c1 Dividends don\u2019t pose a particular problem. They are just income. They can be received as cash: 2014-02-01 * \"Cash dividends received from mutual fund RBF1005\" Assets:Investments:Cash 171.02 CAD Income:Investments:Dividends Or they can be received as stock itself: 2014-02-01 * \"Stock dividends received in shares\" Assets:Investments:RBF1005 7.234 RBF1005 {23.64 CAD} Income:Investments:Dividends In the case of dividends received as stock, as for stock purchases, you provide the cost basis at which the dividend was received (this should be available in your statements). If the account is held at average cost, this posting will simply merge with the other legs at the time an average cost booking is needed to be performed. Average Cost Booking \uf0c1 At the moment, the only way to perform booking at average cost is painful: you would have to use the method outlined in the Stock Split section in order to revalue your inventory. This is impractical, however. There is an active proposal with an associated syntax to fully solve this problem. Once the proposal is implemented, it will look like this: 2014-02-01 * \"Selling 5 shares at market price 550 USD\" Assets:Investments:Stock -5 GOOG {*} Assets:Investments:Cash 2740.05 USD Expenses:Commissions 9.95 USD Income:Investments:CapitalGains Any posting with a cost of \u201c*\u201d acting on an inventory will select all the shares of that currency (GOOG), merge them into a single one at the average cost, and then reduce that position at this new average cost. Future Topics \uf0c1 I\u2019ll be handling the following topics later on: Mark-to-Market : Handling end-of-year mark-to-market for Section 1256 instruments (i.e., futures and options), by re-evaluating the cost basis. This is similar to a cost basis readjustment applied at the end of each year for all of these types of instruments. Short Sales : these require little changes. We just have to allow negative numbers of units held at cost. At the moment we spit a warning when units held at cost go negative in order to detect data entry errors, but it would be easy to extend the Open directive syntax to allow this to occur on specific accounts which can hold short sales, which should just show as negative shares. All the arithmetic should otherwise just work naturally. Interest payments on margins would show up as distinct transactions. Also, when you short the stock, you don\u2019t receive dividends for those positions, but rather you have to pay them out. You would have expense account for this, e.g., Expenses:StockLoans:Dividends . Trading Options : I have no idea how to do this at the moment, but I imagine these could be held like shares of stock, with no distinctions. I don\u2019t foresee any difficulty. Currency Trading : At the moment, I\u2019m not accounting for the positions in my FOREX accounts, just their P/L and interest payments. This poses interesting problems: Positions held in a FOREX account aren\u2019t just long or short the way that stocks are: they are actually offsetting two commodities at the same time. For example, a long position in USD/CAD should increase the exposure of USD and decrease the exposure in CAD, it can be seen as holding a long asset of USD and a short asset in CAD, at the same time. While it is possible to hold these positions as if they were distinct instruments (e.g., units of \u201cUSDCAD\u201d with disregard for its components) but for large positions, especially if held over long periods of time for hedging purposes, it is important to deal with this and somehow allow the user to reflect the net currency exposures of multiple currency positions against the rest of their assets and liabilities. We also need to deal with the gains generated by the closing of these positions: those generate a gain in the currency of the account, after conversion to this currency. For example, if you hold a currency account denominated in USD, and you go long EUR/JPY, when you close the position you will obtain a gain in EUR, and after conversion of the P/L from the EUR into the equivalent number of USD (via EUR/USD) the USD gain will be deposited in your account. This means that two rates are being used to estimate the current market value of any position: the differential between the current rate and the rate at the time of buying, and the rate of the base currency (e.g., EUR) in the account currency (e.g., USD). Some of these involve new features in Beancount, but some not. Ideas welcome. This is a misleading notion, however. In reality, there is no price , there exist only markets where you get a \u201chint\u201d of how much someone else might be willing to exchange your shares for (for different amounts to buy or to sell them, and for some limited number of them, we call this \u201ca market\u201d), but until you\u2019ve actually completed selling your shares, you don\u2019t really know precisely how much you will be able to execute that trade at, only an estimate. If you\u2019re not intimately familiar with trading, this should give you pause, and hopefully a big \u201cah-ha! \u201cmoment about how the world works - there really does not exist a price for anything in the world - but in the context of this discussion, let\u2019s make abstraction of this and assume that you can buy or sell as many shares as you have instantly on the markets at the middle price, such as Google Finance or Yahoo Finance or your broker would report it. The process of deciding that we would be able to sell the shares at 170$ each is called \u201cmarking\u201d, that is, under reasonable assumptions, we believe that we would be able to actually sell those shares at that price (the term of art \u201cmarking to market\u201d refers to the fact that we use the market price as the best indicator to our knowledge of our capability to realize the trade). For an individual buying and selling small amounts of shares that don\u2019t move the market and with an honest broker, this is mostly true in practice. \u21a9 As an aside, putting regular currencies in an account is just the degenerate case of a thing with an empty label. This is an implementation detail that works great in practice. \u21a9","title":"Trading with Beancount"},{"location":"trading_with_beancount.html#trading-with-beancount","text":"Martin Blais , July 2014 http://furius.ca/beancount/doc/trading Introduction What is Profit and Loss? Realized and Unrealized P/L Trade Lots Booking Methods Dated lots Reporting Unrealized P/L Commissions Stock Splits Cost Basis Adjustments Dividends Average Cost Booking Future Topics","title":"Trading with Beancount"},{"location":"trading_with_beancount.html#introduction","text":"This is a companion document for the Command-Line Accounting Cookbook that deals exclusively with the subject of trading and investments in Beancount. You probably should have read an introduction to the double-entry method before reading this document. The subject of stock trading needs to be preceded by a discussion of \u201cprofit and loss,\u201d or P/L, for short (pronounce: \u201cP and L\u201d), also called capital gains or losses. The notion of P/L against multiple trades can be difficult for a novice to understand, and I\u2019ve even seen professional traders lack sophistication in their understanding of P/L over varying time periods. It is worth spending a bit of time to explain this, and necessary to understand how to book your trades in a double-entry system. This discussion will be weaved with detailed examples of how to book these trades in Beancount, wherever possible. There is a related, active proposal for improving the booking methods in Beancount that you might also be interested in. Discussions of basis for tax-deferred accounts will not be treated here, but in the more general cookbook.","title":"Introduction"},{"location":"trading_with_beancount.html#what-is-profit-and-loss","text":"Let\u2019s imagine you have an account at the E*Trade discount broker and you buy some shares of a company, say IBM. If you buy 10 shares of IBM when its price is 160$/share, it will cost you 1600$. That value is what we will call the \u201cbook value\u201d, or equivalently, \u201cthe cost.\u201d This is how much money you had to spend in order to acquire the shares, also called \u201cthe position.\u201d This is how you would enter this transaction in Beancount: 2014-02-16 * \"Buying some IBM\" Assets:US:ETrade:IBM 10 IBM {160.00 USD} Assets:US:ETrade:Cash -1600.00 USD In practice you will probably pay some commission to E*Trade for this service, so let\u2019s put that in for completeness: 2014-02-16 * \"Buying some IBM\" Assets:US:ETrade:IBM 10 IBM {160.00 USD} Assets:US:ETrade:Cash -1609.95 USD Expenses:Financial:Commissions 9.95 USD This is how you tell Beancount to deposit some units \u201cat cost\u201d, in this case, units of \u201cIBM at 160 USD/share\u201d cost. This transaction balances because the sum of its legs is zero: 10 x 160 + -1609.95 + 9.95 = 0. Also note that we\u2019re choosing to use a subaccount dedicated to the shares of IBM; this is not strictly necessary but it is convenient for reporting in, for example, a balance sheet, because it will naturally aggregate all of your shares of each of your positions on their own line. Having a \u201ccash\u201d subaccount also emphasizes that uninvested funds you have there are not providing any return. The next day, the market opens and IBM shares are going for 170$/share. In this context, we will call this \u201cthe price.\u201d 1 The \u201cmarket value\u201d of your position, your shares, is the number of them x the market price, that is, 10 shares x 170$/share = 1700$. The difference between these two amounts is what we will call the P/L: market value - book value = P/L 10 x 170$ - 10 x 160$ = 1700$ - 1600$ = 100$ (profit) We will call a positive amount \u201ca profit\u201d and if the amount is negative, \u201ca loss.\u201d","title":"What is Profit and Loss?"},{"location":"trading_with_beancount.html#realized-and-unrealized-pl","text":"The profit from the previous section is called an \u201cunrealized profit.\u201d That is because the shares have not actually been sold yet - this is a hypothetical profit: if I can sell those shares at the market value, this is how much I would pocket. The 100$ I mentioned in the previous section is actually an \u201cunrealized P/L.\u201d So let\u2019s say you like this unrealized profit and you feel that it\u2019s temporary luck that IBM went up. You decide to sell 3 of these 10 shares to the market at 170$/share. The profit on these share will now be \u201crealized\u201d: market value - book value = P/L 3 x 170$ - 3 x 160$ = 3 x (170 - 160) = 30$ (profit) This 30$ is a \u201crealized P/L.\u201d The remaining portion of your position is still showing an unrealized profit, that is, the price could fluctuate some more until you sell it: market value - book value = P/L 7 x 170$ - 7 x 160$ = 70$ This is how you would book this partial sale of your position in Beancount (again including a commission): 2014-02-17 * \"Selling some IBM\" Assets:US:ETrade:IBM -3 IBM {160.00 USD} Assets:US:ETrade:Cash 500.05 USD Expenses:Financial:Commissions 9.95 USD Do you notice something funny going on here? -3 x 160 = -480, -480 + 500.05 + 9.95 = 30\u2026 This transaction does not balance to zero! The problem is that we received 510$ in cash in exchange for the 3 shares we sold. This is because the actual price we sold them at was 170$: 3 x 170 = 510$. This is where we need to account for the profit, by adding another leg which will absorb this profit, and conveniently enough, automatically calculate and track our profits for us: 2014-02-17 * \"Selling some IBM\" Assets:US:ETrade:IBM -3 IBM {160.00 USD} Assets:US:ETrade:Cash 500.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL The last leg will be automatically filled in by Beancount to -30 USD , as we\u2019re allowed one posting without an amount (and remember that in the double-entry system without credits and debits, a profit is a negative number for \u201cIncome\u201d accounts). This is the number the government is interested in for your taxes. In summary, you now have: A position of 7 \u201cshares at book value of 160$\u201d = 1120$ (its book value) A realized P/L of 30$ An unrealized P/L of 70$ Now at this point, some of you will jump up and down and say: \u201cBut wait, waiiit! I sold at 170$/share, not 160$/share, why do you put 160$ here?\u201d The answer is that you did not have shares held at 170$ to sell. In order to explain this, I need to make a little detour to explain how we keep track of things in accounts... So how do we keep track of these shares? It\u2019s actually easy: when Beancount stores things in accounts, we use something called \u201can inventory.\u201d Imagine that an \u201cinventory\u201d is a bag with the name of that account on it. Each account has one such bag to hold the things in the account at a particular point in time, the \u201cbalance\u201d of this account at that time. Imagine that the things it contains have a little label attached to each of them, with their cost, that is, the price that was paid to acquire them. Whenever you put a thing in the bag, you attach a new label to the thing. For things to work right, all things need to be labeled 2 . In our example, the bag contained 10 items of \u201cshares of IBM bought at 160$/share\u201d. The syntax we used to put the IBM in the account can seem a little misleading; we wrote: Assets:US:ETrade:IBM 10 IBM {160.00 USD} but really, this is understood by Beancount closer to the following syntax: Assets:US:ETrade:IBM 10 {IBM 160.00 USD} But \u2026 it would be annoying to write this, so we use a syntax more intuitive to humans. So the thing is, you can\u2019t subtract units of {IBM at 170.00 USD} ... because there just aren\u2019t any in that bag. What you have in the bag are units of {IBM at 160.00 USD} . You can only take out these ones. Now that being said, do you see how it\u2019s the amount that was exchanged to us for the shares that really helps us track the P/L? Nowhere did we actually need to indicate the price at which we sold the shares. It\u2019s the fact that we received a certain amount of cash that is different than the cost of the position we\u2019re selling that triggers the imbalance, which we book to a capital gain. Hmmm\u2026 Beancount maintains a price database, wouldn\u2019t it be nice to at least record and attach that price to the transaction for documentation purposes? Indeed. Beancount allows you to also attach a price to that posting, but for the purpose of balancing the transaction, it ignores it completely. It is mainly there for documentation, and you can use it if you write scripts. And if you use the beancount.plugins.implicit_prices plugin, it will be used to automatically synthesize a price entry that will enrich our historical price database, which may be used in reporting the market value of the account contents (more details on this follow). So the complete and final transaction for selling those shares should be: 2014-02-17 * \"Selling some IBM\" Assets:US:ETrade:IBM -3 IBM {160.00 USD} @ 170.00 USD Assets:US:ETrade:Cash 500.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL","title":"Realized and Unrealized P/L"},{"location":"trading_with_beancount.html#trade-lots","text":"In practice, the reality of trading gets a tiny bit more complicated than this. You might decide to buy some IBM multiple times, and each time, it is likely that you would buy them at a different price. Let\u2019s see how this works with another example trade. Given your previous position of 7 shares held at 160$ cost, the following day you see that the price went up some more, you change your mind on IBM and decide to \u201cgo long\u201d and buy 5 more shares. The price you get is 180$/share this time: 2014-02-18 * \"I put my chips on big blue!\" Assets:US:ETrade:IBM 5 IBM {180.00 USD} Assets:US:ETrade:Cash -909.95 USD Expenses:Financial:Commissions 9.95 USD Now, what do we have in the bag for Assets:US:ETrade:IBM ? We have two kinds of things: 7 shares of \u201cIBM held at 160 USD/share\u201d, from the first trade 5 shares of \u201cIBM held at 180 USD/share\u201d, from this last trade We will call these \u201clots,\u201d or \u201ctrade lots.\u201d In fact, if you were to sell this entire position, say, a month later, the way to legally sell it in Beancount (that is, without issuing an error), is by specifying both legs. Say the price is 172$/share at that moment: 2014-03-18 * \"Selling all my blue chips.\" Assets:US:ETrade:IBM -7 IBM {160.00 USD} @ 172.00 USD Assets:US:ETrade:IBM -5 IBM {180.00 USD} Assets:US:ETrade:Cash 2054.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Now your final position of IBM would be 0 shares. Alternatively, since you\u2019re selling the entire position, Beancount should be able to unambiguously match all the lots against an unspecified cost. This is equivalent: 2014-03-18 * \"Selling all my blue chips.\" Assets:US:ETrade:IBM -12 IBM {} @ 172.00 USD Assets:US:ETrade:Cash 2054.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Note that this won\u2019t work if the total amount of shares doesn\u2019t match all the lots (this would be ambiguous\u2026 which subset of the lots should be chosen isn\u2019t obvious).","title":"Trade Lots"},{"location":"trading_with_beancount.html#booking-methods","text":"But what if you decided to sell only some of those shares? Say you need some cash to buy a gift to your loved one and you want to sell 4 shares this time. Say the price is now 175$/share. Now you have a choice to make. You can choose to sell the older shares and realize a larger profit: 2014-03-18 * \"Selling my older blue chips.\" Assets:US:ETrade:IBM -4 IBM {160.00 USD} @ 175.00 USD Assets:US:ETrade:Cash 690.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL ;; -60.00 USD (profit) Or you may choose to sell the most recently acquired ones and realize a loss: 2014-03-18 * \"Selling my most recent blue chips.\" Assets:US:ETrade:IBM -4 IBM {180.00 USD} @ 175.00 USD Assets:US:ETrade:Cash 690.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL ;; 20.00 USD (loss) Or you can choose to sell a mix of both: just use two legs. Note that in practice this choice will depend on a number of factors: The tax law of the jurisdiction where you trade the shares may have a defined method for how to book the shares and you may not actually have a choice. For example, they may state that you must trade the oldest lot you bought, a method called \u201cfirst-in-first out.\u201d If you have a choice, the various lots you\u2019re holding may have different taxation characteristics because you\u2019ve held them for a different period of time. In the USA, for example, positions held for more than one year benefit from a lower taxation rate (the \u201clong-term\u201d capital gains rate). You may have other gains or losses that you want to offset in order to minimize your cash flow requirements on your tax liability. This is sometimes called \u201c tax loss harvesting .\u201d There are more\u2026 but I\u2019m not going to elaborate on them here. My goal is to show you how to book these things with the double-entry method.","title":"Booking Methods"},{"location":"trading_with_beancount.html#dated-lots","text":"We\u2019ve almost completed the whole picture of how this works. There is one more rather technical detail to add and it begins with a question: What if I bought multiple lots of share at the same price? As we alluded to in the previous section, the duration for which you held a position may have an impact on your taxation, even if the P/L ends up being the same. How do we differentiate between these lots? Well\u2026 I had simplified things a tiny bit earlier, just to make it simpler to understand. When we put positions in an inventory, on the label that we attach to the things we put in it, we also mark down the date that lot was acquired if you supply it. This is how you would book entering the position this way: 2014-05-20 * \"First trade\" Assets:US:ETrade:IBM 5 IBM {180.00 USD, 2014-05-20} Assets:US:ETrade:Cash -909.95 USD Expenses:Financial:Commissions 9.95 USD 2014-05-21 * \"Second trade\" Assets:US:ETrade:IBM 3 IBM {180.00 USD, 2014-05-21} Assets:US:ETrade:Cash -549.95 USD Expenses:Financial:Commissions 9.95 USD Now when you sell, you can do the same thing to disambiguate which lot\u2019s position you want to reduce: 2014-08-04 * \"Selling off first trade\" Assets:US:ETrade:IBM -5 IBM {180.00 USD, 2014-05-20} Assets:US:ETrade:Cash 815.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Note that it\u2019s really unlikely that your broker will provide the information in the downloadable CSV or OFX files from their website\u2026 you probably won\u2019t be able to automate the lot detail of this transaction, you might have to pick up the PDF trade confirmations your broker provides to enter this manually, if it ever happens. But how often does it happen that you buy two lots at the same price? I trade relatively frequently - about every two weeks - and in 8 years worth of data I don\u2019t have a single occurrence of it. In practice, unless you do thousands of trades per day- and Beancount isn\u2019t really designed to handle that kind of activity, at least not in the most efficient way - it just won\u2019t happen very much. ( Technical Detail : that we\u2019re working on bettering the mechanism for lot selection so that you never have to insert the lot-date yourself, and so that you could disambiguate lot selection by supplying a name instead. See upcoming changes.)","title":"Dated lots"},{"location":"trading_with_beancount.html#reporting-unrealized-pl","text":"Okay, so our account balances are holding the cost of each unit, and that provides us with the book value of these positions. Nice. But what about viewing the market value? The market value of the positions is simply the number of units of these instruments x the market price at the time we\u2019re interested in. This price fluctuates. So we need the price. Beancount supports a type of entry called a price entry that allows you to tell it what the price of an instrument was at a particular point in time, e.g. 2014-05-25 price IBM 182.27 USD In order to keep Beancount simple and with few dependencies, the software does not automatically fetch these prices (you can check out LedgerHub for this purpose, or write your own script that will insert the latest prices in your input file if so desired\u2026 there are many libraries to fetch prices from the internet online). It only knows about market prices from all these price entries. Using these, it builds an in-memory historical database of prices over time and can query it to obtain the most current values. Instead of supporting different reporting modes with options, you can trigger the insertion of unrealized gains by enabling a plugin: plugin \"beancount.plugins.unrealized\" \"Unrealized\" This will create a synthetic transaction at the date of the last of directives, that reflects the unrealized P/L. It books one side as Income and the other side as a change in Asset: 2014-05-25 U \"Unrealized gain for 7 units of IBM (price: 182.2700 USD as of 2014-05-25, average cost: 160.0000 USD)\" Assets:US:ETrade:IBM:Unrealized 155.89 USD Income:US:ETrade:IBM:Unrealized -155.89 USD Note that I used an option in this example to specify a sub-account to book the unrealized gains to. The unrealized P/L shows up on a separate line in the balance sheet and the parent account should show the market value on its balance (which includes that of its sub-accounts).","title":"Reporting Unrealized P/L"},{"location":"trading_with_beancount.html#commissions","text":"So far we have not discussed trading commissions. Depending on the tax law that applies to you, the costs associated with trading may be deductible from the raw capital gain as we\u2019ve calculated it in the previous examples. These are considered expenses by the government, and it is often the case that you can deduct those trading commissions (it\u2019s entirely reasonable from their part, you did not pocket that money after all). In the examples above, the capital gains and commission expenses get tracked into two separate accounts. For example, you could end up with reported balances that look like this: Income:US:ETrade:PnL -645.02 USD Expenses:Financial:Commissions 39.80 USD (Just to be clear, this is to be interpreted as a profit of $645.02 and an expense of $39.80.) You could subtract these numbers to obtain an approximation of the P/L without costs: 645.02 - 39.80 = $605.22. However, this is only an approximation of the correct P/L value. To understand why, we need to look at an example where a partial number of shares are sold across a reporting period. Imagine that we have an account with a commission rate of $10 per trade, 100 shares of ITOT were bought in 2013, 40 of those shares were later sold in that same year, and the remaining 60 were sold the year after, a scenario that looks like this: 2013-09-01 Buy 100 ITOT at $80, commission = 10$ 2013-11-01 Sell 40 ITOT at $82, commission = 10$ 2014-02-01 Sell 60 ITOT at $84, commission = 10$ If you computed the sum of commissions paid at the end of 2013, you would have $20, and using the approximate method outlined previously, for so 2013 and 2014 you would declare 2013: P/L of 40 x ($82 - $80) - ($10 + $10) = $60 2014: P/L of 60 x ($84 - $80) - $10 = $230 However, strictly speaking, this is incorrect. The $10 commission paid on acquiring the 100 shares has to be pro-rated with respect to the number of shares sold. This means that on that first sale of 40 shares only 4$ of the commission is deductible: $10 x (40 shares / 100 shares), and so we obtain: 2013: P/L of 40 x ($82 - $80) - $(4 + 10) = $66 2014: P/L of 60 x ($84 - $80) - $(6 + 10) = $224 As you can see, the P/L declared for each year differs, even if the sum of the P/L for both years is the same ($290). A convenient method to automatically allocate the acquisition costs to the pro-rata value of the number of shares sold is to add the acquisition trading cost to the total book value of the position. In this example, you would say that the position of 100 shares has a book value $8010 instead of $8000: 100 share x $80/share + $10, or equivalently, that the individual shares have a book value of $80.10 each. This would result in the following calculation: 2013: P/L of 40 x ($82 - $80.10) - $10 = $66 2014: P/L of 60 x ($84 - $80.10) - $10 = $224 You could even go one step further and fold the commission on sale into the price of each share sold as well: 2013: P/L of 40 x ($81.75 - $80.10) = $66 2014: P/L of 60 x ($83.8333 - $80.10) = $224 This may seem overkill, but imagine that those costs were much higher, as is the case on large commercial transactions; the details do begin to matter to the tax man. Accurate accounting is important, and we need to develop a method to do this more precisely. We don\u2019t currently have a good method of doing this with our input syntax. A suitable method is currently being developed and a proposal is on the table. Also see mailing-list for details. [June 2014]","title":"Commissions"},{"location":"trading_with_beancount.html#stock-splits","text":"Stock splits are currently dealt with by emptying an account\u2019s positions and recreating the positions at a different price: 2004-12-21 * \"Autodesk stock splits\" Assets:US:MSSB:ADSK -100 ADSK {66.30 USD} Assets:US:MSSB:ADSK 200 ADSK {33.15 USD} The postings balance each other, so the rule is respected. As you can see, this requires no special syntax feature. It also handles more general scenarios, such as the odd split of the Google company that occurred on the NASDAQ exchange in April 2014, into two different classes of stock (voting and non-voting shares, at 50.08% and 49.92%, respectively): 2014-04-07 * \"Stock splits into voting and non-voting shares\" Assets:US:MSSB:GOOG -25 GOOG {1212.51 USD} ; Old GOOG Assets:US:MSSB:GOOG 25 GOOG { 605.2850 USD} ; New GOOG Assets:US:MSSB:GOOGL 25 GOOG { 607.2250 USD} Ultimately, maybe a plug-in module should be provided to more easily create such stock split transactions, as there is some amount of redundancy involved. We need to figure out the most general way to do this. But the above will work for now. One problem with this approach is that the continuity of the trade lots is lost, that is, the purchase date of each lot has now been reset as a result of the transaction above, and it becomes impossible to automatically figure out the duration of the trade and its associated impact on taxation, i.e. long-term vs. short-term trade. Even without this the profit is still calculated correctly, but it is an annoying detail nonetheless. One way to handle this is by using the Dated Lots (see the appropriate section of this doc). That way, the original trade date can be preserved on the new lots. This provides accurate timing information in addition to the capital gain/loss based on the price. Another method for solving this and for easily propagating the lot trade date has been proposed and will be implemented in Beancount later on. A more important problem with the current implementation is that the meaning of a unit of ADSK before and after the stock split is different. The price graph for this commodity unit will show a radical discontinuity! This is a more general problem that has yet to be addressed in both Beancount and Ledger. The Commodity Definition Changes document has a discussion to address this topic.","title":"Stock Splits"},{"location":"trading_with_beancount.html#cost-basis-adjustment-and-return-of-capital","text":"Readjustment in cost basis may occur in managed funds, due to the fund\u2019s internal trading activities. This will typically occur in tax-sheltered accounts where the gain that occurs from such an adjustment has no impact on taxes, and where the cost basis is held at the average cost of all shares in each position. If we have the specific lot prices being adjusted, it is doable to book these in the same manner as we dealt with stock splits: 2014-04-07 * \"Cost basis adjustment for XSP\" Assets:CA:RRSP:XSP -100 ADSK {21.10 CAD} Assets:CA:RRSP:XSP 100 ADSK {23.40 CAD} Income:CA:RRSP:Gains -230.00 CAD However, this is really uncommon. The more common case of this is of an account using the average cost booking method, we don\u2019t currently have a way to deal with this. There is an active proposal in place to make this possible. The cost basis adjustment is commonly found in Return of Capital events. These happen, for example, when funds are returning capital to the shareholders. This can be caused by winding down the operation. From the taxation point of view, these are non-taxable events and affect the cost basis of the equity in the fund. The number of shares might stay the same, but their cost basis needs to be adjusted for potential Gain/Loss calculation at the point of sale in the future.","title":"Cost Basis Adjustment and Return of Capital"},{"location":"trading_with_beancount.html#dividends","text":"Dividends don\u2019t pose a particular problem. They are just income. They can be received as cash: 2014-02-01 * \"Cash dividends received from mutual fund RBF1005\" Assets:Investments:Cash 171.02 CAD Income:Investments:Dividends Or they can be received as stock itself: 2014-02-01 * \"Stock dividends received in shares\" Assets:Investments:RBF1005 7.234 RBF1005 {23.64 CAD} Income:Investments:Dividends In the case of dividends received as stock, as for stock purchases, you provide the cost basis at which the dividend was received (this should be available in your statements). If the account is held at average cost, this posting will simply merge with the other legs at the time an average cost booking is needed to be performed.","title":"Dividends"},{"location":"trading_with_beancount.html#average-cost-booking","text":"At the moment, the only way to perform booking at average cost is painful: you would have to use the method outlined in the Stock Split section in order to revalue your inventory. This is impractical, however. There is an active proposal with an associated syntax to fully solve this problem. Once the proposal is implemented, it will look like this: 2014-02-01 * \"Selling 5 shares at market price 550 USD\" Assets:Investments:Stock -5 GOOG {*} Assets:Investments:Cash 2740.05 USD Expenses:Commissions 9.95 USD Income:Investments:CapitalGains Any posting with a cost of \u201c*\u201d acting on an inventory will select all the shares of that currency (GOOG), merge them into a single one at the average cost, and then reduce that position at this new average cost.","title":"Average Cost Booking"},{"location":"trading_with_beancount.html#future-topics","text":"I\u2019ll be handling the following topics later on: Mark-to-Market : Handling end-of-year mark-to-market for Section 1256 instruments (i.e., futures and options), by re-evaluating the cost basis. This is similar to a cost basis readjustment applied at the end of each year for all of these types of instruments. Short Sales : these require little changes. We just have to allow negative numbers of units held at cost. At the moment we spit a warning when units held at cost go negative in order to detect data entry errors, but it would be easy to extend the Open directive syntax to allow this to occur on specific accounts which can hold short sales, which should just show as negative shares. All the arithmetic should otherwise just work naturally. Interest payments on margins would show up as distinct transactions. Also, when you short the stock, you don\u2019t receive dividends for those positions, but rather you have to pay them out. You would have expense account for this, e.g., Expenses:StockLoans:Dividends . Trading Options : I have no idea how to do this at the moment, but I imagine these could be held like shares of stock, with no distinctions. I don\u2019t foresee any difficulty. Currency Trading : At the moment, I\u2019m not accounting for the positions in my FOREX accounts, just their P/L and interest payments. This poses interesting problems: Positions held in a FOREX account aren\u2019t just long or short the way that stocks are: they are actually offsetting two commodities at the same time. For example, a long position in USD/CAD should increase the exposure of USD and decrease the exposure in CAD, it can be seen as holding a long asset of USD and a short asset in CAD, at the same time. While it is possible to hold these positions as if they were distinct instruments (e.g., units of \u201cUSDCAD\u201d with disregard for its components) but for large positions, especially if held over long periods of time for hedging purposes, it is important to deal with this and somehow allow the user to reflect the net currency exposures of multiple currency positions against the rest of their assets and liabilities. We also need to deal with the gains generated by the closing of these positions: those generate a gain in the currency of the account, after conversion to this currency. For example, if you hold a currency account denominated in USD, and you go long EUR/JPY, when you close the position you will obtain a gain in EUR, and after conversion of the P/L from the EUR into the equivalent number of USD (via EUR/USD) the USD gain will be deposited in your account. This means that two rates are being used to estimate the current market value of any position: the differential between the current rate and the rate at the time of buying, and the rate of the base currency (e.g., EUR) in the account currency (e.g., USD). Some of these involve new features in Beancount, but some not. Ideas welcome. This is a misleading notion, however. In reality, there is no price , there exist only markets where you get a \u201chint\u201d of how much someone else might be willing to exchange your shares for (for different amounts to buy or to sell them, and for some limited number of them, we call this \u201ca market\u201d), but until you\u2019ve actually completed selling your shares, you don\u2019t really know precisely how much you will be able to execute that trade at, only an estimate. If you\u2019re not intimately familiar with trading, this should give you pause, and hopefully a big \u201cah-ha! \u201cmoment about how the world works - there really does not exist a price for anything in the world - but in the context of this discussion, let\u2019s make abstraction of this and assume that you can buy or sell as many shares as you have instantly on the markets at the middle price, such as Google Finance or Yahoo Finance or your broker would report it. The process of deciding that we would be able to sell the shares at 170$ each is called \u201cmarking\u201d, that is, under reasonable assumptions, we believe that we would be able to actually sell those shares at that price (the term of art \u201cmarking to market\u201d refers to the fact that we use the market price as the best indicator to our knowledge of our capability to realize the trade). For an individual buying and selling small amounts of shares that don\u2019t move the market and with an honest broker, this is mostly true in practice. \u21a9 As an aside, putting regular currencies in an account is just the degenerate case of a thing with an empty label. This is an implementation detail that works great in practice. \u21a9","title":"Future Topics"},{"location":"tutorial_example.html","text":"Beancount Example & Tutorial \uf0c1 Martin Blais , October 2014 http://furius.ca/beancount/doc/example Example File Generator Converting to Ledger Input Profile of Example User Future Additions Tutorial Generate an Example File Generating Reports Generating Balances Generating a Balance Sheet and Income Statement Journals Holdings Other Reports Other Formats Viewing Reports through the Web Interface The Future of Beancount Reports Example File Generator \uf0c1 Beancount has a command to generate a few years of a realistic user\u2019s historical entries: bean-example > example.beancount The script checks if the output fails to process cleanly in Beancount (if this occurs, try rerunning or contact the author at blais@furius.ca ). Note that there is an option to fix the random generator\u2019s seed to be used to generate the entries. You can generate multiple years of ledger data for this user; see available options with bean-example --help The purpose of the script is: To provide a realistic corpus of anonymous data for users to experiment with, to kick the tires of Beancount, generate reports, etc. To compare reports with those generated by Ledger. As an example file to be used in this tutorial. As input for stress testing Beancount. Converting to Ledger Input \uf0c1 It should be possible to convert example files generated from the script to Ledger syntax, like this: bean-report example.beancount ledger > example.lgr If you encounter any problems with the conversion, please let me know. While I have an automated test to check that the conversion succeeds, I\u2019m focusing my efforts on Beancount itself and I\u2019m not using Ledger much other than for the occasional comparison or for illustrating something on the mailing-list. Profile of Example User \uf0c1 Here\u2019s a high-level profile of this user. In the text that follows, I\u2019ll use the masculine \u201che\u201d for simplicity\u2019s sake. He lives somewhere in the USA and uses a single operating currency: \u201cUSD\u201d. He rents an apartment. Income : He is a software engineer (sorry I could not resist), receives a modest salary income from a software company. He is paid bi-weekly. He maximizes contributions to his company\u2019s 401k plan. His company offers a match to his contributions. His salary is paid as a direct deposit to his checking account. He benefits from and pays premiums for a health insurance plan provided by his employer. He tracks his vacation hours explicitly. Expenses : Checking account : He pays his apartment\u2019s rent by check. He also pays his electricity, internet ISP and mobile phone bills directly from his checking account. His bank also takes a monthly bank fee. Credit cards : He also has other regular expenses, such as groceries and restaurant outings with friends, which he pays from his credit card. Assets : Banking : He has an account at a major US bank. Retirement account : His 401k account is held at Fidelity or Vanguard and is allocated in its entirety to two funds, a mix of stocks and bonds. There are never any sales in this account. This account is held at average cost. Other investments : Extra money that he is able to save is transferred from his checking account towards a taxable investment account in which he purchases a mix of stocks and ETFs, and occasionally makes sales and realizes profits. Liabilities : Credit cards : He has one or two credit cards issued by another bank. Events/Tags : He takes a short vacation or two per year, where he travels somewhere fun. Future Additions \uf0c1 Note that as I beef up the example generator script, this profile will increase in complexity. If there is a particular situation from the cookbook or otherwise that you\u2019d like to see in the example file, please let me know (a patch is the best way to let me know). Generate fake PDF files for documents (from an option) along with suitable document directives for them, most of them implicit but some explicit. Generate more expenses: Some amount of realistic cash expenses. Add a vehicle: car repairs, gas, etc. Remove tram example, it is too urban-specific. Add donations for charity. Add a second credit card and vary sources of expenses. Add some real uses for links, for the 401k match, for instance, the employer contributions should be linked with the employee contributions. Or other ones. Generate a loan and payments for a student loan. Having a large liability is a common case, I\u2019d like to represent that, even if it\u2019s not a mortgage. Convert retirement accounts to book using the average cost method, and add corresponding fees which would otherwise be impossible to track with explicit lots. Add tracking of health care expenses as per the cookbook. Add foreign accounts in another currency and transactions and transfers in that currency. Tutorial \uf0c1 This section provides basic examples of generating reports with Beancount, on the typical example file that would be output by it. The list of reports here is not meant to be exhaustive. You may follow the tutorial and generate your own example file, or look at the files from beancount/examples/tutorial if you prefer. Generate an Example File \uf0c1 Begin by generating the example file: bean-example > example.beancount Open example.beancount and examine it. Next, before we begin generating reports, verify that the file loads without any errors: bean-check example.beancount It should return quietly, without outputting anything (bean-check only writes errors when there are some, otherwise on success it writes nothing). Generating Reports \uf0c1 Let\u2019s generate a report of the final balances of all accounts [output] : bean-report example.beancount balances As you can see, the bean-report script has subcommands for the various reports it generates. To list the available reports, use --help-reports [output] : bean-report --help-reports To list the options available for a particular report, use --help on it [output] : bean-report example.beancount balances --help The report-specific options vary for each report type. You can also list the global options for the script [output] : bean-report --help The global options allow you to specify the desired output format for the reports. To see which formats are available for which report, use --help-formats [output] : bean-report --help-formats Generating Balances \uf0c1 Good, so we know how to generate a report of balances for all accounts. This is a pretty detailed list of accounts though. Let\u2019s just restrict the output to the accounts that we\u2019re interested in [output] : bean-report example.beancount balances -e ETrade Here you can view the number of units held in each of the investment subaccounts. To render the cost, use the --cost option [output] : bean-report example.beancount balances -e ETrade --cost Formatting Tools \uf0c1 Sometimes it\u2019s nice to render a hierarchical list of accounts as a tree. You can use the \u201ctreeify\u201d tool provided by Beancount to do this: bean-report example.beancount balances | treeify This tool will work on any column of data that looks like a column of account names (you can also configure it work with filenames as well, or other patterns). Generating a Balance Sheet and Income Statement \uf0c1 Let us generate a balance sheet: bean-report example.beancount balsheet Unfortunately, the only output format supported for it at this point is HTML. Also, filtering balance sheet entries from the command-line is not supported. Generate this to a file and open a browser to it [output] : bean-report example.beancount balsheet > /tmp/balsheet.html You can to the same for income statements: bean-report example.beancount income > /tmp/income.html Journals \uf0c1 You can also generate journals (in Ledger parlance, these are \u201cregisters\u201d). Let\u2019s look at a checking account postings, for instance [output] : bean-report example.beancount journal -a Assets:US:BofA:Checking To render a column of running balances, add the --balance option [output] : bean-report example.beancount journal -a Assets:US:BofA:Checking --balance The inventories are rendered in the number of units they\u2019re holding [output] : bean-report example.beancount journal -a Assets:US:ETrade:GLD If you want to render the values at cost, use the --cost option [output] : bean-report example.beancount journal -a Assets:US:ETrade:GLD --cost To render journals, you will typically restrict the set of postings that you want to view. If you did not, the postings would render no change, because all the legs of the transactions would be applied to the running balance and the total would be zero. But try it, nonetheless [output] : bean-report example.beancount journal --balance Holdings \uf0c1 There are a variety of ways to obtain aggregations for the total list of holdings. List the detailed holdings [output] : bean-report example.beancount holdings To aggregate the holdings by account, do this [output] : bean-report example.beancount holdings --by=account Because it\u2019s common practice to create and use a dedicated Beancount subaccount name for each commodity held in a real-world account, we can refine this further to aggregate to the parent (\u201croot\u201d) accounts of those subaccounts [output] : bean-report example.beancount holdings --by=root-account We can aggregate by commodity [output] : bean-report example.beancount holdings --by=commodity Or by the currency in which the commodities are quoted, which gives us a glimpse into our currency exposure [output] : bean-report example.beancount holdings --by=currency Finally, we can aggregate all the holdings to obtain net worth [output] : bean-report example.beancount networth There are a few more options for converting amounts to a common currency. See help for details. Other Reports \uf0c1 There are many other miscellaneous reports available. Try a few of those. Listing all accounts [output] : bean-report example.beancount accounts Listing events [output] : bean-report example.beancount events Number of directives by type [output] : bean-report example.beancount stats-directives Number of postings by type [output] : bean-report example.beancount stats-postings Other Formats \uf0c1 Reports may support various output formats. You can change the output format with the --format (-f) global option [output] : bean-report -f csv example.beancount holdings Note that \u201cbeancount\u201d and \u201cledger\u201d are valid output formats: they refer to the Beancount and Ledger input syntaxes. Viewing Reports through the Web Interface \uf0c1 The original way to access reports in Beancount is via its web interface that serves to a local web server on your machine. Serve the example file like this: bean-web example.beancount Then navigate with a web browser to http://localhost:8080 . From there, you can click on any number of filtered views and access some of the reports previously demonstrated. For example, click on a year view; that will provide balance sheets and income statements and various other reports for this subset of transactions. The bean-web tool has many options for restricting what is being served. (Note that by default the server listens only for connections from your computer; if you want to host this on a web server and accept connections from anywhere, use --public ). Note: There exists a separate project which provides a better web interface than the one which comes with Beancount: Fava . You might want to check it out. The Future of Beancount Reports \uf0c1 I find my command-line reports above to be rather unsatisfying, from the point-of-view of customizability. They bother me a lot. This is largely because I\u2019ve been happy and satisfied with the capabilities of the web interface to Beancount. As I\u2019m beginning to use the command-line interface more and more, I\u2019m finding myself desiring for more explicit and well-defined way to apply all the filters I have available from the web interface, and more. For this reason, I\u2019m currently implementing a query language that looks similar to SQL . This syntax will allow me to remove all the custom reports above and to replace them by a single consistent query syntax. This work is underway. All the holdings reports and even their internal objects will be replaced by the SQL-like query syntax as well. Holdings need not be treated separately: they are simply the list of positions available at a particular point in time, and along with suitable accessors from the query syntax and support for aggregations over their many columns, we should be able to generate all of those reports equivalently and simplify the code. \u2014 Martin Blais, October 2014","title":"Tutorial & Example"},{"location":"tutorial_example.html#beancount-example-tutorial","text":"Martin Blais , October 2014 http://furius.ca/beancount/doc/example Example File Generator Converting to Ledger Input Profile of Example User Future Additions Tutorial Generate an Example File Generating Reports Generating Balances Generating a Balance Sheet and Income Statement Journals Holdings Other Reports Other Formats Viewing Reports through the Web Interface The Future of Beancount Reports","title":"Beancount Example & Tutorial"},{"location":"tutorial_example.html#example-file-generator","text":"Beancount has a command to generate a few years of a realistic user\u2019s historical entries: bean-example > example.beancount The script checks if the output fails to process cleanly in Beancount (if this occurs, try rerunning or contact the author at blais@furius.ca ). Note that there is an option to fix the random generator\u2019s seed to be used to generate the entries. You can generate multiple years of ledger data for this user; see available options with bean-example --help The purpose of the script is: To provide a realistic corpus of anonymous data for users to experiment with, to kick the tires of Beancount, generate reports, etc. To compare reports with those generated by Ledger. As an example file to be used in this tutorial. As input for stress testing Beancount.","title":"Example File Generator"},{"location":"tutorial_example.html#converting-to-ledger-input","text":"It should be possible to convert example files generated from the script to Ledger syntax, like this: bean-report example.beancount ledger > example.lgr If you encounter any problems with the conversion, please let me know. While I have an automated test to check that the conversion succeeds, I\u2019m focusing my efforts on Beancount itself and I\u2019m not using Ledger much other than for the occasional comparison or for illustrating something on the mailing-list.","title":"Converting to Ledger Input"},{"location":"tutorial_example.html#profile-of-example-user","text":"Here\u2019s a high-level profile of this user. In the text that follows, I\u2019ll use the masculine \u201che\u201d for simplicity\u2019s sake. He lives somewhere in the USA and uses a single operating currency: \u201cUSD\u201d. He rents an apartment. Income : He is a software engineer (sorry I could not resist), receives a modest salary income from a software company. He is paid bi-weekly. He maximizes contributions to his company\u2019s 401k plan. His company offers a match to his contributions. His salary is paid as a direct deposit to his checking account. He benefits from and pays premiums for a health insurance plan provided by his employer. He tracks his vacation hours explicitly. Expenses : Checking account : He pays his apartment\u2019s rent by check. He also pays his electricity, internet ISP and mobile phone bills directly from his checking account. His bank also takes a monthly bank fee. Credit cards : He also has other regular expenses, such as groceries and restaurant outings with friends, which he pays from his credit card. Assets : Banking : He has an account at a major US bank. Retirement account : His 401k account is held at Fidelity or Vanguard and is allocated in its entirety to two funds, a mix of stocks and bonds. There are never any sales in this account. This account is held at average cost. Other investments : Extra money that he is able to save is transferred from his checking account towards a taxable investment account in which he purchases a mix of stocks and ETFs, and occasionally makes sales and realizes profits. Liabilities : Credit cards : He has one or two credit cards issued by another bank. Events/Tags : He takes a short vacation or two per year, where he travels somewhere fun.","title":"Profile of Example User"},{"location":"tutorial_example.html#future-additions","text":"Note that as I beef up the example generator script, this profile will increase in complexity. If there is a particular situation from the cookbook or otherwise that you\u2019d like to see in the example file, please let me know (a patch is the best way to let me know). Generate fake PDF files for documents (from an option) along with suitable document directives for them, most of them implicit but some explicit. Generate more expenses: Some amount of realistic cash expenses. Add a vehicle: car repairs, gas, etc. Remove tram example, it is too urban-specific. Add donations for charity. Add a second credit card and vary sources of expenses. Add some real uses for links, for the 401k match, for instance, the employer contributions should be linked with the employee contributions. Or other ones. Generate a loan and payments for a student loan. Having a large liability is a common case, I\u2019d like to represent that, even if it\u2019s not a mortgage. Convert retirement accounts to book using the average cost method, and add corresponding fees which would otherwise be impossible to track with explicit lots. Add tracking of health care expenses as per the cookbook. Add foreign accounts in another currency and transactions and transfers in that currency.","title":"Future Additions"},{"location":"tutorial_example.html#tutorial","text":"This section provides basic examples of generating reports with Beancount, on the typical example file that would be output by it. The list of reports here is not meant to be exhaustive. You may follow the tutorial and generate your own example file, or look at the files from beancount/examples/tutorial if you prefer.","title":"Tutorial"},{"location":"tutorial_example.html#generate-an-example-file","text":"Begin by generating the example file: bean-example > example.beancount Open example.beancount and examine it. Next, before we begin generating reports, verify that the file loads without any errors: bean-check example.beancount It should return quietly, without outputting anything (bean-check only writes errors when there are some, otherwise on success it writes nothing).","title":"Generate an Example File"},{"location":"tutorial_example.html#generating-reports","text":"Let\u2019s generate a report of the final balances of all accounts [output] : bean-report example.beancount balances As you can see, the bean-report script has subcommands for the various reports it generates. To list the available reports, use --help-reports [output] : bean-report --help-reports To list the options available for a particular report, use --help on it [output] : bean-report example.beancount balances --help The report-specific options vary for each report type. You can also list the global options for the script [output] : bean-report --help The global options allow you to specify the desired output format for the reports. To see which formats are available for which report, use --help-formats [output] : bean-report --help-formats","title":"Generating Reports"},{"location":"tutorial_example.html#generating-balances","text":"Good, so we know how to generate a report of balances for all accounts. This is a pretty detailed list of accounts though. Let\u2019s just restrict the output to the accounts that we\u2019re interested in [output] : bean-report example.beancount balances -e ETrade Here you can view the number of units held in each of the investment subaccounts. To render the cost, use the --cost option [output] : bean-report example.beancount balances -e ETrade --cost","title":"Generating Balances"},{"location":"tutorial_example.html#formatting-tools","text":"Sometimes it\u2019s nice to render a hierarchical list of accounts as a tree. You can use the \u201ctreeify\u201d tool provided by Beancount to do this: bean-report example.beancount balances | treeify This tool will work on any column of data that looks like a column of account names (you can also configure it work with filenames as well, or other patterns).","title":"Formatting Tools"},{"location":"tutorial_example.html#generating-a-balance-sheet-and-income-statement","text":"Let us generate a balance sheet: bean-report example.beancount balsheet Unfortunately, the only output format supported for it at this point is HTML. Also, filtering balance sheet entries from the command-line is not supported. Generate this to a file and open a browser to it [output] : bean-report example.beancount balsheet > /tmp/balsheet.html You can to the same for income statements: bean-report example.beancount income > /tmp/income.html","title":"Generating a Balance Sheet and Income Statement"},{"location":"tutorial_example.html#journals","text":"You can also generate journals (in Ledger parlance, these are \u201cregisters\u201d). Let\u2019s look at a checking account postings, for instance [output] : bean-report example.beancount journal -a Assets:US:BofA:Checking To render a column of running balances, add the --balance option [output] : bean-report example.beancount journal -a Assets:US:BofA:Checking --balance The inventories are rendered in the number of units they\u2019re holding [output] : bean-report example.beancount journal -a Assets:US:ETrade:GLD If you want to render the values at cost, use the --cost option [output] : bean-report example.beancount journal -a Assets:US:ETrade:GLD --cost To render journals, you will typically restrict the set of postings that you want to view. If you did not, the postings would render no change, because all the legs of the transactions would be applied to the running balance and the total would be zero. But try it, nonetheless [output] : bean-report example.beancount journal --balance","title":"Journals"},{"location":"tutorial_example.html#holdings","text":"There are a variety of ways to obtain aggregations for the total list of holdings. List the detailed holdings [output] : bean-report example.beancount holdings To aggregate the holdings by account, do this [output] : bean-report example.beancount holdings --by=account Because it\u2019s common practice to create and use a dedicated Beancount subaccount name for each commodity held in a real-world account, we can refine this further to aggregate to the parent (\u201croot\u201d) accounts of those subaccounts [output] : bean-report example.beancount holdings --by=root-account We can aggregate by commodity [output] : bean-report example.beancount holdings --by=commodity Or by the currency in which the commodities are quoted, which gives us a glimpse into our currency exposure [output] : bean-report example.beancount holdings --by=currency Finally, we can aggregate all the holdings to obtain net worth [output] : bean-report example.beancount networth There are a few more options for converting amounts to a common currency. See help for details.","title":"Holdings"},{"location":"tutorial_example.html#other-reports","text":"There are many other miscellaneous reports available. Try a few of those. Listing all accounts [output] : bean-report example.beancount accounts Listing events [output] : bean-report example.beancount events Number of directives by type [output] : bean-report example.beancount stats-directives Number of postings by type [output] : bean-report example.beancount stats-postings","title":"Other Reports"},{"location":"tutorial_example.html#other-formats","text":"Reports may support various output formats. You can change the output format with the --format (-f) global option [output] : bean-report -f csv example.beancount holdings Note that \u201cbeancount\u201d and \u201cledger\u201d are valid output formats: they refer to the Beancount and Ledger input syntaxes.","title":"Other Formats"},{"location":"tutorial_example.html#viewing-reports-through-the-web-interface","text":"The original way to access reports in Beancount is via its web interface that serves to a local web server on your machine. Serve the example file like this: bean-web example.beancount Then navigate with a web browser to http://localhost:8080 . From there, you can click on any number of filtered views and access some of the reports previously demonstrated. For example, click on a year view; that will provide balance sheets and income statements and various other reports for this subset of transactions. The bean-web tool has many options for restricting what is being served. (Note that by default the server listens only for connections from your computer; if you want to host this on a web server and accept connections from anywhere, use --public ). Note: There exists a separate project which provides a better web interface than the one which comes with Beancount: Fava . You might want to check it out.","title":"Viewing Reports through the Web Interface"},{"location":"tutorial_example.html#the-future-of-beancount-reports","text":"I find my command-line reports above to be rather unsatisfying, from the point-of-view of customizability. They bother me a lot. This is largely because I\u2019ve been happy and satisfied with the capabilities of the web interface to Beancount. As I\u2019m beginning to use the command-line interface more and more, I\u2019m finding myself desiring for more explicit and well-defined way to apply all the filters I have available from the web interface, and more. For this reason, I\u2019m currently implementing a query language that looks similar to SQL . This syntax will allow me to remove all the custom reports above and to replace them by a single consistent query syntax. This work is underway. All the holdings reports and even their internal objects will be replaced by the SQL-like query syntax as well. Holdings need not be treated separately: they are simply the list of positions available at a particular point in time, and along with suitable accessors from the query syntax and support for aggregations over their many columns, we should be able to generate all of those reports equivalently and simplify the code. \u2014 Martin Blais, October 2014","title":"The Future of Beancount Reports"},{"location":"api_reference/index.html","text":"beancount \uf0c1 beancount.core beancount.ingest beancount.loader beancount.ops beancount.parser beancount.plugins beancount.prices beancount.query beancount.reports beancount.scripts beancount.tools beancount.utils beancount.web","title":"beancount"},{"location":"api_reference/index.html#beancount","text":"beancount.core beancount.ingest beancount.loader beancount.ops beancount.parser beancount.plugins beancount.prices beancount.query beancount.reports beancount.scripts beancount.tools beancount.utils beancount.web","title":"beancount"},{"location":"api_reference/beancount.core.html","text":"beancount.core \uf0c1 Core basic objects and data structures to represent a list of entries. beancount.core.account \uf0c1 Functions that operate on account strings. These account objects are rather simple and dumb; they do not contain the list of their associated postings. This is achieved by building a realization; see realization.py for details. beancount.core.account.AccountTransformer \uf0c1 Account name transformer. This is used to support Win... huh, filesystems and platforms which do not support colon characters. Attributes: Name Type Description rsep A character string, the new separator to use in link names. beancount.core.account.AccountTransformer.parse(self, transformed_name) \uf0c1 Convert the transform account name to an account name. Source code in beancount/core/account.py def parse(self, transformed_name): \"Convert the transform account name to an account name.\" return (transformed_name if self.rsep is None else transformed_name.replace(self.rsep, sep)) beancount.core.account.AccountTransformer.render(self, account_name) \uf0c1 Convert the account name to a transformed account name. Source code in beancount/core/account.py def render(self, account_name): \"Convert the account name to a transformed account name.\" return (account_name if self.rsep is None else account_name.replace(sep, self.rsep)) beancount.core.account.commonprefix(accounts) \uf0c1 Return the common prefix of a list of account names. Parameters: accounts \u2013 A sequence of account name strings. Returns: A string, the common parent account. If none, returns an empty string. Source code in beancount/core/account.py def commonprefix(accounts): \"\"\"Return the common prefix of a list of account names. Args: accounts: A sequence of account name strings. Returns: A string, the common parent account. If none, returns an empty string. \"\"\" accounts_lists = [account_.split(sep) for account_ in accounts] # Note: the os.path.commonprefix() function just happens to work here. # Inspect its code, and even the special case of no common prefix # works well with str.join() below. common_list = path.commonprefix(accounts_lists) return sep.join(common_list) beancount.core.account.has_component(account_name, component) \uf0c1 Return true if one of the account contains a given component. Parameters: account_name \u2013 A string, an account name. component \u2013 A string, a component of an account name. For instance, Food in Expenses:Food:Restaurant . All components are considered. Returns: Boolean \u2013 true if the component is in the account. Note that a component name must be whole, that is NY is not in Expenses:Taxes:StateNY . Source code in beancount/core/account.py def has_component(account_name, component): \"\"\"Return true if one of the account contains a given component. Args: account_name: A string, an account name. component: A string, a component of an account name. For instance, ``Food`` in ``Expenses:Food:Restaurant``. All components are considered. Returns: Boolean: true if the component is in the account. Note that a component name must be whole, that is ``NY`` is not in ``Expenses:Taxes:StateNY``. \"\"\" return bool(re.search('(^|:){}(:|$)'.format(component), account_name)) beancount.core.account.is_valid(string) \uf0c1 Return true if the given string is a valid account name. This does not check for the root account types, just the general syntax. Parameters: string \u2013 A string, to be checked for account name pattern. Returns: A boolean, true if the string has the form of an account's name. Source code in beancount/core/account.py def is_valid(string): \"\"\"Return true if the given string is a valid account name. This does not check for the root account types, just the general syntax. Args: string: A string, to be checked for account name pattern. Returns: A boolean, true if the string has the form of an account's name. \"\"\" return (isinstance(string, str) and bool(re.match('{}$'.format(ACCOUNT_RE), string))) beancount.core.account.join(*components) \uf0c1 Join the names with the account separator. Parameters: *components \u2013 Strings, the components of an account name. Returns: A string, joined in a single account name. Source code in beancount/core/account.py def join(*components): \"\"\"Join the names with the account separator. Args: *components: Strings, the components of an account name. Returns: A string, joined in a single account name. \"\"\" return sep.join(components) beancount.core.account.leaf(account_name) \uf0c1 Get the name of the leaf of this account. Parameters: account_name \u2013 A string, the name of the account whose leaf name to return. Returns: A string, the name of the leaf of the account. Source code in beancount/core/account.py def leaf(account_name): \"\"\"Get the name of the leaf of this account. Args: account_name: A string, the name of the account whose leaf name to return. Returns: A string, the name of the leaf of the account. \"\"\" assert isinstance(account_name, str) return account_name.split(sep)[-1] if account_name else None beancount.core.account.parent(account_name) \uf0c1 Return the name of the parent account of the given account. Parameters: account_name \u2013 A string, the name of the account whose parent to return. Returns: A string, the name of the parent account of this account. Source code in beancount/core/account.py def parent(account_name): \"\"\"Return the name of the parent account of the given account. Args: account_name: A string, the name of the account whose parent to return. Returns: A string, the name of the parent account of this account. \"\"\" assert isinstance(account_name, str), account_name if not account_name: return None components = account_name.split(sep) components.pop(-1) return sep.join(components) beancount.core.account.parent_matcher(account_name) \uf0c1 Build a predicate that returns whether an account is under the given one. Parameters: account_name \u2013 The name of the parent account we want to check for. Returns: A callable, which, when called, will return true if the given account is a child of account_name . Source code in beancount/core/account.py def parent_matcher(account_name): \"\"\"Build a predicate that returns whether an account is under the given one. Args: account_name: The name of the parent account we want to check for. Returns: A callable, which, when called, will return true if the given account is a child of ``account_name``. \"\"\" return re.compile(r'{}($|{})'.format(re.escape(account_name), sep)).match beancount.core.account.parents(account_name) \uf0c1 A generator of the names of the parents of this account, including this account. Parameters: account_name \u2013 The name of the account we want to start iterating from. Returns: A generator of account name strings. Source code in beancount/core/account.py def parents(account_name): \"\"\"A generator of the names of the parents of this account, including this account. Args: account_name: The name of the account we want to start iterating from. Returns: A generator of account name strings. \"\"\" while account_name: yield account_name account_name = parent(account_name) beancount.core.account.root(num_components, account_name) \uf0c1 Return the first few components of an account's name. Parameters: num_components \u2013 An integer, the number of components to return. account_name \u2013 A string, an account name. Returns: A string, the account root up to 'num_components' components. Source code in beancount/core/account.py def root(num_components, account_name): \"\"\"Return the first few components of an account's name. Args: num_components: An integer, the number of components to return. account_name: A string, an account name. Returns: A string, the account root up to 'num_components' components. \"\"\" return join(*(split(account_name)[:num_components])) beancount.core.account.sans_root(account_name) \uf0c1 Get the name of the account without the root. For example, an input of 'Assets:BofA:Checking' will produce 'BofA:Checking'. Parameters: account_name \u2013 A string, the name of the account whose leaf name to return. Returns: A string, the name of the non-root portion of this account name. Source code in beancount/core/account.py def sans_root(account_name): \"\"\"Get the name of the account without the root. For example, an input of 'Assets:BofA:Checking' will produce 'BofA:Checking'. Args: account_name: A string, the name of the account whose leaf name to return. Returns: A string, the name of the non-root portion of this account name. \"\"\" assert isinstance(account_name, str) components = account_name.split(sep)[1:] return join(*components) if account_name else None beancount.core.account.split(account_name) \uf0c1 Split an account's name into its components. Parameters: account_name \u2013 A string, an account name. Returns: A list of strings, the components of the account name (without the separators). Source code in beancount/core/account.py def split(account_name): \"\"\"Split an account's name into its components. Args: account_name: A string, an account name. Returns: A list of strings, the components of the account name (without the separators). \"\"\" return account_name.split(sep) beancount.core.account.walk(root_directory) \uf0c1 A version of os.walk() which yields directories that are valid account names. This only yields directories that are accounts... it skips the other ones. For convenience, it also yields you the account's name. Parameters: root_directory \u2013 A string, the name of the root of the hierarchy to be walked. Yields: Tuples of (root, account-name, dirs, files), similar to os.walk(). Source code in beancount/core/account.py def walk(root_directory): \"\"\"A version of os.walk() which yields directories that are valid account names. This only yields directories that are accounts... it skips the other ones. For convenience, it also yields you the account's name. Args: root_directory: A string, the name of the root of the hierarchy to be walked. Yields: Tuples of (root, account-name, dirs, files), similar to os.walk(). \"\"\" for root, dirs, files in os.walk(root_directory): dirs.sort() files.sort() relroot = root[len(root_directory)+1:] account_name = relroot.replace(os.sep, sep) if is_valid(account_name): yield (root, account_name, dirs, files) beancount.core.account_types \uf0c1 Definition for global account types. This is where we keep the global account types value and definition. Note that it's unfortunate that we're using globals and side-effect here, but this is the best solution in the short-term, the account types are used in too many places to pass around that state everywhere. Maybe we change this later on. beancount.core.account_types.AccountTypes ( tuple ) \uf0c1 AccountTypes(assets, liabilities, equity, income, expenses) beancount.core.account_types.AccountTypes.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/account_types.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.account_types.AccountTypes.__new__(_cls, assets, liabilities, equity, income, expenses) special staticmethod \uf0c1 Create new instance of AccountTypes(assets, liabilities, equity, income, expenses) beancount.core.account_types.AccountTypes.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/account_types.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.account_types.get_account_sign(account_name, account_types=None) \uf0c1 Return the sign of the normal balance of a particular account. Parameters: account_name \u2013 A string, the name of the account whose sign is to return. account_types \u2013 An optional instance of the current account_types. Returns: +1 or -1, depending on the account's type. Source code in beancount/core/account_types.py def get_account_sign(account_name, account_types=None): \"\"\"Return the sign of the normal balance of a particular account. Args: account_name: A string, the name of the account whose sign is to return. account_types: An optional instance of the current account_types. Returns: +1 or -1, depending on the account's type. \"\"\" if account_types is None: account_types = DEFAULT_ACCOUNT_TYPES assert isinstance(account_name, str), \"Account is not a string: {}\".format(account_name) account_type = get_account_type(account_name) return (+1 if account_type in (account_types.assets, account_types.expenses) else -1) beancount.core.account_types.get_account_sort_key(account_types, account_name) \uf0c1 Return a tuple that can be used to order/sort account names. Parameters: account_types \u2013 An instance of AccountTypes, a tuple of account type names. Returns: A function object to use as the optional 'key' argument to the sort function. It accepts a single argument, the account name to sort and produces a sortable key. Source code in beancount/core/account_types.py def get_account_sort_key(account_types, account_name): \"\"\"Return a tuple that can be used to order/sort account names. Args: account_types: An instance of AccountTypes, a tuple of account type names. Returns: A function object to use as the optional 'key' argument to the sort function. It accepts a single argument, the account name to sort and produces a sortable key. \"\"\" return (account_types.index(get_account_type(account_name)), account_name) beancount.core.account_types.get_account_type(account_name) \uf0c1 Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Parameters: account_name \u2013 A string, the name of the account whose type is to return. Returns: A string, the type of the account in 'account_name'. Source code in beancount/core/account_types.py def get_account_type(account_name): \"\"\"Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Args: account_name: A string, the name of the account whose type is to return. Returns: A string, the type of the account in 'account_name'. \"\"\" assert isinstance(account_name, str), \"Account is not a string: {}\".format(account_name) return account.split(account_name)[0] beancount.core.account_types.is_account_type(account_type, account_name) \uf0c1 Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Parameters: account_type \u2013 A string, the prefix type of the account. account_name \u2013 A string, the name of the account whose type is to return. Returns: A boolean, true if the account is of the given type. Source code in beancount/core/account_types.py def is_account_type(account_type, account_name): \"\"\"Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Args: account_type: A string, the prefix type of the account. account_name: A string, the name of the account whose type is to return. Returns: A boolean, true if the account is of the given type. \"\"\" return bool(re.match('^{}{}'.format(account_type, account.sep), account_name)) beancount.core.account_types.is_balance_sheet_account(account_name, account_types) \uf0c1 Return true if the given account is a balance sheet account. Assets, liabilities and equity accounts are balance sheet accounts. Parameters: account_name \u2013 A string, an account name. account_types \u2013 An instance of AccountTypes. Returns: A boolean, true if the account is a balance sheet account. Source code in beancount/core/account_types.py def is_balance_sheet_account(account_name, account_types): \"\"\"Return true if the given account is a balance sheet account. Assets, liabilities and equity accounts are balance sheet accounts. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account is a balance sheet account. \"\"\" assert isinstance(account_name, str), \"Account is not a string: {}\".format(account_name) assert isinstance(account_types, AccountTypes), ( \"Account types has invalid type: {}\".format(account_types)) account_type = get_account_type(account_name) return account_type in (account_types.assets, account_types.liabilities, account_types.equity) beancount.core.account_types.is_equity_account(account_name, account_types) \uf0c1 Return true if the given account is an equity account. Parameters: account_name \u2013 A string, an account name. account_types \u2013 An instance of AccountTypes. Returns: A boolean, true if the account is an equity account. Source code in beancount/core/account_types.py def is_equity_account(account_name, account_types): \"\"\"Return true if the given account is an equity account. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account is an equity account. \"\"\" assert isinstance(account_name, str), \"Account is not a string: {}\".format(account_name) assert isinstance(account_types, AccountTypes), ( \"Account types has invalid type: {}\".format(account_types)) account_type = get_account_type(account_name) return account_type == account_types.equity beancount.core.account_types.is_income_statement_account(account_name, account_types) \uf0c1 Return true if the given account is an income statement account. Income and expense accounts are income statement accounts. Parameters: account_name \u2013 A string, an account name. account_types \u2013 An instance of AccountTypes. Returns: A boolean, true if the account is an income statement account. Source code in beancount/core/account_types.py def is_income_statement_account(account_name, account_types): \"\"\"Return true if the given account is an income statement account. Income and expense accounts are income statement accounts. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account is an income statement account. \"\"\" assert isinstance(account_name, str), \"Account is not a string: {}\".format(account_name) assert isinstance(account_types, AccountTypes), ( \"Account types has invalid type: {}\".format(account_types)) account_type = get_account_type(account_name) return account_type in (account_types.income, account_types.expenses) beancount.core.account_types.is_root_account(account_name, account_types=None) \uf0c1 Return true if the account name is a root account. This function does not verify whether the account root is a valid one, just that it is a root account or not. Parameters: account_name \u2013 A string, the name of the account to check for. account_types \u2013 An optional instance of the current account_types; if provided, we check against these values. If not provided, we merely check that name pattern is that of an account component with no separator. Returns: A boolean, true if the account is root account. Source code in beancount/core/account_types.py def is_root_account(account_name, account_types=None): \"\"\"Return true if the account name is a root account. This function does not verify whether the account root is a valid one, just that it is a root account or not. Args: account_name: A string, the name of the account to check for. account_types: An optional instance of the current account_types; if provided, we check against these values. If not provided, we merely check that name pattern is that of an account component with no separator. Returns: A boolean, true if the account is root account. \"\"\" assert isinstance(account_name, str), \"Account is not a string: {}\".format(account_name) if account_types is not None: assert isinstance(account_types, AccountTypes), ( \"Account types has invalid type: {}\".format(account_types)) return account_name in account_types else: return (account_name and bool(re.match(r'([A-Z][A-Za-z0-9\\-]+)$', account_name))) beancount.core.amount \uf0c1 Amount class. This simple class is used to associate a number of units of a currency with its currency: (number, currency). beancount.core.amount.Amount ( _Amount ) \uf0c1 An 'Amount' represents a number of a particular unit of something. It's essentially a typed number, with corresponding manipulation operations defined on it. beancount.core.amount.Amount.__bool__(self) special \uf0c1 Boolean predicate returns true if the number is non-zero. Returns: A boolean, true if non-zero number. Source code in beancount/core/amount.py def __bool__(self): \"\"\"Boolean predicate returns true if the number is non-zero. Returns: A boolean, true if non-zero number. \"\"\" return self.number != ZERO beancount.core.amount.Amount.__eq__(self, other) special \uf0c1 Equality predicate. Returns true if both number and currency are equal. Returns: A boolean. Source code in beancount/core/amount.py def __eq__(self, other): \"\"\"Equality predicate. Returns true if both number and currency are equal. Returns: A boolean. \"\"\" if other is None: return False return (self.number, self.currency) == (other.number, other.currency) beancount.core.amount.Amount.__hash__(self) special \uf0c1 A hashing function for amounts. The hash includes the currency. Returns: An integer, the hash for this amount. Source code in beancount/core/amount.py def __hash__(self): \"\"\"A hashing function for amounts. The hash includes the currency. Returns: An integer, the hash for this amount. \"\"\" return hash((self.number, self.currency)) beancount.core.amount.Amount.__lt__(self, other) special \uf0c1 Ordering comparison. This is used in the sorting key of positions. Parameters: other \u2013 An instance of Amount. Returns: True if this is less than the other Amount. Source code in beancount/core/amount.py def __lt__(self, other): \"\"\"Ordering comparison. This is used in the sorting key of positions. Args: other: An instance of Amount. Returns: True if this is less than the other Amount. \"\"\" return sortkey(self) < sortkey(other) beancount.core.amount.Amount.__neg__(self) special \uf0c1 Return the negative of this amount. Returns: A new instance of Amount, with the negative number of units. Source code in beancount/core/amount.py def __neg__(self): \"\"\"Return the negative of this amount. Returns: A new instance of Amount, with the negative number of units. \"\"\" return Amount(-self.number, self.currency) beancount.core.amount.Amount.__new__(cls, number, currency) special staticmethod \uf0c1 Constructor from a number and currency. Parameters: number \u2013 A string or Decimal instance. Will get converted automatically. currency \u2013 A string, the currency symbol to use. Source code in beancount/core/amount.py def __new__(cls, number, currency): \"\"\"Constructor from a number and currency. Args: number: A string or Decimal instance. Will get converted automatically. currency: A string, the currency symbol to use. \"\"\" assert isinstance(number, Amount.valid_types_number), repr(number) assert isinstance(currency, Amount.valid_types_currency), repr(currency) return _Amount.__new__(cls, number, currency) beancount.core.amount.Amount.__repr__(self) special \uf0c1 Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/amount.py def __str__(self): \"\"\"Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. \"\"\" return self.to_string() beancount.core.amount.Amount.__str__(self) special \uf0c1 Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/amount.py def __str__(self): \"\"\"Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. \"\"\" return self.to_string() beancount.core.amount.Amount.from_string(string) staticmethod \uf0c1 Create an amount from a string. This is a miniature parser used for building tests. Parameters: string \u2013 A string of . Returns: A new instance of Amount. Source code in beancount/core/amount.py @staticmethod def from_string(string): \"\"\"Create an amount from a string. This is a miniature parser used for building tests. Args: string: A string of . Returns: A new instance of Amount. \"\"\" match = re.match(r'\\s*([-+]?[0-9.]+)\\s+({currency})'.format(currency=CURRENCY_RE), string) if not match: raise ValueError(\"Invalid string for amount: '{}'\".format(string)) number, currency = match.group(1, 2) return Amount(D(number), currency) beancount.core.amount.Amount.to_string(self, dformat=) \uf0c1 Convert an Amount instance to a printable string. Parameters: dformat \u2013 An instance of DisplayFormatter. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/amount.py def to_string(self, dformat=DEFAULT_FORMATTER): \"\"\"Convert an Amount instance to a printable string. Args: dformat: An instance of DisplayFormatter. Returns: A formatted string of the quantized amount and symbol. \"\"\" number_fmt = (dformat.format(self.number, self.currency) if isinstance(self.number, Decimal) else str(self.number)) return \"{} {}\".format(number_fmt, self.currency) beancount.core.amount.A(string) \uf0c1 Create an amount from a string. This is a miniature parser used for building tests. Parameters: string \u2013 A string of . Returns: A new instance of Amount. Source code in beancount/core/amount.py @staticmethod def from_string(string): \"\"\"Create an amount from a string. This is a miniature parser used for building tests. Args: string: A string of . Returns: A new instance of Amount. \"\"\" match = re.match(r'\\s*([-+]?[0-9.]+)\\s+({currency})'.format(currency=CURRENCY_RE), string) if not match: raise ValueError(\"Invalid string for amount: '{}'\".format(string)) number, currency = match.group(1, 2) return Amount(D(number), currency) beancount.core.amount.abs(amount) \uf0c1 Return the absolute value of the given amount. Parameters: amount \u2013 An instance of Amount. Returns: An instance of Amount. Source code in beancount/core/amount.py def abs(amount): \"\"\"Return the absolute value of the given amount. Args: amount: An instance of Amount. Returns: An instance of Amount. \"\"\" return (amount if amount.number >= ZERO else Amount(-amount.number, amount.currency)) beancount.core.amount.add(amount1, amount2) \uf0c1 Add the given amounts with the same currency. Parameters: amount1 \u2013 An instance of Amount. amount2 \u2013 An instance of Amount. Returns: An instance of Amount, with the sum the two amount's numbers, in the same currency. Source code in beancount/core/amount.py def add(amount1, amount2): \"\"\"Add the given amounts with the same currency. Args: amount1: An instance of Amount. amount2: An instance of Amount. Returns: An instance of Amount, with the sum the two amount's numbers, in the same currency. \"\"\" assert isinstance(amount1.number, Decimal), ( \"Amount1's number is not a Decimal instance: {}\".format(amount1.number)) assert isinstance(amount2.number, Decimal), ( \"Amount2's number is not a Decimal instance: {}\".format(amount2.number)) if amount1.currency != amount2.currency: raise ValueError( \"Unmatching currencies for operation on {} and {}\".format( amount1, amount2)) return Amount(amount1.number + amount2.number, amount1.currency) beancount.core.amount.div(amount, number) \uf0c1 Divide the given amount by a number. Parameters: amount \u2013 An instance of Amount. number \u2013 A decimal number. Returns: An Amount, with the same currency, but with amount units divided by 'number'. Source code in beancount/core/amount.py def div(amount, number): \"\"\"Divide the given amount by a number. Args: amount: An instance of Amount. number: A decimal number. Returns: An Amount, with the same currency, but with amount units divided by 'number'. \"\"\" assert isinstance(amount.number, Decimal), ( \"Amount's number is not a Decimal instance: {}\".format(amount.number)) assert isinstance(number, Decimal), ( \"Number is not a Decimal instance: {}\".format(number)) return Amount(amount.number / number, amount.currency) beancount.core.amount.from_string(string) \uf0c1 Create an amount from a string. This is a miniature parser used for building tests. Parameters: string \u2013 A string of . Returns: A new instance of Amount. Source code in beancount/core/amount.py @staticmethod def from_string(string): \"\"\"Create an amount from a string. This is a miniature parser used for building tests. Args: string: A string of . Returns: A new instance of Amount. \"\"\" match = re.match(r'\\s*([-+]?[0-9.]+)\\s+({currency})'.format(currency=CURRENCY_RE), string) if not match: raise ValueError(\"Invalid string for amount: '{}'\".format(string)) number, currency = match.group(1, 2) return Amount(D(number), currency) beancount.core.amount.mul(amount, number) \uf0c1 Multiply the given amount by a number. Parameters: amount \u2013 An instance of Amount. number \u2013 A decimal number. Returns: An Amount, with the same currency, but with 'number' times units. Source code in beancount/core/amount.py def mul(amount, number): \"\"\"Multiply the given amount by a number. Args: amount: An instance of Amount. number: A decimal number. Returns: An Amount, with the same currency, but with 'number' times units. \"\"\" assert isinstance(amount.number, Decimal), ( \"Amount's number is not a Decimal instance: {}\".format(amount.number)) assert isinstance(number, Decimal), ( \"Number is not a Decimal instance: {}\".format(number)) return Amount(amount.number * number, amount.currency) beancount.core.amount.sortkey(amount) \uf0c1 A comparison function that sorts by currency first. Parameters: amount \u2013 An instance of Amount. Returns: A sort key, composed of the currency first and then the number. Source code in beancount/core/amount.py def sortkey(amount): \"\"\"A comparison function that sorts by currency first. Args: amount: An instance of Amount. Returns: A sort key, composed of the currency first and then the number. \"\"\" return (amount.currency, amount.number) beancount.core.amount.sub(amount1, amount2) \uf0c1 Subtract the given amounts with the same currency. Parameters: amount1 \u2013 An instance of Amount. amount2 \u2013 An instance of Amount. Returns: An instance of Amount, with the difference between the two amount's numbers, in the same currency. Source code in beancount/core/amount.py def sub(amount1, amount2): \"\"\"Subtract the given amounts with the same currency. Args: amount1: An instance of Amount. amount2: An instance of Amount. Returns: An instance of Amount, with the difference between the two amount's numbers, in the same currency. \"\"\" assert isinstance(amount1.number, Decimal), ( \"Amount1's number is not a Decimal instance: {}\".format(amount1.number)) assert isinstance(amount2.number, Decimal), ( \"Amount2's number is not a Decimal instance: {}\".format(amount2.number)) if amount1.currency != amount2.currency: raise ValueError( \"Unmatching currencies for operation on {} and {}\".format( amount1, amount2)) return Amount(amount1.number - amount2.number, amount1.currency) beancount.core.compare \uf0c1 Comparison helpers for data objects. beancount.core.compare.CompareError ( tuple ) \uf0c1 CompareError(source, message, entry) beancount.core.compare.CompareError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/compare.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.compare.CompareError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of CompareError(source, message, entry) beancount.core.compare.CompareError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/compare.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.compare.compare_entries(entries1, entries2) \uf0c1 Compare two lists of entries. This is used for testing. The entries are compared with disregard for their file location. Parameters: entries1 \u2013 A list of directives of any type. entries2 \u2013 Another list of directives of any type. Returns: A tuple of (success, not_found1, not_found2), where the fields are \u2013 success: A boolean, true if all the values are equal. missing1: A list of directives from 'entries1' not found in 'entries2'. missing2: A list of directives from 'entries2' not found in 'entries1'. Exceptions: ValueError \u2013 If a duplicate entry is found. Source code in beancount/core/compare.py def compare_entries(entries1, entries2): \"\"\"Compare two lists of entries. This is used for testing. The entries are compared with disregard for their file location. Args: entries1: A list of directives of any type. entries2: Another list of directives of any type. Returns: A tuple of (success, not_found1, not_found2), where the fields are: success: A boolean, true if all the values are equal. missing1: A list of directives from 'entries1' not found in 'entries2'. missing2: A list of directives from 'entries2' not found in 'entries1'. Raises: ValueError: If a duplicate entry is found. \"\"\" hashes1, errors1 = hash_entries(entries1, exclude_meta=True) hashes2, errors2 = hash_entries(entries2, exclude_meta=True) keys1 = set(hashes1.keys()) keys2 = set(hashes2.keys()) if errors1 or errors2: error = (errors1 + errors2)[0] raise ValueError(str(error)) same = keys1 == keys2 missing1 = data.sorted([hashes1[key] for key in keys1 - keys2]) missing2 = data.sorted([hashes2[key] for key in keys2 - keys1]) return (same, missing1, missing2) beancount.core.compare.excludes_entries(subset_entries, entries) \uf0c1 Check that a list of entries does not appear in another list. Parameters: subset_entries \u2013 The set of entries to look for in 'entries'. entries \u2013 The larger list of entries that should not include 'subset_entries'. Returns: A boolean and a list of entries that are not supposed to appear. Exceptions: ValueError \u2013 If a duplicate entry is found. Source code in beancount/core/compare.py def excludes_entries(subset_entries, entries): \"\"\"Check that a list of entries does not appear in another list. Args: subset_entries: The set of entries to look for in 'entries'. entries: The larger list of entries that should not include 'subset_entries'. Returns: A boolean and a list of entries that are not supposed to appear. Raises: ValueError: If a duplicate entry is found. \"\"\" subset_hashes, subset_errors = hash_entries(subset_entries, exclude_meta=True) subset_keys = set(subset_hashes.keys()) hashes, errors = hash_entries(entries, exclude_meta=True) keys = set(hashes.keys()) if subset_errors or errors: error = (subset_errors + errors)[0] raise ValueError(str(error)) intersection = keys.intersection(subset_keys) excludes = not bool(intersection) extra = data.sorted([subset_hashes[key] for key in intersection]) return (excludes, extra) beancount.core.compare.hash_entries(entries, exclude_meta=False) \uf0c1 Compute unique hashes of each of the entries and return a map of them. This is used for comparisons between sets of entries. Parameters: entries \u2013 A list of directives. exclude_meta \u2013 If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A dict of hash-value to entry (for all entries) and a list of errors. Errors are created when duplicate entries are found. Source code in beancount/core/compare.py def hash_entries(entries, exclude_meta=False): \"\"\"Compute unique hashes of each of the entries and return a map of them. This is used for comparisons between sets of entries. Args: entries: A list of directives. exclude_meta: If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A dict of hash-value to entry (for all entries) and a list of errors. Errors are created when duplicate entries are found. \"\"\" entry_hash_dict = {} errors = [] num_legal_duplicates = 0 for entry in entries: hash_ = hash_entry(entry, exclude_meta) if hash_ in entry_hash_dict: if isinstance(entry, Price): # Note: Allow duplicate Price entries, they should be common # because of the nature of stock markets (if they're closed, the # data source is likely to return an entry for the previously # available date, which may already have been fetched). num_legal_duplicates += 1 else: other_entry = entry_hash_dict[hash_] errors.append( CompareError(entry.meta, \"Duplicate entry: {} == {}\".format(entry, other_entry), entry)) entry_hash_dict[hash_] = entry if not errors: assert len(entry_hash_dict) + num_legal_duplicates == len(entries), ( len(entry_hash_dict), len(entries), num_legal_duplicates) return entry_hash_dict, errors beancount.core.compare.hash_entry(entry, exclude_meta=False) \uf0c1 Compute the stable hash of a single entry. Parameters: entry \u2013 A directive instance. exclude_meta \u2013 If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A stable hexadecimal hash of this entry. Source code in beancount/core/compare.py def hash_entry(entry, exclude_meta=False): \"\"\"Compute the stable hash of a single entry. Args: entry: A directive instance. exclude_meta: If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A stable hexadecimal hash of this entry. \"\"\" return stable_hash_namedtuple(entry, IGNORED_FIELD_NAMES if exclude_meta else frozenset()) beancount.core.compare.includes_entries(subset_entries, entries) \uf0c1 Check if a list of entries is included in another list. Parameters: subset_entries \u2013 The set of entries to look for in 'entries'. entries \u2013 The larger list of entries that could include 'subset_entries'. Returns: A boolean and a list of missing entries. Exceptions: ValueError \u2013 If a duplicate entry is found. Source code in beancount/core/compare.py def includes_entries(subset_entries, entries): \"\"\"Check if a list of entries is included in another list. Args: subset_entries: The set of entries to look for in 'entries'. entries: The larger list of entries that could include 'subset_entries'. Returns: A boolean and a list of missing entries. Raises: ValueError: If a duplicate entry is found. \"\"\" subset_hashes, subset_errors = hash_entries(subset_entries, exclude_meta=True) subset_keys = set(subset_hashes.keys()) hashes, errors = hash_entries(entries, exclude_meta=True) keys = set(hashes.keys()) if subset_errors or errors: error = (subset_errors + errors)[0] raise ValueError(str(error)) includes = subset_keys.issubset(keys) missing = data.sorted([subset_hashes[key] for key in subset_keys - keys]) return (includes, missing) beancount.core.compare.stable_hash_namedtuple(objtuple, ignore=frozenset()) \uf0c1 Hash the given namedtuple and its child fields. This iterates over all the members of objtuple, skipping the attributes from the 'ignore' set, and computes a unique hash string code. If the elements are lists or sets, sorts them for stability. Parameters: objtuple \u2013 A tuple object or other. ignore \u2013 A set of strings, attribute names to be skipped in computing a stable hash. For instance, circular references to objects or irrelevant data. Source code in beancount/core/compare.py def stable_hash_namedtuple(objtuple, ignore=frozenset()): \"\"\"Hash the given namedtuple and its child fields. This iterates over all the members of objtuple, skipping the attributes from the 'ignore' set, and computes a unique hash string code. If the elements are lists or sets, sorts them for stability. Args: objtuple: A tuple object or other. ignore: A set of strings, attribute names to be skipped in computing a stable hash. For instance, circular references to objects or irrelevant data. \"\"\" # Note: this routine is slow and would stand to be implemented in C. hashobj = hashlib.md5() for attr_name, attr_value in zip(objtuple._fields, objtuple): if attr_name in ignore: continue if isinstance(attr_value, (list, set, frozenset)): subhashes = set() for element in attr_value: if isinstance(element, tuple): subhashes.add(stable_hash_namedtuple(element, ignore)) else: md5 = hashlib.md5() md5.update(str(element).encode()) subhashes.add(md5.hexdigest()) for subhash in sorted(subhashes): hashobj.update(subhash.encode()) else: hashobj.update(str(attr_value).encode()) return hashobj.hexdigest() beancount.core.convert \uf0c1 Conversions from Position (or Posting) to units, cost, weight, market value. Units: Just the primary amount of the position. Cost: The cost basis of the position, if available. Weight: The cost basis or price of the position. Market Value: The units converted to a value via a price map. To convert an inventory's contents, simply use these functions in conjunction with Inventory.reduce() , like cost_inv = inv.reduce(convert.get_cost) This module equivalently converts Position and Posting instances. Note that we're specifically avoiding to create an import dependency on beancount.core.data in order to keep this module isolatable, but it works on postings due to duck-typing. Function named get_*() are used to compute values from postings to their price currency. Functions named convert_*() are used to convert postings and amounts to any currency. beancount.core.convert.convert_amount(amt, target_currency, price_map, date=None, via=None) \uf0c1 Return the market value of an Amount in a particular currency. In addition, if a conversion rate isn't available, you can provide a list of currencies to attempt to synthesize a rate for via implied rates. Parameters: amt \u2013 An instance of Amount. target_currency \u2013 The target currency to convert to. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance to evaluate the value at, or None. via \u2013 A list of currencies to attempt to synthesize an implied rate if the direct conversion fails. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, the amount itself, unmodified. Source code in beancount/core/convert.py def convert_amount(amt, target_currency, price_map, date=None, via=None): \"\"\"Return the market value of an Amount in a particular currency. In addition, if a conversion rate isn't available, you can provide a list of currencies to attempt to synthesize a rate for via implied rates. Args: amt: An instance of Amount. target_currency: The target currency to convert to. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. via: A list of currencies to attempt to synthesize an implied rate if the direct conversion fails. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, the amount itself, unmodified. \"\"\" # First, attempt to convert directly. This should be the most # straightforward conversion. base_quote = (amt.currency, target_currency) _, rate = prices.get_price(price_map, base_quote, date) if rate is not None: # On success, just make the conversion directly. return Amount(amt.number * rate, target_currency) elif via: assert isinstance(via, (tuple, list)) # A price is unavailable, attempt to convert via cost/price currency # hop, if the value currency isn't the target currency. for implied_currency in via: if implied_currency == target_currency: continue base_quote1 = (amt.currency, implied_currency) _, rate1 = prices.get_price(price_map, base_quote1, date) if rate1 is not None: base_quote2 = (implied_currency, target_currency) _, rate2 = prices.get_price(price_map, base_quote2, date) if rate2 is not None: return Amount(amt.number * rate1 * rate2, target_currency) # We failed to infer a conversion rate; return the amt. return amt beancount.core.convert.convert_position(pos, target_currency, price_map, date=None) \uf0c1 Return the market value of a Position or Posting in a particular currency. In addition, if the rate from the position's currency to target_currency isn't available, an attempt is made to convert from its cost currency, if one is available. Parameters: pos \u2013 An instance of Position or Posting, equivalently. target_currency \u2013 The target currency to convert to. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance to evaluate the value at, or None. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. (See get_value() above for details.) Source code in beancount/core/convert.py def convert_position(pos, target_currency, price_map, date=None): \"\"\"Return the market value of a Position or Posting in a particular currency. In addition, if the rate from the position's currency to target_currency isn't available, an attempt is made to convert from its cost currency, if one is available. Args: pos: An instance of Position or Posting, equivalently. target_currency: The target currency to convert to. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. (See get_value() above for details.) \"\"\" cost = pos.cost value_currency = ( (isinstance(cost, Cost) and cost.currency) or (hasattr(pos, 'price') and pos.price and pos.price.currency) or None) return convert_amount(pos.units, target_currency, price_map, date=date, via=(value_currency,)) beancount.core.convert.get_cost(pos) \uf0c1 Return the total cost of a Position or Posting. Parameters: pos \u2013 An instance of Position or Posting, equivalently. Returns: An Amount. Source code in beancount/core/convert.py def get_cost(pos): \"\"\"Return the total cost of a Position or Posting. Args: pos: An instance of Position or Posting, equivalently. Returns: An Amount. \"\"\" assert isinstance(pos, Position) or type(pos).__name__ == 'Posting' cost = pos.cost return (Amount(cost.number * pos.units.number, cost.currency) if (isinstance(cost, Cost) and isinstance(cost.number, Decimal)) else pos.units) beancount.core.convert.get_units(pos) \uf0c1 Return the units of a Position or Posting. Parameters: pos \u2013 An instance of Position or Posting, equivalently. Returns: An Amount. Source code in beancount/core/convert.py def get_units(pos): \"\"\"Return the units of a Position or Posting. Args: pos: An instance of Position or Posting, equivalently. Returns: An Amount. \"\"\" assert isinstance(pos, Position) or type(pos).__name__ == 'Posting' return pos.units beancount.core.convert.get_value(pos, price_map, date=None) \uf0c1 Return the market value of a Position or Posting. Note that if the position is not held at cost, this does not convert anything, even if a price is available in the 'price_map'. We don't specify a target currency here. If you're attempting to make such a conversion, see convert_*() functions below. Parameters: pos \u2013 An instance of Position or Posting, equivalently. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance to evaluate the value at, or None. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. This is designed so that you could reduce an inventory with this and not lose any information silently in case of failure to convert (possibly due to an empty price map). Compare the returned currency to that of the input position if you need to check for success. Source code in beancount/core/convert.py def get_value(pos, price_map, date=None): \"\"\"Return the market value of a Position or Posting. Note that if the position is not held at cost, this does not convert anything, even if a price is available in the 'price_map'. We don't specify a target currency here. If you're attempting to make such a conversion, see ``convert_*()`` functions below. Args: pos: An instance of Position or Posting, equivalently. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. This is designed so that you could reduce an inventory with this and not lose any information silently in case of failure to convert (possibly due to an empty price map). Compare the returned currency to that of the input position if you need to check for success. \"\"\" assert isinstance(pos, Position) or type(pos).__name__ == 'Posting' units = pos.units cost = pos.cost # Try to infer what the cost/price currency should be. value_currency = ( (isinstance(cost, Cost) and cost.currency) or (hasattr(pos, 'price') and pos.price and pos.price.currency) or None) if isinstance(value_currency, str): # We have a value currency; hit the price database. base_quote = (units.currency, value_currency) _, price_number = prices.get_price(price_map, base_quote, date) if price_number is not None: return Amount(units.number * price_number, value_currency) # We failed to infer a conversion rate; return the units. return units beancount.core.convert.get_weight(pos) \uf0c1 Return the weight of a Position or Posting. This is the amount that will need to be balanced from a posting of a transaction. This is a key element of the semantics of transactions in this software. A balance amount is the amount used to check the balance of a transaction. Here are all relevant examples, with the amounts used to balance the postings: Assets:Account 5234.50 USD -> 5234.50 USD Assets:Account 3877.41 EUR @ 1.35 USD -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} @ 545.60 CAD -> 5234.50 USD Parameters: pos \u2013 An instance of Position or Posting, equivalently. Returns: An Amount. Source code in beancount/core/convert.py def get_weight(pos): \"\"\"Return the weight of a Position or Posting. This is the amount that will need to be balanced from a posting of a transaction. This is a *key* element of the semantics of transactions in this software. A balance amount is the amount used to check the balance of a transaction. Here are all relevant examples, with the amounts used to balance the postings: Assets:Account 5234.50 USD -> 5234.50 USD Assets:Account 3877.41 EUR @ 1.35 USD -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} @ 545.60 CAD -> 5234.50 USD Args: pos: An instance of Position or Posting, equivalently. Returns: An Amount. \"\"\" assert isinstance(pos, Position) or type(pos).__name__ == 'Posting' units = pos.units cost = pos.cost # It the object has a cost, use that as the weight, to balance. if isinstance(cost, Cost) and isinstance(cost.number, Decimal): weight = Amount(cost.number * pos.units.number, cost.currency) else: # Otherwise use the postings. weight = units # Unless there is a price available; use that if present. if not isinstance(pos, Position): price = pos.price if price is not None: # Note: Here we could assert that price.currency == units.currency. if price.number is MISSING or units.number is MISSING: converted_number = MISSING else: converted_number = price.number * units.number weight = Amount(converted_number, price.currency) return weight beancount.core.data \uf0c1 Basic data structures used to represent the Ledger entries. beancount.core.data.Balance ( tuple ) \uf0c1 Balance(meta, date, account, amount, tolerance, diff_amount) beancount.core.data.Balance.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Balance.__new__(_cls, meta, date, account, amount, tolerance, diff_amount) special staticmethod \uf0c1 Create new instance of Balance(meta, date, account, amount, tolerance, diff_amount) beancount.core.data.Balance.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.Close ( tuple ) \uf0c1 Close(meta, date, account) beancount.core.data.Close.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Close.__new__(_cls, meta, date, account) special staticmethod \uf0c1 Create new instance of Close(meta, date, account) beancount.core.data.Close.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.Commodity ( tuple ) \uf0c1 Commodity(meta, date, currency) beancount.core.data.Commodity.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Commodity.__new__(_cls, meta, date, currency) special staticmethod \uf0c1 Create new instance of Commodity(meta, date, currency) beancount.core.data.Commodity.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.Custom ( tuple ) \uf0c1 Custom(meta, date, type, values) beancount.core.data.Custom.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Custom.__new__(_cls, meta, date, type, values) special staticmethod \uf0c1 Create new instance of Custom(meta, date, type, values) beancount.core.data.Custom.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.Document ( tuple ) \uf0c1 Document(meta, date, account, filename, tags, links) beancount.core.data.Document.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Document.__new__(_cls, meta, date, account, filename, tags, links) special staticmethod \uf0c1 Create new instance of Document(meta, date, account, filename, tags, links) beancount.core.data.Document.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.Event ( tuple ) \uf0c1 Event(meta, date, type, description) beancount.core.data.Event.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Event.__new__(_cls, meta, date, type, description) special staticmethod \uf0c1 Create new instance of Event(meta, date, type, description) beancount.core.data.Event.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.Note ( tuple ) \uf0c1 Note(meta, date, account, comment) beancount.core.data.Note.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Note.__new__(_cls, meta, date, account, comment) special staticmethod \uf0c1 Create new instance of Note(meta, date, account, comment) beancount.core.data.Note.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.Open ( tuple ) \uf0c1 Open(meta, date, account, currencies, booking) beancount.core.data.Open.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Open.__new__(_cls, meta, date, account, currencies, booking) special staticmethod \uf0c1 Create new instance of Open(meta, date, account, currencies, booking) beancount.core.data.Open.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.Pad ( tuple ) \uf0c1 Pad(meta, date, account, source_account) beancount.core.data.Pad.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Pad.__new__(_cls, meta, date, account, source_account) special staticmethod \uf0c1 Create new instance of Pad(meta, date, account, source_account) beancount.core.data.Pad.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.Posting ( tuple ) \uf0c1 Posting(account, units, cost, price, flag, meta) beancount.core.data.Posting.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Posting.__new__(_cls, account, units, cost, price, flag, meta) special staticmethod \uf0c1 Create new instance of Posting(account, units, cost, price, flag, meta) beancount.core.data.Posting.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.Price ( tuple ) \uf0c1 Price(meta, date, currency, amount) beancount.core.data.Price.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Price.__new__(_cls, meta, date, currency, amount) special staticmethod \uf0c1 Create new instance of Price(meta, date, currency, amount) beancount.core.data.Price.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.Query ( tuple ) \uf0c1 Query(meta, date, name, query_string) beancount.core.data.Query.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Query.__new__(_cls, meta, date, name, query_string) special staticmethod \uf0c1 Create new instance of Query(meta, date, name, query_string) beancount.core.data.Query.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.Transaction ( tuple ) \uf0c1 Transaction(meta, date, flag, payee, narration, tags, links, postings) beancount.core.data.Transaction.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.Transaction.__new__(_cls, meta, date, flag, payee, narration, tags, links, postings) special staticmethod \uf0c1 Create new instance of Transaction(meta, date, flag, payee, narration, tags, links, postings) beancount.core.data.Transaction.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.TxnPosting ( tuple ) \uf0c1 TxnPosting(txn, posting) beancount.core.data.TxnPosting.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.data.TxnPosting.__new__(_cls, txn, posting) special staticmethod \uf0c1 Create new instance of TxnPosting(txn, posting) beancount.core.data.TxnPosting.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.data.create_simple_posting(entry, account, number, currency) \uf0c1 Create a simple posting on the entry, with just a number and currency (no cost). Parameters: entry \u2013 The entry instance to add the posting to. account \u2013 A string, the account to use on the posting. number \u2013 A Decimal number or string to use in the posting's Amount. currency \u2013 A string, the currency for the Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. Source code in beancount/core/data.py def create_simple_posting(entry, account, number, currency): \"\"\"Create a simple posting on the entry, with just a number and currency (no cost). Args: entry: The entry instance to add the posting to. account: A string, the account to use on the posting. number: A Decimal number or string to use in the posting's Amount. currency: A string, the currency for the Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. \"\"\" if isinstance(account, str): pass if number is None: units = None else: if not isinstance(number, Decimal): number = D(number) units = Amount(number, currency) posting = Posting(account, units, None, None, None, None) if entry is not None: entry.postings.append(posting) return posting beancount.core.data.create_simple_posting_with_cost(entry, account, number, currency, cost_number, cost_currency) \uf0c1 Create a simple posting on the entry, with just a number and currency (no cost). Parameters: entry \u2013 The entry instance to add the posting to. account \u2013 A string, the account to use on the posting. number \u2013 A Decimal number or string to use in the posting's Amount. currency \u2013 A string, the currency for the Amount. cost_number \u2013 A Decimal number or string to use for the posting's cost Amount. cost_currency \u2013 a string, the currency for the cost Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. Source code in beancount/core/data.py def create_simple_posting_with_cost(entry, account, number, currency, cost_number, cost_currency): \"\"\"Create a simple posting on the entry, with just a number and currency (no cost). Args: entry: The entry instance to add the posting to. account: A string, the account to use on the posting. number: A Decimal number or string to use in the posting's Amount. currency: A string, the currency for the Amount. cost_number: A Decimal number or string to use for the posting's cost Amount. cost_currency: a string, the currency for the cost Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. \"\"\" if isinstance(account, str): pass if not isinstance(number, Decimal): number = D(number) if cost_number and not isinstance(cost_number, Decimal): cost_number = D(cost_number) units = Amount(number, currency) cost = Cost(cost_number, cost_currency, None, None) posting = Posting(account, units, cost, None, None, None) if entry is not None: entry.postings.append(posting) return posting beancount.core.data.entry_sortkey(entry) \uf0c1 Sort-key for entries. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Parameters: entry \u2013 An entry instance. Returns: A tuple of (date, integer, integer), that forms the sort key for the entry. Source code in beancount/core/data.py def entry_sortkey(entry): \"\"\"Sort-key for entries. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Args: entry: An entry instance. Returns: A tuple of (date, integer, integer), that forms the sort key for the entry. \"\"\" return (entry.date, SORT_ORDER.get(type(entry), 0), entry.meta[\"lineno\"]) beancount.core.data.filter_txns(entries) \uf0c1 A generator that yields only the Transaction instances. This is such an incredibly common operation that it deserves a terse filtering mechanism. Parameters: entries \u2013 A list of directives. Yields: A sorted list of only the Transaction directives. Source code in beancount/core/data.py def filter_txns(entries): \"\"\"A generator that yields only the Transaction instances. This is such an incredibly common operation that it deserves a terse filtering mechanism. Args: entries: A list of directives. Yields: A sorted list of only the Transaction directives. \"\"\" for entry in entries: if isinstance(entry, Transaction): yield entry beancount.core.data.find_closest(entries, filename, lineno) \uf0c1 Find the closest entry from entries to (filename, lineno). Parameters: entries \u2013 A list of directives. filename \u2013 A string, the name of the ledger file to look for. Be careful to provide the very same filename, and note that the parser stores the absolute path of the filename here. lineno \u2013 An integer, the line number closest after the directive we're looking for. This may be the exact/first line of the directive. Returns: The closest entry found in the given file for the given filename, or None, if none could be found. Source code in beancount/core/data.py def find_closest(entries, filename, lineno): \"\"\"Find the closest entry from entries to (filename, lineno). Args: entries: A list of directives. filename: A string, the name of the ledger file to look for. Be careful to provide the very same filename, and note that the parser stores the absolute path of the filename here. lineno: An integer, the line number closest after the directive we're looking for. This may be the exact/first line of the directive. Returns: The closest entry found in the given file for the given filename, or None, if none could be found. \"\"\" min_diffline = sys.maxsize closest_entry = None for entry in entries: emeta = entry.meta if emeta[\"filename\"] == filename and emeta[\"lineno\"] > 0: diffline = lineno - emeta[\"lineno\"] if 0 <= diffline < min_diffline: min_diffline = diffline closest_entry = entry return closest_entry beancount.core.data.get_entry(posting_or_entry) \uf0c1 Return the entry associated with the posting or entry. Parameters: entry \u2013 A TxnPosting or entry instance Returns: A datetime instance. Source code in beancount/core/data.py def get_entry(posting_or_entry): \"\"\"Return the entry associated with the posting or entry. Args: entry: A TxnPosting or entry instance Returns: A datetime instance. \"\"\" return (posting_or_entry.txn if isinstance(posting_or_entry, TxnPosting) else posting_or_entry) beancount.core.data.has_entry_account_component(entry, component) \uf0c1 Return true if one of the entry's postings has an account component. Parameters: entry \u2013 A Transaction entry. component \u2013 A string, a component of an account name. For instance, Food in Expenses:Food:Restaurant . All components are considered. Returns: Boolean \u2013 true if the component is in the account. Note that a component name must be whole, that is NY is not in Expenses:Taxes:StateNY . Source code in beancount/core/data.py def has_entry_account_component(entry, component): \"\"\"Return true if one of the entry's postings has an account component. Args: entry: A Transaction entry. component: A string, a component of an account name. For instance, ``Food`` in ``Expenses:Food:Restaurant``. All components are considered. Returns: Boolean: true if the component is in the account. Note that a component name must be whole, that is ``NY`` is not in ``Expenses:Taxes:StateNY``. \"\"\" return (isinstance(entry, Transaction) and any(has_component(posting.account, component) for posting in entry.postings)) beancount.core.data.iter_entry_dates(entries, date_begin, date_end) \uf0c1 Iterate over the entries in a date window. Parameters: entries \u2013 A date-sorted list of dated directives. date_begin \u2013 A datetime.date instance, the first date to include. date_end \u2013 A datetime.date instance, one day beyond the last date. Yields: Instances of the dated directives, between the dates, and in the order in which they appear. Source code in beancount/core/data.py def iter_entry_dates(entries, date_begin, date_end): \"\"\"Iterate over the entries in a date window. Args: entries: A date-sorted list of dated directives. date_begin: A datetime.date instance, the first date to include. date_end: A datetime.date instance, one day beyond the last date. Yields: Instances of the dated directives, between the dates, and in the order in which they appear. \"\"\" getdate = lambda entry: entry.date index_begin = bisect_left_with_key(entries, date_begin, key=getdate) index_end = bisect_left_with_key(entries, date_end, key=getdate) for index in range(index_begin, index_end): yield entries[index] beancount.core.data.new_directive(clsname, fields) \uf0c1 Create a directive class. Do not include default fields. This should probably be carried out through inheritance. Parameters: name \u2013 A string, the capitalized name of the directive. fields ( List[Tuple] ) \u2013 A string or the list of strings, names for the fields to add to the base tuple. Returns: \u2013 A type object for the new directive type. Source code in beancount/core/data.py def new_directive(clsname, fields: List[Tuple]) -> NamedTuple: \"\"\"Create a directive class. Do not include default fields. This should probably be carried out through inheritance. Args: name: A string, the capitalized name of the directive. fields: A string or the list of strings, names for the fields to add to the base tuple. Returns: A type object for the new directive type. \"\"\" return NamedTuple( clsname, [('meta', Meta), ('date', datetime.date)] + fields) beancount.core.data.new_metadata(filename, lineno, kvlist=None) \uf0c1 Create a new metadata container from the filename and line number. Parameters: filename \u2013 A string, the filename for the creator of this directive. lineno \u2013 An integer, the line number where the directive has been created. kvlist \u2013 An optional container of key-values. Returns: A metadata dict. Source code in beancount/core/data.py def new_metadata(filename, lineno, kvlist=None): \"\"\"Create a new metadata container from the filename and line number. Args: filename: A string, the filename for the creator of this directive. lineno: An integer, the line number where the directive has been created. kvlist: An optional container of key-values. Returns: A metadata dict. \"\"\" meta = {'filename': filename, 'lineno': lineno} if kvlist: meta.update(kvlist) return meta beancount.core.data.posting_has_conversion(posting) \uf0c1 Return true if this position involves a conversion. A conversion is when there is a price attached to the amount but no cost. This is used on transactions to convert between units. Parameters: posting \u2013 an instance of Posting Returns: A boolean, true if this posting has a price conversion. Source code in beancount/core/data.py def posting_has_conversion(posting): \"\"\"Return true if this position involves a conversion. A conversion is when there is a price attached to the amount but no cost. This is used on transactions to convert between units. Args: posting: an instance of Posting Return: A boolean, true if this posting has a price conversion. \"\"\" return (posting.cost is None and posting.price is not None) beancount.core.data.posting_sortkey(entry) \uf0c1 Sort-key for entries or postings. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Parameters: entry \u2013 A Posting or entry instance Returns: A tuple of (date, integer, integer), that forms the sort key for the posting or entry. Source code in beancount/core/data.py def posting_sortkey(entry): \"\"\"Sort-key for entries or postings. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Args: entry: A Posting or entry instance Returns: A tuple of (date, integer, integer), that forms the sort key for the posting or entry. \"\"\" if isinstance(entry, TxnPosting): entry = entry.txn return (entry.date, SORT_ORDER.get(type(entry), 0), entry.meta[\"lineno\"]) beancount.core.data.remove_account_postings(account, entries) \uf0c1 Remove all postings with the given account. Parameters: account \u2013 A string, the account name whose postings we want to remove. Returns: A list of entries without the rounding postings. Source code in beancount/core/data.py def remove_account_postings(account, entries): \"\"\"Remove all postings with the given account. Args: account: A string, the account name whose postings we want to remove. Returns: A list of entries without the rounding postings. \"\"\" new_entries = [] for entry in entries: if isinstance(entry, Transaction) and ( any(posting.account == account for posting in entry.postings)): entry = entry._replace(postings=[posting for posting in entry.postings if posting.account != account]) new_entries.append(entry) return new_entries beancount.core.data.sanity_check_types(entry, allow_none_for_tags_and_links=False) \uf0c1 Check that the entry and its postings has all correct data types. Parameters: entry \u2013 An instance of one of the entries to be checked. allow_none_for_tags_and_links \u2013 A boolean, whether to allow plugins to generate Transaction objects with None as value for the 'tags' or 'links' attributes. Exceptions: AssertionError \u2013 If there is anything that is unexpected, raises an exception. Source code in beancount/core/data.py def sanity_check_types(entry, allow_none_for_tags_and_links=False): \"\"\"Check that the entry and its postings has all correct data types. Args: entry: An instance of one of the entries to be checked. allow_none_for_tags_and_links: A boolean, whether to allow plugins to generate Transaction objects with None as value for the 'tags' or 'links' attributes. Raises: AssertionError: If there is anything that is unexpected, raises an exception. \"\"\" assert isinstance(entry, ALL_DIRECTIVES), \"Invalid directive type\" assert isinstance(entry.meta, dict), \"Invalid type for meta\" assert 'filename' in entry.meta, \"Missing filename in metadata\" assert 'lineno' in entry.meta, \"Missing line number in metadata\" assert isinstance(entry.date, datetime.date), \"Invalid date type\" if isinstance(entry, Transaction): assert isinstance(entry.flag, (NoneType, str)), \"Invalid flag type\" assert isinstance(entry.payee, (NoneType, str)), \"Invalid payee type\" assert isinstance(entry.narration, (NoneType, str)), \"Invalid narration type\" set_types = ((NoneType, set, frozenset) if allow_none_for_tags_and_links else (set, frozenset)) assert isinstance(entry.tags, set_types), ( \"Invalid tags type: {}\".format(type(entry.tags))) assert isinstance(entry.links, set_types), ( \"Invalid links type: {}\".format(type(entry.links))) assert isinstance(entry.postings, list), \"Invalid postings list type\" for posting in entry.postings: assert isinstance(posting, Posting), \"Invalid posting type\" assert isinstance(posting.account, str), \"Invalid account type\" assert isinstance(posting.units, (Amount, NoneType)), \"Invalid units type\" assert isinstance(posting.cost, (Cost, CostSpec, NoneType)), \"Invalid cost type\" assert isinstance(posting.price, (Amount, NoneType)), \"Invalid price type\" assert isinstance(posting.flag, (str, NoneType)), \"Invalid flag type\" beancount.core.data.sorted(entries) \uf0c1 A convenience to sort a list of entries, using entry_sortkey(). Parameters: entries \u2013 A list of directives. Returns: A sorted list of directives. Source code in beancount/core/data.py def sorted(entries): \"\"\"A convenience to sort a list of entries, using entry_sortkey(). Args: entries: A list of directives. Returns: A sorted list of directives. \"\"\" return builtins.sorted(entries, key=entry_sortkey) beancount.core.data.transaction_has_conversion(transaction) \uf0c1 Given a Transaction entry, return true if at least one of the postings has a price conversion (without an associated cost). These are the source of non-zero conversion balances. Parameters: transaction \u2013 an instance of a Transaction entry. Returns: A boolean, true if this transaction contains at least one posting with a price conversion. Source code in beancount/core/data.py def transaction_has_conversion(transaction): \"\"\"Given a Transaction entry, return true if at least one of the postings has a price conversion (without an associated cost). These are the source of non-zero conversion balances. Args: transaction: an instance of a Transaction entry. Returns: A boolean, true if this transaction contains at least one posting with a price conversion. \"\"\" assert isinstance(transaction, Transaction), ( \"Invalid type of entry for transaction: {}\".format(transaction)) for posting in transaction.postings: if posting_has_conversion(posting): return True return False beancount.core.display_context \uf0c1 A settings class to offer control over the number of digits rendered. This module contains routines that can accumulate information on the width and precision of numbers to be rendered and derive the precision required to render all of them consistently and under certain common alignment requirements. This is required in order to output neatly lined up columns of numbers in various styles. A common case is that the precision can be observed for numbers present in the input file. This display precision can be used as the \"precision by default\" if we write a routine for which it is inconvenient to feed all the numbers to build such an accumulator. Here are all the aspects supported by this module: PRECISION: Numbers for a particular currency are always rendered to the same precision, and they can be rendered to one of two precisions; either the most common number of fractional digits, or the maximum number of digits seen (this is useful for rendering prices). ALIGNMENT: Several alignment methods are supported. \"natural\": Render the strings as small as possible with no padding, but to their currency's precision. Like this: '1.2345' '764' '-7,409.01' '0.00000125' \"dot-aligned\": The periods will align vertically, the left and right sides are padded so that the column of numbers has the same width: ' 1.2345 ' ' 764 ' '-7,409.01 ' ' 0.00000125' \"right\": The strings are all flushed right, the left side is padded so that the column of numbers has the same width: ' 1.2345' ' 764' ' -7,409.01' ' 0.00000125' SIGN: If a negative sign is present in the input numbers, the rendered numbers reserve a space for it. If not, then we save the space. COMMAS: If the user requests to render commas, commas are rendered in the output. RESERVED: A number of extra integral digits reserved on the left in order to allow rendering novel numbers that haven't yet been seen. For example, balances may contains much larger numbers than the numbers seen in input files, and these need to be accommodated when aligning to the right. beancount.core.display_context.Align ( Enum ) \uf0c1 Alignment style for numbers. beancount.core.display_context.DisplayContext \uf0c1 A builder object used to construct a DisplayContext from a series of numbers. Attributes: Name Type Description ccontexts A dict of currency string to CurrencyContext instance. commas A bool, true if we should render commas. This just gets propagated onwards as the default value of to build with. beancount.core.display_context.DisplayContext.build(self, alignment=, precision=, commas=None, reserved=0) \uf0c1 Build a formatter for the given display context. Parameters: alignment \u2013 The desired alignment. precision \u2013 The desired precision. commas \u2013 Whether to render commas or not. If 'None', the default value carried by the context will be used. reserved \u2013 An integer, the number of extra digits to be allocated in the maximum width calculations. Source code in beancount/core/display_context.py def build(self, alignment=Align.NATURAL, precision=Precision.MOST_COMMON, commas=None, reserved=0): \"\"\"Build a formatter for the given display context. Args: alignment: The desired alignment. precision: The desired precision. commas: Whether to render commas or not. If 'None', the default value carried by the context will be used. reserved: An integer, the number of extra digits to be allocated in the maximum width calculations. \"\"\" if commas is None: commas = self.commas if alignment == Align.NATURAL: build_method = self._build_natural elif alignment == Align.RIGHT: build_method = self._build_right elif alignment == Align.DOT: build_method = self._build_dot else: raise ValueError(\"Unknown alignment: {}\".format(alignment)) fmtstrings = build_method(precision, commas, reserved) return DisplayFormatter(self, precision, fmtstrings) beancount.core.display_context.DisplayContext.quantize(self, number, currency, precision=) \uf0c1 Quantize the given number to the given precision. Parameters: number \u2013 A Decimal instance, the number to be quantized. currency \u2013 A currency string. precision \u2013 Which precision to use. Returns: A Decimal instance, the quantized number. Source code in beancount/core/display_context.py def quantize(self, number, currency, precision=Precision.MOST_COMMON): \"\"\"Quantize the given number to the given precision. Args: number: A Decimal instance, the number to be quantized. currency: A currency string. precision: Which precision to use. Returns: A Decimal instance, the quantized number. \"\"\" assert isinstance(number, Decimal), \"Invalid data: {}\".format(number) ccontext = self.ccontexts[currency] num_fractional_digits = ccontext.get_fractional(precision) if num_fractional_digits is None: # Note: We could probably logging.warn() this situation here. return number qdigit = Decimal(1).scaleb(-num_fractional_digits) return number.quantize(qdigit) beancount.core.display_context.DisplayContext.set_commas(self, commas) \uf0c1 Set the default value for rendering commas. Source code in beancount/core/display_context.py def set_commas(self, commas): \"\"\"Set the default value for rendering commas.\"\"\" self.commas = commas beancount.core.display_context.DisplayContext.update(self, number, currency='__default__') \uf0c1 Update the builder with the given number for the given currency. Parameters: number \u2013 An instance of Decimal to consider for this currency. currency \u2013 An optional string, the currency this numbers applies to. Source code in beancount/core/display_context.py def update(self, number, currency='__default__'): \"\"\"Update the builder with the given number for the given currency. Args: number: An instance of Decimal to consider for this currency. currency: An optional string, the currency this numbers applies to. \"\"\" self.ccontexts[currency].update(number) beancount.core.display_context.DisplayFormatter \uf0c1 A class used to contain various settings that control how we output numbers. In particular, the precision used for each currency, and whether or not commas should be printed. This object is intended to be passed around to all functions that format numbers to strings. Attributes: Name Type Description dcontext A DisplayContext instance. precision An enum of Precision from which it was built. fmtstrings A dict of currency to pre-baked format strings for it. fmtfuncs A dict of currency to pre-baked formatting functions for it. beancount.core.display_context.Precision ( Enum ) \uf0c1 The type of precision required. beancount.core.distribution \uf0c1 A simple accumulator for data about a mathematical distribution. beancount.core.distribution.Distribution \uf0c1 A class that computes a histogram of integer values. This is used to compute a length that will cover at least some decent fraction of the samples. beancount.core.distribution.Distribution.empty(self) \uf0c1 Return true if the distribution is empty. Returns: A boolean. Source code in beancount/core/distribution.py def empty(self): \"\"\"Return true if the distribution is empty. Returns: A boolean. \"\"\" return len(self.hist) == 0 beancount.core.distribution.Distribution.max(self) \uf0c1 Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. Source code in beancount/core/distribution.py def max(self): \"\"\"Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. \"\"\" if not self.hist: return None value, _ = sorted(self.hist.items())[-1] return value beancount.core.distribution.Distribution.min(self) \uf0c1 Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. Source code in beancount/core/distribution.py def min(self): \"\"\"Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. \"\"\" if not self.hist: return None value, _ = sorted(self.hist.items())[0] return value beancount.core.distribution.Distribution.mode(self) \uf0c1 Return the mode of the distribution. Returns: An element of the value type, or None, if the distribution was empty. Source code in beancount/core/distribution.py def mode(self): \"\"\"Return the mode of the distribution. Returns: An element of the value type, or None, if the distribution was empty. \"\"\" if not self.hist: return None max_value = 0 max_count = 0 for value, count in sorted(self.hist.items()): if count >= max_count: max_count = count max_value = value return max_value beancount.core.distribution.Distribution.update(self, value) \uf0c1 Add a sample to the distribution. Parameters: value \u2013 A value of the function. Source code in beancount/core/distribution.py def update(self, value): \"\"\"Add a sample to the distribution. Args: value: A value of the function. \"\"\" self.hist[value] += 1 beancount.core.flags \uf0c1 Flag constants. beancount.core.getters \uf0c1 Getter functions that operate on lists of entries to return various lists of things that they reference, accounts, tags, links, currencies, etc. beancount.core.getters.GetAccounts \uf0c1 Accounts gatherer. beancount.core.getters.GetAccounts.Balance(_, entry) \uf0c1 Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one(_, entry): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return (entry.account,) beancount.core.getters.GetAccounts.Close(_, entry) \uf0c1 Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one(_, entry): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return (entry.account,) beancount.core.getters.GetAccounts.Commodity(_, entry) \uf0c1 Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero(_, entry): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return () beancount.core.getters.GetAccounts.Custom(_, entry) \uf0c1 Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero(_, entry): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return () beancount.core.getters.GetAccounts.Document(_, entry) \uf0c1 Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one(_, entry): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return (entry.account,) beancount.core.getters.GetAccounts.Event(_, entry) \uf0c1 Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero(_, entry): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return () beancount.core.getters.GetAccounts.Note(_, entry) \uf0c1 Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one(_, entry): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return (entry.account,) beancount.core.getters.GetAccounts.Open(_, entry) \uf0c1 Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one(_, entry): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return (entry.account,) beancount.core.getters.GetAccounts.Pad(_, entry) \uf0c1 Process a Pad directive. Parameters: entry \u2013 An instance of Pad. Returns: The two accounts of the Pad directive. Source code in beancount/core/getters.py def Pad(_, entry): \"\"\"Process a Pad directive. Args: entry: An instance of Pad. Returns: The two accounts of the Pad directive. \"\"\" return (entry.account, entry.source_account) beancount.core.getters.GetAccounts.Price(_, entry) \uf0c1 Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero(_, entry): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return () beancount.core.getters.GetAccounts.Query(_, entry) \uf0c1 Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero(_, entry): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return () beancount.core.getters.GetAccounts.Transaction(_, entry) \uf0c1 Process a Transaction directive. Parameters: entry \u2013 An instance of Transaction. Yields: The accounts of the legs of the transaction. Source code in beancount/core/getters.py def Transaction(_, entry): \"\"\"Process a Transaction directive. Args: entry: An instance of Transaction. Yields: The accounts of the legs of the transaction. \"\"\" for posting in entry.postings: yield posting.account beancount.core.getters.GetAccounts.get_accounts_use_map(self, entries) \uf0c1 Gather the list of accounts from the list of entries. Parameters: entries \u2013 A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. Source code in beancount/core/getters.py def get_accounts_use_map(self, entries): \"\"\"Gather the list of accounts from the list of entries. Args: entries: A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. \"\"\" accounts_first = {} accounts_last = {} for entry in entries: method = getattr(self, entry.__class__.__name__) for account_ in method(entry): if account_ not in accounts_first: accounts_first[account_] = entry.date accounts_last[account_] = entry.date return accounts_first, accounts_last beancount.core.getters.GetAccounts.get_entry_accounts(self, entry) \uf0c1 Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Parameters: entry \u2013 A directive instance. Returns: A set of account name strings. Source code in beancount/core/getters.py def get_entry_accounts(self, entry): \"\"\"Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Args: entry: A directive instance. Returns: A set of account name strings. \"\"\" method = getattr(self, entry.__class__.__name__) return set(method(entry)) beancount.core.getters.get_account_components(entries) \uf0c1 Gather all the account components available in the given directives. Parameters: entries \u2013 A list of directive instances. Returns: A list of strings, the unique account components, including the root account names. Source code in beancount/core/getters.py def get_account_components(entries): \"\"\"Gather all the account components available in the given directives. Args: entries: A list of directive instances. Returns: A list of strings, the unique account components, including the root account names. \"\"\" accounts = get_accounts(entries) components = set() for account_name in accounts: components.update(account.split(account_name)) return sorted(components) beancount.core.getters.get_account_open_close(entries) \uf0c1 Fetch the open/close entries for each of the accounts. If an open or close entry happens to be duplicated, accept the earliest entry (chronologically). Parameters: entries \u2013 A list of directive instances. Returns: A map of account name strings to pairs of (open-directive, close-directive) tuples. Source code in beancount/core/getters.py def get_account_open_close(entries): \"\"\"Fetch the open/close entries for each of the accounts. If an open or close entry happens to be duplicated, accept the earliest entry (chronologically). Args: entries: A list of directive instances. Returns: A map of account name strings to pairs of (open-directive, close-directive) tuples. \"\"\" # A dict of account name to (open-entry, close-entry). open_close_map = defaultdict(lambda: [None, None]) for entry in entries: if not isinstance(entry, (Open, Close)): continue open_close = open_close_map[entry.account] index = 0 if isinstance(entry, Open) else 1 previous_entry = open_close[index] if previous_entry is not None: if previous_entry.date <= entry.date: entry = previous_entry open_close[index] = entry return dict(open_close_map) beancount.core.getters.get_accounts(entries) \uf0c1 Gather all the accounts references by a list of directives. Parameters: entries \u2013 A list of directive instances. Returns: A set of account strings. Source code in beancount/core/getters.py def get_accounts(entries): \"\"\"Gather all the accounts references by a list of directives. Args: entries: A list of directive instances. Returns: A set of account strings. \"\"\" _, accounts_last = _GetAccounts.get_accounts_use_map(entries) return accounts_last.keys() beancount.core.getters.get_accounts_use_map(entries) \uf0c1 Gather all the accounts references by a list of directives. Parameters: entries \u2013 A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. Source code in beancount/core/getters.py def get_accounts_use_map(entries): \"\"\"Gather all the accounts references by a list of directives. Args: entries: A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. \"\"\" return _GetAccounts.get_accounts_use_map(entries) beancount.core.getters.get_active_years(entries) \uf0c1 Yield all the years that have at least one entry in them. Parameters: entries \u2013 A list of directive instances. Yields: Unique dates see in the list of directives. Source code in beancount/core/getters.py def get_active_years(entries): \"\"\"Yield all the years that have at least one entry in them. Args: entries: A list of directive instances. Yields: Unique dates see in the list of directives. \"\"\" seen = set() prev_year = None for entry in entries: year = entry.date.year if year != prev_year: prev_year = year assert year not in seen seen.add(year) yield year beancount.core.getters.get_all_links(entries) \uf0c1 Return a list of all the links seen in the given entries. Parameters: entries \u2013 A list of directive instances. Returns: A set of links strings. Source code in beancount/core/getters.py def get_all_links(entries): \"\"\"Return a list of all the links seen in the given entries. Args: entries: A list of directive instances. Returns: A set of links strings. \"\"\" all_links = set() for entry in entries: if not isinstance(entry, Transaction): continue if entry.links: all_links.update(entry.links) return sorted(all_links) beancount.core.getters.get_all_payees(entries) \uf0c1 Return a list of all the unique payees seen in the given entries. Parameters: entries \u2013 A list of directive instances. Returns: A set of payee strings. Source code in beancount/core/getters.py def get_all_payees(entries): \"\"\"Return a list of all the unique payees seen in the given entries. Args: entries: A list of directive instances. Returns: A set of payee strings. \"\"\" all_payees = set() for entry in entries: if not isinstance(entry, Transaction): continue all_payees.add(entry.payee) all_payees.discard(None) return sorted(all_payees) beancount.core.getters.get_all_tags(entries) \uf0c1 Return a list of all the tags seen in the given entries. Parameters: entries \u2013 A list of directive instances. Returns: A set of tag strings. Source code in beancount/core/getters.py def get_all_tags(entries): \"\"\"Return a list of all the tags seen in the given entries. Args: entries: A list of directive instances. Returns: A set of tag strings. \"\"\" all_tags = set() for entry in entries: if not isinstance(entry, Transaction): continue if entry.tags: all_tags.update(entry.tags) return sorted(all_tags) beancount.core.getters.get_commodity_map(entries, create_missing=True) \uf0c1 Create map of commodity names to Commodity entries. Parameters: entries \u2013 A list of directive instances. create_missing \u2013 A boolean, true if you want to automatically generate missing commodity directives if not present in the output map. Returns: A map of commodity name strings to Commodity directives. Source code in beancount/core/getters.py def get_commodity_map(entries, create_missing=True): \"\"\"Create map of commodity names to Commodity entries. Args: entries: A list of directive instances. create_missing: A boolean, true if you want to automatically generate missing commodity directives if not present in the output map. Returns: A map of commodity name strings to Commodity directives. \"\"\" if not entries: return {} commodities_map = {} for entry in entries: if isinstance(entry, Commodity): commodities_map[entry.currency] = entry elif isinstance(entry, Transaction): for posting in entry.postings: # Main currency. units = posting.units commodities_map.setdefault(units.currency, None) # Currency in cost. cost = posting.cost if cost: commodities_map.setdefault(cost.currency, None) # Currency in price. price = posting.price if price: commodities_map.setdefault(price.currency, None) elif isinstance(entry, Balance): commodities_map.setdefault(entry.amount.currency, None) elif isinstance(entry, Price): commodities_map.setdefault(entry.currency, None) if create_missing: # Create missing Commodity directives when they haven't been specified explicitly. # (I think it might be better to always do this from the loader.) date = entries[0].date meta = data.new_metadata('', 0) commodities_map = { commodity: (entry if entry is not None else Commodity(meta, date, commodity)) for commodity, entry in commodities_map.items()} return commodities_map beancount.core.getters.get_dict_accounts(account_names) \uf0c1 Return a nested dict of all the unique leaf names. account names are labelled with LABEL=True Parameters: account_names \u2013 An iterable of account names (strings) Returns: A nested OrderedDict of account leafs Source code in beancount/core/getters.py def get_dict_accounts(account_names): \"\"\"Return a nested dict of all the unique leaf names. account names are labelled with LABEL=True Args: account_names: An iterable of account names (strings) Returns: A nested OrderedDict of account leafs \"\"\" leveldict = OrderedDict() for account_name in account_names: nested_dict = leveldict for component in account.split(account_name): nested_dict = nested_dict.setdefault(component, OrderedDict()) nested_dict[get_dict_accounts.ACCOUNT_LABEL] = True return leveldict beancount.core.getters.get_entry_accounts(entry) \uf0c1 Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Parameters: entries \u2013 A directive instance. Returns: A set of account strings. Source code in beancount/core/getters.py def get_entry_accounts(entry): \"\"\"Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Args: entries: A directive instance. Returns: A set of account strings. \"\"\" return _GetAccounts.get_entry_accounts(entry) beancount.core.getters.get_leveln_parent_accounts(account_names, level, nrepeats=0) \uf0c1 Return a list of all the unique leaf names at level N in an account hierarchy. Parameters: account_names \u2013 A list of account names (strings) level \u2013 The level to cross-cut. 0 is for root accounts. nrepeats \u2013 A minimum number of times a leaf is required to be present in the the list of unique account names in order to be returned by this function. Returns: A list of leaf node names. Source code in beancount/core/getters.py def get_leveln_parent_accounts(account_names, level, nrepeats=0): \"\"\"Return a list of all the unique leaf names at level N in an account hierarchy. Args: account_names: A list of account names (strings) level: The level to cross-cut. 0 is for root accounts. nrepeats: A minimum number of times a leaf is required to be present in the the list of unique account names in order to be returned by this function. Returns: A list of leaf node names. \"\"\" leveldict = defaultdict(int) for account_name in set(account_names): components = account.split(account_name) if level < len(components): leveldict[components[level]] += 1 levels = {level_ for level_, count in leveldict.items() if count > nrepeats} return sorted(levels) beancount.core.getters.get_min_max_dates(entries, types=None) \uf0c1 Return the minimum and maximum dates in the list of entries. Parameters: entries \u2013 A list of directive instances. types \u2013 An optional tuple of types to restrict the entries to. Returns: A pair of datetime.date dates, the minimum and maximum dates seen in the directives. Source code in beancount/core/getters.py def get_min_max_dates(entries, types=None): \"\"\"Return the minimum and maximum dates in the list of entries. Args: entries: A list of directive instances. types: An optional tuple of types to restrict the entries to. Returns: A pair of datetime.date dates, the minimum and maximum dates seen in the directives. \"\"\" date_first = date_last = None for entry in entries: if types and not isinstance(entry, types): continue date_first = entry.date break for entry in reversed(entries): if types and not isinstance(entry, types): continue date_last = entry.date break return (date_first, date_last) beancount.core.getters.get_values_meta(name_to_entries_map, *meta_keys, *, default=None) \uf0c1 Get a map of the metadata from a map of entries values. Given a dict of some key to a directive instance (or None), return a mapping of the key to the metadata extracted from each directive, or a default value. This can be used to gather a particular piece of metadata from an accounts map or a commodities map. Parameters: name_to_entries_map \u2013 A dict of something to an entry or None. meta_keys \u2013 A list of strings, the keys to fetch from the metadata. default \u2013 The default value to use if the metadata is not available or if the value/entry is None. Returns: A mapping of the keys of name_to_entries_map to the values of the 'meta_keys' metadata. If there are multiple 'meta_keys', each value is a tuple of them. On the other hand, if there is only a single one, the value itself is returned. Source code in beancount/core/getters.py def get_values_meta(name_to_entries_map, *meta_keys, default=None): \"\"\"Get a map of the metadata from a map of entries values. Given a dict of some key to a directive instance (or None), return a mapping of the key to the metadata extracted from each directive, or a default value. This can be used to gather a particular piece of metadata from an accounts map or a commodities map. Args: name_to_entries_map: A dict of something to an entry or None. meta_keys: A list of strings, the keys to fetch from the metadata. default: The default value to use if the metadata is not available or if the value/entry is None. Returns: A mapping of the keys of name_to_entries_map to the values of the 'meta_keys' metadata. If there are multiple 'meta_keys', each value is a tuple of them. On the other hand, if there is only a single one, the value itself is returned. \"\"\" value_map = {} for key, entry in name_to_entries_map.items(): value_list = [] for meta_key in meta_keys: value_list.append(entry.meta.get(meta_key, default) if entry is not None else default) value_map[key] = (value_list[0] if len(meta_keys) == 1 else tuple(value_list)) return value_map beancount.core.interpolate \uf0c1 Code used to automatically complete postings without positions. beancount.core.interpolate.BalanceError ( tuple ) \uf0c1 BalanceError(source, message, entry) beancount.core.interpolate.BalanceError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/interpolate.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.interpolate.BalanceError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of BalanceError(source, message, entry) beancount.core.interpolate.BalanceError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/interpolate.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.interpolate.compute_entries_balance(entries, prefix=None, date=None) \uf0c1 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. Parameters: entries \u2013 A list of directives. prefix \u2013 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 \u2013 A datetime.date instance at which to stop adding up the balance. The date is exclusive. Returns: An instance of Inventory. Source code in beancount/core/interpolate.py 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 beancount.core.interpolate.compute_entry_context(entries, context_entry) \uf0c1 Compute the balances of all accounts referenced by entry up to entry. This provides the inventory of the accounts to which the entry is to be applied, before and after. Parameters: entries \u2013 A list of directives. context_entry \u2013 The entry for which we want to obtain the before and after context. Returns: Two dicts of account-name to Inventory instance, one which represents the context before the entry is applied, and one that represents the context after it has been applied. Source code in beancount/core/interpolate.py def compute_entry_context(entries, context_entry): \"\"\"Compute the balances of all accounts referenced by entry up to entry. This provides the inventory of the accounts to which the entry is to be applied, before and after. Args: entries: A list of directives. context_entry: The entry for which we want to obtain the before and after context. Returns: Two dicts of account-name to Inventory instance, one which represents the context before the entry is applied, and one that represents the context after it has been applied. \"\"\" assert context_entry is not None, \"context_entry is missing.\" # Get the set of accounts for which to compute the context. context_accounts = getters.get_entry_accounts(context_entry) # Iterate over the entries until we find the target one and accumulate the # balance. context_before = collections.defaultdict(inventory.Inventory) for entry in entries: if entry is context_entry: break if isinstance(entry, Transaction): for posting in entry.postings: if not any(posting.account == account for account in context_accounts): continue balance = context_before[posting.account] balance.add_position(posting) # Compute the after context for the entry. context_after = copy.deepcopy(context_before) if isinstance(context_entry, Transaction): for posting in entry.postings: balance = context_after[posting.account] balance.add_position(posting) return context_before, context_after beancount.core.interpolate.compute_residual(postings) \uf0c1 Compute the residual of a set of complete postings, and the per-currency precision. This is used to cross-check a balanced transaction. The precision is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. Parameters: postings \u2013 A list of Posting instances. Returns: An instance of Inventory, with the residual of the given list of postings. Source code in beancount/core/interpolate.py def compute_residual(postings): \"\"\"Compute the residual of a set of complete postings, and the per-currency precision. This is used to cross-check a balanced transaction. The precision is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. Args: postings: A list of Posting instances. Returns: An instance of Inventory, with the residual of the given list of postings. \"\"\" inventory = Inventory() for posting in postings: # Skip auto-postings inserted to absorb the residual (rounding error). if posting.meta and posting.meta.get(AUTOMATIC_RESIDUAL, False): continue # Add to total residual balance. inventory.add_amount(convert.get_weight(posting)) return inventory beancount.core.interpolate.fill_residual_posting(entry, account_rounding) \uf0c1 If necessary, insert a posting to absorb the residual. This makes the transaction balance exactly. Note: This was developed in order to tweak transactions before exporting them to Ledger. A better method would be to enable the feature that automatically inserts these rounding postings on all transactions, and so maybe this method can be deprecated if we do so. Parameters: entry \u2013 An instance of a Transaction. account_rounding \u2013 A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A possibly new, modified entry with a new posting. If a residual was not needed - the transaction already balanced perfectly - no new leg is inserted. Source code in beancount/core/interpolate.py def fill_residual_posting(entry, account_rounding): \"\"\"If necessary, insert a posting to absorb the residual. This makes the transaction balance exactly. Note: This was developed in order to tweak transactions before exporting them to Ledger. A better method would be to enable the feature that automatically inserts these rounding postings on all transactions, and so maybe this method can be deprecated if we do so. Args: entry: An instance of a Transaction. account_rounding: A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A possibly new, modified entry with a new posting. If a residual was not needed - the transaction already balanced perfectly - no new leg is inserted. \"\"\" residual = compute_residual(entry.postings) if not residual.is_empty(): new_postings = list(entry.postings) new_postings.extend(get_residual_postings(residual, account_rounding)) entry = entry._replace(postings=new_postings) return entry beancount.core.interpolate.get_residual_postings(residual, account_rounding) \uf0c1 Create postings to book the given residuals. Parameters: residual \u2013 An Inventory, the residual positions. account_rounding \u2013 A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A list of new postings to be inserted to reduce the given residual. Source code in beancount/core/interpolate.py def get_residual_postings(residual, account_rounding): \"\"\"Create postings to book the given residuals. Args: residual: An Inventory, the residual positions. account_rounding: A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A list of new postings to be inserted to reduce the given residual. \"\"\" meta = {AUTOMATIC_META: True, AUTOMATIC_RESIDUAL: True} return [Posting(account_rounding, -position.units, position.cost, None, None, meta.copy()) for position in residual.get_positions()] beancount.core.interpolate.has_nontrivial_balance(posting) \uf0c1 Return True if a Posting has a balance amount that would have to be calculated. Parameters: posting \u2013 A Posting instance. Returns: A boolean. Source code in beancount/core/interpolate.py def has_nontrivial_balance(posting): \"\"\"Return True if a Posting has a balance amount that would have to be calculated. Args: posting: A Posting instance. Returns: A boolean. \"\"\" return posting.cost or posting.price beancount.core.interpolate.infer_tolerances(postings, options_map, use_cost=None) \uf0c1 Infer tolerances from a list of postings. The tolerance is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. The 'use_cost' option allows one to experiment with letting postings at cost and at price influence the maximum value of the tolerance. It's tricky to use and alters the definition of the tolerance in a non-trivial way, if you use it. The tolerance is expanded by the sum of the cost times a fraction 'M' of the smallest digits in the number of units for all postings held at cost. For example, in this transaction: 2006-01-17 * \"Plan Contribution\" Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:Cash -1150.00 USD The tolerance for units of USD will calculated as the MAXIMUM of: 0.01 * M = 0.005 (from the 1150.00 USD leg) The sum of 0.001 * M x 30.96 = 0.01548 + 0.001 * M x 30.96 = 0.01548 = 0.03096 So the tolerance for USD in this case is max(0.005, 0.03096) = 0.03096. Prices contribute similarly to the maximum tolerance allowed. Note that 'M' above is the inferred_tolerance_multiplier and its default value is 0.5. Parameters: postings \u2013 A list of Posting instances. options_map \u2013 A dict of options. use_cost \u2013 A boolean, true if we should be using a combination of the smallest digit of the number times the cost or price in order to infer the tolerance. If the value is left unspecified (as 'None'), the default value can be overridden by setting an option. Returns: A dict of currency to the tolerated difference amount to be used for it, e.g. 0.005. Source code in beancount/core/interpolate.py def infer_tolerances(postings, options_map, use_cost=None): \"\"\"Infer tolerances from a list of postings. The tolerance is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. The 'use_cost' option allows one to experiment with letting postings at cost and at price influence the maximum value of the tolerance. It's tricky to use and alters the definition of the tolerance in a non-trivial way, if you use it. The tolerance is expanded by the sum of the cost times a fraction 'M' of the smallest digits in the number of units for all postings held at cost. For example, in this transaction: 2006-01-17 * \"Plan Contribution\" Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:Cash -1150.00 USD The tolerance for units of USD will calculated as the MAXIMUM of: 0.01 * M = 0.005 (from the 1150.00 USD leg) The sum of 0.001 * M x 30.96 = 0.01548 + 0.001 * M x 30.96 = 0.01548 = 0.03096 So the tolerance for USD in this case is max(0.005, 0.03096) = 0.03096. Prices contribute similarly to the maximum tolerance allowed. Note that 'M' above is the inferred_tolerance_multiplier and its default value is 0.5. Args: postings: A list of Posting instances. options_map: A dict of options. use_cost: A boolean, true if we should be using a combination of the smallest digit of the number times the cost or price in order to infer the tolerance. If the value is left unspecified (as 'None'), the default value can be overridden by setting an option. Returns: A dict of currency to the tolerated difference amount to be used for it, e.g. 0.005. \"\"\" if use_cost is None: use_cost = options_map[\"infer_tolerance_from_cost\"] inferred_tolerance_multiplier = options_map[\"inferred_tolerance_multiplier\"] default_tolerances = options_map[\"inferred_tolerance_default\"] tolerances = default_tolerances.copy() cost_tolerances = collections.defaultdict(D) for posting in postings: # Skip the precision on automatically inferred postings. if posting.meta and AUTOMATIC_META in posting.meta: continue units = posting.units if not (isinstance(units, Amount) and isinstance(units.number, Decimal)): continue # Compute bounds on the number. currency = units.currency expo = units.number.as_tuple().exponent if expo < 0: # Note: the exponent is a negative value. tolerance = ONE.scaleb(expo) * inferred_tolerance_multiplier tolerances[currency] = max(tolerance, tolerances.get(currency, -1024)) if not use_cost: continue # Compute bounds on the smallest digit of the number implied as cost. cost = posting.cost if cost is not None: cost_currency = cost.currency if isinstance(cost, Cost): cost_tolerance = min(tolerance * cost.number, MAXIMUM_TOLERANCE) else: assert isinstance(cost, CostSpec) cost_tolerance = MAXIMUM_TOLERANCE for cost_number in cost.number_total, cost.number_per: if cost_number is None or cost_number is MISSING: continue cost_tolerance = min(tolerance * cost_number, cost_tolerance) cost_tolerances[cost_currency] += cost_tolerance # Compute bounds on the smallest digit of the number implied as cost. price = posting.price if isinstance(price, Amount) and isinstance(price.number, Decimal): price_currency = price.currency price_tolerance = min(tolerance * price.number, MAXIMUM_TOLERANCE) cost_tolerances[price_currency] += price_tolerance for currency, tolerance in cost_tolerances.items(): tolerances[currency] = max(tolerance, tolerances.get(currency, -1024)) default = tolerances.pop('*', ZERO) return defdict.ImmutableDictWithDefault(tolerances, default=default) beancount.core.interpolate.is_tolerance_user_specified(tolerance) \uf0c1 Return true if the given tolerance number was user-specified. This would allow the user to provide a tolerance like # 0.1234 but not 0.123456. This is used to detect whether a tolerance value # is input by the user and not inferred automatically. Parameters: tolerance \u2013 An instance of Decimal. Returns: A boolean. Source code in beancount/core/interpolate.py def is_tolerance_user_specified(tolerance): \"\"\"Return true if the given tolerance number was user-specified. This would allow the user to provide a tolerance like # 0.1234 but not 0.123456. This is used to detect whether a tolerance value # is input by the user and not inferred automatically. Args: tolerance: An instance of Decimal. Returns: A boolean. \"\"\" return len(tolerance.as_tuple().digits) < MAX_TOLERANCE_DIGITS beancount.core.interpolate.quantize_with_tolerance(tolerances, currency, number) \uf0c1 Quantize the units using the tolerance dict. Parameters: tolerances \u2013 A dict of currency to tolerance Decimalvalues. number \u2013 A number to quantize. currency \u2013 A string currency. Returns: A Decimal, the number possibly quantized. Source code in beancount/core/interpolate.py def quantize_with_tolerance(tolerances, currency, number): \"\"\"Quantize the units using the tolerance dict. Args: tolerances: A dict of currency to tolerance Decimalvalues. number: A number to quantize. currency: A string currency. Returns: A Decimal, the number possibly quantized. \"\"\" # Applying rounding to the default tolerance, if there is one. tolerance = tolerances.get(currency) if tolerance: quantum = (tolerance * 2).normalize() # If the tolerance is a neat number provided by the user, # quantize the inferred numbers. See doc on quantize(): # # Unlike other operations, if the length of the coefficient # after the quantize operation would be greater than # precision, then an InvalidOperation is signaled. This # guarantees that, unless there is an error condition, the # quantized exponent is always equal to that of the # right-hand operand. if is_tolerance_user_specified(quantum): number = number.quantize(quantum) return number beancount.core.inventory \uf0c1 A container for an inventory of positions. This module provides a container class that can hold positions. An inventory is a mapping of positions, where each position is keyed by (currency: str, cost: Cost) -> position: Position where 'currency': The commodity under consideration, USD, CAD, or stock units such as HOOL, MSFT, AAPL, etc.; 'cost': None or a Cost instance existing of cost currency, number, date, and label; 'position': A Position object, whose 'units' attribute is guaranteed to have the same currency as 'currency' and whose 'cost' attribute is equal to the 'cost' key. It basically stores the number of units. This is meant to accommodate both booked and non-booked amounts. The clever trick that we pull to do this is that for positions which aren't booked, we simply leave the 'cost' as None. This is the case for most of the transactions. beancount.core.inventory.Booking ( Enum ) \uf0c1 Result of booking a new lot to an existing inventory. beancount.core.inventory.Inventory ( dict ) \uf0c1 An Inventory is a set of positions. Attributes: Name Type Description positions A list of Position instances, held in this Inventory object. beancount.core.inventory.Inventory.__abs__(self) special \uf0c1 Return an inventory with the absolute value of each position. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def __abs__(self): \"\"\"Return an inventory with the absolute value of each position. Returns: An instance of Inventory. \"\"\" return Inventory({key: abs(pos) for key, pos in self.items()}) beancount.core.inventory.Inventory.__add__(self, other) special \uf0c1 Add another inventory to this one. This inventory is not modified. Parameters: other \u2013 An instance of Inventory. Returns: A new instance of Inventory. Source code in beancount/core/inventory.py def __add__(self, other): \"\"\"Add another inventory to this one. This inventory is not modified. Args: other: An instance of Inventory. Returns: A new instance of Inventory. \"\"\" new_inventory = self.__copy__() new_inventory.add_inventory(other) return new_inventory beancount.core.inventory.Inventory.__copy__(self) special \uf0c1 A shallow copy of this inventory object. Returns: An instance of Inventory, equal to this one. Source code in beancount/core/inventory.py def __copy__(self): \"\"\"A shallow copy of this inventory object. Returns: An instance of Inventory, equal to this one. \"\"\" return Inventory(self) beancount.core.inventory.Inventory.__iadd__(self, other) special \uf0c1 Add all the positions of another Inventory instance to this one. Parameters: other \u2013 An instance of Inventory to add to this one. Returns: This inventory, modified. Source code in beancount/core/inventory.py def add_inventory(self, other): \"\"\"Add all the positions of another Inventory instance to this one. Args: other: An instance of Inventory to add to this one. Returns: This inventory, modified. \"\"\" if self.is_empty(): # Optimization for empty inventories; if the current one is empty, # adopt all of the other inventory's positions without running # through the full aggregation checks. This should be very cheap. We # can do this because the positions are immutable. self.update(other) else: for position in other.get_positions(): self.add_position(position) return self beancount.core.inventory.Inventory.__init__(self, positions=None) special \uf0c1 Create a new inventory using a list of existing positions. Parameters: positions \u2013 A list of Position instances or an existing dict or Inventory instance. Source code in beancount/core/inventory.py def __init__(self, positions=None): \"\"\"Create a new inventory using a list of existing positions. Args: positions: A list of Position instances or an existing dict or Inventory instance. \"\"\" if isinstance(positions, (dict, Inventory)): dict.__init__(self, positions) else: dict.__init__(self) if positions: assert isinstance(positions, Iterable) for position in positions: self.add_position(position) beancount.core.inventory.Inventory.__iter__(self) special \uf0c1 Iterate over the positions. Note that there is no guaranteed order. Source code in beancount/core/inventory.py def __iter__(self): \"\"\"Iterate over the positions. Note that there is no guaranteed order.\"\"\" return iter(self.values()) beancount.core.inventory.Inventory.__lt__(self, other) special \uf0c1 Inequality comparison operator. Source code in beancount/core/inventory.py def __lt__(self, other): \"\"\"Inequality comparison operator.\"\"\" return sorted(self) < sorted(other) beancount.core.inventory.Inventory.__mul__(self, scalar) special \uf0c1 Scale/multiply the contents of the inventory. Parameters: scalar \u2013 A Decimal. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def __mul__(self, scalar): \"\"\"Scale/multiply the contents of the inventory. Args: scalar: A Decimal. Returns: An instance of Inventory. \"\"\" return Inventory({key: pos * scalar for key, pos in self.items()}) beancount.core.inventory.Inventory.__neg__(self) special \uf0c1 Return an inventory with the negative of values of this one. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def __neg__(self): \"\"\"Return an inventory with the negative of values of this one. Returns: An instance of Inventory. \"\"\" return Inventory({key: -pos for key, pos in self.items()}) beancount.core.inventory.Inventory.__repr__(self) special \uf0c1 Render as a human-readable string. Returns: A string, for human consumption. Source code in beancount/core/inventory.py def __str__(self): \"\"\"Render as a human-readable string. Returns: A string, for human consumption. \"\"\" return self.to_string() beancount.core.inventory.Inventory.__str__(self) special \uf0c1 Render as a human-readable string. Returns: A string, for human consumption. Source code in beancount/core/inventory.py def __str__(self): \"\"\"Render as a human-readable string. Returns: A string, for human consumption. \"\"\" return self.to_string() beancount.core.inventory.Inventory.add_amount(self, units, cost=None) \uf0c1 Add to this inventory using amount and cost. This adds with strict lot matching, that is, no partial matches are done on the arguments to the keys of the inventory. Parameters: units \u2013 An Amount instance to add. cost \u2013 An instance of Cost or None, as a key to the inventory. Returns: A pair of (position, booking) where 'position' is the position that that was modified BEFORE it was modified, and where 'booking' is a Booking enum that hints at how the lot was booked to this inventory. Position may be None if there is no corresponding Position object, e.g. the position was deleted. Source code in beancount/core/inventory.py def add_amount(self, units, cost=None): \"\"\"Add to this inventory using amount and cost. This adds with strict lot matching, that is, no partial matches are done on the arguments to the keys of the inventory. Args: units: An Amount instance to add. cost: An instance of Cost or None, as a key to the inventory. Returns: A pair of (position, booking) where 'position' is the position that that was modified BEFORE it was modified, and where 'booking' is a Booking enum that hints at how the lot was booked to this inventory. Position may be None if there is no corresponding Position object, e.g. the position was deleted. \"\"\" if ASSERTS_TYPES: assert isinstance(units, Amount), ( \"Internal error: {!r} (type: {})\".format(units, type(units).__name__)) assert cost is None or isinstance(cost, Cost), ( \"Internal error: {!r} (type: {})\".format(cost, type(cost).__name__)) # Find the position. key = (units.currency, cost) pos = self.get(key, None) if pos is not None: # Note: In order to augment or reduce, all the fields have to match. # Check if reducing. booking = (Booking.REDUCED if not same_sign(pos.units.number, units.number) else Booking.AUGMENTED) # Compute the new number of units. number = pos.units.number + units.number if number == ZERO: # If empty, delete the position. del self[key] else: # Otherwise update it. self[key] = Position(Amount(number, units.currency), cost) else: # If not found, create a new one. if units.number == ZERO: booking = Booking.IGNORED else: self[key] = Position(units, cost) booking = Booking.CREATED return pos, booking beancount.core.inventory.Inventory.add_inventory(self, other) \uf0c1 Add all the positions of another Inventory instance to this one. Parameters: other \u2013 An instance of Inventory to add to this one. Returns: This inventory, modified. Source code in beancount/core/inventory.py def add_inventory(self, other): \"\"\"Add all the positions of another Inventory instance to this one. Args: other: An instance of Inventory to add to this one. Returns: This inventory, modified. \"\"\" if self.is_empty(): # Optimization for empty inventories; if the current one is empty, # adopt all of the other inventory's positions without running # through the full aggregation checks. This should be very cheap. We # can do this because the positions are immutable. self.update(other) else: for position in other.get_positions(): self.add_position(position) return self beancount.core.inventory.Inventory.add_position(self, position) \uf0c1 Add using a position (with strict lot matching). Return True if this position was booked against and reduced another. Parameters: position \u2013 The Posting or Position to add to this inventory. Returns: A pair of (position, booking) where 'position' is the position that that was modified, and where 'booking' is a Booking enum that hints at how the lot was booked to this inventory. Source code in beancount/core/inventory.py def add_position(self, position): \"\"\"Add using a position (with strict lot matching). Return True if this position was booked against and reduced another. Args: position: The Posting or Position to add to this inventory. Returns: A pair of (position, booking) where 'position' is the position that that was modified, and where 'booking' is a Booking enum that hints at how the lot was booked to this inventory. \"\"\" if ASSERTS_TYPES: assert hasattr(position, 'units') and hasattr(position, 'cost'), ( \"Invalid type for position: {}\".format(position)) assert isinstance(position.cost, (type(None), Cost)), ( \"Invalid type for cost: {}\".format(position.cost)) return self.add_amount(position.units, position.cost) beancount.core.inventory.Inventory.average(self) \uf0c1 Average all lots of the same currency together. Use the minimum date from each aggregated set of lots. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def average(self): \"\"\"Average all lots of the same currency together. Use the minimum date from each aggregated set of lots. Returns: An instance of Inventory. \"\"\" groups = collections.defaultdict(list) for position in self: key = (position.units.currency, position.cost.currency if position.cost else None) groups[key].append(position) average_inventory = Inventory() for (currency, cost_currency), positions in groups.items(): total_units = sum(position.units.number for position in positions) # Explicitly skip aggregates when resulting in zero units. if total_units == ZERO: continue units_amount = Amount(total_units, currency) if cost_currency: total_cost = sum(convert.get_cost(position).number for position in positions) cost_number = (Decimal('Infinity') if total_units == ZERO else (total_cost / total_units)) min_date = None for pos in positions: pos_date = pos.cost.date if pos.cost else None if pos_date is not None: min_date = (pos_date if min_date is None else min(min_date, pos_date)) cost = Cost(cost_number, cost_currency, min_date, None) else: cost = None average_inventory.add_amount(units_amount, cost) return average_inventory beancount.core.inventory.Inventory.cost_currencies(self) \uf0c1 Return the list of unit currencies held in this inventory. Returns: A set of currency strings. Source code in beancount/core/inventory.py def cost_currencies(self): \"\"\"Return the list of unit currencies held in this inventory. Returns: A set of currency strings. \"\"\" return set(cost.currency for _, cost in self.keys() if cost is not None) beancount.core.inventory.Inventory.currencies(self) \uf0c1 Return the list of unit currencies held in this inventory. Returns: A list of currency strings. Source code in beancount/core/inventory.py def currencies(self): \"\"\"Return the list of unit currencies held in this inventory. Returns: A list of currency strings. \"\"\" return set(currency for currency, _ in self.keys()) beancount.core.inventory.Inventory.currency_pairs(self) \uf0c1 Return the commodities held in this inventory. Returns: A set of currency strings. Source code in beancount/core/inventory.py def currency_pairs(self): \"\"\"Return the commodities held in this inventory. Returns: A set of currency strings. \"\"\" return set(position.currency_pair() for position in self) beancount.core.inventory.Inventory.from_string(string) staticmethod \uf0c1 Create an Inventory from a string. This is useful for writing tests. Parameters: string \u2013 A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. Source code in beancount/core/inventory.py @staticmethod def from_string(string): \"\"\"Create an Inventory from a string. This is useful for writing tests. Args: string: A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. \"\"\" new_inventory = Inventory() # We need to split the comma-separated positions but ignore commas # occurring within a {...cost...} specification. position_strs = re.split( r'([-+]?[0-9,.]+\\s+[A-Z]+\\s*(?:{[^}]*})?)\\s*,?\\s*', string)[1::2] for position_str in position_strs: new_inventory.add_position(position_from_string(position_str)) return new_inventory beancount.core.inventory.Inventory.get_currency_units(self, currency) \uf0c1 Fetch the total amount across all the position in the given currency. This may sum multiple lots in the same currency denomination. Parameters: currency \u2013 A string, the currency to filter the positions with. Returns: An instance of Amount, with the given currency. Source code in beancount/core/inventory.py def get_currency_units(self, currency): \"\"\"Fetch the total amount across all the position in the given currency. This may sum multiple lots in the same currency denomination. Args: currency: A string, the currency to filter the positions with. Returns: An instance of Amount, with the given currency. \"\"\" total_units = ZERO for position in self: if position.units.currency == currency: total_units += position.units.number return Amount(total_units, currency) beancount.core.inventory.Inventory.get_only_position(self) \uf0c1 Return the first position and assert there are no more. If the inventory is empty, return None. Source code in beancount/core/inventory.py def get_only_position(self): \"\"\"Return the first position and assert there are no more. If the inventory is empty, return None. \"\"\" if len(self) > 0: if len(self) > 1: raise AssertionError(\"Inventory has more than one expected \" \"position: {}\".format(self)) return next(iter(self)) beancount.core.inventory.Inventory.get_positions(self) \uf0c1 Return the positions in this inventory. Returns: A shallow copy of the list of positions. Source code in beancount/core/inventory.py def get_positions(self): \"\"\"Return the positions in this inventory. Returns: A shallow copy of the list of positions. \"\"\" return list(iter(self)) beancount.core.inventory.Inventory.is_empty(self) \uf0c1 Return true if the inventory is empty, that is, has no positions. Returns: A boolean. Source code in beancount/core/inventory.py def is_empty(self): \"\"\"Return true if the inventory is empty, that is, has no positions. Returns: A boolean. \"\"\" return len(self) == 0 beancount.core.inventory.Inventory.is_mixed(self) \uf0c1 Return true if the inventory contains a mix of positive and negative lots for at least one instrument. Returns: A boolean. Source code in beancount/core/inventory.py def is_mixed(self): \"\"\"Return true if the inventory contains a mix of positive and negative lots for at least one instrument. Returns: A boolean. \"\"\" signs_map = {} for position in self: sign = position.units.number >= 0 prev_sign = signs_map.setdefault(position.units.currency, sign) if sign != prev_sign: return True return False beancount.core.inventory.Inventory.is_reduced_by(self, ramount) \uf0c1 Return true if the amount could reduce this inventory. Parameters: ramount \u2013 An instance of Amount. Returns: A boolean. Source code in beancount/core/inventory.py def is_reduced_by(self, ramount): \"\"\"Return true if the amount could reduce this inventory. Args: ramount: An instance of Amount. Returns: A boolean. \"\"\" if ramount.number == ZERO: return False for position in self: units = position.units if (ramount.currency == units.currency and not same_sign(ramount.number, units.number)): return True return False beancount.core.inventory.Inventory.is_small(self, tolerances) \uf0c1 Return true if all the positions in the inventory are small. Parameters: tolerances \u2013 A Decimal, the small number of units under which a position is considered small, or a dict of currency to such epsilon precision. Returns: A boolean. Source code in beancount/core/inventory.py def is_small(self, tolerances): \"\"\"Return true if all the positions in the inventory are small. Args: tolerances: A Decimal, the small number of units under which a position is considered small, or a dict of currency to such epsilon precision. Returns: A boolean. \"\"\" if isinstance(tolerances, dict): for position in self: tolerance = tolerances.get(position.units.currency, ZERO) if abs(position.units.number) > tolerance: return False small = True else: small = not any(abs(position.units.number) > tolerances for position in self) return small beancount.core.inventory.Inventory.reduce(self, reducer, *args) \uf0c1 Reduce an inventory using one of the conversion functions. See functions in beancount.core.convert. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def reduce(self, reducer, *args): \"\"\"Reduce an inventory using one of the conversion functions. See functions in beancount.core.convert. Returns: An instance of Inventory. \"\"\" inventory = Inventory() for position in self: inventory.add_amount(reducer(position, *args)) return inventory beancount.core.inventory.Inventory.segregate_units(self, currencies) \uf0c1 Split up the list of positions to the given currencies. Parameters: currencies \u2013 A list of currency strings, the currencies to isolate. Returns: A dict of currency to Inventory instances. Source code in beancount/core/inventory.py def segregate_units(self, currencies): \"\"\"Split up the list of positions to the given currencies. Args: currencies: A list of currency strings, the currencies to isolate. Returns: A dict of currency to Inventory instances. \"\"\" per_currency_dict = {currency: Inventory() for currency in currencies} per_currency_dict[None] = Inventory() for position in self: currency = position.units.currency key = (currency if currency in currencies else None) per_currency_dict[key].add_position(position) return per_currency_dict beancount.core.inventory.Inventory.to_string(self, dformat=, parens=True) \uf0c1 Convert an Inventory instance to a printable string. Parameters: dformat \u2013 An instance of DisplayFormatter. parents \u2013 A boolean, true if we should surround the results by parentheses. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/inventory.py def to_string(self, dformat=DEFAULT_FORMATTER, parens=True): \"\"\"Convert an Inventory instance to a printable string. Args: dformat: An instance of DisplayFormatter. parents: A boolean, true if we should surround the results by parentheses. Returns: A formatted string of the quantized amount and symbol. \"\"\" fmt = '({})' if parens else '{}' return fmt.format( ', '.join(pos.to_string(dformat) for pos in sorted(self))) beancount.core.inventory.check_invariants(inv) \uf0c1 Check the invariants of the Inventory. Parameters: inventory \u2013 An instance of Inventory. Returns: True if the invariants are respected. Source code in beancount/core/inventory.py def check_invariants(inv): \"\"\"Check the invariants of the Inventory. Args: inventory: An instance of Inventory. Returns: True if the invariants are respected. \"\"\" # Check that all the keys are unique. lots = set((pos.units.currency, pos.cost) for pos in inv) assert len(lots) == len(inv), \"Invalid inventory: {}\".format(inv) # Check that none of the amounts is zero. for pos in inv: assert pos.units.number != ZERO, \"Invalid position size: {}\".format(pos) beancount.core.inventory.from_string(string) \uf0c1 Create an Inventory from a string. This is useful for writing tests. Parameters: string \u2013 A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. Source code in beancount/core/inventory.py @staticmethod def from_string(string): \"\"\"Create an Inventory from a string. This is useful for writing tests. Args: string: A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. \"\"\" new_inventory = Inventory() # We need to split the comma-separated positions but ignore commas # occurring within a {...cost...} specification. position_strs = re.split( r'([-+]?[0-9,.]+\\s+[A-Z]+\\s*(?:{[^}]*})?)\\s*,?\\s*', string)[1::2] for position_str in position_strs: new_inventory.add_position(position_from_string(position_str)) return new_inventory beancount.core.number \uf0c1 The module contains the basic Decimal type import. About Decimal usage: Do not import Decimal from the 'decimal' or 'cdecimal' modules; always import your Decimal class from beancount.core.amount. Prefer to use D() to create new instances of Decimal objects, which handles more syntax, e.g., handles None, and numbers with commas. beancount.core.number.D(strord=None) \uf0c1 Convert a string into a Decimal object. This is used in parsing amounts from files in the importers. This is the main function you should use to build all numbers the system manipulates (never use floating-point in an accounting system). Commas are stripped and ignored, as they are assumed to be thousands separators (the French comma separator as decimal is not supported). This function just returns the argument if it is already a Decimal object, for convenience. Parameters: strord \u2013 A string or Decimal instance. Returns: A Decimal instance. Source code in beancount/core/number.py def D(strord=None): \"\"\"Convert a string into a Decimal object. This is used in parsing amounts from files in the importers. This is the main function you should use to build all numbers the system manipulates (never use floating-point in an accounting system). Commas are stripped and ignored, as they are assumed to be thousands separators (the French comma separator as decimal is not supported). This function just returns the argument if it is already a Decimal object, for convenience. Args: strord: A string or Decimal instance. Returns: A Decimal instance. \"\"\" try: # Note: try a map lookup and optimize performance here. if strord is None or strord == '': return Decimal() elif isinstance(strord, str): return Decimal(_CLEAN_NUMBER_RE.sub('', strord)) elif isinstance(strord, Decimal): return strord elif isinstance(strord, (int, float)): return Decimal(strord) else: assert strord is None, \"Invalid value to convert: {}\".format(strord) except Exception as exc: raise ValueError(\"Impossible to create Decimal instance from {!s}: {}\".format( strord, exc)) beancount.core.number.is_fast_decimal(decimal_module) \uf0c1 Return true if a fast C decimal implementation is installed. Source code in beancount/core/number.py def is_fast_decimal(decimal_module): \"Return true if a fast C decimal implementation is installed.\" return isinstance(decimal_module.Decimal().sqrt, types.BuiltinFunctionType) beancount.core.number.round_to(number, increment) \uf0c1 Round a number down to a particular increment. Parameters: number \u2013 A Decimal, the number to be rounded. increment \u2013 A Decimal, the size of the increment. Returns: A Decimal, the rounded number. Source code in beancount/core/number.py def round_to(number, increment): \"\"\"Round a number *down* to a particular increment. Args: number: A Decimal, the number to be rounded. increment: A Decimal, the size of the increment. Returns: A Decimal, the rounded number. \"\"\" return int((number / increment)) * increment beancount.core.number.same_sign(number1, number2) \uf0c1 Return true if both numbers have the same sign. Parameters: number1 \u2013 An instance of Decimal. number2 \u2013 An instance of Decimal. Returns: A boolean. Source code in beancount/core/number.py def same_sign(number1, number2): \"\"\"Return true if both numbers have the same sign. Args: number1: An instance of Decimal. number2: An instance of Decimal. Returns: A boolean. \"\"\" return (number1 >= 0) == (number2 >= 0) beancount.core.position \uf0c1 A position object, which consists of units Amount and cost Cost. See types below for details. beancount.core.position.Cost ( tuple ) \uf0c1 Cost(number, currency, date, label) beancount.core.position.Cost.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/position.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.position.Cost.__new__(_cls, number, currency, date, label) special staticmethod \uf0c1 Create new instance of Cost(number, currency, date, label) beancount.core.position.Cost.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/position.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.position.CostSpec ( tuple ) \uf0c1 CostSpec(number_per, number_total, currency, date, label, merge) beancount.core.position.CostSpec.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/position.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.core.position.CostSpec.__new__(_cls, number_per, number_total, currency, date, label, merge) special staticmethod \uf0c1 Create new instance of CostSpec(number_per, number_total, currency, date, label, merge) beancount.core.position.CostSpec.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/position.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.core.position.Position ( _Position ) \uf0c1 A 'Position' is a pair of units and optional cost. This is used to track inventories. Attributes: Name Type Description units Amount An Amount, the number of units and its currency. cost Cost A Cost that represents the lot, or None. beancount.core.position.Position.__abs__(self) special \uf0c1 Return the absolute value of the position. Returns: An instance of Position with the absolute units. Source code in beancount/core/position.py def __abs__(self): \"\"\"Return the absolute value of the position. Returns: An instance of Position with the absolute units. \"\"\" return Position(amount_abs(self.units), self.cost) beancount.core.position.Position.__copy__(self) special \uf0c1 Shallow copy, except for the lot, which can be shared. This is important for performance reasons; a lot of time is spent here during balancing. Returns: A shallow copy of this position. Source code in beancount/core/position.py def __copy__(self): \"\"\"Shallow copy, except for the lot, which can be shared. This is important for performance reasons; a lot of time is spent here during balancing. Returns: A shallow copy of this position. \"\"\" # Note: We use Decimal() for efficiency. return Position(copy.copy(self.units), copy.copy(self.cost)) beancount.core.position.Position.__eq__(self, other) special \uf0c1 Equality comparison with another Position. The objects are considered equal if both number and lot are matching, and if the number of units is zero and the other position is None, that is also okay. Parameters: other \u2013 An instance of Position, or None. Returns: A boolean, true if the positions are equal. Source code in beancount/core/position.py def __eq__(self, other): \"\"\"Equality comparison with another Position. The objects are considered equal if both number and lot are matching, and if the number of units is zero and the other position is None, that is also okay. Args: other: An instance of Position, or None. Returns: A boolean, true if the positions are equal. \"\"\" return (self.units.number == ZERO if other is None else (self.units == other.units and self.cost == other.cost)) beancount.core.position.Position.__hash__(self) special \uf0c1 Compute a hash for this position. Returns: A hash of this position object. Source code in beancount/core/position.py def __hash__(self): \"\"\"Compute a hash for this position. Returns: A hash of this position object. \"\"\" return hash((self.units, self.cost)) beancount.core.position.Position.__lt__(self, other) special \uf0c1 A less-than comparison operator for positions. Parameters: other \u2013 Another instance of Position. Returns: True if this positions is smaller than the other position. Source code in beancount/core/position.py def __lt__(self, other): \"\"\"A less-than comparison operator for positions. Args: other: Another instance of Position. Returns: True if this positions is smaller than the other position. \"\"\" return self.sortkey() < other.sortkey() beancount.core.position.Position.__mul__(self, scalar) special \uf0c1 Scale/multiply the contents of the position. Parameters: scalar \u2013 A Decimal. Returns: An instance of Inventory. Source code in beancount/core/position.py def __mul__(self, scalar): \"\"\"Scale/multiply the contents of the position. Args: scalar: A Decimal. Returns: An instance of Inventory. \"\"\" return Position(amount_mul(self.units, scalar), self.cost) beancount.core.position.Position.__neg__(self) special \uf0c1 Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. Source code in beancount/core/position.py def get_negative(self): \"\"\"Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. \"\"\" # Note: We use Decimal() for efficiency. return Position(-self.units, self.cost) beancount.core.position.Position.__new__(cls, units, cost=None) special staticmethod \uf0c1 Create new instance of _Position(units, cost) Source code in beancount/core/position.py def __new__(cls, units, cost=None): assert isinstance(units, Amount), ( \"Expected an Amount for units; received '{}'\".format(units)) assert cost is None or isinstance(cost, Position.cost_types), ( \"Expected a Cost for cost; received '{}'\".format(cost)) return _Position.__new__(cls, units, cost) beancount.core.position.Position.__repr__(self) special \uf0c1 Return a string representation of the position. Returns: A string, a printable representation of the position. Source code in beancount/core/position.py def __str__(self): \"\"\"Return a string representation of the position. Returns: A string, a printable representation of the position. \"\"\" return self.to_string() beancount.core.position.Position.__str__(self) special \uf0c1 Return a string representation of the position. Returns: A string, a printable representation of the position. Source code in beancount/core/position.py def __str__(self): \"\"\"Return a string representation of the position. Returns: A string, a printable representation of the position. \"\"\" return self.to_string() beancount.core.position.Position.currency_pair(self) \uf0c1 Return the currency pair associated with this position. Returns: A pair of a currency string and a cost currency string or None. Source code in beancount/core/position.py def currency_pair(self): \"\"\"Return the currency pair associated with this position. Returns: A pair of a currency string and a cost currency string or None. \"\"\" return (self.units.currency, self.cost.currency if self.cost else None) beancount.core.position.Position.from_amounts(units, cost_amount=None) staticmethod \uf0c1 Create a position from an amount and a cost. Parameters: amount \u2013 An amount, that represents the number of units and the lot currency. cost_amount \u2013 If not None, represents the cost amount. Returns: A Position instance. Source code in beancount/core/position.py @staticmethod def from_amounts(units, cost_amount=None): \"\"\"Create a position from an amount and a cost. Args: amount: An amount, that represents the number of units and the lot currency. cost_amount: If not None, represents the cost amount. Returns: A Position instance. \"\"\" assert cost_amount is None or isinstance(cost_amount, Amount), ( \"Invalid type for cost: {}\".format(cost_amount)) cost = (Cost(cost_amount.number, cost_amount.currency, None, None) if cost_amount else None) return Position(units, cost) beancount.core.position.Position.from_string(string) staticmethod \uf0c1 Create a position from a string specification. This is a miniature parser used for building tests. Parameters: string \u2013 A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. Source code in beancount/core/position.py @staticmethod def from_string(string): \"\"\"Create a position from a string specification. This is a miniature parser used for building tests. Args: string: A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. \"\"\" match = re.match( (r'\\s*({})\\s+({})' r'(?:\\s+{{([^}}]*)}})?' r'\\s*$').format(NUMBER_RE, CURRENCY_RE), string) if not match: raise ValueError(\"Invalid string for position: '{}'\".format(string)) number = D(match.group(1)) currency = match.group(2) # Parse a cost expression. cost_number = None cost_currency = None date = None label = None cost_expression = match.group(3) if match.group(3): expressions = [expr.strip() for expr in re.split('[,/]', cost_expression)] for expr in expressions: # Match a compound number. match = re.match( r'({NUMBER_RE})\\s*(?:#\\s*({NUMBER_RE}))?\\s+({CURRENCY_RE})$' .format(NUMBER_RE=NUMBER_RE, CURRENCY_RE=CURRENCY_RE), expr ) if match: per_number, total_number, cost_currency = match.group(1, 2, 3) per_number = D(per_number) if per_number else ZERO total_number = D(total_number) if total_number else ZERO if total_number: # Calculate the per-unit cost. total = number * per_number + total_number per_number = total / number cost_number = per_number continue # Match a date. match = re.match(r'(\\d\\d\\d\\d)[-/](\\d\\d)[-/](\\d\\d)$', expr) if match: date = datetime.date(*map(int, match.group(1, 2, 3))) continue # Match a label. match = re.match(r'\"([^\"]+)*\"$', expr) if match: label = match.group(1) continue # Match a merge-cost marker. match = re.match(r'\\*$', expr) if match: raise ValueError(\"Merge-code not supported in string constructor.\") raise ValueError(\"Invalid cost component: '{}'\".format(expr)) cost = Cost(cost_number, cost_currency, date, label) else: cost = None return Position(Amount(number, currency), cost) beancount.core.position.Position.get_negative(self) \uf0c1 Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. Source code in beancount/core/position.py def get_negative(self): \"\"\"Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. \"\"\" # Note: We use Decimal() for efficiency. return Position(-self.units, self.cost) beancount.core.position.Position.is_negative_at_cost(self) \uf0c1 Return true if the position is held at cost and negative. Returns: A boolean. Source code in beancount/core/position.py def is_negative_at_cost(self): \"\"\"Return true if the position is held at cost and negative. Returns: A boolean. \"\"\" return (self.units.number < ZERO and self.cost is not None) beancount.core.position.Position.sortkey(self) \uf0c1 Return a key to sort positions by. This key depends on the order of the currency of the lot (we want to order common currencies first) and the number of units. Returns: A tuple, used to sort lists of positions. Source code in beancount/core/position.py def sortkey(self): \"\"\"Return a key to sort positions by. This key depends on the order of the currency of the lot (we want to order common currencies first) and the number of units. Returns: A tuple, used to sort lists of positions. \"\"\" currency = self.units.currency order_units = CURRENCY_ORDER.get(currency, NCURRENCIES + len(currency)) if self.cost is not None: cost_number = self.cost.number cost_currency = self.cost.currency else: cost_number = ZERO cost_currency = '' return (order_units, cost_number, cost_currency, self.units.number) beancount.core.position.Position.to_string(self, dformat=, detail=True) \uf0c1 Render the position to a string.See to_string() for details. Source code in beancount/core/position.py def to_string(self, dformat=DEFAULT_FORMATTER, detail=True): \"\"\"Render the position to a string.See to_string() for details. \"\"\" return to_string(self, dformat, detail) beancount.core.position.cost_to_str(cost, dformat, detail=True) \uf0c1 Format an instance of Cost or a CostSpec to a string. Parameters: cost \u2013 An instance of Cost or CostSpec. dformat \u2013 A DisplayFormatter object. detail \u2013 A boolean, true if we should render the non-amount components. Returns: A string, suitable for formatting. Source code in beancount/core/position.py def cost_to_str(cost, dformat, detail=True): \"\"\"Format an instance of Cost or a CostSpec to a string. Args: cost: An instance of Cost or CostSpec. dformat: A DisplayFormatter object. detail: A boolean, true if we should render the non-amount components. Returns: A string, suitable for formatting. \"\"\" strlist = [] if isinstance(cost, Cost): if isinstance(cost.number, Decimal): strlist.append(Amount(cost.number, cost.currency).to_string(dformat)) if detail: if cost.date: strlist.append(cost.date.isoformat()) if cost.label: strlist.append('\"{}\"'.format(cost.label)) elif isinstance(cost, CostSpec): if isinstance(cost.number_per, Decimal) or isinstance(cost.number_total, Decimal): amountlist = [] if isinstance(cost.number_per, Decimal): amountlist.append(dformat.format(cost.number_per)) if isinstance(cost.number_total, Decimal): amountlist.append('#') amountlist.append(dformat.format(cost.number_total)) if isinstance(cost.currency, str): amountlist.append(cost.currency) strlist.append(' '.join(amountlist)) if detail: if cost.date: strlist.append(cost.date.isoformat()) if cost.label: strlist.append('\"{}\"'.format(cost.label)) if cost.merge: strlist.append('*') return ', '.join(strlist) beancount.core.position.from_amounts(units, cost_amount=None) \uf0c1 Create a position from an amount and a cost. Parameters: amount \u2013 An amount, that represents the number of units and the lot currency. cost_amount \u2013 If not None, represents the cost amount. Returns: A Position instance. Source code in beancount/core/position.py @staticmethod def from_amounts(units, cost_amount=None): \"\"\"Create a position from an amount and a cost. Args: amount: An amount, that represents the number of units and the lot currency. cost_amount: If not None, represents the cost amount. Returns: A Position instance. \"\"\" assert cost_amount is None or isinstance(cost_amount, Amount), ( \"Invalid type for cost: {}\".format(cost_amount)) cost = (Cost(cost_amount.number, cost_amount.currency, None, None) if cost_amount else None) return Position(units, cost) beancount.core.position.from_string(string) \uf0c1 Create a position from a string specification. This is a miniature parser used for building tests. Parameters: string \u2013 A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. Source code in beancount/core/position.py @staticmethod def from_string(string): \"\"\"Create a position from a string specification. This is a miniature parser used for building tests. Args: string: A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. \"\"\" match = re.match( (r'\\s*({})\\s+({})' r'(?:\\s+{{([^}}]*)}})?' r'\\s*$').format(NUMBER_RE, CURRENCY_RE), string) if not match: raise ValueError(\"Invalid string for position: '{}'\".format(string)) number = D(match.group(1)) currency = match.group(2) # Parse a cost expression. cost_number = None cost_currency = None date = None label = None cost_expression = match.group(3) if match.group(3): expressions = [expr.strip() for expr in re.split('[,/]', cost_expression)] for expr in expressions: # Match a compound number. match = re.match( r'({NUMBER_RE})\\s*(?:#\\s*({NUMBER_RE}))?\\s+({CURRENCY_RE})$' .format(NUMBER_RE=NUMBER_RE, CURRENCY_RE=CURRENCY_RE), expr ) if match: per_number, total_number, cost_currency = match.group(1, 2, 3) per_number = D(per_number) if per_number else ZERO total_number = D(total_number) if total_number else ZERO if total_number: # Calculate the per-unit cost. total = number * per_number + total_number per_number = total / number cost_number = per_number continue # Match a date. match = re.match(r'(\\d\\d\\d\\d)[-/](\\d\\d)[-/](\\d\\d)$', expr) if match: date = datetime.date(*map(int, match.group(1, 2, 3))) continue # Match a label. match = re.match(r'\"([^\"]+)*\"$', expr) if match: label = match.group(1) continue # Match a merge-cost marker. match = re.match(r'\\*$', expr) if match: raise ValueError(\"Merge-code not supported in string constructor.\") raise ValueError(\"Invalid cost component: '{}'\".format(expr)) cost = Cost(cost_number, cost_currency, date, label) else: cost = None return Position(Amount(number, currency), cost) beancount.core.position.get_position(posting) \uf0c1 Build a Position instance from a Posting instance. Parameters: posting \u2013 An instance of Posting. Returns: An instance of Position. Source code in beancount/core/position.py def get_position(posting): \"\"\"Build a Position instance from a Posting instance. Args: posting: An instance of Posting. Returns: An instance of Position. \"\"\" return Position(posting.units, posting.cost) beancount.core.position.to_string(pos, dformat=, detail=True) \uf0c1 Render the Position or Posting instance to a string. Parameters: pos \u2013 An instance of Position or Posting. dformat \u2013 An instance of DisplayFormatter. detail \u2013 A boolean, true if we should only render the lot details beyond the cost (lot-date, label, etc.). If false, we only render the cost, if present. Returns: A string, the rendered position. Source code in beancount/core/position.py def to_string(pos, dformat=DEFAULT_FORMATTER, detail=True): \"\"\"Render the Position or Posting instance to a string. Args: pos: An instance of Position or Posting. dformat: An instance of DisplayFormatter. detail: A boolean, true if we should only render the lot details beyond the cost (lot-date, label, etc.). If false, we only render the cost, if present. Returns: A string, the rendered position. \"\"\" pos_str = pos.units.to_string(dformat) if pos.cost is not None: pos_str = '{} {{{}}}'.format(pos_str, cost_to_str(pos.cost, dformat, detail)) return pos_str beancount.core.prices \uf0c1 This module has code that can build a database of historical prices at various times, from which unrealized capital gains and market value can be deduced. Prices are deduced from Price entries found in the file, or perhaps created by scripts (for example you could build a script that will fetch live prices online and create entries on-the-fly). beancount.core.prices.PriceMap ( dict ) \uf0c1 A price map dictionary. The keys include both the set of forward (base, quote) pairs and their inverse. In order to determine which are the forward pairs, access the 'forward_pairs' attribute Attributes: Name Type Description forward_pairs A list of (base, quote) keys for the forward pairs. beancount.core.prices.build_price_map(entries) \uf0c1 Build a price map from a list of arbitrary entries. If multiple prices are found for the same (currency, cost-currency) pair at the same date, the latest date is kept and the earlier ones (for that day) are discarded. If inverse price pairs are found, e.g. USD in AUD and AUD in USD, the inverse that has the smallest number of price points is converted into the one that has the most price points. In that way they are reconciled into a single one. Parameters: entries \u2013 A list of directives, hopefully including some Price and/or Returns: A dict of (currency, cost-currency) keys to sorted lists of (date, number) pairs, where 'date' is the date the price occurs at and 'number' a Decimal that represents the price, or rate, between these two currencies/commodities. Each date occurs only once in the sorted list of prices of a particular key. All of the inverses are automatically generated in the price map. Source code in beancount/core/prices.py def build_price_map(entries): \"\"\"Build a price map from a list of arbitrary entries. If multiple prices are found for the same (currency, cost-currency) pair at the same date, the latest date is kept and the earlier ones (for that day) are discarded. If inverse price pairs are found, e.g. USD in AUD and AUD in USD, the inverse that has the smallest number of price points is converted into the one that has the most price points. In that way they are reconciled into a single one. Args: entries: A list of directives, hopefully including some Price and/or Transaction entries. Returns: A dict of (currency, cost-currency) keys to sorted lists of (date, number) pairs, where 'date' is the date the price occurs at and 'number' a Decimal that represents the price, or rate, between these two currencies/commodities. Each date occurs only once in the sorted list of prices of a particular key. All of the inverses are automatically generated in the price map. \"\"\" # Fetch a list of all the price entries seen in the ledger. price_entries = [entry for entry in entries if isinstance(entry, Price)] # Build a map of exchange rates between these units. # (base-currency, quote-currency) -> List of (date, rate). price_map = collections.defaultdict(list) for price in price_entries: base_quote = (price.currency, price.amount.currency) price_map[base_quote].append((price.date, price.amount.number)) # Find pairs of inversed units. inversed_units = [] for base_quote, values in price_map.items(): base, quote = base_quote if (quote, base) in price_map: inversed_units.append(base_quote) # Find pairs of inversed units, and swallow the conversion with the smaller # number of rates into the other one. for base, quote in inversed_units: bq_prices = price_map[(base, quote)] qb_prices = price_map[(quote, base)] remove = ((base, quote) if len(bq_prices) < len(qb_prices) else (quote, base)) base, quote = remove remove_list = price_map[remove] insert_list = price_map[(quote, base)] del price_map[remove] inverted_list = [(date, ONE/rate) for (date, rate) in remove_list if rate != ZERO] insert_list.extend(inverted_list) # Unzip and sort each of the entries and eliminate duplicates on the date. sorted_price_map = PriceMap({ base_quote: list(misc_utils.sorted_uniquify(date_rates, lambda x: x[0], last=True)) for (base_quote, date_rates) in price_map.items()}) # Compute and insert all the inverted rates. forward_pairs = list(sorted_price_map.keys()) for (base, quote), price_list in list(sorted_price_map.items()): # Note: You have to filter out zero prices for zero-cost postings, like # gifted options. sorted_price_map[(quote, base)] = [ (date, ONE/price) for date, price in price_list if price != ZERO] sorted_price_map.forward_pairs = forward_pairs return sorted_price_map beancount.core.prices.get_all_prices(price_map, base_quote) \uf0c1 Return a sorted list of all (date, number) price pairs. Parameters: price_map \u2013 A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote \u2013 A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A list of (date, Decimal) pairs, sorted by date. Exceptions: KeyError \u2013 If the base/quote could not be found. Source code in beancount/core/prices.py def get_all_prices(price_map, base_quote): \"\"\"Return a sorted list of all (date, number) price pairs. Args: price_map: A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote: A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A list of (date, Decimal) pairs, sorted by date. Raises: KeyError: If the base/quote could not be found. \"\"\" base_quote = normalize_base_quote(base_quote) return _lookup_price_and_inverse(price_map, base_quote) beancount.core.prices.get_last_price_entries(entries, date) \uf0c1 Run through the entries until the given date and return the last Price entry encountered for each (currency, cost-currency) pair. Parameters: entries \u2013 A list of directives. date \u2013 An instance of datetime.date. If None, the very latest price is returned. Returns: A list of price entries. Source code in beancount/core/prices.py def get_last_price_entries(entries, date): \"\"\"Run through the entries until the given date and return the last Price entry encountered for each (currency, cost-currency) pair. Args: entries: A list of directives. date: An instance of datetime.date. If None, the very latest price is returned. Returns: A list of price entries. \"\"\" price_entry_map = {} for entry in entries: if date is not None and entry.date >= date: break if isinstance(entry, Price): base_quote = (entry.currency, entry.amount.currency) price_entry_map[base_quote] = entry return sorted(price_entry_map.values(), key=data.entry_sortkey) beancount.core.prices.get_latest_price(price_map, base_quote) \uf0c1 Return the latest price/rate from a price map for the given base/quote pair. This is often used to just get the 'current' price if you're looking at the entire set of entries. Parameters: price_map \u2013 A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. Returns: A pair of (date, number), where 'date' is a datetime.date instance and 'number' is a Decimal of the price, or rate, at that date. The date is the latest date which we have an available price for in the price map. Source code in beancount/core/prices.py def get_latest_price(price_map, base_quote): \"\"\"Return the latest price/rate from a price map for the given base/quote pair. This is often used to just get the 'current' price if you're looking at the entire set of entries. Args: price_map: A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. Returns: A pair of (date, number), where 'date' is a datetime.date instance and 'number' is a Decimal of the price, or rate, at that date. The date is the latest date which we have an available price for in the price map. \"\"\" base_quote = normalize_base_quote(base_quote) # Handle the degenerate case of a currency priced into its own. base, quote = base_quote if quote is None or base == quote: return (None, ONE) # Look up the list and return the latest element. The lists are assumed to # be sorted. try: price_list = _lookup_price_and_inverse(price_map, base_quote) except KeyError: price_list = None if price_list: return price_list[-1] else: return None, None beancount.core.prices.get_price(price_map, base_quote, date=None) \uf0c1 Return the price as of the given date. If the date is unspecified, return the latest price. Parameters: price_map \u2013 A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote \u2013 A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. date \u2013 A datetime.date instance, the date at which we want the conversion rate. Returns: A pair of (datetime.date, Decimal) instance. If no price information could be found, return (None, None). Source code in beancount/core/prices.py def get_price(price_map, base_quote, date=None): \"\"\"Return the price as of the given date. If the date is unspecified, return the latest price. Args: price_map: A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote: A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. date: A datetime.date instance, the date at which we want the conversion rate. Returns: A pair of (datetime.date, Decimal) instance. If no price information could be found, return (None, None). \"\"\" if date is None: return get_latest_price(price_map, base_quote) base_quote = normalize_base_quote(base_quote) # Handle the degenerate case of a currency priced into its own. base, quote = base_quote if quote is None or base == quote: return (None, ONE) try: price_list = _lookup_price_and_inverse(price_map, base_quote) index = bisect_key.bisect_right_with_key(price_list, date, key=lambda x: x[0]) if index == 0: return None, None else: return price_list[index-1] except KeyError: return None, None beancount.core.prices.normalize_base_quote(base_quote) \uf0c1 Convert a slash-separated string to a pair of strings. Parameters: base_quote \u2013 A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A pair of strings. Source code in beancount/core/prices.py def normalize_base_quote(base_quote): \"\"\"Convert a slash-separated string to a pair of strings. Args: base_quote: A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A pair of strings. \"\"\" if isinstance(base_quote, str): base_quote_norm = tuple(base_quote.split('/')) assert len(base_quote_norm) == 2, base_quote base_quote = base_quote_norm assert isinstance(base_quote, tuple), base_quote return base_quote beancount.core.realization \uf0c1 Realization of specific lists of account postings into reports. This code converts a list of entries into a tree of RealAccount nodes (which stands for \"realized accounts\"). The RealAccount objects contain lists of Posting instances instead of Transactions, or other entry types that are attached to an account, such as a Balance or Note entry. The interface of RealAccount corresponds to that of a regular Python dict, where the keys are the names of the individual components of an account's name, and the values are always other RealAccount instances. If you want to get an account by long account name, there are helper functions in this module for this purpose (see realization.get(), for instance). RealAccount instances also contain the final balance of that account, resulting from its list of postings. You should not build RealAccount trees yourself; instead, you should filter the list of desired directives to display and call the realize() function with them. beancount.core.realization.RealAccount ( dict ) \uf0c1 A realized account, inserted in a tree, that contains the list of realized entries. Attributes: Name Type Description account A string, the full name of the corresponding account. postings A list of postings associated with this accounting (does not include the postings of children accounts). balance The final balance of the list of postings associated with this account. beancount.core.realization.RealAccount.__eq__(self, other) special \uf0c1 Equality predicate. All attributes are compared. Parameters: other \u2013 Another instance of RealAccount. Returns: A boolean, True if the two real accounts are equal. Source code in beancount/core/realization.py def __eq__(self, other): \"\"\"Equality predicate. All attributes are compared. Args: other: Another instance of RealAccount. Returns: A boolean, True if the two real accounts are equal. \"\"\" return (dict.__eq__(self, other) and self.account == other.account and self.balance == other.balance and self.txn_postings == other.txn_postings) beancount.core.realization.RealAccount.__init__(self, account_name, *args, **kwargs) special \uf0c1 Create a RealAccount instance. Parameters: account_name \u2013 a string, the name of the account. Maybe not be None. Source code in beancount/core/realization.py def __init__(self, account_name, *args, **kwargs): \"\"\"Create a RealAccount instance. Args: account_name: a string, the name of the account. Maybe not be None. \"\"\" super().__init__(*args, **kwargs) assert isinstance(account_name, str) self.account = account_name self.txn_postings = [] self.balance = inventory.Inventory() beancount.core.realization.RealAccount.__ne__(self, other) special \uf0c1 Not-equality predicate. See eq . Parameters: other \u2013 Another instance of RealAccount. Returns: A boolean, True if the two real accounts are not equal. Source code in beancount/core/realization.py def __ne__(self, other): \"\"\"Not-equality predicate. See __eq__. Args: other: Another instance of RealAccount. Returns: A boolean, True if the two real accounts are not equal. \"\"\" return not self.__eq__(other) beancount.core.realization.RealAccount.__setitem__(self, key, value) special \uf0c1 Prevent the setting of non-string or non-empty keys on this dict. Parameters: key \u2013 The dictionary key. Must be a string. value \u2013 The value, must be a RealAccount instance. Exceptions: KeyError \u2013 If the key is not a string, or is invalid. ValueError \u2013 If the value is not a RealAccount instance. Source code in beancount/core/realization.py def __setitem__(self, key, value): \"\"\"Prevent the setting of non-string or non-empty keys on this dict. Args: key: The dictionary key. Must be a string. value: The value, must be a RealAccount instance. Raises: KeyError: If the key is not a string, or is invalid. ValueError: If the value is not a RealAccount instance. \"\"\" if not isinstance(key, str) or not key: raise KeyError(\"Invalid RealAccount key: '{}'\".format(key)) if not isinstance(value, RealAccount): raise ValueError(\"Invalid RealAccount value: '{}'\".format(value)) if not value.account.endswith(key): raise ValueError(\"RealAccount name '{}' inconsistent with key: '{}'\".format( value.account, key)) return super().__setitem__(key, value) beancount.core.realization.RealAccount.copy(self) \uf0c1 Override dict.copy() to clone a RealAccount. This is only necessary to correctly implement the copy method. Otherwise, calling .copy() on a RealAccount instance invokes the base class' method, which return just a dict. Returns: A cloned instance of RealAccount, with all members shallow-copied. Source code in beancount/core/realization.py def copy(self): \"\"\"Override dict.copy() to clone a RealAccount. This is only necessary to correctly implement the copy method. Otherwise, calling .copy() on a RealAccount instance invokes the base class' method, which return just a dict. Returns: A cloned instance of RealAccount, with all members shallow-copied. \"\"\" return copy.copy(self) beancount.core.realization.compute_balance(real_account, leaf_only=False) \uf0c1 Compute the total balance of this account and all its subaccounts. Parameters: real_account \u2013 A RealAccount instance. leaf_only \u2013 A boolean flag, true if we should yield only leaves. Returns: An Inventory. Source code in beancount/core/realization.py def compute_balance(real_account, leaf_only=False): \"\"\"Compute the total balance of this account and all its subaccounts. Args: real_account: A RealAccount instance. leaf_only: A boolean flag, true if we should yield only leaves. Returns: An Inventory. \"\"\" return functools.reduce(operator.add, [ ra.balance for ra in iter_children(real_account, leaf_only)]) beancount.core.realization.compute_postings_balance(txn_postings) \uf0c1 Compute the balance of a list of Postings's or TxnPosting's positions. Parameters: postings \u2013 A list of Posting instances and other directives (which are skipped). Returns: An Inventory. Source code in beancount/core/realization.py def compute_postings_balance(txn_postings): \"\"\"Compute the balance of a list of Postings's or TxnPosting's positions. Args: postings: A list of Posting instances and other directives (which are skipped). Returns: An Inventory. \"\"\" final_balance = inventory.Inventory() for txn_posting in txn_postings: if isinstance(txn_posting, Posting): final_balance.add_position(txn_posting) elif isinstance(txn_posting, TxnPosting): final_balance.add_position(txn_posting.posting) return final_balance beancount.core.realization.contains(real_account, account_name) \uf0c1 True if the given account node contains the subaccount name. Parameters: account_name \u2013 A string, the name of a direct or indirect subaccount of this node. Returns: A boolean, true the name is a child of this node. Source code in beancount/core/realization.py def contains(real_account, account_name): \"\"\"True if the given account node contains the subaccount name. Args: account_name: A string, the name of a direct or indirect subaccount of this node. Returns: A boolean, true the name is a child of this node. \"\"\" return get(real_account, account_name) is not None beancount.core.realization.dump(root_account) \uf0c1 Convert a RealAccount node to a line of lines. Note: this is not meant to be used to produce text reports; the reporting code should produce an intermediate object that contains the structure of it, which can then be rendered to ASCII, HTML or CSV formats. This is intended as a convenient little function for dumping trees of data for debugging purposes. Parameters: root_account \u2013 A RealAccount instance. Returns: A list of tuples of (first_line, continuation_line, real_account) where first_line \u2013 A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. real_account: The RealAccount instance which corresponds to this line. Source code in beancount/core/realization.py def dump(root_account): \"\"\"Convert a RealAccount node to a line of lines. Note: this is not meant to be used to produce text reports; the reporting code should produce an intermediate object that contains the structure of it, which can then be rendered to ASCII, HTML or CSV formats. This is intended as a convenient little function for dumping trees of data for debugging purposes. Args: root_account: A RealAccount instance. Returns: A list of tuples of (first_line, continuation_line, real_account) where first_line: A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. real_account: The RealAccount instance which corresponds to this line. \"\"\" # Compute all the lines ahead of time in order to calculate the width. lines = [] # Start with the root node. We push the constant prefix before this node, # the account name, and the RealAccount instance. We will maintain a stack # of children nodes to render. stack = [('', root_account.account, root_account, True)] while stack: prefix, name, real_account, is_last = stack.pop(-1) if real_account is root_account: # For the root node, we don't want to render any prefix. first = cont = '' else: # Compute the string that precedes the name directly and the one below # that for the continuation lines. # | # @@@ Bank1 <---------------- # @@@ | # | |-- Checking if is_last: first = prefix + PREFIX_LEAF_1 cont = prefix + PREFIX_LEAF_C else: first = prefix + PREFIX_CHILD_1 cont = prefix + PREFIX_CHILD_C # Compute the name to render for continuation lines. # | # |-- Bank1 # | @@@ <---------------- # | |-- Checking if len(real_account) > 0: cont_name = PREFIX_CHILD_C else: cont_name = PREFIX_LEAF_C # Add a line for this account. if not (real_account is root_account and not name): lines.append((first + name, cont + cont_name, real_account)) # Push the children onto the stack, being careful with ordering and # marking the last node as such. child_items = sorted(real_account.items(), reverse=True) if child_items: child_iter = iter(child_items) child_name, child_account = next(child_iter) stack.append((cont, child_name, child_account, True)) for child_name, child_account in child_iter: stack.append((cont, child_name, child_account, False)) if not lines: return lines # Compute the maximum width of the lines and convert all of them to the same # maximal width. This makes it easy on the client. max_width = max(len(first_line) for first_line, _, __ in lines) line_format = '{{:{width}}}'.format(width=max_width) return [(line_format.format(first_line), line_format.format(cont_line), real_node) for (first_line, cont_line, real_node) in lines] beancount.core.realization.dump_balances(real_root, dformat, at_cost=False, fullnames=False, file=None) \uf0c1 Dump a realization tree with balances. Parameters: real_root \u2013 An instance of RealAccount. dformat \u2013 An instance of DisplayFormatter to format the numbers with. at_cost \u2013 A boolean, if true, render the values at cost. fullnames \u2013 A boolean, if true, don't render a tree of accounts and render the full account names. file \u2013 A file object to dump the output to. If not specified, we return the output as a string. Returns: A string, the rendered tree, or nothing, if 'file' was provided. Source code in beancount/core/realization.py def dump_balances(real_root, dformat, at_cost=False, fullnames=False, file=None): \"\"\"Dump a realization tree with balances. Args: real_root: An instance of RealAccount. dformat: An instance of DisplayFormatter to format the numbers with. at_cost: A boolean, if true, render the values at cost. fullnames: A boolean, if true, don't render a tree of accounts and render the full account names. file: A file object to dump the output to. If not specified, we return the output as a string. Returns: A string, the rendered tree, or nothing, if 'file' was provided. \"\"\" if fullnames: # Compute the maximum account name length; maxlen = max(len(real_child.account) for real_child in iter_children(real_root, leaf_only=True)) line_format = '{{:{width}}} {{}}\\n'.format(width=maxlen) else: line_format = '{} {}\\n' output = file or io.StringIO() for first_line, cont_line, real_account in dump(real_root): if not real_account.balance.is_empty(): if at_cost: rinv = real_account.balance.reduce(convert.get_cost) else: rinv = real_account.balance.reduce(convert.get_units) amounts = [position.units for position in rinv.get_positions()] positions = [amount_.to_string(dformat) for amount_ in sorted(amounts, key=amount.sortkey)] else: positions = [''] if fullnames: for position in positions: if not position and len(real_account) > 0: continue # Skip parent accounts with no position to render. output.write(line_format.format(real_account.account, position)) else: line = first_line for position in positions: output.write(line_format.format(line, position)) line = cont_line if file is None: return output.getvalue() beancount.core.realization.filter(real_account, predicate) \uf0c1 Filter a RealAccount tree of nodes by the predicate. This function visits the tree and applies the predicate on each node. It returns a partial clone of RealAccount whereby on each node - either the predicate is true, or - for at least one child of the node the predicate is true. All the leaves have the predicate be true. Parameters: real_account \u2013 An instance of RealAccount. predicate \u2013 A callable/function which accepts a real_account and returns a boolean. If the function returns True, the node is kept. Returns: A shallow clone of RealAccount is always returned. Source code in beancount/core/realization.py def filter(real_account, predicate): \"\"\"Filter a RealAccount tree of nodes by the predicate. This function visits the tree and applies the predicate on each node. It returns a partial clone of RealAccount whereby on each node - either the predicate is true, or - for at least one child of the node the predicate is true. All the leaves have the predicate be true. Args: real_account: An instance of RealAccount. predicate: A callable/function which accepts a real_account and returns a boolean. If the function returns True, the node is kept. Returns: A shallow clone of RealAccount is always returned. \"\"\" assert isinstance(real_account, RealAccount) real_copy = RealAccount(real_account.account) real_copy.balance = real_account.balance real_copy.txn_postings = real_account.txn_postings for child_name, real_child in real_account.items(): real_child_copy = filter(real_child, predicate) if real_child_copy is not None: real_copy[child_name] = real_child_copy if len(real_copy) > 0 or predicate(real_account): return real_copy beancount.core.realization.find_last_active_posting(txn_postings) \uf0c1 Look at the end of the list of postings, and find the last posting or entry that is not an automatically added directive. Note that if the account is closed, the last posting is assumed to be a Close directive (this is the case if the input is valid and checks without errors. Parameters: txn_postings \u2013 a list of postings or entries. Returns: An entry, or None, if the input list was empty. Source code in beancount/core/realization.py def find_last_active_posting(txn_postings): \"\"\"Look at the end of the list of postings, and find the last posting or entry that is not an automatically added directive. Note that if the account is closed, the last posting is assumed to be a Close directive (this is the case if the input is valid and checks without errors. Args: txn_postings: a list of postings or entries. Returns: An entry, or None, if the input list was empty. \"\"\" for txn_posting in reversed(txn_postings): assert not isinstance(txn_posting, Posting) if not isinstance(txn_posting, (TxnPosting, Open, Close, Pad, Balance, Note)): continue # pylint: disable=bad-continuation if (isinstance(txn_posting, TxnPosting) and txn_posting.txn.flag == flags.FLAG_UNREALIZED): continue return txn_posting beancount.core.realization.get(real_account, account_name, default=None) \uf0c1 Fetch the subaccount name from the real_account node. Parameters: real_account \u2013 An instance of RealAccount, the parent node to look for children of. account_name \u2013 A string, the name of a possibly indirect child leaf found down the tree of 'real_account' nodes. default \u2013 The default value that should be returned if the child subaccount is not found. Returns: A RealAccount instance for the child, or the default value, if the child is not found. Source code in beancount/core/realization.py def get(real_account, account_name, default=None): \"\"\"Fetch the subaccount name from the real_account node. Args: real_account: An instance of RealAccount, the parent node to look for children of. account_name: A string, the name of a possibly indirect child leaf found down the tree of 'real_account' nodes. default: The default value that should be returned if the child subaccount is not found. Returns: A RealAccount instance for the child, or the default value, if the child is not found. \"\"\" if not isinstance(account_name, str): raise ValueError components = account.split(account_name) for component in components: real_child = real_account.get(component, default) if real_child is default: return default real_account = real_child return real_account beancount.core.realization.get_or_create(real_account, account_name) \uf0c1 Fetch the subaccount name from the real_account node. Parameters: real_account \u2013 An instance of RealAccount, the parent node to look for children of, or create under. account_name \u2013 A string, the name of the direct or indirect child leaf to get or create. Returns: A RealAccount instance for the child, or the default value, if the child is not found. Source code in beancount/core/realization.py def get_or_create(real_account, account_name): \"\"\"Fetch the subaccount name from the real_account node. Args: real_account: An instance of RealAccount, the parent node to look for children of, or create under. account_name: A string, the name of the direct or indirect child leaf to get or create. Returns: A RealAccount instance for the child, or the default value, if the child is not found. \"\"\" if not isinstance(account_name, str): raise ValueError components = account.split(account_name) path = [] for component in components: path.append(component) real_child = real_account.get(component, None) if real_child is None: real_child = RealAccount(account.join(*path)) real_account[component] = real_child real_account = real_child return real_account beancount.core.realization.get_postings(real_account) \uf0c1 Return a sorted list a RealAccount's postings and children. Parameters: real_account \u2013 An instance of RealAccount. Returns: A list of Posting or directories. Source code in beancount/core/realization.py def get_postings(real_account): \"\"\"Return a sorted list a RealAccount's postings and children. Args: real_account: An instance of RealAccount. Returns: A list of Posting or directories. \"\"\" # We accumulate all the postings at once here instead of incrementally # because we need to return them sorted. accumulator = [] for real_child in iter_children(real_account): accumulator.extend(real_child.txn_postings) accumulator.sort(key=data.posting_sortkey) return accumulator beancount.core.realization.index_key(sequence, value, key, cmp) \uf0c1 Find the index of the first element in 'sequence' which is equal to 'value'. If 'key' is specified, the value compared to the value returned by this function. If the value is not found, return None. Parameters: sequence \u2013 The sequence to search. value \u2013 The value to search for. key \u2013 A predicate to call to obtain the value to compare against. cmp \u2013 A comparison predicate. Returns: The index of the first element found, or None, if the element was not found. Source code in beancount/core/realization.py def index_key(sequence, value, key, cmp): \"\"\"Find the index of the first element in 'sequence' which is equal to 'value'. If 'key' is specified, the value compared to the value returned by this function. If the value is not found, return None. Args: sequence: The sequence to search. value: The value to search for. key: A predicate to call to obtain the value to compare against. cmp: A comparison predicate. Returns: The index of the first element found, or None, if the element was not found. \"\"\" for index, element in enumerate(sequence): if cmp(key(element), value): return index return beancount.core.realization.iter_children(real_account, leaf_only=False) \uf0c1 Iterate this account node and all its children, depth-first. Parameters: real_account \u2013 An instance of RealAccount. leaf_only \u2013 A boolean flag, true if we should yield only leaves. Yields: Instances of RealAccount, beginning with this account. The order is undetermined. Source code in beancount/core/realization.py def iter_children(real_account, leaf_only=False): \"\"\"Iterate this account node and all its children, depth-first. Args: real_account: An instance of RealAccount. leaf_only: A boolean flag, true if we should yield only leaves. Yields: Instances of RealAccount, beginning with this account. The order is undetermined. \"\"\" if leaf_only: if len(real_account) == 0: yield real_account else: for key, real_child in sorted(real_account.items()): for real_subchild in iter_children(real_child, leaf_only): yield real_subchild else: yield real_account for key, real_child in sorted(real_account.items()): for real_subchild in iter_children(real_child): yield real_subchild beancount.core.realization.iterate_with_balance(txn_postings) \uf0c1 Iterate over the entries, accumulating the running balance. For each entry, this yields tuples of the form: (entry, postings, change, balance) entry: This is the directive for this line. If the list contained Posting instance, this yields the corresponding Transaction object. postings: A list of postings on this entry that affect the balance. Only the postings encountered in the input list are included; only those affect the balance. If 'entry' is not a Transaction directive, this should always be an empty list. We preserve the original ordering of the postings as they appear in the input list. change: An Inventory object that reflects the total change due to the postings from this entry that appear in the list. For example, if a Transaction has three postings and two are in the input list, the sum of the two postings will be in the 'change' Inventory object. However, the position for the transactions' third posting--the one not included in the input list--will not be in this inventory. balance: An Inventory object that reflects the balance after adding the 'change' inventory due to this entry's transaction. The 'balance' yielded is never None, even for entries that do not affect the balance, that is, with an empty 'change' inventory. It's up to the caller, the one rendering the entry, to decide whether to render this entry's change for a particular entry type. Note that if the input list of postings-or-entries is not in sorted date order, two postings for the same entry appearing twice with a different date in between will cause the entry appear twice. This is correct behavior, and it is expected that in practice this should never happen anyway, because the list of postings or entries should always be sorted. This method attempts to detect this and raises an assertion if this is seen. Parameters: txn_postings \u2013 A list of postings or directive instances. Postings affect the balance; other entries do not. Yields: Tuples of (entry, postings, change, balance) as described above. Source code in beancount/core/realization.py def iterate_with_balance(txn_postings): \"\"\"Iterate over the entries, accumulating the running balance. For each entry, this yields tuples of the form: (entry, postings, change, balance) entry: This is the directive for this line. If the list contained Posting instance, this yields the corresponding Transaction object. postings: A list of postings on this entry that affect the balance. Only the postings encountered in the input list are included; only those affect the balance. If 'entry' is not a Transaction directive, this should always be an empty list. We preserve the original ordering of the postings as they appear in the input list. change: An Inventory object that reflects the total change due to the postings from this entry that appear in the list. For example, if a Transaction has three postings and two are in the input list, the sum of the two postings will be in the 'change' Inventory object. However, the position for the transactions' third posting--the one not included in the input list--will not be in this inventory. balance: An Inventory object that reflects the balance *after* adding the 'change' inventory due to this entry's transaction. The 'balance' yielded is never None, even for entries that do not affect the balance, that is, with an empty 'change' inventory. It's up to the caller, the one rendering the entry, to decide whether to render this entry's change for a particular entry type. Note that if the input list of postings-or-entries is not in sorted date order, two postings for the same entry appearing twice with a different date in between will cause the entry appear twice. This is correct behavior, and it is expected that in practice this should never happen anyway, because the list of postings or entries should always be sorted. This method attempts to detect this and raises an assertion if this is seen. Args: txn_postings: A list of postings or directive instances. Postings affect the balance; other entries do not. Yields: Tuples of (entry, postings, change, balance) as described above. \"\"\" # The running balance. running_balance = inventory.Inventory() # Previous date. prev_date = None # A list of entries at the current date. date_entries = [] first = lambda pair: pair[0] for txn_posting in txn_postings: # Get the posting if we are dealing with one. assert not isinstance(txn_posting, Posting) if isinstance(txn_posting, TxnPosting): posting = txn_posting.posting entry = txn_posting.txn else: posting = None entry = txn_posting if entry.date != prev_date: assert prev_date is None or entry.date > prev_date, ( \"Invalid date order for postings: {} > {}\".format(prev_date, entry.date)) prev_date = entry.date # Flush the dated entries. for date_entry, date_postings in date_entries: change = inventory.Inventory() if date_postings: # Compute the change due to this transaction and update the # total balance at the same time. for date_posting in date_postings: change.add_position(date_posting) running_balance.add_position(date_posting) yield date_entry, date_postings, change, running_balance date_entries.clear() assert not date_entries if posting is not None: # De-dup multiple postings on the same transaction entry by # grouping their positions together. index = index_key(date_entries, entry, first, operator.is_) if index is None: date_entries.append((entry, [posting])) else: # We are indeed de-duping! postings = date_entries[index][1] postings.append(posting) else: # This is a regular entry; nothing to add/remove. date_entries.append((entry, [])) # Flush the final dated entries if any, same as above. for date_entry, date_postings in date_entries: change = inventory.Inventory() if date_postings: for date_posting in date_postings: change.add_position(date_posting) running_balance.add_position(date_posting) yield date_entry, date_postings, change, running_balance date_entries.clear() beancount.core.realization.postings_by_account(entries) \uf0c1 Create lists of postings and balances by account. This routine aggregates postings and entries grouping them by account name. The resulting lists contain postings in-lieu of Transaction directives, but the other directives are stored as entries. This yields a list of postings or other entries by account. All references to accounts are taken into account. Parameters: entries \u2013 A list of directives. Returns: A mapping of account name to list of TxnPosting instances or non-Transaction directives, sorted in the same order as the entries. Source code in beancount/core/realization.py def postings_by_account(entries): \"\"\"Create lists of postings and balances by account. This routine aggregates postings and entries grouping them by account name. The resulting lists contain postings in-lieu of Transaction directives, but the other directives are stored as entries. This yields a list of postings or other entries by account. All references to accounts are taken into account. Args: entries: A list of directives. Returns: A mapping of account name to list of TxnPosting instances or non-Transaction directives, sorted in the same order as the entries. \"\"\" txn_postings_map = collections.defaultdict(list) for entry in entries: if isinstance(entry, Transaction): # Insert an entry for each of the postings. for posting in entry.postings: txn_postings_map[posting.account].append( TxnPosting(entry, posting)) elif isinstance(entry, (Open, Close, Balance, Note, Document)): # Append some other entries in the realized list. txn_postings_map[entry.account].append(entry) elif isinstance(entry, Pad): # Insert the pad entry in both realized accounts. txn_postings_map[entry.account].append(entry) txn_postings_map[entry.source_account].append(entry) elif isinstance(entry, Custom): # Insert custom entry for each account in its values. for custom_value in entry.values: if custom_value.dtype == account.TYPE: txn_postings_map[custom_value.value].append(entry) return txn_postings_map beancount.core.realization.realize(entries, min_accounts=None, compute_balance=True) \uf0c1 Group entries by account, into a \"tree\" of realized accounts. RealAccount's are essentially containers for lists of postings and the final balance of each account, and may be non-leaf accounts (used strictly for organizing accounts into a hierarchy). This is then used to issue reports. The lists of postings in each account my be any of the entry types, except for Transaction, whereby Transaction entries are replaced by the specific Posting legs that belong to the account. Here's a simple diagram that summarizes this seemingly complex, but rather simple data structure: +-------------+ postings +------+ | RealAccount |---------->| Open | +-------------+ +------+ | v +------------+ +-------------+ | TxnPosting |---->| Transaction | +------------+ +-------------+ | \\ \\\\ v \\.__ +---------+ +-----+ -------->| Posting | | Pad | +---------+ +-----+ | v +---------+ | Balance | +---------+ | v +-------+ | Close | +-------+ | . Parameters: entries \u2013 A list of directives. min_accounts \u2013 A list of strings, account names to ensure we create, regardless of whether there are postings on those accounts or not. This can be used to ensure the root accounts all exist. compute_balance \u2013 A boolean, true if we should compute the final balance on the realization. Returns: The root RealAccount instance. Source code in beancount/core/realization.py def realize(entries, min_accounts=None, compute_balance=True): r\"\"\"Group entries by account, into a \"tree\" of realized accounts. RealAccount's are essentially containers for lists of postings and the final balance of each account, and may be non-leaf accounts (used strictly for organizing accounts into a hierarchy). This is then used to issue reports. The lists of postings in each account my be any of the entry types, except for Transaction, whereby Transaction entries are replaced by the specific Posting legs that belong to the account. Here's a simple diagram that summarizes this seemingly complex, but rather simple data structure: +-------------+ postings +------+ | RealAccount |---------->| Open | +-------------+ +------+ | v +------------+ +-------------+ | TxnPosting |---->| Transaction | +------------+ +-------------+ | \\ \\\\\\ v `\\.__ +---------+ +-----+ `-------->| Posting | | Pad | +---------+ +-----+ | v +---------+ | Balance | +---------+ | v +-------+ | Close | +-------+ | . Args: entries: A list of directives. min_accounts: A list of strings, account names to ensure we create, regardless of whether there are postings on those accounts or not. This can be used to ensure the root accounts all exist. compute_balance: A boolean, true if we should compute the final balance on the realization. Returns: The root RealAccount instance. \"\"\" # Create lists of the entries by account. txn_postings_map = postings_by_account(entries) # Create a RealAccount tree and compute the balance for each. real_root = RealAccount('') for account_name, txn_postings in txn_postings_map.items(): real_account = get_or_create(real_root, account_name) real_account.txn_postings = txn_postings if compute_balance: real_account.balance = compute_postings_balance(txn_postings) # Ensure a minimum set of accounts that should exist. This is typically # called with an instance of AccountTypes to make sure that those exist. if min_accounts: for account_name in min_accounts: get_or_create(real_root, account_name) return real_root","title":"beancount.core"},{"location":"api_reference/beancount.core.html#beancountcore","text":"Core basic objects and data structures to represent a list of entries.","title":"beancount.core"},{"location":"api_reference/beancount.core.html#beancount.core.account","text":"Functions that operate on account strings. These account objects are rather simple and dumb; they do not contain the list of their associated postings. This is achieved by building a realization; see realization.py for details.","title":"account"},{"location":"api_reference/beancount.core.html#beancount.core.account.AccountTransformer","text":"Account name transformer. This is used to support Win... huh, filesystems and platforms which do not support colon characters. Attributes: Name Type Description rsep A character string, the new separator to use in link names.","title":"AccountTransformer"},{"location":"api_reference/beancount.core.html#beancount.core.account.AccountTransformer.parse","text":"Convert the transform account name to an account name. Source code in beancount/core/account.py def parse(self, transformed_name): \"Convert the transform account name to an account name.\" return (transformed_name if self.rsep is None else transformed_name.replace(self.rsep, sep))","title":"parse()"},{"location":"api_reference/beancount.core.html#beancount.core.account.AccountTransformer.render","text":"Convert the account name to a transformed account name. Source code in beancount/core/account.py def render(self, account_name): \"Convert the account name to a transformed account name.\" return (account_name if self.rsep is None else account_name.replace(sep, self.rsep))","title":"render()"},{"location":"api_reference/beancount.core.html#beancount.core.account.commonprefix","text":"Return the common prefix of a list of account names. Parameters: accounts \u2013 A sequence of account name strings. Returns: A string, the common parent account. If none, returns an empty string. Source code in beancount/core/account.py def commonprefix(accounts): \"\"\"Return the common prefix of a list of account names. Args: accounts: A sequence of account name strings. Returns: A string, the common parent account. If none, returns an empty string. \"\"\" accounts_lists = [account_.split(sep) for account_ in accounts] # Note: the os.path.commonprefix() function just happens to work here. # Inspect its code, and even the special case of no common prefix # works well with str.join() below. common_list = path.commonprefix(accounts_lists) return sep.join(common_list)","title":"commonprefix()"},{"location":"api_reference/beancount.core.html#beancount.core.account.has_component","text":"Return true if one of the account contains a given component. Parameters: account_name \u2013 A string, an account name. component \u2013 A string, a component of an account name. For instance, Food in Expenses:Food:Restaurant . All components are considered. Returns: Boolean \u2013 true if the component is in the account. Note that a component name must be whole, that is NY is not in Expenses:Taxes:StateNY . Source code in beancount/core/account.py def has_component(account_name, component): \"\"\"Return true if one of the account contains a given component. Args: account_name: A string, an account name. component: A string, a component of an account name. For instance, ``Food`` in ``Expenses:Food:Restaurant``. All components are considered. Returns: Boolean: true if the component is in the account. Note that a component name must be whole, that is ``NY`` is not in ``Expenses:Taxes:StateNY``. \"\"\" return bool(re.search('(^|:){}(:|$)'.format(component), account_name))","title":"has_component()"},{"location":"api_reference/beancount.core.html#beancount.core.account.is_valid","text":"Return true if the given string is a valid account name. This does not check for the root account types, just the general syntax. Parameters: string \u2013 A string, to be checked for account name pattern. Returns: A boolean, true if the string has the form of an account's name. Source code in beancount/core/account.py def is_valid(string): \"\"\"Return true if the given string is a valid account name. This does not check for the root account types, just the general syntax. Args: string: A string, to be checked for account name pattern. Returns: A boolean, true if the string has the form of an account's name. \"\"\" return (isinstance(string, str) and bool(re.match('{}$'.format(ACCOUNT_RE), string)))","title":"is_valid()"},{"location":"api_reference/beancount.core.html#beancount.core.account.join","text":"Join the names with the account separator. Parameters: *components \u2013 Strings, the components of an account name. Returns: A string, joined in a single account name. Source code in beancount/core/account.py def join(*components): \"\"\"Join the names with the account separator. Args: *components: Strings, the components of an account name. Returns: A string, joined in a single account name. \"\"\" return sep.join(components)","title":"join()"},{"location":"api_reference/beancount.core.html#beancount.core.account.leaf","text":"Get the name of the leaf of this account. Parameters: account_name \u2013 A string, the name of the account whose leaf name to return. Returns: A string, the name of the leaf of the account. Source code in beancount/core/account.py def leaf(account_name): \"\"\"Get the name of the leaf of this account. Args: account_name: A string, the name of the account whose leaf name to return. Returns: A string, the name of the leaf of the account. \"\"\" assert isinstance(account_name, str) return account_name.split(sep)[-1] if account_name else None","title":"leaf()"},{"location":"api_reference/beancount.core.html#beancount.core.account.parent","text":"Return the name of the parent account of the given account. Parameters: account_name \u2013 A string, the name of the account whose parent to return. Returns: A string, the name of the parent account of this account. Source code in beancount/core/account.py def parent(account_name): \"\"\"Return the name of the parent account of the given account. Args: account_name: A string, the name of the account whose parent to return. Returns: A string, the name of the parent account of this account. \"\"\" assert isinstance(account_name, str), account_name if not account_name: return None components = account_name.split(sep) components.pop(-1) return sep.join(components)","title":"parent()"},{"location":"api_reference/beancount.core.html#beancount.core.account.parent_matcher","text":"Build a predicate that returns whether an account is under the given one. Parameters: account_name \u2013 The name of the parent account we want to check for. Returns: A callable, which, when called, will return true if the given account is a child of account_name . Source code in beancount/core/account.py def parent_matcher(account_name): \"\"\"Build a predicate that returns whether an account is under the given one. Args: account_name: The name of the parent account we want to check for. Returns: A callable, which, when called, will return true if the given account is a child of ``account_name``. \"\"\" return re.compile(r'{}($|{})'.format(re.escape(account_name), sep)).match","title":"parent_matcher()"},{"location":"api_reference/beancount.core.html#beancount.core.account.parents","text":"A generator of the names of the parents of this account, including this account. Parameters: account_name \u2013 The name of the account we want to start iterating from. Returns: A generator of account name strings. Source code in beancount/core/account.py def parents(account_name): \"\"\"A generator of the names of the parents of this account, including this account. Args: account_name: The name of the account we want to start iterating from. Returns: A generator of account name strings. \"\"\" while account_name: yield account_name account_name = parent(account_name)","title":"parents()"},{"location":"api_reference/beancount.core.html#beancount.core.account.root","text":"Return the first few components of an account's name. Parameters: num_components \u2013 An integer, the number of components to return. account_name \u2013 A string, an account name. Returns: A string, the account root up to 'num_components' components. Source code in beancount/core/account.py def root(num_components, account_name): \"\"\"Return the first few components of an account's name. Args: num_components: An integer, the number of components to return. account_name: A string, an account name. Returns: A string, the account root up to 'num_components' components. \"\"\" return join(*(split(account_name)[:num_components]))","title":"root()"},{"location":"api_reference/beancount.core.html#beancount.core.account.sans_root","text":"Get the name of the account without the root. For example, an input of 'Assets:BofA:Checking' will produce 'BofA:Checking'. Parameters: account_name \u2013 A string, the name of the account whose leaf name to return. Returns: A string, the name of the non-root portion of this account name. Source code in beancount/core/account.py def sans_root(account_name): \"\"\"Get the name of the account without the root. For example, an input of 'Assets:BofA:Checking' will produce 'BofA:Checking'. Args: account_name: A string, the name of the account whose leaf name to return. Returns: A string, the name of the non-root portion of this account name. \"\"\" assert isinstance(account_name, str) components = account_name.split(sep)[1:] return join(*components) if account_name else None","title":"sans_root()"},{"location":"api_reference/beancount.core.html#beancount.core.account.split","text":"Split an account's name into its components. Parameters: account_name \u2013 A string, an account name. Returns: A list of strings, the components of the account name (without the separators). Source code in beancount/core/account.py def split(account_name): \"\"\"Split an account's name into its components. Args: account_name: A string, an account name. Returns: A list of strings, the components of the account name (without the separators). \"\"\" return account_name.split(sep)","title":"split()"},{"location":"api_reference/beancount.core.html#beancount.core.account.walk","text":"A version of os.walk() which yields directories that are valid account names. This only yields directories that are accounts... it skips the other ones. For convenience, it also yields you the account's name. Parameters: root_directory \u2013 A string, the name of the root of the hierarchy to be walked. Yields: Tuples of (root, account-name, dirs, files), similar to os.walk(). Source code in beancount/core/account.py def walk(root_directory): \"\"\"A version of os.walk() which yields directories that are valid account names. This only yields directories that are accounts... it skips the other ones. For convenience, it also yields you the account's name. Args: root_directory: A string, the name of the root of the hierarchy to be walked. Yields: Tuples of (root, account-name, dirs, files), similar to os.walk(). \"\"\" for root, dirs, files in os.walk(root_directory): dirs.sort() files.sort() relroot = root[len(root_directory)+1:] account_name = relroot.replace(os.sep, sep) if is_valid(account_name): yield (root, account_name, dirs, files)","title":"walk()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types","text":"Definition for global account types. This is where we keep the global account types value and definition. Note that it's unfortunate that we're using globals and side-effect here, but this is the best solution in the short-term, the account types are used in too many places to pass around that state everywhere. Maybe we change this later on.","title":"account_types"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.AccountTypes","text":"AccountTypes(assets, liabilities, equity, income, expenses)","title":"AccountTypes"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.AccountTypes.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/account_types.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.AccountTypes.__new__","text":"Create new instance of AccountTypes(assets, liabilities, equity, income, expenses)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.AccountTypes.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/account_types.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.get_account_sign","text":"Return the sign of the normal balance of a particular account. Parameters: account_name \u2013 A string, the name of the account whose sign is to return. account_types \u2013 An optional instance of the current account_types. Returns: +1 or -1, depending on the account's type. Source code in beancount/core/account_types.py def get_account_sign(account_name, account_types=None): \"\"\"Return the sign of the normal balance of a particular account. Args: account_name: A string, the name of the account whose sign is to return. account_types: An optional instance of the current account_types. Returns: +1 or -1, depending on the account's type. \"\"\" if account_types is None: account_types = DEFAULT_ACCOUNT_TYPES assert isinstance(account_name, str), \"Account is not a string: {}\".format(account_name) account_type = get_account_type(account_name) return (+1 if account_type in (account_types.assets, account_types.expenses) else -1)","title":"get_account_sign()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.get_account_sort_key","text":"Return a tuple that can be used to order/sort account names. Parameters: account_types \u2013 An instance of AccountTypes, a tuple of account type names. Returns: A function object to use as the optional 'key' argument to the sort function. It accepts a single argument, the account name to sort and produces a sortable key. Source code in beancount/core/account_types.py def get_account_sort_key(account_types, account_name): \"\"\"Return a tuple that can be used to order/sort account names. Args: account_types: An instance of AccountTypes, a tuple of account type names. Returns: A function object to use as the optional 'key' argument to the sort function. It accepts a single argument, the account name to sort and produces a sortable key. \"\"\" return (account_types.index(get_account_type(account_name)), account_name)","title":"get_account_sort_key()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.get_account_type","text":"Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Parameters: account_name \u2013 A string, the name of the account whose type is to return. Returns: A string, the type of the account in 'account_name'. Source code in beancount/core/account_types.py def get_account_type(account_name): \"\"\"Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Args: account_name: A string, the name of the account whose type is to return. Returns: A string, the type of the account in 'account_name'. \"\"\" assert isinstance(account_name, str), \"Account is not a string: {}\".format(account_name) return account.split(account_name)[0]","title":"get_account_type()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.is_account_type","text":"Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Parameters: account_type \u2013 A string, the prefix type of the account. account_name \u2013 A string, the name of the account whose type is to return. Returns: A boolean, true if the account is of the given type. Source code in beancount/core/account_types.py def is_account_type(account_type, account_name): \"\"\"Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Args: account_type: A string, the prefix type of the account. account_name: A string, the name of the account whose type is to return. Returns: A boolean, true if the account is of the given type. \"\"\" return bool(re.match('^{}{}'.format(account_type, account.sep), account_name))","title":"is_account_type()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.is_balance_sheet_account","text":"Return true if the given account is a balance sheet account. Assets, liabilities and equity accounts are balance sheet accounts. Parameters: account_name \u2013 A string, an account name. account_types \u2013 An instance of AccountTypes. Returns: A boolean, true if the account is a balance sheet account. Source code in beancount/core/account_types.py def is_balance_sheet_account(account_name, account_types): \"\"\"Return true if the given account is a balance sheet account. Assets, liabilities and equity accounts are balance sheet accounts. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account is a balance sheet account. \"\"\" assert isinstance(account_name, str), \"Account is not a string: {}\".format(account_name) assert isinstance(account_types, AccountTypes), ( \"Account types has invalid type: {}\".format(account_types)) account_type = get_account_type(account_name) return account_type in (account_types.assets, account_types.liabilities, account_types.equity)","title":"is_balance_sheet_account()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.is_equity_account","text":"Return true if the given account is an equity account. Parameters: account_name \u2013 A string, an account name. account_types \u2013 An instance of AccountTypes. Returns: A boolean, true if the account is an equity account. Source code in beancount/core/account_types.py def is_equity_account(account_name, account_types): \"\"\"Return true if the given account is an equity account. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account is an equity account. \"\"\" assert isinstance(account_name, str), \"Account is not a string: {}\".format(account_name) assert isinstance(account_types, AccountTypes), ( \"Account types has invalid type: {}\".format(account_types)) account_type = get_account_type(account_name) return account_type == account_types.equity","title":"is_equity_account()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.is_income_statement_account","text":"Return true if the given account is an income statement account. Income and expense accounts are income statement accounts. Parameters: account_name \u2013 A string, an account name. account_types \u2013 An instance of AccountTypes. Returns: A boolean, true if the account is an income statement account. Source code in beancount/core/account_types.py def is_income_statement_account(account_name, account_types): \"\"\"Return true if the given account is an income statement account. Income and expense accounts are income statement accounts. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account is an income statement account. \"\"\" assert isinstance(account_name, str), \"Account is not a string: {}\".format(account_name) assert isinstance(account_types, AccountTypes), ( \"Account types has invalid type: {}\".format(account_types)) account_type = get_account_type(account_name) return account_type in (account_types.income, account_types.expenses)","title":"is_income_statement_account()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.is_root_account","text":"Return true if the account name is a root account. This function does not verify whether the account root is a valid one, just that it is a root account or not. Parameters: account_name \u2013 A string, the name of the account to check for. account_types \u2013 An optional instance of the current account_types; if provided, we check against these values. If not provided, we merely check that name pattern is that of an account component with no separator. Returns: A boolean, true if the account is root account. Source code in beancount/core/account_types.py def is_root_account(account_name, account_types=None): \"\"\"Return true if the account name is a root account. This function does not verify whether the account root is a valid one, just that it is a root account or not. Args: account_name: A string, the name of the account to check for. account_types: An optional instance of the current account_types; if provided, we check against these values. If not provided, we merely check that name pattern is that of an account component with no separator. Returns: A boolean, true if the account is root account. \"\"\" assert isinstance(account_name, str), \"Account is not a string: {}\".format(account_name) if account_types is not None: assert isinstance(account_types, AccountTypes), ( \"Account types has invalid type: {}\".format(account_types)) return account_name in account_types else: return (account_name and bool(re.match(r'([A-Z][A-Za-z0-9\\-]+)$', account_name)))","title":"is_root_account()"},{"location":"api_reference/beancount.core.html#beancount.core.amount","text":"Amount class. This simple class is used to associate a number of units of a currency with its currency: (number, currency).","title":"amount"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount","text":"An 'Amount' represents a number of a particular unit of something. It's essentially a typed number, with corresponding manipulation operations defined on it.","title":"Amount"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__bool__","text":"Boolean predicate returns true if the number is non-zero. Returns: A boolean, true if non-zero number. Source code in beancount/core/amount.py def __bool__(self): \"\"\"Boolean predicate returns true if the number is non-zero. Returns: A boolean, true if non-zero number. \"\"\" return self.number != ZERO","title":"__bool__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__eq__","text":"Equality predicate. Returns true if both number and currency are equal. Returns: A boolean. Source code in beancount/core/amount.py def __eq__(self, other): \"\"\"Equality predicate. Returns true if both number and currency are equal. Returns: A boolean. \"\"\" if other is None: return False return (self.number, self.currency) == (other.number, other.currency)","title":"__eq__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__hash__","text":"A hashing function for amounts. The hash includes the currency. Returns: An integer, the hash for this amount. Source code in beancount/core/amount.py def __hash__(self): \"\"\"A hashing function for amounts. The hash includes the currency. Returns: An integer, the hash for this amount. \"\"\" return hash((self.number, self.currency))","title":"__hash__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__lt__","text":"Ordering comparison. This is used in the sorting key of positions. Parameters: other \u2013 An instance of Amount. Returns: True if this is less than the other Amount. Source code in beancount/core/amount.py def __lt__(self, other): \"\"\"Ordering comparison. This is used in the sorting key of positions. Args: other: An instance of Amount. Returns: True if this is less than the other Amount. \"\"\" return sortkey(self) < sortkey(other)","title":"__lt__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__neg__","text":"Return the negative of this amount. Returns: A new instance of Amount, with the negative number of units. Source code in beancount/core/amount.py def __neg__(self): \"\"\"Return the negative of this amount. Returns: A new instance of Amount, with the negative number of units. \"\"\" return Amount(-self.number, self.currency)","title":"__neg__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__new__","text":"Constructor from a number and currency. Parameters: number \u2013 A string or Decimal instance. Will get converted automatically. currency \u2013 A string, the currency symbol to use. Source code in beancount/core/amount.py def __new__(cls, number, currency): \"\"\"Constructor from a number and currency. Args: number: A string or Decimal instance. Will get converted automatically. currency: A string, the currency symbol to use. \"\"\" assert isinstance(number, Amount.valid_types_number), repr(number) assert isinstance(currency, Amount.valid_types_currency), repr(currency) return _Amount.__new__(cls, number, currency)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__repr__","text":"Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/amount.py def __str__(self): \"\"\"Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. \"\"\" return self.to_string()","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__str__","text":"Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/amount.py def __str__(self): \"\"\"Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. \"\"\" return self.to_string()","title":"__str__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.from_string","text":"Create an amount from a string. This is a miniature parser used for building tests. Parameters: string \u2013 A string of . Returns: A new instance of Amount. Source code in beancount/core/amount.py @staticmethod def from_string(string): \"\"\"Create an amount from a string. This is a miniature parser used for building tests. Args: string: A string of . Returns: A new instance of Amount. \"\"\" match = re.match(r'\\s*([-+]?[0-9.]+)\\s+({currency})'.format(currency=CURRENCY_RE), string) if not match: raise ValueError(\"Invalid string for amount: '{}'\".format(string)) number, currency = match.group(1, 2) return Amount(D(number), currency)","title":"from_string()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.to_string","text":"Convert an Amount instance to a printable string. Parameters: dformat \u2013 An instance of DisplayFormatter. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/amount.py def to_string(self, dformat=DEFAULT_FORMATTER): \"\"\"Convert an Amount instance to a printable string. Args: dformat: An instance of DisplayFormatter. Returns: A formatted string of the quantized amount and symbol. \"\"\" number_fmt = (dformat.format(self.number, self.currency) if isinstance(self.number, Decimal) else str(self.number)) return \"{} {}\".format(number_fmt, self.currency)","title":"to_string()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.A","text":"Create an amount from a string. This is a miniature parser used for building tests. Parameters: string \u2013 A string of . Returns: A new instance of Amount. Source code in beancount/core/amount.py @staticmethod def from_string(string): \"\"\"Create an amount from a string. This is a miniature parser used for building tests. Args: string: A string of . Returns: A new instance of Amount. \"\"\" match = re.match(r'\\s*([-+]?[0-9.]+)\\s+({currency})'.format(currency=CURRENCY_RE), string) if not match: raise ValueError(\"Invalid string for amount: '{}'\".format(string)) number, currency = match.group(1, 2) return Amount(D(number), currency)","title":"A()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.abs","text":"Return the absolute value of the given amount. Parameters: amount \u2013 An instance of Amount. Returns: An instance of Amount. Source code in beancount/core/amount.py def abs(amount): \"\"\"Return the absolute value of the given amount. Args: amount: An instance of Amount. Returns: An instance of Amount. \"\"\" return (amount if amount.number >= ZERO else Amount(-amount.number, amount.currency))","title":"abs()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.add","text":"Add the given amounts with the same currency. Parameters: amount1 \u2013 An instance of Amount. amount2 \u2013 An instance of Amount. Returns: An instance of Amount, with the sum the two amount's numbers, in the same currency. Source code in beancount/core/amount.py def add(amount1, amount2): \"\"\"Add the given amounts with the same currency. Args: amount1: An instance of Amount. amount2: An instance of Amount. Returns: An instance of Amount, with the sum the two amount's numbers, in the same currency. \"\"\" assert isinstance(amount1.number, Decimal), ( \"Amount1's number is not a Decimal instance: {}\".format(amount1.number)) assert isinstance(amount2.number, Decimal), ( \"Amount2's number is not a Decimal instance: {}\".format(amount2.number)) if amount1.currency != amount2.currency: raise ValueError( \"Unmatching currencies for operation on {} and {}\".format( amount1, amount2)) return Amount(amount1.number + amount2.number, amount1.currency)","title":"add()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.div","text":"Divide the given amount by a number. Parameters: amount \u2013 An instance of Amount. number \u2013 A decimal number. Returns: An Amount, with the same currency, but with amount units divided by 'number'. Source code in beancount/core/amount.py def div(amount, number): \"\"\"Divide the given amount by a number. Args: amount: An instance of Amount. number: A decimal number. Returns: An Amount, with the same currency, but with amount units divided by 'number'. \"\"\" assert isinstance(amount.number, Decimal), ( \"Amount's number is not a Decimal instance: {}\".format(amount.number)) assert isinstance(number, Decimal), ( \"Number is not a Decimal instance: {}\".format(number)) return Amount(amount.number / number, amount.currency)","title":"div()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.from_string","text":"Create an amount from a string. This is a miniature parser used for building tests. Parameters: string \u2013 A string of . Returns: A new instance of Amount. Source code in beancount/core/amount.py @staticmethod def from_string(string): \"\"\"Create an amount from a string. This is a miniature parser used for building tests. Args: string: A string of . Returns: A new instance of Amount. \"\"\" match = re.match(r'\\s*([-+]?[0-9.]+)\\s+({currency})'.format(currency=CURRENCY_RE), string) if not match: raise ValueError(\"Invalid string for amount: '{}'\".format(string)) number, currency = match.group(1, 2) return Amount(D(number), currency)","title":"from_string()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.mul","text":"Multiply the given amount by a number. Parameters: amount \u2013 An instance of Amount. number \u2013 A decimal number. Returns: An Amount, with the same currency, but with 'number' times units. Source code in beancount/core/amount.py def mul(amount, number): \"\"\"Multiply the given amount by a number. Args: amount: An instance of Amount. number: A decimal number. Returns: An Amount, with the same currency, but with 'number' times units. \"\"\" assert isinstance(amount.number, Decimal), ( \"Amount's number is not a Decimal instance: {}\".format(amount.number)) assert isinstance(number, Decimal), ( \"Number is not a Decimal instance: {}\".format(number)) return Amount(amount.number * number, amount.currency)","title":"mul()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.sortkey","text":"A comparison function that sorts by currency first. Parameters: amount \u2013 An instance of Amount. Returns: A sort key, composed of the currency first and then the number. Source code in beancount/core/amount.py def sortkey(amount): \"\"\"A comparison function that sorts by currency first. Args: amount: An instance of Amount. Returns: A sort key, composed of the currency first and then the number. \"\"\" return (amount.currency, amount.number)","title":"sortkey()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.sub","text":"Subtract the given amounts with the same currency. Parameters: amount1 \u2013 An instance of Amount. amount2 \u2013 An instance of Amount. Returns: An instance of Amount, with the difference between the two amount's numbers, in the same currency. Source code in beancount/core/amount.py def sub(amount1, amount2): \"\"\"Subtract the given amounts with the same currency. Args: amount1: An instance of Amount. amount2: An instance of Amount. Returns: An instance of Amount, with the difference between the two amount's numbers, in the same currency. \"\"\" assert isinstance(amount1.number, Decimal), ( \"Amount1's number is not a Decimal instance: {}\".format(amount1.number)) assert isinstance(amount2.number, Decimal), ( \"Amount2's number is not a Decimal instance: {}\".format(amount2.number)) if amount1.currency != amount2.currency: raise ValueError( \"Unmatching currencies for operation on {} and {}\".format( amount1, amount2)) return Amount(amount1.number - amount2.number, amount1.currency)","title":"sub()"},{"location":"api_reference/beancount.core.html#beancount.core.compare","text":"Comparison helpers for data objects.","title":"compare"},{"location":"api_reference/beancount.core.html#beancount.core.compare.CompareError","text":"CompareError(source, message, entry)","title":"CompareError"},{"location":"api_reference/beancount.core.html#beancount.core.compare.CompareError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/compare.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.CompareError.__new__","text":"Create new instance of CompareError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.CompareError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/compare.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.compare_entries","text":"Compare two lists of entries. This is used for testing. The entries are compared with disregard for their file location. Parameters: entries1 \u2013 A list of directives of any type. entries2 \u2013 Another list of directives of any type. Returns: A tuple of (success, not_found1, not_found2), where the fields are \u2013 success: A boolean, true if all the values are equal. missing1: A list of directives from 'entries1' not found in 'entries2'. missing2: A list of directives from 'entries2' not found in 'entries1'. Exceptions: ValueError \u2013 If a duplicate entry is found. Source code in beancount/core/compare.py def compare_entries(entries1, entries2): \"\"\"Compare two lists of entries. This is used for testing. The entries are compared with disregard for their file location. Args: entries1: A list of directives of any type. entries2: Another list of directives of any type. Returns: A tuple of (success, not_found1, not_found2), where the fields are: success: A boolean, true if all the values are equal. missing1: A list of directives from 'entries1' not found in 'entries2'. missing2: A list of directives from 'entries2' not found in 'entries1'. Raises: ValueError: If a duplicate entry is found. \"\"\" hashes1, errors1 = hash_entries(entries1, exclude_meta=True) hashes2, errors2 = hash_entries(entries2, exclude_meta=True) keys1 = set(hashes1.keys()) keys2 = set(hashes2.keys()) if errors1 or errors2: error = (errors1 + errors2)[0] raise ValueError(str(error)) same = keys1 == keys2 missing1 = data.sorted([hashes1[key] for key in keys1 - keys2]) missing2 = data.sorted([hashes2[key] for key in keys2 - keys1]) return (same, missing1, missing2)","title":"compare_entries()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.excludes_entries","text":"Check that a list of entries does not appear in another list. Parameters: subset_entries \u2013 The set of entries to look for in 'entries'. entries \u2013 The larger list of entries that should not include 'subset_entries'. Returns: A boolean and a list of entries that are not supposed to appear. Exceptions: ValueError \u2013 If a duplicate entry is found. Source code in beancount/core/compare.py def excludes_entries(subset_entries, entries): \"\"\"Check that a list of entries does not appear in another list. Args: subset_entries: The set of entries to look for in 'entries'. entries: The larger list of entries that should not include 'subset_entries'. Returns: A boolean and a list of entries that are not supposed to appear. Raises: ValueError: If a duplicate entry is found. \"\"\" subset_hashes, subset_errors = hash_entries(subset_entries, exclude_meta=True) subset_keys = set(subset_hashes.keys()) hashes, errors = hash_entries(entries, exclude_meta=True) keys = set(hashes.keys()) if subset_errors or errors: error = (subset_errors + errors)[0] raise ValueError(str(error)) intersection = keys.intersection(subset_keys) excludes = not bool(intersection) extra = data.sorted([subset_hashes[key] for key in intersection]) return (excludes, extra)","title":"excludes_entries()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.hash_entries","text":"Compute unique hashes of each of the entries and return a map of them. This is used for comparisons between sets of entries. Parameters: entries \u2013 A list of directives. exclude_meta \u2013 If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A dict of hash-value to entry (for all entries) and a list of errors. Errors are created when duplicate entries are found. Source code in beancount/core/compare.py def hash_entries(entries, exclude_meta=False): \"\"\"Compute unique hashes of each of the entries and return a map of them. This is used for comparisons between sets of entries. Args: entries: A list of directives. exclude_meta: If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A dict of hash-value to entry (for all entries) and a list of errors. Errors are created when duplicate entries are found. \"\"\" entry_hash_dict = {} errors = [] num_legal_duplicates = 0 for entry in entries: hash_ = hash_entry(entry, exclude_meta) if hash_ in entry_hash_dict: if isinstance(entry, Price): # Note: Allow duplicate Price entries, they should be common # because of the nature of stock markets (if they're closed, the # data source is likely to return an entry for the previously # available date, which may already have been fetched). num_legal_duplicates += 1 else: other_entry = entry_hash_dict[hash_] errors.append( CompareError(entry.meta, \"Duplicate entry: {} == {}\".format(entry, other_entry), entry)) entry_hash_dict[hash_] = entry if not errors: assert len(entry_hash_dict) + num_legal_duplicates == len(entries), ( len(entry_hash_dict), len(entries), num_legal_duplicates) return entry_hash_dict, errors","title":"hash_entries()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.hash_entry","text":"Compute the stable hash of a single entry. Parameters: entry \u2013 A directive instance. exclude_meta \u2013 If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A stable hexadecimal hash of this entry. Source code in beancount/core/compare.py def hash_entry(entry, exclude_meta=False): \"\"\"Compute the stable hash of a single entry. Args: entry: A directive instance. exclude_meta: If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A stable hexadecimal hash of this entry. \"\"\" return stable_hash_namedtuple(entry, IGNORED_FIELD_NAMES if exclude_meta else frozenset())","title":"hash_entry()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.includes_entries","text":"Check if a list of entries is included in another list. Parameters: subset_entries \u2013 The set of entries to look for in 'entries'. entries \u2013 The larger list of entries that could include 'subset_entries'. Returns: A boolean and a list of missing entries. Exceptions: ValueError \u2013 If a duplicate entry is found. Source code in beancount/core/compare.py def includes_entries(subset_entries, entries): \"\"\"Check if a list of entries is included in another list. Args: subset_entries: The set of entries to look for in 'entries'. entries: The larger list of entries that could include 'subset_entries'. Returns: A boolean and a list of missing entries. Raises: ValueError: If a duplicate entry is found. \"\"\" subset_hashes, subset_errors = hash_entries(subset_entries, exclude_meta=True) subset_keys = set(subset_hashes.keys()) hashes, errors = hash_entries(entries, exclude_meta=True) keys = set(hashes.keys()) if subset_errors or errors: error = (subset_errors + errors)[0] raise ValueError(str(error)) includes = subset_keys.issubset(keys) missing = data.sorted([subset_hashes[key] for key in subset_keys - keys]) return (includes, missing)","title":"includes_entries()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.stable_hash_namedtuple","text":"Hash the given namedtuple and its child fields. This iterates over all the members of objtuple, skipping the attributes from the 'ignore' set, and computes a unique hash string code. If the elements are lists or sets, sorts them for stability. Parameters: objtuple \u2013 A tuple object or other. ignore \u2013 A set of strings, attribute names to be skipped in computing a stable hash. For instance, circular references to objects or irrelevant data. Source code in beancount/core/compare.py def stable_hash_namedtuple(objtuple, ignore=frozenset()): \"\"\"Hash the given namedtuple and its child fields. This iterates over all the members of objtuple, skipping the attributes from the 'ignore' set, and computes a unique hash string code. If the elements are lists or sets, sorts them for stability. Args: objtuple: A tuple object or other. ignore: A set of strings, attribute names to be skipped in computing a stable hash. For instance, circular references to objects or irrelevant data. \"\"\" # Note: this routine is slow and would stand to be implemented in C. hashobj = hashlib.md5() for attr_name, attr_value in zip(objtuple._fields, objtuple): if attr_name in ignore: continue if isinstance(attr_value, (list, set, frozenset)): subhashes = set() for element in attr_value: if isinstance(element, tuple): subhashes.add(stable_hash_namedtuple(element, ignore)) else: md5 = hashlib.md5() md5.update(str(element).encode()) subhashes.add(md5.hexdigest()) for subhash in sorted(subhashes): hashobj.update(subhash.encode()) else: hashobj.update(str(attr_value).encode()) return hashobj.hexdigest()","title":"stable_hash_namedtuple()"},{"location":"api_reference/beancount.core.html#beancount.core.convert","text":"Conversions from Position (or Posting) to units, cost, weight, market value. Units: Just the primary amount of the position. Cost: The cost basis of the position, if available. Weight: The cost basis or price of the position. Market Value: The units converted to a value via a price map. To convert an inventory's contents, simply use these functions in conjunction with Inventory.reduce() , like cost_inv = inv.reduce(convert.get_cost) This module equivalently converts Position and Posting instances. Note that we're specifically avoiding to create an import dependency on beancount.core.data in order to keep this module isolatable, but it works on postings due to duck-typing. Function named get_*() are used to compute values from postings to their price currency. Functions named convert_*() are used to convert postings and amounts to any currency.","title":"convert"},{"location":"api_reference/beancount.core.html#beancount.core.convert.convert_amount","text":"Return the market value of an Amount in a particular currency. In addition, if a conversion rate isn't available, you can provide a list of currencies to attempt to synthesize a rate for via implied rates. Parameters: amt \u2013 An instance of Amount. target_currency \u2013 The target currency to convert to. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance to evaluate the value at, or None. via \u2013 A list of currencies to attempt to synthesize an implied rate if the direct conversion fails. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, the amount itself, unmodified. Source code in beancount/core/convert.py def convert_amount(amt, target_currency, price_map, date=None, via=None): \"\"\"Return the market value of an Amount in a particular currency. In addition, if a conversion rate isn't available, you can provide a list of currencies to attempt to synthesize a rate for via implied rates. Args: amt: An instance of Amount. target_currency: The target currency to convert to. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. via: A list of currencies to attempt to synthesize an implied rate if the direct conversion fails. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, the amount itself, unmodified. \"\"\" # First, attempt to convert directly. This should be the most # straightforward conversion. base_quote = (amt.currency, target_currency) _, rate = prices.get_price(price_map, base_quote, date) if rate is not None: # On success, just make the conversion directly. return Amount(amt.number * rate, target_currency) elif via: assert isinstance(via, (tuple, list)) # A price is unavailable, attempt to convert via cost/price currency # hop, if the value currency isn't the target currency. for implied_currency in via: if implied_currency == target_currency: continue base_quote1 = (amt.currency, implied_currency) _, rate1 = prices.get_price(price_map, base_quote1, date) if rate1 is not None: base_quote2 = (implied_currency, target_currency) _, rate2 = prices.get_price(price_map, base_quote2, date) if rate2 is not None: return Amount(amt.number * rate1 * rate2, target_currency) # We failed to infer a conversion rate; return the amt. return amt","title":"convert_amount()"},{"location":"api_reference/beancount.core.html#beancount.core.convert.convert_position","text":"Return the market value of a Position or Posting in a particular currency. In addition, if the rate from the position's currency to target_currency isn't available, an attempt is made to convert from its cost currency, if one is available. Parameters: pos \u2013 An instance of Position or Posting, equivalently. target_currency \u2013 The target currency to convert to. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance to evaluate the value at, or None. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. (See get_value() above for details.) Source code in beancount/core/convert.py def convert_position(pos, target_currency, price_map, date=None): \"\"\"Return the market value of a Position or Posting in a particular currency. In addition, if the rate from the position's currency to target_currency isn't available, an attempt is made to convert from its cost currency, if one is available. Args: pos: An instance of Position or Posting, equivalently. target_currency: The target currency to convert to. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. (See get_value() above for details.) \"\"\" cost = pos.cost value_currency = ( (isinstance(cost, Cost) and cost.currency) or (hasattr(pos, 'price') and pos.price and pos.price.currency) or None) return convert_amount(pos.units, target_currency, price_map, date=date, via=(value_currency,))","title":"convert_position()"},{"location":"api_reference/beancount.core.html#beancount.core.convert.get_cost","text":"Return the total cost of a Position or Posting. Parameters: pos \u2013 An instance of Position or Posting, equivalently. Returns: An Amount. Source code in beancount/core/convert.py def get_cost(pos): \"\"\"Return the total cost of a Position or Posting. Args: pos: An instance of Position or Posting, equivalently. Returns: An Amount. \"\"\" assert isinstance(pos, Position) or type(pos).__name__ == 'Posting' cost = pos.cost return (Amount(cost.number * pos.units.number, cost.currency) if (isinstance(cost, Cost) and isinstance(cost.number, Decimal)) else pos.units)","title":"get_cost()"},{"location":"api_reference/beancount.core.html#beancount.core.convert.get_units","text":"Return the units of a Position or Posting. Parameters: pos \u2013 An instance of Position or Posting, equivalently. Returns: An Amount. Source code in beancount/core/convert.py def get_units(pos): \"\"\"Return the units of a Position or Posting. Args: pos: An instance of Position or Posting, equivalently. Returns: An Amount. \"\"\" assert isinstance(pos, Position) or type(pos).__name__ == 'Posting' return pos.units","title":"get_units()"},{"location":"api_reference/beancount.core.html#beancount.core.convert.get_value","text":"Return the market value of a Position or Posting. Note that if the position is not held at cost, this does not convert anything, even if a price is available in the 'price_map'. We don't specify a target currency here. If you're attempting to make such a conversion, see convert_*() functions below. Parameters: pos \u2013 An instance of Position or Posting, equivalently. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance to evaluate the value at, or None. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. This is designed so that you could reduce an inventory with this and not lose any information silently in case of failure to convert (possibly due to an empty price map). Compare the returned currency to that of the input position if you need to check for success. Source code in beancount/core/convert.py def get_value(pos, price_map, date=None): \"\"\"Return the market value of a Position or Posting. Note that if the position is not held at cost, this does not convert anything, even if a price is available in the 'price_map'. We don't specify a target currency here. If you're attempting to make such a conversion, see ``convert_*()`` functions below. Args: pos: An instance of Position or Posting, equivalently. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. This is designed so that you could reduce an inventory with this and not lose any information silently in case of failure to convert (possibly due to an empty price map). Compare the returned currency to that of the input position if you need to check for success. \"\"\" assert isinstance(pos, Position) or type(pos).__name__ == 'Posting' units = pos.units cost = pos.cost # Try to infer what the cost/price currency should be. value_currency = ( (isinstance(cost, Cost) and cost.currency) or (hasattr(pos, 'price') and pos.price and pos.price.currency) or None) if isinstance(value_currency, str): # We have a value currency; hit the price database. base_quote = (units.currency, value_currency) _, price_number = prices.get_price(price_map, base_quote, date) if price_number is not None: return Amount(units.number * price_number, value_currency) # We failed to infer a conversion rate; return the units. return units","title":"get_value()"},{"location":"api_reference/beancount.core.html#beancount.core.convert.get_weight","text":"Return the weight of a Position or Posting. This is the amount that will need to be balanced from a posting of a transaction. This is a key element of the semantics of transactions in this software. A balance amount is the amount used to check the balance of a transaction. Here are all relevant examples, with the amounts used to balance the postings: Assets:Account 5234.50 USD -> 5234.50 USD Assets:Account 3877.41 EUR @ 1.35 USD -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} @ 545.60 CAD -> 5234.50 USD Parameters: pos \u2013 An instance of Position or Posting, equivalently. Returns: An Amount. Source code in beancount/core/convert.py def get_weight(pos): \"\"\"Return the weight of a Position or Posting. This is the amount that will need to be balanced from a posting of a transaction. This is a *key* element of the semantics of transactions in this software. A balance amount is the amount used to check the balance of a transaction. Here are all relevant examples, with the amounts used to balance the postings: Assets:Account 5234.50 USD -> 5234.50 USD Assets:Account 3877.41 EUR @ 1.35 USD -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} @ 545.60 CAD -> 5234.50 USD Args: pos: An instance of Position or Posting, equivalently. Returns: An Amount. \"\"\" assert isinstance(pos, Position) or type(pos).__name__ == 'Posting' units = pos.units cost = pos.cost # It the object has a cost, use that as the weight, to balance. if isinstance(cost, Cost) and isinstance(cost.number, Decimal): weight = Amount(cost.number * pos.units.number, cost.currency) else: # Otherwise use the postings. weight = units # Unless there is a price available; use that if present. if not isinstance(pos, Position): price = pos.price if price is not None: # Note: Here we could assert that price.currency == units.currency. if price.number is MISSING or units.number is MISSING: converted_number = MISSING else: converted_number = price.number * units.number weight = Amount(converted_number, price.currency) return weight","title":"get_weight()"},{"location":"api_reference/beancount.core.html#beancount.core.data","text":"Basic data structures used to represent the Ledger entries.","title":"data"},{"location":"api_reference/beancount.core.html#beancount.core.data.Balance","text":"Balance(meta, date, account, amount, tolerance, diff_amount)","title":"Balance"},{"location":"api_reference/beancount.core.html#beancount.core.data.Balance.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Balance.__new__","text":"Create new instance of Balance(meta, date, account, amount, tolerance, diff_amount)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Balance.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Close","text":"Close(meta, date, account)","title":"Close"},{"location":"api_reference/beancount.core.html#beancount.core.data.Close.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Close.__new__","text":"Create new instance of Close(meta, date, account)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Close.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Commodity","text":"Commodity(meta, date, currency)","title":"Commodity"},{"location":"api_reference/beancount.core.html#beancount.core.data.Commodity.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Commodity.__new__","text":"Create new instance of Commodity(meta, date, currency)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Commodity.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Custom","text":"Custom(meta, date, type, values)","title":"Custom"},{"location":"api_reference/beancount.core.html#beancount.core.data.Custom.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Custom.__new__","text":"Create new instance of Custom(meta, date, type, values)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Custom.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Document","text":"Document(meta, date, account, filename, tags, links)","title":"Document"},{"location":"api_reference/beancount.core.html#beancount.core.data.Document.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Document.__new__","text":"Create new instance of Document(meta, date, account, filename, tags, links)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Document.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Event","text":"Event(meta, date, type, description)","title":"Event"},{"location":"api_reference/beancount.core.html#beancount.core.data.Event.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Event.__new__","text":"Create new instance of Event(meta, date, type, description)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Event.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Note","text":"Note(meta, date, account, comment)","title":"Note"},{"location":"api_reference/beancount.core.html#beancount.core.data.Note.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Note.__new__","text":"Create new instance of Note(meta, date, account, comment)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Note.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Open","text":"Open(meta, date, account, currencies, booking)","title":"Open"},{"location":"api_reference/beancount.core.html#beancount.core.data.Open.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Open.__new__","text":"Create new instance of Open(meta, date, account, currencies, booking)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Open.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Pad","text":"Pad(meta, date, account, source_account)","title":"Pad"},{"location":"api_reference/beancount.core.html#beancount.core.data.Pad.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Pad.__new__","text":"Create new instance of Pad(meta, date, account, source_account)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Pad.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Posting","text":"Posting(account, units, cost, price, flag, meta)","title":"Posting"},{"location":"api_reference/beancount.core.html#beancount.core.data.Posting.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Posting.__new__","text":"Create new instance of Posting(account, units, cost, price, flag, meta)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Posting.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Price","text":"Price(meta, date, currency, amount)","title":"Price"},{"location":"api_reference/beancount.core.html#beancount.core.data.Price.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Price.__new__","text":"Create new instance of Price(meta, date, currency, amount)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Price.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Query","text":"Query(meta, date, name, query_string)","title":"Query"},{"location":"api_reference/beancount.core.html#beancount.core.data.Query.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Query.__new__","text":"Create new instance of Query(meta, date, name, query_string)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Query.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Transaction","text":"Transaction(meta, date, flag, payee, narration, tags, links, postings)","title":"Transaction"},{"location":"api_reference/beancount.core.html#beancount.core.data.Transaction.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Transaction.__new__","text":"Create new instance of Transaction(meta, date, flag, payee, narration, tags, links, postings)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Transaction.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.TxnPosting","text":"TxnPosting(txn, posting)","title":"TxnPosting"},{"location":"api_reference/beancount.core.html#beancount.core.data.TxnPosting.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.TxnPosting.__new__","text":"Create new instance of TxnPosting(txn, posting)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.TxnPosting.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.create_simple_posting","text":"Create a simple posting on the entry, with just a number and currency (no cost). Parameters: entry \u2013 The entry instance to add the posting to. account \u2013 A string, the account to use on the posting. number \u2013 A Decimal number or string to use in the posting's Amount. currency \u2013 A string, the currency for the Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. Source code in beancount/core/data.py def create_simple_posting(entry, account, number, currency): \"\"\"Create a simple posting on the entry, with just a number and currency (no cost). Args: entry: The entry instance to add the posting to. account: A string, the account to use on the posting. number: A Decimal number or string to use in the posting's Amount. currency: A string, the currency for the Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. \"\"\" if isinstance(account, str): pass if number is None: units = None else: if not isinstance(number, Decimal): number = D(number) units = Amount(number, currency) posting = Posting(account, units, None, None, None, None) if entry is not None: entry.postings.append(posting) return posting","title":"create_simple_posting()"},{"location":"api_reference/beancount.core.html#beancount.core.data.create_simple_posting_with_cost","text":"Create a simple posting on the entry, with just a number and currency (no cost). Parameters: entry \u2013 The entry instance to add the posting to. account \u2013 A string, the account to use on the posting. number \u2013 A Decimal number or string to use in the posting's Amount. currency \u2013 A string, the currency for the Amount. cost_number \u2013 A Decimal number or string to use for the posting's cost Amount. cost_currency \u2013 a string, the currency for the cost Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. Source code in beancount/core/data.py def create_simple_posting_with_cost(entry, account, number, currency, cost_number, cost_currency): \"\"\"Create a simple posting on the entry, with just a number and currency (no cost). Args: entry: The entry instance to add the posting to. account: A string, the account to use on the posting. number: A Decimal number or string to use in the posting's Amount. currency: A string, the currency for the Amount. cost_number: A Decimal number or string to use for the posting's cost Amount. cost_currency: a string, the currency for the cost Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. \"\"\" if isinstance(account, str): pass if not isinstance(number, Decimal): number = D(number) if cost_number and not isinstance(cost_number, Decimal): cost_number = D(cost_number) units = Amount(number, currency) cost = Cost(cost_number, cost_currency, None, None) posting = Posting(account, units, cost, None, None, None) if entry is not None: entry.postings.append(posting) return posting","title":"create_simple_posting_with_cost()"},{"location":"api_reference/beancount.core.html#beancount.core.data.entry_sortkey","text":"Sort-key for entries. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Parameters: entry \u2013 An entry instance. Returns: A tuple of (date, integer, integer), that forms the sort key for the entry. Source code in beancount/core/data.py def entry_sortkey(entry): \"\"\"Sort-key for entries. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Args: entry: An entry instance. Returns: A tuple of (date, integer, integer), that forms the sort key for the entry. \"\"\" return (entry.date, SORT_ORDER.get(type(entry), 0), entry.meta[\"lineno\"])","title":"entry_sortkey()"},{"location":"api_reference/beancount.core.html#beancount.core.data.filter_txns","text":"A generator that yields only the Transaction instances. This is such an incredibly common operation that it deserves a terse filtering mechanism. Parameters: entries \u2013 A list of directives. Yields: A sorted list of only the Transaction directives. Source code in beancount/core/data.py def filter_txns(entries): \"\"\"A generator that yields only the Transaction instances. This is such an incredibly common operation that it deserves a terse filtering mechanism. Args: entries: A list of directives. Yields: A sorted list of only the Transaction directives. \"\"\" for entry in entries: if isinstance(entry, Transaction): yield entry","title":"filter_txns()"},{"location":"api_reference/beancount.core.html#beancount.core.data.find_closest","text":"Find the closest entry from entries to (filename, lineno). Parameters: entries \u2013 A list of directives. filename \u2013 A string, the name of the ledger file to look for. Be careful to provide the very same filename, and note that the parser stores the absolute path of the filename here. lineno \u2013 An integer, the line number closest after the directive we're looking for. This may be the exact/first line of the directive. Returns: The closest entry found in the given file for the given filename, or None, if none could be found. Source code in beancount/core/data.py def find_closest(entries, filename, lineno): \"\"\"Find the closest entry from entries to (filename, lineno). Args: entries: A list of directives. filename: A string, the name of the ledger file to look for. Be careful to provide the very same filename, and note that the parser stores the absolute path of the filename here. lineno: An integer, the line number closest after the directive we're looking for. This may be the exact/first line of the directive. Returns: The closest entry found in the given file for the given filename, or None, if none could be found. \"\"\" min_diffline = sys.maxsize closest_entry = None for entry in entries: emeta = entry.meta if emeta[\"filename\"] == filename and emeta[\"lineno\"] > 0: diffline = lineno - emeta[\"lineno\"] if 0 <= diffline < min_diffline: min_diffline = diffline closest_entry = entry return closest_entry","title":"find_closest()"},{"location":"api_reference/beancount.core.html#beancount.core.data.get_entry","text":"Return the entry associated with the posting or entry. Parameters: entry \u2013 A TxnPosting or entry instance Returns: A datetime instance. Source code in beancount/core/data.py def get_entry(posting_or_entry): \"\"\"Return the entry associated with the posting or entry. Args: entry: A TxnPosting or entry instance Returns: A datetime instance. \"\"\" return (posting_or_entry.txn if isinstance(posting_or_entry, TxnPosting) else posting_or_entry)","title":"get_entry()"},{"location":"api_reference/beancount.core.html#beancount.core.data.has_entry_account_component","text":"Return true if one of the entry's postings has an account component. Parameters: entry \u2013 A Transaction entry. component \u2013 A string, a component of an account name. For instance, Food in Expenses:Food:Restaurant . All components are considered. Returns: Boolean \u2013 true if the component is in the account. Note that a component name must be whole, that is NY is not in Expenses:Taxes:StateNY . Source code in beancount/core/data.py def has_entry_account_component(entry, component): \"\"\"Return true if one of the entry's postings has an account component. Args: entry: A Transaction entry. component: A string, a component of an account name. For instance, ``Food`` in ``Expenses:Food:Restaurant``. All components are considered. Returns: Boolean: true if the component is in the account. Note that a component name must be whole, that is ``NY`` is not in ``Expenses:Taxes:StateNY``. \"\"\" return (isinstance(entry, Transaction) and any(has_component(posting.account, component) for posting in entry.postings))","title":"has_entry_account_component()"},{"location":"api_reference/beancount.core.html#beancount.core.data.iter_entry_dates","text":"Iterate over the entries in a date window. Parameters: entries \u2013 A date-sorted list of dated directives. date_begin \u2013 A datetime.date instance, the first date to include. date_end \u2013 A datetime.date instance, one day beyond the last date. Yields: Instances of the dated directives, between the dates, and in the order in which they appear. Source code in beancount/core/data.py def iter_entry_dates(entries, date_begin, date_end): \"\"\"Iterate over the entries in a date window. Args: entries: A date-sorted list of dated directives. date_begin: A datetime.date instance, the first date to include. date_end: A datetime.date instance, one day beyond the last date. Yields: Instances of the dated directives, between the dates, and in the order in which they appear. \"\"\" getdate = lambda entry: entry.date index_begin = bisect_left_with_key(entries, date_begin, key=getdate) index_end = bisect_left_with_key(entries, date_end, key=getdate) for index in range(index_begin, index_end): yield entries[index]","title":"iter_entry_dates()"},{"location":"api_reference/beancount.core.html#beancount.core.data.new_directive","text":"Create a directive class. Do not include default fields. This should probably be carried out through inheritance. Parameters: name \u2013 A string, the capitalized name of the directive. fields ( List[Tuple] ) \u2013 A string or the list of strings, names for the fields to add to the base tuple. Returns: \u2013 A type object for the new directive type. Source code in beancount/core/data.py def new_directive(clsname, fields: List[Tuple]) -> NamedTuple: \"\"\"Create a directive class. Do not include default fields. This should probably be carried out through inheritance. Args: name: A string, the capitalized name of the directive. fields: A string or the list of strings, names for the fields to add to the base tuple. Returns: A type object for the new directive type. \"\"\" return NamedTuple( clsname, [('meta', Meta), ('date', datetime.date)] + fields)","title":"new_directive()"},{"location":"api_reference/beancount.core.html#beancount.core.data.new_metadata","text":"Create a new metadata container from the filename and line number. Parameters: filename \u2013 A string, the filename for the creator of this directive. lineno \u2013 An integer, the line number where the directive has been created. kvlist \u2013 An optional container of key-values. Returns: A metadata dict. Source code in beancount/core/data.py def new_metadata(filename, lineno, kvlist=None): \"\"\"Create a new metadata container from the filename and line number. Args: filename: A string, the filename for the creator of this directive. lineno: An integer, the line number where the directive has been created. kvlist: An optional container of key-values. Returns: A metadata dict. \"\"\" meta = {'filename': filename, 'lineno': lineno} if kvlist: meta.update(kvlist) return meta","title":"new_metadata()"},{"location":"api_reference/beancount.core.html#beancount.core.data.posting_has_conversion","text":"Return true if this position involves a conversion. A conversion is when there is a price attached to the amount but no cost. This is used on transactions to convert between units. Parameters: posting \u2013 an instance of Posting Returns: A boolean, true if this posting has a price conversion. Source code in beancount/core/data.py def posting_has_conversion(posting): \"\"\"Return true if this position involves a conversion. A conversion is when there is a price attached to the amount but no cost. This is used on transactions to convert between units. Args: posting: an instance of Posting Return: A boolean, true if this posting has a price conversion. \"\"\" return (posting.cost is None and posting.price is not None)","title":"posting_has_conversion()"},{"location":"api_reference/beancount.core.html#beancount.core.data.posting_sortkey","text":"Sort-key for entries or postings. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Parameters: entry \u2013 A Posting or entry instance Returns: A tuple of (date, integer, integer), that forms the sort key for the posting or entry. Source code in beancount/core/data.py def posting_sortkey(entry): \"\"\"Sort-key for entries or postings. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Args: entry: A Posting or entry instance Returns: A tuple of (date, integer, integer), that forms the sort key for the posting or entry. \"\"\" if isinstance(entry, TxnPosting): entry = entry.txn return (entry.date, SORT_ORDER.get(type(entry), 0), entry.meta[\"lineno\"])","title":"posting_sortkey()"},{"location":"api_reference/beancount.core.html#beancount.core.data.remove_account_postings","text":"Remove all postings with the given account. Parameters: account \u2013 A string, the account name whose postings we want to remove. Returns: A list of entries without the rounding postings. Source code in beancount/core/data.py def remove_account_postings(account, entries): \"\"\"Remove all postings with the given account. Args: account: A string, the account name whose postings we want to remove. Returns: A list of entries without the rounding postings. \"\"\" new_entries = [] for entry in entries: if isinstance(entry, Transaction) and ( any(posting.account == account for posting in entry.postings)): entry = entry._replace(postings=[posting for posting in entry.postings if posting.account != account]) new_entries.append(entry) return new_entries","title":"remove_account_postings()"},{"location":"api_reference/beancount.core.html#beancount.core.data.sanity_check_types","text":"Check that the entry and its postings has all correct data types. Parameters: entry \u2013 An instance of one of the entries to be checked. allow_none_for_tags_and_links \u2013 A boolean, whether to allow plugins to generate Transaction objects with None as value for the 'tags' or 'links' attributes. Exceptions: AssertionError \u2013 If there is anything that is unexpected, raises an exception. Source code in beancount/core/data.py def sanity_check_types(entry, allow_none_for_tags_and_links=False): \"\"\"Check that the entry and its postings has all correct data types. Args: entry: An instance of one of the entries to be checked. allow_none_for_tags_and_links: A boolean, whether to allow plugins to generate Transaction objects with None as value for the 'tags' or 'links' attributes. Raises: AssertionError: If there is anything that is unexpected, raises an exception. \"\"\" assert isinstance(entry, ALL_DIRECTIVES), \"Invalid directive type\" assert isinstance(entry.meta, dict), \"Invalid type for meta\" assert 'filename' in entry.meta, \"Missing filename in metadata\" assert 'lineno' in entry.meta, \"Missing line number in metadata\" assert isinstance(entry.date, datetime.date), \"Invalid date type\" if isinstance(entry, Transaction): assert isinstance(entry.flag, (NoneType, str)), \"Invalid flag type\" assert isinstance(entry.payee, (NoneType, str)), \"Invalid payee type\" assert isinstance(entry.narration, (NoneType, str)), \"Invalid narration type\" set_types = ((NoneType, set, frozenset) if allow_none_for_tags_and_links else (set, frozenset)) assert isinstance(entry.tags, set_types), ( \"Invalid tags type: {}\".format(type(entry.tags))) assert isinstance(entry.links, set_types), ( \"Invalid links type: {}\".format(type(entry.links))) assert isinstance(entry.postings, list), \"Invalid postings list type\" for posting in entry.postings: assert isinstance(posting, Posting), \"Invalid posting type\" assert isinstance(posting.account, str), \"Invalid account type\" assert isinstance(posting.units, (Amount, NoneType)), \"Invalid units type\" assert isinstance(posting.cost, (Cost, CostSpec, NoneType)), \"Invalid cost type\" assert isinstance(posting.price, (Amount, NoneType)), \"Invalid price type\" assert isinstance(posting.flag, (str, NoneType)), \"Invalid flag type\"","title":"sanity_check_types()"},{"location":"api_reference/beancount.core.html#beancount.core.data.sorted","text":"A convenience to sort a list of entries, using entry_sortkey(). Parameters: entries \u2013 A list of directives. Returns: A sorted list of directives. Source code in beancount/core/data.py def sorted(entries): \"\"\"A convenience to sort a list of entries, using entry_sortkey(). Args: entries: A list of directives. Returns: A sorted list of directives. \"\"\" return builtins.sorted(entries, key=entry_sortkey)","title":"sorted()"},{"location":"api_reference/beancount.core.html#beancount.core.data.transaction_has_conversion","text":"Given a Transaction entry, return true if at least one of the postings has a price conversion (without an associated cost). These are the source of non-zero conversion balances. Parameters: transaction \u2013 an instance of a Transaction entry. Returns: A boolean, true if this transaction contains at least one posting with a price conversion. Source code in beancount/core/data.py def transaction_has_conversion(transaction): \"\"\"Given a Transaction entry, return true if at least one of the postings has a price conversion (without an associated cost). These are the source of non-zero conversion balances. Args: transaction: an instance of a Transaction entry. Returns: A boolean, true if this transaction contains at least one posting with a price conversion. \"\"\" assert isinstance(transaction, Transaction), ( \"Invalid type of entry for transaction: {}\".format(transaction)) for posting in transaction.postings: if posting_has_conversion(posting): return True return False","title":"transaction_has_conversion()"},{"location":"api_reference/beancount.core.html#beancount.core.display_context","text":"A settings class to offer control over the number of digits rendered. This module contains routines that can accumulate information on the width and precision of numbers to be rendered and derive the precision required to render all of them consistently and under certain common alignment requirements. This is required in order to output neatly lined up columns of numbers in various styles. A common case is that the precision can be observed for numbers present in the input file. This display precision can be used as the \"precision by default\" if we write a routine for which it is inconvenient to feed all the numbers to build such an accumulator. Here are all the aspects supported by this module: PRECISION: Numbers for a particular currency are always rendered to the same precision, and they can be rendered to one of two precisions; either the most common number of fractional digits, or the maximum number of digits seen (this is useful for rendering prices). ALIGNMENT: Several alignment methods are supported. \"natural\": Render the strings as small as possible with no padding, but to their currency's precision. Like this: '1.2345' '764' '-7,409.01' '0.00000125' \"dot-aligned\": The periods will align vertically, the left and right sides are padded so that the column of numbers has the same width: ' 1.2345 ' ' 764 ' '-7,409.01 ' ' 0.00000125' \"right\": The strings are all flushed right, the left side is padded so that the column of numbers has the same width: ' 1.2345' ' 764' ' -7,409.01' ' 0.00000125' SIGN: If a negative sign is present in the input numbers, the rendered numbers reserve a space for it. If not, then we save the space. COMMAS: If the user requests to render commas, commas are rendered in the output. RESERVED: A number of extra integral digits reserved on the left in order to allow rendering novel numbers that haven't yet been seen. For example, balances may contains much larger numbers than the numbers seen in input files, and these need to be accommodated when aligning to the right.","title":"display_context"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.Align","text":"Alignment style for numbers.","title":"Align"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayContext","text":"A builder object used to construct a DisplayContext from a series of numbers. Attributes: Name Type Description ccontexts A dict of currency string to CurrencyContext instance. commas A bool, true if we should render commas. This just gets propagated onwards as the default value of to build with.","title":"DisplayContext"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayContext.build","text":"Build a formatter for the given display context. Parameters: alignment \u2013 The desired alignment. precision \u2013 The desired precision. commas \u2013 Whether to render commas or not. If 'None', the default value carried by the context will be used. reserved \u2013 An integer, the number of extra digits to be allocated in the maximum width calculations. Source code in beancount/core/display_context.py def build(self, alignment=Align.NATURAL, precision=Precision.MOST_COMMON, commas=None, reserved=0): \"\"\"Build a formatter for the given display context. Args: alignment: The desired alignment. precision: The desired precision. commas: Whether to render commas or not. If 'None', the default value carried by the context will be used. reserved: An integer, the number of extra digits to be allocated in the maximum width calculations. \"\"\" if commas is None: commas = self.commas if alignment == Align.NATURAL: build_method = self._build_natural elif alignment == Align.RIGHT: build_method = self._build_right elif alignment == Align.DOT: build_method = self._build_dot else: raise ValueError(\"Unknown alignment: {}\".format(alignment)) fmtstrings = build_method(precision, commas, reserved) return DisplayFormatter(self, precision, fmtstrings)","title":"build()"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayContext.quantize","text":"Quantize the given number to the given precision. Parameters: number \u2013 A Decimal instance, the number to be quantized. currency \u2013 A currency string. precision \u2013 Which precision to use. Returns: A Decimal instance, the quantized number. Source code in beancount/core/display_context.py def quantize(self, number, currency, precision=Precision.MOST_COMMON): \"\"\"Quantize the given number to the given precision. Args: number: A Decimal instance, the number to be quantized. currency: A currency string. precision: Which precision to use. Returns: A Decimal instance, the quantized number. \"\"\" assert isinstance(number, Decimal), \"Invalid data: {}\".format(number) ccontext = self.ccontexts[currency] num_fractional_digits = ccontext.get_fractional(precision) if num_fractional_digits is None: # Note: We could probably logging.warn() this situation here. return number qdigit = Decimal(1).scaleb(-num_fractional_digits) return number.quantize(qdigit)","title":"quantize()"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayContext.set_commas","text":"Set the default value for rendering commas. Source code in beancount/core/display_context.py def set_commas(self, commas): \"\"\"Set the default value for rendering commas.\"\"\" self.commas = commas","title":"set_commas()"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayContext.update","text":"Update the builder with the given number for the given currency. Parameters: number \u2013 An instance of Decimal to consider for this currency. currency \u2013 An optional string, the currency this numbers applies to. Source code in beancount/core/display_context.py def update(self, number, currency='__default__'): \"\"\"Update the builder with the given number for the given currency. Args: number: An instance of Decimal to consider for this currency. currency: An optional string, the currency this numbers applies to. \"\"\" self.ccontexts[currency].update(number)","title":"update()"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayFormatter","text":"A class used to contain various settings that control how we output numbers. In particular, the precision used for each currency, and whether or not commas should be printed. This object is intended to be passed around to all functions that format numbers to strings. Attributes: Name Type Description dcontext A DisplayContext instance. precision An enum of Precision from which it was built. fmtstrings A dict of currency to pre-baked format strings for it. fmtfuncs A dict of currency to pre-baked formatting functions for it.","title":"DisplayFormatter"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.Precision","text":"The type of precision required.","title":"Precision"},{"location":"api_reference/beancount.core.html#beancount.core.distribution","text":"A simple accumulator for data about a mathematical distribution.","title":"distribution"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution","text":"A class that computes a histogram of integer values. This is used to compute a length that will cover at least some decent fraction of the samples.","title":"Distribution"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution.empty","text":"Return true if the distribution is empty. Returns: A boolean. Source code in beancount/core/distribution.py def empty(self): \"\"\"Return true if the distribution is empty. Returns: A boolean. \"\"\" return len(self.hist) == 0","title":"empty()"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution.max","text":"Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. Source code in beancount/core/distribution.py def max(self): \"\"\"Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. \"\"\" if not self.hist: return None value, _ = sorted(self.hist.items())[-1] return value","title":"max()"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution.min","text":"Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. Source code in beancount/core/distribution.py def min(self): \"\"\"Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. \"\"\" if not self.hist: return None value, _ = sorted(self.hist.items())[0] return value","title":"min()"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution.mode","text":"Return the mode of the distribution. Returns: An element of the value type, or None, if the distribution was empty. Source code in beancount/core/distribution.py def mode(self): \"\"\"Return the mode of the distribution. Returns: An element of the value type, or None, if the distribution was empty. \"\"\" if not self.hist: return None max_value = 0 max_count = 0 for value, count in sorted(self.hist.items()): if count >= max_count: max_count = count max_value = value return max_value","title":"mode()"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution.update","text":"Add a sample to the distribution. Parameters: value \u2013 A value of the function. Source code in beancount/core/distribution.py def update(self, value): \"\"\"Add a sample to the distribution. Args: value: A value of the function. \"\"\" self.hist[value] += 1","title":"update()"},{"location":"api_reference/beancount.core.html#beancount.core.flags","text":"Flag constants.","title":"flags"},{"location":"api_reference/beancount.core.html#beancount.core.getters","text":"Getter functions that operate on lists of entries to return various lists of things that they reference, accounts, tags, links, currencies, etc.","title":"getters"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts","text":"Accounts gatherer.","title":"GetAccounts"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Balance","text":"Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one(_, entry): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return (entry.account,)","title":"Balance()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Close","text":"Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one(_, entry): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return (entry.account,)","title":"Close()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Commodity","text":"Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero(_, entry): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return ()","title":"Commodity()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Custom","text":"Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero(_, entry): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return ()","title":"Custom()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Document","text":"Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one(_, entry): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return (entry.account,)","title":"Document()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Event","text":"Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero(_, entry): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return ()","title":"Event()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Note","text":"Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one(_, entry): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return (entry.account,)","title":"Note()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Open","text":"Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one(_, entry): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return (entry.account,)","title":"Open()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Pad","text":"Process a Pad directive. Parameters: entry \u2013 An instance of Pad. Returns: The two accounts of the Pad directive. Source code in beancount/core/getters.py def Pad(_, entry): \"\"\"Process a Pad directive. Args: entry: An instance of Pad. Returns: The two accounts of the Pad directive. \"\"\" return (entry.account, entry.source_account)","title":"Pad()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Price","text":"Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero(_, entry): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return ()","title":"Price()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Query","text":"Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero(_, entry): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return ()","title":"Query()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Transaction","text":"Process a Transaction directive. Parameters: entry \u2013 An instance of Transaction. Yields: The accounts of the legs of the transaction. Source code in beancount/core/getters.py def Transaction(_, entry): \"\"\"Process a Transaction directive. Args: entry: An instance of Transaction. Yields: The accounts of the legs of the transaction. \"\"\" for posting in entry.postings: yield posting.account","title":"Transaction()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.get_accounts_use_map","text":"Gather the list of accounts from the list of entries. Parameters: entries \u2013 A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. Source code in beancount/core/getters.py def get_accounts_use_map(self, entries): \"\"\"Gather the list of accounts from the list of entries. Args: entries: A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. \"\"\" accounts_first = {} accounts_last = {} for entry in entries: method = getattr(self, entry.__class__.__name__) for account_ in method(entry): if account_ not in accounts_first: accounts_first[account_] = entry.date accounts_last[account_] = entry.date return accounts_first, accounts_last","title":"get_accounts_use_map()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.get_entry_accounts","text":"Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Parameters: entry \u2013 A directive instance. Returns: A set of account name strings. Source code in beancount/core/getters.py def get_entry_accounts(self, entry): \"\"\"Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Args: entry: A directive instance. Returns: A set of account name strings. \"\"\" method = getattr(self, entry.__class__.__name__) return set(method(entry))","title":"get_entry_accounts()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_account_components","text":"Gather all the account components available in the given directives. Parameters: entries \u2013 A list of directive instances. Returns: A list of strings, the unique account components, including the root account names. Source code in beancount/core/getters.py def get_account_components(entries): \"\"\"Gather all the account components available in the given directives. Args: entries: A list of directive instances. Returns: A list of strings, the unique account components, including the root account names. \"\"\" accounts = get_accounts(entries) components = set() for account_name in accounts: components.update(account.split(account_name)) return sorted(components)","title":"get_account_components()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_account_open_close","text":"Fetch the open/close entries for each of the accounts. If an open or close entry happens to be duplicated, accept the earliest entry (chronologically). Parameters: entries \u2013 A list of directive instances. Returns: A map of account name strings to pairs of (open-directive, close-directive) tuples. Source code in beancount/core/getters.py def get_account_open_close(entries): \"\"\"Fetch the open/close entries for each of the accounts. If an open or close entry happens to be duplicated, accept the earliest entry (chronologically). Args: entries: A list of directive instances. Returns: A map of account name strings to pairs of (open-directive, close-directive) tuples. \"\"\" # A dict of account name to (open-entry, close-entry). open_close_map = defaultdict(lambda: [None, None]) for entry in entries: if not isinstance(entry, (Open, Close)): continue open_close = open_close_map[entry.account] index = 0 if isinstance(entry, Open) else 1 previous_entry = open_close[index] if previous_entry is not None: if previous_entry.date <= entry.date: entry = previous_entry open_close[index] = entry return dict(open_close_map)","title":"get_account_open_close()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_accounts","text":"Gather all the accounts references by a list of directives. Parameters: entries \u2013 A list of directive instances. Returns: A set of account strings. Source code in beancount/core/getters.py def get_accounts(entries): \"\"\"Gather all the accounts references by a list of directives. Args: entries: A list of directive instances. Returns: A set of account strings. \"\"\" _, accounts_last = _GetAccounts.get_accounts_use_map(entries) return accounts_last.keys()","title":"get_accounts()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_accounts_use_map","text":"Gather all the accounts references by a list of directives. Parameters: entries \u2013 A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. Source code in beancount/core/getters.py def get_accounts_use_map(entries): \"\"\"Gather all the accounts references by a list of directives. Args: entries: A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. \"\"\" return _GetAccounts.get_accounts_use_map(entries)","title":"get_accounts_use_map()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_active_years","text":"Yield all the years that have at least one entry in them. Parameters: entries \u2013 A list of directive instances. Yields: Unique dates see in the list of directives. Source code in beancount/core/getters.py def get_active_years(entries): \"\"\"Yield all the years that have at least one entry in them. Args: entries: A list of directive instances. Yields: Unique dates see in the list of directives. \"\"\" seen = set() prev_year = None for entry in entries: year = entry.date.year if year != prev_year: prev_year = year assert year not in seen seen.add(year) yield year","title":"get_active_years()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_all_links","text":"Return a list of all the links seen in the given entries. Parameters: entries \u2013 A list of directive instances. Returns: A set of links strings. Source code in beancount/core/getters.py def get_all_links(entries): \"\"\"Return a list of all the links seen in the given entries. Args: entries: A list of directive instances. Returns: A set of links strings. \"\"\" all_links = set() for entry in entries: if not isinstance(entry, Transaction): continue if entry.links: all_links.update(entry.links) return sorted(all_links)","title":"get_all_links()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_all_payees","text":"Return a list of all the unique payees seen in the given entries. Parameters: entries \u2013 A list of directive instances. Returns: A set of payee strings. Source code in beancount/core/getters.py def get_all_payees(entries): \"\"\"Return a list of all the unique payees seen in the given entries. Args: entries: A list of directive instances. Returns: A set of payee strings. \"\"\" all_payees = set() for entry in entries: if not isinstance(entry, Transaction): continue all_payees.add(entry.payee) all_payees.discard(None) return sorted(all_payees)","title":"get_all_payees()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_all_tags","text":"Return a list of all the tags seen in the given entries. Parameters: entries \u2013 A list of directive instances. Returns: A set of tag strings. Source code in beancount/core/getters.py def get_all_tags(entries): \"\"\"Return a list of all the tags seen in the given entries. Args: entries: A list of directive instances. Returns: A set of tag strings. \"\"\" all_tags = set() for entry in entries: if not isinstance(entry, Transaction): continue if entry.tags: all_tags.update(entry.tags) return sorted(all_tags)","title":"get_all_tags()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_commodity_map","text":"Create map of commodity names to Commodity entries. Parameters: entries \u2013 A list of directive instances. create_missing \u2013 A boolean, true if you want to automatically generate missing commodity directives if not present in the output map. Returns: A map of commodity name strings to Commodity directives. Source code in beancount/core/getters.py def get_commodity_map(entries, create_missing=True): \"\"\"Create map of commodity names to Commodity entries. Args: entries: A list of directive instances. create_missing: A boolean, true if you want to automatically generate missing commodity directives if not present in the output map. Returns: A map of commodity name strings to Commodity directives. \"\"\" if not entries: return {} commodities_map = {} for entry in entries: if isinstance(entry, Commodity): commodities_map[entry.currency] = entry elif isinstance(entry, Transaction): for posting in entry.postings: # Main currency. units = posting.units commodities_map.setdefault(units.currency, None) # Currency in cost. cost = posting.cost if cost: commodities_map.setdefault(cost.currency, None) # Currency in price. price = posting.price if price: commodities_map.setdefault(price.currency, None) elif isinstance(entry, Balance): commodities_map.setdefault(entry.amount.currency, None) elif isinstance(entry, Price): commodities_map.setdefault(entry.currency, None) if create_missing: # Create missing Commodity directives when they haven't been specified explicitly. # (I think it might be better to always do this from the loader.) date = entries[0].date meta = data.new_metadata('', 0) commodities_map = { commodity: (entry if entry is not None else Commodity(meta, date, commodity)) for commodity, entry in commodities_map.items()} return commodities_map","title":"get_commodity_map()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_dict_accounts","text":"Return a nested dict of all the unique leaf names. account names are labelled with LABEL=True Parameters: account_names \u2013 An iterable of account names (strings) Returns: A nested OrderedDict of account leafs Source code in beancount/core/getters.py def get_dict_accounts(account_names): \"\"\"Return a nested dict of all the unique leaf names. account names are labelled with LABEL=True Args: account_names: An iterable of account names (strings) Returns: A nested OrderedDict of account leafs \"\"\" leveldict = OrderedDict() for account_name in account_names: nested_dict = leveldict for component in account.split(account_name): nested_dict = nested_dict.setdefault(component, OrderedDict()) nested_dict[get_dict_accounts.ACCOUNT_LABEL] = True return leveldict","title":"get_dict_accounts()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_entry_accounts","text":"Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Parameters: entries \u2013 A directive instance. Returns: A set of account strings. Source code in beancount/core/getters.py def get_entry_accounts(entry): \"\"\"Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Args: entries: A directive instance. Returns: A set of account strings. \"\"\" return _GetAccounts.get_entry_accounts(entry)","title":"get_entry_accounts()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_leveln_parent_accounts","text":"Return a list of all the unique leaf names at level N in an account hierarchy. Parameters: account_names \u2013 A list of account names (strings) level \u2013 The level to cross-cut. 0 is for root accounts. nrepeats \u2013 A minimum number of times a leaf is required to be present in the the list of unique account names in order to be returned by this function. Returns: A list of leaf node names. Source code in beancount/core/getters.py def get_leveln_parent_accounts(account_names, level, nrepeats=0): \"\"\"Return a list of all the unique leaf names at level N in an account hierarchy. Args: account_names: A list of account names (strings) level: The level to cross-cut. 0 is for root accounts. nrepeats: A minimum number of times a leaf is required to be present in the the list of unique account names in order to be returned by this function. Returns: A list of leaf node names. \"\"\" leveldict = defaultdict(int) for account_name in set(account_names): components = account.split(account_name) if level < len(components): leveldict[components[level]] += 1 levels = {level_ for level_, count in leveldict.items() if count > nrepeats} return sorted(levels)","title":"get_leveln_parent_accounts()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_min_max_dates","text":"Return the minimum and maximum dates in the list of entries. Parameters: entries \u2013 A list of directive instances. types \u2013 An optional tuple of types to restrict the entries to. Returns: A pair of datetime.date dates, the minimum and maximum dates seen in the directives. Source code in beancount/core/getters.py def get_min_max_dates(entries, types=None): \"\"\"Return the minimum and maximum dates in the list of entries. Args: entries: A list of directive instances. types: An optional tuple of types to restrict the entries to. Returns: A pair of datetime.date dates, the minimum and maximum dates seen in the directives. \"\"\" date_first = date_last = None for entry in entries: if types and not isinstance(entry, types): continue date_first = entry.date break for entry in reversed(entries): if types and not isinstance(entry, types): continue date_last = entry.date break return (date_first, date_last)","title":"get_min_max_dates()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_values_meta","text":"Get a map of the metadata from a map of entries values. Given a dict of some key to a directive instance (or None), return a mapping of the key to the metadata extracted from each directive, or a default value. This can be used to gather a particular piece of metadata from an accounts map or a commodities map. Parameters: name_to_entries_map \u2013 A dict of something to an entry or None. meta_keys \u2013 A list of strings, the keys to fetch from the metadata. default \u2013 The default value to use if the metadata is not available or if the value/entry is None. Returns: A mapping of the keys of name_to_entries_map to the values of the 'meta_keys' metadata. If there are multiple 'meta_keys', each value is a tuple of them. On the other hand, if there is only a single one, the value itself is returned. Source code in beancount/core/getters.py def get_values_meta(name_to_entries_map, *meta_keys, default=None): \"\"\"Get a map of the metadata from a map of entries values. Given a dict of some key to a directive instance (or None), return a mapping of the key to the metadata extracted from each directive, or a default value. This can be used to gather a particular piece of metadata from an accounts map or a commodities map. Args: name_to_entries_map: A dict of something to an entry or None. meta_keys: A list of strings, the keys to fetch from the metadata. default: The default value to use if the metadata is not available or if the value/entry is None. Returns: A mapping of the keys of name_to_entries_map to the values of the 'meta_keys' metadata. If there are multiple 'meta_keys', each value is a tuple of them. On the other hand, if there is only a single one, the value itself is returned. \"\"\" value_map = {} for key, entry in name_to_entries_map.items(): value_list = [] for meta_key in meta_keys: value_list.append(entry.meta.get(meta_key, default) if entry is not None else default) value_map[key] = (value_list[0] if len(meta_keys) == 1 else tuple(value_list)) return value_map","title":"get_values_meta()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate","text":"Code used to automatically complete postings without positions.","title":"interpolate"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.BalanceError","text":"BalanceError(source, message, entry)","title":"BalanceError"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.BalanceError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/interpolate.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.BalanceError.__new__","text":"Create new instance of BalanceError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.BalanceError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/interpolate.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.compute_entries_balance","text":"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. Parameters: entries \u2013 A list of directives. prefix \u2013 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 \u2013 A datetime.date instance at which to stop adding up the balance. The date is exclusive. Returns: An instance of Inventory. Source code in beancount/core/interpolate.py 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","title":"compute_entries_balance()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.compute_entry_context","text":"Compute the balances of all accounts referenced by entry up to entry. This provides the inventory of the accounts to which the entry is to be applied, before and after. Parameters: entries \u2013 A list of directives. context_entry \u2013 The entry for which we want to obtain the before and after context. Returns: Two dicts of account-name to Inventory instance, one which represents the context before the entry is applied, and one that represents the context after it has been applied. Source code in beancount/core/interpolate.py def compute_entry_context(entries, context_entry): \"\"\"Compute the balances of all accounts referenced by entry up to entry. This provides the inventory of the accounts to which the entry is to be applied, before and after. Args: entries: A list of directives. context_entry: The entry for which we want to obtain the before and after context. Returns: Two dicts of account-name to Inventory instance, one which represents the context before the entry is applied, and one that represents the context after it has been applied. \"\"\" assert context_entry is not None, \"context_entry is missing.\" # Get the set of accounts for which to compute the context. context_accounts = getters.get_entry_accounts(context_entry) # Iterate over the entries until we find the target one and accumulate the # balance. context_before = collections.defaultdict(inventory.Inventory) for entry in entries: if entry is context_entry: break if isinstance(entry, Transaction): for posting in entry.postings: if not any(posting.account == account for account in context_accounts): continue balance = context_before[posting.account] balance.add_position(posting) # Compute the after context for the entry. context_after = copy.deepcopy(context_before) if isinstance(context_entry, Transaction): for posting in entry.postings: balance = context_after[posting.account] balance.add_position(posting) return context_before, context_after","title":"compute_entry_context()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.compute_residual","text":"Compute the residual of a set of complete postings, and the per-currency precision. This is used to cross-check a balanced transaction. The precision is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. Parameters: postings \u2013 A list of Posting instances. Returns: An instance of Inventory, with the residual of the given list of postings. Source code in beancount/core/interpolate.py def compute_residual(postings): \"\"\"Compute the residual of a set of complete postings, and the per-currency precision. This is used to cross-check a balanced transaction. The precision is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. Args: postings: A list of Posting instances. Returns: An instance of Inventory, with the residual of the given list of postings. \"\"\" inventory = Inventory() for posting in postings: # Skip auto-postings inserted to absorb the residual (rounding error). if posting.meta and posting.meta.get(AUTOMATIC_RESIDUAL, False): continue # Add to total residual balance. inventory.add_amount(convert.get_weight(posting)) return inventory","title":"compute_residual()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.fill_residual_posting","text":"If necessary, insert a posting to absorb the residual. This makes the transaction balance exactly. Note: This was developed in order to tweak transactions before exporting them to Ledger. A better method would be to enable the feature that automatically inserts these rounding postings on all transactions, and so maybe this method can be deprecated if we do so. Parameters: entry \u2013 An instance of a Transaction. account_rounding \u2013 A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A possibly new, modified entry with a new posting. If a residual was not needed - the transaction already balanced perfectly - no new leg is inserted. Source code in beancount/core/interpolate.py def fill_residual_posting(entry, account_rounding): \"\"\"If necessary, insert a posting to absorb the residual. This makes the transaction balance exactly. Note: This was developed in order to tweak transactions before exporting them to Ledger. A better method would be to enable the feature that automatically inserts these rounding postings on all transactions, and so maybe this method can be deprecated if we do so. Args: entry: An instance of a Transaction. account_rounding: A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A possibly new, modified entry with a new posting. If a residual was not needed - the transaction already balanced perfectly - no new leg is inserted. \"\"\" residual = compute_residual(entry.postings) if not residual.is_empty(): new_postings = list(entry.postings) new_postings.extend(get_residual_postings(residual, account_rounding)) entry = entry._replace(postings=new_postings) return entry","title":"fill_residual_posting()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.get_residual_postings","text":"Create postings to book the given residuals. Parameters: residual \u2013 An Inventory, the residual positions. account_rounding \u2013 A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A list of new postings to be inserted to reduce the given residual. Source code in beancount/core/interpolate.py def get_residual_postings(residual, account_rounding): \"\"\"Create postings to book the given residuals. Args: residual: An Inventory, the residual positions. account_rounding: A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A list of new postings to be inserted to reduce the given residual. \"\"\" meta = {AUTOMATIC_META: True, AUTOMATIC_RESIDUAL: True} return [Posting(account_rounding, -position.units, position.cost, None, None, meta.copy()) for position in residual.get_positions()]","title":"get_residual_postings()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.has_nontrivial_balance","text":"Return True if a Posting has a balance amount that would have to be calculated. Parameters: posting \u2013 A Posting instance. Returns: A boolean. Source code in beancount/core/interpolate.py def has_nontrivial_balance(posting): \"\"\"Return True if a Posting has a balance amount that would have to be calculated. Args: posting: A Posting instance. Returns: A boolean. \"\"\" return posting.cost or posting.price","title":"has_nontrivial_balance()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.infer_tolerances","text":"Infer tolerances from a list of postings. The tolerance is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. The 'use_cost' option allows one to experiment with letting postings at cost and at price influence the maximum value of the tolerance. It's tricky to use and alters the definition of the tolerance in a non-trivial way, if you use it. The tolerance is expanded by the sum of the cost times a fraction 'M' of the smallest digits in the number of units for all postings held at cost. For example, in this transaction: 2006-01-17 * \"Plan Contribution\" Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:Cash -1150.00 USD The tolerance for units of USD will calculated as the MAXIMUM of: 0.01 * M = 0.005 (from the 1150.00 USD leg) The sum of 0.001 * M x 30.96 = 0.01548 + 0.001 * M x 30.96 = 0.01548 = 0.03096 So the tolerance for USD in this case is max(0.005, 0.03096) = 0.03096. Prices contribute similarly to the maximum tolerance allowed. Note that 'M' above is the inferred_tolerance_multiplier and its default value is 0.5. Parameters: postings \u2013 A list of Posting instances. options_map \u2013 A dict of options. use_cost \u2013 A boolean, true if we should be using a combination of the smallest digit of the number times the cost or price in order to infer the tolerance. If the value is left unspecified (as 'None'), the default value can be overridden by setting an option. Returns: A dict of currency to the tolerated difference amount to be used for it, e.g. 0.005. Source code in beancount/core/interpolate.py def infer_tolerances(postings, options_map, use_cost=None): \"\"\"Infer tolerances from a list of postings. The tolerance is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. The 'use_cost' option allows one to experiment with letting postings at cost and at price influence the maximum value of the tolerance. It's tricky to use and alters the definition of the tolerance in a non-trivial way, if you use it. The tolerance is expanded by the sum of the cost times a fraction 'M' of the smallest digits in the number of units for all postings held at cost. For example, in this transaction: 2006-01-17 * \"Plan Contribution\" Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:Cash -1150.00 USD The tolerance for units of USD will calculated as the MAXIMUM of: 0.01 * M = 0.005 (from the 1150.00 USD leg) The sum of 0.001 * M x 30.96 = 0.01548 + 0.001 * M x 30.96 = 0.01548 = 0.03096 So the tolerance for USD in this case is max(0.005, 0.03096) = 0.03096. Prices contribute similarly to the maximum tolerance allowed. Note that 'M' above is the inferred_tolerance_multiplier and its default value is 0.5. Args: postings: A list of Posting instances. options_map: A dict of options. use_cost: A boolean, true if we should be using a combination of the smallest digit of the number times the cost or price in order to infer the tolerance. If the value is left unspecified (as 'None'), the default value can be overridden by setting an option. Returns: A dict of currency to the tolerated difference amount to be used for it, e.g. 0.005. \"\"\" if use_cost is None: use_cost = options_map[\"infer_tolerance_from_cost\"] inferred_tolerance_multiplier = options_map[\"inferred_tolerance_multiplier\"] default_tolerances = options_map[\"inferred_tolerance_default\"] tolerances = default_tolerances.copy() cost_tolerances = collections.defaultdict(D) for posting in postings: # Skip the precision on automatically inferred postings. if posting.meta and AUTOMATIC_META in posting.meta: continue units = posting.units if not (isinstance(units, Amount) and isinstance(units.number, Decimal)): continue # Compute bounds on the number. currency = units.currency expo = units.number.as_tuple().exponent if expo < 0: # Note: the exponent is a negative value. tolerance = ONE.scaleb(expo) * inferred_tolerance_multiplier tolerances[currency] = max(tolerance, tolerances.get(currency, -1024)) if not use_cost: continue # Compute bounds on the smallest digit of the number implied as cost. cost = posting.cost if cost is not None: cost_currency = cost.currency if isinstance(cost, Cost): cost_tolerance = min(tolerance * cost.number, MAXIMUM_TOLERANCE) else: assert isinstance(cost, CostSpec) cost_tolerance = MAXIMUM_TOLERANCE for cost_number in cost.number_total, cost.number_per: if cost_number is None or cost_number is MISSING: continue cost_tolerance = min(tolerance * cost_number, cost_tolerance) cost_tolerances[cost_currency] += cost_tolerance # Compute bounds on the smallest digit of the number implied as cost. price = posting.price if isinstance(price, Amount) and isinstance(price.number, Decimal): price_currency = price.currency price_tolerance = min(tolerance * price.number, MAXIMUM_TOLERANCE) cost_tolerances[price_currency] += price_tolerance for currency, tolerance in cost_tolerances.items(): tolerances[currency] = max(tolerance, tolerances.get(currency, -1024)) default = tolerances.pop('*', ZERO) return defdict.ImmutableDictWithDefault(tolerances, default=default)","title":"infer_tolerances()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.is_tolerance_user_specified","text":"Return true if the given tolerance number was user-specified. This would allow the user to provide a tolerance like # 0.1234 but not 0.123456. This is used to detect whether a tolerance value # is input by the user and not inferred automatically. Parameters: tolerance \u2013 An instance of Decimal. Returns: A boolean. Source code in beancount/core/interpolate.py def is_tolerance_user_specified(tolerance): \"\"\"Return true if the given tolerance number was user-specified. This would allow the user to provide a tolerance like # 0.1234 but not 0.123456. This is used to detect whether a tolerance value # is input by the user and not inferred automatically. Args: tolerance: An instance of Decimal. Returns: A boolean. \"\"\" return len(tolerance.as_tuple().digits) < MAX_TOLERANCE_DIGITS","title":"is_tolerance_user_specified()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.quantize_with_tolerance","text":"Quantize the units using the tolerance dict. Parameters: tolerances \u2013 A dict of currency to tolerance Decimalvalues. number \u2013 A number to quantize. currency \u2013 A string currency. Returns: A Decimal, the number possibly quantized. Source code in beancount/core/interpolate.py def quantize_with_tolerance(tolerances, currency, number): \"\"\"Quantize the units using the tolerance dict. Args: tolerances: A dict of currency to tolerance Decimalvalues. number: A number to quantize. currency: A string currency. Returns: A Decimal, the number possibly quantized. \"\"\" # Applying rounding to the default tolerance, if there is one. tolerance = tolerances.get(currency) if tolerance: quantum = (tolerance * 2).normalize() # If the tolerance is a neat number provided by the user, # quantize the inferred numbers. See doc on quantize(): # # Unlike other operations, if the length of the coefficient # after the quantize operation would be greater than # precision, then an InvalidOperation is signaled. This # guarantees that, unless there is an error condition, the # quantized exponent is always equal to that of the # right-hand operand. if is_tolerance_user_specified(quantum): number = number.quantize(quantum) return number","title":"quantize_with_tolerance()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory","text":"A container for an inventory of positions. This module provides a container class that can hold positions. An inventory is a mapping of positions, where each position is keyed by (currency: str, cost: Cost) -> position: Position where 'currency': The commodity under consideration, USD, CAD, or stock units such as HOOL, MSFT, AAPL, etc.; 'cost': None or a Cost instance existing of cost currency, number, date, and label; 'position': A Position object, whose 'units' attribute is guaranteed to have the same currency as 'currency' and whose 'cost' attribute is equal to the 'cost' key. It basically stores the number of units. This is meant to accommodate both booked and non-booked amounts. The clever trick that we pull to do this is that for positions which aren't booked, we simply leave the 'cost' as None. This is the case for most of the transactions.","title":"inventory"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Booking","text":"Result of booking a new lot to an existing inventory.","title":"Booking"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory","text":"An Inventory is a set of positions. Attributes: Name Type Description positions A list of Position instances, held in this Inventory object.","title":"Inventory"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__abs__","text":"Return an inventory with the absolute value of each position. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def __abs__(self): \"\"\"Return an inventory with the absolute value of each position. Returns: An instance of Inventory. \"\"\" return Inventory({key: abs(pos) for key, pos in self.items()})","title":"__abs__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__add__","text":"Add another inventory to this one. This inventory is not modified. Parameters: other \u2013 An instance of Inventory. Returns: A new instance of Inventory. Source code in beancount/core/inventory.py def __add__(self, other): \"\"\"Add another inventory to this one. This inventory is not modified. Args: other: An instance of Inventory. Returns: A new instance of Inventory. \"\"\" new_inventory = self.__copy__() new_inventory.add_inventory(other) return new_inventory","title":"__add__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__copy__","text":"A shallow copy of this inventory object. Returns: An instance of Inventory, equal to this one. Source code in beancount/core/inventory.py def __copy__(self): \"\"\"A shallow copy of this inventory object. Returns: An instance of Inventory, equal to this one. \"\"\" return Inventory(self)","title":"__copy__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__iadd__","text":"Add all the positions of another Inventory instance to this one. Parameters: other \u2013 An instance of Inventory to add to this one. Returns: This inventory, modified. Source code in beancount/core/inventory.py def add_inventory(self, other): \"\"\"Add all the positions of another Inventory instance to this one. Args: other: An instance of Inventory to add to this one. Returns: This inventory, modified. \"\"\" if self.is_empty(): # Optimization for empty inventories; if the current one is empty, # adopt all of the other inventory's positions without running # through the full aggregation checks. This should be very cheap. We # can do this because the positions are immutable. self.update(other) else: for position in other.get_positions(): self.add_position(position) return self","title":"__iadd__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__init__","text":"Create a new inventory using a list of existing positions. Parameters: positions \u2013 A list of Position instances or an existing dict or Inventory instance. Source code in beancount/core/inventory.py def __init__(self, positions=None): \"\"\"Create a new inventory using a list of existing positions. Args: positions: A list of Position instances or an existing dict or Inventory instance. \"\"\" if isinstance(positions, (dict, Inventory)): dict.__init__(self, positions) else: dict.__init__(self) if positions: assert isinstance(positions, Iterable) for position in positions: self.add_position(position)","title":"__init__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__iter__","text":"Iterate over the positions. Note that there is no guaranteed order. Source code in beancount/core/inventory.py def __iter__(self): \"\"\"Iterate over the positions. Note that there is no guaranteed order.\"\"\" return iter(self.values())","title":"__iter__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__lt__","text":"Inequality comparison operator. Source code in beancount/core/inventory.py def __lt__(self, other): \"\"\"Inequality comparison operator.\"\"\" return sorted(self) < sorted(other)","title":"__lt__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__mul__","text":"Scale/multiply the contents of the inventory. Parameters: scalar \u2013 A Decimal. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def __mul__(self, scalar): \"\"\"Scale/multiply the contents of the inventory. Args: scalar: A Decimal. Returns: An instance of Inventory. \"\"\" return Inventory({key: pos * scalar for key, pos in self.items()})","title":"__mul__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__neg__","text":"Return an inventory with the negative of values of this one. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def __neg__(self): \"\"\"Return an inventory with the negative of values of this one. Returns: An instance of Inventory. \"\"\" return Inventory({key: -pos for key, pos in self.items()})","title":"__neg__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__repr__","text":"Render as a human-readable string. Returns: A string, for human consumption. Source code in beancount/core/inventory.py def __str__(self): \"\"\"Render as a human-readable string. Returns: A string, for human consumption. \"\"\" return self.to_string()","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__str__","text":"Render as a human-readable string. Returns: A string, for human consumption. Source code in beancount/core/inventory.py def __str__(self): \"\"\"Render as a human-readable string. Returns: A string, for human consumption. \"\"\" return self.to_string()","title":"__str__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.add_amount","text":"Add to this inventory using amount and cost. This adds with strict lot matching, that is, no partial matches are done on the arguments to the keys of the inventory. Parameters: units \u2013 An Amount instance to add. cost \u2013 An instance of Cost or None, as a key to the inventory. Returns: A pair of (position, booking) where 'position' is the position that that was modified BEFORE it was modified, and where 'booking' is a Booking enum that hints at how the lot was booked to this inventory. Position may be None if there is no corresponding Position object, e.g. the position was deleted. Source code in beancount/core/inventory.py def add_amount(self, units, cost=None): \"\"\"Add to this inventory using amount and cost. This adds with strict lot matching, that is, no partial matches are done on the arguments to the keys of the inventory. Args: units: An Amount instance to add. cost: An instance of Cost or None, as a key to the inventory. Returns: A pair of (position, booking) where 'position' is the position that that was modified BEFORE it was modified, and where 'booking' is a Booking enum that hints at how the lot was booked to this inventory. Position may be None if there is no corresponding Position object, e.g. the position was deleted. \"\"\" if ASSERTS_TYPES: assert isinstance(units, Amount), ( \"Internal error: {!r} (type: {})\".format(units, type(units).__name__)) assert cost is None or isinstance(cost, Cost), ( \"Internal error: {!r} (type: {})\".format(cost, type(cost).__name__)) # Find the position. key = (units.currency, cost) pos = self.get(key, None) if pos is not None: # Note: In order to augment or reduce, all the fields have to match. # Check if reducing. booking = (Booking.REDUCED if not same_sign(pos.units.number, units.number) else Booking.AUGMENTED) # Compute the new number of units. number = pos.units.number + units.number if number == ZERO: # If empty, delete the position. del self[key] else: # Otherwise update it. self[key] = Position(Amount(number, units.currency), cost) else: # If not found, create a new one. if units.number == ZERO: booking = Booking.IGNORED else: self[key] = Position(units, cost) booking = Booking.CREATED return pos, booking","title":"add_amount()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.add_inventory","text":"Add all the positions of another Inventory instance to this one. Parameters: other \u2013 An instance of Inventory to add to this one. Returns: This inventory, modified. Source code in beancount/core/inventory.py def add_inventory(self, other): \"\"\"Add all the positions of another Inventory instance to this one. Args: other: An instance of Inventory to add to this one. Returns: This inventory, modified. \"\"\" if self.is_empty(): # Optimization for empty inventories; if the current one is empty, # adopt all of the other inventory's positions without running # through the full aggregation checks. This should be very cheap. We # can do this because the positions are immutable. self.update(other) else: for position in other.get_positions(): self.add_position(position) return self","title":"add_inventory()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.add_position","text":"Add using a position (with strict lot matching). Return True if this position was booked against and reduced another. Parameters: position \u2013 The Posting or Position to add to this inventory. Returns: A pair of (position, booking) where 'position' is the position that that was modified, and where 'booking' is a Booking enum that hints at how the lot was booked to this inventory. Source code in beancount/core/inventory.py def add_position(self, position): \"\"\"Add using a position (with strict lot matching). Return True if this position was booked against and reduced another. Args: position: The Posting or Position to add to this inventory. Returns: A pair of (position, booking) where 'position' is the position that that was modified, and where 'booking' is a Booking enum that hints at how the lot was booked to this inventory. \"\"\" if ASSERTS_TYPES: assert hasattr(position, 'units') and hasattr(position, 'cost'), ( \"Invalid type for position: {}\".format(position)) assert isinstance(position.cost, (type(None), Cost)), ( \"Invalid type for cost: {}\".format(position.cost)) return self.add_amount(position.units, position.cost)","title":"add_position()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.average","text":"Average all lots of the same currency together. Use the minimum date from each aggregated set of lots. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def average(self): \"\"\"Average all lots of the same currency together. Use the minimum date from each aggregated set of lots. Returns: An instance of Inventory. \"\"\" groups = collections.defaultdict(list) for position in self: key = (position.units.currency, position.cost.currency if position.cost else None) groups[key].append(position) average_inventory = Inventory() for (currency, cost_currency), positions in groups.items(): total_units = sum(position.units.number for position in positions) # Explicitly skip aggregates when resulting in zero units. if total_units == ZERO: continue units_amount = Amount(total_units, currency) if cost_currency: total_cost = sum(convert.get_cost(position).number for position in positions) cost_number = (Decimal('Infinity') if total_units == ZERO else (total_cost / total_units)) min_date = None for pos in positions: pos_date = pos.cost.date if pos.cost else None if pos_date is not None: min_date = (pos_date if min_date is None else min(min_date, pos_date)) cost = Cost(cost_number, cost_currency, min_date, None) else: cost = None average_inventory.add_amount(units_amount, cost) return average_inventory","title":"average()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.cost_currencies","text":"Return the list of unit currencies held in this inventory. Returns: A set of currency strings. Source code in beancount/core/inventory.py def cost_currencies(self): \"\"\"Return the list of unit currencies held in this inventory. Returns: A set of currency strings. \"\"\" return set(cost.currency for _, cost in self.keys() if cost is not None)","title":"cost_currencies()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.currencies","text":"Return the list of unit currencies held in this inventory. Returns: A list of currency strings. Source code in beancount/core/inventory.py def currencies(self): \"\"\"Return the list of unit currencies held in this inventory. Returns: A list of currency strings. \"\"\" return set(currency for currency, _ in self.keys())","title":"currencies()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.currency_pairs","text":"Return the commodities held in this inventory. Returns: A set of currency strings. Source code in beancount/core/inventory.py def currency_pairs(self): \"\"\"Return the commodities held in this inventory. Returns: A set of currency strings. \"\"\" return set(position.currency_pair() for position in self)","title":"currency_pairs()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.from_string","text":"Create an Inventory from a string. This is useful for writing tests. Parameters: string \u2013 A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. Source code in beancount/core/inventory.py @staticmethod def from_string(string): \"\"\"Create an Inventory from a string. This is useful for writing tests. Args: string: A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. \"\"\" new_inventory = Inventory() # We need to split the comma-separated positions but ignore commas # occurring within a {...cost...} specification. position_strs = re.split( r'([-+]?[0-9,.]+\\s+[A-Z]+\\s*(?:{[^}]*})?)\\s*,?\\s*', string)[1::2] for position_str in position_strs: new_inventory.add_position(position_from_string(position_str)) return new_inventory","title":"from_string()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.get_currency_units","text":"Fetch the total amount across all the position in the given currency. This may sum multiple lots in the same currency denomination. Parameters: currency \u2013 A string, the currency to filter the positions with. Returns: An instance of Amount, with the given currency. Source code in beancount/core/inventory.py def get_currency_units(self, currency): \"\"\"Fetch the total amount across all the position in the given currency. This may sum multiple lots in the same currency denomination. Args: currency: A string, the currency to filter the positions with. Returns: An instance of Amount, with the given currency. \"\"\" total_units = ZERO for position in self: if position.units.currency == currency: total_units += position.units.number return Amount(total_units, currency)","title":"get_currency_units()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.get_only_position","text":"Return the first position and assert there are no more. If the inventory is empty, return None. Source code in beancount/core/inventory.py def get_only_position(self): \"\"\"Return the first position and assert there are no more. If the inventory is empty, return None. \"\"\" if len(self) > 0: if len(self) > 1: raise AssertionError(\"Inventory has more than one expected \" \"position: {}\".format(self)) return next(iter(self))","title":"get_only_position()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.get_positions","text":"Return the positions in this inventory. Returns: A shallow copy of the list of positions. Source code in beancount/core/inventory.py def get_positions(self): \"\"\"Return the positions in this inventory. Returns: A shallow copy of the list of positions. \"\"\" return list(iter(self))","title":"get_positions()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.is_empty","text":"Return true if the inventory is empty, that is, has no positions. Returns: A boolean. Source code in beancount/core/inventory.py def is_empty(self): \"\"\"Return true if the inventory is empty, that is, has no positions. Returns: A boolean. \"\"\" return len(self) == 0","title":"is_empty()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.is_mixed","text":"Return true if the inventory contains a mix of positive and negative lots for at least one instrument. Returns: A boolean. Source code in beancount/core/inventory.py def is_mixed(self): \"\"\"Return true if the inventory contains a mix of positive and negative lots for at least one instrument. Returns: A boolean. \"\"\" signs_map = {} for position in self: sign = position.units.number >= 0 prev_sign = signs_map.setdefault(position.units.currency, sign) if sign != prev_sign: return True return False","title":"is_mixed()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.is_reduced_by","text":"Return true if the amount could reduce this inventory. Parameters: ramount \u2013 An instance of Amount. Returns: A boolean. Source code in beancount/core/inventory.py def is_reduced_by(self, ramount): \"\"\"Return true if the amount could reduce this inventory. Args: ramount: An instance of Amount. Returns: A boolean. \"\"\" if ramount.number == ZERO: return False for position in self: units = position.units if (ramount.currency == units.currency and not same_sign(ramount.number, units.number)): return True return False","title":"is_reduced_by()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.is_small","text":"Return true if all the positions in the inventory are small. Parameters: tolerances \u2013 A Decimal, the small number of units under which a position is considered small, or a dict of currency to such epsilon precision. Returns: A boolean. Source code in beancount/core/inventory.py def is_small(self, tolerances): \"\"\"Return true if all the positions in the inventory are small. Args: tolerances: A Decimal, the small number of units under which a position is considered small, or a dict of currency to such epsilon precision. Returns: A boolean. \"\"\" if isinstance(tolerances, dict): for position in self: tolerance = tolerances.get(position.units.currency, ZERO) if abs(position.units.number) > tolerance: return False small = True else: small = not any(abs(position.units.number) > tolerances for position in self) return small","title":"is_small()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.reduce","text":"Reduce an inventory using one of the conversion functions. See functions in beancount.core.convert. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def reduce(self, reducer, *args): \"\"\"Reduce an inventory using one of the conversion functions. See functions in beancount.core.convert. Returns: An instance of Inventory. \"\"\" inventory = Inventory() for position in self: inventory.add_amount(reducer(position, *args)) return inventory","title":"reduce()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.segregate_units","text":"Split up the list of positions to the given currencies. Parameters: currencies \u2013 A list of currency strings, the currencies to isolate. Returns: A dict of currency to Inventory instances. Source code in beancount/core/inventory.py def segregate_units(self, currencies): \"\"\"Split up the list of positions to the given currencies. Args: currencies: A list of currency strings, the currencies to isolate. Returns: A dict of currency to Inventory instances. \"\"\" per_currency_dict = {currency: Inventory() for currency in currencies} per_currency_dict[None] = Inventory() for position in self: currency = position.units.currency key = (currency if currency in currencies else None) per_currency_dict[key].add_position(position) return per_currency_dict","title":"segregate_units()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.to_string","text":"Convert an Inventory instance to a printable string. Parameters: dformat \u2013 An instance of DisplayFormatter. parents \u2013 A boolean, true if we should surround the results by parentheses. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/inventory.py def to_string(self, dformat=DEFAULT_FORMATTER, parens=True): \"\"\"Convert an Inventory instance to a printable string. Args: dformat: An instance of DisplayFormatter. parents: A boolean, true if we should surround the results by parentheses. Returns: A formatted string of the quantized amount and symbol. \"\"\" fmt = '({})' if parens else '{}' return fmt.format( ', '.join(pos.to_string(dformat) for pos in sorted(self)))","title":"to_string()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.check_invariants","text":"Check the invariants of the Inventory. Parameters: inventory \u2013 An instance of Inventory. Returns: True if the invariants are respected. Source code in beancount/core/inventory.py def check_invariants(inv): \"\"\"Check the invariants of the Inventory. Args: inventory: An instance of Inventory. Returns: True if the invariants are respected. \"\"\" # Check that all the keys are unique. lots = set((pos.units.currency, pos.cost) for pos in inv) assert len(lots) == len(inv), \"Invalid inventory: {}\".format(inv) # Check that none of the amounts is zero. for pos in inv: assert pos.units.number != ZERO, \"Invalid position size: {}\".format(pos)","title":"check_invariants()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.from_string","text":"Create an Inventory from a string. This is useful for writing tests. Parameters: string \u2013 A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. Source code in beancount/core/inventory.py @staticmethod def from_string(string): \"\"\"Create an Inventory from a string. This is useful for writing tests. Args: string: A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. \"\"\" new_inventory = Inventory() # We need to split the comma-separated positions but ignore commas # occurring within a {...cost...} specification. position_strs = re.split( r'([-+]?[0-9,.]+\\s+[A-Z]+\\s*(?:{[^}]*})?)\\s*,?\\s*', string)[1::2] for position_str in position_strs: new_inventory.add_position(position_from_string(position_str)) return new_inventory","title":"from_string()"},{"location":"api_reference/beancount.core.html#beancount.core.number","text":"The module contains the basic Decimal type import. About Decimal usage: Do not import Decimal from the 'decimal' or 'cdecimal' modules; always import your Decimal class from beancount.core.amount. Prefer to use D() to create new instances of Decimal objects, which handles more syntax, e.g., handles None, and numbers with commas.","title":"number"},{"location":"api_reference/beancount.core.html#beancount.core.number.D","text":"Convert a string into a Decimal object. This is used in parsing amounts from files in the importers. This is the main function you should use to build all numbers the system manipulates (never use floating-point in an accounting system). Commas are stripped and ignored, as they are assumed to be thousands separators (the French comma separator as decimal is not supported). This function just returns the argument if it is already a Decimal object, for convenience. Parameters: strord \u2013 A string or Decimal instance. Returns: A Decimal instance. Source code in beancount/core/number.py def D(strord=None): \"\"\"Convert a string into a Decimal object. This is used in parsing amounts from files in the importers. This is the main function you should use to build all numbers the system manipulates (never use floating-point in an accounting system). Commas are stripped and ignored, as they are assumed to be thousands separators (the French comma separator as decimal is not supported). This function just returns the argument if it is already a Decimal object, for convenience. Args: strord: A string or Decimal instance. Returns: A Decimal instance. \"\"\" try: # Note: try a map lookup and optimize performance here. if strord is None or strord == '': return Decimal() elif isinstance(strord, str): return Decimal(_CLEAN_NUMBER_RE.sub('', strord)) elif isinstance(strord, Decimal): return strord elif isinstance(strord, (int, float)): return Decimal(strord) else: assert strord is None, \"Invalid value to convert: {}\".format(strord) except Exception as exc: raise ValueError(\"Impossible to create Decimal instance from {!s}: {}\".format( strord, exc))","title":"D()"},{"location":"api_reference/beancount.core.html#beancount.core.number.is_fast_decimal","text":"Return true if a fast C decimal implementation is installed. Source code in beancount/core/number.py def is_fast_decimal(decimal_module): \"Return true if a fast C decimal implementation is installed.\" return isinstance(decimal_module.Decimal().sqrt, types.BuiltinFunctionType)","title":"is_fast_decimal()"},{"location":"api_reference/beancount.core.html#beancount.core.number.round_to","text":"Round a number down to a particular increment. Parameters: number \u2013 A Decimal, the number to be rounded. increment \u2013 A Decimal, the size of the increment. Returns: A Decimal, the rounded number. Source code in beancount/core/number.py def round_to(number, increment): \"\"\"Round a number *down* to a particular increment. Args: number: A Decimal, the number to be rounded. increment: A Decimal, the size of the increment. Returns: A Decimal, the rounded number. \"\"\" return int((number / increment)) * increment","title":"round_to()"},{"location":"api_reference/beancount.core.html#beancount.core.number.same_sign","text":"Return true if both numbers have the same sign. Parameters: number1 \u2013 An instance of Decimal. number2 \u2013 An instance of Decimal. Returns: A boolean. Source code in beancount/core/number.py def same_sign(number1, number2): \"\"\"Return true if both numbers have the same sign. Args: number1: An instance of Decimal. number2: An instance of Decimal. Returns: A boolean. \"\"\" return (number1 >= 0) == (number2 >= 0)","title":"same_sign()"},{"location":"api_reference/beancount.core.html#beancount.core.position","text":"A position object, which consists of units Amount and cost Cost. See types below for details.","title":"position"},{"location":"api_reference/beancount.core.html#beancount.core.position.Cost","text":"Cost(number, currency, date, label)","title":"Cost"},{"location":"api_reference/beancount.core.html#beancount.core.position.Cost.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/position.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Cost.__new__","text":"Create new instance of Cost(number, currency, date, label)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Cost.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/position.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.CostSpec","text":"CostSpec(number_per, number_total, currency, date, label, merge)","title":"CostSpec"},{"location":"api_reference/beancount.core.html#beancount.core.position.CostSpec.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/position.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.CostSpec.__new__","text":"Create new instance of CostSpec(number_per, number_total, currency, date, label, merge)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.CostSpec.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/position.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position","text":"A 'Position' is a pair of units and optional cost. This is used to track inventories. Attributes: Name Type Description units Amount An Amount, the number of units and its currency. cost Cost A Cost that represents the lot, or None.","title":"Position"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__abs__","text":"Return the absolute value of the position. Returns: An instance of Position with the absolute units. Source code in beancount/core/position.py def __abs__(self): \"\"\"Return the absolute value of the position. Returns: An instance of Position with the absolute units. \"\"\" return Position(amount_abs(self.units), self.cost)","title":"__abs__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__copy__","text":"Shallow copy, except for the lot, which can be shared. This is important for performance reasons; a lot of time is spent here during balancing. Returns: A shallow copy of this position. Source code in beancount/core/position.py def __copy__(self): \"\"\"Shallow copy, except for the lot, which can be shared. This is important for performance reasons; a lot of time is spent here during balancing. Returns: A shallow copy of this position. \"\"\" # Note: We use Decimal() for efficiency. return Position(copy.copy(self.units), copy.copy(self.cost))","title":"__copy__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__eq__","text":"Equality comparison with another Position. The objects are considered equal if both number and lot are matching, and if the number of units is zero and the other position is None, that is also okay. Parameters: other \u2013 An instance of Position, or None. Returns: A boolean, true if the positions are equal. Source code in beancount/core/position.py def __eq__(self, other): \"\"\"Equality comparison with another Position. The objects are considered equal if both number and lot are matching, and if the number of units is zero and the other position is None, that is also okay. Args: other: An instance of Position, or None. Returns: A boolean, true if the positions are equal. \"\"\" return (self.units.number == ZERO if other is None else (self.units == other.units and self.cost == other.cost))","title":"__eq__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__hash__","text":"Compute a hash for this position. Returns: A hash of this position object. Source code in beancount/core/position.py def __hash__(self): \"\"\"Compute a hash for this position. Returns: A hash of this position object. \"\"\" return hash((self.units, self.cost))","title":"__hash__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__lt__","text":"A less-than comparison operator for positions. Parameters: other \u2013 Another instance of Position. Returns: True if this positions is smaller than the other position. Source code in beancount/core/position.py def __lt__(self, other): \"\"\"A less-than comparison operator for positions. Args: other: Another instance of Position. Returns: True if this positions is smaller than the other position. \"\"\" return self.sortkey() < other.sortkey()","title":"__lt__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__mul__","text":"Scale/multiply the contents of the position. Parameters: scalar \u2013 A Decimal. Returns: An instance of Inventory. Source code in beancount/core/position.py def __mul__(self, scalar): \"\"\"Scale/multiply the contents of the position. Args: scalar: A Decimal. Returns: An instance of Inventory. \"\"\" return Position(amount_mul(self.units, scalar), self.cost)","title":"__mul__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__neg__","text":"Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. Source code in beancount/core/position.py def get_negative(self): \"\"\"Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. \"\"\" # Note: We use Decimal() for efficiency. return Position(-self.units, self.cost)","title":"__neg__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__new__","text":"Create new instance of _Position(units, cost) Source code in beancount/core/position.py def __new__(cls, units, cost=None): assert isinstance(units, Amount), ( \"Expected an Amount for units; received '{}'\".format(units)) assert cost is None or isinstance(cost, Position.cost_types), ( \"Expected a Cost for cost; received '{}'\".format(cost)) return _Position.__new__(cls, units, cost)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__repr__","text":"Return a string representation of the position. Returns: A string, a printable representation of the position. Source code in beancount/core/position.py def __str__(self): \"\"\"Return a string representation of the position. Returns: A string, a printable representation of the position. \"\"\" return self.to_string()","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__str__","text":"Return a string representation of the position. Returns: A string, a printable representation of the position. Source code in beancount/core/position.py def __str__(self): \"\"\"Return a string representation of the position. Returns: A string, a printable representation of the position. \"\"\" return self.to_string()","title":"__str__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.currency_pair","text":"Return the currency pair associated with this position. Returns: A pair of a currency string and a cost currency string or None. Source code in beancount/core/position.py def currency_pair(self): \"\"\"Return the currency pair associated with this position. Returns: A pair of a currency string and a cost currency string or None. \"\"\" return (self.units.currency, self.cost.currency if self.cost else None)","title":"currency_pair()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.from_amounts","text":"Create a position from an amount and a cost. Parameters: amount \u2013 An amount, that represents the number of units and the lot currency. cost_amount \u2013 If not None, represents the cost amount. Returns: A Position instance. Source code in beancount/core/position.py @staticmethod def from_amounts(units, cost_amount=None): \"\"\"Create a position from an amount and a cost. Args: amount: An amount, that represents the number of units and the lot currency. cost_amount: If not None, represents the cost amount. Returns: A Position instance. \"\"\" assert cost_amount is None or isinstance(cost_amount, Amount), ( \"Invalid type for cost: {}\".format(cost_amount)) cost = (Cost(cost_amount.number, cost_amount.currency, None, None) if cost_amount else None) return Position(units, cost)","title":"from_amounts()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.from_string","text":"Create a position from a string specification. This is a miniature parser used for building tests. Parameters: string \u2013 A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. Source code in beancount/core/position.py @staticmethod def from_string(string): \"\"\"Create a position from a string specification. This is a miniature parser used for building tests. Args: string: A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. \"\"\" match = re.match( (r'\\s*({})\\s+({})' r'(?:\\s+{{([^}}]*)}})?' r'\\s*$').format(NUMBER_RE, CURRENCY_RE), string) if not match: raise ValueError(\"Invalid string for position: '{}'\".format(string)) number = D(match.group(1)) currency = match.group(2) # Parse a cost expression. cost_number = None cost_currency = None date = None label = None cost_expression = match.group(3) if match.group(3): expressions = [expr.strip() for expr in re.split('[,/]', cost_expression)] for expr in expressions: # Match a compound number. match = re.match( r'({NUMBER_RE})\\s*(?:#\\s*({NUMBER_RE}))?\\s+({CURRENCY_RE})$' .format(NUMBER_RE=NUMBER_RE, CURRENCY_RE=CURRENCY_RE), expr ) if match: per_number, total_number, cost_currency = match.group(1, 2, 3) per_number = D(per_number) if per_number else ZERO total_number = D(total_number) if total_number else ZERO if total_number: # Calculate the per-unit cost. total = number * per_number + total_number per_number = total / number cost_number = per_number continue # Match a date. match = re.match(r'(\\d\\d\\d\\d)[-/](\\d\\d)[-/](\\d\\d)$', expr) if match: date = datetime.date(*map(int, match.group(1, 2, 3))) continue # Match a label. match = re.match(r'\"([^\"]+)*\"$', expr) if match: label = match.group(1) continue # Match a merge-cost marker. match = re.match(r'\\*$', expr) if match: raise ValueError(\"Merge-code not supported in string constructor.\") raise ValueError(\"Invalid cost component: '{}'\".format(expr)) cost = Cost(cost_number, cost_currency, date, label) else: cost = None return Position(Amount(number, currency), cost)","title":"from_string()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.get_negative","text":"Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. Source code in beancount/core/position.py def get_negative(self): \"\"\"Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. \"\"\" # Note: We use Decimal() for efficiency. return Position(-self.units, self.cost)","title":"get_negative()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.is_negative_at_cost","text":"Return true if the position is held at cost and negative. Returns: A boolean. Source code in beancount/core/position.py def is_negative_at_cost(self): \"\"\"Return true if the position is held at cost and negative. Returns: A boolean. \"\"\" return (self.units.number < ZERO and self.cost is not None)","title":"is_negative_at_cost()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.sortkey","text":"Return a key to sort positions by. This key depends on the order of the currency of the lot (we want to order common currencies first) and the number of units. Returns: A tuple, used to sort lists of positions. Source code in beancount/core/position.py def sortkey(self): \"\"\"Return a key to sort positions by. This key depends on the order of the currency of the lot (we want to order common currencies first) and the number of units. Returns: A tuple, used to sort lists of positions. \"\"\" currency = self.units.currency order_units = CURRENCY_ORDER.get(currency, NCURRENCIES + len(currency)) if self.cost is not None: cost_number = self.cost.number cost_currency = self.cost.currency else: cost_number = ZERO cost_currency = '' return (order_units, cost_number, cost_currency, self.units.number)","title":"sortkey()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.to_string","text":"Render the position to a string.See to_string() for details. Source code in beancount/core/position.py def to_string(self, dformat=DEFAULT_FORMATTER, detail=True): \"\"\"Render the position to a string.See to_string() for details. \"\"\" return to_string(self, dformat, detail)","title":"to_string()"},{"location":"api_reference/beancount.core.html#beancount.core.position.cost_to_str","text":"Format an instance of Cost or a CostSpec to a string. Parameters: cost \u2013 An instance of Cost or CostSpec. dformat \u2013 A DisplayFormatter object. detail \u2013 A boolean, true if we should render the non-amount components. Returns: A string, suitable for formatting. Source code in beancount/core/position.py def cost_to_str(cost, dformat, detail=True): \"\"\"Format an instance of Cost or a CostSpec to a string. Args: cost: An instance of Cost or CostSpec. dformat: A DisplayFormatter object. detail: A boolean, true if we should render the non-amount components. Returns: A string, suitable for formatting. \"\"\" strlist = [] if isinstance(cost, Cost): if isinstance(cost.number, Decimal): strlist.append(Amount(cost.number, cost.currency).to_string(dformat)) if detail: if cost.date: strlist.append(cost.date.isoformat()) if cost.label: strlist.append('\"{}\"'.format(cost.label)) elif isinstance(cost, CostSpec): if isinstance(cost.number_per, Decimal) or isinstance(cost.number_total, Decimal): amountlist = [] if isinstance(cost.number_per, Decimal): amountlist.append(dformat.format(cost.number_per)) if isinstance(cost.number_total, Decimal): amountlist.append('#') amountlist.append(dformat.format(cost.number_total)) if isinstance(cost.currency, str): amountlist.append(cost.currency) strlist.append(' '.join(amountlist)) if detail: if cost.date: strlist.append(cost.date.isoformat()) if cost.label: strlist.append('\"{}\"'.format(cost.label)) if cost.merge: strlist.append('*') return ', '.join(strlist)","title":"cost_to_str()"},{"location":"api_reference/beancount.core.html#beancount.core.position.from_amounts","text":"Create a position from an amount and a cost. Parameters: amount \u2013 An amount, that represents the number of units and the lot currency. cost_amount \u2013 If not None, represents the cost amount. Returns: A Position instance. Source code in beancount/core/position.py @staticmethod def from_amounts(units, cost_amount=None): \"\"\"Create a position from an amount and a cost. Args: amount: An amount, that represents the number of units and the lot currency. cost_amount: If not None, represents the cost amount. Returns: A Position instance. \"\"\" assert cost_amount is None or isinstance(cost_amount, Amount), ( \"Invalid type for cost: {}\".format(cost_amount)) cost = (Cost(cost_amount.number, cost_amount.currency, None, None) if cost_amount else None) return Position(units, cost)","title":"from_amounts()"},{"location":"api_reference/beancount.core.html#beancount.core.position.from_string","text":"Create a position from a string specification. This is a miniature parser used for building tests. Parameters: string \u2013 A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. Source code in beancount/core/position.py @staticmethod def from_string(string): \"\"\"Create a position from a string specification. This is a miniature parser used for building tests. Args: string: A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. \"\"\" match = re.match( (r'\\s*({})\\s+({})' r'(?:\\s+{{([^}}]*)}})?' r'\\s*$').format(NUMBER_RE, CURRENCY_RE), string) if not match: raise ValueError(\"Invalid string for position: '{}'\".format(string)) number = D(match.group(1)) currency = match.group(2) # Parse a cost expression. cost_number = None cost_currency = None date = None label = None cost_expression = match.group(3) if match.group(3): expressions = [expr.strip() for expr in re.split('[,/]', cost_expression)] for expr in expressions: # Match a compound number. match = re.match( r'({NUMBER_RE})\\s*(?:#\\s*({NUMBER_RE}))?\\s+({CURRENCY_RE})$' .format(NUMBER_RE=NUMBER_RE, CURRENCY_RE=CURRENCY_RE), expr ) if match: per_number, total_number, cost_currency = match.group(1, 2, 3) per_number = D(per_number) if per_number else ZERO total_number = D(total_number) if total_number else ZERO if total_number: # Calculate the per-unit cost. total = number * per_number + total_number per_number = total / number cost_number = per_number continue # Match a date. match = re.match(r'(\\d\\d\\d\\d)[-/](\\d\\d)[-/](\\d\\d)$', expr) if match: date = datetime.date(*map(int, match.group(1, 2, 3))) continue # Match a label. match = re.match(r'\"([^\"]+)*\"$', expr) if match: label = match.group(1) continue # Match a merge-cost marker. match = re.match(r'\\*$', expr) if match: raise ValueError(\"Merge-code not supported in string constructor.\") raise ValueError(\"Invalid cost component: '{}'\".format(expr)) cost = Cost(cost_number, cost_currency, date, label) else: cost = None return Position(Amount(number, currency), cost)","title":"from_string()"},{"location":"api_reference/beancount.core.html#beancount.core.position.get_position","text":"Build a Position instance from a Posting instance. Parameters: posting \u2013 An instance of Posting. Returns: An instance of Position. Source code in beancount/core/position.py def get_position(posting): \"\"\"Build a Position instance from a Posting instance. Args: posting: An instance of Posting. Returns: An instance of Position. \"\"\" return Position(posting.units, posting.cost)","title":"get_position()"},{"location":"api_reference/beancount.core.html#beancount.core.position.to_string","text":"Render the Position or Posting instance to a string. Parameters: pos \u2013 An instance of Position or Posting. dformat \u2013 An instance of DisplayFormatter. detail \u2013 A boolean, true if we should only render the lot details beyond the cost (lot-date, label, etc.). If false, we only render the cost, if present. Returns: A string, the rendered position. Source code in beancount/core/position.py def to_string(pos, dformat=DEFAULT_FORMATTER, detail=True): \"\"\"Render the Position or Posting instance to a string. Args: pos: An instance of Position or Posting. dformat: An instance of DisplayFormatter. detail: A boolean, true if we should only render the lot details beyond the cost (lot-date, label, etc.). If false, we only render the cost, if present. Returns: A string, the rendered position. \"\"\" pos_str = pos.units.to_string(dformat) if pos.cost is not None: pos_str = '{} {{{}}}'.format(pos_str, cost_to_str(pos.cost, dformat, detail)) return pos_str","title":"to_string()"},{"location":"api_reference/beancount.core.html#beancount.core.prices","text":"This module has code that can build a database of historical prices at various times, from which unrealized capital gains and market value can be deduced. Prices are deduced from Price entries found in the file, or perhaps created by scripts (for example you could build a script that will fetch live prices online and create entries on-the-fly).","title":"prices"},{"location":"api_reference/beancount.core.html#beancount.core.prices.PriceMap","text":"A price map dictionary. The keys include both the set of forward (base, quote) pairs and their inverse. In order to determine which are the forward pairs, access the 'forward_pairs' attribute Attributes: Name Type Description forward_pairs A list of (base, quote) keys for the forward pairs.","title":"PriceMap"},{"location":"api_reference/beancount.core.html#beancount.core.prices.build_price_map","text":"Build a price map from a list of arbitrary entries. If multiple prices are found for the same (currency, cost-currency) pair at the same date, the latest date is kept and the earlier ones (for that day) are discarded. If inverse price pairs are found, e.g. USD in AUD and AUD in USD, the inverse that has the smallest number of price points is converted into the one that has the most price points. In that way they are reconciled into a single one. Parameters: entries \u2013 A list of directives, hopefully including some Price and/or Returns: A dict of (currency, cost-currency) keys to sorted lists of (date, number) pairs, where 'date' is the date the price occurs at and 'number' a Decimal that represents the price, or rate, between these two currencies/commodities. Each date occurs only once in the sorted list of prices of a particular key. All of the inverses are automatically generated in the price map. Source code in beancount/core/prices.py def build_price_map(entries): \"\"\"Build a price map from a list of arbitrary entries. If multiple prices are found for the same (currency, cost-currency) pair at the same date, the latest date is kept and the earlier ones (for that day) are discarded. If inverse price pairs are found, e.g. USD in AUD and AUD in USD, the inverse that has the smallest number of price points is converted into the one that has the most price points. In that way they are reconciled into a single one. Args: entries: A list of directives, hopefully including some Price and/or Transaction entries. Returns: A dict of (currency, cost-currency) keys to sorted lists of (date, number) pairs, where 'date' is the date the price occurs at and 'number' a Decimal that represents the price, or rate, between these two currencies/commodities. Each date occurs only once in the sorted list of prices of a particular key. All of the inverses are automatically generated in the price map. \"\"\" # Fetch a list of all the price entries seen in the ledger. price_entries = [entry for entry in entries if isinstance(entry, Price)] # Build a map of exchange rates between these units. # (base-currency, quote-currency) -> List of (date, rate). price_map = collections.defaultdict(list) for price in price_entries: base_quote = (price.currency, price.amount.currency) price_map[base_quote].append((price.date, price.amount.number)) # Find pairs of inversed units. inversed_units = [] for base_quote, values in price_map.items(): base, quote = base_quote if (quote, base) in price_map: inversed_units.append(base_quote) # Find pairs of inversed units, and swallow the conversion with the smaller # number of rates into the other one. for base, quote in inversed_units: bq_prices = price_map[(base, quote)] qb_prices = price_map[(quote, base)] remove = ((base, quote) if len(bq_prices) < len(qb_prices) else (quote, base)) base, quote = remove remove_list = price_map[remove] insert_list = price_map[(quote, base)] del price_map[remove] inverted_list = [(date, ONE/rate) for (date, rate) in remove_list if rate != ZERO] insert_list.extend(inverted_list) # Unzip and sort each of the entries and eliminate duplicates on the date. sorted_price_map = PriceMap({ base_quote: list(misc_utils.sorted_uniquify(date_rates, lambda x: x[0], last=True)) for (base_quote, date_rates) in price_map.items()}) # Compute and insert all the inverted rates. forward_pairs = list(sorted_price_map.keys()) for (base, quote), price_list in list(sorted_price_map.items()): # Note: You have to filter out zero prices for zero-cost postings, like # gifted options. sorted_price_map[(quote, base)] = [ (date, ONE/price) for date, price in price_list if price != ZERO] sorted_price_map.forward_pairs = forward_pairs return sorted_price_map","title":"build_price_map()"},{"location":"api_reference/beancount.core.html#beancount.core.prices.get_all_prices","text":"Return a sorted list of all (date, number) price pairs. Parameters: price_map \u2013 A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote \u2013 A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A list of (date, Decimal) pairs, sorted by date. Exceptions: KeyError \u2013 If the base/quote could not be found. Source code in beancount/core/prices.py def get_all_prices(price_map, base_quote): \"\"\"Return a sorted list of all (date, number) price pairs. Args: price_map: A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote: A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A list of (date, Decimal) pairs, sorted by date. Raises: KeyError: If the base/quote could not be found. \"\"\" base_quote = normalize_base_quote(base_quote) return _lookup_price_and_inverse(price_map, base_quote)","title":"get_all_prices()"},{"location":"api_reference/beancount.core.html#beancount.core.prices.get_last_price_entries","text":"Run through the entries until the given date and return the last Price entry encountered for each (currency, cost-currency) pair. Parameters: entries \u2013 A list of directives. date \u2013 An instance of datetime.date. If None, the very latest price is returned. Returns: A list of price entries. Source code in beancount/core/prices.py def get_last_price_entries(entries, date): \"\"\"Run through the entries until the given date and return the last Price entry encountered for each (currency, cost-currency) pair. Args: entries: A list of directives. date: An instance of datetime.date. If None, the very latest price is returned. Returns: A list of price entries. \"\"\" price_entry_map = {} for entry in entries: if date is not None and entry.date >= date: break if isinstance(entry, Price): base_quote = (entry.currency, entry.amount.currency) price_entry_map[base_quote] = entry return sorted(price_entry_map.values(), key=data.entry_sortkey)","title":"get_last_price_entries()"},{"location":"api_reference/beancount.core.html#beancount.core.prices.get_latest_price","text":"Return the latest price/rate from a price map for the given base/quote pair. This is often used to just get the 'current' price if you're looking at the entire set of entries. Parameters: price_map \u2013 A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. Returns: A pair of (date, number), where 'date' is a datetime.date instance and 'number' is a Decimal of the price, or rate, at that date. The date is the latest date which we have an available price for in the price map. Source code in beancount/core/prices.py def get_latest_price(price_map, base_quote): \"\"\"Return the latest price/rate from a price map for the given base/quote pair. This is often used to just get the 'current' price if you're looking at the entire set of entries. Args: price_map: A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. Returns: A pair of (date, number), where 'date' is a datetime.date instance and 'number' is a Decimal of the price, or rate, at that date. The date is the latest date which we have an available price for in the price map. \"\"\" base_quote = normalize_base_quote(base_quote) # Handle the degenerate case of a currency priced into its own. base, quote = base_quote if quote is None or base == quote: return (None, ONE) # Look up the list and return the latest element. The lists are assumed to # be sorted. try: price_list = _lookup_price_and_inverse(price_map, base_quote) except KeyError: price_list = None if price_list: return price_list[-1] else: return None, None","title":"get_latest_price()"},{"location":"api_reference/beancount.core.html#beancount.core.prices.get_price","text":"Return the price as of the given date. If the date is unspecified, return the latest price. Parameters: price_map \u2013 A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote \u2013 A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. date \u2013 A datetime.date instance, the date at which we want the conversion rate. Returns: A pair of (datetime.date, Decimal) instance. If no price information could be found, return (None, None). Source code in beancount/core/prices.py def get_price(price_map, base_quote, date=None): \"\"\"Return the price as of the given date. If the date is unspecified, return the latest price. Args: price_map: A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote: A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. date: A datetime.date instance, the date at which we want the conversion rate. Returns: A pair of (datetime.date, Decimal) instance. If no price information could be found, return (None, None). \"\"\" if date is None: return get_latest_price(price_map, base_quote) base_quote = normalize_base_quote(base_quote) # Handle the degenerate case of a currency priced into its own. base, quote = base_quote if quote is None or base == quote: return (None, ONE) try: price_list = _lookup_price_and_inverse(price_map, base_quote) index = bisect_key.bisect_right_with_key(price_list, date, key=lambda x: x[0]) if index == 0: return None, None else: return price_list[index-1] except KeyError: return None, None","title":"get_price()"},{"location":"api_reference/beancount.core.html#beancount.core.prices.normalize_base_quote","text":"Convert a slash-separated string to a pair of strings. Parameters: base_quote \u2013 A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A pair of strings. Source code in beancount/core/prices.py def normalize_base_quote(base_quote): \"\"\"Convert a slash-separated string to a pair of strings. Args: base_quote: A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A pair of strings. \"\"\" if isinstance(base_quote, str): base_quote_norm = tuple(base_quote.split('/')) assert len(base_quote_norm) == 2, base_quote base_quote = base_quote_norm assert isinstance(base_quote, tuple), base_quote return base_quote","title":"normalize_base_quote()"},{"location":"api_reference/beancount.core.html#beancount.core.realization","text":"Realization of specific lists of account postings into reports. This code converts a list of entries into a tree of RealAccount nodes (which stands for \"realized accounts\"). The RealAccount objects contain lists of Posting instances instead of Transactions, or other entry types that are attached to an account, such as a Balance or Note entry. The interface of RealAccount corresponds to that of a regular Python dict, where the keys are the names of the individual components of an account's name, and the values are always other RealAccount instances. If you want to get an account by long account name, there are helper functions in this module for this purpose (see realization.get(), for instance). RealAccount instances also contain the final balance of that account, resulting from its list of postings. You should not build RealAccount trees yourself; instead, you should filter the list of desired directives to display and call the realize() function with them.","title":"realization"},{"location":"api_reference/beancount.core.html#beancount.core.realization.RealAccount","text":"A realized account, inserted in a tree, that contains the list of realized entries. Attributes: Name Type Description account A string, the full name of the corresponding account. postings A list of postings associated with this accounting (does not include the postings of children accounts). balance The final balance of the list of postings associated with this account.","title":"RealAccount"},{"location":"api_reference/beancount.core.html#beancount.core.realization.RealAccount.__eq__","text":"Equality predicate. All attributes are compared. Parameters: other \u2013 Another instance of RealAccount. Returns: A boolean, True if the two real accounts are equal. Source code in beancount/core/realization.py def __eq__(self, other): \"\"\"Equality predicate. All attributes are compared. Args: other: Another instance of RealAccount. Returns: A boolean, True if the two real accounts are equal. \"\"\" return (dict.__eq__(self, other) and self.account == other.account and self.balance == other.balance and self.txn_postings == other.txn_postings)","title":"__eq__()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.RealAccount.__init__","text":"Create a RealAccount instance. Parameters: account_name \u2013 a string, the name of the account. Maybe not be None. Source code in beancount/core/realization.py def __init__(self, account_name, *args, **kwargs): \"\"\"Create a RealAccount instance. Args: account_name: a string, the name of the account. Maybe not be None. \"\"\" super().__init__(*args, **kwargs) assert isinstance(account_name, str) self.account = account_name self.txn_postings = [] self.balance = inventory.Inventory()","title":"__init__()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.RealAccount.__ne__","text":"Not-equality predicate. See eq . Parameters: other \u2013 Another instance of RealAccount. Returns: A boolean, True if the two real accounts are not equal. Source code in beancount/core/realization.py def __ne__(self, other): \"\"\"Not-equality predicate. See __eq__. Args: other: Another instance of RealAccount. Returns: A boolean, True if the two real accounts are not equal. \"\"\" return not self.__eq__(other)","title":"__ne__()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.RealAccount.__setitem__","text":"Prevent the setting of non-string or non-empty keys on this dict. Parameters: key \u2013 The dictionary key. Must be a string. value \u2013 The value, must be a RealAccount instance. Exceptions: KeyError \u2013 If the key is not a string, or is invalid. ValueError \u2013 If the value is not a RealAccount instance. Source code in beancount/core/realization.py def __setitem__(self, key, value): \"\"\"Prevent the setting of non-string or non-empty keys on this dict. Args: key: The dictionary key. Must be a string. value: The value, must be a RealAccount instance. Raises: KeyError: If the key is not a string, or is invalid. ValueError: If the value is not a RealAccount instance. \"\"\" if not isinstance(key, str) or not key: raise KeyError(\"Invalid RealAccount key: '{}'\".format(key)) if not isinstance(value, RealAccount): raise ValueError(\"Invalid RealAccount value: '{}'\".format(value)) if not value.account.endswith(key): raise ValueError(\"RealAccount name '{}' inconsistent with key: '{}'\".format( value.account, key)) return super().__setitem__(key, value)","title":"__setitem__()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.RealAccount.copy","text":"Override dict.copy() to clone a RealAccount. This is only necessary to correctly implement the copy method. Otherwise, calling .copy() on a RealAccount instance invokes the base class' method, which return just a dict. Returns: A cloned instance of RealAccount, with all members shallow-copied. Source code in beancount/core/realization.py def copy(self): \"\"\"Override dict.copy() to clone a RealAccount. This is only necessary to correctly implement the copy method. Otherwise, calling .copy() on a RealAccount instance invokes the base class' method, which return just a dict. Returns: A cloned instance of RealAccount, with all members shallow-copied. \"\"\" return copy.copy(self)","title":"copy()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.compute_balance","text":"Compute the total balance of this account and all its subaccounts. Parameters: real_account \u2013 A RealAccount instance. leaf_only \u2013 A boolean flag, true if we should yield only leaves. Returns: An Inventory. Source code in beancount/core/realization.py def compute_balance(real_account, leaf_only=False): \"\"\"Compute the total balance of this account and all its subaccounts. Args: real_account: A RealAccount instance. leaf_only: A boolean flag, true if we should yield only leaves. Returns: An Inventory. \"\"\" return functools.reduce(operator.add, [ ra.balance for ra in iter_children(real_account, leaf_only)])","title":"compute_balance()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.compute_postings_balance","text":"Compute the balance of a list of Postings's or TxnPosting's positions. Parameters: postings \u2013 A list of Posting instances and other directives (which are skipped). Returns: An Inventory. Source code in beancount/core/realization.py def compute_postings_balance(txn_postings): \"\"\"Compute the balance of a list of Postings's or TxnPosting's positions. Args: postings: A list of Posting instances and other directives (which are skipped). Returns: An Inventory. \"\"\" final_balance = inventory.Inventory() for txn_posting in txn_postings: if isinstance(txn_posting, Posting): final_balance.add_position(txn_posting) elif isinstance(txn_posting, TxnPosting): final_balance.add_position(txn_posting.posting) return final_balance","title":"compute_postings_balance()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.contains","text":"True if the given account node contains the subaccount name. Parameters: account_name \u2013 A string, the name of a direct or indirect subaccount of this node. Returns: A boolean, true the name is a child of this node. Source code in beancount/core/realization.py def contains(real_account, account_name): \"\"\"True if the given account node contains the subaccount name. Args: account_name: A string, the name of a direct or indirect subaccount of this node. Returns: A boolean, true the name is a child of this node. \"\"\" return get(real_account, account_name) is not None","title":"contains()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.dump","text":"Convert a RealAccount node to a line of lines. Note: this is not meant to be used to produce text reports; the reporting code should produce an intermediate object that contains the structure of it, which can then be rendered to ASCII, HTML or CSV formats. This is intended as a convenient little function for dumping trees of data for debugging purposes. Parameters: root_account \u2013 A RealAccount instance. Returns: A list of tuples of (first_line, continuation_line, real_account) where first_line \u2013 A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. real_account: The RealAccount instance which corresponds to this line. Source code in beancount/core/realization.py def dump(root_account): \"\"\"Convert a RealAccount node to a line of lines. Note: this is not meant to be used to produce text reports; the reporting code should produce an intermediate object that contains the structure of it, which can then be rendered to ASCII, HTML or CSV formats. This is intended as a convenient little function for dumping trees of data for debugging purposes. Args: root_account: A RealAccount instance. Returns: A list of tuples of (first_line, continuation_line, real_account) where first_line: A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. real_account: The RealAccount instance which corresponds to this line. \"\"\" # Compute all the lines ahead of time in order to calculate the width. lines = [] # Start with the root node. We push the constant prefix before this node, # the account name, and the RealAccount instance. We will maintain a stack # of children nodes to render. stack = [('', root_account.account, root_account, True)] while stack: prefix, name, real_account, is_last = stack.pop(-1) if real_account is root_account: # For the root node, we don't want to render any prefix. first = cont = '' else: # Compute the string that precedes the name directly and the one below # that for the continuation lines. # | # @@@ Bank1 <---------------- # @@@ | # | |-- Checking if is_last: first = prefix + PREFIX_LEAF_1 cont = prefix + PREFIX_LEAF_C else: first = prefix + PREFIX_CHILD_1 cont = prefix + PREFIX_CHILD_C # Compute the name to render for continuation lines. # | # |-- Bank1 # | @@@ <---------------- # | |-- Checking if len(real_account) > 0: cont_name = PREFIX_CHILD_C else: cont_name = PREFIX_LEAF_C # Add a line for this account. if not (real_account is root_account and not name): lines.append((first + name, cont + cont_name, real_account)) # Push the children onto the stack, being careful with ordering and # marking the last node as such. child_items = sorted(real_account.items(), reverse=True) if child_items: child_iter = iter(child_items) child_name, child_account = next(child_iter) stack.append((cont, child_name, child_account, True)) for child_name, child_account in child_iter: stack.append((cont, child_name, child_account, False)) if not lines: return lines # Compute the maximum width of the lines and convert all of them to the same # maximal width. This makes it easy on the client. max_width = max(len(first_line) for first_line, _, __ in lines) line_format = '{{:{width}}}'.format(width=max_width) return [(line_format.format(first_line), line_format.format(cont_line), real_node) for (first_line, cont_line, real_node) in lines]","title":"dump()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.dump_balances","text":"Dump a realization tree with balances. Parameters: real_root \u2013 An instance of RealAccount. dformat \u2013 An instance of DisplayFormatter to format the numbers with. at_cost \u2013 A boolean, if true, render the values at cost. fullnames \u2013 A boolean, if true, don't render a tree of accounts and render the full account names. file \u2013 A file object to dump the output to. If not specified, we return the output as a string. Returns: A string, the rendered tree, or nothing, if 'file' was provided. Source code in beancount/core/realization.py def dump_balances(real_root, dformat, at_cost=False, fullnames=False, file=None): \"\"\"Dump a realization tree with balances. Args: real_root: An instance of RealAccount. dformat: An instance of DisplayFormatter to format the numbers with. at_cost: A boolean, if true, render the values at cost. fullnames: A boolean, if true, don't render a tree of accounts and render the full account names. file: A file object to dump the output to. If not specified, we return the output as a string. Returns: A string, the rendered tree, or nothing, if 'file' was provided. \"\"\" if fullnames: # Compute the maximum account name length; maxlen = max(len(real_child.account) for real_child in iter_children(real_root, leaf_only=True)) line_format = '{{:{width}}} {{}}\\n'.format(width=maxlen) else: line_format = '{} {}\\n' output = file or io.StringIO() for first_line, cont_line, real_account in dump(real_root): if not real_account.balance.is_empty(): if at_cost: rinv = real_account.balance.reduce(convert.get_cost) else: rinv = real_account.balance.reduce(convert.get_units) amounts = [position.units for position in rinv.get_positions()] positions = [amount_.to_string(dformat) for amount_ in sorted(amounts, key=amount.sortkey)] else: positions = [''] if fullnames: for position in positions: if not position and len(real_account) > 0: continue # Skip parent accounts with no position to render. output.write(line_format.format(real_account.account, position)) else: line = first_line for position in positions: output.write(line_format.format(line, position)) line = cont_line if file is None: return output.getvalue()","title":"dump_balances()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.filter","text":"Filter a RealAccount tree of nodes by the predicate. This function visits the tree and applies the predicate on each node. It returns a partial clone of RealAccount whereby on each node - either the predicate is true, or - for at least one child of the node the predicate is true. All the leaves have the predicate be true. Parameters: real_account \u2013 An instance of RealAccount. predicate \u2013 A callable/function which accepts a real_account and returns a boolean. If the function returns True, the node is kept. Returns: A shallow clone of RealAccount is always returned. Source code in beancount/core/realization.py def filter(real_account, predicate): \"\"\"Filter a RealAccount tree of nodes by the predicate. This function visits the tree and applies the predicate on each node. It returns a partial clone of RealAccount whereby on each node - either the predicate is true, or - for at least one child of the node the predicate is true. All the leaves have the predicate be true. Args: real_account: An instance of RealAccount. predicate: A callable/function which accepts a real_account and returns a boolean. If the function returns True, the node is kept. Returns: A shallow clone of RealAccount is always returned. \"\"\" assert isinstance(real_account, RealAccount) real_copy = RealAccount(real_account.account) real_copy.balance = real_account.balance real_copy.txn_postings = real_account.txn_postings for child_name, real_child in real_account.items(): real_child_copy = filter(real_child, predicate) if real_child_copy is not None: real_copy[child_name] = real_child_copy if len(real_copy) > 0 or predicate(real_account): return real_copy","title":"filter()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.find_last_active_posting","text":"Look at the end of the list of postings, and find the last posting or entry that is not an automatically added directive. Note that if the account is closed, the last posting is assumed to be a Close directive (this is the case if the input is valid and checks without errors. Parameters: txn_postings \u2013 a list of postings or entries. Returns: An entry, or None, if the input list was empty. Source code in beancount/core/realization.py def find_last_active_posting(txn_postings): \"\"\"Look at the end of the list of postings, and find the last posting or entry that is not an automatically added directive. Note that if the account is closed, the last posting is assumed to be a Close directive (this is the case if the input is valid and checks without errors. Args: txn_postings: a list of postings or entries. Returns: An entry, or None, if the input list was empty. \"\"\" for txn_posting in reversed(txn_postings): assert not isinstance(txn_posting, Posting) if not isinstance(txn_posting, (TxnPosting, Open, Close, Pad, Balance, Note)): continue # pylint: disable=bad-continuation if (isinstance(txn_posting, TxnPosting) and txn_posting.txn.flag == flags.FLAG_UNREALIZED): continue return txn_posting","title":"find_last_active_posting()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.get","text":"Fetch the subaccount name from the real_account node. Parameters: real_account \u2013 An instance of RealAccount, the parent node to look for children of. account_name \u2013 A string, the name of a possibly indirect child leaf found down the tree of 'real_account' nodes. default \u2013 The default value that should be returned if the child subaccount is not found. Returns: A RealAccount instance for the child, or the default value, if the child is not found. Source code in beancount/core/realization.py def get(real_account, account_name, default=None): \"\"\"Fetch the subaccount name from the real_account node. Args: real_account: An instance of RealAccount, the parent node to look for children of. account_name: A string, the name of a possibly indirect child leaf found down the tree of 'real_account' nodes. default: The default value that should be returned if the child subaccount is not found. Returns: A RealAccount instance for the child, or the default value, if the child is not found. \"\"\" if not isinstance(account_name, str): raise ValueError components = account.split(account_name) for component in components: real_child = real_account.get(component, default) if real_child is default: return default real_account = real_child return real_account","title":"get()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.get_or_create","text":"Fetch the subaccount name from the real_account node. Parameters: real_account \u2013 An instance of RealAccount, the parent node to look for children of, or create under. account_name \u2013 A string, the name of the direct or indirect child leaf to get or create. Returns: A RealAccount instance for the child, or the default value, if the child is not found. Source code in beancount/core/realization.py def get_or_create(real_account, account_name): \"\"\"Fetch the subaccount name from the real_account node. Args: real_account: An instance of RealAccount, the parent node to look for children of, or create under. account_name: A string, the name of the direct or indirect child leaf to get or create. Returns: A RealAccount instance for the child, or the default value, if the child is not found. \"\"\" if not isinstance(account_name, str): raise ValueError components = account.split(account_name) path = [] for component in components: path.append(component) real_child = real_account.get(component, None) if real_child is None: real_child = RealAccount(account.join(*path)) real_account[component] = real_child real_account = real_child return real_account","title":"get_or_create()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.get_postings","text":"Return a sorted list a RealAccount's postings and children. Parameters: real_account \u2013 An instance of RealAccount. Returns: A list of Posting or directories. Source code in beancount/core/realization.py def get_postings(real_account): \"\"\"Return a sorted list a RealAccount's postings and children. Args: real_account: An instance of RealAccount. Returns: A list of Posting or directories. \"\"\" # We accumulate all the postings at once here instead of incrementally # because we need to return them sorted. accumulator = [] for real_child in iter_children(real_account): accumulator.extend(real_child.txn_postings) accumulator.sort(key=data.posting_sortkey) return accumulator","title":"get_postings()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.index_key","text":"Find the index of the first element in 'sequence' which is equal to 'value'. If 'key' is specified, the value compared to the value returned by this function. If the value is not found, return None. Parameters: sequence \u2013 The sequence to search. value \u2013 The value to search for. key \u2013 A predicate to call to obtain the value to compare against. cmp \u2013 A comparison predicate. Returns: The index of the first element found, or None, if the element was not found. Source code in beancount/core/realization.py def index_key(sequence, value, key, cmp): \"\"\"Find the index of the first element in 'sequence' which is equal to 'value'. If 'key' is specified, the value compared to the value returned by this function. If the value is not found, return None. Args: sequence: The sequence to search. value: The value to search for. key: A predicate to call to obtain the value to compare against. cmp: A comparison predicate. Returns: The index of the first element found, or None, if the element was not found. \"\"\" for index, element in enumerate(sequence): if cmp(key(element), value): return index return","title":"index_key()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.iter_children","text":"Iterate this account node and all its children, depth-first. Parameters: real_account \u2013 An instance of RealAccount. leaf_only \u2013 A boolean flag, true if we should yield only leaves. Yields: Instances of RealAccount, beginning with this account. The order is undetermined. Source code in beancount/core/realization.py def iter_children(real_account, leaf_only=False): \"\"\"Iterate this account node and all its children, depth-first. Args: real_account: An instance of RealAccount. leaf_only: A boolean flag, true if we should yield only leaves. Yields: Instances of RealAccount, beginning with this account. The order is undetermined. \"\"\" if leaf_only: if len(real_account) == 0: yield real_account else: for key, real_child in sorted(real_account.items()): for real_subchild in iter_children(real_child, leaf_only): yield real_subchild else: yield real_account for key, real_child in sorted(real_account.items()): for real_subchild in iter_children(real_child): yield real_subchild","title":"iter_children()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.iterate_with_balance","text":"Iterate over the entries, accumulating the running balance. For each entry, this yields tuples of the form: (entry, postings, change, balance) entry: This is the directive for this line. If the list contained Posting instance, this yields the corresponding Transaction object. postings: A list of postings on this entry that affect the balance. Only the postings encountered in the input list are included; only those affect the balance. If 'entry' is not a Transaction directive, this should always be an empty list. We preserve the original ordering of the postings as they appear in the input list. change: An Inventory object that reflects the total change due to the postings from this entry that appear in the list. For example, if a Transaction has three postings and two are in the input list, the sum of the two postings will be in the 'change' Inventory object. However, the position for the transactions' third posting--the one not included in the input list--will not be in this inventory. balance: An Inventory object that reflects the balance after adding the 'change' inventory due to this entry's transaction. The 'balance' yielded is never None, even for entries that do not affect the balance, that is, with an empty 'change' inventory. It's up to the caller, the one rendering the entry, to decide whether to render this entry's change for a particular entry type. Note that if the input list of postings-or-entries is not in sorted date order, two postings for the same entry appearing twice with a different date in between will cause the entry appear twice. This is correct behavior, and it is expected that in practice this should never happen anyway, because the list of postings or entries should always be sorted. This method attempts to detect this and raises an assertion if this is seen. Parameters: txn_postings \u2013 A list of postings or directive instances. Postings affect the balance; other entries do not. Yields: Tuples of (entry, postings, change, balance) as described above. Source code in beancount/core/realization.py def iterate_with_balance(txn_postings): \"\"\"Iterate over the entries, accumulating the running balance. For each entry, this yields tuples of the form: (entry, postings, change, balance) entry: This is the directive for this line. If the list contained Posting instance, this yields the corresponding Transaction object. postings: A list of postings on this entry that affect the balance. Only the postings encountered in the input list are included; only those affect the balance. If 'entry' is not a Transaction directive, this should always be an empty list. We preserve the original ordering of the postings as they appear in the input list. change: An Inventory object that reflects the total change due to the postings from this entry that appear in the list. For example, if a Transaction has three postings and two are in the input list, the sum of the two postings will be in the 'change' Inventory object. However, the position for the transactions' third posting--the one not included in the input list--will not be in this inventory. balance: An Inventory object that reflects the balance *after* adding the 'change' inventory due to this entry's transaction. The 'balance' yielded is never None, even for entries that do not affect the balance, that is, with an empty 'change' inventory. It's up to the caller, the one rendering the entry, to decide whether to render this entry's change for a particular entry type. Note that if the input list of postings-or-entries is not in sorted date order, two postings for the same entry appearing twice with a different date in between will cause the entry appear twice. This is correct behavior, and it is expected that in practice this should never happen anyway, because the list of postings or entries should always be sorted. This method attempts to detect this and raises an assertion if this is seen. Args: txn_postings: A list of postings or directive instances. Postings affect the balance; other entries do not. Yields: Tuples of (entry, postings, change, balance) as described above. \"\"\" # The running balance. running_balance = inventory.Inventory() # Previous date. prev_date = None # A list of entries at the current date. date_entries = [] first = lambda pair: pair[0] for txn_posting in txn_postings: # Get the posting if we are dealing with one. assert not isinstance(txn_posting, Posting) if isinstance(txn_posting, TxnPosting): posting = txn_posting.posting entry = txn_posting.txn else: posting = None entry = txn_posting if entry.date != prev_date: assert prev_date is None or entry.date > prev_date, ( \"Invalid date order for postings: {} > {}\".format(prev_date, entry.date)) prev_date = entry.date # Flush the dated entries. for date_entry, date_postings in date_entries: change = inventory.Inventory() if date_postings: # Compute the change due to this transaction and update the # total balance at the same time. for date_posting in date_postings: change.add_position(date_posting) running_balance.add_position(date_posting) yield date_entry, date_postings, change, running_balance date_entries.clear() assert not date_entries if posting is not None: # De-dup multiple postings on the same transaction entry by # grouping their positions together. index = index_key(date_entries, entry, first, operator.is_) if index is None: date_entries.append((entry, [posting])) else: # We are indeed de-duping! postings = date_entries[index][1] postings.append(posting) else: # This is a regular entry; nothing to add/remove. date_entries.append((entry, [])) # Flush the final dated entries if any, same as above. for date_entry, date_postings in date_entries: change = inventory.Inventory() if date_postings: for date_posting in date_postings: change.add_position(date_posting) running_balance.add_position(date_posting) yield date_entry, date_postings, change, running_balance date_entries.clear()","title":"iterate_with_balance()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.postings_by_account","text":"Create lists of postings and balances by account. This routine aggregates postings and entries grouping them by account name. The resulting lists contain postings in-lieu of Transaction directives, but the other directives are stored as entries. This yields a list of postings or other entries by account. All references to accounts are taken into account. Parameters: entries \u2013 A list of directives. Returns: A mapping of account name to list of TxnPosting instances or non-Transaction directives, sorted in the same order as the entries. Source code in beancount/core/realization.py def postings_by_account(entries): \"\"\"Create lists of postings and balances by account. This routine aggregates postings and entries grouping them by account name. The resulting lists contain postings in-lieu of Transaction directives, but the other directives are stored as entries. This yields a list of postings or other entries by account. All references to accounts are taken into account. Args: entries: A list of directives. Returns: A mapping of account name to list of TxnPosting instances or non-Transaction directives, sorted in the same order as the entries. \"\"\" txn_postings_map = collections.defaultdict(list) for entry in entries: if isinstance(entry, Transaction): # Insert an entry for each of the postings. for posting in entry.postings: txn_postings_map[posting.account].append( TxnPosting(entry, posting)) elif isinstance(entry, (Open, Close, Balance, Note, Document)): # Append some other entries in the realized list. txn_postings_map[entry.account].append(entry) elif isinstance(entry, Pad): # Insert the pad entry in both realized accounts. txn_postings_map[entry.account].append(entry) txn_postings_map[entry.source_account].append(entry) elif isinstance(entry, Custom): # Insert custom entry for each account in its values. for custom_value in entry.values: if custom_value.dtype == account.TYPE: txn_postings_map[custom_value.value].append(entry) return txn_postings_map","title":"postings_by_account()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.realize","text":"Group entries by account, into a \"tree\" of realized accounts. RealAccount's are essentially containers for lists of postings and the final balance of each account, and may be non-leaf accounts (used strictly for organizing accounts into a hierarchy). This is then used to issue reports. The lists of postings in each account my be any of the entry types, except for Transaction, whereby Transaction entries are replaced by the specific Posting legs that belong to the account. Here's a simple diagram that summarizes this seemingly complex, but rather simple data structure: +-------------+ postings +------+ | RealAccount |---------->| Open | +-------------+ +------+ | v +------------+ +-------------+ | TxnPosting |---->| Transaction | +------------+ +-------------+ | \\ \\\\ v \\.__ +---------+ +-----+ -------->| Posting | | Pad | +---------+ +-----+ | v +---------+ | Balance | +---------+ | v +-------+ | Close | +-------+ | . Parameters: entries \u2013 A list of directives. min_accounts \u2013 A list of strings, account names to ensure we create, regardless of whether there are postings on those accounts or not. This can be used to ensure the root accounts all exist. compute_balance \u2013 A boolean, true if we should compute the final balance on the realization. Returns: The root RealAccount instance. Source code in beancount/core/realization.py def realize(entries, min_accounts=None, compute_balance=True): r\"\"\"Group entries by account, into a \"tree\" of realized accounts. RealAccount's are essentially containers for lists of postings and the final balance of each account, and may be non-leaf accounts (used strictly for organizing accounts into a hierarchy). This is then used to issue reports. The lists of postings in each account my be any of the entry types, except for Transaction, whereby Transaction entries are replaced by the specific Posting legs that belong to the account. Here's a simple diagram that summarizes this seemingly complex, but rather simple data structure: +-------------+ postings +------+ | RealAccount |---------->| Open | +-------------+ +------+ | v +------------+ +-------------+ | TxnPosting |---->| Transaction | +------------+ +-------------+ | \\ \\\\\\ v `\\.__ +---------+ +-----+ `-------->| Posting | | Pad | +---------+ +-----+ | v +---------+ | Balance | +---------+ | v +-------+ | Close | +-------+ | . Args: entries: A list of directives. min_accounts: A list of strings, account names to ensure we create, regardless of whether there are postings on those accounts or not. This can be used to ensure the root accounts all exist. compute_balance: A boolean, true if we should compute the final balance on the realization. Returns: The root RealAccount instance. \"\"\" # Create lists of the entries by account. txn_postings_map = postings_by_account(entries) # Create a RealAccount tree and compute the balance for each. real_root = RealAccount('') for account_name, txn_postings in txn_postings_map.items(): real_account = get_or_create(real_root, account_name) real_account.txn_postings = txn_postings if compute_balance: real_account.balance = compute_postings_balance(txn_postings) # Ensure a minimum set of accounts that should exist. This is typically # called with an instance of AccountTypes to make sure that those exist. if min_accounts: for account_name in min_accounts: get_or_create(real_root, account_name) return real_root","title":"realize()"},{"location":"api_reference/beancount.ingest.html","text":"beancount.ingest \uf0c1 Code to help identify, extract, and file external downloads. This package contains code to help you build importers and drive the process of identifying which importer to run on an externally downloaded file, extract transactions from them and file away these files under a clean and rigidly named hierarchy for preservation. beancount.ingest.cache \uf0c1 A file wrapper which acts as a cache for on-demand evaluation of conversions. This object is used in lieu of a file in order to allow the various importers to reuse each others' conversion results. Converting file contents, e.g. PDF to text, can be expensive. beancount.ingest.cache.contents(filename) \uf0c1 A converter that just reads the entire contents of a file. Parameters: num_bytes \u2013 The number of bytes to read. Returns: A converter function. Source code in beancount/ingest/cache.py def contents(filename): \"\"\"A converter that just reads the entire contents of a file. Args: num_bytes: The number of bytes to read. Returns: A converter function. \"\"\" # Attempt to detect the input encoding automatically, using chardet and a # decent amount of input. rawdata = open(filename, 'rb').read(HEAD_DETECT_MAX_BYTES) detected = chardet.detect(rawdata) encoding = detected['encoding'] # Ignore encoding errors for reading the contents because input files # routinely break this assumption. errors = 'ignore' with open(filename, encoding=encoding, errors=errors) as file: return file.read() beancount.ingest.cache.get_file(filename) \uf0c1 Create or reuse a globally registered instance of a FileMemo. Note: the FileMemo objects' lifetimes are reused for the duration of the process. This is usually the intended behavior. Always create them by calling this constructor. Parameters: filename \u2013 A path string, the absolute name of the file whose memo to create. Returns: A FileMemo instance. Source code in beancount/ingest/cache.py def get_file(filename): \"\"\"Create or reuse a globally registered instance of a FileMemo. Note: the FileMemo objects' lifetimes are reused for the duration of the process. This is usually the intended behavior. Always create them by calling this constructor. Args: filename: A path string, the absolute name of the file whose memo to create. Returns: A FileMemo instance. \"\"\" assert path.isabs(filename), ( \"Path should be absolute in order to guarantee a single call.\") return _CACHE[filename] beancount.ingest.cache.head(num_bytes=8192) \uf0c1 A converter that just reads the first bytes of a file. Parameters: num_bytes \u2013 The number of bytes to read. Returns: A converter function. Source code in beancount/ingest/cache.py def head(num_bytes=8192): \"\"\"A converter that just reads the first bytes of a file. Args: num_bytes: The number of bytes to read. Returns: A converter function. \"\"\" def head_reader(filename): with open(filename, 'rb') as file: rawdata = file.read(num_bytes) detected = chardet.detect(rawdata) encoding = detected['encoding'] return rawdata.decode(encoding) return head_reader beancount.ingest.cache.mimetype(filename) \uf0c1 A converter that computes the MIME type of the file. Returns: A converter function. Source code in beancount/ingest/cache.py def mimetype(filename): \"\"\"A converter that computes the MIME type of the file. Returns: A converter function. \"\"\" return file_type.guess_file_type(filename) beancount.ingest.extract \uf0c1 Extract script. Read an import script and a list of downloaded filenames or directories of downloaded files, and for each of those files, extract transactions from it. beancount.ingest.extract.add_arguments(parser) \uf0c1 Add arguments for the extract command. Source code in beancount/ingest/extract.py def add_arguments(parser): \"\"\"Add arguments for the extract command.\"\"\" parser.add_argument('-e', '-f', '--existing', '--previous', metavar='BEANCOUNT_FILE', default=None, help=('Beancount file or existing entries for de-duplication ' '(optional)')) parser.add_argument('-r', '--reverse', '--descending', action='store_const', dest='ascending', default=True, const=False, help='Write out the entries in descending order') beancount.ingest.extract.extract(importer_config, files_or_directories, output, entries=None, options_map=None, mindate=None, ascending=True, hooks=None) \uf0c1 Given an importer configuration, search for files that can be imported in the list of files or directories, run the signature checks on them, and if it succeeds, run the importer on the file. A list of entries for an existing ledger can be provided in order to perform de-duplication and a minimum date can be provided to filter out old entries. Parameters: importer_config \u2013 A list of (regexps, importer) pairs, the configuration. files_or_directories \u2013 A list of strings, filenames or directories to be processed. output \u2013 A file object, to be written to. entries \u2013 A list of directives loaded from the existing file for the newly extracted entries to be merged in. options_map \u2013 The options parsed from existing file. mindate \u2013 Optional minimum date to output transactions for. ascending \u2013 A boolean, true to print entries in ascending order, false if descending is desired. hooks \u2013 An optional list of hook functions to apply to the list of extract (filename, entries) pairs, in order. If not specified, find_duplicate_entries() is used, automatically. Source code in beancount/ingest/extract.py def extract(importer_config, files_or_directories, output, entries=None, options_map=None, mindate=None, ascending=True, hooks=None): \"\"\"Given an importer configuration, search for files that can be imported in the list of files or directories, run the signature checks on them, and if it succeeds, run the importer on the file. A list of entries for an existing ledger can be provided in order to perform de-duplication and a minimum date can be provided to filter out old entries. Args: importer_config: A list of (regexps, importer) pairs, the configuration. files_or_directories: A list of strings, filenames or directories to be processed. output: A file object, to be written to. entries: A list of directives loaded from the existing file for the newly extracted entries to be merged in. options_map: The options parsed from existing file. mindate: Optional minimum date to output transactions for. ascending: A boolean, true to print entries in ascending order, false if descending is desired. hooks: An optional list of hook functions to apply to the list of extract (filename, entries) pairs, in order. If not specified, find_duplicate_entries() is used, automatically. \"\"\" allow_none_for_tags_and_links = ( options_map and options_map[\"allow_deprecated_none_for_tags_and_links\"]) # Run all the importers and gather their result sets. new_entries_list = [] for filename, importers in identify.find_imports(importer_config, files_or_directories): for importer in importers: # Import and process the file. try: new_entries = extract_from_file( filename, importer, existing_entries=entries, min_date=mindate, allow_none_for_tags_and_links=allow_none_for_tags_and_links) new_entries_list.append((filename, new_entries)) except Exception as exc: logging.exception(\"Importer %s.extract() raised an unexpected error: %s\", importer.name(), exc) continue # Find potential duplicate entries in the result sets, either against the # list of existing ones, or against each other. A single call to this # function is made on purpose, so that the function be able to merge # entries. if hooks is None: hooks = [find_duplicate_entries] for hook_fn in hooks: new_entries_list = hook_fn(new_entries_list, entries) assert isinstance(new_entries_list, list) assert all(isinstance(new_entries, tuple) for new_entries in new_entries_list) assert all(isinstance(new_entries[0], str) for new_entries in new_entries_list) assert all(isinstance(new_entries[1], list) for new_entries in new_entries_list) # Print out the results. output.write(HEADER) for key, new_entries in new_entries_list: output.write(identify.SECTION.format(key)) output.write('\\n') if not ascending: new_entries.reverse() print_extracted_entries(new_entries, output) beancount.ingest.extract.extract_from_file(filename, importer, existing_entries=None, min_date=None, allow_none_for_tags_and_links=False) \uf0c1 Import entries from file 'filename' with the given matches, Also cross-check against a list of provided 'existing_entries' entries, de-duplicating and possibly auto-categorizing. Parameters: filename \u2013 The name of the file to import. importer \u2013 An importer object that matched the file. existing_entries \u2013 A list of existing entries parsed from a ledger, used to detect duplicates and automatically complete or categorize transactions. min_date \u2013 A date before which entries should be ignored. This is useful when an account has a valid check/assert; we could just ignore whatever comes before, if desired. allow_none_for_tags_and_links \u2013 A boolean, whether to allow plugins to generate Transaction objects with None as value for the 'tags' or 'links' attributes. Returns: A list of new imported entries. Exceptions: Exception \u2013 If there is an error in the importer's extract() method. Source code in beancount/ingest/extract.py def extract_from_file(filename, importer, existing_entries=None, min_date=None, allow_none_for_tags_and_links=False): \"\"\"Import entries from file 'filename' with the given matches, Also cross-check against a list of provided 'existing_entries' entries, de-duplicating and possibly auto-categorizing. Args: filename: The name of the file to import. importer: An importer object that matched the file. existing_entries: A list of existing entries parsed from a ledger, used to detect duplicates and automatically complete or categorize transactions. min_date: A date before which entries should be ignored. This is useful when an account has a valid check/assert; we could just ignore whatever comes before, if desired. allow_none_for_tags_and_links: A boolean, whether to allow plugins to generate Transaction objects with None as value for the 'tags' or 'links' attributes. Returns: A list of new imported entries. Raises: Exception: If there is an error in the importer's extract() method. \"\"\" # Extract the entries. file = cache.get_file(filename) # Note: Let the exception through on purpose. This makes developing # importers much easier by rendering the details of the exceptions. # # Note: For legacy support, support calling without the existing entries. kwargs = {} if 'existing_entries' in inspect.signature(importer.extract).parameters: kwargs['existing_entries'] = existing_entries new_entries = importer.extract(file, **kwargs) if not new_entries: return [] # Make sure the newly imported entries are sorted; don't trust the importer. new_entries.sort(key=data.entry_sortkey) # Ensure that the entries are typed correctly. for entry in new_entries: data.sanity_check_types(entry, allow_none_for_tags_and_links) # Filter out entries with dates before 'min_date'. if min_date: new_entries = list(itertools.dropwhile(lambda x: x.date < min_date, new_entries)) return new_entries beancount.ingest.extract.find_duplicate_entries(new_entries_list, existing_entries) \uf0c1 Flag potentially duplicate entries. Parameters: new_entries_list \u2013 A list of pairs of (key, lists of imported entries), one for each importer. The key identifies the filename and/or importer that yielded those new entries. existing_entries \u2013 A list of previously existing entries from the target ledger. Returns: A list of lists of modified new entries (like new_entries_list), potentially with modified metadata to indicate those which are duplicated. Source code in beancount/ingest/extract.py def find_duplicate_entries(new_entries_list, existing_entries): \"\"\"Flag potentially duplicate entries. Args: new_entries_list: A list of pairs of (key, lists of imported entries), one for each importer. The key identifies the filename and/or importer that yielded those new entries. existing_entries: A list of previously existing entries from the target ledger. Returns: A list of lists of modified new entries (like new_entries_list), potentially with modified metadata to indicate those which are duplicated. \"\"\" mod_entries_list = [] for key, new_entries in new_entries_list: # Find similar entries against the existing ledger only. duplicate_pairs = similar.find_similar_entries(new_entries, existing_entries) # Add a metadata marker to the extracted entries for duplicates. duplicate_set = set(id(entry) for entry, _ in duplicate_pairs) mod_entries = [] for entry in new_entries: if id(entry) in duplicate_set: marked_meta = entry.meta.copy() marked_meta[DUPLICATE_META] = True entry = entry._replace(meta=marked_meta) mod_entries.append(entry) mod_entries_list.append((key, mod_entries)) return mod_entries_list beancount.ingest.extract.print_extracted_entries(entries, file) \uf0c1 Print a list of entries. Parameters: entries \u2013 A list of extracted entries. file \u2013 A file object to write to. Source code in beancount/ingest/extract.py def print_extracted_entries(entries, file): \"\"\"Print a list of entries. Args: entries: A list of extracted entries. file: A file object to write to. \"\"\" # Print the filename and which modules matched. # pylint: disable=invalid-name pr = lambda *args: print(*args, file=file) pr('') # Print out the entries. for entry in entries: # Check if this entry is a dup, and if so, comment it out. if DUPLICATE_META in entry.meta: meta = entry.meta.copy() meta.pop(DUPLICATE_META) entry = entry._replace(meta=meta) entry_string = textwrap.indent(printer.format_entry(entry), '; ') else: entry_string = printer.format_entry(entry) pr(entry_string) pr('') beancount.ingest.extract.run(args, _, importers_list, files_or_directories, hooks=None) \uf0c1 Run the subcommand. Source code in beancount/ingest/extract.py def run(args, _, importers_list, files_or_directories, hooks=None): \"\"\"Run the subcommand.\"\"\" # Load the ledger, if one is specified. if args.existing: entries, _, options_map = loader.load_file(args.existing) else: entries, options_map = None, None extract(importers_list, files_or_directories, sys.stdout, entries=entries, options_map=options_map, mindate=None, ascending=args.ascending, hooks=hooks) return 0 beancount.ingest.file \uf0c1 Filing script. Read an import script and a list of downloaded filenames or directories of downloaded files, and for each of those files, move the file under an account corresponding to the filing directory. beancount.ingest.file.add_arguments(parser) \uf0c1 Add arguments for the extract command. Source code in beancount/ingest/file.py def add_arguments(parser): \"\"\"Add arguments for the extract command.\"\"\" parser.add_argument('-o', '--output', '--output-dir', '--destination', dest='output_dir', action='store', help=\"The root of the documents tree to move the files to.\") parser.add_argument('-n', '--dry-run', action='store_true', help=(\"Just print where the files would be moved; \" \"don't actually move them.\")) parser.add_argument('--no-overwrite', dest='overwrite', action='store_false', default=True, help=\"Don't overwrite destination files with the same name.\") beancount.ingest.file.file(importer_config, files_or_directories, destination, dry_run=False, mkdirs=False, overwrite=False, idify=False, logfile=None) \uf0c1 File importable files under a destination directory. Given an importer configuration object, search for files that can be imported under the given list of files or directories and moved them under the given destination directory with the date computed by the module prepended to the filename. If the date cannot be extracted, use a reasonable default for the date (e.g. the last modified time of the file itself). If 'mkdirs' is True, create the destination directories before moving the files. Parameters: importer_config \u2013 A list of importer instances that define the config. files_or_directories \u2013 a list of files of directories to walk recursively and hunt for files to import. destination \u2013 A string, the root destination directory where the files are to be filed. The files are organized there under a hierarchy mirroring that of the chart of accounts. dry_run \u2013 A flag, if true, don't actually move the files. mkdirs \u2013 A flag, if true, make all the intervening directories; otherwise, fail to move files to non-existing dirs. overwrite \u2013 A flag, if true, overwrite an existing destination file. idify \u2013 A flag, if true, remove whitespace and funky characters in the destination filename. logfile \u2013 A file object to write log entries to, or None, in which case no log is written out. Source code in beancount/ingest/file.py def file(importer_config, files_or_directories, destination, dry_run=False, mkdirs=False, overwrite=False, idify=False, logfile=None): \"\"\"File importable files under a destination directory. Given an importer configuration object, search for files that can be imported under the given list of files or directories and moved them under the given destination directory with the date computed by the module prepended to the filename. If the date cannot be extracted, use a reasonable default for the date (e.g. the last modified time of the file itself). If 'mkdirs' is True, create the destination directories before moving the files. Args: importer_config: A list of importer instances that define the config. files_or_directories: a list of files of directories to walk recursively and hunt for files to import. destination: A string, the root destination directory where the files are to be filed. The files are organized there under a hierarchy mirroring that of the chart of accounts. dry_run: A flag, if true, don't actually move the files. mkdirs: A flag, if true, make all the intervening directories; otherwise, fail to move files to non-existing dirs. overwrite: A flag, if true, overwrite an existing destination file. idify: A flag, if true, remove whitespace and funky characters in the destination filename. logfile: A file object to write log entries to, or None, in which case no log is written out. \"\"\" jobs = [] has_errors = False for filename, importers in identify.find_imports(importer_config, files_or_directories, logfile): # If we're debugging, print out the match text. # This option is useful when we're building our importer configuration, # to figure out which patterns to create as unique signatures. if not importers: continue # Process a single file. new_fullname = file_one_file(filename, importers, destination, idify, logfile) if new_fullname is None: continue # Check if the destination directory exists. new_dirname = path.dirname(new_fullname) if not path.exists(new_dirname) and not mkdirs: logging.error(\"Destination directory '{}' does not exist.\".format(new_dirname)) has_errors = True continue # Check if the destination file already exists; we don't want to clobber # it by accident. if not overwrite and path.exists(new_fullname): logging.error(\"Destination file '{}' already exists.\".format(new_fullname)) has_errors = True continue jobs.append((filename, new_fullname)) # Check if any two imported files would be colliding in their destination # name, before we move anything. destmap = collections.defaultdict(list) for src, dest in jobs: destmap[dest].append(src) for dest, sources in destmap.items(): if len(sources) != 1: logging.error(\"Collision in destination filenames '{}': from {}.\".format( dest, \", \".join([\"'{}'\".format(source) for source in sources]))) has_errors = True # If there are any errors, just don't do anything at all. This is a nicer # behaviour than moving just *some* files. if dry_run or has_errors: return # Actually carry out the moving job. for old_filename, new_filename in jobs: move_xdev_file(old_filename, new_filename, mkdirs) return jobs beancount.ingest.file.file_one_file(filename, importers, destination, idify=False, logfile=None) \uf0c1 Move a single filename using its matched importers. Parameters: filename \u2013 A string, the name of the downloaded file to be processed. importers \u2013 A list of importer instances that handle this file. destination \u2013 A string, the root destination directory where the files are to be filed. The files are organized there under a hierarchy mirroring that of the chart of accounts. idify \u2013 A flag, if true, remove whitespace and funky characters in the destination filename. logfile \u2013 A file object to write log entries to, or None, in which case no log is written out. Returns: The full new destination filename on success, and None if there was an error. Source code in beancount/ingest/file.py def file_one_file(filename, importers, destination, idify=False, logfile=None): \"\"\"Move a single filename using its matched importers. Args: filename: A string, the name of the downloaded file to be processed. importers: A list of importer instances that handle this file. destination: A string, the root destination directory where the files are to be filed. The files are organized there under a hierarchy mirroring that of the chart of accounts. idify: A flag, if true, remove whitespace and funky characters in the destination filename. logfile: A file object to write log entries to, or None, in which case no log is written out. Returns: The full new destination filename on success, and None if there was an error. \"\"\" # Create an object to cache all the conversions between the importers # and phases and what-not. file = cache.get_file(filename) # Get the account corresponding to the file. file_accounts = [] for index, importer in enumerate(importers): try: account_ = importer.file_account(file) except Exception as exc: account_ = None logging.exception(\"Importer %s.file_account() raised an unexpected error: %s\", importer.name(), exc) if account_ is not None: file_accounts.append(account_) file_accounts_set = set(file_accounts) if not file_accounts_set: logging.error(\"No account provided by importers: {}\".format( \", \".join(imp.name() for imp in importers))) return None if len(file_accounts_set) > 1: logging.warning(\"Ambiguous accounts from many importers: {}\".format( ', '.join(file_accounts_set))) # Note: Don't exit; select the first matching importer's account. file_account = file_accounts.pop(0) # Given multiple importers, select the first one that was yielded to # obtain the date and process the filename. importer = importers[0] # Compute the date from the last modified time. mtime = path.getmtime(filename) mtime_date = datetime.datetime.fromtimestamp(mtime).date() # Try to get the file's date by calling a module support function. The # module may be able to extract the date from the filename, from the # contents of the file itself (e.g. scraping some text from the PDF # contents, or grabbing the last line of a CSV file). try: date = importer.file_date(file) except Exception as exc: logging.exception(\"Importer %s.file_date() raised an unexpected error: %s\", importer.name(), exc) date = None if date is None: # Fallback on the last modified time of the file. date = mtime_date date_source = 'mtime' else: date_source = 'contents' # Apply filename renaming, if implemented. # Otherwise clean up the filename. try: clean_filename = importer.file_name(file) # Warn the importer implementor if a name is returned and it's an # absolute filename. if clean_filename and (path.isabs(clean_filename) or os.sep in clean_filename): logging.error((\"The importer '%s' file_name() method should return a relative \" \"filename; the filename '%s' is absolute or contains path \" \"separators\"), importer.name(), clean_filename) except Exception as exc: logging.exception(\"Importer %s.file_name() raised an unexpected error: %s\", importer.name(), exc) clean_filename = None if clean_filename is None: # If no filename has been provided, use the basename. clean_filename = path.basename(file.name) elif re.match(r'\\d\\d\\d\\d-\\d\\d-\\d\\d', clean_filename): logging.error(\"The importer '%s' file_name() method should not date the \" \"returned filename. Implement file_date() instead.\") # We need a simple filename; remove the directory part if there is one. clean_basename = path.basename(clean_filename) # Remove whitespace if requested. if idify: clean_basename = misc_utils.idify(clean_basename) # Prepend the date prefix. new_filename = '{0:%Y-%m-%d}.{1}'.format(date, clean_basename) # Prepend destination directory. new_fullname = path.normpath(path.join(destination, file_account.replace(account.sep, os.sep), new_filename)) # Print the filename and which modules matched. if logfile is not None: logfile.write('Importer: {}\\n'.format(importer.name() if importer else '-')) logfile.write('Account: {}\\n'.format(file_account)) logfile.write('Date: {} (from {})\\n'.format(date, date_source)) logfile.write('Destination: {}\\n'.format(new_fullname)) logfile.write('\\n') return new_fullname beancount.ingest.file.move_xdev_file(src_filename, dst_filename, mkdirs=False) \uf0c1 Move a file, potentially across devices. Parameters: src_filename \u2013 A string, the name of the file to copy. dst_filename \u2013 A string, where to copy the file. mkdirs \u2013 A flag, true if we should create a non-existing destination directory. Source code in beancount/ingest/file.py def move_xdev_file(src_filename, dst_filename, mkdirs=False): \"\"\"Move a file, potentially across devices. Args: src_filename: A string, the name of the file to copy. dst_filename: A string, where to copy the file. mkdirs: A flag, true if we should create a non-existing destination directory. \"\"\" # Create missing directory if required. dst_dirname = path.dirname(dst_filename) if mkdirs: if not path.exists(dst_dirname): os.makedirs(dst_dirname) else: if not path.exists(dst_dirname): raise OSError(\"Destination directory '{}' does not exist.\".format(dst_dirname)) # Copy the file to its new name. shutil.copyfile(src_filename, dst_filename) # Remove the old file. Note that we copy and remove to support # cross-device moves, because it's sensible that the destination might # be on an encrypted device. os.remove(src_filename) beancount.ingest.file.run(args, parser, importers_list, files_or_directories, hooks=None) \uf0c1 Run the subcommand. Source code in beancount/ingest/file.py def run(args, parser, importers_list, files_or_directories, hooks=None): \"\"\"Run the subcommand.\"\"\" # If the output directory is not specified, move the files at the root where # the import configuration file is located. (Providing this default seems # better than using a required option.) if args.output_dir is None: if hasattr(args, 'config'): args.output_dir = path.dirname(path.abspath(args.config)) else: import __main__ # pylint: disable=import-outside-toplevel args.output_dir = path.dirname(path.abspath(__main__.__file__)) # Make sure the output directory exists. if not path.exists(args.output_dir): parser.error('Output directory \"{}\" does not exist.'.format(args.output_dir)) file(importers_list, files_or_directories, args.output_dir, dry_run=args.dry_run, mkdirs=True, overwrite=args.overwrite, idify=True, logfile=sys.stdout) return 0 beancount.ingest.identify \uf0c1 Identify script. Read an import script and a list of downloaded filenames or directories of 2downloaded files, and for each of those files, identify which importer it should be associated with. beancount.ingest.identify.add_arguments(parser) \uf0c1 Add arguments for the identify command. Source code in beancount/ingest/identify.py def add_arguments(parser): \"\"\"Add arguments for the identify command.\"\"\" beancount.ingest.identify.find_imports(importer_config, files_or_directories, logfile=None) \uf0c1 Given an importer configuration, search for files that can be imported in the list of files or directories, run the signature checks on them and return a list of (filename, importers), where 'importers' is a list of importers that matched the file. Parameters: importer_config \u2013 a list of importer instances that define the config. files_or_directories \u2013 a list of files of directories to walk recursively and hunt for files to import. logfile \u2013 A file object to write log entries to, or None, in which case no log is written out. Yields: Triples of filename found, textified contents of the file, and list of importers matching this file. Source code in beancount/ingest/identify.py def find_imports(importer_config, files_or_directories, logfile=None): \"\"\"Given an importer configuration, search for files that can be imported in the list of files or directories, run the signature checks on them and return a list of (filename, importers), where 'importers' is a list of importers that matched the file. Args: importer_config: a list of importer instances that define the config. files_or_directories: a list of files of directories to walk recursively and hunt for files to import. logfile: A file object to write log entries to, or None, in which case no log is written out. Yields: Triples of filename found, textified contents of the file, and list of importers matching this file. \"\"\" # Iterate over all files found; accumulate the entries by identification. for filename in file_utils.find_files(files_or_directories): if logfile is not None: logfile.write(SECTION.format(filename)) logfile.write('\\n') # Skip files that are simply too large. size = path.getsize(filename) if size > FILE_TOO_LARGE_THRESHOLD: logging.warning(\"File too large: '{}' ({} bytes); skipping.\".format( filename, size)) continue # For each of the sources the user has declared, identify which # match the text. file = cache.get_file(filename) matching_importers = [] for importer in importer_config: try: matched = importer.identify(file) if matched: matching_importers.append(importer) except Exception as exc: logging.exception(\"Importer %s.identify() raised an unexpected error: %s\", importer.name(), exc) yield (filename, matching_importers) beancount.ingest.identify.identify(importers_list, files_or_directories) \uf0c1 Run the identification loop. Parameters: importers_list \u2013 A list of importer instances. files_or_directories \u2013 A list of strings, files or directories. Source code in beancount/ingest/identify.py def identify(importers_list, files_or_directories): \"\"\"Run the identification loop. Args: importers_list: A list of importer instances. files_or_directories: A list of strings, files or directories. \"\"\" logfile = sys.stdout for filename, importers in find_imports(importers_list, files_or_directories, logfile=logfile): file = cache.get_file(filename) for importer in importers: logfile.write('Importer: {}\\n'.format(importer.name() if importer else '-')) logfile.write('Account: {}\\n'.format(importer.file_account(file))) logfile.write('\\n') beancount.ingest.identify.run(_, __, importers_list, files_or_directories, hooks=None) \uf0c1 Run the subcommand. Source code in beancount/ingest/identify.py def run(_, __, importers_list, files_or_directories, hooks=None): \"\"\"Run the subcommand.\"\"\" return identify(importers_list, files_or_directories) beancount.ingest.importer \uf0c1 Importer protocol. All importers must comply with this interface and implement at least some of its methods. A configuration consists in a simple list of such importer instances. The importer processes run through the importers, calling some of its methods in order to identify, extract and file the downloaded files. Each of the methods accept a cache.FileMemo object which has a 'name' attribute with the filename to process, but which also provides a place to cache conversions. Use its convert() method whenever possible to avoid carrying out the same conversion multiple times. See beancount.ingest.cache for more details. Synopsis: name(): Return a unique identifier for the importer instance. identify(): Return true if the identifier is able to process the file. extract(): Extract directives from a file's contents and return of list of entries. file_account(): Return an account name associated with the given file for this importer. file_date(): Return a date associated with the downloaded file (e.g., the statement date). file_name(): Return a cleaned up filename for storage (optional). Just to be clear: Although this importer will not raise NotImplementedError exceptions (it returns default values for each method), you NEED to derive from it in order to do anything meaningful. Simply instantiating this importer will not match not provide any useful information. It just defines the protocol for all importers. beancount.ingest.importer.ImporterProtocol \uf0c1 Interface that all source importers need to comply with. beancount.ingest.importer.ImporterProtocol.__str__(self) special \uf0c1 Return a unique id/name for this importer. Returns: A string which uniquely identifies this importer. Source code in beancount/ingest/importer.py def name(self): \"\"\"Return a unique id/name for this importer. Returns: A string which uniquely identifies this importer. \"\"\" cls = self.__class__ return '{}.{}'.format(cls.__module__, cls.__name__) beancount.ingest.importer.ImporterProtocol.extract(self, file, existing_entries=None) \uf0c1 Extract transactions from a file. If the importer would like to flag a returned transaction as a known duplicate, it may opt to set the special flag \" duplicate \" to True, and the transaction should be treated as a duplicate by the extraction code. This is a way to let the importer use particular information about previously imported transactions in order to flag them as duplicates. For example, if an importer has a way to get a persistent unique id for each of the imported transactions. (See this discussion for context: https://groups.google.com/d/msg/beancount/0iV-ipBJb8g/-uk4wsH2AgAJ) Parameters: file \u2013 A cache.FileMemo instance. existing_entries \u2013 An optional list of existing directives loaded from the ledger which is intended to contain the extracted entries. This is only provided if the user provides them via a flag in the extractor program. Returns: A list of new, imported directives (usually mostly Transactions) extracted from the file. Source code in beancount/ingest/importer.py def extract(self, file, existing_entries=None): \"\"\"Extract transactions from a file. If the importer would like to flag a returned transaction as a known duplicate, it may opt to set the special flag \"__duplicate__\" to True, and the transaction should be treated as a duplicate by the extraction code. This is a way to let the importer use particular information about previously imported transactions in order to flag them as duplicates. For example, if an importer has a way to get a persistent unique id for each of the imported transactions. (See this discussion for context: https://groups.google.com/d/msg/beancount/0iV-ipBJb8g/-uk4wsH2AgAJ) Args: file: A cache.FileMemo instance. existing_entries: An optional list of existing directives loaded from the ledger which is intended to contain the extracted entries. This is only provided if the user provides them via a flag in the extractor program. Returns: A list of new, imported directives (usually mostly Transactions) extracted from the file. \"\"\" beancount.ingest.importer.ImporterProtocol.file_account(self, file) \uf0c1 Return an account associated with the given file. Note: If you don't implement this method you won't be able to move the files into its preservation hierarchy; the bean-file command won't work. Also, normally the returned account is not a function of the input file--just of the importer--but it is provided anyhow. Parameters: file \u2013 A cache.FileMemo instance. Returns: The name of the account that corresponds to this importer. Source code in beancount/ingest/importer.py def file_account(self, file): \"\"\"Return an account associated with the given file. Note: If you don't implement this method you won't be able to move the files into its preservation hierarchy; the bean-file command won't work. Also, normally the returned account is not a function of the input file--just of the importer--but it is provided anyhow. Args: file: A cache.FileMemo instance. Returns: The name of the account that corresponds to this importer. \"\"\" beancount.ingest.importer.ImporterProtocol.file_date(self, file) \uf0c1 Attempt to obtain a date that corresponds to the given file. Parameters: file \u2013 A cache.FileMemo instance. Returns: A date object, if successful, or None if a date could not be extracted. (If no date is returned, the file creation time is used. This is the default.) Source code in beancount/ingest/importer.py def file_date(self, file): \"\"\"Attempt to obtain a date that corresponds to the given file. Args: file: A cache.FileMemo instance. Returns: A date object, if successful, or None if a date could not be extracted. (If no date is returned, the file creation time is used. This is the default.) \"\"\" beancount.ingest.importer.ImporterProtocol.file_name(self, file) \uf0c1 A filter that optionally renames a file before filing. This is used to make tidy filenames for filed/stored document files. If you don't implement this and return None, the same filename is used. Note that if you return a filename, a simple, RELATIVE filename must be returned, not an absolute filename. Parameters: file \u2013 A cache.FileMemo instance. Returns: The tidied up, new filename to store it as. Source code in beancount/ingest/importer.py def file_name(self, file): \"\"\"A filter that optionally renames a file before filing. This is used to make tidy filenames for filed/stored document files. If you don't implement this and return None, the same filename is used. Note that if you return a filename, a simple, RELATIVE filename must be returned, not an absolute filename. Args: file: A cache.FileMemo instance. Returns: The tidied up, new filename to store it as. \"\"\" beancount.ingest.importer.ImporterProtocol.identify(self, file) \uf0c1 Return true if this importer matches the given file. Parameters: file \u2013 A cache.FileMemo instance. Returns: A boolean, true if this importer can handle this file. Source code in beancount/ingest/importer.py def identify(self, file): \"\"\"Return true if this importer matches the given file. Args: file: A cache.FileMemo instance. Returns: A boolean, true if this importer can handle this file. \"\"\" beancount.ingest.importer.ImporterProtocol.name(self) \uf0c1 Return a unique id/name for this importer. Returns: A string which uniquely identifies this importer. Source code in beancount/ingest/importer.py def name(self): \"\"\"Return a unique id/name for this importer. Returns: A string which uniquely identifies this importer. \"\"\" cls = self.__class__ return '{}.{}'.format(cls.__module__, cls.__name__) beancount.ingest.importers special \uf0c1 beancount.ingest.importers.config \uf0c1 Mixin to add support for configuring importers with multiple accounts. This importer implements some simple common functionality to create importers which accept a long number of account names or regular expressions on the set of account names. This is inspired by functionality in the importers in the previous iteration of the ingest code, which used to be its own project. beancount.ingest.importers.config.ConfigImporterMixin \uf0c1 A mixin class which supports configuration of account names. Mix this into the implementation of a importer.ImporterProtocol. beancount.ingest.importers.config.ConfigImporterMixin.__init__(self, config) special \uf0c1 Provide a list of accounts and regexps as configuration to the importer. Parameters: config \u2013 A dict of configuration accounts, that must match the values declared in the class' REQUIRED_CONFIG. Source code in beancount/ingest/importers/config.py def __init__(self, config): \"\"\"Provide a list of accounts and regexps as configuration to the importer. Args: config: A dict of configuration accounts, that must match the values declared in the class' REQUIRED_CONFIG. \"\"\" super().__init__() # Check that the required configuration values are present. assert isinstance(config, dict), \"Configuration must be a dict type\" if not self._verify_config(config): raise ValueError(\"Invalid config {}, requires {}\".format( config, self.REQUIRED_CONFIG)) self.config = config beancount.ingest.importers.csv \uf0c1 CSV importer. beancount.ingest.importers.csv.Col ( Enum ) \uf0c1 The set of interpretable columns. beancount.ingest.importers.csv.Importer ( IdentifyMixin , FilingMixin ) \uf0c1 Importer for CSV files. beancount.ingest.importers.csv.Importer.__init__(self, config, account, currency, regexps=None, skip_lines=0, last4_map=None, categorizer=None, institution=None, debug=False, csv_dialect='excel', dateutil_kwds=None, narration_sep='; ', encoding=None, invert_sign=False, **kwds) special \uf0c1 Constructor. Parameters: config \u2013 A dict of Col enum types to the names or indexes of the columns. account \u2013 An account string, the account to post this to. currency \u2013 A currency string, the currency of this account. regexps \u2013 A list of regular expression strings. skip_lines ( int ) \u2013 Skip first x (garbage) lines of file. last4_map ( Optional[Dict] ) \u2013 A dict that maps last 4 digits of the card to a friendly string. categorizer ( Optional[Callable] ) \u2013 A callable that attaches the other posting (usually expenses) to a transaction with only single posting. institution ( Optional[str] ) \u2013 An optional name of an institution to rename the files to. debug ( bool ) \u2013 Whether or not to print debug information csv_dialect ( Union[str, csv.Dialect] ) \u2013 A csv dialect given either as string or as instance or subclass of csv.Dialect . dateutil_kwds ( Optional[Dict] ) \u2013 An optional dict defining the dateutil parser kwargs. narration_sep ( str ) \u2013 A string, a separator to use for splitting up the payee and narration fields of a source field. encoding ( Optional[str] ) \u2013 An optional encoding for the file. Typically useful for files encoded in 'latin1' instead of 'utf-8' (the default). invert_sign ( Optional[bool] ) \u2013 If true, invert the amount's sign unconditionally. **kwds \u2013 Extra keyword arguments to provide to the base mixins. Source code in beancount/ingest/importers/csv.py def __init__(self, config, account, currency, regexps=None, skip_lines: int = 0, last4_map: Optional[Dict] = None, categorizer: Optional[Callable] = None, institution: Optional[str] = None, debug: bool = False, csv_dialect: Union[str, csv.Dialect] = 'excel', dateutil_kwds: Optional[Dict] = None, narration_sep: str = '; ', encoding: Optional[str] = None, invert_sign: Optional[bool] = False, **kwds): \"\"\"Constructor. Args: config: A dict of Col enum types to the names or indexes of the columns. account: An account string, the account to post this to. currency: A currency string, the currency of this account. regexps: A list of regular expression strings. skip_lines: Skip first x (garbage) lines of file. last4_map: A dict that maps last 4 digits of the card to a friendly string. categorizer: A callable that attaches the other posting (usually expenses) to a transaction with only single posting. institution: An optional name of an institution to rename the files to. debug: Whether or not to print debug information csv_dialect: A `csv` dialect given either as string or as instance or subclass of `csv.Dialect`. dateutil_kwds: An optional dict defining the dateutil parser kwargs. narration_sep: A string, a separator to use for splitting up the payee and narration fields of a source field. encoding: An optional encoding for the file. Typically useful for files encoded in 'latin1' instead of 'utf-8' (the default). invert_sign: If true, invert the amount's sign unconditionally. **kwds: Extra keyword arguments to provide to the base mixins. \"\"\" assert isinstance(config, dict), \"Invalid type: {}\".format(config) self.config = config self.currency = currency assert isinstance(skip_lines, int) self.skip_lines = skip_lines self.last4_map = last4_map or {} self.debug = debug self.dateutil_kwds = dateutil_kwds self.csv_dialect = csv_dialect self.narration_sep = narration_sep self.encoding = encoding self.invert_sign = invert_sign self.categorizer = categorizer # Prepare kwds for filing mixin. kwds['filing'] = account if institution: prefix = kwds.get('prefix', None) assert prefix is None kwds['prefix'] = institution # Prepare kwds for identifier mixin. if isinstance(regexps, str): regexps = [regexps] matchers = kwds.setdefault('matchers', []) matchers.append(('mime', 'text/csv')) if regexps: for regexp in regexps: matchers.append(('content', regexp)) super().__init__(**kwds) beancount.ingest.importers.csv.Importer.extract(self, file, existing_entries=None) \uf0c1 Extract transactions from a file. If the importer would like to flag a returned transaction as a known duplicate, it may opt to set the special flag \" duplicate \" to True, and the transaction should be treated as a duplicate by the extraction code. This is a way to let the importer use particular information about previously imported transactions in order to flag them as duplicates. For example, if an importer has a way to get a persistent unique id for each of the imported transactions. (See this discussion for context: https://groups.google.com/d/msg/beancount/0iV-ipBJb8g/-uk4wsH2AgAJ) Parameters: file \u2013 A cache.FileMemo instance. existing_entries \u2013 An optional list of existing directives loaded from the ledger which is intended to contain the extracted entries. This is only provided if the user provides them via a flag in the extractor program. Returns: A list of new, imported directives (usually mostly Transactions) extracted from the file. Source code in beancount/ingest/importers/csv.py def extract(self, file, existing_entries=None): account = self.file_account(file) entries = [] # Normalize the configuration to fetch by index. iconfig, has_header = normalize_config( self.config, file.head(), self.csv_dialect, self.skip_lines) reader = iter(csv.reader(open(file.name, encoding=self.encoding), dialect=self.csv_dialect)) # Skip garbage lines for _ in range(self.skip_lines): next(reader) # Skip header, if one was detected. if has_header: next(reader) def get(row, ftype): try: return row[iconfig[ftype]] if ftype in iconfig else None except IndexError: # FIXME: this should not happen return None # Parse all the transactions. first_row = last_row = None for index, row in enumerate(reader, 1): if not row: continue if row[0].startswith('#'): continue # If debugging, print out the rows. if self.debug: print(row) if first_row is None: first_row = row last_row = row # Extract the data we need from the row, based on the configuration. date = get(row, Col.DATE) txn_date = get(row, Col.TXN_DATE) txn_time = get(row, Col.TXN_TIME) payee = get(row, Col.PAYEE) if payee: payee = payee.strip() fields = filter(None, [get(row, field) for field in (Col.NARRATION1, Col.NARRATION2, Col.NARRATION3)]) narration = self.narration_sep.join( field.strip() for field in fields).replace('\\n', '; ') tag = get(row, Col.TAG) tags = {tag} if tag is not None else data.EMPTY_SET link = get(row, Col.REFERENCE_ID) links = {link} if link is not None else data.EMPTY_SET last4 = get(row, Col.LAST4) balance = get(row, Col.BALANCE) # Create a transaction meta = data.new_metadata(file.name, index) if txn_date is not None: meta['date'] = parse_date_liberally(txn_date, self.dateutil_kwds) if txn_time is not None: meta['time'] = str(dateutil.parser.parse(txn_time).time()) if balance is not None: meta['balance'] = D(balance) if last4: last4_friendly = self.last4_map.get(last4.strip()) meta['card'] = last4_friendly if last4_friendly else last4 date = parse_date_liberally(date, self.dateutil_kwds) txn = data.Transaction(meta, date, self.FLAG, payee, narration, tags, links, []) # Attach one posting to the transaction amount_debit, amount_credit = self.get_amounts(iconfig, row) # Skip empty transactions if amount_debit is None and amount_credit is None: continue for amount in [amount_debit, amount_credit]: if amount is None: continue if self.invert_sign: amount = -amount units = Amount(amount, self.currency) txn.postings.append( data.Posting(account, units, None, None, None, None)) # Attach the other posting(s) to the transaction. if isinstance(self.categorizer, collections.abc.Callable): txn = self.categorizer(txn) # Add the transaction to the output list entries.append(txn) # Figure out if the file is in ascending or descending order. first_date = parse_date_liberally(get(first_row, Col.DATE), self.dateutil_kwds) last_date = parse_date_liberally(get(last_row, Col.DATE), self.dateutil_kwds) is_ascending = first_date < last_date # Reverse the list if the file is in descending order if not is_ascending: entries = list(reversed(entries)) # Add a balance entry if possible if Col.BALANCE in iconfig and entries: entry = entries[-1] date = entry.date + datetime.timedelta(days=1) balance = entry.meta.get('balance', None) if balance is not None: meta = data.new_metadata(file.name, index) entries.append( data.Balance(meta, date, account, Amount(balance, self.currency), None, None)) # Remove the 'balance' metadata. for entry in entries: entry.meta.pop('balance', None) return entries beancount.ingest.importers.csv.Importer.file_date(self, file) \uf0c1 Get the maximum date from the file. Source code in beancount/ingest/importers/csv.py def file_date(self, file): \"Get the maximum date from the file.\" iconfig, has_header = normalize_config( self.config, file.head(), self.csv_dialect, self.skip_lines) if Col.DATE in iconfig: reader = iter(csv.reader(open(file.name), dialect=self.csv_dialect)) for _ in range(self.skip_lines): next(reader) if has_header: next(reader) max_date = None for row in reader: if not row: continue if row[0].startswith('#'): continue date_str = row[iconfig[Col.DATE]] date = parse_date_liberally(date_str, self.dateutil_kwds) if max_date is None or date > max_date: max_date = date return max_date beancount.ingest.importers.csv.Importer.get_amounts(self, iconfig, row, allow_zero_amounts=False) \uf0c1 See function get_amounts() for details. This method is present to allow clients to override it in order to deal with special cases, e.g., columns with currency symbols in them. Source code in beancount/ingest/importers/csv.py def get_amounts(self, iconfig, row, allow_zero_amounts=False): \"\"\"See function get_amounts() for details. This method is present to allow clients to override it in order to deal with special cases, e.g., columns with currency symbols in them. \"\"\" return get_amounts(iconfig, row, allow_zero_amounts) beancount.ingest.importers.csv.get_amounts(iconfig, row, allow_zero_amounts=False) \uf0c1 Get the amount columns of a row. Parameters: iconfig \u2013 A dict of Col to row index. row \u2013 A row array containing the values of the given row. allow_zero_amounts \u2013 Is a transaction with amount D('0.00') okay? If not, return (None, None). Returns: A pair of (debit-amount, credit-amount), both of which are either an instance of Decimal or None, or not available. Source code in beancount/ingest/importers/csv.py def get_amounts(iconfig, row, allow_zero_amounts=False): \"\"\"Get the amount columns of a row. Args: iconfig: A dict of Col to row index. row: A row array containing the values of the given row. allow_zero_amounts: Is a transaction with amount D('0.00') okay? If not, return (None, None). Returns: A pair of (debit-amount, credit-amount), both of which are either an instance of Decimal or None, or not available. \"\"\" debit, credit = None, None if Col.AMOUNT in iconfig: credit = row[iconfig[Col.AMOUNT]] else: debit, credit = [row[iconfig[col]] if col in iconfig else None for col in [Col.AMOUNT_DEBIT, Col.AMOUNT_CREDIT]] # If zero amounts aren't allowed, return null value. is_zero_amount = ((credit is not None and D(credit) == ZERO) and (debit is not None and D(debit) == ZERO)) if not allow_zero_amounts and is_zero_amount: return (None, None) return (-D(debit) if debit else None, D(credit) if credit else None) beancount.ingest.importers.csv.normalize_config(config, head, dialect='excel', skip_lines=0) \uf0c1 Using the header line, convert the configuration field name lookups to int indexes. Parameters: config \u2013 A dict of Col types to string or indexes. head \u2013 A string, some decent number of bytes of the head of the file. dialect \u2013 A dialect definition to parse the header skip_lines ( int ) \u2013 Skip first x (garbage) lines of file. Returns: A pair of A dict of Col types to integer indexes of the fields, and a boolean, true if the file has a header. Exceptions: ValueError \u2013 If there is no header and the configuration does not consist entirely of integer indexes. Source code in beancount/ingest/importers/csv.py def normalize_config(config, head, dialect='excel', skip_lines: int = 0): \"\"\"Using the header line, convert the configuration field name lookups to int indexes. Args: config: A dict of Col types to string or indexes. head: A string, some decent number of bytes of the head of the file. dialect: A dialect definition to parse the header skip_lines: Skip first x (garbage) lines of file. Returns: A pair of A dict of Col types to integer indexes of the fields, and a boolean, true if the file has a header. Raises: ValueError: If there is no header and the configuration does not consist entirely of integer indexes. \"\"\" # Skip garbage lines before sniffing the header assert isinstance(skip_lines, int) assert skip_lines >= 0 for _ in range(skip_lines): head = head[head.find('\\n')+1:] has_header = csv.Sniffer().has_header(head) if has_header: header = next(csv.reader(io.StringIO(head), dialect=dialect)) field_map = {field_name.strip(): index for index, field_name in enumerate(header)} index_config = {} for field_type, field in config.items(): if isinstance(field, str): field = field_map[field] index_config[field_type] = field else: if any(not isinstance(field, int) for field_type, field in config.items()): raise ValueError(\"CSV config without header has non-index fields: \" \"{}\".format(config)) index_config = config return index_config, has_header beancount.ingest.importers.fileonly \uf0c1 A simplistic importer that can be used just to file away some download. Sometimes you just want to save and accumulate data beancount.ingest.importers.fileonly.Importer ( FilingMixin , IdentifyMixin ) \uf0c1 An importer that supports only matching (identification) and filing. beancount.ingest.importers.mixins special \uf0c1 beancount.ingest.importers.mixins.config \uf0c1 Base class that implements configuration and a filing account. beancount.ingest.importers.mixins.config.ConfigMixin ( ImporterProtocol ) \uf0c1 beancount.ingest.importers.mixins.config.ConfigMixin.__init__(self, **kwds) special \uf0c1 Pull 'config' from kwds. Source code in beancount/ingest/importers/mixins/config.py def __init__(self, **kwds): \"\"\"Pull 'config' from kwds.\"\"\" config = kwds.pop('config', None) schema = self.REQUIRED_CONFIG if config or schema: assert config is not None assert schema is not None self.config = validate_config(config, config, self) else: self.config = None super().__init__(**kwds) beancount.ingest.importers.mixins.config.validate_config(config, schema, importer) \uf0c1 Check the configuration account provided by the user against the accounts required by the source importer. Parameters: config \u2013 A config dict of actual values on an importer. schema \u2013 A dict of declarations of required values. Exceptions: ValueError \u2013 If the configuration is invalid. Returns: A validated configuration dict. Source code in beancount/ingest/importers/mixins/config.py def validate_config(config, schema, importer): \"\"\"Check the configuration account provided by the user against the accounts required by the source importer. Args: config: A config dict of actual values on an importer. schema: A dict of declarations of required values. Raises: ValueError: If the configuration is invalid. Returns: A validated configuration dict. \"\"\" provided_options = set(config) required_options = set(schema) for option in (required_options - provided_options): raise ValueError(\"Missing value from user configuration for importer {}: {}\".format( importer.__class__.__name__, option)) for option in (provided_options - required_options): raise ValueError(\"Unknown value in user configuration for importer {}: {}\".format( importer.__class__.__name__, option)) # FIXME: Validate types as well, including account type as a default. # FIXME: Here we could validate account names by looking them up from the # existing ledger. return config beancount.ingest.importers.mixins.filing \uf0c1 Base class that implements filing account. It also sports an optional prefix to prepend to the renamed filename. Typically you can put the name of the institution there, so you get a renamed filename like this: YYYY-MM-DD.institution.Original_File_Name.pdf beancount.ingest.importers.mixins.filing.FilingMixin ( ImporterProtocol ) \uf0c1 beancount.ingest.importers.mixins.filing.FilingMixin.__init__(self, **kwds) special \uf0c1 Pull 'filing' and 'prefix' from kwds. Parameters: filing \u2013 The name of the account to file to. prefix \u2013 The name of the institution prefix to insert. Source code in beancount/ingest/importers/mixins/filing.py 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) beancount.ingest.importers.mixins.filing.FilingMixin.file_account(self, file) \uf0c1 Return an account associated with the given file. Note: If you don't implement this method you won't be able to move the files into its preservation hierarchy; the bean-file command won't work. Also, normally the returned account is not a function of the input file--just of the importer--but it is provided anyhow. Parameters: file \u2013 A cache.FileMemo instance. Returns: The name of the account that corresponds to this importer. Source code in beancount/ingest/importers/mixins/filing.py def file_account(self, file): return self.filing_account beancount.ingest.importers.mixins.filing.FilingMixin.file_name(self, file) \uf0c1 Return the optional renamed account filename. Source code in beancount/ingest/importers/mixins/filing.py def file_name(self, file): \"\"\"Return the optional renamed account filename.\"\"\" supername = super().file_name(file) if not self.prefix: return supername else: return '.'.join(filter(None, [self.prefix, supername or path.basename(file.name)])) beancount.ingest.importers.mixins.filing.FilingMixin.name(self) \uf0c1 Include the filing account in the name. Source code in beancount/ingest/importers/mixins/filing.py def name(self): \"\"\"Include the filing account in the name.\"\"\" return '{}: \"{}\"'.format(super().name(), self.filing_account) beancount.ingest.importers.mixins.identifier \uf0c1 Base class that implements identification using regular expressions. beancount.ingest.importers.mixins.identifier.IdentifyMixin ( ImporterProtocol ) \uf0c1 beancount.ingest.importers.mixins.identifier.IdentifyMixin.__init__(self, **kwds) special \uf0c1 Pull 'matchers' and 'converter' from kwds. Source code in beancount/ingest/importers/mixins/identifier.py def __init__(self, **kwds): \"\"\"Pull 'matchers' and 'converter' from kwds.\"\"\" self.remap = collections.defaultdict(list) matchers = kwds.pop('matchers', []) cls_matchers = getattr(self, 'matchers', []) assert isinstance(matchers, list) assert isinstance(cls_matchers, list) for part, regexp in itertools.chain(matchers, cls_matchers): assert part in _PARTS, repr(part) assert isinstance(regexp, str), repr(regexp) self.remap[part].append(re.compile(regexp)) # Converter is a fn(filename: Text) -> contents: Text. self.converter = kwds.pop('converter', getattr(self, 'converter', None)) super().__init__(**kwds) beancount.ingest.importers.mixins.identifier.IdentifyMixin.identify(self, file) \uf0c1 Return true if this importer matches the given file. Parameters: file \u2013 A cache.FileMemo instance. Returns: A boolean, true if this importer can handle this file. Source code in beancount/ingest/importers/mixins/identifier.py def identify(self, file): return identify(self.remap, self.converter, file) beancount.ingest.importers.mixins.identifier.identify(remap, converter, file) \uf0c1 Identify the contents of a file. Parameters: remap \u2013 A dict of 'part' to list-of-compiled-regexp objects, where each item is a specification to match against its part. The 'part' can be one of 'mime', 'filename' or 'content'. converter \u2013 A Returns: A boolean, true if the file is not rejected by the constraints. Source code in beancount/ingest/importers/mixins/identifier.py def identify(remap, converter, file): \"\"\"Identify the contents of a file. Args: remap: A dict of 'part' to list-of-compiled-regexp objects, where each item is a specification to match against its part. The 'part' can be one of 'mime', 'filename' or 'content'. converter: A Returns: A boolean, true if the file is not rejected by the constraints. \"\"\" if remap.get('mime', None): mimetype = file.convert(cache.mimetype) if not all(regexp.search(mimetype) for regexp in remap['mime']): return False if remap.get('filename', None): if not all(regexp.search(file.name) for regexp in remap['filename']): return False if remap.get('content', None): # If this is a text file, read the whole thing in memory. text = file.convert(converter or cache.contents) if not all(regexp.search(text) for regexp in remap['content']): return False return True beancount.ingest.importers.ofx \uf0c1 OFX file format importer for bank and credit card statements. https://en.wikipedia.org/wiki/Open_Financial_Exchange This importer will parse a single account in the OFX file. Instantiate it multiple times with different accounts if it has many accounts. It makes more sense to do it this way so that you can define your importer configuration account by account. Note that this importer is provided as an example and with no guarantees. It's not really super great. On the other hand, I've been using it for more than five years over multiple accounts, so it has been useful to me (it works, by some measure of \"works\"). If you need a more powerful or compliant OFX importer please consider either writing one or contributing changes. Also, this importer does its own very basic parsing; a better one would probably use (and depend on) the ofxparse module (see https://sites.google.com/site/ofxparse/). beancount.ingest.importers.ofx.BalanceType ( Enum ) \uf0c1 Type of Balance directive to be inserted. beancount.ingest.importers.ofx.Importer ( ImporterProtocol ) \uf0c1 An importer for Open Financial Exchange files. beancount.ingest.importers.ofx.Importer.__init__(self, acctid_regexp, account, basename=None, balance_type=) special \uf0c1 Create a new importer posting to the given account. Parameters: account \u2013 An account string, the account onto which to post all the amounts parsed. acctid_regexp \u2013 A regexp, to match against the tag of the OFX file. basename \u2013 An optional string, the name of the new files. balance_type \u2013 An enum of type BalanceType. Source code in beancount/ingest/importers/ofx.py def __init__(self, acctid_regexp, account, basename=None, balance_type=BalanceType.DECLARED): \"\"\"Create a new importer posting to the given account. Args: account: An account string, the account onto which to post all the amounts parsed. acctid_regexp: A regexp, to match against the tag of the OFX file. basename: An optional string, the name of the new files. balance_type: An enum of type BalanceType. \"\"\" self.acctid_regexp = acctid_regexp self.account = account self.basename = basename self.balance_type = balance_type beancount.ingest.importers.ofx.Importer.extract(self, file, existing_entries=None) \uf0c1 Extract a list of partially complete transactions from the file. Source code in beancount/ingest/importers/ofx.py def extract(self, file, existing_entries=None): \"\"\"Extract a list of partially complete transactions from the file.\"\"\" soup = bs4.BeautifulSoup(file.contents(), 'lxml') return extract(soup, file.name, self.acctid_regexp, self.account, self.FLAG, self.balance_type) beancount.ingest.importers.ofx.Importer.file_account(self, _) \uf0c1 Return the account against which we post transactions. Source code in beancount/ingest/importers/ofx.py def file_account(self, _): \"\"\"Return the account against which we post transactions.\"\"\" return self.account beancount.ingest.importers.ofx.Importer.file_date(self, file) \uf0c1 Return the optional renamed account filename. Source code in beancount/ingest/importers/ofx.py def file_date(self, file): \"\"\"Return the optional renamed account filename.\"\"\" return find_max_date(file.contents()) beancount.ingest.importers.ofx.Importer.file_name(self, file) \uf0c1 Return the optional renamed account filename. Source code in beancount/ingest/importers/ofx.py def file_name(self, file): \"\"\"Return the optional renamed account filename.\"\"\" if self.basename: return self.basename + path.splitext(file.name)[1] beancount.ingest.importers.ofx.Importer.identify(self, file) \uf0c1 Return true if this importer matches the given file. Parameters: file \u2013 A cache.FileMemo instance. Returns: A boolean, true if this importer can handle this file. Source code in beancount/ingest/importers/ofx.py def identify(self, file): # Match for a compatible MIME type. if file.mimetype() not in {'application/x-ofx', 'application/vnd.intu.qbo', 'application/vnd.intu.qfx'}: return False # Match the account id. return any(re.match(self.acctid_regexp, acctid) for acctid in find_acctids(file.contents())) beancount.ingest.importers.ofx.Importer.name(self) \uf0c1 Include the filing account in the name. Source code in beancount/ingest/importers/ofx.py def name(self): \"\"\"Include the filing account in the name.\"\"\" return '{}: \"{}\"'.format(super().name(), self.file_account(None)) beancount.ingest.importers.ofx.build_transaction(stmttrn, flag, account, currency) \uf0c1 Build a single transaction. Parameters: stmttrn \u2013 A bs4.element.Tag. flag \u2013 A single-character string. account \u2013 An account string, the account to insert. currency \u2013 A currency string. Returns: A Transaction instance. Source code in beancount/ingest/importers/ofx.py def build_transaction(stmttrn, flag, account, currency): \"\"\"Build a single transaction. Args: stmttrn: A bs4.element.Tag. flag: A single-character string. account: An account string, the account to insert. currency: A currency string. Returns: A Transaction instance. \"\"\" # Find the date. date = parse_ofx_time(find_child(stmttrn, 'dtposted')).date() # There's no distinct payee. payee = None # Construct a description that represents all the text content in the node. name = find_child(stmttrn, 'name', saxutils.unescape) memo = find_child(stmttrn, 'memo', saxutils.unescape) # Remove memos duplicated from the name. if memo == name: memo = None # Add the transaction type to the description, unless it's not useful. trntype = find_child(stmttrn, 'trntype', saxutils.unescape) if trntype in ('DEBIT', 'CREDIT'): trntype = None narration = ' / '.join(filter(None, [name, memo, trntype])) # Create a single posting for it; the user will have to manually categorize # the other side. number = find_child(stmttrn, 'trnamt', D) units = amount.Amount(number, currency) posting = data.Posting(account, units, None, None, None, None) # Build the transaction with a single leg. fileloc = data.new_metadata('', 0) return data.Transaction(fileloc, date, flag, payee, narration, data.EMPTY_SET, data.EMPTY_SET, [posting]) beancount.ingest.importers.ofx.extract(soup, filename, acctid_regexp, account, flag, balance_type) \uf0c1 Extract transactions from an OFX file. Parameters: soup \u2013 A BeautifulSoup root node. acctid_regexp \u2013 A regular expression string matching the account we're interested in. account \u2013 An account string onto which to post the amounts found in the file. flag \u2013 A single-character string. balance_type \u2013 An enum of type BalanceType. Returns: A sorted list of entries. Source code in beancount/ingest/importers/ofx.py def extract(soup, filename, acctid_regexp, account, flag, balance_type): \"\"\"Extract transactions from an OFX file. Args: soup: A BeautifulSoup root node. acctid_regexp: A regular expression string matching the account we're interested in. account: An account string onto which to post the amounts found in the file. flag: A single-character string. balance_type: An enum of type BalanceType. Returns: A sorted list of entries. \"\"\" new_entries = [] counter = itertools.count() for acctid, currency, transactions, balance in find_statement_transactions(soup): if not re.match(acctid_regexp, acctid): continue # Create Transaction directives. stmt_entries = [] for stmttrn in transactions: entry = build_transaction(stmttrn, flag, account, currency) entry = entry._replace(meta=data.new_metadata(filename, next(counter))) stmt_entries.append(entry) stmt_entries = data.sorted(stmt_entries) new_entries.extend(stmt_entries) # Create a Balance directive. if balance and balance_type is not BalanceType.NONE: date, number = balance if balance_type is BalanceType.LAST and stmt_entries: date = stmt_entries[-1].date # The Balance assertion occurs at the beginning of the date, so move # it to the following day. date += datetime.timedelta(days=1) meta = data.new_metadata(filename, next(counter)) balance_entry = data.Balance(meta, date, account, amount.Amount(number, currency), None, None) new_entries.append(balance_entry) return data.sorted(new_entries) beancount.ingest.importers.ofx.find_acctids(contents) \uf0c1 Find the list of tags. Parameters: contents \u2013 A string, the contents of the OFX file. Returns: A list of strings, the contents of the tags. Source code in beancount/ingest/importers/ofx.py def find_acctids(contents): \"\"\"Find the list of tags. Args: contents: A string, the contents of the OFX file. Returns: A list of strings, the contents of the tags. \"\"\" # Match the account id. Don't bother parsing the entire thing as XML, just # match the tag for this purpose. This'll work fine enough. for match in re.finditer('([^<]*)', contents): yield match.group(1) beancount.ingest.importers.ofx.find_child(node, name, conversion=None) \uf0c1 Find a child under the given node and return its value. Parameters: node \u2013 A bs4.element.Tag. name \u2013 A string, the name of the child node. conversion \u2013 A callable object used to convert the value to a new data type. Returns: A string, or None. Source code in beancount/ingest/importers/ofx.py def find_child(node, name, conversion=None): \"\"\"Find a child under the given node and return its value. Args: node: A bs4.element.Tag. name: A string, the name of the child node. conversion: A callable object used to convert the value to a new data type. Returns: A string, or None. \"\"\" child = node.find(name) if not child: return None value = child.contents[0].strip() if conversion: value = conversion(value) return value beancount.ingest.importers.ofx.find_currency(soup) \uf0c1 Find the first currency in the XML tree. Parameters: soup \u2013 A BeautifulSoup root node. Returns: A string, the first currency found in the file. Returns None if no currency is found. Source code in beancount/ingest/importers/ofx.py def find_currency(soup): \"\"\"Find the first currency in the XML tree. Args: soup: A BeautifulSoup root node. Returns: A string, the first currency found in the file. Returns None if no currency is found. \"\"\" for stmtrs in soup.find_all(re.compile('.*stmtrs$')): for currency_node in stmtrs.find_all('curdef'): currency = currency_node.contents[0] if currency is not None: return currency beancount.ingest.importers.ofx.find_max_date(contents) \uf0c1 Extract the report date from the file. Source code in beancount/ingest/importers/ofx.py def find_max_date(contents): \"\"\"Extract the report date from the file.\"\"\" soup = bs4.BeautifulSoup(contents, 'lxml') dates = [] for ledgerbal in soup.find_all('ledgerbal'): dtasof = ledgerbal.find('dtasof') dates.append(parse_ofx_time(dtasof.contents[0]).date()) if dates: return max(dates) beancount.ingest.importers.ofx.find_statement_transactions(soup) \uf0c1 Find the statement transaction sections in the file. Parameters: soup \u2013 A BeautifulSoup root node. Yields: A trip of An account id string, A currency string, A list of transaction nodes ( BeautifulSoup tags), and A (date, balance amount) for the . Source code in beancount/ingest/importers/ofx.py def find_statement_transactions(soup): \"\"\"Find the statement transaction sections in the file. Args: soup: A BeautifulSoup root node. Yields: A trip of An account id string, A currency string, A list of transaction nodes ( BeautifulSoup tags), and A (date, balance amount) for the . \"\"\" # Process STMTTRNRS and CCSTMTTRNRS tags. for stmtrs in soup.find_all(re.compile('.*stmtrs$')): # For each CURDEF tag. for currency_node in stmtrs.find_all('curdef'): currency = currency_node.contents[0].strip() # Extract ACCTID account information. acctid_node = stmtrs.find('acctid') if acctid_node: acctid = next(acctid_node.children).strip() else: acctid = '' # Get the LEDGERBAL node. There appears to be a single one for all # transaction lists. ledgerbal = stmtrs.find('ledgerbal') balance = None if ledgerbal: dtasof = find_child(ledgerbal, 'dtasof', parse_ofx_time).date() balamt = find_child(ledgerbal, 'balamt', D) balance = (dtasof, balamt) # Process transaction lists (regular or credit-card). for tranlist in stmtrs.find_all(re.compile('(|bank|cc)tranlist')): yield acctid, currency, tranlist.find_all('stmttrn'), balance beancount.ingest.importers.ofx.parse_ofx_time(date_str) \uf0c1 Parse an OFX time string and return a datetime object. Parameters: date_str \u2013 A string, the date to be parsed. Returns: A datetime.datetime instance. Source code in beancount/ingest/importers/ofx.py def parse_ofx_time(date_str): \"\"\"Parse an OFX time string and return a datetime object. Args: date_str: A string, the date to be parsed. Returns: A datetime.datetime instance. \"\"\" if len(date_str) < 14: return datetime.datetime.strptime(date_str[:8], '%Y%m%d') else: return datetime.datetime.strptime(date_str[:14], '%Y%m%d%H%M%S') beancount.ingest.regression \uf0c1 Support for implementing regression tests on sample files using nose. NOTE: This itself is not a regression test. It's a library used to create regression tests for your importers. Use it like this in your own importer code: def test(): importer = Importer([], { 'FILE' : 'Assets:US:MyBank:Main', }) yield from regression.compare_sample_files(importer, file ) WARNING: This is deprecated. Nose itself has been deprecated for a while and Beancount is now using only pytest. Ignore this and use beancount.ingest.regression_ptest instead. beancount.ingest.regression.ImportFileTestCase ( TestCase ) \uf0c1 Base class for importer tests that compare output to an expected output text. beancount.ingest.regression.ImportFileTestCase.test_expect_extract(self, filename, msg) \uf0c1 Extract entries from a test file and compare against expected output. If an expected file (as .extract) is not present, we issue a warning. Missing expected files can be written out by removing them before running the tests. Parameters: filename \u2013 A string, the name of the file to import using self.importer. Exceptions: AssertionError \u2013 If the contents differ from the expected file. Source code in beancount/ingest/regression.py @test_utils.skipIfRaises(ToolNotInstalled) def test_expect_extract(self, filename, msg): \"\"\"Extract entries from a test file and compare against expected output. If an expected file (as .extract) is not present, we issue a warning. Missing expected files can be written out by removing them before running the tests. Args: filename: A string, the name of the file to import using self.importer. Raises: AssertionError: If the contents differ from the expected file. \"\"\" # Import the file. entries = extract.extract_from_file(filename, self.importer, None, None) # Render the entries to a string. oss = io.StringIO() printer.print_entries(entries, file=oss) string = oss.getvalue() expect_filename = '{}.extract'.format(filename) if path.exists(expect_filename): expect_string = open(expect_filename, encoding='utf-8').read() self.assertEqual(expect_string.strip(), string.strip()) else: # Write out the expected file for review. open(expect_filename, 'w', encoding='utf-8').write(string) self.skipTest(\"Expected file not present; generating '{}'\".format( expect_filename)) beancount.ingest.regression.ImportFileTestCase.test_expect_file_date(self, filename, msg) \uf0c1 Compute the imported file date and compare to an expected output. If an expected file (as .file_date) is not present, we issue a warning. Missing expected files can be written out by removing them before running the tests. Parameters: filename \u2013 A string, the name of the file to import using self.importer. Exceptions: AssertionError \u2013 If the contents differ from the expected file. Source code in beancount/ingest/regression.py @test_utils.skipIfRaises(ToolNotInstalled) def test_expect_file_date(self, filename, msg): \"\"\"Compute the imported file date and compare to an expected output. If an expected file (as .file_date) is not present, we issue a warning. Missing expected files can be written out by removing them before running the tests. Args: filename: A string, the name of the file to import using self.importer. Raises: AssertionError: If the contents differ from the expected file. \"\"\" # Import the date. file = cache.get_file(filename) date = self.importer.file_date(file) if date is None: self.fail(\"No date produced from {}\".format(file.name)) expect_filename = '{}.file_date'.format(file.name) if path.exists(expect_filename) and path.getsize(expect_filename) > 0: expect_date_str = open(expect_filename, encoding='utf-8').read().strip() expect_date = datetime.datetime.strptime(expect_date_str, '%Y-%m-%d').date() self.assertEqual(expect_date, date) else: # Write out the expected file for review. with open(expect_filename, 'w', encoding='utf-8') as outfile: print(date.strftime('%Y-%m-%d'), file=outfile) self.skipTest(\"Expected file not present; generating '{}'\".format( expect_filename)) beancount.ingest.regression.ImportFileTestCase.test_expect_file_name(self, filename, msg) \uf0c1 Compute the imported file name and compare to an expected output. If an expected file (as .file_name) is not present, we issue a warning. Missing expected files can be written out by removing them before running the tests. Parameters: filename \u2013 A string, the name of the file to import using self.importer. Exceptions: AssertionError \u2013 If the contents differ from the expected file. Source code in beancount/ingest/regression.py @test_utils.skipIfRaises(ToolNotInstalled) def test_expect_file_name(self, filename, msg): \"\"\"Compute the imported file name and compare to an expected output. If an expected file (as .file_name) is not present, we issue a warning. Missing expected files can be written out by removing them before running the tests. Args: filename: A string, the name of the file to import using self.importer. Raises: AssertionError: If the contents differ from the expected file. \"\"\" # Import the date. file = cache.get_file(filename) generated_basename = self.importer.file_name(file) if generated_basename is None: self.fail(\"No filename produced from {}\".format(filename)) # Check that we're getting a non-null relative simple filename. self.assertFalse(path.isabs(generated_basename), generated_basename) self.assertNotRegex(generated_basename, os.sep) expect_filename = '{}.file_name'.format(file.name) if path.exists(expect_filename) and path.getsize(expect_filename) > 0: expect_filename = open(expect_filename, encoding='utf-8').read().strip() self.assertEqual(expect_filename, generated_basename) else: # Write out the expected file for review. with open(expect_filename, 'w', encoding='utf-8') as file: print(generated_basename, file=file) self.skipTest(\"Expected file not present; generating '{}'\".format( expect_filename)) beancount.ingest.regression.ImportFileTestCase.test_expect_identify(self, filename, msg) \uf0c1 Attempt to identify a file and expect results to be true. Parameters: filename \u2013 A string, the name of the file to import using self.importer. Exceptions: AssertionError \u2013 If the contents differ from the expected file. Source code in beancount/ingest/regression.py @test_utils.skipIfRaises(ToolNotInstalled) def test_expect_identify(self, filename, msg): \"\"\"Attempt to identify a file and expect results to be true. Args: filename: A string, the name of the file to import using self.importer. Raises: AssertionError: If the contents differ from the expected file. \"\"\" file = cache.get_file(filename) matched = self.importer.identify(file) self.assertTrue(matched) beancount.ingest.regression.ToolNotInstalled ( OSError ) \uf0c1 An error to be used by converters when necessary software isn't there. Raising this exception from your converter code when the tool is not installed will make the tests defined in this file skipped instead of failing. This will happen when you test your converters on different computers and/or platforms. beancount.ingest.regression.compare_sample_files(importer, directory=None, ignore_cls=None) \uf0c1 Compare the sample files under a directory. Parameters: importer \u2013 An instance of an Importer. directory \u2013 A string, the directory to scour for sample files or a filename in that directory. If a directory is not provided, the directory of the file from which the importer class is defined is used. ignore_cls \u2013 An optional base class of the importer whose methods should not trigger the addition of a test. For example, if you are deriving from a base class which is already well-tested, you may not want to have a regression test case generated for those methods. This was used to ignore methods provided from a common backwards compatibility support class. Yields: Generated tests as per nose's requirements (a callable and arguments for it). Source code in beancount/ingest/regression.py @deprecated(\"Use beancount.ingest.regression_pytest instead\") def compare_sample_files(importer, directory=None, ignore_cls=None): \"\"\"Compare the sample files under a directory. Args: importer: An instance of an Importer. directory: A string, the directory to scour for sample files or a filename in that directory. If a directory is not provided, the directory of the file from which the importer class is defined is used. ignore_cls: An optional base class of the importer whose methods should not trigger the addition of a test. For example, if you are deriving from a base class which is already well-tested, you may not want to have a regression test case generated for those methods. This was used to ignore methods provided from a common backwards compatibility support class. Yields: Generated tests as per nose's requirements (a callable and arguments for it). \"\"\" # If the directory is not specified, use the directory where the importer # class was defined. if not directory: directory = sys.modules[type(importer).__module__].__file__ if path.isfile(directory): directory = path.dirname(directory) for filename in find_input_files(directory): # For each of the methods to be tested, check if there is an actual # implementation and if so, run a comparison with an expected file. for name in ['identify', 'extract', 'file_date', 'file_name']: # Check if the method has been overridden from the protocol # interface. If so, even if it's provided by concretely inherited # method, we want to require a test against that method. func = getattr(importer, name).__func__ if (func is not getattr(ImporterProtocol, name) and (ignore_cls is None or (func is not getattr(ignore_cls, name, None)))): method = getattr(ImportFileTestCase(importer), 'test_expect_{}'.format(name)) yield (method, filename, name) beancount.ingest.regression.find_input_files(directory) \uf0c1 Find the input files in the module where the class is defined. Parameters: directory \u2013 A string, the path to a root directory to check for. Yields: Strings, the absolute filenames of sample input and expected files. Source code in beancount/ingest/regression.py def find_input_files(directory): \"\"\"Find the input files in the module where the class is defined. Args: directory: A string, the path to a root directory to check for. Yields: Strings, the absolute filenames of sample input and expected files. \"\"\" for sroot, dirs, files in os.walk(directory): for filename in files: if re.match(r'.*\\.(extract|file_date|file_name|py|pyc|DS_Store)$', filename): continue yield path.join(sroot, filename) beancount.ingest.regression_pytest \uf0c1 Support for implementing regression tests on sample files using pytest. This module provides definitions for testing a custom importer against a set of existing downloaded files, running the various importer interface methods on it, and comparing the output to an expected text file. (Expected test files can be auto-generated using the --generate option). You use it like this: from beancount.ingest import regression_pytest ... import mymodule ... # Create your importer instance used for testing. importer = mymodule.Importer(...) # Select a directory where your test files are to be located. directory = ... # Create a test case using the base in this class. @regression_pytest.with_importer(importer) @regression_pytest.with_testdir(directory) class TestImporter(regtest.ImporterTestBase): pass Also, to add the --generate option to 'pytest', you must create a conftest.py somewhere in one of the roots above your importers with this module as a plugin: pytest_plugins = \"beancount.ingest.regression_pytest\" See beancount/example/ingest for a full working example. How to invoke the tests: Via pytest. First run your test with the --generate option to generate all the expected files. Then inspect them visually for correctness. Finally, check them in to preserve them. You should be able to regress against those correct outputs in the future. Use version control to your advantage to visualize the differences. beancount.ingest.regression_pytest.ImporterTestBase \uf0c1 beancount.ingest.regression_pytest.ImporterTestBase.test_extract(self, importer, file, pytestconfig) \uf0c1 Extract entries from a test file and compare against expected output. Source code in beancount/ingest/regression_pytest.py def test_extract(self, importer, file, pytestconfig): \"\"\"Extract entries from a test file and compare against expected output.\"\"\" entries = extract.extract_from_file(file.name, importer, None, None) oss = io.StringIO() printer.print_entries(entries, file=oss) string = oss.getvalue() compare_contents_or_generate(string, '{}.extract'.format(file.name), pytestconfig.getoption(\"generate\", False)) beancount.ingest.regression_pytest.ImporterTestBase.test_file_account(self, importer, file, pytestconfig) \uf0c1 Compute the selected filing account and compare to an expected output. Source code in beancount/ingest/regression_pytest.py def test_file_account(self, importer, file, pytestconfig): \"\"\"Compute the selected filing account and compare to an expected output.\"\"\" account = importer.file_account(file) or '' compare_contents_or_generate(account, '{}.file_account'.format(file.name), pytestconfig.getoption(\"generate\", False)) beancount.ingest.regression_pytest.ImporterTestBase.test_file_date(self, importer, file, pytestconfig) \uf0c1 Compute the imported file date and compare to an expected output. Source code in beancount/ingest/regression_pytest.py def test_file_date(self, importer, file, pytestconfig): \"\"\"Compute the imported file date and compare to an expected output.\"\"\" date = importer.file_date(file) string = date.isoformat() if date else '' compare_contents_or_generate(string, '{}.file_date'.format(file.name), pytestconfig.getoption(\"generate\", False)) beancount.ingest.regression_pytest.ImporterTestBase.test_file_name(self, importer, file, pytestconfig) \uf0c1 Compute the imported file name and compare to an expected output. Source code in beancount/ingest/regression_pytest.py def test_file_name(self, importer, file, pytestconfig): \"\"\"Compute the imported file name and compare to an expected output.\"\"\" filename = importer.file_name(file) or '' compare_contents_or_generate(filename, '{}.file_name'.format(file.name), pytestconfig.getoption(\"generate\", False)) beancount.ingest.regression_pytest.ImporterTestBase.test_identify(self, importer, file) \uf0c1 Attempt to identify a file and expect results to be true. This method does not need to check against an existing expect file. It is just assumed it should return True if your test is setup well (the importer should always identify the test file). Source code in beancount/ingest/regression_pytest.py def test_identify(self, importer, file): \"\"\"Attempt to identify a file and expect results to be true. This method does not need to check against an existing expect file. It is just assumed it should return True if your test is setup well (the importer should always identify the test file). \"\"\" assert importer.identify(file) beancount.ingest.regression_pytest.compare_contents_or_generate(actual_string, expect_fn, generate) \uf0c1 Compare a string to the contents of an expect file. Assert if different; auto-generate otherwise. Parameters: actual_string \u2013 The expected string contents. expect_fn \u2013 The filename whose contents to read and compare against. generate \u2013 A boolean, true if we are to generate the tests. Source code in beancount/ingest/regression_pytest.py def compare_contents_or_generate(actual_string, expect_fn, generate): \"\"\"Compare a string to the contents of an expect file. Assert if different; auto-generate otherwise. Args: actual_string: The expected string contents. expect_fn: The filename whose contents to read and compare against. generate: A boolean, true if we are to generate the tests. \"\"\" if generate: with open(expect_fn, 'w', encoding='utf-8') as expect_file: expect_file.write(actual_string) if actual_string and not actual_string.endswith('\\n'): expect_file.write('\\n') pytest.skip(\"Generated '{}'\".format(expect_fn)) else: # Run the test on an existing expected file. assert path.exists(expect_fn), ( \"Expected file '{}' is missing. Generate it?\".format(expect_fn)) with open(expect_fn, encoding='utf-8') as infile: expect_string = infile.read() assert expect_string.strip() == actual_string.strip() beancount.ingest.regression_pytest.find_input_files(directory) \uf0c1 Find the input files in the module where the class is defined. Parameters: directory \u2013 A string, the path to a root directory to check for. Yields: Strings, the absolute filenames of sample input and expected files. Source code in beancount/ingest/regression_pytest.py def find_input_files(directory): \"\"\"Find the input files in the module where the class is defined. Args: directory: A string, the path to a root directory to check for. Yields: Strings, the absolute filenames of sample input and expected files. \"\"\" for sroot, dirs, files in os.walk(directory): for filename in files: if re.match(r'.*\\.(extract|file_date|file_name|file_account|py|pyc|DS_Store)$', filename): continue yield path.join(sroot, filename) beancount.ingest.regression_pytest.pytest_addoption(parser) \uf0c1 Add an option to generate the expected files for the tests. Source code in beancount/ingest/regression_pytest.py def pytest_addoption(parser): \"\"\"Add an option to generate the expected files for the tests.\"\"\" group = parser.getgroup(\"beancount\") group.addoption(\"--generate\", \"--gen\", action=\"store_true\", help=\"Don't test; rather, generate the expected files\") beancount.ingest.regression_pytest.with_importer(importer) \uf0c1 Parametrizing fixture that provides the importer to test. Source code in beancount/ingest/regression_pytest.py def with_importer(importer): \"\"\"Parametrizing fixture that provides the importer to test.\"\"\" return pytest.mark.parametrize(\"importer\", [importer]) beancount.ingest.regression_pytest.with_testdir(directory) \uf0c1 Parametrizing fixture that provides files from a directory. Source code in beancount/ingest/regression_pytest.py def with_testdir(directory): \"\"\"Parametrizing fixture that provides files from a directory.\"\"\" return pytest.mark.parametrize( \"file\", [cache.get_file(fn) for fn in find_input_files(directory)]) beancount.ingest.scripts_utils \uf0c1 Common front-end to all ingestion tools. beancount.ingest.scripts_utils.TestScriptsBase ( TestTempdirMixin , TestCase ) \uf0c1 beancount.ingest.scripts_utils.TestScriptsBase.setUp(self) \uf0c1 Hook method for setting up the test fixture before exercising it. Source code in beancount/ingest/scripts_utils.py def setUp(self): super().setUp() for filename, contents in self.FILES.items(): absname = path.join(self.tempdir, filename) os.makedirs(path.dirname(absname), exist_ok=True) with open(absname, 'w') as file: file.write(contents) if filename.endswith('.py') or filename.endswith('.sh'): os.chmod(absname, stat.S_IRUSR|stat.S_IXUSR) beancount.ingest.scripts_utils.create_legacy_arguments_parser(description, run_func) \uf0c1 Create an arguments parser for all the ingestion bean-tools. Parameters: description ( str ) \u2013 The program description string. func \u2013 A callable function to run the particular command. Returns: An argparse.Namespace instance with the rest of arguments in 'rest'. Source code in beancount/ingest/scripts_utils.py def create_legacy_arguments_parser(description: str, run_func: callable): \"\"\"Create an arguments parser for all the ingestion bean-tools. Args: description: The program description string. func: A callable function to run the particular command. Returns: An argparse.Namespace instance with the rest of arguments in 'rest'. \"\"\" parser = version.ArgumentParser(description=description) parser.add_argument('config', action='store', metavar='CONFIG_FILENAME', help=('Importer configuration file. ' 'This is a Python file with a data structure that ' 'is specific to your accounts')) parser.add_argument('downloads', nargs='+', metavar='DIR-OR-FILE', default=[], help='Filenames or directories to search for files to import') parser.set_defaults(command=run_func) return parser beancount.ingest.scripts_utils.ingest(importers_list, detect_duplicates_func=None, hooks=None) \uf0c1 Driver function that calls all the ingestion tools. Put a call to this function at the end of your importer configuration to make your import script; this should be its main function, like this: from beancount.ingest.scripts_utils import ingest my_importers = [ ... ] ingest(my_importers) This more explicit way of invoking the ingestion is now the preferred way to invoke the various tools, and replaces calling the bean-identify, bean-extract, bean-file tools with a --config argument. When you call the import script itself (as as program) it will parse the arguments, expecting a subcommand ('identify', 'extract' or 'file') and corresponding subcommand-specific arguments. Here you can override some importer values, such as installing a custom duplicate finding hook, and eventually more. Note that this newer invocation method is optional and if it is not present, a call to ingest() is generated implicitly, and it functions as it used to. Future configurable customization of the ingestion process will be implemented by inserting new arguments to this function, this is the motivation behind doing this. Note that invocation by the three bean-* ingestion tools is still supported, and calling ingest() explicitly from your import configuration file will not break these tools either, if you invoke them on it; the values you provide to this function will be used by those tools. Parameters: importers_list \u2013 A list of importer instances. This is used as a chain-of-responsibility, called on each file. detect_duplicates_func \u2013 (DEPRECATED) An optional function which accepts a list of lists of imported entries and a list of entries already existing in the user's ledger. See function find_duplicate_entries(), which is the default implementation for this. Use 'filter_funcs' instead. hooks \u2013 An optional list of hook functions to apply to the list of extract (filename, entries) pairs, in order. This replaces 'detect_duplicates_func'. Source code in beancount/ingest/scripts_utils.py def ingest(importers_list, detect_duplicates_func=None, hooks=None): \"\"\"Driver function that calls all the ingestion tools. Put a call to this function at the end of your importer configuration to make your import script; this should be its main function, like this: from beancount.ingest.scripts_utils import ingest my_importers = [ ... ] ingest(my_importers) This more explicit way of invoking the ingestion is now the preferred way to invoke the various tools, and replaces calling the bean-identify, bean-extract, bean-file tools with a --config argument. When you call the import script itself (as as program) it will parse the arguments, expecting a subcommand ('identify', 'extract' or 'file') and corresponding subcommand-specific arguments. Here you can override some importer values, such as installing a custom duplicate finding hook, and eventually more. Note that this newer invocation method is optional and if it is not present, a call to ingest() is generated implicitly, and it functions as it used to. Future configurable customization of the ingestion process will be implemented by inserting new arguments to this function, this is the motivation behind doing this. Note that invocation by the three bean-* ingestion tools is still supported, and calling ingest() explicitly from your import configuration file will not break these tools either, if you invoke them on it; the values you provide to this function will be used by those tools. Args: importers_list: A list of importer instances. This is used as a chain-of-responsibility, called on each file. detect_duplicates_func: (DEPRECATED) An optional function which accepts a list of lists of imported entries and a list of entries already existing in the user's ledger. See function find_duplicate_entries(), which is the default implementation for this. Use 'filter_funcs' instead. hooks: An optional list of hook functions to apply to the list of extract (filename, entries) pairs, in order. This replaces 'detect_duplicates_func'. \"\"\" if detect_duplicates_func is not None: warnings.warn(\"Argument 'detect_duplicates_func' is deprecated.\") # Fold it in hooks. if hooks is None: hooks = [] hooks.insert(0, detect_duplicates_func) del detect_duplicates_func if ingest.args is not None: # The script has been called from one of the bean-* ingestion tools. # 'ingest.args' is only set when we're being invoked from one of the # bean-xxx tools (see below). # Mark this function as called, so that if it is called from an import # triggered by one of the ingestion tools, it won't be called again # afterwards. ingest.was_called = True # Use those args rather than to try to parse the command-line arguments # from a naked ingest() call as a script. {39c7af4f6af5} args, parser = ingest.args else: # The script is called directly. This is the main program of the import # script itself. This is the new invocation method. parser = version.ArgumentParser(description=DESCRIPTION) # Use required on subparsers. # FIXME: Remove this when we require version 3.7 or above. kwargs = {} if sys.version_info >= (3, 7): kwargs['required'] = True subparsers = parser.add_subparsers(dest='command', **kwargs) parser.add_argument('--downloads', '-d', metavar='DIR-OR-FILE', action='append', default=[], help='Filenames or directories to search for files to import') for cmdname, module in [('identify', identify), ('extract', extract), ('file', file)]: parser_cmd = subparsers.add_parser(cmdname, help=module.DESCRIPTION) parser_cmd.set_defaults(command=module.run) module.add_arguments(parser_cmd) args = parser.parse_args() if not args.downloads: args.downloads.append(os.getcwd()) # Implement required ourselves. # FIXME: Remove this when we require version 3.7 or above. if not (sys.version_info >= (3, 7)): if not hasattr(args, 'command'): parser.error(\"Subcommand is required.\") abs_downloads = list(map(path.abspath, args.downloads)) args.command(args, parser, importers_list, abs_downloads, hooks=hooks) return 0 beancount.ingest.scripts_utils.run_import_script_and_ingest(parser, argv=None, importers_attr_name='CONFIG') \uf0c1 Run the import script and optionally call ingest(). This path is only called when trampolined by one of the bean-* ingestion tools. Parameters: parser \u2013 The parser instance, used only to report errors. importers_attr_name \u2013 The name of the special attribute in the module which defines the importers list. Returns: An execution return code. Source code in beancount/ingest/scripts_utils.py def run_import_script_and_ingest(parser, argv=None, importers_attr_name='CONFIG'): \"\"\"Run the import script and optionally call ingest(). This path is only called when trampolined by one of the bean-* ingestion tools. Args: parser: The parser instance, used only to report errors. importers_attr_name: The name of the special attribute in the module which defines the importers list. Returns: An execution return code. \"\"\" args = parser.parse_args(args=argv) # Check the existence of the config. if not path.exists(args.config) or path.isdir(args.config): parser.error(\"File does not exist: '{}'\".format(args.config)) # Check the existence of all specified files. for filename in args.downloads: if not path.exists(filename): parser.error(\"File does not exist: '{}'\".format(filename)) # Reset the state of ingest() being called (for unit tests, which use the # same runtime with run_with_args). ingest.was_called = False # Save the arguments parsed from the command-line as default for # {39c7af4f6af5}. ingest.args = args, parser # Evaluate the importer script/module. mod = runpy.run_path(args.config) # If the importer script has already called ingest() within itself, don't # call it again. We're done. This allows the use to insert an explicit call # to ingest() while still running the bean-* ingestion tools on the file. if ingest.was_called: return 0 # ingest() hasn't been called by the script so we assume it isn't # present in it. So we now run the ingestion by ourselves here, without # specifying any of the newer optional arguments. importers_list = mod[importers_attr_name] return ingest(importers_list) beancount.ingest.scripts_utils.trampoline_to_ingest(module) \uf0c1 Parse arguments for bean tool, import config script and ingest. This function is called by the three bean-* tools to support the older import files, which only required a CONFIG object to be defined in them. Parameters: module \u2013 One of the identify, extract or file module objects. Returns: An execution return code. Source code in beancount/ingest/scripts_utils.py def trampoline_to_ingest(module): \"\"\"Parse arguments for bean tool, import config script and ingest. This function is called by the three bean-* tools to support the older import files, which only required a CONFIG object to be defined in them. Args: module: One of the identify, extract or file module objects. Returns: An execution return code. \"\"\" # Disable debugging logging which is turned on by default in chardet. logging.getLogger('chardet.charsetprober').setLevel(logging.INFO) logging.getLogger('chardet.universaldetector').setLevel(logging.INFO) parser = create_legacy_arguments_parser(module.DESCRIPTION, module.run) module.add_arguments(parser) return run_import_script_and_ingest(parser) beancount.ingest.similar \uf0c1 Identify similar entries. This can be used during import in order to identify and flag duplicate entries. beancount.ingest.similar.SimilarityComparator \uf0c1 Similarity comparator of transactions. This comparator needs to be able to handle Transaction instances which are incomplete on one side, which have slightly different dates, or potentially slightly different numbers. beancount.ingest.similar.SimilarityComparator.__call__(self, entry1, entry2) special \uf0c1 Compare two entries, return true if they are deemed similar. Parameters: entry1 \u2013 A first Transaction directive. entry2 \u2013 A second Transaction directive. Returns: A boolean. Source code in beancount/ingest/similar.py def __call__(self, entry1, entry2): \"\"\"Compare two entries, return true if they are deemed similar. Args: entry1: A first Transaction directive. entry2: A second Transaction directive. Returns: A boolean. \"\"\" # Check the date difference. if self.max_date_delta is not None: delta = ((entry1.date - entry2.date) if entry1.date > entry2.date else (entry2.date - entry1.date)) if delta > self.max_date_delta: return False try: amounts1 = self.cache[id(entry1)] except KeyError: amounts1 = self.cache[id(entry1)] = amounts_map(entry1) try: amounts2 = self.cache[id(entry2)] except KeyError: amounts2 = self.cache[id(entry2)] = amounts_map(entry2) # Look for amounts on common accounts. common_keys = set(amounts1) & set(amounts2) for key in sorted(common_keys): # Compare the amounts. number1 = amounts1[key] number2 = amounts2[key] if number1 == ZERO and number2 == ZERO: break diff = abs((number1 / number2) if number2 != ZERO else (number2 / number1)) if diff == ZERO: return False if diff < ONE: diff = ONE/diff if (diff - ONE) < self.EPSILON: break else: return False # Here, we have found at least one common account with a close # amount. Now, we require that the set of accounts are equal or that # one be a subset of the other. accounts1 = set(posting.account for posting in entry1.postings) accounts2 = set(posting.account for posting in entry2.postings) return accounts1.issubset(accounts2) or accounts2.issubset(accounts1) beancount.ingest.similar.SimilarityComparator.__init__(self, max_date_delta=None) special \uf0c1 Constructor a comparator of entries. Parameters: max_date_delta \u2013 A datetime.timedelta instance of the max tolerated distance between dates. Source code in beancount/ingest/similar.py def __init__(self, max_date_delta=None): \"\"\"Constructor a comparator of entries. Args: max_date_delta: A datetime.timedelta instance of the max tolerated distance between dates. \"\"\" self.cache = {} self.max_date_delta = max_date_delta beancount.ingest.similar.amounts_map(entry) \uf0c1 Compute a mapping of (account, currency) -> Decimal balances. Parameters: entry \u2013 A Transaction instance. Returns: A dict of account -> Amount balance. Source code in beancount/ingest/similar.py def amounts_map(entry): \"\"\"Compute a mapping of (account, currency) -> Decimal balances. Args: entry: A Transaction instance. Returns: A dict of account -> Amount balance. \"\"\" amounts = collections.defaultdict(D) for posting in entry.postings: # Skip interpolated postings. if posting.meta and interpolate.AUTOMATIC_META in posting.meta: continue currency = isinstance(posting.units, amount.Amount) and posting.units.currency if isinstance(currency, str): key = (posting.account, currency) amounts[key] += posting.units.number return amounts beancount.ingest.similar.find_similar_entries(entries, source_entries, comparator=None, window_days=2) \uf0c1 Find which entries from a list are potential duplicates of a set. Note: If there are multiple entries from 'source_entries' matching an entry in 'entries', only the first match is returned. Note that this function could in theory decide to merge some of the imported entries with each other. Parameters: entries \u2013 The list of entries to classify as duplicate or note. source_entries \u2013 The list of entries against which to match. This is the previous, or existing set of entries to compare against. This may be null or empty. comparator \u2013 A functor used to establish the similarity of two entries. window_days \u2013 The number of days (inclusive) before or after to scan the entries to classify against. Returns: A list of pairs of entries (entry, source_entry) where entry is from 'entries' and is deemed to be a duplicate of source_entry, from 'source_entries'. Source code in beancount/ingest/similar.py def find_similar_entries(entries, source_entries, comparator=None, window_days=2): \"\"\"Find which entries from a list are potential duplicates of a set. Note: If there are multiple entries from 'source_entries' matching an entry in 'entries', only the first match is returned. Note that this function could in theory decide to merge some of the imported entries with each other. Args: entries: The list of entries to classify as duplicate or note. source_entries: The list of entries against which to match. This is the previous, or existing set of entries to compare against. This may be null or empty. comparator: A functor used to establish the similarity of two entries. window_days: The number of days (inclusive) before or after to scan the entries to classify against. Returns: A list of pairs of entries (entry, source_entry) where entry is from 'entries' and is deemed to be a duplicate of source_entry, from 'source_entries'. \"\"\" window_head = datetime.timedelta(days=window_days) window_tail = datetime.timedelta(days=window_days + 1) if comparator is None: comparator = SimilarityComparator() # For each of the new entries, look at existing entries at a nearby date. duplicates = [] if source_entries is not None: for entry in data.filter_txns(entries): for source_entry in data.filter_txns( data.iter_entry_dates(source_entries, entry.date - window_head, entry.date + window_tail)): if comparator(entry, source_entry): duplicates.append((entry, source_entry)) break return duplicates","title":"beancount.ingest"},{"location":"api_reference/beancount.ingest.html#beancountingest","text":"Code to help identify, extract, and file external downloads. This package contains code to help you build importers and drive the process of identifying which importer to run on an externally downloaded file, extract transactions from them and file away these files under a clean and rigidly named hierarchy for preservation.","title":"beancount.ingest"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.cache","text":"A file wrapper which acts as a cache for on-demand evaluation of conversions. This object is used in lieu of a file in order to allow the various importers to reuse each others' conversion results. Converting file contents, e.g. PDF to text, can be expensive.","title":"cache"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.cache.contents","text":"A converter that just reads the entire contents of a file. Parameters: num_bytes \u2013 The number of bytes to read. Returns: A converter function. Source code in beancount/ingest/cache.py def contents(filename): \"\"\"A converter that just reads the entire contents of a file. Args: num_bytes: The number of bytes to read. Returns: A converter function. \"\"\" # Attempt to detect the input encoding automatically, using chardet and a # decent amount of input. rawdata = open(filename, 'rb').read(HEAD_DETECT_MAX_BYTES) detected = chardet.detect(rawdata) encoding = detected['encoding'] # Ignore encoding errors for reading the contents because input files # routinely break this assumption. errors = 'ignore' with open(filename, encoding=encoding, errors=errors) as file: return file.read()","title":"contents()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.cache.get_file","text":"Create or reuse a globally registered instance of a FileMemo. Note: the FileMemo objects' lifetimes are reused for the duration of the process. This is usually the intended behavior. Always create them by calling this constructor. Parameters: filename \u2013 A path string, the absolute name of the file whose memo to create. Returns: A FileMemo instance. Source code in beancount/ingest/cache.py def get_file(filename): \"\"\"Create or reuse a globally registered instance of a FileMemo. Note: the FileMemo objects' lifetimes are reused for the duration of the process. This is usually the intended behavior. Always create them by calling this constructor. Args: filename: A path string, the absolute name of the file whose memo to create. Returns: A FileMemo instance. \"\"\" assert path.isabs(filename), ( \"Path should be absolute in order to guarantee a single call.\") return _CACHE[filename]","title":"get_file()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.cache.head","text":"A converter that just reads the first bytes of a file. Parameters: num_bytes \u2013 The number of bytes to read. Returns: A converter function. Source code in beancount/ingest/cache.py def head(num_bytes=8192): \"\"\"A converter that just reads the first bytes of a file. Args: num_bytes: The number of bytes to read. Returns: A converter function. \"\"\" def head_reader(filename): with open(filename, 'rb') as file: rawdata = file.read(num_bytes) detected = chardet.detect(rawdata) encoding = detected['encoding'] return rawdata.decode(encoding) return head_reader","title":"head()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.cache.mimetype","text":"A converter that computes the MIME type of the file. Returns: A converter function. Source code in beancount/ingest/cache.py def mimetype(filename): \"\"\"A converter that computes the MIME type of the file. Returns: A converter function. \"\"\" return file_type.guess_file_type(filename)","title":"mimetype()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.extract","text":"Extract script. Read an import script and a list of downloaded filenames or directories of downloaded files, and for each of those files, extract transactions from it.","title":"extract"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.extract.add_arguments","text":"Add arguments for the extract command. Source code in beancount/ingest/extract.py def add_arguments(parser): \"\"\"Add arguments for the extract command.\"\"\" parser.add_argument('-e', '-f', '--existing', '--previous', metavar='BEANCOUNT_FILE', default=None, help=('Beancount file or existing entries for de-duplication ' '(optional)')) parser.add_argument('-r', '--reverse', '--descending', action='store_const', dest='ascending', default=True, const=False, help='Write out the entries in descending order')","title":"add_arguments()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.extract.extract","text":"Given an importer configuration, search for files that can be imported in the list of files or directories, run the signature checks on them, and if it succeeds, run the importer on the file. A list of entries for an existing ledger can be provided in order to perform de-duplication and a minimum date can be provided to filter out old entries. Parameters: importer_config \u2013 A list of (regexps, importer) pairs, the configuration. files_or_directories \u2013 A list of strings, filenames or directories to be processed. output \u2013 A file object, to be written to. entries \u2013 A list of directives loaded from the existing file for the newly extracted entries to be merged in. options_map \u2013 The options parsed from existing file. mindate \u2013 Optional minimum date to output transactions for. ascending \u2013 A boolean, true to print entries in ascending order, false if descending is desired. hooks \u2013 An optional list of hook functions to apply to the list of extract (filename, entries) pairs, in order. If not specified, find_duplicate_entries() is used, automatically. Source code in beancount/ingest/extract.py def extract(importer_config, files_or_directories, output, entries=None, options_map=None, mindate=None, ascending=True, hooks=None): \"\"\"Given an importer configuration, search for files that can be imported in the list of files or directories, run the signature checks on them, and if it succeeds, run the importer on the file. A list of entries for an existing ledger can be provided in order to perform de-duplication and a minimum date can be provided to filter out old entries. Args: importer_config: A list of (regexps, importer) pairs, the configuration. files_or_directories: A list of strings, filenames or directories to be processed. output: A file object, to be written to. entries: A list of directives loaded from the existing file for the newly extracted entries to be merged in. options_map: The options parsed from existing file. mindate: Optional minimum date to output transactions for. ascending: A boolean, true to print entries in ascending order, false if descending is desired. hooks: An optional list of hook functions to apply to the list of extract (filename, entries) pairs, in order. If not specified, find_duplicate_entries() is used, automatically. \"\"\" allow_none_for_tags_and_links = ( options_map and options_map[\"allow_deprecated_none_for_tags_and_links\"]) # Run all the importers and gather their result sets. new_entries_list = [] for filename, importers in identify.find_imports(importer_config, files_or_directories): for importer in importers: # Import and process the file. try: new_entries = extract_from_file( filename, importer, existing_entries=entries, min_date=mindate, allow_none_for_tags_and_links=allow_none_for_tags_and_links) new_entries_list.append((filename, new_entries)) except Exception as exc: logging.exception(\"Importer %s.extract() raised an unexpected error: %s\", importer.name(), exc) continue # Find potential duplicate entries in the result sets, either against the # list of existing ones, or against each other. A single call to this # function is made on purpose, so that the function be able to merge # entries. if hooks is None: hooks = [find_duplicate_entries] for hook_fn in hooks: new_entries_list = hook_fn(new_entries_list, entries) assert isinstance(new_entries_list, list) assert all(isinstance(new_entries, tuple) for new_entries in new_entries_list) assert all(isinstance(new_entries[0], str) for new_entries in new_entries_list) assert all(isinstance(new_entries[1], list) for new_entries in new_entries_list) # Print out the results. output.write(HEADER) for key, new_entries in new_entries_list: output.write(identify.SECTION.format(key)) output.write('\\n') if not ascending: new_entries.reverse() print_extracted_entries(new_entries, output)","title":"extract()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.extract.extract_from_file","text":"Import entries from file 'filename' with the given matches, Also cross-check against a list of provided 'existing_entries' entries, de-duplicating and possibly auto-categorizing. Parameters: filename \u2013 The name of the file to import. importer \u2013 An importer object that matched the file. existing_entries \u2013 A list of existing entries parsed from a ledger, used to detect duplicates and automatically complete or categorize transactions. min_date \u2013 A date before which entries should be ignored. This is useful when an account has a valid check/assert; we could just ignore whatever comes before, if desired. allow_none_for_tags_and_links \u2013 A boolean, whether to allow plugins to generate Transaction objects with None as value for the 'tags' or 'links' attributes. Returns: A list of new imported entries. Exceptions: Exception \u2013 If there is an error in the importer's extract() method. Source code in beancount/ingest/extract.py def extract_from_file(filename, importer, existing_entries=None, min_date=None, allow_none_for_tags_and_links=False): \"\"\"Import entries from file 'filename' with the given matches, Also cross-check against a list of provided 'existing_entries' entries, de-duplicating and possibly auto-categorizing. Args: filename: The name of the file to import. importer: An importer object that matched the file. existing_entries: A list of existing entries parsed from a ledger, used to detect duplicates and automatically complete or categorize transactions. min_date: A date before which entries should be ignored. This is useful when an account has a valid check/assert; we could just ignore whatever comes before, if desired. allow_none_for_tags_and_links: A boolean, whether to allow plugins to generate Transaction objects with None as value for the 'tags' or 'links' attributes. Returns: A list of new imported entries. Raises: Exception: If there is an error in the importer's extract() method. \"\"\" # Extract the entries. file = cache.get_file(filename) # Note: Let the exception through on purpose. This makes developing # importers much easier by rendering the details of the exceptions. # # Note: For legacy support, support calling without the existing entries. kwargs = {} if 'existing_entries' in inspect.signature(importer.extract).parameters: kwargs['existing_entries'] = existing_entries new_entries = importer.extract(file, **kwargs) if not new_entries: return [] # Make sure the newly imported entries are sorted; don't trust the importer. new_entries.sort(key=data.entry_sortkey) # Ensure that the entries are typed correctly. for entry in new_entries: data.sanity_check_types(entry, allow_none_for_tags_and_links) # Filter out entries with dates before 'min_date'. if min_date: new_entries = list(itertools.dropwhile(lambda x: x.date < min_date, new_entries)) return new_entries","title":"extract_from_file()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.extract.find_duplicate_entries","text":"Flag potentially duplicate entries. Parameters: new_entries_list \u2013 A list of pairs of (key, lists of imported entries), one for each importer. The key identifies the filename and/or importer that yielded those new entries. existing_entries \u2013 A list of previously existing entries from the target ledger. Returns: A list of lists of modified new entries (like new_entries_list), potentially with modified metadata to indicate those which are duplicated. Source code in beancount/ingest/extract.py def find_duplicate_entries(new_entries_list, existing_entries): \"\"\"Flag potentially duplicate entries. Args: new_entries_list: A list of pairs of (key, lists of imported entries), one for each importer. The key identifies the filename and/or importer that yielded those new entries. existing_entries: A list of previously existing entries from the target ledger. Returns: A list of lists of modified new entries (like new_entries_list), potentially with modified metadata to indicate those which are duplicated. \"\"\" mod_entries_list = [] for key, new_entries in new_entries_list: # Find similar entries against the existing ledger only. duplicate_pairs = similar.find_similar_entries(new_entries, existing_entries) # Add a metadata marker to the extracted entries for duplicates. duplicate_set = set(id(entry) for entry, _ in duplicate_pairs) mod_entries = [] for entry in new_entries: if id(entry) in duplicate_set: marked_meta = entry.meta.copy() marked_meta[DUPLICATE_META] = True entry = entry._replace(meta=marked_meta) mod_entries.append(entry) mod_entries_list.append((key, mod_entries)) return mod_entries_list","title":"find_duplicate_entries()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.extract.print_extracted_entries","text":"Print a list of entries. Parameters: entries \u2013 A list of extracted entries. file \u2013 A file object to write to. Source code in beancount/ingest/extract.py def print_extracted_entries(entries, file): \"\"\"Print a list of entries. Args: entries: A list of extracted entries. file: A file object to write to. \"\"\" # Print the filename and which modules matched. # pylint: disable=invalid-name pr = lambda *args: print(*args, file=file) pr('') # Print out the entries. for entry in entries: # Check if this entry is a dup, and if so, comment it out. if DUPLICATE_META in entry.meta: meta = entry.meta.copy() meta.pop(DUPLICATE_META) entry = entry._replace(meta=meta) entry_string = textwrap.indent(printer.format_entry(entry), '; ') else: entry_string = printer.format_entry(entry) pr(entry_string) pr('')","title":"print_extracted_entries()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.extract.run","text":"Run the subcommand. Source code in beancount/ingest/extract.py def run(args, _, importers_list, files_or_directories, hooks=None): \"\"\"Run the subcommand.\"\"\" # Load the ledger, if one is specified. if args.existing: entries, _, options_map = loader.load_file(args.existing) else: entries, options_map = None, None extract(importers_list, files_or_directories, sys.stdout, entries=entries, options_map=options_map, mindate=None, ascending=args.ascending, hooks=hooks) return 0","title":"run()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.file","text":"Filing script. Read an import script and a list of downloaded filenames or directories of downloaded files, and for each of those files, move the file under an account corresponding to the filing directory.","title":"file"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.file.add_arguments","text":"Add arguments for the extract command. Source code in beancount/ingest/file.py def add_arguments(parser): \"\"\"Add arguments for the extract command.\"\"\" parser.add_argument('-o', '--output', '--output-dir', '--destination', dest='output_dir', action='store', help=\"The root of the documents tree to move the files to.\") parser.add_argument('-n', '--dry-run', action='store_true', help=(\"Just print where the files would be moved; \" \"don't actually move them.\")) parser.add_argument('--no-overwrite', dest='overwrite', action='store_false', default=True, help=\"Don't overwrite destination files with the same name.\")","title":"add_arguments()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.file.file","text":"File importable files under a destination directory. Given an importer configuration object, search for files that can be imported under the given list of files or directories and moved them under the given destination directory with the date computed by the module prepended to the filename. If the date cannot be extracted, use a reasonable default for the date (e.g. the last modified time of the file itself). If 'mkdirs' is True, create the destination directories before moving the files. Parameters: importer_config \u2013 A list of importer instances that define the config. files_or_directories \u2013 a list of files of directories to walk recursively and hunt for files to import. destination \u2013 A string, the root destination directory where the files are to be filed. The files are organized there under a hierarchy mirroring that of the chart of accounts. dry_run \u2013 A flag, if true, don't actually move the files. mkdirs \u2013 A flag, if true, make all the intervening directories; otherwise, fail to move files to non-existing dirs. overwrite \u2013 A flag, if true, overwrite an existing destination file. idify \u2013 A flag, if true, remove whitespace and funky characters in the destination filename. logfile \u2013 A file object to write log entries to, or None, in which case no log is written out. Source code in beancount/ingest/file.py def file(importer_config, files_or_directories, destination, dry_run=False, mkdirs=False, overwrite=False, idify=False, logfile=None): \"\"\"File importable files under a destination directory. Given an importer configuration object, search for files that can be imported under the given list of files or directories and moved them under the given destination directory with the date computed by the module prepended to the filename. If the date cannot be extracted, use a reasonable default for the date (e.g. the last modified time of the file itself). If 'mkdirs' is True, create the destination directories before moving the files. Args: importer_config: A list of importer instances that define the config. files_or_directories: a list of files of directories to walk recursively and hunt for files to import. destination: A string, the root destination directory where the files are to be filed. The files are organized there under a hierarchy mirroring that of the chart of accounts. dry_run: A flag, if true, don't actually move the files. mkdirs: A flag, if true, make all the intervening directories; otherwise, fail to move files to non-existing dirs. overwrite: A flag, if true, overwrite an existing destination file. idify: A flag, if true, remove whitespace and funky characters in the destination filename. logfile: A file object to write log entries to, or None, in which case no log is written out. \"\"\" jobs = [] has_errors = False for filename, importers in identify.find_imports(importer_config, files_or_directories, logfile): # If we're debugging, print out the match text. # This option is useful when we're building our importer configuration, # to figure out which patterns to create as unique signatures. if not importers: continue # Process a single file. new_fullname = file_one_file(filename, importers, destination, idify, logfile) if new_fullname is None: continue # Check if the destination directory exists. new_dirname = path.dirname(new_fullname) if not path.exists(new_dirname) and not mkdirs: logging.error(\"Destination directory '{}' does not exist.\".format(new_dirname)) has_errors = True continue # Check if the destination file already exists; we don't want to clobber # it by accident. if not overwrite and path.exists(new_fullname): logging.error(\"Destination file '{}' already exists.\".format(new_fullname)) has_errors = True continue jobs.append((filename, new_fullname)) # Check if any two imported files would be colliding in their destination # name, before we move anything. destmap = collections.defaultdict(list) for src, dest in jobs: destmap[dest].append(src) for dest, sources in destmap.items(): if len(sources) != 1: logging.error(\"Collision in destination filenames '{}': from {}.\".format( dest, \", \".join([\"'{}'\".format(source) for source in sources]))) has_errors = True # If there are any errors, just don't do anything at all. This is a nicer # behaviour than moving just *some* files. if dry_run or has_errors: return # Actually carry out the moving job. for old_filename, new_filename in jobs: move_xdev_file(old_filename, new_filename, mkdirs) return jobs","title":"file()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.file.file_one_file","text":"Move a single filename using its matched importers. Parameters: filename \u2013 A string, the name of the downloaded file to be processed. importers \u2013 A list of importer instances that handle this file. destination \u2013 A string, the root destination directory where the files are to be filed. The files are organized there under a hierarchy mirroring that of the chart of accounts. idify \u2013 A flag, if true, remove whitespace and funky characters in the destination filename. logfile \u2013 A file object to write log entries to, or None, in which case no log is written out. Returns: The full new destination filename on success, and None if there was an error. Source code in beancount/ingest/file.py def file_one_file(filename, importers, destination, idify=False, logfile=None): \"\"\"Move a single filename using its matched importers. Args: filename: A string, the name of the downloaded file to be processed. importers: A list of importer instances that handle this file. destination: A string, the root destination directory where the files are to be filed. The files are organized there under a hierarchy mirroring that of the chart of accounts. idify: A flag, if true, remove whitespace and funky characters in the destination filename. logfile: A file object to write log entries to, or None, in which case no log is written out. Returns: The full new destination filename on success, and None if there was an error. \"\"\" # Create an object to cache all the conversions between the importers # and phases and what-not. file = cache.get_file(filename) # Get the account corresponding to the file. file_accounts = [] for index, importer in enumerate(importers): try: account_ = importer.file_account(file) except Exception as exc: account_ = None logging.exception(\"Importer %s.file_account() raised an unexpected error: %s\", importer.name(), exc) if account_ is not None: file_accounts.append(account_) file_accounts_set = set(file_accounts) if not file_accounts_set: logging.error(\"No account provided by importers: {}\".format( \", \".join(imp.name() for imp in importers))) return None if len(file_accounts_set) > 1: logging.warning(\"Ambiguous accounts from many importers: {}\".format( ', '.join(file_accounts_set))) # Note: Don't exit; select the first matching importer's account. file_account = file_accounts.pop(0) # Given multiple importers, select the first one that was yielded to # obtain the date and process the filename. importer = importers[0] # Compute the date from the last modified time. mtime = path.getmtime(filename) mtime_date = datetime.datetime.fromtimestamp(mtime).date() # Try to get the file's date by calling a module support function. The # module may be able to extract the date from the filename, from the # contents of the file itself (e.g. scraping some text from the PDF # contents, or grabbing the last line of a CSV file). try: date = importer.file_date(file) except Exception as exc: logging.exception(\"Importer %s.file_date() raised an unexpected error: %s\", importer.name(), exc) date = None if date is None: # Fallback on the last modified time of the file. date = mtime_date date_source = 'mtime' else: date_source = 'contents' # Apply filename renaming, if implemented. # Otherwise clean up the filename. try: clean_filename = importer.file_name(file) # Warn the importer implementor if a name is returned and it's an # absolute filename. if clean_filename and (path.isabs(clean_filename) or os.sep in clean_filename): logging.error((\"The importer '%s' file_name() method should return a relative \" \"filename; the filename '%s' is absolute or contains path \" \"separators\"), importer.name(), clean_filename) except Exception as exc: logging.exception(\"Importer %s.file_name() raised an unexpected error: %s\", importer.name(), exc) clean_filename = None if clean_filename is None: # If no filename has been provided, use the basename. clean_filename = path.basename(file.name) elif re.match(r'\\d\\d\\d\\d-\\d\\d-\\d\\d', clean_filename): logging.error(\"The importer '%s' file_name() method should not date the \" \"returned filename. Implement file_date() instead.\") # We need a simple filename; remove the directory part if there is one. clean_basename = path.basename(clean_filename) # Remove whitespace if requested. if idify: clean_basename = misc_utils.idify(clean_basename) # Prepend the date prefix. new_filename = '{0:%Y-%m-%d}.{1}'.format(date, clean_basename) # Prepend destination directory. new_fullname = path.normpath(path.join(destination, file_account.replace(account.sep, os.sep), new_filename)) # Print the filename and which modules matched. if logfile is not None: logfile.write('Importer: {}\\n'.format(importer.name() if importer else '-')) logfile.write('Account: {}\\n'.format(file_account)) logfile.write('Date: {} (from {})\\n'.format(date, date_source)) logfile.write('Destination: {}\\n'.format(new_fullname)) logfile.write('\\n') return new_fullname","title":"file_one_file()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.file.move_xdev_file","text":"Move a file, potentially across devices. Parameters: src_filename \u2013 A string, the name of the file to copy. dst_filename \u2013 A string, where to copy the file. mkdirs \u2013 A flag, true if we should create a non-existing destination directory. Source code in beancount/ingest/file.py def move_xdev_file(src_filename, dst_filename, mkdirs=False): \"\"\"Move a file, potentially across devices. Args: src_filename: A string, the name of the file to copy. dst_filename: A string, where to copy the file. mkdirs: A flag, true if we should create a non-existing destination directory. \"\"\" # Create missing directory if required. dst_dirname = path.dirname(dst_filename) if mkdirs: if not path.exists(dst_dirname): os.makedirs(dst_dirname) else: if not path.exists(dst_dirname): raise OSError(\"Destination directory '{}' does not exist.\".format(dst_dirname)) # Copy the file to its new name. shutil.copyfile(src_filename, dst_filename) # Remove the old file. Note that we copy and remove to support # cross-device moves, because it's sensible that the destination might # be on an encrypted device. os.remove(src_filename)","title":"move_xdev_file()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.file.run","text":"Run the subcommand. Source code in beancount/ingest/file.py def run(args, parser, importers_list, files_or_directories, hooks=None): \"\"\"Run the subcommand.\"\"\" # If the output directory is not specified, move the files at the root where # the import configuration file is located. (Providing this default seems # better than using a required option.) if args.output_dir is None: if hasattr(args, 'config'): args.output_dir = path.dirname(path.abspath(args.config)) else: import __main__ # pylint: disable=import-outside-toplevel args.output_dir = path.dirname(path.abspath(__main__.__file__)) # Make sure the output directory exists. if not path.exists(args.output_dir): parser.error('Output directory \"{}\" does not exist.'.format(args.output_dir)) file(importers_list, files_or_directories, args.output_dir, dry_run=args.dry_run, mkdirs=True, overwrite=args.overwrite, idify=True, logfile=sys.stdout) return 0","title":"run()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.identify","text":"Identify script. Read an import script and a list of downloaded filenames or directories of 2downloaded files, and for each of those files, identify which importer it should be associated with.","title":"identify"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.identify.add_arguments","text":"Add arguments for the identify command. Source code in beancount/ingest/identify.py def add_arguments(parser): \"\"\"Add arguments for the identify command.\"\"\"","title":"add_arguments()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.identify.find_imports","text":"Given an importer configuration, search for files that can be imported in the list of files or directories, run the signature checks on them and return a list of (filename, importers), where 'importers' is a list of importers that matched the file. Parameters: importer_config \u2013 a list of importer instances that define the config. files_or_directories \u2013 a list of files of directories to walk recursively and hunt for files to import. logfile \u2013 A file object to write log entries to, or None, in which case no log is written out. Yields: Triples of filename found, textified contents of the file, and list of importers matching this file. Source code in beancount/ingest/identify.py def find_imports(importer_config, files_or_directories, logfile=None): \"\"\"Given an importer configuration, search for files that can be imported in the list of files or directories, run the signature checks on them and return a list of (filename, importers), where 'importers' is a list of importers that matched the file. Args: importer_config: a list of importer instances that define the config. files_or_directories: a list of files of directories to walk recursively and hunt for files to import. logfile: A file object to write log entries to, or None, in which case no log is written out. Yields: Triples of filename found, textified contents of the file, and list of importers matching this file. \"\"\" # Iterate over all files found; accumulate the entries by identification. for filename in file_utils.find_files(files_or_directories): if logfile is not None: logfile.write(SECTION.format(filename)) logfile.write('\\n') # Skip files that are simply too large. size = path.getsize(filename) if size > FILE_TOO_LARGE_THRESHOLD: logging.warning(\"File too large: '{}' ({} bytes); skipping.\".format( filename, size)) continue # For each of the sources the user has declared, identify which # match the text. file = cache.get_file(filename) matching_importers = [] for importer in importer_config: try: matched = importer.identify(file) if matched: matching_importers.append(importer) except Exception as exc: logging.exception(\"Importer %s.identify() raised an unexpected error: %s\", importer.name(), exc) yield (filename, matching_importers)","title":"find_imports()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.identify.identify","text":"Run the identification loop. Parameters: importers_list \u2013 A list of importer instances. files_or_directories \u2013 A list of strings, files or directories. Source code in beancount/ingest/identify.py def identify(importers_list, files_or_directories): \"\"\"Run the identification loop. Args: importers_list: A list of importer instances. files_or_directories: A list of strings, files or directories. \"\"\" logfile = sys.stdout for filename, importers in find_imports(importers_list, files_or_directories, logfile=logfile): file = cache.get_file(filename) for importer in importers: logfile.write('Importer: {}\\n'.format(importer.name() if importer else '-')) logfile.write('Account: {}\\n'.format(importer.file_account(file))) logfile.write('\\n')","title":"identify()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.identify.run","text":"Run the subcommand. Source code in beancount/ingest/identify.py def run(_, __, importers_list, files_or_directories, hooks=None): \"\"\"Run the subcommand.\"\"\" return identify(importers_list, files_or_directories)","title":"run()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importer","text":"Importer protocol. All importers must comply with this interface and implement at least some of its methods. A configuration consists in a simple list of such importer instances. The importer processes run through the importers, calling some of its methods in order to identify, extract and file the downloaded files. Each of the methods accept a cache.FileMemo object which has a 'name' attribute with the filename to process, but which also provides a place to cache conversions. Use its convert() method whenever possible to avoid carrying out the same conversion multiple times. See beancount.ingest.cache for more details. Synopsis: name(): Return a unique identifier for the importer instance. identify(): Return true if the identifier is able to process the file. extract(): Extract directives from a file's contents and return of list of entries. file_account(): Return an account name associated with the given file for this importer. file_date(): Return a date associated with the downloaded file (e.g., the statement date). file_name(): Return a cleaned up filename for storage (optional). Just to be clear: Although this importer will not raise NotImplementedError exceptions (it returns default values for each method), you NEED to derive from it in order to do anything meaningful. Simply instantiating this importer will not match not provide any useful information. It just defines the protocol for all importers.","title":"importer"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importer.ImporterProtocol","text":"Interface that all source importers need to comply with.","title":"ImporterProtocol"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importer.ImporterProtocol.__str__","text":"Return a unique id/name for this importer. Returns: A string which uniquely identifies this importer. Source code in beancount/ingest/importer.py def name(self): \"\"\"Return a unique id/name for this importer. Returns: A string which uniquely identifies this importer. \"\"\" cls = self.__class__ return '{}.{}'.format(cls.__module__, cls.__name__)","title":"__str__()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importer.ImporterProtocol.extract","text":"Extract transactions from a file. If the importer would like to flag a returned transaction as a known duplicate, it may opt to set the special flag \" duplicate \" to True, and the transaction should be treated as a duplicate by the extraction code. This is a way to let the importer use particular information about previously imported transactions in order to flag them as duplicates. For example, if an importer has a way to get a persistent unique id for each of the imported transactions. (See this discussion for context: https://groups.google.com/d/msg/beancount/0iV-ipBJb8g/-uk4wsH2AgAJ) Parameters: file \u2013 A cache.FileMemo instance. existing_entries \u2013 An optional list of existing directives loaded from the ledger which is intended to contain the extracted entries. This is only provided if the user provides them via a flag in the extractor program. Returns: A list of new, imported directives (usually mostly Transactions) extracted from the file. Source code in beancount/ingest/importer.py def extract(self, file, existing_entries=None): \"\"\"Extract transactions from a file. If the importer would like to flag a returned transaction as a known duplicate, it may opt to set the special flag \"__duplicate__\" to True, and the transaction should be treated as a duplicate by the extraction code. This is a way to let the importer use particular information about previously imported transactions in order to flag them as duplicates. For example, if an importer has a way to get a persistent unique id for each of the imported transactions. (See this discussion for context: https://groups.google.com/d/msg/beancount/0iV-ipBJb8g/-uk4wsH2AgAJ) Args: file: A cache.FileMemo instance. existing_entries: An optional list of existing directives loaded from the ledger which is intended to contain the extracted entries. This is only provided if the user provides them via a flag in the extractor program. Returns: A list of new, imported directives (usually mostly Transactions) extracted from the file. \"\"\"","title":"extract()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importer.ImporterProtocol.file_account","text":"Return an account associated with the given file. Note: If you don't implement this method you won't be able to move the files into its preservation hierarchy; the bean-file command won't work. Also, normally the returned account is not a function of the input file--just of the importer--but it is provided anyhow. Parameters: file \u2013 A cache.FileMemo instance. Returns: The name of the account that corresponds to this importer. Source code in beancount/ingest/importer.py def file_account(self, file): \"\"\"Return an account associated with the given file. Note: If you don't implement this method you won't be able to move the files into its preservation hierarchy; the bean-file command won't work. Also, normally the returned account is not a function of the input file--just of the importer--but it is provided anyhow. Args: file: A cache.FileMemo instance. Returns: The name of the account that corresponds to this importer. \"\"\"","title":"file_account()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importer.ImporterProtocol.file_date","text":"Attempt to obtain a date that corresponds to the given file. Parameters: file \u2013 A cache.FileMemo instance. Returns: A date object, if successful, or None if a date could not be extracted. (If no date is returned, the file creation time is used. This is the default.) Source code in beancount/ingest/importer.py def file_date(self, file): \"\"\"Attempt to obtain a date that corresponds to the given file. Args: file: A cache.FileMemo instance. Returns: A date object, if successful, or None if a date could not be extracted. (If no date is returned, the file creation time is used. This is the default.) \"\"\"","title":"file_date()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importer.ImporterProtocol.file_name","text":"A filter that optionally renames a file before filing. This is used to make tidy filenames for filed/stored document files. If you don't implement this and return None, the same filename is used. Note that if you return a filename, a simple, RELATIVE filename must be returned, not an absolute filename. Parameters: file \u2013 A cache.FileMemo instance. Returns: The tidied up, new filename to store it as. Source code in beancount/ingest/importer.py def file_name(self, file): \"\"\"A filter that optionally renames a file before filing. This is used to make tidy filenames for filed/stored document files. If you don't implement this and return None, the same filename is used. Note that if you return a filename, a simple, RELATIVE filename must be returned, not an absolute filename. Args: file: A cache.FileMemo instance. Returns: The tidied up, new filename to store it as. \"\"\"","title":"file_name()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importer.ImporterProtocol.identify","text":"Return true if this importer matches the given file. Parameters: file \u2013 A cache.FileMemo instance. Returns: A boolean, true if this importer can handle this file. Source code in beancount/ingest/importer.py def identify(self, file): \"\"\"Return true if this importer matches the given file. Args: file: A cache.FileMemo instance. Returns: A boolean, true if this importer can handle this file. \"\"\"","title":"identify()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importer.ImporterProtocol.name","text":"Return a unique id/name for this importer. Returns: A string which uniquely identifies this importer. Source code in beancount/ingest/importer.py def name(self): \"\"\"Return a unique id/name for this importer. Returns: A string which uniquely identifies this importer. \"\"\" cls = self.__class__ return '{}.{}'.format(cls.__module__, cls.__name__)","title":"name()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers","text":"","title":"importers"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.config","text":"Mixin to add support for configuring importers with multiple accounts. This importer implements some simple common functionality to create importers which accept a long number of account names or regular expressions on the set of account names. This is inspired by functionality in the importers in the previous iteration of the ingest code, which used to be its own project.","title":"config"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.config.ConfigImporterMixin","text":"A mixin class which supports configuration of account names. Mix this into the implementation of a importer.ImporterProtocol.","title":"ConfigImporterMixin"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.config.ConfigImporterMixin.__init__","text":"Provide a list of accounts and regexps as configuration to the importer. Parameters: config \u2013 A dict of configuration accounts, that must match the values declared in the class' REQUIRED_CONFIG. Source code in beancount/ingest/importers/config.py def __init__(self, config): \"\"\"Provide a list of accounts and regexps as configuration to the importer. Args: config: A dict of configuration accounts, that must match the values declared in the class' REQUIRED_CONFIG. \"\"\" super().__init__() # Check that the required configuration values are present. assert isinstance(config, dict), \"Configuration must be a dict type\" if not self._verify_config(config): raise ValueError(\"Invalid config {}, requires {}\".format( config, self.REQUIRED_CONFIG)) self.config = config","title":"__init__()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.csv","text":"CSV importer.","title":"csv"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.csv.Col","text":"The set of interpretable columns.","title":"Col"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.csv.Importer","text":"Importer for CSV files.","title":"Importer"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.csv.Importer.__init__","text":"Constructor. Parameters: config \u2013 A dict of Col enum types to the names or indexes of the columns. account \u2013 An account string, the account to post this to. currency \u2013 A currency string, the currency of this account. regexps \u2013 A list of regular expression strings. skip_lines ( int ) \u2013 Skip first x (garbage) lines of file. last4_map ( Optional[Dict] ) \u2013 A dict that maps last 4 digits of the card to a friendly string. categorizer ( Optional[Callable] ) \u2013 A callable that attaches the other posting (usually expenses) to a transaction with only single posting. institution ( Optional[str] ) \u2013 An optional name of an institution to rename the files to. debug ( bool ) \u2013 Whether or not to print debug information csv_dialect ( Union[str, csv.Dialect] ) \u2013 A csv dialect given either as string or as instance or subclass of csv.Dialect . dateutil_kwds ( Optional[Dict] ) \u2013 An optional dict defining the dateutil parser kwargs. narration_sep ( str ) \u2013 A string, a separator to use for splitting up the payee and narration fields of a source field. encoding ( Optional[str] ) \u2013 An optional encoding for the file. Typically useful for files encoded in 'latin1' instead of 'utf-8' (the default). invert_sign ( Optional[bool] ) \u2013 If true, invert the amount's sign unconditionally. **kwds \u2013 Extra keyword arguments to provide to the base mixins. Source code in beancount/ingest/importers/csv.py def __init__(self, config, account, currency, regexps=None, skip_lines: int = 0, last4_map: Optional[Dict] = None, categorizer: Optional[Callable] = None, institution: Optional[str] = None, debug: bool = False, csv_dialect: Union[str, csv.Dialect] = 'excel', dateutil_kwds: Optional[Dict] = None, narration_sep: str = '; ', encoding: Optional[str] = None, invert_sign: Optional[bool] = False, **kwds): \"\"\"Constructor. Args: config: A dict of Col enum types to the names or indexes of the columns. account: An account string, the account to post this to. currency: A currency string, the currency of this account. regexps: A list of regular expression strings. skip_lines: Skip first x (garbage) lines of file. last4_map: A dict that maps last 4 digits of the card to a friendly string. categorizer: A callable that attaches the other posting (usually expenses) to a transaction with only single posting. institution: An optional name of an institution to rename the files to. debug: Whether or not to print debug information csv_dialect: A `csv` dialect given either as string or as instance or subclass of `csv.Dialect`. dateutil_kwds: An optional dict defining the dateutil parser kwargs. narration_sep: A string, a separator to use for splitting up the payee and narration fields of a source field. encoding: An optional encoding for the file. Typically useful for files encoded in 'latin1' instead of 'utf-8' (the default). invert_sign: If true, invert the amount's sign unconditionally. **kwds: Extra keyword arguments to provide to the base mixins. \"\"\" assert isinstance(config, dict), \"Invalid type: {}\".format(config) self.config = config self.currency = currency assert isinstance(skip_lines, int) self.skip_lines = skip_lines self.last4_map = last4_map or {} self.debug = debug self.dateutil_kwds = dateutil_kwds self.csv_dialect = csv_dialect self.narration_sep = narration_sep self.encoding = encoding self.invert_sign = invert_sign self.categorizer = categorizer # Prepare kwds for filing mixin. kwds['filing'] = account if institution: prefix = kwds.get('prefix', None) assert prefix is None kwds['prefix'] = institution # Prepare kwds for identifier mixin. if isinstance(regexps, str): regexps = [regexps] matchers = kwds.setdefault('matchers', []) matchers.append(('mime', 'text/csv')) if regexps: for regexp in regexps: matchers.append(('content', regexp)) super().__init__(**kwds)","title":"__init__()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.csv.Importer.extract","text":"Extract transactions from a file. If the importer would like to flag a returned transaction as a known duplicate, it may opt to set the special flag \" duplicate \" to True, and the transaction should be treated as a duplicate by the extraction code. This is a way to let the importer use particular information about previously imported transactions in order to flag them as duplicates. For example, if an importer has a way to get a persistent unique id for each of the imported transactions. (See this discussion for context: https://groups.google.com/d/msg/beancount/0iV-ipBJb8g/-uk4wsH2AgAJ) Parameters: file \u2013 A cache.FileMemo instance. existing_entries \u2013 An optional list of existing directives loaded from the ledger which is intended to contain the extracted entries. This is only provided if the user provides them via a flag in the extractor program. Returns: A list of new, imported directives (usually mostly Transactions) extracted from the file. Source code in beancount/ingest/importers/csv.py def extract(self, file, existing_entries=None): account = self.file_account(file) entries = [] # Normalize the configuration to fetch by index. iconfig, has_header = normalize_config( self.config, file.head(), self.csv_dialect, self.skip_lines) reader = iter(csv.reader(open(file.name, encoding=self.encoding), dialect=self.csv_dialect)) # Skip garbage lines for _ in range(self.skip_lines): next(reader) # Skip header, if one was detected. if has_header: next(reader) def get(row, ftype): try: return row[iconfig[ftype]] if ftype in iconfig else None except IndexError: # FIXME: this should not happen return None # Parse all the transactions. first_row = last_row = None for index, row in enumerate(reader, 1): if not row: continue if row[0].startswith('#'): continue # If debugging, print out the rows. if self.debug: print(row) if first_row is None: first_row = row last_row = row # Extract the data we need from the row, based on the configuration. date = get(row, Col.DATE) txn_date = get(row, Col.TXN_DATE) txn_time = get(row, Col.TXN_TIME) payee = get(row, Col.PAYEE) if payee: payee = payee.strip() fields = filter(None, [get(row, field) for field in (Col.NARRATION1, Col.NARRATION2, Col.NARRATION3)]) narration = self.narration_sep.join( field.strip() for field in fields).replace('\\n', '; ') tag = get(row, Col.TAG) tags = {tag} if tag is not None else data.EMPTY_SET link = get(row, Col.REFERENCE_ID) links = {link} if link is not None else data.EMPTY_SET last4 = get(row, Col.LAST4) balance = get(row, Col.BALANCE) # Create a transaction meta = data.new_metadata(file.name, index) if txn_date is not None: meta['date'] = parse_date_liberally(txn_date, self.dateutil_kwds) if txn_time is not None: meta['time'] = str(dateutil.parser.parse(txn_time).time()) if balance is not None: meta['balance'] = D(balance) if last4: last4_friendly = self.last4_map.get(last4.strip()) meta['card'] = last4_friendly if last4_friendly else last4 date = parse_date_liberally(date, self.dateutil_kwds) txn = data.Transaction(meta, date, self.FLAG, payee, narration, tags, links, []) # Attach one posting to the transaction amount_debit, amount_credit = self.get_amounts(iconfig, row) # Skip empty transactions if amount_debit is None and amount_credit is None: continue for amount in [amount_debit, amount_credit]: if amount is None: continue if self.invert_sign: amount = -amount units = Amount(amount, self.currency) txn.postings.append( data.Posting(account, units, None, None, None, None)) # Attach the other posting(s) to the transaction. if isinstance(self.categorizer, collections.abc.Callable): txn = self.categorizer(txn) # Add the transaction to the output list entries.append(txn) # Figure out if the file is in ascending or descending order. first_date = parse_date_liberally(get(first_row, Col.DATE), self.dateutil_kwds) last_date = parse_date_liberally(get(last_row, Col.DATE), self.dateutil_kwds) is_ascending = first_date < last_date # Reverse the list if the file is in descending order if not is_ascending: entries = list(reversed(entries)) # Add a balance entry if possible if Col.BALANCE in iconfig and entries: entry = entries[-1] date = entry.date + datetime.timedelta(days=1) balance = entry.meta.get('balance', None) if balance is not None: meta = data.new_metadata(file.name, index) entries.append( data.Balance(meta, date, account, Amount(balance, self.currency), None, None)) # Remove the 'balance' metadata. for entry in entries: entry.meta.pop('balance', None) return entries","title":"extract()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.csv.Importer.file_date","text":"Get the maximum date from the file. Source code in beancount/ingest/importers/csv.py def file_date(self, file): \"Get the maximum date from the file.\" iconfig, has_header = normalize_config( self.config, file.head(), self.csv_dialect, self.skip_lines) if Col.DATE in iconfig: reader = iter(csv.reader(open(file.name), dialect=self.csv_dialect)) for _ in range(self.skip_lines): next(reader) if has_header: next(reader) max_date = None for row in reader: if not row: continue if row[0].startswith('#'): continue date_str = row[iconfig[Col.DATE]] date = parse_date_liberally(date_str, self.dateutil_kwds) if max_date is None or date > max_date: max_date = date return max_date","title":"file_date()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.csv.Importer.get_amounts","text":"See function get_amounts() for details. This method is present to allow clients to override it in order to deal with special cases, e.g., columns with currency symbols in them. Source code in beancount/ingest/importers/csv.py def get_amounts(self, iconfig, row, allow_zero_amounts=False): \"\"\"See function get_amounts() for details. This method is present to allow clients to override it in order to deal with special cases, e.g., columns with currency symbols in them. \"\"\" return get_amounts(iconfig, row, allow_zero_amounts)","title":"get_amounts()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.csv.get_amounts","text":"Get the amount columns of a row. Parameters: iconfig \u2013 A dict of Col to row index. row \u2013 A row array containing the values of the given row. allow_zero_amounts \u2013 Is a transaction with amount D('0.00') okay? If not, return (None, None). Returns: A pair of (debit-amount, credit-amount), both of which are either an instance of Decimal or None, or not available. Source code in beancount/ingest/importers/csv.py def get_amounts(iconfig, row, allow_zero_amounts=False): \"\"\"Get the amount columns of a row. Args: iconfig: A dict of Col to row index. row: A row array containing the values of the given row. allow_zero_amounts: Is a transaction with amount D('0.00') okay? If not, return (None, None). Returns: A pair of (debit-amount, credit-amount), both of which are either an instance of Decimal or None, or not available. \"\"\" debit, credit = None, None if Col.AMOUNT in iconfig: credit = row[iconfig[Col.AMOUNT]] else: debit, credit = [row[iconfig[col]] if col in iconfig else None for col in [Col.AMOUNT_DEBIT, Col.AMOUNT_CREDIT]] # If zero amounts aren't allowed, return null value. is_zero_amount = ((credit is not None and D(credit) == ZERO) and (debit is not None and D(debit) == ZERO)) if not allow_zero_amounts and is_zero_amount: return (None, None) return (-D(debit) if debit else None, D(credit) if credit else None)","title":"get_amounts()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.csv.normalize_config","text":"Using the header line, convert the configuration field name lookups to int indexes. Parameters: config \u2013 A dict of Col types to string or indexes. head \u2013 A string, some decent number of bytes of the head of the file. dialect \u2013 A dialect definition to parse the header skip_lines ( int ) \u2013 Skip first x (garbage) lines of file. Returns: A pair of A dict of Col types to integer indexes of the fields, and a boolean, true if the file has a header. Exceptions: ValueError \u2013 If there is no header and the configuration does not consist entirely of integer indexes. Source code in beancount/ingest/importers/csv.py def normalize_config(config, head, dialect='excel', skip_lines: int = 0): \"\"\"Using the header line, convert the configuration field name lookups to int indexes. Args: config: A dict of Col types to string or indexes. head: A string, some decent number of bytes of the head of the file. dialect: A dialect definition to parse the header skip_lines: Skip first x (garbage) lines of file. Returns: A pair of A dict of Col types to integer indexes of the fields, and a boolean, true if the file has a header. Raises: ValueError: If there is no header and the configuration does not consist entirely of integer indexes. \"\"\" # Skip garbage lines before sniffing the header assert isinstance(skip_lines, int) assert skip_lines >= 0 for _ in range(skip_lines): head = head[head.find('\\n')+1:] has_header = csv.Sniffer().has_header(head) if has_header: header = next(csv.reader(io.StringIO(head), dialect=dialect)) field_map = {field_name.strip(): index for index, field_name in enumerate(header)} index_config = {} for field_type, field in config.items(): if isinstance(field, str): field = field_map[field] index_config[field_type] = field else: if any(not isinstance(field, int) for field_type, field in config.items()): raise ValueError(\"CSV config without header has non-index fields: \" \"{}\".format(config)) index_config = config return index_config, has_header","title":"normalize_config()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.fileonly","text":"A simplistic importer that can be used just to file away some download. Sometimes you just want to save and accumulate data","title":"fileonly"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.fileonly.Importer","text":"An importer that supports only matching (identification) and filing.","title":"Importer"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins","text":"","title":"mixins"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.config","text":"Base class that implements configuration and a filing account.","title":"config"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.config.ConfigMixin","text":"","title":"ConfigMixin"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.config.ConfigMixin.__init__","text":"Pull 'config' from kwds. Source code in beancount/ingest/importers/mixins/config.py def __init__(self, **kwds): \"\"\"Pull 'config' from kwds.\"\"\" config = kwds.pop('config', None) schema = self.REQUIRED_CONFIG if config or schema: assert config is not None assert schema is not None self.config = validate_config(config, config, self) else: self.config = None super().__init__(**kwds)","title":"__init__()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.config.validate_config","text":"Check the configuration account provided by the user against the accounts required by the source importer. Parameters: config \u2013 A config dict of actual values on an importer. schema \u2013 A dict of declarations of required values. Exceptions: ValueError \u2013 If the configuration is invalid. Returns: A validated configuration dict. Source code in beancount/ingest/importers/mixins/config.py def validate_config(config, schema, importer): \"\"\"Check the configuration account provided by the user against the accounts required by the source importer. Args: config: A config dict of actual values on an importer. schema: A dict of declarations of required values. Raises: ValueError: If the configuration is invalid. Returns: A validated configuration dict. \"\"\" provided_options = set(config) required_options = set(schema) for option in (required_options - provided_options): raise ValueError(\"Missing value from user configuration for importer {}: {}\".format( importer.__class__.__name__, option)) for option in (provided_options - required_options): raise ValueError(\"Unknown value in user configuration for importer {}: {}\".format( importer.__class__.__name__, option)) # FIXME: Validate types as well, including account type as a default. # FIXME: Here we could validate account names by looking them up from the # existing ledger. return config","title":"validate_config()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.filing","text":"Base class that implements filing account. It also sports an optional prefix to prepend to the renamed filename. Typically you can put the name of the institution there, so you get a renamed filename like this: YYYY-MM-DD.institution.Original_File_Name.pdf","title":"filing"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.filing.FilingMixin","text":"","title":"FilingMixin"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.filing.FilingMixin.__init__","text":"Pull 'filing' and 'prefix' from kwds. Parameters: filing \u2013 The name of the account to file to. prefix \u2013 The name of the institution prefix to insert. Source code in beancount/ingest/importers/mixins/filing.py 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)","title":"__init__()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.filing.FilingMixin.file_account","text":"Return an account associated with the given file. Note: If you don't implement this method you won't be able to move the files into its preservation hierarchy; the bean-file command won't work. Also, normally the returned account is not a function of the input file--just of the importer--but it is provided anyhow. Parameters: file \u2013 A cache.FileMemo instance. Returns: The name of the account that corresponds to this importer. Source code in beancount/ingest/importers/mixins/filing.py def file_account(self, file): return self.filing_account","title":"file_account()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.filing.FilingMixin.file_name","text":"Return the optional renamed account filename. Source code in beancount/ingest/importers/mixins/filing.py def file_name(self, file): \"\"\"Return the optional renamed account filename.\"\"\" supername = super().file_name(file) if not self.prefix: return supername else: return '.'.join(filter(None, [self.prefix, supername or path.basename(file.name)]))","title":"file_name()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.filing.FilingMixin.name","text":"Include the filing account in the name. Source code in beancount/ingest/importers/mixins/filing.py def name(self): \"\"\"Include the filing account in the name.\"\"\" return '{}: \"{}\"'.format(super().name(), self.filing_account)","title":"name()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.identifier","text":"Base class that implements identification using regular expressions.","title":"identifier"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.identifier.IdentifyMixin","text":"","title":"IdentifyMixin"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.identifier.IdentifyMixin.__init__","text":"Pull 'matchers' and 'converter' from kwds. Source code in beancount/ingest/importers/mixins/identifier.py def __init__(self, **kwds): \"\"\"Pull 'matchers' and 'converter' from kwds.\"\"\" self.remap = collections.defaultdict(list) matchers = kwds.pop('matchers', []) cls_matchers = getattr(self, 'matchers', []) assert isinstance(matchers, list) assert isinstance(cls_matchers, list) for part, regexp in itertools.chain(matchers, cls_matchers): assert part in _PARTS, repr(part) assert isinstance(regexp, str), repr(regexp) self.remap[part].append(re.compile(regexp)) # Converter is a fn(filename: Text) -> contents: Text. self.converter = kwds.pop('converter', getattr(self, 'converter', None)) super().__init__(**kwds)","title":"__init__()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.identifier.IdentifyMixin.identify","text":"Return true if this importer matches the given file. Parameters: file \u2013 A cache.FileMemo instance. Returns: A boolean, true if this importer can handle this file. Source code in beancount/ingest/importers/mixins/identifier.py def identify(self, file): return identify(self.remap, self.converter, file)","title":"identify()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.mixins.identifier.identify","text":"Identify the contents of a file. Parameters: remap \u2013 A dict of 'part' to list-of-compiled-regexp objects, where each item is a specification to match against its part. The 'part' can be one of 'mime', 'filename' or 'content'. converter \u2013 A Returns: A boolean, true if the file is not rejected by the constraints. Source code in beancount/ingest/importers/mixins/identifier.py def identify(remap, converter, file): \"\"\"Identify the contents of a file. Args: remap: A dict of 'part' to list-of-compiled-regexp objects, where each item is a specification to match against its part. The 'part' can be one of 'mime', 'filename' or 'content'. converter: A Returns: A boolean, true if the file is not rejected by the constraints. \"\"\" if remap.get('mime', None): mimetype = file.convert(cache.mimetype) if not all(regexp.search(mimetype) for regexp in remap['mime']): return False if remap.get('filename', None): if not all(regexp.search(file.name) for regexp in remap['filename']): return False if remap.get('content', None): # If this is a text file, read the whole thing in memory. text = file.convert(converter or cache.contents) if not all(regexp.search(text) for regexp in remap['content']): return False return True","title":"identify()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx","text":"OFX file format importer for bank and credit card statements. https://en.wikipedia.org/wiki/Open_Financial_Exchange This importer will parse a single account in the OFX file. Instantiate it multiple times with different accounts if it has many accounts. It makes more sense to do it this way so that you can define your importer configuration account by account. Note that this importer is provided as an example and with no guarantees. It's not really super great. On the other hand, I've been using it for more than five years over multiple accounts, so it has been useful to me (it works, by some measure of \"works\"). If you need a more powerful or compliant OFX importer please consider either writing one or contributing changes. Also, this importer does its own very basic parsing; a better one would probably use (and depend on) the ofxparse module (see https://sites.google.com/site/ofxparse/).","title":"ofx"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.BalanceType","text":"Type of Balance directive to be inserted.","title":"BalanceType"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.Importer","text":"An importer for Open Financial Exchange files.","title":"Importer"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.Importer.__init__","text":"Create a new importer posting to the given account. Parameters: account \u2013 An account string, the account onto which to post all the amounts parsed. acctid_regexp \u2013 A regexp, to match against the tag of the OFX file. basename \u2013 An optional string, the name of the new files. balance_type \u2013 An enum of type BalanceType. Source code in beancount/ingest/importers/ofx.py def __init__(self, acctid_regexp, account, basename=None, balance_type=BalanceType.DECLARED): \"\"\"Create a new importer posting to the given account. Args: account: An account string, the account onto which to post all the amounts parsed. acctid_regexp: A regexp, to match against the tag of the OFX file. basename: An optional string, the name of the new files. balance_type: An enum of type BalanceType. \"\"\" self.acctid_regexp = acctid_regexp self.account = account self.basename = basename self.balance_type = balance_type","title":"__init__()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.Importer.extract","text":"Extract a list of partially complete transactions from the file. Source code in beancount/ingest/importers/ofx.py def extract(self, file, existing_entries=None): \"\"\"Extract a list of partially complete transactions from the file.\"\"\" soup = bs4.BeautifulSoup(file.contents(), 'lxml') return extract(soup, file.name, self.acctid_regexp, self.account, self.FLAG, self.balance_type)","title":"extract()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.Importer.file_account","text":"Return the account against which we post transactions. Source code in beancount/ingest/importers/ofx.py def file_account(self, _): \"\"\"Return the account against which we post transactions.\"\"\" return self.account","title":"file_account()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.Importer.file_date","text":"Return the optional renamed account filename. Source code in beancount/ingest/importers/ofx.py def file_date(self, file): \"\"\"Return the optional renamed account filename.\"\"\" return find_max_date(file.contents())","title":"file_date()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.Importer.file_name","text":"Return the optional renamed account filename. Source code in beancount/ingest/importers/ofx.py def file_name(self, file): \"\"\"Return the optional renamed account filename.\"\"\" if self.basename: return self.basename + path.splitext(file.name)[1]","title":"file_name()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.Importer.identify","text":"Return true if this importer matches the given file. Parameters: file \u2013 A cache.FileMemo instance. Returns: A boolean, true if this importer can handle this file. Source code in beancount/ingest/importers/ofx.py def identify(self, file): # Match for a compatible MIME type. if file.mimetype() not in {'application/x-ofx', 'application/vnd.intu.qbo', 'application/vnd.intu.qfx'}: return False # Match the account id. return any(re.match(self.acctid_regexp, acctid) for acctid in find_acctids(file.contents()))","title":"identify()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.Importer.name","text":"Include the filing account in the name. Source code in beancount/ingest/importers/ofx.py def name(self): \"\"\"Include the filing account in the name.\"\"\" return '{}: \"{}\"'.format(super().name(), self.file_account(None))","title":"name()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.build_transaction","text":"Build a single transaction. Parameters: stmttrn \u2013 A bs4.element.Tag. flag \u2013 A single-character string. account \u2013 An account string, the account to insert. currency \u2013 A currency string. Returns: A Transaction instance. Source code in beancount/ingest/importers/ofx.py def build_transaction(stmttrn, flag, account, currency): \"\"\"Build a single transaction. Args: stmttrn: A bs4.element.Tag. flag: A single-character string. account: An account string, the account to insert. currency: A currency string. Returns: A Transaction instance. \"\"\" # Find the date. date = parse_ofx_time(find_child(stmttrn, 'dtposted')).date() # There's no distinct payee. payee = None # Construct a description that represents all the text content in the node. name = find_child(stmttrn, 'name', saxutils.unescape) memo = find_child(stmttrn, 'memo', saxutils.unescape) # Remove memos duplicated from the name. if memo == name: memo = None # Add the transaction type to the description, unless it's not useful. trntype = find_child(stmttrn, 'trntype', saxutils.unescape) if trntype in ('DEBIT', 'CREDIT'): trntype = None narration = ' / '.join(filter(None, [name, memo, trntype])) # Create a single posting for it; the user will have to manually categorize # the other side. number = find_child(stmttrn, 'trnamt', D) units = amount.Amount(number, currency) posting = data.Posting(account, units, None, None, None, None) # Build the transaction with a single leg. fileloc = data.new_metadata('', 0) return data.Transaction(fileloc, date, flag, payee, narration, data.EMPTY_SET, data.EMPTY_SET, [posting])","title":"build_transaction()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.extract","text":"Extract transactions from an OFX file. Parameters: soup \u2013 A BeautifulSoup root node. acctid_regexp \u2013 A regular expression string matching the account we're interested in. account \u2013 An account string onto which to post the amounts found in the file. flag \u2013 A single-character string. balance_type \u2013 An enum of type BalanceType. Returns: A sorted list of entries. Source code in beancount/ingest/importers/ofx.py def extract(soup, filename, acctid_regexp, account, flag, balance_type): \"\"\"Extract transactions from an OFX file. Args: soup: A BeautifulSoup root node. acctid_regexp: A regular expression string matching the account we're interested in. account: An account string onto which to post the amounts found in the file. flag: A single-character string. balance_type: An enum of type BalanceType. Returns: A sorted list of entries. \"\"\" new_entries = [] counter = itertools.count() for acctid, currency, transactions, balance in find_statement_transactions(soup): if not re.match(acctid_regexp, acctid): continue # Create Transaction directives. stmt_entries = [] for stmttrn in transactions: entry = build_transaction(stmttrn, flag, account, currency) entry = entry._replace(meta=data.new_metadata(filename, next(counter))) stmt_entries.append(entry) stmt_entries = data.sorted(stmt_entries) new_entries.extend(stmt_entries) # Create a Balance directive. if balance and balance_type is not BalanceType.NONE: date, number = balance if balance_type is BalanceType.LAST and stmt_entries: date = stmt_entries[-1].date # The Balance assertion occurs at the beginning of the date, so move # it to the following day. date += datetime.timedelta(days=1) meta = data.new_metadata(filename, next(counter)) balance_entry = data.Balance(meta, date, account, amount.Amount(number, currency), None, None) new_entries.append(balance_entry) return data.sorted(new_entries)","title":"extract()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.find_acctids","text":"Find the list of tags. Parameters: contents \u2013 A string, the contents of the OFX file. Returns: A list of strings, the contents of the tags. Source code in beancount/ingest/importers/ofx.py def find_acctids(contents): \"\"\"Find the list of tags. Args: contents: A string, the contents of the OFX file. Returns: A list of strings, the contents of the tags. \"\"\" # Match the account id. Don't bother parsing the entire thing as XML, just # match the tag for this purpose. This'll work fine enough. for match in re.finditer('([^<]*)', contents): yield match.group(1)","title":"find_acctids()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.find_child","text":"Find a child under the given node and return its value. Parameters: node \u2013 A bs4.element.Tag. name \u2013 A string, the name of the child node. conversion \u2013 A callable object used to convert the value to a new data type. Returns: A string, or None. Source code in beancount/ingest/importers/ofx.py def find_child(node, name, conversion=None): \"\"\"Find a child under the given node and return its value. Args: node: A bs4.element.Tag. name: A string, the name of the child node. conversion: A callable object used to convert the value to a new data type. Returns: A string, or None. \"\"\" child = node.find(name) if not child: return None value = child.contents[0].strip() if conversion: value = conversion(value) return value","title":"find_child()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.find_currency","text":"Find the first currency in the XML tree. Parameters: soup \u2013 A BeautifulSoup root node. Returns: A string, the first currency found in the file. Returns None if no currency is found. Source code in beancount/ingest/importers/ofx.py def find_currency(soup): \"\"\"Find the first currency in the XML tree. Args: soup: A BeautifulSoup root node. Returns: A string, the first currency found in the file. Returns None if no currency is found. \"\"\" for stmtrs in soup.find_all(re.compile('.*stmtrs$')): for currency_node in stmtrs.find_all('curdef'): currency = currency_node.contents[0] if currency is not None: return currency","title":"find_currency()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.find_max_date","text":"Extract the report date from the file. Source code in beancount/ingest/importers/ofx.py def find_max_date(contents): \"\"\"Extract the report date from the file.\"\"\" soup = bs4.BeautifulSoup(contents, 'lxml') dates = [] for ledgerbal in soup.find_all('ledgerbal'): dtasof = ledgerbal.find('dtasof') dates.append(parse_ofx_time(dtasof.contents[0]).date()) if dates: return max(dates)","title":"find_max_date()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.find_statement_transactions","text":"Find the statement transaction sections in the file. Parameters: soup \u2013 A BeautifulSoup root node. Yields: A trip of An account id string, A currency string, A list of transaction nodes ( BeautifulSoup tags), and A (date, balance amount) for the . Source code in beancount/ingest/importers/ofx.py def find_statement_transactions(soup): \"\"\"Find the statement transaction sections in the file. Args: soup: A BeautifulSoup root node. Yields: A trip of An account id string, A currency string, A list of transaction nodes ( BeautifulSoup tags), and A (date, balance amount) for the . \"\"\" # Process STMTTRNRS and CCSTMTTRNRS tags. for stmtrs in soup.find_all(re.compile('.*stmtrs$')): # For each CURDEF tag. for currency_node in stmtrs.find_all('curdef'): currency = currency_node.contents[0].strip() # Extract ACCTID account information. acctid_node = stmtrs.find('acctid') if acctid_node: acctid = next(acctid_node.children).strip() else: acctid = '' # Get the LEDGERBAL node. There appears to be a single one for all # transaction lists. ledgerbal = stmtrs.find('ledgerbal') balance = None if ledgerbal: dtasof = find_child(ledgerbal, 'dtasof', parse_ofx_time).date() balamt = find_child(ledgerbal, 'balamt', D) balance = (dtasof, balamt) # Process transaction lists (regular or credit-card). for tranlist in stmtrs.find_all(re.compile('(|bank|cc)tranlist')): yield acctid, currency, tranlist.find_all('stmttrn'), balance","title":"find_statement_transactions()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.importers.ofx.parse_ofx_time","text":"Parse an OFX time string and return a datetime object. Parameters: date_str \u2013 A string, the date to be parsed. Returns: A datetime.datetime instance. Source code in beancount/ingest/importers/ofx.py def parse_ofx_time(date_str): \"\"\"Parse an OFX time string and return a datetime object. Args: date_str: A string, the date to be parsed. Returns: A datetime.datetime instance. \"\"\" if len(date_str) < 14: return datetime.datetime.strptime(date_str[:8], '%Y%m%d') else: return datetime.datetime.strptime(date_str[:14], '%Y%m%d%H%M%S')","title":"parse_ofx_time()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression","text":"Support for implementing regression tests on sample files using nose. NOTE: This itself is not a regression test. It's a library used to create regression tests for your importers. Use it like this in your own importer code: def test(): importer = Importer([], { 'FILE' : 'Assets:US:MyBank:Main', }) yield from regression.compare_sample_files(importer, file ) WARNING: This is deprecated. Nose itself has been deprecated for a while and Beancount is now using only pytest. Ignore this and use beancount.ingest.regression_ptest instead.","title":"regression"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression.ImportFileTestCase","text":"Base class for importer tests that compare output to an expected output text.","title":"ImportFileTestCase"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression.ImportFileTestCase.test_expect_extract","text":"Extract entries from a test file and compare against expected output. If an expected file (as .extract) is not present, we issue a warning. Missing expected files can be written out by removing them before running the tests. Parameters: filename \u2013 A string, the name of the file to import using self.importer. Exceptions: AssertionError \u2013 If the contents differ from the expected file. Source code in beancount/ingest/regression.py @test_utils.skipIfRaises(ToolNotInstalled) def test_expect_extract(self, filename, msg): \"\"\"Extract entries from a test file and compare against expected output. If an expected file (as .extract) is not present, we issue a warning. Missing expected files can be written out by removing them before running the tests. Args: filename: A string, the name of the file to import using self.importer. Raises: AssertionError: If the contents differ from the expected file. \"\"\" # Import the file. entries = extract.extract_from_file(filename, self.importer, None, None) # Render the entries to a string. oss = io.StringIO() printer.print_entries(entries, file=oss) string = oss.getvalue() expect_filename = '{}.extract'.format(filename) if path.exists(expect_filename): expect_string = open(expect_filename, encoding='utf-8').read() self.assertEqual(expect_string.strip(), string.strip()) else: # Write out the expected file for review. open(expect_filename, 'w', encoding='utf-8').write(string) self.skipTest(\"Expected file not present; generating '{}'\".format( expect_filename))","title":"test_expect_extract()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression.ImportFileTestCase.test_expect_file_date","text":"Compute the imported file date and compare to an expected output. If an expected file (as .file_date) is not present, we issue a warning. Missing expected files can be written out by removing them before running the tests. Parameters: filename \u2013 A string, the name of the file to import using self.importer. Exceptions: AssertionError \u2013 If the contents differ from the expected file. Source code in beancount/ingest/regression.py @test_utils.skipIfRaises(ToolNotInstalled) def test_expect_file_date(self, filename, msg): \"\"\"Compute the imported file date and compare to an expected output. If an expected file (as .file_date) is not present, we issue a warning. Missing expected files can be written out by removing them before running the tests. Args: filename: A string, the name of the file to import using self.importer. Raises: AssertionError: If the contents differ from the expected file. \"\"\" # Import the date. file = cache.get_file(filename) date = self.importer.file_date(file) if date is None: self.fail(\"No date produced from {}\".format(file.name)) expect_filename = '{}.file_date'.format(file.name) if path.exists(expect_filename) and path.getsize(expect_filename) > 0: expect_date_str = open(expect_filename, encoding='utf-8').read().strip() expect_date = datetime.datetime.strptime(expect_date_str, '%Y-%m-%d').date() self.assertEqual(expect_date, date) else: # Write out the expected file for review. with open(expect_filename, 'w', encoding='utf-8') as outfile: print(date.strftime('%Y-%m-%d'), file=outfile) self.skipTest(\"Expected file not present; generating '{}'\".format( expect_filename))","title":"test_expect_file_date()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression.ImportFileTestCase.test_expect_file_name","text":"Compute the imported file name and compare to an expected output. If an expected file (as .file_name) is not present, we issue a warning. Missing expected files can be written out by removing them before running the tests. Parameters: filename \u2013 A string, the name of the file to import using self.importer. Exceptions: AssertionError \u2013 If the contents differ from the expected file. Source code in beancount/ingest/regression.py @test_utils.skipIfRaises(ToolNotInstalled) def test_expect_file_name(self, filename, msg): \"\"\"Compute the imported file name and compare to an expected output. If an expected file (as .file_name) is not present, we issue a warning. Missing expected files can be written out by removing them before running the tests. Args: filename: A string, the name of the file to import using self.importer. Raises: AssertionError: If the contents differ from the expected file. \"\"\" # Import the date. file = cache.get_file(filename) generated_basename = self.importer.file_name(file) if generated_basename is None: self.fail(\"No filename produced from {}\".format(filename)) # Check that we're getting a non-null relative simple filename. self.assertFalse(path.isabs(generated_basename), generated_basename) self.assertNotRegex(generated_basename, os.sep) expect_filename = '{}.file_name'.format(file.name) if path.exists(expect_filename) and path.getsize(expect_filename) > 0: expect_filename = open(expect_filename, encoding='utf-8').read().strip() self.assertEqual(expect_filename, generated_basename) else: # Write out the expected file for review. with open(expect_filename, 'w', encoding='utf-8') as file: print(generated_basename, file=file) self.skipTest(\"Expected file not present; generating '{}'\".format( expect_filename))","title":"test_expect_file_name()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression.ImportFileTestCase.test_expect_identify","text":"Attempt to identify a file and expect results to be true. Parameters: filename \u2013 A string, the name of the file to import using self.importer. Exceptions: AssertionError \u2013 If the contents differ from the expected file. Source code in beancount/ingest/regression.py @test_utils.skipIfRaises(ToolNotInstalled) def test_expect_identify(self, filename, msg): \"\"\"Attempt to identify a file and expect results to be true. Args: filename: A string, the name of the file to import using self.importer. Raises: AssertionError: If the contents differ from the expected file. \"\"\" file = cache.get_file(filename) matched = self.importer.identify(file) self.assertTrue(matched)","title":"test_expect_identify()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression.ToolNotInstalled","text":"An error to be used by converters when necessary software isn't there. Raising this exception from your converter code when the tool is not installed will make the tests defined in this file skipped instead of failing. This will happen when you test your converters on different computers and/or platforms.","title":"ToolNotInstalled"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression.compare_sample_files","text":"Compare the sample files under a directory. Parameters: importer \u2013 An instance of an Importer. directory \u2013 A string, the directory to scour for sample files or a filename in that directory. If a directory is not provided, the directory of the file from which the importer class is defined is used. ignore_cls \u2013 An optional base class of the importer whose methods should not trigger the addition of a test. For example, if you are deriving from a base class which is already well-tested, you may not want to have a regression test case generated for those methods. This was used to ignore methods provided from a common backwards compatibility support class. Yields: Generated tests as per nose's requirements (a callable and arguments for it). Source code in beancount/ingest/regression.py @deprecated(\"Use beancount.ingest.regression_pytest instead\") def compare_sample_files(importer, directory=None, ignore_cls=None): \"\"\"Compare the sample files under a directory. Args: importer: An instance of an Importer. directory: A string, the directory to scour for sample files or a filename in that directory. If a directory is not provided, the directory of the file from which the importer class is defined is used. ignore_cls: An optional base class of the importer whose methods should not trigger the addition of a test. For example, if you are deriving from a base class which is already well-tested, you may not want to have a regression test case generated for those methods. This was used to ignore methods provided from a common backwards compatibility support class. Yields: Generated tests as per nose's requirements (a callable and arguments for it). \"\"\" # If the directory is not specified, use the directory where the importer # class was defined. if not directory: directory = sys.modules[type(importer).__module__].__file__ if path.isfile(directory): directory = path.dirname(directory) for filename in find_input_files(directory): # For each of the methods to be tested, check if there is an actual # implementation and if so, run a comparison with an expected file. for name in ['identify', 'extract', 'file_date', 'file_name']: # Check if the method has been overridden from the protocol # interface. If so, even if it's provided by concretely inherited # method, we want to require a test against that method. func = getattr(importer, name).__func__ if (func is not getattr(ImporterProtocol, name) and (ignore_cls is None or (func is not getattr(ignore_cls, name, None)))): method = getattr(ImportFileTestCase(importer), 'test_expect_{}'.format(name)) yield (method, filename, name)","title":"compare_sample_files()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression.find_input_files","text":"Find the input files in the module where the class is defined. Parameters: directory \u2013 A string, the path to a root directory to check for. Yields: Strings, the absolute filenames of sample input and expected files. Source code in beancount/ingest/regression.py def find_input_files(directory): \"\"\"Find the input files in the module where the class is defined. Args: directory: A string, the path to a root directory to check for. Yields: Strings, the absolute filenames of sample input and expected files. \"\"\" for sroot, dirs, files in os.walk(directory): for filename in files: if re.match(r'.*\\.(extract|file_date|file_name|py|pyc|DS_Store)$', filename): continue yield path.join(sroot, filename)","title":"find_input_files()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression_pytest","text":"Support for implementing regression tests on sample files using pytest. This module provides definitions for testing a custom importer against a set of existing downloaded files, running the various importer interface methods on it, and comparing the output to an expected text file. (Expected test files can be auto-generated using the --generate option). You use it like this: from beancount.ingest import regression_pytest ... import mymodule ... # Create your importer instance used for testing. importer = mymodule.Importer(...) # Select a directory where your test files are to be located. directory = ... # Create a test case using the base in this class. @regression_pytest.with_importer(importer) @regression_pytest.with_testdir(directory) class TestImporter(regtest.ImporterTestBase): pass Also, to add the --generate option to 'pytest', you must create a conftest.py somewhere in one of the roots above your importers with this module as a plugin: pytest_plugins = \"beancount.ingest.regression_pytest\" See beancount/example/ingest for a full working example. How to invoke the tests: Via pytest. First run your test with the --generate option to generate all the expected files. Then inspect them visually for correctness. Finally, check them in to preserve them. You should be able to regress against those correct outputs in the future. Use version control to your advantage to visualize the differences.","title":"regression_pytest"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression_pytest.ImporterTestBase","text":"","title":"ImporterTestBase"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression_pytest.ImporterTestBase.test_extract","text":"Extract entries from a test file and compare against expected output. Source code in beancount/ingest/regression_pytest.py def test_extract(self, importer, file, pytestconfig): \"\"\"Extract entries from a test file and compare against expected output.\"\"\" entries = extract.extract_from_file(file.name, importer, None, None) oss = io.StringIO() printer.print_entries(entries, file=oss) string = oss.getvalue() compare_contents_or_generate(string, '{}.extract'.format(file.name), pytestconfig.getoption(\"generate\", False))","title":"test_extract()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression_pytest.ImporterTestBase.test_file_account","text":"Compute the selected filing account and compare to an expected output. Source code in beancount/ingest/regression_pytest.py def test_file_account(self, importer, file, pytestconfig): \"\"\"Compute the selected filing account and compare to an expected output.\"\"\" account = importer.file_account(file) or '' compare_contents_or_generate(account, '{}.file_account'.format(file.name), pytestconfig.getoption(\"generate\", False))","title":"test_file_account()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression_pytest.ImporterTestBase.test_file_date","text":"Compute the imported file date and compare to an expected output. Source code in beancount/ingest/regression_pytest.py def test_file_date(self, importer, file, pytestconfig): \"\"\"Compute the imported file date and compare to an expected output.\"\"\" date = importer.file_date(file) string = date.isoformat() if date else '' compare_contents_or_generate(string, '{}.file_date'.format(file.name), pytestconfig.getoption(\"generate\", False))","title":"test_file_date()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression_pytest.ImporterTestBase.test_file_name","text":"Compute the imported file name and compare to an expected output. Source code in beancount/ingest/regression_pytest.py def test_file_name(self, importer, file, pytestconfig): \"\"\"Compute the imported file name and compare to an expected output.\"\"\" filename = importer.file_name(file) or '' compare_contents_or_generate(filename, '{}.file_name'.format(file.name), pytestconfig.getoption(\"generate\", False))","title":"test_file_name()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression_pytest.ImporterTestBase.test_identify","text":"Attempt to identify a file and expect results to be true. This method does not need to check against an existing expect file. It is just assumed it should return True if your test is setup well (the importer should always identify the test file). Source code in beancount/ingest/regression_pytest.py def test_identify(self, importer, file): \"\"\"Attempt to identify a file and expect results to be true. This method does not need to check against an existing expect file. It is just assumed it should return True if your test is setup well (the importer should always identify the test file). \"\"\" assert importer.identify(file)","title":"test_identify()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression_pytest.compare_contents_or_generate","text":"Compare a string to the contents of an expect file. Assert if different; auto-generate otherwise. Parameters: actual_string \u2013 The expected string contents. expect_fn \u2013 The filename whose contents to read and compare against. generate \u2013 A boolean, true if we are to generate the tests. Source code in beancount/ingest/regression_pytest.py def compare_contents_or_generate(actual_string, expect_fn, generate): \"\"\"Compare a string to the contents of an expect file. Assert if different; auto-generate otherwise. Args: actual_string: The expected string contents. expect_fn: The filename whose contents to read and compare against. generate: A boolean, true if we are to generate the tests. \"\"\" if generate: with open(expect_fn, 'w', encoding='utf-8') as expect_file: expect_file.write(actual_string) if actual_string and not actual_string.endswith('\\n'): expect_file.write('\\n') pytest.skip(\"Generated '{}'\".format(expect_fn)) else: # Run the test on an existing expected file. assert path.exists(expect_fn), ( \"Expected file '{}' is missing. Generate it?\".format(expect_fn)) with open(expect_fn, encoding='utf-8') as infile: expect_string = infile.read() assert expect_string.strip() == actual_string.strip()","title":"compare_contents_or_generate()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression_pytest.find_input_files","text":"Find the input files in the module where the class is defined. Parameters: directory \u2013 A string, the path to a root directory to check for. Yields: Strings, the absolute filenames of sample input and expected files. Source code in beancount/ingest/regression_pytest.py def find_input_files(directory): \"\"\"Find the input files in the module where the class is defined. Args: directory: A string, the path to a root directory to check for. Yields: Strings, the absolute filenames of sample input and expected files. \"\"\" for sroot, dirs, files in os.walk(directory): for filename in files: if re.match(r'.*\\.(extract|file_date|file_name|file_account|py|pyc|DS_Store)$', filename): continue yield path.join(sroot, filename)","title":"find_input_files()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression_pytest.pytest_addoption","text":"Add an option to generate the expected files for the tests. Source code in beancount/ingest/regression_pytest.py def pytest_addoption(parser): \"\"\"Add an option to generate the expected files for the tests.\"\"\" group = parser.getgroup(\"beancount\") group.addoption(\"--generate\", \"--gen\", action=\"store_true\", help=\"Don't test; rather, generate the expected files\")","title":"pytest_addoption()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression_pytest.with_importer","text":"Parametrizing fixture that provides the importer to test. Source code in beancount/ingest/regression_pytest.py def with_importer(importer): \"\"\"Parametrizing fixture that provides the importer to test.\"\"\" return pytest.mark.parametrize(\"importer\", [importer])","title":"with_importer()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.regression_pytest.with_testdir","text":"Parametrizing fixture that provides files from a directory. Source code in beancount/ingest/regression_pytest.py def with_testdir(directory): \"\"\"Parametrizing fixture that provides files from a directory.\"\"\" return pytest.mark.parametrize( \"file\", [cache.get_file(fn) for fn in find_input_files(directory)])","title":"with_testdir()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.scripts_utils","text":"Common front-end to all ingestion tools.","title":"scripts_utils"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.scripts_utils.TestScriptsBase","text":"","title":"TestScriptsBase"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.scripts_utils.TestScriptsBase.setUp","text":"Hook method for setting up the test fixture before exercising it. Source code in beancount/ingest/scripts_utils.py def setUp(self): super().setUp() for filename, contents in self.FILES.items(): absname = path.join(self.tempdir, filename) os.makedirs(path.dirname(absname), exist_ok=True) with open(absname, 'w') as file: file.write(contents) if filename.endswith('.py') or filename.endswith('.sh'): os.chmod(absname, stat.S_IRUSR|stat.S_IXUSR)","title":"setUp()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.scripts_utils.create_legacy_arguments_parser","text":"Create an arguments parser for all the ingestion bean-tools. Parameters: description ( str ) \u2013 The program description string. func \u2013 A callable function to run the particular command. Returns: An argparse.Namespace instance with the rest of arguments in 'rest'. Source code in beancount/ingest/scripts_utils.py def create_legacy_arguments_parser(description: str, run_func: callable): \"\"\"Create an arguments parser for all the ingestion bean-tools. Args: description: The program description string. func: A callable function to run the particular command. Returns: An argparse.Namespace instance with the rest of arguments in 'rest'. \"\"\" parser = version.ArgumentParser(description=description) parser.add_argument('config', action='store', metavar='CONFIG_FILENAME', help=('Importer configuration file. ' 'This is a Python file with a data structure that ' 'is specific to your accounts')) parser.add_argument('downloads', nargs='+', metavar='DIR-OR-FILE', default=[], help='Filenames or directories to search for files to import') parser.set_defaults(command=run_func) return parser","title":"create_legacy_arguments_parser()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.scripts_utils.ingest","text":"Driver function that calls all the ingestion tools. Put a call to this function at the end of your importer configuration to make your import script; this should be its main function, like this: from beancount.ingest.scripts_utils import ingest my_importers = [ ... ] ingest(my_importers) This more explicit way of invoking the ingestion is now the preferred way to invoke the various tools, and replaces calling the bean-identify, bean-extract, bean-file tools with a --config argument. When you call the import script itself (as as program) it will parse the arguments, expecting a subcommand ('identify', 'extract' or 'file') and corresponding subcommand-specific arguments. Here you can override some importer values, such as installing a custom duplicate finding hook, and eventually more. Note that this newer invocation method is optional and if it is not present, a call to ingest() is generated implicitly, and it functions as it used to. Future configurable customization of the ingestion process will be implemented by inserting new arguments to this function, this is the motivation behind doing this. Note that invocation by the three bean-* ingestion tools is still supported, and calling ingest() explicitly from your import configuration file will not break these tools either, if you invoke them on it; the values you provide to this function will be used by those tools. Parameters: importers_list \u2013 A list of importer instances. This is used as a chain-of-responsibility, called on each file. detect_duplicates_func \u2013 (DEPRECATED) An optional function which accepts a list of lists of imported entries and a list of entries already existing in the user's ledger. See function find_duplicate_entries(), which is the default implementation for this. Use 'filter_funcs' instead. hooks \u2013 An optional list of hook functions to apply to the list of extract (filename, entries) pairs, in order. This replaces 'detect_duplicates_func'. Source code in beancount/ingest/scripts_utils.py def ingest(importers_list, detect_duplicates_func=None, hooks=None): \"\"\"Driver function that calls all the ingestion tools. Put a call to this function at the end of your importer configuration to make your import script; this should be its main function, like this: from beancount.ingest.scripts_utils import ingest my_importers = [ ... ] ingest(my_importers) This more explicit way of invoking the ingestion is now the preferred way to invoke the various tools, and replaces calling the bean-identify, bean-extract, bean-file tools with a --config argument. When you call the import script itself (as as program) it will parse the arguments, expecting a subcommand ('identify', 'extract' or 'file') and corresponding subcommand-specific arguments. Here you can override some importer values, such as installing a custom duplicate finding hook, and eventually more. Note that this newer invocation method is optional and if it is not present, a call to ingest() is generated implicitly, and it functions as it used to. Future configurable customization of the ingestion process will be implemented by inserting new arguments to this function, this is the motivation behind doing this. Note that invocation by the three bean-* ingestion tools is still supported, and calling ingest() explicitly from your import configuration file will not break these tools either, if you invoke them on it; the values you provide to this function will be used by those tools. Args: importers_list: A list of importer instances. This is used as a chain-of-responsibility, called on each file. detect_duplicates_func: (DEPRECATED) An optional function which accepts a list of lists of imported entries and a list of entries already existing in the user's ledger. See function find_duplicate_entries(), which is the default implementation for this. Use 'filter_funcs' instead. hooks: An optional list of hook functions to apply to the list of extract (filename, entries) pairs, in order. This replaces 'detect_duplicates_func'. \"\"\" if detect_duplicates_func is not None: warnings.warn(\"Argument 'detect_duplicates_func' is deprecated.\") # Fold it in hooks. if hooks is None: hooks = [] hooks.insert(0, detect_duplicates_func) del detect_duplicates_func if ingest.args is not None: # The script has been called from one of the bean-* ingestion tools. # 'ingest.args' is only set when we're being invoked from one of the # bean-xxx tools (see below). # Mark this function as called, so that if it is called from an import # triggered by one of the ingestion tools, it won't be called again # afterwards. ingest.was_called = True # Use those args rather than to try to parse the command-line arguments # from a naked ingest() call as a script. {39c7af4f6af5} args, parser = ingest.args else: # The script is called directly. This is the main program of the import # script itself. This is the new invocation method. parser = version.ArgumentParser(description=DESCRIPTION) # Use required on subparsers. # FIXME: Remove this when we require version 3.7 or above. kwargs = {} if sys.version_info >= (3, 7): kwargs['required'] = True subparsers = parser.add_subparsers(dest='command', **kwargs) parser.add_argument('--downloads', '-d', metavar='DIR-OR-FILE', action='append', default=[], help='Filenames or directories to search for files to import') for cmdname, module in [('identify', identify), ('extract', extract), ('file', file)]: parser_cmd = subparsers.add_parser(cmdname, help=module.DESCRIPTION) parser_cmd.set_defaults(command=module.run) module.add_arguments(parser_cmd) args = parser.parse_args() if not args.downloads: args.downloads.append(os.getcwd()) # Implement required ourselves. # FIXME: Remove this when we require version 3.7 or above. if not (sys.version_info >= (3, 7)): if not hasattr(args, 'command'): parser.error(\"Subcommand is required.\") abs_downloads = list(map(path.abspath, args.downloads)) args.command(args, parser, importers_list, abs_downloads, hooks=hooks) return 0","title":"ingest()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.scripts_utils.run_import_script_and_ingest","text":"Run the import script and optionally call ingest(). This path is only called when trampolined by one of the bean-* ingestion tools. Parameters: parser \u2013 The parser instance, used only to report errors. importers_attr_name \u2013 The name of the special attribute in the module which defines the importers list. Returns: An execution return code. Source code in beancount/ingest/scripts_utils.py def run_import_script_and_ingest(parser, argv=None, importers_attr_name='CONFIG'): \"\"\"Run the import script and optionally call ingest(). This path is only called when trampolined by one of the bean-* ingestion tools. Args: parser: The parser instance, used only to report errors. importers_attr_name: The name of the special attribute in the module which defines the importers list. Returns: An execution return code. \"\"\" args = parser.parse_args(args=argv) # Check the existence of the config. if not path.exists(args.config) or path.isdir(args.config): parser.error(\"File does not exist: '{}'\".format(args.config)) # Check the existence of all specified files. for filename in args.downloads: if not path.exists(filename): parser.error(\"File does not exist: '{}'\".format(filename)) # Reset the state of ingest() being called (for unit tests, which use the # same runtime with run_with_args). ingest.was_called = False # Save the arguments parsed from the command-line as default for # {39c7af4f6af5}. ingest.args = args, parser # Evaluate the importer script/module. mod = runpy.run_path(args.config) # If the importer script has already called ingest() within itself, don't # call it again. We're done. This allows the use to insert an explicit call # to ingest() while still running the bean-* ingestion tools on the file. if ingest.was_called: return 0 # ingest() hasn't been called by the script so we assume it isn't # present in it. So we now run the ingestion by ourselves here, without # specifying any of the newer optional arguments. importers_list = mod[importers_attr_name] return ingest(importers_list)","title":"run_import_script_and_ingest()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.scripts_utils.trampoline_to_ingest","text":"Parse arguments for bean tool, import config script and ingest. This function is called by the three bean-* tools to support the older import files, which only required a CONFIG object to be defined in them. Parameters: module \u2013 One of the identify, extract or file module objects. Returns: An execution return code. Source code in beancount/ingest/scripts_utils.py def trampoline_to_ingest(module): \"\"\"Parse arguments for bean tool, import config script and ingest. This function is called by the three bean-* tools to support the older import files, which only required a CONFIG object to be defined in them. Args: module: One of the identify, extract or file module objects. Returns: An execution return code. \"\"\" # Disable debugging logging which is turned on by default in chardet. logging.getLogger('chardet.charsetprober').setLevel(logging.INFO) logging.getLogger('chardet.universaldetector').setLevel(logging.INFO) parser = create_legacy_arguments_parser(module.DESCRIPTION, module.run) module.add_arguments(parser) return run_import_script_and_ingest(parser)","title":"trampoline_to_ingest()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.similar","text":"Identify similar entries. This can be used during import in order to identify and flag duplicate entries.","title":"similar"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.similar.SimilarityComparator","text":"Similarity comparator of transactions. This comparator needs to be able to handle Transaction instances which are incomplete on one side, which have slightly different dates, or potentially slightly different numbers.","title":"SimilarityComparator"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.similar.SimilarityComparator.__call__","text":"Compare two entries, return true if they are deemed similar. Parameters: entry1 \u2013 A first Transaction directive. entry2 \u2013 A second Transaction directive. Returns: A boolean. Source code in beancount/ingest/similar.py def __call__(self, entry1, entry2): \"\"\"Compare two entries, return true if they are deemed similar. Args: entry1: A first Transaction directive. entry2: A second Transaction directive. Returns: A boolean. \"\"\" # Check the date difference. if self.max_date_delta is not None: delta = ((entry1.date - entry2.date) if entry1.date > entry2.date else (entry2.date - entry1.date)) if delta > self.max_date_delta: return False try: amounts1 = self.cache[id(entry1)] except KeyError: amounts1 = self.cache[id(entry1)] = amounts_map(entry1) try: amounts2 = self.cache[id(entry2)] except KeyError: amounts2 = self.cache[id(entry2)] = amounts_map(entry2) # Look for amounts on common accounts. common_keys = set(amounts1) & set(amounts2) for key in sorted(common_keys): # Compare the amounts. number1 = amounts1[key] number2 = amounts2[key] if number1 == ZERO and number2 == ZERO: break diff = abs((number1 / number2) if number2 != ZERO else (number2 / number1)) if diff == ZERO: return False if diff < ONE: diff = ONE/diff if (diff - ONE) < self.EPSILON: break else: return False # Here, we have found at least one common account with a close # amount. Now, we require that the set of accounts are equal or that # one be a subset of the other. accounts1 = set(posting.account for posting in entry1.postings) accounts2 = set(posting.account for posting in entry2.postings) return accounts1.issubset(accounts2) or accounts2.issubset(accounts1)","title":"__call__()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.similar.SimilarityComparator.__init__","text":"Constructor a comparator of entries. Parameters: max_date_delta \u2013 A datetime.timedelta instance of the max tolerated distance between dates. Source code in beancount/ingest/similar.py def __init__(self, max_date_delta=None): \"\"\"Constructor a comparator of entries. Args: max_date_delta: A datetime.timedelta instance of the max tolerated distance between dates. \"\"\" self.cache = {} self.max_date_delta = max_date_delta","title":"__init__()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.similar.amounts_map","text":"Compute a mapping of (account, currency) -> Decimal balances. Parameters: entry \u2013 A Transaction instance. Returns: A dict of account -> Amount balance. Source code in beancount/ingest/similar.py def amounts_map(entry): \"\"\"Compute a mapping of (account, currency) -> Decimal balances. Args: entry: A Transaction instance. Returns: A dict of account -> Amount balance. \"\"\" amounts = collections.defaultdict(D) for posting in entry.postings: # Skip interpolated postings. if posting.meta and interpolate.AUTOMATIC_META in posting.meta: continue currency = isinstance(posting.units, amount.Amount) and posting.units.currency if isinstance(currency, str): key = (posting.account, currency) amounts[key] += posting.units.number return amounts","title":"amounts_map()"},{"location":"api_reference/beancount.ingest.html#beancount.ingest.similar.find_similar_entries","text":"Find which entries from a list are potential duplicates of a set. Note: If there are multiple entries from 'source_entries' matching an entry in 'entries', only the first match is returned. Note that this function could in theory decide to merge some of the imported entries with each other. Parameters: entries \u2013 The list of entries to classify as duplicate or note. source_entries \u2013 The list of entries against which to match. This is the previous, or existing set of entries to compare against. This may be null or empty. comparator \u2013 A functor used to establish the similarity of two entries. window_days \u2013 The number of days (inclusive) before or after to scan the entries to classify against. Returns: A list of pairs of entries (entry, source_entry) where entry is from 'entries' and is deemed to be a duplicate of source_entry, from 'source_entries'. Source code in beancount/ingest/similar.py def find_similar_entries(entries, source_entries, comparator=None, window_days=2): \"\"\"Find which entries from a list are potential duplicates of a set. Note: If there are multiple entries from 'source_entries' matching an entry in 'entries', only the first match is returned. Note that this function could in theory decide to merge some of the imported entries with each other. Args: entries: The list of entries to classify as duplicate or note. source_entries: The list of entries against which to match. This is the previous, or existing set of entries to compare against. This may be null or empty. comparator: A functor used to establish the similarity of two entries. window_days: The number of days (inclusive) before or after to scan the entries to classify against. Returns: A list of pairs of entries (entry, source_entry) where entry is from 'entries' and is deemed to be a duplicate of source_entry, from 'source_entries'. \"\"\" window_head = datetime.timedelta(days=window_days) window_tail = datetime.timedelta(days=window_days + 1) if comparator is None: comparator = SimilarityComparator() # For each of the new entries, look at existing entries at a nearby date. duplicates = [] if source_entries is not None: for entry in data.filter_txns(entries): for source_entry in data.filter_txns( data.iter_entry_dates(source_entries, entry.date - window_head, entry.date + window_tail)): if comparator(entry, source_entry): duplicates.append((entry, source_entry)) break return duplicates","title":"find_similar_entries()"},{"location":"api_reference/beancount.loader.html","text":"beancount.loader \uf0c1 Loader code. This is the main entry point to load up a file. beancount.loader.LoadError ( tuple ) \uf0c1 LoadError(source, message, entry) beancount.loader.LoadError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/loader.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.loader.LoadError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of LoadError(source, message, entry) beancount.loader.LoadError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/loader.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.loader.aggregate_options_map(options_map, src_options_map) \uf0c1 Aggregate some of the attributes of options map. Parameters: options_map \u2013 The target map in which we want to aggregate attributes. Note: This value is mutated in-place. src_options_map \u2013 A source map whose values we'd like to see aggregated. Source code in beancount/loader.py def aggregate_options_map(options_map, src_options_map): \"\"\"Aggregate some of the attributes of options map. Args: options_map: The target map in which we want to aggregate attributes. Note: This value is mutated in-place. src_options_map: A source map whose values we'd like to see aggregated. \"\"\" op_currencies = options_map[\"operating_currency\"] for currency in src_options_map[\"operating_currency\"]: if currency not in op_currencies: op_currencies.append(currency) commodities = options_map[\"commodities\"] for currency in src_options_map[\"commodities\"]: commodities.add(currency) beancount.loader.combine_plugins(*plugin_modules) \uf0c1 Combine the plugins from the given plugin modules. This is used to create plugins of plugins. Parameters: *plugins_modules \u2013 A sequence of module objects. Returns: A list that can be assigned to the new module's plugins attribute. Source code in beancount/loader.py def combine_plugins(*plugin_modules): \"\"\"Combine the plugins from the given plugin modules. This is used to create plugins of plugins. Args: *plugins_modules: A sequence of module objects. Returns: A list that can be assigned to the new module's __plugins__ attribute. \"\"\" modules = [] for module in plugin_modules: modules.extend([getattr(module, name) for name in module.__plugins__]) return modules beancount.loader.compute_input_hash(filenames) \uf0c1 Compute a hash of the input data. Parameters: filenames \u2013 A list of input files. Order is not relevant. Source code in beancount/loader.py def compute_input_hash(filenames): \"\"\"Compute a hash of the input data. Args: filenames: A list of input files. Order is not relevant. \"\"\" md5 = hashlib.md5() for filename in sorted(filenames): md5.update(filename.encode('utf8')) if not path.exists(filename): continue stat = os.stat(filename) md5.update(struct.pack('dd', stat.st_mtime_ns, stat.st_size)) return md5.hexdigest() beancount.loader.delete_cache_function(cache_getter, function) \uf0c1 A wrapper that removes the cached filename. Parameters: cache_getter \u2013 A function of one argument, the top-level filename, which will return the name of the corresponding cache file. function \u2013 A function object to decorate for caching. Returns: A decorated function which will delete the cached filename, if it exists. Source code in beancount/loader.py def delete_cache_function(cache_getter, function): \"\"\"A wrapper that removes the cached filename. Args: cache_getter: A function of one argument, the top-level filename, which will return the name of the corresponding cache file. function: A function object to decorate for caching. Returns: A decorated function which will delete the cached filename, if it exists. \"\"\" @functools.wraps(function) def wrapped(toplevel_filename, *args, **kw): # Delete the cache. cache_filename = cache_getter(toplevel_filename) if path.exists(cache_filename): os.remove(cache_filename) # Invoke the original function. return function(toplevel_filename, *args, **kw) return wrapped beancount.loader.get_cache_filename(pattern, filename) \uf0c1 Compute the cache filename from a given pattern and the top-level filename. Parameters: pattern ( str ) \u2013 A cache filename or pattern. If the pattern contains '{filename}' this will get replaced by the top-level filename. This may be absolute or relative. filename ( str ) \u2013 The top-level filename. Returns: str \u2013 The resolved cache filename. Source code in beancount/loader.py def get_cache_filename(pattern: str, filename: str) -> str: \"\"\"Compute the cache filename from a given pattern and the top-level filename. Args: pattern: A cache filename or pattern. If the pattern contains '{filename}' this will get replaced by the top-level filename. This may be absolute or relative. filename: The top-level filename. Returns: The resolved cache filename. \"\"\" abs_filename = path.abspath(filename) if path.isabs(pattern): abs_pattern = pattern else: abs_pattern = path.join(path.dirname(abs_filename), pattern) return abs_pattern.format(filename=path.basename(filename)) beancount.loader.initialize(use_cache, cache_filename=None) \uf0c1 Initialize the loader. Source code in beancount/loader.py def initialize(use_cache: bool, cache_filename: Optional[str] = None): \"\"\"Initialize the loader.\"\"\" # Unless an environment variable disables it, use the pickle load cache # automatically. Note that this works across all Python programs running the # loader which is why it's located here. # pylint: disable=invalid-name global _load_file # Make a function to compute the cache filename. cache_pattern = (cache_filename or os.getenv('BEANCOUNT_LOAD_CACHE_FILENAME') or PICKLE_CACHE_FILENAME) cache_getter = functools.partial(get_cache_filename, cache_pattern) if use_cache: _load_file = pickle_cache_function(cache_getter, PICKLE_CACHE_THRESHOLD, _uncached_load_file) else: if cache_filename is not None: logging.warning(\"Cache disabled; \" \"Explicitly overridden cache filename %s will be ignored.\", cache_filename) _load_file = delete_cache_function(cache_getter, _uncached_load_file) beancount.loader.load_doc(expect_errors=False) \uf0c1 A factory of decorators that loads the docstring and calls the function with entries. This is an incredibly convenient tool to write lots of tests. Write a unittest using the standard TestCase class and put the input entries in the function's docstring. Parameters: expect_errors \u2013 A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. Returns: A wrapped method that accepts a single 'self' argument. Source code in beancount/loader.py def load_doc(expect_errors=False): \"\"\"A factory of decorators that loads the docstring and calls the function with entries. This is an incredibly convenient tool to write lots of tests. Write a unittest using the standard TestCase class and put the input entries in the function's docstring. Args: expect_errors: A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. Returns: A wrapped method that accepts a single 'self' argument. \"\"\" def decorator(fun): \"\"\"A decorator that parses the function's docstring as an argument. Args: fun: A callable method, that accepts the three return arguments that load() returns. Returns: A decorated test function. \"\"\" @functools.wraps(fun) def wrapper(self): entries, errors, options_map = load_string(fun.__doc__, dedent=True) if expect_errors is not None: if expect_errors is False and errors: oss = io.StringIO() printer.print_errors(errors, file=oss) self.fail(\"Unexpected errors found:\\n{}\".format(oss.getvalue())) elif expect_errors is True and not errors: self.fail(\"Expected errors, none found:\") # Note: Even if we expected no errors, we call this function with an # empty 'errors' list. This is so that the interface does not change # based on the arguments to the decorator, which would be somewhat # ugly and which would require explanation. return fun(self, entries, errors, options_map) wrapper.__input__ = wrapper.__doc__ wrapper.__doc__ = None return wrapper return decorator beancount.loader.load_encrypted_file(filename, log_timings=None, log_errors=None, extra_validations=None, dedent=False, encoding=None) \uf0c1 Load an encrypted Beancount input file. Parameters: filename \u2013 The name of an encrypted file to be parsed. log_timings \u2013 See load_string(). log_errors \u2013 See load_string(). extra_validations \u2013 See load_string(). dedent \u2013 See load_string(). encoding \u2013 See load_string(). Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. Source code in beancount/loader.py def load_encrypted_file(filename, log_timings=None, log_errors=None, extra_validations=None, dedent=False, encoding=None): \"\"\"Load an encrypted Beancount input file. Args: filename: The name of an encrypted file to be parsed. log_timings: See load_string(). log_errors: See load_string(). extra_validations: See load_string(). dedent: See load_string(). encoding: See load_string(). Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. \"\"\" contents = encryption.read_encrypted_file(filename) return load_string(contents, log_timings=log_timings, log_errors=log_errors, extra_validations=extra_validations, encoding=encoding) beancount.loader.load_file(filename, log_timings=None, log_errors=None, extra_validations=None, encoding=None) \uf0c1 Open a Beancount input file, parse it, run transformations and validate. Parameters: filename \u2013 The name of the file to be parsed. log_timings \u2013 A file object or function to write timings to, or None, if it should remain quiet. log_errors \u2013 A file object or function to write errors to, or None, if it should remain quiet. extra_validations \u2013 A list of extra validation functions to run after loading this list of entries. encoding \u2013 A string or None, the encoding to decode the input filename with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. Source code in beancount/loader.py def load_file(filename, log_timings=None, log_errors=None, extra_validations=None, encoding=None): \"\"\"Open a Beancount input file, parse it, run transformations and validate. Args: filename: The name of the file to be parsed. log_timings: A file object or function to write timings to, or None, if it should remain quiet. log_errors: A file object or function to write errors to, or None, if it should remain quiet. extra_validations: A list of extra validation functions to run after loading this list of entries. encoding: A string or None, the encoding to decode the input filename with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. \"\"\" filename = path.expandvars(path.expanduser(filename)) if not path.isabs(filename): filename = path.normpath(path.join(os.getcwd(), filename)) if encryption.is_encrypted_file(filename): # Note: Caching is not supported for encrypted files. entries, errors, options_map = load_encrypted_file( filename, log_timings, log_errors, extra_validations, False, encoding) else: entries, errors, options_map = _load_file( filename, log_timings, extra_validations, encoding) _log_errors(errors, log_errors) return entries, errors, options_map beancount.loader.load_string(string, log_timings=None, log_errors=None, extra_validations=None, dedent=False, encoding=None) \uf0c1 Open a Beancount input string, parse it, run transformations and validate. Parameters: string \u2013 A Beancount input string. log_timings \u2013 A file object or function to write timings to, or None, if it should remain quiet. log_errors \u2013 A file object or function to write errors to, or None, if it should remain quiet. extra_validations \u2013 A list of extra validation functions to run after loading this list of entries. dedent \u2013 A boolean, if set, remove the whitespace in front of the lines. encoding \u2013 A string or None, the encoding to decode the input string with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the string, \"errors\" a list of error objects generated while parsing and validating the string, and \"options_map\", a dict of the options parsed from the string. Source code in beancount/loader.py def load_string(string, log_timings=None, log_errors=None, extra_validations=None, dedent=False, encoding=None): \"\"\"Open a Beancount input string, parse it, run transformations and validate. Args: string: A Beancount input string. log_timings: A file object or function to write timings to, or None, if it should remain quiet. log_errors: A file object or function to write errors to, or None, if it should remain quiet. extra_validations: A list of extra validation functions to run after loading this list of entries. dedent: A boolean, if set, remove the whitespace in front of the lines. encoding: A string or None, the encoding to decode the input string with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the string, \"errors\" a list of error objects generated while parsing and validating the string, and \"options_map\", a dict of the options parsed from the string. \"\"\" if dedent: string = textwrap.dedent(string) entries, errors, options_map = _load([(string, False)], log_timings, extra_validations, encoding) _log_errors(errors, log_errors) return entries, errors, options_map beancount.loader.needs_refresh(options_map) \uf0c1 Predicate that returns true if at least one of the input files may have changed. Parameters: options_map \u2013 An options dict as per the parser. mtime \u2013 A modified time, to check if it covers the include files in the options_map. Returns: A boolean, true if the input is obsoleted by changes in the input files. Source code in beancount/loader.py def needs_refresh(options_map): \"\"\"Predicate that returns true if at least one of the input files may have changed. Args: options_map: An options dict as per the parser. mtime: A modified time, to check if it covers the include files in the options_map. Returns: A boolean, true if the input is obsoleted by changes in the input files. \"\"\" if options_map is None: return True input_hash = compute_input_hash(options_map['include']) return 'input_hash' not in options_map or input_hash != options_map['input_hash'] beancount.loader.pickle_cache_function(cache_getter, time_threshold, function) \uf0c1 Decorate a loader function to make it loads its result from a pickle cache. This considers the first argument as a top-level filename and assumes the function to be cached returns an (entries, errors, options_map) triple. We use the 'include' option value in order to check whether any of the included files has changed. It's essentially a special case for an on-disk memoizer. If any of the included files are more recent than the cache, the function is recomputed and the cache refreshed. Parameters: cache_getter \u2013 A function of one argument, the top-level filename, which will return the name of the corresponding cache file. time_threshold \u2013 A float, the number of seconds below which we don't bother caching. function \u2013 A function object to decorate for caching. Returns: A decorated function which will pull its result from a cache file if it is available. Source code in beancount/loader.py def pickle_cache_function(cache_getter, time_threshold, function): \"\"\"Decorate a loader function to make it loads its result from a pickle cache. This considers the first argument as a top-level filename and assumes the function to be cached returns an (entries, errors, options_map) triple. We use the 'include' option value in order to check whether any of the included files has changed. It's essentially a special case for an on-disk memoizer. If any of the included files are more recent than the cache, the function is recomputed and the cache refreshed. Args: cache_getter: A function of one argument, the top-level filename, which will return the name of the corresponding cache file. time_threshold: A float, the number of seconds below which we don't bother caching. function: A function object to decorate for caching. Returns: A decorated function which will pull its result from a cache file if it is available. \"\"\" @functools.wraps(function) def wrapped(toplevel_filename, *args, **kw): cache_filename = cache_getter(toplevel_filename) # Read the cache if it exists in order to get the list of files whose # timestamps to check. exists = path.exists(cache_filename) if exists: with open(cache_filename, 'rb') as file: try: result = pickle.load(file) except Exception as exc: # Note: Not a big fan of doing this, but here we handle all # possible exceptions because unpickling of an old or # corrupted pickle file manifests as a variety of different # exception types. # The cache file is corrupted; ignore it and recompute. logging.error(\"Cache file is corrupted: %s; recomputing.\", exc) result = None else: # Check that the latest timestamp has not been written after the # cache file. entries, errors, options_map = result if not needs_refresh(options_map): # All timestamps are legit; cache hit. return result # We failed; recompute the value. if exists: try: os.remove(cache_filename) except OSError as exc: # Warn for errors on read-only filesystems. logging.warning(\"Could not remove picklecache file %s: %s\", cache_filename, exc) time_before = time.time() result = function(toplevel_filename, *args, **kw) time_after = time.time() # Overwrite the cache file if the time it takes to compute it # justifies it. if time_after - time_before > time_threshold: try: with open(cache_filename, 'wb') as file: pickle.dump(result, file) except Exception as exc: logging.warning(\"Could not write to picklecache file %s: %s\", cache_filename, exc) return result return wrapped beancount.loader.run_transformations(entries, parse_errors, options_map, log_timings) \uf0c1 Run the various transformations on the entries. This is where entries are being synthesized, checked, plugins are run, etc. Parameters: entries \u2013 A list of directives as read from the parser. parse_errors \u2013 A list of errors so far. options_map \u2013 An options dict as read from the parser. log_timings \u2013 A function to write timing log entries to, or None, if it should be quiet. Returns: A list of modified entries, and a list of errors, also possibly modified. Source code in beancount/loader.py def run_transformations(entries, parse_errors, options_map, log_timings): \"\"\"Run the various transformations on the entries. This is where entries are being synthesized, checked, plugins are run, etc. Args: entries: A list of directives as read from the parser. parse_errors: A list of errors so far. options_map: An options dict as read from the parser. log_timings: A function to write timing log entries to, or None, if it should be quiet. Returns: A list of modified entries, and a list of errors, also possibly modified. \"\"\" # A list of errors to extend (make a copy to avoid modifying the input). errors = list(parse_errors) # Process the plugins. if options_map['plugin_processing_mode'] == 'raw': plugins_iter = options_map[\"plugin\"] elif options_map['plugin_processing_mode'] == 'default': plugins_iter = itertools.chain(DEFAULT_PLUGINS_PRE, options_map[\"plugin\"], DEFAULT_PLUGINS_POST) else: assert \"Invalid value for plugin_processing_mode: {}\".format( options_map['plugin_processing_mode']) for plugin_name, plugin_config in plugins_iter: # Issue a warning on a renamed module. renamed_name = RENAMED_MODULES.get(plugin_name, None) if renamed_name: warnings.warn(\"Deprecation notice: Module '{}' has been renamed to '{}'; \" \"please adjust your plugin directive.\".format( plugin_name, renamed_name)) plugin_name = renamed_name # Try to import the module. try: module = importlib.import_module(plugin_name) if not hasattr(module, '__plugins__'): continue with misc_utils.log_time(plugin_name, log_timings, indent=2): # Run each transformer function in the plugin. for function_name in module.__plugins__: if isinstance(function_name, str): # Support plugin functions provided by name. callback = getattr(module, function_name) else: # Support function types directly, not just names. callback = function_name if plugin_config is not None: entries, plugin_errors = callback(entries, options_map, plugin_config) else: entries, plugin_errors = callback(entries, options_map) errors.extend(plugin_errors) # Ensure that the entries are sorted. Don't trust the plugins # themselves. entries.sort(key=data.entry_sortkey) except (ImportError, TypeError) as exc: # Upon failure, just issue an error. errors.append(LoadError(data.new_metadata(\"\", 0), 'Error importing \"{}\": {}'.format( plugin_name, str(exc)), None)) return entries, errors","title":"beancount.loader"},{"location":"api_reference/beancount.loader.html#beancountloader","text":"Loader code. This is the main entry point to load up a file.","title":"beancount.loader"},{"location":"api_reference/beancount.loader.html#beancount.loader.LoadError","text":"LoadError(source, message, entry)","title":"LoadError"},{"location":"api_reference/beancount.loader.html#beancount.loader.LoadError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/loader.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.loader.html#beancount.loader.LoadError.__new__","text":"Create new instance of LoadError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.loader.html#beancount.loader.LoadError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/loader.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.loader.html#beancount.loader.aggregate_options_map","text":"Aggregate some of the attributes of options map. Parameters: options_map \u2013 The target map in which we want to aggregate attributes. Note: This value is mutated in-place. src_options_map \u2013 A source map whose values we'd like to see aggregated. Source code in beancount/loader.py def aggregate_options_map(options_map, src_options_map): \"\"\"Aggregate some of the attributes of options map. Args: options_map: The target map in which we want to aggregate attributes. Note: This value is mutated in-place. src_options_map: A source map whose values we'd like to see aggregated. \"\"\" op_currencies = options_map[\"operating_currency\"] for currency in src_options_map[\"operating_currency\"]: if currency not in op_currencies: op_currencies.append(currency) commodities = options_map[\"commodities\"] for currency in src_options_map[\"commodities\"]: commodities.add(currency)","title":"aggregate_options_map()"},{"location":"api_reference/beancount.loader.html#beancount.loader.combine_plugins","text":"Combine the plugins from the given plugin modules. This is used to create plugins of plugins. Parameters: *plugins_modules \u2013 A sequence of module objects. Returns: A list that can be assigned to the new module's plugins attribute. Source code in beancount/loader.py def combine_plugins(*plugin_modules): \"\"\"Combine the plugins from the given plugin modules. This is used to create plugins of plugins. Args: *plugins_modules: A sequence of module objects. Returns: A list that can be assigned to the new module's __plugins__ attribute. \"\"\" modules = [] for module in plugin_modules: modules.extend([getattr(module, name) for name in module.__plugins__]) return modules","title":"combine_plugins()"},{"location":"api_reference/beancount.loader.html#beancount.loader.compute_input_hash","text":"Compute a hash of the input data. Parameters: filenames \u2013 A list of input files. Order is not relevant. Source code in beancount/loader.py def compute_input_hash(filenames): \"\"\"Compute a hash of the input data. Args: filenames: A list of input files. Order is not relevant. \"\"\" md5 = hashlib.md5() for filename in sorted(filenames): md5.update(filename.encode('utf8')) if not path.exists(filename): continue stat = os.stat(filename) md5.update(struct.pack('dd', stat.st_mtime_ns, stat.st_size)) return md5.hexdigest()","title":"compute_input_hash()"},{"location":"api_reference/beancount.loader.html#beancount.loader.delete_cache_function","text":"A wrapper that removes the cached filename. Parameters: cache_getter \u2013 A function of one argument, the top-level filename, which will return the name of the corresponding cache file. function \u2013 A function object to decorate for caching. Returns: A decorated function which will delete the cached filename, if it exists. Source code in beancount/loader.py def delete_cache_function(cache_getter, function): \"\"\"A wrapper that removes the cached filename. Args: cache_getter: A function of one argument, the top-level filename, which will return the name of the corresponding cache file. function: A function object to decorate for caching. Returns: A decorated function which will delete the cached filename, if it exists. \"\"\" @functools.wraps(function) def wrapped(toplevel_filename, *args, **kw): # Delete the cache. cache_filename = cache_getter(toplevel_filename) if path.exists(cache_filename): os.remove(cache_filename) # Invoke the original function. return function(toplevel_filename, *args, **kw) return wrapped","title":"delete_cache_function()"},{"location":"api_reference/beancount.loader.html#beancount.loader.get_cache_filename","text":"Compute the cache filename from a given pattern and the top-level filename. Parameters: pattern ( str ) \u2013 A cache filename or pattern. If the pattern contains '{filename}' this will get replaced by the top-level filename. This may be absolute or relative. filename ( str ) \u2013 The top-level filename. Returns: str \u2013 The resolved cache filename. Source code in beancount/loader.py def get_cache_filename(pattern: str, filename: str) -> str: \"\"\"Compute the cache filename from a given pattern and the top-level filename. Args: pattern: A cache filename or pattern. If the pattern contains '{filename}' this will get replaced by the top-level filename. This may be absolute or relative. filename: The top-level filename. Returns: The resolved cache filename. \"\"\" abs_filename = path.abspath(filename) if path.isabs(pattern): abs_pattern = pattern else: abs_pattern = path.join(path.dirname(abs_filename), pattern) return abs_pattern.format(filename=path.basename(filename))","title":"get_cache_filename()"},{"location":"api_reference/beancount.loader.html#beancount.loader.initialize","text":"Initialize the loader. Source code in beancount/loader.py def initialize(use_cache: bool, cache_filename: Optional[str] = None): \"\"\"Initialize the loader.\"\"\" # Unless an environment variable disables it, use the pickle load cache # automatically. Note that this works across all Python programs running the # loader which is why it's located here. # pylint: disable=invalid-name global _load_file # Make a function to compute the cache filename. cache_pattern = (cache_filename or os.getenv('BEANCOUNT_LOAD_CACHE_FILENAME') or PICKLE_CACHE_FILENAME) cache_getter = functools.partial(get_cache_filename, cache_pattern) if use_cache: _load_file = pickle_cache_function(cache_getter, PICKLE_CACHE_THRESHOLD, _uncached_load_file) else: if cache_filename is not None: logging.warning(\"Cache disabled; \" \"Explicitly overridden cache filename %s will be ignored.\", cache_filename) _load_file = delete_cache_function(cache_getter, _uncached_load_file)","title":"initialize()"},{"location":"api_reference/beancount.loader.html#beancount.loader.load_doc","text":"A factory of decorators that loads the docstring and calls the function with entries. This is an incredibly convenient tool to write lots of tests. Write a unittest using the standard TestCase class and put the input entries in the function's docstring. Parameters: expect_errors \u2013 A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. Returns: A wrapped method that accepts a single 'self' argument. Source code in beancount/loader.py def load_doc(expect_errors=False): \"\"\"A factory of decorators that loads the docstring and calls the function with entries. This is an incredibly convenient tool to write lots of tests. Write a unittest using the standard TestCase class and put the input entries in the function's docstring. Args: expect_errors: A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. Returns: A wrapped method that accepts a single 'self' argument. \"\"\" def decorator(fun): \"\"\"A decorator that parses the function's docstring as an argument. Args: fun: A callable method, that accepts the three return arguments that load() returns. Returns: A decorated test function. \"\"\" @functools.wraps(fun) def wrapper(self): entries, errors, options_map = load_string(fun.__doc__, dedent=True) if expect_errors is not None: if expect_errors is False and errors: oss = io.StringIO() printer.print_errors(errors, file=oss) self.fail(\"Unexpected errors found:\\n{}\".format(oss.getvalue())) elif expect_errors is True and not errors: self.fail(\"Expected errors, none found:\") # Note: Even if we expected no errors, we call this function with an # empty 'errors' list. This is so that the interface does not change # based on the arguments to the decorator, which would be somewhat # ugly and which would require explanation. return fun(self, entries, errors, options_map) wrapper.__input__ = wrapper.__doc__ wrapper.__doc__ = None return wrapper return decorator","title":"load_doc()"},{"location":"api_reference/beancount.loader.html#beancount.loader.load_encrypted_file","text":"Load an encrypted Beancount input file. Parameters: filename \u2013 The name of an encrypted file to be parsed. log_timings \u2013 See load_string(). log_errors \u2013 See load_string(). extra_validations \u2013 See load_string(). dedent \u2013 See load_string(). encoding \u2013 See load_string(). Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. Source code in beancount/loader.py def load_encrypted_file(filename, log_timings=None, log_errors=None, extra_validations=None, dedent=False, encoding=None): \"\"\"Load an encrypted Beancount input file. Args: filename: The name of an encrypted file to be parsed. log_timings: See load_string(). log_errors: See load_string(). extra_validations: See load_string(). dedent: See load_string(). encoding: See load_string(). Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. \"\"\" contents = encryption.read_encrypted_file(filename) return load_string(contents, log_timings=log_timings, log_errors=log_errors, extra_validations=extra_validations, encoding=encoding)","title":"load_encrypted_file()"},{"location":"api_reference/beancount.loader.html#beancount.loader.load_file","text":"Open a Beancount input file, parse it, run transformations and validate. Parameters: filename \u2013 The name of the file to be parsed. log_timings \u2013 A file object or function to write timings to, or None, if it should remain quiet. log_errors \u2013 A file object or function to write errors to, or None, if it should remain quiet. extra_validations \u2013 A list of extra validation functions to run after loading this list of entries. encoding \u2013 A string or None, the encoding to decode the input filename with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. Source code in beancount/loader.py def load_file(filename, log_timings=None, log_errors=None, extra_validations=None, encoding=None): \"\"\"Open a Beancount input file, parse it, run transformations and validate. Args: filename: The name of the file to be parsed. log_timings: A file object or function to write timings to, or None, if it should remain quiet. log_errors: A file object or function to write errors to, or None, if it should remain quiet. extra_validations: A list of extra validation functions to run after loading this list of entries. encoding: A string or None, the encoding to decode the input filename with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. \"\"\" filename = path.expandvars(path.expanduser(filename)) if not path.isabs(filename): filename = path.normpath(path.join(os.getcwd(), filename)) if encryption.is_encrypted_file(filename): # Note: Caching is not supported for encrypted files. entries, errors, options_map = load_encrypted_file( filename, log_timings, log_errors, extra_validations, False, encoding) else: entries, errors, options_map = _load_file( filename, log_timings, extra_validations, encoding) _log_errors(errors, log_errors) return entries, errors, options_map","title":"load_file()"},{"location":"api_reference/beancount.loader.html#beancount.loader.load_string","text":"Open a Beancount input string, parse it, run transformations and validate. Parameters: string \u2013 A Beancount input string. log_timings \u2013 A file object or function to write timings to, or None, if it should remain quiet. log_errors \u2013 A file object or function to write errors to, or None, if it should remain quiet. extra_validations \u2013 A list of extra validation functions to run after loading this list of entries. dedent \u2013 A boolean, if set, remove the whitespace in front of the lines. encoding \u2013 A string or None, the encoding to decode the input string with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the string, \"errors\" a list of error objects generated while parsing and validating the string, and \"options_map\", a dict of the options parsed from the string. Source code in beancount/loader.py def load_string(string, log_timings=None, log_errors=None, extra_validations=None, dedent=False, encoding=None): \"\"\"Open a Beancount input string, parse it, run transformations and validate. Args: string: A Beancount input string. log_timings: A file object or function to write timings to, or None, if it should remain quiet. log_errors: A file object or function to write errors to, or None, if it should remain quiet. extra_validations: A list of extra validation functions to run after loading this list of entries. dedent: A boolean, if set, remove the whitespace in front of the lines. encoding: A string or None, the encoding to decode the input string with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the string, \"errors\" a list of error objects generated while parsing and validating the string, and \"options_map\", a dict of the options parsed from the string. \"\"\" if dedent: string = textwrap.dedent(string) entries, errors, options_map = _load([(string, False)], log_timings, extra_validations, encoding) _log_errors(errors, log_errors) return entries, errors, options_map","title":"load_string()"},{"location":"api_reference/beancount.loader.html#beancount.loader.needs_refresh","text":"Predicate that returns true if at least one of the input files may have changed. Parameters: options_map \u2013 An options dict as per the parser. mtime \u2013 A modified time, to check if it covers the include files in the options_map. Returns: A boolean, true if the input is obsoleted by changes in the input files. Source code in beancount/loader.py def needs_refresh(options_map): \"\"\"Predicate that returns true if at least one of the input files may have changed. Args: options_map: An options dict as per the parser. mtime: A modified time, to check if it covers the include files in the options_map. Returns: A boolean, true if the input is obsoleted by changes in the input files. \"\"\" if options_map is None: return True input_hash = compute_input_hash(options_map['include']) return 'input_hash' not in options_map or input_hash != options_map['input_hash']","title":"needs_refresh()"},{"location":"api_reference/beancount.loader.html#beancount.loader.pickle_cache_function","text":"Decorate a loader function to make it loads its result from a pickle cache. This considers the first argument as a top-level filename and assumes the function to be cached returns an (entries, errors, options_map) triple. We use the 'include' option value in order to check whether any of the included files has changed. It's essentially a special case for an on-disk memoizer. If any of the included files are more recent than the cache, the function is recomputed and the cache refreshed. Parameters: cache_getter \u2013 A function of one argument, the top-level filename, which will return the name of the corresponding cache file. time_threshold \u2013 A float, the number of seconds below which we don't bother caching. function \u2013 A function object to decorate for caching. Returns: A decorated function which will pull its result from a cache file if it is available. Source code in beancount/loader.py def pickle_cache_function(cache_getter, time_threshold, function): \"\"\"Decorate a loader function to make it loads its result from a pickle cache. This considers the first argument as a top-level filename and assumes the function to be cached returns an (entries, errors, options_map) triple. We use the 'include' option value in order to check whether any of the included files has changed. It's essentially a special case for an on-disk memoizer. If any of the included files are more recent than the cache, the function is recomputed and the cache refreshed. Args: cache_getter: A function of one argument, the top-level filename, which will return the name of the corresponding cache file. time_threshold: A float, the number of seconds below which we don't bother caching. function: A function object to decorate for caching. Returns: A decorated function which will pull its result from a cache file if it is available. \"\"\" @functools.wraps(function) def wrapped(toplevel_filename, *args, **kw): cache_filename = cache_getter(toplevel_filename) # Read the cache if it exists in order to get the list of files whose # timestamps to check. exists = path.exists(cache_filename) if exists: with open(cache_filename, 'rb') as file: try: result = pickle.load(file) except Exception as exc: # Note: Not a big fan of doing this, but here we handle all # possible exceptions because unpickling of an old or # corrupted pickle file manifests as a variety of different # exception types. # The cache file is corrupted; ignore it and recompute. logging.error(\"Cache file is corrupted: %s; recomputing.\", exc) result = None else: # Check that the latest timestamp has not been written after the # cache file. entries, errors, options_map = result if not needs_refresh(options_map): # All timestamps are legit; cache hit. return result # We failed; recompute the value. if exists: try: os.remove(cache_filename) except OSError as exc: # Warn for errors on read-only filesystems. logging.warning(\"Could not remove picklecache file %s: %s\", cache_filename, exc) time_before = time.time() result = function(toplevel_filename, *args, **kw) time_after = time.time() # Overwrite the cache file if the time it takes to compute it # justifies it. if time_after - time_before > time_threshold: try: with open(cache_filename, 'wb') as file: pickle.dump(result, file) except Exception as exc: logging.warning(\"Could not write to picklecache file %s: %s\", cache_filename, exc) return result return wrapped","title":"pickle_cache_function()"},{"location":"api_reference/beancount.loader.html#beancount.loader.run_transformations","text":"Run the various transformations on the entries. This is where entries are being synthesized, checked, plugins are run, etc. Parameters: entries \u2013 A list of directives as read from the parser. parse_errors \u2013 A list of errors so far. options_map \u2013 An options dict as read from the parser. log_timings \u2013 A function to write timing log entries to, or None, if it should be quiet. Returns: A list of modified entries, and a list of errors, also possibly modified. Source code in beancount/loader.py def run_transformations(entries, parse_errors, options_map, log_timings): \"\"\"Run the various transformations on the entries. This is where entries are being synthesized, checked, plugins are run, etc. Args: entries: A list of directives as read from the parser. parse_errors: A list of errors so far. options_map: An options dict as read from the parser. log_timings: A function to write timing log entries to, or None, if it should be quiet. Returns: A list of modified entries, and a list of errors, also possibly modified. \"\"\" # A list of errors to extend (make a copy to avoid modifying the input). errors = list(parse_errors) # Process the plugins. if options_map['plugin_processing_mode'] == 'raw': plugins_iter = options_map[\"plugin\"] elif options_map['plugin_processing_mode'] == 'default': plugins_iter = itertools.chain(DEFAULT_PLUGINS_PRE, options_map[\"plugin\"], DEFAULT_PLUGINS_POST) else: assert \"Invalid value for plugin_processing_mode: {}\".format( options_map['plugin_processing_mode']) for plugin_name, plugin_config in plugins_iter: # Issue a warning on a renamed module. renamed_name = RENAMED_MODULES.get(plugin_name, None) if renamed_name: warnings.warn(\"Deprecation notice: Module '{}' has been renamed to '{}'; \" \"please adjust your plugin directive.\".format( plugin_name, renamed_name)) plugin_name = renamed_name # Try to import the module. try: module = importlib.import_module(plugin_name) if not hasattr(module, '__plugins__'): continue with misc_utils.log_time(plugin_name, log_timings, indent=2): # Run each transformer function in the plugin. for function_name in module.__plugins__: if isinstance(function_name, str): # Support plugin functions provided by name. callback = getattr(module, function_name) else: # Support function types directly, not just names. callback = function_name if plugin_config is not None: entries, plugin_errors = callback(entries, options_map, plugin_config) else: entries, plugin_errors = callback(entries, options_map) errors.extend(plugin_errors) # Ensure that the entries are sorted. Don't trust the plugins # themselves. entries.sort(key=data.entry_sortkey) except (ImportError, TypeError) as exc: # Upon failure, just issue an error. errors.append(LoadError(data.new_metadata(\"\", 0), 'Error importing \"{}\": {}'.format( plugin_name, str(exc)), None)) return entries, errors","title":"run_transformations()"},{"location":"api_reference/beancount.ops.html","text":"beancount.ops \uf0c1 Operations on the entries defined in the core modules. This package contains various functions which operate on lists of entries. beancount.ops.balance \uf0c1 Automatic padding of gaps between entries. beancount.ops.balance.BalanceError ( tuple ) \uf0c1 BalanceError(source, message, entry) beancount.ops.balance.BalanceError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/balance.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.ops.balance.BalanceError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of BalanceError(source, message, entry) beancount.ops.balance.BalanceError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/ops/balance.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.ops.balance.check(entries, options_map) \uf0c1 Process the balance assertion directives. For each Balance directive, check that their expected balance corresponds to the actual balance computed at that time and replace failing ones by new ones with a flag that indicates failure. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, parsed from the input file. Returns: A pair of a list of directives and a list of balance check errors. Source code in beancount/ops/balance.py def check(entries, options_map): \"\"\"Process the balance assertion directives. For each Balance directive, check that their expected balance corresponds to the actual balance computed at that time and replace failing ones by new ones with a flag that indicates failure. Args: entries: A list of directives. options_map: A dict of options, parsed from the input file. Returns: A pair of a list of directives and a list of balance check errors. \"\"\" new_entries = [] check_errors = [] # This is similar to realization, but performed in a different order, and # where we only accumulate inventories for accounts that have balance # assertions in them (this saves on time). Here we process the entries one # by one along with the balance checks. We use a temporary realization in # order to hold the incremental tree of balances, so that we can easily get # the amounts of an account's subaccounts for making checks on parent # accounts. real_root = realization.RealAccount('') # Figure out the set of accounts for which we need to compute a running # inventory balance. asserted_accounts = {entry.account for entry in entries if isinstance(entry, Balance)} # Add all children accounts of an asserted account to be calculated as well, # and pre-create these accounts, and only those (we're just being tight to # make sure). asserted_match_list = [account.parent_matcher(account_) for account_ in asserted_accounts] for account_ in getters.get_accounts(entries): if (account_ in asserted_accounts or any(match(account_) for match in asserted_match_list)): realization.get_or_create(real_root, account_) # Get the Open directives for each account. open_close_map = getters.get_account_open_close(entries) for entry in entries: if isinstance(entry, Transaction): # For each of the postings' accounts, update the balance inventory. for posting in entry.postings: real_account = realization.get(real_root, posting.account) # The account will have been created only if we're meant to track it. if real_account is not None: # Note: Always allow negative lots for the purpose of balancing. # This error should show up somewhere else than here. real_account.balance.add_position(posting) elif isinstance(entry, Balance): # Check that the currency of the balance check is one of the allowed # currencies for that account. expected_amount = entry.amount try: open, _ = open_close_map[entry.account] except KeyError: check_errors.append( BalanceError(entry.meta, \"Account '{}' does not exist: \".format(entry.account), entry)) continue if (expected_amount is not None and open and open.currencies and expected_amount.currency not in open.currencies): check_errors.append( BalanceError(entry.meta, \"Invalid currency '{}' for Balance directive: \".format( expected_amount.currency), entry)) # Sum up the current balances for this account and its # sub-accounts. We want to support checks for parent accounts # for the total sum of their subaccounts. # # FIXME: Improve the performance further by computing the balance # for the desired currency only. This won't allow us to cache in # this way but may be faster, if we're not asserting all the # currencies. Furthermore, we could probably avoid recomputing the # balance if a subtree of positions hasn't been invalidated by a new # position added to the realization. Do this. real_account = realization.get(real_root, entry.account) assert real_account is not None, \"Missing {}\".format(entry.account) subtree_balance = realization.compute_balance(real_account, leaf_only=False) # Get only the amount in the desired currency. balance_amount = subtree_balance.get_currency_units(expected_amount.currency) # Check if the amount is within bounds of the expected amount. diff_amount = amount.sub(balance_amount, expected_amount) # Use the specified tolerance or automatically infer it. tolerance = get_balance_tolerance(entry, options_map) if abs(diff_amount.number) > tolerance: check_errors.append( BalanceError(entry.meta, (\"Balance failed for '{}': \" \"expected {} != accumulated {} ({} {})\").format( entry.account, expected_amount, balance_amount, abs(diff_amount.number), ('too much' if diff_amount.number > 0 else 'too little')), entry)) # Substitute the entry by a failing entry, with the diff_amount # field set on it. I'm not entirely sure that this is the best # of ideas, maybe leaving the original check intact and insert a # new error entry might be more functional or easier to # understand. entry = entry._replace( meta=entry.meta.copy(), diff_amount=diff_amount) new_entries.append(entry) return new_entries, check_errors beancount.ops.balance.get_balance_tolerance(balance_entry, options_map) \uf0c1 Get the tolerance amount for a single entry. Parameters: balance_entry \u2013 An instance of data.Balance options_map \u2013 An options dict, as per the parser. Returns: A Decimal, the amount of tolerance implied by the directive. Source code in beancount/ops/balance.py def get_balance_tolerance(balance_entry, options_map): \"\"\"Get the tolerance amount for a single entry. Args: balance_entry: An instance of data.Balance options_map: An options dict, as per the parser. Returns: A Decimal, the amount of tolerance implied by the directive. \"\"\" if balance_entry.tolerance is not None: # Use the balance-specific tolerance override if it is provided. tolerance = balance_entry.tolerance else: expo = balance_entry.amount.number.as_tuple().exponent if expo < 0: # Be generous and always allow twice the multiplier on Balance and # Pad because the user creates these and the rounding of those # balances may often be further off than those used within a single # transaction. tolerance = options_map[\"inferred_tolerance_multiplier\"] * 2 tolerance = ONE.scaleb(expo) * tolerance else: tolerance = ZERO return tolerance beancount.ops.basicops \uf0c1 Basic filtering and aggregation operations on lists of entries. This module contains some common basic operations on entries that are complex enough not to belong in core/data.py. beancount.ops.basicops.filter_link(link, entries) \uf0c1 Yield all the entries which have the given link. Parameters: link \u2013 A string, the link we are interested in. Yields: Every entry in 'entries' that links to 'link. Source code in beancount/ops/basicops.py def filter_link(link, entries): \"\"\"Yield all the entries which have the given link. Args: link: A string, the link we are interested in. Yields: Every entry in 'entries' that links to 'link. \"\"\" for entry in entries: # pylint: disable=bad-continuation if (isinstance(entry, data.Transaction) and entry.links and link in entry.links): yield entry beancount.ops.basicops.filter_tag(tag, entries) \uf0c1 Yield all the entries which have the given tag. Parameters: tag \u2013 A string, the tag we are interested in. Yields: Every entry in 'entries' that tags to 'tag. Source code in beancount/ops/basicops.py def filter_tag(tag, entries): \"\"\"Yield all the entries which have the given tag. Args: tag: A string, the tag we are interested in. Yields: Every entry in 'entries' that tags to 'tag. \"\"\" for entry in entries: # pylint: disable=bad-continuation if (isinstance(entry, data.Transaction) and entry.tags and tag in entry.tags): yield entry beancount.ops.basicops.get_common_accounts(entries) \uf0c1 Compute the intersection of the accounts on the given entries. Parameters: entries \u2013 A list of Transaction entries to process. Returns: A set of strings, the names of the common accounts from these entries. Source code in beancount/ops/basicops.py def get_common_accounts(entries): \"\"\"Compute the intersection of the accounts on the given entries. Args: entries: A list of Transaction entries to process. Returns: A set of strings, the names of the common accounts from these entries. \"\"\" assert all(isinstance(entry, data.Transaction) for entry in entries) # If there is a single entry, the common accounts to it is all its accounts. # Note that this also works with no entries (yields an empty set). if len(entries) < 2: if entries: intersection = {posting.account for posting in entries[0].postings} else: intersection = set() else: entries_iter = iter(entries) intersection = set(posting.account for posting in next(entries_iter).postings) for entry in entries_iter: accounts = set(posting.account for posting in entry.postings) intersection &= accounts if not intersection: break return intersection beancount.ops.basicops.group_entries_by_link(entries) \uf0c1 Group the list of entries by link. Parameters: entries \u2013 A list of directives/transactions to process. Returns: A dict of link-name to list of entries. Source code in beancount/ops/basicops.py def group_entries_by_link(entries): \"\"\"Group the list of entries by link. Args: entries: A list of directives/transactions to process. Returns: A dict of link-name to list of entries. \"\"\" link_groups = defaultdict(list) for entry in entries: if not (isinstance(entry, data.Transaction) and entry.links): continue for link in entry.links: link_groups[link].append(entry) return link_groups beancount.ops.compress \uf0c1 Compress multiple entries into a single one. This can be used during import to compress the effective output, for accounts with a large number of similar entries. For example, I had a trading account which would pay out interest every single day. I have no desire to import the full detail of these daily interests, and compressing these interest-only entries to monthly ones made sense. This is the code that was used to carry this out. beancount.ops.compress.compress(entries, predicate) \uf0c1 Compress multiple transactions into single transactions. Replace consecutive sequences of Transaction entries that fulfill the given predicate by a single entry at the date of the last matching entry. 'predicate' is the function that determines if an entry should be compressed. This can be used to simply a list of transactions that are similar and occur frequently. As an example, in a retail FOREX trading account, differential interest of very small amounts is paid every day; it is not relevant to look at the full detail of this interest unless there are other transactions. You can use this to compress it into single entries between other types of transactions. Parameters: entries \u2013 A list of directives. predicate \u2013 A callable which accepts an entry and return true if the entry is intended to be compressed. Returns: A list of directives, with compressible transactions replaced by a summary equivalent. Source code in beancount/ops/compress.py def compress(entries, predicate): \"\"\"Compress multiple transactions into single transactions. Replace consecutive sequences of Transaction entries that fulfill the given predicate by a single entry at the date of the last matching entry. 'predicate' is the function that determines if an entry should be compressed. This can be used to simply a list of transactions that are similar and occur frequently. As an example, in a retail FOREX trading account, differential interest of very small amounts is paid every day; it is not relevant to look at the full detail of this interest unless there are other transactions. You can use this to compress it into single entries between other types of transactions. Args: entries: A list of directives. predicate: A callable which accepts an entry and return true if the entry is intended to be compressed. Returns: A list of directives, with compressible transactions replaced by a summary equivalent. \"\"\" new_entries = [] pending = [] for entry in entries: if isinstance(entry, data.Transaction) and predicate(entry): # Save for compressing later. pending.append(entry) else: # Compress and output all the pending entries. if pending: new_entries.append(merge(pending, pending[-1])) pending.clear() # Output the differing entry. new_entries.append(entry) if pending: new_entries.append(merge(pending, pending[-1])) return new_entries beancount.ops.compress.merge(entries, prototype_txn) \uf0c1 Merge the postings of a list of Transactions into a single one. Merge postings the given entries into a single entry with the Transaction attributes of the prototype. Return the new entry. The combined list of postings are merged if everything about the postings is the same except the number. Parameters: entries \u2013 A list of directives. prototype_txn \u2013 A Transaction which is used to create the compressed Transaction instance. Its list of postings is ignored. Returns: A new Transaction instance which contains all the postings from the input entries merged together. Source code in beancount/ops/compress.py def merge(entries, prototype_txn): \"\"\"Merge the postings of a list of Transactions into a single one. Merge postings the given entries into a single entry with the Transaction attributes of the prototype. Return the new entry. The combined list of postings are merged if everything about the postings is the same except the number. Args: entries: A list of directives. prototype_txn: A Transaction which is used to create the compressed Transaction instance. Its list of postings is ignored. Returns: A new Transaction instance which contains all the postings from the input entries merged together. \"\"\" # Aggregate the postings together. This is a mapping of numberless postings # to their number of units. postings_map = collections.defaultdict(Decimal) for entry in data.filter_txns(entries): for posting in entry.postings: # We strip the number off the posting to act as an aggregation key. key = data.Posting(posting.account, Amount(None, posting.units.currency), posting.cost, posting.price, posting.flag, None) postings_map[key] += posting.units.number # Create a new transaction with the aggregated postings. new_entry = data.Transaction(prototype_txn.meta, prototype_txn.date, prototype_txn.flag, prototype_txn.payee, prototype_txn.narration, data.EMPTY_SET, data.EMPTY_SET, []) # Sort for at least some stability of output. sorted_items = sorted(postings_map.items(), key=lambda item: (item[0].account, item[0].units.currency, item[1])) # Issue the merged postings. for posting, number in sorted_items: units = Amount(number, posting.units.currency) new_entry.postings.append( data.Posting(posting.account, units, posting.cost, posting.price, posting.flag, posting.meta)) return new_entry beancount.ops.documents \uf0c1 Everything that relates to creating the Document directives. beancount.ops.documents.DocumentError ( tuple ) \uf0c1 DocumentError(source, message, entry) beancount.ops.documents.DocumentError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/documents.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.ops.documents.DocumentError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of DocumentError(source, message, entry) beancount.ops.documents.DocumentError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/ops/documents.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.ops.documents.find_documents(directory, input_filename, accounts_only=None, strict=False) \uf0c1 Find dated document files under the given directory. If a restricting set of accounts is provided in 'accounts_only', only return entries that correspond to one of the given accounts. Parameters: directory \u2013 A string, the name of the root of the directory hierarchy to be searched. input_filename \u2013 The name of the file to be used for the Document directives. This is also used to resolve relative directory names. accounts_only \u2013 A set of valid accounts strings to search for. strict \u2013 A boolean, set to true if you want to generate errors on documents found in accounts not provided in accounts_only. This is only meaningful if accounts_only is specified. Returns: A list of new Document objects that were created from the files found, and a list of new errors generated. Source code in beancount/ops/documents.py def find_documents(directory, input_filename, accounts_only=None, strict=False): \"\"\"Find dated document files under the given directory. If a restricting set of accounts is provided in 'accounts_only', only return entries that correspond to one of the given accounts. Args: directory: A string, the name of the root of the directory hierarchy to be searched. input_filename: The name of the file to be used for the Document directives. This is also used to resolve relative directory names. accounts_only: A set of valid accounts strings to search for. strict: A boolean, set to true if you want to generate errors on documents found in accounts not provided in accounts_only. This is only meaningful if accounts_only is specified. Returns: A list of new Document objects that were created from the files found, and a list of new errors generated. \"\"\" errors = [] # Compute the documents directory name relative to the beancount input # file itself. if not path.isabs(directory): input_directory = path.dirname(input_filename) directory = path.abspath(path.normpath(path.join(input_directory, directory))) # If the directory does not exist, just generate an error and return. if not path.exists(directory): meta = data.new_metadata(input_filename, 0) error = DocumentError( meta, \"Document root '{}' does not exist\".format(directory), None) return ([], [error]) # Walk the hierarchy of files. entries = [] for root, account_name, dirs, files in account.walk(directory): # Look for files that have a dated filename. for filename in files: match = re.match(r'(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d).(.*)', filename) if not match: continue # If a restricting set of accounts was specified, skip document # directives found in accounts with no corresponding account name. if accounts_only is not None and not account_name in accounts_only: if strict: if any(account_name.startswith(account) for account in accounts_only): errors.append(DocumentError( data.new_metadata(input_filename, 0), \"Document '{}' found in child account {}\".format( filename, account_name), None)) elif any(account.startswith(account_name) for account in accounts_only): errors.append(DocumentError( data.new_metadata(input_filename, 0), \"Document '{}' found in parent account {}\".format( filename, account_name), None)) continue # Create a new directive. meta = data.new_metadata(input_filename, 0) try: date = datetime.date(*map(int, match.group(1, 2, 3))) except ValueError as exc: errors.append(DocumentError( data.new_metadata(input_filename, 0), \"Invalid date on document file '{}': {}\".format( filename, exc), None)) else: entry = data.Document(meta, date, account_name, path.join(root, filename), data.EMPTY_SET, data.EMPTY_SET) entries.append(entry) return (entries, errors) beancount.ops.documents.process_documents(entries, options_map) \uf0c1 Check files for document directives and create documents directives automatically. Parameters: entries \u2013 A list of all directives parsed from the file. options_map \u2013 An options dict, as is output by the parser. We're using its 'filename' option to figure out relative path to search for documents. Returns: A pair of list of all entries (including new ones), and errors generated during the process of creating document directives. Source code in beancount/ops/documents.py def process_documents(entries, options_map): \"\"\"Check files for document directives and create documents directives automatically. Args: entries: A list of all directives parsed from the file. options_map: An options dict, as is output by the parser. We're using its 'filename' option to figure out relative path to search for documents. Returns: A pair of list of all entries (including new ones), and errors generated during the process of creating document directives. \"\"\" filename = options_map[\"filename\"] # Detect filenames that should convert into entries. autodoc_entries = [] autodoc_errors = [] document_dirs = options_map['documents'] if document_dirs: # Restrict to the list of valid accounts only. accounts = getters.get_accounts(entries) # Accumulate all the entries. for directory in map(path.normpath, document_dirs): new_entries, new_errors = find_documents(directory, filename, accounts) autodoc_entries.extend(new_entries) autodoc_errors.extend(new_errors) # Merge the two lists of entries and errors. Keep the entries sorted. entries.extend(autodoc_entries) entries.sort(key=data.entry_sortkey) return (entries, autodoc_errors) beancount.ops.documents.verify_document_files_exist(entries, unused_options_map) \uf0c1 Verify that the document entries point to existing files. Parameters: entries \u2013 a list of directives whose documents need to be validated. unused_options_map \u2013 A parser options dict. We're not using it. Returns: The same list of entries, and a list of new errors, if any were encountered. Source code in beancount/ops/documents.py def verify_document_files_exist(entries, unused_options_map): \"\"\"Verify that the document entries point to existing files. Args: entries: a list of directives whose documents need to be validated. unused_options_map: A parser options dict. We're not using it. Returns: The same list of entries, and a list of new errors, if any were encountered. \"\"\" errors = [] for entry in entries: if not isinstance(entry, data.Document): continue if not path.exists(entry.filename): errors.append( DocumentError(entry.meta, 'File does not exist: \"{}\"'.format(entry.filename), entry)) return entries, errors beancount.ops.holdings \uf0c1 Compute final holdings for a list of entries. beancount.ops.holdings.Holding ( tuple ) \uf0c1 Holding(account, number, currency, cost_number, cost_currency, book_value, market_value, price_number, price_date) beancount.ops.holdings.Holding.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/holdings.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.ops.holdings.Holding.__new__(_cls, account, number, currency, cost_number, cost_currency, book_value, market_value, price_number, price_date) special staticmethod \uf0c1 Create new instance of Holding(account, number, currency, cost_number, cost_currency, book_value, market_value, price_number, price_date) beancount.ops.holdings.Holding.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/ops/holdings.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.ops.holdings.aggregate_holdings_by(holdings, keyfun) \uf0c1 Aggregate holdings by some key. Note that the cost-currency must always be included in the group-key (sums over multiple currency units do not make sense), so it is appended to the sort-key automatically. Parameters: keyfun \u2013 A callable, which returns the key to aggregate by. This key need not include the cost-currency. Returns: A list of aggregated holdings. Source code in beancount/ops/holdings.py def aggregate_holdings_by(holdings, keyfun): \"\"\"Aggregate holdings by some key. Note that the cost-currency must always be included in the group-key (sums over multiple currency units do not make sense), so it is appended to the sort-key automatically. Args: keyfun: A callable, which returns the key to aggregate by. This key need not include the cost-currency. Returns: A list of aggregated holdings. \"\"\" # Aggregate the groups of holdings. grouped = collections.defaultdict(list) for holding in holdings: key = (keyfun(holding), holding.cost_currency) grouped[key].append(holding) grouped_holdings = (aggregate_holdings_list(key_holdings) for key_holdings in grouped.values()) # We could potentially filter out holdings with zero units here. These types # of holdings might occur on a group with leaked (i.e., non-zero) cost basis # and zero units. However, sometimes are valid merging of multiple # currencies may occur, and the number value will be legitimately set to # ZERO (for various reasons downstream), so we prefer not to ignore the # holding. Callers must be prepared to deal with a holding of ZERO units and # a non-zero cost basis. {0ed05c502e63, b/16} ## nonzero_holdings = (holding ## for holding in grouped_holdings ## if holding.number != ZERO) # Return the holdings in order. return sorted(grouped_holdings, key=lambda holding: (holding.account, holding.currency)) beancount.ops.holdings.aggregate_holdings_list(holdings) \uf0c1 Aggregate a list of holdings. If there are varying 'account', 'currency' or 'cost_currency' attributes, their values are replaced by '*'. Otherwise they are preserved. Note that all the cost-currency values must be equal in order for aggregations to succeed (without this constraint a sum of units in different currencies has no meaning). Parameters: holdings \u2013 A list of Holding instances. Returns: A single Holding instance, or None, if there are no holdings in the input list. Exceptions: ValueError \u2013 If multiple cost currencies encountered. Source code in beancount/ops/holdings.py def aggregate_holdings_list(holdings): \"\"\"Aggregate a list of holdings. If there are varying 'account', 'currency' or 'cost_currency' attributes, their values are replaced by '*'. Otherwise they are preserved. Note that all the cost-currency values must be equal in order for aggregations to succeed (without this constraint a sum of units in different currencies has no meaning). Args: holdings: A list of Holding instances. Returns: A single Holding instance, or None, if there are no holdings in the input list. Raises: ValueError: If multiple cost currencies encountered. \"\"\" if not holdings: return None # Note: Holding is a bit overspecified with book and market values. We # recompute them from cost and price numbers here anyhow. units, total_book_value, total_market_value = ZERO, ZERO, ZERO accounts = set() currencies = set() cost_currencies = set() price_dates = set() book_value_seen = False market_value_seen = False for holding in holdings: units += holding.number accounts.add(holding.account) price_dates.add(holding.price_date) currencies.add(holding.currency) cost_currencies.add(holding.cost_currency) if holding.book_value is not None: total_book_value += holding.book_value book_value_seen = True elif holding.cost_number is not None: total_book_value += holding.number * holding.cost_number book_value_seen = True if holding.market_value is not None: total_market_value += holding.market_value market_value_seen = True elif holding.price_number is not None: total_market_value += holding.number * holding.price_number market_value_seen = True if book_value_seen: average_cost = total_book_value / units if units else None else: total_book_value = None average_cost = None if market_value_seen: average_price = total_market_value / units if units else None else: total_market_value = None average_price = None if len(cost_currencies) != 1: raise ValueError(\"Cost currencies are not homogeneous for aggregation: {}\".format( ','.join(map(str, cost_currencies)))) units = units if len(currencies) == 1 else ZERO currency = currencies.pop() if len(currencies) == 1 else '*' cost_currency = cost_currencies.pop() account_ = (accounts.pop() if len(accounts) == 1 else account.commonprefix(accounts)) price_date = price_dates.pop() if len(price_dates) == 1 else None return Holding(account_, units, currency, average_cost, cost_currency, total_book_value, total_market_value, average_price, price_date) beancount.ops.holdings.convert_to_currency(price_map, target_currency, holdings_list) \uf0c1 Convert the given list of holdings's fields to a common currency. If the rate is not available to convert, leave the fields empty. Parameters: price_map \u2013 A price-map, as built by prices.build_price_map(). target_currency \u2013 The target common currency to convert amounts to. holdings_list \u2013 A list of holdings.Holding instances. Returns: A modified list of holdings, with the 'extra' field set to the value in 'currency', or None, if it was not possible to convert. Source code in beancount/ops/holdings.py def convert_to_currency(price_map, target_currency, holdings_list): \"\"\"Convert the given list of holdings's fields to a common currency. If the rate is not available to convert, leave the fields empty. Args: price_map: A price-map, as built by prices.build_price_map(). target_currency: The target common currency to convert amounts to. holdings_list: A list of holdings.Holding instances. Returns: A modified list of holdings, with the 'extra' field set to the value in 'currency', or None, if it was not possible to convert. \"\"\" # A list of the fields we should convert. convert_fields = ('cost_number', 'book_value', 'market_value', 'price_number') new_holdings = [] for holding in holdings_list: if holding.cost_currency == target_currency: # The holding is already priced in the target currency; do nothing. new_holding = holding else: if holding.cost_currency is None: # There is no cost currency; make the holding priced in its own # units. The price-map should yield a rate of 1.0 and everything # else works out. if holding.currency is None: raise ValueError(\"Invalid currency '{}'\".format(holding.currency)) holding = holding._replace(cost_currency=holding.currency) # Fill in with book and market value as well. if holding.book_value is None: holding = holding._replace(book_value=holding.number) if holding.market_value is None: holding = holding._replace(market_value=holding.number) assert holding.cost_currency, \"Missing cost currency: {}\".format(holding) base_quote = (holding.cost_currency, target_currency) # Get the conversion rate and replace the required numerical # fields.. _, rate = prices.get_latest_price(price_map, base_quote) if rate is not None: new_holding = misc_utils.map_namedtuple_attributes( convert_fields, lambda number, r=rate: number if number is None else number * r, holding) # Ensure we set the new cost currency after conversion. new_holding = new_holding._replace(cost_currency=target_currency) else: # Could not get the rate... clear every field and set the cost # currency to None. This enough marks the holding conversion as # a failure. new_holding = misc_utils.map_namedtuple_attributes( convert_fields, lambda number: None, holding) new_holding = new_holding._replace(cost_currency=None) new_holdings.append(new_holding) return new_holdings beancount.ops.holdings.get_commodities_at_date(entries, options_map, date=None) \uf0c1 Return a list of commodities present at a particular date. This routine fetches the holdings present at a particular date and returns a list of the commodities held in those holdings. This should define the list of price date points required to assess the market value of this portfolio. Notes: The ticker symbol will be fetched from the corresponding Commodity directive. If there is no ticker symbol defined for a directive or no corresponding Commodity directive, the currency is still included, but 'None' is specified for the symbol. The code that uses this routine should be free to use the currency name to make an attempt to fetch the currency using its name, or to ignore it. The 'cost-currency' is that which is found on the holdings instance and can be ignored. The 'quote-currency' is that which is declared on the Commodity directive from its 'quote' metadata field. This is used in a routine that fetches prices from a data source on the internet (either from LedgerHub, but you can reuse this in your own script if you build one). Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance, the date at which to get the list of relevant holdings. Returns: A list of (currency, cost-currency, quote-currency, ticker) tuples, where currency \u2013 The Beancount base currency to fetch a price for. cost-currency: The cost-currency of the holdings found at the given date. quote-currency: The currency formally declared as quote currency in the metadata of Commodity directives. ticker: The ticker symbol to use for fetching the price (extracted from the metadata of Commodity directives). Source code in beancount/ops/holdings.py def get_commodities_at_date(entries, options_map, date=None): \"\"\"Return a list of commodities present at a particular date. This routine fetches the holdings present at a particular date and returns a list of the commodities held in those holdings. This should define the list of price date points required to assess the market value of this portfolio. Notes: * The ticker symbol will be fetched from the corresponding Commodity directive. If there is no ticker symbol defined for a directive or no corresponding Commodity directive, the currency is still included, but 'None' is specified for the symbol. The code that uses this routine should be free to use the currency name to make an attempt to fetch the currency using its name, or to ignore it. * The 'cost-currency' is that which is found on the holdings instance and can be ignored. The 'quote-currency' is that which is declared on the Commodity directive from its 'quote' metadata field. This is used in a routine that fetches prices from a data source on the internet (either from LedgerHub, but you can reuse this in your own script if you build one). Args: entries: A list of directives. date: A datetime.date instance, the date at which to get the list of relevant holdings. Returns: A list of (currency, cost-currency, quote-currency, ticker) tuples, where currency: The Beancount base currency to fetch a price for. cost-currency: The cost-currency of the holdings found at the given date. quote-currency: The currency formally declared as quote currency in the metadata of Commodity directives. ticker: The ticker symbol to use for fetching the price (extracted from the metadata of Commodity directives). \"\"\" # Remove all the entries after the given date, if requested. if date is not None: entries = summarize.truncate(entries, date) # Get the list of holdings at the particular date. holdings_list = get_final_holdings(entries) # Obtain the unique list of currencies we need to fetch. commodities_list = {(holding.currency, holding.cost_currency) for holding in holdings_list} # Add in the associated ticker symbols. commodities_map = getters.get_commodity_map(entries) commodities_symbols_list = [] for currency, cost_currency in sorted(commodities_list): try: commodity_entry = commodities_map[currency] ticker = commodity_entry.meta.get('ticker', None) quote_currency = commodity_entry.meta.get('quote', None) except KeyError: ticker = None quote_currency = None commodities_symbols_list.append( (currency, cost_currency, quote_currency, ticker)) return commodities_symbols_list beancount.ops.holdings.get_final_holdings(entries, included_account_types=None, price_map=None, date=None) \uf0c1 Get a dictionary of the latest holdings by account. This basically just flattens the balance sheet's final positions, including that of equity accounts. If a 'price_map' is provided, insert price information in the flattened holdings at the latest date, or at the given date, if one is provided. Only the accounts in 'included_account_types' will be included, and this is always called for Assets and Liabilities only. If left unspecified, holdings from all account types will be included, including Equity, Income and Expenses. Parameters: entries \u2013 A list of directives. included_account_types \u2013 A sequence of strings, the account types to include in the output. A reasonable example would be ('Assets', 'Liabilities'). If not specified, include all account types. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance, the date at which to price the holdings. If left unspecified, we use the latest price information. Returns: A list of dicts, with the following fields \u2013 Source code in beancount/ops/holdings.py def get_final_holdings(entries, included_account_types=None, price_map=None, date=None): \"\"\"Get a dictionary of the latest holdings by account. This basically just flattens the balance sheet's final positions, including that of equity accounts. If a 'price_map' is provided, insert price information in the flattened holdings at the latest date, or at the given date, if one is provided. Only the accounts in 'included_account_types' will be included, and this is always called for Assets and Liabilities only. If left unspecified, holdings from all account types will be included, including Equity, Income and Expenses. Args: entries: A list of directives. included_account_types: A sequence of strings, the account types to include in the output. A reasonable example would be ('Assets', 'Liabilities'). If not specified, include all account types. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance, the date at which to price the holdings. If left unspecified, we use the latest price information. Returns: A list of dicts, with the following fields: \"\"\" # Remove the entries inserted by unrealized gains/losses. Those entries do # affect asset accounts, and we don't want them to appear in holdings. # # Note: Perhaps it would make sense to generalize this concept of \"inserted # unrealized gains.\" simple_entries = [entry for entry in entries if (not isinstance(entry, data.Transaction) or entry.flag != flags.FLAG_UNREALIZED)] # Realize the accounts into a tree (because we want the positions by-account). root_account = realization.realize(simple_entries) # For each account, look at the list of positions and build a list. holdings = [] for real_account in sorted(list(realization.iter_children(root_account)), key=lambda ra: ra.account): if included_account_types: # Skip accounts of invalid types, we only want to reflect the requested # account types, typically assets and liabilities. account_type = account_types.get_account_type(real_account.account) if account_type not in included_account_types: continue for pos in real_account.balance.get_positions(): if pos.cost is not None: # Get price information if we have a price_map. market_value = None if price_map is not None: base_quote = (pos.units.currency, pos.cost.currency) price_date, price_number = prices.get_price(price_map, base_quote, date) if price_number is not None: market_value = pos.units.number * price_number else: price_date, price_number = None, None holding = Holding(real_account.account, pos.units.number, pos.units.currency, pos.cost.number, pos.cost.currency, pos.units.number * pos.cost.number, market_value, price_number, price_date) else: holding = Holding(real_account.account, pos.units.number, pos.units.currency, None, pos.units.currency, pos.units.number, pos.units.number, None, None) holdings.append(holding) return holdings beancount.ops.holdings.holding_to_position(holding) \uf0c1 Convert the holding to a position. Parameters: holding \u2013 An instance of Holding. Returns: An instance of Position. Source code in beancount/ops/holdings.py def holding_to_position(holding): \"\"\"Convert the holding to a position. Args: holding: An instance of Holding. Returns: An instance of Position. \"\"\" return position.Position( amount.Amount(holding.number, holding.currency), (position.Cost(holding.cost_number, holding.cost_currency, None, None) if holding.cost_number else None)) beancount.ops.holdings.holding_to_posting(holding) \uf0c1 Convert the holding to an instance of Posting. Parameters: holding \u2013 An instance of Holding. Returns: An instance of Position. Source code in beancount/ops/holdings.py def holding_to_posting(holding): \"\"\"Convert the holding to an instance of Posting. Args: holding: An instance of Holding. Returns: An instance of Position. \"\"\" position_ = holding_to_position(holding) price = (amount.Amount(holding.price_number, holding.cost_currency) if holding.price_number else None) return data.Posting(holding.account, position_.units, position_.cost, price, None, None) beancount.ops.holdings.reduce_relative(holdings) \uf0c1 Convert the market and book values of the given list of holdings to relative data. Parameters: holdings \u2013 A list of Holding instances. Returns: A list of holdings instances with the absolute value fields replaced by fractions of total portfolio. The new list of holdings is sorted by currency, and the relative fractions are also relative to that currency. Source code in beancount/ops/holdings.py def reduce_relative(holdings): \"\"\"Convert the market and book values of the given list of holdings to relative data. Args: holdings: A list of Holding instances. Returns: A list of holdings instances with the absolute value fields replaced by fractions of total portfolio. The new list of holdings is sorted by currency, and the relative fractions are also relative to that currency. \"\"\" # Group holdings by value currency. by_currency = collections.defaultdict(list) ordering = {} for index, holding in enumerate(holdings): ordering.setdefault(holding.cost_currency, index) by_currency[holding.cost_currency].append(holding) fractional_holdings = [] for currency in sorted(by_currency, key=ordering.get): currency_holdings = by_currency[currency] # Compute total market value for that currency. total_book_value = ZERO total_market_value = ZERO for holding in currency_holdings: if holding.book_value: total_book_value += holding.book_value if holding.market_value: total_market_value += holding.market_value # Sort the currency's holdings with decreasing values of market value. currency_holdings.sort( key=lambda holding: holding.market_value or ZERO, reverse=True) # Output new holdings with the relevant values replaced. for holding in currency_holdings: fractional_holdings.append( holding._replace(book_value=(holding.book_value / total_book_value if holding.book_value is not None else None), market_value=(holding.market_value / total_market_value if holding.market_value is not None else None))) return fractional_holdings beancount.ops.holdings.scale_holding(holding, scale_factor) \uf0c1 Scale the values of a holding. Parameters: holding \u2013 An instance of Holding. scale_factor \u2013 A float or Decimal number. Returns: A scaled copy of the holding. Source code in beancount/ops/holdings.py def scale_holding(holding, scale_factor): \"\"\"Scale the values of a holding. Args: holding: An instance of Holding. scale_factor: A float or Decimal number. Returns: A scaled copy of the holding. \"\"\" return Holding( holding.account, holding.number * scale_factor if holding.number else None, holding.currency, holding.cost_number, holding.cost_currency, holding.book_value * scale_factor if holding.book_value else None, holding.market_value * scale_factor if holding.market_value else None, holding.price_number, holding.price_date) beancount.ops.lifetimes \uf0c1 Given a Beancount ledger, compute time intervals where we hold each commodity. This script computes, for each commodity, which time intervals it is required at. This can then be used to identify a list of dates at which we need to fetch prices in order to properly fill the price database. beancount.ops.lifetimes.compress_intervals_days(intervals, num_days) \uf0c1 Compress a list of date pairs to ignore short stretches of unused days. Parameters: intervals \u2013 A list of pairs of datetime.date instances. num_days \u2013 An integer, the number of unused days to require for intervals to be distinct, to allow a gap. Returns: A new dict of lifetimes map where some intervals may have been joined. Source code in beancount/ops/lifetimes.py def compress_intervals_days(intervals, num_days): \"\"\"Compress a list of date pairs to ignore short stretches of unused days. Args: intervals: A list of pairs of datetime.date instances. num_days: An integer, the number of unused days to require for intervals to be distinct, to allow a gap. Returns: A new dict of lifetimes map where some intervals may have been joined. \"\"\" ignore_interval = datetime.timedelta(days=num_days) new_intervals = [] iter_intervals = iter(intervals) last_begin, last_end = next(iter_intervals) for date_begin, date_end in iter_intervals: if date_begin - last_end < ignore_interval: # Compress. last_end = date_end continue new_intervals.append((last_begin, last_end)) last_begin, last_end = date_begin, date_end new_intervals.append((last_begin, last_end)) return new_intervals beancount.ops.lifetimes.compress_lifetimes_days(lifetimes_map, num_days) \uf0c1 Compress a lifetimes map to ignore short stretches of unused days. Parameters: lifetimes_map \u2013 A dict of currency intervals as returned by get_commodity_lifetimes. num_days \u2013 An integer, the number of unused days to ignore. Returns: A new dict of lifetimes map where some intervals may have been joined. Source code in beancount/ops/lifetimes.py def compress_lifetimes_days(lifetimes_map, num_days): \"\"\"Compress a lifetimes map to ignore short stretches of unused days. Args: lifetimes_map: A dict of currency intervals as returned by get_commodity_lifetimes. num_days: An integer, the number of unused days to ignore. Returns: A new dict of lifetimes map where some intervals may have been joined. \"\"\" return {currency_pair: compress_intervals_days(intervals, num_days) for currency_pair, intervals in lifetimes_map.items()} beancount.ops.lifetimes.get_commodity_lifetimes(entries) \uf0c1 Given a list of directives, figure out the life of each commodity. Parameters: entries \u2013 A list of directives. Returns: A dict of (currency, cost-currency) commodity strings to lists of (start, end) datetime.date pairs. The dates are inclusive of the day the commodity was seen; the end/last dates are one day after the last date seen. Source code in beancount/ops/lifetimes.py def get_commodity_lifetimes(entries): \"\"\"Given a list of directives, figure out the life of each commodity. Args: entries: A list of directives. Returns: A dict of (currency, cost-currency) commodity strings to lists of (start, end) datetime.date pairs. The dates are inclusive of the day the commodity was seen; the end/last dates are one day _after_ the last date seen. \"\"\" lifetimes = collections.defaultdict(list) # The current set of active commodities. commodities = set() # The current balances across all accounts. balances = collections.defaultdict(inventory.Inventory) for entry in entries: # Process only transaction entries. if not isinstance(entry, data.Transaction): continue # Update the balance of affected accounts and check locally whether that # triggered a change in the set of commodities. commodities_changed = False for posting in entry.postings: balance = balances[posting.account] commodities_before = balance.currency_pairs() balance.add_position(posting) commodities_after = balance.currency_pairs() if commodities_after != commodities_before: commodities_changed = True # If there was a change in one of the affected account's list of # commodities, recompute the total set globally. This should not # occur very frequently. if commodities_changed: new_commodities = set( itertools.chain(*(inv.currency_pairs() for inv in balances.values()))) if new_commodities != commodities: # The new global set of commodities has changed; update our # the dictionary of intervals. for currency in new_commodities - commodities: lifetimes[currency].append((entry.date, None)) for currency in commodities - new_commodities: lifetime = lifetimes[currency] begin_date, end_date = lifetime.pop(-1) assert end_date is None lifetime.append((begin_date, entry.date + ONEDAY)) # Update our current set. commodities = new_commodities return lifetimes beancount.ops.lifetimes.required_weekly_prices(lifetimes_map, date_last) \uf0c1 Enumerate all the commodities and Fridays where the price is required. Given a map of lifetimes for a set of commodities, enumerate all the Fridays for each commodity where it is active. This can be used to connect to a historical price fetcher routine to fill in missing price entries from an existing ledger. Parameters: lifetimes_map \u2013 A dict of currency to active intervals as returned by get_commodity_lifetimes(). date_last \u2013 A datetime.date instance, the last date which we're interested in. Returns: Tuples of (date, currency, cost-currency). Source code in beancount/ops/lifetimes.py def required_weekly_prices(lifetimes_map, date_last): \"\"\"Enumerate all the commodities and Fridays where the price is required. Given a map of lifetimes for a set of commodities, enumerate all the Fridays for each commodity where it is active. This can be used to connect to a historical price fetcher routine to fill in missing price entries from an existing ledger. Args: lifetimes_map: A dict of currency to active intervals as returned by get_commodity_lifetimes(). date_last: A datetime.date instance, the last date which we're interested in. Returns: Tuples of (date, currency, cost-currency). \"\"\" results = [] for currency_pair, intervals in lifetimes_map.items(): if currency_pair[1] is None: continue for date_begin, date_end in intervals: # Find first Friday before the minimum date. diff_days = 4 - date_begin.weekday() if diff_days > 1: diff_days -= 7 date = date_begin + datetime.timedelta(days=diff_days) # Iterate over all Fridays. if date_end is None: date_end = date_last while date < date_end: results.append((date, currency_pair[0], currency_pair[1])) date += ONE_WEEK return sorted(results) beancount.ops.pad \uf0c1 Automatic padding of gaps between entries. beancount.ops.pad.PadError ( tuple ) \uf0c1 PadError(source, message, entry) beancount.ops.pad.PadError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/pad.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.ops.pad.PadError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of PadError(source, message, entry) beancount.ops.pad.PadError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/ops/pad.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.ops.pad.pad(entries, options_map) \uf0c1 Insert transaction entries for to fulfill a subsequent balance check. Synthesize and insert Transaction entries right after Pad entries in order to fulfill checks in the padded accounts. Returns a new list of entries. Note that this doesn't pad across parent-child relationships, it is a very simple kind of pad. (I have found this to be sufficient in practice, and simpler to implement and understand.) Furthermore, this pads for a single currency only, that is, balance checks are specified only for one currency at a time, and pads will only be inserted for those currencies. Parameters: entries \u2013 A list of directives. options_map \u2013 A parser options dict. Returns: A new list of directives, with Pad entries inserted, and a list of new errors produced. Source code in beancount/ops/pad.py def pad(entries, options_map): \"\"\"Insert transaction entries for to fulfill a subsequent balance check. Synthesize and insert Transaction entries right after Pad entries in order to fulfill checks in the padded accounts. Returns a new list of entries. Note that this doesn't pad across parent-child relationships, it is a very simple kind of pad. (I have found this to be sufficient in practice, and simpler to implement and understand.) Furthermore, this pads for a single currency only, that is, balance checks are specified only for one currency at a time, and pads will only be inserted for those currencies. Args: entries: A list of directives. options_map: A parser options dict. Returns: A new list of directives, with Pad entries inserted, and a list of new errors produced. \"\"\" pad_errors = [] # Find all the pad entries and group them by account. pads = list(misc_utils.filter_type(entries, data.Pad)) pad_dict = misc_utils.groupby(lambda x: x.account, pads) # Partially realize the postings, so we can iterate them by account. by_account = realization.postings_by_account(entries) # A dict of pad -> list of entries to be inserted. new_entries = {id(pad): [] for pad in pads} # Process each account that has a padding group. for account_, pad_list in sorted(pad_dict.items()): # Last encountered / currency active pad entry. active_pad = None # Gather all the postings for the account and its children. postings = [] is_child = account.parent_matcher(account_) for item_account, item_postings in by_account.items(): if is_child(item_account): postings.extend(item_postings) postings.sort(key=data.posting_sortkey) # A set of currencies already padded so far in this account. padded_lots = set() pad_balance = inventory.Inventory() for entry in postings: assert not isinstance(entry, data.Posting) if isinstance(entry, data.TxnPosting): # This is a transaction; update the running balance for this # account. pad_balance.add_position(entry.posting) elif isinstance(entry, data.Pad): if entry.account == account_: # Mark this newly encountered pad as active and allow all lots # to be padded heretofore. active_pad = entry padded_lots = set() elif isinstance(entry, data.Balance): check_amount = entry.amount # Compare the current balance amount to the expected one from # the check entry. IMPORTANT: You need to understand that this # does not check a single position, but rather checks that the # total amount for a particular currency (which itself is # distinct from the cost). balance_amount = pad_balance.get_currency_units(check_amount.currency) diff_amount = amount.sub(balance_amount, check_amount) # Use the specified tolerance or automatically infer it. tolerance = balance.get_balance_tolerance(entry, options_map) if abs(diff_amount.number) > tolerance: # The check fails; we need to pad. # Pad only if pad entry is active and we haven't already # padded that lot since it was last encountered. if active_pad and (check_amount.currency not in padded_lots): # Note: we decide that it's an error to try to pad # positions at cost; we check here that all the existing # positions with that currency have no cost. positions = [pos for pos in pad_balance.get_positions() if pos.units.currency == check_amount.currency] for position_ in positions: if position_.cost is not None: pad_errors.append( PadError(entry.meta, (\"Attempt to pad an entry with cost for \" \"balance: {}\".format(pad_balance)), active_pad)) # Thus our padding lot is without cost by default. diff_position = position.Position.from_amounts( amount.Amount(check_amount.number - balance_amount.number, check_amount.currency)) # Synthesize a new transaction entry for the difference. narration = ('(Padding inserted for Balance of {} for ' 'difference {})').format(check_amount, diff_position) new_entry = data.Transaction( active_pad.meta.copy(), active_pad.date, flags.FLAG_PADDING, None, narration, data.EMPTY_SET, data.EMPTY_SET, []) new_entry.postings.append( data.Posting(active_pad.account, diff_position.units, diff_position.cost, None, None, None)) neg_diff_position = -diff_position new_entry.postings.append( data.Posting(active_pad.source_account, neg_diff_position.units, neg_diff_position.cost, None, None, None)) # Save it for later insertion after the active pad. new_entries[id(active_pad)].append(new_entry) # Fixup the running balance. pos, _ = pad_balance.add_position(diff_position) if pos is not None and pos.is_negative_at_cost(): raise ValueError( \"Position held at cost goes negative: {}\".format(pos)) # Mark this lot as padded. Further checks should not pad this lot. padded_lots.add(check_amount.currency) # Insert the newly created entries right after the pad entries that created them. padded_entries = [] for entry in entries: padded_entries.append(entry) if isinstance(entry, data.Pad): entry_list = new_entries[id(entry)] if entry_list: padded_entries.extend(entry_list) else: # Generate errors on unused pad entries. pad_errors.append( PadError(entry.meta, \"Unused Pad entry\", entry)) return padded_entries, pad_errors beancount.ops.summarize \uf0c1 Summarization of entries. This code is used to summarize a sequence of entries (e.g. during a time period) into a few \"opening balance\" entries. This is when computing a balance sheet for a specific time period: we don't want to see the entries from before some period of time, so we fold them into a single transaction per account that has the sum total amount of that account. beancount.ops.summarize.balance_by_account(entries, date=None) \uf0c1 Sum up the balance per account for all entries strictly before 'date'. Parameters: entries \u2013 A list of directives. date \u2013 An optional datetime.date instance. If provided, stop accumulating on and after this date. This is useful for summarization before a specific date. Returns: A pair of a dict of account string to instance Inventory (the balance of this account before the given date), and the index in the list of entries where the date was encountered. If all entries are located before the cutoff date, an index one beyond the last entry is returned. Source code in beancount/ops/summarize.py def balance_by_account(entries, date=None): \"\"\"Sum up the balance per account for all entries strictly before 'date'. Args: entries: A list of directives. date: An optional datetime.date instance. If provided, stop accumulating on and after this date. This is useful for summarization before a specific date. Returns: A pair of a dict of account string to instance Inventory (the balance of this account before the given date), and the index in the list of entries where the date was encountered. If all entries are located before the cutoff date, an index one beyond the last entry is returned. \"\"\" balances = collections.defaultdict(inventory.Inventory) for index, entry in enumerate(entries): if date and entry.date >= date: break if isinstance(entry, Transaction): for posting in entry.postings: account_balance = balances[posting.account] # Note: We must allow negative lots at cost, because this may be # used to reduce a filtered list of entries which may not # include the entries necessary to keep units at cost always # above zero. The only summation that is guaranteed to be above # zero is if all the entries are being summed together, no # entries are filtered, at least for a particular account's # postings. account_balance.add_position(posting) else: index = len(entries) return balances, index beancount.ops.summarize.cap(entries, account_types, conversion_currency, account_earnings, account_conversions) \uf0c1 Transfer net income to equity and insert a final conversion entry. This is used to move and nullify balances from the income and expense accounts to an equity account in order to draw up a balance sheet with a balance of precisely zero. Parameters: entries \u2013 A list of directives. account_types \u2013 An instance of AccountTypes. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_earnings \u2013 A string, the name of the equity account to transfer final balances of the income and expense accounts to. account_conversions \u2013 A string, the name of the equity account to use as the source for currency conversions. Returns: A modified list of entries, with the income and expense accounts transferred. Source code in beancount/ops/summarize.py def cap(entries, account_types, conversion_currency, account_earnings, account_conversions): \"\"\"Transfer net income to equity and insert a final conversion entry. This is used to move and nullify balances from the income and expense accounts to an equity account in order to draw up a balance sheet with a balance of precisely zero. Args: entries: A list of directives. account_types: An instance of AccountTypes. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_earnings: A string, the name of the equity account to transfer final balances of the income and expense accounts to. account_conversions: A string, the name of the equity account to use as the source for currency conversions. Returns: A modified list of entries, with the income and expense accounts transferred. \"\"\" # Transfer the balances of income and expense accounts as earnings / net # income. income_statement_account_pred = ( lambda account: is_income_statement_account(account, account_types)) entries = transfer_balances(entries, None, income_statement_account_pred, account_earnings) # Insert final conversion entries. entries = conversions(entries, account_conversions, conversion_currency, None) return entries beancount.ops.summarize.cap_opt(entries, options_map) \uf0c1 Close by getting all the parameters from an options map. See cap() for details. Parameters: entries \u2013 See cap(). options_map \u2013 A parser's option_map. Returns: Same as close(). Source code in beancount/ops/summarize.py def cap_opt(entries, options_map): \"\"\"Close by getting all the parameters from an options map. See cap() for details. Args: entries: See cap(). options_map: A parser's option_map. Returns: Same as close(). \"\"\" account_types = options.get_account_types(options_map) current_accounts = options.get_current_accounts(options_map) conversion_currency = options_map['conversion_currency'] return cap(entries, account_types, conversion_currency, *current_accounts) beancount.ops.summarize.clamp(entries, begin_date, end_date, account_types, conversion_currency, account_earnings, account_opening, account_conversions) \uf0c1 Filter entries to include only those during a specified time period. Firstly, this method will transfer all balances for the income and expense accounts occurring before the given period begin date to the 'account_earnings' account (earnings before the period, or \"retained earnings\") and summarize all of the transactions before that date against the 'account_opening' account (usually \"opening balances\"). The resulting income and expense accounts should have no transactions (since their balances have been transferred out and summarization of zero balances should not add any transactions). Secondly, all the entries after the period end date will be truncated and a conversion entry will be added for the resulting transactions that reflect changes occurring between the beginning and end of the exercise period. The resulting balance of all account should be empty. Parameters: entries \u2013 A list of directive tuples. begin_date \u2013 A datetime.date instance, the beginning of the period. end_date \u2013 A datetime.date instance, one day beyond the end of the period. account_types \u2013 An instance of AccountTypes. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_earnings \u2013 A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening \u2013 A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions \u2013 A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. Source code in beancount/ops/summarize.py def clamp(entries, begin_date, end_date, account_types, conversion_currency, account_earnings, account_opening, account_conversions): \"\"\"Filter entries to include only those during a specified time period. Firstly, this method will transfer all balances for the income and expense accounts occurring before the given period begin date to the 'account_earnings' account (earnings before the period, or \"retained earnings\") and summarize all of the transactions before that date against the 'account_opening' account (usually \"opening balances\"). The resulting income and expense accounts should have no transactions (since their balances have been transferred out and summarization of zero balances should not add any transactions). Secondly, all the entries after the period end date will be truncated and a conversion entry will be added for the resulting transactions that reflect changes occurring between the beginning and end of the exercise period. The resulting balance of all account should be empty. Args: entries: A list of directive tuples. begin_date: A datetime.date instance, the beginning of the period. end_date: A datetime.date instance, one day beyond the end of the period. account_types: An instance of AccountTypes. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_earnings: A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening: A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions: A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. \"\"\" # Transfer income and expenses before the period to equity. income_statement_account_pred = ( lambda account: is_income_statement_account(account, account_types)) entries = transfer_balances(entries, begin_date, income_statement_account_pred, account_earnings) # Summarize all the previous balances, after transferring the income and # expense balances, so all entries for those accounts before the begin date # should now disappear. entries, index = summarize(entries, begin_date, account_opening) # Truncate the entries after this. entries = truncate(entries, end_date) # Insert conversion entries. entries = conversions(entries, account_conversions, conversion_currency, end_date) return entries, index beancount.ops.summarize.clamp_opt(entries, begin_date, end_date, options_map) \uf0c1 Clamp by getting all the parameters from an options map. See clamp() for details. Parameters: entries \u2013 See clamp(). begin_date \u2013 See clamp(). end_date \u2013 See clamp(). options_map \u2013 A parser's option_map. Returns: Same as clamp(). Source code in beancount/ops/summarize.py def clamp_opt(entries, begin_date, end_date, options_map): \"\"\"Clamp by getting all the parameters from an options map. See clamp() for details. Args: entries: See clamp(). begin_date: See clamp(). end_date: See clamp(). options_map: A parser's option_map. Returns: Same as clamp(). \"\"\" account_types = options.get_account_types(options_map) previous_accounts = options.get_previous_accounts(options_map) conversion_currency = options_map['conversion_currency'] return clamp(entries, begin_date, end_date, account_types, conversion_currency, *previous_accounts) beancount.ops.summarize.clear(entries, date, account_types, account_earnings) \uf0c1 Transfer income and expenses balances at the given date to the equity accounts. This method insert entries to zero out balances on income and expenses accounts by transferring them to an equity account. Parameters: entries \u2013 A list of directive tuples. date \u2013 A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. account_types \u2013 An instance of AccountTypes. account_earnings \u2013 A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. Returns: A new list of entries is returned, and the index that points to one before the last original transaction before the transfers. Source code in beancount/ops/summarize.py def clear(entries, date, account_types, account_earnings): \"\"\"Transfer income and expenses balances at the given date to the equity accounts. This method insert entries to zero out balances on income and expenses accounts by transferring them to an equity account. Args: entries: A list of directive tuples. date: A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. account_types: An instance of AccountTypes. account_earnings: A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. Returns: A new list of entries is returned, and the index that points to one before the last original transaction before the transfers. \"\"\" index = len(entries) # Transfer income and expenses before the period to equity. income_statement_account_pred = ( lambda account: is_income_statement_account(account, account_types)) new_entries = transfer_balances(entries, date, income_statement_account_pred, account_earnings) return new_entries, index beancount.ops.summarize.clear_opt(entries, date, options_map) \uf0c1 Convenience function to clear() using an options map. Source code in beancount/ops/summarize.py def clear_opt(entries, date, options_map): \"\"\"Convenience function to clear() using an options map. \"\"\" account_types = options.get_account_types(options_map) current_accounts = options.get_current_accounts(options_map) return clear(entries, date, account_types, current_accounts[0]) beancount.ops.summarize.close(entries, date, conversion_currency, account_conversions) \uf0c1 Truncate entries that occur after a particular date and ensure balance. This method essentially removes entries after a date. It truncates the future. To do so, it will Remove all entries which occur after 'date', if given. Insert conversion transactions at the end of the list of entries to ensure that the total balance of all postings sums up to empty. The result is a list of entries with a total balance of zero, with possibly non-zero balances for the income/expense accounts. To produce a final balance sheet, use transfer() to move the net income to the equity accounts. Parameters: entries \u2013 A list of directive tuples. date \u2013 A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_conversions \u2013 A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to one beyond the last original transaction that was provided. Further entries may have been inserted to normalize conversions and ensure the total balance sums to zero. Source code in beancount/ops/summarize.py def close(entries, date, conversion_currency, account_conversions): \"\"\"Truncate entries that occur after a particular date and ensure balance. This method essentially removes entries after a date. It truncates the future. To do so, it will 1. Remove all entries which occur after 'date', if given. 2. Insert conversion transactions at the end of the list of entries to ensure that the total balance of all postings sums up to empty. The result is a list of entries with a total balance of zero, with possibly non-zero balances for the income/expense accounts. To produce a final balance sheet, use transfer() to move the net income to the equity accounts. Args: entries: A list of directive tuples. date: A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_conversions: A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to one beyond the last original transaction that was provided. Further entries may have been inserted to normalize conversions and ensure the total balance sums to zero. \"\"\" # Truncate the entries after the date, if a date has been provided. if date is not None: entries = truncate(entries, date) # Keep an index to the truncated list of entries (before conversions). index = len(entries) # Insert a conversions entry to ensure the total balance of all accounts is # flush zero. entries = conversions(entries, account_conversions, conversion_currency, date) return entries, index beancount.ops.summarize.close_opt(entries, date, options_map) \uf0c1 Convenience function to close() using an options map. Source code in beancount/ops/summarize.py def close_opt(entries, date, options_map): \"\"\"Convenience function to close() using an options map. \"\"\" conversion_currency = options_map['conversion_currency'] current_accounts = options.get_current_accounts(options_map) return close(entries, date, conversion_currency, current_accounts[1]) beancount.ops.summarize.conversions(entries, conversion_account, conversion_currency, date=None) \uf0c1 Insert a conversion entry at date 'date' at the given account. Parameters: entries \u2013 A list of entries. conversion_account \u2013 A string, the account to book against. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. date \u2013 The date before which to insert the conversion entry. The new entry will be inserted as the last entry of the date just previous to this date. Returns: A modified list of entries. Source code in beancount/ops/summarize.py def conversions(entries, conversion_account, conversion_currency, date=None): \"\"\"Insert a conversion entry at date 'date' at the given account. Args: entries: A list of entries. conversion_account: A string, the account to book against. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. date: The date before which to insert the conversion entry. The new entry will be inserted as the last entry of the date just previous to this date. Returns: A modified list of entries. \"\"\" # Compute the balance at the given date. conversion_balance = interpolate.compute_entries_balance(entries, date=date) # Early exit if there is nothing to do. conversion_cost_balance = conversion_balance.reduce(convert.get_cost) if conversion_cost_balance.is_empty(): return entries # Calculate the index and the date for the new entry. We want to store it as # the last transaction of the day before. if date is not None: index = bisect_key.bisect_left_with_key(entries, date, key=lambda entry: entry.date) last_date = date - datetime.timedelta(days=1) else: index = len(entries) last_date = entries[-1].date meta = data.new_metadata('', -1) narration = 'Conversion for {}'.format(conversion_balance) conversion_entry = Transaction(meta, last_date, flags.FLAG_CONVERSIONS, None, narration, data.EMPTY_SET, data.EMPTY_SET, []) for position in conversion_cost_balance.get_positions(): # Important note: Set the cost to zero here to maintain the balance # invariant. (This is the only single place we cheat on the balance rule # in the entire system and this is necessary; see documentation on # Conversions.) price = amount.Amount(ZERO, conversion_currency) neg_pos = -position conversion_entry.postings.append( data.Posting(conversion_account, neg_pos.units, neg_pos.cost, price, None, None)) # Make a copy of the list of entries and insert the new transaction into it. new_entries = list(entries) new_entries.insert(index, conversion_entry) return new_entries beancount.ops.summarize.create_entries_from_balances(balances, date, source_account, direction, meta, flag, narration_template) \uf0c1 \"Create a list of entries from a dict of balances. This method creates a list of new entries to transfer the amounts in the 'balances' dict to/from another account specified in 'source_account'. The balancing posting is created with the equivalent at cost. In other words, if you attempt to balance 10 HOOL {500 USD}, this will synthesize a posting with this position on one leg, and with 5000 USD on the 'source_account' leg. Parameters: balances \u2013 A dict of account name strings to Inventory instances. date \u2013 A datetime.date object, the date at which to create the transaction. source_account \u2013 A string, the name of the account to pull the balances from. This is the magician's hat to pull the rabbit from. direction \u2013 If 'direction' is True, the new entries transfer TO the balances account from the source account; otherwise the new entries transfer FROM the balances into the source account. meta \u2013 A dict to use as metadata for the transactions. flag \u2013 A string, the flag to use for the transactions. narration_template \u2013 A format string for creating the narration. It is formatted with 'account' and 'date' replacement variables. Returns: A list of newly synthesizes Transaction entries. Source code in beancount/ops/summarize.py def create_entries_from_balances(balances, date, source_account, direction, meta, flag, narration_template): \"\"\"\"Create a list of entries from a dict of balances. This method creates a list of new entries to transfer the amounts in the 'balances' dict to/from another account specified in 'source_account'. The balancing posting is created with the equivalent at cost. In other words, if you attempt to balance 10 HOOL {500 USD}, this will synthesize a posting with this position on one leg, and with 5000 USD on the 'source_account' leg. Args: balances: A dict of account name strings to Inventory instances. date: A datetime.date object, the date at which to create the transaction. source_account: A string, the name of the account to pull the balances from. This is the magician's hat to pull the rabbit from. direction: If 'direction' is True, the new entries transfer TO the balances account from the source account; otherwise the new entries transfer FROM the balances into the source account. meta: A dict to use as metadata for the transactions. flag: A string, the flag to use for the transactions. narration_template: A format string for creating the narration. It is formatted with 'account' and 'date' replacement variables. Returns: A list of newly synthesizes Transaction entries. \"\"\" new_entries = [] for account, account_balance in sorted(balances.items()): # Don't create new entries where there is no balance. if account_balance.is_empty(): continue narration = narration_template.format(account=account, date=date) if not direction: account_balance = -account_balance postings = [] new_entry = Transaction( meta, date, flag, None, narration, data.EMPTY_SET, data.EMPTY_SET, postings) for position in account_balance.get_positions(): postings.append(data.Posting(account, position.units, position.cost, None, None, None)) cost = -convert.get_cost(position) postings.append(data.Posting(source_account, cost, None, None, None, None)) new_entries.append(new_entry) return new_entries beancount.ops.summarize.get_open_entries(entries, date) \uf0c1 Gather the list of active Open entries at date. This returns the list of Open entries that have not been closed at the given date, in the same order they were observed in the document. Parameters: entries \u2013 A list of directives. date \u2013 The date at which to look for an open entry. If not specified, will return the entries still open at the latest date. Returns: A list of Open directives. Source code in beancount/ops/summarize.py def get_open_entries(entries, date): \"\"\"Gather the list of active Open entries at date. This returns the list of Open entries that have not been closed at the given date, in the same order they were observed in the document. Args: entries: A list of directives. date: The date at which to look for an open entry. If not specified, will return the entries still open at the latest date. Returns: A list of Open directives. \"\"\" open_entries = {} for index, entry in enumerate(entries): if date is not None and entry.date >= date: break if isinstance(entry, Open): try: ex_index, ex_entry = open_entries[entry.account] if entry.date < ex_entry.date: open_entries[entry.account] = (index, entry) except KeyError: open_entries[entry.account] = (index, entry) elif isinstance(entry, Close): # If there is no corresponding open, don't raise an error. open_entries.pop(entry.account, None) return [entry for (index, entry) in sorted(open_entries.values())] beancount.ops.summarize.open(entries, date, account_types, conversion_currency, account_earnings, account_opening, account_conversions) \uf0c1 Summarize entries before a date and transfer income/expenses to equity. This method essentially prepares a list of directives to contain only transactions that occur after a particular date. It truncates the past. To do so, it will Insert conversion transactions at the given open date, then Insert transactions at that date to move accumulated balances from before that date from the income and expenses accounts to an equity account, and finally It removes all the transactions previous to the date and replaces them by opening balances entries to bring the balances to the same amount. The result is a list of entries for which the income and expense accounts are beginning with a balance of zero, and all other accounts begin with a transaction that brings their balance to the expected amount. All the past has been summarized at that point. An index is returned to the first transaction past the balance opening transactions, so you can keep just those in order to render a balance sheet for only the opening balances. Parameters: entries \u2013 A list of directive tuples. date \u2013 A datetime.date instance, the date at which to do this. account_types \u2013 An instance of AccountTypes. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_earnings \u2013 A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening \u2013 A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions \u2013 A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. Source code in beancount/ops/summarize.py def open(entries, date, account_types, conversion_currency, account_earnings, account_opening, account_conversions): \"\"\"Summarize entries before a date and transfer income/expenses to equity. This method essentially prepares a list of directives to contain only transactions that occur after a particular date. It truncates the past. To do so, it will 1. Insert conversion transactions at the given open date, then 2. Insert transactions at that date to move accumulated balances from before that date from the income and expenses accounts to an equity account, and finally 3. It removes all the transactions previous to the date and replaces them by opening balances entries to bring the balances to the same amount. The result is a list of entries for which the income and expense accounts are beginning with a balance of zero, and all other accounts begin with a transaction that brings their balance to the expected amount. All the past has been summarized at that point. An index is returned to the first transaction past the balance opening transactions, so you can keep just those in order to render a balance sheet for only the opening balances. Args: entries: A list of directive tuples. date: A datetime.date instance, the date at which to do this. account_types: An instance of AccountTypes. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_earnings: A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening: A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions: A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. \"\"\" # Insert conversion entries. entries = conversions(entries, account_conversions, conversion_currency, date) # Transfer income and expenses before the period to equity. entries, _ = clear(entries, date, account_types, account_earnings) # Summarize all the previous balances, after transferring the income and # expense balances, so all entries for those accounts before the begin date # should now disappear. entries, index = summarize(entries, date, account_opening) return entries, index beancount.ops.summarize.open_opt(entries, date, options_map) \uf0c1 Convenience function to open() using an options map. Source code in beancount/ops/summarize.py def open_opt(entries, date, options_map): \"\"\"Convenience function to open() using an options map. \"\"\" account_types = options.get_account_types(options_map) previous_accounts = options.get_previous_accounts(options_map) conversion_currency = options_map['conversion_currency'] return open(entries, date, account_types, conversion_currency, *previous_accounts) beancount.ops.summarize.summarize(entries, date, account_opening) \uf0c1 Summarize all entries before a date by replacing then with summarization entries. This function replaces the transactions up to (and not including) the given date with a opening balance transactions, one for each account. It returns new entries, all of the transactions before the given date having been replaced by a few summarization entries, one for each account. Notes: - Open entries are preserved for active accounts. - The last relevant price entry for each (base, quote) pair is preserved. - All other entries before the cutoff date are culled. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance, the cutoff date before which to summarize. account_opening \u2013 A string, the name of the source account to book summarization entries against. Returns: The function returns a list of new entries and the integer index at which the entries on or after the cutoff date begin. Source code in beancount/ops/summarize.py def summarize(entries, date, account_opening): \"\"\"Summarize all entries before a date by replacing then with summarization entries. This function replaces the transactions up to (and not including) the given date with a opening balance transactions, one for each account. It returns new entries, all of the transactions before the given date having been replaced by a few summarization entries, one for each account. Notes: - Open entries are preserved for active accounts. - The last relevant price entry for each (base, quote) pair is preserved. - All other entries before the cutoff date are culled. Args: entries: A list of directives. date: A datetime.date instance, the cutoff date before which to summarize. account_opening: A string, the name of the source account to book summarization entries against. Returns: The function returns a list of new entries and the integer index at which the entries on or after the cutoff date begin. \"\"\" # Compute balances at date. balances, index = balance_by_account(entries, date) # We need to insert the entries with a date previous to subsequent checks, # to maintain ensure the open directives show up before any transaction. summarize_date = date - datetime.timedelta(days=1) # Create summarization / opening balance entries. summarizing_entries = create_entries_from_balances( balances, summarize_date, account_opening, True, data.new_metadata('', 0), flags.FLAG_SUMMARIZE, \"Opening balance for '{account}' (Summarization)\") # Insert the last price entry for each commodity from before the date. price_entries = prices.get_last_price_entries(entries, date) # Gather the list of active open entries at date. open_entries = get_open_entries(entries, date) # Compute entries before the date and preserve the entries after the date. before_entries = sorted(open_entries + price_entries + summarizing_entries, key=data.entry_sortkey) after_entries = entries[index:] # Return a new list of entries and the index that points after the entries # were inserted. return (before_entries + after_entries), len(before_entries) beancount.ops.summarize.transfer_balances(entries, date, account_pred, transfer_account) \uf0c1 Synthesize transactions to transfer balances from some accounts at a given date. For all accounts that match the 'account_pred' predicate, create new entries to transfer the balance at the given date from the account to the transfer account. This is used to transfer balances from income and expenses from a previous period to a \"retained earnings\" account. This is accomplished by creating new entries. Note that inserting transfers breaks any following balance checks that are in the transferred accounts. For this reason, all balance assertion entries following the cutoff date for those accounts are removed from the list in output. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance, the date at which to make the transfer. account_pred \u2013 A predicate function that, given an account string, returns true if the account is meant to be transferred. transfer_account \u2013 A string, the name of the source account to be used on the transfer entries to receive balances at the given date. Returns: A new list of entries, with the new transfer entries added in. Source code in beancount/ops/summarize.py def transfer_balances(entries, date, account_pred, transfer_account): \"\"\"Synthesize transactions to transfer balances from some accounts at a given date. For all accounts that match the 'account_pred' predicate, create new entries to transfer the balance at the given date from the account to the transfer account. This is used to transfer balances from income and expenses from a previous period to a \"retained earnings\" account. This is accomplished by creating new entries. Note that inserting transfers breaks any following balance checks that are in the transferred accounts. For this reason, all balance assertion entries following the cutoff date for those accounts are removed from the list in output. Args: entries: A list of directives. date: A datetime.date instance, the date at which to make the transfer. account_pred: A predicate function that, given an account string, returns true if the account is meant to be transferred. transfer_account: A string, the name of the source account to be used on the transfer entries to receive balances at the given date. Returns: A new list of entries, with the new transfer entries added in. \"\"\" # Don't bother doing anything if there are no entries. if not entries: return entries # Compute balances at date. balances, index = balance_by_account(entries, date) # Filter out to keep only the accounts we want. transfer_balances = {account: balance for account, balance in balances.items() if account_pred(account)} # We need to insert the entries at the end of the previous day. if date: transfer_date = date - datetime.timedelta(days=1) else: transfer_date = entries[-1].date # Create transfer entries. transfer_entries = create_entries_from_balances( transfer_balances, transfer_date, transfer_account, False, data.new_metadata('', 0), flags.FLAG_TRANSFER, \"Transfer balance for '{account}' (Transfer balance)\") # Remove balance assertions that occur after a transfer on an account that # has been transferred away; they would break. after_entries = [entry for entry in entries[index:] if not (isinstance(entry, balance.Balance) and entry.account in transfer_balances)] # Split the new entries in a new list. return (entries[:index] + transfer_entries + after_entries) beancount.ops.summarize.truncate(entries, date) \uf0c1 Filter out all the entries at and after date. Returns a new list of entries. Parameters: entries \u2013 A sorted list of directives. date \u2013 A datetime.date instance. Returns: A truncated list of directives. Source code in beancount/ops/summarize.py def truncate(entries, date): \"\"\"Filter out all the entries at and after date. Returns a new list of entries. Args: entries: A sorted list of directives. date: A datetime.date instance. Returns: A truncated list of directives. \"\"\" index = bisect_key.bisect_left_with_key(entries, date, key=lambda entry: entry.date) return entries[:index] beancount.ops.validation \uf0c1 Validation checks. These checks are intended to be run after all the plugins have transformed the list of entries, just before serving them or generating reports from them. The idea is to ensure a reasonable set of invariants and generate errors if those invariants are violated. They are not sanity checks--user data is subject to constraints which are hopefully detected here and which will result in errors trickled up to the user. beancount.ops.validation.ValidationError ( tuple ) \uf0c1 ValidationError(source, message, entry) beancount.ops.validation.ValidationError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/validation.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.ops.validation.ValidationError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of ValidationError(source, message, entry) beancount.ops.validation.ValidationError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/ops/validation.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.ops.validation.validate(entries, options_map, log_timings=None, extra_validations=None) \uf0c1 Perform all the standard checks on parsed contents. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. log_timings \u2013 An optional function to use for logging the time of individual operations. extra_validations \u2013 A list of extra validation functions to run after loading this list of entries. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate(entries, options_map, log_timings=None, extra_validations=None): \"\"\"Perform all the standard checks on parsed contents. Args: entries: A list of directives. unused_options_map: An options map. log_timings: An optional function to use for logging the time of individual operations. extra_validations: A list of extra validation functions to run after loading this list of entries. Returns: A list of new errors, if any were found. \"\"\" validation_tests = VALIDATIONS if extra_validations: validation_tests += extra_validations # Run various validation routines define above. errors = [] for validation_function in validation_tests: with misc_utils.log_time('function: {}'.format(validation_function.__name__), log_timings, indent=2): new_errors = validation_function(entries, options_map) errors.extend(new_errors) return errors beancount.ops.validation.validate_active_accounts(entries, unused_options_map) \uf0c1 Check that all references to accounts occurs on active accounts. We basically check that references to accounts from all directives other than Open and Close occur at dates the open-close interval of that account. This should be good for all of the directive types where we can extract an account name. Note that this is more strict a check than comparing the dates: we actually check that no references to account are made on the same day before the open directive appears for that account. This is a nice property to have, and is supported by our custom sorting routine that will sort open entries before transaction entries, given the same date. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_active_accounts(entries, unused_options_map): \"\"\"Check that all references to accounts occurs on active accounts. We basically check that references to accounts from all directives other than Open and Close occur at dates the open-close interval of that account. This should be good for all of the directive types where we can extract an account name. Note that this is more strict a check than comparing the dates: we actually check that no references to account are made on the same day before the open directive appears for that account. This is a nice property to have, and is supported by our custom sorting routine that will sort open entries before transaction entries, given the same date. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" error_pairs = [] active_set = set() opened_accounts = set() for entry in entries: if isinstance(entry, data.Open): active_set.add(entry.account) opened_accounts.add(entry.account) elif isinstance(entry, data.Close): active_set.discard(entry.account) else: for account in getters.get_entry_accounts(entry): if account not in active_set: # Allow document and note directives that occur after an # account is closed. if (isinstance(entry, ALLOW_AFTER_CLOSE) and account in opened_accounts): continue # Register an error to be logged later, with an appropriate # message. error_pairs.append((account, entry)) # Refine the error message to disambiguate between the case of an account # that has never been seen and one that was simply not active at the time. errors = [] for account, entry in error_pairs: if account in opened_accounts: message = \"Invalid reference to inactive account '{}'\".format(account) else: message = \"Invalid reference to unknown account '{}'\".format(account) errors.append(ValidationError(entry.meta, message, entry)) return errors beancount.ops.validation.validate_check_transaction_balances(entries, options_map) \uf0c1 Check again that all transaction postings balance, as users may have transformed transactions. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_check_transaction_balances(entries, options_map): \"\"\"Check again that all transaction postings balance, as users may have transformed transactions. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" # Note: this is a bit slow; we could limit our checks to the original # transactions by using the hash function in the loader. errors = [] for entry in entries: if isinstance(entry, Transaction): # IMPORTANT: This validation is _crucial_ and cannot be skipped. # This is where we actually detect and warn on unbalancing # transactions. This _must_ come after the user routines, because # unbalancing input is legal, as those types of transactions may be # \"fixed up\" by a user-plugin. In other words, we want to allow # users to input unbalancing transactions as long as the final # transactions objects that appear on the stream (after processing # the plugins) are balanced. See {9e6c14b51a59}. # # Detect complete sets of postings that have residual balance; residual = interpolate.compute_residual(entry.postings) tolerances = interpolate.infer_tolerances(entry.postings, options_map) if not residual.is_small(tolerances): errors.append( ValidationError(entry.meta, \"Transaction does not balance: {}\".format(residual), entry)) return errors beancount.ops.validation.validate_currency_constraints(entries, options_map) \uf0c1 Check the currency constraints from account open declarations. Open directives admit an optional list of currencies that specify the only types of commodities that the running inventory for this account may contain. This function checks that all postings are only made in those commodities. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_currency_constraints(entries, options_map): \"\"\"Check the currency constraints from account open declarations. Open directives admit an optional list of currencies that specify the only types of commodities that the running inventory for this account may contain. This function checks that all postings are only made in those commodities. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" # Get all the open entries with currency constraints. open_map = {entry.account: entry for entry in entries if isinstance(entry, Open) and entry.currencies} errors = [] for entry in entries: if not isinstance(entry, Transaction): continue for posting in entry.postings: # Look up the corresponding account's valid currencies; skip the # check if there are none specified. try: open_entry = open_map[posting.account] valid_currencies = open_entry.currencies if not valid_currencies: continue except KeyError: continue # Perform the check. if posting.units.currency not in valid_currencies: errors.append( ValidationError( entry.meta, \"Invalid currency {} for account '{}'\".format( posting.units.currency, posting.account), entry)) return errors beancount.ops.validation.validate_data_types(entries, options_map) \uf0c1 Check that all the data types of the attributes of entries are as expected. Users are provided with a means to filter the list of entries. They're able to write code that manipulates those tuple objects without any type constraints. With discipline, this mostly works, but I know better: check, just to make sure. This routine checks all the data types and assumptions on entries. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_data_types(entries, options_map): \"\"\"Check that all the data types of the attributes of entries are as expected. Users are provided with a means to filter the list of entries. They're able to write code that manipulates those tuple objects without any type constraints. With discipline, this mostly works, but I know better: check, just to make sure. This routine checks all the data types and assumptions on entries. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] for entry in entries: try: data.sanity_check_types( entry, options_map[\"allow_deprecated_none_for_tags_and_links\"]) except AssertionError as exc: errors.append( ValidationError(entry.meta, \"Invalid data types: {}\".format(exc), entry)) return errors beancount.ops.validation.validate_documents_paths(entries, options_map) \uf0c1 Check that all filenames in resolved Document entries are absolute filenames. The processing of document entries is assumed to result in absolute paths. Relative paths are resolved at the parsing stage and at point we want to make sure we don't have to do any further processing on them. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_documents_paths(entries, options_map): \"\"\"Check that all filenames in resolved Document entries are absolute filenames. The processing of document entries is assumed to result in absolute paths. Relative paths are resolved at the parsing stage and at point we want to make sure we don't have to do any further processing on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" return [ValidationError(entry.meta, \"Invalid relative path for entry\", entry) for entry in entries if (isinstance(entry, Document) and not path.isabs(entry.filename))] beancount.ops.validation.validate_duplicate_balances(entries, unused_options_map) \uf0c1 Check that balance entries occur only once per day. Because we do not support time, and the declaration order of entries is meant to be kept irrelevant, two balance entries with different amounts should not occur in the file. We do allow two identical balance assertions, however, because this may occur during import. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_duplicate_balances(entries, unused_options_map): \"\"\"Check that balance entries occur only once per day. Because we do not support time, and the declaration order of entries is meant to be kept irrelevant, two balance entries with different amounts should not occur in the file. We do allow two identical balance assertions, however, because this may occur during import. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] # Mapping of (account, currency, date) to Balance entry. balance_entries = {} for entry in entries: if not isinstance(entry, data.Balance): continue key = (entry.account, entry.amount.currency, entry.date) try: previous_entry = balance_entries[key] if entry.amount != previous_entry.amount: errors.append( ValidationError( entry.meta, \"Duplicate balance assertion with different amounts\", entry)) except KeyError: balance_entries[key] = entry return errors beancount.ops.validation.validate_duplicate_commodities(entries, unused_options_map) \uf0c1 Check that commodity entries are unique for each commodity. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_duplicate_commodities(entries, unused_options_map): \"\"\"Check that commodity entries are unique for each commodity. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] # Mapping of (account, currency, date) to Balance entry. commodity_entries = {} for entry in entries: if not isinstance(entry, data.Commodity): continue key = entry.currency try: previous_entry = commodity_entries[key] if previous_entry: errors.append( ValidationError( entry.meta, \"Duplicate commodity directives for '{}'\".format(key), entry)) except KeyError: commodity_entries[key] = entry return errors beancount.ops.validation.validate_open_close(entries, unused_options_map) \uf0c1 Check constraints on open and close directives themselves. This method checks two kinds of constraints: An open or a close directive may only show up once for each account. If a duplicate is detected, an error is generated. Close directives may only appears if an open directive has been seen previous (chronologically). The date of close directives must be strictly greater than their corresponding open directive. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_open_close(entries, unused_options_map): \"\"\"Check constraints on open and close directives themselves. This method checks two kinds of constraints: 1. An open or a close directive may only show up once for each account. If a duplicate is detected, an error is generated. 2. Close directives may only appears if an open directive has been seen previous (chronologically). 3. The date of close directives must be strictly greater than their corresponding open directive. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] open_map = {} close_map = {} for entry in entries: if isinstance(entry, Open): if entry.account in open_map: errors.append( ValidationError( entry.meta, \"Duplicate open directive for {}\".format(entry.account), entry)) else: open_map[entry.account] = entry elif isinstance(entry, Close): if entry.account in close_map: errors.append( ValidationError( entry.meta, \"Duplicate close directive for {}\".format(entry.account), entry)) else: try: open_entry = open_map[entry.account] if entry.date <= open_entry.date: errors.append( ValidationError( entry.meta, \"Internal error: closing date for {} \" \"appears before opening date\".format(entry.account), entry)) except KeyError: errors.append( ValidationError( entry.meta, \"Unopened account {} is being closed\".format(entry.account), entry)) close_map[entry.account] = entry return errors","title":"beancount.ops"},{"location":"api_reference/beancount.ops.html#beancountops","text":"Operations on the entries defined in the core modules. This package contains various functions which operate on lists of entries.","title":"beancount.ops"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance","text":"Automatic padding of gaps between entries.","title":"balance"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance.BalanceError","text":"BalanceError(source, message, entry)","title":"BalanceError"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance.BalanceError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/balance.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance.BalanceError.__new__","text":"Create new instance of BalanceError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance.BalanceError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/ops/balance.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance.check","text":"Process the balance assertion directives. For each Balance directive, check that their expected balance corresponds to the actual balance computed at that time and replace failing ones by new ones with a flag that indicates failure. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, parsed from the input file. Returns: A pair of a list of directives and a list of balance check errors. Source code in beancount/ops/balance.py def check(entries, options_map): \"\"\"Process the balance assertion directives. For each Balance directive, check that their expected balance corresponds to the actual balance computed at that time and replace failing ones by new ones with a flag that indicates failure. Args: entries: A list of directives. options_map: A dict of options, parsed from the input file. Returns: A pair of a list of directives and a list of balance check errors. \"\"\" new_entries = [] check_errors = [] # This is similar to realization, but performed in a different order, and # where we only accumulate inventories for accounts that have balance # assertions in them (this saves on time). Here we process the entries one # by one along with the balance checks. We use a temporary realization in # order to hold the incremental tree of balances, so that we can easily get # the amounts of an account's subaccounts for making checks on parent # accounts. real_root = realization.RealAccount('') # Figure out the set of accounts for which we need to compute a running # inventory balance. asserted_accounts = {entry.account for entry in entries if isinstance(entry, Balance)} # Add all children accounts of an asserted account to be calculated as well, # and pre-create these accounts, and only those (we're just being tight to # make sure). asserted_match_list = [account.parent_matcher(account_) for account_ in asserted_accounts] for account_ in getters.get_accounts(entries): if (account_ in asserted_accounts or any(match(account_) for match in asserted_match_list)): realization.get_or_create(real_root, account_) # Get the Open directives for each account. open_close_map = getters.get_account_open_close(entries) for entry in entries: if isinstance(entry, Transaction): # For each of the postings' accounts, update the balance inventory. for posting in entry.postings: real_account = realization.get(real_root, posting.account) # The account will have been created only if we're meant to track it. if real_account is not None: # Note: Always allow negative lots for the purpose of balancing. # This error should show up somewhere else than here. real_account.balance.add_position(posting) elif isinstance(entry, Balance): # Check that the currency of the balance check is one of the allowed # currencies for that account. expected_amount = entry.amount try: open, _ = open_close_map[entry.account] except KeyError: check_errors.append( BalanceError(entry.meta, \"Account '{}' does not exist: \".format(entry.account), entry)) continue if (expected_amount is not None and open and open.currencies and expected_amount.currency not in open.currencies): check_errors.append( BalanceError(entry.meta, \"Invalid currency '{}' for Balance directive: \".format( expected_amount.currency), entry)) # Sum up the current balances for this account and its # sub-accounts. We want to support checks for parent accounts # for the total sum of their subaccounts. # # FIXME: Improve the performance further by computing the balance # for the desired currency only. This won't allow us to cache in # this way but may be faster, if we're not asserting all the # currencies. Furthermore, we could probably avoid recomputing the # balance if a subtree of positions hasn't been invalidated by a new # position added to the realization. Do this. real_account = realization.get(real_root, entry.account) assert real_account is not None, \"Missing {}\".format(entry.account) subtree_balance = realization.compute_balance(real_account, leaf_only=False) # Get only the amount in the desired currency. balance_amount = subtree_balance.get_currency_units(expected_amount.currency) # Check if the amount is within bounds of the expected amount. diff_amount = amount.sub(balance_amount, expected_amount) # Use the specified tolerance or automatically infer it. tolerance = get_balance_tolerance(entry, options_map) if abs(diff_amount.number) > tolerance: check_errors.append( BalanceError(entry.meta, (\"Balance failed for '{}': \" \"expected {} != accumulated {} ({} {})\").format( entry.account, expected_amount, balance_amount, abs(diff_amount.number), ('too much' if diff_amount.number > 0 else 'too little')), entry)) # Substitute the entry by a failing entry, with the diff_amount # field set on it. I'm not entirely sure that this is the best # of ideas, maybe leaving the original check intact and insert a # new error entry might be more functional or easier to # understand. entry = entry._replace( meta=entry.meta.copy(), diff_amount=diff_amount) new_entries.append(entry) return new_entries, check_errors","title":"check()"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance.get_balance_tolerance","text":"Get the tolerance amount for a single entry. Parameters: balance_entry \u2013 An instance of data.Balance options_map \u2013 An options dict, as per the parser. Returns: A Decimal, the amount of tolerance implied by the directive. Source code in beancount/ops/balance.py def get_balance_tolerance(balance_entry, options_map): \"\"\"Get the tolerance amount for a single entry. Args: balance_entry: An instance of data.Balance options_map: An options dict, as per the parser. Returns: A Decimal, the amount of tolerance implied by the directive. \"\"\" if balance_entry.tolerance is not None: # Use the balance-specific tolerance override if it is provided. tolerance = balance_entry.tolerance else: expo = balance_entry.amount.number.as_tuple().exponent if expo < 0: # Be generous and always allow twice the multiplier on Balance and # Pad because the user creates these and the rounding of those # balances may often be further off than those used within a single # transaction. tolerance = options_map[\"inferred_tolerance_multiplier\"] * 2 tolerance = ONE.scaleb(expo) * tolerance else: tolerance = ZERO return tolerance","title":"get_balance_tolerance()"},{"location":"api_reference/beancount.ops.html#beancount.ops.basicops","text":"Basic filtering and aggregation operations on lists of entries. This module contains some common basic operations on entries that are complex enough not to belong in core/data.py.","title":"basicops"},{"location":"api_reference/beancount.ops.html#beancount.ops.basicops.filter_link","text":"Yield all the entries which have the given link. Parameters: link \u2013 A string, the link we are interested in. Yields: Every entry in 'entries' that links to 'link. Source code in beancount/ops/basicops.py def filter_link(link, entries): \"\"\"Yield all the entries which have the given link. Args: link: A string, the link we are interested in. Yields: Every entry in 'entries' that links to 'link. \"\"\" for entry in entries: # pylint: disable=bad-continuation if (isinstance(entry, data.Transaction) and entry.links and link in entry.links): yield entry","title":"filter_link()"},{"location":"api_reference/beancount.ops.html#beancount.ops.basicops.filter_tag","text":"Yield all the entries which have the given tag. Parameters: tag \u2013 A string, the tag we are interested in. Yields: Every entry in 'entries' that tags to 'tag. Source code in beancount/ops/basicops.py def filter_tag(tag, entries): \"\"\"Yield all the entries which have the given tag. Args: tag: A string, the tag we are interested in. Yields: Every entry in 'entries' that tags to 'tag. \"\"\" for entry in entries: # pylint: disable=bad-continuation if (isinstance(entry, data.Transaction) and entry.tags and tag in entry.tags): yield entry","title":"filter_tag()"},{"location":"api_reference/beancount.ops.html#beancount.ops.basicops.get_common_accounts","text":"Compute the intersection of the accounts on the given entries. Parameters: entries \u2013 A list of Transaction entries to process. Returns: A set of strings, the names of the common accounts from these entries. Source code in beancount/ops/basicops.py def get_common_accounts(entries): \"\"\"Compute the intersection of the accounts on the given entries. Args: entries: A list of Transaction entries to process. Returns: A set of strings, the names of the common accounts from these entries. \"\"\" assert all(isinstance(entry, data.Transaction) for entry in entries) # If there is a single entry, the common accounts to it is all its accounts. # Note that this also works with no entries (yields an empty set). if len(entries) < 2: if entries: intersection = {posting.account for posting in entries[0].postings} else: intersection = set() else: entries_iter = iter(entries) intersection = set(posting.account for posting in next(entries_iter).postings) for entry in entries_iter: accounts = set(posting.account for posting in entry.postings) intersection &= accounts if not intersection: break return intersection","title":"get_common_accounts()"},{"location":"api_reference/beancount.ops.html#beancount.ops.basicops.group_entries_by_link","text":"Group the list of entries by link. Parameters: entries \u2013 A list of directives/transactions to process. Returns: A dict of link-name to list of entries. Source code in beancount/ops/basicops.py def group_entries_by_link(entries): \"\"\"Group the list of entries by link. Args: entries: A list of directives/transactions to process. Returns: A dict of link-name to list of entries. \"\"\" link_groups = defaultdict(list) for entry in entries: if not (isinstance(entry, data.Transaction) and entry.links): continue for link in entry.links: link_groups[link].append(entry) return link_groups","title":"group_entries_by_link()"},{"location":"api_reference/beancount.ops.html#beancount.ops.compress","text":"Compress multiple entries into a single one. This can be used during import to compress the effective output, for accounts with a large number of similar entries. For example, I had a trading account which would pay out interest every single day. I have no desire to import the full detail of these daily interests, and compressing these interest-only entries to monthly ones made sense. This is the code that was used to carry this out.","title":"compress"},{"location":"api_reference/beancount.ops.html#beancount.ops.compress.compress","text":"Compress multiple transactions into single transactions. Replace consecutive sequences of Transaction entries that fulfill the given predicate by a single entry at the date of the last matching entry. 'predicate' is the function that determines if an entry should be compressed. This can be used to simply a list of transactions that are similar and occur frequently. As an example, in a retail FOREX trading account, differential interest of very small amounts is paid every day; it is not relevant to look at the full detail of this interest unless there are other transactions. You can use this to compress it into single entries between other types of transactions. Parameters: entries \u2013 A list of directives. predicate \u2013 A callable which accepts an entry and return true if the entry is intended to be compressed. Returns: A list of directives, with compressible transactions replaced by a summary equivalent. Source code in beancount/ops/compress.py def compress(entries, predicate): \"\"\"Compress multiple transactions into single transactions. Replace consecutive sequences of Transaction entries that fulfill the given predicate by a single entry at the date of the last matching entry. 'predicate' is the function that determines if an entry should be compressed. This can be used to simply a list of transactions that are similar and occur frequently. As an example, in a retail FOREX trading account, differential interest of very small amounts is paid every day; it is not relevant to look at the full detail of this interest unless there are other transactions. You can use this to compress it into single entries between other types of transactions. Args: entries: A list of directives. predicate: A callable which accepts an entry and return true if the entry is intended to be compressed. Returns: A list of directives, with compressible transactions replaced by a summary equivalent. \"\"\" new_entries = [] pending = [] for entry in entries: if isinstance(entry, data.Transaction) and predicate(entry): # Save for compressing later. pending.append(entry) else: # Compress and output all the pending entries. if pending: new_entries.append(merge(pending, pending[-1])) pending.clear() # Output the differing entry. new_entries.append(entry) if pending: new_entries.append(merge(pending, pending[-1])) return new_entries","title":"compress()"},{"location":"api_reference/beancount.ops.html#beancount.ops.compress.merge","text":"Merge the postings of a list of Transactions into a single one. Merge postings the given entries into a single entry with the Transaction attributes of the prototype. Return the new entry. The combined list of postings are merged if everything about the postings is the same except the number. Parameters: entries \u2013 A list of directives. prototype_txn \u2013 A Transaction which is used to create the compressed Transaction instance. Its list of postings is ignored. Returns: A new Transaction instance which contains all the postings from the input entries merged together. Source code in beancount/ops/compress.py def merge(entries, prototype_txn): \"\"\"Merge the postings of a list of Transactions into a single one. Merge postings the given entries into a single entry with the Transaction attributes of the prototype. Return the new entry. The combined list of postings are merged if everything about the postings is the same except the number. Args: entries: A list of directives. prototype_txn: A Transaction which is used to create the compressed Transaction instance. Its list of postings is ignored. Returns: A new Transaction instance which contains all the postings from the input entries merged together. \"\"\" # Aggregate the postings together. This is a mapping of numberless postings # to their number of units. postings_map = collections.defaultdict(Decimal) for entry in data.filter_txns(entries): for posting in entry.postings: # We strip the number off the posting to act as an aggregation key. key = data.Posting(posting.account, Amount(None, posting.units.currency), posting.cost, posting.price, posting.flag, None) postings_map[key] += posting.units.number # Create a new transaction with the aggregated postings. new_entry = data.Transaction(prototype_txn.meta, prototype_txn.date, prototype_txn.flag, prototype_txn.payee, prototype_txn.narration, data.EMPTY_SET, data.EMPTY_SET, []) # Sort for at least some stability of output. sorted_items = sorted(postings_map.items(), key=lambda item: (item[0].account, item[0].units.currency, item[1])) # Issue the merged postings. for posting, number in sorted_items: units = Amount(number, posting.units.currency) new_entry.postings.append( data.Posting(posting.account, units, posting.cost, posting.price, posting.flag, posting.meta)) return new_entry","title":"merge()"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents","text":"Everything that relates to creating the Document directives.","title":"documents"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.DocumentError","text":"DocumentError(source, message, entry)","title":"DocumentError"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.DocumentError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/documents.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.DocumentError.__new__","text":"Create new instance of DocumentError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.DocumentError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/ops/documents.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.find_documents","text":"Find dated document files under the given directory. If a restricting set of accounts is provided in 'accounts_only', only return entries that correspond to one of the given accounts. Parameters: directory \u2013 A string, the name of the root of the directory hierarchy to be searched. input_filename \u2013 The name of the file to be used for the Document directives. This is also used to resolve relative directory names. accounts_only \u2013 A set of valid accounts strings to search for. strict \u2013 A boolean, set to true if you want to generate errors on documents found in accounts not provided in accounts_only. This is only meaningful if accounts_only is specified. Returns: A list of new Document objects that were created from the files found, and a list of new errors generated. Source code in beancount/ops/documents.py def find_documents(directory, input_filename, accounts_only=None, strict=False): \"\"\"Find dated document files under the given directory. If a restricting set of accounts is provided in 'accounts_only', only return entries that correspond to one of the given accounts. Args: directory: A string, the name of the root of the directory hierarchy to be searched. input_filename: The name of the file to be used for the Document directives. This is also used to resolve relative directory names. accounts_only: A set of valid accounts strings to search for. strict: A boolean, set to true if you want to generate errors on documents found in accounts not provided in accounts_only. This is only meaningful if accounts_only is specified. Returns: A list of new Document objects that were created from the files found, and a list of new errors generated. \"\"\" errors = [] # Compute the documents directory name relative to the beancount input # file itself. if not path.isabs(directory): input_directory = path.dirname(input_filename) directory = path.abspath(path.normpath(path.join(input_directory, directory))) # If the directory does not exist, just generate an error and return. if not path.exists(directory): meta = data.new_metadata(input_filename, 0) error = DocumentError( meta, \"Document root '{}' does not exist\".format(directory), None) return ([], [error]) # Walk the hierarchy of files. entries = [] for root, account_name, dirs, files in account.walk(directory): # Look for files that have a dated filename. for filename in files: match = re.match(r'(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d).(.*)', filename) if not match: continue # If a restricting set of accounts was specified, skip document # directives found in accounts with no corresponding account name. if accounts_only is not None and not account_name in accounts_only: if strict: if any(account_name.startswith(account) for account in accounts_only): errors.append(DocumentError( data.new_metadata(input_filename, 0), \"Document '{}' found in child account {}\".format( filename, account_name), None)) elif any(account.startswith(account_name) for account in accounts_only): errors.append(DocumentError( data.new_metadata(input_filename, 0), \"Document '{}' found in parent account {}\".format( filename, account_name), None)) continue # Create a new directive. meta = data.new_metadata(input_filename, 0) try: date = datetime.date(*map(int, match.group(1, 2, 3))) except ValueError as exc: errors.append(DocumentError( data.new_metadata(input_filename, 0), \"Invalid date on document file '{}': {}\".format( filename, exc), None)) else: entry = data.Document(meta, date, account_name, path.join(root, filename), data.EMPTY_SET, data.EMPTY_SET) entries.append(entry) return (entries, errors)","title":"find_documents()"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.process_documents","text":"Check files for document directives and create documents directives automatically. Parameters: entries \u2013 A list of all directives parsed from the file. options_map \u2013 An options dict, as is output by the parser. We're using its 'filename' option to figure out relative path to search for documents. Returns: A pair of list of all entries (including new ones), and errors generated during the process of creating document directives. Source code in beancount/ops/documents.py def process_documents(entries, options_map): \"\"\"Check files for document directives and create documents directives automatically. Args: entries: A list of all directives parsed from the file. options_map: An options dict, as is output by the parser. We're using its 'filename' option to figure out relative path to search for documents. Returns: A pair of list of all entries (including new ones), and errors generated during the process of creating document directives. \"\"\" filename = options_map[\"filename\"] # Detect filenames that should convert into entries. autodoc_entries = [] autodoc_errors = [] document_dirs = options_map['documents'] if document_dirs: # Restrict to the list of valid accounts only. accounts = getters.get_accounts(entries) # Accumulate all the entries. for directory in map(path.normpath, document_dirs): new_entries, new_errors = find_documents(directory, filename, accounts) autodoc_entries.extend(new_entries) autodoc_errors.extend(new_errors) # Merge the two lists of entries and errors. Keep the entries sorted. entries.extend(autodoc_entries) entries.sort(key=data.entry_sortkey) return (entries, autodoc_errors)","title":"process_documents()"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.verify_document_files_exist","text":"Verify that the document entries point to existing files. Parameters: entries \u2013 a list of directives whose documents need to be validated. unused_options_map \u2013 A parser options dict. We're not using it. Returns: The same list of entries, and a list of new errors, if any were encountered. Source code in beancount/ops/documents.py def verify_document_files_exist(entries, unused_options_map): \"\"\"Verify that the document entries point to existing files. Args: entries: a list of directives whose documents need to be validated. unused_options_map: A parser options dict. We're not using it. Returns: The same list of entries, and a list of new errors, if any were encountered. \"\"\" errors = [] for entry in entries: if not isinstance(entry, data.Document): continue if not path.exists(entry.filename): errors.append( DocumentError(entry.meta, 'File does not exist: \"{}\"'.format(entry.filename), entry)) return entries, errors","title":"verify_document_files_exist()"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings","text":"Compute final holdings for a list of entries.","title":"holdings"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.Holding","text":"Holding(account, number, currency, cost_number, cost_currency, book_value, market_value, price_number, price_date)","title":"Holding"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.Holding.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/holdings.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.Holding.__new__","text":"Create new instance of Holding(account, number, currency, cost_number, cost_currency, book_value, market_value, price_number, price_date)","title":"__new__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.Holding.__repr__","text":"Return a nicely formatted representation string Source code in beancount/ops/holdings.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.aggregate_holdings_by","text":"Aggregate holdings by some key. Note that the cost-currency must always be included in the group-key (sums over multiple currency units do not make sense), so it is appended to the sort-key automatically. Parameters: keyfun \u2013 A callable, which returns the key to aggregate by. This key need not include the cost-currency. Returns: A list of aggregated holdings. Source code in beancount/ops/holdings.py def aggregate_holdings_by(holdings, keyfun): \"\"\"Aggregate holdings by some key. Note that the cost-currency must always be included in the group-key (sums over multiple currency units do not make sense), so it is appended to the sort-key automatically. Args: keyfun: A callable, which returns the key to aggregate by. This key need not include the cost-currency. Returns: A list of aggregated holdings. \"\"\" # Aggregate the groups of holdings. grouped = collections.defaultdict(list) for holding in holdings: key = (keyfun(holding), holding.cost_currency) grouped[key].append(holding) grouped_holdings = (aggregate_holdings_list(key_holdings) for key_holdings in grouped.values()) # We could potentially filter out holdings with zero units here. These types # of holdings might occur on a group with leaked (i.e., non-zero) cost basis # and zero units. However, sometimes are valid merging of multiple # currencies may occur, and the number value will be legitimately set to # ZERO (for various reasons downstream), so we prefer not to ignore the # holding. Callers must be prepared to deal with a holding of ZERO units and # a non-zero cost basis. {0ed05c502e63, b/16} ## nonzero_holdings = (holding ## for holding in grouped_holdings ## if holding.number != ZERO) # Return the holdings in order. return sorted(grouped_holdings, key=lambda holding: (holding.account, holding.currency))","title":"aggregate_holdings_by()"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.aggregate_holdings_list","text":"Aggregate a list of holdings. If there are varying 'account', 'currency' or 'cost_currency' attributes, their values are replaced by '*'. Otherwise they are preserved. Note that all the cost-currency values must be equal in order for aggregations to succeed (without this constraint a sum of units in different currencies has no meaning). Parameters: holdings \u2013 A list of Holding instances. Returns: A single Holding instance, or None, if there are no holdings in the input list. Exceptions: ValueError \u2013 If multiple cost currencies encountered. Source code in beancount/ops/holdings.py def aggregate_holdings_list(holdings): \"\"\"Aggregate a list of holdings. If there are varying 'account', 'currency' or 'cost_currency' attributes, their values are replaced by '*'. Otherwise they are preserved. Note that all the cost-currency values must be equal in order for aggregations to succeed (without this constraint a sum of units in different currencies has no meaning). Args: holdings: A list of Holding instances. Returns: A single Holding instance, or None, if there are no holdings in the input list. Raises: ValueError: If multiple cost currencies encountered. \"\"\" if not holdings: return None # Note: Holding is a bit overspecified with book and market values. We # recompute them from cost and price numbers here anyhow. units, total_book_value, total_market_value = ZERO, ZERO, ZERO accounts = set() currencies = set() cost_currencies = set() price_dates = set() book_value_seen = False market_value_seen = False for holding in holdings: units += holding.number accounts.add(holding.account) price_dates.add(holding.price_date) currencies.add(holding.currency) cost_currencies.add(holding.cost_currency) if holding.book_value is not None: total_book_value += holding.book_value book_value_seen = True elif holding.cost_number is not None: total_book_value += holding.number * holding.cost_number book_value_seen = True if holding.market_value is not None: total_market_value += holding.market_value market_value_seen = True elif holding.price_number is not None: total_market_value += holding.number * holding.price_number market_value_seen = True if book_value_seen: average_cost = total_book_value / units if units else None else: total_book_value = None average_cost = None if market_value_seen: average_price = total_market_value / units if units else None else: total_market_value = None average_price = None if len(cost_currencies) != 1: raise ValueError(\"Cost currencies are not homogeneous for aggregation: {}\".format( ','.join(map(str, cost_currencies)))) units = units if len(currencies) == 1 else ZERO currency = currencies.pop() if len(currencies) == 1 else '*' cost_currency = cost_currencies.pop() account_ = (accounts.pop() if len(accounts) == 1 else account.commonprefix(accounts)) price_date = price_dates.pop() if len(price_dates) == 1 else None return Holding(account_, units, currency, average_cost, cost_currency, total_book_value, total_market_value, average_price, price_date)","title":"aggregate_holdings_list()"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.convert_to_currency","text":"Convert the given list of holdings's fields to a common currency. If the rate is not available to convert, leave the fields empty. Parameters: price_map \u2013 A price-map, as built by prices.build_price_map(). target_currency \u2013 The target common currency to convert amounts to. holdings_list \u2013 A list of holdings.Holding instances. Returns: A modified list of holdings, with the 'extra' field set to the value in 'currency', or None, if it was not possible to convert. Source code in beancount/ops/holdings.py def convert_to_currency(price_map, target_currency, holdings_list): \"\"\"Convert the given list of holdings's fields to a common currency. If the rate is not available to convert, leave the fields empty. Args: price_map: A price-map, as built by prices.build_price_map(). target_currency: The target common currency to convert amounts to. holdings_list: A list of holdings.Holding instances. Returns: A modified list of holdings, with the 'extra' field set to the value in 'currency', or None, if it was not possible to convert. \"\"\" # A list of the fields we should convert. convert_fields = ('cost_number', 'book_value', 'market_value', 'price_number') new_holdings = [] for holding in holdings_list: if holding.cost_currency == target_currency: # The holding is already priced in the target currency; do nothing. new_holding = holding else: if holding.cost_currency is None: # There is no cost currency; make the holding priced in its own # units. The price-map should yield a rate of 1.0 and everything # else works out. if holding.currency is None: raise ValueError(\"Invalid currency '{}'\".format(holding.currency)) holding = holding._replace(cost_currency=holding.currency) # Fill in with book and market value as well. if holding.book_value is None: holding = holding._replace(book_value=holding.number) if holding.market_value is None: holding = holding._replace(market_value=holding.number) assert holding.cost_currency, \"Missing cost currency: {}\".format(holding) base_quote = (holding.cost_currency, target_currency) # Get the conversion rate and replace the required numerical # fields.. _, rate = prices.get_latest_price(price_map, base_quote) if rate is not None: new_holding = misc_utils.map_namedtuple_attributes( convert_fields, lambda number, r=rate: number if number is None else number * r, holding) # Ensure we set the new cost currency after conversion. new_holding = new_holding._replace(cost_currency=target_currency) else: # Could not get the rate... clear every field and set the cost # currency to None. This enough marks the holding conversion as # a failure. new_holding = misc_utils.map_namedtuple_attributes( convert_fields, lambda number: None, holding) new_holding = new_holding._replace(cost_currency=None) new_holdings.append(new_holding) return new_holdings","title":"convert_to_currency()"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.get_commodities_at_date","text":"Return a list of commodities present at a particular date. This routine fetches the holdings present at a particular date and returns a list of the commodities held in those holdings. This should define the list of price date points required to assess the market value of this portfolio. Notes: The ticker symbol will be fetched from the corresponding Commodity directive. If there is no ticker symbol defined for a directive or no corresponding Commodity directive, the currency is still included, but 'None' is specified for the symbol. The code that uses this routine should be free to use the currency name to make an attempt to fetch the currency using its name, or to ignore it. The 'cost-currency' is that which is found on the holdings instance and can be ignored. The 'quote-currency' is that which is declared on the Commodity directive from its 'quote' metadata field. This is used in a routine that fetches prices from a data source on the internet (either from LedgerHub, but you can reuse this in your own script if you build one). Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance, the date at which to get the list of relevant holdings. Returns: A list of (currency, cost-currency, quote-currency, ticker) tuples, where currency \u2013 The Beancount base currency to fetch a price for. cost-currency: The cost-currency of the holdings found at the given date. quote-currency: The currency formally declared as quote currency in the metadata of Commodity directives. ticker: The ticker symbol to use for fetching the price (extracted from the metadata of Commodity directives). Source code in beancount/ops/holdings.py def get_commodities_at_date(entries, options_map, date=None): \"\"\"Return a list of commodities present at a particular date. This routine fetches the holdings present at a particular date and returns a list of the commodities held in those holdings. This should define the list of price date points required to assess the market value of this portfolio. Notes: * The ticker symbol will be fetched from the corresponding Commodity directive. If there is no ticker symbol defined for a directive or no corresponding Commodity directive, the currency is still included, but 'None' is specified for the symbol. The code that uses this routine should be free to use the currency name to make an attempt to fetch the currency using its name, or to ignore it. * The 'cost-currency' is that which is found on the holdings instance and can be ignored. The 'quote-currency' is that which is declared on the Commodity directive from its 'quote' metadata field. This is used in a routine that fetches prices from a data source on the internet (either from LedgerHub, but you can reuse this in your own script if you build one). Args: entries: A list of directives. date: A datetime.date instance, the date at which to get the list of relevant holdings. Returns: A list of (currency, cost-currency, quote-currency, ticker) tuples, where currency: The Beancount base currency to fetch a price for. cost-currency: The cost-currency of the holdings found at the given date. quote-currency: The currency formally declared as quote currency in the metadata of Commodity directives. ticker: The ticker symbol to use for fetching the price (extracted from the metadata of Commodity directives). \"\"\" # Remove all the entries after the given date, if requested. if date is not None: entries = summarize.truncate(entries, date) # Get the list of holdings at the particular date. holdings_list = get_final_holdings(entries) # Obtain the unique list of currencies we need to fetch. commodities_list = {(holding.currency, holding.cost_currency) for holding in holdings_list} # Add in the associated ticker symbols. commodities_map = getters.get_commodity_map(entries) commodities_symbols_list = [] for currency, cost_currency in sorted(commodities_list): try: commodity_entry = commodities_map[currency] ticker = commodity_entry.meta.get('ticker', None) quote_currency = commodity_entry.meta.get('quote', None) except KeyError: ticker = None quote_currency = None commodities_symbols_list.append( (currency, cost_currency, quote_currency, ticker)) return commodities_symbols_list","title":"get_commodities_at_date()"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.get_final_holdings","text":"Get a dictionary of the latest holdings by account. This basically just flattens the balance sheet's final positions, including that of equity accounts. If a 'price_map' is provided, insert price information in the flattened holdings at the latest date, or at the given date, if one is provided. Only the accounts in 'included_account_types' will be included, and this is always called for Assets and Liabilities only. If left unspecified, holdings from all account types will be included, including Equity, Income and Expenses. Parameters: entries \u2013 A list of directives. included_account_types \u2013 A sequence of strings, the account types to include in the output. A reasonable example would be ('Assets', 'Liabilities'). If not specified, include all account types. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance, the date at which to price the holdings. If left unspecified, we use the latest price information. Returns: A list of dicts, with the following fields \u2013 Source code in beancount/ops/holdings.py def get_final_holdings(entries, included_account_types=None, price_map=None, date=None): \"\"\"Get a dictionary of the latest holdings by account. This basically just flattens the balance sheet's final positions, including that of equity accounts. If a 'price_map' is provided, insert price information in the flattened holdings at the latest date, or at the given date, if one is provided. Only the accounts in 'included_account_types' will be included, and this is always called for Assets and Liabilities only. If left unspecified, holdings from all account types will be included, including Equity, Income and Expenses. Args: entries: A list of directives. included_account_types: A sequence of strings, the account types to include in the output. A reasonable example would be ('Assets', 'Liabilities'). If not specified, include all account types. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance, the date at which to price the holdings. If left unspecified, we use the latest price information. Returns: A list of dicts, with the following fields: \"\"\" # Remove the entries inserted by unrealized gains/losses. Those entries do # affect asset accounts, and we don't want them to appear in holdings. # # Note: Perhaps it would make sense to generalize this concept of \"inserted # unrealized gains.\" simple_entries = [entry for entry in entries if (not isinstance(entry, data.Transaction) or entry.flag != flags.FLAG_UNREALIZED)] # Realize the accounts into a tree (because we want the positions by-account). root_account = realization.realize(simple_entries) # For each account, look at the list of positions and build a list. holdings = [] for real_account in sorted(list(realization.iter_children(root_account)), key=lambda ra: ra.account): if included_account_types: # Skip accounts of invalid types, we only want to reflect the requested # account types, typically assets and liabilities. account_type = account_types.get_account_type(real_account.account) if account_type not in included_account_types: continue for pos in real_account.balance.get_positions(): if pos.cost is not None: # Get price information if we have a price_map. market_value = None if price_map is not None: base_quote = (pos.units.currency, pos.cost.currency) price_date, price_number = prices.get_price(price_map, base_quote, date) if price_number is not None: market_value = pos.units.number * price_number else: price_date, price_number = None, None holding = Holding(real_account.account, pos.units.number, pos.units.currency, pos.cost.number, pos.cost.currency, pos.units.number * pos.cost.number, market_value, price_number, price_date) else: holding = Holding(real_account.account, pos.units.number, pos.units.currency, None, pos.units.currency, pos.units.number, pos.units.number, None, None) holdings.append(holding) return holdings","title":"get_final_holdings()"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.holding_to_position","text":"Convert the holding to a position. Parameters: holding \u2013 An instance of Holding. Returns: An instance of Position. Source code in beancount/ops/holdings.py def holding_to_position(holding): \"\"\"Convert the holding to a position. Args: holding: An instance of Holding. Returns: An instance of Position. \"\"\" return position.Position( amount.Amount(holding.number, holding.currency), (position.Cost(holding.cost_number, holding.cost_currency, None, None) if holding.cost_number else None))","title":"holding_to_position()"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.holding_to_posting","text":"Convert the holding to an instance of Posting. Parameters: holding \u2013 An instance of Holding. Returns: An instance of Position. Source code in beancount/ops/holdings.py def holding_to_posting(holding): \"\"\"Convert the holding to an instance of Posting. Args: holding: An instance of Holding. Returns: An instance of Position. \"\"\" position_ = holding_to_position(holding) price = (amount.Amount(holding.price_number, holding.cost_currency) if holding.price_number else None) return data.Posting(holding.account, position_.units, position_.cost, price, None, None)","title":"holding_to_posting()"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.reduce_relative","text":"Convert the market and book values of the given list of holdings to relative data. Parameters: holdings \u2013 A list of Holding instances. Returns: A list of holdings instances with the absolute value fields replaced by fractions of total portfolio. The new list of holdings is sorted by currency, and the relative fractions are also relative to that currency. Source code in beancount/ops/holdings.py def reduce_relative(holdings): \"\"\"Convert the market and book values of the given list of holdings to relative data. Args: holdings: A list of Holding instances. Returns: A list of holdings instances with the absolute value fields replaced by fractions of total portfolio. The new list of holdings is sorted by currency, and the relative fractions are also relative to that currency. \"\"\" # Group holdings by value currency. by_currency = collections.defaultdict(list) ordering = {} for index, holding in enumerate(holdings): ordering.setdefault(holding.cost_currency, index) by_currency[holding.cost_currency].append(holding) fractional_holdings = [] for currency in sorted(by_currency, key=ordering.get): currency_holdings = by_currency[currency] # Compute total market value for that currency. total_book_value = ZERO total_market_value = ZERO for holding in currency_holdings: if holding.book_value: total_book_value += holding.book_value if holding.market_value: total_market_value += holding.market_value # Sort the currency's holdings with decreasing values of market value. currency_holdings.sort( key=lambda holding: holding.market_value or ZERO, reverse=True) # Output new holdings with the relevant values replaced. for holding in currency_holdings: fractional_holdings.append( holding._replace(book_value=(holding.book_value / total_book_value if holding.book_value is not None else None), market_value=(holding.market_value / total_market_value if holding.market_value is not None else None))) return fractional_holdings","title":"reduce_relative()"},{"location":"api_reference/beancount.ops.html#beancount.ops.holdings.scale_holding","text":"Scale the values of a holding. Parameters: holding \u2013 An instance of Holding. scale_factor \u2013 A float or Decimal number. Returns: A scaled copy of the holding. Source code in beancount/ops/holdings.py def scale_holding(holding, scale_factor): \"\"\"Scale the values of a holding. Args: holding: An instance of Holding. scale_factor: A float or Decimal number. Returns: A scaled copy of the holding. \"\"\" return Holding( holding.account, holding.number * scale_factor if holding.number else None, holding.currency, holding.cost_number, holding.cost_currency, holding.book_value * scale_factor if holding.book_value else None, holding.market_value * scale_factor if holding.market_value else None, holding.price_number, holding.price_date)","title":"scale_holding()"},{"location":"api_reference/beancount.ops.html#beancount.ops.lifetimes","text":"Given a Beancount ledger, compute time intervals where we hold each commodity. This script computes, for each commodity, which time intervals it is required at. This can then be used to identify a list of dates at which we need to fetch prices in order to properly fill the price database.","title":"lifetimes"},{"location":"api_reference/beancount.ops.html#beancount.ops.lifetimes.compress_intervals_days","text":"Compress a list of date pairs to ignore short stretches of unused days. Parameters: intervals \u2013 A list of pairs of datetime.date instances. num_days \u2013 An integer, the number of unused days to require for intervals to be distinct, to allow a gap. Returns: A new dict of lifetimes map where some intervals may have been joined. Source code in beancount/ops/lifetimes.py def compress_intervals_days(intervals, num_days): \"\"\"Compress a list of date pairs to ignore short stretches of unused days. Args: intervals: A list of pairs of datetime.date instances. num_days: An integer, the number of unused days to require for intervals to be distinct, to allow a gap. Returns: A new dict of lifetimes map where some intervals may have been joined. \"\"\" ignore_interval = datetime.timedelta(days=num_days) new_intervals = [] iter_intervals = iter(intervals) last_begin, last_end = next(iter_intervals) for date_begin, date_end in iter_intervals: if date_begin - last_end < ignore_interval: # Compress. last_end = date_end continue new_intervals.append((last_begin, last_end)) last_begin, last_end = date_begin, date_end new_intervals.append((last_begin, last_end)) return new_intervals","title":"compress_intervals_days()"},{"location":"api_reference/beancount.ops.html#beancount.ops.lifetimes.compress_lifetimes_days","text":"Compress a lifetimes map to ignore short stretches of unused days. Parameters: lifetimes_map \u2013 A dict of currency intervals as returned by get_commodity_lifetimes. num_days \u2013 An integer, the number of unused days to ignore. Returns: A new dict of lifetimes map where some intervals may have been joined. Source code in beancount/ops/lifetimes.py def compress_lifetimes_days(lifetimes_map, num_days): \"\"\"Compress a lifetimes map to ignore short stretches of unused days. Args: lifetimes_map: A dict of currency intervals as returned by get_commodity_lifetimes. num_days: An integer, the number of unused days to ignore. Returns: A new dict of lifetimes map where some intervals may have been joined. \"\"\" return {currency_pair: compress_intervals_days(intervals, num_days) for currency_pair, intervals in lifetimes_map.items()}","title":"compress_lifetimes_days()"},{"location":"api_reference/beancount.ops.html#beancount.ops.lifetimes.get_commodity_lifetimes","text":"Given a list of directives, figure out the life of each commodity. Parameters: entries \u2013 A list of directives. Returns: A dict of (currency, cost-currency) commodity strings to lists of (start, end) datetime.date pairs. The dates are inclusive of the day the commodity was seen; the end/last dates are one day after the last date seen. Source code in beancount/ops/lifetimes.py def get_commodity_lifetimes(entries): \"\"\"Given a list of directives, figure out the life of each commodity. Args: entries: A list of directives. Returns: A dict of (currency, cost-currency) commodity strings to lists of (start, end) datetime.date pairs. The dates are inclusive of the day the commodity was seen; the end/last dates are one day _after_ the last date seen. \"\"\" lifetimes = collections.defaultdict(list) # The current set of active commodities. commodities = set() # The current balances across all accounts. balances = collections.defaultdict(inventory.Inventory) for entry in entries: # Process only transaction entries. if not isinstance(entry, data.Transaction): continue # Update the balance of affected accounts and check locally whether that # triggered a change in the set of commodities. commodities_changed = False for posting in entry.postings: balance = balances[posting.account] commodities_before = balance.currency_pairs() balance.add_position(posting) commodities_after = balance.currency_pairs() if commodities_after != commodities_before: commodities_changed = True # If there was a change in one of the affected account's list of # commodities, recompute the total set globally. This should not # occur very frequently. if commodities_changed: new_commodities = set( itertools.chain(*(inv.currency_pairs() for inv in balances.values()))) if new_commodities != commodities: # The new global set of commodities has changed; update our # the dictionary of intervals. for currency in new_commodities - commodities: lifetimes[currency].append((entry.date, None)) for currency in commodities - new_commodities: lifetime = lifetimes[currency] begin_date, end_date = lifetime.pop(-1) assert end_date is None lifetime.append((begin_date, entry.date + ONEDAY)) # Update our current set. commodities = new_commodities return lifetimes","title":"get_commodity_lifetimes()"},{"location":"api_reference/beancount.ops.html#beancount.ops.lifetimes.required_weekly_prices","text":"Enumerate all the commodities and Fridays where the price is required. Given a map of lifetimes for a set of commodities, enumerate all the Fridays for each commodity where it is active. This can be used to connect to a historical price fetcher routine to fill in missing price entries from an existing ledger. Parameters: lifetimes_map \u2013 A dict of currency to active intervals as returned by get_commodity_lifetimes(). date_last \u2013 A datetime.date instance, the last date which we're interested in. Returns: Tuples of (date, currency, cost-currency). Source code in beancount/ops/lifetimes.py def required_weekly_prices(lifetimes_map, date_last): \"\"\"Enumerate all the commodities and Fridays where the price is required. Given a map of lifetimes for a set of commodities, enumerate all the Fridays for each commodity where it is active. This can be used to connect to a historical price fetcher routine to fill in missing price entries from an existing ledger. Args: lifetimes_map: A dict of currency to active intervals as returned by get_commodity_lifetimes(). date_last: A datetime.date instance, the last date which we're interested in. Returns: Tuples of (date, currency, cost-currency). \"\"\" results = [] for currency_pair, intervals in lifetimes_map.items(): if currency_pair[1] is None: continue for date_begin, date_end in intervals: # Find first Friday before the minimum date. diff_days = 4 - date_begin.weekday() if diff_days > 1: diff_days -= 7 date = date_begin + datetime.timedelta(days=diff_days) # Iterate over all Fridays. if date_end is None: date_end = date_last while date < date_end: results.append((date, currency_pair[0], currency_pair[1])) date += ONE_WEEK return sorted(results)","title":"required_weekly_prices()"},{"location":"api_reference/beancount.ops.html#beancount.ops.pad","text":"Automatic padding of gaps between entries.","title":"pad"},{"location":"api_reference/beancount.ops.html#beancount.ops.pad.PadError","text":"PadError(source, message, entry)","title":"PadError"},{"location":"api_reference/beancount.ops.html#beancount.ops.pad.PadError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/pad.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.pad.PadError.__new__","text":"Create new instance of PadError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.pad.PadError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/ops/pad.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.pad.pad","text":"Insert transaction entries for to fulfill a subsequent balance check. Synthesize and insert Transaction entries right after Pad entries in order to fulfill checks in the padded accounts. Returns a new list of entries. Note that this doesn't pad across parent-child relationships, it is a very simple kind of pad. (I have found this to be sufficient in practice, and simpler to implement and understand.) Furthermore, this pads for a single currency only, that is, balance checks are specified only for one currency at a time, and pads will only be inserted for those currencies. Parameters: entries \u2013 A list of directives. options_map \u2013 A parser options dict. Returns: A new list of directives, with Pad entries inserted, and a list of new errors produced. Source code in beancount/ops/pad.py def pad(entries, options_map): \"\"\"Insert transaction entries for to fulfill a subsequent balance check. Synthesize and insert Transaction entries right after Pad entries in order to fulfill checks in the padded accounts. Returns a new list of entries. Note that this doesn't pad across parent-child relationships, it is a very simple kind of pad. (I have found this to be sufficient in practice, and simpler to implement and understand.) Furthermore, this pads for a single currency only, that is, balance checks are specified only for one currency at a time, and pads will only be inserted for those currencies. Args: entries: A list of directives. options_map: A parser options dict. Returns: A new list of directives, with Pad entries inserted, and a list of new errors produced. \"\"\" pad_errors = [] # Find all the pad entries and group them by account. pads = list(misc_utils.filter_type(entries, data.Pad)) pad_dict = misc_utils.groupby(lambda x: x.account, pads) # Partially realize the postings, so we can iterate them by account. by_account = realization.postings_by_account(entries) # A dict of pad -> list of entries to be inserted. new_entries = {id(pad): [] for pad in pads} # Process each account that has a padding group. for account_, pad_list in sorted(pad_dict.items()): # Last encountered / currency active pad entry. active_pad = None # Gather all the postings for the account and its children. postings = [] is_child = account.parent_matcher(account_) for item_account, item_postings in by_account.items(): if is_child(item_account): postings.extend(item_postings) postings.sort(key=data.posting_sortkey) # A set of currencies already padded so far in this account. padded_lots = set() pad_balance = inventory.Inventory() for entry in postings: assert not isinstance(entry, data.Posting) if isinstance(entry, data.TxnPosting): # This is a transaction; update the running balance for this # account. pad_balance.add_position(entry.posting) elif isinstance(entry, data.Pad): if entry.account == account_: # Mark this newly encountered pad as active and allow all lots # to be padded heretofore. active_pad = entry padded_lots = set() elif isinstance(entry, data.Balance): check_amount = entry.amount # Compare the current balance amount to the expected one from # the check entry. IMPORTANT: You need to understand that this # does not check a single position, but rather checks that the # total amount for a particular currency (which itself is # distinct from the cost). balance_amount = pad_balance.get_currency_units(check_amount.currency) diff_amount = amount.sub(balance_amount, check_amount) # Use the specified tolerance or automatically infer it. tolerance = balance.get_balance_tolerance(entry, options_map) if abs(diff_amount.number) > tolerance: # The check fails; we need to pad. # Pad only if pad entry is active and we haven't already # padded that lot since it was last encountered. if active_pad and (check_amount.currency not in padded_lots): # Note: we decide that it's an error to try to pad # positions at cost; we check here that all the existing # positions with that currency have no cost. positions = [pos for pos in pad_balance.get_positions() if pos.units.currency == check_amount.currency] for position_ in positions: if position_.cost is not None: pad_errors.append( PadError(entry.meta, (\"Attempt to pad an entry with cost for \" \"balance: {}\".format(pad_balance)), active_pad)) # Thus our padding lot is without cost by default. diff_position = position.Position.from_amounts( amount.Amount(check_amount.number - balance_amount.number, check_amount.currency)) # Synthesize a new transaction entry for the difference. narration = ('(Padding inserted for Balance of {} for ' 'difference {})').format(check_amount, diff_position) new_entry = data.Transaction( active_pad.meta.copy(), active_pad.date, flags.FLAG_PADDING, None, narration, data.EMPTY_SET, data.EMPTY_SET, []) new_entry.postings.append( data.Posting(active_pad.account, diff_position.units, diff_position.cost, None, None, None)) neg_diff_position = -diff_position new_entry.postings.append( data.Posting(active_pad.source_account, neg_diff_position.units, neg_diff_position.cost, None, None, None)) # Save it for later insertion after the active pad. new_entries[id(active_pad)].append(new_entry) # Fixup the running balance. pos, _ = pad_balance.add_position(diff_position) if pos is not None and pos.is_negative_at_cost(): raise ValueError( \"Position held at cost goes negative: {}\".format(pos)) # Mark this lot as padded. Further checks should not pad this lot. padded_lots.add(check_amount.currency) # Insert the newly created entries right after the pad entries that created them. padded_entries = [] for entry in entries: padded_entries.append(entry) if isinstance(entry, data.Pad): entry_list = new_entries[id(entry)] if entry_list: padded_entries.extend(entry_list) else: # Generate errors on unused pad entries. pad_errors.append( PadError(entry.meta, \"Unused Pad entry\", entry)) return padded_entries, pad_errors","title":"pad()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize","text":"Summarization of entries. This code is used to summarize a sequence of entries (e.g. during a time period) into a few \"opening balance\" entries. This is when computing a balance sheet for a specific time period: we don't want to see the entries from before some period of time, so we fold them into a single transaction per account that has the sum total amount of that account.","title":"summarize"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.balance_by_account","text":"Sum up the balance per account for all entries strictly before 'date'. Parameters: entries \u2013 A list of directives. date \u2013 An optional datetime.date instance. If provided, stop accumulating on and after this date. This is useful for summarization before a specific date. Returns: A pair of a dict of account string to instance Inventory (the balance of this account before the given date), and the index in the list of entries where the date was encountered. If all entries are located before the cutoff date, an index one beyond the last entry is returned. Source code in beancount/ops/summarize.py def balance_by_account(entries, date=None): \"\"\"Sum up the balance per account for all entries strictly before 'date'. Args: entries: A list of directives. date: An optional datetime.date instance. If provided, stop accumulating on and after this date. This is useful for summarization before a specific date. Returns: A pair of a dict of account string to instance Inventory (the balance of this account before the given date), and the index in the list of entries where the date was encountered. If all entries are located before the cutoff date, an index one beyond the last entry is returned. \"\"\" balances = collections.defaultdict(inventory.Inventory) for index, entry in enumerate(entries): if date and entry.date >= date: break if isinstance(entry, Transaction): for posting in entry.postings: account_balance = balances[posting.account] # Note: We must allow negative lots at cost, because this may be # used to reduce a filtered list of entries which may not # include the entries necessary to keep units at cost always # above zero. The only summation that is guaranteed to be above # zero is if all the entries are being summed together, no # entries are filtered, at least for a particular account's # postings. account_balance.add_position(posting) else: index = len(entries) return balances, index","title":"balance_by_account()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.cap","text":"Transfer net income to equity and insert a final conversion entry. This is used to move and nullify balances from the income and expense accounts to an equity account in order to draw up a balance sheet with a balance of precisely zero. Parameters: entries \u2013 A list of directives. account_types \u2013 An instance of AccountTypes. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_earnings \u2013 A string, the name of the equity account to transfer final balances of the income and expense accounts to. account_conversions \u2013 A string, the name of the equity account to use as the source for currency conversions. Returns: A modified list of entries, with the income and expense accounts transferred. Source code in beancount/ops/summarize.py def cap(entries, account_types, conversion_currency, account_earnings, account_conversions): \"\"\"Transfer net income to equity and insert a final conversion entry. This is used to move and nullify balances from the income and expense accounts to an equity account in order to draw up a balance sheet with a balance of precisely zero. Args: entries: A list of directives. account_types: An instance of AccountTypes. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_earnings: A string, the name of the equity account to transfer final balances of the income and expense accounts to. account_conversions: A string, the name of the equity account to use as the source for currency conversions. Returns: A modified list of entries, with the income and expense accounts transferred. \"\"\" # Transfer the balances of income and expense accounts as earnings / net # income. income_statement_account_pred = ( lambda account: is_income_statement_account(account, account_types)) entries = transfer_balances(entries, None, income_statement_account_pred, account_earnings) # Insert final conversion entries. entries = conversions(entries, account_conversions, conversion_currency, None) return entries","title":"cap()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.cap_opt","text":"Close by getting all the parameters from an options map. See cap() for details. Parameters: entries \u2013 See cap(). options_map \u2013 A parser's option_map. Returns: Same as close(). Source code in beancount/ops/summarize.py def cap_opt(entries, options_map): \"\"\"Close by getting all the parameters from an options map. See cap() for details. Args: entries: See cap(). options_map: A parser's option_map. Returns: Same as close(). \"\"\" account_types = options.get_account_types(options_map) current_accounts = options.get_current_accounts(options_map) conversion_currency = options_map['conversion_currency'] return cap(entries, account_types, conversion_currency, *current_accounts)","title":"cap_opt()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.clamp","text":"Filter entries to include only those during a specified time period. Firstly, this method will transfer all balances for the income and expense accounts occurring before the given period begin date to the 'account_earnings' account (earnings before the period, or \"retained earnings\") and summarize all of the transactions before that date against the 'account_opening' account (usually \"opening balances\"). The resulting income and expense accounts should have no transactions (since their balances have been transferred out and summarization of zero balances should not add any transactions). Secondly, all the entries after the period end date will be truncated and a conversion entry will be added for the resulting transactions that reflect changes occurring between the beginning and end of the exercise period. The resulting balance of all account should be empty. Parameters: entries \u2013 A list of directive tuples. begin_date \u2013 A datetime.date instance, the beginning of the period. end_date \u2013 A datetime.date instance, one day beyond the end of the period. account_types \u2013 An instance of AccountTypes. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_earnings \u2013 A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening \u2013 A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions \u2013 A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. Source code in beancount/ops/summarize.py def clamp(entries, begin_date, end_date, account_types, conversion_currency, account_earnings, account_opening, account_conversions): \"\"\"Filter entries to include only those during a specified time period. Firstly, this method will transfer all balances for the income and expense accounts occurring before the given period begin date to the 'account_earnings' account (earnings before the period, or \"retained earnings\") and summarize all of the transactions before that date against the 'account_opening' account (usually \"opening balances\"). The resulting income and expense accounts should have no transactions (since their balances have been transferred out and summarization of zero balances should not add any transactions). Secondly, all the entries after the period end date will be truncated and a conversion entry will be added for the resulting transactions that reflect changes occurring between the beginning and end of the exercise period. The resulting balance of all account should be empty. Args: entries: A list of directive tuples. begin_date: A datetime.date instance, the beginning of the period. end_date: A datetime.date instance, one day beyond the end of the period. account_types: An instance of AccountTypes. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_earnings: A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening: A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions: A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. \"\"\" # Transfer income and expenses before the period to equity. income_statement_account_pred = ( lambda account: is_income_statement_account(account, account_types)) entries = transfer_balances(entries, begin_date, income_statement_account_pred, account_earnings) # Summarize all the previous balances, after transferring the income and # expense balances, so all entries for those accounts before the begin date # should now disappear. entries, index = summarize(entries, begin_date, account_opening) # Truncate the entries after this. entries = truncate(entries, end_date) # Insert conversion entries. entries = conversions(entries, account_conversions, conversion_currency, end_date) return entries, index","title":"clamp()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.clamp_opt","text":"Clamp by getting all the parameters from an options map. See clamp() for details. Parameters: entries \u2013 See clamp(). begin_date \u2013 See clamp(). end_date \u2013 See clamp(). options_map \u2013 A parser's option_map. Returns: Same as clamp(). Source code in beancount/ops/summarize.py def clamp_opt(entries, begin_date, end_date, options_map): \"\"\"Clamp by getting all the parameters from an options map. See clamp() for details. Args: entries: See clamp(). begin_date: See clamp(). end_date: See clamp(). options_map: A parser's option_map. Returns: Same as clamp(). \"\"\" account_types = options.get_account_types(options_map) previous_accounts = options.get_previous_accounts(options_map) conversion_currency = options_map['conversion_currency'] return clamp(entries, begin_date, end_date, account_types, conversion_currency, *previous_accounts)","title":"clamp_opt()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.clear","text":"Transfer income and expenses balances at the given date to the equity accounts. This method insert entries to zero out balances on income and expenses accounts by transferring them to an equity account. Parameters: entries \u2013 A list of directive tuples. date \u2013 A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. account_types \u2013 An instance of AccountTypes. account_earnings \u2013 A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. Returns: A new list of entries is returned, and the index that points to one before the last original transaction before the transfers. Source code in beancount/ops/summarize.py def clear(entries, date, account_types, account_earnings): \"\"\"Transfer income and expenses balances at the given date to the equity accounts. This method insert entries to zero out balances on income and expenses accounts by transferring them to an equity account. Args: entries: A list of directive tuples. date: A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. account_types: An instance of AccountTypes. account_earnings: A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. Returns: A new list of entries is returned, and the index that points to one before the last original transaction before the transfers. \"\"\" index = len(entries) # Transfer income and expenses before the period to equity. income_statement_account_pred = ( lambda account: is_income_statement_account(account, account_types)) new_entries = transfer_balances(entries, date, income_statement_account_pred, account_earnings) return new_entries, index","title":"clear()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.clear_opt","text":"Convenience function to clear() using an options map. Source code in beancount/ops/summarize.py def clear_opt(entries, date, options_map): \"\"\"Convenience function to clear() using an options map. \"\"\" account_types = options.get_account_types(options_map) current_accounts = options.get_current_accounts(options_map) return clear(entries, date, account_types, current_accounts[0])","title":"clear_opt()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.close","text":"Truncate entries that occur after a particular date and ensure balance. This method essentially removes entries after a date. It truncates the future. To do so, it will Remove all entries which occur after 'date', if given. Insert conversion transactions at the end of the list of entries to ensure that the total balance of all postings sums up to empty. The result is a list of entries with a total balance of zero, with possibly non-zero balances for the income/expense accounts. To produce a final balance sheet, use transfer() to move the net income to the equity accounts. Parameters: entries \u2013 A list of directive tuples. date \u2013 A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_conversions \u2013 A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to one beyond the last original transaction that was provided. Further entries may have been inserted to normalize conversions and ensure the total balance sums to zero. Source code in beancount/ops/summarize.py def close(entries, date, conversion_currency, account_conversions): \"\"\"Truncate entries that occur after a particular date and ensure balance. This method essentially removes entries after a date. It truncates the future. To do so, it will 1. Remove all entries which occur after 'date', if given. 2. Insert conversion transactions at the end of the list of entries to ensure that the total balance of all postings sums up to empty. The result is a list of entries with a total balance of zero, with possibly non-zero balances for the income/expense accounts. To produce a final balance sheet, use transfer() to move the net income to the equity accounts. Args: entries: A list of directive tuples. date: A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_conversions: A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to one beyond the last original transaction that was provided. Further entries may have been inserted to normalize conversions and ensure the total balance sums to zero. \"\"\" # Truncate the entries after the date, if a date has been provided. if date is not None: entries = truncate(entries, date) # Keep an index to the truncated list of entries (before conversions). index = len(entries) # Insert a conversions entry to ensure the total balance of all accounts is # flush zero. entries = conversions(entries, account_conversions, conversion_currency, date) return entries, index","title":"close()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.close_opt","text":"Convenience function to close() using an options map. Source code in beancount/ops/summarize.py def close_opt(entries, date, options_map): \"\"\"Convenience function to close() using an options map. \"\"\" conversion_currency = options_map['conversion_currency'] current_accounts = options.get_current_accounts(options_map) return close(entries, date, conversion_currency, current_accounts[1])","title":"close_opt()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.conversions","text":"Insert a conversion entry at date 'date' at the given account. Parameters: entries \u2013 A list of entries. conversion_account \u2013 A string, the account to book against. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. date \u2013 The date before which to insert the conversion entry. The new entry will be inserted as the last entry of the date just previous to this date. Returns: A modified list of entries. Source code in beancount/ops/summarize.py def conversions(entries, conversion_account, conversion_currency, date=None): \"\"\"Insert a conversion entry at date 'date' at the given account. Args: entries: A list of entries. conversion_account: A string, the account to book against. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. date: The date before which to insert the conversion entry. The new entry will be inserted as the last entry of the date just previous to this date. Returns: A modified list of entries. \"\"\" # Compute the balance at the given date. conversion_balance = interpolate.compute_entries_balance(entries, date=date) # Early exit if there is nothing to do. conversion_cost_balance = conversion_balance.reduce(convert.get_cost) if conversion_cost_balance.is_empty(): return entries # Calculate the index and the date for the new entry. We want to store it as # the last transaction of the day before. if date is not None: index = bisect_key.bisect_left_with_key(entries, date, key=lambda entry: entry.date) last_date = date - datetime.timedelta(days=1) else: index = len(entries) last_date = entries[-1].date meta = data.new_metadata('', -1) narration = 'Conversion for {}'.format(conversion_balance) conversion_entry = Transaction(meta, last_date, flags.FLAG_CONVERSIONS, None, narration, data.EMPTY_SET, data.EMPTY_SET, []) for position in conversion_cost_balance.get_positions(): # Important note: Set the cost to zero here to maintain the balance # invariant. (This is the only single place we cheat on the balance rule # in the entire system and this is necessary; see documentation on # Conversions.) price = amount.Amount(ZERO, conversion_currency) neg_pos = -position conversion_entry.postings.append( data.Posting(conversion_account, neg_pos.units, neg_pos.cost, price, None, None)) # Make a copy of the list of entries and insert the new transaction into it. new_entries = list(entries) new_entries.insert(index, conversion_entry) return new_entries","title":"conversions()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.create_entries_from_balances","text":"\"Create a list of entries from a dict of balances. This method creates a list of new entries to transfer the amounts in the 'balances' dict to/from another account specified in 'source_account'. The balancing posting is created with the equivalent at cost. In other words, if you attempt to balance 10 HOOL {500 USD}, this will synthesize a posting with this position on one leg, and with 5000 USD on the 'source_account' leg. Parameters: balances \u2013 A dict of account name strings to Inventory instances. date \u2013 A datetime.date object, the date at which to create the transaction. source_account \u2013 A string, the name of the account to pull the balances from. This is the magician's hat to pull the rabbit from. direction \u2013 If 'direction' is True, the new entries transfer TO the balances account from the source account; otherwise the new entries transfer FROM the balances into the source account. meta \u2013 A dict to use as metadata for the transactions. flag \u2013 A string, the flag to use for the transactions. narration_template \u2013 A format string for creating the narration. It is formatted with 'account' and 'date' replacement variables. Returns: A list of newly synthesizes Transaction entries. Source code in beancount/ops/summarize.py def create_entries_from_balances(balances, date, source_account, direction, meta, flag, narration_template): \"\"\"\"Create a list of entries from a dict of balances. This method creates a list of new entries to transfer the amounts in the 'balances' dict to/from another account specified in 'source_account'. The balancing posting is created with the equivalent at cost. In other words, if you attempt to balance 10 HOOL {500 USD}, this will synthesize a posting with this position on one leg, and with 5000 USD on the 'source_account' leg. Args: balances: A dict of account name strings to Inventory instances. date: A datetime.date object, the date at which to create the transaction. source_account: A string, the name of the account to pull the balances from. This is the magician's hat to pull the rabbit from. direction: If 'direction' is True, the new entries transfer TO the balances account from the source account; otherwise the new entries transfer FROM the balances into the source account. meta: A dict to use as metadata for the transactions. flag: A string, the flag to use for the transactions. narration_template: A format string for creating the narration. It is formatted with 'account' and 'date' replacement variables. Returns: A list of newly synthesizes Transaction entries. \"\"\" new_entries = [] for account, account_balance in sorted(balances.items()): # Don't create new entries where there is no balance. if account_balance.is_empty(): continue narration = narration_template.format(account=account, date=date) if not direction: account_balance = -account_balance postings = [] new_entry = Transaction( meta, date, flag, None, narration, data.EMPTY_SET, data.EMPTY_SET, postings) for position in account_balance.get_positions(): postings.append(data.Posting(account, position.units, position.cost, None, None, None)) cost = -convert.get_cost(position) postings.append(data.Posting(source_account, cost, None, None, None, None)) new_entries.append(new_entry) return new_entries","title":"create_entries_from_balances()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.get_open_entries","text":"Gather the list of active Open entries at date. This returns the list of Open entries that have not been closed at the given date, in the same order they were observed in the document. Parameters: entries \u2013 A list of directives. date \u2013 The date at which to look for an open entry. If not specified, will return the entries still open at the latest date. Returns: A list of Open directives. Source code in beancount/ops/summarize.py def get_open_entries(entries, date): \"\"\"Gather the list of active Open entries at date. This returns the list of Open entries that have not been closed at the given date, in the same order they were observed in the document. Args: entries: A list of directives. date: The date at which to look for an open entry. If not specified, will return the entries still open at the latest date. Returns: A list of Open directives. \"\"\" open_entries = {} for index, entry in enumerate(entries): if date is not None and entry.date >= date: break if isinstance(entry, Open): try: ex_index, ex_entry = open_entries[entry.account] if entry.date < ex_entry.date: open_entries[entry.account] = (index, entry) except KeyError: open_entries[entry.account] = (index, entry) elif isinstance(entry, Close): # If there is no corresponding open, don't raise an error. open_entries.pop(entry.account, None) return [entry for (index, entry) in sorted(open_entries.values())]","title":"get_open_entries()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.open","text":"Summarize entries before a date and transfer income/expenses to equity. This method essentially prepares a list of directives to contain only transactions that occur after a particular date. It truncates the past. To do so, it will Insert conversion transactions at the given open date, then Insert transactions at that date to move accumulated balances from before that date from the income and expenses accounts to an equity account, and finally It removes all the transactions previous to the date and replaces them by opening balances entries to bring the balances to the same amount. The result is a list of entries for which the income and expense accounts are beginning with a balance of zero, and all other accounts begin with a transaction that brings their balance to the expected amount. All the past has been summarized at that point. An index is returned to the first transaction past the balance opening transactions, so you can keep just those in order to render a balance sheet for only the opening balances. Parameters: entries \u2013 A list of directive tuples. date \u2013 A datetime.date instance, the date at which to do this. account_types \u2013 An instance of AccountTypes. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_earnings \u2013 A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening \u2013 A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions \u2013 A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. Source code in beancount/ops/summarize.py def open(entries, date, account_types, conversion_currency, account_earnings, account_opening, account_conversions): \"\"\"Summarize entries before a date and transfer income/expenses to equity. This method essentially prepares a list of directives to contain only transactions that occur after a particular date. It truncates the past. To do so, it will 1. Insert conversion transactions at the given open date, then 2. Insert transactions at that date to move accumulated balances from before that date from the income and expenses accounts to an equity account, and finally 3. It removes all the transactions previous to the date and replaces them by opening balances entries to bring the balances to the same amount. The result is a list of entries for which the income and expense accounts are beginning with a balance of zero, and all other accounts begin with a transaction that brings their balance to the expected amount. All the past has been summarized at that point. An index is returned to the first transaction past the balance opening transactions, so you can keep just those in order to render a balance sheet for only the opening balances. Args: entries: A list of directive tuples. date: A datetime.date instance, the date at which to do this. account_types: An instance of AccountTypes. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_earnings: A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening: A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions: A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. \"\"\" # Insert conversion entries. entries = conversions(entries, account_conversions, conversion_currency, date) # Transfer income and expenses before the period to equity. entries, _ = clear(entries, date, account_types, account_earnings) # Summarize all the previous balances, after transferring the income and # expense balances, so all entries for those accounts before the begin date # should now disappear. entries, index = summarize(entries, date, account_opening) return entries, index","title":"open()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.open_opt","text":"Convenience function to open() using an options map. Source code in beancount/ops/summarize.py def open_opt(entries, date, options_map): \"\"\"Convenience function to open() using an options map. \"\"\" account_types = options.get_account_types(options_map) previous_accounts = options.get_previous_accounts(options_map) conversion_currency = options_map['conversion_currency'] return open(entries, date, account_types, conversion_currency, *previous_accounts)","title":"open_opt()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.summarize","text":"Summarize all entries before a date by replacing then with summarization entries. This function replaces the transactions up to (and not including) the given date with a opening balance transactions, one for each account. It returns new entries, all of the transactions before the given date having been replaced by a few summarization entries, one for each account. Notes: - Open entries are preserved for active accounts. - The last relevant price entry for each (base, quote) pair is preserved. - All other entries before the cutoff date are culled. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance, the cutoff date before which to summarize. account_opening \u2013 A string, the name of the source account to book summarization entries against. Returns: The function returns a list of new entries and the integer index at which the entries on or after the cutoff date begin. Source code in beancount/ops/summarize.py def summarize(entries, date, account_opening): \"\"\"Summarize all entries before a date by replacing then with summarization entries. This function replaces the transactions up to (and not including) the given date with a opening balance transactions, one for each account. It returns new entries, all of the transactions before the given date having been replaced by a few summarization entries, one for each account. Notes: - Open entries are preserved for active accounts. - The last relevant price entry for each (base, quote) pair is preserved. - All other entries before the cutoff date are culled. Args: entries: A list of directives. date: A datetime.date instance, the cutoff date before which to summarize. account_opening: A string, the name of the source account to book summarization entries against. Returns: The function returns a list of new entries and the integer index at which the entries on or after the cutoff date begin. \"\"\" # Compute balances at date. balances, index = balance_by_account(entries, date) # We need to insert the entries with a date previous to subsequent checks, # to maintain ensure the open directives show up before any transaction. summarize_date = date - datetime.timedelta(days=1) # Create summarization / opening balance entries. summarizing_entries = create_entries_from_balances( balances, summarize_date, account_opening, True, data.new_metadata('', 0), flags.FLAG_SUMMARIZE, \"Opening balance for '{account}' (Summarization)\") # Insert the last price entry for each commodity from before the date. price_entries = prices.get_last_price_entries(entries, date) # Gather the list of active open entries at date. open_entries = get_open_entries(entries, date) # Compute entries before the date and preserve the entries after the date. before_entries = sorted(open_entries + price_entries + summarizing_entries, key=data.entry_sortkey) after_entries = entries[index:] # Return a new list of entries and the index that points after the entries # were inserted. return (before_entries + after_entries), len(before_entries)","title":"summarize()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.transfer_balances","text":"Synthesize transactions to transfer balances from some accounts at a given date. For all accounts that match the 'account_pred' predicate, create new entries to transfer the balance at the given date from the account to the transfer account. This is used to transfer balances from income and expenses from a previous period to a \"retained earnings\" account. This is accomplished by creating new entries. Note that inserting transfers breaks any following balance checks that are in the transferred accounts. For this reason, all balance assertion entries following the cutoff date for those accounts are removed from the list in output. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance, the date at which to make the transfer. account_pred \u2013 A predicate function that, given an account string, returns true if the account is meant to be transferred. transfer_account \u2013 A string, the name of the source account to be used on the transfer entries to receive balances at the given date. Returns: A new list of entries, with the new transfer entries added in. Source code in beancount/ops/summarize.py def transfer_balances(entries, date, account_pred, transfer_account): \"\"\"Synthesize transactions to transfer balances from some accounts at a given date. For all accounts that match the 'account_pred' predicate, create new entries to transfer the balance at the given date from the account to the transfer account. This is used to transfer balances from income and expenses from a previous period to a \"retained earnings\" account. This is accomplished by creating new entries. Note that inserting transfers breaks any following balance checks that are in the transferred accounts. For this reason, all balance assertion entries following the cutoff date for those accounts are removed from the list in output. Args: entries: A list of directives. date: A datetime.date instance, the date at which to make the transfer. account_pred: A predicate function that, given an account string, returns true if the account is meant to be transferred. transfer_account: A string, the name of the source account to be used on the transfer entries to receive balances at the given date. Returns: A new list of entries, with the new transfer entries added in. \"\"\" # Don't bother doing anything if there are no entries. if not entries: return entries # Compute balances at date. balances, index = balance_by_account(entries, date) # Filter out to keep only the accounts we want. transfer_balances = {account: balance for account, balance in balances.items() if account_pred(account)} # We need to insert the entries at the end of the previous day. if date: transfer_date = date - datetime.timedelta(days=1) else: transfer_date = entries[-1].date # Create transfer entries. transfer_entries = create_entries_from_balances( transfer_balances, transfer_date, transfer_account, False, data.new_metadata('', 0), flags.FLAG_TRANSFER, \"Transfer balance for '{account}' (Transfer balance)\") # Remove balance assertions that occur after a transfer on an account that # has been transferred away; they would break. after_entries = [entry for entry in entries[index:] if not (isinstance(entry, balance.Balance) and entry.account in transfer_balances)] # Split the new entries in a new list. return (entries[:index] + transfer_entries + after_entries)","title":"transfer_balances()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.truncate","text":"Filter out all the entries at and after date. Returns a new list of entries. Parameters: entries \u2013 A sorted list of directives. date \u2013 A datetime.date instance. Returns: A truncated list of directives. Source code in beancount/ops/summarize.py def truncate(entries, date): \"\"\"Filter out all the entries at and after date. Returns a new list of entries. Args: entries: A sorted list of directives. date: A datetime.date instance. Returns: A truncated list of directives. \"\"\" index = bisect_key.bisect_left_with_key(entries, date, key=lambda entry: entry.date) return entries[:index]","title":"truncate()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation","text":"Validation checks. These checks are intended to be run after all the plugins have transformed the list of entries, just before serving them or generating reports from them. The idea is to ensure a reasonable set of invariants and generate errors if those invariants are violated. They are not sanity checks--user data is subject to constraints which are hopefully detected here and which will result in errors trickled up to the user.","title":"validation"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.ValidationError","text":"ValidationError(source, message, entry)","title":"ValidationError"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.ValidationError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/validation.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.ValidationError.__new__","text":"Create new instance of ValidationError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.ValidationError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/ops/validation.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate","text":"Perform all the standard checks on parsed contents. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. log_timings \u2013 An optional function to use for logging the time of individual operations. extra_validations \u2013 A list of extra validation functions to run after loading this list of entries. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate(entries, options_map, log_timings=None, extra_validations=None): \"\"\"Perform all the standard checks on parsed contents. Args: entries: A list of directives. unused_options_map: An options map. log_timings: An optional function to use for logging the time of individual operations. extra_validations: A list of extra validation functions to run after loading this list of entries. Returns: A list of new errors, if any were found. \"\"\" validation_tests = VALIDATIONS if extra_validations: validation_tests += extra_validations # Run various validation routines define above. errors = [] for validation_function in validation_tests: with misc_utils.log_time('function: {}'.format(validation_function.__name__), log_timings, indent=2): new_errors = validation_function(entries, options_map) errors.extend(new_errors) return errors","title":"validate()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_active_accounts","text":"Check that all references to accounts occurs on active accounts. We basically check that references to accounts from all directives other than Open and Close occur at dates the open-close interval of that account. This should be good for all of the directive types where we can extract an account name. Note that this is more strict a check than comparing the dates: we actually check that no references to account are made on the same day before the open directive appears for that account. This is a nice property to have, and is supported by our custom sorting routine that will sort open entries before transaction entries, given the same date. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_active_accounts(entries, unused_options_map): \"\"\"Check that all references to accounts occurs on active accounts. We basically check that references to accounts from all directives other than Open and Close occur at dates the open-close interval of that account. This should be good for all of the directive types where we can extract an account name. Note that this is more strict a check than comparing the dates: we actually check that no references to account are made on the same day before the open directive appears for that account. This is a nice property to have, and is supported by our custom sorting routine that will sort open entries before transaction entries, given the same date. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" error_pairs = [] active_set = set() opened_accounts = set() for entry in entries: if isinstance(entry, data.Open): active_set.add(entry.account) opened_accounts.add(entry.account) elif isinstance(entry, data.Close): active_set.discard(entry.account) else: for account in getters.get_entry_accounts(entry): if account not in active_set: # Allow document and note directives that occur after an # account is closed. if (isinstance(entry, ALLOW_AFTER_CLOSE) and account in opened_accounts): continue # Register an error to be logged later, with an appropriate # message. error_pairs.append((account, entry)) # Refine the error message to disambiguate between the case of an account # that has never been seen and one that was simply not active at the time. errors = [] for account, entry in error_pairs: if account in opened_accounts: message = \"Invalid reference to inactive account '{}'\".format(account) else: message = \"Invalid reference to unknown account '{}'\".format(account) errors.append(ValidationError(entry.meta, message, entry)) return errors","title":"validate_active_accounts()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_check_transaction_balances","text":"Check again that all transaction postings balance, as users may have transformed transactions. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_check_transaction_balances(entries, options_map): \"\"\"Check again that all transaction postings balance, as users may have transformed transactions. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" # Note: this is a bit slow; we could limit our checks to the original # transactions by using the hash function in the loader. errors = [] for entry in entries: if isinstance(entry, Transaction): # IMPORTANT: This validation is _crucial_ and cannot be skipped. # This is where we actually detect and warn on unbalancing # transactions. This _must_ come after the user routines, because # unbalancing input is legal, as those types of transactions may be # \"fixed up\" by a user-plugin. In other words, we want to allow # users to input unbalancing transactions as long as the final # transactions objects that appear on the stream (after processing # the plugins) are balanced. See {9e6c14b51a59}. # # Detect complete sets of postings that have residual balance; residual = interpolate.compute_residual(entry.postings) tolerances = interpolate.infer_tolerances(entry.postings, options_map) if not residual.is_small(tolerances): errors.append( ValidationError(entry.meta, \"Transaction does not balance: {}\".format(residual), entry)) return errors","title":"validate_check_transaction_balances()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_currency_constraints","text":"Check the currency constraints from account open declarations. Open directives admit an optional list of currencies that specify the only types of commodities that the running inventory for this account may contain. This function checks that all postings are only made in those commodities. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_currency_constraints(entries, options_map): \"\"\"Check the currency constraints from account open declarations. Open directives admit an optional list of currencies that specify the only types of commodities that the running inventory for this account may contain. This function checks that all postings are only made in those commodities. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" # Get all the open entries with currency constraints. open_map = {entry.account: entry for entry in entries if isinstance(entry, Open) and entry.currencies} errors = [] for entry in entries: if not isinstance(entry, Transaction): continue for posting in entry.postings: # Look up the corresponding account's valid currencies; skip the # check if there are none specified. try: open_entry = open_map[posting.account] valid_currencies = open_entry.currencies if not valid_currencies: continue except KeyError: continue # Perform the check. if posting.units.currency not in valid_currencies: errors.append( ValidationError( entry.meta, \"Invalid currency {} for account '{}'\".format( posting.units.currency, posting.account), entry)) return errors","title":"validate_currency_constraints()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_data_types","text":"Check that all the data types of the attributes of entries are as expected. Users are provided with a means to filter the list of entries. They're able to write code that manipulates those tuple objects without any type constraints. With discipline, this mostly works, but I know better: check, just to make sure. This routine checks all the data types and assumptions on entries. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_data_types(entries, options_map): \"\"\"Check that all the data types of the attributes of entries are as expected. Users are provided with a means to filter the list of entries. They're able to write code that manipulates those tuple objects without any type constraints. With discipline, this mostly works, but I know better: check, just to make sure. This routine checks all the data types and assumptions on entries. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] for entry in entries: try: data.sanity_check_types( entry, options_map[\"allow_deprecated_none_for_tags_and_links\"]) except AssertionError as exc: errors.append( ValidationError(entry.meta, \"Invalid data types: {}\".format(exc), entry)) return errors","title":"validate_data_types()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_documents_paths","text":"Check that all filenames in resolved Document entries are absolute filenames. The processing of document entries is assumed to result in absolute paths. Relative paths are resolved at the parsing stage and at point we want to make sure we don't have to do any further processing on them. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_documents_paths(entries, options_map): \"\"\"Check that all filenames in resolved Document entries are absolute filenames. The processing of document entries is assumed to result in absolute paths. Relative paths are resolved at the parsing stage and at point we want to make sure we don't have to do any further processing on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" return [ValidationError(entry.meta, \"Invalid relative path for entry\", entry) for entry in entries if (isinstance(entry, Document) and not path.isabs(entry.filename))]","title":"validate_documents_paths()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_duplicate_balances","text":"Check that balance entries occur only once per day. Because we do not support time, and the declaration order of entries is meant to be kept irrelevant, two balance entries with different amounts should not occur in the file. We do allow two identical balance assertions, however, because this may occur during import. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_duplicate_balances(entries, unused_options_map): \"\"\"Check that balance entries occur only once per day. Because we do not support time, and the declaration order of entries is meant to be kept irrelevant, two balance entries with different amounts should not occur in the file. We do allow two identical balance assertions, however, because this may occur during import. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] # Mapping of (account, currency, date) to Balance entry. balance_entries = {} for entry in entries: if not isinstance(entry, data.Balance): continue key = (entry.account, entry.amount.currency, entry.date) try: previous_entry = balance_entries[key] if entry.amount != previous_entry.amount: errors.append( ValidationError( entry.meta, \"Duplicate balance assertion with different amounts\", entry)) except KeyError: balance_entries[key] = entry return errors","title":"validate_duplicate_balances()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_duplicate_commodities","text":"Check that commodity entries are unique for each commodity. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_duplicate_commodities(entries, unused_options_map): \"\"\"Check that commodity entries are unique for each commodity. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] # Mapping of (account, currency, date) to Balance entry. commodity_entries = {} for entry in entries: if not isinstance(entry, data.Commodity): continue key = entry.currency try: previous_entry = commodity_entries[key] if previous_entry: errors.append( ValidationError( entry.meta, \"Duplicate commodity directives for '{}'\".format(key), entry)) except KeyError: commodity_entries[key] = entry return errors","title":"validate_duplicate_commodities()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_open_close","text":"Check constraints on open and close directives themselves. This method checks two kinds of constraints: An open or a close directive may only show up once for each account. If a duplicate is detected, an error is generated. Close directives may only appears if an open directive has been seen previous (chronologically). The date of close directives must be strictly greater than their corresponding open directive. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_open_close(entries, unused_options_map): \"\"\"Check constraints on open and close directives themselves. This method checks two kinds of constraints: 1. An open or a close directive may only show up once for each account. If a duplicate is detected, an error is generated. 2. Close directives may only appears if an open directive has been seen previous (chronologically). 3. The date of close directives must be strictly greater than their corresponding open directive. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] open_map = {} close_map = {} for entry in entries: if isinstance(entry, Open): if entry.account in open_map: errors.append( ValidationError( entry.meta, \"Duplicate open directive for {}\".format(entry.account), entry)) else: open_map[entry.account] = entry elif isinstance(entry, Close): if entry.account in close_map: errors.append( ValidationError( entry.meta, \"Duplicate close directive for {}\".format(entry.account), entry)) else: try: open_entry = open_map[entry.account] if entry.date <= open_entry.date: errors.append( ValidationError( entry.meta, \"Internal error: closing date for {} \" \"appears before opening date\".format(entry.account), entry)) except KeyError: errors.append( ValidationError( entry.meta, \"Unopened account {} is being closed\".format(entry.account), entry)) close_map[entry.account] = entry return errors","title":"validate_open_close()"},{"location":"api_reference/beancount.parser.html","text":"beancount.parser \uf0c1 Parser module for beancount input files. beancount.parser.booking \uf0c1 Algorithms for 'booking' inventory, that is, the process of finding a matching lot when reducing the content of an inventory. beancount.parser.booking.BookingError ( tuple ) \uf0c1 BookingError(source, message, entry) beancount.parser.booking.BookingError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.booking.BookingError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of BookingError(source, message, entry) beancount.parser.booking.BookingError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.booking.book(incomplete_entries, options_map) \uf0c1 Book inventory lots and complete all positions with incomplete numbers. Parameters: incomplete_entries \u2013 A list of directives, with some postings possibly left with incomplete amounts as produced by the parser. options_map \u2013 An options dict as produced by the parser. Returns: A pair of entries \u2013 A list of completed entries with all their postings completed. errors: New errors produced during interpolation. Source code in beancount/parser/booking.py def book(incomplete_entries, options_map): \"\"\"Book inventory lots and complete all positions with incomplete numbers. Args: incomplete_entries: A list of directives, with some postings possibly left with incomplete amounts as produced by the parser. options_map: An options dict as produced by the parser. Returns: A pair of entries: A list of completed entries with all their postings completed. errors: New errors produced during interpolation. \"\"\" # Get the list of booking methods for each account. booking_methods = collections.defaultdict(lambda: options_map[\"booking_method\"]) for entry in incomplete_entries: if isinstance(entry, data.Open) and entry.booking: booking_methods[entry.account] = entry.booking # Do the booking here! entries, booking_errors = booking_full.book(incomplete_entries, options_map, booking_methods) # Check for MISSING elements remaining. missing_errors = validate_missing_eliminated(entries, options_map) return entries, (booking_errors + missing_errors) beancount.parser.booking.validate_inventory_booking(entries, unused_options_map, booking_methods) \uf0c1 Validate that no position at cost is allowed to go negative. This routine checks that when a posting reduces a position, existing or not, that the subsequent inventory does not result in a position with a negative number of units. A negative number of units would only be required for short trades of trading spreads on futures, and right now this is not supported. It would not be difficult to support this, however, but we want to be strict about it, because being pedantic about this is otherwise a great way to detect user data entry mistakes. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. booking_methods \u2013 A mapping of account name to booking method, accumulated in the main loop. Returns: A list of errors. Source code in beancount/parser/booking.py def validate_inventory_booking(entries, unused_options_map, booking_methods): \"\"\"Validate that no position at cost is allowed to go negative. This routine checks that when a posting reduces a position, existing or not, that the subsequent inventory does not result in a position with a negative number of units. A negative number of units would only be required for short trades of trading spreads on futures, and right now this is not supported. It would not be difficult to support this, however, but we want to be strict about it, because being pedantic about this is otherwise a great way to detect user data entry mistakes. Args: entries: A list of directives. unused_options_map: An options map. booking_methods: A mapping of account name to booking method, accumulated in the main loop. Returns: A list of errors. \"\"\" errors = [] balances = collections.defaultdict(inventory.Inventory) for entry in entries: if isinstance(entry, data.Transaction): for posting in entry.postings: # Update the balance of each posting on its respective account # without allowing booking to a negative position, and if an error # is encountered, catch it and return it. running_balance = balances[posting.account] position_, _ = running_balance.add_position(posting) # Skip this check if the booking method is set to ignore it. if booking_methods.get(posting.account, None) == data.Booking.NONE: continue # Check if the resulting inventory is mixed, which is not # allowed under the STRICT method. if running_balance.is_mixed(): errors.append( BookingError( entry.meta, (\"Reducing position results in inventory with positive \" \"and negative lots: {}\").format(position_), entry)) return errors beancount.parser.booking.validate_missing_eliminated(entries, unused_options_map) \uf0c1 Validate that all the missing bits of postings have been eliminated. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of errors. Source code in beancount/parser/booking.py def validate_missing_eliminated(entries, unused_options_map): \"\"\"Validate that all the missing bits of postings have been eliminated. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of errors. \"\"\" errors = [] for entry in entries: if isinstance(entry, data.Transaction): for posting in entry.postings: units = posting.units cost = posting.cost if (MISSING in (units.number, units.currency) or cost is not None and MISSING in (cost.number, cost.currency, cost.date, cost.label)): errors.append( BookingError(entry.meta, \"Transaction has incomplete elements\", entry)) break return errors beancount.parser.booking_full \uf0c1 Full (new) booking implementation. Problem description: Interpolation and booking feed on each other, that is, numbers filled in from interpolation might affect the booking process, and numbers derived from the booking process may help carry out interpolation that would otherwise be under-defined. Here's an example of interpolation helping the booking process: Assume the ante-inventory of Assets:Investments contains two lots of shares of HOOL, one at 100.00 USD and the other at 101.00 USD and apply this transaction: 2015-09-30 * Assets:Investments -10 HOOL {USD} Assets:Cash 1000 USD Income:Gains -200 USD Interpolation is unambiguously able to back out a cost of 100 USD / HOOL, which would then result in an unambiguous booking result. On the other hand, consider this transaction: 2015-09-30 * Assets:Investments -10 HOOL {USD} Assets:Cash 1000 USD Income:Gains Now the interpolation cannot succeed. If the Assets:Investments account is configured to use the FIFO method, the 10 oldest shares would be selected for the cost, and we could then interpolate the capital gains correctly. First observation: The second case is much more frequent than the first, and the first is easily resolved manually by requiring a particular cost be specified. Moreover, in many cases there isn't just a single lot of shares to be reduced from and figuring out the correct set of shares given a target cost is an underspecified problem. Second observation: Booking can only be achieved for inventory reductions, not for augmentations. Therefore, we should carry out booking on inventory reductions and fail early if reduction is undefined there, and leave inventory augmentations with missing numbers undefined, so that interpolation can fill them in at a later stage. Note that one case we'd like to but may not be able to handle is of a reduction with interpolated price, like this: 2015-09-30 * Assets:Investments -10 HOOL {100.00 # USD} Expenses:Commission 9.95 USD Assets:Cash 990.05 USD Therefore we choose to 1) Carry out booking first, on inventory reductions only, and leave inventory augmentations as they are, possibly undefined. The 'cost' attributed of booked postings are converted from CostSpec to Cost. Augmented postings with missing amounts are left as CostSpec instances in order to allow for interpolation of total vs. per-unit amount. 2) Compute interpolations on the resulting postings. Undefined costs for inventory augmentations may be filled in by interpolations at this stage (if possible). 3) Finally, convert the interpolated CostSpec instances to Cost instances. Improving on this algorithm would require running a loop over the booking and interpolation steps until all numbers are resolved or no more inference can occur. We may consider that for later, as an experimental feature. My hunch is that there are so few cases for which this would be useful that we won't bother improving on the algorithm above. beancount.parser.booking_full.CategorizationError ( tuple ) \uf0c1 CategorizationError(source, message, entry) beancount.parser.booking_full.CategorizationError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.booking_full.CategorizationError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of CategorizationError(source, message, entry) beancount.parser.booking_full.CategorizationError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.booking_full.InterpolationError ( tuple ) \uf0c1 InterpolationError(source, message, entry) beancount.parser.booking_full.InterpolationError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.booking_full.InterpolationError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of InterpolationError(source, message, entry) beancount.parser.booking_full.InterpolationError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.booking_full.MissingType ( Enum ) \uf0c1 The type of missing number. beancount.parser.booking_full.ReductionError ( tuple ) \uf0c1 ReductionError(source, message, entry) beancount.parser.booking_full.ReductionError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.booking_full.ReductionError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of ReductionError(source, message, entry) beancount.parser.booking_full.ReductionError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.booking_full.Refer ( tuple ) \uf0c1 Refer(index, units_currency, cost_currency, price_currency) beancount.parser.booking_full.Refer.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.booking_full.Refer.__new__(_cls, index, units_currency, cost_currency, price_currency) special staticmethod \uf0c1 Create new instance of Refer(index, units_currency, cost_currency, price_currency) beancount.parser.booking_full.Refer.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.booking_full.SelfReduxError ( tuple ) \uf0c1 SelfReduxError(source, message, entry) beancount.parser.booking_full.SelfReduxError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.booking_full.SelfReduxError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of SelfReduxError(source, message, entry) beancount.parser.booking_full.SelfReduxError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.booking_full.book(entries, options_map, methods) \uf0c1 Interpolate missing data from the entries using the full historical algorithm. See the internal implementation _book() for details. This method only stripes some of the return values. See _book() for arguments and return values. Source code in beancount/parser/booking_full.py def book(entries, options_map, methods): \"\"\"Interpolate missing data from the entries using the full historical algorithm. See the internal implementation _book() for details. This method only stripes some of the return values. See _book() for arguments and return values. \"\"\" entries, errors, _ = _book(entries, options_map, methods) return entries, errors beancount.parser.booking_full.book_reductions(entry, group_postings, balances, methods) \uf0c1 Book inventory reductions against the ante-balances. This function accepts a dict of (account, Inventory balance) and for each posting that is a reduction against its inventory, attempts to find a corresponding lot or list of lots to reduce the balance with. For reducing lots, the CostSpec instance of the posting is replaced by a Cost instance. For augmenting lots, the CostSpec instance of the posting is left alone, except for its date, which is inherited from the parent Transaction. Parameters: entry \u2013 An instance of Transaction. This is only used to refer to when logging errors. group_postings \u2013 A list of Posting instances for the group. balances \u2013 A dict of account name to inventory contents. methods \u2013 A mapping of account name to their corresponding booking method enum. Returns: A pair of booked_postings \u2013 A list of booked postings, with reducing lots resolved against specific position in the corresponding accounts' ante-inventory balances. Note single reducing posting in the input may result in multiple postings in the output. Also note that augmenting postings held-at-cost will still refer to 'cost' instances of CostSpec, left to be interpolated later. errors: A list of errors, if there were any. Source code in beancount/parser/booking_full.py def book_reductions(entry, group_postings, balances, methods): \"\"\"Book inventory reductions against the ante-balances. This function accepts a dict of (account, Inventory balance) and for each posting that is a reduction against its inventory, attempts to find a corresponding lot or list of lots to reduce the balance with. * For reducing lots, the CostSpec instance of the posting is replaced by a Cost instance. * For augmenting lots, the CostSpec instance of the posting is left alone, except for its date, which is inherited from the parent Transaction. Args: entry: An instance of Transaction. This is only used to refer to when logging errors. group_postings: A list of Posting instances for the group. balances: A dict of account name to inventory contents. methods: A mapping of account name to their corresponding booking method enum. Returns: A pair of booked_postings: A list of booked postings, with reducing lots resolved against specific position in the corresponding accounts' ante-inventory balances. Note single reducing posting in the input may result in multiple postings in the output. Also note that augmenting postings held-at-cost will still refer to 'cost' instances of CostSpec, left to be interpolated later. errors: A list of errors, if there were any. \"\"\" errors = [] # A local copy of the balances dictionary which is updated just for the # duration of this function's updates, in order to take into account the # cumulative effect of all the postings inferred here local_balances = {} empty = inventory.Inventory() booked_postings = [] for posting in group_postings: # Process a single posting. units = posting.units costspec = posting.cost account = posting.account # Note: We ensure there is no mutation on 'balances' to keep this # function without side-effects. Note that we may be able to optimize # performance later on by giving up this property. # # Also note that if there is no existing balance, then won't be any lot # reduction because none of the postings will be able to match against # any currencies of the balance. previous_balance = balances.get(account, empty) balance = local_balances.setdefault(account, copy.copy(previous_balance)) # Check if this is a lot held at cost. if costspec is None or units.number is MISSING: # This posting is not held at cost; we do nothing. booked_postings.append(posting) else: # This posting is held at cost; figure out if it's a reduction or an # augmentation. method = methods[account] if (method is not Booking.NONE and balance is not None and balance.is_reduced_by(units)): # This posting is a reduction. # Match the positions. cost_number = compute_cost_number(costspec, units) matches = [] for position in balance: # Skip inventory contents of a different currency. if (units.currency and position.units.currency != units.currency): continue # Skip balance positions not held at cost. if position.cost is None: continue if (cost_number is not None and position.cost.number != cost_number): continue if (isinstance(costspec.currency, str) and position.cost.currency != costspec.currency): continue if (costspec.date and position.cost.date != costspec.date): continue if (costspec.label and position.cost.label != costspec.label): continue matches.append(position) # Check for ambiguous matches. if len(matches) == 0: errors.append( ReductionError(entry.meta, 'No position matches \"{}\" against balance {}'.format( posting, balance), entry)) return [], errors # This is irreconcilable, remove these postings. reduction_postings, matched_postings, ambi_errors = ( booking_method.handle_ambiguous_matches(entry, posting, matches, method)) if ambi_errors: errors.extend(ambi_errors) return [], errors # Add the reductions to the resulting list of booked postings. booked_postings.extend(reduction_postings) # Update the local balance in order to avoid matching against # the same postings twice when processing multiple postings in # the same transaction. Note that we only do this for postings # held at cost because the other postings may need interpolation # in order to be resolved properly. for posting in reduction_postings: balance.add_position(posting) else: # This posting is an augmentation. # # Note that we do not convert the CostSpec instances to Cost # instances, because we want to let the subsequent interpolation # process able to interpolate either the cost per-unit or the # total cost, separately. # Put in the date of the parent Transaction if there is no # explicit date specified on the spec. if costspec.date is None: dated_costspec = costspec._replace(date=entry.date) posting = posting._replace(cost=dated_costspec) # FIXME: Insert unique ids for trade tracking; right now this # creates ambiguous matches errors (and it shouldn't). # # Insert a unique label if there isn't one. # if posting.cost is not None and posting.cost.label is None: # posting = posting._replace( # cost=posting.cost._replace(label=unique_label())) booked_postings.append(posting) return booked_postings, errors beancount.parser.booking_full.categorize_by_currency(entry, balances) \uf0c1 Group the postings by the currency they declare. This is used to prepare the postings for the next stages: Interpolation and booking will then be carried out separately on each currency group. At the outset of this routine, we should have distinct groups of currencies without any ambiguities regarding which currency they need to balance against. Here's how this works. First we apply the constraint that cost-currency and price-currency must match, if there is both a cost and a price. This reduces the space of possibilities somewhat. If the currency is explicitly specified, we put the posting in that currency's bucket. If not, we have a few methods left to disambiguate the currency: We look at the remaining postings... if they are all of a single currency, the posting must be in that currency too. If we cannot do that, we inspect the contents of the inventory of the account for the posting. If all the contents are of a single currency, we use that one. Parameters: postings \u2013 A list of incomplete postings to categorize. balances \u2013 A dict of currency to inventory contents before the transaction is applied. Returns: A list of (currency string, list of tuples) items describing each postings and its interpolated currencies, and a list of generated errors for currency interpolation. The entry's original postings are left unmodified. Each tuple in the value-list contains \u2013 index: The posting index in the original entry. units_currency: The interpolated currency for units. cost_currency: The interpolated currency for cost. price_currency: The interpolated currency for price. Source code in beancount/parser/booking_full.py def categorize_by_currency(entry, balances): \"\"\"Group the postings by the currency they declare. This is used to prepare the postings for the next stages: Interpolation and booking will then be carried out separately on each currency group. At the outset of this routine, we should have distinct groups of currencies without any ambiguities regarding which currency they need to balance against. Here's how this works. - First we apply the constraint that cost-currency and price-currency must match, if there is both a cost and a price. This reduces the space of possibilities somewhat. - If the currency is explicitly specified, we put the posting in that currency's bucket. - If not, we have a few methods left to disambiguate the currency: 1. We look at the remaining postings... if they are all of a single currency, the posting must be in that currency too. 2. If we cannot do that, we inspect the contents of the inventory of the account for the posting. If all the contents are of a single currency, we use that one. Args: postings: A list of incomplete postings to categorize. balances: A dict of currency to inventory contents before the transaction is applied. Returns: A list of (currency string, list of tuples) items describing each postings and its interpolated currencies, and a list of generated errors for currency interpolation. The entry's original postings are left unmodified. Each tuple in the value-list contains: index: The posting index in the original entry. units_currency: The interpolated currency for units. cost_currency: The interpolated currency for cost. price_currency: The interpolated currency for price. \"\"\" errors = [] groups = collections.defaultdict(list) sortdict = {} auto_postings = [] unknown = [] for index, posting in enumerate(entry.postings): units = posting.units cost = posting.cost price = posting.price # Extract and override the currencies locally. units_currency = (units.currency if units is not MISSING and units is not None else None) cost_currency = (cost.currency if cost is not MISSING and cost is not None else None) price_currency = (price.currency if price is not MISSING and price is not None else None) # First we apply the constraint that cost-currency and price-currency # must match, if there is both a cost and a price. This reduces the # space of possibilities somewhat. if cost_currency is MISSING and isinstance(price_currency, str): cost_currency = price_currency if price_currency is MISSING and isinstance(cost_currency, str): price_currency = cost_currency refer = Refer(index, units_currency, cost_currency, price_currency) if units is MISSING and price_currency is None: # Bucket auto-postings separately from unknown. auto_postings.append(refer) else: # Bucket with what we know so far. currency = get_bucket_currency(refer) if currency is not None: sortdict.setdefault(currency, index) groups[currency].append(refer) else: # If we need to infer the currency, store in unknown. unknown.append(refer) # We look at the remaining postings... if they are all of a single currency, # the posting must be in that currency too. if unknown and len(unknown) == 1 and len(groups) == 1: (index, units_currency, cost_currency, price_currency) = unknown.pop() other_currency = next(iter(groups.keys())) if price_currency is None and cost_currency is None: # Infer to the units currency. units_currency = other_currency else: # Infer to the cost and price currencies. if price_currency is MISSING: price_currency = other_currency if cost_currency is MISSING: cost_currency = other_currency refer = Refer(index, units_currency, cost_currency, price_currency) currency = get_bucket_currency(refer) assert currency is not None sortdict.setdefault(currency, index) groups[currency].append(refer) # Finally, try to resolve all the unknown legs using the inventory contents # of each account. for refer in unknown: (index, units_currency, cost_currency, price_currency) = refer posting = entry.postings[index] balance = balances.get(posting.account, None) if balance is None: balance = inventory.Inventory() if units_currency is MISSING: balance_currencies = balance.currencies() if len(balance_currencies) == 1: units_currency = balance_currencies.pop() if cost_currency is MISSING or price_currency is MISSING: balance_cost_currencies = balance.cost_currencies() if len(balance_cost_currencies) == 1: balance_cost_currency = balance_cost_currencies.pop() if price_currency is MISSING: price_currency = balance_cost_currency if cost_currency is MISSING: cost_currency = balance_cost_currency refer = Refer(index, units_currency, cost_currency, price_currency) currency = get_bucket_currency(refer) if currency is not None: sortdict.setdefault(currency, index) groups[currency].append(refer) else: errors.append( CategorizationError(posting.meta, \"Failed to categorize posting {}\".format(index + 1), entry)) # Fill in missing units currencies if some remain as missing. This may occur # if we used the cost or price to bucket the currency but the units currency # was missing. for currency, refers in groups.items(): for rindex, refer in enumerate(refers): if refer.units_currency is MISSING: posting = entry.postings[refer.index] balance = balances.get(posting.account, None) if balance is None: continue balance_currencies = balance.currencies() if len(balance_currencies) == 1: refers[rindex] = refer._replace(units_currency=balance_currencies.pop()) # Deal with auto-postings. if len(auto_postings) > 1: refer = auto_postings[-1] posting = entry.postings[refer.index] errors.append( CategorizationError(posting.meta, \"You may not have more than one auto-posting per currency\", entry)) auto_postings = auto_postings[0:1] for refer in auto_postings: for currency in groups.keys(): sortdict.setdefault(currency, refer.index) groups[currency].append(Refer(refer.index, currency, None, None)) # Issue error for all currencies which we could not resolve. for currency, refers in groups.items(): for refer in refers: posting = entry.postings[refer.index] for currency, name in [(refer.units_currency, 'units'), (refer.cost_currency, 'cost'), (refer.price_currency, 'price')]: if currency is MISSING: errors.append(CategorizationError( posting.meta, \"Could not resolve {} currency\".format(name), entry)) sorted_groups = sorted(groups.items(), key=lambda item: sortdict[item[0]]) return sorted_groups, errors beancount.parser.booking_full.compute_cost_number(costspec, units) \uf0c1 Given a CostSpec, return the cost number, if possible to compute. Parameters: costspec \u2013 A parsed instance of CostSpec. units \u2013 An instance of Amount for the units of the position. Returns: If it is not possible to calculate the cost, return None. Otherwise, returns a Decimal instance, the per-unit cost. Source code in beancount/parser/booking_full.py def compute_cost_number(costspec, units): \"\"\"Given a CostSpec, return the cost number, if possible to compute. Args: costspec: A parsed instance of CostSpec. units: An instance of Amount for the units of the position. Returns: If it is not possible to calculate the cost, return None. Otherwise, returns a Decimal instance, the per-unit cost. \"\"\" number_per = costspec.number_per number_total = costspec.number_total if MISSING in (number_per, number_total): return None if number_total is not None: # Compute the per-unit cost if there is some total cost # component involved. cost_total = number_total units_number = units.number if number_per is not None: cost_total += number_per * units_number unit_cost = cost_total / abs(units_number) elif number_per is None: return None else: unit_cost = number_per return unit_cost beancount.parser.booking_full.convert_costspec_to_cost(posting) \uf0c1 Convert an instance of CostSpec to Cost, if present on the posting. If the posting has no cost, it itself is just returned. Parameters: posting \u2013 An instance of Posting. Returns: An instance of Posting with a possibly replaced 'cost' attribute. Source code in beancount/parser/booking_full.py def convert_costspec_to_cost(posting): \"\"\"Convert an instance of CostSpec to Cost, if present on the posting. If the posting has no cost, it itself is just returned. Args: posting: An instance of Posting. Returns: An instance of Posting with a possibly replaced 'cost' attribute. \"\"\" cost = posting.cost if isinstance(cost, position.CostSpec): if cost is not None: units_number = posting.units.number number_per = cost.number_per number_total = cost.number_total if number_total is not None: # Compute the per-unit cost if there is some total cost # component involved. cost_total = number_total if number_per is not MISSING: cost_total += number_per * units_number unit_cost = cost_total / abs(units_number) else: unit_cost = number_per new_cost = Cost(unit_cost, cost.currency, cost.date, cost.label) posting = posting._replace(units=posting.units, cost=new_cost) return posting beancount.parser.booking_full.get_bucket_currency(refer) \uf0c1 Given currency references for a posting, return the bucket currency. Parameters: refer \u2013 An instance of Refer. Returns: A currency string. Source code in beancount/parser/booking_full.py def get_bucket_currency(refer): \"\"\"Given currency references for a posting, return the bucket currency. Args: refer: An instance of Refer. Returns: A currency string. \"\"\" currency = None if isinstance(refer.cost_currency, str): currency = refer.cost_currency elif isinstance(refer.price_currency, str): currency = refer.price_currency elif (refer.cost_currency is None and refer.price_currency is None and isinstance(refer.units_currency, str)): currency = refer.units_currency return currency beancount.parser.booking_full.has_self_reduction(postings, methods) \uf0c1 Return true if the postings potentially reduce each other at cost. Parameters: postings \u2013 A list of postings with uninterpolated CostSpec cost instances. methods \u2013 A mapping of account name to their corresponding booking method. Returns: A boolean, true if there's a potential for self-reduction. Source code in beancount/parser/booking_full.py def has_self_reduction(postings, methods): \"\"\"Return true if the postings potentially reduce each other at cost. Args: postings: A list of postings with uninterpolated CostSpec cost instances. methods: A mapping of account name to their corresponding booking method. Returns: A boolean, true if there's a potential for self-reduction. \"\"\" # A mapping of (currency, cost-currency) and sign. cost_changes = {} for posting in postings: cost = posting.cost if cost is None: continue if methods[posting.account] is Booking.NONE: continue key = (posting.account, posting.units.currency) sign = 1 if posting.units.number > ZERO else -1 if cost_changes.setdefault(key, sign) != sign: return True return False beancount.parser.booking_full.interpolate_group(postings, balances, currency, tolerances) \uf0c1 Interpolate missing numbers in the set of postings. Parameters: postings \u2013 A list of Posting instances. balances \u2013 A dict of account to its ante-inventory. currency \u2013 The weight currency of this group, used for reporting errors. tolerances \u2013 A dict of currency to tolerance values. Returns: A tuple of postings \u2013 A lit of new posting instances. errors: A list of errors generated during interpolation. interpolated: A boolean, true if we did have to interpolate. In the case of an error, this returns the original list of postings, which is still incomplete. If an error is returned, you should probably skip the transaction altogether, or just not include the postings in it. (An alternative behaviour would be to return only the list of valid postings, but that would likely result in an unbalanced transaction. We do it this way by choice.) Source code in beancount/parser/booking_full.py def interpolate_group(postings, balances, currency, tolerances): \"\"\"Interpolate missing numbers in the set of postings. Args: postings: A list of Posting instances. balances: A dict of account to its ante-inventory. currency: The weight currency of this group, used for reporting errors. tolerances: A dict of currency to tolerance values. Returns: A tuple of postings: A lit of new posting instances. errors: A list of errors generated during interpolation. interpolated: A boolean, true if we did have to interpolate. In the case of an error, this returns the original list of postings, which is still incomplete. If an error is returned, you should probably skip the transaction altogether, or just not include the postings in it. (An alternative behaviour would be to return only the list of valid postings, but that would likely result in an unbalanced transaction. We do it this way by choice.) \"\"\" errors = [] # Figure out which type of amount is missing, by creating a list of # incomplete postings and which type of units is missing. incomplete = [] for index, posting in enumerate(postings): units = posting.units cost = posting.cost price = posting.price # Identify incomplete parts of the Posting components. if units.number is MISSING: incomplete.append((MissingType.UNITS, index)) if isinstance(cost, CostSpec): if cost and cost.number_per is MISSING: incomplete.append((MissingType.COST_PER, index)) if cost and cost.number_total is MISSING: incomplete.append((MissingType.COST_TOTAL, index)) else: # Check that a resolved instance of Cost never needs interpolation. # # Note that in theory we could support the interpolation of regular # per-unit costs in these if we wanted to; but because they're all # reducing postings that have been booked earlier, those never need # to be interpolated. if cost is not None: assert isinstance(cost.number, Decimal), ( \"Internal error: cost has no number: {}\".format(cost)) if price and price.number is MISSING: incomplete.append((MissingType.PRICE, index)) # The replacement posting for the incomplete posting of this group. new_posting = None if len(incomplete) == 0: # If there are no missing numbers, just convert the CostSpec to Cost and # return that. out_postings = [convert_costspec_to_cost(posting) for posting in postings] elif len(incomplete) > 1: # If there is more than a single value to be interpolated, generate an # error and return no postings. _, posting_index = incomplete[0] errors.append(InterpolationError( postings[posting_index].meta, \"Too many missing numbers for currency group '{}'\".format(currency), None)) out_postings = [] else: # If there is a single missing number, calculate it and fill it in here. missing, index = incomplete[0] incomplete_posting = postings[index] # Convert augmenting postings' costs from CostSpec to corresponding Cost # instances, except for the incomplete posting. new_postings = [(posting if posting is incomplete_posting else convert_costspec_to_cost(posting)) for posting in postings] # Compute the balance of the other postings. residual = interpolate.compute_residual(posting for posting in new_postings if posting is not incomplete_posting) assert len(residual) < 2, \"Internal error in grouping postings by currencies.\" if not residual.is_empty(): respos = next(iter(residual)) assert respos.cost is None, ( \"Internal error; cost appears in weight calculation.\") assert respos.units.currency == currency, ( \"Internal error; residual different than currency group.\") weight = -respos.units.number weight_currency = respos.units.currency else: weight = ZERO weight_currency = currency if missing == MissingType.UNITS: units = incomplete_posting.units cost = incomplete_posting.cost if cost: # Handle the special case where we only have total cost. if cost.number_per == ZERO: errors.append(InterpolationError( incomplete_posting.meta, \"Cannot infer per-unit cost only from total\", None)) return postings, errors, True assert cost.currency == weight_currency, ( \"Internal error; residual currency different than missing currency.\") cost_total = cost.number_total or ZERO units_number = (weight - cost_total) / cost.number_per elif incomplete_posting.price: assert incomplete_posting.price.currency == weight_currency, ( \"Internal error; residual currency different than missing currency.\") units_number = weight / incomplete_posting.price.number else: assert units.currency == weight_currency, ( \"Internal error; residual currency different than missing currency.\") units_number = weight # Quantize the interpolated units if necessary. units_number = interpolate.quantize_with_tolerance(tolerances, units.currency, units_number) if weight != ZERO: new_pos = Position(Amount(units_number, units.currency), cost) new_posting = incomplete_posting._replace(units=new_pos.units, cost=new_pos.cost) else: new_posting = None elif missing == MissingType.COST_PER: units = incomplete_posting.units cost = incomplete_posting.cost assert cost.currency == weight_currency, ( \"Internal error; residual currency different than missing currency.\") if units.number != ZERO: number_per = (weight - (cost.number_total or ZERO)) / units.number new_cost = cost._replace(number_per=number_per) new_pos = Position(units, new_cost) new_posting = incomplete_posting._replace(units=new_pos.units, cost=new_pos.cost) else: new_posting = None elif missing == MissingType.COST_TOTAL: units = incomplete_posting.units cost = incomplete_posting.cost assert cost.currency == weight_currency, ( \"Internal error; residual currency different than missing currency.\") number_total = (weight - cost.number_per * units.number) new_cost = cost._replace(number_total=number_total) new_pos = Position(units, new_cost) new_posting = incomplete_posting._replace(units=new_pos.units, cost=new_pos.cost) elif missing == MissingType.PRICE: units = incomplete_posting.units cost = incomplete_posting.cost if cost is not None: errors.append(InterpolationError( incomplete_posting.meta, \"Cannot infer price for postings with units held at cost\", None)) return postings, errors, True else: price = incomplete_posting.price assert price.currency == weight_currency, ( \"Internal error; residual currency different than missing currency.\") new_price_number = abs(weight / units.number) new_posting = incomplete_posting._replace(price=Amount(new_price_number, price.currency)) else: assert False, \"Internal error; Invalid missing type.\" # Replace the number in the posting. if new_posting is not None: # Set meta-data on the new posting to indicate it was interpolated. if new_posting.meta is None: new_posting = new_posting._replace(meta={}) new_posting.meta[interpolate.AUTOMATIC_META] = True # Convert augmenting posting costs from CostSpec to a corresponding # Cost instance. new_postings[index] = convert_costspec_to_cost(new_posting) else: del new_postings[index] out_postings = new_postings assert all(not isinstance(posting.cost, CostSpec) for posting in out_postings) # Check that units are non-zero and that no cost remains negative; issue an # error if this is the case. for posting in out_postings: if posting.cost is None: continue # If there is a cost, we don't allow either a cost value of zero, # nor a zero number of units. Note that we allow a price of zero as # the only special case allowed (for conversion entries), but never # for costs. if posting.units.number == ZERO: errors.append(InterpolationError( posting.meta, 'Amount is zero: \"{}\"'.format(posting.units), None)) if posting.cost.number < ZERO: errors.append(InterpolationError( posting.meta, 'Cost is negative: \"{}\"'.format(posting.cost), None)) return out_postings, errors, (new_posting is not None) beancount.parser.booking_full.replace_currencies(postings, refer_groups) \uf0c1 Replace resolved currencies in the entry's Postings. This essentially applies the findings of categorize_by_currency() to produce new postings with all currencies resolved. Parameters: postings \u2013 A list of Posting instances to replace. refer_groups \u2013 A list of (currency, list of posting references) items as returned by categorize_by_currency(). Returns: A new list of items of (currency, list of Postings), postings for which the currencies have been replaced by their interpolated currency values. Source code in beancount/parser/booking_full.py def replace_currencies(postings, refer_groups): \"\"\"Replace resolved currencies in the entry's Postings. This essentially applies the findings of categorize_by_currency() to produce new postings with all currencies resolved. Args: postings: A list of Posting instances to replace. refer_groups: A list of (currency, list of posting references) items as returned by categorize_by_currency(). Returns: A new list of items of (currency, list of Postings), postings for which the currencies have been replaced by their interpolated currency values. \"\"\" new_groups = [] for currency, refers in refer_groups: new_postings = [] for refer in sorted(refers, key=lambda r: r.index): posting = postings[refer.index] units = posting.units if units is MISSING or units is None: posting = posting._replace(units=Amount(MISSING, refer.units_currency)) else: replace = False cost = posting.cost price = posting.price if units.currency is MISSING: units = Amount(units.number, refer.units_currency) replace = True if cost and cost.currency is MISSING: cost = cost._replace(currency=refer.cost_currency) replace = True if price and price.currency is MISSING: price = Amount(price.number, refer.price_currency) replace = True if replace: posting = posting._replace(units=units, cost=cost, price=price) new_postings.append(posting) new_groups.append((currency, new_postings)) return new_groups beancount.parser.booking_full.unique_label() \uf0c1 Return a globally unique label for cost entries. Source code in beancount/parser/booking_full.py def unique_label() -> Text: \"Return a globally unique label for cost entries.\" return str(uuid.uuid4()) beancount.parser.booking_method \uf0c1 Implementations of all the particular booking methods. This code is used by the full booking algorithm. beancount.parser.booking_method.AmbiguousMatchError ( tuple ) \uf0c1 AmbiguousMatchError(source, message, entry) beancount.parser.booking_method.AmbiguousMatchError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_method.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.booking_method.AmbiguousMatchError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of AmbiguousMatchError(source, message, entry) beancount.parser.booking_method.AmbiguousMatchError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking_method.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.booking_method.booking_method_AVERAGE(entry, posting, matches) \uf0c1 AVERAGE booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_AVERAGE(entry, posting, matches): \"\"\"AVERAGE booking method implementation.\"\"\" booked_reductions = [] booked_matches = [] errors = [AmbiguousMatchError(entry.meta, \"AVERAGE method is not supported\", entry)] return booked_reductions, booked_matches, errors, False # FIXME: Future implementation here. # pylint: disable=unreachable if False: # pylint: disable=using-constant-test # DISABLED - This is the code for AVERAGE, which is currently disabled. # If there is more than a single match we need to ultimately merge the # postings. Also, if the reducing posting provides a specific cost, we # need to update the cost basis as well. Both of these cases are carried # out by removing all the matches and readding them later on. if len(matches) == 1 and ( not isinstance(posting.cost.number_per, Decimal) and not isinstance(posting.cost.number_total, Decimal)): # There is no cost. Just reduce the one leg. This should be the # normal case if we always merge augmentations and the user lets # Beancount deal with the cost. match = matches[0] sign = -1 if posting.units.number < ZERO else 1 number = min(abs(match.units.number), abs(posting.units.number)) match_units = Amount(number * sign, match.units.currency) booked_reductions.append(posting._replace(units=match_units, cost=match.cost)) insufficient = (match_units.number != posting.units.number) else: # Merge the matching postings to a single one. merged_units = inventory.Inventory() merged_cost = inventory.Inventory() for match in matches: merged_units.add_amount(match.units) merged_cost.add_amount(convert.get_weight(match)) if len(merged_units) != 1 or len(merged_cost) != 1: errors.append( AmbiguousMatchError( entry.meta, 'Cannot merge positions in multiple currencies: {}'.format( ', '.join(position.to_string(match_posting) for match_posting in matches)), entry)) else: if (isinstance(posting.cost.number_per, Decimal) or isinstance(posting.cost.number_total, Decimal)): errors.append( AmbiguousMatchError( entry.meta, \"Explicit cost reductions aren't supported yet: {}\".format( position.to_string(posting)), entry)) else: # Insert postings to remove all the matches. booked_reductions.extend( posting._replace(units=-match.units, cost=match.cost, flag=flags.FLAG_MERGING) for match in matches) units = merged_units[0].units date = matches[0].cost.date ## FIXME: Select which one, ## oldest or latest. cost_units = merged_cost[0].units cost = Cost(cost_units.number/units.number, cost_units.currency, date, None) # Insert a posting to refill those with a replacement match. booked_reductions.append( posting._replace(units=units, cost=cost, flag=flags.FLAG_MERGING)) # Now, match the reducing request against this lot. booked_reductions.append( posting._replace(units=posting.units, cost=cost)) insufficient = abs(posting.units.number) > abs(units.number) beancount.parser.booking_method.booking_method_FIFO(entry, posting, matches) \uf0c1 FIFO booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_FIFO(entry, posting, matches): \"\"\"FIFO booking method implementation.\"\"\" return _booking_method_xifo(entry, posting, matches, False) beancount.parser.booking_method.booking_method_LIFO(entry, posting, matches) \uf0c1 LIFO booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_LIFO(entry, posting, matches): \"\"\"LIFO booking method implementation.\"\"\" return _booking_method_xifo(entry, posting, matches, True) beancount.parser.booking_method.booking_method_NONE(entry, posting, matches) \uf0c1 NONE booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_NONE(entry, posting, matches): \"\"\"NONE booking method implementation.\"\"\" # This never needs to match against any existing positions... we # disregard the matches, there's never any error. Note that this never # gets called in practice, we want to treat NONE postings as # augmentations. Default behaviour is to return them with their original # CostSpec, and the augmentation code will handle signaling an error if # there is insufficient detail to carry out the conversion to an # instance of Cost. # Note that it's an interesting question whether a reduction on an # account with NONE method which happens to match a single position # ought to be matched against it. We don't allow it for now. return [posting], [], False beancount.parser.booking_method.booking_method_STRICT(entry, posting, matches) \uf0c1 Strict booking method. Parameters: entry \u2013 The parent Transaction instance. posting \u2013 An instance of Posting, the reducing posting which we're attempting to match. matches \u2013 A list of matching Position instances from the ante-inventory. Those positions are known to already match the 'posting' spec. Returns: A triple of booked_reductions \u2013 A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. errors: A list of errors to be generated. insufficient: A boolean, true if we could not find enough matches to fulfill the reduction. Source code in beancount/parser/booking_method.py def booking_method_STRICT(entry, posting, matches): \"\"\"Strict booking method. Args: entry: The parent Transaction instance. posting: An instance of Posting, the reducing posting which we're attempting to match. matches: A list of matching Position instances from the ante-inventory. Those positions are known to already match the 'posting' spec. Returns: A triple of booked_reductions: A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. errors: A list of errors to be generated. insufficient: A boolean, true if we could not find enough matches to fulfill the reduction. \"\"\" booked_reductions = [] booked_matches = [] errors = [] insufficient = False # In strict mode, we require at most a single matching posting. if len(matches) > 1: # If the total requested to reduce matches the sum of all the # ambiguous postings, match against all of them. sum_matches = sum(p.units.number for p in matches) if sum_matches == -posting.units.number: booked_reductions.extend( posting._replace(units=-match.units, cost=match.cost) for match in matches) else: errors.append( AmbiguousMatchError(entry.meta, 'Ambiguous matches for \"{}\": {}'.format( position.to_string(posting), ', '.join(position.to_string(match_posting) for match_posting in matches)), entry)) else: # Replace the posting's units and cost values. match = matches[0] sign = -1 if posting.units.number < ZERO else 1 number = min(abs(match.units.number), abs(posting.units.number)) match_units = Amount(number * sign, match.units.currency) booked_reductions.append(posting._replace(units=match_units, cost=match.cost)) booked_matches.append(match) insufficient = (match_units.number != posting.units.number) return booked_reductions, booked_matches, errors, insufficient beancount.parser.booking_method.handle_ambiguous_matches(entry, posting, matches, method) \uf0c1 Handle ambiguous matches by dispatching to a particular method. Parameters: entry \u2013 The parent Transaction instance. posting \u2013 An instance of Posting, the reducing posting which we're attempting to match. matches \u2013 A list of matching Position instances from the ante-inventory. Those positions are known to already match the 'posting' spec. methods \u2013 A mapping of account name to their corresponding booking method. Returns: A pair of booked_reductions \u2013 A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. errors: A list of errors to be generated. Source code in beancount/parser/booking_method.py def handle_ambiguous_matches(entry, posting, matches, method): \"\"\"Handle ambiguous matches by dispatching to a particular method. Args: entry: The parent Transaction instance. posting: An instance of Posting, the reducing posting which we're attempting to match. matches: A list of matching Position instances from the ante-inventory. Those positions are known to already match the 'posting' spec. methods: A mapping of account name to their corresponding booking method. Returns: A pair of booked_reductions: A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. errors: A list of errors to be generated. \"\"\" assert isinstance(method, Booking), ( \"Invalid type: {}\".format(method)) assert matches, \"Internal error: Invalid call with no matches\" #method = globals()['booking_method_{}'.format(method.name)] method = _BOOKING_METHODS[method] (booked_reductions, booked_matches, errors, insufficient) = method(entry, posting, matches) if insufficient: errors.append( AmbiguousMatchError(entry.meta, 'Not enough lots to reduce \"{}\": {}'.format( position.to_string(posting), ', '.join(position.to_string(match_posting) for match_posting in matches)), entry)) return booked_reductions, booked_matches, errors beancount.parser.cmptest \uf0c1 Support utilities for testing scripts. beancount.parser.cmptest.TestError ( Exception ) \uf0c1 Errors within the test implementation itself. These should never occur. beancount.parser.cmptest.assertEqualEntries(expected_entries, actual_entries, failfunc=, allow_incomplete=False) \uf0c1 Compare two lists of entries exactly and print missing entries verbosely if they occur. Parameters: expected_entries \u2013 Either a list of directives or a string, in which case the string is run through beancount.parser.parse_string() and the resulting list is used. actual_entries \u2013 Same treatment as expected_entries, the other list of directives to compare to. failfunc \u2013 A function to call on failure. allow_incomplete \u2013 A boolean, true if we allow incomplete inputs and perform light-weight booking. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/parser/cmptest.py def assertEqualEntries(expected_entries, actual_entries, failfunc=DEFAULT_FAILFUNC, allow_incomplete=False): \"\"\"Compare two lists of entries exactly and print missing entries verbosely if they occur. Args: expected_entries: Either a list of directives or a string, in which case the string is run through beancount.parser.parse_string() and the resulting list is used. actual_entries: Same treatment as expected_entries, the other list of directives to compare to. failfunc: A function to call on failure. allow_incomplete: A boolean, true if we allow incomplete inputs and perform light-weight booking. Raises: AssertionError: If the exception fails. \"\"\" expected_entries = read_string_or_entries(expected_entries, allow_incomplete) actual_entries = read_string_or_entries(actual_entries, allow_incomplete) same, expected_missing, actual_missing = compare.compare_entries(expected_entries, actual_entries) if not same: assert expected_missing or actual_missing, \"Missing is missing: {}, {}\".format( expected_missing, actual_missing) oss = io.StringIO() if expected_missing: oss.write(\"Present in expected set and not in actual set:\\n\\n\") for entry in expected_missing: oss.write(printer.format_entry(entry)) oss.write('\\n') if actual_missing: oss.write(\"Present in actual set and not in expected set:\\n\\n\") for entry in actual_missing: oss.write(printer.format_entry(entry)) oss.write('\\n') failfunc(oss.getvalue()) beancount.parser.cmptest.assertExcludesEntries(subset_entries, entries, failfunc=, allow_incomplete=False) \uf0c1 Check that subset_entries is not included in entries and print extra entries. Parameters: subset_entries \u2013 Either a list of directives or a string, in which case the string is run through beancount.parser.parse_string() and the resulting list is used. entries \u2013 Same treatment as subset_entries, the other list of directives to compare to. failfunc \u2013 A function to call on failure. allow_incomplete \u2013 A boolean, true if we allow incomplete inputs and perform light-weight booking. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/parser/cmptest.py def assertExcludesEntries(subset_entries, entries, failfunc=DEFAULT_FAILFUNC, allow_incomplete=False): \"\"\"Check that subset_entries is not included in entries and print extra entries. Args: subset_entries: Either a list of directives or a string, in which case the string is run through beancount.parser.parse_string() and the resulting list is used. entries: Same treatment as subset_entries, the other list of directives to compare to. failfunc: A function to call on failure. allow_incomplete: A boolean, true if we allow incomplete inputs and perform light-weight booking. Raises: AssertionError: If the exception fails. \"\"\" subset_entries = read_string_or_entries(subset_entries, allow_incomplete) entries = read_string_or_entries(entries) excludes, extra = compare.excludes_entries(subset_entries, entries) if not excludes: assert extra, \"Extra is empty: {}\".format(extra) oss = io.StringIO() if extra: oss.write(\"Extra from from first/excluded set:\\n\\n\") for entry in extra: oss.write(printer.format_entry(entry)) oss.write('\\n') failfunc(oss.getvalue()) beancount.parser.cmptest.assertIncludesEntries(subset_entries, entries, failfunc=, allow_incomplete=False) \uf0c1 Check that subset_entries is included in entries and print missing entries. Parameters: subset_entries \u2013 Either a list of directives or a string, in which case the string is run through beancount.parser.parse_string() and the resulting list is used. entries \u2013 Same treatment as subset_entries, the other list of directives to compare to. failfunc \u2013 A function to call on failure. allow_incomplete \u2013 A boolean, true if we allow incomplete inputs and perform light-weight booking. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/parser/cmptest.py def assertIncludesEntries(subset_entries, entries, failfunc=DEFAULT_FAILFUNC, allow_incomplete=False): \"\"\"Check that subset_entries is included in entries and print missing entries. Args: subset_entries: Either a list of directives or a string, in which case the string is run through beancount.parser.parse_string() and the resulting list is used. entries: Same treatment as subset_entries, the other list of directives to compare to. failfunc: A function to call on failure. allow_incomplete: A boolean, true if we allow incomplete inputs and perform light-weight booking. Raises: AssertionError: If the exception fails. \"\"\" subset_entries = read_string_or_entries(subset_entries, allow_incomplete) entries = read_string_or_entries(entries) includes, missing = compare.includes_entries(subset_entries, entries) if not includes: assert missing, \"Missing is empty: {}\".format(missing) oss = io.StringIO() if missing: oss.write(\"Missing from from expected set:\\n\\n\") for entry in missing: oss.write(printer.format_entry(entry)) oss.write('\\n') failfunc(oss.getvalue()) beancount.parser.cmptest.read_string_or_entries(entries_or_str, allow_incomplete=False) \uf0c1 Read a string of entries or just entries. Parameters: entries_or_str \u2013 Either a list of directives, or a string containing directives. allow_incomplete \u2013 A boolean, true if we allow incomplete inputs and perform light-weight booking. Returns: A list of directives. Source code in beancount/parser/cmptest.py def read_string_or_entries(entries_or_str, allow_incomplete=False): \"\"\"Read a string of entries or just entries. Args: entries_or_str: Either a list of directives, or a string containing directives. allow_incomplete: A boolean, true if we allow incomplete inputs and perform light-weight booking. Returns: A list of directives. \"\"\" if isinstance(entries_or_str, str): entries, errors, options_map = parser.parse_string( textwrap.dedent(entries_or_str)) if allow_incomplete: # Do a simplistic local conversion in order to call the comparison. entries = [_local_booking(entry) for entry in entries] else: # Don't accept incomplete entries either. if any(parser.is_entry_incomplete(entry) for entry in entries): raise TestError(\"Entries in assertions may not use interpolation.\") entries, booking_errors = booking.book(entries, options_map) errors = errors + booking_errors # Don't tolerate errors. if errors: oss = io.StringIO() printer.print_errors(errors, file=oss) raise TestError(\"Unexpected errors in expected: {}\".format(oss.getvalue())) else: assert isinstance(entries_or_str, list), \"Expecting list: {}\".format(entries_or_str) entries = entries_or_str return entries beancount.parser.grammar \uf0c1 Builder for Beancount grammar. beancount.parser.grammar.Builder ( LexBuilder ) \uf0c1 A builder used by the lexer and grammar parser as callbacks to create the data objects corresponding to rules parsed from the input file. beancount.parser.grammar.Builder.amount(self, number, currency) \uf0c1 Process an amount grammar rule. Parameters: number \u2013 a Decimal instance, the number of the amount. currency \u2013 a currency object (a str, really, see CURRENCY above) Returns: An instance of Amount. Source code in beancount/parser/grammar.py def amount(self, number, currency): \"\"\"Process an amount grammar rule. Args: number: a Decimal instance, the number of the amount. currency: a currency object (a str, really, see CURRENCY above) Returns: An instance of Amount. \"\"\" # Update the mapping that stores the parsed precisions. # Note: This is relatively slow, adds about 70ms because of number.as_tuple(). self.dcupdate(number, currency) return Amount(number, currency) beancount.parser.grammar.Builder.balance(self, filename, lineno, date, account, amount, tolerance, kvlist) \uf0c1 Process an assertion directive. We produce no errors here by default. We replace the failing ones in the routine that does the verification later one, that these have succeeded or failed. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account to balance. amount \u2013 The expected amount, to be checked. tolerance \u2013 The tolerance number. kvlist \u2013 a list of KeyValue instances. Returns: A new Balance object. Source code in beancount/parser/grammar.py def balance(self, filename, lineno, date, account, amount, tolerance, kvlist): \"\"\"Process an assertion directive. We produce no errors here by default. We replace the failing ones in the routine that does the verification later one, that these have succeeded or failed. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to balance. amount: The expected amount, to be checked. tolerance: The tolerance number. kvlist: a list of KeyValue instances. Returns: A new Balance object. \"\"\" diff_amount = None meta = new_metadata(filename, lineno, kvlist) return Balance(meta, date, account, amount, tolerance, diff_amount) beancount.parser.grammar.Builder.build_grammar_error(self, filename, lineno, exc_value, exc_type=None, exc_traceback=None) \uf0c1 Build a grammar error and appends it to the list of pending errors. Parameters: filename \u2013 The current filename lineno \u2013 The current line number excvalue \u2013 The exception value, or a str, the message of the error. exc_type \u2013 An exception type, if an exception occurred. exc_traceback \u2013 A traceback object. Source code in beancount/parser/grammar.py def build_grammar_error(self, filename, lineno, exc_value, exc_type=None, exc_traceback=None): \"\"\"Build a grammar error and appends it to the list of pending errors. Args: filename: The current filename lineno: The current line number excvalue: The exception value, or a str, the message of the error. exc_type: An exception type, if an exception occurred. exc_traceback: A traceback object. \"\"\" if exc_type is not None: assert not isinstance(exc_value, str) strings = traceback.format_exception_only(exc_type, exc_value) tblist = traceback.extract_tb(exc_traceback) filename, lineno, _, __ = tblist[0] message = '{} ({}:{})'.format(strings[0], filename, lineno) else: message = str(exc_value) meta = new_metadata(filename, lineno) self.errors.append( ParserSyntaxError(meta, message, None)) beancount.parser.grammar.Builder.close(self, filename, lineno, date, account, kvlist) \uf0c1 Process a close directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the name of the account. kvlist \u2013 a list of KeyValue instances. Returns: A new Close object. Source code in beancount/parser/grammar.py def close(self, filename, lineno, date, account, kvlist): \"\"\"Process a close directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the name of the account. kvlist: a list of KeyValue instances. Returns: A new Close object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Close(meta, date, account) beancount.parser.grammar.Builder.commodity(self, filename, lineno, date, currency, kvlist) \uf0c1 Process a close directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. currency \u2013 A string, the commodity being declared. kvlist \u2013 a list of KeyValue instances. Returns: A new Close object. Source code in beancount/parser/grammar.py def commodity(self, filename, lineno, date, currency, kvlist): \"\"\"Process a close directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. currency: A string, the commodity being declared. kvlist: a list of KeyValue instances. Returns: A new Close object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Commodity(meta, date, currency) beancount.parser.grammar.Builder.compound_amount(self, number_per, number_total, currency) \uf0c1 Process an amount grammar rule. Parameters: number_per \u2013 a Decimal instance, the number of the cost per share. number_total \u2013 a Decimal instance, the number of the cost over all shares. currency \u2013 a currency object (a str, really, see CURRENCY above) Returns: A triple of (Decimal, Decimal, currency string) to be processed further when creating the final per-unit cost number. Source code in beancount/parser/grammar.py def compound_amount(self, number_per, number_total, currency): \"\"\"Process an amount grammar rule. Args: number_per: a Decimal instance, the number of the cost per share. number_total: a Decimal instance, the number of the cost over all shares. currency: a currency object (a str, really, see CURRENCY above) Returns: A triple of (Decimal, Decimal, currency string) to be processed further when creating the final per-unit cost number. \"\"\" # Update the mapping that stores the parsed precisions. # Note: This is relatively slow, adds about 70ms because of number.as_tuple(). self.dcupdate(number_per, currency) self.dcupdate(number_total, currency) # Note that we are not able to reduce the value to a number per-share # here because we only get the number of units in the full lot spec. return CompoundAmount(number_per, number_total, currency) beancount.parser.grammar.Builder.cost_merge(self, _) \uf0c1 Create a 'merge cost' token. Source code in beancount/parser/grammar.py def cost_merge(self, _): \"\"\"Create a 'merge cost' token.\"\"\" return MERGE_COST beancount.parser.grammar.Builder.cost_spec(self, cost_comp_list, is_total) \uf0c1 Process a cost_spec grammar rule. Parameters: cost_comp_list \u2013 A list of CompoundAmount, a datetime.date, or label ID strings. is_total \u2013 Assume only the total cost is specified; reject the # syntax, that is, no compound amounts may be specified. This is used to support the {{...}} syntax. Returns: A cost-info tuple of CompoundAmount, lot date and label string. Any of these may be set to a sentinel indicating \"unset\". Source code in beancount/parser/grammar.py def cost_spec(self, cost_comp_list, is_total): \"\"\"Process a cost_spec grammar rule. Args: cost_comp_list: A list of CompoundAmount, a datetime.date, or label ID strings. is_total: Assume only the total cost is specified; reject the # syntax, that is, no compound amounts may be specified. This is used to support the {{...}} syntax. Returns: A cost-info tuple of CompoundAmount, lot date and label string. Any of these may be set to a sentinel indicating \"unset\". \"\"\" if not cost_comp_list: return CostSpec(MISSING, None, MISSING, None, None, False) assert isinstance(cost_comp_list, list), ( \"Internal error in parser: {}\".format(cost_comp_list)) compound_cost = None date_ = None label = None merge = None for comp in cost_comp_list: if isinstance(comp, CompoundAmount): if compound_cost is None: compound_cost = comp else: self.errors.append( ParserError(self.get_lexer_location(), \"Duplicate cost: '{}'.\".format(comp), None)) elif isinstance(comp, date): if date_ is None: date_ = comp else: self.errors.append( ParserError(self.get_lexer_location(), \"Duplicate date: '{}'.\".format(comp), None)) elif comp is MERGE_COST: if merge is None: merge = True self.errors.append( ParserError(self.get_lexer_location(), \"Cost merging is not supported yet\", None)) else: self.errors.append( ParserError(self.get_lexer_location(), \"Duplicate merge-cost spec\", None)) else: assert isinstance(comp, str), ( \"Currency component is not string: '{}'\".format(comp)) if label is None: label = comp else: self.errors.append( ParserError(self.get_lexer_location(), \"Duplicate label: '{}'.\".format(comp), None)) # If there was a cost_comp_list, thus a \"{...}\" cost basis spec, you must # indicate that by creating a CompoundAmount(), always. if compound_cost is None: number_per, number_total, currency = MISSING, None, MISSING else: number_per, number_total, currency = compound_cost if is_total: if number_total is not None: self.errors.append( ParserError( self.get_lexer_location(), (\"Per-unit cost may not be specified using total cost \" \"syntax: '{}'; ignoring per-unit cost\").format(compound_cost), None)) # Ignore per-unit number. number_per = ZERO else: # There's a single number specified; interpret it as a total cost. number_total = number_per number_per = ZERO if merge is None: merge = False return CostSpec(number_per, number_total, currency, date_, label, merge) beancount.parser.grammar.Builder.custom(self, filename, lineno, date, dir_type, custom_values, kvlist) \uf0c1 Process a custom directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. dir_type \u2013 A string, a type for the custom directive being parsed. custom_values \u2013 A list of the various tokens seen on the same line. kvlist \u2013 a list of KeyValue instances. Returns: A new Custom object. Source code in beancount/parser/grammar.py def custom(self, filename, lineno, date, dir_type, custom_values, kvlist): \"\"\"Process a custom directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. dir_type: A string, a type for the custom directive being parsed. custom_values: A list of the various tokens seen on the same line. kvlist: a list of KeyValue instances. Returns: A new Custom object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Custom(meta, date, dir_type, custom_values) beancount.parser.grammar.Builder.custom_value(self, value, dtype=None) \uf0c1 Create a custom value object, along with its type. Parameters: value \u2013 One of the accepted custom values. Returns: A pair of (value, dtype) where 'dtype' is the datatype is that of the value. Source code in beancount/parser/grammar.py def custom_value(self, value, dtype=None): \"\"\"Create a custom value object, along with its type. Args: value: One of the accepted custom values. Returns: A pair of (value, dtype) where 'dtype' is the datatype is that of the value. \"\"\" if dtype is None: dtype = type(value) return ValueType(value, dtype) beancount.parser.grammar.Builder.dcupdate(self, number, currency) \uf0c1 Update the display context. Source code in beancount/parser/grammar.py def dcupdate(self, number, currency): \"\"\"Update the display context.\"\"\" if isinstance(number, Decimal) and currency and currency is not MISSING: self._dcupdate(number, currency) beancount.parser.grammar.Builder.document(self, filename, lineno, date, account, document_filename, tags_links, kvlist) \uf0c1 Process a document directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. account \u2013 an Account instance. document_filename \u2013 a str, the name of the document file. tags_links \u2013 The current TagsLinks accumulator. kvlist \u2013 a list of KeyValue instances. Returns: A new Document object. Source code in beancount/parser/grammar.py def document(self, filename, lineno, date, account, document_filename, tags_links, kvlist): \"\"\"Process a document directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. account: an Account instance. document_filename: a str, the name of the document file. tags_links: The current TagsLinks accumulator. kvlist: a list of KeyValue instances. Returns: A new Document object. \"\"\" meta = new_metadata(filename, lineno, kvlist) if not path.isabs(document_filename): document_filename = path.abspath(path.join(path.dirname(filename), document_filename)) tags, links = self.finalize_tags_links(tags_links.tags, tags_links.links) return Document(meta, date, account, document_filename, tags, links) beancount.parser.grammar.Builder.event(self, filename, lineno, date, event_type, description, kvlist) \uf0c1 Process an event directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. event_type \u2013 a str, the name of the event type. description \u2013 a str, the event value, the contents. kvlist \u2013 a list of KeyValue instances. Returns: A new Event object. Source code in beancount/parser/grammar.py def event(self, filename, lineno, date, event_type, description, kvlist): \"\"\"Process an event directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. event_type: a str, the name of the event type. description: a str, the event value, the contents. kvlist: a list of KeyValue instances. Returns: A new Event object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Event(meta, date, event_type, description) beancount.parser.grammar.Builder.finalize(self) \uf0c1 Finalize the parser, check for final errors and return the triple. Returns: A triple of entries \u2013 A list of parsed directives, which may need completion. errors: A list of errors, hopefully empty. options_map: A dict of options. Source code in beancount/parser/grammar.py def finalize(self): \"\"\"Finalize the parser, check for final errors and return the triple. Returns: A triple of entries: A list of parsed directives, which may need completion. errors: A list of errors, hopefully empty. options_map: A dict of options. \"\"\" # If the user left some tags unbalanced, issue an error. for tag in self.tags: meta = new_metadata(self.options['filename'], 0) self.errors.append( ParserError(meta, \"Unbalanced pushed tag: '{}'\".format(tag), None)) # If the user left some metadata unpopped, issue an error. for key, value_list in self.meta.items(): meta = new_metadata(self.options['filename'], 0) self.errors.append( ParserError(meta, ( \"Unbalanced metadata key '{}'; leftover metadata '{}'\").format( key, ', '.join(value_list)), None)) # Weave the commas option in the DisplayContext itself, so it propagates # everywhere it is used automatically. self.dcontext.set_commas(self.options['render_commas']) return (self.get_entries(), self.errors, self.get_options()) beancount.parser.grammar.Builder.finalize_tags_links(self, tags, links) \uf0c1 Finally amend tags and links and return final objects to be inserted. Parameters: tags \u2013 A set of tag strings (warning: this gets mutated in-place). links \u2013 A set of link strings. Returns: A sanitized pair of (tags, links). Source code in beancount/parser/grammar.py def finalize_tags_links(self, tags, links): \"\"\"Finally amend tags and links and return final objects to be inserted. Args: tags: A set of tag strings (warning: this gets mutated in-place). links: A set of link strings. Returns: A sanitized pair of (tags, links). \"\"\" if self.tags: tags.update(self.tags) return (frozenset(tags) if tags else EMPTY_SET, frozenset(links) if links else EMPTY_SET) beancount.parser.grammar.Builder.get_entries(self) \uf0c1 Return the accumulated entries. Returns: A list of sorted directives. Source code in beancount/parser/grammar.py def get_entries(self): \"\"\"Return the accumulated entries. Returns: A list of sorted directives. \"\"\" return sorted(self.entries, key=data.entry_sortkey) beancount.parser.grammar.Builder.get_invalid_account(self) \uf0c1 See base class. Source code in beancount/parser/grammar.py def get_invalid_account(self): \"\"\"See base class.\"\"\" return account.join(self.options['name_equity'], 'InvalidAccountName') beancount.parser.grammar.Builder.get_long_string_maxlines(self) \uf0c1 See base class. Source code in beancount/parser/grammar.py def get_long_string_maxlines(self): \"\"\"See base class.\"\"\" return self.options['long_string_maxlines'] beancount.parser.grammar.Builder.get_options(self) \uf0c1 Return the final options map. Returns: A dict of option names to options. Source code in beancount/parser/grammar.py def get_options(self): \"\"\"Return the final options map. Returns: A dict of option names to options. \"\"\" # Build and store the inferred DisplayContext instance. self.options['dcontext'] = self.dcontext # Add the full list of seen commodities. # # IMPORTANT: This is currently where the list of all commodities seen # from the parser lives. The # beancount.core.getters.get_commodities_map() routine uses this to # automatically generate a full list of directives. An alternative would # be to implement a plugin that enforces the generate of these # post-parsing so that they are always guaranteed to live within the # flow of entries. This would allow us to keep all the data in that list # of entries and to avoid depending on the options to store that output. self.options['commodities'] = self.commodities return self.options beancount.parser.grammar.Builder.handle_list(self, object_list, new_object) \uf0c1 Handle a recursive list grammar rule, generically. Parameters: object_list \u2013 the current list of objects. new_object \u2013 the new object to be added. Returns: The new, updated list of objects. Source code in beancount/parser/grammar.py def handle_list(self, object_list, new_object): \"\"\"Handle a recursive list grammar rule, generically. Args: object_list: the current list of objects. new_object: the new object to be added. Returns: The new, updated list of objects. \"\"\" if object_list is None: object_list = [] if new_object is not None: object_list.append(new_object) return object_list beancount.parser.grammar.Builder.include(self, filename, lineno, include_filename) \uf0c1 Process an include directive. Parameters: filename \u2013 current filename. lineno \u2013 current line number. include_name \u2013 A string, the name of the file to include. Source code in beancount/parser/grammar.py def include(self, filename, lineno, include_filename): \"\"\"Process an include directive. Args: filename: current filename. lineno: current line number. include_name: A string, the name of the file to include. \"\"\" self.options['include'].append(include_filename) beancount.parser.grammar.Builder.key_value(self, key, value) \uf0c1 Process a document directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account the document relates to. document_filename \u2013 A str, the name of the document file. Returns: A new KeyValue object. Source code in beancount/parser/grammar.py def key_value(self, key, value): \"\"\"Process a document directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account the document relates to. document_filename: A str, the name of the document file. Returns: A new KeyValue object. \"\"\" return KeyValue(key, value) beancount.parser.grammar.Builder.note(self, filename, lineno, date, account, comment, kvlist) \uf0c1 Process a note directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account to attach the note to. comment \u2013 A str, the note's comments contents. kvlist \u2013 a list of KeyValue instances. Returns: A new Note object. Source code in beancount/parser/grammar.py def note(self, filename, lineno, date, account, comment, kvlist): \"\"\"Process a note directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to attach the note to. comment: A str, the note's comments contents. kvlist: a list of KeyValue instances. Returns: A new Note object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Note(meta, date, account, comment) beancount.parser.grammar.Builder.open(self, filename, lineno, date, account, currencies, booking_str, kvlist) \uf0c1 Process an open directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the name of the account. currencies \u2013 A list of constraint currencies. booking_str \u2013 A string, the booking method, or None if none was specified. kvlist \u2013 a list of KeyValue instances. Returns: A new Open object. Source code in beancount/parser/grammar.py def open(self, filename, lineno, date, account, currencies, booking_str, kvlist): \"\"\"Process an open directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the name of the account. currencies: A list of constraint currencies. booking_str: A string, the booking method, or None if none was specified. kvlist: a list of KeyValue instances. Returns: A new Open object. \"\"\" meta = new_metadata(filename, lineno, kvlist) error = False if booking_str: try: # Note: Somehow the 'in' membership operator is not defined on Enum. booking = Booking[booking_str] except KeyError: # If the per-account method is invalid, set it to the global # default method and continue. booking = self.options['booking_method'] error = True else: booking = None entry = Open(meta, date, account, currencies, booking) if error: self.errors.append(ParserError(meta, \"Invalid booking method: {}\".format(booking_str), entry)) return entry beancount.parser.grammar.Builder.option(self, filename, lineno, key, value) \uf0c1 Process an option directive. Parameters: filename \u2013 current filename. lineno \u2013 current line number. key \u2013 option's key (str) value \u2013 option's value Source code in beancount/parser/grammar.py def option(self, filename, lineno, key, value): \"\"\"Process an option directive. Args: filename: current filename. lineno: current line number. key: option's key (str) value: option's value \"\"\" if key not in self.options: meta = new_metadata(filename, lineno) self.errors.append( ParserError(meta, \"Invalid option: '{}'\".format(key), None)) elif key in options.READ_ONLY_OPTIONS: meta = new_metadata(filename, lineno) self.errors.append( ParserError(meta, \"Option '{}' may not be set\".format(key), None)) else: option_descriptor = options.OPTIONS[key] # Issue a warning if the option is deprecated. if option_descriptor.deprecated: assert isinstance(option_descriptor.deprecated, str), \"Internal error.\" meta = new_metadata(filename, lineno) self.errors.append( DeprecatedError(meta, option_descriptor.deprecated, None)) # Rename the option if it has an alias. if option_descriptor.alias: key = option_descriptor.alias option_descriptor = options.OPTIONS[key] # Convert the value, if necessary. if option_descriptor.converter: try: value = option_descriptor.converter(value) except ValueError as exc: meta = new_metadata(filename, lineno) self.errors.append( ParserError(meta, \"Error for option '{}': {}\".format(key, exc), None)) return option = self.options[key] if isinstance(option, list): # Append to a list of values. option.append(value) elif isinstance(option, dict): # Set to a dict of values. if not (isinstance(value, tuple) and len(value) == 2): self.errors.append( ParserError( meta, \"Error for option '{}': {}\".format(key, value), None)) return dict_key, dict_value = value option[dict_key] = dict_value elif isinstance(option, bool): # Convert to a boolean. if not isinstance(value, bool): value = (value.lower() in {'true', 'on'}) or (value == '1') self.options[key] = value else: # Set the value. self.options[key] = value # Refresh the list of valid account regexps as we go along. if key.startswith('name_'): # Update the set of valid account types. self.account_regexp = valid_account_regexp(self.options) elif key == 'insert_pythonpath': # Insert the PYTHONPATH to this file when and only if you # encounter this option. sys.path.insert(0, path.dirname(filename)) beancount.parser.grammar.Builder.pad(self, filename, lineno, date, account, source_account, kvlist) \uf0c1 Process a pad directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account to be padded. source_account \u2013 A string, the account to pad from. kvlist \u2013 a list of KeyValue instances. Returns: A new Pad object. Source code in beancount/parser/grammar.py def pad(self, filename, lineno, date, account, source_account, kvlist): \"\"\"Process a pad directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to be padded. source_account: A string, the account to pad from. kvlist: a list of KeyValue instances. Returns: A new Pad object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Pad(meta, date, account, source_account) beancount.parser.grammar.Builder.pipe_deprecated_error(self, filename, lineno) \uf0c1 Issue a 'Pipe deprecated' error. Parameters: filename \u2013 The current filename lineno \u2013 The current line number Source code in beancount/parser/grammar.py def pipe_deprecated_error(self, filename, lineno): \"\"\"Issue a 'Pipe deprecated' error. Args: filename: The current filename lineno: The current line number \"\"\" if self.options['allow_pipe_separator']: return meta = new_metadata(filename, lineno) self.errors.append( ParserSyntaxError(meta, \"Pipe symbol is deprecated.\", None)) beancount.parser.grammar.Builder.plugin(self, filename, lineno, plugin_name, plugin_config) \uf0c1 Process a plugin directive. Parameters: filename \u2013 current filename. lineno \u2013 current line number. plugin_name \u2013 A string, the name of the plugin module to import. plugin_config \u2013 A string or None, an optional configuration string to pass in to the plugin module. Source code in beancount/parser/grammar.py def plugin(self, filename, lineno, plugin_name, plugin_config): \"\"\"Process a plugin directive. Args: filename: current filename. lineno: current line number. plugin_name: A string, the name of the plugin module to import. plugin_config: A string or None, an optional configuration string to pass in to the plugin module. \"\"\" self.options['plugin'].append((plugin_name, plugin_config)) beancount.parser.grammar.Builder.popmeta(self, key) \uf0c1 Removed a key off the current set of stacks. Parameters: key \u2013 A string, a key to be removed from the meta dict. Source code in beancount/parser/grammar.py def popmeta(self, key): \"\"\"Removed a key off the current set of stacks. Args: key: A string, a key to be removed from the meta dict. \"\"\" try: if key not in self.meta: raise IndexError value_list = self.meta[key] value_list.pop(-1) if not value_list: self.meta.pop(key) except IndexError: meta = new_metadata(self.options['filename'], 0) self.errors.append( ParserError(meta, \"Attempting to pop absent metadata key: '{}'\".format(key), None)) beancount.parser.grammar.Builder.poptag(self, tag) \uf0c1 Pop a tag off the current set of stacks. Parameters: tag \u2013 A string, a tag to be removed from the current set of tags. Source code in beancount/parser/grammar.py def poptag(self, tag): \"\"\"Pop a tag off the current set of stacks. Args: tag: A string, a tag to be removed from the current set of tags. \"\"\" try: self.tags.remove(tag) except ValueError: meta = new_metadata(self.options['filename'], 0) self.errors.append( ParserError(meta, \"Attempting to pop absent tag: '{}'\".format(tag), None)) beancount.parser.grammar.Builder.posting(self, filename, lineno, account, units, cost, price, istotal, flag) \uf0c1 Process a posting grammar rule. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. account \u2013 A string, the account of the posting. units \u2013 An instance of Amount for the units. cost \u2013 An instance of CostSpec for the cost. price \u2013 Either None, or an instance of Amount that is the cost of the position. istotal \u2013 A bool, True if the price is for the total amount being parsed, or False if the price is for each lot of the position. flag \u2013 A string, one-character, the flag associated with this posting. Returns: A new Posting object, with no parent entry. Source code in beancount/parser/grammar.py def posting(self, filename, lineno, account, units, cost, price, istotal, flag): \"\"\"Process a posting grammar rule. Args: filename: the current filename. lineno: the current line number. account: A string, the account of the posting. units: An instance of Amount for the units. cost: An instance of CostSpec for the cost. price: Either None, or an instance of Amount that is the cost of the position. istotal: A bool, True if the price is for the total amount being parsed, or False if the price is for each lot of the position. flag: A string, one-character, the flag associated with this posting. Returns: A new Posting object, with no parent entry. \"\"\" meta = new_metadata(filename, lineno) # Prices may not be negative. if price and isinstance(price.number, Decimal) and price.number < ZERO: self.errors.append( ParserError(meta, ( \"Negative prices are not allowed: {} \" \"(see http://furius.ca/beancount/doc/bug-negative-prices \" \"for workaround)\" ).format(price), None)) # Fix it and continue. price = Amount(abs(price.number), price.currency) # If the price is specified for the entire amount, compute the effective # price here and forget about that detail of the input syntax. if istotal: if units.number == ZERO: number = ZERO else: number = price.number if number is not MISSING: number = number/abs(units.number) price = Amount(number, price.currency) # Note: Allow zero prices because we need them for round-trips for # conversion entries. # # if price is not None and price.number == ZERO: # self.errors.append( # ParserError(meta, \"Price is zero: {}\".format(price), None)) # If both cost and price are specified, the currencies must match, or # that is an error. if (cost is not None and price is not None and isinstance(cost.currency, str) and isinstance(price.currency, str) and cost.currency != price.currency): self.errors.append( ParserError(meta, \"Cost and price currencies must match: {} != {}\".format( cost.currency, price.currency), None)) return Posting(account, units, cost, price, chr(flag) if flag else None, meta) beancount.parser.grammar.Builder.price(self, filename, lineno, date, currency, amount, kvlist) \uf0c1 Process a price directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. currency \u2013 the currency to be priced. amount \u2013 an instance of Amount, that is the price of the currency. kvlist \u2013 a list of KeyValue instances. Returns: A new Price object. Source code in beancount/parser/grammar.py def price(self, filename, lineno, date, currency, amount, kvlist): \"\"\"Process a price directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. currency: the currency to be priced. amount: an instance of Amount, that is the price of the currency. kvlist: a list of KeyValue instances. Returns: A new Price object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Price(meta, date, currency, amount) beancount.parser.grammar.Builder.pushmeta(self, key, value) \uf0c1 Set a metadata field on the current key-value pairs to be added to transactions. Parameters: key_value \u2013 A KeyValue instance, to be added to the dict of metadata. Source code in beancount/parser/grammar.py def pushmeta(self, key, value): \"\"\"Set a metadata field on the current key-value pairs to be added to transactions. Args: key_value: A KeyValue instance, to be added to the dict of metadata. \"\"\" self.meta[key].append(value) beancount.parser.grammar.Builder.pushtag(self, tag) \uf0c1 Push a tag on the current set of tags. Note that this does not need to be stack ordered. Parameters: tag \u2013 A string, a tag to be added. Source code in beancount/parser/grammar.py def pushtag(self, tag): \"\"\"Push a tag on the current set of tags. Note that this does not need to be stack ordered. Args: tag: A string, a tag to be added. \"\"\" self.tags.append(tag) beancount.parser.grammar.Builder.query(self, filename, lineno, date, query_name, query_string, kvlist) \uf0c1 Process a document directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. query_name \u2013 a str, the name of the query. query_string \u2013 a str, the SQL query itself. kvlist \u2013 a list of KeyValue instances. Returns: A new Query object. Source code in beancount/parser/grammar.py def query(self, filename, lineno, date, query_name, query_string, kvlist): \"\"\"Process a document directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. query_name: a str, the name of the query. query_string: a str, the SQL query itself. kvlist: a list of KeyValue instances. Returns: A new Query object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Query(meta, date, query_name, query_string) beancount.parser.grammar.Builder.store_result(self, entries) \uf0c1 Start rule stores the final result here. Parameters: entries \u2013 A list of entries to store. Source code in beancount/parser/grammar.py def store_result(self, entries): \"\"\"Start rule stores the final result here. Args: entries: A list of entries to store. \"\"\" if entries: self.entries = entries beancount.parser.grammar.Builder.tag_link_LINK(self, tags_links, link) \uf0c1 Add a link to the TagsLinks accumulator. Parameters: tags_links \u2013 The current TagsLinks accumulator. link \u2013 A string, the new link to insert. Returns: An updated TagsLinks instance. Source code in beancount/parser/grammar.py def tag_link_LINK(self, tags_links, link): \"\"\"Add a link to the TagsLinks accumulator. Args: tags_links: The current TagsLinks accumulator. link: A string, the new link to insert. Returns: An updated TagsLinks instance. \"\"\" tags_links.links.add(link) return tags_links beancount.parser.grammar.Builder.tag_link_STRING(self, tags_links, string) \uf0c1 Add a string to the TagsLinks accumulator. Parameters: tags_links \u2013 The current TagsLinks accumulator. string \u2013 A string, the new string to insert in the list. Returns: An updated TagsLinks instance. Source code in beancount/parser/grammar.py def tag_link_STRING(self, tags_links, string): \"\"\"Add a string to the TagsLinks accumulator. Args: tags_links: The current TagsLinks accumulator. string: A string, the new string to insert in the list. Returns: An updated TagsLinks instance. \"\"\" tags_links.strings.append(string) return tags_links beancount.parser.grammar.Builder.tag_link_TAG(self, tags_links, tag) \uf0c1 Add a tag to the TagsLinks accumulator. Parameters: tags_links \u2013 The current TagsLinks accumulator. tag \u2013 A string, the new tag to insert. Returns: An updated TagsLinks instance. Source code in beancount/parser/grammar.py def tag_link_TAG(self, tags_links, tag): \"\"\"Add a tag to the TagsLinks accumulator. Args: tags_links: The current TagsLinks accumulator. tag: A string, the new tag to insert. Returns: An updated TagsLinks instance. \"\"\" tags_links.tags.add(tag) return tags_links beancount.parser.grammar.Builder.tag_link_new(self, _) \uf0c1 Create a new TagsLinks instance. Returns: An instance of TagsLinks, initialized with expected attributes. Source code in beancount/parser/grammar.py def tag_link_new(self, _): \"\"\"Create a new TagsLinks instance. Returns: An instance of TagsLinks, initialized with expected attributes. \"\"\" return TagsLinks(set(), set()) beancount.parser.grammar.Builder.transaction(self, filename, lineno, date, flag, txn_strings, tags_links, posting_or_kv_list) \uf0c1 Process a transaction directive. All the postings of the transaction are available at this point, and so the the transaction is balanced here, incomplete postings are completed with the appropriate position, and errors are being accumulated on the builder to be reported later on. This is the main routine that takes up most of the parsing time; be very careful with modifications here, they have an impact on performance. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. flag \u2013 a str, one-character, the flag associated with this transaction. txn_strings \u2013 A list of strings, possibly empty, possibly longer. tags_links \u2013 A TagsLinks namedtuple of tags, and/or links. posting_or_kv_list \u2013 a list of Posting or KeyValue instances, to be inserted in this transaction, or None, if no postings have been declared. Returns: A new Transaction object. Source code in beancount/parser/grammar.py def transaction(self, filename, lineno, date, flag, txn_strings, tags_links, posting_or_kv_list): \"\"\"Process a transaction directive. All the postings of the transaction are available at this point, and so the the transaction is balanced here, incomplete postings are completed with the appropriate position, and errors are being accumulated on the builder to be reported later on. This is the main routine that takes up most of the parsing time; be very careful with modifications here, they have an impact on performance. Args: filename: the current filename. lineno: the current line number. date: a datetime object. flag: a str, one-character, the flag associated with this transaction. txn_strings: A list of strings, possibly empty, possibly longer. tags_links: A TagsLinks namedtuple of tags, and/or links. posting_or_kv_list: a list of Posting or KeyValue instances, to be inserted in this transaction, or None, if no postings have been declared. Returns: A new Transaction object. \"\"\" meta = new_metadata(filename, lineno) # Separate postings and key-values. explicit_meta = {} postings = [] tags, links = tags_links.tags, tags_links.links if posting_or_kv_list: last_posting = None for posting_or_kv in posting_or_kv_list: if isinstance(posting_or_kv, Posting): postings.append(posting_or_kv) last_posting = posting_or_kv elif isinstance(posting_or_kv, TagsLinks): if postings: self.errors.append(ParserError( meta, \"Tags or links not allowed after first \" + \"Posting: {}\".format(posting_or_kv), None)) else: tags.update(posting_or_kv.tags) links.update(posting_or_kv.links) else: if last_posting is None: value = explicit_meta.setdefault(posting_or_kv.key, posting_or_kv.value) if value is not posting_or_kv.value: self.errors.append(ParserError( meta, \"Duplicate metadata field on entry: {}\".format( posting_or_kv), None)) else: if last_posting.meta is None: last_posting = last_posting._replace(meta={}) postings.pop(-1) postings.append(last_posting) value = last_posting.meta.setdefault(posting_or_kv.key, posting_or_kv.value) if value is not posting_or_kv.value: self.errors.append(ParserError( meta, \"Duplicate posting metadata field: {}\".format( posting_or_kv), None)) # Freeze the tags & links or set to default empty values. tags, links = self.finalize_tags_links(tags, links) # Initialize the metadata fields from the set of active values. if self.meta: for key, value_list in self.meta.items(): meta[key] = value_list[-1] # Add on explicitly defined values. if explicit_meta: meta.update(explicit_meta) # Unpack the transaction fields. payee_narration = self.unpack_txn_strings(txn_strings, meta) if payee_narration is None: return None payee, narration = payee_narration # We now allow a single posting when its balance is zero, so we # commented out the check below. If a transaction has a single posting # with a non-zero balance, it'll get caught below in the booking code. # # # Detect when a transaction does not have at least two legs. # if postings is None or len(postings) < 2: # self.errors.append( # ParserError(meta, # \"Transaction with only one posting: {}\".format(postings), # None)) # return None # If there are no postings, make sure we insert a list object. if postings is None: postings = [] # Create the transaction. return Transaction(meta, date, chr(flag), payee, narration, tags, links, postings) beancount.parser.grammar.Builder.unpack_txn_strings(self, txn_strings, meta) \uf0c1 Unpack a tags_links accumulator to its payee and narration fields. Parameters: txn_strings \u2013 A list of strings. meta \u2013 A metadata dict for errors generated in this routine. Returns: A pair of (payee, narration) strings or None objects, or None, if there was an error. Source code in beancount/parser/grammar.py def unpack_txn_strings(self, txn_strings, meta): \"\"\"Unpack a tags_links accumulator to its payee and narration fields. Args: txn_strings: A list of strings. meta: A metadata dict for errors generated in this routine. Returns: A pair of (payee, narration) strings or None objects, or None, if there was an error. \"\"\" num_strings = 0 if txn_strings is None else len(txn_strings) if num_strings == 1: payee, narration = None, txn_strings[0] elif num_strings == 2: payee, narration = txn_strings elif num_strings == 0: payee, narration = None, \"\" else: self.errors.append( ParserError(meta, \"Too many strings on transaction description: {}\".format( txn_strings), None)) return None return payee, narration beancount.parser.grammar.CompoundAmount ( tuple ) \uf0c1 CompoundAmount(number_per, number_total, currency) beancount.parser.grammar.CompoundAmount.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.grammar.CompoundAmount.__new__(_cls, number_per, number_total, currency) special staticmethod \uf0c1 Create new instance of CompoundAmount(number_per, number_total, currency) beancount.parser.grammar.CompoundAmount.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.grammar.DeprecatedError ( tuple ) \uf0c1 DeprecatedError(source, message, entry) beancount.parser.grammar.DeprecatedError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.grammar.DeprecatedError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of DeprecatedError(source, message, entry) beancount.parser.grammar.DeprecatedError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.grammar.KeyValue ( tuple ) \uf0c1 KeyValue(key, value) beancount.parser.grammar.KeyValue.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.grammar.KeyValue.__new__(_cls, key, value) special staticmethod \uf0c1 Create new instance of KeyValue(key, value) beancount.parser.grammar.KeyValue.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.grammar.ParserError ( tuple ) \uf0c1 ParserError(source, message, entry) beancount.parser.grammar.ParserError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.grammar.ParserError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of ParserError(source, message, entry) beancount.parser.grammar.ParserError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.grammar.ParserSyntaxError ( tuple ) \uf0c1 ParserSyntaxError(source, message, entry) beancount.parser.grammar.ParserSyntaxError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.grammar.ParserSyntaxError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of ParserSyntaxError(source, message, entry) beancount.parser.grammar.ParserSyntaxError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.grammar.TagsLinks ( tuple ) \uf0c1 TagsLinks(tags, links) beancount.parser.grammar.TagsLinks.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.grammar.TagsLinks.__new__(_cls, tags, links) special staticmethod \uf0c1 Create new instance of TagsLinks(tags, links) beancount.parser.grammar.TagsLinks.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.grammar.ValueType ( tuple ) \uf0c1 ValueType(value, dtype) beancount.parser.grammar.ValueType.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.grammar.ValueType.__new__(_cls, value, dtype) special staticmethod \uf0c1 Create new instance of ValueType(value, dtype) beancount.parser.grammar.ValueType.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.grammar.valid_account_regexp(options) \uf0c1 Build a regexp to validate account names from the options. Parameters: options \u2013 A dict of options, as per beancount.parser.options. Returns: A string, a regular expression that will match all account names. Source code in beancount/parser/grammar.py def valid_account_regexp(options): \"\"\"Build a regexp to validate account names from the options. Args: options: A dict of options, as per beancount.parser.options. Returns: A string, a regular expression that will match all account names. \"\"\" names = map(options.__getitem__, ('name_assets', 'name_liabilities', 'name_equity', 'name_income', 'name_expenses')) # Replace the first term of the account regular expression with the specific # names allowed under the options configuration. This code is kept in sync # with {5672c7270e1e}. return re.compile(\"(?:{})(?:{}{})+\".format('|'.join(names), account.sep, account.ACC_COMP_NAME_RE)) beancount.parser.hashsrc \uf0c1 Compute a hash of the source files in order to warn when the source goes out of date. beancount.parser.hashsrc.check_parser_source_files() \uf0c1 Check the extension module's source hash and issue a warning if the current source differs from that of the module. If the source files aren't located in the Python source directory, ignore the warning, we're probably running this from an installed based, in which case we don't need to check anything (this check is useful only for people running directly from source). Source code in beancount/parser/hashsrc.py def check_parser_source_files(): \"\"\"Check the extension module's source hash and issue a warning if the current source differs from that of the module. If the source files aren't located in the Python source directory, ignore the warning, we're probably running this from an installed based, in which case we don't need to check anything (this check is useful only for people running directly from source). \"\"\" parser_source_hash = hash_parser_source_files() if parser_source_hash is None: return # pylint: disable=import-outside-toplevel from . import _parser if _parser.SOURCE_HASH and _parser.SOURCE_HASH != parser_source_hash: warnings.warn( (\"The Beancount parser C extension module is out-of-date ('{}' != '{}'). \" \"You need to rebuild.\").format(_parser.SOURCE_HASH, parser_source_hash)) beancount.parser.hashsrc.hash_parser_source_files() \uf0c1 Compute a unique hash of the parser's Python code in order to bake that into the extension module. This is used at load-time to verify that the extension module and the corresponding Python codes match each other. If not, it issues a warning that you should rebuild your extension module. Returns: A string, the hexadecimal unique hash of relevant source code that should trigger a recompilation. Source code in beancount/parser/hashsrc.py def hash_parser_source_files(): \"\"\"Compute a unique hash of the parser's Python code in order to bake that into the extension module. This is used at load-time to verify that the extension module and the corresponding Python codes match each other. If not, it issues a warning that you should rebuild your extension module. Returns: A string, the hexadecimal unique hash of relevant source code that should trigger a recompilation. \"\"\" md5 = hashlib.md5() for filename in PARSER_SOURCE_FILES: fullname = path.join(path.dirname(__file__), filename) if not path.exists(fullname): return None with open(fullname, 'rb') as file: md5.update(file.read()) # Note: Prepend a character in front of the hash because under Windows MSDEV # removes escapes, and if the hash starts with a number it fails to # recognize this is a string. A small compromise for portability. return md5.hexdigest() beancount.parser.lexer \uf0c1 Beancount syntax lexer. beancount.parser.lexer.LexBuilder \uf0c1 A builder used only for building lexer objects. Attributes: Name Type Description long_string_maxlines_default Number of lines for a string to trigger a warning. This is meant to help users detecting dangling quotes in their source. beancount.parser.lexer.LexBuilder.ACCOUNT(self, account_name) \uf0c1 Process an ACCOUNT token. This function attempts to reuse an existing account if one exists, otherwise creates one on-demand. Parameters: account_name \u2013 a str, the valid name of an account. Returns: A string, the name of the account. Source code in beancount/parser/lexer.py def ACCOUNT(self, account_name): \"\"\"Process an ACCOUNT token. This function attempts to reuse an existing account if one exists, otherwise creates one on-demand. Args: account_name: a str, the valid name of an account. Returns: A string, the name of the account. \"\"\" # Check account name validity. if not self.account_regexp.match(account_name): raise ValueError(\"Invalid account name: {}\".format(account_name)) # Reuse (intern) account strings as much as possible. This potentially # reduces memory usage a fair bit, because these strings are repeated # liberally. return self.accounts.setdefault(account_name, account_name) beancount.parser.lexer.LexBuilder.CURRENCY(self, currency_name) \uf0c1 Process a CURRENCY token. Parameters: currency_name \u2013 the name of the currency. Returns: A new currency object; for now, these are simply represented as the currency name. Source code in beancount/parser/lexer.py def CURRENCY(self, currency_name): \"\"\"Process a CURRENCY token. Args: currency_name: the name of the currency. Returns: A new currency object; for now, these are simply represented as the currency name. \"\"\" self.commodities.add(currency_name) return currency_name beancount.parser.lexer.LexBuilder.DATE(self, year, month, day) \uf0c1 Process a DATE token. Parameters: year \u2013 integer year. month \u2013 integer month. day \u2013 integer day Returns: A new datetime object. Source code in beancount/parser/lexer.py def DATE(self, year, month, day): \"\"\"Process a DATE token. Args: year: integer year. month: integer month. day: integer day Returns: A new datetime object. \"\"\" return datetime.date(year, month, day) beancount.parser.lexer.LexBuilder.KEY(self, ident) \uf0c1 Process an identifier token. Parameters: ident \u2013 a str, the name of the key string. Returns: The link string itself. For now we don't need to represent this by an object. Source code in beancount/parser/lexer.py def KEY(self, ident): \"\"\"Process an identifier token. Args: ident: a str, the name of the key string. Returns: The link string itself. For now we don't need to represent this by an object. \"\"\" return ident beancount.parser.lexer.LexBuilder.LINK(self, link) \uf0c1 Process a LINK token. Parameters: link \u2013 a str, the name of the string. Returns: The link string itself. For now we don't need to represent this by an object. Source code in beancount/parser/lexer.py def LINK(self, link): \"\"\"Process a LINK token. Args: link: a str, the name of the string. Returns: The link string itself. For now we don't need to represent this by an object. \"\"\" return link beancount.parser.lexer.LexBuilder.NUMBER(self, number) \uf0c1 Process a NUMBER token. Convert into Decimal. Parameters: number \u2013 a str, the number to be converted. Returns: A Decimal instance built of the number string. Source code in beancount/parser/lexer.py def NUMBER(self, number): \"\"\"Process a NUMBER token. Convert into Decimal. Args: number: a str, the number to be converted. Returns: A Decimal instance built of the number string. \"\"\" # Note: We don't use D() for efficiency here. # The lexer will only yield valid number strings. if ',' in number: # Extract the integer part and check the commas match the # locale-aware formatted version. This match = re.match(r\"([\\d,]*)(\\.\\d*)?$\", number) if not match: # This path is never taken because the lexer will parse a comma # in the fractional part as two NUMBERs with a COMMA token in # between. self.errors.append( LexerError(self.get_lexer_location(), \"Invalid number format: '{}'\".format(number), None)) else: int_string, float_string = match.groups() reformatted_number = r\"{:,.0f}\".format(int(int_string.replace(\",\", \"\"))) if int_string != reformatted_number: self.errors.append( LexerError(self.get_lexer_location(), \"Invalid commas: '{}'\".format(number), None)) number = number.replace(',', '') return Decimal(number) beancount.parser.lexer.LexBuilder.STRING(self, string) \uf0c1 Process a STRING token. Parameters: string \u2013 the string to process. Returns: The string. Nothing to be done or cleaned up. Eventually we might do some decoding here. Source code in beancount/parser/lexer.py def STRING(self, string): \"\"\"Process a STRING token. Args: string: the string to process. Returns: The string. Nothing to be done or cleaned up. Eventually we might do some decoding here. \"\"\" # If a multiline string, warm over a certain number of lines. if '\\n' in string: num_lines = string.count('\\n') + 1 if num_lines > self.long_string_maxlines_default: # This is just a warning; accept the string anyhow. self.errors.append( LexerError( self.get_lexer_location(), \"String too long ({} lines); possible error\".format(num_lines), None)) return string beancount.parser.lexer.LexBuilder.TAG(self, tag) \uf0c1 Process a TAG token. Parameters: tag \u2013 a str, the tag to be processed. Returns: The tag string itself. For now we don't need an object to represent those; keeping it simple. Source code in beancount/parser/lexer.py def TAG(self, tag): \"\"\"Process a TAG token. Args: tag: a str, the tag to be processed. Returns: The tag string itself. For now we don't need an object to represent those; keeping it simple. \"\"\" return tag beancount.parser.lexer.LexBuilder.build_lexer_error(self, message, exc_type=None) \uf0c1 Build a lexer error and appends it to the list of pending errors. Parameters: message \u2013 The message of the error. exc_type \u2013 An exception type, if an exception occurred. Source code in beancount/parser/lexer.py def build_lexer_error(self, message, exc_type=None): # {0e31aeca3363} \"\"\"Build a lexer error and appends it to the list of pending errors. Args: message: The message of the error. exc_type: An exception type, if an exception occurred. \"\"\" if not isinstance(message, str): message = str(message) if exc_type is not None: message = '{}: {}'.format(exc_type.__name__, message) self.errors.append( LexerError(self.get_lexer_location(), message, None)) beancount.parser.lexer.LexBuilder.get_invalid_account(self) \uf0c1 Return the name of an invalid account placeholder. When an account name is not deemed a valid one, replace it by this account name. This can be overridden by the parser to take into account the options. Returns: A string, the name of the root/type for invalid account names. Source code in beancount/parser/lexer.py def get_invalid_account(self): \"\"\"Return the name of an invalid account placeholder. When an account name is not deemed a valid one, replace it by this account name. This can be overridden by the parser to take into account the options. Returns: A string, the name of the root/type for invalid account names. \"\"\" return 'Equity:InvalidAccountName' beancount.parser.lexer.LexerError ( tuple ) \uf0c1 LexerError(source, message, entry) beancount.parser.lexer.LexerError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/lexer.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.lexer.LexerError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of LexerError(source, message, entry) beancount.parser.lexer.LexerError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/lexer.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.lexer.lex_iter(file, builder=None, encoding=None) \uf0c1 An iterator that yields all the tokens in the given file. Parameters: file \u2013 A string, the filename to run the lexer on, or a file object. builder \u2013 A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors). encoding \u2013 A string (or None), the default encoding to use for strings. Yields: Tuples of the token (a string), the matched text (a string), and the line no (an integer). Source code in beancount/parser/lexer.py def lex_iter(file, builder=None, encoding=None): \"\"\"An iterator that yields all the tokens in the given file. Args: file: A string, the filename to run the lexer on, or a file object. builder: A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors). encoding: A string (or None), the default encoding to use for strings. Yields: Tuples of the token (a string), the matched text (a string), and the line no (an integer). \"\"\" if isinstance(file, str): filename = file else: filename = file.name if builder is None: builder = LexBuilder() _parser.lexer_initialize(filename, builder, encoding) try: while 1: token_tuple = _parser.lexer_next() if token_tuple is None: break yield token_tuple finally: _parser.lexer_finalize() beancount.parser.lexer.lex_iter_string(string, builder=None, encoding=None) \uf0c1 Parse an input string and print the tokens to an output file. Parameters: input_string \u2013 a str or bytes, the contents of the ledger to be parsed. builder \u2013 A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors). encoding \u2013 A string (or None), the default encoding to use for strings. Returns: A iterator on the string. See lex_iter() for details. Source code in beancount/parser/lexer.py def lex_iter_string(string, builder=None, encoding=None): \"\"\"Parse an input string and print the tokens to an output file. Args: input_string: a str or bytes, the contents of the ledger to be parsed. builder: A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors). encoding: A string (or None), the default encoding to use for strings. Returns: A iterator on the string. See lex_iter() for details. \"\"\" tmp_file = tempfile.NamedTemporaryFile('w' if isinstance(string, str) else 'wb') tmp_file.write(string) tmp_file.flush() # Note: We pass in the file object in order to keep it alive during parsing. return lex_iter(tmp_file, builder, encoding) beancount.parser.options \uf0c1 Declaration of options and their default values. beancount.parser.options.OptDesc ( tuple ) \uf0c1 OptDesc(name, default_value, example_value, converter, deprecated, alias) beancount.parser.options.OptDesc.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/options.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.options.OptDesc.__new__(_cls, name, default_value, example_value, converter, deprecated, alias) special staticmethod \uf0c1 Create new instance of OptDesc(name, default_value, example_value, converter, deprecated, alias) beancount.parser.options.OptDesc.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/options.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.options.OptGroup ( tuple ) \uf0c1 OptGroup(description, options) beancount.parser.options.OptGroup.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/options.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.parser.options.OptGroup.__new__(_cls, description, options) special staticmethod \uf0c1 Create new instance of OptGroup(description, options) beancount.parser.options.OptGroup.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/options.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.parser.options.Opt(name, default_value, example_value=, converter=None, deprecated=False, alias=None) \uf0c1 Alternative constructor for OptDesc, with default values. Parameters: name \u2013 See OptDesc. default_value \u2013 See OptDesc. example_value \u2013 See OptDesc. converter \u2013 See OptDesc. deprecated \u2013 See OptDesc. alias \u2013 See OptDesc. Returns: An instance of OptDesc. Source code in beancount/parser/options.py def Opt(name, default_value, example_value=UNSET, converter=None, deprecated=False, alias=None): \"\"\"Alternative constructor for OptDesc, with default values. Args: name: See OptDesc. default_value: See OptDesc. example_value: See OptDesc. converter: See OptDesc. deprecated: See OptDesc. alias: See OptDesc. Returns: An instance of OptDesc. \"\"\" if example_value is UNSET: example_value = default_value return OptDesc(name, default_value, example_value, converter, deprecated, alias) beancount.parser.options.get_account_types(options) \uf0c1 Extract the account type names from the parser's options. Parameters: options \u2013 a dict of ledger options. Returns: An instance of AccountTypes, that contains all the prefixes. Source code in beancount/parser/options.py def get_account_types(options): \"\"\"Extract the account type names from the parser's options. Args: options: a dict of ledger options. Returns: An instance of AccountTypes, that contains all the prefixes. \"\"\" return account_types.AccountTypes( *[options[key] for key in (\"name_assets\", \"name_liabilities\", \"name_equity\", \"name_income\", \"name_expenses\")]) beancount.parser.options.get_current_accounts(options) \uf0c1 Return account names for the current earnings and conversion accounts. Parameters: options \u2013 a dict of ledger options. Returns: A tuple of 2 account objects, one for booking current earnings, and one for current conversions. Source code in beancount/parser/options.py def get_current_accounts(options): \"\"\"Return account names for the current earnings and conversion accounts. Args: options: a dict of ledger options. Returns: A tuple of 2 account objects, one for booking current earnings, and one for current conversions. \"\"\" equity = options['name_equity'] account_current_earnings = account.join(equity, options['account_current_earnings']) account_current_conversions = account.join(equity, options['account_current_conversions']) return (account_current_earnings, account_current_conversions) beancount.parser.options.get_previous_accounts(options) \uf0c1 Return account names for the previous earnings, balances and conversion accounts. Parameters: options \u2013 a dict of ledger options. Returns: A tuple of 3 account objects, for booking previous earnings, previous balances, and previous conversions. Source code in beancount/parser/options.py def get_previous_accounts(options): \"\"\"Return account names for the previous earnings, balances and conversion accounts. Args: options: a dict of ledger options. Returns: A tuple of 3 account objects, for booking previous earnings, previous balances, and previous conversions. \"\"\" equity = options['name_equity'] account_previous_earnings = account.join(equity, options['account_previous_earnings']) account_previous_balances = account.join(equity, options['account_previous_balances']) account_previous_conversions = account.join(equity, options['account_previous_conversions']) return (account_previous_earnings, account_previous_balances, account_previous_conversions) beancount.parser.options.list_options() \uf0c1 Produce a formatted text of the available options and their description. Returns: A string, formatted nicely to be printed in 80 columns. Source code in beancount/parser/options.py def list_options(): \"\"\"Produce a formatted text of the available options and their description. Returns: A string, formatted nicely to be printed in 80 columns. \"\"\" oss = io.StringIO() for group in PUBLIC_OPTION_GROUPS: for desc in group.options: oss.write('option \"{}\" \"{}\"\\n'.format(desc.name, desc.example_value)) if desc.deprecated: oss.write(textwrap.fill( \"THIS OPTION IS DEPRECATED: {}\".format(desc.deprecated), initial_indent=\" \", subsequent_indent=\" \")) oss.write('\\n\\n') description = ' '.join(line.strip() for line in group.description.strip().splitlines()) oss.write(textwrap.fill(description, initial_indent=' ', subsequent_indent=' ')) oss.write('\\n') if isinstance(desc.default_value, (list, dict, set)): oss.write('\\n') oss.write(' (This option may be supplied multiple times.)\\n') oss.write('\\n\\n') return oss.getvalue() beancount.parser.options.options_validate_booking_method(value) \uf0c1 Validate a booking method name. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_booking_method(value): \"\"\"Validate a booking method name. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" try: return data.Booking[value] except KeyError as exc: raise ValueError(str(exc)) beancount.parser.options.options_validate_boolean(value) \uf0c1 Validate a boolean option. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_boolean(value): \"\"\"Validate a boolean option. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" return value.lower() in ('1', 'true', 'yes') beancount.parser.options.options_validate_plugin(value) \uf0c1 Validate the plugin option. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_plugin(value): \"\"\"Validate the plugin option. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" # Process the 'plugin' option specially: accept an optional # argument from it. NOTE: We will eventually phase this out and # replace it by a dedicated 'plugin' directive. match = re.match('(.*):(.*)', value) if match: plugin_name, plugin_config = match.groups() else: plugin_name, plugin_config = value, None return (plugin_name, plugin_config) beancount.parser.options.options_validate_processing_mode(value) \uf0c1 Validate the options processing mode. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_processing_mode(value): \"\"\"Validate the options processing mode. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" if value not in ('raw', 'default'): raise ValueError(\"Invalid value '{}'\".format(value)) return value beancount.parser.options.options_validate_tolerance(value) \uf0c1 Validate the tolerance option. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_tolerance(value): \"\"\"Validate the tolerance option. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" return D(value) beancount.parser.options.options_validate_tolerance_map(value) \uf0c1 Validate an option with a map of currency/tolerance pairs in a string. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_tolerance_map(value): \"\"\"Validate an option with a map of currency/tolerance pairs in a string. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" # Process the setting of a key-value, whereby the value is a Decimal # representation. match = re.match('(.*):(.*)', value) if not match: raise ValueError(\"Invalid value '{}'\".format(value)) currency, tolerance_str = match.groups() return (currency, D(tolerance_str)) beancount.parser.parser \uf0c1 Beancount syntax parser. IMPORTANT: The parser (and its grammar builder) produces \"incomplete\" Transaction objects. This means that some of the data can be found missing from the output of the parser and some of the data types vary slightly. Missing components are replaced not by None, but by a special constant 'NA' which helps diagnose problems if a user inadvertently attempts to work on an incomplete posting instead of a complete one. Those incomplete entries are then run through the \"booking\" routines which do two things simultaneously: They find matching lots for reducing inventory positions, and They interpolate missing numbers. In doing so they normalize the entries to \"complete\" entries by converting a position/lot's \"cost\" attribute from a CostSpec to a Cost. A Cost is similar to an Amount in that it shares \"number\" and \"currency\" attributes, but also has a label and a lot date. A CostSpec is similar to a Cost, but has all optional data; it consists in a specification for matching against a particular inventory lot. Other parts of a posting may also be missing, not just parts of the cost. Leaving out some parts of the input is used to invoke interpolation, to tell Beancount to automatically compute the missing numbers (if possible). Missing components will be set to the special value \"beancount.core.number.MISSING\" until inventory booking and number interpolation has been completed. The \"MISSING\" value should never appear in completed, loaded transaction postings. For instance, all of the units may be missing: INPUT: Assets:Account posting.units = MISSING Or just the number of the units: INPUT: Assets:Account USD posting.units = Amount(MISSING, \"USD\") You must always specify the currency. If a price annotation is simply absent, it appears as None: INPUT: Assets:Account 2 MXN posting.price = None However, you may indicate that there is a price but have Beancount compute it automatically: INPUT: Assets:Account 2 MXN @ posting.price = Amount(MISSING, MISSING) Indicating the conversion currency is also possible (and recommended): INPUT: Assets:Account 2 MXN @ USD posting.price = Amount(MISSING, \"USD\") If a cost specification is provided, a \"cost\" attribute it set but it does not refer to a Cost instance (as in complete entries) but rather to a CostSpec instance. Some of the fields of a CostSpec may be MISSING if they were not specified in the input. For example: INPUT: Assets:Account 1 HOOL {100 # 5 USD} posting.cost = CostSpec(Decimal(\"100\"), Decimal(\"5\"), \"USD\", None, None, False)) Note how we never consider the label of date override to be MISSING; this is because those inputs are optional: A missing label is simply left unset in the computed Cost, and a missing date override uses the date of the transaction that contains the posting. You can indicate that there is a total number to be filled in like this: INPUT: Assets:Account 1 HOOL {100 # USD} posting.cost = CostSpec(Decimal(\"100\"), MISSING, \"USD\", None, None, False)) This is in contrast to the total value simple not being used: INPUT: Assets:Account 1 HOOL {100 USD} posting.cost = CostSpec(Decimal(\"100\"), None, \"USD\", None, None, False)) Both per-unit and total numbers may be omitted as well, in which case, only the number-per-unit portion of the CostSpec will appear as MISSING: INPUT: Assets:Account 1 HOOL {USD} posting.cost = CostSpec(MISSING, None, \"USD\", None, None, False)) And furthermore, all the cost basis may be missing: INPUT: Assets:Account 1 HOOL {} posting.cost = CostSpec(MISSING, None, MISSING, None, None, False)) If you ask for the lots to be merged, you get this: INPUT: Assets:Account 1 HOOL {*} posting.cost = CostSpec(MISSING, None, MISSING, None, None, True)) The numbers have to be computed by Beancount, so we output this with MISSING values. Of course, you can provide only the non-basis information, like just the date or label: INPUT: Assets:Account 1 HOOL {2015-09-21} posting.cost = CostSpec(MISSING, None, MISSING, date(2015, 9, 21), None, True) See the test beancount.parser.grammar_test.TestIncompleteInputs for examples and corresponding expected values. beancount.parser.parser.is_entry_incomplete(entry) \uf0c1 Detect the presence of elided amounts in Transactions. Parameters: entries \u2013 A directive. Returns: A boolean, true if there are some missing portions of any postings found. Source code in beancount/parser/parser.py def is_entry_incomplete(entry): \"\"\"Detect the presence of elided amounts in Transactions. Args: entries: A directive. Returns: A boolean, true if there are some missing portions of any postings found. \"\"\" if isinstance(entry, data.Transaction): if any(is_posting_incomplete(posting) for posting in entry.postings): return True return False beancount.parser.parser.is_posting_incomplete(posting) \uf0c1 Detect the presence of any elided amounts in a Posting. If any of the possible amounts are missing, this returns True. Parameters: entries \u2013 A directive. Returns: A boolean, true if there are some missing portions of any postings found. Source code in beancount/parser/parser.py def is_posting_incomplete(posting): \"\"\"Detect the presence of any elided amounts in a Posting. If any of the possible amounts are missing, this returns True. Args: entries: A directive. Returns: A boolean, true if there are some missing portions of any postings found. \"\"\" units = posting.units if (units is MISSING or units.number is MISSING or units.currency is MISSING): return True price = posting.price if (price is MISSING or price is not None and (price.number is MISSING or price.currency is MISSING)): return True cost = posting.cost if cost is not None and (cost.number_per is MISSING or cost.number_total is MISSING or cost.currency is MISSING): return True return False beancount.parser.parser.parse_doc(expect_errors=False, allow_incomplete=False) \uf0c1 Factory of decorators that parse the function's docstring as an argument. Note that the decorators thus generated only run the parser on the tests, not the loader, so is no validation, balance checks, nor plugins applied to the parsed text. Parameters: expect_errors \u2013 A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. allow_incomplete \u2013 A boolean, if true, allow incomplete input. Otherwise barf if the input would require interpolation. The default value is set not to allow it because we want to minimize the features tests depend on. Returns: A decorator for test functions. Source code in beancount/parser/parser.py def parse_doc(expect_errors=False, allow_incomplete=False): \"\"\"Factory of decorators that parse the function's docstring as an argument. Note that the decorators thus generated only run the parser on the tests, not the loader, so is no validation, balance checks, nor plugins applied to the parsed text. Args: expect_errors: A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. allow_incomplete: A boolean, if true, allow incomplete input. Otherwise barf if the input would require interpolation. The default value is set not to allow it because we want to minimize the features tests depend on. Returns: A decorator for test functions. \"\"\" def decorator(fun): \"\"\"A decorator that parses the function's docstring as an argument. Args: fun: the function object to be decorated. Returns: A decorated test function. \"\"\" filename = inspect.getfile(fun) lines, lineno = inspect.getsourcelines(fun) # decorator line + function definition line (I realize this is largely # imperfect, but it's only for reporting in our tests) - empty first line # stripped away. lineno += 1 @functools.wraps(fun) def wrapper(self): assert fun.__doc__ is not None, ( \"You need to insert a docstring on {}\".format(fun.__name__)) entries, errors, options_map = parse_string(fun.__doc__, report_filename=filename, report_firstline=lineno, dedent=True) if not allow_incomplete and any(is_entry_incomplete(entry) for entry in entries): self.fail(\"parse_doc() may not use interpolation.\") if expect_errors is not None: if expect_errors is False and errors: oss = io.StringIO() printer.print_errors(errors, file=oss) self.fail(\"Unexpected errors found:\\n{}\".format(oss.getvalue())) elif expect_errors is True and not errors: self.fail(\"Expected errors, none found:\") return fun(self, entries, errors, options_map) wrapper.__input__ = wrapper.__doc__ wrapper.__doc__ = None return wrapper return decorator beancount.parser.parser.parse_file(filename, **kw) \uf0c1 Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Parameters: filename \u2013 the name of the file to be parsed. kw \u2013 a dict of keywords to be applied to the C parser. Returns: A tuple of ( list of entries parsed in the file, list of errors that were encountered during parsing, and a dict of the option values that were parsed from the file.) Source code in beancount/parser/parser.py def parse_file(filename, **kw): \"\"\"Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Args: filename: the name of the file to be parsed. kw: a dict of keywords to be applied to the C parser. Returns: A tuple of ( list of entries parsed in the file, list of errors that were encountered during parsing, and a dict of the option values that were parsed from the file.) \"\"\" abs_filename = path.abspath(filename) if filename else None builder = grammar.Builder(abs_filename) _parser.parse_file(filename, builder, **kw) return builder.finalize() beancount.parser.parser.parse_many(string, level=0) \uf0c1 Parse a string with a snippet of Beancount input and replace vars from caller. Parameters: string \u2013 A string with some Beancount input. level \u2013 The number of extra stacks to ignore. Returns: A list of entries. Exceptions: AssertionError \u2013 If there are any errors. Source code in beancount/parser/parser.py def parse_many(string, level=0): \"\"\"Parse a string with a snippet of Beancount input and replace vars from caller. Args: string: A string with some Beancount input. level: The number of extra stacks to ignore. Returns: A list of entries. Raises: AssertionError: If there are any errors. \"\"\" # Get the locals in the stack for the callers and produce the final text. frame = inspect.stack()[level+1] varkwds = frame[0].f_locals input_string = textwrap.dedent(string.format(**varkwds)) # Parse entries and check there are no errors. entries, errors, __ = parse_string(input_string) assert not errors return entries beancount.parser.parser.parse_one(string) \uf0c1 Parse a string with single Beancount directive and replace vars from caller. Parameters: string \u2013 A string with some Beancount input. level \u2013 The number of extra stacks to ignore. Returns: A list of entries. Exceptions: AssertionError \u2013 If there are any errors. Source code in beancount/parser/parser.py def parse_one(string): \"\"\"Parse a string with single Beancount directive and replace vars from caller. Args: string: A string with some Beancount input. level: The number of extra stacks to ignore. Returns: A list of entries. Raises: AssertionError: If there are any errors. \"\"\" entries = parse_many(string, level=1) assert len(entries) == 1 return entries[0] beancount.parser.parser.parse_string(string, report_filename=None, **kw) \uf0c1 Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Parameters: string \u2013 A string, the contents to be parsed instead of a file's. report_filename \u2013 A string, the source filename from which this string has been extracted, if any. This is stored in the metadata of the parsed entries. **kw \u2013 See parse.c. This function parses out 'dedent' which removes whitespace from the front of the text (default is False). Returns: Same as the output of parse_file(). Source code in beancount/parser/parser.py def parse_string(string, report_filename=None, **kw): \"\"\"Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Args: string: A string, the contents to be parsed instead of a file's. report_filename: A string, the source filename from which this string has been extracted, if any. This is stored in the metadata of the parsed entries. **kw: See parse.c. This function parses out 'dedent' which removes whitespace from the front of the text (default is False). Return: Same as the output of parse_file(). \"\"\" if kw.pop('dedent', None): string = textwrap.dedent(string) builder = grammar.Builder(report_filename or '') _parser.parse_string(string, builder, report_filename=report_filename, **kw) return builder.finalize() beancount.parser.printer \uf0c1 Conversion from internal data structures to text. beancount.parser.printer.EntryPrinter \uf0c1 A multi-method interface for printing all directive types. Attributes: Name Type Description dcontext An instance of DisplayContext with which to render all the numbers. render_weight A boolean, true if we should render the weight of the postings as a comment, for debugging. min_width_account An integer, the minimum width to leave for the account name. beancount.parser.printer.EntryPrinter.__call__(self, obj) special \uf0c1 Render a directive. Parameters: obj \u2013 The directive to be rendered. Returns: A string, the rendered directive. Source code in beancount/parser/printer.py def __call__(self, obj): \"\"\"Render a directive. Args: obj: The directive to be rendered. Returns: A string, the rendered directive. \"\"\" oss = io.StringIO() method = getattr(self, obj.__class__.__name__) method(obj, oss) return oss.getvalue() beancount.parser.printer.EntryPrinter.render_posting_strings(self, posting) \uf0c1 This renders the three components of a posting: the account and its optional posting flag, the position, and finally, the weight of the position. The purpose is to align these in the caller. Parameters: posting \u2013 An instance of Posting, the posting to render. Returns: A tuple of flag_account \u2013 A string, the account name including the flag. position_str: A string, the rendered position string. weight_str: A string, the rendered weight of the posting. Source code in beancount/parser/printer.py def render_posting_strings(self, posting): \"\"\"This renders the three components of a posting: the account and its optional posting flag, the position, and finally, the weight of the position. The purpose is to align these in the caller. Args: posting: An instance of Posting, the posting to render. Returns: A tuple of flag_account: A string, the account name including the flag. position_str: A string, the rendered position string. weight_str: A string, the rendered weight of the posting. \"\"\" # Render a string of the flag and the account. flag = '{} '.format(posting.flag) if posting.flag else '' flag_account = flag + posting.account # Render a string with the amount and cost and optional price, if # present. Also render a string with the weight. weight_str = '' if isinstance(posting.units, amount.Amount): position_str = position.to_string(posting, self.dformat) # Note: we render weights at maximum precision, for debugging. if posting.cost is None or (isinstance(posting.cost, position.Cost) and isinstance(posting.cost.number, Decimal)): weight_str = str(convert.get_weight(posting)) else: position_str = '' if posting.price is not None: position_str += ' @ {}'.format(posting.price.to_string(self.dformat_max)) return flag_account, position_str, weight_str beancount.parser.printer.EntryPrinter.write_metadata(self, meta, oss, prefix=None) \uf0c1 Write metadata to the file object, excluding filename and line number. Parameters: meta \u2013 A dict that contains the metadata for this directive. oss \u2013 A file object to write to. Source code in beancount/parser/printer.py def write_metadata(self, meta, oss, prefix=None): \"\"\"Write metadata to the file object, excluding filename and line number. Args: meta: A dict that contains the metadata for this directive. oss: A file object to write to. \"\"\" if meta is None: return if prefix is None: prefix = self.prefix for key, value in sorted(meta.items()): if key not in self.META_IGNORE: value_str = None if isinstance(value, str): value_str = '\"{}\"'.format(misc_utils.escape_string(value)) elif isinstance(value, (Decimal, datetime.date, amount.Amount)): value_str = str(value) elif isinstance(value, bool): value_str = 'TRUE' if value else 'FALSE' elif isinstance(value, (dict, inventory.Inventory)): pass # Ignore dicts, don't print them out. elif value is None: value_str = '' # Render null metadata as empty, on purpose. else: raise ValueError(\"Unexpected value: '{!r}'\".format(value)) if value_str is not None: oss.write(\"{}{}: {}\\n\".format(prefix, key, value_str)) beancount.parser.printer.align_position_strings(strings) \uf0c1 A helper used to align rendered amounts positions to their first currency character (an uppercase letter). This class accepts a list of rendered positions and calculates the necessary width to render them stacked in a column so that the first currency word aligns. It does not go beyond that (further currencies, e.g. for the price or cost, are not aligned). This is perhaps best explained with an example. The following positions will be aligned around the column marked with '^': 45 HOOL {504.30 USD} 4 HOOL {504.30 USD, 2014-11-11} 9.95 USD -22473.32 CAD @ 1.10 USD ^ Strings without a currency character will be rendered flush left. Parameters: strings \u2013 A list of rendered position or amount strings. Returns: A pair of a list of aligned strings and the width of the aligned strings. Source code in beancount/parser/printer.py def align_position_strings(strings): \"\"\"A helper used to align rendered amounts positions to their first currency character (an uppercase letter). This class accepts a list of rendered positions and calculates the necessary width to render them stacked in a column so that the first currency word aligns. It does not go beyond that (further currencies, e.g. for the price or cost, are not aligned). This is perhaps best explained with an example. The following positions will be aligned around the column marked with '^': 45 HOOL {504.30 USD} 4 HOOL {504.30 USD, 2014-11-11} 9.95 USD -22473.32 CAD @ 1.10 USD ^ Strings without a currency character will be rendered flush left. Args: strings: A list of rendered position or amount strings. Returns: A pair of a list of aligned strings and the width of the aligned strings. \"\"\" # Maximum length before the alignment character. max_before = 0 # Maximum length after the alignment character. max_after = 0 # Maximum length of unknown strings. max_unknown = 0 string_items = [] search = re.compile('[A-Z]').search for string in strings: match = search(string) if match: index = match.start() if index != 0: max_before = max(index, max_before) max_after = max(len(string) - index, max_after) string_items.append((index, string)) continue # else max_unknown = max(len(string), max_unknown) string_items.append((None, string)) # Compute formatting string. max_total = max(max_before + max_after, max_unknown) max_after_prime = max_total - max_before fmt = \"{{:>{0}}}{{:{1}}}\".format(max_before, max_after_prime).format fmt_unknown = \"{{:<{0}}}\".format(max_total).format # Align the strings and return them. aligned_strings = [] for index, string in string_items: if index is not None: string = fmt(string[:index], string[index:]) else: string = fmt_unknown(string) aligned_strings.append(string) return aligned_strings, max_total beancount.parser.printer.format_entry(entry, dcontext=None, render_weights=False, prefix=None) \uf0c1 Format an entry into a string in the same input syntax the parser accepts. Parameters: entry \u2013 An entry instance. dcontext \u2013 An instance of DisplayContext used to format the numbers. render_weights \u2013 A boolean, true to render the weights for debugging. Returns: A string, the formatted entry. Source code in beancount/parser/printer.py def format_entry(entry, dcontext=None, render_weights=False, prefix=None): \"\"\"Format an entry into a string in the same input syntax the parser accepts. Args: entry: An entry instance. dcontext: An instance of DisplayContext used to format the numbers. render_weights: A boolean, true to render the weights for debugging. Returns: A string, the formatted entry. \"\"\" return EntryPrinter(dcontext, render_weights, prefix=prefix)(entry) beancount.parser.printer.format_error(error) \uf0c1 Given an error objects, return a formatted string for it. Parameters: error \u2013 a namedtuple objects representing an error. It has to have an 'entry' attribute that may be either a single directive object or a list of directive objects. Returns: A string, the errors rendered. Source code in beancount/parser/printer.py def format_error(error): \"\"\"Given an error objects, return a formatted string for it. Args: error: a namedtuple objects representing an error. It has to have an 'entry' attribute that may be either a single directive object or a list of directive objects. Returns: A string, the errors rendered. \"\"\" oss = io.StringIO() oss.write('{} {}\\n'.format(render_source(error.source), error.message)) if error.entry is not None: entries = error.entry if isinstance(error.entry, list) else [error.entry] error_string = '\\n'.join(format_entry(entry) for entry in entries) oss.write('\\n') oss.write(textwrap.indent(error_string, ' ')) oss.write('\\n') return oss.getvalue() beancount.parser.printer.print_entries(entries, dcontext=None, render_weights=False, file=None, prefix=None) \uf0c1 A convenience function that prints a list of entries to a file. Parameters: entries \u2013 A list of directives. dcontext \u2013 An instance of DisplayContext used to format the numbers. render_weights \u2013 A boolean, true to render the weights for debugging. file \u2013 An optional file object to write the entries to. Source code in beancount/parser/printer.py def print_entries(entries, dcontext=None, render_weights=False, file=None, prefix=None): \"\"\"A convenience function that prints a list of entries to a file. Args: entries: A list of directives. dcontext: An instance of DisplayContext used to format the numbers. render_weights: A boolean, true to render the weights for debugging. file: An optional file object to write the entries to. \"\"\" assert isinstance(entries, list), \"Entries is not a list: {}\".format(entries) output = file or (codecs.getwriter(\"utf-8\")(sys.stdout.buffer) if hasattr(sys.stdout, 'buffer') else sys.stdout) if prefix: output.write(prefix) previous_type = type(entries[0]) if entries else None eprinter = EntryPrinter(dcontext, render_weights) for entry in entries: # Insert a newline between transactions and between blocks of directives # of the same type. entry_type = type(entry) if (entry_type in (data.Transaction, data.Commodity) or entry_type is not previous_type): output.write('\\n') previous_type = entry_type string = eprinter(entry) output.write(string) beancount.parser.printer.print_entry(entry, dcontext=None, render_weights=False, file=None) \uf0c1 A convenience function that prints a single entry to a file. Parameters: entry \u2013 A directive entry. dcontext \u2013 An instance of DisplayContext used to format the numbers. render_weights \u2013 A boolean, true to render the weights for debugging. file \u2013 An optional file object to write the entries to. Source code in beancount/parser/printer.py def print_entry(entry, dcontext=None, render_weights=False, file=None): \"\"\"A convenience function that prints a single entry to a file. Args: entry: A directive entry. dcontext: An instance of DisplayContext used to format the numbers. render_weights: A boolean, true to render the weights for debugging. file: An optional file object to write the entries to. \"\"\" output = file or (codecs.getwriter(\"utf-8\")(sys.stdout.buffer) if hasattr(sys.stdout, 'buffer') else sys.stdout) output.write(format_entry(entry, dcontext, render_weights)) output.write('\\n') beancount.parser.printer.print_error(error, file=None) \uf0c1 A convenience function that prints a single error to a file. Parameters: error \u2013 An error object. file \u2013 An optional file object to write the errors to. Source code in beancount/parser/printer.py def print_error(error, file=None): \"\"\"A convenience function that prints a single error to a file. Args: error: An error object. file: An optional file object to write the errors to. \"\"\" output = file or sys.stdout output.write(format_error(error)) output.write('\\n') beancount.parser.printer.print_errors(errors, file=None, prefix=None) \uf0c1 A convenience function that prints a list of errors to a file. Parameters: errors \u2013 A list of errors. file \u2013 An optional file object to write the errors to. Source code in beancount/parser/printer.py def print_errors(errors, file=None, prefix=None): \"\"\"A convenience function that prints a list of errors to a file. Args: errors: A list of errors. file: An optional file object to write the errors to. \"\"\" output = file or sys.stdout if prefix: output.write(prefix) for error in errors: output.write(format_error(error)) output.write('\\n') beancount.parser.printer.render_source(meta) \uf0c1 Render the source for errors in a way that it will be both detected by Emacs and align and rendered nicely. Parameters: meta \u2013 A dict with the metadata. Returns: A string, rendered to be interpretable as a message location for Emacs or other editors. Source code in beancount/parser/printer.py def render_source(meta): \"\"\"Render the source for errors in a way that it will be both detected by Emacs and align and rendered nicely. Args: meta: A dict with the metadata. Returns: A string, rendered to be interpretable as a message location for Emacs or other editors. \"\"\" return '{}:{:8}'.format(meta['filename'], '{}:'.format(meta['lineno']))","title":"beancount.parser"},{"location":"api_reference/beancount.parser.html#beancountparser","text":"Parser module for beancount input files.","title":"beancount.parser"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking","text":"Algorithms for 'booking' inventory, that is, the process of finding a matching lot when reducing the content of an inventory.","title":"booking"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.BookingError","text":"BookingError(source, message, entry)","title":"BookingError"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.BookingError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.BookingError.__new__","text":"Create new instance of BookingError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.BookingError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.book","text":"Book inventory lots and complete all positions with incomplete numbers. Parameters: incomplete_entries \u2013 A list of directives, with some postings possibly left with incomplete amounts as produced by the parser. options_map \u2013 An options dict as produced by the parser. Returns: A pair of entries \u2013 A list of completed entries with all their postings completed. errors: New errors produced during interpolation. Source code in beancount/parser/booking.py def book(incomplete_entries, options_map): \"\"\"Book inventory lots and complete all positions with incomplete numbers. Args: incomplete_entries: A list of directives, with some postings possibly left with incomplete amounts as produced by the parser. options_map: An options dict as produced by the parser. Returns: A pair of entries: A list of completed entries with all their postings completed. errors: New errors produced during interpolation. \"\"\" # Get the list of booking methods for each account. booking_methods = collections.defaultdict(lambda: options_map[\"booking_method\"]) for entry in incomplete_entries: if isinstance(entry, data.Open) and entry.booking: booking_methods[entry.account] = entry.booking # Do the booking here! entries, booking_errors = booking_full.book(incomplete_entries, options_map, booking_methods) # Check for MISSING elements remaining. missing_errors = validate_missing_eliminated(entries, options_map) return entries, (booking_errors + missing_errors)","title":"book()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.validate_inventory_booking","text":"Validate that no position at cost is allowed to go negative. This routine checks that when a posting reduces a position, existing or not, that the subsequent inventory does not result in a position with a negative number of units. A negative number of units would only be required for short trades of trading spreads on futures, and right now this is not supported. It would not be difficult to support this, however, but we want to be strict about it, because being pedantic about this is otherwise a great way to detect user data entry mistakes. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. booking_methods \u2013 A mapping of account name to booking method, accumulated in the main loop. Returns: A list of errors. Source code in beancount/parser/booking.py def validate_inventory_booking(entries, unused_options_map, booking_methods): \"\"\"Validate that no position at cost is allowed to go negative. This routine checks that when a posting reduces a position, existing or not, that the subsequent inventory does not result in a position with a negative number of units. A negative number of units would only be required for short trades of trading spreads on futures, and right now this is not supported. It would not be difficult to support this, however, but we want to be strict about it, because being pedantic about this is otherwise a great way to detect user data entry mistakes. Args: entries: A list of directives. unused_options_map: An options map. booking_methods: A mapping of account name to booking method, accumulated in the main loop. Returns: A list of errors. \"\"\" errors = [] balances = collections.defaultdict(inventory.Inventory) for entry in entries: if isinstance(entry, data.Transaction): for posting in entry.postings: # Update the balance of each posting on its respective account # without allowing booking to a negative position, and if an error # is encountered, catch it and return it. running_balance = balances[posting.account] position_, _ = running_balance.add_position(posting) # Skip this check if the booking method is set to ignore it. if booking_methods.get(posting.account, None) == data.Booking.NONE: continue # Check if the resulting inventory is mixed, which is not # allowed under the STRICT method. if running_balance.is_mixed(): errors.append( BookingError( entry.meta, (\"Reducing position results in inventory with positive \" \"and negative lots: {}\").format(position_), entry)) return errors","title":"validate_inventory_booking()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.validate_missing_eliminated","text":"Validate that all the missing bits of postings have been eliminated. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of errors. Source code in beancount/parser/booking.py def validate_missing_eliminated(entries, unused_options_map): \"\"\"Validate that all the missing bits of postings have been eliminated. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of errors. \"\"\" errors = [] for entry in entries: if isinstance(entry, data.Transaction): for posting in entry.postings: units = posting.units cost = posting.cost if (MISSING in (units.number, units.currency) or cost is not None and MISSING in (cost.number, cost.currency, cost.date, cost.label)): errors.append( BookingError(entry.meta, \"Transaction has incomplete elements\", entry)) break return errors","title":"validate_missing_eliminated()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full","text":"Full (new) booking implementation. Problem description: Interpolation and booking feed on each other, that is, numbers filled in from interpolation might affect the booking process, and numbers derived from the booking process may help carry out interpolation that would otherwise be under-defined. Here's an example of interpolation helping the booking process: Assume the ante-inventory of Assets:Investments contains two lots of shares of HOOL, one at 100.00 USD and the other at 101.00 USD and apply this transaction: 2015-09-30 * Assets:Investments -10 HOOL {USD} Assets:Cash 1000 USD Income:Gains -200 USD Interpolation is unambiguously able to back out a cost of 100 USD / HOOL, which would then result in an unambiguous booking result. On the other hand, consider this transaction: 2015-09-30 * Assets:Investments -10 HOOL {USD} Assets:Cash 1000 USD Income:Gains Now the interpolation cannot succeed. If the Assets:Investments account is configured to use the FIFO method, the 10 oldest shares would be selected for the cost, and we could then interpolate the capital gains correctly. First observation: The second case is much more frequent than the first, and the first is easily resolved manually by requiring a particular cost be specified. Moreover, in many cases there isn't just a single lot of shares to be reduced from and figuring out the correct set of shares given a target cost is an underspecified problem. Second observation: Booking can only be achieved for inventory reductions, not for augmentations. Therefore, we should carry out booking on inventory reductions and fail early if reduction is undefined there, and leave inventory augmentations with missing numbers undefined, so that interpolation can fill them in at a later stage. Note that one case we'd like to but may not be able to handle is of a reduction with interpolated price, like this: 2015-09-30 * Assets:Investments -10 HOOL {100.00 # USD} Expenses:Commission 9.95 USD Assets:Cash 990.05 USD Therefore we choose to 1) Carry out booking first, on inventory reductions only, and leave inventory augmentations as they are, possibly undefined. The 'cost' attributed of booked postings are converted from CostSpec to Cost. Augmented postings with missing amounts are left as CostSpec instances in order to allow for interpolation of total vs. per-unit amount. 2) Compute interpolations on the resulting postings. Undefined costs for inventory augmentations may be filled in by interpolations at this stage (if possible). 3) Finally, convert the interpolated CostSpec instances to Cost instances. Improving on this algorithm would require running a loop over the booking and interpolation steps until all numbers are resolved or no more inference can occur. We may consider that for later, as an experimental feature. My hunch is that there are so few cases for which this would be useful that we won't bother improving on the algorithm above.","title":"booking_full"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.CategorizationError","text":"CategorizationError(source, message, entry)","title":"CategorizationError"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.CategorizationError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.CategorizationError.__new__","text":"Create new instance of CategorizationError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.CategorizationError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.InterpolationError","text":"InterpolationError(source, message, entry)","title":"InterpolationError"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.InterpolationError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.InterpolationError.__new__","text":"Create new instance of InterpolationError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.InterpolationError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.MissingType","text":"The type of missing number.","title":"MissingType"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.ReductionError","text":"ReductionError(source, message, entry)","title":"ReductionError"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.ReductionError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.ReductionError.__new__","text":"Create new instance of ReductionError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.ReductionError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.Refer","text":"Refer(index, units_currency, cost_currency, price_currency)","title":"Refer"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.Refer.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.Refer.__new__","text":"Create new instance of Refer(index, units_currency, cost_currency, price_currency)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.Refer.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.SelfReduxError","text":"SelfReduxError(source, message, entry)","title":"SelfReduxError"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.SelfReduxError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.SelfReduxError.__new__","text":"Create new instance of SelfReduxError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.SelfReduxError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.book","text":"Interpolate missing data from the entries using the full historical algorithm. See the internal implementation _book() for details. This method only stripes some of the return values. See _book() for arguments and return values. Source code in beancount/parser/booking_full.py def book(entries, options_map, methods): \"\"\"Interpolate missing data from the entries using the full historical algorithm. See the internal implementation _book() for details. This method only stripes some of the return values. See _book() for arguments and return values. \"\"\" entries, errors, _ = _book(entries, options_map, methods) return entries, errors","title":"book()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.book_reductions","text":"Book inventory reductions against the ante-balances. This function accepts a dict of (account, Inventory balance) and for each posting that is a reduction against its inventory, attempts to find a corresponding lot or list of lots to reduce the balance with. For reducing lots, the CostSpec instance of the posting is replaced by a Cost instance. For augmenting lots, the CostSpec instance of the posting is left alone, except for its date, which is inherited from the parent Transaction. Parameters: entry \u2013 An instance of Transaction. This is only used to refer to when logging errors. group_postings \u2013 A list of Posting instances for the group. balances \u2013 A dict of account name to inventory contents. methods \u2013 A mapping of account name to their corresponding booking method enum. Returns: A pair of booked_postings \u2013 A list of booked postings, with reducing lots resolved against specific position in the corresponding accounts' ante-inventory balances. Note single reducing posting in the input may result in multiple postings in the output. Also note that augmenting postings held-at-cost will still refer to 'cost' instances of CostSpec, left to be interpolated later. errors: A list of errors, if there were any. Source code in beancount/parser/booking_full.py def book_reductions(entry, group_postings, balances, methods): \"\"\"Book inventory reductions against the ante-balances. This function accepts a dict of (account, Inventory balance) and for each posting that is a reduction against its inventory, attempts to find a corresponding lot or list of lots to reduce the balance with. * For reducing lots, the CostSpec instance of the posting is replaced by a Cost instance. * For augmenting lots, the CostSpec instance of the posting is left alone, except for its date, which is inherited from the parent Transaction. Args: entry: An instance of Transaction. This is only used to refer to when logging errors. group_postings: A list of Posting instances for the group. balances: A dict of account name to inventory contents. methods: A mapping of account name to their corresponding booking method enum. Returns: A pair of booked_postings: A list of booked postings, with reducing lots resolved against specific position in the corresponding accounts' ante-inventory balances. Note single reducing posting in the input may result in multiple postings in the output. Also note that augmenting postings held-at-cost will still refer to 'cost' instances of CostSpec, left to be interpolated later. errors: A list of errors, if there were any. \"\"\" errors = [] # A local copy of the balances dictionary which is updated just for the # duration of this function's updates, in order to take into account the # cumulative effect of all the postings inferred here local_balances = {} empty = inventory.Inventory() booked_postings = [] for posting in group_postings: # Process a single posting. units = posting.units costspec = posting.cost account = posting.account # Note: We ensure there is no mutation on 'balances' to keep this # function without side-effects. Note that we may be able to optimize # performance later on by giving up this property. # # Also note that if there is no existing balance, then won't be any lot # reduction because none of the postings will be able to match against # any currencies of the balance. previous_balance = balances.get(account, empty) balance = local_balances.setdefault(account, copy.copy(previous_balance)) # Check if this is a lot held at cost. if costspec is None or units.number is MISSING: # This posting is not held at cost; we do nothing. booked_postings.append(posting) else: # This posting is held at cost; figure out if it's a reduction or an # augmentation. method = methods[account] if (method is not Booking.NONE and balance is not None and balance.is_reduced_by(units)): # This posting is a reduction. # Match the positions. cost_number = compute_cost_number(costspec, units) matches = [] for position in balance: # Skip inventory contents of a different currency. if (units.currency and position.units.currency != units.currency): continue # Skip balance positions not held at cost. if position.cost is None: continue if (cost_number is not None and position.cost.number != cost_number): continue if (isinstance(costspec.currency, str) and position.cost.currency != costspec.currency): continue if (costspec.date and position.cost.date != costspec.date): continue if (costspec.label and position.cost.label != costspec.label): continue matches.append(position) # Check for ambiguous matches. if len(matches) == 0: errors.append( ReductionError(entry.meta, 'No position matches \"{}\" against balance {}'.format( posting, balance), entry)) return [], errors # This is irreconcilable, remove these postings. reduction_postings, matched_postings, ambi_errors = ( booking_method.handle_ambiguous_matches(entry, posting, matches, method)) if ambi_errors: errors.extend(ambi_errors) return [], errors # Add the reductions to the resulting list of booked postings. booked_postings.extend(reduction_postings) # Update the local balance in order to avoid matching against # the same postings twice when processing multiple postings in # the same transaction. Note that we only do this for postings # held at cost because the other postings may need interpolation # in order to be resolved properly. for posting in reduction_postings: balance.add_position(posting) else: # This posting is an augmentation. # # Note that we do not convert the CostSpec instances to Cost # instances, because we want to let the subsequent interpolation # process able to interpolate either the cost per-unit or the # total cost, separately. # Put in the date of the parent Transaction if there is no # explicit date specified on the spec. if costspec.date is None: dated_costspec = costspec._replace(date=entry.date) posting = posting._replace(cost=dated_costspec) # FIXME: Insert unique ids for trade tracking; right now this # creates ambiguous matches errors (and it shouldn't). # # Insert a unique label if there isn't one. # if posting.cost is not None and posting.cost.label is None: # posting = posting._replace( # cost=posting.cost._replace(label=unique_label())) booked_postings.append(posting) return booked_postings, errors","title":"book_reductions()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.categorize_by_currency","text":"Group the postings by the currency they declare. This is used to prepare the postings for the next stages: Interpolation and booking will then be carried out separately on each currency group. At the outset of this routine, we should have distinct groups of currencies without any ambiguities regarding which currency they need to balance against. Here's how this works. First we apply the constraint that cost-currency and price-currency must match, if there is both a cost and a price. This reduces the space of possibilities somewhat. If the currency is explicitly specified, we put the posting in that currency's bucket. If not, we have a few methods left to disambiguate the currency: We look at the remaining postings... if they are all of a single currency, the posting must be in that currency too. If we cannot do that, we inspect the contents of the inventory of the account for the posting. If all the contents are of a single currency, we use that one. Parameters: postings \u2013 A list of incomplete postings to categorize. balances \u2013 A dict of currency to inventory contents before the transaction is applied. Returns: A list of (currency string, list of tuples) items describing each postings and its interpolated currencies, and a list of generated errors for currency interpolation. The entry's original postings are left unmodified. Each tuple in the value-list contains \u2013 index: The posting index in the original entry. units_currency: The interpolated currency for units. cost_currency: The interpolated currency for cost. price_currency: The interpolated currency for price. Source code in beancount/parser/booking_full.py def categorize_by_currency(entry, balances): \"\"\"Group the postings by the currency they declare. This is used to prepare the postings for the next stages: Interpolation and booking will then be carried out separately on each currency group. At the outset of this routine, we should have distinct groups of currencies without any ambiguities regarding which currency they need to balance against. Here's how this works. - First we apply the constraint that cost-currency and price-currency must match, if there is both a cost and a price. This reduces the space of possibilities somewhat. - If the currency is explicitly specified, we put the posting in that currency's bucket. - If not, we have a few methods left to disambiguate the currency: 1. We look at the remaining postings... if they are all of a single currency, the posting must be in that currency too. 2. If we cannot do that, we inspect the contents of the inventory of the account for the posting. If all the contents are of a single currency, we use that one. Args: postings: A list of incomplete postings to categorize. balances: A dict of currency to inventory contents before the transaction is applied. Returns: A list of (currency string, list of tuples) items describing each postings and its interpolated currencies, and a list of generated errors for currency interpolation. The entry's original postings are left unmodified. Each tuple in the value-list contains: index: The posting index in the original entry. units_currency: The interpolated currency for units. cost_currency: The interpolated currency for cost. price_currency: The interpolated currency for price. \"\"\" errors = [] groups = collections.defaultdict(list) sortdict = {} auto_postings = [] unknown = [] for index, posting in enumerate(entry.postings): units = posting.units cost = posting.cost price = posting.price # Extract and override the currencies locally. units_currency = (units.currency if units is not MISSING and units is not None else None) cost_currency = (cost.currency if cost is not MISSING and cost is not None else None) price_currency = (price.currency if price is not MISSING and price is not None else None) # First we apply the constraint that cost-currency and price-currency # must match, if there is both a cost and a price. This reduces the # space of possibilities somewhat. if cost_currency is MISSING and isinstance(price_currency, str): cost_currency = price_currency if price_currency is MISSING and isinstance(cost_currency, str): price_currency = cost_currency refer = Refer(index, units_currency, cost_currency, price_currency) if units is MISSING and price_currency is None: # Bucket auto-postings separately from unknown. auto_postings.append(refer) else: # Bucket with what we know so far. currency = get_bucket_currency(refer) if currency is not None: sortdict.setdefault(currency, index) groups[currency].append(refer) else: # If we need to infer the currency, store in unknown. unknown.append(refer) # We look at the remaining postings... if they are all of a single currency, # the posting must be in that currency too. if unknown and len(unknown) == 1 and len(groups) == 1: (index, units_currency, cost_currency, price_currency) = unknown.pop() other_currency = next(iter(groups.keys())) if price_currency is None and cost_currency is None: # Infer to the units currency. units_currency = other_currency else: # Infer to the cost and price currencies. if price_currency is MISSING: price_currency = other_currency if cost_currency is MISSING: cost_currency = other_currency refer = Refer(index, units_currency, cost_currency, price_currency) currency = get_bucket_currency(refer) assert currency is not None sortdict.setdefault(currency, index) groups[currency].append(refer) # Finally, try to resolve all the unknown legs using the inventory contents # of each account. for refer in unknown: (index, units_currency, cost_currency, price_currency) = refer posting = entry.postings[index] balance = balances.get(posting.account, None) if balance is None: balance = inventory.Inventory() if units_currency is MISSING: balance_currencies = balance.currencies() if len(balance_currencies) == 1: units_currency = balance_currencies.pop() if cost_currency is MISSING or price_currency is MISSING: balance_cost_currencies = balance.cost_currencies() if len(balance_cost_currencies) == 1: balance_cost_currency = balance_cost_currencies.pop() if price_currency is MISSING: price_currency = balance_cost_currency if cost_currency is MISSING: cost_currency = balance_cost_currency refer = Refer(index, units_currency, cost_currency, price_currency) currency = get_bucket_currency(refer) if currency is not None: sortdict.setdefault(currency, index) groups[currency].append(refer) else: errors.append( CategorizationError(posting.meta, \"Failed to categorize posting {}\".format(index + 1), entry)) # Fill in missing units currencies if some remain as missing. This may occur # if we used the cost or price to bucket the currency but the units currency # was missing. for currency, refers in groups.items(): for rindex, refer in enumerate(refers): if refer.units_currency is MISSING: posting = entry.postings[refer.index] balance = balances.get(posting.account, None) if balance is None: continue balance_currencies = balance.currencies() if len(balance_currencies) == 1: refers[rindex] = refer._replace(units_currency=balance_currencies.pop()) # Deal with auto-postings. if len(auto_postings) > 1: refer = auto_postings[-1] posting = entry.postings[refer.index] errors.append( CategorizationError(posting.meta, \"You may not have more than one auto-posting per currency\", entry)) auto_postings = auto_postings[0:1] for refer in auto_postings: for currency in groups.keys(): sortdict.setdefault(currency, refer.index) groups[currency].append(Refer(refer.index, currency, None, None)) # Issue error for all currencies which we could not resolve. for currency, refers in groups.items(): for refer in refers: posting = entry.postings[refer.index] for currency, name in [(refer.units_currency, 'units'), (refer.cost_currency, 'cost'), (refer.price_currency, 'price')]: if currency is MISSING: errors.append(CategorizationError( posting.meta, \"Could not resolve {} currency\".format(name), entry)) sorted_groups = sorted(groups.items(), key=lambda item: sortdict[item[0]]) return sorted_groups, errors","title":"categorize_by_currency()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.compute_cost_number","text":"Given a CostSpec, return the cost number, if possible to compute. Parameters: costspec \u2013 A parsed instance of CostSpec. units \u2013 An instance of Amount for the units of the position. Returns: If it is not possible to calculate the cost, return None. Otherwise, returns a Decimal instance, the per-unit cost. Source code in beancount/parser/booking_full.py def compute_cost_number(costspec, units): \"\"\"Given a CostSpec, return the cost number, if possible to compute. Args: costspec: A parsed instance of CostSpec. units: An instance of Amount for the units of the position. Returns: If it is not possible to calculate the cost, return None. Otherwise, returns a Decimal instance, the per-unit cost. \"\"\" number_per = costspec.number_per number_total = costspec.number_total if MISSING in (number_per, number_total): return None if number_total is not None: # Compute the per-unit cost if there is some total cost # component involved. cost_total = number_total units_number = units.number if number_per is not None: cost_total += number_per * units_number unit_cost = cost_total / abs(units_number) elif number_per is None: return None else: unit_cost = number_per return unit_cost","title":"compute_cost_number()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.convert_costspec_to_cost","text":"Convert an instance of CostSpec to Cost, if present on the posting. If the posting has no cost, it itself is just returned. Parameters: posting \u2013 An instance of Posting. Returns: An instance of Posting with a possibly replaced 'cost' attribute. Source code in beancount/parser/booking_full.py def convert_costspec_to_cost(posting): \"\"\"Convert an instance of CostSpec to Cost, if present on the posting. If the posting has no cost, it itself is just returned. Args: posting: An instance of Posting. Returns: An instance of Posting with a possibly replaced 'cost' attribute. \"\"\" cost = posting.cost if isinstance(cost, position.CostSpec): if cost is not None: units_number = posting.units.number number_per = cost.number_per number_total = cost.number_total if number_total is not None: # Compute the per-unit cost if there is some total cost # component involved. cost_total = number_total if number_per is not MISSING: cost_total += number_per * units_number unit_cost = cost_total / abs(units_number) else: unit_cost = number_per new_cost = Cost(unit_cost, cost.currency, cost.date, cost.label) posting = posting._replace(units=posting.units, cost=new_cost) return posting","title":"convert_costspec_to_cost()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.get_bucket_currency","text":"Given currency references for a posting, return the bucket currency. Parameters: refer \u2013 An instance of Refer. Returns: A currency string. Source code in beancount/parser/booking_full.py def get_bucket_currency(refer): \"\"\"Given currency references for a posting, return the bucket currency. Args: refer: An instance of Refer. Returns: A currency string. \"\"\" currency = None if isinstance(refer.cost_currency, str): currency = refer.cost_currency elif isinstance(refer.price_currency, str): currency = refer.price_currency elif (refer.cost_currency is None and refer.price_currency is None and isinstance(refer.units_currency, str)): currency = refer.units_currency return currency","title":"get_bucket_currency()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.has_self_reduction","text":"Return true if the postings potentially reduce each other at cost. Parameters: postings \u2013 A list of postings with uninterpolated CostSpec cost instances. methods \u2013 A mapping of account name to their corresponding booking method. Returns: A boolean, true if there's a potential for self-reduction. Source code in beancount/parser/booking_full.py def has_self_reduction(postings, methods): \"\"\"Return true if the postings potentially reduce each other at cost. Args: postings: A list of postings with uninterpolated CostSpec cost instances. methods: A mapping of account name to their corresponding booking method. Returns: A boolean, true if there's a potential for self-reduction. \"\"\" # A mapping of (currency, cost-currency) and sign. cost_changes = {} for posting in postings: cost = posting.cost if cost is None: continue if methods[posting.account] is Booking.NONE: continue key = (posting.account, posting.units.currency) sign = 1 if posting.units.number > ZERO else -1 if cost_changes.setdefault(key, sign) != sign: return True return False","title":"has_self_reduction()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.interpolate_group","text":"Interpolate missing numbers in the set of postings. Parameters: postings \u2013 A list of Posting instances. balances \u2013 A dict of account to its ante-inventory. currency \u2013 The weight currency of this group, used for reporting errors. tolerances \u2013 A dict of currency to tolerance values. Returns: A tuple of postings \u2013 A lit of new posting instances. errors: A list of errors generated during interpolation. interpolated: A boolean, true if we did have to interpolate. In the case of an error, this returns the original list of postings, which is still incomplete. If an error is returned, you should probably skip the transaction altogether, or just not include the postings in it. (An alternative behaviour would be to return only the list of valid postings, but that would likely result in an unbalanced transaction. We do it this way by choice.) Source code in beancount/parser/booking_full.py def interpolate_group(postings, balances, currency, tolerances): \"\"\"Interpolate missing numbers in the set of postings. Args: postings: A list of Posting instances. balances: A dict of account to its ante-inventory. currency: The weight currency of this group, used for reporting errors. tolerances: A dict of currency to tolerance values. Returns: A tuple of postings: A lit of new posting instances. errors: A list of errors generated during interpolation. interpolated: A boolean, true if we did have to interpolate. In the case of an error, this returns the original list of postings, which is still incomplete. If an error is returned, you should probably skip the transaction altogether, or just not include the postings in it. (An alternative behaviour would be to return only the list of valid postings, but that would likely result in an unbalanced transaction. We do it this way by choice.) \"\"\" errors = [] # Figure out which type of amount is missing, by creating a list of # incomplete postings and which type of units is missing. incomplete = [] for index, posting in enumerate(postings): units = posting.units cost = posting.cost price = posting.price # Identify incomplete parts of the Posting components. if units.number is MISSING: incomplete.append((MissingType.UNITS, index)) if isinstance(cost, CostSpec): if cost and cost.number_per is MISSING: incomplete.append((MissingType.COST_PER, index)) if cost and cost.number_total is MISSING: incomplete.append((MissingType.COST_TOTAL, index)) else: # Check that a resolved instance of Cost never needs interpolation. # # Note that in theory we could support the interpolation of regular # per-unit costs in these if we wanted to; but because they're all # reducing postings that have been booked earlier, those never need # to be interpolated. if cost is not None: assert isinstance(cost.number, Decimal), ( \"Internal error: cost has no number: {}\".format(cost)) if price and price.number is MISSING: incomplete.append((MissingType.PRICE, index)) # The replacement posting for the incomplete posting of this group. new_posting = None if len(incomplete) == 0: # If there are no missing numbers, just convert the CostSpec to Cost and # return that. out_postings = [convert_costspec_to_cost(posting) for posting in postings] elif len(incomplete) > 1: # If there is more than a single value to be interpolated, generate an # error and return no postings. _, posting_index = incomplete[0] errors.append(InterpolationError( postings[posting_index].meta, \"Too many missing numbers for currency group '{}'\".format(currency), None)) out_postings = [] else: # If there is a single missing number, calculate it and fill it in here. missing, index = incomplete[0] incomplete_posting = postings[index] # Convert augmenting postings' costs from CostSpec to corresponding Cost # instances, except for the incomplete posting. new_postings = [(posting if posting is incomplete_posting else convert_costspec_to_cost(posting)) for posting in postings] # Compute the balance of the other postings. residual = interpolate.compute_residual(posting for posting in new_postings if posting is not incomplete_posting) assert len(residual) < 2, \"Internal error in grouping postings by currencies.\" if not residual.is_empty(): respos = next(iter(residual)) assert respos.cost is None, ( \"Internal error; cost appears in weight calculation.\") assert respos.units.currency == currency, ( \"Internal error; residual different than currency group.\") weight = -respos.units.number weight_currency = respos.units.currency else: weight = ZERO weight_currency = currency if missing == MissingType.UNITS: units = incomplete_posting.units cost = incomplete_posting.cost if cost: # Handle the special case where we only have total cost. if cost.number_per == ZERO: errors.append(InterpolationError( incomplete_posting.meta, \"Cannot infer per-unit cost only from total\", None)) return postings, errors, True assert cost.currency == weight_currency, ( \"Internal error; residual currency different than missing currency.\") cost_total = cost.number_total or ZERO units_number = (weight - cost_total) / cost.number_per elif incomplete_posting.price: assert incomplete_posting.price.currency == weight_currency, ( \"Internal error; residual currency different than missing currency.\") units_number = weight / incomplete_posting.price.number else: assert units.currency == weight_currency, ( \"Internal error; residual currency different than missing currency.\") units_number = weight # Quantize the interpolated units if necessary. units_number = interpolate.quantize_with_tolerance(tolerances, units.currency, units_number) if weight != ZERO: new_pos = Position(Amount(units_number, units.currency), cost) new_posting = incomplete_posting._replace(units=new_pos.units, cost=new_pos.cost) else: new_posting = None elif missing == MissingType.COST_PER: units = incomplete_posting.units cost = incomplete_posting.cost assert cost.currency == weight_currency, ( \"Internal error; residual currency different than missing currency.\") if units.number != ZERO: number_per = (weight - (cost.number_total or ZERO)) / units.number new_cost = cost._replace(number_per=number_per) new_pos = Position(units, new_cost) new_posting = incomplete_posting._replace(units=new_pos.units, cost=new_pos.cost) else: new_posting = None elif missing == MissingType.COST_TOTAL: units = incomplete_posting.units cost = incomplete_posting.cost assert cost.currency == weight_currency, ( \"Internal error; residual currency different than missing currency.\") number_total = (weight - cost.number_per * units.number) new_cost = cost._replace(number_total=number_total) new_pos = Position(units, new_cost) new_posting = incomplete_posting._replace(units=new_pos.units, cost=new_pos.cost) elif missing == MissingType.PRICE: units = incomplete_posting.units cost = incomplete_posting.cost if cost is not None: errors.append(InterpolationError( incomplete_posting.meta, \"Cannot infer price for postings with units held at cost\", None)) return postings, errors, True else: price = incomplete_posting.price assert price.currency == weight_currency, ( \"Internal error; residual currency different than missing currency.\") new_price_number = abs(weight / units.number) new_posting = incomplete_posting._replace(price=Amount(new_price_number, price.currency)) else: assert False, \"Internal error; Invalid missing type.\" # Replace the number in the posting. if new_posting is not None: # Set meta-data on the new posting to indicate it was interpolated. if new_posting.meta is None: new_posting = new_posting._replace(meta={}) new_posting.meta[interpolate.AUTOMATIC_META] = True # Convert augmenting posting costs from CostSpec to a corresponding # Cost instance. new_postings[index] = convert_costspec_to_cost(new_posting) else: del new_postings[index] out_postings = new_postings assert all(not isinstance(posting.cost, CostSpec) for posting in out_postings) # Check that units are non-zero and that no cost remains negative; issue an # error if this is the case. for posting in out_postings: if posting.cost is None: continue # If there is a cost, we don't allow either a cost value of zero, # nor a zero number of units. Note that we allow a price of zero as # the only special case allowed (for conversion entries), but never # for costs. if posting.units.number == ZERO: errors.append(InterpolationError( posting.meta, 'Amount is zero: \"{}\"'.format(posting.units), None)) if posting.cost.number < ZERO: errors.append(InterpolationError( posting.meta, 'Cost is negative: \"{}\"'.format(posting.cost), None)) return out_postings, errors, (new_posting is not None)","title":"interpolate_group()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.replace_currencies","text":"Replace resolved currencies in the entry's Postings. This essentially applies the findings of categorize_by_currency() to produce new postings with all currencies resolved. Parameters: postings \u2013 A list of Posting instances to replace. refer_groups \u2013 A list of (currency, list of posting references) items as returned by categorize_by_currency(). Returns: A new list of items of (currency, list of Postings), postings for which the currencies have been replaced by their interpolated currency values. Source code in beancount/parser/booking_full.py def replace_currencies(postings, refer_groups): \"\"\"Replace resolved currencies in the entry's Postings. This essentially applies the findings of categorize_by_currency() to produce new postings with all currencies resolved. Args: postings: A list of Posting instances to replace. refer_groups: A list of (currency, list of posting references) items as returned by categorize_by_currency(). Returns: A new list of items of (currency, list of Postings), postings for which the currencies have been replaced by their interpolated currency values. \"\"\" new_groups = [] for currency, refers in refer_groups: new_postings = [] for refer in sorted(refers, key=lambda r: r.index): posting = postings[refer.index] units = posting.units if units is MISSING or units is None: posting = posting._replace(units=Amount(MISSING, refer.units_currency)) else: replace = False cost = posting.cost price = posting.price if units.currency is MISSING: units = Amount(units.number, refer.units_currency) replace = True if cost and cost.currency is MISSING: cost = cost._replace(currency=refer.cost_currency) replace = True if price and price.currency is MISSING: price = Amount(price.number, refer.price_currency) replace = True if replace: posting = posting._replace(units=units, cost=cost, price=price) new_postings.append(posting) new_groups.append((currency, new_postings)) return new_groups","title":"replace_currencies()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.unique_label","text":"Return a globally unique label for cost entries. Source code in beancount/parser/booking_full.py def unique_label() -> Text: \"Return a globally unique label for cost entries.\" return str(uuid.uuid4())","title":"unique_label()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method","text":"Implementations of all the particular booking methods. This code is used by the full booking algorithm.","title":"booking_method"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.AmbiguousMatchError","text":"AmbiguousMatchError(source, message, entry)","title":"AmbiguousMatchError"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.AmbiguousMatchError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_method.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.AmbiguousMatchError.__new__","text":"Create new instance of AmbiguousMatchError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.AmbiguousMatchError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking_method.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.booking_method_AVERAGE","text":"AVERAGE booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_AVERAGE(entry, posting, matches): \"\"\"AVERAGE booking method implementation.\"\"\" booked_reductions = [] booked_matches = [] errors = [AmbiguousMatchError(entry.meta, \"AVERAGE method is not supported\", entry)] return booked_reductions, booked_matches, errors, False # FIXME: Future implementation here. # pylint: disable=unreachable if False: # pylint: disable=using-constant-test # DISABLED - This is the code for AVERAGE, which is currently disabled. # If there is more than a single match we need to ultimately merge the # postings. Also, if the reducing posting provides a specific cost, we # need to update the cost basis as well. Both of these cases are carried # out by removing all the matches and readding them later on. if len(matches) == 1 and ( not isinstance(posting.cost.number_per, Decimal) and not isinstance(posting.cost.number_total, Decimal)): # There is no cost. Just reduce the one leg. This should be the # normal case if we always merge augmentations and the user lets # Beancount deal with the cost. match = matches[0] sign = -1 if posting.units.number < ZERO else 1 number = min(abs(match.units.number), abs(posting.units.number)) match_units = Amount(number * sign, match.units.currency) booked_reductions.append(posting._replace(units=match_units, cost=match.cost)) insufficient = (match_units.number != posting.units.number) else: # Merge the matching postings to a single one. merged_units = inventory.Inventory() merged_cost = inventory.Inventory() for match in matches: merged_units.add_amount(match.units) merged_cost.add_amount(convert.get_weight(match)) if len(merged_units) != 1 or len(merged_cost) != 1: errors.append( AmbiguousMatchError( entry.meta, 'Cannot merge positions in multiple currencies: {}'.format( ', '.join(position.to_string(match_posting) for match_posting in matches)), entry)) else: if (isinstance(posting.cost.number_per, Decimal) or isinstance(posting.cost.number_total, Decimal)): errors.append( AmbiguousMatchError( entry.meta, \"Explicit cost reductions aren't supported yet: {}\".format( position.to_string(posting)), entry)) else: # Insert postings to remove all the matches. booked_reductions.extend( posting._replace(units=-match.units, cost=match.cost, flag=flags.FLAG_MERGING) for match in matches) units = merged_units[0].units date = matches[0].cost.date ## FIXME: Select which one, ## oldest or latest. cost_units = merged_cost[0].units cost = Cost(cost_units.number/units.number, cost_units.currency, date, None) # Insert a posting to refill those with a replacement match. booked_reductions.append( posting._replace(units=units, cost=cost, flag=flags.FLAG_MERGING)) # Now, match the reducing request against this lot. booked_reductions.append( posting._replace(units=posting.units, cost=cost)) insufficient = abs(posting.units.number) > abs(units.number)","title":"booking_method_AVERAGE()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.booking_method_FIFO","text":"FIFO booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_FIFO(entry, posting, matches): \"\"\"FIFO booking method implementation.\"\"\" return _booking_method_xifo(entry, posting, matches, False)","title":"booking_method_FIFO()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.booking_method_LIFO","text":"LIFO booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_LIFO(entry, posting, matches): \"\"\"LIFO booking method implementation.\"\"\" return _booking_method_xifo(entry, posting, matches, True)","title":"booking_method_LIFO()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.booking_method_NONE","text":"NONE booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_NONE(entry, posting, matches): \"\"\"NONE booking method implementation.\"\"\" # This never needs to match against any existing positions... we # disregard the matches, there's never any error. Note that this never # gets called in practice, we want to treat NONE postings as # augmentations. Default behaviour is to return them with their original # CostSpec, and the augmentation code will handle signaling an error if # there is insufficient detail to carry out the conversion to an # instance of Cost. # Note that it's an interesting question whether a reduction on an # account with NONE method which happens to match a single position # ought to be matched against it. We don't allow it for now. return [posting], [], False","title":"booking_method_NONE()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.booking_method_STRICT","text":"Strict booking method. Parameters: entry \u2013 The parent Transaction instance. posting \u2013 An instance of Posting, the reducing posting which we're attempting to match. matches \u2013 A list of matching Position instances from the ante-inventory. Those positions are known to already match the 'posting' spec. Returns: A triple of booked_reductions \u2013 A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. errors: A list of errors to be generated. insufficient: A boolean, true if we could not find enough matches to fulfill the reduction. Source code in beancount/parser/booking_method.py def booking_method_STRICT(entry, posting, matches): \"\"\"Strict booking method. Args: entry: The parent Transaction instance. posting: An instance of Posting, the reducing posting which we're attempting to match. matches: A list of matching Position instances from the ante-inventory. Those positions are known to already match the 'posting' spec. Returns: A triple of booked_reductions: A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. errors: A list of errors to be generated. insufficient: A boolean, true if we could not find enough matches to fulfill the reduction. \"\"\" booked_reductions = [] booked_matches = [] errors = [] insufficient = False # In strict mode, we require at most a single matching posting. if len(matches) > 1: # If the total requested to reduce matches the sum of all the # ambiguous postings, match against all of them. sum_matches = sum(p.units.number for p in matches) if sum_matches == -posting.units.number: booked_reductions.extend( posting._replace(units=-match.units, cost=match.cost) for match in matches) else: errors.append( AmbiguousMatchError(entry.meta, 'Ambiguous matches for \"{}\": {}'.format( position.to_string(posting), ', '.join(position.to_string(match_posting) for match_posting in matches)), entry)) else: # Replace the posting's units and cost values. match = matches[0] sign = -1 if posting.units.number < ZERO else 1 number = min(abs(match.units.number), abs(posting.units.number)) match_units = Amount(number * sign, match.units.currency) booked_reductions.append(posting._replace(units=match_units, cost=match.cost)) booked_matches.append(match) insufficient = (match_units.number != posting.units.number) return booked_reductions, booked_matches, errors, insufficient","title":"booking_method_STRICT()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.handle_ambiguous_matches","text":"Handle ambiguous matches by dispatching to a particular method. Parameters: entry \u2013 The parent Transaction instance. posting \u2013 An instance of Posting, the reducing posting which we're attempting to match. matches \u2013 A list of matching Position instances from the ante-inventory. Those positions are known to already match the 'posting' spec. methods \u2013 A mapping of account name to their corresponding booking method. Returns: A pair of booked_reductions \u2013 A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. errors: A list of errors to be generated. Source code in beancount/parser/booking_method.py def handle_ambiguous_matches(entry, posting, matches, method): \"\"\"Handle ambiguous matches by dispatching to a particular method. Args: entry: The parent Transaction instance. posting: An instance of Posting, the reducing posting which we're attempting to match. matches: A list of matching Position instances from the ante-inventory. Those positions are known to already match the 'posting' spec. methods: A mapping of account name to their corresponding booking method. Returns: A pair of booked_reductions: A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. errors: A list of errors to be generated. \"\"\" assert isinstance(method, Booking), ( \"Invalid type: {}\".format(method)) assert matches, \"Internal error: Invalid call with no matches\" #method = globals()['booking_method_{}'.format(method.name)] method = _BOOKING_METHODS[method] (booked_reductions, booked_matches, errors, insufficient) = method(entry, posting, matches) if insufficient: errors.append( AmbiguousMatchError(entry.meta, 'Not enough lots to reduce \"{}\": {}'.format( position.to_string(posting), ', '.join(position.to_string(match_posting) for match_posting in matches)), entry)) return booked_reductions, booked_matches, errors","title":"handle_ambiguous_matches()"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest","text":"Support utilities for testing scripts.","title":"cmptest"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest.TestError","text":"Errors within the test implementation itself. These should never occur.","title":"TestError"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest.assertEqualEntries","text":"Compare two lists of entries exactly and print missing entries verbosely if they occur. Parameters: expected_entries \u2013 Either a list of directives or a string, in which case the string is run through beancount.parser.parse_string() and the resulting list is used. actual_entries \u2013 Same treatment as expected_entries, the other list of directives to compare to. failfunc \u2013 A function to call on failure. allow_incomplete \u2013 A boolean, true if we allow incomplete inputs and perform light-weight booking. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/parser/cmptest.py def assertEqualEntries(expected_entries, actual_entries, failfunc=DEFAULT_FAILFUNC, allow_incomplete=False): \"\"\"Compare two lists of entries exactly and print missing entries verbosely if they occur. Args: expected_entries: Either a list of directives or a string, in which case the string is run through beancount.parser.parse_string() and the resulting list is used. actual_entries: Same treatment as expected_entries, the other list of directives to compare to. failfunc: A function to call on failure. allow_incomplete: A boolean, true if we allow incomplete inputs and perform light-weight booking. Raises: AssertionError: If the exception fails. \"\"\" expected_entries = read_string_or_entries(expected_entries, allow_incomplete) actual_entries = read_string_or_entries(actual_entries, allow_incomplete) same, expected_missing, actual_missing = compare.compare_entries(expected_entries, actual_entries) if not same: assert expected_missing or actual_missing, \"Missing is missing: {}, {}\".format( expected_missing, actual_missing) oss = io.StringIO() if expected_missing: oss.write(\"Present in expected set and not in actual set:\\n\\n\") for entry in expected_missing: oss.write(printer.format_entry(entry)) oss.write('\\n') if actual_missing: oss.write(\"Present in actual set and not in expected set:\\n\\n\") for entry in actual_missing: oss.write(printer.format_entry(entry)) oss.write('\\n') failfunc(oss.getvalue())","title":"assertEqualEntries()"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest.assertExcludesEntries","text":"Check that subset_entries is not included in entries and print extra entries. Parameters: subset_entries \u2013 Either a list of directives or a string, in which case the string is run through beancount.parser.parse_string() and the resulting list is used. entries \u2013 Same treatment as subset_entries, the other list of directives to compare to. failfunc \u2013 A function to call on failure. allow_incomplete \u2013 A boolean, true if we allow incomplete inputs and perform light-weight booking. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/parser/cmptest.py def assertExcludesEntries(subset_entries, entries, failfunc=DEFAULT_FAILFUNC, allow_incomplete=False): \"\"\"Check that subset_entries is not included in entries and print extra entries. Args: subset_entries: Either a list of directives or a string, in which case the string is run through beancount.parser.parse_string() and the resulting list is used. entries: Same treatment as subset_entries, the other list of directives to compare to. failfunc: A function to call on failure. allow_incomplete: A boolean, true if we allow incomplete inputs and perform light-weight booking. Raises: AssertionError: If the exception fails. \"\"\" subset_entries = read_string_or_entries(subset_entries, allow_incomplete) entries = read_string_or_entries(entries) excludes, extra = compare.excludes_entries(subset_entries, entries) if not excludes: assert extra, \"Extra is empty: {}\".format(extra) oss = io.StringIO() if extra: oss.write(\"Extra from from first/excluded set:\\n\\n\") for entry in extra: oss.write(printer.format_entry(entry)) oss.write('\\n') failfunc(oss.getvalue())","title":"assertExcludesEntries()"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest.assertIncludesEntries","text":"Check that subset_entries is included in entries and print missing entries. Parameters: subset_entries \u2013 Either a list of directives or a string, in which case the string is run through beancount.parser.parse_string() and the resulting list is used. entries \u2013 Same treatment as subset_entries, the other list of directives to compare to. failfunc \u2013 A function to call on failure. allow_incomplete \u2013 A boolean, true if we allow incomplete inputs and perform light-weight booking. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/parser/cmptest.py def assertIncludesEntries(subset_entries, entries, failfunc=DEFAULT_FAILFUNC, allow_incomplete=False): \"\"\"Check that subset_entries is included in entries and print missing entries. Args: subset_entries: Either a list of directives or a string, in which case the string is run through beancount.parser.parse_string() and the resulting list is used. entries: Same treatment as subset_entries, the other list of directives to compare to. failfunc: A function to call on failure. allow_incomplete: A boolean, true if we allow incomplete inputs and perform light-weight booking. Raises: AssertionError: If the exception fails. \"\"\" subset_entries = read_string_or_entries(subset_entries, allow_incomplete) entries = read_string_or_entries(entries) includes, missing = compare.includes_entries(subset_entries, entries) if not includes: assert missing, \"Missing is empty: {}\".format(missing) oss = io.StringIO() if missing: oss.write(\"Missing from from expected set:\\n\\n\") for entry in missing: oss.write(printer.format_entry(entry)) oss.write('\\n') failfunc(oss.getvalue())","title":"assertIncludesEntries()"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest.read_string_or_entries","text":"Read a string of entries or just entries. Parameters: entries_or_str \u2013 Either a list of directives, or a string containing directives. allow_incomplete \u2013 A boolean, true if we allow incomplete inputs and perform light-weight booking. Returns: A list of directives. Source code in beancount/parser/cmptest.py def read_string_or_entries(entries_or_str, allow_incomplete=False): \"\"\"Read a string of entries or just entries. Args: entries_or_str: Either a list of directives, or a string containing directives. allow_incomplete: A boolean, true if we allow incomplete inputs and perform light-weight booking. Returns: A list of directives. \"\"\" if isinstance(entries_or_str, str): entries, errors, options_map = parser.parse_string( textwrap.dedent(entries_or_str)) if allow_incomplete: # Do a simplistic local conversion in order to call the comparison. entries = [_local_booking(entry) for entry in entries] else: # Don't accept incomplete entries either. if any(parser.is_entry_incomplete(entry) for entry in entries): raise TestError(\"Entries in assertions may not use interpolation.\") entries, booking_errors = booking.book(entries, options_map) errors = errors + booking_errors # Don't tolerate errors. if errors: oss = io.StringIO() printer.print_errors(errors, file=oss) raise TestError(\"Unexpected errors in expected: {}\".format(oss.getvalue())) else: assert isinstance(entries_or_str, list), \"Expecting list: {}\".format(entries_or_str) entries = entries_or_str return entries","title":"read_string_or_entries()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar","text":"Builder for Beancount grammar.","title":"grammar"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder","text":"A builder used by the lexer and grammar parser as callbacks to create the data objects corresponding to rules parsed from the input file.","title":"Builder"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.amount","text":"Process an amount grammar rule. Parameters: number \u2013 a Decimal instance, the number of the amount. currency \u2013 a currency object (a str, really, see CURRENCY above) Returns: An instance of Amount. Source code in beancount/parser/grammar.py def amount(self, number, currency): \"\"\"Process an amount grammar rule. Args: number: a Decimal instance, the number of the amount. currency: a currency object (a str, really, see CURRENCY above) Returns: An instance of Amount. \"\"\" # Update the mapping that stores the parsed precisions. # Note: This is relatively slow, adds about 70ms because of number.as_tuple(). self.dcupdate(number, currency) return Amount(number, currency)","title":"amount()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.balance","text":"Process an assertion directive. We produce no errors here by default. We replace the failing ones in the routine that does the verification later one, that these have succeeded or failed. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account to balance. amount \u2013 The expected amount, to be checked. tolerance \u2013 The tolerance number. kvlist \u2013 a list of KeyValue instances. Returns: A new Balance object. Source code in beancount/parser/grammar.py def balance(self, filename, lineno, date, account, amount, tolerance, kvlist): \"\"\"Process an assertion directive. We produce no errors here by default. We replace the failing ones in the routine that does the verification later one, that these have succeeded or failed. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to balance. amount: The expected amount, to be checked. tolerance: The tolerance number. kvlist: a list of KeyValue instances. Returns: A new Balance object. \"\"\" diff_amount = None meta = new_metadata(filename, lineno, kvlist) return Balance(meta, date, account, amount, tolerance, diff_amount)","title":"balance()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.build_grammar_error","text":"Build a grammar error and appends it to the list of pending errors. Parameters: filename \u2013 The current filename lineno \u2013 The current line number excvalue \u2013 The exception value, or a str, the message of the error. exc_type \u2013 An exception type, if an exception occurred. exc_traceback \u2013 A traceback object. Source code in beancount/parser/grammar.py def build_grammar_error(self, filename, lineno, exc_value, exc_type=None, exc_traceback=None): \"\"\"Build a grammar error and appends it to the list of pending errors. Args: filename: The current filename lineno: The current line number excvalue: The exception value, or a str, the message of the error. exc_type: An exception type, if an exception occurred. exc_traceback: A traceback object. \"\"\" if exc_type is not None: assert not isinstance(exc_value, str) strings = traceback.format_exception_only(exc_type, exc_value) tblist = traceback.extract_tb(exc_traceback) filename, lineno, _, __ = tblist[0] message = '{} ({}:{})'.format(strings[0], filename, lineno) else: message = str(exc_value) meta = new_metadata(filename, lineno) self.errors.append( ParserSyntaxError(meta, message, None))","title":"build_grammar_error()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.close","text":"Process a close directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the name of the account. kvlist \u2013 a list of KeyValue instances. Returns: A new Close object. Source code in beancount/parser/grammar.py def close(self, filename, lineno, date, account, kvlist): \"\"\"Process a close directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the name of the account. kvlist: a list of KeyValue instances. Returns: A new Close object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Close(meta, date, account)","title":"close()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.commodity","text":"Process a close directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. currency \u2013 A string, the commodity being declared. kvlist \u2013 a list of KeyValue instances. Returns: A new Close object. Source code in beancount/parser/grammar.py def commodity(self, filename, lineno, date, currency, kvlist): \"\"\"Process a close directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. currency: A string, the commodity being declared. kvlist: a list of KeyValue instances. Returns: A new Close object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Commodity(meta, date, currency)","title":"commodity()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.compound_amount","text":"Process an amount grammar rule. Parameters: number_per \u2013 a Decimal instance, the number of the cost per share. number_total \u2013 a Decimal instance, the number of the cost over all shares. currency \u2013 a currency object (a str, really, see CURRENCY above) Returns: A triple of (Decimal, Decimal, currency string) to be processed further when creating the final per-unit cost number. Source code in beancount/parser/grammar.py def compound_amount(self, number_per, number_total, currency): \"\"\"Process an amount grammar rule. Args: number_per: a Decimal instance, the number of the cost per share. number_total: a Decimal instance, the number of the cost over all shares. currency: a currency object (a str, really, see CURRENCY above) Returns: A triple of (Decimal, Decimal, currency string) to be processed further when creating the final per-unit cost number. \"\"\" # Update the mapping that stores the parsed precisions. # Note: This is relatively slow, adds about 70ms because of number.as_tuple(). self.dcupdate(number_per, currency) self.dcupdate(number_total, currency) # Note that we are not able to reduce the value to a number per-share # here because we only get the number of units in the full lot spec. return CompoundAmount(number_per, number_total, currency)","title":"compound_amount()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.cost_merge","text":"Create a 'merge cost' token. Source code in beancount/parser/grammar.py def cost_merge(self, _): \"\"\"Create a 'merge cost' token.\"\"\" return MERGE_COST","title":"cost_merge()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.cost_spec","text":"Process a cost_spec grammar rule. Parameters: cost_comp_list \u2013 A list of CompoundAmount, a datetime.date, or label ID strings. is_total \u2013 Assume only the total cost is specified; reject the # syntax, that is, no compound amounts may be specified. This is used to support the {{...}} syntax. Returns: A cost-info tuple of CompoundAmount, lot date and label string. Any of these may be set to a sentinel indicating \"unset\". Source code in beancount/parser/grammar.py def cost_spec(self, cost_comp_list, is_total): \"\"\"Process a cost_spec grammar rule. Args: cost_comp_list: A list of CompoundAmount, a datetime.date, or label ID strings. is_total: Assume only the total cost is specified; reject the # syntax, that is, no compound amounts may be specified. This is used to support the {{...}} syntax. Returns: A cost-info tuple of CompoundAmount, lot date and label string. Any of these may be set to a sentinel indicating \"unset\". \"\"\" if not cost_comp_list: return CostSpec(MISSING, None, MISSING, None, None, False) assert isinstance(cost_comp_list, list), ( \"Internal error in parser: {}\".format(cost_comp_list)) compound_cost = None date_ = None label = None merge = None for comp in cost_comp_list: if isinstance(comp, CompoundAmount): if compound_cost is None: compound_cost = comp else: self.errors.append( ParserError(self.get_lexer_location(), \"Duplicate cost: '{}'.\".format(comp), None)) elif isinstance(comp, date): if date_ is None: date_ = comp else: self.errors.append( ParserError(self.get_lexer_location(), \"Duplicate date: '{}'.\".format(comp), None)) elif comp is MERGE_COST: if merge is None: merge = True self.errors.append( ParserError(self.get_lexer_location(), \"Cost merging is not supported yet\", None)) else: self.errors.append( ParserError(self.get_lexer_location(), \"Duplicate merge-cost spec\", None)) else: assert isinstance(comp, str), ( \"Currency component is not string: '{}'\".format(comp)) if label is None: label = comp else: self.errors.append( ParserError(self.get_lexer_location(), \"Duplicate label: '{}'.\".format(comp), None)) # If there was a cost_comp_list, thus a \"{...}\" cost basis spec, you must # indicate that by creating a CompoundAmount(), always. if compound_cost is None: number_per, number_total, currency = MISSING, None, MISSING else: number_per, number_total, currency = compound_cost if is_total: if number_total is not None: self.errors.append( ParserError( self.get_lexer_location(), (\"Per-unit cost may not be specified using total cost \" \"syntax: '{}'; ignoring per-unit cost\").format(compound_cost), None)) # Ignore per-unit number. number_per = ZERO else: # There's a single number specified; interpret it as a total cost. number_total = number_per number_per = ZERO if merge is None: merge = False return CostSpec(number_per, number_total, currency, date_, label, merge)","title":"cost_spec()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.custom","text":"Process a custom directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. dir_type \u2013 A string, a type for the custom directive being parsed. custom_values \u2013 A list of the various tokens seen on the same line. kvlist \u2013 a list of KeyValue instances. Returns: A new Custom object. Source code in beancount/parser/grammar.py def custom(self, filename, lineno, date, dir_type, custom_values, kvlist): \"\"\"Process a custom directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. dir_type: A string, a type for the custom directive being parsed. custom_values: A list of the various tokens seen on the same line. kvlist: a list of KeyValue instances. Returns: A new Custom object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Custom(meta, date, dir_type, custom_values)","title":"custom()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.custom_value","text":"Create a custom value object, along with its type. Parameters: value \u2013 One of the accepted custom values. Returns: A pair of (value, dtype) where 'dtype' is the datatype is that of the value. Source code in beancount/parser/grammar.py def custom_value(self, value, dtype=None): \"\"\"Create a custom value object, along with its type. Args: value: One of the accepted custom values. Returns: A pair of (value, dtype) where 'dtype' is the datatype is that of the value. \"\"\" if dtype is None: dtype = type(value) return ValueType(value, dtype)","title":"custom_value()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.dcupdate","text":"Update the display context. Source code in beancount/parser/grammar.py def dcupdate(self, number, currency): \"\"\"Update the display context.\"\"\" if isinstance(number, Decimal) and currency and currency is not MISSING: self._dcupdate(number, currency)","title":"dcupdate()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.document","text":"Process a document directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. account \u2013 an Account instance. document_filename \u2013 a str, the name of the document file. tags_links \u2013 The current TagsLinks accumulator. kvlist \u2013 a list of KeyValue instances. Returns: A new Document object. Source code in beancount/parser/grammar.py def document(self, filename, lineno, date, account, document_filename, tags_links, kvlist): \"\"\"Process a document directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. account: an Account instance. document_filename: a str, the name of the document file. tags_links: The current TagsLinks accumulator. kvlist: a list of KeyValue instances. Returns: A new Document object. \"\"\" meta = new_metadata(filename, lineno, kvlist) if not path.isabs(document_filename): document_filename = path.abspath(path.join(path.dirname(filename), document_filename)) tags, links = self.finalize_tags_links(tags_links.tags, tags_links.links) return Document(meta, date, account, document_filename, tags, links)","title":"document()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.event","text":"Process an event directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. event_type \u2013 a str, the name of the event type. description \u2013 a str, the event value, the contents. kvlist \u2013 a list of KeyValue instances. Returns: A new Event object. Source code in beancount/parser/grammar.py def event(self, filename, lineno, date, event_type, description, kvlist): \"\"\"Process an event directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. event_type: a str, the name of the event type. description: a str, the event value, the contents. kvlist: a list of KeyValue instances. Returns: A new Event object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Event(meta, date, event_type, description)","title":"event()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.finalize","text":"Finalize the parser, check for final errors and return the triple. Returns: A triple of entries \u2013 A list of parsed directives, which may need completion. errors: A list of errors, hopefully empty. options_map: A dict of options. Source code in beancount/parser/grammar.py def finalize(self): \"\"\"Finalize the parser, check for final errors and return the triple. Returns: A triple of entries: A list of parsed directives, which may need completion. errors: A list of errors, hopefully empty. options_map: A dict of options. \"\"\" # If the user left some tags unbalanced, issue an error. for tag in self.tags: meta = new_metadata(self.options['filename'], 0) self.errors.append( ParserError(meta, \"Unbalanced pushed tag: '{}'\".format(tag), None)) # If the user left some metadata unpopped, issue an error. for key, value_list in self.meta.items(): meta = new_metadata(self.options['filename'], 0) self.errors.append( ParserError(meta, ( \"Unbalanced metadata key '{}'; leftover metadata '{}'\").format( key, ', '.join(value_list)), None)) # Weave the commas option in the DisplayContext itself, so it propagates # everywhere it is used automatically. self.dcontext.set_commas(self.options['render_commas']) return (self.get_entries(), self.errors, self.get_options())","title":"finalize()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.finalize_tags_links","text":"Finally amend tags and links and return final objects to be inserted. Parameters: tags \u2013 A set of tag strings (warning: this gets mutated in-place). links \u2013 A set of link strings. Returns: A sanitized pair of (tags, links). Source code in beancount/parser/grammar.py def finalize_tags_links(self, tags, links): \"\"\"Finally amend tags and links and return final objects to be inserted. Args: tags: A set of tag strings (warning: this gets mutated in-place). links: A set of link strings. Returns: A sanitized pair of (tags, links). \"\"\" if self.tags: tags.update(self.tags) return (frozenset(tags) if tags else EMPTY_SET, frozenset(links) if links else EMPTY_SET)","title":"finalize_tags_links()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.get_entries","text":"Return the accumulated entries. Returns: A list of sorted directives. Source code in beancount/parser/grammar.py def get_entries(self): \"\"\"Return the accumulated entries. Returns: A list of sorted directives. \"\"\" return sorted(self.entries, key=data.entry_sortkey)","title":"get_entries()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.get_invalid_account","text":"See base class. Source code in beancount/parser/grammar.py def get_invalid_account(self): \"\"\"See base class.\"\"\" return account.join(self.options['name_equity'], 'InvalidAccountName')","title":"get_invalid_account()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.get_long_string_maxlines","text":"See base class. Source code in beancount/parser/grammar.py def get_long_string_maxlines(self): \"\"\"See base class.\"\"\" return self.options['long_string_maxlines']","title":"get_long_string_maxlines()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.get_options","text":"Return the final options map. Returns: A dict of option names to options. Source code in beancount/parser/grammar.py def get_options(self): \"\"\"Return the final options map. Returns: A dict of option names to options. \"\"\" # Build and store the inferred DisplayContext instance. self.options['dcontext'] = self.dcontext # Add the full list of seen commodities. # # IMPORTANT: This is currently where the list of all commodities seen # from the parser lives. The # beancount.core.getters.get_commodities_map() routine uses this to # automatically generate a full list of directives. An alternative would # be to implement a plugin that enforces the generate of these # post-parsing so that they are always guaranteed to live within the # flow of entries. This would allow us to keep all the data in that list # of entries and to avoid depending on the options to store that output. self.options['commodities'] = self.commodities return self.options","title":"get_options()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.handle_list","text":"Handle a recursive list grammar rule, generically. Parameters: object_list \u2013 the current list of objects. new_object \u2013 the new object to be added. Returns: The new, updated list of objects. Source code in beancount/parser/grammar.py def handle_list(self, object_list, new_object): \"\"\"Handle a recursive list grammar rule, generically. Args: object_list: the current list of objects. new_object: the new object to be added. Returns: The new, updated list of objects. \"\"\" if object_list is None: object_list = [] if new_object is not None: object_list.append(new_object) return object_list","title":"handle_list()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.include","text":"Process an include directive. Parameters: filename \u2013 current filename. lineno \u2013 current line number. include_name \u2013 A string, the name of the file to include. Source code in beancount/parser/grammar.py def include(self, filename, lineno, include_filename): \"\"\"Process an include directive. Args: filename: current filename. lineno: current line number. include_name: A string, the name of the file to include. \"\"\" self.options['include'].append(include_filename)","title":"include()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.key_value","text":"Process a document directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account the document relates to. document_filename \u2013 A str, the name of the document file. Returns: A new KeyValue object. Source code in beancount/parser/grammar.py def key_value(self, key, value): \"\"\"Process a document directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account the document relates to. document_filename: A str, the name of the document file. Returns: A new KeyValue object. \"\"\" return KeyValue(key, value)","title":"key_value()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.note","text":"Process a note directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account to attach the note to. comment \u2013 A str, the note's comments contents. kvlist \u2013 a list of KeyValue instances. Returns: A new Note object. Source code in beancount/parser/grammar.py def note(self, filename, lineno, date, account, comment, kvlist): \"\"\"Process a note directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to attach the note to. comment: A str, the note's comments contents. kvlist: a list of KeyValue instances. Returns: A new Note object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Note(meta, date, account, comment)","title":"note()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.open","text":"Process an open directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the name of the account. currencies \u2013 A list of constraint currencies. booking_str \u2013 A string, the booking method, or None if none was specified. kvlist \u2013 a list of KeyValue instances. Returns: A new Open object. Source code in beancount/parser/grammar.py def open(self, filename, lineno, date, account, currencies, booking_str, kvlist): \"\"\"Process an open directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the name of the account. currencies: A list of constraint currencies. booking_str: A string, the booking method, or None if none was specified. kvlist: a list of KeyValue instances. Returns: A new Open object. \"\"\" meta = new_metadata(filename, lineno, kvlist) error = False if booking_str: try: # Note: Somehow the 'in' membership operator is not defined on Enum. booking = Booking[booking_str] except KeyError: # If the per-account method is invalid, set it to the global # default method and continue. booking = self.options['booking_method'] error = True else: booking = None entry = Open(meta, date, account, currencies, booking) if error: self.errors.append(ParserError(meta, \"Invalid booking method: {}\".format(booking_str), entry)) return entry","title":"open()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.option","text":"Process an option directive. Parameters: filename \u2013 current filename. lineno \u2013 current line number. key \u2013 option's key (str) value \u2013 option's value Source code in beancount/parser/grammar.py def option(self, filename, lineno, key, value): \"\"\"Process an option directive. Args: filename: current filename. lineno: current line number. key: option's key (str) value: option's value \"\"\" if key not in self.options: meta = new_metadata(filename, lineno) self.errors.append( ParserError(meta, \"Invalid option: '{}'\".format(key), None)) elif key in options.READ_ONLY_OPTIONS: meta = new_metadata(filename, lineno) self.errors.append( ParserError(meta, \"Option '{}' may not be set\".format(key), None)) else: option_descriptor = options.OPTIONS[key] # Issue a warning if the option is deprecated. if option_descriptor.deprecated: assert isinstance(option_descriptor.deprecated, str), \"Internal error.\" meta = new_metadata(filename, lineno) self.errors.append( DeprecatedError(meta, option_descriptor.deprecated, None)) # Rename the option if it has an alias. if option_descriptor.alias: key = option_descriptor.alias option_descriptor = options.OPTIONS[key] # Convert the value, if necessary. if option_descriptor.converter: try: value = option_descriptor.converter(value) except ValueError as exc: meta = new_metadata(filename, lineno) self.errors.append( ParserError(meta, \"Error for option '{}': {}\".format(key, exc), None)) return option = self.options[key] if isinstance(option, list): # Append to a list of values. option.append(value) elif isinstance(option, dict): # Set to a dict of values. if not (isinstance(value, tuple) and len(value) == 2): self.errors.append( ParserError( meta, \"Error for option '{}': {}\".format(key, value), None)) return dict_key, dict_value = value option[dict_key] = dict_value elif isinstance(option, bool): # Convert to a boolean. if not isinstance(value, bool): value = (value.lower() in {'true', 'on'}) or (value == '1') self.options[key] = value else: # Set the value. self.options[key] = value # Refresh the list of valid account regexps as we go along. if key.startswith('name_'): # Update the set of valid account types. self.account_regexp = valid_account_regexp(self.options) elif key == 'insert_pythonpath': # Insert the PYTHONPATH to this file when and only if you # encounter this option. sys.path.insert(0, path.dirname(filename))","title":"option()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.pad","text":"Process a pad directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account to be padded. source_account \u2013 A string, the account to pad from. kvlist \u2013 a list of KeyValue instances. Returns: A new Pad object. Source code in beancount/parser/grammar.py def pad(self, filename, lineno, date, account, source_account, kvlist): \"\"\"Process a pad directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to be padded. source_account: A string, the account to pad from. kvlist: a list of KeyValue instances. Returns: A new Pad object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Pad(meta, date, account, source_account)","title":"pad()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.pipe_deprecated_error","text":"Issue a 'Pipe deprecated' error. Parameters: filename \u2013 The current filename lineno \u2013 The current line number Source code in beancount/parser/grammar.py def pipe_deprecated_error(self, filename, lineno): \"\"\"Issue a 'Pipe deprecated' error. Args: filename: The current filename lineno: The current line number \"\"\" if self.options['allow_pipe_separator']: return meta = new_metadata(filename, lineno) self.errors.append( ParserSyntaxError(meta, \"Pipe symbol is deprecated.\", None))","title":"pipe_deprecated_error()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.plugin","text":"Process a plugin directive. Parameters: filename \u2013 current filename. lineno \u2013 current line number. plugin_name \u2013 A string, the name of the plugin module to import. plugin_config \u2013 A string or None, an optional configuration string to pass in to the plugin module. Source code in beancount/parser/grammar.py def plugin(self, filename, lineno, plugin_name, plugin_config): \"\"\"Process a plugin directive. Args: filename: current filename. lineno: current line number. plugin_name: A string, the name of the plugin module to import. plugin_config: A string or None, an optional configuration string to pass in to the plugin module. \"\"\" self.options['plugin'].append((plugin_name, plugin_config))","title":"plugin()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.popmeta","text":"Removed a key off the current set of stacks. Parameters: key \u2013 A string, a key to be removed from the meta dict. Source code in beancount/parser/grammar.py def popmeta(self, key): \"\"\"Removed a key off the current set of stacks. Args: key: A string, a key to be removed from the meta dict. \"\"\" try: if key not in self.meta: raise IndexError value_list = self.meta[key] value_list.pop(-1) if not value_list: self.meta.pop(key) except IndexError: meta = new_metadata(self.options['filename'], 0) self.errors.append( ParserError(meta, \"Attempting to pop absent metadata key: '{}'\".format(key), None))","title":"popmeta()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.poptag","text":"Pop a tag off the current set of stacks. Parameters: tag \u2013 A string, a tag to be removed from the current set of tags. Source code in beancount/parser/grammar.py def poptag(self, tag): \"\"\"Pop a tag off the current set of stacks. Args: tag: A string, a tag to be removed from the current set of tags. \"\"\" try: self.tags.remove(tag) except ValueError: meta = new_metadata(self.options['filename'], 0) self.errors.append( ParserError(meta, \"Attempting to pop absent tag: '{}'\".format(tag), None))","title":"poptag()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.posting","text":"Process a posting grammar rule. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. account \u2013 A string, the account of the posting. units \u2013 An instance of Amount for the units. cost \u2013 An instance of CostSpec for the cost. price \u2013 Either None, or an instance of Amount that is the cost of the position. istotal \u2013 A bool, True if the price is for the total amount being parsed, or False if the price is for each lot of the position. flag \u2013 A string, one-character, the flag associated with this posting. Returns: A new Posting object, with no parent entry. Source code in beancount/parser/grammar.py def posting(self, filename, lineno, account, units, cost, price, istotal, flag): \"\"\"Process a posting grammar rule. Args: filename: the current filename. lineno: the current line number. account: A string, the account of the posting. units: An instance of Amount for the units. cost: An instance of CostSpec for the cost. price: Either None, or an instance of Amount that is the cost of the position. istotal: A bool, True if the price is for the total amount being parsed, or False if the price is for each lot of the position. flag: A string, one-character, the flag associated with this posting. Returns: A new Posting object, with no parent entry. \"\"\" meta = new_metadata(filename, lineno) # Prices may not be negative. if price and isinstance(price.number, Decimal) and price.number < ZERO: self.errors.append( ParserError(meta, ( \"Negative prices are not allowed: {} \" \"(see http://furius.ca/beancount/doc/bug-negative-prices \" \"for workaround)\" ).format(price), None)) # Fix it and continue. price = Amount(abs(price.number), price.currency) # If the price is specified for the entire amount, compute the effective # price here and forget about that detail of the input syntax. if istotal: if units.number == ZERO: number = ZERO else: number = price.number if number is not MISSING: number = number/abs(units.number) price = Amount(number, price.currency) # Note: Allow zero prices because we need them for round-trips for # conversion entries. # # if price is not None and price.number == ZERO: # self.errors.append( # ParserError(meta, \"Price is zero: {}\".format(price), None)) # If both cost and price are specified, the currencies must match, or # that is an error. if (cost is not None and price is not None and isinstance(cost.currency, str) and isinstance(price.currency, str) and cost.currency != price.currency): self.errors.append( ParserError(meta, \"Cost and price currencies must match: {} != {}\".format( cost.currency, price.currency), None)) return Posting(account, units, cost, price, chr(flag) if flag else None, meta)","title":"posting()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.price","text":"Process a price directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. currency \u2013 the currency to be priced. amount \u2013 an instance of Amount, that is the price of the currency. kvlist \u2013 a list of KeyValue instances. Returns: A new Price object. Source code in beancount/parser/grammar.py def price(self, filename, lineno, date, currency, amount, kvlist): \"\"\"Process a price directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. currency: the currency to be priced. amount: an instance of Amount, that is the price of the currency. kvlist: a list of KeyValue instances. Returns: A new Price object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Price(meta, date, currency, amount)","title":"price()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.pushmeta","text":"Set a metadata field on the current key-value pairs to be added to transactions. Parameters: key_value \u2013 A KeyValue instance, to be added to the dict of metadata. Source code in beancount/parser/grammar.py def pushmeta(self, key, value): \"\"\"Set a metadata field on the current key-value pairs to be added to transactions. Args: key_value: A KeyValue instance, to be added to the dict of metadata. \"\"\" self.meta[key].append(value)","title":"pushmeta()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.pushtag","text":"Push a tag on the current set of tags. Note that this does not need to be stack ordered. Parameters: tag \u2013 A string, a tag to be added. Source code in beancount/parser/grammar.py def pushtag(self, tag): \"\"\"Push a tag on the current set of tags. Note that this does not need to be stack ordered. Args: tag: A string, a tag to be added. \"\"\" self.tags.append(tag)","title":"pushtag()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.query","text":"Process a document directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. query_name \u2013 a str, the name of the query. query_string \u2013 a str, the SQL query itself. kvlist \u2013 a list of KeyValue instances. Returns: A new Query object. Source code in beancount/parser/grammar.py def query(self, filename, lineno, date, query_name, query_string, kvlist): \"\"\"Process a document directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. query_name: a str, the name of the query. query_string: a str, the SQL query itself. kvlist: a list of KeyValue instances. Returns: A new Query object. \"\"\" meta = new_metadata(filename, lineno, kvlist) return Query(meta, date, query_name, query_string)","title":"query()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.store_result","text":"Start rule stores the final result here. Parameters: entries \u2013 A list of entries to store. Source code in beancount/parser/grammar.py def store_result(self, entries): \"\"\"Start rule stores the final result here. Args: entries: A list of entries to store. \"\"\" if entries: self.entries = entries","title":"store_result()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.tag_link_LINK","text":"Add a link to the TagsLinks accumulator. Parameters: tags_links \u2013 The current TagsLinks accumulator. link \u2013 A string, the new link to insert. Returns: An updated TagsLinks instance. Source code in beancount/parser/grammar.py def tag_link_LINK(self, tags_links, link): \"\"\"Add a link to the TagsLinks accumulator. Args: tags_links: The current TagsLinks accumulator. link: A string, the new link to insert. Returns: An updated TagsLinks instance. \"\"\" tags_links.links.add(link) return tags_links","title":"tag_link_LINK()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.tag_link_STRING","text":"Add a string to the TagsLinks accumulator. Parameters: tags_links \u2013 The current TagsLinks accumulator. string \u2013 A string, the new string to insert in the list. Returns: An updated TagsLinks instance. Source code in beancount/parser/grammar.py def tag_link_STRING(self, tags_links, string): \"\"\"Add a string to the TagsLinks accumulator. Args: tags_links: The current TagsLinks accumulator. string: A string, the new string to insert in the list. Returns: An updated TagsLinks instance. \"\"\" tags_links.strings.append(string) return tags_links","title":"tag_link_STRING()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.tag_link_TAG","text":"Add a tag to the TagsLinks accumulator. Parameters: tags_links \u2013 The current TagsLinks accumulator. tag \u2013 A string, the new tag to insert. Returns: An updated TagsLinks instance. Source code in beancount/parser/grammar.py def tag_link_TAG(self, tags_links, tag): \"\"\"Add a tag to the TagsLinks accumulator. Args: tags_links: The current TagsLinks accumulator. tag: A string, the new tag to insert. Returns: An updated TagsLinks instance. \"\"\" tags_links.tags.add(tag) return tags_links","title":"tag_link_TAG()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.tag_link_new","text":"Create a new TagsLinks instance. Returns: An instance of TagsLinks, initialized with expected attributes. Source code in beancount/parser/grammar.py def tag_link_new(self, _): \"\"\"Create a new TagsLinks instance. Returns: An instance of TagsLinks, initialized with expected attributes. \"\"\" return TagsLinks(set(), set())","title":"tag_link_new()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.transaction","text":"Process a transaction directive. All the postings of the transaction are available at this point, and so the the transaction is balanced here, incomplete postings are completed with the appropriate position, and errors are being accumulated on the builder to be reported later on. This is the main routine that takes up most of the parsing time; be very careful with modifications here, they have an impact on performance. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. flag \u2013 a str, one-character, the flag associated with this transaction. txn_strings \u2013 A list of strings, possibly empty, possibly longer. tags_links \u2013 A TagsLinks namedtuple of tags, and/or links. posting_or_kv_list \u2013 a list of Posting or KeyValue instances, to be inserted in this transaction, or None, if no postings have been declared. Returns: A new Transaction object. Source code in beancount/parser/grammar.py def transaction(self, filename, lineno, date, flag, txn_strings, tags_links, posting_or_kv_list): \"\"\"Process a transaction directive. All the postings of the transaction are available at this point, and so the the transaction is balanced here, incomplete postings are completed with the appropriate position, and errors are being accumulated on the builder to be reported later on. This is the main routine that takes up most of the parsing time; be very careful with modifications here, they have an impact on performance. Args: filename: the current filename. lineno: the current line number. date: a datetime object. flag: a str, one-character, the flag associated with this transaction. txn_strings: A list of strings, possibly empty, possibly longer. tags_links: A TagsLinks namedtuple of tags, and/or links. posting_or_kv_list: a list of Posting or KeyValue instances, to be inserted in this transaction, or None, if no postings have been declared. Returns: A new Transaction object. \"\"\" meta = new_metadata(filename, lineno) # Separate postings and key-values. explicit_meta = {} postings = [] tags, links = tags_links.tags, tags_links.links if posting_or_kv_list: last_posting = None for posting_or_kv in posting_or_kv_list: if isinstance(posting_or_kv, Posting): postings.append(posting_or_kv) last_posting = posting_or_kv elif isinstance(posting_or_kv, TagsLinks): if postings: self.errors.append(ParserError( meta, \"Tags or links not allowed after first \" + \"Posting: {}\".format(posting_or_kv), None)) else: tags.update(posting_or_kv.tags) links.update(posting_or_kv.links) else: if last_posting is None: value = explicit_meta.setdefault(posting_or_kv.key, posting_or_kv.value) if value is not posting_or_kv.value: self.errors.append(ParserError( meta, \"Duplicate metadata field on entry: {}\".format( posting_or_kv), None)) else: if last_posting.meta is None: last_posting = last_posting._replace(meta={}) postings.pop(-1) postings.append(last_posting) value = last_posting.meta.setdefault(posting_or_kv.key, posting_or_kv.value) if value is not posting_or_kv.value: self.errors.append(ParserError( meta, \"Duplicate posting metadata field: {}\".format( posting_or_kv), None)) # Freeze the tags & links or set to default empty values. tags, links = self.finalize_tags_links(tags, links) # Initialize the metadata fields from the set of active values. if self.meta: for key, value_list in self.meta.items(): meta[key] = value_list[-1] # Add on explicitly defined values. if explicit_meta: meta.update(explicit_meta) # Unpack the transaction fields. payee_narration = self.unpack_txn_strings(txn_strings, meta) if payee_narration is None: return None payee, narration = payee_narration # We now allow a single posting when its balance is zero, so we # commented out the check below. If a transaction has a single posting # with a non-zero balance, it'll get caught below in the booking code. # # # Detect when a transaction does not have at least two legs. # if postings is None or len(postings) < 2: # self.errors.append( # ParserError(meta, # \"Transaction with only one posting: {}\".format(postings), # None)) # return None # If there are no postings, make sure we insert a list object. if postings is None: postings = [] # Create the transaction. return Transaction(meta, date, chr(flag), payee, narration, tags, links, postings)","title":"transaction()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.unpack_txn_strings","text":"Unpack a tags_links accumulator to its payee and narration fields. Parameters: txn_strings \u2013 A list of strings. meta \u2013 A metadata dict for errors generated in this routine. Returns: A pair of (payee, narration) strings or None objects, or None, if there was an error. Source code in beancount/parser/grammar.py def unpack_txn_strings(self, txn_strings, meta): \"\"\"Unpack a tags_links accumulator to its payee and narration fields. Args: txn_strings: A list of strings. meta: A metadata dict for errors generated in this routine. Returns: A pair of (payee, narration) strings or None objects, or None, if there was an error. \"\"\" num_strings = 0 if txn_strings is None else len(txn_strings) if num_strings == 1: payee, narration = None, txn_strings[0] elif num_strings == 2: payee, narration = txn_strings elif num_strings == 0: payee, narration = None, \"\" else: self.errors.append( ParserError(meta, \"Too many strings on transaction description: {}\".format( txn_strings), None)) return None return payee, narration","title":"unpack_txn_strings()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.CompoundAmount","text":"CompoundAmount(number_per, number_total, currency)","title":"CompoundAmount"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.CompoundAmount.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.CompoundAmount.__new__","text":"Create new instance of CompoundAmount(number_per, number_total, currency)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.CompoundAmount.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.DeprecatedError","text":"DeprecatedError(source, message, entry)","title":"DeprecatedError"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.DeprecatedError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.DeprecatedError.__new__","text":"Create new instance of DeprecatedError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.DeprecatedError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.KeyValue","text":"KeyValue(key, value)","title":"KeyValue"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.KeyValue.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.KeyValue.__new__","text":"Create new instance of KeyValue(key, value)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.KeyValue.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserError","text":"ParserError(source, message, entry)","title":"ParserError"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserError.__new__","text":"Create new instance of ParserError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserSyntaxError","text":"ParserSyntaxError(source, message, entry)","title":"ParserSyntaxError"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserSyntaxError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserSyntaxError.__new__","text":"Create new instance of ParserSyntaxError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserSyntaxError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.TagsLinks","text":"TagsLinks(tags, links)","title":"TagsLinks"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.TagsLinks.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.TagsLinks.__new__","text":"Create new instance of TagsLinks(tags, links)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.TagsLinks.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ValueType","text":"ValueType(value, dtype)","title":"ValueType"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ValueType.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ValueType.__new__","text":"Create new instance of ValueType(value, dtype)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ValueType.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.valid_account_regexp","text":"Build a regexp to validate account names from the options. Parameters: options \u2013 A dict of options, as per beancount.parser.options. Returns: A string, a regular expression that will match all account names. Source code in beancount/parser/grammar.py def valid_account_regexp(options): \"\"\"Build a regexp to validate account names from the options. Args: options: A dict of options, as per beancount.parser.options. Returns: A string, a regular expression that will match all account names. \"\"\" names = map(options.__getitem__, ('name_assets', 'name_liabilities', 'name_equity', 'name_income', 'name_expenses')) # Replace the first term of the account regular expression with the specific # names allowed under the options configuration. This code is kept in sync # with {5672c7270e1e}. return re.compile(\"(?:{})(?:{}{})+\".format('|'.join(names), account.sep, account.ACC_COMP_NAME_RE))","title":"valid_account_regexp()"},{"location":"api_reference/beancount.parser.html#beancount.parser.hashsrc","text":"Compute a hash of the source files in order to warn when the source goes out of date.","title":"hashsrc"},{"location":"api_reference/beancount.parser.html#beancount.parser.hashsrc.check_parser_source_files","text":"Check the extension module's source hash and issue a warning if the current source differs from that of the module. If the source files aren't located in the Python source directory, ignore the warning, we're probably running this from an installed based, in which case we don't need to check anything (this check is useful only for people running directly from source). Source code in beancount/parser/hashsrc.py def check_parser_source_files(): \"\"\"Check the extension module's source hash and issue a warning if the current source differs from that of the module. If the source files aren't located in the Python source directory, ignore the warning, we're probably running this from an installed based, in which case we don't need to check anything (this check is useful only for people running directly from source). \"\"\" parser_source_hash = hash_parser_source_files() if parser_source_hash is None: return # pylint: disable=import-outside-toplevel from . import _parser if _parser.SOURCE_HASH and _parser.SOURCE_HASH != parser_source_hash: warnings.warn( (\"The Beancount parser C extension module is out-of-date ('{}' != '{}'). \" \"You need to rebuild.\").format(_parser.SOURCE_HASH, parser_source_hash))","title":"check_parser_source_files()"},{"location":"api_reference/beancount.parser.html#beancount.parser.hashsrc.hash_parser_source_files","text":"Compute a unique hash of the parser's Python code in order to bake that into the extension module. This is used at load-time to verify that the extension module and the corresponding Python codes match each other. If not, it issues a warning that you should rebuild your extension module. Returns: A string, the hexadecimal unique hash of relevant source code that should trigger a recompilation. Source code in beancount/parser/hashsrc.py def hash_parser_source_files(): \"\"\"Compute a unique hash of the parser's Python code in order to bake that into the extension module. This is used at load-time to verify that the extension module and the corresponding Python codes match each other. If not, it issues a warning that you should rebuild your extension module. Returns: A string, the hexadecimal unique hash of relevant source code that should trigger a recompilation. \"\"\" md5 = hashlib.md5() for filename in PARSER_SOURCE_FILES: fullname = path.join(path.dirname(__file__), filename) if not path.exists(fullname): return None with open(fullname, 'rb') as file: md5.update(file.read()) # Note: Prepend a character in front of the hash because under Windows MSDEV # removes escapes, and if the hash starts with a number it fails to # recognize this is a string. A small compromise for portability. return md5.hexdigest()","title":"hash_parser_source_files()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer","text":"Beancount syntax lexer.","title":"lexer"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder","text":"A builder used only for building lexer objects. Attributes: Name Type Description long_string_maxlines_default Number of lines for a string to trigger a warning. This is meant to help users detecting dangling quotes in their source.","title":"LexBuilder"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder.ACCOUNT","text":"Process an ACCOUNT token. This function attempts to reuse an existing account if one exists, otherwise creates one on-demand. Parameters: account_name \u2013 a str, the valid name of an account. Returns: A string, the name of the account. Source code in beancount/parser/lexer.py def ACCOUNT(self, account_name): \"\"\"Process an ACCOUNT token. This function attempts to reuse an existing account if one exists, otherwise creates one on-demand. Args: account_name: a str, the valid name of an account. Returns: A string, the name of the account. \"\"\" # Check account name validity. if not self.account_regexp.match(account_name): raise ValueError(\"Invalid account name: {}\".format(account_name)) # Reuse (intern) account strings as much as possible. This potentially # reduces memory usage a fair bit, because these strings are repeated # liberally. return self.accounts.setdefault(account_name, account_name)","title":"ACCOUNT()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder.CURRENCY","text":"Process a CURRENCY token. Parameters: currency_name \u2013 the name of the currency. Returns: A new currency object; for now, these are simply represented as the currency name. Source code in beancount/parser/lexer.py def CURRENCY(self, currency_name): \"\"\"Process a CURRENCY token. Args: currency_name: the name of the currency. Returns: A new currency object; for now, these are simply represented as the currency name. \"\"\" self.commodities.add(currency_name) return currency_name","title":"CURRENCY()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder.DATE","text":"Process a DATE token. Parameters: year \u2013 integer year. month \u2013 integer month. day \u2013 integer day Returns: A new datetime object. Source code in beancount/parser/lexer.py def DATE(self, year, month, day): \"\"\"Process a DATE token. Args: year: integer year. month: integer month. day: integer day Returns: A new datetime object. \"\"\" return datetime.date(year, month, day)","title":"DATE()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder.KEY","text":"Process an identifier token. Parameters: ident \u2013 a str, the name of the key string. Returns: The link string itself. For now we don't need to represent this by an object. Source code in beancount/parser/lexer.py def KEY(self, ident): \"\"\"Process an identifier token. Args: ident: a str, the name of the key string. Returns: The link string itself. For now we don't need to represent this by an object. \"\"\" return ident","title":"KEY()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder.LINK","text":"Process a LINK token. Parameters: link \u2013 a str, the name of the string. Returns: The link string itself. For now we don't need to represent this by an object. Source code in beancount/parser/lexer.py def LINK(self, link): \"\"\"Process a LINK token. Args: link: a str, the name of the string. Returns: The link string itself. For now we don't need to represent this by an object. \"\"\" return link","title":"LINK()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder.NUMBER","text":"Process a NUMBER token. Convert into Decimal. Parameters: number \u2013 a str, the number to be converted. Returns: A Decimal instance built of the number string. Source code in beancount/parser/lexer.py def NUMBER(self, number): \"\"\"Process a NUMBER token. Convert into Decimal. Args: number: a str, the number to be converted. Returns: A Decimal instance built of the number string. \"\"\" # Note: We don't use D() for efficiency here. # The lexer will only yield valid number strings. if ',' in number: # Extract the integer part and check the commas match the # locale-aware formatted version. This match = re.match(r\"([\\d,]*)(\\.\\d*)?$\", number) if not match: # This path is never taken because the lexer will parse a comma # in the fractional part as two NUMBERs with a COMMA token in # between. self.errors.append( LexerError(self.get_lexer_location(), \"Invalid number format: '{}'\".format(number), None)) else: int_string, float_string = match.groups() reformatted_number = r\"{:,.0f}\".format(int(int_string.replace(\",\", \"\"))) if int_string != reformatted_number: self.errors.append( LexerError(self.get_lexer_location(), \"Invalid commas: '{}'\".format(number), None)) number = number.replace(',', '') return Decimal(number)","title":"NUMBER()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder.STRING","text":"Process a STRING token. Parameters: string \u2013 the string to process. Returns: The string. Nothing to be done or cleaned up. Eventually we might do some decoding here. Source code in beancount/parser/lexer.py def STRING(self, string): \"\"\"Process a STRING token. Args: string: the string to process. Returns: The string. Nothing to be done or cleaned up. Eventually we might do some decoding here. \"\"\" # If a multiline string, warm over a certain number of lines. if '\\n' in string: num_lines = string.count('\\n') + 1 if num_lines > self.long_string_maxlines_default: # This is just a warning; accept the string anyhow. self.errors.append( LexerError( self.get_lexer_location(), \"String too long ({} lines); possible error\".format(num_lines), None)) return string","title":"STRING()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder.TAG","text":"Process a TAG token. Parameters: tag \u2013 a str, the tag to be processed. Returns: The tag string itself. For now we don't need an object to represent those; keeping it simple. Source code in beancount/parser/lexer.py def TAG(self, tag): \"\"\"Process a TAG token. Args: tag: a str, the tag to be processed. Returns: The tag string itself. For now we don't need an object to represent those; keeping it simple. \"\"\" return tag","title":"TAG()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder.build_lexer_error","text":"Build a lexer error and appends it to the list of pending errors. Parameters: message \u2013 The message of the error. exc_type \u2013 An exception type, if an exception occurred. Source code in beancount/parser/lexer.py def build_lexer_error(self, message, exc_type=None): # {0e31aeca3363} \"\"\"Build a lexer error and appends it to the list of pending errors. Args: message: The message of the error. exc_type: An exception type, if an exception occurred. \"\"\" if not isinstance(message, str): message = str(message) if exc_type is not None: message = '{}: {}'.format(exc_type.__name__, message) self.errors.append( LexerError(self.get_lexer_location(), message, None))","title":"build_lexer_error()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder.get_invalid_account","text":"Return the name of an invalid account placeholder. When an account name is not deemed a valid one, replace it by this account name. This can be overridden by the parser to take into account the options. Returns: A string, the name of the root/type for invalid account names. Source code in beancount/parser/lexer.py def get_invalid_account(self): \"\"\"Return the name of an invalid account placeholder. When an account name is not deemed a valid one, replace it by this account name. This can be overridden by the parser to take into account the options. Returns: A string, the name of the root/type for invalid account names. \"\"\" return 'Equity:InvalidAccountName'","title":"get_invalid_account()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexerError","text":"LexerError(source, message, entry)","title":"LexerError"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexerError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/lexer.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexerError.__new__","text":"Create new instance of LexerError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexerError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/lexer.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.lex_iter","text":"An iterator that yields all the tokens in the given file. Parameters: file \u2013 A string, the filename to run the lexer on, or a file object. builder \u2013 A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors). encoding \u2013 A string (or None), the default encoding to use for strings. Yields: Tuples of the token (a string), the matched text (a string), and the line no (an integer). Source code in beancount/parser/lexer.py def lex_iter(file, builder=None, encoding=None): \"\"\"An iterator that yields all the tokens in the given file. Args: file: A string, the filename to run the lexer on, or a file object. builder: A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors). encoding: A string (or None), the default encoding to use for strings. Yields: Tuples of the token (a string), the matched text (a string), and the line no (an integer). \"\"\" if isinstance(file, str): filename = file else: filename = file.name if builder is None: builder = LexBuilder() _parser.lexer_initialize(filename, builder, encoding) try: while 1: token_tuple = _parser.lexer_next() if token_tuple is None: break yield token_tuple finally: _parser.lexer_finalize()","title":"lex_iter()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.lex_iter_string","text":"Parse an input string and print the tokens to an output file. Parameters: input_string \u2013 a str or bytes, the contents of the ledger to be parsed. builder \u2013 A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors). encoding \u2013 A string (or None), the default encoding to use for strings. Returns: A iterator on the string. See lex_iter() for details. Source code in beancount/parser/lexer.py def lex_iter_string(string, builder=None, encoding=None): \"\"\"Parse an input string and print the tokens to an output file. Args: input_string: a str or bytes, the contents of the ledger to be parsed. builder: A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors). encoding: A string (or None), the default encoding to use for strings. Returns: A iterator on the string. See lex_iter() for details. \"\"\" tmp_file = tempfile.NamedTemporaryFile('w' if isinstance(string, str) else 'wb') tmp_file.write(string) tmp_file.flush() # Note: We pass in the file object in order to keep it alive during parsing. return lex_iter(tmp_file, builder, encoding)","title":"lex_iter_string()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options","text":"Declaration of options and their default values.","title":"options"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptDesc","text":"OptDesc(name, default_value, example_value, converter, deprecated, alias)","title":"OptDesc"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptDesc.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/options.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptDesc.__new__","text":"Create new instance of OptDesc(name, default_value, example_value, converter, deprecated, alias)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptDesc.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/options.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptGroup","text":"OptGroup(description, options)","title":"OptGroup"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptGroup.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/options.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptGroup.__new__","text":"Create new instance of OptGroup(description, options)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptGroup.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/options.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.Opt","text":"Alternative constructor for OptDesc, with default values. Parameters: name \u2013 See OptDesc. default_value \u2013 See OptDesc. example_value \u2013 See OptDesc. converter \u2013 See OptDesc. deprecated \u2013 See OptDesc. alias \u2013 See OptDesc. Returns: An instance of OptDesc. Source code in beancount/parser/options.py def Opt(name, default_value, example_value=UNSET, converter=None, deprecated=False, alias=None): \"\"\"Alternative constructor for OptDesc, with default values. Args: name: See OptDesc. default_value: See OptDesc. example_value: See OptDesc. converter: See OptDesc. deprecated: See OptDesc. alias: See OptDesc. Returns: An instance of OptDesc. \"\"\" if example_value is UNSET: example_value = default_value return OptDesc(name, default_value, example_value, converter, deprecated, alias)","title":"Opt()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.get_account_types","text":"Extract the account type names from the parser's options. Parameters: options \u2013 a dict of ledger options. Returns: An instance of AccountTypes, that contains all the prefixes. Source code in beancount/parser/options.py def get_account_types(options): \"\"\"Extract the account type names from the parser's options. Args: options: a dict of ledger options. Returns: An instance of AccountTypes, that contains all the prefixes. \"\"\" return account_types.AccountTypes( *[options[key] for key in (\"name_assets\", \"name_liabilities\", \"name_equity\", \"name_income\", \"name_expenses\")])","title":"get_account_types()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.get_current_accounts","text":"Return account names for the current earnings and conversion accounts. Parameters: options \u2013 a dict of ledger options. Returns: A tuple of 2 account objects, one for booking current earnings, and one for current conversions. Source code in beancount/parser/options.py def get_current_accounts(options): \"\"\"Return account names for the current earnings and conversion accounts. Args: options: a dict of ledger options. Returns: A tuple of 2 account objects, one for booking current earnings, and one for current conversions. \"\"\" equity = options['name_equity'] account_current_earnings = account.join(equity, options['account_current_earnings']) account_current_conversions = account.join(equity, options['account_current_conversions']) return (account_current_earnings, account_current_conversions)","title":"get_current_accounts()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.get_previous_accounts","text":"Return account names for the previous earnings, balances and conversion accounts. Parameters: options \u2013 a dict of ledger options. Returns: A tuple of 3 account objects, for booking previous earnings, previous balances, and previous conversions. Source code in beancount/parser/options.py def get_previous_accounts(options): \"\"\"Return account names for the previous earnings, balances and conversion accounts. Args: options: a dict of ledger options. Returns: A tuple of 3 account objects, for booking previous earnings, previous balances, and previous conversions. \"\"\" equity = options['name_equity'] account_previous_earnings = account.join(equity, options['account_previous_earnings']) account_previous_balances = account.join(equity, options['account_previous_balances']) account_previous_conversions = account.join(equity, options['account_previous_conversions']) return (account_previous_earnings, account_previous_balances, account_previous_conversions)","title":"get_previous_accounts()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.list_options","text":"Produce a formatted text of the available options and their description. Returns: A string, formatted nicely to be printed in 80 columns. Source code in beancount/parser/options.py def list_options(): \"\"\"Produce a formatted text of the available options and their description. Returns: A string, formatted nicely to be printed in 80 columns. \"\"\" oss = io.StringIO() for group in PUBLIC_OPTION_GROUPS: for desc in group.options: oss.write('option \"{}\" \"{}\"\\n'.format(desc.name, desc.example_value)) if desc.deprecated: oss.write(textwrap.fill( \"THIS OPTION IS DEPRECATED: {}\".format(desc.deprecated), initial_indent=\" \", subsequent_indent=\" \")) oss.write('\\n\\n') description = ' '.join(line.strip() for line in group.description.strip().splitlines()) oss.write(textwrap.fill(description, initial_indent=' ', subsequent_indent=' ')) oss.write('\\n') if isinstance(desc.default_value, (list, dict, set)): oss.write('\\n') oss.write(' (This option may be supplied multiple times.)\\n') oss.write('\\n\\n') return oss.getvalue()","title":"list_options()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.options_validate_booking_method","text":"Validate a booking method name. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_booking_method(value): \"\"\"Validate a booking method name. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" try: return data.Booking[value] except KeyError as exc: raise ValueError(str(exc))","title":"options_validate_booking_method()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.options_validate_boolean","text":"Validate a boolean option. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_boolean(value): \"\"\"Validate a boolean option. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" return value.lower() in ('1', 'true', 'yes')","title":"options_validate_boolean()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.options_validate_plugin","text":"Validate the plugin option. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_plugin(value): \"\"\"Validate the plugin option. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" # Process the 'plugin' option specially: accept an optional # argument from it. NOTE: We will eventually phase this out and # replace it by a dedicated 'plugin' directive. match = re.match('(.*):(.*)', value) if match: plugin_name, plugin_config = match.groups() else: plugin_name, plugin_config = value, None return (plugin_name, plugin_config)","title":"options_validate_plugin()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.options_validate_processing_mode","text":"Validate the options processing mode. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_processing_mode(value): \"\"\"Validate the options processing mode. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" if value not in ('raw', 'default'): raise ValueError(\"Invalid value '{}'\".format(value)) return value","title":"options_validate_processing_mode()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.options_validate_tolerance","text":"Validate the tolerance option. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_tolerance(value): \"\"\"Validate the tolerance option. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" return D(value)","title":"options_validate_tolerance()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.options_validate_tolerance_map","text":"Validate an option with a map of currency/tolerance pairs in a string. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_tolerance_map(value): \"\"\"Validate an option with a map of currency/tolerance pairs in a string. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" # Process the setting of a key-value, whereby the value is a Decimal # representation. match = re.match('(.*):(.*)', value) if not match: raise ValueError(\"Invalid value '{}'\".format(value)) currency, tolerance_str = match.groups() return (currency, D(tolerance_str))","title":"options_validate_tolerance_map()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser","text":"Beancount syntax parser. IMPORTANT: The parser (and its grammar builder) produces \"incomplete\" Transaction objects. This means that some of the data can be found missing from the output of the parser and some of the data types vary slightly. Missing components are replaced not by None, but by a special constant 'NA' which helps diagnose problems if a user inadvertently attempts to work on an incomplete posting instead of a complete one. Those incomplete entries are then run through the \"booking\" routines which do two things simultaneously: They find matching lots for reducing inventory positions, and They interpolate missing numbers. In doing so they normalize the entries to \"complete\" entries by converting a position/lot's \"cost\" attribute from a CostSpec to a Cost. A Cost is similar to an Amount in that it shares \"number\" and \"currency\" attributes, but also has a label and a lot date. A CostSpec is similar to a Cost, but has all optional data; it consists in a specification for matching against a particular inventory lot. Other parts of a posting may also be missing, not just parts of the cost. Leaving out some parts of the input is used to invoke interpolation, to tell Beancount to automatically compute the missing numbers (if possible). Missing components will be set to the special value \"beancount.core.number.MISSING\" until inventory booking and number interpolation has been completed. The \"MISSING\" value should never appear in completed, loaded transaction postings. For instance, all of the units may be missing: INPUT: Assets:Account posting.units = MISSING Or just the number of the units: INPUT: Assets:Account USD posting.units = Amount(MISSING, \"USD\") You must always specify the currency. If a price annotation is simply absent, it appears as None: INPUT: Assets:Account 2 MXN posting.price = None However, you may indicate that there is a price but have Beancount compute it automatically: INPUT: Assets:Account 2 MXN @ posting.price = Amount(MISSING, MISSING) Indicating the conversion currency is also possible (and recommended): INPUT: Assets:Account 2 MXN @ USD posting.price = Amount(MISSING, \"USD\") If a cost specification is provided, a \"cost\" attribute it set but it does not refer to a Cost instance (as in complete entries) but rather to a CostSpec instance. Some of the fields of a CostSpec may be MISSING if they were not specified in the input. For example: INPUT: Assets:Account 1 HOOL {100 # 5 USD} posting.cost = CostSpec(Decimal(\"100\"), Decimal(\"5\"), \"USD\", None, None, False)) Note how we never consider the label of date override to be MISSING; this is because those inputs are optional: A missing label is simply left unset in the computed Cost, and a missing date override uses the date of the transaction that contains the posting. You can indicate that there is a total number to be filled in like this: INPUT: Assets:Account 1 HOOL {100 # USD} posting.cost = CostSpec(Decimal(\"100\"), MISSING, \"USD\", None, None, False)) This is in contrast to the total value simple not being used: INPUT: Assets:Account 1 HOOL {100 USD} posting.cost = CostSpec(Decimal(\"100\"), None, \"USD\", None, None, False)) Both per-unit and total numbers may be omitted as well, in which case, only the number-per-unit portion of the CostSpec will appear as MISSING: INPUT: Assets:Account 1 HOOL {USD} posting.cost = CostSpec(MISSING, None, \"USD\", None, None, False)) And furthermore, all the cost basis may be missing: INPUT: Assets:Account 1 HOOL {} posting.cost = CostSpec(MISSING, None, MISSING, None, None, False)) If you ask for the lots to be merged, you get this: INPUT: Assets:Account 1 HOOL {*} posting.cost = CostSpec(MISSING, None, MISSING, None, None, True)) The numbers have to be computed by Beancount, so we output this with MISSING values. Of course, you can provide only the non-basis information, like just the date or label: INPUT: Assets:Account 1 HOOL {2015-09-21} posting.cost = CostSpec(MISSING, None, MISSING, date(2015, 9, 21), None, True) See the test beancount.parser.grammar_test.TestIncompleteInputs for examples and corresponding expected values.","title":"parser"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.is_entry_incomplete","text":"Detect the presence of elided amounts in Transactions. Parameters: entries \u2013 A directive. Returns: A boolean, true if there are some missing portions of any postings found. Source code in beancount/parser/parser.py def is_entry_incomplete(entry): \"\"\"Detect the presence of elided amounts in Transactions. Args: entries: A directive. Returns: A boolean, true if there are some missing portions of any postings found. \"\"\" if isinstance(entry, data.Transaction): if any(is_posting_incomplete(posting) for posting in entry.postings): return True return False","title":"is_entry_incomplete()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.is_posting_incomplete","text":"Detect the presence of any elided amounts in a Posting. If any of the possible amounts are missing, this returns True. Parameters: entries \u2013 A directive. Returns: A boolean, true if there are some missing portions of any postings found. Source code in beancount/parser/parser.py def is_posting_incomplete(posting): \"\"\"Detect the presence of any elided amounts in a Posting. If any of the possible amounts are missing, this returns True. Args: entries: A directive. Returns: A boolean, true if there are some missing portions of any postings found. \"\"\" units = posting.units if (units is MISSING or units.number is MISSING or units.currency is MISSING): return True price = posting.price if (price is MISSING or price is not None and (price.number is MISSING or price.currency is MISSING)): return True cost = posting.cost if cost is not None and (cost.number_per is MISSING or cost.number_total is MISSING or cost.currency is MISSING): return True return False","title":"is_posting_incomplete()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.parse_doc","text":"Factory of decorators that parse the function's docstring as an argument. Note that the decorators thus generated only run the parser on the tests, not the loader, so is no validation, balance checks, nor plugins applied to the parsed text. Parameters: expect_errors \u2013 A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. allow_incomplete \u2013 A boolean, if true, allow incomplete input. Otherwise barf if the input would require interpolation. The default value is set not to allow it because we want to minimize the features tests depend on. Returns: A decorator for test functions. Source code in beancount/parser/parser.py def parse_doc(expect_errors=False, allow_incomplete=False): \"\"\"Factory of decorators that parse the function's docstring as an argument. Note that the decorators thus generated only run the parser on the tests, not the loader, so is no validation, balance checks, nor plugins applied to the parsed text. Args: expect_errors: A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. allow_incomplete: A boolean, if true, allow incomplete input. Otherwise barf if the input would require interpolation. The default value is set not to allow it because we want to minimize the features tests depend on. Returns: A decorator for test functions. \"\"\" def decorator(fun): \"\"\"A decorator that parses the function's docstring as an argument. Args: fun: the function object to be decorated. Returns: A decorated test function. \"\"\" filename = inspect.getfile(fun) lines, lineno = inspect.getsourcelines(fun) # decorator line + function definition line (I realize this is largely # imperfect, but it's only for reporting in our tests) - empty first line # stripped away. lineno += 1 @functools.wraps(fun) def wrapper(self): assert fun.__doc__ is not None, ( \"You need to insert a docstring on {}\".format(fun.__name__)) entries, errors, options_map = parse_string(fun.__doc__, report_filename=filename, report_firstline=lineno, dedent=True) if not allow_incomplete and any(is_entry_incomplete(entry) for entry in entries): self.fail(\"parse_doc() may not use interpolation.\") if expect_errors is not None: if expect_errors is False and errors: oss = io.StringIO() printer.print_errors(errors, file=oss) self.fail(\"Unexpected errors found:\\n{}\".format(oss.getvalue())) elif expect_errors is True and not errors: self.fail(\"Expected errors, none found:\") return fun(self, entries, errors, options_map) wrapper.__input__ = wrapper.__doc__ wrapper.__doc__ = None return wrapper return decorator","title":"parse_doc()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.parse_file","text":"Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Parameters: filename \u2013 the name of the file to be parsed. kw \u2013 a dict of keywords to be applied to the C parser. Returns: A tuple of ( list of entries parsed in the file, list of errors that were encountered during parsing, and a dict of the option values that were parsed from the file.) Source code in beancount/parser/parser.py def parse_file(filename, **kw): \"\"\"Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Args: filename: the name of the file to be parsed. kw: a dict of keywords to be applied to the C parser. Returns: A tuple of ( list of entries parsed in the file, list of errors that were encountered during parsing, and a dict of the option values that were parsed from the file.) \"\"\" abs_filename = path.abspath(filename) if filename else None builder = grammar.Builder(abs_filename) _parser.parse_file(filename, builder, **kw) return builder.finalize()","title":"parse_file()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.parse_many","text":"Parse a string with a snippet of Beancount input and replace vars from caller. Parameters: string \u2013 A string with some Beancount input. level \u2013 The number of extra stacks to ignore. Returns: A list of entries. Exceptions: AssertionError \u2013 If there are any errors. Source code in beancount/parser/parser.py def parse_many(string, level=0): \"\"\"Parse a string with a snippet of Beancount input and replace vars from caller. Args: string: A string with some Beancount input. level: The number of extra stacks to ignore. Returns: A list of entries. Raises: AssertionError: If there are any errors. \"\"\" # Get the locals in the stack for the callers and produce the final text. frame = inspect.stack()[level+1] varkwds = frame[0].f_locals input_string = textwrap.dedent(string.format(**varkwds)) # Parse entries and check there are no errors. entries, errors, __ = parse_string(input_string) assert not errors return entries","title":"parse_many()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.parse_one","text":"Parse a string with single Beancount directive and replace vars from caller. Parameters: string \u2013 A string with some Beancount input. level \u2013 The number of extra stacks to ignore. Returns: A list of entries. Exceptions: AssertionError \u2013 If there are any errors. Source code in beancount/parser/parser.py def parse_one(string): \"\"\"Parse a string with single Beancount directive and replace vars from caller. Args: string: A string with some Beancount input. level: The number of extra stacks to ignore. Returns: A list of entries. Raises: AssertionError: If there are any errors. \"\"\" entries = parse_many(string, level=1) assert len(entries) == 1 return entries[0]","title":"parse_one()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.parse_string","text":"Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Parameters: string \u2013 A string, the contents to be parsed instead of a file's. report_filename \u2013 A string, the source filename from which this string has been extracted, if any. This is stored in the metadata of the parsed entries. **kw \u2013 See parse.c. This function parses out 'dedent' which removes whitespace from the front of the text (default is False). Returns: Same as the output of parse_file(). Source code in beancount/parser/parser.py def parse_string(string, report_filename=None, **kw): \"\"\"Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Args: string: A string, the contents to be parsed instead of a file's. report_filename: A string, the source filename from which this string has been extracted, if any. This is stored in the metadata of the parsed entries. **kw: See parse.c. This function parses out 'dedent' which removes whitespace from the front of the text (default is False). Return: Same as the output of parse_file(). \"\"\" if kw.pop('dedent', None): string = textwrap.dedent(string) builder = grammar.Builder(report_filename or '') _parser.parse_string(string, builder, report_filename=report_filename, **kw) return builder.finalize()","title":"parse_string()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer","text":"Conversion from internal data structures to text.","title":"printer"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.EntryPrinter","text":"A multi-method interface for printing all directive types. Attributes: Name Type Description dcontext An instance of DisplayContext with which to render all the numbers. render_weight A boolean, true if we should render the weight of the postings as a comment, for debugging. min_width_account An integer, the minimum width to leave for the account name.","title":"EntryPrinter"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.EntryPrinter.__call__","text":"Render a directive. Parameters: obj \u2013 The directive to be rendered. Returns: A string, the rendered directive. Source code in beancount/parser/printer.py def __call__(self, obj): \"\"\"Render a directive. Args: obj: The directive to be rendered. Returns: A string, the rendered directive. \"\"\" oss = io.StringIO() method = getattr(self, obj.__class__.__name__) method(obj, oss) return oss.getvalue()","title":"__call__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.EntryPrinter.render_posting_strings","text":"This renders the three components of a posting: the account and its optional posting flag, the position, and finally, the weight of the position. The purpose is to align these in the caller. Parameters: posting \u2013 An instance of Posting, the posting to render. Returns: A tuple of flag_account \u2013 A string, the account name including the flag. position_str: A string, the rendered position string. weight_str: A string, the rendered weight of the posting. Source code in beancount/parser/printer.py def render_posting_strings(self, posting): \"\"\"This renders the three components of a posting: the account and its optional posting flag, the position, and finally, the weight of the position. The purpose is to align these in the caller. Args: posting: An instance of Posting, the posting to render. Returns: A tuple of flag_account: A string, the account name including the flag. position_str: A string, the rendered position string. weight_str: A string, the rendered weight of the posting. \"\"\" # Render a string of the flag and the account. flag = '{} '.format(posting.flag) if posting.flag else '' flag_account = flag + posting.account # Render a string with the amount and cost and optional price, if # present. Also render a string with the weight. weight_str = '' if isinstance(posting.units, amount.Amount): position_str = position.to_string(posting, self.dformat) # Note: we render weights at maximum precision, for debugging. if posting.cost is None or (isinstance(posting.cost, position.Cost) and isinstance(posting.cost.number, Decimal)): weight_str = str(convert.get_weight(posting)) else: position_str = '' if posting.price is not None: position_str += ' @ {}'.format(posting.price.to_string(self.dformat_max)) return flag_account, position_str, weight_str","title":"render_posting_strings()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.EntryPrinter.write_metadata","text":"Write metadata to the file object, excluding filename and line number. Parameters: meta \u2013 A dict that contains the metadata for this directive. oss \u2013 A file object to write to. Source code in beancount/parser/printer.py def write_metadata(self, meta, oss, prefix=None): \"\"\"Write metadata to the file object, excluding filename and line number. Args: meta: A dict that contains the metadata for this directive. oss: A file object to write to. \"\"\" if meta is None: return if prefix is None: prefix = self.prefix for key, value in sorted(meta.items()): if key not in self.META_IGNORE: value_str = None if isinstance(value, str): value_str = '\"{}\"'.format(misc_utils.escape_string(value)) elif isinstance(value, (Decimal, datetime.date, amount.Amount)): value_str = str(value) elif isinstance(value, bool): value_str = 'TRUE' if value else 'FALSE' elif isinstance(value, (dict, inventory.Inventory)): pass # Ignore dicts, don't print them out. elif value is None: value_str = '' # Render null metadata as empty, on purpose. else: raise ValueError(\"Unexpected value: '{!r}'\".format(value)) if value_str is not None: oss.write(\"{}{}: {}\\n\".format(prefix, key, value_str))","title":"write_metadata()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.align_position_strings","text":"A helper used to align rendered amounts positions to their first currency character (an uppercase letter). This class accepts a list of rendered positions and calculates the necessary width to render them stacked in a column so that the first currency word aligns. It does not go beyond that (further currencies, e.g. for the price or cost, are not aligned). This is perhaps best explained with an example. The following positions will be aligned around the column marked with '^': 45 HOOL {504.30 USD} 4 HOOL {504.30 USD, 2014-11-11} 9.95 USD -22473.32 CAD @ 1.10 USD ^ Strings without a currency character will be rendered flush left. Parameters: strings \u2013 A list of rendered position or amount strings. Returns: A pair of a list of aligned strings and the width of the aligned strings. Source code in beancount/parser/printer.py def align_position_strings(strings): \"\"\"A helper used to align rendered amounts positions to their first currency character (an uppercase letter). This class accepts a list of rendered positions and calculates the necessary width to render them stacked in a column so that the first currency word aligns. It does not go beyond that (further currencies, e.g. for the price or cost, are not aligned). This is perhaps best explained with an example. The following positions will be aligned around the column marked with '^': 45 HOOL {504.30 USD} 4 HOOL {504.30 USD, 2014-11-11} 9.95 USD -22473.32 CAD @ 1.10 USD ^ Strings without a currency character will be rendered flush left. Args: strings: A list of rendered position or amount strings. Returns: A pair of a list of aligned strings and the width of the aligned strings. \"\"\" # Maximum length before the alignment character. max_before = 0 # Maximum length after the alignment character. max_after = 0 # Maximum length of unknown strings. max_unknown = 0 string_items = [] search = re.compile('[A-Z]').search for string in strings: match = search(string) if match: index = match.start() if index != 0: max_before = max(index, max_before) max_after = max(len(string) - index, max_after) string_items.append((index, string)) continue # else max_unknown = max(len(string), max_unknown) string_items.append((None, string)) # Compute formatting string. max_total = max(max_before + max_after, max_unknown) max_after_prime = max_total - max_before fmt = \"{{:>{0}}}{{:{1}}}\".format(max_before, max_after_prime).format fmt_unknown = \"{{:<{0}}}\".format(max_total).format # Align the strings and return them. aligned_strings = [] for index, string in string_items: if index is not None: string = fmt(string[:index], string[index:]) else: string = fmt_unknown(string) aligned_strings.append(string) return aligned_strings, max_total","title":"align_position_strings()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.format_entry","text":"Format an entry into a string in the same input syntax the parser accepts. Parameters: entry \u2013 An entry instance. dcontext \u2013 An instance of DisplayContext used to format the numbers. render_weights \u2013 A boolean, true to render the weights for debugging. Returns: A string, the formatted entry. Source code in beancount/parser/printer.py def format_entry(entry, dcontext=None, render_weights=False, prefix=None): \"\"\"Format an entry into a string in the same input syntax the parser accepts. Args: entry: An entry instance. dcontext: An instance of DisplayContext used to format the numbers. render_weights: A boolean, true to render the weights for debugging. Returns: A string, the formatted entry. \"\"\" return EntryPrinter(dcontext, render_weights, prefix=prefix)(entry)","title":"format_entry()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.format_error","text":"Given an error objects, return a formatted string for it. Parameters: error \u2013 a namedtuple objects representing an error. It has to have an 'entry' attribute that may be either a single directive object or a list of directive objects. Returns: A string, the errors rendered. Source code in beancount/parser/printer.py def format_error(error): \"\"\"Given an error objects, return a formatted string for it. Args: error: a namedtuple objects representing an error. It has to have an 'entry' attribute that may be either a single directive object or a list of directive objects. Returns: A string, the errors rendered. \"\"\" oss = io.StringIO() oss.write('{} {}\\n'.format(render_source(error.source), error.message)) if error.entry is not None: entries = error.entry if isinstance(error.entry, list) else [error.entry] error_string = '\\n'.join(format_entry(entry) for entry in entries) oss.write('\\n') oss.write(textwrap.indent(error_string, ' ')) oss.write('\\n') return oss.getvalue()","title":"format_error()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.print_entries","text":"A convenience function that prints a list of entries to a file. Parameters: entries \u2013 A list of directives. dcontext \u2013 An instance of DisplayContext used to format the numbers. render_weights \u2013 A boolean, true to render the weights for debugging. file \u2013 An optional file object to write the entries to. Source code in beancount/parser/printer.py def print_entries(entries, dcontext=None, render_weights=False, file=None, prefix=None): \"\"\"A convenience function that prints a list of entries to a file. Args: entries: A list of directives. dcontext: An instance of DisplayContext used to format the numbers. render_weights: A boolean, true to render the weights for debugging. file: An optional file object to write the entries to. \"\"\" assert isinstance(entries, list), \"Entries is not a list: {}\".format(entries) output = file or (codecs.getwriter(\"utf-8\")(sys.stdout.buffer) if hasattr(sys.stdout, 'buffer') else sys.stdout) if prefix: output.write(prefix) previous_type = type(entries[0]) if entries else None eprinter = EntryPrinter(dcontext, render_weights) for entry in entries: # Insert a newline between transactions and between blocks of directives # of the same type. entry_type = type(entry) if (entry_type in (data.Transaction, data.Commodity) or entry_type is not previous_type): output.write('\\n') previous_type = entry_type string = eprinter(entry) output.write(string)","title":"print_entries()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.print_entry","text":"A convenience function that prints a single entry to a file. Parameters: entry \u2013 A directive entry. dcontext \u2013 An instance of DisplayContext used to format the numbers. render_weights \u2013 A boolean, true to render the weights for debugging. file \u2013 An optional file object to write the entries to. Source code in beancount/parser/printer.py def print_entry(entry, dcontext=None, render_weights=False, file=None): \"\"\"A convenience function that prints a single entry to a file. Args: entry: A directive entry. dcontext: An instance of DisplayContext used to format the numbers. render_weights: A boolean, true to render the weights for debugging. file: An optional file object to write the entries to. \"\"\" output = file or (codecs.getwriter(\"utf-8\")(sys.stdout.buffer) if hasattr(sys.stdout, 'buffer') else sys.stdout) output.write(format_entry(entry, dcontext, render_weights)) output.write('\\n')","title":"print_entry()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.print_error","text":"A convenience function that prints a single error to a file. Parameters: error \u2013 An error object. file \u2013 An optional file object to write the errors to. Source code in beancount/parser/printer.py def print_error(error, file=None): \"\"\"A convenience function that prints a single error to a file. Args: error: An error object. file: An optional file object to write the errors to. \"\"\" output = file or sys.stdout output.write(format_error(error)) output.write('\\n')","title":"print_error()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.print_errors","text":"A convenience function that prints a list of errors to a file. Parameters: errors \u2013 A list of errors. file \u2013 An optional file object to write the errors to. Source code in beancount/parser/printer.py def print_errors(errors, file=None, prefix=None): \"\"\"A convenience function that prints a list of errors to a file. Args: errors: A list of errors. file: An optional file object to write the errors to. \"\"\" output = file or sys.stdout if prefix: output.write(prefix) for error in errors: output.write(format_error(error)) output.write('\\n')","title":"print_errors()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.render_source","text":"Render the source for errors in a way that it will be both detected by Emacs and align and rendered nicely. Parameters: meta \u2013 A dict with the metadata. Returns: A string, rendered to be interpretable as a message location for Emacs or other editors. Source code in beancount/parser/printer.py def render_source(meta): \"\"\"Render the source for errors in a way that it will be both detected by Emacs and align and rendered nicely. Args: meta: A dict with the metadata. Returns: A string, rendered to be interpretable as a message location for Emacs or other editors. \"\"\" return '{}:{:8}'.format(meta['filename'], '{}:'.format(meta['lineno']))","title":"render_source()"},{"location":"api_reference/beancount.plugins.html","text":"beancount.plugins \uf0c1 Example plugins for filtering transactions. These are various examples of how to filter entries in various creative ways. IMPORTANT: These are not meant to be complete features, rather just experiments in problem-solving using Beancount, work-in-progress that can be selectively installed via a --plugin option, or one-offs to answer questions on the mailing-list. beancount.plugins.auto \uf0c1 A plugin of plugins which triggers are all the automatic and lax plugins. In a sense, this is the inverse of \"pedantic.\" This is useful when doing some types of quick and dirty tests. You can just import the \"auto\" plugin. Put that in a macro. Also see: the 'pedantic' plugin. beancount.plugins.auto_accounts \uf0c1 This module automatically inserts Open directives for accounts not opened (at the date of the first entry) and automatically removes open directives for unused accounts. This can be used as a convenience for doing demos, or when setting up your initial transactions, as an intermediate step. beancount.plugins.auto_accounts.auto_insert_open(entries, unused_options_map) \uf0c1 Insert implicitly defined prices from Transactions. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. Source code in beancount/plugins/auto_accounts.py def auto_insert_open(entries, unused_options_map): \"\"\"Insert implicitly defined prices from Transactions. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. \"\"\" opened_accounts = {entry.account for entry in entries if isinstance(entry, data.Open)} new_entries = [] accounts_first, _ = getters.get_accounts_use_map(entries) for index, (account, date_first_used) in enumerate(sorted(accounts_first.items())): if account not in opened_accounts: meta = data.new_metadata('', index) new_entries.append(data.Open(meta, date_first_used, account, None, None)) if new_entries: new_entries.extend(entries) new_entries.sort(key=data.entry_sortkey) else: new_entries = entries return new_entries, [] beancount.plugins.book_conversions \uf0c1 A plugin that automatically converts postings at price to postings held at cost, applying an automatic booking algorithm in assigning the cost bases and matching lots. This plugin restricts itself to applying these transformations within a particular account, which you provide. For each of those accounts, it also requires a corresponding Income account to book the profit/loss of reducing lots (i.e., sales): plugin \"beancount.plugins.book_conversions\" \"Assets:Bitcoin,Income:Bitcoin\" Then, simply input the transactions with price conversion. We use \"Bitcoins\" in this example, converting Bitcoin purchases that were carried out as currency into maintaining these with cost basis, for tax reporting purposes: 2015-09-04 * \"Buy some bitcoins\" Assets:Bank -1000.00 USD Assets:Bitcoin 4.333507 BTC @ 230.76 USD 2015-09-05 * \"Buy some more bitcoins at a different price\" Assets:Bank -1000.00 USD Assets:Bitcoin 4.345747 BTC @ 230.11 USD 2015-09-20 * \"Use (sell) some bitcoins\" Assets:Bitcoin -6.000000 BTC @ 230.50 USD Expenses:Something The result is that cost bases are inserted on augmenting lots: 2015-09-04 * \"Buy some bitcoins\" Assets:Bitcoin 4.333507 BTC {230.76 USD} @ 230.76 USD Assets:Bank -1000.00 USD 2015-09-05 * \"Buy some more bitcoins at a different price\" Assets:Bitcoin 4.345747 BTC {230.11 USD} @ 230.11 USD Assets:Bank -1000.00 USD While on reducing lots, matching FIFO lots are automatically found and the corresponding cost basis added: 2015-09-20 * \"Use (sell) some bitcoins\" Assets:Bitcoin -4.333507 BTC {230.76 USD} @ 230.50 USD Assets:Bitcoin -1.666493 BTC {230.11 USD} @ 230.50 USD Income:Bitcoin 0.47677955 USD Expenses:Something 1383.00000000 USD Note that multiple lots were required to fulfill the sale quantity here. As in this example, this may result in multiple lots being created for a single one. Finally, Beancount will eventually support booking methods built-in, but this is a quick method that shows how to hack your own booking method via transformations of the postings that run in a plugin. Implementation notes: This code uses the FIFO method only for now. However, it would be very easy to customize it to provide other booking methods, e.g. LIFO, or otherwise. This will be added eventually, and I'm hoping to reuse the same inventory abstractions that will be used to implement the fallback booking methods from the booking proposal review (http://furius.ca/beancount/doc/proposal-booking). Instead of keeping a list of (Position, Transaction) pairs for the pending FIFO lots, we really ought to use a beancount.core.inventory.Inventory instance. However, the class does not contain sufficient data to carry out FIFO booking at the moment. A newer implementation, living in the \"booking\" branch, does, and will be used in the future. This code assumes that a positive number of units is an augmenting lot and a reducing one has a negative number of units, though we never call them that way on purpose (to eventually allow this code to handle short positions). This is not strictly true; however, we would need an Inventory in order to figrue this out. This will be done in the future and is not difficult to do. IMPORTANT: This plugin was developed before the booking methods (FIFO, LIFO, and others) were fully implemented in Beancount. It was built to answer a question on the mailing-list about FIFO booking. You probably don't need to use them anymore. Always prefer to use the native syntax instead of this. beancount.plugins.book_conversions.BookConversionError ( tuple ) \uf0c1 BookConversionError(source, message, entry) beancount.plugins.book_conversions.BookConversionError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/book_conversions.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.book_conversions.BookConversionError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of BookConversionError(source, message, entry) beancount.plugins.book_conversions.BookConversionError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/book_conversions.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.book_conversions.ConfigError ( tuple ) \uf0c1 ConfigError(source, message, entry) beancount.plugins.book_conversions.ConfigError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/book_conversions.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.book_conversions.ConfigError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of ConfigError(source, message, entry) beancount.plugins.book_conversions.ConfigError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/book_conversions.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.book_conversions.augment_inventory(pending_lots, posting, entry, eindex) \uf0c1 Add the lots from the given posting to the running inventory. Parameters: pending_lots \u2013 A list of pending ([number], Posting, Transaction) to be matched. The number is modified in-place, destructively. posting \u2013 The posting whose position is to be added. entry \u2013 The parent transaction. eindex \u2013 The index of the parent transaction housing this posting. Returns: A new posting with cost basis inserted to be added to a transformed transaction. Source code in beancount/plugins/book_conversions.py def augment_inventory(pending_lots, posting, entry, eindex): \"\"\"Add the lots from the given posting to the running inventory. Args: pending_lots: A list of pending ([number], Posting, Transaction) to be matched. The number is modified in-place, destructively. posting: The posting whose position is to be added. entry: The parent transaction. eindex: The index of the parent transaction housing this posting. Returns: A new posting with cost basis inserted to be added to a transformed transaction. \"\"\" number = posting.units.number new_posting = posting._replace( units=copy.copy(posting.units), cost=position.Cost(posting.price.number, posting.price.currency, entry.date, None)) pending_lots.append(([number], new_posting, eindex)) return new_posting beancount.plugins.book_conversions.book_price_conversions(entries, assets_account, income_account) \uf0c1 Rewrite transactions to insert cost basis according to a booking method. See module docstring for full details. Parameters: entries \u2013 A list of entry instances. assets_account \u2013 An account string, the name of the account to process. income_account \u2013 An account string, the name of the account to use for booking realized profit/loss. Returns: A tuple of entries \u2013 A list of new, modified entries. errors: A list of errors generated by this plugin. matches: A list of (number, augmenting-posting, reducing-postings) for all matched lots. Source code in beancount/plugins/book_conversions.py def book_price_conversions(entries, assets_account, income_account): \"\"\"Rewrite transactions to insert cost basis according to a booking method. See module docstring for full details. Args: entries: A list of entry instances. assets_account: An account string, the name of the account to process. income_account: An account string, the name of the account to use for booking realized profit/loss. Returns: A tuple of entries: A list of new, modified entries. errors: A list of errors generated by this plugin. matches: A list of (number, augmenting-posting, reducing-postings) for all matched lots. \"\"\" # Pairs of (Position, Transaction) instances used to match augmenting # entries with reducing ones. pending_lots = [] # A list of pairs of matching (augmenting, reducing) postings. all_matches = [] new_entries = [] errors = [] for eindex, entry in enumerate(entries): # Figure out if this transaction has postings in Bitcoins without a cost. # The purpose of this plugin is to fixup those. if isinstance(entry, data.Transaction) and any(is_matching(posting, assets_account) for posting in entry.postings): # Segregate the reducing lots, augmenting lots and other lots. augmenting, reducing, other = [], [], [] for pindex, posting in enumerate(entry.postings): if is_matching(posting, assets_account): out = augmenting if posting.units.number >= ZERO else reducing else: out = other out.append(posting) # We will create a replacement list of postings with costs filled # in, possibly more than the original list, to account for the # different lots. new_postings = [] # Convert all the augmenting postings to cost basis. for posting in augmenting: new_postings.append(augment_inventory(pending_lots, posting, entry, eindex)) # Then process reducing postings. if reducing: # Process all the reducing postings, booking them to matching lots. pnl = inventory.Inventory() for posting in reducing: rpostings, matches, posting_pnl, new_errors = ( reduce_inventory(pending_lots, posting, eindex)) new_postings.extend(rpostings) all_matches.extend(matches) errors.extend(new_errors) pnl.add_amount(amount.Amount(posting_pnl, posting.price.currency)) # If some reducing lots were seen in this transaction, insert an # Income leg to absorb the P/L. We need to do this for each currency # which incurred P/L. if not pnl.is_empty(): for pos in pnl: meta = data.new_metadata('', 0) new_postings.append( data.Posting(income_account, -pos.units, None, None, None, meta)) # Third, add back all the other unrelated legs in. for posting in other: new_postings.append(posting) # Create a replacement entry. entry = entry._replace(postings=new_postings) new_entries.append(entry) # Add matching metadata to all matching postings. mod_matches = link_entries_with_metadata(new_entries, all_matches) # Resolve the indexes to their possibly modified Transaction instances. matches = [(data.TxnPosting(new_entries[aug_index], aug_posting), data.TxnPosting(new_entries[red_index], red_posting)) for (aug_index, aug_posting), (red_index, red_posting) in mod_matches] return new_entries, errors, matches beancount.plugins.book_conversions.book_price_conversions_plugin(entries, options_map, config) \uf0c1 Plugin that rewrites transactions to insert cost basis according to a booking method. See book_price_conversions() for details. Parameters: entries \u2013 A list of entry instances. options_map \u2013 A dict of options parsed from the file. config \u2013 A string, in \",\" format. Returns: A tuple of entries \u2013 A list of new, modified entries. errors: A list of errors generated by this plugin. Source code in beancount/plugins/book_conversions.py 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 \",\" 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 beancount.plugins.book_conversions.extract_trades(entries) \uf0c1 Find all the matching trades from the metadata attached to postings. This inspects all the postings and pairs them up using the special metadata field that was added by this plugin when booking matching lots, and returns pairs of those postings. Parameters: entries \u2013 The list of directives to extract from. Returns: A list of (number, augmenting-posting, reducing-posting). Source code in beancount/plugins/book_conversions.py def extract_trades(entries): \"\"\"Find all the matching trades from the metadata attached to postings. This inspects all the postings and pairs them up using the special metadata field that was added by this plugin when booking matching lots, and returns pairs of those postings. Args: entries: The list of directives to extract from. Returns: A list of (number, augmenting-posting, reducing-posting). \"\"\" trade_map = collections.defaultdict(list) for index, entry in enumerate(entries): if not isinstance(entry, data.Transaction): continue for posting in entry.postings: links_str = posting.meta.get(META, None) if links_str: links = links_str.split(',') for link in links: trade_map[link].append((index, entry, posting)) # Sort matches according to the index of the first entry, drop the index # used for doing this, and convert the objects to tuples.. trades = [(data.TxnPosting(augmenting[1], augmenting[2]), data.TxnPosting(reducing[1], reducing[2])) for augmenting, reducing in sorted(trade_map.values())] # Sanity check. for matches in trades: assert len(matches) == 2 return trades beancount.plugins.book_conversions.is_matching(posting, account) \uf0c1 \"Identify if the given posting is one to be booked. Parameters: posting \u2013 An instance of a Posting. account \u2013 The account name configured. Returns: A boolean, true if this posting is one that we should be adding a cost to. Source code in beancount/plugins/book_conversions.py def is_matching(posting, account): \"\"\"\"Identify if the given posting is one to be booked. Args: posting: An instance of a Posting. account: The account name configured. Returns: A boolean, true if this posting is one that we should be adding a cost to. \"\"\" return (posting.account == account and posting.cost is None and posting.price is not None) beancount.plugins.book_conversions.link_entries_with_metadata(entries, all_matches) \uf0c1 Modify the entries in-place to add matching links to postings. Parameters: entries \u2013 The list of entries to modify. all_matches \u2013 A list of pairs of (augmenting-posting, reducing-posting). Returns: A list of pairs of (index, Posting) for the new (augmenting, reducing) annotated postings. Source code in beancount/plugins/book_conversions.py def link_entries_with_metadata(entries, all_matches): \"\"\"Modify the entries in-place to add matching links to postings. Args: entries: The list of entries to modify. all_matches: A list of pairs of (augmenting-posting, reducing-posting). Returns: A list of pairs of (index, Posting) for the new (augmenting, reducing) annotated postings. \"\"\" # Allocate trade names and compute a map of posting to trade names. link_map = collections.defaultdict(list) for (aug_index, aug_posting), (red_index, red_posting) in all_matches: link = 'trade-{}'.format(str(uuid.uuid4()).split('-')[-1]) link_map[id(aug_posting)].append(link) link_map[id(red_posting)].append(link) # Modify the postings. postings_repl_map = {} for entry in entries: if isinstance(entry, data.Transaction): for index, posting in enumerate(entry.postings): links = link_map.pop(id(posting), None) if links: new_posting = posting._replace(meta=posting.meta.copy()) new_posting.meta[META] = ','.join(links) entry.postings[index] = new_posting postings_repl_map[id(posting)] = new_posting # Just a sanity check. assert not link_map, \"Internal error: not all matches found.\" # Return a list of the modified postings (mapping the old matches to the # newly created ones). return [((aug_index, postings_repl_map[id(aug_posting)]), (red_index, postings_repl_map[id(red_posting)])) for (aug_index, aug_posting), (red_index, red_posting) in all_matches] beancount.plugins.book_conversions.main() \uf0c1 Extract trades from metadata-annotated postings and report on them. Source code in beancount/plugins/book_conversions.py def main(): \"\"\"Extract trades from metadata-annotated postings and report on them. \"\"\" logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s') parser = version.ArgumentParser(description=__doc__.strip()) parser.add_argument('filename', help='Beancount input filename') oparser = parser.add_argument_group('Outputs') oparser.add_argument('-o', '--output', action='store', help=\"Filename to output results to (default goes to stdout)\") oparser.add_argument('-f', '--format', default='text', choices=['text', 'csv'], help=\"Output format to render to (text, csv)\") args = parser.parse_args() # Load the input file. entries, errors, options_map = loader.load_file(args.filename) # Get the list of trades. trades = extract_trades(entries) # Produce a table of all the trades. columns = ('units currency cost_currency ' 'buy_date buy_price sell_date sell_price pnl').split() header = ['Units', 'Currency', 'Cost Currency', 'Buy Date', 'Buy Price', 'Sell Date', 'Sell Price', 'P/L'] body = [] for aug, red in trades: units = -red.posting.units.number buy_price = aug.posting.price.number sell_price = red.posting.price.number pnl = (units * (sell_price - buy_price)).quantize(buy_price) body.append([ -red.posting.units.number, red.posting.units.currency, red.posting.price.currency, aug.txn.date.isoformat(), buy_price, red.txn.date.isoformat(), sell_price, pnl ]) trades_table = table.Table(columns, header, body) # Render the table as text or CSV. outfile = open(args.output, 'w') if args.output else sys.stdout table.render_table(trades_table, outfile, args.format) beancount.plugins.book_conversions.reduce_inventory(pending_lots, posting, eindex) \uf0c1 Match a reducing posting against a list of lots (using FIFO order). Parameters: pending_lots \u2013 A list of pending ([number], Posting, Transaction) to be matched. The number is modified in-place, destructively. posting \u2013 The posting whose position is to be added. eindex \u2013 The index of the parent transaction housing this posting. Returns: A tuple of postings \u2013 A list of new Posting instances corresponding to the given posting, that were booked to the current list of lots. matches: A list of pairs of (augmenting-posting, reducing-posting). pnl: A Decimal, the P/L incurred in reducing these lots. errors: A list of new errors generated in reducing these lots. Source code in beancount/plugins/book_conversions.py def reduce_inventory(pending_lots, posting, eindex): \"\"\"Match a reducing posting against a list of lots (using FIFO order). Args: pending_lots: A list of pending ([number], Posting, Transaction) to be matched. The number is modified in-place, destructively. posting: The posting whose position is to be added. eindex: The index of the parent transaction housing this posting. Returns: A tuple of postings: A list of new Posting instances corresponding to the given posting, that were booked to the current list of lots. matches: A list of pairs of (augmenting-posting, reducing-posting). pnl: A Decimal, the P/L incurred in reducing these lots. errors: A list of new errors generated in reducing these lots. \"\"\" new_postings = [] matches = [] pnl = ZERO errors = [] match_number = -posting.units.number match_currency = posting.units.currency cost_currency = posting.price.currency while match_number != ZERO: # Find the first lot with matching currency. for fnumber, fposting, findex in pending_lots: funits = fposting.units fcost = fposting.cost if (funits.currency == match_currency and fcost and fcost.currency == cost_currency): assert fnumber[0] > ZERO, \"Internal error, zero lot\" break else: errors.append( BookConversionError(posting.meta, \"Could not match position {}\".format(posting), None)) break # Reduce the pending lots. number = min(match_number, fnumber[0]) cost = fcost match_number -= number fnumber[0] -= number if fnumber[0] == ZERO: pending_lots.pop(0) # Add a corresponding posting. rposting = posting._replace( units=amount.Amount(-number, posting.units.currency), cost=copy.copy(cost)) new_postings.append(rposting) # Update the P/L. pnl += number * (posting.price.number - cost.number) # Add to the list of matches. matches.append(((findex, fposting), (eindex, rposting))) return new_postings, matches, pnl, errors beancount.plugins.check_average_cost \uf0c1 A plugin that ensures cost basis is preserved in unbooked transactions. This is intended to be used in accounts using the \"NONE\" booking method, to manually ensure that the sum total of the cost basis of reducing legs matches the average of what's in the account inventory. This is a partial first step toward implementing the \"AVERAGE\" booking method. In other words, this plugins provides assertions that will constrain you to approximate what the \"AVERAGE\" booking method will do, manually, and not to leak too much cost basis through unmatching bookings without checks. (Note the contrived context here: Ideally the \"NONE\" booking method would simply not exist.) beancount.plugins.check_average_cost.MatchBasisError ( tuple ) \uf0c1 MatchBasisError(source, message, entry) beancount.plugins.check_average_cost.MatchBasisError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/check_average_cost.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.check_average_cost.MatchBasisError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of MatchBasisError(source, message, entry) beancount.plugins.check_average_cost.MatchBasisError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/check_average_cost.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.check_average_cost.validate_average_cost(entries, options_map, config_str=None) \uf0c1 Check that reducing legs on unbooked postings are near the average cost basis. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config_str \u2013 The configuration as a string version of a float. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_average_cost.py def validate_average_cost(entries, options_map, config_str=None): \"\"\"Check that reducing legs on unbooked postings are near the average cost basis. Args: entries: A list of directives. unused_options_map: An options map. config_str: The configuration as a string version of a float. Returns: A list of new errors, if any were found. \"\"\" # Initialize tolerance bounds. if config_str and config_str.strip(): # pylint: disable=eval-used config_obj = eval(config_str, {}, {}) if not isinstance(config_obj, float): raise RuntimeError(\"Invalid configuration for check_average_cost: \" \"must be a float\") tolerance = config_obj else: tolerance = DEFAULT_TOLERANCE min_tolerance = D(1 - tolerance) max_tolerance = D(1 + tolerance) errors = [] ocmap = getters.get_account_open_close(entries) balances = collections.defaultdict(inventory.Inventory) for entry in entries: if isinstance(entry, Transaction): for posting in entry.postings: dopen = ocmap.get(posting.account, None) # Only process accounts with a NONE booking value. if dopen and dopen[0] and dopen[0].booking == Booking.NONE: balance = balances[(posting.account, posting.units.currency, posting.cost.currency if posting.cost else None)] if posting.units.number < ZERO: average = balance.average().get_only_position() if average is not None: number = average.cost.number min_valid = number * min_tolerance max_valid = number * max_tolerance if not (min_valid <= posting.cost.number <= max_valid): errors.append( MatchBasisError( entry.meta, (\"Cost basis on reducing posting is too far from \" \"the average cost ({} vs. {})\".format( posting.cost.number, average.cost.number)), entry)) balance.add_position(posting) return entries, errors beancount.plugins.check_closing \uf0c1 A plugin that automatically inserts a balance check on a tagged closing posting. Some postings are known to the user to be \"closing trades\", which means that the resulting position of the instrument just after the trade should be zero. For instance, this is the case for most ordinary options trading, only one lot of a particular instrument is held, and eventually expires or gets sold off in its entirely. One would like to confirm that, and the way to do this in Beancount is to insert a balance check. This plugin allows you to do that more simply, by inserting metadata. For example, this transaction: 2018-02-16 * \"SOLD -14 QQQ 100 16 FEB 18 160 CALL @5.31\" Assets:US:Brokerage:Main:Options -1400 QQQ180216C160 {2.70 USD} @ 5.31 USD closing: TRUE Expenses:Financial:Commissions 17.45 USD Expenses:Financial:Fees 0.42 USD Assets:US:Brokerage:Main:Cash 7416.13 USD Income:US:Brokerage:Main:PnL Would expand into the following two directives: 2018-02-16 * \"SOLD -14 QQQ 100 16 FEB 18 160 CALL @5.31\" Assets:US:Brokerage:Main:Options -1400 QQQ180216C160 {2.70 USD} @ 5.31 USD Expenses:Financial:Commissions 17.45 USD Expenses:Financial:Fees 0.42 USD Assets:US:Brokerage:Main:Cash 7416.13 USD Income:US:Brokerage:Main:PnL 2018-02-17 balance Assets:US:Brokerage:Main:Options 0 QQQ180216C160 Insert the closing line when you know you're closing the position. beancount.plugins.check_closing.check_closing(entries, options_map) \uf0c1 Expand 'closing' metadata to a zero balance check. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_closing.py def check_closing(entries, options_map): \"\"\"Expand 'closing' metadata to a zero balance check. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" new_entries = [] for entry in entries: if isinstance(entry, data.Transaction): for posting in entry.postings: if posting.meta and posting.meta.get('closing', False): # Remove the metadata. meta = posting.meta.copy() del meta['closing'] entry = entry._replace(meta=meta) # Insert a balance. date = entry.date + datetime.timedelta(days=1) balance = data.Balance(data.new_metadata(\"\", 0), date, posting.account, amount.Amount(ZERO, posting.units.currency), None, None) new_entries.append(balance) new_entries.append(entry) return new_entries, [] beancount.plugins.check_commodity \uf0c1 A plugin that verifies that all seen commodities have a Commodity directive. This is useful if you're a bit pedantic and like to make sure that you're declared attributes for each of the commodities you use. It's useful if you use the portfolio export, for example. beancount.plugins.check_commodity.CheckCommodityError ( tuple ) \uf0c1 CheckCommodityError(source, message, entry) beancount.plugins.check_commodity.CheckCommodityError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/check_commodity.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.check_commodity.CheckCommodityError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of CheckCommodityError(source, message, entry) beancount.plugins.check_commodity.CheckCommodityError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/check_commodity.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.check_commodity.validate_commodity_directives(entries, options_map) \uf0c1 Find all commodities used and ensure they have a corresponding Commodity directive. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_commodity.py def validate_commodity_directives(entries, options_map): \"\"\"Find all commodities used and ensure they have a corresponding Commodity directive. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" commodities_used = options_map['commodities'] errors = [] meta = data.new_metadata('', 0) commodity_map = getters.get_commodity_map(entries, create_missing=False) for currency in commodities_used: commodity_entry = commodity_map.get(currency, None) if commodity_entry is None: errors.append( CheckCommodityError( meta, \"Missing Commodity directive for '{}'\".format(currency), None)) return entries, errors beancount.plugins.coherent_cost \uf0c1 This plugin validates that currencies held at cost aren't ever converted at price and vice-versa. This is usually the case, and using it will prevent users from making the mistake of selling a lot without specifying it via its cost basis. beancount.plugins.coherent_cost.CoherentCostError ( tuple ) \uf0c1 CoherentCostError(source, message, entry) beancount.plugins.coherent_cost.CoherentCostError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/coherent_cost.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.coherent_cost.CoherentCostError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of CoherentCostError(source, message, entry) beancount.plugins.coherent_cost.CoherentCostError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/coherent_cost.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.coherent_cost.validate_coherent_cost(entries, unused_options_map) \uf0c1 Check that all currencies are either used at cost or not at all, but never both. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/coherent_cost.py def validate_coherent_cost(entries, unused_options_map): \"\"\"Check that all currencies are either used at cost or not at all, but never both. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] with_cost = {} without_cost = {} for entry in data.filter_txns(entries): for posting in entry.postings: target_set = without_cost if posting.cost is None else with_cost currency = posting.units.currency target_set.setdefault(currency, entry) for currency in set(with_cost) & set(without_cost): errors.append( CoherentCostError( without_cost[currency].meta, \"Currency '{}' is used both with and without cost\".format(currency), with_cost[currency])) # Note: We really ought to include both of the first transactions here. return entries, errors beancount.plugins.commodity_attr \uf0c1 A plugin that asserts that all Commodity directives have a particular attribute and that it is part of a set of enum values. The configuration must be a mapping of attribute name to list of valid values, like this: plugin \"beancount.plugins.commodity_attr\" \"{ 'sector': ['Technology', 'Financials', 'Energy'], 'name': None, }\" The plugin issues an error if a Commodity directive is missing the attribute, or if the attribute value is not in the valid set. If you'd like to just ensure the attribute is set, set the list of valid values to None, as in the 'name' attribute in the example above. beancount.plugins.commodity_attr.CommodityError ( tuple ) \uf0c1 CommodityError(source, message, entry) beancount.plugins.commodity_attr.CommodityError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/commodity_attr.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.commodity_attr.CommodityError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of CommodityError(source, message, entry) beancount.plugins.commodity_attr.CommodityError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/commodity_attr.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.commodity_attr.ConfigError ( tuple ) \uf0c1 ConfigError(source, message, entry) beancount.plugins.commodity_attr.ConfigError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/commodity_attr.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.commodity_attr.ConfigError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of ConfigError(source, message, entry) beancount.plugins.commodity_attr.ConfigError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/commodity_attr.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.commodity_attr.validate_commodity_attr(entries, unused_options_map, config_str) \uf0c1 Check that all Commodity directives have a valid attribute. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config_str \u2013 A configuration string. Returns: A list of new errors, if any were found. Source code in beancount/plugins/commodity_attr.py def validate_commodity_attr(entries, unused_options_map, config_str): \"\"\"Check that all Commodity directives have a valid attribute. Args: entries: A list of directives. unused_options_map: An options map. config_str: A configuration string. Returns: A list of new errors, if any were found. \"\"\" errors = [] # pylint: disable=eval-used config_obj = eval(config_str, {}, {}) if not isinstance(config_obj, dict): errors.append(ConfigError( data.new_metadata('', 0), \"Invalid configuration for commodity_attr plugin; skipping.\", None)) return entries, errors validmap = {attr: frozenset(values) if values is not None else None for attr, values in config_obj.items()} for entry in entries: if not isinstance(entry, data.Commodity): continue for attr, values in validmap.items(): value = entry.meta.get(attr, None) if value is None: errors.append(CommodityError( entry.meta, \"Missing attribute '{}' for Commodity directive {}\".format( attr, entry.currency), None)) continue if values and value not in values: errors.append(CommodityError( entry.meta, \"Invalid attribute '{}' for Commodity\".format(value) + \" directive {}; valid options: {}\".format( entry.currency, ', '.join(values)), None)) return entries, errors beancount.plugins.currency_accounts \uf0c1 An implementation of currency accounts. This is an automatic implementation of the method described here: https://www.mathstat.dal.ca/~selinger/accounting/tutorial.html You enable it just like this: plugin \"beancount.plugins.currency_accounts\" \"Equity:CurrencyAccounts\" Accounts will be automatically created under the given base account, with the currency name appended to it, e.g., Equity:CurrencyAccounts:CAD Equity:CurrencyAccounts:USD etc., where used. You can have a look at the account balances with a query like this: bean-query $L \"select account, sum(position), convert(sum(position), 'USD') where date >= 2018-01-01 and account ~ 'CurrencyAccounts' \" The sum total of the converted amounts should be a number not too large: bean-query $L \"select convert(sum(position), 'USD') where date >= 2018-01-01 and account ~ 'CurrencyAccounts'\" WARNING: This is a prototype. Note the FIXMEs in the code below, which indicate some potential problems. beancount.plugins.currency_accounts.get_neutralizing_postings(curmap, base_account, new_accounts) \uf0c1 Process an entry. Parameters: curmap \u2013 A dict of currency to a list of Postings of this transaction. base_account \u2013 A string, the root account name to insert. new_accounts \u2013 A set, a mutable accumulator of new account names. Returns: A modified entry, with new postings inserted to rebalance currency trading accounts. Source code in beancount/plugins/currency_accounts.py def get_neutralizing_postings(curmap, base_account, new_accounts): \"\"\"Process an entry. Args: curmap: A dict of currency to a list of Postings of this transaction. base_account: A string, the root account name to insert. new_accounts: A set, a mutable accumulator of new account names. Returns: A modified entry, with new postings inserted to rebalance currency trading accounts. \"\"\" new_postings = [] for currency, postings in curmap.items(): # Compute the per-currency balance. inv = inventory.Inventory() for posting in postings: inv.add_amount(convert.get_cost(posting)) if inv.is_empty(): new_postings.extend(postings) continue # Re-insert original postings and remove price conversions. # # Note: This may cause problems if the implicit_prices plugin is # configured to run after this one, or if you need the price annotations # for some scripting or serious work. # # FIXME: We need to handle these important cases (they're not frivolous, # this is a prototype), probably by inserting some exceptions with # collaborating code in the booking (e.g. insert some metadata that # disables price conversions on those postings). # # FIXME(2): Ouch! Some of the residual seeps through here, where there # are more than a single currency block. This needs fixing too. You can # easily mitigate some of this to some extent, by excluding transactions # which don't have any price conversion in them. for pos in postings: if pos.price is not None: pos = pos._replace(price=None) new_postings.append(pos) # Insert the currency trading accounts postings. amount = inv.get_only_position().units acc = account.join(base_account, currency) new_accounts.add(acc) new_postings.append( Posting(acc, -amount, None, None, None, None)) return new_postings beancount.plugins.currency_accounts.group_postings_by_weight_currency(entry) \uf0c1 Return where this entry might require adjustment. Source code in beancount/plugins/currency_accounts.py def group_postings_by_weight_currency(entry: Transaction): \"\"\"Return where this entry might require adjustment.\"\"\" curmap = collections.defaultdict(list) has_price = False for posting in entry.postings: currency = posting.units.currency if posting.cost: currency = posting.cost.currency if posting.price: assert posting.price.currency == currency elif posting.price: has_price = True currency = posting.price.currency if posting.price: has_price = True curmap[currency].append(posting) return curmap, has_price beancount.plugins.currency_accounts.insert_currency_trading_postings(entries, options_map, config) \uf0c1 Insert currency trading postings. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config \u2013 The base account name for currency trading accounts. Returns: A list of new errors, if any were found. Source code in beancount/plugins/currency_accounts.py 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('', index), earliest_date, acc, None, None) for index, acc in enumerate(sorted(new_accounts))] return open_entries + new_entries, errors beancount.plugins.divert_expenses \uf0c1 For tagged transactions, convert expenses to a single account. This plugin allows you to select a tag and it automatically converts all the Expenses postings to use a single account. For example, with this input: plugin \"divert_expenses\" \"['kid', 'Expenses:Child']\" 2018-01-28 * \"CVS\" \"Formula\" #kid Liabilities:CreditCard -10.27 USD Expenses:Food:Grocery 10.27 USD It will output: 2018-01-28 * \"CVS\" \"Formula\" #kid Liabilities:CreditCard -10.27 USD Expenses:Child 10.27 USD You can limit the diversion to one posting only, like this: 2018-05-05 * \"CVS/PHARMACY\" \"\" #kai Liabilities:CreditCard -66.38 USD Expenses:Pharmacy 21.00 USD ;; Vitamins for Kai Expenses:Pharmacy 45.38 USD divert: FALSE See unit test for details. See this thread for context: https://docs.google.com/drawings/d/18fTrrGlmz0jFbfcGGHTffbdRwbmST8r9_3O26Dd1Xww/edit?usp=sharing beancount.plugins.divert_expenses.divert_expenses(entries, options_map, config_str) \uf0c1 Divert expenses. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. options_map \u2013 A parser options dict. config_str \u2013 A configuration string, which is intended to be a list of two strings, a tag, and an account to replace expenses with. Returns: A modified list of entries. Source code in beancount/plugins/divert_expenses.py def divert_expenses(entries, options_map, config_str): \"\"\"Divert expenses. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Args: entries: A list of directives. We're interested only in the Transaction instances. options_map: A parser options dict. config_str: A configuration string, which is intended to be a list of two strings, a tag, and an account to replace expenses with. Returns: A modified list of entries. \"\"\" # pylint: disable=eval-used config_obj = eval(config_str, {}, {}) if not isinstance(config_obj, dict): raise RuntimeError(\"Invalid plugin configuration: should be a single dict.\") tag = config_obj['tag'] replacement_account = config_obj['account'] acctypes = options.get_account_types(options_map) new_entries = [] errors = [] for entry in entries: if isinstance(entry, Transaction) and tag in entry.tags: entry = replace_diverted_accounts(entry, replacement_account, acctypes) new_entries.append(entry) return new_entries, errors beancount.plugins.divert_expenses.replace_diverted_accounts(entry, replacement_account, acctypes) \uf0c1 Replace the Expenses accounts from the entry. Parameters: entry \u2013 A Transaction directive. replacement_account \u2013 A string, the account to use for replacement. acctypes \u2013 An AccountTypes instance. Returns: A possibly entry directive. Source code in beancount/plugins/divert_expenses.py def replace_diverted_accounts(entry, replacement_account, acctypes): \"\"\"Replace the Expenses accounts from the entry. Args: entry: A Transaction directive. replacement_account: A string, the account to use for replacement. acctypes: An AccountTypes instance. Returns: A possibly entry directive. \"\"\" new_postings = [] for posting in entry.postings: divert = posting.meta.get('divert', None) if posting.meta else None if (divert is True or ( divert is None and account_types.is_account_type(acctypes.expenses, posting.account))): posting = posting._replace(account=replacement_account, meta={'diverted_account': posting.account}) new_postings.append(posting) return entry._replace(postings=new_postings) beancount.plugins.exclude_tag \uf0c1 Exclude #virtual tags. This is used to demonstrate excluding a set of transactions from a particular tag. In this example module, the tag name is fixed, but if we integrated this we could provide a way to choose which tags to exclude. This is simply just another mechanism for selecting a subset of transactions. See discussion here for details: https://groups.google.com/d/msg/ledger-cli/N8Slh2t45K0/aAz0i3Be4LYJ beancount.plugins.exclude_tag.exclude_tag(entries, options_map) \uf0c1 Select all transactions that do not have a #virtual tag. Parameters: entries \u2013 A list of entry instances. options_map \u2013 A dict of options parsed from the file. Returns: A tuple of entries and errors. Source code in beancount/plugins/exclude_tag.py def exclude_tag(entries, options_map): \"\"\"Select all transactions that do not have a #virtual tag. Args: entries: A list of entry instances. options_map: A dict of options parsed from the file. Returns: A tuple of entries and errors. \"\"\" filtered_entries = [entry for entry in entries if (not isinstance(entry, data.Transaction) or not entry.tags or EXCLUDED_TAG not in entry.tags)] return (filtered_entries, []) beancount.plugins.fill_account \uf0c1 Insert an posting with a default account when there is only a single posting. This is convenient to use in files which have mostly expenses, such as during a trip. Set the name of the default account to fill in as an option. beancount.plugins.fill_account.FillAccountError ( tuple ) \uf0c1 FillAccountError(source, message, entry) beancount.plugins.fill_account.FillAccountError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/fill_account.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.fill_account.FillAccountError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of FillAccountError(source, message, entry) beancount.plugins.fill_account.FillAccountError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/fill_account.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.fill_account.fill_account(entries, unused_options_map, insert_account) \uf0c1 Insert an posting with a default account when there is only a single posting. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 A parser options dict. insert_account \u2013 A string, the name of the account. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. Source code in beancount/plugins/fill_account.py 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('', 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, [] beancount.plugins.fix_payees \uf0c1 Rename payees based on a set of rules. This can be used to clean up dirty imported payee names. This plugin accepts a list of rules in this format: plugin \"beancount.plugins.fix_payees\" \"[ (PAYEE, MATCH1, MATCH2, ...), ]\" Each of the \"MATCH\" clauses is a string, in the format: \"A:<regexp>\" : Match the account name. \"D:<regexp>\" : Match the payee or the narration. The plugin matches the Transactions in the file and if there is a case-insensitive match against the regular expression (we use re.search()), replaces the payee name by \"PAYEE\". If multiple rules match, only the first rule is used. For example: plugin \"beancount.plugins.fix_payees\" \"[ (\"T-Mobile USA\", \"A:Expenses:Communications:Phone\", \"D:t-mobile\"), (\"Con Edison\", \"A:Expenses:Home:Electricity\", \"D:con ?ed\"), (\"Birreria @ Eataly\", \"D:EATALY BIRRERIA\"), ]\" beancount.plugins.fix_payees.FixPayeesError ( tuple ) \uf0c1 FixPayeesError(source, message, entry) beancount.plugins.fix_payees.FixPayeesError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/fix_payees.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.fix_payees.FixPayeesError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of FixPayeesError(source, message, entry) beancount.plugins.fix_payees.FixPayeesError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/fix_payees.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.fix_payees.fix_payees(entries, options_map, config) \uf0c1 Rename payees based on a set of rules. See module docstring for details. Parameters: entries \u2013 a list of entry instances options_map \u2013 a dict of options parsed from the file config \u2013 A configuration string, which is intended to be a list of (PAYEE, MATCH, ...) rules. See module docstring for details. Returns: A tuple of entries and errors. Source code in beancount/plugins/fix_payees.py def fix_payees(entries, options_map, config): \"\"\"Rename payees based on a set of rules. See module docstring for details. Args: entries: a list of entry instances options_map: a dict of options parsed from the file config: A configuration string, which is intended to be a list of (PAYEE, MATCH, ...) rules. See module docstring for details. Returns: A tuple of entries and errors. \"\"\" errors = [] if config.strip(): try: expr = ast.literal_eval(config) except (SyntaxError, ValueError): meta = data.new_metadata(options_map['filename'], 0) errors.append(FixPayeesError(meta, \"Syntax error in config: {}\".format(config), None)) return entries, errors else: return entries, errors # Pre-compile the regular expressions for performance. rules = [] for rule in ast.literal_eval(config): clauses = iter(rule) new_payee = next(clauses) regexps = [] for clause in clauses: match = re.match('([AD]):(.*)', clause) if not match: meta = data.new_metadata(options_map['filename'], 0) errors.append(FixPayeesError(meta, \"Invalid clause: {}\".format(clause), None)) continue command, regexp = match.groups() regexps.append((command, re.compile(regexp, re.I).search)) new_rule = [new_payee] + regexps rules.append(tuple(new_rule)) # Run the rules over the transaction objects. new_entries = [] replaced_entries = {rule[0]: [] for rule in rules} for entry in entries: if isinstance(entry, data.Transaction): for rule in rules: clauses = iter(rule) new_payee = next(clauses) # Attempt to match all the clauses. for clause in clauses: command, func = clause if command == 'D': if not ((entry.payee is not None and func(entry.payee)) or (entry.narration is not None and func(entry.narration))): break elif command == 'A': if not any(func(posting.account) for posting in entry.postings): break else: # Make the replacement. entry = entry._replace(payee=new_payee) replaced_entries[new_payee].append(entry) new_entries.append(entry) if _DEBUG: # Print debugging info. for payee, repl_entries in sorted(replaced_entries.items(), key=lambda x: len(x[1]), reverse=True): print('{:60}: {}'.format(payee, len(repl_entries))) return new_entries, errors beancount.plugins.forecast \uf0c1 An example of adding a forecasting feature to Beancount via a plugin. This entry filter plugin uses existing syntax to define and automatically inserted transactions in the future based on a convention. It serves mostly as an example of how you can experiment by creating and installing a local filter, and not so much as a serious forecasting feature (though the experiment is a good way to get something more general kickstarted eventually, I think the concept would generalize nicely and should eventually be added as a common feature of Beancount). A user can create a transaction like this: 2014-03-08 # \"Electricity bill [MONTHLY]\" Expenses:Electricity 50.10 USD Assets:Checking -50.10 USD and new transactions will be created monthly for the following year. Note the use of the '#' flag and the word 'MONTHLY' which defines the periodicity. The number of recurrences can optionally be specified either by providing an end date or by specifying the number of times that the transaction will be repeated. For example: 2014-03-08 # \"Electricity bill [MONTHLY UNTIL 2019-12-31]\" Expenses:Electricity 50.10 USD Assets:Checking -50.10 USD 2014-03-08 # \"Electricity bill [MONTHLY REPEAT 10 TIMES]\" Expenses:Electricity 50.10 USD Assets:Checking -50.10 USD Transactions can also be repeated at yearly intervals, e.g.: 2014-03-08 # \"Electricity bill [YEARLY REPEAT 10 TIMES]\" Expenses:Electricity 50.10 USD Assets:Checking -50.10 USD Other examples: 2014-03-08 # \"Electricity bill [WEEKLY SKIP 1 TIME REPEAT 10 TIMES]\" Expenses:Electricity 50.10 USD Assets:Checking -50.10 USD 2014-03-08 # \"Electricity bill [DAILY SKIP 3 TIMES REPEAT 1 TIME]\" Expenses:Electricity 50.10 USD Assets:Checking -50.10 USD beancount.plugins.forecast.forecast_plugin(entries, options_map) \uf0c1 An example filter that piggybacks on top of the Beancount input syntax to insert forecast entries automatically. This functions accepts the return value of beancount.loader.load_file() and must return the same type of output. Parameters: entries \u2013 a list of entry instances options_map \u2013 a dict of options parsed from the file Returns: A tuple of entries and errors. Source code in beancount/plugins/forecast.py def forecast_plugin(entries, options_map): \"\"\"An example filter that piggybacks on top of the Beancount input syntax to insert forecast entries automatically. This functions accepts the return value of beancount.loader.load_file() and must return the same type of output. Args: entries: a list of entry instances options_map: a dict of options parsed from the file Returns: A tuple of entries and errors. \"\"\" # Find the last entry's date. date_today = entries[-1].date # Filter out forecast entries from the list of valid entries. forecast_entries = [] filtered_entries = [] for entry in entries: outlist = (forecast_entries if (isinstance(entry, data.Transaction) and entry.flag == '#') else filtered_entries) outlist.append(entry) # Generate forecast entries up to the end of the current year. new_entries = [] for entry in forecast_entries: # Parse the periodicity. match = re.search(r'(^.*)\\[(MONTHLY|YEARLY|WEEKLY|DAILY)' r'(\\s+SKIP\\s+([1-9][0-9]*)\\s+TIME.?)' r'?(\\s+REPEAT\\s+([1-9][0-9]*)\\s+TIME.?)' r'?(\\s+UNTIL\\s+([0-9\\-]+))?\\]', entry.narration) if not match: new_entries.append(entry) continue forecast_narration = match.group(1).strip() forecast_interval = ( rrule.YEARLY if match.group(2).strip() == 'YEARLY' else rrule.WEEKLY if match.group(2).strip() == 'WEEKLY' else rrule.DAILY if match.group(2).strip() == 'DAILY' else rrule.MONTHLY) forecast_periodicity = {'dtstart': entry.date} if match.group(6): # e.g., [MONTHLY REPEAT 3 TIMES]: forecast_periodicity['count'] = int(match.group(6)) elif match.group(8): # e.g., [MONTHLY UNTIL 2020-01-01]: forecast_periodicity['until'] = datetime.datetime.strptime( match.group(8), '%Y-%m-%d').date() else: # e.g., [MONTHLY] forecast_periodicity['until'] = datetime.date( datetime.date.today().year, 12, 31) if match.group(4): # SKIP forecast_periodicity['interval'] = int(match.group(4)) + 1 # Generate a new entry for each forecast date. forecast_dates = [dt.date() for dt in rrule.rrule(forecast_interval, **forecast_periodicity)] for forecast_date in forecast_dates: forecast_entry = entry._replace(date=forecast_date, narration=forecast_narration) new_entries.append(forecast_entry) # Make sure the new entries inserted are sorted. new_entries.sort(key=data.entry_sortkey) return (filtered_entries + new_entries, []) beancount.plugins.implicit_prices \uf0c1 This plugin synthesizes Price directives for all Postings with a price or directive or if it is an augmenting posting, has a cost directive. beancount.plugins.implicit_prices.ImplicitPriceError ( tuple ) \uf0c1 ImplicitPriceError(source, message, entry) beancount.plugins.implicit_prices.ImplicitPriceError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/implicit_prices.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.implicit_prices.ImplicitPriceError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of ImplicitPriceError(source, message, entry) beancount.plugins.implicit_prices.ImplicitPriceError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/implicit_prices.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.implicit_prices.add_implicit_prices(entries, unused_options_map) \uf0c1 Insert implicitly defined prices from Transactions. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. Source code in beancount/plugins/implicit_prices.py def add_implicit_prices(entries, unused_options_map): \"\"\"Insert implicitly defined prices from Transactions. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. \"\"\" new_entries = [] errors = [] # A dict of (date, currency, cost-currency) to price entry. new_price_entry_map = {} balances = collections.defaultdict(inventory.Inventory) for entry in entries: # Always replicate the existing entries. new_entries.append(entry) if isinstance(entry, Transaction): # Inspect all the postings in the transaction. for posting in entry.postings: units = posting.units cost = posting.cost # Check if the position is matching against an existing # position. _, booking = balances[posting.account].add_position(posting) # Add prices when they're explicitly specified on a posting. An # explicitly specified price may occur in a conversion, e.g. # Assets:Account 100 USD @ 1.10 CAD # or, if a cost is also specified, as the current price of the # underlying instrument, e.g. # Assets:Account 100 HOOL {564.20} @ {581.97} USD if posting.price is not None: meta = data.new_metadata(entry.meta[\"filename\"], entry.meta[\"lineno\"]) price_entry = data.Price(meta, entry.date, units.currency, posting.price) # Add costs, when we're not matching against an existing # position. This happens when we're just specifying the cost, # e.g. # Assets:Account 100 HOOL {564.20} elif (cost is not None and booking != inventory.Booking.REDUCED): meta = data.new_metadata(entry.meta[\"filename\"], entry.meta[\"lineno\"]) price_entry = data.Price(meta, entry.date, units.currency, amount.Amount(cost.number, cost.currency)) else: price_entry = None if price_entry is not None: key = (price_entry.date, price_entry.currency, price_entry.amount.number, # Ideally should be removed. price_entry.amount.currency) try: new_price_entry_map[key] ## Do not fail for now. We still have many valid use ## cases of duplicate prices on the same date, for ## example, stock splits, or trades on two dates with ## two separate reported prices. We need to figure out a ## more elegant solution for this in the long term. ## Keeping both for now. We should ideally not use the ## number in the de-dup key above. # # dup_entry = new_price_entry_map[key] # if price_entry.amount.number == dup_entry.amount.number: # # Skip duplicates. # continue # else: # errors.append( # ImplicitPriceError( # entry.meta, # \"Duplicate prices for {} on {}\".format(entry, # dup_entry), # entry)) except KeyError: new_price_entry_map[key] = price_entry new_entries.append(price_entry) return new_entries, errors beancount.plugins.ira_contribs \uf0c1 Automatically adding IRA contributions postings. This plugin looks for increasing postings on specified accounts ('+' sign for Assets and Expenses accounts, '-' sign for the others), or postings with a particular flag on them and when it finds some, inserts a pair of postings on that transaction of the corresponding amounts in a different currency. The currency is intended to be an imaginary currency used to track the number of dollars contributed to a retirement account over time. For example, a possible configuration could be: plugin \"beancount.plugins.ira_contribs\" \"{ 'currency': 'IRAUSD', 'flag': 'M', 'accounts': { 'Income:US:Acme:Match401k': ( 'Assets:US:Federal:Match401k', 'Expenses:Taxes:TY{year}:US:Federal:Match401k'), ('C', 'Assets:US:Fidelity:PreTax401k:Cash'): ( 'Assets:US:Federal:PreTax401k', 'Expenses:Taxes:TY{year}:US:Federal:PreTax401k'), } }\" Note: In this example, the configuration that triggers on the \"Income:US:Acme:Match401k\" account does not require a flag for those accounts; the configuration for the \"Assets:US:Fidelity:PreTax401k:Cash\" account requires postings to have a \"C\" flag to trigger an insertion. Given a transaction like the following, which would be typical for a salary entry where the employer is automatically diverting some of the pre-tax money to a retirement account (in this example, at Fidelity): 2013-02-15 * \"ACME INC PAYROLL\" Income:US:Acme:Salary ... ... Assets:US:BofA:Checking ... Assets:US:Fidelity:PreTax401k:Cash 620.50 USD ... A posting with account 'Assets:US:Fidelity:PreTax401k:Cash', which is configured to match, would be found. The configuration above instructs the plugin to automatically insert new postings like this: 2013-02-15 * \"ACME INC PAYROLL\" ... Assets:US:Fidelity:PreTax401k:Cash 620.50 USD M Assets:US:Federal:PreTax401k -620.50 IRAUSD M Expenses:Taxes:TY2013:US:Federal:PreTax401k 620.50 IRAUSD ... Notice that the \"{year}\" string in the configuration's account names is automatically replaced by the current year in the account name. This is useful if you maintain separate tax accounts per year. Furthermore, as in the configuration example above, you may have multiple matching entries to trigger multiple insertions. For example, the employer may also match the employee's retirement contribution by depositing some money in the retirement account: 2013-02-15 * \"BUYMF - MATCH\" \"Employer match, invested in SaveEasy 2030 fund\" Assets:US:Fidelity:Match401k:SE2030 34.793 SE2030 {17.834 USD} Income:US:Acme:Match401k -620.50 USD In this example the funds get reported as invested immediately (an intermediate deposit into a cash account does not take place). The plugin configuration would match against the 'Income:US:Acme:Match401k' account and since it increases its value (the normal balance of an Income account is negative), postings would be inserted like this: 2013-02-15 * \"BUYMF - MATCH\" \"Employer match, invested in SaveEasy 2030 fund\" Assets:US:Fidelity:Match401k:SE2030 34.793 SE2030 {17.834 USD} Income:US:Acme:Match401k -620.50 USD M Assets:US:Federal:Match401k -620.50 IRAUSD M Expenses:Taxes:TY2013:US:Federal:Match401k 620.50 IRAUSD Note that the special dict keys 'currency' and 'flag' are used to specify which currency to use for the inserted postings, and if set, which flag to mark these postings with. beancount.plugins.ira_contribs.add_ira_contribs(entries, options_map, config_str) \uf0c1 Add legs for 401k employer match contributions. See module docstring for an example configuration. Parameters: entries \u2013 a list of entry instances options_map \u2013 a dict of options parsed from the file config_str \u2013 A configuration string, which is intended to be a Python dict mapping match-accounts to a pair of (negative-account, position-account) account names. Returns: A tuple of entries and errors. Source code in beancount/plugins/ira_contribs.py def add_ira_contribs(entries, options_map, config_str): \"\"\"Add legs for 401k employer match contributions. See module docstring for an example configuration. Args: entries: a list of entry instances options_map: a dict of options parsed from the file config_str: A configuration string, which is intended to be a Python dict mapping match-accounts to a pair of (negative-account, position-account) account names. Returns: A tuple of entries and errors. \"\"\" # Parse and extract configuration values. # FIXME: Use ast.literal_eval() here; you need to convert this code and the getters. # FIXME: Also, don't raise a RuntimeError, return an error object; review # this for all the plugins. # FIXME: This too is temporary. # pylint: disable=eval-used config_obj = eval(config_str, {}, {}) if not isinstance(config_obj, dict): raise RuntimeError(\"Invalid plugin configuration: should be a single dict.\") # Currency of the inserted postings. currency = config_obj.pop('currency', 'UNKNOWN') # Flag to attach to the inserted postings. insert_flag = config_obj.pop('flag', None) # A dict of account names that trigger the insertion of postings to pairs of # inserted accounts when triggered. accounts = config_obj.pop('accounts', {}) # Convert the key in the accounts configuration for matching. account_transforms = {} for key, config in accounts.items(): if isinstance(key, str): flag = None account = key else: assert isinstance(key, tuple) flag, account = key account_transforms[account] = (flag, config) new_entries = [] for entry in entries: if isinstance(entry, data.Transaction): orig_entry = entry for posting in entry.postings: if (posting.units is not MISSING and (posting.account in account_transforms) and (account_types.get_account_sign(posting.account) * posting.units.number > 0)): # Get the new account legs to insert. required_flag, (neg_account, pos_account) = account_transforms[posting.account] assert posting.cost is None # Check required flag if present. if (required_flag is None or (required_flag and required_flag == posting.flag)): # Insert income/expense entries for 401k. entry = add_postings( entry, amount.Amount(abs(posting.units.number), currency), neg_account.format(year=entry.date.year), pos_account.format(year=entry.date.year), insert_flag) if DEBUG and orig_entry is not entry: printer.print_entry(orig_entry) printer.print_entry(entry) new_entries.append(entry) return new_entries, [] beancount.plugins.ira_contribs.add_postings(entry, amount_, neg_account, pos_account, flag) \uf0c1 Insert positive and negative postings of a position in an entry. Parameters: entry \u2013 A Transaction instance. amount_ \u2013 An Amount instance to create the position, with positive number. neg_account \u2013 An account for the posting with the negative amount. pos_account \u2013 An account for the posting with the positive amount. flag \u2013 A string, that is to be set as flag for the new postings. Returns: A new, modified entry. Source code in beancount/plugins/ira_contribs.py def add_postings(entry, amount_, neg_account, pos_account, flag): \"\"\"Insert positive and negative postings of a position in an entry. Args: entry: A Transaction instance. amount_: An Amount instance to create the position, with positive number. neg_account: An account for the posting with the negative amount. pos_account: An account for the posting with the positive amount. flag: A string, that is to be set as flag for the new postings. Returns: A new, modified entry. \"\"\" return entry._replace(postings=entry.postings + [ data.Posting(neg_account, -amount_, None, None, flag, None), data.Posting(pos_account, amount_, None, None, flag, None), ]) beancount.plugins.leafonly \uf0c1 A plugin that issues errors when amounts are posted to non-leaf accounts, that is, accounts with child accounts. This is an extra constraint that you may want to apply optionally. If you install this plugin, it will issue errors for all accounts that have postings to non-leaf accounts. Some users may want to disallow this and enforce that only leaf accounts may have postings on them. beancount.plugins.leafonly.LeafOnlyError ( tuple ) \uf0c1 LeafOnlyError(source, message, entry) beancount.plugins.leafonly.LeafOnlyError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/leafonly.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.leafonly.LeafOnlyError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of LeafOnlyError(source, message, entry) beancount.plugins.leafonly.LeafOnlyError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/leafonly.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.leafonly.validate_leaf_only(entries, unused_options_map) \uf0c1 Check for non-leaf accounts that have postings on them. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/leafonly.py def validate_leaf_only(entries, unused_options_map): \"\"\"Check for non-leaf accounts that have postings on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" real_root = realization.realize(entries, compute_balance=False) default_meta = data.new_metadata('', 0) open_close_map = None # Lazily computed. errors = [] for real_account in realization.iter_children(real_root): if len(real_account) > 0 and real_account.txn_postings: if open_close_map is None: open_close_map = getters.get_account_open_close(entries) try: open_entry = open_close_map[real_account.account][0] except KeyError: open_entry = None errors.append(LeafOnlyError( open_entry.meta if open_entry else default_meta, \"Non-leaf account '{}' has postings on it\".format(real_account.account), open_entry)) return entries, errors beancount.plugins.mark_unverified \uf0c1 Add metadata to Postings which occur after their last Balance directives. Some people use Balance directives as a way to indicate that all postings before them are verified. They want to compute balances in each account as of the date of that last Balance directives. One way to do that is to use this plugin to mark the postings which occur after and to then filter them out using a WHERE clause on that metadata: SELECT account, sum(position) WHERE NOT meta(\"unverified\") Note that doing such a filtering may result in a list of balances which may not add to zero. Also, postings for accounts without a single Balance directive on them will not be marked as unverified as all (otherwise all the postings would be marked, this would make no sense). beancount.plugins.mark_unverified.mark_unverified(entries, options_map) \uf0c1 Add metadata to postings after the last Balance entry. See module doc. Parameters: entries \u2013 A list of data directives. options_map \u2013 A dict of options, that confirms to beancount.parser.options. 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. Source code in beancount/plugins/mark_unverified.py def mark_unverified(entries, options_map): \"\"\"Add metadata to postings after the last Balance entry. See module doc. Args: entries: A list of data directives. options_map: A dict of options, that confirms to beancount.parser.options. 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. \"\"\" # The last Balance directive seen for each account. last_balances = {} for entry in entries: if isinstance(entry, data.Balance): last_balances[entry.account] = entry new_entries = [] for entry in entries: if isinstance(entry, data.Transaction): postings = entry.postings new_postings = postings for index, posting in enumerate(postings): balance = last_balances.get(posting.account, None) if balance and balance.date <= entry.date: if new_postings is postings: new_postings = postings.copy() new_meta = posting.meta.copy() new_meta['unverified'] = True new_postings[index] = posting._replace(meta=new_meta) if new_postings is not postings: entry = entry._replace(postings=new_postings) new_entries.append(entry) return new_entries, [] beancount.plugins.merge_meta \uf0c1 Merge the metadata from a second file into the current set of entries. This is useful if you like to keep more sensitive private data, such as account numbers or passwords, in a second, possibly encrypted file. This can be used to generate a will, for instance, for your loved ones to be able to figure where all your assets are in case you pass away. You can store all the super secret stuff in a more closely guarded, hidden away separate file. The metadata from Open directives: Account name must match. Close directives: Account name must match. Commodity directives: Currency must match. are copied over. Metadata from the external file conflicting with that present in the main file overwrites it (external data wins). WARNING! If you include an encrypted file and the main file is not encrypted, the contents extraction from the encrypted file may appear in the cache. beancount.plugins.merge_meta.merge_meta(entries, options_map, config) \uf0c1 Load a secondary file and merge its metadata in our given set of entries. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. config \u2013 The plugin configuration string. Returns: A list of entries, with more metadata attached to them. Source code in beancount/plugins/merge_meta.py def merge_meta(entries, options_map, config): \"\"\"Load a secondary file and merge its metadata in our given set of entries. Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. config: The plugin configuration string. Returns: A list of entries, with more metadata attached to them. \"\"\" external_filename = config new_entries = list(entries) ext_entries, ext_errors, ext_options_map = loader.load_file(external_filename) # Map Open and Close directives. oc_map = getters.get_account_open_close(entries) ext_oc_map = getters.get_account_open_close(ext_entries) for account in set(oc_map.keys()) & set(ext_oc_map.keys()): open_entry, close_entry = oc_map[account] ext_open_entry, ext_close_entry = ext_oc_map[account] if open_entry and ext_open_entry: open_entry.meta.update(ext_open_entry.meta) if close_entry and ext_close_entry: close_entry.meta.update(ext_close_entry.meta) # Map Commodity directives. comm_map = getters.get_commodity_map(entries, False) ext_comm_map = getters.get_commodity_map(ext_entries, False) for currency in set(comm_map) & set(ext_comm_map): comm_entry = comm_map[currency] ext_comm_entry = ext_comm_map[currency] if comm_entry and ext_comm_entry: comm_entry.meta.update(ext_comm_entry.meta) # Note: We cannot include the external file in the list of inputs so that a # change of it triggers a cache rebuild because side-effects on options_map # aren't cascaded through. This is something that should be defined better # in the plugin interface and perhaps improved upon. return new_entries, ext_errors beancount.plugins.noduplicates \uf0c1 This plugin validates that there are no duplicate transactions. beancount.plugins.noduplicates.validate_no_duplicates(entries, unused_options_map) \uf0c1 Check that the entries are unique, by computing hashes. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/noduplicates.py def validate_no_duplicates(entries, unused_options_map): \"\"\"Check that the entries are unique, by computing hashes. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" unused_hashes, errors = compare.hash_entries(entries, exclude_meta=True) return entries, errors beancount.plugins.nounused \uf0c1 This plugin validates that there are no unused accounts. beancount.plugins.nounused.UnusedAccountError ( tuple ) \uf0c1 UnusedAccountError(source, message, entry) beancount.plugins.nounused.UnusedAccountError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/nounused.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.nounused.UnusedAccountError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of UnusedAccountError(source, message, entry) beancount.plugins.nounused.UnusedAccountError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/nounused.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.nounused.validate_unused_accounts(entries, unused_options_map) \uf0c1 Check that all accounts declared open are actually used. We check that all of the accounts that are open are at least referred to by another directive. These are probably unused, so issue a warning (we like to be pedantic). Note that an account that is open and then closed is considered used--this is a valid use case that may occur in reality. If you have a use case for an account to be open but never used, you can quiet that warning by initializing the account with a balance asserts or a pad directive, or even use a note will be sufficient. (This is probably a good candidate for optional inclusion as a \"pedantic\" plugin.) Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/nounused.py def validate_unused_accounts(entries, unused_options_map): \"\"\"Check that all accounts declared open are actually used. We check that all of the accounts that are open are at least referred to by another directive. These are probably unused, so issue a warning (we like to be pedantic). Note that an account that is open and then closed is considered used--this is a valid use case that may occur in reality. If you have a use case for an account to be open but never used, you can quiet that warning by initializing the account with a balance asserts or a pad directive, or even use a note will be sufficient. (This is probably a good candidate for optional inclusion as a \"pedantic\" plugin.) Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" # Find all the accounts referenced by entries which are not Open, and the # open directives for error reporting below. open_map = {} referenced_accounts = set() for entry in entries: if isinstance(entry, data.Open): open_map[entry.account] = entry continue referenced_accounts.update(getters.get_entry_accounts(entry)) # Create a list of suitable errors, with the location of the Open directives # corresponding to the unused accounts. errors = [UnusedAccountError(open_entry.meta, \"Unused account '{}'\".format(account), open_entry) for account, open_entry in open_map.items() if account not in referenced_accounts] return entries, errors beancount.plugins.onecommodity \uf0c1 A plugin that issues errors when more than one commodity is used in an account. For investments or trading accounts, it can make it easier to filter the action around a single stock by using the name of the stock as the leaf of the account name. Notes: The plugin will automatically skip accounts that have explicitly declared commodities in their Open directive. You can also set the metadata \"onecommodity: FALSE\" on an account's Open directive to skip the checks for that account. If provided, the configuration should be a regular expression restricting the set of accounts to check. beancount.plugins.onecommodity.OneCommodityError ( tuple ) \uf0c1 OneCommodityError(source, message, entry) beancount.plugins.onecommodity.OneCommodityError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/onecommodity.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.onecommodity.OneCommodityError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of OneCommodityError(source, message, entry) beancount.plugins.onecommodity.OneCommodityError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/onecommodity.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.onecommodity.validate_one_commodity(entries, unused_options_map, config=None) \uf0c1 Check that each account has units in only a single commodity. This is an extra constraint that you may want to apply optionally, despite Beancount's ability to support inventories and aggregations with more than one commodity. I believe this also matches GnuCash's model, where each account has a single commodity attached to it. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config \u2013 The plugin configuration string, a regular expression to match against the subset of accounts to check. Returns: A list of new errors, if any were found. Source code in beancount/plugins/onecommodity.py def validate_one_commodity(entries, unused_options_map, config=None): \"\"\"Check that each account has units in only a single commodity. This is an extra constraint that you may want to apply optionally, despite Beancount's ability to support inventories and aggregations with more than one commodity. I believe this also matches GnuCash's model, where each account has a single commodity attached to it. Args: entries: A list of directives. unused_options_map: An options map. config: The plugin configuration string, a regular expression to match against the subset of accounts to check. Returns: A list of new errors, if any were found. \"\"\" accounts_re = re.compile(config) if config else None # Mappings of account name to lists of currencies for each units and cost. units_map = collections.defaultdict(set) cost_map = collections.defaultdict(set) # Mappings to use just for getting a relevant source. units_source_map = {} cost_source_map = {} # Gather the set of accounts to skip from the Open directives. skip_accounts = set() for entry in entries: if not isinstance(entry, data.Open): continue if (not entry.meta.get(\"onecommodity\", True) or (accounts_re and not accounts_re.match(entry.account)) or (entry.currencies and len(entry.currencies) > 1)): skip_accounts.add(entry.account) # Accumulate all the commodities used. for entry in entries: if isinstance(entry, data.Transaction): for posting in entry.postings: if posting.account in skip_accounts: continue units = posting.units units_map[posting.account].add(units.currency) if len(units_map[posting.account]) > 1: units_source_map[posting.account] = entry cost = posting.cost if cost: cost_map[posting.account].add(cost.currency) if len(cost_map[posting.account]) > 1: units_source_map[posting.account] = entry elif isinstance(entry, data.Balance): if entry.account in skip_accounts: continue units_map[entry.account].add(entry.amount.currency) if len(units_map[entry.account]) > 1: units_source_map[entry.account] = entry elif isinstance(entry, data.Open): if entry.currencies and len(entry.currencies) > 1: skip_accounts.add(entry.account) # Check units. errors = [] for account, currencies in units_map.items(): if account in skip_accounts: continue if len(currencies) > 1: errors.append(OneCommodityError( units_source_map[account].meta, \"More than one currency in account '{}': {}\".format( account, ','.join(currencies)), None)) # Check costs. for account, currencies in cost_map.items(): if account in skip_accounts: continue if len(currencies) > 1: errors.append(OneCommodityError( cost_source_map[account].meta, \"More than one cost currency in account '{}': {}\".format( account, ','.join(currencies)), None)) return entries, errors beancount.plugins.pedantic \uf0c1 A plugin of plugins which triggers are all the pedantic plugins. In a sense, this is the inverse of \"pedantic.\" This is useful when doing some types of quick and dirty tests. beancount.plugins.sellgains \uf0c1 A plugin that cross-checks declared gains against prices on lot sales. When you sell stock, the gains can be automatically implied by the corresponding cash amounts. For example, in the following transaction the 2nd and 3rd postings should match the value of the stock sold: 1999-07-31 * \"Sell\" Assets:US:BRS:Company:ESPP -81 ADSK {26.3125 USD} Assets:US:BRS:Company:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:US:Company:ESPP:PnL -10.125 USD The cost basis is checked against: 2141.36 + 008 + -10.125. That is, the balance checks computes -81 x 26.3125 = -2131.3125 + 2141.36 + 0.08 + -10.125 and checks that the residual is below a small tolerance. But... usually the income leg isn't given to you in statements. Beancount can automatically infer it using the balance, which is convenient, like this: 1999-07-31 * \"Sell\" Assets:US:BRS:Company:ESPP -81 ADSK {26.3125 USD} Assets:US:BRS:Company:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:US:Company:ESPP:PnL Additionally, most often you have the sales prices given to you on your transaction confirmation statement, so you can enter this: 1999-07-31 * \"Sell\" Assets:US:BRS:Company:ESPP -81 ADSK {26.3125 USD} @ 26.4375 USD Assets:US:BRS:Company:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:US:Company:ESPP:PnL So in theory, if the price is given (26.4375 USD), we could verify that the proceeds from the sale at the given price match non-Income postings. That is, verify that -81 x 26.4375 = -2141.4375 + 2141.36 + 0.08 + is below a small tolerance value. So this plugin does this. In general terms, it does the following: For transactions with postings that have a cost and a price, it verifies that the sum of the positions on all postings to non-income accounts is below tolerance. This provides yet another level of verification and allows you to elide the income amounts, knowing that the price is there to provide an extra level of error-checking in case you enter a typo. beancount.plugins.sellgains.SellGainsError ( tuple ) \uf0c1 SellGainsError(source, message, entry) beancount.plugins.sellgains.SellGainsError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/sellgains.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.sellgains.SellGainsError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of SellGainsError(source, message, entry) beancount.plugins.sellgains.SellGainsError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/sellgains.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.sellgains.validate_sell_gains(entries, options_map) \uf0c1 Check the sum of asset account totals for lots sold with a price on them. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/sellgains.py def validate_sell_gains(entries, options_map): \"\"\"Check the sum of asset account totals for lots sold with a price on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] acc_types = options.get_account_types(options_map) proceed_types = set([acc_types.assets, acc_types.liabilities, acc_types.equity, acc_types.expenses]) for entry in entries: if not isinstance(entry, data.Transaction): continue # Find transactions whose lots at cost all have a price. postings_at_cost = [posting for posting in entry.postings if posting.cost is not None] if not postings_at_cost or not all(posting.price is not None for posting in postings_at_cost): continue # Accumulate the total expected proceeds and the sum of the asset and # expenses legs. total_price = inventory.Inventory() total_proceeds = inventory.Inventory() for posting in entry.postings: # If the posting is held at cost, add the priced value to the balance. if posting.cost is not None: assert posting.price is not None price = posting.price total_price.add_amount(amount.mul(price, -posting.units.number)) else: # Otherwise, use the weight and ignore postings to Income accounts. atype = account_types.get_account_type(posting.account) if atype in proceed_types: total_proceeds.add_amount(convert.get_weight(posting)) # Compare inventories, currency by currency. dict_price = {pos.units.currency: pos.units.number for pos in total_price} dict_proceeds = {pos.units.currency: pos.units.number for pos in total_proceeds} tolerances = interpolate.infer_tolerances(entry.postings, options_map) invalid = False for currency, price_number in dict_price.items(): # Accept a looser than usual tolerance because rounding occurs # differently. Also, it would be difficult for the user to satisfy # two sets of constraints manually. tolerance = tolerances.get(currency) * EXTRA_TOLERANCE_MULTIPLIER proceeds_number = dict_proceeds.pop(currency, ZERO) diff = abs(price_number - proceeds_number) if diff > tolerance: invalid = True break if invalid or dict_proceeds: errors.append( SellGainsError( entry.meta, \"Invalid price vs. proceeds/gains: {} vs. {}\".format( total_price, total_proceeds), entry)) return entries, errors beancount.plugins.split_expenses \uf0c1 Split expenses of a Beancount ledger between multiple people. This plugin is given a list of names. It assumes that any Expenses account whose components do not include any of the given names are to be split between the members. It goes through all the transactions and converts all such postings into multiple postings, one for each member. For example, given the names 'Martin' and 'Caroline', the following transaction: 2015-02-01 * \"Aqua Viva Tulum - two nights\" Income:Caroline:CreditCard -269.00 USD Expenses:Accommodation Will be converted to this: 2015-02-01 * \"Aqua Viva Tulum - two nights\" Income:Caroline:CreditCard -269.00 USD Expenses:Accommodation:Martin 134.50 USD Expenses:Accommodation:Caroline 134.50 USD After these transformations, all account names should include the name of a member. You can generate reports for a particular person by filtering postings to accounts with a component by their name. beancount.plugins.split_expenses.get_participants(filename, options_map) \uf0c1 Get the list of participants from the plugin configuration in the input file. Parameters: options_map \u2013 The options map, as produced by the parser. Returns: A list of strings, the names of participants as they should appear in the account names. Exceptions: KeyError \u2013 If the configuration does not contain configuration for the list Source code in beancount/plugins/split_expenses.py def get_participants(filename, options_map): \"\"\"Get the list of participants from the plugin configuration in the input file. Args: options_map: The options map, as produced by the parser. Returns: A list of strings, the names of participants as they should appear in the account names. Raises: KeyError: If the configuration does not contain configuration for the list of participants. \"\"\" plugin_options = dict(options_map[\"plugin\"]) try: return plugin_options[\"beancount.plugins.split_expenses\"].split() except KeyError: raise KeyError(\"Could not find the split_expenses plugin configuration.\") beancount.plugins.split_expenses.main() \uf0c1 Generate final reports for a shared expenses on a trip or project. For each of many participants, generate a detailed list of expenses, contributions, a categorized summary of expenses, and a final balance. Also produce a global list of final balances so that participants can reconcile between each other. Source code in beancount/plugins/split_expenses.py def main(): \"\"\"Generate final reports for a shared expenses on a trip or project. For each of many participants, generate a detailed list of expenses, contributions, a categorized summary of expenses, and a final balance. Also produce a global list of final balances so that participants can reconcile between each other. \"\"\" logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s') parser = version.ArgumentParser(description=__doc__.strip()) parser.add_argument('filename', help='Beancount input filename') parser.add_argument('-c', '--currency', action='store', help=\"Convert all the amounts to a single common currency\") oparser = parser.add_argument_group('Outputs') oparser.add_argument('-o', '--output-text', '--text', action='store', help=\"Render results to text boxes\") oparser.add_argument('--output-csv', '--csv', action='store', help=\"Render results to CSV files\") oparser.add_argument('--output-stdout', '--stdout', action='store_true', help=\"Render results to stdout\") args = parser.parse_args() # Ensure the directories exist. for directory in [args.output_text, args.output_csv]: if directory and not path.exists(directory): os.makedirs(directory, exist_ok=True) # Load the input file and get the list of participants. entries, errors, options_map = loader.load_file(args.filename) participants = get_participants(args.filename, options_map) for participant in participants: print(\"Participant: {}\".format(participant)) save_query(\"balances\", participant, entries, options_map, r\"\"\" SELECT PARENT(account) AS account, CONV[SUM(position)] AS amount WHERE account ~ ':\\b{}' GROUP BY 1 ORDER BY 2 DESC \"\"\", participant, boxed=False, args=args) save_query(\"expenses\", participant, entries, options_map, r\"\"\" SELECT date, flag, description, PARENT(account) AS account, JOINSTR(links) AS links, CONV[position] AS amount, CONV[balance] AS balance WHERE account ~ 'Expenses.*\\b{}' \"\"\", participant, args=args) save_query(\"income\", participant, entries, options_map, r\"\"\" SELECT date, flag, description, account, JOINSTR(links) AS links, CONV[position] AS amount, CONV[balance] AS balance WHERE account ~ 'Income.*\\b{}' \"\"\", participant, args=args) save_query(\"final\", None, entries, options_map, r\"\"\" SELECT GREP('\\b({})\\b', account) AS participant, CONV[SUM(position)] AS balance GROUP BY 1 ORDER BY 2 \"\"\", '|'.join(participants), args=args) # FIXME: Make this output to CSV files and upload to a spreadsheet. # FIXME: Add a fixed with option. This requires changing adding this to the # the renderer to be able to have elastic space and line splitting.. beancount.plugins.split_expenses.save_query(title, participant, entries, options_map, sql_query, *format_args, *, boxed=True, spaced=False, args=None) \uf0c1 Save the multiple files for this query. Parameters: title \u2013 A string, the title of this particular report to render. participant \u2013 A string, the name of the participant under consideration. entries \u2013 A list of directives (as per the loader). options_map \u2013 A dict of options (as per the loader). sql_query \u2013 A string with the SQL query, possibly with some placeholders left for *format_args to replace. *format_args \u2013 A tuple of arguments to be formatted into the SQL query string. This is provided as a convenience. boxed \u2013 A boolean, true if we should render the results in a fancy-looking ASCII box. spaced \u2013 If true, leave an empty line between each of the rows. This is useful if the results have a lot of rows that render over multiple lines. args \u2013 A dummy object with the following attributes: output_text: An optional directory name, to produce a text rendering of the report. output_csv: An optional directory name, to produce a CSV rendering of the report. output_stdout: A boolean, if true, also render the output to stdout. currency: An optional currency (a string). If you use this, you should wrap query targets to be converted with the pseudo-function \"CONV[...]\" and it will get replaced to CONVERT(..., CURRENCY) automatically. Source code in beancount/plugins/split_expenses.py def save_query(title, participant, entries, options_map, sql_query, *format_args, boxed=True, spaced=False, args=None): \"\"\"Save the multiple files for this query. Args: title: A string, the title of this particular report to render. participant: A string, the name of the participant under consideration. entries: A list of directives (as per the loader). options_map: A dict of options (as per the loader). sql_query: A string with the SQL query, possibly with some placeholders left for *format_args to replace. *format_args: A tuple of arguments to be formatted into the SQL query string. This is provided as a convenience. boxed: A boolean, true if we should render the results in a fancy-looking ASCII box. spaced: If true, leave an empty line between each of the rows. This is useful if the results have a lot of rows that render over multiple lines. args: A dummy object with the following attributes: output_text: An optional directory name, to produce a text rendering of the report. output_csv: An optional directory name, to produce a CSV rendering of the report. output_stdout: A boolean, if true, also render the output to stdout. currency: An optional currency (a string). If you use this, you should wrap query targets to be converted with the pseudo-function \"CONV[...]\" and it will get replaced to CONVERT(..., CURRENCY) automatically. \"\"\" # Replace CONV() to convert the currencies or not; if so, replace to # CONVERT(..., currency). replacement = (r'\\1' if args.currency is None else r'CONVERT(\\1, \"{}\")'.format(args.currency)) sql_query = re.sub(r'CONV\\[(.*?)\\]', replacement, sql_query) # Run the query. rtypes, rrows = query.run_query(entries, options_map, sql_query, *format_args, numberify=True) # The base of all filenames. filebase = title.replace(' ', '_') fmtopts = dict(boxed=boxed, spaced=spaced) # Output the text files. if args.output_text: basedir = (path.join(args.output_text, participant) if participant else args.output_text) os.makedirs(basedir, exist_ok=True) filename = path.join(basedir, filebase + '.txt') with open(filename, 'w') as file: query_render.render_text(rtypes, rrows, options_map['dcontext'], file, **fmtopts) # Output the CSV files. if args.output_csv: basedir = (path.join(args.output_csv, participant) if participant else args.output_csv) os.makedirs(basedir, exist_ok=True) filename = path.join(basedir, filebase + '.csv') with open(filename, 'w') as file: query_render.render_csv(rtypes, rrows, options_map['dcontext'], file, expand=False) if args.output_stdout: # Write out the query to stdout. query_render.render_text(rtypes, rrows, options_map['dcontext'], sys.stdout, **fmtopts) beancount.plugins.split_expenses.split_expenses(entries, options_map, config) \uf0c1 Split postings according to expenses (see module docstring for details). Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. config \u2013 The plugin configuration string. Returns: A list of entries, with potentially more accounts and potentially more postings with smaller amounts. Source code in beancount/plugins/split_expenses.py def split_expenses(entries, options_map, config): \"\"\"Split postings according to expenses (see module docstring for details). Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. config: The plugin configuration string. Returns: A list of entries, with potentially more accounts and potentially more postings with smaller amounts. \"\"\" # Validate and sanitize configuration. if isinstance(config, str): members = config.split() elif isinstance(config, (tuple, list)): members = config else: raise RuntimeError(\"Invalid plugin configuration: configuration for split_expenses \" \"should be a string or a sequence.\") acctypes = options.get_account_types(options_map) def is_expense_account(account): return account_types.get_account_type(account) == acctypes.expenses # A predicate to quickly identify if an account contains the name of a # member. is_individual_account = re.compile('|'.join(map(re.escape, members))).search # Existing and previously unseen accounts. new_accounts = set() # Filter the entries and transform transactions. new_entries = [] for entry in entries: if isinstance(entry, data.Transaction): new_postings = [] for posting in entry.postings: if (is_expense_account(posting.account) and not is_individual_account(posting.account)): # Split this posting into multiple postings. split_units = amount.Amount(posting.units.number / len(members), posting.units.currency) for member in members: # Mark the account as new if never seen before. subaccount = account.join(posting.account, member) new_accounts.add(subaccount) # Ensure the modified postings are marked as # automatically calculated, so that the resulting # calculated amounts aren't used to affect inferred # tolerances. meta = posting.meta.copy() if posting.meta else {} meta[interpolate.AUTOMATIC_META] = True # Add a new posting for each member, to a new account # with the name of this member. new_postings.append( posting._replace(meta=meta, account=subaccount, units=split_units, cost=posting.cost)) else: new_postings.append(posting) # Modify the entry in-place, replace its postings. entry = entry._replace(postings=new_postings) new_entries.append(entry) # Create Open directives for new subaccounts if necessary. oc_map = getters.get_account_open_close(entries) open_date = entries[0].date meta = data.new_metadata('', 0) open_entries = [] for new_account in new_accounts: if new_account not in oc_map: entry = data.Open(meta, open_date, new_account, None, None) open_entries.append(entry) return open_entries + new_entries, [] beancount.plugins.tag_pending \uf0c1 An example of tracking unpaid payables or receivables. A user with lots of invoices to track may want to produce a report of pending or incomplete payables or receivables. Beancount does not by default offer such a dedicated feature, but it is easy to build one by using existing link attributes on transactions. This is an example on how to implement that with a plugin. For example, assuming the user enters linked transactions like this: 2013-03-28 * \"Bill for datacenter electricity\" ^invoice-27a30ab61191 Expenses:Electricity 450.82 USD Liabilities:AccountsPayable 2013-04-15 * \"Paying electricity company\" ^invoice-27a30ab61191 Assets:Checking -450.82 USD Liabilities:AccountsPayable Transactions are grouped by link (\"invoice-27a30ab61191\") and then the intersection of their common accounts is automatically calculated (\"Liabilities:AccountsPayable\"). We then add up the balance of all the postings for this account in this link group and check if the sum is zero. If there is a residual amount in this balance, we mark the associated entries as incomplete by inserting a #PENDING tag on them. The user can then use that tag to navigate to the corresponding view in the web interface, or just find the entries and produce a listing of them. beancount.plugins.tag_pending.tag_pending_plugin(entries, options_map) \uf0c1 A plugin that finds and tags pending transactions. Parameters: entries \u2013 A list of entry instances. options_map \u2013 A dict of options parsed from the file. Returns: A tuple of entries and errors. Source code in beancount/plugins/tag_pending.py def tag_pending_plugin(entries, options_map): \"\"\"A plugin that finds and tags pending transactions. Args: entries: A list of entry instances. options_map: A dict of options parsed from the file. Returns: A tuple of entries and errors. \"\"\" return (tag_pending_transactions(entries, 'PENDING'), []) beancount.plugins.tag_pending.tag_pending_transactions(entries, tag_name='PENDING') \uf0c1 Filter out incomplete linked transactions to a transfer account. Given a list of entries, group the entries by their link and compute the balance of the intersection of their common accounts. If the balance does not sum to zero, insert a 'tag_name' tag in the entries. Parameters: entries \u2013 A list of directives/transactions to process. tag_name \u2013 A string, the name of the tag to be inserted if a linked group of entries is found not to match Returns: A modified set of entries, possibly tagged as pending. Source code in beancount/plugins/tag_pending.py def tag_pending_transactions(entries, tag_name='PENDING'): \"\"\"Filter out incomplete linked transactions to a transfer account. Given a list of entries, group the entries by their link and compute the balance of the intersection of their common accounts. If the balance does not sum to zero, insert a 'tag_name' tag in the entries. Args: entries: A list of directives/transactions to process. tag_name: A string, the name of the tag to be inserted if a linked group of entries is found not to match Returns: A modified set of entries, possibly tagged as pending. \"\"\" link_groups = basicops.group_entries_by_link(entries) pending_entry_ids = set() for link, link_entries in link_groups.items(): assert link_entries if len(link_entries) == 1: # If a single entry is present, it is assumed incomplete. pending_entry_ids.add(id(link_entries[0])) else: # Compute the sum total balance of the common accounts. common_accounts = basicops.get_common_accounts(link_entries) common_balance = inventory.Inventory() for entry in link_entries: for posting in entry.postings: if posting.account in common_accounts: common_balance.add_position(posting) # Mark entries as pending if a residual balance is found. if not common_balance.is_empty(): for entry in link_entries: pending_entry_ids.add(id(entry)) # Insert tags if marked. return [(entry._replace(tags=(entry.tags or set()) | set((tag_name,))) if id(entry) in pending_entry_ids else entry) for entry in entries] beancount.plugins.unique_prices \uf0c1 This module adds validation that there is a single price defined per date and base/quote currencies. If multiple conflicting price values are declared, an error is generated. Note that multiple price entries with the same number do not generate an error. This is meant to be turned on if you want to use a very strict mode for entering prices, and may not be realistic usage. For example, if you have (1) a transaction with an implicitly generated price during the day (from its cost) and (2) a separate explicit price directive that declares a different price for the day's closing price, this would generate an error. I'm not certain this will be useful in the long run, so placing it in a plugin. beancount.plugins.unique_prices.UniquePricesError ( tuple ) \uf0c1 UniquePricesError(source, message, entry) beancount.plugins.unique_prices.UniquePricesError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/unique_prices.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.unique_prices.UniquePricesError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of UniquePricesError(source, message, entry) beancount.plugins.unique_prices.UniquePricesError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/unique_prices.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.unique_prices.validate_unique_prices(entries, unused_options_map) \uf0c1 Check that there is only a single price per day for a particular base/quote. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. Returns: The list of input entries, and a list of new UniquePricesError instances generated. Source code in beancount/plugins/unique_prices.py def validate_unique_prices(entries, unused_options_map): \"\"\"Check that there is only a single price per day for a particular base/quote. Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. Returns: The list of input entries, and a list of new UniquePricesError instances generated. \"\"\" new_entries = [] errors = [] prices = collections.defaultdict(list) for entry in entries: if not isinstance(entry, data.Price): continue key = (entry.date, entry.currency, entry.amount.currency) prices[key].append(entry) errors = [] for price_entries in prices.values(): if len(price_entries) > 1: number_map = {price_entry.amount.number: price_entry for price_entry in price_entries} if len(number_map) > 1: # Note: This should be a list of entries for better error # reporting. (Later.) error_entry = next(iter(number_map.values())) errors.append( UniquePricesError(error_entry.meta, \"Disagreeing price entries\", price_entries)) return new_entries, errors beancount.plugins.unrealized \uf0c1 Compute unrealized gains. The configuration for this plugin is a single string, the name of the subaccount to add to post the unrealized gains to, like this: plugin \"beancount.plugins.unrealized\" \"Unrealized\" If you don't specify a name for the subaccount (the configuration value is optional), by default it inserts the unrealized gains in the same account that is being adjusted. beancount.plugins.unrealized.UnrealizedError ( tuple ) \uf0c1 UnrealizedError(source, message, entry) beancount.plugins.unrealized.UnrealizedError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/unrealized.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.plugins.unrealized.UnrealizedError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of UnrealizedError(source, message, entry) beancount.plugins.unrealized.UnrealizedError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/unrealized.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.plugins.unrealized.add_unrealized_gains(entries, options_map, subaccount=None) \uf0c1 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. Parameters: entries \u2013 A list of data directives. options_map \u2013 A dict of options, that confirms to beancount.parser.options. subaccount \u2013 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. Source code in beancount/plugins/unrealized.py 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('', 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) beancount.plugins.unrealized.get_unrealized_entries(entries) \uf0c1 Return entries automatically created for unrealized gains. Parameters: entries \u2013 A list of directives. Returns: A list of directives, all of which are in the original list. Source code in beancount/plugins/unrealized.py def get_unrealized_entries(entries): \"\"\"Return entries automatically created for unrealized gains. Args: entries: A list of directives. Returns: A list of directives, all of which are in the original list. \"\"\" return [entry for entry in entries if (isinstance(entry, data.Transaction) and entry.flag == flags.FLAG_UNREALIZED)]","title":"beancount.plugins"},{"location":"api_reference/beancount.plugins.html#beancountplugins","text":"Example plugins for filtering transactions. These are various examples of how to filter entries in various creative ways. IMPORTANT: These are not meant to be complete features, rather just experiments in problem-solving using Beancount, work-in-progress that can be selectively installed via a --plugin option, or one-offs to answer questions on the mailing-list.","title":"beancount.plugins"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.auto","text":"A plugin of plugins which triggers are all the automatic and lax plugins. In a sense, this is the inverse of \"pedantic.\" This is useful when doing some types of quick and dirty tests. You can just import the \"auto\" plugin. Put that in a macro. Also see: the 'pedantic' plugin.","title":"auto"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.auto_accounts","text":"This module automatically inserts Open directives for accounts not opened (at the date of the first entry) and automatically removes open directives for unused accounts. This can be used as a convenience for doing demos, or when setting up your initial transactions, as an intermediate step.","title":"auto_accounts"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.auto_accounts.auto_insert_open","text":"Insert implicitly defined prices from Transactions. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. Source code in beancount/plugins/auto_accounts.py def auto_insert_open(entries, unused_options_map): \"\"\"Insert implicitly defined prices from Transactions. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. \"\"\" opened_accounts = {entry.account for entry in entries if isinstance(entry, data.Open)} new_entries = [] accounts_first, _ = getters.get_accounts_use_map(entries) for index, (account, date_first_used) in enumerate(sorted(accounts_first.items())): if account not in opened_accounts: meta = data.new_metadata('', index) new_entries.append(data.Open(meta, date_first_used, account, None, None)) if new_entries: new_entries.extend(entries) new_entries.sort(key=data.entry_sortkey) else: new_entries = entries return new_entries, []","title":"auto_insert_open()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions","text":"A plugin that automatically converts postings at price to postings held at cost, applying an automatic booking algorithm in assigning the cost bases and matching lots. This plugin restricts itself to applying these transformations within a particular account, which you provide. For each of those accounts, it also requires a corresponding Income account to book the profit/loss of reducing lots (i.e., sales): plugin \"beancount.plugins.book_conversions\" \"Assets:Bitcoin,Income:Bitcoin\" Then, simply input the transactions with price conversion. We use \"Bitcoins\" in this example, converting Bitcoin purchases that were carried out as currency into maintaining these with cost basis, for tax reporting purposes: 2015-09-04 * \"Buy some bitcoins\" Assets:Bank -1000.00 USD Assets:Bitcoin 4.333507 BTC @ 230.76 USD 2015-09-05 * \"Buy some more bitcoins at a different price\" Assets:Bank -1000.00 USD Assets:Bitcoin 4.345747 BTC @ 230.11 USD 2015-09-20 * \"Use (sell) some bitcoins\" Assets:Bitcoin -6.000000 BTC @ 230.50 USD Expenses:Something The result is that cost bases are inserted on augmenting lots: 2015-09-04 * \"Buy some bitcoins\" Assets:Bitcoin 4.333507 BTC {230.76 USD} @ 230.76 USD Assets:Bank -1000.00 USD 2015-09-05 * \"Buy some more bitcoins at a different price\" Assets:Bitcoin 4.345747 BTC {230.11 USD} @ 230.11 USD Assets:Bank -1000.00 USD While on reducing lots, matching FIFO lots are automatically found and the corresponding cost basis added: 2015-09-20 * \"Use (sell) some bitcoins\" Assets:Bitcoin -4.333507 BTC {230.76 USD} @ 230.50 USD Assets:Bitcoin -1.666493 BTC {230.11 USD} @ 230.50 USD Income:Bitcoin 0.47677955 USD Expenses:Something 1383.00000000 USD Note that multiple lots were required to fulfill the sale quantity here. As in this example, this may result in multiple lots being created for a single one. Finally, Beancount will eventually support booking methods built-in, but this is a quick method that shows how to hack your own booking method via transformations of the postings that run in a plugin. Implementation notes: This code uses the FIFO method only for now. However, it would be very easy to customize it to provide other booking methods, e.g. LIFO, or otherwise. This will be added eventually, and I'm hoping to reuse the same inventory abstractions that will be used to implement the fallback booking methods from the booking proposal review (http://furius.ca/beancount/doc/proposal-booking). Instead of keeping a list of (Position, Transaction) pairs for the pending FIFO lots, we really ought to use a beancount.core.inventory.Inventory instance. However, the class does not contain sufficient data to carry out FIFO booking at the moment. A newer implementation, living in the \"booking\" branch, does, and will be used in the future. This code assumes that a positive number of units is an augmenting lot and a reducing one has a negative number of units, though we never call them that way on purpose (to eventually allow this code to handle short positions). This is not strictly true; however, we would need an Inventory in order to figrue this out. This will be done in the future and is not difficult to do. IMPORTANT: This plugin was developed before the booking methods (FIFO, LIFO, and others) were fully implemented in Beancount. It was built to answer a question on the mailing-list about FIFO booking. You probably don't need to use them anymore. Always prefer to use the native syntax instead of this.","title":"book_conversions"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.BookConversionError","text":"BookConversionError(source, message, entry)","title":"BookConversionError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.BookConversionError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/book_conversions.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.BookConversionError.__new__","text":"Create new instance of BookConversionError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.BookConversionError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/book_conversions.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.ConfigError","text":"ConfigError(source, message, entry)","title":"ConfigError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.ConfigError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/book_conversions.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.ConfigError.__new__","text":"Create new instance of ConfigError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.ConfigError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/book_conversions.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.augment_inventory","text":"Add the lots from the given posting to the running inventory. Parameters: pending_lots \u2013 A list of pending ([number], Posting, Transaction) to be matched. The number is modified in-place, destructively. posting \u2013 The posting whose position is to be added. entry \u2013 The parent transaction. eindex \u2013 The index of the parent transaction housing this posting. Returns: A new posting with cost basis inserted to be added to a transformed transaction. Source code in beancount/plugins/book_conversions.py def augment_inventory(pending_lots, posting, entry, eindex): \"\"\"Add the lots from the given posting to the running inventory. Args: pending_lots: A list of pending ([number], Posting, Transaction) to be matched. The number is modified in-place, destructively. posting: The posting whose position is to be added. entry: The parent transaction. eindex: The index of the parent transaction housing this posting. Returns: A new posting with cost basis inserted to be added to a transformed transaction. \"\"\" number = posting.units.number new_posting = posting._replace( units=copy.copy(posting.units), cost=position.Cost(posting.price.number, posting.price.currency, entry.date, None)) pending_lots.append(([number], new_posting, eindex)) return new_posting","title":"augment_inventory()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.book_price_conversions","text":"Rewrite transactions to insert cost basis according to a booking method. See module docstring for full details. Parameters: entries \u2013 A list of entry instances. assets_account \u2013 An account string, the name of the account to process. income_account \u2013 An account string, the name of the account to use for booking realized profit/loss. Returns: A tuple of entries \u2013 A list of new, modified entries. errors: A list of errors generated by this plugin. matches: A list of (number, augmenting-posting, reducing-postings) for all matched lots. Source code in beancount/plugins/book_conversions.py def book_price_conversions(entries, assets_account, income_account): \"\"\"Rewrite transactions to insert cost basis according to a booking method. See module docstring for full details. Args: entries: A list of entry instances. assets_account: An account string, the name of the account to process. income_account: An account string, the name of the account to use for booking realized profit/loss. Returns: A tuple of entries: A list of new, modified entries. errors: A list of errors generated by this plugin. matches: A list of (number, augmenting-posting, reducing-postings) for all matched lots. \"\"\" # Pairs of (Position, Transaction) instances used to match augmenting # entries with reducing ones. pending_lots = [] # A list of pairs of matching (augmenting, reducing) postings. all_matches = [] new_entries = [] errors = [] for eindex, entry in enumerate(entries): # Figure out if this transaction has postings in Bitcoins without a cost. # The purpose of this plugin is to fixup those. if isinstance(entry, data.Transaction) and any(is_matching(posting, assets_account) for posting in entry.postings): # Segregate the reducing lots, augmenting lots and other lots. augmenting, reducing, other = [], [], [] for pindex, posting in enumerate(entry.postings): if is_matching(posting, assets_account): out = augmenting if posting.units.number >= ZERO else reducing else: out = other out.append(posting) # We will create a replacement list of postings with costs filled # in, possibly more than the original list, to account for the # different lots. new_postings = [] # Convert all the augmenting postings to cost basis. for posting in augmenting: new_postings.append(augment_inventory(pending_lots, posting, entry, eindex)) # Then process reducing postings. if reducing: # Process all the reducing postings, booking them to matching lots. pnl = inventory.Inventory() for posting in reducing: rpostings, matches, posting_pnl, new_errors = ( reduce_inventory(pending_lots, posting, eindex)) new_postings.extend(rpostings) all_matches.extend(matches) errors.extend(new_errors) pnl.add_amount(amount.Amount(posting_pnl, posting.price.currency)) # If some reducing lots were seen in this transaction, insert an # Income leg to absorb the P/L. We need to do this for each currency # which incurred P/L. if not pnl.is_empty(): for pos in pnl: meta = data.new_metadata('', 0) new_postings.append( data.Posting(income_account, -pos.units, None, None, None, meta)) # Third, add back all the other unrelated legs in. for posting in other: new_postings.append(posting) # Create a replacement entry. entry = entry._replace(postings=new_postings) new_entries.append(entry) # Add matching metadata to all matching postings. mod_matches = link_entries_with_metadata(new_entries, all_matches) # Resolve the indexes to their possibly modified Transaction instances. matches = [(data.TxnPosting(new_entries[aug_index], aug_posting), data.TxnPosting(new_entries[red_index], red_posting)) for (aug_index, aug_posting), (red_index, red_posting) in mod_matches] return new_entries, errors, matches","title":"book_price_conversions()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.book_price_conversions_plugin","text":"Plugin that rewrites transactions to insert cost basis according to a booking method. See book_price_conversions() for details. Parameters: entries \u2013 A list of entry instances. options_map \u2013 A dict of options parsed from the file. config \u2013 A string, in \",\" format. Returns: A tuple of entries \u2013 A list of new, modified entries. errors: A list of errors generated by this plugin. Source code in beancount/plugins/book_conversions.py 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 \",\" 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","title":"book_price_conversions_plugin()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.extract_trades","text":"Find all the matching trades from the metadata attached to postings. This inspects all the postings and pairs them up using the special metadata field that was added by this plugin when booking matching lots, and returns pairs of those postings. Parameters: entries \u2013 The list of directives to extract from. Returns: A list of (number, augmenting-posting, reducing-posting). Source code in beancount/plugins/book_conversions.py def extract_trades(entries): \"\"\"Find all the matching trades from the metadata attached to postings. This inspects all the postings and pairs them up using the special metadata field that was added by this plugin when booking matching lots, and returns pairs of those postings. Args: entries: The list of directives to extract from. Returns: A list of (number, augmenting-posting, reducing-posting). \"\"\" trade_map = collections.defaultdict(list) for index, entry in enumerate(entries): if not isinstance(entry, data.Transaction): continue for posting in entry.postings: links_str = posting.meta.get(META, None) if links_str: links = links_str.split(',') for link in links: trade_map[link].append((index, entry, posting)) # Sort matches according to the index of the first entry, drop the index # used for doing this, and convert the objects to tuples.. trades = [(data.TxnPosting(augmenting[1], augmenting[2]), data.TxnPosting(reducing[1], reducing[2])) for augmenting, reducing in sorted(trade_map.values())] # Sanity check. for matches in trades: assert len(matches) == 2 return trades","title":"extract_trades()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.is_matching","text":"\"Identify if the given posting is one to be booked. Parameters: posting \u2013 An instance of a Posting. account \u2013 The account name configured. Returns: A boolean, true if this posting is one that we should be adding a cost to. Source code in beancount/plugins/book_conversions.py def is_matching(posting, account): \"\"\"\"Identify if the given posting is one to be booked. Args: posting: An instance of a Posting. account: The account name configured. Returns: A boolean, true if this posting is one that we should be adding a cost to. \"\"\" return (posting.account == account and posting.cost is None and posting.price is not None)","title":"is_matching()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.link_entries_with_metadata","text":"Modify the entries in-place to add matching links to postings. Parameters: entries \u2013 The list of entries to modify. all_matches \u2013 A list of pairs of (augmenting-posting, reducing-posting). Returns: A list of pairs of (index, Posting) for the new (augmenting, reducing) annotated postings. Source code in beancount/plugins/book_conversions.py def link_entries_with_metadata(entries, all_matches): \"\"\"Modify the entries in-place to add matching links to postings. Args: entries: The list of entries to modify. all_matches: A list of pairs of (augmenting-posting, reducing-posting). Returns: A list of pairs of (index, Posting) for the new (augmenting, reducing) annotated postings. \"\"\" # Allocate trade names and compute a map of posting to trade names. link_map = collections.defaultdict(list) for (aug_index, aug_posting), (red_index, red_posting) in all_matches: link = 'trade-{}'.format(str(uuid.uuid4()).split('-')[-1]) link_map[id(aug_posting)].append(link) link_map[id(red_posting)].append(link) # Modify the postings. postings_repl_map = {} for entry in entries: if isinstance(entry, data.Transaction): for index, posting in enumerate(entry.postings): links = link_map.pop(id(posting), None) if links: new_posting = posting._replace(meta=posting.meta.copy()) new_posting.meta[META] = ','.join(links) entry.postings[index] = new_posting postings_repl_map[id(posting)] = new_posting # Just a sanity check. assert not link_map, \"Internal error: not all matches found.\" # Return a list of the modified postings (mapping the old matches to the # newly created ones). return [((aug_index, postings_repl_map[id(aug_posting)]), (red_index, postings_repl_map[id(red_posting)])) for (aug_index, aug_posting), (red_index, red_posting) in all_matches]","title":"link_entries_with_metadata()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.main","text":"Extract trades from metadata-annotated postings and report on them. Source code in beancount/plugins/book_conversions.py def main(): \"\"\"Extract trades from metadata-annotated postings and report on them. \"\"\" logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s') parser = version.ArgumentParser(description=__doc__.strip()) parser.add_argument('filename', help='Beancount input filename') oparser = parser.add_argument_group('Outputs') oparser.add_argument('-o', '--output', action='store', help=\"Filename to output results to (default goes to stdout)\") oparser.add_argument('-f', '--format', default='text', choices=['text', 'csv'], help=\"Output format to render to (text, csv)\") args = parser.parse_args() # Load the input file. entries, errors, options_map = loader.load_file(args.filename) # Get the list of trades. trades = extract_trades(entries) # Produce a table of all the trades. columns = ('units currency cost_currency ' 'buy_date buy_price sell_date sell_price pnl').split() header = ['Units', 'Currency', 'Cost Currency', 'Buy Date', 'Buy Price', 'Sell Date', 'Sell Price', 'P/L'] body = [] for aug, red in trades: units = -red.posting.units.number buy_price = aug.posting.price.number sell_price = red.posting.price.number pnl = (units * (sell_price - buy_price)).quantize(buy_price) body.append([ -red.posting.units.number, red.posting.units.currency, red.posting.price.currency, aug.txn.date.isoformat(), buy_price, red.txn.date.isoformat(), sell_price, pnl ]) trades_table = table.Table(columns, header, body) # Render the table as text or CSV. outfile = open(args.output, 'w') if args.output else sys.stdout table.render_table(trades_table, outfile, args.format)","title":"main()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.book_conversions.reduce_inventory","text":"Match a reducing posting against a list of lots (using FIFO order). Parameters: pending_lots \u2013 A list of pending ([number], Posting, Transaction) to be matched. The number is modified in-place, destructively. posting \u2013 The posting whose position is to be added. eindex \u2013 The index of the parent transaction housing this posting. Returns: A tuple of postings \u2013 A list of new Posting instances corresponding to the given posting, that were booked to the current list of lots. matches: A list of pairs of (augmenting-posting, reducing-posting). pnl: A Decimal, the P/L incurred in reducing these lots. errors: A list of new errors generated in reducing these lots. Source code in beancount/plugins/book_conversions.py def reduce_inventory(pending_lots, posting, eindex): \"\"\"Match a reducing posting against a list of lots (using FIFO order). Args: pending_lots: A list of pending ([number], Posting, Transaction) to be matched. The number is modified in-place, destructively. posting: The posting whose position is to be added. eindex: The index of the parent transaction housing this posting. Returns: A tuple of postings: A list of new Posting instances corresponding to the given posting, that were booked to the current list of lots. matches: A list of pairs of (augmenting-posting, reducing-posting). pnl: A Decimal, the P/L incurred in reducing these lots. errors: A list of new errors generated in reducing these lots. \"\"\" new_postings = [] matches = [] pnl = ZERO errors = [] match_number = -posting.units.number match_currency = posting.units.currency cost_currency = posting.price.currency while match_number != ZERO: # Find the first lot with matching currency. for fnumber, fposting, findex in pending_lots: funits = fposting.units fcost = fposting.cost if (funits.currency == match_currency and fcost and fcost.currency == cost_currency): assert fnumber[0] > ZERO, \"Internal error, zero lot\" break else: errors.append( BookConversionError(posting.meta, \"Could not match position {}\".format(posting), None)) break # Reduce the pending lots. number = min(match_number, fnumber[0]) cost = fcost match_number -= number fnumber[0] -= number if fnumber[0] == ZERO: pending_lots.pop(0) # Add a corresponding posting. rposting = posting._replace( units=amount.Amount(-number, posting.units.currency), cost=copy.copy(cost)) new_postings.append(rposting) # Update the P/L. pnl += number * (posting.price.number - cost.number) # Add to the list of matches. matches.append(((findex, fposting), (eindex, rposting))) return new_postings, matches, pnl, errors","title":"reduce_inventory()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_average_cost","text":"A plugin that ensures cost basis is preserved in unbooked transactions. This is intended to be used in accounts using the \"NONE\" booking method, to manually ensure that the sum total of the cost basis of reducing legs matches the average of what's in the account inventory. This is a partial first step toward implementing the \"AVERAGE\" booking method. In other words, this plugins provides assertions that will constrain you to approximate what the \"AVERAGE\" booking method will do, manually, and not to leak too much cost basis through unmatching bookings without checks. (Note the contrived context here: Ideally the \"NONE\" booking method would simply not exist.)","title":"check_average_cost"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_average_cost.MatchBasisError","text":"MatchBasisError(source, message, entry)","title":"MatchBasisError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_average_cost.MatchBasisError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/check_average_cost.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_average_cost.MatchBasisError.__new__","text":"Create new instance of MatchBasisError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_average_cost.MatchBasisError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/check_average_cost.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_average_cost.validate_average_cost","text":"Check that reducing legs on unbooked postings are near the average cost basis. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config_str \u2013 The configuration as a string version of a float. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_average_cost.py def validate_average_cost(entries, options_map, config_str=None): \"\"\"Check that reducing legs on unbooked postings are near the average cost basis. Args: entries: A list of directives. unused_options_map: An options map. config_str: The configuration as a string version of a float. Returns: A list of new errors, if any were found. \"\"\" # Initialize tolerance bounds. if config_str and config_str.strip(): # pylint: disable=eval-used config_obj = eval(config_str, {}, {}) if not isinstance(config_obj, float): raise RuntimeError(\"Invalid configuration for check_average_cost: \" \"must be a float\") tolerance = config_obj else: tolerance = DEFAULT_TOLERANCE min_tolerance = D(1 - tolerance) max_tolerance = D(1 + tolerance) errors = [] ocmap = getters.get_account_open_close(entries) balances = collections.defaultdict(inventory.Inventory) for entry in entries: if isinstance(entry, Transaction): for posting in entry.postings: dopen = ocmap.get(posting.account, None) # Only process accounts with a NONE booking value. if dopen and dopen[0] and dopen[0].booking == Booking.NONE: balance = balances[(posting.account, posting.units.currency, posting.cost.currency if posting.cost else None)] if posting.units.number < ZERO: average = balance.average().get_only_position() if average is not None: number = average.cost.number min_valid = number * min_tolerance max_valid = number * max_tolerance if not (min_valid <= posting.cost.number <= max_valid): errors.append( MatchBasisError( entry.meta, (\"Cost basis on reducing posting is too far from \" \"the average cost ({} vs. {})\".format( posting.cost.number, average.cost.number)), entry)) balance.add_position(posting) return entries, errors","title":"validate_average_cost()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_closing","text":"A plugin that automatically inserts a balance check on a tagged closing posting. Some postings are known to the user to be \"closing trades\", which means that the resulting position of the instrument just after the trade should be zero. For instance, this is the case for most ordinary options trading, only one lot of a particular instrument is held, and eventually expires or gets sold off in its entirely. One would like to confirm that, and the way to do this in Beancount is to insert a balance check. This plugin allows you to do that more simply, by inserting metadata. For example, this transaction: 2018-02-16 * \"SOLD -14 QQQ 100 16 FEB 18 160 CALL @5.31\" Assets:US:Brokerage:Main:Options -1400 QQQ180216C160 {2.70 USD} @ 5.31 USD closing: TRUE Expenses:Financial:Commissions 17.45 USD Expenses:Financial:Fees 0.42 USD Assets:US:Brokerage:Main:Cash 7416.13 USD Income:US:Brokerage:Main:PnL Would expand into the following two directives: 2018-02-16 * \"SOLD -14 QQQ 100 16 FEB 18 160 CALL @5.31\" Assets:US:Brokerage:Main:Options -1400 QQQ180216C160 {2.70 USD} @ 5.31 USD Expenses:Financial:Commissions 17.45 USD Expenses:Financial:Fees 0.42 USD Assets:US:Brokerage:Main:Cash 7416.13 USD Income:US:Brokerage:Main:PnL 2018-02-17 balance Assets:US:Brokerage:Main:Options 0 QQQ180216C160 Insert the closing line when you know you're closing the position.","title":"check_closing"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_closing.check_closing","text":"Expand 'closing' metadata to a zero balance check. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_closing.py def check_closing(entries, options_map): \"\"\"Expand 'closing' metadata to a zero balance check. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" new_entries = [] for entry in entries: if isinstance(entry, data.Transaction): for posting in entry.postings: if posting.meta and posting.meta.get('closing', False): # Remove the metadata. meta = posting.meta.copy() del meta['closing'] entry = entry._replace(meta=meta) # Insert a balance. date = entry.date + datetime.timedelta(days=1) balance = data.Balance(data.new_metadata(\"\", 0), date, posting.account, amount.Amount(ZERO, posting.units.currency), None, None) new_entries.append(balance) new_entries.append(entry) return new_entries, []","title":"check_closing()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity","text":"A plugin that verifies that all seen commodities have a Commodity directive. This is useful if you're a bit pedantic and like to make sure that you're declared attributes for each of the commodities you use. It's useful if you use the portfolio export, for example.","title":"check_commodity"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.CheckCommodityError","text":"CheckCommodityError(source, message, entry)","title":"CheckCommodityError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.CheckCommodityError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/check_commodity.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.CheckCommodityError.__new__","text":"Create new instance of CheckCommodityError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.CheckCommodityError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/check_commodity.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.validate_commodity_directives","text":"Find all commodities used and ensure they have a corresponding Commodity directive. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_commodity.py def validate_commodity_directives(entries, options_map): \"\"\"Find all commodities used and ensure they have a corresponding Commodity directive. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" commodities_used = options_map['commodities'] errors = [] meta = data.new_metadata('', 0) commodity_map = getters.get_commodity_map(entries, create_missing=False) for currency in commodities_used: commodity_entry = commodity_map.get(currency, None) if commodity_entry is None: errors.append( CheckCommodityError( meta, \"Missing Commodity directive for '{}'\".format(currency), None)) return entries, errors","title":"validate_commodity_directives()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.coherent_cost","text":"This plugin validates that currencies held at cost aren't ever converted at price and vice-versa. This is usually the case, and using it will prevent users from making the mistake of selling a lot without specifying it via its cost basis.","title":"coherent_cost"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.coherent_cost.CoherentCostError","text":"CoherentCostError(source, message, entry)","title":"CoherentCostError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.coherent_cost.CoherentCostError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/coherent_cost.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.coherent_cost.CoherentCostError.__new__","text":"Create new instance of CoherentCostError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.coherent_cost.CoherentCostError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/coherent_cost.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.coherent_cost.validate_coherent_cost","text":"Check that all currencies are either used at cost or not at all, but never both. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/coherent_cost.py def validate_coherent_cost(entries, unused_options_map): \"\"\"Check that all currencies are either used at cost or not at all, but never both. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] with_cost = {} without_cost = {} for entry in data.filter_txns(entries): for posting in entry.postings: target_set = without_cost if posting.cost is None else with_cost currency = posting.units.currency target_set.setdefault(currency, entry) for currency in set(with_cost) & set(without_cost): errors.append( CoherentCostError( without_cost[currency].meta, \"Currency '{}' is used both with and without cost\".format(currency), with_cost[currency])) # Note: We really ought to include both of the first transactions here. return entries, errors","title":"validate_coherent_cost()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr","text":"A plugin that asserts that all Commodity directives have a particular attribute and that it is part of a set of enum values. The configuration must be a mapping of attribute name to list of valid values, like this: plugin \"beancount.plugins.commodity_attr\" \"{ 'sector': ['Technology', 'Financials', 'Energy'], 'name': None, }\" The plugin issues an error if a Commodity directive is missing the attribute, or if the attribute value is not in the valid set. If you'd like to just ensure the attribute is set, set the list of valid values to None, as in the 'name' attribute in the example above.","title":"commodity_attr"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.CommodityError","text":"CommodityError(source, message, entry)","title":"CommodityError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.CommodityError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/commodity_attr.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.CommodityError.__new__","text":"Create new instance of CommodityError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.CommodityError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/commodity_attr.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.ConfigError","text":"ConfigError(source, message, entry)","title":"ConfigError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.ConfigError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/commodity_attr.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.ConfigError.__new__","text":"Create new instance of ConfigError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.ConfigError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/commodity_attr.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.validate_commodity_attr","text":"Check that all Commodity directives have a valid attribute. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config_str \u2013 A configuration string. Returns: A list of new errors, if any were found. Source code in beancount/plugins/commodity_attr.py def validate_commodity_attr(entries, unused_options_map, config_str): \"\"\"Check that all Commodity directives have a valid attribute. Args: entries: A list of directives. unused_options_map: An options map. config_str: A configuration string. Returns: A list of new errors, if any were found. \"\"\" errors = [] # pylint: disable=eval-used config_obj = eval(config_str, {}, {}) if not isinstance(config_obj, dict): errors.append(ConfigError( data.new_metadata('', 0), \"Invalid configuration for commodity_attr plugin; skipping.\", None)) return entries, errors validmap = {attr: frozenset(values) if values is not None else None for attr, values in config_obj.items()} for entry in entries: if not isinstance(entry, data.Commodity): continue for attr, values in validmap.items(): value = entry.meta.get(attr, None) if value is None: errors.append(CommodityError( entry.meta, \"Missing attribute '{}' for Commodity directive {}\".format( attr, entry.currency), None)) continue if values and value not in values: errors.append(CommodityError( entry.meta, \"Invalid attribute '{}' for Commodity\".format(value) + \" directive {}; valid options: {}\".format( entry.currency, ', '.join(values)), None)) return entries, errors","title":"validate_commodity_attr()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.currency_accounts","text":"An implementation of currency accounts. This is an automatic implementation of the method described here: https://www.mathstat.dal.ca/~selinger/accounting/tutorial.html You enable it just like this: plugin \"beancount.plugins.currency_accounts\" \"Equity:CurrencyAccounts\" Accounts will be automatically created under the given base account, with the currency name appended to it, e.g., Equity:CurrencyAccounts:CAD Equity:CurrencyAccounts:USD etc., where used. You can have a look at the account balances with a query like this: bean-query $L \"select account, sum(position), convert(sum(position), 'USD') where date >= 2018-01-01 and account ~ 'CurrencyAccounts' \" The sum total of the converted amounts should be a number not too large: bean-query $L \"select convert(sum(position), 'USD') where date >= 2018-01-01 and account ~ 'CurrencyAccounts'\" WARNING: This is a prototype. Note the FIXMEs in the code below, which indicate some potential problems.","title":"currency_accounts"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.currency_accounts.get_neutralizing_postings","text":"Process an entry. Parameters: curmap \u2013 A dict of currency to a list of Postings of this transaction. base_account \u2013 A string, the root account name to insert. new_accounts \u2013 A set, a mutable accumulator of new account names. Returns: A modified entry, with new postings inserted to rebalance currency trading accounts. Source code in beancount/plugins/currency_accounts.py def get_neutralizing_postings(curmap, base_account, new_accounts): \"\"\"Process an entry. Args: curmap: A dict of currency to a list of Postings of this transaction. base_account: A string, the root account name to insert. new_accounts: A set, a mutable accumulator of new account names. Returns: A modified entry, with new postings inserted to rebalance currency trading accounts. \"\"\" new_postings = [] for currency, postings in curmap.items(): # Compute the per-currency balance. inv = inventory.Inventory() for posting in postings: inv.add_amount(convert.get_cost(posting)) if inv.is_empty(): new_postings.extend(postings) continue # Re-insert original postings and remove price conversions. # # Note: This may cause problems if the implicit_prices plugin is # configured to run after this one, or if you need the price annotations # for some scripting or serious work. # # FIXME: We need to handle these important cases (they're not frivolous, # this is a prototype), probably by inserting some exceptions with # collaborating code in the booking (e.g. insert some metadata that # disables price conversions on those postings). # # FIXME(2): Ouch! Some of the residual seeps through here, where there # are more than a single currency block. This needs fixing too. You can # easily mitigate some of this to some extent, by excluding transactions # which don't have any price conversion in them. for pos in postings: if pos.price is not None: pos = pos._replace(price=None) new_postings.append(pos) # Insert the currency trading accounts postings. amount = inv.get_only_position().units acc = account.join(base_account, currency) new_accounts.add(acc) new_postings.append( Posting(acc, -amount, None, None, None, None)) return new_postings","title":"get_neutralizing_postings()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.currency_accounts.group_postings_by_weight_currency","text":"Return where this entry might require adjustment. Source code in beancount/plugins/currency_accounts.py def group_postings_by_weight_currency(entry: Transaction): \"\"\"Return where this entry might require adjustment.\"\"\" curmap = collections.defaultdict(list) has_price = False for posting in entry.postings: currency = posting.units.currency if posting.cost: currency = posting.cost.currency if posting.price: assert posting.price.currency == currency elif posting.price: has_price = True currency = posting.price.currency if posting.price: has_price = True curmap[currency].append(posting) return curmap, has_price","title":"group_postings_by_weight_currency()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.currency_accounts.insert_currency_trading_postings","text":"Insert currency trading postings. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config \u2013 The base account name for currency trading accounts. Returns: A list of new errors, if any were found. Source code in beancount/plugins/currency_accounts.py 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('', index), earliest_date, acc, None, None) for index, acc in enumerate(sorted(new_accounts))] return open_entries + new_entries, errors","title":"insert_currency_trading_postings()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.divert_expenses","text":"For tagged transactions, convert expenses to a single account. This plugin allows you to select a tag and it automatically converts all the Expenses postings to use a single account. For example, with this input: plugin \"divert_expenses\" \"['kid', 'Expenses:Child']\" 2018-01-28 * \"CVS\" \"Formula\" #kid Liabilities:CreditCard -10.27 USD Expenses:Food:Grocery 10.27 USD It will output: 2018-01-28 * \"CVS\" \"Formula\" #kid Liabilities:CreditCard -10.27 USD Expenses:Child 10.27 USD You can limit the diversion to one posting only, like this: 2018-05-05 * \"CVS/PHARMACY\" \"\" #kai Liabilities:CreditCard -66.38 USD Expenses:Pharmacy 21.00 USD ;; Vitamins for Kai Expenses:Pharmacy 45.38 USD divert: FALSE See unit test for details. See this thread for context: https://docs.google.com/drawings/d/18fTrrGlmz0jFbfcGGHTffbdRwbmST8r9_3O26Dd1Xww/edit?usp=sharing","title":"divert_expenses"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.divert_expenses.divert_expenses","text":"Divert expenses. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. options_map \u2013 A parser options dict. config_str \u2013 A configuration string, which is intended to be a list of two strings, a tag, and an account to replace expenses with. Returns: A modified list of entries. Source code in beancount/plugins/divert_expenses.py def divert_expenses(entries, options_map, config_str): \"\"\"Divert expenses. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Args: entries: A list of directives. We're interested only in the Transaction instances. options_map: A parser options dict. config_str: A configuration string, which is intended to be a list of two strings, a tag, and an account to replace expenses with. Returns: A modified list of entries. \"\"\" # pylint: disable=eval-used config_obj = eval(config_str, {}, {}) if not isinstance(config_obj, dict): raise RuntimeError(\"Invalid plugin configuration: should be a single dict.\") tag = config_obj['tag'] replacement_account = config_obj['account'] acctypes = options.get_account_types(options_map) new_entries = [] errors = [] for entry in entries: if isinstance(entry, Transaction) and tag in entry.tags: entry = replace_diverted_accounts(entry, replacement_account, acctypes) new_entries.append(entry) return new_entries, errors","title":"divert_expenses()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.divert_expenses.replace_diverted_accounts","text":"Replace the Expenses accounts from the entry. Parameters: entry \u2013 A Transaction directive. replacement_account \u2013 A string, the account to use for replacement. acctypes \u2013 An AccountTypes instance. Returns: A possibly entry directive. Source code in beancount/plugins/divert_expenses.py def replace_diverted_accounts(entry, replacement_account, acctypes): \"\"\"Replace the Expenses accounts from the entry. Args: entry: A Transaction directive. replacement_account: A string, the account to use for replacement. acctypes: An AccountTypes instance. Returns: A possibly entry directive. \"\"\" new_postings = [] for posting in entry.postings: divert = posting.meta.get('divert', None) if posting.meta else None if (divert is True or ( divert is None and account_types.is_account_type(acctypes.expenses, posting.account))): posting = posting._replace(account=replacement_account, meta={'diverted_account': posting.account}) new_postings.append(posting) return entry._replace(postings=new_postings)","title":"replace_diverted_accounts()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.exclude_tag","text":"Exclude #virtual tags. This is used to demonstrate excluding a set of transactions from a particular tag. In this example module, the tag name is fixed, but if we integrated this we could provide a way to choose which tags to exclude. This is simply just another mechanism for selecting a subset of transactions. See discussion here for details: https://groups.google.com/d/msg/ledger-cli/N8Slh2t45K0/aAz0i3Be4LYJ","title":"exclude_tag"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.exclude_tag.exclude_tag","text":"Select all transactions that do not have a #virtual tag. Parameters: entries \u2013 A list of entry instances. options_map \u2013 A dict of options parsed from the file. Returns: A tuple of entries and errors. Source code in beancount/plugins/exclude_tag.py def exclude_tag(entries, options_map): \"\"\"Select all transactions that do not have a #virtual tag. Args: entries: A list of entry instances. options_map: A dict of options parsed from the file. Returns: A tuple of entries and errors. \"\"\" filtered_entries = [entry for entry in entries if (not isinstance(entry, data.Transaction) or not entry.tags or EXCLUDED_TAG not in entry.tags)] return (filtered_entries, [])","title":"exclude_tag()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.fill_account","text":"Insert an posting with a default account when there is only a single posting. This is convenient to use in files which have mostly expenses, such as during a trip. Set the name of the default account to fill in as an option.","title":"fill_account"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.fill_account.FillAccountError","text":"FillAccountError(source, message, entry)","title":"FillAccountError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.fill_account.FillAccountError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/fill_account.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.fill_account.FillAccountError.__new__","text":"Create new instance of FillAccountError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.fill_account.FillAccountError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/fill_account.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.fill_account.fill_account","text":"Insert an posting with a default account when there is only a single posting. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 A parser options dict. insert_account \u2013 A string, the name of the account. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. Source code in beancount/plugins/fill_account.py 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('', 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, []","title":"fill_account()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.fix_payees","text":"Rename payees based on a set of rules. This can be used to clean up dirty imported payee names. This plugin accepts a list of rules in this format: plugin \"beancount.plugins.fix_payees\" \"[ (PAYEE, MATCH1, MATCH2, ...), ]\" Each of the \"MATCH\" clauses is a string, in the format: \"A:<regexp>\" : Match the account name. \"D:<regexp>\" : Match the payee or the narration. The plugin matches the Transactions in the file and if there is a case-insensitive match against the regular expression (we use re.search()), replaces the payee name by \"PAYEE\". If multiple rules match, only the first rule is used. For example: plugin \"beancount.plugins.fix_payees\" \"[ (\"T-Mobile USA\", \"A:Expenses:Communications:Phone\", \"D:t-mobile\"), (\"Con Edison\", \"A:Expenses:Home:Electricity\", \"D:con ?ed\"), (\"Birreria @ Eataly\", \"D:EATALY BIRRERIA\"), ]\"","title":"fix_payees"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.fix_payees.FixPayeesError","text":"FixPayeesError(source, message, entry)","title":"FixPayeesError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.fix_payees.FixPayeesError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/fix_payees.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.fix_payees.FixPayeesError.__new__","text":"Create new instance of FixPayeesError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.fix_payees.FixPayeesError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/fix_payees.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.fix_payees.fix_payees","text":"Rename payees based on a set of rules. See module docstring for details. Parameters: entries \u2013 a list of entry instances options_map \u2013 a dict of options parsed from the file config \u2013 A configuration string, which is intended to be a list of (PAYEE, MATCH, ...) rules. See module docstring for details. Returns: A tuple of entries and errors. Source code in beancount/plugins/fix_payees.py def fix_payees(entries, options_map, config): \"\"\"Rename payees based on a set of rules. See module docstring for details. Args: entries: a list of entry instances options_map: a dict of options parsed from the file config: A configuration string, which is intended to be a list of (PAYEE, MATCH, ...) rules. See module docstring for details. Returns: A tuple of entries and errors. \"\"\" errors = [] if config.strip(): try: expr = ast.literal_eval(config) except (SyntaxError, ValueError): meta = data.new_metadata(options_map['filename'], 0) errors.append(FixPayeesError(meta, \"Syntax error in config: {}\".format(config), None)) return entries, errors else: return entries, errors # Pre-compile the regular expressions for performance. rules = [] for rule in ast.literal_eval(config): clauses = iter(rule) new_payee = next(clauses) regexps = [] for clause in clauses: match = re.match('([AD]):(.*)', clause) if not match: meta = data.new_metadata(options_map['filename'], 0) errors.append(FixPayeesError(meta, \"Invalid clause: {}\".format(clause), None)) continue command, regexp = match.groups() regexps.append((command, re.compile(regexp, re.I).search)) new_rule = [new_payee] + regexps rules.append(tuple(new_rule)) # Run the rules over the transaction objects. new_entries = [] replaced_entries = {rule[0]: [] for rule in rules} for entry in entries: if isinstance(entry, data.Transaction): for rule in rules: clauses = iter(rule) new_payee = next(clauses) # Attempt to match all the clauses. for clause in clauses: command, func = clause if command == 'D': if not ((entry.payee is not None and func(entry.payee)) or (entry.narration is not None and func(entry.narration))): break elif command == 'A': if not any(func(posting.account) for posting in entry.postings): break else: # Make the replacement. entry = entry._replace(payee=new_payee) replaced_entries[new_payee].append(entry) new_entries.append(entry) if _DEBUG: # Print debugging info. for payee, repl_entries in sorted(replaced_entries.items(), key=lambda x: len(x[1]), reverse=True): print('{:60}: {}'.format(payee, len(repl_entries))) return new_entries, errors","title":"fix_payees()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.forecast","text":"An example of adding a forecasting feature to Beancount via a plugin. This entry filter plugin uses existing syntax to define and automatically inserted transactions in the future based on a convention. It serves mostly as an example of how you can experiment by creating and installing a local filter, and not so much as a serious forecasting feature (though the experiment is a good way to get something more general kickstarted eventually, I think the concept would generalize nicely and should eventually be added as a common feature of Beancount). A user can create a transaction like this: 2014-03-08 # \"Electricity bill [MONTHLY]\" Expenses:Electricity 50.10 USD Assets:Checking -50.10 USD and new transactions will be created monthly for the following year. Note the use of the '#' flag and the word 'MONTHLY' which defines the periodicity. The number of recurrences can optionally be specified either by providing an end date or by specifying the number of times that the transaction will be repeated. For example: 2014-03-08 # \"Electricity bill [MONTHLY UNTIL 2019-12-31]\" Expenses:Electricity 50.10 USD Assets:Checking -50.10 USD 2014-03-08 # \"Electricity bill [MONTHLY REPEAT 10 TIMES]\" Expenses:Electricity 50.10 USD Assets:Checking -50.10 USD Transactions can also be repeated at yearly intervals, e.g.: 2014-03-08 # \"Electricity bill [YEARLY REPEAT 10 TIMES]\" Expenses:Electricity 50.10 USD Assets:Checking -50.10 USD Other examples: 2014-03-08 # \"Electricity bill [WEEKLY SKIP 1 TIME REPEAT 10 TIMES]\" Expenses:Electricity 50.10 USD Assets:Checking -50.10 USD 2014-03-08 # \"Electricity bill [DAILY SKIP 3 TIMES REPEAT 1 TIME]\" Expenses:Electricity 50.10 USD Assets:Checking -50.10 USD","title":"forecast"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.forecast.forecast_plugin","text":"An example filter that piggybacks on top of the Beancount input syntax to insert forecast entries automatically. This functions accepts the return value of beancount.loader.load_file() and must return the same type of output. Parameters: entries \u2013 a list of entry instances options_map \u2013 a dict of options parsed from the file Returns: A tuple of entries and errors. Source code in beancount/plugins/forecast.py def forecast_plugin(entries, options_map): \"\"\"An example filter that piggybacks on top of the Beancount input syntax to insert forecast entries automatically. This functions accepts the return value of beancount.loader.load_file() and must return the same type of output. Args: entries: a list of entry instances options_map: a dict of options parsed from the file Returns: A tuple of entries and errors. \"\"\" # Find the last entry's date. date_today = entries[-1].date # Filter out forecast entries from the list of valid entries. forecast_entries = [] filtered_entries = [] for entry in entries: outlist = (forecast_entries if (isinstance(entry, data.Transaction) and entry.flag == '#') else filtered_entries) outlist.append(entry) # Generate forecast entries up to the end of the current year. new_entries = [] for entry in forecast_entries: # Parse the periodicity. match = re.search(r'(^.*)\\[(MONTHLY|YEARLY|WEEKLY|DAILY)' r'(\\s+SKIP\\s+([1-9][0-9]*)\\s+TIME.?)' r'?(\\s+REPEAT\\s+([1-9][0-9]*)\\s+TIME.?)' r'?(\\s+UNTIL\\s+([0-9\\-]+))?\\]', entry.narration) if not match: new_entries.append(entry) continue forecast_narration = match.group(1).strip() forecast_interval = ( rrule.YEARLY if match.group(2).strip() == 'YEARLY' else rrule.WEEKLY if match.group(2).strip() == 'WEEKLY' else rrule.DAILY if match.group(2).strip() == 'DAILY' else rrule.MONTHLY) forecast_periodicity = {'dtstart': entry.date} if match.group(6): # e.g., [MONTHLY REPEAT 3 TIMES]: forecast_periodicity['count'] = int(match.group(6)) elif match.group(8): # e.g., [MONTHLY UNTIL 2020-01-01]: forecast_periodicity['until'] = datetime.datetime.strptime( match.group(8), '%Y-%m-%d').date() else: # e.g., [MONTHLY] forecast_periodicity['until'] = datetime.date( datetime.date.today().year, 12, 31) if match.group(4): # SKIP forecast_periodicity['interval'] = int(match.group(4)) + 1 # Generate a new entry for each forecast date. forecast_dates = [dt.date() for dt in rrule.rrule(forecast_interval, **forecast_periodicity)] for forecast_date in forecast_dates: forecast_entry = entry._replace(date=forecast_date, narration=forecast_narration) new_entries.append(forecast_entry) # Make sure the new entries inserted are sorted. new_entries.sort(key=data.entry_sortkey) return (filtered_entries + new_entries, [])","title":"forecast_plugin()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.implicit_prices","text":"This plugin synthesizes Price directives for all Postings with a price or directive or if it is an augmenting posting, has a cost directive.","title":"implicit_prices"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.implicit_prices.ImplicitPriceError","text":"ImplicitPriceError(source, message, entry)","title":"ImplicitPriceError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.implicit_prices.ImplicitPriceError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/implicit_prices.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.implicit_prices.ImplicitPriceError.__new__","text":"Create new instance of ImplicitPriceError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.implicit_prices.ImplicitPriceError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/implicit_prices.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.implicit_prices.add_implicit_prices","text":"Insert implicitly defined prices from Transactions. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. Source code in beancount/plugins/implicit_prices.py def add_implicit_prices(entries, unused_options_map): \"\"\"Insert implicitly defined prices from Transactions. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. \"\"\" new_entries = [] errors = [] # A dict of (date, currency, cost-currency) to price entry. new_price_entry_map = {} balances = collections.defaultdict(inventory.Inventory) for entry in entries: # Always replicate the existing entries. new_entries.append(entry) if isinstance(entry, Transaction): # Inspect all the postings in the transaction. for posting in entry.postings: units = posting.units cost = posting.cost # Check if the position is matching against an existing # position. _, booking = balances[posting.account].add_position(posting) # Add prices when they're explicitly specified on a posting. An # explicitly specified price may occur in a conversion, e.g. # Assets:Account 100 USD @ 1.10 CAD # or, if a cost is also specified, as the current price of the # underlying instrument, e.g. # Assets:Account 100 HOOL {564.20} @ {581.97} USD if posting.price is not None: meta = data.new_metadata(entry.meta[\"filename\"], entry.meta[\"lineno\"]) price_entry = data.Price(meta, entry.date, units.currency, posting.price) # Add costs, when we're not matching against an existing # position. This happens when we're just specifying the cost, # e.g. # Assets:Account 100 HOOL {564.20} elif (cost is not None and booking != inventory.Booking.REDUCED): meta = data.new_metadata(entry.meta[\"filename\"], entry.meta[\"lineno\"]) price_entry = data.Price(meta, entry.date, units.currency, amount.Amount(cost.number, cost.currency)) else: price_entry = None if price_entry is not None: key = (price_entry.date, price_entry.currency, price_entry.amount.number, # Ideally should be removed. price_entry.amount.currency) try: new_price_entry_map[key] ## Do not fail for now. We still have many valid use ## cases of duplicate prices on the same date, for ## example, stock splits, or trades on two dates with ## two separate reported prices. We need to figure out a ## more elegant solution for this in the long term. ## Keeping both for now. We should ideally not use the ## number in the de-dup key above. # # dup_entry = new_price_entry_map[key] # if price_entry.amount.number == dup_entry.amount.number: # # Skip duplicates. # continue # else: # errors.append( # ImplicitPriceError( # entry.meta, # \"Duplicate prices for {} on {}\".format(entry, # dup_entry), # entry)) except KeyError: new_price_entry_map[key] = price_entry new_entries.append(price_entry) return new_entries, errors","title":"add_implicit_prices()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.ira_contribs","text":"Automatically adding IRA contributions postings. This plugin looks for increasing postings on specified accounts ('+' sign for Assets and Expenses accounts, '-' sign for the others), or postings with a particular flag on them and when it finds some, inserts a pair of postings on that transaction of the corresponding amounts in a different currency. The currency is intended to be an imaginary currency used to track the number of dollars contributed to a retirement account over time. For example, a possible configuration could be: plugin \"beancount.plugins.ira_contribs\" \"{ 'currency': 'IRAUSD', 'flag': 'M', 'accounts': { 'Income:US:Acme:Match401k': ( 'Assets:US:Federal:Match401k', 'Expenses:Taxes:TY{year}:US:Federal:Match401k'), ('C', 'Assets:US:Fidelity:PreTax401k:Cash'): ( 'Assets:US:Federal:PreTax401k', 'Expenses:Taxes:TY{year}:US:Federal:PreTax401k'), } }\" Note: In this example, the configuration that triggers on the \"Income:US:Acme:Match401k\" account does not require a flag for those accounts; the configuration for the \"Assets:US:Fidelity:PreTax401k:Cash\" account requires postings to have a \"C\" flag to trigger an insertion. Given a transaction like the following, which would be typical for a salary entry where the employer is automatically diverting some of the pre-tax money to a retirement account (in this example, at Fidelity): 2013-02-15 * \"ACME INC PAYROLL\" Income:US:Acme:Salary ... ... Assets:US:BofA:Checking ... Assets:US:Fidelity:PreTax401k:Cash 620.50 USD ... A posting with account 'Assets:US:Fidelity:PreTax401k:Cash', which is configured to match, would be found. The configuration above instructs the plugin to automatically insert new postings like this: 2013-02-15 * \"ACME INC PAYROLL\" ... Assets:US:Fidelity:PreTax401k:Cash 620.50 USD M Assets:US:Federal:PreTax401k -620.50 IRAUSD M Expenses:Taxes:TY2013:US:Federal:PreTax401k 620.50 IRAUSD ... Notice that the \"{year}\" string in the configuration's account names is automatically replaced by the current year in the account name. This is useful if you maintain separate tax accounts per year. Furthermore, as in the configuration example above, you may have multiple matching entries to trigger multiple insertions. For example, the employer may also match the employee's retirement contribution by depositing some money in the retirement account: 2013-02-15 * \"BUYMF - MATCH\" \"Employer match, invested in SaveEasy 2030 fund\" Assets:US:Fidelity:Match401k:SE2030 34.793 SE2030 {17.834 USD} Income:US:Acme:Match401k -620.50 USD In this example the funds get reported as invested immediately (an intermediate deposit into a cash account does not take place). The plugin configuration would match against the 'Income:US:Acme:Match401k' account and since it increases its value (the normal balance of an Income account is negative), postings would be inserted like this: 2013-02-15 * \"BUYMF - MATCH\" \"Employer match, invested in SaveEasy 2030 fund\" Assets:US:Fidelity:Match401k:SE2030 34.793 SE2030 {17.834 USD} Income:US:Acme:Match401k -620.50 USD M Assets:US:Federal:Match401k -620.50 IRAUSD M Expenses:Taxes:TY2013:US:Federal:Match401k 620.50 IRAUSD Note that the special dict keys 'currency' and 'flag' are used to specify which currency to use for the inserted postings, and if set, which flag to mark these postings with.","title":"ira_contribs"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.ira_contribs.add_ira_contribs","text":"Add legs for 401k employer match contributions. See module docstring for an example configuration. Parameters: entries \u2013 a list of entry instances options_map \u2013 a dict of options parsed from the file config_str \u2013 A configuration string, which is intended to be a Python dict mapping match-accounts to a pair of (negative-account, position-account) account names. Returns: A tuple of entries and errors. Source code in beancount/plugins/ira_contribs.py def add_ira_contribs(entries, options_map, config_str): \"\"\"Add legs for 401k employer match contributions. See module docstring for an example configuration. Args: entries: a list of entry instances options_map: a dict of options parsed from the file config_str: A configuration string, which is intended to be a Python dict mapping match-accounts to a pair of (negative-account, position-account) account names. Returns: A tuple of entries and errors. \"\"\" # Parse and extract configuration values. # FIXME: Use ast.literal_eval() here; you need to convert this code and the getters. # FIXME: Also, don't raise a RuntimeError, return an error object; review # this for all the plugins. # FIXME: This too is temporary. # pylint: disable=eval-used config_obj = eval(config_str, {}, {}) if not isinstance(config_obj, dict): raise RuntimeError(\"Invalid plugin configuration: should be a single dict.\") # Currency of the inserted postings. currency = config_obj.pop('currency', 'UNKNOWN') # Flag to attach to the inserted postings. insert_flag = config_obj.pop('flag', None) # A dict of account names that trigger the insertion of postings to pairs of # inserted accounts when triggered. accounts = config_obj.pop('accounts', {}) # Convert the key in the accounts configuration for matching. account_transforms = {} for key, config in accounts.items(): if isinstance(key, str): flag = None account = key else: assert isinstance(key, tuple) flag, account = key account_transforms[account] = (flag, config) new_entries = [] for entry in entries: if isinstance(entry, data.Transaction): orig_entry = entry for posting in entry.postings: if (posting.units is not MISSING and (posting.account in account_transforms) and (account_types.get_account_sign(posting.account) * posting.units.number > 0)): # Get the new account legs to insert. required_flag, (neg_account, pos_account) = account_transforms[posting.account] assert posting.cost is None # Check required flag if present. if (required_flag is None or (required_flag and required_flag == posting.flag)): # Insert income/expense entries for 401k. entry = add_postings( entry, amount.Amount(abs(posting.units.number), currency), neg_account.format(year=entry.date.year), pos_account.format(year=entry.date.year), insert_flag) if DEBUG and orig_entry is not entry: printer.print_entry(orig_entry) printer.print_entry(entry) new_entries.append(entry) return new_entries, []","title":"add_ira_contribs()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.ira_contribs.add_postings","text":"Insert positive and negative postings of a position in an entry. Parameters: entry \u2013 A Transaction instance. amount_ \u2013 An Amount instance to create the position, with positive number. neg_account \u2013 An account for the posting with the negative amount. pos_account \u2013 An account for the posting with the positive amount. flag \u2013 A string, that is to be set as flag for the new postings. Returns: A new, modified entry. Source code in beancount/plugins/ira_contribs.py def add_postings(entry, amount_, neg_account, pos_account, flag): \"\"\"Insert positive and negative postings of a position in an entry. Args: entry: A Transaction instance. amount_: An Amount instance to create the position, with positive number. neg_account: An account for the posting with the negative amount. pos_account: An account for the posting with the positive amount. flag: A string, that is to be set as flag for the new postings. Returns: A new, modified entry. \"\"\" return entry._replace(postings=entry.postings + [ data.Posting(neg_account, -amount_, None, None, flag, None), data.Posting(pos_account, amount_, None, None, flag, None), ])","title":"add_postings()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.leafonly","text":"A plugin that issues errors when amounts are posted to non-leaf accounts, that is, accounts with child accounts. This is an extra constraint that you may want to apply optionally. If you install this plugin, it will issue errors for all accounts that have postings to non-leaf accounts. Some users may want to disallow this and enforce that only leaf accounts may have postings on them.","title":"leafonly"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.leafonly.LeafOnlyError","text":"LeafOnlyError(source, message, entry)","title":"LeafOnlyError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.leafonly.LeafOnlyError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/leafonly.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.leafonly.LeafOnlyError.__new__","text":"Create new instance of LeafOnlyError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.leafonly.LeafOnlyError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/leafonly.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.leafonly.validate_leaf_only","text":"Check for non-leaf accounts that have postings on them. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/leafonly.py def validate_leaf_only(entries, unused_options_map): \"\"\"Check for non-leaf accounts that have postings on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" real_root = realization.realize(entries, compute_balance=False) default_meta = data.new_metadata('', 0) open_close_map = None # Lazily computed. errors = [] for real_account in realization.iter_children(real_root): if len(real_account) > 0 and real_account.txn_postings: if open_close_map is None: open_close_map = getters.get_account_open_close(entries) try: open_entry = open_close_map[real_account.account][0] except KeyError: open_entry = None errors.append(LeafOnlyError( open_entry.meta if open_entry else default_meta, \"Non-leaf account '{}' has postings on it\".format(real_account.account), open_entry)) return entries, errors","title":"validate_leaf_only()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.mark_unverified","text":"Add metadata to Postings which occur after their last Balance directives. Some people use Balance directives as a way to indicate that all postings before them are verified. They want to compute balances in each account as of the date of that last Balance directives. One way to do that is to use this plugin to mark the postings which occur after and to then filter them out using a WHERE clause on that metadata: SELECT account, sum(position) WHERE NOT meta(\"unverified\") Note that doing such a filtering may result in a list of balances which may not add to zero. Also, postings for accounts without a single Balance directive on them will not be marked as unverified as all (otherwise all the postings would be marked, this would make no sense).","title":"mark_unverified"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.mark_unverified.mark_unverified","text":"Add metadata to postings after the last Balance entry. See module doc. Parameters: entries \u2013 A list of data directives. options_map \u2013 A dict of options, that confirms to beancount.parser.options. 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. Source code in beancount/plugins/mark_unverified.py def mark_unverified(entries, options_map): \"\"\"Add metadata to postings after the last Balance entry. See module doc. Args: entries: A list of data directives. options_map: A dict of options, that confirms to beancount.parser.options. 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. \"\"\" # The last Balance directive seen for each account. last_balances = {} for entry in entries: if isinstance(entry, data.Balance): last_balances[entry.account] = entry new_entries = [] for entry in entries: if isinstance(entry, data.Transaction): postings = entry.postings new_postings = postings for index, posting in enumerate(postings): balance = last_balances.get(posting.account, None) if balance and balance.date <= entry.date: if new_postings is postings: new_postings = postings.copy() new_meta = posting.meta.copy() new_meta['unverified'] = True new_postings[index] = posting._replace(meta=new_meta) if new_postings is not postings: entry = entry._replace(postings=new_postings) new_entries.append(entry) return new_entries, []","title":"mark_unverified()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.merge_meta","text":"Merge the metadata from a second file into the current set of entries. This is useful if you like to keep more sensitive private data, such as account numbers or passwords, in a second, possibly encrypted file. This can be used to generate a will, for instance, for your loved ones to be able to figure where all your assets are in case you pass away. You can store all the super secret stuff in a more closely guarded, hidden away separate file. The metadata from Open directives: Account name must match. Close directives: Account name must match. Commodity directives: Currency must match. are copied over. Metadata from the external file conflicting with that present in the main file overwrites it (external data wins). WARNING! If you include an encrypted file and the main file is not encrypted, the contents extraction from the encrypted file may appear in the cache.","title":"merge_meta"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.merge_meta.merge_meta","text":"Load a secondary file and merge its metadata in our given set of entries. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. config \u2013 The plugin configuration string. Returns: A list of entries, with more metadata attached to them. Source code in beancount/plugins/merge_meta.py def merge_meta(entries, options_map, config): \"\"\"Load a secondary file and merge its metadata in our given set of entries. Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. config: The plugin configuration string. Returns: A list of entries, with more metadata attached to them. \"\"\" external_filename = config new_entries = list(entries) ext_entries, ext_errors, ext_options_map = loader.load_file(external_filename) # Map Open and Close directives. oc_map = getters.get_account_open_close(entries) ext_oc_map = getters.get_account_open_close(ext_entries) for account in set(oc_map.keys()) & set(ext_oc_map.keys()): open_entry, close_entry = oc_map[account] ext_open_entry, ext_close_entry = ext_oc_map[account] if open_entry and ext_open_entry: open_entry.meta.update(ext_open_entry.meta) if close_entry and ext_close_entry: close_entry.meta.update(ext_close_entry.meta) # Map Commodity directives. comm_map = getters.get_commodity_map(entries, False) ext_comm_map = getters.get_commodity_map(ext_entries, False) for currency in set(comm_map) & set(ext_comm_map): comm_entry = comm_map[currency] ext_comm_entry = ext_comm_map[currency] if comm_entry and ext_comm_entry: comm_entry.meta.update(ext_comm_entry.meta) # Note: We cannot include the external file in the list of inputs so that a # change of it triggers a cache rebuild because side-effects on options_map # aren't cascaded through. This is something that should be defined better # in the plugin interface and perhaps improved upon. return new_entries, ext_errors","title":"merge_meta()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.noduplicates","text":"This plugin validates that there are no duplicate transactions.","title":"noduplicates"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.noduplicates.validate_no_duplicates","text":"Check that the entries are unique, by computing hashes. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/noduplicates.py def validate_no_duplicates(entries, unused_options_map): \"\"\"Check that the entries are unique, by computing hashes. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" unused_hashes, errors = compare.hash_entries(entries, exclude_meta=True) return entries, errors","title":"validate_no_duplicates()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.nounused","text":"This plugin validates that there are no unused accounts.","title":"nounused"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.nounused.UnusedAccountError","text":"UnusedAccountError(source, message, entry)","title":"UnusedAccountError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.nounused.UnusedAccountError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/nounused.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.nounused.UnusedAccountError.__new__","text":"Create new instance of UnusedAccountError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.nounused.UnusedAccountError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/nounused.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.nounused.validate_unused_accounts","text":"Check that all accounts declared open are actually used. We check that all of the accounts that are open are at least referred to by another directive. These are probably unused, so issue a warning (we like to be pedantic). Note that an account that is open and then closed is considered used--this is a valid use case that may occur in reality. If you have a use case for an account to be open but never used, you can quiet that warning by initializing the account with a balance asserts or a pad directive, or even use a note will be sufficient. (This is probably a good candidate for optional inclusion as a \"pedantic\" plugin.) Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/nounused.py def validate_unused_accounts(entries, unused_options_map): \"\"\"Check that all accounts declared open are actually used. We check that all of the accounts that are open are at least referred to by another directive. These are probably unused, so issue a warning (we like to be pedantic). Note that an account that is open and then closed is considered used--this is a valid use case that may occur in reality. If you have a use case for an account to be open but never used, you can quiet that warning by initializing the account with a balance asserts or a pad directive, or even use a note will be sufficient. (This is probably a good candidate for optional inclusion as a \"pedantic\" plugin.) Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" # Find all the accounts referenced by entries which are not Open, and the # open directives for error reporting below. open_map = {} referenced_accounts = set() for entry in entries: if isinstance(entry, data.Open): open_map[entry.account] = entry continue referenced_accounts.update(getters.get_entry_accounts(entry)) # Create a list of suitable errors, with the location of the Open directives # corresponding to the unused accounts. errors = [UnusedAccountError(open_entry.meta, \"Unused account '{}'\".format(account), open_entry) for account, open_entry in open_map.items() if account not in referenced_accounts] return entries, errors","title":"validate_unused_accounts()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.onecommodity","text":"A plugin that issues errors when more than one commodity is used in an account. For investments or trading accounts, it can make it easier to filter the action around a single stock by using the name of the stock as the leaf of the account name. Notes: The plugin will automatically skip accounts that have explicitly declared commodities in their Open directive. You can also set the metadata \"onecommodity: FALSE\" on an account's Open directive to skip the checks for that account. If provided, the configuration should be a regular expression restricting the set of accounts to check.","title":"onecommodity"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.onecommodity.OneCommodityError","text":"OneCommodityError(source, message, entry)","title":"OneCommodityError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.onecommodity.OneCommodityError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/onecommodity.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.onecommodity.OneCommodityError.__new__","text":"Create new instance of OneCommodityError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.onecommodity.OneCommodityError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/onecommodity.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.onecommodity.validate_one_commodity","text":"Check that each account has units in only a single commodity. This is an extra constraint that you may want to apply optionally, despite Beancount's ability to support inventories and aggregations with more than one commodity. I believe this also matches GnuCash's model, where each account has a single commodity attached to it. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config \u2013 The plugin configuration string, a regular expression to match against the subset of accounts to check. Returns: A list of new errors, if any were found. Source code in beancount/plugins/onecommodity.py def validate_one_commodity(entries, unused_options_map, config=None): \"\"\"Check that each account has units in only a single commodity. This is an extra constraint that you may want to apply optionally, despite Beancount's ability to support inventories and aggregations with more than one commodity. I believe this also matches GnuCash's model, where each account has a single commodity attached to it. Args: entries: A list of directives. unused_options_map: An options map. config: The plugin configuration string, a regular expression to match against the subset of accounts to check. Returns: A list of new errors, if any were found. \"\"\" accounts_re = re.compile(config) if config else None # Mappings of account name to lists of currencies for each units and cost. units_map = collections.defaultdict(set) cost_map = collections.defaultdict(set) # Mappings to use just for getting a relevant source. units_source_map = {} cost_source_map = {} # Gather the set of accounts to skip from the Open directives. skip_accounts = set() for entry in entries: if not isinstance(entry, data.Open): continue if (not entry.meta.get(\"onecommodity\", True) or (accounts_re and not accounts_re.match(entry.account)) or (entry.currencies and len(entry.currencies) > 1)): skip_accounts.add(entry.account) # Accumulate all the commodities used. for entry in entries: if isinstance(entry, data.Transaction): for posting in entry.postings: if posting.account in skip_accounts: continue units = posting.units units_map[posting.account].add(units.currency) if len(units_map[posting.account]) > 1: units_source_map[posting.account] = entry cost = posting.cost if cost: cost_map[posting.account].add(cost.currency) if len(cost_map[posting.account]) > 1: units_source_map[posting.account] = entry elif isinstance(entry, data.Balance): if entry.account in skip_accounts: continue units_map[entry.account].add(entry.amount.currency) if len(units_map[entry.account]) > 1: units_source_map[entry.account] = entry elif isinstance(entry, data.Open): if entry.currencies and len(entry.currencies) > 1: skip_accounts.add(entry.account) # Check units. errors = [] for account, currencies in units_map.items(): if account in skip_accounts: continue if len(currencies) > 1: errors.append(OneCommodityError( units_source_map[account].meta, \"More than one currency in account '{}': {}\".format( account, ','.join(currencies)), None)) # Check costs. for account, currencies in cost_map.items(): if account in skip_accounts: continue if len(currencies) > 1: errors.append(OneCommodityError( cost_source_map[account].meta, \"More than one cost currency in account '{}': {}\".format( account, ','.join(currencies)), None)) return entries, errors","title":"validate_one_commodity()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.pedantic","text":"A plugin of plugins which triggers are all the pedantic plugins. In a sense, this is the inverse of \"pedantic.\" This is useful when doing some types of quick and dirty tests.","title":"pedantic"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.sellgains","text":"A plugin that cross-checks declared gains against prices on lot sales. When you sell stock, the gains can be automatically implied by the corresponding cash amounts. For example, in the following transaction the 2nd and 3rd postings should match the value of the stock sold: 1999-07-31 * \"Sell\" Assets:US:BRS:Company:ESPP -81 ADSK {26.3125 USD} Assets:US:BRS:Company:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:US:Company:ESPP:PnL -10.125 USD The cost basis is checked against: 2141.36 + 008 + -10.125. That is, the balance checks computes -81 x 26.3125 = -2131.3125 + 2141.36 + 0.08 + -10.125 and checks that the residual is below a small tolerance. But... usually the income leg isn't given to you in statements. Beancount can automatically infer it using the balance, which is convenient, like this: 1999-07-31 * \"Sell\" Assets:US:BRS:Company:ESPP -81 ADSK {26.3125 USD} Assets:US:BRS:Company:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:US:Company:ESPP:PnL Additionally, most often you have the sales prices given to you on your transaction confirmation statement, so you can enter this: 1999-07-31 * \"Sell\" Assets:US:BRS:Company:ESPP -81 ADSK {26.3125 USD} @ 26.4375 USD Assets:US:BRS:Company:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:US:Company:ESPP:PnL So in theory, if the price is given (26.4375 USD), we could verify that the proceeds from the sale at the given price match non-Income postings. That is, verify that -81 x 26.4375 = -2141.4375 + 2141.36 + 0.08 + is below a small tolerance value. So this plugin does this. In general terms, it does the following: For transactions with postings that have a cost and a price, it verifies that the sum of the positions on all postings to non-income accounts is below tolerance. This provides yet another level of verification and allows you to elide the income amounts, knowing that the price is there to provide an extra level of error-checking in case you enter a typo.","title":"sellgains"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.sellgains.SellGainsError","text":"SellGainsError(source, message, entry)","title":"SellGainsError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.sellgains.SellGainsError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/sellgains.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.sellgains.SellGainsError.__new__","text":"Create new instance of SellGainsError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.sellgains.SellGainsError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/sellgains.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.sellgains.validate_sell_gains","text":"Check the sum of asset account totals for lots sold with a price on them. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/sellgains.py def validate_sell_gains(entries, options_map): \"\"\"Check the sum of asset account totals for lots sold with a price on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] acc_types = options.get_account_types(options_map) proceed_types = set([acc_types.assets, acc_types.liabilities, acc_types.equity, acc_types.expenses]) for entry in entries: if not isinstance(entry, data.Transaction): continue # Find transactions whose lots at cost all have a price. postings_at_cost = [posting for posting in entry.postings if posting.cost is not None] if not postings_at_cost or not all(posting.price is not None for posting in postings_at_cost): continue # Accumulate the total expected proceeds and the sum of the asset and # expenses legs. total_price = inventory.Inventory() total_proceeds = inventory.Inventory() for posting in entry.postings: # If the posting is held at cost, add the priced value to the balance. if posting.cost is not None: assert posting.price is not None price = posting.price total_price.add_amount(amount.mul(price, -posting.units.number)) else: # Otherwise, use the weight and ignore postings to Income accounts. atype = account_types.get_account_type(posting.account) if atype in proceed_types: total_proceeds.add_amount(convert.get_weight(posting)) # Compare inventories, currency by currency. dict_price = {pos.units.currency: pos.units.number for pos in total_price} dict_proceeds = {pos.units.currency: pos.units.number for pos in total_proceeds} tolerances = interpolate.infer_tolerances(entry.postings, options_map) invalid = False for currency, price_number in dict_price.items(): # Accept a looser than usual tolerance because rounding occurs # differently. Also, it would be difficult for the user to satisfy # two sets of constraints manually. tolerance = tolerances.get(currency) * EXTRA_TOLERANCE_MULTIPLIER proceeds_number = dict_proceeds.pop(currency, ZERO) diff = abs(price_number - proceeds_number) if diff > tolerance: invalid = True break if invalid or dict_proceeds: errors.append( SellGainsError( entry.meta, \"Invalid price vs. proceeds/gains: {} vs. {}\".format( total_price, total_proceeds), entry)) return entries, errors","title":"validate_sell_gains()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.split_expenses","text":"Split expenses of a Beancount ledger between multiple people. This plugin is given a list of names. It assumes that any Expenses account whose components do not include any of the given names are to be split between the members. It goes through all the transactions and converts all such postings into multiple postings, one for each member. For example, given the names 'Martin' and 'Caroline', the following transaction: 2015-02-01 * \"Aqua Viva Tulum - two nights\" Income:Caroline:CreditCard -269.00 USD Expenses:Accommodation Will be converted to this: 2015-02-01 * \"Aqua Viva Tulum - two nights\" Income:Caroline:CreditCard -269.00 USD Expenses:Accommodation:Martin 134.50 USD Expenses:Accommodation:Caroline 134.50 USD After these transformations, all account names should include the name of a member. You can generate reports for a particular person by filtering postings to accounts with a component by their name.","title":"split_expenses"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.split_expenses.get_participants","text":"Get the list of participants from the plugin configuration in the input file. Parameters: options_map \u2013 The options map, as produced by the parser. Returns: A list of strings, the names of participants as they should appear in the account names. Exceptions: KeyError \u2013 If the configuration does not contain configuration for the list Source code in beancount/plugins/split_expenses.py def get_participants(filename, options_map): \"\"\"Get the list of participants from the plugin configuration in the input file. Args: options_map: The options map, as produced by the parser. Returns: A list of strings, the names of participants as they should appear in the account names. Raises: KeyError: If the configuration does not contain configuration for the list of participants. \"\"\" plugin_options = dict(options_map[\"plugin\"]) try: return plugin_options[\"beancount.plugins.split_expenses\"].split() except KeyError: raise KeyError(\"Could not find the split_expenses plugin configuration.\")","title":"get_participants()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.split_expenses.main","text":"Generate final reports for a shared expenses on a trip or project. For each of many participants, generate a detailed list of expenses, contributions, a categorized summary of expenses, and a final balance. Also produce a global list of final balances so that participants can reconcile between each other. Source code in beancount/plugins/split_expenses.py def main(): \"\"\"Generate final reports for a shared expenses on a trip or project. For each of many participants, generate a detailed list of expenses, contributions, a categorized summary of expenses, and a final balance. Also produce a global list of final balances so that participants can reconcile between each other. \"\"\" logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s') parser = version.ArgumentParser(description=__doc__.strip()) parser.add_argument('filename', help='Beancount input filename') parser.add_argument('-c', '--currency', action='store', help=\"Convert all the amounts to a single common currency\") oparser = parser.add_argument_group('Outputs') oparser.add_argument('-o', '--output-text', '--text', action='store', help=\"Render results to text boxes\") oparser.add_argument('--output-csv', '--csv', action='store', help=\"Render results to CSV files\") oparser.add_argument('--output-stdout', '--stdout', action='store_true', help=\"Render results to stdout\") args = parser.parse_args() # Ensure the directories exist. for directory in [args.output_text, args.output_csv]: if directory and not path.exists(directory): os.makedirs(directory, exist_ok=True) # Load the input file and get the list of participants. entries, errors, options_map = loader.load_file(args.filename) participants = get_participants(args.filename, options_map) for participant in participants: print(\"Participant: {}\".format(participant)) save_query(\"balances\", participant, entries, options_map, r\"\"\" SELECT PARENT(account) AS account, CONV[SUM(position)] AS amount WHERE account ~ ':\\b{}' GROUP BY 1 ORDER BY 2 DESC \"\"\", participant, boxed=False, args=args) save_query(\"expenses\", participant, entries, options_map, r\"\"\" SELECT date, flag, description, PARENT(account) AS account, JOINSTR(links) AS links, CONV[position] AS amount, CONV[balance] AS balance WHERE account ~ 'Expenses.*\\b{}' \"\"\", participant, args=args) save_query(\"income\", participant, entries, options_map, r\"\"\" SELECT date, flag, description, account, JOINSTR(links) AS links, CONV[position] AS amount, CONV[balance] AS balance WHERE account ~ 'Income.*\\b{}' \"\"\", participant, args=args) save_query(\"final\", None, entries, options_map, r\"\"\" SELECT GREP('\\b({})\\b', account) AS participant, CONV[SUM(position)] AS balance GROUP BY 1 ORDER BY 2 \"\"\", '|'.join(participants), args=args) # FIXME: Make this output to CSV files and upload to a spreadsheet. # FIXME: Add a fixed with option. This requires changing adding this to the # the renderer to be able to have elastic space and line splitting..","title":"main()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.split_expenses.save_query","text":"Save the multiple files for this query. Parameters: title \u2013 A string, the title of this particular report to render. participant \u2013 A string, the name of the participant under consideration. entries \u2013 A list of directives (as per the loader). options_map \u2013 A dict of options (as per the loader). sql_query \u2013 A string with the SQL query, possibly with some placeholders left for *format_args to replace. *format_args \u2013 A tuple of arguments to be formatted into the SQL query string. This is provided as a convenience. boxed \u2013 A boolean, true if we should render the results in a fancy-looking ASCII box. spaced \u2013 If true, leave an empty line between each of the rows. This is useful if the results have a lot of rows that render over multiple lines. args \u2013 A dummy object with the following attributes: output_text: An optional directory name, to produce a text rendering of the report. output_csv: An optional directory name, to produce a CSV rendering of the report. output_stdout: A boolean, if true, also render the output to stdout. currency: An optional currency (a string). If you use this, you should wrap query targets to be converted with the pseudo-function \"CONV[...]\" and it will get replaced to CONVERT(..., CURRENCY) automatically. Source code in beancount/plugins/split_expenses.py def save_query(title, participant, entries, options_map, sql_query, *format_args, boxed=True, spaced=False, args=None): \"\"\"Save the multiple files for this query. Args: title: A string, the title of this particular report to render. participant: A string, the name of the participant under consideration. entries: A list of directives (as per the loader). options_map: A dict of options (as per the loader). sql_query: A string with the SQL query, possibly with some placeholders left for *format_args to replace. *format_args: A tuple of arguments to be formatted into the SQL query string. This is provided as a convenience. boxed: A boolean, true if we should render the results in a fancy-looking ASCII box. spaced: If true, leave an empty line between each of the rows. This is useful if the results have a lot of rows that render over multiple lines. args: A dummy object with the following attributes: output_text: An optional directory name, to produce a text rendering of the report. output_csv: An optional directory name, to produce a CSV rendering of the report. output_stdout: A boolean, if true, also render the output to stdout. currency: An optional currency (a string). If you use this, you should wrap query targets to be converted with the pseudo-function \"CONV[...]\" and it will get replaced to CONVERT(..., CURRENCY) automatically. \"\"\" # Replace CONV() to convert the currencies or not; if so, replace to # CONVERT(..., currency). replacement = (r'\\1' if args.currency is None else r'CONVERT(\\1, \"{}\")'.format(args.currency)) sql_query = re.sub(r'CONV\\[(.*?)\\]', replacement, sql_query) # Run the query. rtypes, rrows = query.run_query(entries, options_map, sql_query, *format_args, numberify=True) # The base of all filenames. filebase = title.replace(' ', '_') fmtopts = dict(boxed=boxed, spaced=spaced) # Output the text files. if args.output_text: basedir = (path.join(args.output_text, participant) if participant else args.output_text) os.makedirs(basedir, exist_ok=True) filename = path.join(basedir, filebase + '.txt') with open(filename, 'w') as file: query_render.render_text(rtypes, rrows, options_map['dcontext'], file, **fmtopts) # Output the CSV files. if args.output_csv: basedir = (path.join(args.output_csv, participant) if participant else args.output_csv) os.makedirs(basedir, exist_ok=True) filename = path.join(basedir, filebase + '.csv') with open(filename, 'w') as file: query_render.render_csv(rtypes, rrows, options_map['dcontext'], file, expand=False) if args.output_stdout: # Write out the query to stdout. query_render.render_text(rtypes, rrows, options_map['dcontext'], sys.stdout, **fmtopts)","title":"save_query()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.split_expenses.split_expenses","text":"Split postings according to expenses (see module docstring for details). Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. config \u2013 The plugin configuration string. Returns: A list of entries, with potentially more accounts and potentially more postings with smaller amounts. Source code in beancount/plugins/split_expenses.py def split_expenses(entries, options_map, config): \"\"\"Split postings according to expenses (see module docstring for details). Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. config: The plugin configuration string. Returns: A list of entries, with potentially more accounts and potentially more postings with smaller amounts. \"\"\" # Validate and sanitize configuration. if isinstance(config, str): members = config.split() elif isinstance(config, (tuple, list)): members = config else: raise RuntimeError(\"Invalid plugin configuration: configuration for split_expenses \" \"should be a string or a sequence.\") acctypes = options.get_account_types(options_map) def is_expense_account(account): return account_types.get_account_type(account) == acctypes.expenses # A predicate to quickly identify if an account contains the name of a # member. is_individual_account = re.compile('|'.join(map(re.escape, members))).search # Existing and previously unseen accounts. new_accounts = set() # Filter the entries and transform transactions. new_entries = [] for entry in entries: if isinstance(entry, data.Transaction): new_postings = [] for posting in entry.postings: if (is_expense_account(posting.account) and not is_individual_account(posting.account)): # Split this posting into multiple postings. split_units = amount.Amount(posting.units.number / len(members), posting.units.currency) for member in members: # Mark the account as new if never seen before. subaccount = account.join(posting.account, member) new_accounts.add(subaccount) # Ensure the modified postings are marked as # automatically calculated, so that the resulting # calculated amounts aren't used to affect inferred # tolerances. meta = posting.meta.copy() if posting.meta else {} meta[interpolate.AUTOMATIC_META] = True # Add a new posting for each member, to a new account # with the name of this member. new_postings.append( posting._replace(meta=meta, account=subaccount, units=split_units, cost=posting.cost)) else: new_postings.append(posting) # Modify the entry in-place, replace its postings. entry = entry._replace(postings=new_postings) new_entries.append(entry) # Create Open directives for new subaccounts if necessary. oc_map = getters.get_account_open_close(entries) open_date = entries[0].date meta = data.new_metadata('', 0) open_entries = [] for new_account in new_accounts: if new_account not in oc_map: entry = data.Open(meta, open_date, new_account, None, None) open_entries.append(entry) return open_entries + new_entries, []","title":"split_expenses()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.tag_pending","text":"An example of tracking unpaid payables or receivables. A user with lots of invoices to track may want to produce a report of pending or incomplete payables or receivables. Beancount does not by default offer such a dedicated feature, but it is easy to build one by using existing link attributes on transactions. This is an example on how to implement that with a plugin. For example, assuming the user enters linked transactions like this: 2013-03-28 * \"Bill for datacenter electricity\" ^invoice-27a30ab61191 Expenses:Electricity 450.82 USD Liabilities:AccountsPayable 2013-04-15 * \"Paying electricity company\" ^invoice-27a30ab61191 Assets:Checking -450.82 USD Liabilities:AccountsPayable Transactions are grouped by link (\"invoice-27a30ab61191\") and then the intersection of their common accounts is automatically calculated (\"Liabilities:AccountsPayable\"). We then add up the balance of all the postings for this account in this link group and check if the sum is zero. If there is a residual amount in this balance, we mark the associated entries as incomplete by inserting a #PENDING tag on them. The user can then use that tag to navigate to the corresponding view in the web interface, or just find the entries and produce a listing of them.","title":"tag_pending"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.tag_pending.tag_pending_plugin","text":"A plugin that finds and tags pending transactions. Parameters: entries \u2013 A list of entry instances. options_map \u2013 A dict of options parsed from the file. Returns: A tuple of entries and errors. Source code in beancount/plugins/tag_pending.py def tag_pending_plugin(entries, options_map): \"\"\"A plugin that finds and tags pending transactions. Args: entries: A list of entry instances. options_map: A dict of options parsed from the file. Returns: A tuple of entries and errors. \"\"\" return (tag_pending_transactions(entries, 'PENDING'), [])","title":"tag_pending_plugin()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.tag_pending.tag_pending_transactions","text":"Filter out incomplete linked transactions to a transfer account. Given a list of entries, group the entries by their link and compute the balance of the intersection of their common accounts. If the balance does not sum to zero, insert a 'tag_name' tag in the entries. Parameters: entries \u2013 A list of directives/transactions to process. tag_name \u2013 A string, the name of the tag to be inserted if a linked group of entries is found not to match Returns: A modified set of entries, possibly tagged as pending. Source code in beancount/plugins/tag_pending.py def tag_pending_transactions(entries, tag_name='PENDING'): \"\"\"Filter out incomplete linked transactions to a transfer account. Given a list of entries, group the entries by their link and compute the balance of the intersection of their common accounts. If the balance does not sum to zero, insert a 'tag_name' tag in the entries. Args: entries: A list of directives/transactions to process. tag_name: A string, the name of the tag to be inserted if a linked group of entries is found not to match Returns: A modified set of entries, possibly tagged as pending. \"\"\" link_groups = basicops.group_entries_by_link(entries) pending_entry_ids = set() for link, link_entries in link_groups.items(): assert link_entries if len(link_entries) == 1: # If a single entry is present, it is assumed incomplete. pending_entry_ids.add(id(link_entries[0])) else: # Compute the sum total balance of the common accounts. common_accounts = basicops.get_common_accounts(link_entries) common_balance = inventory.Inventory() for entry in link_entries: for posting in entry.postings: if posting.account in common_accounts: common_balance.add_position(posting) # Mark entries as pending if a residual balance is found. if not common_balance.is_empty(): for entry in link_entries: pending_entry_ids.add(id(entry)) # Insert tags if marked. return [(entry._replace(tags=(entry.tags or set()) | set((tag_name,))) if id(entry) in pending_entry_ids else entry) for entry in entries]","title":"tag_pending_transactions()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unique_prices","text":"This module adds validation that there is a single price defined per date and base/quote currencies. If multiple conflicting price values are declared, an error is generated. Note that multiple price entries with the same number do not generate an error. This is meant to be turned on if you want to use a very strict mode for entering prices, and may not be realistic usage. For example, if you have (1) a transaction with an implicitly generated price during the day (from its cost) and (2) a separate explicit price directive that declares a different price for the day's closing price, this would generate an error. I'm not certain this will be useful in the long run, so placing it in a plugin.","title":"unique_prices"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unique_prices.UniquePricesError","text":"UniquePricesError(source, message, entry)","title":"UniquePricesError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unique_prices.UniquePricesError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/unique_prices.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unique_prices.UniquePricesError.__new__","text":"Create new instance of UniquePricesError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unique_prices.UniquePricesError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/unique_prices.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unique_prices.validate_unique_prices","text":"Check that there is only a single price per day for a particular base/quote. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. Returns: The list of input entries, and a list of new UniquePricesError instances generated. Source code in beancount/plugins/unique_prices.py def validate_unique_prices(entries, unused_options_map): \"\"\"Check that there is only a single price per day for a particular base/quote. Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. Returns: The list of input entries, and a list of new UniquePricesError instances generated. \"\"\" new_entries = [] errors = [] prices = collections.defaultdict(list) for entry in entries: if not isinstance(entry, data.Price): continue key = (entry.date, entry.currency, entry.amount.currency) prices[key].append(entry) errors = [] for price_entries in prices.values(): if len(price_entries) > 1: number_map = {price_entry.amount.number: price_entry for price_entry in price_entries} if len(number_map) > 1: # Note: This should be a list of entries for better error # reporting. (Later.) error_entry = next(iter(number_map.values())) errors.append( UniquePricesError(error_entry.meta, \"Disagreeing price entries\", price_entries)) return new_entries, errors","title":"validate_unique_prices()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unrealized","text":"Compute unrealized gains. The configuration for this plugin is a single string, the name of the subaccount to add to post the unrealized gains to, like this: plugin \"beancount.plugins.unrealized\" \"Unrealized\" If you don't specify a name for the subaccount (the configuration value is optional), by default it inserts the unrealized gains in the same account that is being adjusted.","title":"unrealized"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unrealized.UnrealizedError","text":"UnrealizedError(source, message, entry)","title":"UnrealizedError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unrealized.UnrealizedError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/unrealized.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unrealized.UnrealizedError.__new__","text":"Create new instance of UnrealizedError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unrealized.UnrealizedError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/unrealized.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unrealized.add_unrealized_gains","text":"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. Parameters: entries \u2013 A list of data directives. options_map \u2013 A dict of options, that confirms to beancount.parser.options. subaccount \u2013 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. Source code in beancount/plugins/unrealized.py 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('', 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)","title":"add_unrealized_gains()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unrealized.get_unrealized_entries","text":"Return entries automatically created for unrealized gains. Parameters: entries \u2013 A list of directives. Returns: A list of directives, all of which are in the original list. Source code in beancount/plugins/unrealized.py def get_unrealized_entries(entries): \"\"\"Return entries automatically created for unrealized gains. Args: entries: A list of directives. Returns: A list of directives, all of which are in the original list. \"\"\" return [entry for entry in entries if (isinstance(entry, data.Transaction) and entry.flag == flags.FLAG_UNREALIZED)]","title":"get_unrealized_entries()"},{"location":"api_reference/beancount.prices.html","text":"beancount.prices \uf0c1 Fetch prices from the internet and output them as Beancount price directives. This script accepts a list of Beancount input filenames, and fetches prices required to compute market values for current positions: bean-price /home/joe/finances/joe.beancount The list of fetching jobs to carry out is derived automatically from the input file (see section below for full details). It is also possible to provide a list of specific price fetching jobs to run, e.g., bean-price -e google/TSE:XUS yahoo/AAPL mysources.morningstar/RBF1005 The general format of each of these \"source strings\" is /[^] The \"module\" is the name of a Python module that contains a Source class which can be instantiated and connect to a data source to extract price data. These modules are automatically imported by name and instantiated in order to pull the price from a particular data source. This allows you to write your own supplementary fetcher codes without having to modify this script. Default implementations are provided to provide access to prices from Yahoo! Finance or Google Finance, which cover a large universe of common public investment types (e.g. stock tickers). As a convenience, the module name is always first searched under the \"beancount.prices.sources\" package, where those default source implementations live. This is how, for example, in order to use the provided Google Finance data fetcher you don't have to write \"beancount.prices.sources.yahoo/AAPL\" but simply \"yahoo/AAPL\". Date \uf0c1 By default, this script will fetch prices at the latest available date & time. You can use an option to fetch historical prices for a desired date instead: bean-price --date=2015-02-03 Inverse \uf0c1 Sometimes, prices are available for the inverse of an instrument. This is often the case for currencies. For example, the price of \"CAD\" in USD\" is provided by the USD/CAD market, which gives the price of a US dollar in Canadian dollars. In order specify this, you can prepend \"^\" to the instrument to instruct the driver to compute the inverse of the given price: bean-price -e USD:google/^CURRENCY:USDCAD If a source price is to be inverted, like this, the precision could be different than what is fetched. For instance, if the price of USD/CAD is 1.32759, it would output be this from the above directive: 2015-10-28 price CAD 0.753244601119 USD By default, inverted rates will be rounded similarly to how other Price directives were rounding those numbers. Swap Inverted \uf0c1 If you prefer to have the output Price entries with swapped currencies instead of inverting the rate itself, you can use the --swap-inverted option. In the previous example for the price of CAD, it would output this: 2015-10-28 price USD 1.32759 CAD This works since the Beancount price database computes and interpolates the reciprocals automatically for all pairs of commodities in its database. Prices Needed for a Beancount File \uf0c1 You can also provide a filename to extract the list of tickers to fetch from a Beancount input file, e.g.: bean-price /home/joe/finances/joe.beancount There are many ways to extract a list of commodities with needed prices from a Beancount input file: Prices for all the holdings that were seen held-at-cost at a particular date. Prices for holdings held at a particular date which were price converted from some other commodity in the past (i.e., for currencies). The list of all Commodity directives present in the file. For each of those holdings, the corresponding Commodity directive is consulted and its \"ticker\" metadata field is used to specify where to attempt to fetch prices. You should have directives like this in your input file: 2007-07-20 commodity VEA price: \"google/NYSEARCA:VEA\" The \"price\" metadata can be a comma-separated list of sources to try out, in which case each of the sources will be looked at : 2007-07-20 commodity VEA price: \"google/CURRENCY:USDCAD,yahoo/USDCAD\" Existing price directives for the same data are excluded by default, since the price is already in the file. By default, the list of tickers to be fetched includes only the intersection of these lists. The general intent of the user of this script is to fetch missing prices, and only needed ones, for a particular date. Use the --date option to change the applied date. Use the --all option to fetch the entire set of prices, regardless of holdings and date. Use --clobber to ignore existing price directives. You can also print the list of prices to be fetched with the --dry-run option, which stops short of actually fetching the missing prices (it just prints the list of fetches it would otherwise attempt). Caching \uf0c1 Prices are automatically cached. You can disable the cache with an option: bean-price --no-cache You can also instruct the script to clear the cache before fetching its prices: bean-price --clear-cache About Sources and Data Availability \uf0c1 IMPORTANT: Note that each source may support a different routine for getting its latest data and for fetching historical/dated data, and that each of these may differ in their support. For example, Google Finance does not support fetching historical data for its CURRENCY:* instruments. beancount.prices.find_prices \uf0c1 A library of codes create price fetching jobs from strings and files. beancount.prices.find_prices.DatedPrice ( tuple ) \uf0c1 DatedPrice(base, quote, date, sources) beancount.prices.find_prices.DatedPrice.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/prices/find_prices.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.prices.find_prices.DatedPrice.__new__(_cls, base, quote, date, sources) special staticmethod \uf0c1 Create new instance of DatedPrice(base, quote, date, sources) beancount.prices.find_prices.DatedPrice.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/prices/find_prices.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.prices.find_prices.PriceSource ( tuple ) \uf0c1 PriceSource(module, symbol, invert) beancount.prices.find_prices.PriceSource.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/prices/find_prices.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.prices.find_prices.PriceSource.__new__(_cls, module, symbol, invert) special staticmethod \uf0c1 Create new instance of PriceSource(module, symbol, invert) beancount.prices.find_prices.PriceSource.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/prices/find_prices.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.prices.find_prices.find_balance_currencies(entries, date=None) \uf0c1 Return currencies relevant for the given date. This computes the account balances as of the date, and returns the union of: a) The currencies held at cost, and b) Currency pairs from previous conversions, but only for currencies with non-zero balances. This is intended to produce the list of currencies whose prices are relevant at a particular date, based on previous history. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A set of (base, quote) currencies. Source code in beancount/prices/find_prices.py def find_balance_currencies(entries, date=None): \"\"\"Return currencies relevant for the given date. This computes the account balances as of the date, and returns the union of: a) The currencies held at cost, and b) Currency pairs from previous conversions, but only for currencies with non-zero balances. This is intended to produce the list of currencies whose prices are relevant at a particular date, based on previous history. Args: entries: A list of directives. date: A datetime.date instance. Returns: A set of (base, quote) currencies. \"\"\" # Compute the balances. currencies = set() currencies_on_books = set() balances, _ = summarize.balance_by_account(entries, date) for _, balance in balances.items(): for pos in balance: if pos.cost is not None: # Add currencies held at cost. currencies.add((pos.units.currency, pos.cost.currency)) else: # Add regular currencies. currencies_on_books.add(pos.units.currency) # Create currency pairs from the currencies which are on account balances. # In order to figure out the quote currencies, we use the list of price # conversions until this date. converted = (find_currencies_converted(entries, date) | find_currencies_priced(entries, date)) for cbase in currencies_on_books: for base_quote in converted: base, quote = base_quote if base == cbase: currencies.add(base_quote) return currencies beancount.prices.find_prices.find_currencies_at_cost(entries) \uf0c1 Return all currencies that were held at cost at some point. This returns all of them, even if not on the books at a particular point in time. This code does not look at account balances. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote) currencies. Source code in beancount/prices/find_prices.py def find_currencies_at_cost(entries): \"\"\"Return all currencies that were held at cost at some point. This returns all of them, even if not on the books at a particular point in time. This code does not look at account balances. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote) currencies. \"\"\" currencies = set() for entry in entries: if not isinstance(entry, data.Transaction): continue for posting in entry.postings: if posting.cost is not None and posting.cost.number is not None: currencies.add((posting.units.currency, posting.cost.currency)) return currencies beancount.prices.find_prices.find_currencies_converted(entries, date=None) \uf0c1 Return currencies from price conversions. This function looks at all price conversions that occurred until some date and produces a list of them. Note: This does not include Price directives, only postings with price conversions. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote) currencies. Source code in beancount/prices/find_prices.py def find_currencies_converted(entries, date=None): \"\"\"Return currencies from price conversions. This function looks at all price conversions that occurred until some date and produces a list of them. Note: This does not include Price directives, only postings with price conversions. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote) currencies. \"\"\" currencies = set() for entry in entries: if not isinstance(entry, data.Transaction): continue if date and entry.date >= date: break for posting in entry.postings: price = posting.price if posting.cost is not None or price is None: continue currencies.add((posting.units.currency, price.currency)) return currencies beancount.prices.find_prices.find_currencies_declared(entries, date=None) \uf0c1 Return currencies declared in Commodity directives. If a 'price' metadata field is provided, include all the quote currencies there-in. Otherwise, the Commodity directive is ignored. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote, list of PriceSource) currencies. The list of (base, quote) pairs is guaranteed to be unique. Source code in beancount/prices/find_prices.py def find_currencies_declared(entries, date=None): \"\"\"Return currencies declared in Commodity directives. If a 'price' metadata field is provided, include all the quote currencies there-in. Otherwise, the Commodity directive is ignored. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote, list of PriceSource) currencies. The list of (base, quote) pairs is guaranteed to be unique. \"\"\" currencies = [] for entry in entries: if not isinstance(entry, data.Commodity): continue if date and entry.date >= date: break # Here we have to infer which quote currencies the commodity is for # (maybe down the road this should be better handled by providing a list # of quote currencies in the Commodity directive itself). # # First, we look for a \"price\" metadata field, which defines conversions # for various currencies. Each of these quote currencies generates a # pair in the output. source_str = entry.meta.get('price', None) if source_str is not None: if source_str == \"\": logging.debug(\"Skipping ignored currency (with empty price): %s\", entry.currency) continue try: source_map = parse_source_map(source_str) except ValueError: logging.warning(\"Ignoring currency with invalid 'price' source: %s\", entry.currency) else: for quote, psources in source_map.items(): currencies.append((entry.currency, quote, psources)) else: # Otherwise we simply ignore the declaration. That is, a Commodity # directive without any \"price\" metadata would not register as a # declared currency. logging.debug(\"Ignoring currency with no metadata: %s\", entry.currency) return currencies beancount.prices.find_prices.find_currencies_priced(entries, date=None) \uf0c1 Return currencies seen in Price directives. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote) currencies. Source code in beancount/prices/find_prices.py def find_currencies_priced(entries, date=None): \"\"\"Return currencies seen in Price directives. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote) currencies. \"\"\" currencies = set() for entry in entries: if not isinstance(entry, data.Price): continue if date and entry.date >= date: break currencies.add((entry.currency, entry.amount.currency)) return currencies beancount.prices.find_prices.format_dated_price_str(dprice) \uf0c1 Convert a dated price to a one-line printable string. Parameters: dprice \u2013 A DatedPrice instance. Returns: The string for a DatedPrice instance. Source code in beancount/prices/find_prices.py def format_dated_price_str(dprice): \"\"\"Convert a dated price to a one-line printable string. Args: dprice: A DatedPrice instance. Returns: The string for a DatedPrice instance. \"\"\" psstrs = ['{}({}{})'.format(psource.module.__name__, '1/' if psource.invert else '', psource.symbol) for psource in dprice.sources] base_quote = '{} /{}'.format(dprice.base, dprice.quote) return '{:<32} @ {:10} [ {} ]'.format( base_quote, dprice.date.isoformat() if dprice.date else 'latest', ','.join(psstrs)) beancount.prices.find_prices.get_price_jobs_at_date(entries, date=None, inactive=False, undeclared_source=None) \uf0c1 Get a list of prices to fetch from a stream of entries. The active holdings held on the given date are included. Parameters: filename \u2013 A string, the name of a file to process. date \u2013 A datetime.date instance. inactive \u2013 Include currencies with no balance at the given date. The default is to only include those currencies which have a non-zero balance. undeclared_source \u2013 A string, the name of the default source module to use to pull prices for commodities without a price source metadata on their Commodity directive declaration. Returns: A list of DatedPrice instances. Source code in beancount/prices/find_prices.py def get_price_jobs_at_date(entries, date=None, inactive=False, undeclared_source=None): \"\"\"Get a list of prices to fetch from a stream of entries. The active holdings held on the given date are included. Args: filename: A string, the name of a file to process. date: A datetime.date instance. inactive: Include currencies with no balance at the given date. The default is to only include those currencies which have a non-zero balance. undeclared_source: A string, the name of the default source module to use to pull prices for commodities without a price source metadata on their Commodity directive declaration. Returns: A list of DatedPrice instances. \"\"\" # Find the list of declared currencies, and from it build a mapping for # tickers for each (base, quote) pair. This is the only place tickers # appear. declared_triples = find_currencies_declared(entries, date) currency_map = {(base, quote): psources for base, quote, psources in declared_triples} # Compute the initial list of currencies to consider. if undeclared_source: # Use the full set of possible currencies. cur_at_cost = find_currencies_at_cost(entries) cur_converted = find_currencies_converted(entries, date) cur_priced = find_currencies_priced(entries, date) currencies = cur_at_cost | cur_converted | cur_priced log_currency_list(\"Currency held at cost\", cur_at_cost) log_currency_list(\"Currency converted\", cur_converted) log_currency_list(\"Currency priced\", cur_priced) default_source = import_source(undeclared_source) else: # Use the currencies from the Commodity directives. currencies = set(currency_map.keys()) default_source = None log_currency_list(\"Currencies in primary list\", currencies) # By default, restrict to only the currencies with non-zero balances at the # given date. if not inactive: balance_currencies = find_balance_currencies(entries, date) log_currency_list(\"Currencies held in assets\", balance_currencies) currencies = currencies & balance_currencies log_currency_list(\"Currencies to fetch\", currencies) # Build up the list of jobs to fetch prices for. jobs = [] for base_quote in currencies: psources = currency_map.get(base_quote, None) base, quote = base_quote # If there are no sources, create a default one. if not psources: psources = [PriceSource(default_source, base, False)] jobs.append(DatedPrice(base, quote, date, psources)) return sorted(jobs) beancount.prices.find_prices.import_source(module_name) \uf0c1 Import the source module defined by the given name. The default location is handled here. Parameters: short_module_name \u2013 A string, the name of a Python module, which may be within the default package or a full name. Returns: A corresponding Python module object. Exceptions: ImportError \u2013 If the module cannot be imported. Source code in beancount/prices/find_prices.py def import_source(module_name): \"\"\"Import the source module defined by the given name. The default location is handled here. Args: short_module_name: A string, the name of a Python module, which may be within the default package or a full name. Returns: A corresponding Python module object. Raises: ImportError: If the module cannot be imported. \"\"\" default_name = '{}.{}'.format(DEFAULT_PACKAGE, module_name) try: __import__(default_name) return sys.modules[default_name] except ImportError: try: __import__(module_name) return sys.modules[module_name] except ImportError as exc: raise ImportError('Could not find price source module \"{}\": {}'.format( module_name, exc)) beancount.prices.find_prices.log_currency_list(message, currencies) \uf0c1 Log a list of currencies to debug output. Parameters: message \u2013 A message string to prepend. currencies \u2013 A list of (base, quote) currency pair. Source code in beancount/prices/find_prices.py def log_currency_list(message, currencies): \"\"\"Log a list of currencies to debug output. Args: message: A message string to prepend. currencies: A list of (base, quote) currency pair. \"\"\" logging.debug(\"-------- {}:\".format(message)) for base, quote in currencies: logging.debug(\" {:>32}\".format('{} /{}'.format(base, quote))) beancount.prices.find_prices.parse_single_source(source) \uf0c1 Parse a single source string. Source specifications follow the syntax: /[^] The is resolved against the Python path, but first looked up under the package where the default price extractors lie. Parameters: source \u2013 A single source string specification. Returns: A PriceSource tuple, or Exceptions: ValueError \u2013 If invalid. Source code in beancount/prices/find_prices.py def parse_single_source(source): \"\"\"Parse a single source string. Source specifications follow the syntax: /[^] The is resolved against the Python path, but first looked up under the package where the default price extractors lie. Args: source: A single source string specification. Returns: A PriceSource tuple, or Raises: ValueError: If invalid. \"\"\" match = re.match(r'([a-zA-Z]+[a-zA-Z0-9\\._]+)/(\\^?)([a-zA-Z0-9:=_\\-\\.]+)$', source) if not match: raise ValueError('Invalid source name: \"{}\"'.format(source)) short_module_name, invert, symbol = match.groups() module = import_source(short_module_name) return PriceSource(module, symbol, bool(invert)) beancount.prices.find_prices.parse_source_map(source_map_spec) \uf0c1 Parse a source map specification string. Source map specifications allow the specification of multiple sources for multiple quote currencies and follow the following syntax: :,,... :,... Where a itself follows: /[^] The is resolved against the Python path, but first looked up under the package where the default price extractors lie. The presence of a '^' character indicates that we should use the inverse of the rate pull from this source. For example, for prices of AAPL in USD: USD:google/NASDAQ:AAPL,yahoo/AAPL Or for the exchange rate of a currency, such as INR in USD or in CAD: USD:google/^CURRENCY:USDINR CAD:google/^CURRENCY:CADINR Parameters: source_map_spec \u2013 A string, a full source map specification to be parsed. Returns: FIXME \u2013 TODO Exceptions: ValueError \u2013 If an invalid pattern has been specified. Source code in beancount/prices/find_prices.py def parse_source_map(source_map_spec): \"\"\"Parse a source map specification string. Source map specifications allow the specification of multiple sources for multiple quote currencies and follow the following syntax: :,,... :,... Where a itself follows: /[^] The is resolved against the Python path, but first looked up under the package where the default price extractors lie. The presence of a '^' character indicates that we should use the inverse of the rate pull from this source. For example, for prices of AAPL in USD: USD:google/NASDAQ:AAPL,yahoo/AAPL Or for the exchange rate of a currency, such as INR in USD or in CAD: USD:google/^CURRENCY:USDINR CAD:google/^CURRENCY:CADINR Args: source_map_spec: A string, a full source map specification to be parsed. Returns: FIXME: TODO Raises: ValueError: If an invalid pattern has been specified. \"\"\" source_map = collections.defaultdict(list) for source_list_spec in re.split('[ ;]', source_map_spec): match = re.match('({}):(.*)$'.format(amount.CURRENCY_RE), source_list_spec) if not match: raise ValueError('Invalid source map pattern: \"{}\"'.format(source_list_spec)) currency, source_strs = match.groups() source_map[currency].extend( parse_single_source(source_str) for source_str in source_strs.split(',')) return source_map beancount.prices.price \uf0c1 Driver code for the price script. beancount.prices.price.fetch_cached_price(source, symbol, date) \uf0c1 Call Source to fetch a price, but look and/or update the cache first. This function entirely deals with caching and correct expiration. It keeps old prices if they were fetched in the past, and it quickly expires intra-day prices if they are fetched on the same day. Parameters: source \u2013 A Python module object. symbol \u2013 A string, the ticker to fetch. date \u2013 A datetime.date instance, None if we're to fetch the latest date. Returns: A SourcePrice instance. Source code in beancount/prices/price.py def fetch_cached_price(source, symbol, date): \"\"\"Call Source to fetch a price, but look and/or update the cache first. This function entirely deals with caching and correct expiration. It keeps old prices if they were fetched in the past, and it quickly expires intra-day prices if they are fetched on the same day. Args: source: A Python module object. symbol: A string, the ticker to fetch. date: A datetime.date instance, None if we're to fetch the latest date. Returns: A SourcePrice instance. \"\"\" # Compute a suitable timestamp from the date, if specified. if date is not None: # We query as for 4pm for the given date of the current timezone, if # specified. query_time = datetime.time(16, 0, 0) time_local = datetime.datetime.combine(date, query_time, tzinfo=tz.tzlocal()) time = time_local.astimezone(tz.tzutc()) else: time = None if _CACHE is None: # The cache is disabled; just call and return. result = (source.get_latest_price(symbol) if time is None else source.get_historical_price(symbol, time)) else: # The cache is enabled and we have to compute the current/latest price. # Try to fetch from the cache but miss if the price is too old. md5 = hashlib.md5() md5.update(str((type(source).__module__, symbol, date)).encode('utf-8')) key = md5.hexdigest() timestamp_now = int(now().timestamp()) try: timestamp_created, result_naive = _CACHE[key] # Convert naive timezone to UTC, which is what the cache is always # assumed to store. (The reason for this is that timezones from # aware datetime objects cannot be serialized properly due to bug.) if result_naive.time is not None: result = result_naive._replace( time=result_naive.time.replace(tzinfo=tz.tzutc())) else: result = result_naive if (timestamp_now - timestamp_created) > _CACHE.expiration.total_seconds(): raise KeyError except KeyError: logging.info(\"Fetching: %s (time: %s)\", symbol, time) try: result = (source.get_latest_price(symbol) if time is None else source.get_historical_price(symbol, time)) except ValueError as exc: logging.error(\"Error fetching %s: %s\", symbol, exc) result = None # Make sure the timezone is UTC and make naive before serialization. if result and result.time is not None: time_utc = result.time.astimezone(tz.tzutc()) time_naive = time_utc.replace(tzinfo=None) result_naive = result._replace(time=time_naive) else: result_naive = result if result_naive is not None: _CACHE[key] = (timestamp_now, result_naive) return result beancount.prices.price.fetch_price(dprice, swap_inverted=False) \uf0c1 Fetch a price for the DatedPrice job. Parameters: dprice \u2013 A DatedPrice instances. swap_inverted \u2013 A boolean, true if we should invert currencies instead of rate for an inverted price source. Returns: A Price entry corresponding to the output of the jobs processed. Source code in beancount/prices/price.py def fetch_price(dprice, swap_inverted=False): \"\"\"Fetch a price for the DatedPrice job. Args: dprice: A DatedPrice instances. swap_inverted: A boolean, true if we should invert currencies instead of rate for an inverted price source. Returns: A Price entry corresponding to the output of the jobs processed. \"\"\" for psource in dprice.sources: try: source = psource.module.Source() except AttributeError: continue srcprice = fetch_cached_price(source, psource.symbol, dprice.date) if srcprice is not None: break else: if dprice.sources: logging.error(\"Could not fetch for job: %s\", dprice) return None base = dprice.base quote = dprice.quote or srcprice.quote_currency price = srcprice.price # Invert the rate if requested. if psource.invert: if swap_inverted: base, quote = quote, base else: price = ONE/price assert base is not None fileloc = data.new_metadata('<{}>'.format(type(psource.module).__name__), 0) # The datetime instance is required to be aware. We always convert to the # user's timezone before extracting the date. This means that if the market # returns a timestamp for a particular date, once we convert to the user's # timezone the returned date may be different by a day. The intent is that # whatever we print is assumed coherent with the user's timezone. See # discussion at # https://groups.google.com/d/msg/beancount/9j1E_HLEMBQ/fYRuCQK_BwAJ srctime = srcprice.time if srctime.tzinfo is None: raise ValueError(\"Time returned by the price source is not timezone aware.\") date = srctime.astimezone(tz.tzlocal()).date() return data.Price(fileloc, date, base, amount.Amount(price, quote or UNKNOWN_CURRENCY)) beancount.prices.price.filter_redundant_prices(price_entries, existing_entries, diffs=False) \uf0c1 Filter out new entries that are redundant from an existing set. If the price differs, we override it with the new entry only on demand. This is because this would create conflict with existing price entries when parsing, if the new entries are simply inserted into the input. Parameters: price_entries \u2013 A list of newly created, proposed to be added Price directives. existing_entries \u2013 A list of existing entries we are proposing to add to. diffs \u2013 A boolean, true if we should output differing price entries at the same date. Returns: A filtered list of remaining entries, and a list of ignored entries. Source code in beancount/prices/price.py def filter_redundant_prices(price_entries, existing_entries, diffs=False): \"\"\"Filter out new entries that are redundant from an existing set. If the price differs, we override it with the new entry only on demand. This is because this would create conflict with existing price entries when parsing, if the new entries are simply inserted into the input. Args: price_entries: A list of newly created, proposed to be added Price directives. existing_entries: A list of existing entries we are proposing to add to. diffs: A boolean, true if we should output differing price entries at the same date. Returns: A filtered list of remaining entries, and a list of ignored entries. \"\"\" # Note: We have to be careful with the dates, because requesting the latest # price for a date may yield the price at a previous date. Clobber needs to # take this into account. See {1cfa25e37fc1}. existing_prices = {(entry.date, entry.currency): entry for entry in existing_entries if isinstance(entry, data.Price)} filtered_prices = [] ignored_prices = [] for entry in price_entries: key = (entry.date, entry.currency) if key in existing_prices: if diffs: existing_entry = existing_prices[key] if existing_entry.amount == entry.amount: output = ignored_prices else: output = ignored_prices else: output = filtered_prices output.append(entry) return filtered_prices, ignored_prices beancount.prices.price.now() \uf0c1 Indirection in order to be able to mock it out in the tests. Source code in beancount/prices/price.py def now(): \"Indirection in order to be able to mock it out in the tests.\" return datetime.datetime.now(datetime.timezone.utc) beancount.prices.price.process_args() \uf0c1 Process the arguments. This also initializes the logging module. Returns: A tuple of \u2013 args: The argparse receiver of command-line arguments. jobs: A list of DatedPrice job objects. entries: A list of all the parsed entries. Source code in beancount/prices/price.py def process_args(): \"\"\"Process the arguments. This also initializes the logging module. Returns: A tuple of: args: The argparse receiver of command-line arguments. jobs: A list of DatedPrice job objects. entries: A list of all the parsed entries. \"\"\" parser = version.ArgumentParser(description=beancount.prices.__doc__.splitlines()[0]) # Input sources or filenames. parser.add_argument('sources', nargs='+', help=( 'A list of filenames (or source \"module/symbol\", if -e is ' 'specified) from which to create a list of jobs.')) parser.add_argument('-e', '--expressions', '--expression', action='store_true', help=( 'Interpret the arguments as \"module/symbol\" source strings.')) # Regular options. parser.add_argument('-v', '--verbose', action='count', help=( \"Print out progress log. Specify twice for debugging info.\")) parser.add_argument('-d', '--date', action='store', type=date_utils.parse_date_liberally, help=( \"Specify the date for which to fetch the prices.\")) parser.add_argument('-i', '--inactive', action='store_true', help=( \"Select all commodities from input files, not just the ones active on the date\")) parser.add_argument('-u', '--undeclared', action='store', help=( \"Include commodities viewed in the file even without a \" \"corresponding Commodity directive, from this default source. \" \"The currency name itself is used as the lookup symbol in this default source.\")) parser.add_argument('-c', '--clobber', action='store_true', help=( \"Do not skip prices which are already present in input files; fetch them anyway.\")) parser.add_argument('-a', '--all', action='store_true', help=( \"A shorthand for --inactive, --undeclared, --clobber.\")) parser.add_argument('-s', '--swap-inverted', action='store_true', help=( \"For inverted sources, swap currencies instead of inverting the rate. \" \"For example, if fetching the rate for CAD from 'USD:google/^CURRENCY:USDCAD' \" \"results in 1.25, by default we would output \\\"price CAD 0.8000 USD\\\". \" \"Using this option we would instead output \\\" price USD 1.2500 CAD\\\".\")) parser.add_argument('-n', '--dry-run', action='store_true', help=( \"Don't actually fetch the prices, just print the list of the ones to be fetched.\")) # Caching options. cache_group = parser.add_argument_group('cache') cache_filename = path.join(tempfile.gettempdir(), \"{}.cache\".format(path.basename(sys.argv[0]))) cache_group.add_argument('--cache', dest='cache_filename', action='store', default=cache_filename, help=\"Enable the cache and with the given cache name.\") cache_group.add_argument('--no-cache', dest='cache_filename', action='store_const', const=None, help=\"Disable the price cache.\") cache_group.add_argument('--clear-cache', action='store_true', help=\"Clear the cache prior to startup\") args = parser.parse_args() verbose_levels = {None: logging.WARN, 0: logging.WARN, 1: logging.INFO, 2: logging.DEBUG} logging.basicConfig(level=verbose_levels[args.verbose], format='%(levelname)-8s: %(message)s') if args.all: args.inactive = args.clobber = True args.undeclared = DEFAULT_SOURCE # Setup for processing. setup_cache(args.cache_filename, args.clear_cache) # Get the list of DatedPrice jobs to get from the arguments. logging.info(\"Processing at date: %s\", args.date or datetime.date.today()) jobs = [] all_entries = [] dcontext = None if args.expressions: # Interpret the arguments as price sources. for source_str in args.sources: psources = [] try: psource_map = find_prices.parse_source_map(source_str) except ValueError: extra = \"; did you provide a filename?\" if path.exists(source_str) else '' msg = ('Invalid source \"{{}}\"{}. '.format(extra) + 'Supported format is \"CCY:module/SYMBOL\"') parser.error(msg.format(source_str)) else: for currency, psources in psource_map.items(): jobs.append(find_prices.DatedPrice( psources[0].symbol, currency, args.date, psources)) else: # Interpret the arguments as Beancount input filenames. for filename in args.sources: if not path.exists(filename) or not path.isfile(filename): parser.error('File does not exist: \"{}\"; ' 'did you mean to use -e?'.format(filename)) continue logging.info('Loading \"%s\"', filename) entries, errors, options_map = loader.load_file(filename, log_errors=sys.stderr) if dcontext is None: dcontext = options_map['dcontext'] jobs.extend( find_prices.get_price_jobs_at_date( entries, args.date, args.inactive, args.undeclared)) all_entries.extend(entries) return args, jobs, data.sorted(all_entries), dcontext beancount.prices.price.reset_cache() \uf0c1 Reset the cache to its uninitialized state. Source code in beancount/prices/price.py def reset_cache(): \"\"\"Reset the cache to its uninitialized state.\"\"\" global _CACHE if _CACHE is not None: _CACHE.close() _CACHE = None beancount.prices.price.setup_cache(cache_filename, clear_cache) \uf0c1 Setup the results cache. Parameters: cache_filename \u2013 A string or None, the filename for the cache. clear_cache \u2013 A boolean, if true, delete the cache before beginning. Source code in beancount/prices/price.py def setup_cache(cache_filename, clear_cache): \"\"\"Setup the results cache. Args: cache_filename: A string or None, the filename for the cache. clear_cache: A boolean, if true, delete the cache before beginning. \"\"\" if clear_cache and cache_filename and path.exists(cache_filename): logging.info(\"Clearing cache %s\", cache_filename) os.remove(cache_filename) if cache_filename: logging.info('Using price cache at \"%s\" (with indefinite expiration)', cache_filename) global _CACHE _CACHE = shelve.open(cache_filename, 'c') _CACHE.expiration = DEFAULT_EXPIRATION beancount.prices.source \uf0c1 Interface definition for all price sources. This module describes the contract to be fulfilled by all implementations of price sources. beancount.prices.source.Source \uf0c1 Interface to be implemented by all price sources. beancount.prices.source.Source.get_historical_price(self, ticker, time) \uf0c1 Return the historical price found for the symbol at the given date. This could be the price of the close of the day, for instance. We assume that there is some single price representative of the day. Parameters: ticker \u2013 A string, the ticker to be fetched by the source. This ticker may include structure, such as the exchange code. Also note that this ticker is source-specified, and is not necessarily the same value as the commodity symbol used in the Beancount file. time \u2013 The timestamp at which to query for the price. This is a timezone-aware timestamp you can convert to any timezone. For past dates we query for a time that is equivalent to 4pm in the user's timezone. Returns: A SourcePrice instance. If the price could not be fetched, None is returned and another source should be consulted. There is never any guarantee that a price source will be able to fetch its value; client code must be able to handle this. Also note that the price's returned time must be timezone-aware. Source code in beancount/prices/source.py def get_historical_price(self, ticker, time): \"\"\"Return the historical price found for the symbol at the given date. This could be the price of the close of the day, for instance. We assume that there is some single price representative of the day. Args: ticker: A string, the ticker to be fetched by the source. This ticker may include structure, such as the exchange code. Also note that this ticker is source-specified, and is not necessarily the same value as the commodity symbol used in the Beancount file. time: The timestamp at which to query for the price. This is a timezone-aware timestamp you can convert to any timezone. For past dates we query for a time that is equivalent to 4pm in the user's timezone. Returns: A SourcePrice instance. If the price could not be fetched, None is returned and another source should be consulted. There is never any guarantee that a price source will be able to fetch its value; client code must be able to handle this. Also note that the price's returned time must be timezone-aware. \"\"\" beancount.prices.source.Source.get_latest_price(self, ticker) \uf0c1 Fetch the current latest price. The date may differ. This routine attempts to fetch the most recent available price, and returns the actual date of the quoted price, which may differ from the date this call is made at. {1cfa25e37fc1} Parameters: ticker \u2013 A string, the ticker to be fetched by the source. This ticker may include structure, such as the exchange code. Also note that this ticker is source-specified, and is not necessarily the same value as the commodity symbol used in the Beancount file. Returns: A SourcePrice instance. If the price could not be fetched, None is returned and another source should be consulted. There is never any guarantee that a price source will be able to fetch its value; client code must be able to handle this. Also note that the price's returned time must be timezone-aware. Source code in beancount/prices/source.py def get_latest_price(self, ticker): \"\"\"Fetch the current latest price. The date may differ. This routine attempts to fetch the most recent available price, and returns the actual date of the quoted price, which may differ from the date this call is made at. {1cfa25e37fc1} Args: ticker: A string, the ticker to be fetched by the source. This ticker may include structure, such as the exchange code. Also note that this ticker is source-specified, and is not necessarily the same value as the commodity symbol used in the Beancount file. Returns: A SourcePrice instance. If the price could not be fetched, None is returned and another source should be consulted. There is never any guarantee that a price source will be able to fetch its value; client code must be able to handle this. Also note that the price's returned time must be timezone-aware. \"\"\" beancount.prices.source.SourcePrice ( tuple ) \uf0c1 SourcePrice(price, time, quote_currency) beancount.prices.source.SourcePrice.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/prices/source.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.prices.source.SourcePrice.__new__(_cls, price, time, quote_currency) special staticmethod \uf0c1 Create new instance of SourcePrice(price, time, quote_currency) beancount.prices.source.SourcePrice.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/prices/source.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.prices.sources special \uf0c1 Implementation of various price extractors. This package is looked up by the driver script to figure out which extractor to use. beancount.prices.sources.coinbase \uf0c1 A source fetching cryptocurrency prices from Coinbase. Valid tickers are in the form \"XXX-YYY\", such as \"BTC-USD\". Here is the API documentation: https://developers.coinbase.com/api/v2 For example: https://api.coinbase.com/v2/prices/BTC-GBP/spot Timezone information: Input and output datetimes are specified via UTC timestamps. beancount.prices.sources.coinbase.CoinbaseError ( ValueError ) \uf0c1 An error from the Coinbase API. beancount.prices.sources.coinbase.Source ( Source ) \uf0c1 Coinbase API price extractor. beancount.prices.sources.coinbase.Source.get_historical_price(self, ticker, time) \uf0c1 See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/coinbase.py def get_historical_price(self, ticker, time): \"\"\"See contract in beancount.prices.source.Source.\"\"\" raise NotImplementedError( \"As of Feb 2019, historical prices are not supported on Coinbase. \" \"Please check the API to see if this has changed: \" \"https://developers.coinbase.com/apo/v2\") beancount.prices.sources.coinbase.Source.get_latest_price(self, ticker) \uf0c1 See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/coinbase.py def get_latest_price(self, ticker): \"\"\"See contract in beancount.prices.source.Source.\"\"\" return fetch_quote(ticker) beancount.prices.sources.coinbase.fetch_quote(ticker) \uf0c1 Fetch a quote from Coinbase. Source code in beancount/prices/sources/coinbase.py def fetch_quote(ticker): \"\"\"Fetch a quote from Coinbase.\"\"\" url = \"https://api.coinbase.com/v2/prices/{}/spot\".format(ticker.lower()) response = requests.get(url) if response.status_code != requests.codes.ok: raise CoinbaseError(\"Invalid response ({}): {}\".format(response.status_code, response.text)) result = response.json() price = D(result['data']['amount']).quantize(D('0.01')) time = datetime.datetime.now(tz.tzutc()) currency = result['data']['currency'] return source.SourcePrice(price, time, currency) beancount.prices.sources.iex \uf0c1 Fetch prices from the IEX 1.0 public API. This is a really fantastic exchange API with a lot of relevant information. Timezone information: There is currency no support for historical prices. The output datetime is provided as a UNIX timestamp. beancount.prices.sources.iex.IEXError ( ValueError ) \uf0c1 An error from the IEX API. beancount.prices.sources.iex.Source ( Source ) \uf0c1 IEX API price extractor. beancount.prices.sources.iex.Source.get_historical_price(self, ticker, time) \uf0c1 See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/iex.py def get_historical_price(self, ticker, time): \"\"\"See contract in beancount.prices.source.Source.\"\"\" raise NotImplementedError( \"This is now implemented at https://iextrading.com/developers/docs/#hist and \" \"needs to be added here.\") beancount.prices.sources.iex.Source.get_latest_price(self, ticker) \uf0c1 See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/iex.py def get_latest_price(self, ticker): \"\"\"See contract in beancount.prices.source.Source.\"\"\" return fetch_quote(ticker) beancount.prices.sources.iex.fetch_quote(ticker) \uf0c1 Fetch the latest price for the given ticker. Source code in beancount/prices/sources/iex.py def fetch_quote(ticker): \"\"\"Fetch the latest price for the given ticker.\"\"\" url = \"https://api.iextrading.com/1.0/tops/last?symbols={}\".format(ticker.upper()) response = requests.get(url) if response.status_code != requests.codes.ok: raise IEXError(\"Invalid response ({}): {}\".format( response.status_code, response.text)) results = response.json() if len(results) != 1: raise IEXError(\"Invalid number of responses from IEX: {}\".format( response.text)) result = results[0] price = D(result['price']).quantize(D('0.01')) # IEX is American markets. us_timezone = tz.gettz(\"America/New_York\") time = datetime.datetime.fromtimestamp(result['time'] / 1000) time = time.astimezone(us_timezone) # As far as can tell, all the instruments on IEX are priced in USD. return source.SourcePrice(price, time, 'USD') beancount.prices.sources.oanda \uf0c1 A source fetching currency prices from OANDA. Valid tickers are in the form \"XXX_YYY\", such as \"EUR_USD\". Here is the API documentation: https://developer.oanda.com/rest-live/rates/ For example: https://api-fxtrade.oanda.com/v1/candles?instrument=EUR_USD&granularity=D&start=2016-03-27T00%3A00%3A00Z&end=2016-04-04T00%3A00%3A00Z&candleFormat=midpoint Timezone information: Input and output datetimes are specified via UTC timestamps. beancount.prices.sources.oanda.Source ( Source ) \uf0c1 OANDA price source extractor. beancount.prices.sources.oanda.Source.get_historical_price(self, ticker, time) \uf0c1 See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/oanda.py def get_historical_price(self, ticker, time): \"\"\"See contract in beancount.prices.source.Source.\"\"\" time = time.astimezone(tz.tzutc()) query_interval_begin = (time - datetime.timedelta(days=5)) query_interval_end = (time + datetime.timedelta(days=1)) params_dict = { 'instrument': ticker, 'granularity': 'H2', # Every two hours. 'candleFormat': 'midpoint', 'start': query_interval_begin.isoformat('T'), 'end': query_interval_end.isoformat('T'), } return _fetch_price(params_dict, time) beancount.prices.sources.oanda.Source.get_latest_price(self, ticker) \uf0c1 See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/oanda.py def get_latest_price(self, ticker): \"\"\"See contract in beancount.prices.source.Source.\"\"\" time = datetime.datetime.now(tz.tzutc()) params_dict = { 'instrument': ticker, 'granularity': 'S5', # Every two hours. 'count': '10', 'candleFormat': 'midpoint', } return _fetch_price(params_dict, time) beancount.prices.sources.quandl \uf0c1 Fetch prices from Quandl's simple URL-based API. Quandl is a useful source of alternative data and it offers a simple REST API that serves CSV and JSON and XML formats. There's also a Python client library, but we specifically avoid using that here, in order to keep Beancount dependency-free. Many of the datasets are freely available, which is why this is included here. You can get information about the available databases and associated lists of symbols you can use here: https://www.quandl.com/search If you have a paid account and would like to be able to access the premium databases from the Quandl site, you can set QUANDL_API_KEY environment variable. Use the \":\" format to refer to Quandl symbols. Note that their symbols are usually identified by \"/\". (For now, this supports only the Time-Series API. There is also a Tables API, which could easily get integrated. We would just have to encode the 'datatable_code' and 'format' and perhaps other fields in the ticker name.) Timezone information: Input and output datetimes are limited to dates, and I believe the dates are presumed to live in the timezone of each particular data source. (It's unclear, not documented.) beancount.prices.sources.quandl.QuandlError ( ValueError ) \uf0c1 An error from the Quandl API. beancount.prices.sources.quandl.Source ( Source ) \uf0c1 Quandl API price extractor. beancount.prices.sources.quandl.Source.get_historical_price(self, ticker, time) \uf0c1 See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/quandl.py def get_historical_price(self, ticker, time): \"\"\"See contract in beancount.prices.source.Source.\"\"\" return fetch_time_series(ticker, time) beancount.prices.sources.quandl.Source.get_latest_price(self, ticker) \uf0c1 See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/quandl.py def get_latest_price(self, ticker): \"\"\"See contract in beancount.prices.source.Source.\"\"\" return fetch_time_series(ticker) beancount.prices.sources.quandl.fetch_time_series(ticker, time=None) \uf0c1 Fetch Source code in beancount/prices/sources/quandl.py def fetch_time_series(ticker, time=None): \"\"\"Fetch\"\"\" # Create request payload. database, dataset = parse_ticker(ticker) url = \"https://www.quandl.com/api/v3/datasets/{}/{}.json\".format(database, dataset) payload = {\"limit\": 1} if time is not None: date = time.date() payload[\"start_date\"] = (date - datetime.timedelta(days=10)).isoformat() payload[\"end_date\"] = date.isoformat() # Add API key, if it is set in the environment. if 'QUANDL_API_KEY' in os.environ: payload['api_key'] = os.environ['QUANDL_API_KEY'] # Fetch and process errors. response = requests.get(url, params=payload) if response.status_code != requests.codes.ok: raise QuandlError(\"Invalid response ({}): {}\".format(response.status_code, response.text)) result = response.json() if 'quandl_error' in result: raise QuandlError(result['quandl_error']['message']) # Parse result container. dataset = result['dataset'] column_names = dataset['column_names'] date_index = column_names.index('Date') try: data_index = column_names.index('Adj. Close') except ValueError: data_index = column_names.index('Close') data = dataset['data'][0] # Gather time and assume it's in UTC timezone (Quandl does not provide the # market's timezone). time = datetime.datetime.strptime(data[date_index], '%Y-%m-%d') time = time.replace(tzinfo=tz.tzutc()) # Gather price. # Quantize with the same precision default rendering of floats occur. price_float = data[data_index] price = D(price_float) match = re.search(r'(\\..*)', str(price_float)) if match: price = price.quantize(D(match.group(1))) # Note: There is no currency information in the response (surprising). return source.SourcePrice(price, time, None) beancount.prices.sources.quandl.parse_ticker(ticker) \uf0c1 Convert ticker to Quandl codes. Source code in beancount/prices/sources/quandl.py def parse_ticker(ticker): \"\"\"Convert ticker to Quandl codes.\"\"\" if not re.match(r\"[A-Z0-9]+:[A-Z0-9]+$\", ticker): raise ValueError('Invalid code. Use \"/\" format.') return tuple(ticker.split(\":\")) beancount.prices.sources.yahoo \uf0c1 Fetch prices from Yahoo Finance's CSV API. As of late 2017, the older Yahoo finance API deprecated. In particular, the ichart endpoint is gone, and the download endpoint requires a cookie (which could be gotten - here's some documentation for that http://blog.bradlucas.com/posts/2017-06-02-new-yahoo-finance-quote-download-url/). We're using both the v7 and v8 APIs here, both of which are, as far as I can tell, undocumented: https://query1.finance.yahoo.com/v7/finance/quote https://query1.finance.yahoo.com/v8/finance/chart/SYMBOL Timezone information: Input and output datetimes are specified via UNIX timestamps, but the timezone of the particular market is included in the output. beancount.prices.sources.yahoo.Source ( Source ) \uf0c1 Yahoo Finance CSV API price extractor. beancount.prices.sources.yahoo.Source.get_historical_price(self, ticker, time) \uf0c1 See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/yahoo.py def get_historical_price(self, ticker, time): \"\"\"See contract in beancount.prices.source.Source.\"\"\" if requests is None: raise YahooError(\"You must install the 'requests' library.\") url = \"https://query1.finance.yahoo.com/v8/finance/chart/{}\".format(ticker) dt_start = time - datetime.timedelta(days=5) dt_end = time payload = { 'period1': int(dt_start.timestamp()), 'period2': int(dt_end.timestamp()), 'interval': '1d', } payload.update(_DEFAULT_PARAMS) response = requests.get(url, params=payload) result = parse_response(response) meta = result['meta'] timezone = datetime.timezone(datetime.timedelta(hours=meta['gmtoffset'] / 3600), meta['exchangeTimezoneName']) timestamp_array = result['timestamp'] close_array = result['indicators']['quote'][0]['close'] series = [(datetime.datetime.fromtimestamp(timestamp, tz=timezone), D(price)) for timestamp, price in zip(timestamp_array, close_array)] # Get the latest data returned. latest = None for data_dt, price in sorted(series): if data_dt >= time: break latest = data_dt, price if latest is None: raise YahooError(\"Could not find price before {} in {}\".format(time, series)) currency = result['meta']['currency'] return source.SourcePrice(price, data_dt, currency) beancount.prices.sources.yahoo.Source.get_latest_price(self, ticker) \uf0c1 See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/yahoo.py def get_latest_price(self, ticker): \"\"\"See contract in beancount.prices.source.Source.\"\"\" url = \"https://query1.finance.yahoo.com/v7/finance/quote\" fields = ['symbol', 'regularMarketPrice', 'regularMarketTime'] payload = { 'symbols': ticker, 'fields': ','.join(fields), 'exchange': 'NYSE', } payload.update(_DEFAULT_PARAMS) response = requests.get(url, params=payload) result = parse_response(response) try: price = D(result['regularMarketPrice']) timezone = datetime.timezone( datetime.timedelta(hours=result['gmtOffSetMilliseconds'] / 3600000), result['exchangeTimezoneName']) trade_time = datetime.datetime.fromtimestamp(result['regularMarketTime'], tz=timezone) except KeyError: raise YahooError(\"Invalid response from Yahoo: {}\".format(repr(result))) currency = parse_currency(result) return source.SourcePrice(price, trade_time, currency) beancount.prices.sources.yahoo.YahooError ( ValueError ) \uf0c1 An error from the Yahoo API. beancount.prices.sources.yahoo.parse_currency(result) \uf0c1 Infer the currency from the result. Source code in beancount/prices/sources/yahoo.py def parse_currency(result: Dict[str, Any]) -> str: \"\"\"Infer the currency from the result.\"\"\" if 'market' not in result: return None return _MARKETS.get(result['market'], None) beancount.prices.sources.yahoo.parse_response(response) \uf0c1 Process as response from Yahoo. Exceptions: YahooError \u2013 If there is an error in the response. Source code in beancount/prices/sources/yahoo.py def parse_response(response: requests.models.Response) -> Dict: \"\"\"Process as response from Yahoo. Raises: YahooError: If there is an error in the response. \"\"\" json = response.json(parse_float=D) content = next(iter(json.values())) if response.status_code != requests.codes.ok: raise YahooError(\"Status {}: {}\".format(response.status_code, content['error'])) if len(json) != 1: raise YahooError(\"Invalid format in response from Yahoo; many keys: {}\".format( ','.join(json.keys()))) if content['error'] is not None: raise YahooError(\"Error fetching Yahoo data: {}\".format(content['error'])) return content['result'][0]","title":"beancount.prices"},{"location":"api_reference/beancount.prices.html#beancountprices","text":"Fetch prices from the internet and output them as Beancount price directives. This script accepts a list of Beancount input filenames, and fetches prices required to compute market values for current positions: bean-price /home/joe/finances/joe.beancount The list of fetching jobs to carry out is derived automatically from the input file (see section below for full details). It is also possible to provide a list of specific price fetching jobs to run, e.g., bean-price -e google/TSE:XUS yahoo/AAPL mysources.morningstar/RBF1005 The general format of each of these \"source strings\" is /[^] The \"module\" is the name of a Python module that contains a Source class which can be instantiated and connect to a data source to extract price data. These modules are automatically imported by name and instantiated in order to pull the price from a particular data source. This allows you to write your own supplementary fetcher codes without having to modify this script. Default implementations are provided to provide access to prices from Yahoo! Finance or Google Finance, which cover a large universe of common public investment types (e.g. stock tickers). As a convenience, the module name is always first searched under the \"beancount.prices.sources\" package, where those default source implementations live. This is how, for example, in order to use the provided Google Finance data fetcher you don't have to write \"beancount.prices.sources.yahoo/AAPL\" but simply \"yahoo/AAPL\".","title":"beancount.prices"},{"location":"api_reference/beancount.prices.html#beancount.prices--date","text":"By default, this script will fetch prices at the latest available date & time. You can use an option to fetch historical prices for a desired date instead: bean-price --date=2015-02-03","title":"Date"},{"location":"api_reference/beancount.prices.html#beancount.prices--inverse","text":"Sometimes, prices are available for the inverse of an instrument. This is often the case for currencies. For example, the price of \"CAD\" in USD\" is provided by the USD/CAD market, which gives the price of a US dollar in Canadian dollars. In order specify this, you can prepend \"^\" to the instrument to instruct the driver to compute the inverse of the given price: bean-price -e USD:google/^CURRENCY:USDCAD If a source price is to be inverted, like this, the precision could be different than what is fetched. For instance, if the price of USD/CAD is 1.32759, it would output be this from the above directive: 2015-10-28 price CAD 0.753244601119 USD By default, inverted rates will be rounded similarly to how other Price directives were rounding those numbers.","title":"Inverse"},{"location":"api_reference/beancount.prices.html#beancount.prices--swap-inverted","text":"If you prefer to have the output Price entries with swapped currencies instead of inverting the rate itself, you can use the --swap-inverted option. In the previous example for the price of CAD, it would output this: 2015-10-28 price USD 1.32759 CAD This works since the Beancount price database computes and interpolates the reciprocals automatically for all pairs of commodities in its database.","title":"Swap Inverted"},{"location":"api_reference/beancount.prices.html#beancount.prices--prices-needed-for-a-beancount-file","text":"You can also provide a filename to extract the list of tickers to fetch from a Beancount input file, e.g.: bean-price /home/joe/finances/joe.beancount There are many ways to extract a list of commodities with needed prices from a Beancount input file: Prices for all the holdings that were seen held-at-cost at a particular date. Prices for holdings held at a particular date which were price converted from some other commodity in the past (i.e., for currencies). The list of all Commodity directives present in the file. For each of those holdings, the corresponding Commodity directive is consulted and its \"ticker\" metadata field is used to specify where to attempt to fetch prices. You should have directives like this in your input file: 2007-07-20 commodity VEA price: \"google/NYSEARCA:VEA\" The \"price\" metadata can be a comma-separated list of sources to try out, in which case each of the sources will be looked at : 2007-07-20 commodity VEA price: \"google/CURRENCY:USDCAD,yahoo/USDCAD\" Existing price directives for the same data are excluded by default, since the price is already in the file. By default, the list of tickers to be fetched includes only the intersection of these lists. The general intent of the user of this script is to fetch missing prices, and only needed ones, for a particular date. Use the --date option to change the applied date. Use the --all option to fetch the entire set of prices, regardless of holdings and date. Use --clobber to ignore existing price directives. You can also print the list of prices to be fetched with the --dry-run option, which stops short of actually fetching the missing prices (it just prints the list of fetches it would otherwise attempt).","title":"Prices Needed for a Beancount File"},{"location":"api_reference/beancount.prices.html#beancount.prices--caching","text":"Prices are automatically cached. You can disable the cache with an option: bean-price --no-cache You can also instruct the script to clear the cache before fetching its prices: bean-price --clear-cache","title":"Caching"},{"location":"api_reference/beancount.prices.html#beancount.prices--about-sources-and-data-availability","text":"IMPORTANT: Note that each source may support a different routine for getting its latest data and for fetching historical/dated data, and that each of these may differ in their support. For example, Google Finance does not support fetching historical data for its CURRENCY:* instruments.","title":"About Sources and Data Availability"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices","text":"A library of codes create price fetching jobs from strings and files.","title":"find_prices"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.DatedPrice","text":"DatedPrice(base, quote, date, sources)","title":"DatedPrice"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.DatedPrice.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/prices/find_prices.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.DatedPrice.__new__","text":"Create new instance of DatedPrice(base, quote, date, sources)","title":"__new__()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.DatedPrice.__repr__","text":"Return a nicely formatted representation string Source code in beancount/prices/find_prices.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.PriceSource","text":"PriceSource(module, symbol, invert)","title":"PriceSource"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.PriceSource.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/prices/find_prices.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.PriceSource.__new__","text":"Create new instance of PriceSource(module, symbol, invert)","title":"__new__()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.PriceSource.__repr__","text":"Return a nicely formatted representation string Source code in beancount/prices/find_prices.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.find_balance_currencies","text":"Return currencies relevant for the given date. This computes the account balances as of the date, and returns the union of: a) The currencies held at cost, and b) Currency pairs from previous conversions, but only for currencies with non-zero balances. This is intended to produce the list of currencies whose prices are relevant at a particular date, based on previous history. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A set of (base, quote) currencies. Source code in beancount/prices/find_prices.py def find_balance_currencies(entries, date=None): \"\"\"Return currencies relevant for the given date. This computes the account balances as of the date, and returns the union of: a) The currencies held at cost, and b) Currency pairs from previous conversions, but only for currencies with non-zero balances. This is intended to produce the list of currencies whose prices are relevant at a particular date, based on previous history. Args: entries: A list of directives. date: A datetime.date instance. Returns: A set of (base, quote) currencies. \"\"\" # Compute the balances. currencies = set() currencies_on_books = set() balances, _ = summarize.balance_by_account(entries, date) for _, balance in balances.items(): for pos in balance: if pos.cost is not None: # Add currencies held at cost. currencies.add((pos.units.currency, pos.cost.currency)) else: # Add regular currencies. currencies_on_books.add(pos.units.currency) # Create currency pairs from the currencies which are on account balances. # In order to figure out the quote currencies, we use the list of price # conversions until this date. converted = (find_currencies_converted(entries, date) | find_currencies_priced(entries, date)) for cbase in currencies_on_books: for base_quote in converted: base, quote = base_quote if base == cbase: currencies.add(base_quote) return currencies","title":"find_balance_currencies()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.find_currencies_at_cost","text":"Return all currencies that were held at cost at some point. This returns all of them, even if not on the books at a particular point in time. This code does not look at account balances. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote) currencies. Source code in beancount/prices/find_prices.py def find_currencies_at_cost(entries): \"\"\"Return all currencies that were held at cost at some point. This returns all of them, even if not on the books at a particular point in time. This code does not look at account balances. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote) currencies. \"\"\" currencies = set() for entry in entries: if not isinstance(entry, data.Transaction): continue for posting in entry.postings: if posting.cost is not None and posting.cost.number is not None: currencies.add((posting.units.currency, posting.cost.currency)) return currencies","title":"find_currencies_at_cost()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.find_currencies_converted","text":"Return currencies from price conversions. This function looks at all price conversions that occurred until some date and produces a list of them. Note: This does not include Price directives, only postings with price conversions. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote) currencies. Source code in beancount/prices/find_prices.py def find_currencies_converted(entries, date=None): \"\"\"Return currencies from price conversions. This function looks at all price conversions that occurred until some date and produces a list of them. Note: This does not include Price directives, only postings with price conversions. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote) currencies. \"\"\" currencies = set() for entry in entries: if not isinstance(entry, data.Transaction): continue if date and entry.date >= date: break for posting in entry.postings: price = posting.price if posting.cost is not None or price is None: continue currencies.add((posting.units.currency, price.currency)) return currencies","title":"find_currencies_converted()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.find_currencies_declared","text":"Return currencies declared in Commodity directives. If a 'price' metadata field is provided, include all the quote currencies there-in. Otherwise, the Commodity directive is ignored. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote, list of PriceSource) currencies. The list of (base, quote) pairs is guaranteed to be unique. Source code in beancount/prices/find_prices.py def find_currencies_declared(entries, date=None): \"\"\"Return currencies declared in Commodity directives. If a 'price' metadata field is provided, include all the quote currencies there-in. Otherwise, the Commodity directive is ignored. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote, list of PriceSource) currencies. The list of (base, quote) pairs is guaranteed to be unique. \"\"\" currencies = [] for entry in entries: if not isinstance(entry, data.Commodity): continue if date and entry.date >= date: break # Here we have to infer which quote currencies the commodity is for # (maybe down the road this should be better handled by providing a list # of quote currencies in the Commodity directive itself). # # First, we look for a \"price\" metadata field, which defines conversions # for various currencies. Each of these quote currencies generates a # pair in the output. source_str = entry.meta.get('price', None) if source_str is not None: if source_str == \"\": logging.debug(\"Skipping ignored currency (with empty price): %s\", entry.currency) continue try: source_map = parse_source_map(source_str) except ValueError: logging.warning(\"Ignoring currency with invalid 'price' source: %s\", entry.currency) else: for quote, psources in source_map.items(): currencies.append((entry.currency, quote, psources)) else: # Otherwise we simply ignore the declaration. That is, a Commodity # directive without any \"price\" metadata would not register as a # declared currency. logging.debug(\"Ignoring currency with no metadata: %s\", entry.currency) return currencies","title":"find_currencies_declared()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.find_currencies_priced","text":"Return currencies seen in Price directives. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote) currencies. Source code in beancount/prices/find_prices.py def find_currencies_priced(entries, date=None): \"\"\"Return currencies seen in Price directives. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote) currencies. \"\"\" currencies = set() for entry in entries: if not isinstance(entry, data.Price): continue if date and entry.date >= date: break currencies.add((entry.currency, entry.amount.currency)) return currencies","title":"find_currencies_priced()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.format_dated_price_str","text":"Convert a dated price to a one-line printable string. Parameters: dprice \u2013 A DatedPrice instance. Returns: The string for a DatedPrice instance. Source code in beancount/prices/find_prices.py def format_dated_price_str(dprice): \"\"\"Convert a dated price to a one-line printable string. Args: dprice: A DatedPrice instance. Returns: The string for a DatedPrice instance. \"\"\" psstrs = ['{}({}{})'.format(psource.module.__name__, '1/' if psource.invert else '', psource.symbol) for psource in dprice.sources] base_quote = '{} /{}'.format(dprice.base, dprice.quote) return '{:<32} @ {:10} [ {} ]'.format( base_quote, dprice.date.isoformat() if dprice.date else 'latest', ','.join(psstrs))","title":"format_dated_price_str()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.get_price_jobs_at_date","text":"Get a list of prices to fetch from a stream of entries. The active holdings held on the given date are included. Parameters: filename \u2013 A string, the name of a file to process. date \u2013 A datetime.date instance. inactive \u2013 Include currencies with no balance at the given date. The default is to only include those currencies which have a non-zero balance. undeclared_source \u2013 A string, the name of the default source module to use to pull prices for commodities without a price source metadata on their Commodity directive declaration. Returns: A list of DatedPrice instances. Source code in beancount/prices/find_prices.py def get_price_jobs_at_date(entries, date=None, inactive=False, undeclared_source=None): \"\"\"Get a list of prices to fetch from a stream of entries. The active holdings held on the given date are included. Args: filename: A string, the name of a file to process. date: A datetime.date instance. inactive: Include currencies with no balance at the given date. The default is to only include those currencies which have a non-zero balance. undeclared_source: A string, the name of the default source module to use to pull prices for commodities without a price source metadata on their Commodity directive declaration. Returns: A list of DatedPrice instances. \"\"\" # Find the list of declared currencies, and from it build a mapping for # tickers for each (base, quote) pair. This is the only place tickers # appear. declared_triples = find_currencies_declared(entries, date) currency_map = {(base, quote): psources for base, quote, psources in declared_triples} # Compute the initial list of currencies to consider. if undeclared_source: # Use the full set of possible currencies. cur_at_cost = find_currencies_at_cost(entries) cur_converted = find_currencies_converted(entries, date) cur_priced = find_currencies_priced(entries, date) currencies = cur_at_cost | cur_converted | cur_priced log_currency_list(\"Currency held at cost\", cur_at_cost) log_currency_list(\"Currency converted\", cur_converted) log_currency_list(\"Currency priced\", cur_priced) default_source = import_source(undeclared_source) else: # Use the currencies from the Commodity directives. currencies = set(currency_map.keys()) default_source = None log_currency_list(\"Currencies in primary list\", currencies) # By default, restrict to only the currencies with non-zero balances at the # given date. if not inactive: balance_currencies = find_balance_currencies(entries, date) log_currency_list(\"Currencies held in assets\", balance_currencies) currencies = currencies & balance_currencies log_currency_list(\"Currencies to fetch\", currencies) # Build up the list of jobs to fetch prices for. jobs = [] for base_quote in currencies: psources = currency_map.get(base_quote, None) base, quote = base_quote # If there are no sources, create a default one. if not psources: psources = [PriceSource(default_source, base, False)] jobs.append(DatedPrice(base, quote, date, psources)) return sorted(jobs)","title":"get_price_jobs_at_date()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.import_source","text":"Import the source module defined by the given name. The default location is handled here. Parameters: short_module_name \u2013 A string, the name of a Python module, which may be within the default package or a full name. Returns: A corresponding Python module object. Exceptions: ImportError \u2013 If the module cannot be imported. Source code in beancount/prices/find_prices.py def import_source(module_name): \"\"\"Import the source module defined by the given name. The default location is handled here. Args: short_module_name: A string, the name of a Python module, which may be within the default package or a full name. Returns: A corresponding Python module object. Raises: ImportError: If the module cannot be imported. \"\"\" default_name = '{}.{}'.format(DEFAULT_PACKAGE, module_name) try: __import__(default_name) return sys.modules[default_name] except ImportError: try: __import__(module_name) return sys.modules[module_name] except ImportError as exc: raise ImportError('Could not find price source module \"{}\": {}'.format( module_name, exc))","title":"import_source()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.log_currency_list","text":"Log a list of currencies to debug output. Parameters: message \u2013 A message string to prepend. currencies \u2013 A list of (base, quote) currency pair. Source code in beancount/prices/find_prices.py def log_currency_list(message, currencies): \"\"\"Log a list of currencies to debug output. Args: message: A message string to prepend. currencies: A list of (base, quote) currency pair. \"\"\" logging.debug(\"-------- {}:\".format(message)) for base, quote in currencies: logging.debug(\" {:>32}\".format('{} /{}'.format(base, quote)))","title":"log_currency_list()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.parse_single_source","text":"Parse a single source string. Source specifications follow the syntax: /[^] The is resolved against the Python path, but first looked up under the package where the default price extractors lie. Parameters: source \u2013 A single source string specification. Returns: A PriceSource tuple, or Exceptions: ValueError \u2013 If invalid. Source code in beancount/prices/find_prices.py def parse_single_source(source): \"\"\"Parse a single source string. Source specifications follow the syntax: /[^] The is resolved against the Python path, but first looked up under the package where the default price extractors lie. Args: source: A single source string specification. Returns: A PriceSource tuple, or Raises: ValueError: If invalid. \"\"\" match = re.match(r'([a-zA-Z]+[a-zA-Z0-9\\._]+)/(\\^?)([a-zA-Z0-9:=_\\-\\.]+)$', source) if not match: raise ValueError('Invalid source name: \"{}\"'.format(source)) short_module_name, invert, symbol = match.groups() module = import_source(short_module_name) return PriceSource(module, symbol, bool(invert))","title":"parse_single_source()"},{"location":"api_reference/beancount.prices.html#beancount.prices.find_prices.parse_source_map","text":"Parse a source map specification string. Source map specifications allow the specification of multiple sources for multiple quote currencies and follow the following syntax: :,,... :,... Where a itself follows: /[^] The is resolved against the Python path, but first looked up under the package where the default price extractors lie. The presence of a '^' character indicates that we should use the inverse of the rate pull from this source. For example, for prices of AAPL in USD: USD:google/NASDAQ:AAPL,yahoo/AAPL Or for the exchange rate of a currency, such as INR in USD or in CAD: USD:google/^CURRENCY:USDINR CAD:google/^CURRENCY:CADINR Parameters: source_map_spec \u2013 A string, a full source map specification to be parsed. Returns: FIXME \u2013 TODO Exceptions: ValueError \u2013 If an invalid pattern has been specified. Source code in beancount/prices/find_prices.py def parse_source_map(source_map_spec): \"\"\"Parse a source map specification string. Source map specifications allow the specification of multiple sources for multiple quote currencies and follow the following syntax: :,,... :,... Where a itself follows: /[^] The is resolved against the Python path, but first looked up under the package where the default price extractors lie. The presence of a '^' character indicates that we should use the inverse of the rate pull from this source. For example, for prices of AAPL in USD: USD:google/NASDAQ:AAPL,yahoo/AAPL Or for the exchange rate of a currency, such as INR in USD or in CAD: USD:google/^CURRENCY:USDINR CAD:google/^CURRENCY:CADINR Args: source_map_spec: A string, a full source map specification to be parsed. Returns: FIXME: TODO Raises: ValueError: If an invalid pattern has been specified. \"\"\" source_map = collections.defaultdict(list) for source_list_spec in re.split('[ ;]', source_map_spec): match = re.match('({}):(.*)$'.format(amount.CURRENCY_RE), source_list_spec) if not match: raise ValueError('Invalid source map pattern: \"{}\"'.format(source_list_spec)) currency, source_strs = match.groups() source_map[currency].extend( parse_single_source(source_str) for source_str in source_strs.split(',')) return source_map","title":"parse_source_map()"},{"location":"api_reference/beancount.prices.html#beancount.prices.price","text":"Driver code for the price script.","title":"price"},{"location":"api_reference/beancount.prices.html#beancount.prices.price.fetch_cached_price","text":"Call Source to fetch a price, but look and/or update the cache first. This function entirely deals with caching and correct expiration. It keeps old prices if they were fetched in the past, and it quickly expires intra-day prices if they are fetched on the same day. Parameters: source \u2013 A Python module object. symbol \u2013 A string, the ticker to fetch. date \u2013 A datetime.date instance, None if we're to fetch the latest date. Returns: A SourcePrice instance. Source code in beancount/prices/price.py def fetch_cached_price(source, symbol, date): \"\"\"Call Source to fetch a price, but look and/or update the cache first. This function entirely deals with caching and correct expiration. It keeps old prices if they were fetched in the past, and it quickly expires intra-day prices if they are fetched on the same day. Args: source: A Python module object. symbol: A string, the ticker to fetch. date: A datetime.date instance, None if we're to fetch the latest date. Returns: A SourcePrice instance. \"\"\" # Compute a suitable timestamp from the date, if specified. if date is not None: # We query as for 4pm for the given date of the current timezone, if # specified. query_time = datetime.time(16, 0, 0) time_local = datetime.datetime.combine(date, query_time, tzinfo=tz.tzlocal()) time = time_local.astimezone(tz.tzutc()) else: time = None if _CACHE is None: # The cache is disabled; just call and return. result = (source.get_latest_price(symbol) if time is None else source.get_historical_price(symbol, time)) else: # The cache is enabled and we have to compute the current/latest price. # Try to fetch from the cache but miss if the price is too old. md5 = hashlib.md5() md5.update(str((type(source).__module__, symbol, date)).encode('utf-8')) key = md5.hexdigest() timestamp_now = int(now().timestamp()) try: timestamp_created, result_naive = _CACHE[key] # Convert naive timezone to UTC, which is what the cache is always # assumed to store. (The reason for this is that timezones from # aware datetime objects cannot be serialized properly due to bug.) if result_naive.time is not None: result = result_naive._replace( time=result_naive.time.replace(tzinfo=tz.tzutc())) else: result = result_naive if (timestamp_now - timestamp_created) > _CACHE.expiration.total_seconds(): raise KeyError except KeyError: logging.info(\"Fetching: %s (time: %s)\", symbol, time) try: result = (source.get_latest_price(symbol) if time is None else source.get_historical_price(symbol, time)) except ValueError as exc: logging.error(\"Error fetching %s: %s\", symbol, exc) result = None # Make sure the timezone is UTC and make naive before serialization. if result and result.time is not None: time_utc = result.time.astimezone(tz.tzutc()) time_naive = time_utc.replace(tzinfo=None) result_naive = result._replace(time=time_naive) else: result_naive = result if result_naive is not None: _CACHE[key] = (timestamp_now, result_naive) return result","title":"fetch_cached_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.price.fetch_price","text":"Fetch a price for the DatedPrice job. Parameters: dprice \u2013 A DatedPrice instances. swap_inverted \u2013 A boolean, true if we should invert currencies instead of rate for an inverted price source. Returns: A Price entry corresponding to the output of the jobs processed. Source code in beancount/prices/price.py def fetch_price(dprice, swap_inverted=False): \"\"\"Fetch a price for the DatedPrice job. Args: dprice: A DatedPrice instances. swap_inverted: A boolean, true if we should invert currencies instead of rate for an inverted price source. Returns: A Price entry corresponding to the output of the jobs processed. \"\"\" for psource in dprice.sources: try: source = psource.module.Source() except AttributeError: continue srcprice = fetch_cached_price(source, psource.symbol, dprice.date) if srcprice is not None: break else: if dprice.sources: logging.error(\"Could not fetch for job: %s\", dprice) return None base = dprice.base quote = dprice.quote or srcprice.quote_currency price = srcprice.price # Invert the rate if requested. if psource.invert: if swap_inverted: base, quote = quote, base else: price = ONE/price assert base is not None fileloc = data.new_metadata('<{}>'.format(type(psource.module).__name__), 0) # The datetime instance is required to be aware. We always convert to the # user's timezone before extracting the date. This means that if the market # returns a timestamp for a particular date, once we convert to the user's # timezone the returned date may be different by a day. The intent is that # whatever we print is assumed coherent with the user's timezone. See # discussion at # https://groups.google.com/d/msg/beancount/9j1E_HLEMBQ/fYRuCQK_BwAJ srctime = srcprice.time if srctime.tzinfo is None: raise ValueError(\"Time returned by the price source is not timezone aware.\") date = srctime.astimezone(tz.tzlocal()).date() return data.Price(fileloc, date, base, amount.Amount(price, quote or UNKNOWN_CURRENCY))","title":"fetch_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.price.filter_redundant_prices","text":"Filter out new entries that are redundant from an existing set. If the price differs, we override it with the new entry only on demand. This is because this would create conflict with existing price entries when parsing, if the new entries are simply inserted into the input. Parameters: price_entries \u2013 A list of newly created, proposed to be added Price directives. existing_entries \u2013 A list of existing entries we are proposing to add to. diffs \u2013 A boolean, true if we should output differing price entries at the same date. Returns: A filtered list of remaining entries, and a list of ignored entries. Source code in beancount/prices/price.py def filter_redundant_prices(price_entries, existing_entries, diffs=False): \"\"\"Filter out new entries that are redundant from an existing set. If the price differs, we override it with the new entry only on demand. This is because this would create conflict with existing price entries when parsing, if the new entries are simply inserted into the input. Args: price_entries: A list of newly created, proposed to be added Price directives. existing_entries: A list of existing entries we are proposing to add to. diffs: A boolean, true if we should output differing price entries at the same date. Returns: A filtered list of remaining entries, and a list of ignored entries. \"\"\" # Note: We have to be careful with the dates, because requesting the latest # price for a date may yield the price at a previous date. Clobber needs to # take this into account. See {1cfa25e37fc1}. existing_prices = {(entry.date, entry.currency): entry for entry in existing_entries if isinstance(entry, data.Price)} filtered_prices = [] ignored_prices = [] for entry in price_entries: key = (entry.date, entry.currency) if key in existing_prices: if diffs: existing_entry = existing_prices[key] if existing_entry.amount == entry.amount: output = ignored_prices else: output = ignored_prices else: output = filtered_prices output.append(entry) return filtered_prices, ignored_prices","title":"filter_redundant_prices()"},{"location":"api_reference/beancount.prices.html#beancount.prices.price.now","text":"Indirection in order to be able to mock it out in the tests. Source code in beancount/prices/price.py def now(): \"Indirection in order to be able to mock it out in the tests.\" return datetime.datetime.now(datetime.timezone.utc)","title":"now()"},{"location":"api_reference/beancount.prices.html#beancount.prices.price.process_args","text":"Process the arguments. This also initializes the logging module. Returns: A tuple of \u2013 args: The argparse receiver of command-line arguments. jobs: A list of DatedPrice job objects. entries: A list of all the parsed entries. Source code in beancount/prices/price.py def process_args(): \"\"\"Process the arguments. This also initializes the logging module. Returns: A tuple of: args: The argparse receiver of command-line arguments. jobs: A list of DatedPrice job objects. entries: A list of all the parsed entries. \"\"\" parser = version.ArgumentParser(description=beancount.prices.__doc__.splitlines()[0]) # Input sources or filenames. parser.add_argument('sources', nargs='+', help=( 'A list of filenames (or source \"module/symbol\", if -e is ' 'specified) from which to create a list of jobs.')) parser.add_argument('-e', '--expressions', '--expression', action='store_true', help=( 'Interpret the arguments as \"module/symbol\" source strings.')) # Regular options. parser.add_argument('-v', '--verbose', action='count', help=( \"Print out progress log. Specify twice for debugging info.\")) parser.add_argument('-d', '--date', action='store', type=date_utils.parse_date_liberally, help=( \"Specify the date for which to fetch the prices.\")) parser.add_argument('-i', '--inactive', action='store_true', help=( \"Select all commodities from input files, not just the ones active on the date\")) parser.add_argument('-u', '--undeclared', action='store', help=( \"Include commodities viewed in the file even without a \" \"corresponding Commodity directive, from this default source. \" \"The currency name itself is used as the lookup symbol in this default source.\")) parser.add_argument('-c', '--clobber', action='store_true', help=( \"Do not skip prices which are already present in input files; fetch them anyway.\")) parser.add_argument('-a', '--all', action='store_true', help=( \"A shorthand for --inactive, --undeclared, --clobber.\")) parser.add_argument('-s', '--swap-inverted', action='store_true', help=( \"For inverted sources, swap currencies instead of inverting the rate. \" \"For example, if fetching the rate for CAD from 'USD:google/^CURRENCY:USDCAD' \" \"results in 1.25, by default we would output \\\"price CAD 0.8000 USD\\\". \" \"Using this option we would instead output \\\" price USD 1.2500 CAD\\\".\")) parser.add_argument('-n', '--dry-run', action='store_true', help=( \"Don't actually fetch the prices, just print the list of the ones to be fetched.\")) # Caching options. cache_group = parser.add_argument_group('cache') cache_filename = path.join(tempfile.gettempdir(), \"{}.cache\".format(path.basename(sys.argv[0]))) cache_group.add_argument('--cache', dest='cache_filename', action='store', default=cache_filename, help=\"Enable the cache and with the given cache name.\") cache_group.add_argument('--no-cache', dest='cache_filename', action='store_const', const=None, help=\"Disable the price cache.\") cache_group.add_argument('--clear-cache', action='store_true', help=\"Clear the cache prior to startup\") args = parser.parse_args() verbose_levels = {None: logging.WARN, 0: logging.WARN, 1: logging.INFO, 2: logging.DEBUG} logging.basicConfig(level=verbose_levels[args.verbose], format='%(levelname)-8s: %(message)s') if args.all: args.inactive = args.clobber = True args.undeclared = DEFAULT_SOURCE # Setup for processing. setup_cache(args.cache_filename, args.clear_cache) # Get the list of DatedPrice jobs to get from the arguments. logging.info(\"Processing at date: %s\", args.date or datetime.date.today()) jobs = [] all_entries = [] dcontext = None if args.expressions: # Interpret the arguments as price sources. for source_str in args.sources: psources = [] try: psource_map = find_prices.parse_source_map(source_str) except ValueError: extra = \"; did you provide a filename?\" if path.exists(source_str) else '' msg = ('Invalid source \"{{}}\"{}. '.format(extra) + 'Supported format is \"CCY:module/SYMBOL\"') parser.error(msg.format(source_str)) else: for currency, psources in psource_map.items(): jobs.append(find_prices.DatedPrice( psources[0].symbol, currency, args.date, psources)) else: # Interpret the arguments as Beancount input filenames. for filename in args.sources: if not path.exists(filename) or not path.isfile(filename): parser.error('File does not exist: \"{}\"; ' 'did you mean to use -e?'.format(filename)) continue logging.info('Loading \"%s\"', filename) entries, errors, options_map = loader.load_file(filename, log_errors=sys.stderr) if dcontext is None: dcontext = options_map['dcontext'] jobs.extend( find_prices.get_price_jobs_at_date( entries, args.date, args.inactive, args.undeclared)) all_entries.extend(entries) return args, jobs, data.sorted(all_entries), dcontext","title":"process_args()"},{"location":"api_reference/beancount.prices.html#beancount.prices.price.reset_cache","text":"Reset the cache to its uninitialized state. Source code in beancount/prices/price.py def reset_cache(): \"\"\"Reset the cache to its uninitialized state.\"\"\" global _CACHE if _CACHE is not None: _CACHE.close() _CACHE = None","title":"reset_cache()"},{"location":"api_reference/beancount.prices.html#beancount.prices.price.setup_cache","text":"Setup the results cache. Parameters: cache_filename \u2013 A string or None, the filename for the cache. clear_cache \u2013 A boolean, if true, delete the cache before beginning. Source code in beancount/prices/price.py def setup_cache(cache_filename, clear_cache): \"\"\"Setup the results cache. Args: cache_filename: A string or None, the filename for the cache. clear_cache: A boolean, if true, delete the cache before beginning. \"\"\" if clear_cache and cache_filename and path.exists(cache_filename): logging.info(\"Clearing cache %s\", cache_filename) os.remove(cache_filename) if cache_filename: logging.info('Using price cache at \"%s\" (with indefinite expiration)', cache_filename) global _CACHE _CACHE = shelve.open(cache_filename, 'c') _CACHE.expiration = DEFAULT_EXPIRATION","title":"setup_cache()"},{"location":"api_reference/beancount.prices.html#beancount.prices.source","text":"Interface definition for all price sources. This module describes the contract to be fulfilled by all implementations of price sources.","title":"source"},{"location":"api_reference/beancount.prices.html#beancount.prices.source.Source","text":"Interface to be implemented by all price sources.","title":"Source"},{"location":"api_reference/beancount.prices.html#beancount.prices.source.Source.get_historical_price","text":"Return the historical price found for the symbol at the given date. This could be the price of the close of the day, for instance. We assume that there is some single price representative of the day. Parameters: ticker \u2013 A string, the ticker to be fetched by the source. This ticker may include structure, such as the exchange code. Also note that this ticker is source-specified, and is not necessarily the same value as the commodity symbol used in the Beancount file. time \u2013 The timestamp at which to query for the price. This is a timezone-aware timestamp you can convert to any timezone. For past dates we query for a time that is equivalent to 4pm in the user's timezone. Returns: A SourcePrice instance. If the price could not be fetched, None is returned and another source should be consulted. There is never any guarantee that a price source will be able to fetch its value; client code must be able to handle this. Also note that the price's returned time must be timezone-aware. Source code in beancount/prices/source.py def get_historical_price(self, ticker, time): \"\"\"Return the historical price found for the symbol at the given date. This could be the price of the close of the day, for instance. We assume that there is some single price representative of the day. Args: ticker: A string, the ticker to be fetched by the source. This ticker may include structure, such as the exchange code. Also note that this ticker is source-specified, and is not necessarily the same value as the commodity symbol used in the Beancount file. time: The timestamp at which to query for the price. This is a timezone-aware timestamp you can convert to any timezone. For past dates we query for a time that is equivalent to 4pm in the user's timezone. Returns: A SourcePrice instance. If the price could not be fetched, None is returned and another source should be consulted. There is never any guarantee that a price source will be able to fetch its value; client code must be able to handle this. Also note that the price's returned time must be timezone-aware. \"\"\"","title":"get_historical_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.source.Source.get_latest_price","text":"Fetch the current latest price. The date may differ. This routine attempts to fetch the most recent available price, and returns the actual date of the quoted price, which may differ from the date this call is made at. {1cfa25e37fc1} Parameters: ticker \u2013 A string, the ticker to be fetched by the source. This ticker may include structure, such as the exchange code. Also note that this ticker is source-specified, and is not necessarily the same value as the commodity symbol used in the Beancount file. Returns: A SourcePrice instance. If the price could not be fetched, None is returned and another source should be consulted. There is never any guarantee that a price source will be able to fetch its value; client code must be able to handle this. Also note that the price's returned time must be timezone-aware. Source code in beancount/prices/source.py def get_latest_price(self, ticker): \"\"\"Fetch the current latest price. The date may differ. This routine attempts to fetch the most recent available price, and returns the actual date of the quoted price, which may differ from the date this call is made at. {1cfa25e37fc1} Args: ticker: A string, the ticker to be fetched by the source. This ticker may include structure, such as the exchange code. Also note that this ticker is source-specified, and is not necessarily the same value as the commodity symbol used in the Beancount file. Returns: A SourcePrice instance. If the price could not be fetched, None is returned and another source should be consulted. There is never any guarantee that a price source will be able to fetch its value; client code must be able to handle this. Also note that the price's returned time must be timezone-aware. \"\"\"","title":"get_latest_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.source.SourcePrice","text":"SourcePrice(price, time, quote_currency)","title":"SourcePrice"},{"location":"api_reference/beancount.prices.html#beancount.prices.source.SourcePrice.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/prices/source.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.prices.html#beancount.prices.source.SourcePrice.__new__","text":"Create new instance of SourcePrice(price, time, quote_currency)","title":"__new__()"},{"location":"api_reference/beancount.prices.html#beancount.prices.source.SourcePrice.__repr__","text":"Return a nicely formatted representation string Source code in beancount/prices/source.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources","text":"Implementation of various price extractors. This package is looked up by the driver script to figure out which extractor to use.","title":"sources"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.coinbase","text":"A source fetching cryptocurrency prices from Coinbase. Valid tickers are in the form \"XXX-YYY\", such as \"BTC-USD\". Here is the API documentation: https://developers.coinbase.com/api/v2 For example: https://api.coinbase.com/v2/prices/BTC-GBP/spot Timezone information: Input and output datetimes are specified via UTC timestamps.","title":"coinbase"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.coinbase.CoinbaseError","text":"An error from the Coinbase API.","title":"CoinbaseError"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.coinbase.Source","text":"Coinbase API price extractor.","title":"Source"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.coinbase.Source.get_historical_price","text":"See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/coinbase.py def get_historical_price(self, ticker, time): \"\"\"See contract in beancount.prices.source.Source.\"\"\" raise NotImplementedError( \"As of Feb 2019, historical prices are not supported on Coinbase. \" \"Please check the API to see if this has changed: \" \"https://developers.coinbase.com/apo/v2\")","title":"get_historical_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.coinbase.Source.get_latest_price","text":"See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/coinbase.py def get_latest_price(self, ticker): \"\"\"See contract in beancount.prices.source.Source.\"\"\" return fetch_quote(ticker)","title":"get_latest_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.coinbase.fetch_quote","text":"Fetch a quote from Coinbase. Source code in beancount/prices/sources/coinbase.py def fetch_quote(ticker): \"\"\"Fetch a quote from Coinbase.\"\"\" url = \"https://api.coinbase.com/v2/prices/{}/spot\".format(ticker.lower()) response = requests.get(url) if response.status_code != requests.codes.ok: raise CoinbaseError(\"Invalid response ({}): {}\".format(response.status_code, response.text)) result = response.json() price = D(result['data']['amount']).quantize(D('0.01')) time = datetime.datetime.now(tz.tzutc()) currency = result['data']['currency'] return source.SourcePrice(price, time, currency)","title":"fetch_quote()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.iex","text":"Fetch prices from the IEX 1.0 public API. This is a really fantastic exchange API with a lot of relevant information. Timezone information: There is currency no support for historical prices. The output datetime is provided as a UNIX timestamp.","title":"iex"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.iex.IEXError","text":"An error from the IEX API.","title":"IEXError"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.iex.Source","text":"IEX API price extractor.","title":"Source"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.iex.Source.get_historical_price","text":"See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/iex.py def get_historical_price(self, ticker, time): \"\"\"See contract in beancount.prices.source.Source.\"\"\" raise NotImplementedError( \"This is now implemented at https://iextrading.com/developers/docs/#hist and \" \"needs to be added here.\")","title":"get_historical_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.iex.Source.get_latest_price","text":"See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/iex.py def get_latest_price(self, ticker): \"\"\"See contract in beancount.prices.source.Source.\"\"\" return fetch_quote(ticker)","title":"get_latest_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.iex.fetch_quote","text":"Fetch the latest price for the given ticker. Source code in beancount/prices/sources/iex.py def fetch_quote(ticker): \"\"\"Fetch the latest price for the given ticker.\"\"\" url = \"https://api.iextrading.com/1.0/tops/last?symbols={}\".format(ticker.upper()) response = requests.get(url) if response.status_code != requests.codes.ok: raise IEXError(\"Invalid response ({}): {}\".format( response.status_code, response.text)) results = response.json() if len(results) != 1: raise IEXError(\"Invalid number of responses from IEX: {}\".format( response.text)) result = results[0] price = D(result['price']).quantize(D('0.01')) # IEX is American markets. us_timezone = tz.gettz(\"America/New_York\") time = datetime.datetime.fromtimestamp(result['time'] / 1000) time = time.astimezone(us_timezone) # As far as can tell, all the instruments on IEX are priced in USD. return source.SourcePrice(price, time, 'USD')","title":"fetch_quote()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.oanda","text":"A source fetching currency prices from OANDA. Valid tickers are in the form \"XXX_YYY\", such as \"EUR_USD\". Here is the API documentation: https://developer.oanda.com/rest-live/rates/ For example: https://api-fxtrade.oanda.com/v1/candles?instrument=EUR_USD&granularity=D&start=2016-03-27T00%3A00%3A00Z&end=2016-04-04T00%3A00%3A00Z&candleFormat=midpoint Timezone information: Input and output datetimes are specified via UTC timestamps.","title":"oanda"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.oanda.Source","text":"OANDA price source extractor.","title":"Source"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.oanda.Source.get_historical_price","text":"See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/oanda.py def get_historical_price(self, ticker, time): \"\"\"See contract in beancount.prices.source.Source.\"\"\" time = time.astimezone(tz.tzutc()) query_interval_begin = (time - datetime.timedelta(days=5)) query_interval_end = (time + datetime.timedelta(days=1)) params_dict = { 'instrument': ticker, 'granularity': 'H2', # Every two hours. 'candleFormat': 'midpoint', 'start': query_interval_begin.isoformat('T'), 'end': query_interval_end.isoformat('T'), } return _fetch_price(params_dict, time)","title":"get_historical_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.oanda.Source.get_latest_price","text":"See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/oanda.py def get_latest_price(self, ticker): \"\"\"See contract in beancount.prices.source.Source.\"\"\" time = datetime.datetime.now(tz.tzutc()) params_dict = { 'instrument': ticker, 'granularity': 'S5', # Every two hours. 'count': '10', 'candleFormat': 'midpoint', } return _fetch_price(params_dict, time)","title":"get_latest_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.quandl","text":"Fetch prices from Quandl's simple URL-based API. Quandl is a useful source of alternative data and it offers a simple REST API that serves CSV and JSON and XML formats. There's also a Python client library, but we specifically avoid using that here, in order to keep Beancount dependency-free. Many of the datasets are freely available, which is why this is included here. You can get information about the available databases and associated lists of symbols you can use here: https://www.quandl.com/search If you have a paid account and would like to be able to access the premium databases from the Quandl site, you can set QUANDL_API_KEY environment variable. Use the \":\" format to refer to Quandl symbols. Note that their symbols are usually identified by \"/\". (For now, this supports only the Time-Series API. There is also a Tables API, which could easily get integrated. We would just have to encode the 'datatable_code' and 'format' and perhaps other fields in the ticker name.) Timezone information: Input and output datetimes are limited to dates, and I believe the dates are presumed to live in the timezone of each particular data source. (It's unclear, not documented.)","title":"quandl"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.quandl.QuandlError","text":"An error from the Quandl API.","title":"QuandlError"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.quandl.Source","text":"Quandl API price extractor.","title":"Source"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.quandl.Source.get_historical_price","text":"See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/quandl.py def get_historical_price(self, ticker, time): \"\"\"See contract in beancount.prices.source.Source.\"\"\" return fetch_time_series(ticker, time)","title":"get_historical_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.quandl.Source.get_latest_price","text":"See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/quandl.py def get_latest_price(self, ticker): \"\"\"See contract in beancount.prices.source.Source.\"\"\" return fetch_time_series(ticker)","title":"get_latest_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.quandl.fetch_time_series","text":"Fetch Source code in beancount/prices/sources/quandl.py def fetch_time_series(ticker, time=None): \"\"\"Fetch\"\"\" # Create request payload. database, dataset = parse_ticker(ticker) url = \"https://www.quandl.com/api/v3/datasets/{}/{}.json\".format(database, dataset) payload = {\"limit\": 1} if time is not None: date = time.date() payload[\"start_date\"] = (date - datetime.timedelta(days=10)).isoformat() payload[\"end_date\"] = date.isoformat() # Add API key, if it is set in the environment. if 'QUANDL_API_KEY' in os.environ: payload['api_key'] = os.environ['QUANDL_API_KEY'] # Fetch and process errors. response = requests.get(url, params=payload) if response.status_code != requests.codes.ok: raise QuandlError(\"Invalid response ({}): {}\".format(response.status_code, response.text)) result = response.json() if 'quandl_error' in result: raise QuandlError(result['quandl_error']['message']) # Parse result container. dataset = result['dataset'] column_names = dataset['column_names'] date_index = column_names.index('Date') try: data_index = column_names.index('Adj. Close') except ValueError: data_index = column_names.index('Close') data = dataset['data'][0] # Gather time and assume it's in UTC timezone (Quandl does not provide the # market's timezone). time = datetime.datetime.strptime(data[date_index], '%Y-%m-%d') time = time.replace(tzinfo=tz.tzutc()) # Gather price. # Quantize with the same precision default rendering of floats occur. price_float = data[data_index] price = D(price_float) match = re.search(r'(\\..*)', str(price_float)) if match: price = price.quantize(D(match.group(1))) # Note: There is no currency information in the response (surprising). return source.SourcePrice(price, time, None)","title":"fetch_time_series()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.quandl.parse_ticker","text":"Convert ticker to Quandl codes. Source code in beancount/prices/sources/quandl.py def parse_ticker(ticker): \"\"\"Convert ticker to Quandl codes.\"\"\" if not re.match(r\"[A-Z0-9]+:[A-Z0-9]+$\", ticker): raise ValueError('Invalid code. Use \"/\" format.') return tuple(ticker.split(\":\"))","title":"parse_ticker()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.yahoo","text":"Fetch prices from Yahoo Finance's CSV API. As of late 2017, the older Yahoo finance API deprecated. In particular, the ichart endpoint is gone, and the download endpoint requires a cookie (which could be gotten - here's some documentation for that http://blog.bradlucas.com/posts/2017-06-02-new-yahoo-finance-quote-download-url/). We're using both the v7 and v8 APIs here, both of which are, as far as I can tell, undocumented: https://query1.finance.yahoo.com/v7/finance/quote https://query1.finance.yahoo.com/v8/finance/chart/SYMBOL Timezone information: Input and output datetimes are specified via UNIX timestamps, but the timezone of the particular market is included in the output.","title":"yahoo"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.yahoo.Source","text":"Yahoo Finance CSV API price extractor.","title":"Source"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.yahoo.Source.get_historical_price","text":"See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/yahoo.py def get_historical_price(self, ticker, time): \"\"\"See contract in beancount.prices.source.Source.\"\"\" if requests is None: raise YahooError(\"You must install the 'requests' library.\") url = \"https://query1.finance.yahoo.com/v8/finance/chart/{}\".format(ticker) dt_start = time - datetime.timedelta(days=5) dt_end = time payload = { 'period1': int(dt_start.timestamp()), 'period2': int(dt_end.timestamp()), 'interval': '1d', } payload.update(_DEFAULT_PARAMS) response = requests.get(url, params=payload) result = parse_response(response) meta = result['meta'] timezone = datetime.timezone(datetime.timedelta(hours=meta['gmtoffset'] / 3600), meta['exchangeTimezoneName']) timestamp_array = result['timestamp'] close_array = result['indicators']['quote'][0]['close'] series = [(datetime.datetime.fromtimestamp(timestamp, tz=timezone), D(price)) for timestamp, price in zip(timestamp_array, close_array)] # Get the latest data returned. latest = None for data_dt, price in sorted(series): if data_dt >= time: break latest = data_dt, price if latest is None: raise YahooError(\"Could not find price before {} in {}\".format(time, series)) currency = result['meta']['currency'] return source.SourcePrice(price, data_dt, currency)","title":"get_historical_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.yahoo.Source.get_latest_price","text":"See contract in beancount.prices.source.Source. Source code in beancount/prices/sources/yahoo.py def get_latest_price(self, ticker): \"\"\"See contract in beancount.prices.source.Source.\"\"\" url = \"https://query1.finance.yahoo.com/v7/finance/quote\" fields = ['symbol', 'regularMarketPrice', 'regularMarketTime'] payload = { 'symbols': ticker, 'fields': ','.join(fields), 'exchange': 'NYSE', } payload.update(_DEFAULT_PARAMS) response = requests.get(url, params=payload) result = parse_response(response) try: price = D(result['regularMarketPrice']) timezone = datetime.timezone( datetime.timedelta(hours=result['gmtOffSetMilliseconds'] / 3600000), result['exchangeTimezoneName']) trade_time = datetime.datetime.fromtimestamp(result['regularMarketTime'], tz=timezone) except KeyError: raise YahooError(\"Invalid response from Yahoo: {}\".format(repr(result))) currency = parse_currency(result) return source.SourcePrice(price, trade_time, currency)","title":"get_latest_price()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.yahoo.YahooError","text":"An error from the Yahoo API.","title":"YahooError"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.yahoo.parse_currency","text":"Infer the currency from the result. Source code in beancount/prices/sources/yahoo.py def parse_currency(result: Dict[str, Any]) -> str: \"\"\"Infer the currency from the result.\"\"\" if 'market' not in result: return None return _MARKETS.get(result['market'], None)","title":"parse_currency()"},{"location":"api_reference/beancount.prices.html#beancount.prices.sources.yahoo.parse_response","text":"Process as response from Yahoo. Exceptions: YahooError \u2013 If there is an error in the response. Source code in beancount/prices/sources/yahoo.py def parse_response(response: requests.models.Response) -> Dict: \"\"\"Process as response from Yahoo. Raises: YahooError: If there is an error in the response. \"\"\" json = response.json(parse_float=D) content = next(iter(json.values())) if response.status_code != requests.codes.ok: raise YahooError(\"Status {}: {}\".format(response.status_code, content['error'])) if len(json) != 1: raise YahooError(\"Invalid format in response from Yahoo; many keys: {}\".format( ','.join(json.keys()))) if content['error'] is not None: raise YahooError(\"Error fetching Yahoo data: {}\".format(content['error'])) return content['result'][0]","title":"parse_response()"},{"location":"api_reference/beancount.query.html","text":"beancount.query \uf0c1 Transaction and postings filtering syntax parser. beancount.query.numberify \uf0c1 Code to split table columns containing amounts and inventories into number columns. For example, given a column with this content: ----- amount ------ 101.23 USD 200 JPY 99.23 USD 38.34 USD, 100 JPY We can convert this into two columns and remove the currencies: -amount (USD)- -amount (JPY)- 101.23 200 99.23 38.34 100 The point is that the columns should be typed as numbers to make this importable into a spreadsheet and able to be processed. Notes: This handles the Amount, Position and Inventory datatypes. There is code to automatically recognize columns containing such types from a table of strings and convert such columns to their corresponding guessed data types. The per-currency columns are ordered in decreasing order of the number of instances of numbers seen for each currency. So if the most numbers you have in a column are USD, then the USD column renders first. Cost basis specifications should be unmodified and reported to a dedicated extra column, like this: ----- amount ------ 1 AAPL {21.23 USD} We can convert this into two columns and remove the currencies: -amount (AAPL)- -Cost basis- 1 {21.23 USD} (Eventually we might support the conversion of cost amounts as well, but they may contain other information, such as a label or a date, so for now we don't convert them. I'm not sure there's a good practical use case in doing that yet.) We may provide some options to break out only some of the currencies into columns, in order to handle the case where an inventory contains a large number of currencies and we want to only operate on a restricted set of operating currencies. If you provide a DisplayFormatter object to the numberification routine, they quantize each column according to their currency's precision. It is recommended that you do that. beancount.query.numberify.AmountConverter \uf0c1 A converter that extracts the number of an amount for a specific currency. beancount.query.numberify.AmountConverter.dtype \uf0c1 Construct a new Decimal object. 'value' can be an integer, string, tuple, or another Decimal object. If no value is given, return Decimal('0'). The context does not affect the conversion and is only passed to determine if the InvalidOperation trap is active. beancount.query.numberify.IdentityConverter \uf0c1 A converter that simply copies its column. beancount.query.numberify.InventoryConverter \uf0c1 A converter that extracts the number of a inventory for a specific currency. If there are multiple lots we aggregate by currency. beancount.query.numberify.InventoryConverter.dtype \uf0c1 Construct a new Decimal object. 'value' can be an integer, string, tuple, or another Decimal object. If no value is given, return Decimal('0'). The context does not affect the conversion and is only passed to determine if the InvalidOperation trap is active. beancount.query.numberify.PositionConverter \uf0c1 A converter that extracts the number of a position for a specific currency. beancount.query.numberify.PositionConverter.dtype \uf0c1 Construct a new Decimal object. 'value' can be an integer, string, tuple, or another Decimal object. If no value is given, return Decimal('0'). The context does not affect the conversion and is only passed to determine if the InvalidOperation trap is active. beancount.query.numberify.convert_col_Amount(name, drows, index) \uf0c1 Create converters for a column of type Amount. Parameters: name \u2013 A string, the column name. drows \u2013 The table of objects. index \u2013 The column number. Returns: A list of Converter instances, one for each of the currency types found. Source code in beancount/query/numberify.py def convert_col_Amount(name, drows, index): \"\"\"Create converters for a column of type Amount. Args: name: A string, the column name. drows: The table of objects. index: The column number. Returns: A list of Converter instances, one for each of the currency types found. \"\"\" currency_map = collections.defaultdict(int) for drow in drows: vamount = drow[index] if vamount and vamount.currency: currency_map[vamount.currency] += 1 return [AmountConverter('{} ({})'.format(name, currency), index, currency) for currency, _ in sorted(currency_map.items(), key=lambda item: (item[1], item[0]), reverse=True)] beancount.query.numberify.convert_col_Inventory(name, drows, index) \uf0c1 Create converters for a column of type Inventory. Parameters: name \u2013 A string, the column name. drows \u2013 The table of objects. index \u2013 The column number. Returns: A list of Converter instances, one for each of the currency types found. Source code in beancount/query/numberify.py def convert_col_Inventory(name, drows, index): \"\"\"Create converters for a column of type Inventory. Args: name: A string, the column name. drows: The table of objects. index: The column number. Returns: A list of Converter instances, one for each of the currency types found. \"\"\" currency_map = collections.defaultdict(int) for drow in drows: inv = drow[index] for currency in inv.currencies(): currency_map[currency] += 1 return [InventoryConverter('{} ({})'.format(name, currency), index, currency) for currency, _ in sorted(currency_map.items(), key=lambda item: (item[1], item[0]), reverse=True)] beancount.query.numberify.convert_col_Position(name, drows, index) \uf0c1 Create converters for a column of type Position. Parameters: name \u2013 A string, the column name. drows \u2013 The table of objects. index \u2013 The column number. Returns: A list of Converter instances, one for each of the currency types found. Source code in beancount/query/numberify.py def convert_col_Position(name, drows, index): \"\"\"Create converters for a column of type Position. Args: name: A string, the column name. drows: The table of objects. index: The column number. Returns: A list of Converter instances, one for each of the currency types found. \"\"\" currency_map = collections.defaultdict(int) for drow in drows: pos = drow[index] if pos and pos.units.currency: currency_map[pos.units.currency] += 1 return [PositionConverter('{} ({})'.format(name, currency), index, currency) for currency, _ in sorted(currency_map.items(), key=lambda item: (item[1], item[0]), reverse=True)] beancount.query.numberify.numberify_results(dtypes, drows, dformat=None) \uf0c1 Number rows containing Amount, Position or Inventory types. Parameters: result_types \u2013 A list of items describing the names and data types of the items in each column. result_rows \u2013 A list of ResultRow instances. dformat \u2013 An optional DisplayFormatter. If set, quantize the numbers by their currency-specific precision when converting the Amount's, Position's or Inventory'es.. Returns: A pair of modified (result_types, result_rows) with converted datatypes. Source code in beancount/query/numberify.py def numberify_results(dtypes, drows, dformat=None): \"\"\"Number rows containing Amount, Position or Inventory types. Args: result_types: A list of items describing the names and data types of the items in each column. result_rows: A list of ResultRow instances. dformat: An optional DisplayFormatter. If set, quantize the numbers by their currency-specific precision when converting the Amount's, Position's or Inventory'es.. Returns: A pair of modified (result_types, result_rows) with converted datatypes. \"\"\" # Build an array of converters. converters = [] for index, col_desc in enumerate(dtypes): name, dtype = col_desc convert_col_fun = CONVERTING_TYPES.get(dtype, None) if convert_col_fun is None: converters.append(IdentityConverter(name, dtype, index)) else: col_converters = convert_col_fun(name, drows, index) converters.extend(col_converters) # Derive the output types from the expected outputs from the converters # themselves. otypes = [(c.name, c.dtype) for c in converters] # Convert the input rows by processing them through the converters. orows = [] for drow in drows: orow = [] for converter in converters: orow.append(converter(drow, dformat)) orows.append(orow) return otypes, orows beancount.query.query \uf0c1 A library to run queries. This glues together all the parts of the query engine. beancount.query.query.run_query(entries, options_map, query, *format_args, *, numberify=False) \uf0c1 Compile and execute a query, return the result types and rows. Parameters: entries \u2013 A list of entries, as produced by the loader. options_map \u2013 A dict of options, as produced by the loader. query \u2013 A string, a single BQL query, optionally containing some new-style (e.g., {}) formatting specifications. format_args \u2013 A tuple of arguments to be formatted in the query. This is just provided as a convenience. numberify \u2013 If true, numberify the results before returning them. Returns: A pair of result types and result rows. Exceptions: ParseError \u2013 If the statement cannot be parsed. CompilationError \u2013 If the statement cannot be compiled. Source code in beancount/query/query.py def run_query(entries, options_map, query, *format_args, numberify=False): \"\"\"Compile and execute a query, return the result types and rows. Args: entries: A list of entries, as produced by the loader. options_map: A dict of options, as produced by the loader. query: A string, a single BQL query, optionally containing some new-style (e.g., {}) formatting specifications. format_args: A tuple of arguments to be formatted in the query. This is just provided as a convenience. numberify: If true, numberify the results before returning them. Returns: A pair of result types and result rows. Raises: ParseError: If the statement cannot be parsed. CompilationError: If the statement cannot be compiled. \"\"\" env_targets = query_env.TargetsEnvironment() env_entries = query_env.FilterEntriesEnvironment() env_postings = query_env.FilterPostingsEnvironment() # Apply formatting to the query. formatted_query = query.format(*format_args) # Parse the statement. parser = query_parser.Parser() statement = parser.parse(formatted_query) # Compile the SELECT statement. c_query = query_compile.compile(statement, env_targets, env_postings, env_entries) # Execute it to obtain the result rows. rtypes, rrows = query_execute.execute_query(c_query, entries, options_map) # Numberify the results, if requested. if numberify: dformat = options_map['dcontext'].build() rtypes, rrows = numberify_lib.numberify_results(rtypes, rrows, dformat) return rtypes, rrows beancount.query.query_compile \uf0c1 Interpreter for the query language's AST. This code accepts the abstract syntax tree produced by the query parser, resolves the column and function names, compiles and interpreter and prepares a query to be run against a list of entries. beancount.query.query_compile.CompilationEnvironment \uf0c1 Base class for all compilation contexts. A compilation context provides column accessors specific to the particular row objects that we will access. beancount.query.query_compile.CompilationEnvironment.get_column(self, name) \uf0c1 Return a column accessor for the given named column. Parameters: name \u2013 A string, the name of the column to access. Source code in beancount/query/query_compile.py def get_column(self, name): \"\"\"Return a column accessor for the given named column. Args: name: A string, the name of the column to access. \"\"\" try: return self.columns[name]() except KeyError: raise CompilationError(\"Invalid column name '{}' in {} context.\".format( name, self.context_name)) beancount.query.query_compile.CompilationEnvironment.get_function(self, name, operands) \uf0c1 Return a function accessor for the given named function. Parameters: name \u2013 A string, the name of the function to access. Source code in beancount/query/query_compile.py def get_function(self, name, operands): \"\"\"Return a function accessor for the given named function. Args: name: A string, the name of the function to access. \"\"\" try: key = tuple([name] + [operand.dtype for operand in operands]) return self.functions[key](operands) except KeyError: # If not found with the operands, try just looking it up by name. try: return self.functions[name](operands) except KeyError: signature = '{}({})'.format(name, ', '.join(operand.dtype.__name__ for operand in operands)) raise CompilationError(\"Invalid function '{}' in {} context\".format( signature, self.context_name)) beancount.query.query_compile.CompilationError ( Exception ) \uf0c1 A compiler/interpreter error. beancount.query.query_compile.EvalAggregator ( EvalFunction ) \uf0c1 Base class for all aggregator evaluator types. beancount.query.query_compile.EvalAggregator.__call__(self, context) special \uf0c1 Return the value on evaluation. Parameters: context \u2013 The evaluation object to which the evaluation need to apply. This is either an entry, a Posting instance, or a particular result set row from a sub-select. This is the provider for the underlying data. Returns: The final aggregated value. Source code in beancount/query/query_compile.py def __call__(self, context): \"\"\"Return the value on evaluation. Args: context: The evaluation object to which the evaluation need to apply. This is either an entry, a Posting instance, or a particular result set row from a sub-select. This is the provider for the underlying data. Returns: The final aggregated value. \"\"\" # Return None by default. beancount.query.query_compile.EvalAggregator.allocate(self, allocator) \uf0c1 Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_compile.py def allocate(self, allocator): \"\"\"Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Args: allocator: An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. \"\"\" # Do nothing by default. beancount.query.query_compile.EvalAggregator.finalize(self, store) \uf0c1 Finalize this node's aggregate data and return it. For aggregate methods, this finalizes the node and returns the final value. The context node will be the alloc instead of the context object. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_compile.py def finalize(self, store): \"\"\"Finalize this node's aggregate data and return it. For aggregate methods, this finalizes the node and returns the final value. The context node will be the alloc instead of the context object. Args: store: An object indexable by handles appropriated during allocate(). \"\"\" # Do nothing by default. beancount.query.query_compile.EvalAggregator.initialize(self, store) \uf0c1 Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_compile.py def initialize(self, store): \"\"\"Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Args: store: An object indexable by handles appropriated during allocate(). \"\"\" # Do nothing by default. beancount.query.query_compile.EvalAggregator.update(self, store, context) \uf0c1 Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_compile.py def update(self, store, context): \"\"\"Evaluate this node. This is designed to recurse on its children. Args: store: An object indexable by handles appropriated during allocate(). context: The object to which the evaluation need to apply (see __call__). \"\"\" # Do nothing by default. beancount.query.query_compile.EvalColumn ( EvalNode ) \uf0c1 Base class for all column accessors. beancount.query.query_compile.EvalFrom ( tuple ) \uf0c1 EvalFrom(c_expr, open, close, clear) beancount.query.query_compile.EvalFrom.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_compile.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.query.query_compile.EvalFrom.__new__(_cls, c_expr, open, close, clear) special staticmethod \uf0c1 Create new instance of EvalFrom(c_expr, open, close, clear) beancount.query.query_compile.EvalFrom.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/query/query_compile.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.query.query_compile.EvalFrom._asdict(self) private \uf0c1 Return a new dict which maps field names to their values. Source code in beancount/query/query_compile.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self)) beancount.query.query_compile.EvalFrom._make(iterable) classmethod private \uf0c1 Make a new EvalFrom object from a sequence or iterable Source code in beancount/query/query_compile.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result beancount.query.query_compile.EvalFrom._replace(/, self, **kwds) private \uf0c1 Return a new EvalFrom object replacing specified fields with new values Source code in beancount/query/query_compile.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result beancount.query.query_compile.EvalFunction ( EvalNode ) \uf0c1 Base class for all function objects. beancount.query.query_compile.EvalNode \uf0c1 beancount.query.query_compile.EvalNode.__call__(self, context) special \uf0c1 Evaluate this node. This is designed to recurse on its children. All subclasses must override and implement this method. Parameters: context \u2013 The evaluation object to which the evaluation need to apply. This is either an entry, a Posting instance, or a particular result set row from a sub-select. This is the provider for the underlying data. Returns: The evaluated value for this sub-expression tree. Source code in beancount/query/query_compile.py def __call__(self, context): \"\"\"Evaluate this node. This is designed to recurse on its children. All subclasses must override and implement this method. Args: context: The evaluation object to which the evaluation need to apply. This is either an entry, a Posting instance, or a particular result set row from a sub-select. This is the provider for the underlying data. Returns: The evaluated value for this sub-expression tree. \"\"\" raise NotImplementedError beancount.query.query_compile.EvalNode.__eq__(self, other) special \uf0c1 Override the equality operator to compare the data type and a all attributes of this node. This is used by tests for comparing nodes. Source code in beancount/query/query_compile.py def __eq__(self, other): \"\"\"Override the equality operator to compare the data type and a all attributes of this node. This is used by tests for comparing nodes. \"\"\" return (isinstance(other, type(self)) and all( getattr(self, attribute) == getattr(other, attribute) for attribute in self.__slots__)) beancount.query.query_compile.EvalNode.__repr__(self) special \uf0c1 Return str(self). Source code in beancount/query/query_compile.py def __str__(self): return \"{}({})\".format(type(self).__name__, ', '.join(repr(getattr(self, child)) for child in self.__slots__)) beancount.query.query_compile.EvalNode.childnodes(self) \uf0c1 Returns the child nodes of this node. Yields: A list of EvalNode instances. Source code in beancount/query/query_compile.py def childnodes(self): \"\"\"Returns the child nodes of this node. Yields: A list of EvalNode instances. \"\"\" for attr in self.__slots__: child = getattr(self, attr) if isinstance(child, EvalNode): yield child elif isinstance(child, list): for element in child: if isinstance(element, EvalNode): yield element beancount.query.query_compile.EvalPrint ( tuple ) \uf0c1 EvalPrint(c_from,) beancount.query.query_compile.EvalPrint.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_compile.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.query.query_compile.EvalPrint.__new__(_cls, c_from) special staticmethod \uf0c1 Create new instance of EvalPrint(c_from,) beancount.query.query_compile.EvalPrint.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/query/query_compile.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.query.query_compile.EvalPrint._asdict(self) private \uf0c1 Return a new dict which maps field names to their values. Source code in beancount/query/query_compile.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self)) beancount.query.query_compile.EvalPrint._make(iterable) classmethod private \uf0c1 Make a new EvalPrint object from a sequence or iterable Source code in beancount/query/query_compile.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result beancount.query.query_compile.EvalPrint._replace(/, self, **kwds) private \uf0c1 Return a new EvalPrint object replacing specified fields with new values Source code in beancount/query/query_compile.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result beancount.query.query_compile.EvalQuery ( tuple ) \uf0c1 EvalQuery(c_targets, c_from, c_where, group_indexes, order_indexes, ordering, limit, distinct, flatten) beancount.query.query_compile.EvalQuery.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_compile.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.query.query_compile.EvalQuery.__new__(_cls, c_targets, c_from, c_where, group_indexes, order_indexes, ordering, limit, distinct, flatten) special staticmethod \uf0c1 Create new instance of EvalQuery(c_targets, c_from, c_where, group_indexes, order_indexes, ordering, limit, distinct, flatten) beancount.query.query_compile.EvalQuery.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/query/query_compile.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.query.query_compile.EvalQuery._asdict(self) private \uf0c1 Return a new dict which maps field names to their values. Source code in beancount/query/query_compile.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self)) beancount.query.query_compile.EvalQuery._make(iterable) classmethod private \uf0c1 Make a new EvalQuery object from a sequence or iterable Source code in beancount/query/query_compile.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result beancount.query.query_compile.EvalQuery._replace(/, self, **kwds) private \uf0c1 Return a new EvalQuery object replacing specified fields with new values Source code in beancount/query/query_compile.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result beancount.query.query_compile.EvalTarget ( tuple ) \uf0c1 EvalTarget(c_expr, name, is_aggregate) beancount.query.query_compile.EvalTarget.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_compile.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.query.query_compile.EvalTarget.__new__(_cls, c_expr, name, is_aggregate) special staticmethod \uf0c1 Create new instance of EvalTarget(c_expr, name, is_aggregate) beancount.query.query_compile.EvalTarget.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/query/query_compile.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.query.query_compile.EvalTarget._asdict(self) private \uf0c1 Return a new dict which maps field names to their values. Source code in beancount/query/query_compile.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self)) beancount.query.query_compile.EvalTarget._make(iterable) classmethod private \uf0c1 Make a new EvalTarget object from a sequence or iterable Source code in beancount/query/query_compile.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result beancount.query.query_compile.EvalTarget._replace(/, self, **kwds) private \uf0c1 Return a new EvalTarget object replacing specified fields with new values Source code in beancount/query/query_compile.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result beancount.query.query_compile.ResultSetEnvironment ( CompilationEnvironment ) \uf0c1 An execution context that provides access to attributes from a result set. beancount.query.query_compile.ResultSetEnvironment.get_column(self, name) \uf0c1 Override the column getter to provide a single attribute getter. Source code in beancount/query/query_compile.py def get_column(self, name): \"\"\"Override the column getter to provide a single attribute getter. \"\"\" # FIXME: How do we figure out the data type here? We need the context. return AttributeColumn(name) beancount.query.query_compile._get_columns_and_aggregates(node, columns, aggregates) private \uf0c1 Walk down a tree of nodes and fetch the column accessors and aggregates. This function ignores all nodes under aggregate nodes. Parameters: node \u2013 An instance of EvalNode. columns \u2013 An accumulator for columns found so far. aggregate \u2013 An accumulator for aggregate notes found so far. Source code in beancount/query/query_compile.py def _get_columns_and_aggregates(node, columns, aggregates): \"\"\"Walk down a tree of nodes and fetch the column accessors and aggregates. This function ignores all nodes under aggregate nodes. Args: node: An instance of EvalNode. columns: An accumulator for columns found so far. aggregate: An accumulator for aggregate notes found so far. \"\"\" if isinstance(node, EvalAggregator): aggregates.append(node) elif isinstance(node, EvalColumn): columns.append(node) else: for child in node.childnodes(): _get_columns_and_aggregates(child, columns, aggregates) beancount.query.query_compile.compile(statement, targets_environ, postings_environ, entries_environ) \uf0c1 Prepare an AST any of the statement into an executable statement. Parameters: statement \u2013 An instance of the parser's Select, Balances, Journal or Print. targets_environ \u2013 A compilation environment for evaluating targets. postings_environ \u2013 : A compilation environment for evaluating postings filters. entries_environ \u2013 : A compilation environment for evaluating entry filters. Returns: An instance of EvalQuery or EvalPrint, ready to be executed. Exceptions: CompilationError \u2013 If the statement cannot be compiled, or is not one of the supported statements. Source code in beancount/query/query_compile.py def compile(statement, targets_environ, postings_environ, entries_environ): \"\"\"Prepare an AST any of the statement into an executable statement. Args: statement: An instance of the parser's Select, Balances, Journal or Print. targets_environ: A compilation environment for evaluating targets. postings_environ: : A compilation environment for evaluating postings filters. entries_environ: : A compilation environment for evaluating entry filters. Returns: An instance of EvalQuery or EvalPrint, ready to be executed. Raises: CompilationError: If the statement cannot be compiled, or is not one of the supported statements. \"\"\" if isinstance(statement, query_parser.Balances): statement = transform_balances(statement) elif isinstance(statement, query_parser.Journal): statement = transform_journal(statement) if isinstance(statement, query_parser.Select): c_query = compile_select(statement, targets_environ, postings_environ, entries_environ) elif isinstance(statement, query_parser.Print): c_query = compile_print(statement, entries_environ) else: raise CompilationError( \"Cannot compile a statement of type '{}'\".format(type(statement))) return c_query beancount.query.query_compile.compile_expression(expr, environ) \uf0c1 Bind an expression to its execution context. Parameters: expr \u2013 The root node of an expression. environ \u2013 An CompilationEnvironment instance. Returns: The root node of a bound expression. Source code in beancount/query/query_compile.py def compile_expression(expr, environ): \"\"\"Bind an expression to its execution context. Args: expr: The root node of an expression. environ: An CompilationEnvironment instance. Returns: The root node of a bound expression. \"\"\" # Convert column references to the context. if isinstance(expr, query_parser.Column): c_expr = environ.get_column(expr.name) elif isinstance(expr, query_parser.Function): c_operands = [compile_expression(operand, environ) for operand in expr.operands] c_expr = environ.get_function(expr.fname, c_operands) elif isinstance(expr, query_parser.UnaryOp): node_type = OPERATORS[type(expr)] c_expr = node_type(compile_expression(expr.operand, environ)) elif isinstance(expr, query_parser.BinaryOp): node_type = OPERATORS[type(expr)] c_expr = node_type(compile_expression(expr.left, environ), compile_expression(expr.right, environ)) elif isinstance(expr, query_parser.Constant): c_expr = EvalConstant(expr.value) else: assert False, \"Invalid expression to compile: {}\".format(expr) return c_expr beancount.query.query_compile.compile_from(from_clause, environ) \uf0c1 Compiled a From clause as provided by the parser, in the given environment. Parameters: select \u2013 An instance of query_parser.Select. environ \u2013 : A compilation context for evaluating entry filters. Returns: An instance of Query, ready to be executed. Source code in beancount/query/query_compile.py def compile_from(from_clause, environ): \"\"\"Compiled a From clause as provided by the parser, in the given environment. Args: select: An instance of query_parser.Select. environ: : A compilation context for evaluating entry filters. Returns: An instance of Query, ready to be executed. \"\"\" if from_clause is not None: c_expression = (compile_expression(from_clause.expression, environ) if from_clause.expression is not None else None) # Check that the from clause does not contain aggregates. if c_expression is not None and is_aggregate(c_expression): raise CompilationError(\"Aggregates are not allowed in from clause\") if (isinstance(from_clause.open, datetime.date) and isinstance(from_clause.close, datetime.date) and from_clause.open > from_clause.close): raise CompilationError(\"Invalid dates: CLOSE date must follow OPEN date\") c_from = EvalFrom(c_expression, from_clause.open, from_clause.close, from_clause.clear) else: c_from = None return c_from beancount.query.query_compile.compile_group_by(group_by, c_targets, environ) \uf0c1 Process a group-by clause. Parameters: group_by \u2013 A GroupBy instance as provided by the parser. c_targets \u2013 A list of compiled target expressions. environ \u2013 A compilation context to be used to evaluate GROUP BY expressions. Returns: A tuple of new_targets \u2013 A list of new compiled target nodes. group_indexes: If the query is an aggregate query, a list of integer indexes to be used for processing grouping. Note that this list may be empty (in the case of targets with only aggregates). On the other hand, if this is not an aggregated query, this is set to None. So do distinguish the empty list vs. None. Source code in beancount/query/query_compile.py def compile_group_by(group_by, c_targets, environ): \"\"\"Process a group-by clause. Args: group_by: A GroupBy instance as provided by the parser. c_targets: A list of compiled target expressions. environ: A compilation context to be used to evaluate GROUP BY expressions. Returns: A tuple of new_targets: A list of new compiled target nodes. group_indexes: If the query is an aggregate query, a list of integer indexes to be used for processing grouping. Note that this list may be empty (in the case of targets with only aggregates). On the other hand, if this is not an aggregated query, this is set to None. So do distinguish the empty list vs. None. \"\"\" new_targets = copy.copy(c_targets) c_target_expressions = [c_target.c_expr for c_target in c_targets] group_indexes = [] if group_by: # Check that HAVING is not supported yet. if group_by and group_by.having is not None: raise CompilationError(\"The HAVING clause is not supported yet\") assert group_by.columns, \"Internal error with GROUP-BY parsing\" # Compile group-by expressions and resolve them to their targets if # possible. A GROUP-BY column may be one of the following: # # * A reference to a target by name. # * A reference to a target by index (starting at one). # * A new, non-aggregate expression. # # References by name are converted to indexes. New expressions are # inserted into the list of targets as invisible targets. targets_name_map = {target.name: index for index, target in enumerate(c_targets)} for column in group_by.columns: index = None # Process target references by index. if isinstance(column, int): index = column - 1 if not (0 <= index < len(c_targets)): raise CompilationError( \"Invalid GROUP-BY column index {}\".format(column)) else: # Process target references by name. These will be parsed as # simple Column expressions. If they refer to a target name, we # resolve them. if isinstance(column, query_parser.Column): name = column.name index = targets_name_map.get(name, None) # Otherwise we compile the expression and add it to the list of # targets to evaluate and index into that new target. if index is None: c_expr = compile_expression(column, environ) # Check if the new expression is an aggregate. aggregate = is_aggregate(c_expr) if aggregate: raise CompilationError( \"GROUP-BY expressions may not be aggregates: '{}'\".format( column)) # Attempt to reconcile the expression with one of the existing # target expressions. try: index = c_target_expressions.index(c_expr) except ValueError: # Add the new target. 'None' for the target name implies it # should be invisible, not to be rendered. index = len(new_targets) new_targets.append(EvalTarget(c_expr, None, aggregate)) c_target_expressions.append(c_expr) assert index is not None, \"Internal error, could not index group-by reference.\" group_indexes.append(index) # Check that the group-by column references a non-aggregate. c_expr = new_targets[index].c_expr if is_aggregate(c_expr): raise CompilationError( \"GROUP-BY expressions may not reference aggregates: '{}'\".format( column)) # Check that the group-by column has a supported hashable type. if not is_hashable_type(c_expr): raise CompilationError( \"GROUP-BY a non-hashable type is not supported: '{}'\".format( column)) else: # If it does not have a GROUP-BY clause... aggregate_bools = [c_target.is_aggregate for c_target in c_targets] if any(aggregate_bools): # If the query is an aggregate query, check that all the targets are # aggregates. if all(aggregate_bools): assert group_indexes == [] else: # If some of the targets aren't aggregates, automatically infer # that they are to be implicit group by targets. This makes for # a much more convenient syntax for our lightweight SQL, where # grouping is optional. if SUPPORT_IMPLICIT_GROUPBY: group_indexes = [index for index, c_target in enumerate(c_targets) if not c_target.is_aggregate] else: raise CompilationError( \"Aggregate query without a GROUP-BY should have only aggregates\") else: # This is not an aggregate query; don't set group_indexes to # anything useful, we won't need it. group_indexes = None return new_targets[len(c_targets):], group_indexes beancount.query.query_compile.compile_order_by(order_by, c_targets, environ) \uf0c1 Process an order-by clause. Parameters: order_by \u2013 A OrderBy instance as provided by the parser. c_targets \u2013 A list of compiled target expressions. environ \u2013 A compilation context to be used to evaluate ORDER BY expressions. Returns: A tuple of new_targets \u2013 A list of new compiled target nodes. order_indexes: A list of integer indexes to be used for processing ordering. Source code in beancount/query/query_compile.py def compile_order_by(order_by, c_targets, environ): \"\"\"Process an order-by clause. Args: order_by: A OrderBy instance as provided by the parser. c_targets: A list of compiled target expressions. environ: A compilation context to be used to evaluate ORDER BY expressions. Returns: A tuple of new_targets: A list of new compiled target nodes. order_indexes: A list of integer indexes to be used for processing ordering. \"\"\" new_targets = copy.copy(c_targets) c_target_expressions = [c_target.c_expr for c_target in c_targets] order_indexes = [] # Compile order-by expressions and resolve them to their targets if # possible. A ORDER-BY column may be one of the following: # # * A reference to a target by name. # * A reference to a target by index (starting at one). # * A new expression, aggregate or not. # # References by name are converted to indexes. New expressions are # inserted into the list of targets as invisible targets. targets_name_map = {target.name: index for index, target in enumerate(c_targets)} for column in order_by.columns: index = None # Process target references by index. if isinstance(column, int): index = column - 1 if not (0 <= index < len(c_targets)): raise CompilationError( \"Invalid ORDER-BY column index {}\".format(column)) else: # Process target references by name. These will be parsed as # simple Column expressions. If they refer to a target name, we # resolve them. if isinstance(column, query_parser.Column): name = column.name index = targets_name_map.get(name, None) # Otherwise we compile the expression and add it to the list of # targets to evaluate and index into that new target. if index is None: c_expr = compile_expression(column, environ) # Attempt to reconcile the expression with one of the existing # target expressions. try: index = c_target_expressions.index(c_expr) except ValueError: # Add the new target. 'None' for the target name implies it # should be invisible, not to be rendered. index = len(new_targets) new_targets.append(EvalTarget(c_expr, None, is_aggregate(c_expr))) c_target_expressions.append(c_expr) assert index is not None, \"Internal error, could not index order-by reference.\" order_indexes.append(index) return (new_targets[len(c_targets):], order_indexes) beancount.query.query_compile.compile_print(print_stmt, env_entries) \uf0c1 Compile a Print statement. Parameters: statement \u2013 An instance of query_parser.Print. entries_environ \u2013 : A compilation environment for evaluating entry filters. Returns: An instance of EvalPrint, ready to be executed. Source code in beancount/query/query_compile.py def compile_print(print_stmt, env_entries): \"\"\"Compile a Print statement. Args: statement: An instance of query_parser.Print. entries_environ: : A compilation environment for evaluating entry filters. Returns: An instance of EvalPrint, ready to be executed. \"\"\" c_from = compile_from(print_stmt.from_clause, env_entries) return EvalPrint(c_from) beancount.query.query_compile.compile_select(select, targets_environ, postings_environ, entries_environ) \uf0c1 Prepare an AST for a Select statement into a very rudimentary execution tree. The execution tree mostly looks much like an AST, but with some nodes replaced with knowledge specific to an execution context and eventually some basic optimizations. Parameters: select \u2013 An instance of query_parser.Select. targets_environ \u2013 A compilation environment for evaluating targets. postings_environ \u2013 A compilation environment for evaluating postings filters. entries_environ \u2013 A compilation environment for evaluating entry filters. Returns: An instance of EvalQuery, ready to be executed. Source code in beancount/query/query_compile.py def compile_select(select, targets_environ, postings_environ, entries_environ): \"\"\"Prepare an AST for a Select statement into a very rudimentary execution tree. The execution tree mostly looks much like an AST, but with some nodes replaced with knowledge specific to an execution context and eventually some basic optimizations. Args: select: An instance of query_parser.Select. targets_environ: A compilation environment for evaluating targets. postings_environ: A compilation environment for evaluating postings filters. entries_environ: A compilation environment for evaluating entry filters. Returns: An instance of EvalQuery, ready to be executed. \"\"\" # Process the FROM clause and figure out the execution environment for the # targets and the where clause. from_clause = select.from_clause if isinstance(from_clause, query_parser.Select): c_from = None environ_target = ResultSetEnvironment() environ_where = ResultSetEnvironment() # Remove this when we add support for nested queries. raise CompilationError(\"Queries from nested SELECT are not supported yet\") if from_clause is None or isinstance(from_clause, query_parser.From): # Bind the from clause contents. c_from = compile_from(from_clause, entries_environ) environ_target = targets_environ environ_where = postings_environ else: raise CompilationError(\"Unexpected from clause in AST: {}\".format(from_clause)) # Compile the targets. c_targets = compile_targets(select.targets, environ_target) # Bind the WHERE expression to the execution environment. if select.where_clause is not None: c_where = compile_expression(select.where_clause, environ_where) # Aggregates are disallowed in this clause. Check for this. # NOTE: This should never trigger if the compilation environment does not # contain any aggregate. Just being manic and safe here. if is_aggregate(c_where): raise CompilationError(\"Aggregates are disallowed in WHERE clause\") else: c_where = None # Process the GROUP-BY clause. new_targets, group_indexes = compile_group_by(select.group_by, c_targets, environ_target) if new_targets: c_targets.extend(new_targets) # Process the ORDER-BY clause. if select.order_by is not None: (new_targets, order_indexes) = compile_order_by(select.order_by, c_targets, environ_target) if new_targets: c_targets.extend(new_targets) ordering = select.order_by.ordering else: order_indexes = None ordering = None # If this is an aggregate query (it groups, see list of indexes), check that # the set of non-aggregates match exactly the group indexes. This should # always be the case at this point, because we have added all the necessary # targets to the list of group-by expressions and should have resolved all # the indexes. if group_indexes is not None: non_aggregate_indexes = set(index for index, c_target in enumerate(c_targets) if not c_target.is_aggregate) if non_aggregate_indexes != set(group_indexes): missing_names = ['\"{}\"'.format(c_targets[index].name) for index in non_aggregate_indexes - set(group_indexes)] raise CompilationError( \"All non-aggregates must be covered by GROUP-BY clause in aggregate query; \" \"the following targets are missing: {}\".format(\",\".join(missing_names))) # Check that PIVOT-BY is not supported yet. if select.pivot_by is not None: raise CompilationError(\"The PIVOT BY clause is not supported yet\") return EvalQuery(c_targets, c_from, c_where, group_indexes, order_indexes, ordering, select.limit, select.distinct, select.flatten) beancount.query.query_compile.compile_targets(targets, environ) \uf0c1 Compile the targets and check for their validity. Process wildcard. Parameters: targets \u2013 A list of target expressions from the parser. environ \u2013 A compilation context for the targets. Returns: A list of compiled target expressions with resolved names. Source code in beancount/query/query_compile.py def compile_targets(targets, environ): \"\"\"Compile the targets and check for their validity. Process wildcard. Args: targets: A list of target expressions from the parser. environ: A compilation context for the targets. Returns: A list of compiled target expressions with resolved names. \"\"\" # Bind the targets expressions to the execution context. if isinstance(targets, query_parser.Wildcard): # Insert the full list of available columns. targets = [query_parser.Target(query_parser.Column(name), None) for name in environ.wildcard_columns] # Compile targets. c_targets = [] target_names = set() for target in targets: c_expr = compile_expression(target.expression, environ) target_name = find_unique_name( target.name or query_parser.get_expression_name(target.expression), target_names) target_names.add(target_name) c_targets.append(EvalTarget(c_expr, target_name, is_aggregate(c_expr))) # Figure out if this query is an aggregate query and check validity of each # target's aggregation type. for index, c_target in enumerate(c_targets): columns, aggregates = get_columns_and_aggregates(c_target.c_expr) # Check for mixed aggregates and non-aggregates. if columns and aggregates: raise CompilationError( \"Mixed aggregates and non-aggregates are not allowed\") if aggregates: # Check for aggregates of aggregates. for aggregate in aggregates: for child in aggregate.childnodes(): if is_aggregate(child): raise CompilationError( \"Aggregates of aggregates are not allowed\") return c_targets beancount.query.query_compile.find_unique_name(name, allocated_set) \uf0c1 Come up with a unique name for 'name' amongst 'allocated_set'. Parameters: name \u2013 A string, the prefix of the name to find a unique for. allocated_set \u2013 A set of string, the set of already allocated names. Returns: A unique name. 'allocated_set' is unmodified. Source code in beancount/query/query_compile.py def find_unique_name(name, allocated_set): \"\"\"Come up with a unique name for 'name' amongst 'allocated_set'. Args: name: A string, the prefix of the name to find a unique for. allocated_set: A set of string, the set of already allocated names. Returns: A unique name. 'allocated_set' is unmodified. \"\"\" # Make sure the name is unique. prefix = name i = 1 while name in allocated_set: name = '{}_{}'.format(prefix, i) i += 1 return name beancount.query.query_compile.get_columns_and_aggregates(node) \uf0c1 Find the columns and aggregate nodes below this tree. All nodes under aggregate nodes are ignored. Parameters: node \u2013 An instance of EvalNode. Returns: A pair of (columns, aggregates), both of which are lists of EvalNode instances. columns \u2013 The list of all columns accessed not under an aggregate node. aggregates: The list of all aggregate nodes. Source code in beancount/query/query_compile.py def get_columns_and_aggregates(node): \"\"\"Find the columns and aggregate nodes below this tree. All nodes under aggregate nodes are ignored. Args: node: An instance of EvalNode. Returns: A pair of (columns, aggregates), both of which are lists of EvalNode instances. columns: The list of all columns accessed not under an aggregate node. aggregates: The list of all aggregate nodes. \"\"\" columns = [] aggregates = [] _get_columns_and_aggregates(node, columns, aggregates) return columns, aggregates beancount.query.query_compile.is_aggregate(node) \uf0c1 Return true if the node is an aggregate. Parameters: node \u2013 An instance of EvalNode. Returns: A boolean. Source code in beancount/query/query_compile.py def is_aggregate(node): \"\"\"Return true if the node is an aggregate. Args: node: An instance of EvalNode. Returns: A boolean. \"\"\" # Note: We could be a tiny bit more efficient here, but it doesn't matter # much. Performance of the query compilation matters very little overall. _, aggregates = get_columns_and_aggregates(node) return bool(aggregates) beancount.query.query_compile.is_hashable_type(node) \uf0c1 Return true if the node is of a hashable type. Parameters: node \u2013 An instance of EvalNode. Returns: A boolean. Source code in beancount/query/query_compile.py def is_hashable_type(node): \"\"\"Return true if the node is of a hashable type. Args: node: An instance of EvalNode. Returns: A boolean. \"\"\" return not issubclass(node.dtype, inventory.Inventory) beancount.query.query_compile.transform_balances(balances) \uf0c1 Translate a Balances entry into an uncompiled Select statement. Parameters: balances \u2013 An instance of a Balance object. Returns: An instance of an uncompiled Select object. Source code in beancount/query/query_compile.py def transform_balances(balances): \"\"\"Translate a Balances entry into an uncompiled Select statement. Args: balances: An instance of a Balance object. Returns: An instance of an uncompiled Select object. \"\"\" ## FIXME: Change the aggregation rules to allow GROUP-BY not to include the ## non-aggregate ORDER-BY columns, so we could just GROUP-BY accounts here ## instead of having to include the sort-key. I think it should be fine if ## the first or last sort-order value gets used, because it would simplify ## the input statement. cooked_select = query_parser.Parser().parse(\"\"\" SELECT account, SUM({}(position)) GROUP BY account, ACCOUNT_SORTKEY(account) ORDER BY ACCOUNT_SORTKEY(account) \"\"\".format(balances.summary_func or \"\")) return query_parser.Select(cooked_select.targets, balances.from_clause, balances.where_clause, cooked_select.group_by, cooked_select.order_by, None, None, None, None) beancount.query.query_compile.transform_journal(journal) \uf0c1 Translate a Journal entry into an uncompiled Select statement. Parameters: journal \u2013 An instance of a Journal object. Returns: An instance of an uncompiled Select object. Source code in beancount/query/query_compile.py def transform_journal(journal): \"\"\"Translate a Journal entry into an uncompiled Select statement. Args: journal: An instance of a Journal object. Returns: An instance of an uncompiled Select object. \"\"\" cooked_select = query_parser.Parser().parse(\"\"\" SELECT date, flag, MAXWIDTH(payee, 48), MAXWIDTH(narration, 80), account, {summary_func}(position), {summary_func}(balance) {where} \"\"\".format(where=('WHERE account ~ \"{}\"'.format(journal.account) if journal.account else ''), summary_func=journal.summary_func or '')) return query_parser.Select(cooked_select.targets, journal.from_clause, cooked_select.where_clause, None, None, None, None, None, None) beancount.query.query_compile_test \uf0c1 beancount.query.query_compile_test.CompileSelectBase ( TestCase ) \uf0c1 beancount.query.query_compile_test.CompileSelectBase.assertCompile(self, expected, query, debug=False) \uf0c1 Assert parsed and compiled contents from 'query' is 'expected'. Parameters: expected \u2013 An expected AST to compare against the parsed value. query \u2013 An SQL query to be parsed. debug \u2013 A boolean, if true, print extra debugging information on the console. Exceptions: AssertionError \u2013 If the actual AST does not match the expected one. Source code in beancount/query/query_compile_test.py def assertCompile(self, expected, query, debug=False): \"\"\"Assert parsed and compiled contents from 'query' is 'expected'. Args: expected: An expected AST to compare against the parsed value. query: An SQL query to be parsed. debug: A boolean, if true, print extra debugging information on the console. Raises: AssertionError: If the actual AST does not match the expected one. \"\"\" actual = self.compile(query) if debug: print() print() print(actual) print() try: self.assertEqual(expected, actual) return actual except AssertionError: print() print(\"Expected: {}\".format(expected)) print(\"Actual : {}\".format(actual)) raise beancount.query.query_compile_test.CompileSelectBase.assertIndexes(self, query, expected_simple_indexes, expected_aggregate_indexes, expected_group_indexes, expected_order_indexes) \uf0c1 Check the four lists of indexes for comparison. Parameters: query \u2013 An instance of EvalQuery, a compiled query statement. expected_simple_indexes \u2013 The expected visible non-aggregate indexes. expected_aggregate_indexes \u2013 The expected visible aggregate indexes. expected_group_indexes \u2013 The expected group_indexes. expected_order_indexes \u2013 The expected order_indexes. Exceptions: AssertionError \u2013 if the check fails. Source code in beancount/query/query_compile_test.py def assertIndexes(self, query, expected_simple_indexes, expected_aggregate_indexes, expected_group_indexes, expected_order_indexes): \"\"\"Check the four lists of indexes for comparison. Args: query: An instance of EvalQuery, a compiled query statement. expected_simple_indexes: The expected visible non-aggregate indexes. expected_aggregate_indexes: The expected visible aggregate indexes. expected_group_indexes: The expected group_indexes. expected_order_indexes: The expected order_indexes. Raises: AssertionError: if the check fails. \"\"\" # Compute the list of _visible_ aggregates and non-aggregates. simple_indexes = [index for index, c_target in enumerate(query.c_targets) if c_target.name and not qc.is_aggregate(c_target.expression)] aggregate_indexes = [index for index, c_target in enumerate(query.c_targets) if c_target.name and qc.is_aggregate(c_target.expression)] self.assertEqual(set(expected_simple_indexes), set(simple_indexes)) self.assertEqual(set(expected_aggregate_indexes), set(aggregate_indexes)) self.assertEqual( set(expected_group_indexes) if expected_group_indexes is not None else None, set(query.group_indexes) if query.group_indexes is not None else None) self.assertEqual( set(expected_order_indexes) if expected_order_indexes is not None else None, set(query.order_indexes) if query.order_indexes is not None else None) beancount.query.query_compile_test.CompileSelectBase.assertSelectInvariants(self, query) \uf0c1 Assert the invariants on the query. Parameters: query \u2013 An instance of EvalQuery, a compiled query statement. Exceptions: AssertionError \u2013 if the check fails. Source code in beancount/query/query_compile_test.py def assertSelectInvariants(self, query): \"\"\"Assert the invariants on the query. Args: query: An instance of EvalQuery, a compiled query statement. Raises: AssertionError: if the check fails. \"\"\" # Check that the group references cover all the simple indexes. if query.group_indexes is not None: non_aggregate_indexes = [index for index, c_target in enumerate(query.c_targets) if not qc.is_aggregate(c_target.c_expr)] self.assertEqual(set(non_aggregate_indexes), set(query.group_indexes), \"Invalid indexes: {}\".format(query)) beancount.query.query_compile_test.CompileSelectBase.compile(self, query) \uf0c1 Parse one query and compile it. Parameters: query \u2013 An SQL query to be parsed. Returns: The AST. Source code in beancount/query/query_compile_test.py def compile(self, query): \"\"\"Parse one query and compile it. Args: query: An SQL query to be parsed. Returns: The AST. \"\"\" statement = self.parse(query) c_query = qc.compile(statement, self.xcontext_targets, self.xcontext_postings, self.xcontext_entries) if isinstance(c_query, qp.Select): self.assertSelectInvariants(c_query) return c_query beancount.query.query_compile_test.CompileSelectBase.setUp(self) \uf0c1 Hook method for setting up the test fixture before exercising it. Source code in beancount/query/query_compile_test.py def setUp(self): self.parser = qp.Parser() beancount.query.query_env \uf0c1 Environment object for compiler. This module contains the various column accessors and function evaluators that are made available by the query compiler via their compilation context objects. Define new columns and functions here. beancount.query.query_env.AbsDecimal ( EvalFunction ) \uf0c1 Compute the length of the argument. This works on sequences. beancount.query.query_env.AbsInventory ( EvalFunction ) \uf0c1 Compute the length of the argument. This works on sequences. beancount.query.query_env.AbsPosition ( EvalFunction ) \uf0c1 Compute the length of the argument. This works on sequences. beancount.query.query_env.AccountColumn ( EvalColumn ) \uf0c1 The account of the posting. beancount.query.query_env.AccountSortKey ( EvalFunction ) \uf0c1 Get a string to sort accounts in order taking into account the types. beancount.query.query_env.AnyMeta ( EvalFunction ) \uf0c1 Get metadata from the posting or its parent transaction's metadata if not present. beancount.query.query_env.BalanceColumn ( EvalColumn ) \uf0c1 The balance for the posting. These can be summed into inventories. beancount.query.query_env.CloseDate ( EvalFunction ) \uf0c1 Get the date of the close directive of the account. beancount.query.query_env.Coalesce ( EvalFunction ) \uf0c1 Return the first non-null argument beancount.query.query_env.ConvertAmount ( EvalFunction ) \uf0c1 Coerce an amount to a particular currency. beancount.query.query_env.ConvertAmountWithDate ( EvalFunction ) \uf0c1 Coerce an amount to a particular currency. beancount.query.query_env.ConvertInventory ( EvalFunction ) \uf0c1 Coerce an inventory to a particular currency. beancount.query.query_env.ConvertInventoryWithDate ( EvalFunction ) \uf0c1 Coerce an inventory to a particular currency. beancount.query.query_env.ConvertPosition ( EvalFunction ) \uf0c1 Coerce an amount to a particular currency. beancount.query.query_env.ConvertPositionWithDate ( EvalFunction ) \uf0c1 Coerce an amount to a particular currency. beancount.query.query_env.CostCurrencyColumn ( EvalColumn ) \uf0c1 The cost currency of the posting. beancount.query.query_env.CostDateColumn ( EvalColumn ) \uf0c1 The cost currency of the posting. beancount.query.query_env.CostInventory ( EvalFunction ) \uf0c1 Get the cost of an inventory. beancount.query.query_env.CostLabelColumn ( EvalColumn ) \uf0c1 The cost currency of the posting. beancount.query.query_env.CostNumberColumn ( EvalColumn ) \uf0c1 The number of cost units of the posting. beancount.query.query_env.CostPosition ( EvalFunction ) \uf0c1 Get the cost of a position. beancount.query.query_env.Count ( EvalAggregator ) \uf0c1 Count the number of occurrences of the argument. beancount.query.query_env.Count.allocate(self, allocator) \uf0c1 Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate() beancount.query.query_env.Count.initialize(self, store) \uf0c1 Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = 0 beancount.query.query_env.Count.update(self, store, unused_ontext) \uf0c1 Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, unused_ontext): store[self.handle] += 1 beancount.query.query_env.Currency ( EvalFunction ) \uf0c1 Extract the currency from an Amount. beancount.query.query_env.CurrencyColumn ( EvalColumn ) \uf0c1 The currency of the posting. beancount.query.query_env.CurrencyMeta ( EvalFunction ) \uf0c1 Get the metadata dict of the commodity directive of the currency. beancount.query.query_env.Date ( EvalFunction ) \uf0c1 Construct a date with year, month, day arguments beancount.query.query_env.DateAdd ( EvalFunction ) \uf0c1 Adds/subtracts number of days from the given date beancount.query.query_env.DateColumn ( EvalColumn ) \uf0c1 The date of the parent transaction for this posting. beancount.query.query_env.DateDiff ( EvalFunction ) \uf0c1 Calculates the difference (in days) between two dates beancount.query.query_env.DateEntryColumn ( EvalColumn ) \uf0c1 The date of the directive. beancount.query.query_env.Day ( EvalFunction ) \uf0c1 Extract the day from a date. beancount.query.query_env.DayColumn ( EvalColumn ) \uf0c1 The day of the date of the parent transaction for this posting. beancount.query.query_env.DayEntryColumn ( EvalColumn ) \uf0c1 The day of the date of the directive. beancount.query.query_env.DescriptionColumn ( EvalColumn ) \uf0c1 A combination of the payee + narration for the transaction of this posting. beancount.query.query_env.DescriptionEntryColumn ( EvalColumn ) \uf0c1 A combination of the payee + narration of the transaction, if present. beancount.query.query_env.EntryMeta ( EvalFunction ) \uf0c1 Get some metadata key of the parent directive (Transaction). beancount.query.query_env.FileLocationColumn ( EvalColumn ) \uf0c1 The filename:lineno where the posting was parsed from or created. If you select this column as the first column, because it renders like errors, Emacs is able to pick those up and you can navigate between an arbitrary list of transactions with next-error and previous-error. beancount.query.query_env.FilenameColumn ( EvalColumn ) \uf0c1 The filename where the posting was parsed from or created. beancount.query.query_env.FilenameEntryColumn ( EvalColumn ) \uf0c1 The filename where the directive was parsed from or created. beancount.query.query_env.FilterCurrencyInventory ( EvalFunction ) \uf0c1 Filter an inventory to just the specified currency. beancount.query.query_env.FilterCurrencyPosition ( EvalFunction ) \uf0c1 Filter an inventory to just the specified currency. beancount.query.query_env.FilterEntriesEnvironment ( CompilationEnvironment ) \uf0c1 An execution context that provides access to attributes on Transactions and other entry types. beancount.query.query_env.FilterPostingsEnvironment ( CompilationEnvironment ) \uf0c1 An execution context that provides access to attributes on Postings. beancount.query.query_env.FindFirst ( EvalFunction ) \uf0c1 Filter a string sequence by regular expression and return the first match. beancount.query.query_env.First ( EvalAggregator ) \uf0c1 Keep the first of the values seen. beancount.query.query_env.First.allocate(self, allocator) \uf0c1 Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate() beancount.query.query_env.First.initialize(self, store) \uf0c1 Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = None beancount.query.query_env.First.update(self, store, context) \uf0c1 Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): if store[self.handle] is None: value = self.eval_args(context)[0] store[self.handle] = value beancount.query.query_env.FlagColumn ( EvalColumn ) \uf0c1 The flag of the parent transaction for this posting. beancount.query.query_env.FlagEntryColumn ( EvalColumn ) \uf0c1 The flag the transaction. beancount.query.query_env.GetItemStr ( EvalFunction ) \uf0c1 Get the string value of a dict. The value is always converted to a string. beancount.query.query_env.Grep ( EvalFunction ) \uf0c1 Match a group against a string and return only the matched portion. beancount.query.query_env.GrepN ( EvalFunction ) \uf0c1 Match a pattern with subgroups against a string and return the subgroup at the index beancount.query.query_env.IdColumn ( EvalColumn ) \uf0c1 The unique id of the parent transaction for this posting. beancount.query.query_env.IdEntryColumn ( EvalColumn ) \uf0c1 Unique id of a directive. beancount.query.query_env.JoinStr ( EvalFunction ) \uf0c1 Join a sequence of strings to a single comma-separated string. beancount.query.query_env.Last ( EvalAggregator ) \uf0c1 Keep the last of the values seen. beancount.query.query_env.Last.allocate(self, allocator) \uf0c1 Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate() beancount.query.query_env.Last.initialize(self, store) \uf0c1 Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = None beancount.query.query_env.Last.update(self, store, context) \uf0c1 Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] store[self.handle] = value beancount.query.query_env.Leaf ( EvalFunction ) \uf0c1 Get the name of the leaf subaccount. beancount.query.query_env.Length ( EvalFunction ) \uf0c1 Compute the length of the argument. This works on sequences. beancount.query.query_env.LineNoColumn ( EvalColumn ) \uf0c1 The line number from the file the posting was parsed from. beancount.query.query_env.LineNoEntryColumn ( EvalColumn ) \uf0c1 The line number from the file the directive was parsed from. beancount.query.query_env.LinksColumn ( EvalColumn ) \uf0c1 The set of links of the parent transaction for this posting. beancount.query.query_env.LinksEntryColumn ( EvalColumn ) \uf0c1 The set of links of the transaction. beancount.query.query_env.MatchAccount ( EvalFunction ) \uf0c1 A predicate, true if the transaction has at least one posting matching the regular expression argument. beancount.query.query_env.Max ( EvalAggregator ) \uf0c1 Compute the maximum of the values. beancount.query.query_env.Max.allocate(self, allocator) \uf0c1 Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate() beancount.query.query_env.Max.initialize(self, store) \uf0c1 Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = self.dtype() beancount.query.query_env.Max.update(self, store, context) \uf0c1 Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] if value > store[self.handle]: store[self.handle] = value beancount.query.query_env.MaxWidth ( EvalFunction ) \uf0c1 Convert the argument to a substring. This can be used to ensure maximum width beancount.query.query_env.Meta ( EvalFunction ) \uf0c1 Get some metadata key of the Posting. beancount.query.query_env.Min ( EvalAggregator ) \uf0c1 Compute the minimum of the values. beancount.query.query_env.Min.allocate(self, allocator) \uf0c1 Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate() beancount.query.query_env.Min.initialize(self, store) \uf0c1 Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = self.dtype() beancount.query.query_env.Min.update(self, store, context) \uf0c1 Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] if value < store[self.handle]: store[self.handle] = value beancount.query.query_env.Month ( EvalFunction ) \uf0c1 Extract the month from a date. beancount.query.query_env.MonthColumn ( EvalColumn ) \uf0c1 The month of the date of the parent transaction for this posting. beancount.query.query_env.MonthEntryColumn ( EvalColumn ) \uf0c1 The month of the date of the directive. beancount.query.query_env.NarrationColumn ( EvalColumn ) \uf0c1 The narration of the parent transaction for this posting. beancount.query.query_env.NarrationEntryColumn ( EvalColumn ) \uf0c1 The narration of the transaction. beancount.query.query_env.Number ( EvalFunction ) \uf0c1 Extract the number from an Amount. beancount.query.query_env.NumberColumn ( EvalColumn ) \uf0c1 The number of units of the posting. beancount.query.query_env.OnlyInventory ( EvalFunction ) \uf0c1 Get one currency's amount from the inventory. beancount.query.query_env.OpenDate ( EvalFunction ) \uf0c1 Get the date of the open directive of the account. beancount.query.query_env.OpenMeta ( EvalFunction ) \uf0c1 Get the metadata dict of the open directive of the account. beancount.query.query_env.OtherAccountsColumn ( EvalColumn ) \uf0c1 The list of other accounts in the transaction, excluding that of this posting. beancount.query.query_env.Parent ( EvalFunction ) \uf0c1 Get the parent name of the account. beancount.query.query_env.ParseDate ( EvalFunction ) \uf0c1 Construct a date with year, month, day arguments beancount.query.query_env.PayeeColumn ( EvalColumn ) \uf0c1 The payee of the parent transaction for this posting. beancount.query.query_env.PayeeEntryColumn ( EvalColumn ) \uf0c1 The payee of the transaction. beancount.query.query_env.PosSignAmount ( EvalFunction ) \uf0c1 Correct sign of an Amount based on the usual balance of associated account. beancount.query.query_env.PosSignDecimal ( EvalFunction ) \uf0c1 Correct sign of an Amount based on the usual balance of associated account. beancount.query.query_env.PosSignInventory ( EvalFunction ) \uf0c1 Correct sign of an Amount based on the usual balance of associated account. beancount.query.query_env.PosSignPosition ( EvalFunction ) \uf0c1 Correct sign of an Amount based on the usual balance of associated account. beancount.query.query_env.PositionColumn ( EvalColumn ) \uf0c1 The position for the posting. These can be summed into inventories. beancount.query.query_env.PostingFlagColumn ( EvalColumn ) \uf0c1 The flag of the posting itself. beancount.query.query_env.Price ( EvalFunction ) \uf0c1 Fetch a price for something at a particular date beancount.query.query_env.PriceColumn ( EvalColumn ) \uf0c1 The price attached to the posting. beancount.query.query_env.PriceWithDate ( EvalFunction ) \uf0c1 Fetch a price for something at a particular date beancount.query.query_env.Quarter ( EvalFunction ) \uf0c1 Extract the quarter from a date. beancount.query.query_env.Root ( EvalFunction ) \uf0c1 Get the root name(s) of the account. beancount.query.query_env.SafeDiv ( EvalFunction ) \uf0c1 A division operation that swallows dbz exceptions and outputs 0 instead. beancount.query.query_env.Str ( EvalFunction ) \uf0c1 Convert the argument to a string. beancount.query.query_env.Subst ( EvalFunction ) \uf0c1 Substitute leftmost non-overlapping occurrences of pattern by replacement. beancount.query.query_env.Sum ( EvalAggregator ) \uf0c1 Calculate the sum of the numerical argument. beancount.query.query_env.Sum.allocate(self, allocator) \uf0c1 Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate() beancount.query.query_env.Sum.initialize(self, store) \uf0c1 Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = self.dtype() beancount.query.query_env.Sum.update(self, store, context) \uf0c1 Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] if value is not None: store[self.handle] += value beancount.query.query_env.SumAmount ( SumBase ) \uf0c1 Calculate the sum of the amount. The result is an Inventory. beancount.query.query_env.SumAmount.update(self, store, context) \uf0c1 Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] store[self.handle].add_amount(value) beancount.query.query_env.SumBase ( EvalAggregator ) \uf0c1 beancount.query.query_env.SumBase.allocate(self, allocator) \uf0c1 Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate() beancount.query.query_env.SumBase.initialize(self, store) \uf0c1 Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = inventory.Inventory() beancount.query.query_env.SumInventory ( SumBase ) \uf0c1 Calculate the sum of the inventories. The result is an Inventory. beancount.query.query_env.SumInventory.update(self, store, context) \uf0c1 Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] store[self.handle].add_inventory(value) beancount.query.query_env.SumPosition ( SumBase ) \uf0c1 Calculate the sum of the position. The result is an Inventory. beancount.query.query_env.SumPosition.update(self, store, context) \uf0c1 Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] store[self.handle].add_position(value) beancount.query.query_env.TagsColumn ( EvalColumn ) \uf0c1 The set of tags of the parent transaction for this posting. beancount.query.query_env.TagsEntryColumn ( EvalColumn ) \uf0c1 The set of tags of the transaction. beancount.query.query_env.TargetsEnvironment ( FilterPostingsEnvironment ) \uf0c1 An execution context that provides access to attributes on Postings. beancount.query.query_env.Today ( EvalFunction ) \uf0c1 Today's date beancount.query.query_env.TypeColumn ( EvalColumn ) \uf0c1 The data type of the parent transaction for this posting. beancount.query.query_env.TypeEntryColumn ( EvalColumn ) \uf0c1 The data type of the directive. beancount.query.query_env.UnitsInventory ( EvalFunction ) \uf0c1 Get the number of units of an inventory (stripping cost). beancount.query.query_env.UnitsPosition ( EvalFunction ) \uf0c1 Get the number of units of a position (stripping cost). beancount.query.query_env.ValueInventory ( EvalFunction ) \uf0c1 Coerce an inventory to its market value at the current date. beancount.query.query_env.ValueInventoryWithDate ( EvalFunction ) \uf0c1 Coerce an inventory to its market value at a particular date. beancount.query.query_env.ValuePosition ( EvalFunction ) \uf0c1 Convert a position to its cost currency at the market value. beancount.query.query_env.ValuePositionWithDate ( EvalFunction ) \uf0c1 Convert a position to its cost currency at the market value of a particular date. beancount.query.query_env.Weekday ( EvalFunction ) \uf0c1 Extract a 3-letter weekday from a date. beancount.query.query_env.WeightColumn ( EvalColumn ) \uf0c1 The computed weight used for this posting. beancount.query.query_env.Year ( EvalFunction ) \uf0c1 Extract the year from a date. beancount.query.query_env.YearColumn ( EvalColumn ) \uf0c1 The year of the date of the parent transaction for this posting. beancount.query.query_env.YearEntryColumn ( EvalColumn ) \uf0c1 The year of the date of the directive. beancount.query.query_env.YearMonth ( EvalFunction ) \uf0c1 Extract the year and month from a date. beancount.query.query_env._Neg ( EvalFunction ) private \uf0c1 Compute the negative value of the argument. This works on various types. beancount.query.query_env_test \uf0c1 beancount.query.query_env_test.TestEnv ( TestCase ) \uf0c1 beancount.query.query_env_test.TestEnv.test_AnyMeta(self, entries, _, options_map) \uf0c1 2016-11-20 * name: \"TheName\" address: \"1 Wrong Way\" empty: \"NotEmpty\" Assets:Banking 1 USD color: \"Green\" address: \"1 Right Way\" empty: Source code in beancount/query/query_env_test.py @parser.parse_doc() def test_AnyMeta(self, entries, _, options_map): \"\"\" 2016-11-20 * name: \"TheName\" address: \"1 Wrong Way\" empty: \"NotEmpty\" Assets:Banking 1 USD color: \"Green\" address: \"1 Right Way\" empty: \"\"\" rtypes, rrows = query.run_query(entries, options_map, 'SELECT ANY_META(\"name\") as m') self.assertEqual([('TheName',)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT ANY_META(\"color\") as m') self.assertEqual([('Green',)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT ANY_META(\"address\") as m') self.assertEqual([('1 Right Way',)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT ANY_META(\"empty\") as m') self.assertEqual([(None,)], rrows) beancount.query.query_env_test.TestEnv.test_Coalesce(self, entries, _, options_map) \uf0c1 2016-11-20 * Assets:Banking 1 USD Source code in beancount/query/query_env_test.py @parser.parse_doc() def test_Coalesce(self, entries, _, options_map): \"\"\" 2016-11-20 * Assets:Banking 1 USD \"\"\" rtypes, rrows = query.run_query(entries, options_map, 'SELECT COALESCE(account, price) as m') self.assertEqual([('Assets:Banking',)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT COALESCE(price, account) as m') self.assertEqual([('Assets:Banking',)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT COALESCE(price, cost_number) as m') self.assertEqual([(None,)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT COALESCE(narration, account) as m') self.assertEqual([('',)], rrows) beancount.query.query_env_test.TestEnv.test_Date(self, entries, _, options_map) \uf0c1 2016-11-20 * \"ok\" Assets:Banking 1 USD Source code in beancount/query/query_env_test.py @parser.parse_doc() def test_Date(self, entries, _, options_map): \"\"\" 2016-11-20 * \"ok\" Assets:Banking 1 USD \"\"\" rtypes, rrows = query.run_query(entries, options_map, 'SELECT date(2020, 1, 2) as m') self.assertEqual([(datetime.date(2020, 1, 2),)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT date(year, month, 1) as m') self.assertEqual([(datetime.date(2016, 11, 1),)], rrows) with self.assertRaisesRegex(ValueError, \"day is out of range for month\"): rtypes, rrows = query.run_query(entries, options_map, 'SELECT date(2020, 2, 32) as m') rtypes, rrows = query.run_query(entries, options_map, 'SELECT date(\"2020-01-02\") as m') self.assertEqual([(datetime.date(2020, 1, 2),)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT date(\"2016/11/1\") as m') self.assertEqual([(datetime.date(2016, 11, 1),)], rrows) beancount.query.query_env_test.TestEnv.test_DateDiffAdjust(self, entries, _, options_map) \uf0c1 2016-11-20 * \"ok\" Assets:Banking -1 STOCK { 5 USD, 2016-10-30 } Source code in beancount/query/query_env_test.py @parser.parse_doc() def test_DateDiffAdjust(self, entries, _, options_map): \"\"\" 2016-11-20 * \"ok\" Assets:Banking -1 STOCK { 5 USD, 2016-10-30 } \"\"\" rtypes, rrows = query.run_query(entries, options_map, 'SELECT date_diff(date, cost_date) as m') self.assertEqual([(21,)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT date_diff(cost_date, date) as m') self.assertEqual([(-21,)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT date_add(date, 1) as m') self.assertEqual([(datetime.date(2016, 11, 21),)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT date_add(date, -1) as m') self.assertEqual([(datetime.date(2016, 11, 19),)], rrows) beancount.query.query_env_test.TestEnv.test_GrepN(self, entries, _, options_map) \uf0c1 2016-11-20 * \"prev match in context next\" Assets:Banking 1 USD Source code in beancount/query/query_env_test.py @parser.parse_doc() def test_GrepN(self, entries, _, options_map): \"\"\" 2016-11-20 * \"prev match in context next\" Assets:Banking 1 USD \"\"\" rtypes, rrows = query.run_query(entries, options_map, ''' SELECT GREPN(\"in\", narration, 0) as m ''') self.assertEqual([('in',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT GREPN(\"match (.*) context\", narration, 1) as m ''') self.assertEqual([('in',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT GREPN(\"(.*) in (.*)\", narration, 2) as m ''') self.assertEqual([('context next',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT GREPN(\"ab(at)hing\", \"abathing\", 1) as m ''') self.assertEqual([('at',)], rrows) beancount.query.query_env_test.TestEnv.test_Subst(self, entries, _, options_map) \uf0c1 2016-11-20 * \"I love candy\" Assets:Banking -1 USD 2016-11-21 * \"Buy thing thing\" Assets:Cash -1 USD Source code in beancount/query/query_env_test.py @parser.parse_doc() def test_Subst(self, entries, _, options_map): \"\"\" 2016-11-20 * \"I love candy\" Assets:Banking -1 USD 2016-11-21 * \"Buy thing thing\" Assets:Cash -1 USD \"\"\" rtypes, rrows = query.run_query(entries, options_map, ''' SELECT SUBST(\"[Cc]andy\", \"carrots\", narration) as m where date = 2016-11-20 ''') self.assertEqual([('I love carrots',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT SUBST(\"thing\", \"t\", narration) as m where date = 2016-11-21 ''') self.assertEqual([('Buy t t',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT SUBST(\"random\", \"t\", narration) as m where date = 2016-11-21 ''') self.assertEqual([('Buy thing thing',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT SUBST(\"(love)\", \"\\\\1 \\\\1\", narration) as m where date = 2016-11-20 ''') self.assertEqual([('I love love candy',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT SUBST(\"Assets:.*\", \"Savings\", account) as a, str(sum(position)) as p ''') self.assertEqual([('Savings', '(-2 USD)')], rrows) beancount.query.query_execute \uf0c1 Execution of interpreter on data rows. beancount.query.query_execute.Allocator \uf0c1 A helper class to count slot allocations and return unique handles to them. beancount.query.query_execute.Allocator.allocate(self) \uf0c1 Allocate a new slot to store row aggregation information. Returns: A unique handle used to index into an row-aggregation store (an integer). Source code in beancount/query/query_execute.py def allocate(self): \"\"\"Allocate a new slot to store row aggregation information. Returns: A unique handle used to index into an row-aggregation store (an integer). \"\"\" handle = self.size self.size += 1 return handle beancount.query.query_execute.Allocator.create_store(self) \uf0c1 Create a new row-aggregation store suitable to contain all the node allocations. Returns: A store that can accommodate and be indexed by all the allocated slot handles. Source code in beancount/query/query_execute.py def create_store(self): \"\"\"Create a new row-aggregation store suitable to contain all the node allocations. Returns: A store that can accommodate and be indexed by all the allocated slot handles. \"\"\" return [None] * self.size beancount.query.query_execute.RowContext \uf0c1 A dumb container for information used by a row expression. beancount.query.query_execute.create_row_context(entries, options_map) \uf0c1 Create the context container which we will use to evaluate rows. Source code in beancount/query/query_execute.py def create_row_context(entries, options_map): \"\"\"Create the context container which we will use to evaluate rows.\"\"\" context = RowContext() context.balance = inventory.Inventory() # Initialize some global properties for use by some of the accessors. context.options_map = options_map context.account_types = options.get_account_types(options_map) context.open_close_map = getters.get_account_open_close(entries) context.commodity_map = getters.get_commodity_map(entries) context.price_map = prices.build_price_map(entries) return context beancount.query.query_execute.execute_print(c_print, entries, options_map, file) \uf0c1 Print entries from a print statement specification. Parameters: c_print \u2013 An instance of a compiled EvalPrint statement. entries \u2013 A list of directives. options_map \u2013 A parser's option_map. file \u2013 The output file to print to. Source code in beancount/query/query_execute.py def execute_print(c_print, entries, options_map, file): \"\"\"Print entries from a print statement specification. Args: c_print: An instance of a compiled EvalPrint statement. entries: A list of directives. options_map: A parser's option_map. file: The output file to print to. \"\"\" if c_print and c_print.c_from is not None: context = create_row_context(entries, options_map) entries = filter_entries(c_print.c_from, entries, options_map, context) # Create a context that renders all numbers with their natural # precision, but honors the commas option. This is kept in sync with # {2c694afe3140} to avoid a dependency. dcontext = display_context.DisplayContext() dcontext.set_commas(options_map['dcontext'].commas) printer.print_entries(entries, dcontext, file=file) beancount.query.query_execute.execute_query(query, entries, options_map) \uf0c1 Given a compiled select statement, execute the query. Parameters: query \u2013 An instance of a query_compile.Query entries \u2013 A list of directives. options_map \u2013 A parser's option_map. Returns: A pair of \u2013 result_types: A list of (name, data-type) item pairs. result_rows: A list of ResultRow tuples of length and types described by 'result_types'. Source code in beancount/query/query_execute.py def execute_query(query, entries, options_map): \"\"\"Given a compiled select statement, execute the query. Args: query: An instance of a query_compile.Query entries: A list of directives. options_map: A parser's option_map. Returns: A pair of: result_types: A list of (name, data-type) item pairs. result_rows: A list of ResultRow tuples of length and types described by 'result_types'. \"\"\" # Figure out the result types that describe what we return. result_types = [(target.name, target.c_expr.dtype) for target in query.c_targets if target.name is not None] # Create a class for each final result. # pylint: disable=invalid-name ResultRow = collections.namedtuple('ResultRow', [target.name for target in query.c_targets if target.name is not None]) # Pre-compute lists of the expressions to evaluate. group_indexes = (set(query.group_indexes) if query.group_indexes is not None else query.group_indexes) # Indexes of the columns for result rows and order rows. result_indexes = [index for index, c_target in enumerate(query.c_targets) if c_target.name] order_indexes = query.order_indexes # Figure out if we need to compute balance. uses_balance = any(uses_balance_column(c_expr) for c_expr in itertools.chain( [c_target.c_expr for c_target in query.c_targets], [query.c_where] if query.c_where else [])) context = create_row_context(entries, options_map) # Filter the entries using the FROM clause. filt_entries = (filter_entries(query.c_from, entries, options_map, context) if query.c_from is not None else entries) # Dispatch between the non-aggregated queries and aggregated queries. c_where = query.c_where schwartz_rows = [] # Precompute a list of expressions to be evaluated. c_target_exprs = [c_target.c_expr for c_target in query.c_targets] if query.group_indexes is None: # This is a non-aggregated query. # Iterate over all the postings once and produce schwartzian rows. for entry in misc_utils.filter_type(filt_entries, data.Transaction): context.entry = entry for posting in entry.postings: context.posting = posting if c_where is None or c_where(context): # Compute the balance. if uses_balance: context.balance.add_position(posting) # Evaluate all the values. values = [c_expr(context) for c_expr in c_target_exprs] # Compute result and sort-key objects. result = ResultRow._make(values[index] for index in result_indexes) sortkey = row_sortkey(order_indexes, values, c_target_exprs) schwartz_rows.append((sortkey, result)) else: # This is an aggregated query. # Precompute lists of non-aggregate and aggregate expressions to # evaluate. For aggregate targets, we hunt down the aggregate # sub-expressions to evaluate, to avoid recursion during iteration. c_nonaggregate_exprs = [] c_aggregate_exprs = [] for index, c_expr in enumerate(c_target_exprs): if index in group_indexes: c_nonaggregate_exprs.append(c_expr) else: _, aggregate_exprs = query_compile.get_columns_and_aggregates(c_expr) c_aggregate_exprs.extend(aggregate_exprs) # Note: it is possible that there are no aggregates to compute here. You could # have all columns be non-aggregates and group-by the entire list of columns. # Pre-allocate handles in aggregation nodes. allocator = Allocator() for c_expr in c_aggregate_exprs: c_expr.allocate(allocator) # Iterate over all the postings to evaluate the aggregates. agg_store = {} for entry in misc_utils.filter_type(filt_entries, data.Transaction): context.entry = entry for posting in entry.postings: context.posting = posting if c_where is None or c_where(context): # Compute the balance. if uses_balance: context.balance.add_position(posting) # Compute the non-aggregate expressions. row_key = tuple(c_expr(context) for c_expr in c_nonaggregate_exprs) # Get an appropriate store for the unique key of this row. try: store = agg_store[row_key] except KeyError: # This is a row; create a new store. store = allocator.create_store() for c_expr in c_aggregate_exprs: c_expr.initialize(store) agg_store[row_key] = store # Update the aggregate expressions. for c_expr in c_aggregate_exprs: c_expr.update(store, context) # Iterate over all the aggregations to produce the schwartzian rows. for key, store in agg_store.items(): key_iter = iter(key) values = [] # Finalize the store. for c_expr in c_aggregate_exprs: c_expr.finalize(store) context.store = store for index, c_expr in enumerate(c_target_exprs): if index in group_indexes: value = next(key_iter) else: value = c_expr(context) values.append(value) # Compute result and sort-key objects. result = ResultRow._make(values[index] for index in result_indexes) sortkey = row_sortkey(order_indexes, values, c_target_exprs) schwartz_rows.append((sortkey, result)) # Order results if requested. if order_indexes is not None: schwartz_rows.sort(key=operator.itemgetter(0), reverse=(query.ordering == 'DESC')) # Extract final results, in sorted order at this point. result_rows = [x[1] for x in schwartz_rows] # Apply distinct. if query.distinct: result_rows = list(misc_utils.uniquify(result_rows)) # Apply limit. if query.limit is not None: result_rows = result_rows[:query.limit] # Flatten inventories if requested. if query.flatten: result_types, result_rows = flatten_results(result_types, result_rows) return (result_types, result_rows) beancount.query.query_execute.filter_entries(c_from, entries, options_map, context) \uf0c1 Filter the entries by the given compiled FROM clause. Parameters: c_from \u2013 A compiled From clause instance. entries \u2013 A list of directives. options_map \u2013 A parser's option_map. context \u2013 A prototype of RowContext to use for evaluation. Returns: A list of filtered entries. Source code in beancount/query/query_execute.py def filter_entries(c_from, entries, options_map, context): \"\"\"Filter the entries by the given compiled FROM clause. Args: c_from: A compiled From clause instance. entries: A list of directives. options_map: A parser's option_map. context: A prototype of RowContext to use for evaluation. Returns: A list of filtered entries. \"\"\" assert c_from is None or isinstance(c_from, query_compile.EvalFrom) assert isinstance(entries, list) context = copy.copy(context) if c_from is None: return entries # Process the OPEN clause. if c_from.open is not None: assert isinstance(c_from.open, datetime.date) open_date = c_from.open entries, index = summarize.open_opt(entries, open_date, options_map) # Process the CLOSE clause. if c_from.close is not None: if isinstance(c_from.close, datetime.date): close_date = c_from.close entries, index = summarize.close_opt(entries, close_date, options_map) elif c_from.close is True: entries, index = summarize.close_opt(entries, None, options_map) # Process the CLEAR clause. if c_from.clear is not None: entries, index = summarize.clear_opt(entries, None, options_map) # Filter the entries with the FROM clause's expression. c_expr = c_from.c_expr if c_expr is not None: # A simple function receives a context; how come close_date() is # accepted in the context of a FROM clause? It shouldn't be. new_entries = [] for entry in entries: context.entry = entry if c_expr(context): new_entries.append(entry) entries = new_entries return entries beancount.query.query_execute.flatten_results(result_types, result_rows) \uf0c1 Convert inventories in result types to have a row for each. This routine will expand all result lines with an inventory into a new line for each position. Parameters: result_types \u2013 A list of (name, data-type) item pairs. result_rows \u2013 A list of ResultRow tuples of length and types described by 'result_types'. Returns: result_types \u2013 A list of (name, data-type) item pairs. There should be no Inventory types anymore. result_rows: A list of ResultRow tuples of length and types described by 'result_types'. All inventories from the input should have been converted to Position types. Source code in beancount/query/query_execute.py def flatten_results(result_types, result_rows): \"\"\"Convert inventories in result types to have a row for each. This routine will expand all result lines with an inventory into a new line for each position. Args: result_types: A list of (name, data-type) item pairs. result_rows: A list of ResultRow tuples of length and types described by 'result_types'. Returns: result_types: A list of (name, data-type) item pairs. There should be no Inventory types anymore. result_rows: A list of ResultRow tuples of length and types described by 'result_types'. All inventories from the input should have been converted to Position types. \"\"\" indexes = set(index for index, (name, result_type) in enumerate(result_types) if result_type is inventory.Inventory) if not indexes: return (result_types, result_rows) # pylint: disable=invalid-name ResultRow = type(result_rows[0]) # We have to make at least some conversions. num_columns = len(result_types) output_rows = [] for result_row in result_rows: max_rows = max(len(result_row[icol]) for icol in indexes) for irow in range(max_rows): output_row = [] for icol in range(num_columns): value = result_row[icol] if icol in indexes: value = value[irow] if irow < len(value) else None output_row.append(value) output_rows.append(ResultRow._make(output_row)) # Convert the types. output_types = [(name, (position.Position if result_type is inventory.Inventory else result_type)) for name, result_type in result_types] return output_types, output_rows beancount.query.query_execute.row_sortkey(order_indexes, values, c_exprs) \uf0c1 Generate a sortkey for the given values. Parameters: order_indexes \u2013 The indexes by which the rows should be sorted. values \u2013 The computed values in the row. c_exprs \u2013 The matching c_expr's. Returns: A tuple, the sortkey. Source code in beancount/query/query_execute.py def row_sortkey(order_indexes, values, c_exprs): \"\"\"Generate a sortkey for the given values. Args: order_indexes: The indexes by which the rows should be sorted. values: The computed values in the row. c_exprs: The matching c_expr's. Returns: A tuple, the sortkey. \"\"\" if order_indexes is None: return None key = [] for index in order_indexes: value = values[index] key.append(_MIN_VALUES.get(c_exprs[index].dtype, None) if value is None else value) return tuple(key) beancount.query.query_execute.uses_balance_column(c_expr) \uf0c1 Return true if the expression accesses the special 'balance' column. Parameters: c_expr \u2013 A compiled expression tree (an EvalNode node). Returns: A boolean, true if the expression contains a BalanceColumn node. Source code in beancount/query/query_execute.py def uses_balance_column(c_expr): \"\"\"Return true if the expression accesses the special 'balance' column. Args: c_expr: A compiled expression tree (an EvalNode node). Returns: A boolean, true if the expression contains a BalanceColumn node. \"\"\" return (isinstance(c_expr, query_env.BalanceColumn) or any(uses_balance_column(c_node) for c_node in c_expr.childnodes())) beancount.query.query_execute_test \uf0c1 beancount.query.query_execute_test.QueryBase ( TestCase ) \uf0c1 beancount.query.query_execute_test.QueryBase.compile(self, bql_string) \uf0c1 Parse a query and compile it. Parameters: bql_string \u2013 An SQL query to be parsed. Returns: A compiled EvalQuery node. Source code in beancount/query/query_execute_test.py def compile(self, bql_string): \"\"\"Parse a query and compile it. Args: bql_string: An SQL query to be parsed. Returns: A compiled EvalQuery node. \"\"\" return qc.compile_select(self.parse(bql_string), self.xcontext_targets, self.xcontext_postings, self.xcontext_entries) beancount.query.query_execute_test.QueryBase.parse(self, bql_string) \uf0c1 Parse a query. Parameters: bql_string \u2013 An SQL query to be parsed. Returns: A parsed statement (Select() node). Source code in beancount/query/query_execute_test.py def parse(self, bql_string): \"\"\"Parse a query. Args: bql_string: An SQL query to be parsed. Returns: A parsed statement (Select() node). \"\"\" return self.parser.parse(bql_string.strip()) beancount.query.query_execute_test.QueryBase.setUp(self) \uf0c1 Hook method for setting up the test fixture before exercising it. Source code in beancount/query/query_execute_test.py def setUp(self): super().setUp() self.parser = query_parser.Parser() beancount.query.query_parser \uf0c1 Parser for Beancount Query Language. beancount.query.query_parser.Balances ( tuple ) \uf0c1 Balances(summary_func, from_clause, where_clause) beancount.query.query_parser.Balances.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.query.query_parser.Balances.__new__(_cls, summary_func, from_clause, where_clause) special staticmethod \uf0c1 Create new instance of Balances(summary_func, from_clause, where_clause) beancount.query.query_parser.Balances.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.query.query_parser.Balances._asdict(self) private \uf0c1 Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self)) beancount.query.query_parser.Balances._make(iterable) classmethod private \uf0c1 Make a new Balances object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result beancount.query.query_parser.Balances._replace(/, self, **kwds) private \uf0c1 Return a new Balances object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result beancount.query.query_parser.Errors ( tuple ) \uf0c1 Errors() beancount.query.query_parser.Errors.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.query.query_parser.Errors.__new__(_cls) special staticmethod \uf0c1 Create new instance of Errors() beancount.query.query_parser.Errors.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.query.query_parser.Errors._asdict(self) private \uf0c1 Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self)) beancount.query.query_parser.Errors._make(iterable) classmethod private \uf0c1 Make a new Errors object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result beancount.query.query_parser.Errors._replace(/, self, **kwds) private \uf0c1 Return a new Errors object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result beancount.query.query_parser.Explain ( tuple ) \uf0c1 Explain(statement,) beancount.query.query_parser.Explain.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.query.query_parser.Explain.__new__(_cls, statement) special staticmethod \uf0c1 Create new instance of Explain(statement,) beancount.query.query_parser.Explain.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.query.query_parser.Explain._asdict(self) private \uf0c1 Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self)) beancount.query.query_parser.Explain._make(iterable) classmethod private \uf0c1 Make a new Explain object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result beancount.query.query_parser.Explain._replace(/, self, **kwds) private \uf0c1 Return a new Explain object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result beancount.query.query_parser.Journal ( tuple ) \uf0c1 Journal(account, summary_func, from_clause) beancount.query.query_parser.Journal.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.query.query_parser.Journal.__new__(_cls, account, summary_func, from_clause) special staticmethod \uf0c1 Create new instance of Journal(account, summary_func, from_clause) beancount.query.query_parser.Journal.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.query.query_parser.Journal._asdict(self) private \uf0c1 Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self)) beancount.query.query_parser.Journal._make(iterable) classmethod private \uf0c1 Make a new Journal object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result beancount.query.query_parser.Journal._replace(/, self, **kwds) private \uf0c1 Return a new Journal object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result beancount.query.query_parser.Lexer \uf0c1 PLY lexer for the Beancount Query Language. beancount.query.query_parser.Lexer.t_DATE(self, token) \uf0c1 (#(\\\"[^\\\"] \\\"|\\'[^\\'] \\')|\\d\\d\\d\\d-\\d\\d-\\d\\d) Source code in beancount/query/query_parser.py def t_DATE(self, token): r\"(\\#(\\\"[^\\\"]*\\\"|\\'[^\\']*\\')|\\d\\d\\d\\d-\\d\\d-\\d\\d)\" if token.value[0] == '#': token.value = dateutil.parser.parse(token.value[2:-1]).date() else: token.value = datetime.datetime.strptime(token.value, '%Y-%m-%d').date() return token beancount.query.query_parser.Lexer.t_DECIMAL(self, token) \uf0c1 [-+]?([0-9]+.[0-9] |[0-9] .[0-9]+) Source code in beancount/query/query_parser.py def t_DECIMAL(self, token): r\"[-+]?([0-9]+\\.[0-9]*|[0-9]*\\.[0-9]+)\" token.value = D(token.value) return token beancount.query.query_parser.Lexer.t_ID(self, token) \uf0c1 a-zA-Z * Source code in beancount/query/query_parser.py def t_ID(self, token): \"[a-zA-Z][a-zA-Z_]*\" utoken = token.value.upper() if utoken in self.keywords: token.type = utoken token.value = token.value.upper() else: token.value = token.value.lower() return token beancount.query.query_parser.Lexer.t_INTEGER(self, token) \uf0c1 [-+]?[0-9]+ Source code in beancount/query/query_parser.py def t_INTEGER(self, token): r\"[-+]?[0-9]+\" token.value = int(token.value) return token beancount.query.query_parser.Lexer.t_STRING(self, token) \uf0c1 (\"[^\"] \"|'[^'] ') Source code in beancount/query/query_parser.py def t_STRING(self, token): \"(\\\"[^\\\"]*\\\"|\\'[^\\']*\\')\" token.value = token.value[1:-1] return token beancount.query.query_parser.ParseError ( Exception ) \uf0c1 A parser error. beancount.query.query_parser.Parser ( SelectParser ) \uf0c1 PLY parser for the Beancount Query Language's full command syntax. beancount.query.query_parser.Parser.p_balances_statement(self, p) \uf0c1 balances_statement : BALANCES summary_func from where Source code in beancount/query/query_parser.py def p_balances_statement(self, p): \"\"\" balances_statement : BALANCES summary_func from where \"\"\" p[0] = Balances(p[2], p[3], p[4]) beancount.query.query_parser.Parser.p_delimiter(self, p) \uf0c1 delimiter : SEMI | empty Source code in beancount/query/query_parser.py def p_delimiter(self, p): \"\"\" delimiter : SEMI | empty \"\"\" beancount.query.query_parser.Parser.p_errors_statement(self, p) \uf0c1 errors_statement : ERRORS Source code in beancount/query/query_parser.py def p_errors_statement(self, p): \"\"\" errors_statement : ERRORS \"\"\" p[0] = Errors() beancount.query.query_parser.Parser.p_explain_statement(self, p) \uf0c1 top_statement : EXPLAIN statement delimiter Source code in beancount/query/query_parser.py def p_explain_statement(self, p): \"top_statement : EXPLAIN statement delimiter\" p[0] = Explain(p[2]) beancount.query.query_parser.Parser.p_journal_statement(self, p) \uf0c1 journal_statement : JOURNAL summary_func from | JOURNAL account summary_func from Source code in beancount/query/query_parser.py def p_journal_statement(self, p): \"\"\" journal_statement : JOURNAL summary_func from | JOURNAL account summary_func from \"\"\" p[0] = Journal(None, p[2], p[3]) if len(p) == 4 else Journal(p[2], p[3], p[4]) beancount.query.query_parser.Parser.p_print_statement(self, p) \uf0c1 print_statement : PRINT from Source code in beancount/query/query_parser.py def p_print_statement(self, p): \"\"\" print_statement : PRINT from \"\"\" p[0] = Print(p[2]) beancount.query.query_parser.Parser.p_regular_statement(self, p) \uf0c1 top_statement : statement delimiter Source code in beancount/query/query_parser.py def p_regular_statement(self, p): \"top_statement : statement delimiter\" p[0] = p[1] beancount.query.query_parser.Parser.p_reload_statement(self, p) \uf0c1 reload_statement : RELOAD Source code in beancount/query/query_parser.py def p_reload_statement(self, p): \"\"\" reload_statement : RELOAD \"\"\" p[0] = Reload() beancount.query.query_parser.Parser.p_run_statement(self, p) \uf0c1 run_statement : RUN ID | RUN STRING | RUN ASTERISK | RUN empty Source code in beancount/query/query_parser.py def p_run_statement(self, p): \"\"\" run_statement : RUN ID | RUN STRING | RUN ASTERISK | RUN empty \"\"\" p[0] = RunCustom(p[2]) beancount.query.query_parser.Parser.p_statement(self, p) \uf0c1 statement : select_statement | balances_statement | journal_statement | print_statement | run_statement | errors_statement | reload_statement Source code in beancount/query/query_parser.py def p_statement(self, p): \"\"\" statement : select_statement | balances_statement | journal_statement | print_statement | run_statement | errors_statement | reload_statement \"\"\" p[0] = p[1] beancount.query.query_parser.Parser.p_summary_func(self, p) \uf0c1 summary_func : empty | AT ID Source code in beancount/query/query_parser.py def p_summary_func(self, p): \"\"\" summary_func : empty | AT ID \"\"\" p[0] = p[2] if len(p) == 3 else None beancount.query.query_parser.Print ( tuple ) \uf0c1 Print(from_clause,) beancount.query.query_parser.Print.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.query.query_parser.Print.__new__(_cls, from_clause) special staticmethod \uf0c1 Create new instance of Print(from_clause,) beancount.query.query_parser.Print.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.query.query_parser.Print._asdict(self) private \uf0c1 Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self)) beancount.query.query_parser.Print._make(iterable) classmethod private \uf0c1 Make a new Print object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result beancount.query.query_parser.Print._replace(/, self, **kwds) private \uf0c1 Return a new Print object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result beancount.query.query_parser.Reload ( tuple ) \uf0c1 Reload() beancount.query.query_parser.Reload.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.query.query_parser.Reload.__new__(_cls) special staticmethod \uf0c1 Create new instance of Reload() beancount.query.query_parser.Reload.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.query.query_parser.Reload._asdict(self) private \uf0c1 Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self)) beancount.query.query_parser.Reload._make(iterable) classmethod private \uf0c1 Make a new Reload object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result beancount.query.query_parser.Reload._replace(/, self, **kwds) private \uf0c1 Return a new Reload object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result beancount.query.query_parser.RunCustom ( tuple ) \uf0c1 RunCustom(query_name,) beancount.query.query_parser.RunCustom.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.query.query_parser.RunCustom.__new__(_cls, query_name) special staticmethod \uf0c1 Create new instance of RunCustom(query_name,) beancount.query.query_parser.RunCustom.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.query.query_parser.RunCustom._asdict(self) private \uf0c1 Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self)) beancount.query.query_parser.RunCustom._make(iterable) classmethod private \uf0c1 Make a new RunCustom object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result beancount.query.query_parser.RunCustom._replace(/, self, **kwds) private \uf0c1 Return a new RunCustom object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result beancount.query.query_parser.Select ( tuple ) \uf0c1 Select(targets, from_clause, where_clause, group_by, order_by, pivot_by, limit, distinct, flatten) beancount.query.query_parser.Select.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.query.query_parser.Select.__new__(_cls, targets, from_clause, where_clause, group_by, order_by, pivot_by, limit, distinct, flatten) special staticmethod \uf0c1 Create new instance of Select(targets, from_clause, where_clause, group_by, order_by, pivot_by, limit, distinct, flatten) beancount.query.query_parser.Select.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.query.query_parser.Select._asdict(self) private \uf0c1 Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self)) beancount.query.query_parser.Select._make(iterable) classmethod private \uf0c1 Make a new Select object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result beancount.query.query_parser.Select._replace(/, self, **kwds) private \uf0c1 Return a new Select object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result beancount.query.query_parser.SelectParser ( Lexer ) \uf0c1 PLY parser for the Beancount Query Language's SELECT statement. beancount.query.query_parser.SelectParser.handle_comma_separated_list(self, p) \uf0c1 Handle a list of 0, 1 or more comma-separated values. Parameters: p \u2013 A grammar object. Source code in beancount/query/query_parser.py def handle_comma_separated_list(self, p): \"\"\"Handle a list of 0, 1 or more comma-separated values. Args: p: A grammar object. \"\"\" if len(p) == 2: return [] if p[1] is None else [p[1]] else: return p[1] + [p[3]] beancount.query.query_parser.SelectParser.p_account(self, p) \uf0c1 account : STRING Source code in beancount/query/query_parser.py def p_account(self, p): \"\"\" account : STRING \"\"\" p[0] = p[1] beancount.query.query_parser.SelectParser.p_boolean(self, p) \uf0c1 boolean : TRUE | FALSE Source code in beancount/query/query_parser.py def p_boolean(self, p): \"\"\" boolean : TRUE | FALSE \"\"\" p[0] = (p[1] == 'TRUE') beancount.query.query_parser.SelectParser.p_column(self, p) \uf0c1 column : ID Source code in beancount/query/query_parser.py def p_column(self, p): \"\"\" column : ID \"\"\" p[0] = Column(p[1]) beancount.query.query_parser.SelectParser.p_column_list(self, p) \uf0c1 column_list : column | column_list COMMA column Source code in beancount/query/query_parser.py def p_column_list(self, p): \"\"\" column_list : column | column_list COMMA column \"\"\" p[0] = self.handle_comma_separated_list(p) beancount.query.query_parser.SelectParser.p_constant(self, p) \uf0c1 constant : NULL | boolean | INTEGER | DECIMAL | STRING | DATE Source code in beancount/query/query_parser.py def p_constant(self, p): \"\"\" constant : NULL | boolean | INTEGER | DECIMAL | STRING | DATE \"\"\" p[0] = Constant(p[1] if p[1] != 'NULL' else None) beancount.query.query_parser.SelectParser.p_distinct(self, p) \uf0c1 distinct : empty | DISTINCT Source code in beancount/query/query_parser.py def p_distinct(self, p): \"\"\" distinct : empty | DISTINCT \"\"\" p[0] = True if p[1] == 'DISTINCT' else None beancount.query.query_parser.SelectParser.p_empty(self, _) \uf0c1 empty : Source code in beancount/query/query_parser.py def p_empty(self, _): \"\"\" empty : \"\"\" beancount.query.query_parser.SelectParser.p_expr_index(self, p) \uf0c1 expr_index : expression | INTEGER Source code in beancount/query/query_parser.py def p_expr_index(self, p): \"\"\" expr_index : expression | INTEGER \"\"\" p[0] = p[1] beancount.query.query_parser.SelectParser.p_expr_index_list(self, p) \uf0c1 expr_index_list : expr_index | expr_index_list COMMA expr_index Source code in beancount/query/query_parser.py def p_expr_index_list(self, p): \"\"\" expr_index_list : expr_index | expr_index_list COMMA expr_index \"\"\" p[0] = self.handle_comma_separated_list(p) beancount.query.query_parser.SelectParser.p_expression_add(self, p) \uf0c1 expression : expression PLUS expression Source code in beancount/query/query_parser.py def p_expression_add(self, p): \"expression : expression PLUS expression\" p[0] = Add(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_and(self, p) \uf0c1 expression : expression AND expression Source code in beancount/query/query_parser.py def p_expression_and(self, p): \"expression : expression AND expression\" p[0] = And(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_column(self, p) \uf0c1 expression : column Source code in beancount/query/query_parser.py def p_expression_column(self, p): \"expression : column\" p[0] = p[1] beancount.query.query_parser.SelectParser.p_expression_constant(self, p) \uf0c1 expression : constant Source code in beancount/query/query_parser.py def p_expression_constant(self, p): \"expression : constant\" p[0] = p[1] beancount.query.query_parser.SelectParser.p_expression_contains(self, p) \uf0c1 expression : expression IN expression Source code in beancount/query/query_parser.py def p_expression_contains(self, p): \"expression : expression IN expression\" p[0] = Contains(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_div(self, p) \uf0c1 expression : expression SLASH expression Source code in beancount/query/query_parser.py def p_expression_div(self, p): \"expression : expression SLASH expression\" p[0] = Div(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_eq(self, p) \uf0c1 expression : expression EQ expression Source code in beancount/query/query_parser.py def p_expression_eq(self, p): \"expression : expression EQ expression\" p[0] = Equal(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_function(self, p) \uf0c1 expression : ID LPAREN expression_list_opt RPAREN Source code in beancount/query/query_parser.py def p_expression_function(self, p): \"expression : ID LPAREN expression_list_opt RPAREN\" p[0] = Function(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_gt(self, p) \uf0c1 expression : expression GT expression Source code in beancount/query/query_parser.py def p_expression_gt(self, p): \"expression : expression GT expression\" p[0] = Greater(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_gte(self, p) \uf0c1 expression : expression GTE expression Source code in beancount/query/query_parser.py def p_expression_gte(self, p): \"expression : expression GTE expression\" p[0] = GreaterEq(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_list(self, p) \uf0c1 expression_list : expression | expression_list COMMA expression Source code in beancount/query/query_parser.py def p_expression_list(self, p): \"\"\" expression_list : expression | expression_list COMMA expression \"\"\" p[0] = self.handle_comma_separated_list(p) beancount.query.query_parser.SelectParser.p_expression_list_opt(self, p) \uf0c1 expression_list_opt : empty | expression | expression_list COMMA expression Source code in beancount/query/query_parser.py def p_expression_list_opt(self, p): \"\"\" expression_list_opt : empty | expression | expression_list COMMA expression \"\"\" p[0] = self.handle_comma_separated_list(p) beancount.query.query_parser.SelectParser.p_expression_lt(self, p) \uf0c1 expression : expression LT expression Source code in beancount/query/query_parser.py def p_expression_lt(self, p): \"expression : expression LT expression\" p[0] = Less(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_lte(self, p) \uf0c1 expression : expression LTE expression Source code in beancount/query/query_parser.py def p_expression_lte(self, p): \"expression : expression LTE expression\" p[0] = LessEq(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_match(self, p) \uf0c1 expression : expression TILDE expression Source code in beancount/query/query_parser.py def p_expression_match(self, p): \"expression : expression TILDE expression\" p[0] = Match(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_mul(self, p) \uf0c1 expression : expression ASTERISK expression Source code in beancount/query/query_parser.py def p_expression_mul(self, p): \"expression : expression ASTERISK expression\" p[0] = Mul(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_ne(self, p) \uf0c1 expression : expression NE expression Source code in beancount/query/query_parser.py def p_expression_ne(self, p): \"expression : expression NE expression\" p[0] = Not(Equal(p[1], p[3])) beancount.query.query_parser.SelectParser.p_expression_not(self, p) \uf0c1 expression : NOT expression Source code in beancount/query/query_parser.py def p_expression_not(self, p): \"expression : NOT expression\" p[0] = Not(p[2]) beancount.query.query_parser.SelectParser.p_expression_or(self, p) \uf0c1 expression : expression OR expression Source code in beancount/query/query_parser.py def p_expression_or(self, p): \"expression : expression OR expression\" p[0] = Or(p[1], p[3]) beancount.query.query_parser.SelectParser.p_expression_paren(self, p) \uf0c1 expression : LPAREN expression RPAREN Source code in beancount/query/query_parser.py def p_expression_paren(self, p): \"expression : LPAREN expression RPAREN\" p[0] = p[2] beancount.query.query_parser.SelectParser.p_expression_sub(self, p) \uf0c1 expression : expression MINUS expression Source code in beancount/query/query_parser.py def p_expression_sub(self, p): \"expression : expression MINUS expression\" p[0] = Sub(p[1], p[3]) beancount.query.query_parser.SelectParser.p_flatten(self, p) \uf0c1 flatten : empty | FLATTEN Source code in beancount/query/query_parser.py def p_flatten(self, p): \"\"\" flatten : empty | FLATTEN \"\"\" p[0] = True if p[1] == 'FLATTEN' else None beancount.query.query_parser.SelectParser.p_from(self, p) \uf0c1 from : empty | FROM opt_expression opt_open opt_close opt_clear Source code in beancount/query/query_parser.py def p_from(self, p): \"\"\" from : empty | FROM opt_expression opt_open opt_close opt_clear \"\"\" if len(p) != 2: if all(p[i] is None for i in range(2, 6)): raise ParseError(\"Empty FROM expression is not allowed\") p[0] = From(p[2], p[3], p[4], p[5]) else: p[0] = None beancount.query.query_parser.SelectParser.p_from_subselect(self, p) \uf0c1 from_subselect : from | FROM LPAREN select_statement RPAREN Source code in beancount/query/query_parser.py def p_from_subselect(self, p): \"\"\" from_subselect : from | FROM LPAREN select_statement RPAREN \"\"\" if len(p) == 2: p[0] = p[1] else: p[0] = p[3] beancount.query.query_parser.SelectParser.p_group_by(self, p) \uf0c1 group_by : empty | GROUP BY expr_index_list having Source code in beancount/query/query_parser.py def p_group_by(self, p): \"\"\" group_by : empty | GROUP BY expr_index_list having \"\"\" p[0] = GroupBy(p[3], p[4]) if len(p) != 2 else None beancount.query.query_parser.SelectParser.p_having(self, p) \uf0c1 having : empty | HAVING expression Source code in beancount/query/query_parser.py def p_having(self, p): \"\"\" having : empty | HAVING expression \"\"\" p[0] = p[2] if len(p) == 3 else None beancount.query.query_parser.SelectParser.p_limit(self, p) \uf0c1 limit : empty | LIMIT INTEGER Source code in beancount/query/query_parser.py def p_limit(self, p): \"\"\" limit : empty | LIMIT INTEGER \"\"\" p[0] = p[2] if len(p) == 3 else None beancount.query.query_parser.SelectParser.p_opt_clear(self, p) \uf0c1 opt_clear : empty | CLEAR Source code in beancount/query/query_parser.py def p_opt_clear(self, p): \"\"\" opt_clear : empty | CLEAR \"\"\" p[0] = True if (p[1] == 'CLEAR') else None beancount.query.query_parser.SelectParser.p_opt_close(self, p) \uf0c1 opt_close : empty | CLOSE | CLOSE ON DATE Source code in beancount/query/query_parser.py def p_opt_close(self, p): \"\"\" opt_close : empty | CLOSE | CLOSE ON DATE \"\"\" p[0] = p[3] if len(p) == 4 else (True if (p[1] == 'CLOSE') else self.default_close_date) beancount.query.query_parser.SelectParser.p_opt_expression(self, p) \uf0c1 opt_expression : empty | expression Source code in beancount/query/query_parser.py def p_opt_expression(self, p): \"\"\" opt_expression : empty | expression \"\"\" p[0] = p[1] beancount.query.query_parser.SelectParser.p_opt_open(self, p) \uf0c1 opt_open : empty | OPEN ON DATE Source code in beancount/query/query_parser.py def p_opt_open(self, p): \"\"\" opt_open : empty | OPEN ON DATE \"\"\" p[0] = p[3] if len(p) == 4 else None beancount.query.query_parser.SelectParser.p_order_by(self, p) \uf0c1 order_by : empty | ORDER BY expr_index_list ordering Source code in beancount/query/query_parser.py def p_order_by(self, p): \"\"\" order_by : empty | ORDER BY expr_index_list ordering \"\"\" p[0] = None if len(p) == 2 else OrderBy(p[3], p[4]) beancount.query.query_parser.SelectParser.p_ordering(self, p) \uf0c1 ordering : empty | ASC | DESC Source code in beancount/query/query_parser.py def p_ordering(self, p): \"\"\" ordering : empty | ASC | DESC \"\"\" p[0] = p[1] beancount.query.query_parser.SelectParser.p_pivot_by(self, p) \uf0c1 pivot_by : empty | PIVOT BY column_list Source code in beancount/query/query_parser.py def p_pivot_by(self, p): \"\"\" pivot_by : empty | PIVOT BY column_list \"\"\" p[0] = PivotBy(p[3]) if len(p) == 4 else None beancount.query.query_parser.SelectParser.p_select_statement(self, p) \uf0c1 select_statement : SELECT distinct target_spec from_subselect where group_by order_by pivot_by limit flatten Source code in beancount/query/query_parser.py def p_select_statement(self, p): \"\"\" select_statement : SELECT distinct target_spec from_subselect where \\ group_by order_by pivot_by limit flatten \"\"\" p[0] = Select(p[3], p[4], p[5], p[6], p[7], p[8], p[9], p[2], p[10]) beancount.query.query_parser.SelectParser.p_target(self, p) \uf0c1 target : expression AS ID | expression Source code in beancount/query/query_parser.py def p_target(self, p): \"\"\" target : expression AS ID | expression \"\"\" p[0] = Target(p[1], p[3] if len(p) == 4 else None) beancount.query.query_parser.SelectParser.p_target_list(self, p) \uf0c1 target_list : target | target_list COMMA target Source code in beancount/query/query_parser.py def p_target_list(self, p): \"\"\" target_list : target | target_list COMMA target \"\"\" p[0] = self.handle_comma_separated_list(p) beancount.query.query_parser.SelectParser.p_target_spec(self, p) \uf0c1 target_spec : ASTERISK | target_list Source code in beancount/query/query_parser.py def p_target_spec(self, p): \"\"\" target_spec : ASTERISK | target_list \"\"\" p[0] = Wildcard() if p[1] == '*' else p[1] beancount.query.query_parser.SelectParser.p_where(self, p) \uf0c1 where : empty | WHERE expression Source code in beancount/query/query_parser.py def p_where(self, p): \"\"\" where : empty | WHERE expression \"\"\" if len(p) == 3: assert p[2], \"Empty WHERE clause is not allowed\" p[0] = p[2] beancount.query.query_parser.get_expression_name(expr) \uf0c1 Come up with a reasonable identifier for an expression. Parameters: expr \u2013 An expression node. Source code in beancount/query/query_parser.py def get_expression_name(expr): \"\"\"Come up with a reasonable identifier for an expression. Args: expr: An expression node. \"\"\" if isinstance(expr, Column): return expr.name.lower() elif isinstance(expr, Function): names = [expr.fname.lower()] for operand in expr.operands: names.append(get_expression_name(operand)) return '_'.join(names) elif isinstance(expr, Constant): return 'c{}'.format(re.sub('[^a-z0-9]+', '_', str(expr.value))) elif isinstance(expr, UnaryOp): return '_'.join([type(expr).__name__.lower(), get_expression_name(expr.operand)]) elif isinstance(expr, BinaryOp): return '_'.join([type(expr).__name__.lower(), get_expression_name(expr.left), get_expression_name(expr.right)]) else: assert False, \"Unknown expression type.\" beancount.query.query_parser_test \uf0c1 beancount.query.query_parser_test.QueryParserTestBase ( TestCase ) \uf0c1 beancount.query.query_parser_test.QueryParserTestBase.assertParse(self, expected, query, debug=False) \uf0c1 Assert parsed contents from 'query' is 'expected'. Parameters: expected \u2013 An expected AST to compare against the parsed value. query \u2013 An SQL query to be parsed. debug \u2013 A boolean, if true, print extra debugging information on the console. Exceptions: AssertionError \u2013 If the actual AST does not match the expected one. Source code in beancount/query/query_parser_test.py def assertParse(self, expected, query, debug=False): \"\"\"Assert parsed contents from 'query' is 'expected'. Args: expected: An expected AST to compare against the parsed value. query: An SQL query to be parsed. debug: A boolean, if true, print extra debugging information on the console. Raises: AssertionError: If the actual AST does not match the expected one. \"\"\" actual = self.parse(query) if debug: print() print() print(actual) print() self.assertEqual(expected, actual) return actual beancount.query.query_parser_test.QueryParserTestBase.parse(self, query) \uf0c1 Parse one query. Parameters: query \u2013 An SQL query to be parsed. Returns: The AST. Source code in beancount/query/query_parser_test.py def parse(self, query): \"\"\"Parse one query. Args: query: An SQL query to be parsed. Returns: The AST. \"\"\" return self.parser.parse(query.strip()) beancount.query.query_parser_test.QueryParserTestBase.setUp(self) \uf0c1 Hook method for setting up the test fixture before exercising it. Source code in beancount/query/query_parser_test.py def setUp(self): self.parser = qp.Parser() beancount.query.query_parser_test.TestSelectFromBase ( QueryParserTestBase ) \uf0c1 beancount.query.query_parser_test.TestSelectFromBase.setUp(self) \uf0c1 Hook method for setting up the test fixture before exercising it. Source code in beancount/query/query_parser_test.py def setUp(self): super().setUp() self.targets = [qp.Target(qp.Column('a'), None), qp.Target(qp.Column('b'), None)] self.expr = qp.Equal(qp.Column('d'), qp.And( qp.Function('max', [qp.Column('e')]), qp.Constant(17))) beancount.query.query_parser_test.qSelect(target_spec=None, from_clause=None, where_clause=None, group_by=None, order_by=None, pivot_by=None, limit=None, distinct=None, flatten=None) \uf0c1 A convenience constructor for writing tests without having to provide all arguments. Source code in beancount/query/query_parser_test.py def qSelect(target_spec=None, from_clause=None, where_clause=None, group_by=None, order_by=None, pivot_by=None, limit=None, distinct=None, flatten=None): \"A convenience constructor for writing tests without having to provide all arguments.\" return qp.Select(target_spec, from_clause, where_clause, group_by, order_by, pivot_by, limit, distinct, flatten) beancount.query.shell \uf0c1 An interactive command-line shell interpreter for the Beancount Query Language. beancount.query.shell.BQLShell ( DispatchingShell ) \uf0c1 An interactive shell interpreter for the Beancount query language. beancount.query.shell.BQLShell.on_Balances(self, balance) \uf0c1 Select balances of some subset of postings. This command is a convenience and converts into an equivalent Select statement, designed to extract the most sensible list of columns for the register of a list of entries as a table. The general form of a JOURNAL statement loosely follows SQL syntax: BALANCE [FROM_CLAUSE] See the SELECT query help for more details on the FROM clause. Source code in beancount/query/shell.py def on_Balances(self, balance): \"\"\" Select balances of some subset of postings. This command is a convenience and converts into an equivalent Select statement, designed to extract the most sensible list of columns for the register of a list of entries as a table. The general form of a JOURNAL statement loosely follows SQL syntax: BALANCE [FROM_CLAUSE] See the SELECT query help for more details on the FROM clause. \"\"\" return self.on_Select(balance) beancount.query.shell.BQLShell.on_Errors(self, errors_statement) \uf0c1 Print the errors that occurred during parsing. Source code in beancount/query/shell.py def on_Errors(self, errors_statement): \"\"\" Print the errors that occurred during parsing. \"\"\" if self.errors: printer.print_errors(self.errors) else: print('(No errors)', file=self.outfile) beancount.query.shell.BQLShell.on_Explain(self, explain) \uf0c1 Compile and print a compiled statement for debugging. Source code in beancount/query/shell.py def on_Explain(self, explain): \"\"\" Compile and print a compiled statement for debugging. \"\"\" # pylint: disable=invalid-name pr = lambda *args: print(*args, file=self.outfile) pr(\"Parsed statement:\") pr(\" {}\".format(explain.statement)) pr() # Compile the select statement and print it uot. try: query = query_compile.compile(explain.statement, self.env_targets, self.env_postings, self.env_entries) except query_compile.CompilationError as exc: pr(str(exc).rstrip('.')) return pr(\"Compiled query:\") pr(\" {}\".format(query)) pr() pr(\"Targets:\") for c_target in query.c_targets: pr(\" '{}'{}: {}\".format( c_target.name or '(invisible)', ' (aggregate)' if query_compile.is_aggregate(c_target.c_expr) else '', c_target.c_expr.dtype.__name__)) pr() beancount.query.shell.BQLShell.on_Journal(self, journal) \uf0c1 Select a journal of some subset of postings. This command is a convenience and converts into an equivalent Select statement, designed to extract the most sensible list of columns for the register of a list of entries as a table. The general form of a JOURNAL statement loosely follows SQL syntax: JOURNAL [FROM_CLAUSE] See the SELECT query help for more details on the FROM clause. Source code in beancount/query/shell.py def on_Journal(self, journal): \"\"\" Select a journal of some subset of postings. This command is a convenience and converts into an equivalent Select statement, designed to extract the most sensible list of columns for the register of a list of entries as a table. The general form of a JOURNAL statement loosely follows SQL syntax: JOURNAL [FROM_CLAUSE] See the SELECT query help for more details on the FROM clause. \"\"\" return self.on_Select(journal) beancount.query.shell.BQLShell.on_Print(self, print_stmt) \uf0c1 Print entries in Beancount format. The general form of a PRINT statement includes an SQL-like FROM selector: PRINT [FROM ...] Where: from_expr: A logical expression that matches on the attributes of the directives. See SELECT command for details (this FROM expression supports all the same expressions including its OPEN, CLOSE and CLEAR operations). Source code in beancount/query/shell.py def on_Print(self, print_stmt): \"\"\" Print entries in Beancount format. The general form of a PRINT statement includes an SQL-like FROM selector: PRINT [FROM ...] Where: from_expr: A logical expression that matches on the attributes of the directives. See SELECT command for details (this FROM expression supports all the same expressions including its OPEN, CLOSE and CLEAR operations). \"\"\" # Compile the print statement. try: c_print = query_compile.compile(print_stmt, self.env_targets, self.env_postings, self.env_entries) except query_compile.CompilationError as exc: print('ERROR: {}.'.format(str(exc).rstrip('.')), file=self.outfile) return if self.outfile is sys.stdout: query_execute.execute_print(c_print, self.entries, self.options_map, file=self.outfile) else: with self.get_pager() as file: query_execute.execute_print(c_print, self.entries, self.options_map, file) beancount.query.shell.BQLShell.on_Reload(self, unused_statement=None) \uf0c1 Reload the input file without restarting the shell. Source code in beancount/query/shell.py def on_Reload(self, unused_statement=None): \"\"\" Reload the input file without restarting the shell. \"\"\" self.entries, self.errors, self.options_map = self.loadfun() if self.is_interactive: print_statistics(self.entries, self.options_map, self.outfile) beancount.query.shell.BQLShell.on_RunCustom(self, run_stmt) \uf0c1 Run a custom query instead of a SQL command. RUN Where: custom-query-name: Should be the name of a custom query to be defined in the Beancount input file. Source code in beancount/query/shell.py def on_RunCustom(self, run_stmt): \"\"\" Run a custom query instead of a SQL command. RUN Where: custom-query-name: Should be the name of a custom query to be defined in the Beancount input file. \"\"\" custom_query_map = create_custom_query_map(self.entries) name = run_stmt.query_name if name is None: # List the available queries. for name in sorted(custom_query_map): print(name) elif name == \"*\": for name, query in sorted(custom_query_map.items()): print('{}:'.format(name)) self.run_parser(query.query_string, default_close_date=query.date) print() print() else: query = None if name in custom_query_map: query = custom_query_map[name] else: # lookup best query match using name as prefix queries = [q for q in custom_query_map if q.startswith(name)] if len(queries) == 1: name = queries[0] query = custom_query_map[name] if query: statement = self.parser.parse(query.query_string) self.dispatch(statement) else: print(\"ERROR: Query '{}' not found\".format(name)) beancount.query.shell.BQLShell.on_Select(self, statement) \uf0c1 Extract data from a query on the postings. The general form of a SELECT statement loosely follows SQL syntax, with some mild and idiomatic extensions: SELECT DISTINCT [FROM [OPEN ON ] [CLOSE [ON ]] [CLEAR]] [WHERE ] [GROUP BY ] [ORDER BY [ASC|DESC]] [LIMIT num] Where: targets: A list of desired output attributes from the postings, and expressions on them. Some of the attributes of the parent transaction directive are made available in this context as well. Simple functions (that return a single value per row) and aggregation functions (that return a single value per group) are available. For the complete list of supported columns and functions, see help on \"targets\". You can also provide a wildcard here, which will select a reasonable default set of columns for rendering a journal. from_expr: A logical expression that matches on the attributes of the directives (not postings). This allows you to select a subset of transactions, so the accounting equation is respected for balance reports. For the complete list of supported columns and functions, see help on \"from\". where_expr: A logical expression that matches on the attributes of postings. The available columns are similar to those in the targets clause, without the aggregation functions. OPEN clause: replace all the transactions before the given date by summarizing entries and transfer Income and Expenses balances to Equity. CLOSE clause: Remove all the transactions after the given date and CLEAR: Transfer final Income and Expenses balances to Equity. Source code in beancount/query/shell.py def on_Select(self, statement): \"\"\" Extract data from a query on the postings. The general form of a SELECT statement loosely follows SQL syntax, with some mild and idiomatic extensions: SELECT [DISTINCT] [|*] [FROM [OPEN ON ] [CLOSE [ON ]] [CLEAR]] [WHERE ] [GROUP BY ] [ORDER BY [ASC|DESC]] [LIMIT num] Where: targets: A list of desired output attributes from the postings, and expressions on them. Some of the attributes of the parent transaction directive are made available in this context as well. Simple functions (that return a single value per row) and aggregation functions (that return a single value per group) are available. For the complete list of supported columns and functions, see help on \"targets\". You can also provide a wildcard here, which will select a reasonable default set of columns for rendering a journal. from_expr: A logical expression that matches on the attributes of the directives (not postings). This allows you to select a subset of transactions, so the accounting equation is respected for balance reports. For the complete list of supported columns and functions, see help on \"from\". where_expr: A logical expression that matches on the attributes of postings. The available columns are similar to those in the targets clause, without the aggregation functions. OPEN clause: replace all the transactions before the given date by summarizing entries and transfer Income and Expenses balances to Equity. CLOSE clause: Remove all the transactions after the given date and CLEAR: Transfer final Income and Expenses balances to Equity. \"\"\" # Compile the SELECT statement. try: c_query = query_compile.compile(statement, self.env_targets, self.env_postings, self.env_entries) except query_compile.CompilationError as exc: print('ERROR: {}.'.format(str(exc).rstrip('.')), file=self.outfile) return # Execute it to obtain the result rows. rtypes, rrows = query_execute.execute_query(c_query, self.entries, self.options_map) # Output the resulting rows. if not rrows: print(\"(empty)\", file=self.outfile) else: output_format = self.vars['format'] if output_format == 'text': kwds = dict(boxed=self.vars['boxed'], spaced=self.vars['spaced'], expand=self.vars['expand']) if self.outfile is sys.stdout: with self.get_pager() as file: query_render.render_text(rtypes, rrows, self.options_map['dcontext'], file, **kwds) else: query_render.render_text(rtypes, rrows, self.options_map['dcontext'], self.outfile, **kwds) elif output_format == 'csv': # Numberify CSV output if requested. if self.vars['numberify']: dformat = self.options_map['dcontext'].build() rtypes, rrows = numberify.numberify_results(rtypes, rrows, dformat) query_render.render_csv(rtypes, rrows, self.options_map['dcontext'], self.outfile, expand=self.vars['expand']) else: assert output_format not in _SUPPORTED_FORMATS print(\"Unsupported output format: '{}'.\".format(output_format), file=self.outfile) beancount.query.shell.DispatchingShell ( Cmd ) \uf0c1 A usable convenient shell for interpreting commands, with history. beancount.query.shell.DispatchingShell.__init__(self, is_interactive, parser, outfile, default_format, do_numberify) special \uf0c1 Create a shell with history. Parameters: is_interactive \u2013 A boolean, true if this serves an interactive tty. parser \u2013 A command parser. outfile \u2013 An output file object to write communications to. default_format \u2013 A string, the default output format. Source code in beancount/query/shell.py def __init__(self, is_interactive, parser, outfile, default_format, do_numberify): \"\"\"Create a shell with history. Args: is_interactive: A boolean, true if this serves an interactive tty. parser: A command parser. outfile: An output file object to write communications to. default_format: A string, the default output format. \"\"\" super().__init__() if is_interactive and readline is not None: load_history(path.expanduser(HISTORY_FILENAME)) self.is_interactive = is_interactive self.parser = parser self.initialize_vars(default_format, do_numberify) self.add_help() self.outfile = outfile beancount.query.shell.DispatchingShell.add_help(self) \uf0c1 Attach help functions for each of the parsed token handlers. Source code in beancount/query/shell.py def add_help(self): \"Attach help functions for each of the parsed token handlers.\" for attrname, func in list(self.__class__.__dict__.items()): match = re.match('on_(.*)', attrname) if not match: continue command_name = match.group(1) setattr(self.__class__, 'help_{}'.format(command_name.lower()), lambda _, fun=func: print(textwrap.dedent(fun.__doc__).strip(), file=self.outfile)) beancount.query.shell.DispatchingShell.cmdloop(self) \uf0c1 Override cmdloop to handle keyboard interrupts. Source code in beancount/query/shell.py def cmdloop(self): \"\"\"Override cmdloop to handle keyboard interrupts.\"\"\" while True: try: super().cmdloop() break except KeyboardInterrupt: print('\\n(Interrupted)', file=self.outfile) beancount.query.shell.DispatchingShell.default(self, line) \uf0c1 Default handling of lines which aren't recognized as native shell commands. Parameters: line \u2013 The string to be parsed. Source code in beancount/query/shell.py def default(self, line): \"\"\"Default handling of lines which aren't recognized as native shell commands. Args: line: The string to be parsed. \"\"\" self.run_parser(line) beancount.query.shell.DispatchingShell.dispatch(self, statement) \uf0c1 Dispatch the given statement to a suitable method. Parameters: statement \u2013 An instance provided by the parser. Returns: Whatever the invoked method happens to return. Source code in beancount/query/shell.py def dispatch(self, statement): \"\"\"Dispatch the given statement to a suitable method. Args: statement: An instance provided by the parser. Returns: Whatever the invoked method happens to return. \"\"\" try: method = getattr(self, 'on_{}'.format(type(statement).__name__)) except AttributeError: print(\"Internal error: statement '{}' is unsupported.\".format(statement), file=self.outfile) else: return method(statement) beancount.query.shell.DispatchingShell.do_EOF(self, _) \uf0c1 Exit the parser. Source code in beancount/query/shell.py def exit(self, _): \"\"\"Exit the parser.\"\"\" print('exit', file=self.outfile) return 1 beancount.query.shell.DispatchingShell.do_clear(self, _) \uf0c1 Clear the history. Source code in beancount/query/shell.py def do_clear(self, _): \"Clear the history.\" readline.clear_history() beancount.query.shell.DispatchingShell.do_exit(self, _) \uf0c1 Exit the parser. Source code in beancount/query/shell.py def exit(self, _): \"\"\"Exit the parser.\"\"\" print('exit', file=self.outfile) return 1 beancount.query.shell.DispatchingShell.do_help(self, command) \uf0c1 Strip superfluous semicolon. Source code in beancount/query/shell.py def do_help(self, command): \"\"\"Strip superfluous semicolon.\"\"\" super().do_help(command.rstrip('; \\t')) beancount.query.shell.DispatchingShell.do_history(self, _) \uf0c1 Print the command-line history statement. Source code in beancount/query/shell.py def do_history(self, _): \"Print the command-line history statement.\" if readline is not None: for index, line in enumerate(get_history(self.max_entries)): print(line, file=self.outfile) beancount.query.shell.DispatchingShell.do_lex(self, line) \uf0c1 Just run the lexer on the following command and print the output. Source code in beancount/query/shell.py def do_lex(self, line): \"Just run the lexer on the following command and print the output.\" try: self.parser.tokenize(line) except query_parser.ParseError as exc: print(exc, file=self.outfile) beancount.query.shell.DispatchingShell.do_parse(self, line) \uf0c1 Just run the parser on the following command and print the output. Source code in beancount/query/shell.py def do_parse(self, line): \"Just run the parser on the following command and print the output.\" print(\"INPUT: {}\".format(repr(line)), file=self.outfile) try: statement = self.parser.parse(line, True) print(statement, file=self.outfile) except (query_parser.ParseError, query_compile.CompilationError) as exc: print(exc, file=self.outfile) except Exception as exc: traceback.print_exc(file=self.outfile) beancount.query.shell.DispatchingShell.do_quit(self, _) \uf0c1 Exit the parser. Source code in beancount/query/shell.py def exit(self, _): \"\"\"Exit the parser.\"\"\" print('exit', file=self.outfile) return 1 beancount.query.shell.DispatchingShell.do_set(self, line) \uf0c1 Get/set shell settings variables. Source code in beancount/query/shell.py def do_set(self, line): \"Get/set shell settings variables.\" if not line: for varname, value in sorted(self.vars.items()): print('{}: {}'.format(varname, value), file=self.outfile) else: components = shlex.split(line) varname = components[0] if len(components) == 1: try: value = self.vars[varname] print('{}: {}'.format(varname, value), file=self.outfile) except KeyError: print(\"Variable '{}' does not exist.\".format(varname), file=self.outfile) elif len(components) == 2: value = components[1] try: converted_value = self.vars_types[varname](value) self.vars[varname] = converted_value print('{}: {}'.format(varname, converted_value), file=self.outfile) except KeyError: print(\"Variable '{}' does not exist.\".format(varname), file=self.outfile) else: print(\"Invalid number of arguments.\", file=self.outfile) beancount.query.shell.DispatchingShell.do_tokenize(self, line) \uf0c1 Just run the lexer on the following command and print the output. Source code in beancount/query/shell.py def do_lex(self, line): \"Just run the lexer on the following command and print the output.\" try: self.parser.tokenize(line) except query_parser.ParseError as exc: print(exc, file=self.outfile) beancount.query.shell.DispatchingShell.emptyline(self) \uf0c1 Do nothing on an empty line. Source code in beancount/query/shell.py def emptyline(self): \"\"\"Do nothing on an empty line.\"\"\" beancount.query.shell.DispatchingShell.exit(self, _) \uf0c1 Exit the parser. Source code in beancount/query/shell.py def exit(self, _): \"\"\"Exit the parser.\"\"\" print('exit', file=self.outfile) return 1 beancount.query.shell.DispatchingShell.get_pager(self) \uf0c1 Create and return a context manager to write to, a pager subprocess if required. Returns: A pair of a file object to write to, and a pipe object to wait on (or None if not necessary to wait). Source code in beancount/query/shell.py def get_pager(self): \"\"\"Create and return a context manager to write to, a pager subprocess if required. Returns: A pair of a file object to write to, and a pipe object to wait on (or None if not necessary to wait). \"\"\" if self.is_interactive: return pager.ConditionalPager(self.vars.get('pager', None), minlines=misc_utils.get_screen_height()) else: file = (codecs.getwriter(\"utf-8\")(sys.stdout.buffer) if hasattr(sys.stdout, 'buffer') else sys.stdout) return pager.flush_only(file) beancount.query.shell.DispatchingShell.initialize_vars(self, default_format, do_numberify) \uf0c1 Initialize the setting variables of the interactive shell. Source code in beancount/query/shell.py def initialize_vars(self, default_format, do_numberify): \"\"\"Initialize the setting variables of the interactive shell.\"\"\" self.vars_types = { 'pager': str, 'format': str, 'boxed': convert_bool, 'spaced': convert_bool, 'expand': convert_bool, 'numberify': convert_bool, } self.vars = { 'pager': os.environ.get('PAGER', None), 'format': default_format, 'boxed': False, 'spaced': False, 'expand': False, 'numberify': do_numberify, } beancount.query.shell.DispatchingShell.run_parser(self, line, default_close_date=None) \uf0c1 Handle statements via our parser instance and dispatch to appropriate methods. Parameters: line \u2013 The string to be parsed. default_close_date \u2013 A datetimed.date instance, the default close date. Source code in beancount/query/shell.py def run_parser(self, line, default_close_date=None): \"\"\"Handle statements via our parser instance and dispatch to appropriate methods. Args: line: The string to be parsed. default_close_date: A datetimed.date instance, the default close date. \"\"\" try: statement = self.parser.parse(line, default_close_date=default_close_date) self.dispatch(statement) except query_parser.ParseError as exc: print(exc, file=self.outfile) except Exception as exc: traceback.print_exc(file=self.outfile) beancount.query.shell.convert_bool(string) \uf0c1 Convert a string to a boolean. Parameters: string \u2013 A string representing a boolean. Returns: The corresponding boolean. Source code in beancount/query/shell.py def convert_bool(string): \"\"\"Convert a string to a boolean. Args: string: A string representing a boolean. Returns: The corresponding boolean. \"\"\" return not string.lower() in ('f', 'false', '0') beancount.query.shell.create_custom_query_map(entries) \uf0c1 Extract a mapping of the custom queries from the list of entries. Parameters: entries \u2013 A list of entries. Returns: A map of query-name strings to Query directives. Source code in beancount/query/shell.py def create_custom_query_map(entries): \"\"\"Extract a mapping of the custom queries from the list of entries. Args: entries: A list of entries. Returns: A map of query-name strings to Query directives. \"\"\" query_map = {} for entry in entries: if not isinstance(entry, data.Query): continue if entry.name in query_map: logging.warning(\"Duplicate query: %s\", entry.name) query_map[entry.name] = entry return query_map beancount.query.shell.generate_env_attribute_list(env) \uf0c1 Generate a dictionary of rendered attribute lists for help. Parameters: env \u2013 An instance of an environment. Returns: A dict with keys 'columns', 'functions' and 'aggregates' to rendered and formatted strings. Source code in beancount/query/shell.py def generate_env_attribute_list(env): \"\"\"Generate a dictionary of rendered attribute lists for help. Args: env: An instance of an environment. Returns: A dict with keys 'columns', 'functions' and 'aggregates' to rendered and formatted strings. \"\"\" wrapper = textwrap.TextWrapper(initial_indent=' ', subsequent_indent=' ', drop_whitespace=True, width=80) str_columns = generate_env_attributes( wrapper, env.columns) str_simple = generate_env_attributes( wrapper, env.functions, lambda node: not issubclass(node, query_compile.EvalAggregator)) str_aggregate = generate_env_attributes( wrapper, env.functions, lambda node: issubclass(node, query_compile.EvalAggregator)) return dict(columns=str_columns, functions=str_simple, aggregates=str_aggregate) beancount.query.shell.generate_env_attributes(wrapper, field_dict, filter_pred=None) \uf0c1 Generate a string of all the help functions of the attributes. Parameters: wrapper \u2013 A TextWrapper instance to format the paragraphs. field_dict \u2013 A dict of the field-names to the node instances, fetch from an environment. filter_pred \u2013 A predicate to filter the desired columns. This is applied to the evaluator node instances. Returns: A formatted multiline string, ready for insertion in a help text. Source code in beancount/query/shell.py def generate_env_attributes(wrapper, field_dict, filter_pred=None): \"\"\"Generate a string of all the help functions of the attributes. Args: wrapper: A TextWrapper instance to format the paragraphs. field_dict: A dict of the field-names to the node instances, fetch from an environment. filter_pred: A predicate to filter the desired columns. This is applied to the evaluator node instances. Returns: A formatted multiline string, ready for insertion in a help text. \"\"\" # Expand the name if its key has argument types. # # FIXME: Render the __intypes__ here nicely instead of the key. flat_items = [] for name, column_cls in field_dict.items(): if isinstance(name, tuple): name = name[0] if issubclass(column_cls, query_compile.EvalFunction): name = name.upper() args = [] for dtypes in column_cls.__intypes__: if isinstance(dtypes, (tuple, list)): arg = '|'.join(dtype.__name__ for dtype in dtypes) else: arg = dtypes.__name__ args.append(arg) name = \"{}({})\".format(name, ','.join(args)) flat_items.append((name, column_cls)) # Render each of the attributes. oss = io.StringIO() for name, column_cls in sorted(flat_items): if filter_pred and not filter_pred(column_cls): continue docstring = column_cls.__doc__ or \"[See class {}]\".format(column_cls.__name__) if issubclass(column_cls, query_compile.EvalColumn): docstring += \" Type: {}.\".format(column_cls().dtype.__name__) # if hasattr(column_cls, '__equivalent__'): # docstring += \" Attribute:{}.\".format(column_cls.__equivalent__) text = re.sub('[ \\t]+', ' ', docstring.strip().replace('\\n', ' ')) doc = \"'{}': {}\".format(name, text) oss.write(wrapper.fill(doc)) oss.write('\\n') return oss.getvalue().rstrip() beancount.query.shell.get_history(max_entries) \uf0c1 Return the history in the readline buffer. Parameters: max_entries \u2013 An integer, the maximum number of entries to return. Returns: A list of string, the previous history of commands. Source code in beancount/query/shell.py def get_history(max_entries): \"\"\"Return the history in the readline buffer. Args: max_entries: An integer, the maximum number of entries to return. Returns: A list of string, the previous history of commands. \"\"\" num_entries = readline.get_current_history_length() assert num_entries >= 0 start = max(0, num_entries - max_entries) return [readline.get_history_item(index+1) for index in range(start, num_entries)] beancount.query.shell.load_history(filename) \uf0c1 Load the shell's past history. Parameters: filename \u2013 A string, the name of the file containing the shell history. Source code in beancount/query/shell.py def load_history(filename): \"\"\"Load the shell's past history. Args: filename: A string, the name of the file containing the shell history. \"\"\" readline.parse_and_bind(\"tab:complete\") if hasattr(readline, \"read_history_file\"): try: readline.read_history_file(filename) except IOError: # Don't error on absent file. pass atexit.register(save_history, filename) beancount.query.shell.print_statistics(entries, options_map, outfile) \uf0c1 Print summary statistics to stdout. Parameters: entries \u2013 A list of directives. options_map \u2013 An options map. as produced by the parser. outfile \u2013 A file object to write to. Source code in beancount/query/shell.py def print_statistics(entries, options_map, outfile): \"\"\"Print summary statistics to stdout. Args: entries: A list of directives. options_map: An options map. as produced by the parser. outfile: A file object to write to. \"\"\" num_directives, num_transactions, num_postings = summary_statistics(entries) if 'title' in options_map: print('Input file: \"{}\"'.format(options_map['title']), file=outfile) print(\"Ready with {} directives ({} postings in {} transactions).\".format( num_directives, num_postings, num_transactions), file=outfile) beancount.query.shell.save_history(filename) \uf0c1 Save the shell history. This should be invoked on exit. Parameters: filename \u2013 A string, the name of the file to save the history to. Source code in beancount/query/shell.py def save_history(filename): \"\"\"Save the shell history. This should be invoked on exit. Args: filename: A string, the name of the file to save the history to. \"\"\" readline.write_history_file(filename) beancount.query.shell.summary_statistics(entries) \uf0c1 Calculate basic summary statistics to output a brief welcome message. Parameters: entries \u2013 A list of directives. Returns: A tuple of three integers, the total number of directives parsed, the total number of transactions and the total number of postings there in. Source code in beancount/query/shell.py def summary_statistics(entries): \"\"\"Calculate basic summary statistics to output a brief welcome message. Args: entries: A list of directives. Returns: A tuple of three integers, the total number of directives parsed, the total number of transactions and the total number of postings there in. \"\"\" num_directives = len(entries) num_transactions = 0 num_postings = 0 for entry in entries: if isinstance(entry, data.Transaction): num_transactions += 1 num_postings += len(entry.postings) return (num_directives, num_transactions, num_postings) beancount.query.shell_test \uf0c1 beancount.query.shell_test.TestShell ( TestCase ) \uf0c1 beancount.query.shell_test.TestShell.test_success(self, filename) \uf0c1 2013-01-01 open Assets:Account1 2013-01-01 open Assets:Account2 2013-01-01 open Assets:Account3 2013-01-01 open Equity:Unknown 2013-04-05 * Equity:Unknown Assets:Account1 5000 USD 2013-04-05 * Assets:Account1 -3000 USD Assets:Account2 30 BOOG {100 USD} 2013-04-05 * Assets:Account1 -1000 USD Assets:Account3 800 EUR @ 1.25 USD Source code in beancount/query/shell_test.py @test_utils.docfile def test_success(self, filename): \"\"\" 2013-01-01 open Assets:Account1 2013-01-01 open Assets:Account2 2013-01-01 open Assets:Account3 2013-01-01 open Equity:Unknown 2013-04-05 * Equity:Unknown Assets:Account1 5000 USD 2013-04-05 * Assets:Account1 -3000 USD Assets:Account2 30 BOOG {100 USD} 2013-04-05 * Assets:Account1 -1000 USD Assets:Account3 800 EUR @ 1.25 USD \"\"\" with test_utils.capture('stdout', 'stderr') as (stdout, _): test_utils.run_with_args(shell.main, [filename, \"SELECT 1;\"]) self.assertTrue(stdout.getvalue()) beancount.query.shell_test.TestUseCases ( TestCase ) \uf0c1 Testing all the use cases from the proposal here. I'm hoping to replace reports by these queries instead. beancount.query.shell_test.runshell(function) \uf0c1 Decorate a function to run the shell and return the output. Source code in beancount/query/shell_test.py def runshell(function): \"\"\"Decorate a function to run the shell and return the output.\"\"\" def test_function(self): def loadfun(): return entries, errors, options_map with test_utils.capture('stdout') as stdout: shell_obj = shell.BQLShell(False, loadfun, sys.stdout) shell_obj.on_Reload() shell_obj.onecmd(function.__doc__) return function(self, stdout.getvalue()) return test_function","title":"beancount.query"},{"location":"api_reference/beancount.query.html#beancountquery","text":"Transaction and postings filtering syntax parser.","title":"beancount.query"},{"location":"api_reference/beancount.query.html#beancount.query.numberify","text":"Code to split table columns containing amounts and inventories into number columns. For example, given a column with this content: ----- amount ------ 101.23 USD 200 JPY 99.23 USD 38.34 USD, 100 JPY We can convert this into two columns and remove the currencies: -amount (USD)- -amount (JPY)- 101.23 200 99.23 38.34 100 The point is that the columns should be typed as numbers to make this importable into a spreadsheet and able to be processed. Notes: This handles the Amount, Position and Inventory datatypes. There is code to automatically recognize columns containing such types from a table of strings and convert such columns to their corresponding guessed data types. The per-currency columns are ordered in decreasing order of the number of instances of numbers seen for each currency. So if the most numbers you have in a column are USD, then the USD column renders first. Cost basis specifications should be unmodified and reported to a dedicated extra column, like this: ----- amount ------ 1 AAPL {21.23 USD} We can convert this into two columns and remove the currencies: -amount (AAPL)- -Cost basis- 1 {21.23 USD} (Eventually we might support the conversion of cost amounts as well, but they may contain other information, such as a label or a date, so for now we don't convert them. I'm not sure there's a good practical use case in doing that yet.) We may provide some options to break out only some of the currencies into columns, in order to handle the case where an inventory contains a large number of currencies and we want to only operate on a restricted set of operating currencies. If you provide a DisplayFormatter object to the numberification routine, they quantize each column according to their currency's precision. It is recommended that you do that.","title":"numberify"},{"location":"api_reference/beancount.query.html#beancount.query.numberify.AmountConverter","text":"A converter that extracts the number of an amount for a specific currency.","title":"AmountConverter"},{"location":"api_reference/beancount.query.html#beancount.query.numberify.AmountConverter.dtype","text":"Construct a new Decimal object. 'value' can be an integer, string, tuple, or another Decimal object. If no value is given, return Decimal('0'). The context does not affect the conversion and is only passed to determine if the InvalidOperation trap is active.","title":"dtype"},{"location":"api_reference/beancount.query.html#beancount.query.numberify.IdentityConverter","text":"A converter that simply copies its column.","title":"IdentityConverter"},{"location":"api_reference/beancount.query.html#beancount.query.numberify.InventoryConverter","text":"A converter that extracts the number of a inventory for a specific currency. If there are multiple lots we aggregate by currency.","title":"InventoryConverter"},{"location":"api_reference/beancount.query.html#beancount.query.numberify.InventoryConverter.dtype","text":"Construct a new Decimal object. 'value' can be an integer, string, tuple, or another Decimal object. If no value is given, return Decimal('0'). The context does not affect the conversion and is only passed to determine if the InvalidOperation trap is active.","title":"dtype"},{"location":"api_reference/beancount.query.html#beancount.query.numberify.PositionConverter","text":"A converter that extracts the number of a position for a specific currency.","title":"PositionConverter"},{"location":"api_reference/beancount.query.html#beancount.query.numberify.PositionConverter.dtype","text":"Construct a new Decimal object. 'value' can be an integer, string, tuple, or another Decimal object. If no value is given, return Decimal('0'). The context does not affect the conversion and is only passed to determine if the InvalidOperation trap is active.","title":"dtype"},{"location":"api_reference/beancount.query.html#beancount.query.numberify.convert_col_Amount","text":"Create converters for a column of type Amount. Parameters: name \u2013 A string, the column name. drows \u2013 The table of objects. index \u2013 The column number. Returns: A list of Converter instances, one for each of the currency types found. Source code in beancount/query/numberify.py def convert_col_Amount(name, drows, index): \"\"\"Create converters for a column of type Amount. Args: name: A string, the column name. drows: The table of objects. index: The column number. Returns: A list of Converter instances, one for each of the currency types found. \"\"\" currency_map = collections.defaultdict(int) for drow in drows: vamount = drow[index] if vamount and vamount.currency: currency_map[vamount.currency] += 1 return [AmountConverter('{} ({})'.format(name, currency), index, currency) for currency, _ in sorted(currency_map.items(), key=lambda item: (item[1], item[0]), reverse=True)]","title":"convert_col_Amount()"},{"location":"api_reference/beancount.query.html#beancount.query.numberify.convert_col_Inventory","text":"Create converters for a column of type Inventory. Parameters: name \u2013 A string, the column name. drows \u2013 The table of objects. index \u2013 The column number. Returns: A list of Converter instances, one for each of the currency types found. Source code in beancount/query/numberify.py def convert_col_Inventory(name, drows, index): \"\"\"Create converters for a column of type Inventory. Args: name: A string, the column name. drows: The table of objects. index: The column number. Returns: A list of Converter instances, one for each of the currency types found. \"\"\" currency_map = collections.defaultdict(int) for drow in drows: inv = drow[index] for currency in inv.currencies(): currency_map[currency] += 1 return [InventoryConverter('{} ({})'.format(name, currency), index, currency) for currency, _ in sorted(currency_map.items(), key=lambda item: (item[1], item[0]), reverse=True)]","title":"convert_col_Inventory()"},{"location":"api_reference/beancount.query.html#beancount.query.numberify.convert_col_Position","text":"Create converters for a column of type Position. Parameters: name \u2013 A string, the column name. drows \u2013 The table of objects. index \u2013 The column number. Returns: A list of Converter instances, one for each of the currency types found. Source code in beancount/query/numberify.py def convert_col_Position(name, drows, index): \"\"\"Create converters for a column of type Position. Args: name: A string, the column name. drows: The table of objects. index: The column number. Returns: A list of Converter instances, one for each of the currency types found. \"\"\" currency_map = collections.defaultdict(int) for drow in drows: pos = drow[index] if pos and pos.units.currency: currency_map[pos.units.currency] += 1 return [PositionConverter('{} ({})'.format(name, currency), index, currency) for currency, _ in sorted(currency_map.items(), key=lambda item: (item[1], item[0]), reverse=True)]","title":"convert_col_Position()"},{"location":"api_reference/beancount.query.html#beancount.query.numberify.numberify_results","text":"Number rows containing Amount, Position or Inventory types. Parameters: result_types \u2013 A list of items describing the names and data types of the items in each column. result_rows \u2013 A list of ResultRow instances. dformat \u2013 An optional DisplayFormatter. If set, quantize the numbers by their currency-specific precision when converting the Amount's, Position's or Inventory'es.. Returns: A pair of modified (result_types, result_rows) with converted datatypes. Source code in beancount/query/numberify.py def numberify_results(dtypes, drows, dformat=None): \"\"\"Number rows containing Amount, Position or Inventory types. Args: result_types: A list of items describing the names and data types of the items in each column. result_rows: A list of ResultRow instances. dformat: An optional DisplayFormatter. If set, quantize the numbers by their currency-specific precision when converting the Amount's, Position's or Inventory'es.. Returns: A pair of modified (result_types, result_rows) with converted datatypes. \"\"\" # Build an array of converters. converters = [] for index, col_desc in enumerate(dtypes): name, dtype = col_desc convert_col_fun = CONVERTING_TYPES.get(dtype, None) if convert_col_fun is None: converters.append(IdentityConverter(name, dtype, index)) else: col_converters = convert_col_fun(name, drows, index) converters.extend(col_converters) # Derive the output types from the expected outputs from the converters # themselves. otypes = [(c.name, c.dtype) for c in converters] # Convert the input rows by processing them through the converters. orows = [] for drow in drows: orow = [] for converter in converters: orow.append(converter(drow, dformat)) orows.append(orow) return otypes, orows","title":"numberify_results()"},{"location":"api_reference/beancount.query.html#beancount.query.query","text":"A library to run queries. This glues together all the parts of the query engine.","title":"query"},{"location":"api_reference/beancount.query.html#beancount.query.query.run_query","text":"Compile and execute a query, return the result types and rows. Parameters: entries \u2013 A list of entries, as produced by the loader. options_map \u2013 A dict of options, as produced by the loader. query \u2013 A string, a single BQL query, optionally containing some new-style (e.g., {}) formatting specifications. format_args \u2013 A tuple of arguments to be formatted in the query. This is just provided as a convenience. numberify \u2013 If true, numberify the results before returning them. Returns: A pair of result types and result rows. Exceptions: ParseError \u2013 If the statement cannot be parsed. CompilationError \u2013 If the statement cannot be compiled. Source code in beancount/query/query.py def run_query(entries, options_map, query, *format_args, numberify=False): \"\"\"Compile and execute a query, return the result types and rows. Args: entries: A list of entries, as produced by the loader. options_map: A dict of options, as produced by the loader. query: A string, a single BQL query, optionally containing some new-style (e.g., {}) formatting specifications. format_args: A tuple of arguments to be formatted in the query. This is just provided as a convenience. numberify: If true, numberify the results before returning them. Returns: A pair of result types and result rows. Raises: ParseError: If the statement cannot be parsed. CompilationError: If the statement cannot be compiled. \"\"\" env_targets = query_env.TargetsEnvironment() env_entries = query_env.FilterEntriesEnvironment() env_postings = query_env.FilterPostingsEnvironment() # Apply formatting to the query. formatted_query = query.format(*format_args) # Parse the statement. parser = query_parser.Parser() statement = parser.parse(formatted_query) # Compile the SELECT statement. c_query = query_compile.compile(statement, env_targets, env_postings, env_entries) # Execute it to obtain the result rows. rtypes, rrows = query_execute.execute_query(c_query, entries, options_map) # Numberify the results, if requested. if numberify: dformat = options_map['dcontext'].build() rtypes, rrows = numberify_lib.numberify_results(rtypes, rrows, dformat) return rtypes, rrows","title":"run_query()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile","text":"Interpreter for the query language's AST. This code accepts the abstract syntax tree produced by the query parser, resolves the column and function names, compiles and interpreter and prepares a query to be run against a list of entries.","title":"query_compile"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.CompilationEnvironment","text":"Base class for all compilation contexts. A compilation context provides column accessors specific to the particular row objects that we will access.","title":"CompilationEnvironment"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.CompilationEnvironment.get_column","text":"Return a column accessor for the given named column. Parameters: name \u2013 A string, the name of the column to access. Source code in beancount/query/query_compile.py def get_column(self, name): \"\"\"Return a column accessor for the given named column. Args: name: A string, the name of the column to access. \"\"\" try: return self.columns[name]() except KeyError: raise CompilationError(\"Invalid column name '{}' in {} context.\".format( name, self.context_name))","title":"get_column()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.CompilationEnvironment.get_function","text":"Return a function accessor for the given named function. Parameters: name \u2013 A string, the name of the function to access. Source code in beancount/query/query_compile.py def get_function(self, name, operands): \"\"\"Return a function accessor for the given named function. Args: name: A string, the name of the function to access. \"\"\" try: key = tuple([name] + [operand.dtype for operand in operands]) return self.functions[key](operands) except KeyError: # If not found with the operands, try just looking it up by name. try: return self.functions[name](operands) except KeyError: signature = '{}({})'.format(name, ', '.join(operand.dtype.__name__ for operand in operands)) raise CompilationError(\"Invalid function '{}' in {} context\".format( signature, self.context_name))","title":"get_function()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.CompilationError","text":"A compiler/interpreter error.","title":"CompilationError"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalAggregator","text":"Base class for all aggregator evaluator types.","title":"EvalAggregator"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalAggregator.__call__","text":"Return the value on evaluation. Parameters: context \u2013 The evaluation object to which the evaluation need to apply. This is either an entry, a Posting instance, or a particular result set row from a sub-select. This is the provider for the underlying data. Returns: The final aggregated value. Source code in beancount/query/query_compile.py def __call__(self, context): \"\"\"Return the value on evaluation. Args: context: The evaluation object to which the evaluation need to apply. This is either an entry, a Posting instance, or a particular result set row from a sub-select. This is the provider for the underlying data. Returns: The final aggregated value. \"\"\" # Return None by default.","title":"__call__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalAggregator.allocate","text":"Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_compile.py def allocate(self, allocator): \"\"\"Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Args: allocator: An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. \"\"\" # Do nothing by default.","title":"allocate()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalAggregator.finalize","text":"Finalize this node's aggregate data and return it. For aggregate methods, this finalizes the node and returns the final value. The context node will be the alloc instead of the context object. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_compile.py def finalize(self, store): \"\"\"Finalize this node's aggregate data and return it. For aggregate methods, this finalizes the node and returns the final value. The context node will be the alloc instead of the context object. Args: store: An object indexable by handles appropriated during allocate(). \"\"\" # Do nothing by default.","title":"finalize()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalAggregator.initialize","text":"Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_compile.py def initialize(self, store): \"\"\"Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Args: store: An object indexable by handles appropriated during allocate(). \"\"\" # Do nothing by default.","title":"initialize()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalAggregator.update","text":"Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_compile.py def update(self, store, context): \"\"\"Evaluate this node. This is designed to recurse on its children. Args: store: An object indexable by handles appropriated during allocate(). context: The object to which the evaluation need to apply (see __call__). \"\"\" # Do nothing by default.","title":"update()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalColumn","text":"Base class for all column accessors.","title":"EvalColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalFrom","text":"EvalFrom(c_expr, open, close, clear)","title":"EvalFrom"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalFrom.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_compile.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalFrom.__new__","text":"Create new instance of EvalFrom(c_expr, open, close, clear)","title":"__new__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalFrom.__repr__","text":"Return a nicely formatted representation string Source code in beancount/query/query_compile.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalFrom._asdict","text":"Return a new dict which maps field names to their values. Source code in beancount/query/query_compile.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self))","title":"_asdict()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalFrom._make","text":"Make a new EvalFrom object from a sequence or iterable Source code in beancount/query/query_compile.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result","title":"_make()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalFrom._replace","text":"Return a new EvalFrom object replacing specified fields with new values Source code in beancount/query/query_compile.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result","title":"_replace()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalFunction","text":"Base class for all function objects.","title":"EvalFunction"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalNode","text":"","title":"EvalNode"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalNode.__call__","text":"Evaluate this node. This is designed to recurse on its children. All subclasses must override and implement this method. Parameters: context \u2013 The evaluation object to which the evaluation need to apply. This is either an entry, a Posting instance, or a particular result set row from a sub-select. This is the provider for the underlying data. Returns: The evaluated value for this sub-expression tree. Source code in beancount/query/query_compile.py def __call__(self, context): \"\"\"Evaluate this node. This is designed to recurse on its children. All subclasses must override and implement this method. Args: context: The evaluation object to which the evaluation need to apply. This is either an entry, a Posting instance, or a particular result set row from a sub-select. This is the provider for the underlying data. Returns: The evaluated value for this sub-expression tree. \"\"\" raise NotImplementedError","title":"__call__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalNode.__eq__","text":"Override the equality operator to compare the data type and a all attributes of this node. This is used by tests for comparing nodes. Source code in beancount/query/query_compile.py def __eq__(self, other): \"\"\"Override the equality operator to compare the data type and a all attributes of this node. This is used by tests for comparing nodes. \"\"\" return (isinstance(other, type(self)) and all( getattr(self, attribute) == getattr(other, attribute) for attribute in self.__slots__))","title":"__eq__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalNode.__repr__","text":"Return str(self). Source code in beancount/query/query_compile.py def __str__(self): return \"{}({})\".format(type(self).__name__, ', '.join(repr(getattr(self, child)) for child in self.__slots__))","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalNode.childnodes","text":"Returns the child nodes of this node. Yields: A list of EvalNode instances. Source code in beancount/query/query_compile.py def childnodes(self): \"\"\"Returns the child nodes of this node. Yields: A list of EvalNode instances. \"\"\" for attr in self.__slots__: child = getattr(self, attr) if isinstance(child, EvalNode): yield child elif isinstance(child, list): for element in child: if isinstance(element, EvalNode): yield element","title":"childnodes()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalPrint","text":"EvalPrint(c_from,)","title":"EvalPrint"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalPrint.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_compile.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalPrint.__new__","text":"Create new instance of EvalPrint(c_from,)","title":"__new__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalPrint.__repr__","text":"Return a nicely formatted representation string Source code in beancount/query/query_compile.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalPrint._asdict","text":"Return a new dict which maps field names to their values. Source code in beancount/query/query_compile.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self))","title":"_asdict()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalPrint._make","text":"Make a new EvalPrint object from a sequence or iterable Source code in beancount/query/query_compile.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result","title":"_make()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalPrint._replace","text":"Return a new EvalPrint object replacing specified fields with new values Source code in beancount/query/query_compile.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result","title":"_replace()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalQuery","text":"EvalQuery(c_targets, c_from, c_where, group_indexes, order_indexes, ordering, limit, distinct, flatten)","title":"EvalQuery"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalQuery.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_compile.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalQuery.__new__","text":"Create new instance of EvalQuery(c_targets, c_from, c_where, group_indexes, order_indexes, ordering, limit, distinct, flatten)","title":"__new__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalQuery.__repr__","text":"Return a nicely formatted representation string Source code in beancount/query/query_compile.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalQuery._asdict","text":"Return a new dict which maps field names to their values. Source code in beancount/query/query_compile.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self))","title":"_asdict()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalQuery._make","text":"Make a new EvalQuery object from a sequence or iterable Source code in beancount/query/query_compile.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result","title":"_make()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalQuery._replace","text":"Return a new EvalQuery object replacing specified fields with new values Source code in beancount/query/query_compile.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result","title":"_replace()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalTarget","text":"EvalTarget(c_expr, name, is_aggregate)","title":"EvalTarget"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalTarget.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_compile.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalTarget.__new__","text":"Create new instance of EvalTarget(c_expr, name, is_aggregate)","title":"__new__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalTarget.__repr__","text":"Return a nicely formatted representation string Source code in beancount/query/query_compile.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalTarget._asdict","text":"Return a new dict which maps field names to their values. Source code in beancount/query/query_compile.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self))","title":"_asdict()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalTarget._make","text":"Make a new EvalTarget object from a sequence or iterable Source code in beancount/query/query_compile.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result","title":"_make()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.EvalTarget._replace","text":"Return a new EvalTarget object replacing specified fields with new values Source code in beancount/query/query_compile.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result","title":"_replace()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.ResultSetEnvironment","text":"An execution context that provides access to attributes from a result set.","title":"ResultSetEnvironment"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.ResultSetEnvironment.get_column","text":"Override the column getter to provide a single attribute getter. Source code in beancount/query/query_compile.py def get_column(self, name): \"\"\"Override the column getter to provide a single attribute getter. \"\"\" # FIXME: How do we figure out the data type here? We need the context. return AttributeColumn(name)","title":"get_column()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile._get_columns_and_aggregates","text":"Walk down a tree of nodes and fetch the column accessors and aggregates. This function ignores all nodes under aggregate nodes. Parameters: node \u2013 An instance of EvalNode. columns \u2013 An accumulator for columns found so far. aggregate \u2013 An accumulator for aggregate notes found so far. Source code in beancount/query/query_compile.py def _get_columns_and_aggregates(node, columns, aggregates): \"\"\"Walk down a tree of nodes and fetch the column accessors and aggregates. This function ignores all nodes under aggregate nodes. Args: node: An instance of EvalNode. columns: An accumulator for columns found so far. aggregate: An accumulator for aggregate notes found so far. \"\"\" if isinstance(node, EvalAggregator): aggregates.append(node) elif isinstance(node, EvalColumn): columns.append(node) else: for child in node.childnodes(): _get_columns_and_aggregates(child, columns, aggregates)","title":"_get_columns_and_aggregates()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.compile","text":"Prepare an AST any of the statement into an executable statement. Parameters: statement \u2013 An instance of the parser's Select, Balances, Journal or Print. targets_environ \u2013 A compilation environment for evaluating targets. postings_environ \u2013 : A compilation environment for evaluating postings filters. entries_environ \u2013 : A compilation environment for evaluating entry filters. Returns: An instance of EvalQuery or EvalPrint, ready to be executed. Exceptions: CompilationError \u2013 If the statement cannot be compiled, or is not one of the supported statements. Source code in beancount/query/query_compile.py def compile(statement, targets_environ, postings_environ, entries_environ): \"\"\"Prepare an AST any of the statement into an executable statement. Args: statement: An instance of the parser's Select, Balances, Journal or Print. targets_environ: A compilation environment for evaluating targets. postings_environ: : A compilation environment for evaluating postings filters. entries_environ: : A compilation environment for evaluating entry filters. Returns: An instance of EvalQuery or EvalPrint, ready to be executed. Raises: CompilationError: If the statement cannot be compiled, or is not one of the supported statements. \"\"\" if isinstance(statement, query_parser.Balances): statement = transform_balances(statement) elif isinstance(statement, query_parser.Journal): statement = transform_journal(statement) if isinstance(statement, query_parser.Select): c_query = compile_select(statement, targets_environ, postings_environ, entries_environ) elif isinstance(statement, query_parser.Print): c_query = compile_print(statement, entries_environ) else: raise CompilationError( \"Cannot compile a statement of type '{}'\".format(type(statement))) return c_query","title":"compile()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.compile_expression","text":"Bind an expression to its execution context. Parameters: expr \u2013 The root node of an expression. environ \u2013 An CompilationEnvironment instance. Returns: The root node of a bound expression. Source code in beancount/query/query_compile.py def compile_expression(expr, environ): \"\"\"Bind an expression to its execution context. Args: expr: The root node of an expression. environ: An CompilationEnvironment instance. Returns: The root node of a bound expression. \"\"\" # Convert column references to the context. if isinstance(expr, query_parser.Column): c_expr = environ.get_column(expr.name) elif isinstance(expr, query_parser.Function): c_operands = [compile_expression(operand, environ) for operand in expr.operands] c_expr = environ.get_function(expr.fname, c_operands) elif isinstance(expr, query_parser.UnaryOp): node_type = OPERATORS[type(expr)] c_expr = node_type(compile_expression(expr.operand, environ)) elif isinstance(expr, query_parser.BinaryOp): node_type = OPERATORS[type(expr)] c_expr = node_type(compile_expression(expr.left, environ), compile_expression(expr.right, environ)) elif isinstance(expr, query_parser.Constant): c_expr = EvalConstant(expr.value) else: assert False, \"Invalid expression to compile: {}\".format(expr) return c_expr","title":"compile_expression()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.compile_from","text":"Compiled a From clause as provided by the parser, in the given environment. Parameters: select \u2013 An instance of query_parser.Select. environ \u2013 : A compilation context for evaluating entry filters. Returns: An instance of Query, ready to be executed. Source code in beancount/query/query_compile.py def compile_from(from_clause, environ): \"\"\"Compiled a From clause as provided by the parser, in the given environment. Args: select: An instance of query_parser.Select. environ: : A compilation context for evaluating entry filters. Returns: An instance of Query, ready to be executed. \"\"\" if from_clause is not None: c_expression = (compile_expression(from_clause.expression, environ) if from_clause.expression is not None else None) # Check that the from clause does not contain aggregates. if c_expression is not None and is_aggregate(c_expression): raise CompilationError(\"Aggregates are not allowed in from clause\") if (isinstance(from_clause.open, datetime.date) and isinstance(from_clause.close, datetime.date) and from_clause.open > from_clause.close): raise CompilationError(\"Invalid dates: CLOSE date must follow OPEN date\") c_from = EvalFrom(c_expression, from_clause.open, from_clause.close, from_clause.clear) else: c_from = None return c_from","title":"compile_from()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.compile_group_by","text":"Process a group-by clause. Parameters: group_by \u2013 A GroupBy instance as provided by the parser. c_targets \u2013 A list of compiled target expressions. environ \u2013 A compilation context to be used to evaluate GROUP BY expressions. Returns: A tuple of new_targets \u2013 A list of new compiled target nodes. group_indexes: If the query is an aggregate query, a list of integer indexes to be used for processing grouping. Note that this list may be empty (in the case of targets with only aggregates). On the other hand, if this is not an aggregated query, this is set to None. So do distinguish the empty list vs. None. Source code in beancount/query/query_compile.py def compile_group_by(group_by, c_targets, environ): \"\"\"Process a group-by clause. Args: group_by: A GroupBy instance as provided by the parser. c_targets: A list of compiled target expressions. environ: A compilation context to be used to evaluate GROUP BY expressions. Returns: A tuple of new_targets: A list of new compiled target nodes. group_indexes: If the query is an aggregate query, a list of integer indexes to be used for processing grouping. Note that this list may be empty (in the case of targets with only aggregates). On the other hand, if this is not an aggregated query, this is set to None. So do distinguish the empty list vs. None. \"\"\" new_targets = copy.copy(c_targets) c_target_expressions = [c_target.c_expr for c_target in c_targets] group_indexes = [] if group_by: # Check that HAVING is not supported yet. if group_by and group_by.having is not None: raise CompilationError(\"The HAVING clause is not supported yet\") assert group_by.columns, \"Internal error with GROUP-BY parsing\" # Compile group-by expressions and resolve them to their targets if # possible. A GROUP-BY column may be one of the following: # # * A reference to a target by name. # * A reference to a target by index (starting at one). # * A new, non-aggregate expression. # # References by name are converted to indexes. New expressions are # inserted into the list of targets as invisible targets. targets_name_map = {target.name: index for index, target in enumerate(c_targets)} for column in group_by.columns: index = None # Process target references by index. if isinstance(column, int): index = column - 1 if not (0 <= index < len(c_targets)): raise CompilationError( \"Invalid GROUP-BY column index {}\".format(column)) else: # Process target references by name. These will be parsed as # simple Column expressions. If they refer to a target name, we # resolve them. if isinstance(column, query_parser.Column): name = column.name index = targets_name_map.get(name, None) # Otherwise we compile the expression and add it to the list of # targets to evaluate and index into that new target. if index is None: c_expr = compile_expression(column, environ) # Check if the new expression is an aggregate. aggregate = is_aggregate(c_expr) if aggregate: raise CompilationError( \"GROUP-BY expressions may not be aggregates: '{}'\".format( column)) # Attempt to reconcile the expression with one of the existing # target expressions. try: index = c_target_expressions.index(c_expr) except ValueError: # Add the new target. 'None' for the target name implies it # should be invisible, not to be rendered. index = len(new_targets) new_targets.append(EvalTarget(c_expr, None, aggregate)) c_target_expressions.append(c_expr) assert index is not None, \"Internal error, could not index group-by reference.\" group_indexes.append(index) # Check that the group-by column references a non-aggregate. c_expr = new_targets[index].c_expr if is_aggregate(c_expr): raise CompilationError( \"GROUP-BY expressions may not reference aggregates: '{}'\".format( column)) # Check that the group-by column has a supported hashable type. if not is_hashable_type(c_expr): raise CompilationError( \"GROUP-BY a non-hashable type is not supported: '{}'\".format( column)) else: # If it does not have a GROUP-BY clause... aggregate_bools = [c_target.is_aggregate for c_target in c_targets] if any(aggregate_bools): # If the query is an aggregate query, check that all the targets are # aggregates. if all(aggregate_bools): assert group_indexes == [] else: # If some of the targets aren't aggregates, automatically infer # that they are to be implicit group by targets. This makes for # a much more convenient syntax for our lightweight SQL, where # grouping is optional. if SUPPORT_IMPLICIT_GROUPBY: group_indexes = [index for index, c_target in enumerate(c_targets) if not c_target.is_aggregate] else: raise CompilationError( \"Aggregate query without a GROUP-BY should have only aggregates\") else: # This is not an aggregate query; don't set group_indexes to # anything useful, we won't need it. group_indexes = None return new_targets[len(c_targets):], group_indexes","title":"compile_group_by()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.compile_order_by","text":"Process an order-by clause. Parameters: order_by \u2013 A OrderBy instance as provided by the parser. c_targets \u2013 A list of compiled target expressions. environ \u2013 A compilation context to be used to evaluate ORDER BY expressions. Returns: A tuple of new_targets \u2013 A list of new compiled target nodes. order_indexes: A list of integer indexes to be used for processing ordering. Source code in beancount/query/query_compile.py def compile_order_by(order_by, c_targets, environ): \"\"\"Process an order-by clause. Args: order_by: A OrderBy instance as provided by the parser. c_targets: A list of compiled target expressions. environ: A compilation context to be used to evaluate ORDER BY expressions. Returns: A tuple of new_targets: A list of new compiled target nodes. order_indexes: A list of integer indexes to be used for processing ordering. \"\"\" new_targets = copy.copy(c_targets) c_target_expressions = [c_target.c_expr for c_target in c_targets] order_indexes = [] # Compile order-by expressions and resolve them to their targets if # possible. A ORDER-BY column may be one of the following: # # * A reference to a target by name. # * A reference to a target by index (starting at one). # * A new expression, aggregate or not. # # References by name are converted to indexes. New expressions are # inserted into the list of targets as invisible targets. targets_name_map = {target.name: index for index, target in enumerate(c_targets)} for column in order_by.columns: index = None # Process target references by index. if isinstance(column, int): index = column - 1 if not (0 <= index < len(c_targets)): raise CompilationError( \"Invalid ORDER-BY column index {}\".format(column)) else: # Process target references by name. These will be parsed as # simple Column expressions. If they refer to a target name, we # resolve them. if isinstance(column, query_parser.Column): name = column.name index = targets_name_map.get(name, None) # Otherwise we compile the expression and add it to the list of # targets to evaluate and index into that new target. if index is None: c_expr = compile_expression(column, environ) # Attempt to reconcile the expression with one of the existing # target expressions. try: index = c_target_expressions.index(c_expr) except ValueError: # Add the new target. 'None' for the target name implies it # should be invisible, not to be rendered. index = len(new_targets) new_targets.append(EvalTarget(c_expr, None, is_aggregate(c_expr))) c_target_expressions.append(c_expr) assert index is not None, \"Internal error, could not index order-by reference.\" order_indexes.append(index) return (new_targets[len(c_targets):], order_indexes)","title":"compile_order_by()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.compile_print","text":"Compile a Print statement. Parameters: statement \u2013 An instance of query_parser.Print. entries_environ \u2013 : A compilation environment for evaluating entry filters. Returns: An instance of EvalPrint, ready to be executed. Source code in beancount/query/query_compile.py def compile_print(print_stmt, env_entries): \"\"\"Compile a Print statement. Args: statement: An instance of query_parser.Print. entries_environ: : A compilation environment for evaluating entry filters. Returns: An instance of EvalPrint, ready to be executed. \"\"\" c_from = compile_from(print_stmt.from_clause, env_entries) return EvalPrint(c_from)","title":"compile_print()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.compile_select","text":"Prepare an AST for a Select statement into a very rudimentary execution tree. The execution tree mostly looks much like an AST, but with some nodes replaced with knowledge specific to an execution context and eventually some basic optimizations. Parameters: select \u2013 An instance of query_parser.Select. targets_environ \u2013 A compilation environment for evaluating targets. postings_environ \u2013 A compilation environment for evaluating postings filters. entries_environ \u2013 A compilation environment for evaluating entry filters. Returns: An instance of EvalQuery, ready to be executed. Source code in beancount/query/query_compile.py def compile_select(select, targets_environ, postings_environ, entries_environ): \"\"\"Prepare an AST for a Select statement into a very rudimentary execution tree. The execution tree mostly looks much like an AST, but with some nodes replaced with knowledge specific to an execution context and eventually some basic optimizations. Args: select: An instance of query_parser.Select. targets_environ: A compilation environment for evaluating targets. postings_environ: A compilation environment for evaluating postings filters. entries_environ: A compilation environment for evaluating entry filters. Returns: An instance of EvalQuery, ready to be executed. \"\"\" # Process the FROM clause and figure out the execution environment for the # targets and the where clause. from_clause = select.from_clause if isinstance(from_clause, query_parser.Select): c_from = None environ_target = ResultSetEnvironment() environ_where = ResultSetEnvironment() # Remove this when we add support for nested queries. raise CompilationError(\"Queries from nested SELECT are not supported yet\") if from_clause is None or isinstance(from_clause, query_parser.From): # Bind the from clause contents. c_from = compile_from(from_clause, entries_environ) environ_target = targets_environ environ_where = postings_environ else: raise CompilationError(\"Unexpected from clause in AST: {}\".format(from_clause)) # Compile the targets. c_targets = compile_targets(select.targets, environ_target) # Bind the WHERE expression to the execution environment. if select.where_clause is not None: c_where = compile_expression(select.where_clause, environ_where) # Aggregates are disallowed in this clause. Check for this. # NOTE: This should never trigger if the compilation environment does not # contain any aggregate. Just being manic and safe here. if is_aggregate(c_where): raise CompilationError(\"Aggregates are disallowed in WHERE clause\") else: c_where = None # Process the GROUP-BY clause. new_targets, group_indexes = compile_group_by(select.group_by, c_targets, environ_target) if new_targets: c_targets.extend(new_targets) # Process the ORDER-BY clause. if select.order_by is not None: (new_targets, order_indexes) = compile_order_by(select.order_by, c_targets, environ_target) if new_targets: c_targets.extend(new_targets) ordering = select.order_by.ordering else: order_indexes = None ordering = None # If this is an aggregate query (it groups, see list of indexes), check that # the set of non-aggregates match exactly the group indexes. This should # always be the case at this point, because we have added all the necessary # targets to the list of group-by expressions and should have resolved all # the indexes. if group_indexes is not None: non_aggregate_indexes = set(index for index, c_target in enumerate(c_targets) if not c_target.is_aggregate) if non_aggregate_indexes != set(group_indexes): missing_names = ['\"{}\"'.format(c_targets[index].name) for index in non_aggregate_indexes - set(group_indexes)] raise CompilationError( \"All non-aggregates must be covered by GROUP-BY clause in aggregate query; \" \"the following targets are missing: {}\".format(\",\".join(missing_names))) # Check that PIVOT-BY is not supported yet. if select.pivot_by is not None: raise CompilationError(\"The PIVOT BY clause is not supported yet\") return EvalQuery(c_targets, c_from, c_where, group_indexes, order_indexes, ordering, select.limit, select.distinct, select.flatten)","title":"compile_select()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.compile_targets","text":"Compile the targets and check for their validity. Process wildcard. Parameters: targets \u2013 A list of target expressions from the parser. environ \u2013 A compilation context for the targets. Returns: A list of compiled target expressions with resolved names. Source code in beancount/query/query_compile.py def compile_targets(targets, environ): \"\"\"Compile the targets and check for their validity. Process wildcard. Args: targets: A list of target expressions from the parser. environ: A compilation context for the targets. Returns: A list of compiled target expressions with resolved names. \"\"\" # Bind the targets expressions to the execution context. if isinstance(targets, query_parser.Wildcard): # Insert the full list of available columns. targets = [query_parser.Target(query_parser.Column(name), None) for name in environ.wildcard_columns] # Compile targets. c_targets = [] target_names = set() for target in targets: c_expr = compile_expression(target.expression, environ) target_name = find_unique_name( target.name or query_parser.get_expression_name(target.expression), target_names) target_names.add(target_name) c_targets.append(EvalTarget(c_expr, target_name, is_aggregate(c_expr))) # Figure out if this query is an aggregate query and check validity of each # target's aggregation type. for index, c_target in enumerate(c_targets): columns, aggregates = get_columns_and_aggregates(c_target.c_expr) # Check for mixed aggregates and non-aggregates. if columns and aggregates: raise CompilationError( \"Mixed aggregates and non-aggregates are not allowed\") if aggregates: # Check for aggregates of aggregates. for aggregate in aggregates: for child in aggregate.childnodes(): if is_aggregate(child): raise CompilationError( \"Aggregates of aggregates are not allowed\") return c_targets","title":"compile_targets()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.find_unique_name","text":"Come up with a unique name for 'name' amongst 'allocated_set'. Parameters: name \u2013 A string, the prefix of the name to find a unique for. allocated_set \u2013 A set of string, the set of already allocated names. Returns: A unique name. 'allocated_set' is unmodified. Source code in beancount/query/query_compile.py def find_unique_name(name, allocated_set): \"\"\"Come up with a unique name for 'name' amongst 'allocated_set'. Args: name: A string, the prefix of the name to find a unique for. allocated_set: A set of string, the set of already allocated names. Returns: A unique name. 'allocated_set' is unmodified. \"\"\" # Make sure the name is unique. prefix = name i = 1 while name in allocated_set: name = '{}_{}'.format(prefix, i) i += 1 return name","title":"find_unique_name()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.get_columns_and_aggregates","text":"Find the columns and aggregate nodes below this tree. All nodes under aggregate nodes are ignored. Parameters: node \u2013 An instance of EvalNode. Returns: A pair of (columns, aggregates), both of which are lists of EvalNode instances. columns \u2013 The list of all columns accessed not under an aggregate node. aggregates: The list of all aggregate nodes. Source code in beancount/query/query_compile.py def get_columns_and_aggregates(node): \"\"\"Find the columns and aggregate nodes below this tree. All nodes under aggregate nodes are ignored. Args: node: An instance of EvalNode. Returns: A pair of (columns, aggregates), both of which are lists of EvalNode instances. columns: The list of all columns accessed not under an aggregate node. aggregates: The list of all aggregate nodes. \"\"\" columns = [] aggregates = [] _get_columns_and_aggregates(node, columns, aggregates) return columns, aggregates","title":"get_columns_and_aggregates()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.is_aggregate","text":"Return true if the node is an aggregate. Parameters: node \u2013 An instance of EvalNode. Returns: A boolean. Source code in beancount/query/query_compile.py def is_aggregate(node): \"\"\"Return true if the node is an aggregate. Args: node: An instance of EvalNode. Returns: A boolean. \"\"\" # Note: We could be a tiny bit more efficient here, but it doesn't matter # much. Performance of the query compilation matters very little overall. _, aggregates = get_columns_and_aggregates(node) return bool(aggregates)","title":"is_aggregate()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.is_hashable_type","text":"Return true if the node is of a hashable type. Parameters: node \u2013 An instance of EvalNode. Returns: A boolean. Source code in beancount/query/query_compile.py def is_hashable_type(node): \"\"\"Return true if the node is of a hashable type. Args: node: An instance of EvalNode. Returns: A boolean. \"\"\" return not issubclass(node.dtype, inventory.Inventory)","title":"is_hashable_type()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.transform_balances","text":"Translate a Balances entry into an uncompiled Select statement. Parameters: balances \u2013 An instance of a Balance object. Returns: An instance of an uncompiled Select object. Source code in beancount/query/query_compile.py def transform_balances(balances): \"\"\"Translate a Balances entry into an uncompiled Select statement. Args: balances: An instance of a Balance object. Returns: An instance of an uncompiled Select object. \"\"\" ## FIXME: Change the aggregation rules to allow GROUP-BY not to include the ## non-aggregate ORDER-BY columns, so we could just GROUP-BY accounts here ## instead of having to include the sort-key. I think it should be fine if ## the first or last sort-order value gets used, because it would simplify ## the input statement. cooked_select = query_parser.Parser().parse(\"\"\" SELECT account, SUM({}(position)) GROUP BY account, ACCOUNT_SORTKEY(account) ORDER BY ACCOUNT_SORTKEY(account) \"\"\".format(balances.summary_func or \"\")) return query_parser.Select(cooked_select.targets, balances.from_clause, balances.where_clause, cooked_select.group_by, cooked_select.order_by, None, None, None, None)","title":"transform_balances()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile.transform_journal","text":"Translate a Journal entry into an uncompiled Select statement. Parameters: journal \u2013 An instance of a Journal object. Returns: An instance of an uncompiled Select object. Source code in beancount/query/query_compile.py def transform_journal(journal): \"\"\"Translate a Journal entry into an uncompiled Select statement. Args: journal: An instance of a Journal object. Returns: An instance of an uncompiled Select object. \"\"\" cooked_select = query_parser.Parser().parse(\"\"\" SELECT date, flag, MAXWIDTH(payee, 48), MAXWIDTH(narration, 80), account, {summary_func}(position), {summary_func}(balance) {where} \"\"\".format(where=('WHERE account ~ \"{}\"'.format(journal.account) if journal.account else ''), summary_func=journal.summary_func or '')) return query_parser.Select(cooked_select.targets, journal.from_clause, cooked_select.where_clause, None, None, None, None, None, None)","title":"transform_journal()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile_test","text":"","title":"query_compile_test"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile_test.CompileSelectBase","text":"","title":"CompileSelectBase"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile_test.CompileSelectBase.assertCompile","text":"Assert parsed and compiled contents from 'query' is 'expected'. Parameters: expected \u2013 An expected AST to compare against the parsed value. query \u2013 An SQL query to be parsed. debug \u2013 A boolean, if true, print extra debugging information on the console. Exceptions: AssertionError \u2013 If the actual AST does not match the expected one. Source code in beancount/query/query_compile_test.py def assertCompile(self, expected, query, debug=False): \"\"\"Assert parsed and compiled contents from 'query' is 'expected'. Args: expected: An expected AST to compare against the parsed value. query: An SQL query to be parsed. debug: A boolean, if true, print extra debugging information on the console. Raises: AssertionError: If the actual AST does not match the expected one. \"\"\" actual = self.compile(query) if debug: print() print() print(actual) print() try: self.assertEqual(expected, actual) return actual except AssertionError: print() print(\"Expected: {}\".format(expected)) print(\"Actual : {}\".format(actual)) raise","title":"assertCompile()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile_test.CompileSelectBase.assertIndexes","text":"Check the four lists of indexes for comparison. Parameters: query \u2013 An instance of EvalQuery, a compiled query statement. expected_simple_indexes \u2013 The expected visible non-aggregate indexes. expected_aggregate_indexes \u2013 The expected visible aggregate indexes. expected_group_indexes \u2013 The expected group_indexes. expected_order_indexes \u2013 The expected order_indexes. Exceptions: AssertionError \u2013 if the check fails. Source code in beancount/query/query_compile_test.py def assertIndexes(self, query, expected_simple_indexes, expected_aggregate_indexes, expected_group_indexes, expected_order_indexes): \"\"\"Check the four lists of indexes for comparison. Args: query: An instance of EvalQuery, a compiled query statement. expected_simple_indexes: The expected visible non-aggregate indexes. expected_aggregate_indexes: The expected visible aggregate indexes. expected_group_indexes: The expected group_indexes. expected_order_indexes: The expected order_indexes. Raises: AssertionError: if the check fails. \"\"\" # Compute the list of _visible_ aggregates and non-aggregates. simple_indexes = [index for index, c_target in enumerate(query.c_targets) if c_target.name and not qc.is_aggregate(c_target.expression)] aggregate_indexes = [index for index, c_target in enumerate(query.c_targets) if c_target.name and qc.is_aggregate(c_target.expression)] self.assertEqual(set(expected_simple_indexes), set(simple_indexes)) self.assertEqual(set(expected_aggregate_indexes), set(aggregate_indexes)) self.assertEqual( set(expected_group_indexes) if expected_group_indexes is not None else None, set(query.group_indexes) if query.group_indexes is not None else None) self.assertEqual( set(expected_order_indexes) if expected_order_indexes is not None else None, set(query.order_indexes) if query.order_indexes is not None else None)","title":"assertIndexes()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile_test.CompileSelectBase.assertSelectInvariants","text":"Assert the invariants on the query. Parameters: query \u2013 An instance of EvalQuery, a compiled query statement. Exceptions: AssertionError \u2013 if the check fails. Source code in beancount/query/query_compile_test.py def assertSelectInvariants(self, query): \"\"\"Assert the invariants on the query. Args: query: An instance of EvalQuery, a compiled query statement. Raises: AssertionError: if the check fails. \"\"\" # Check that the group references cover all the simple indexes. if query.group_indexes is not None: non_aggregate_indexes = [index for index, c_target in enumerate(query.c_targets) if not qc.is_aggregate(c_target.c_expr)] self.assertEqual(set(non_aggregate_indexes), set(query.group_indexes), \"Invalid indexes: {}\".format(query))","title":"assertSelectInvariants()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile_test.CompileSelectBase.compile","text":"Parse one query and compile it. Parameters: query \u2013 An SQL query to be parsed. Returns: The AST. Source code in beancount/query/query_compile_test.py def compile(self, query): \"\"\"Parse one query and compile it. Args: query: An SQL query to be parsed. Returns: The AST. \"\"\" statement = self.parse(query) c_query = qc.compile(statement, self.xcontext_targets, self.xcontext_postings, self.xcontext_entries) if isinstance(c_query, qp.Select): self.assertSelectInvariants(c_query) return c_query","title":"compile()"},{"location":"api_reference/beancount.query.html#beancount.query.query_compile_test.CompileSelectBase.setUp","text":"Hook method for setting up the test fixture before exercising it. Source code in beancount/query/query_compile_test.py def setUp(self): self.parser = qp.Parser()","title":"setUp()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env","text":"Environment object for compiler. This module contains the various column accessors and function evaluators that are made available by the query compiler via their compilation context objects. Define new columns and functions here.","title":"query_env"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.AbsDecimal","text":"Compute the length of the argument. This works on sequences.","title":"AbsDecimal"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.AbsInventory","text":"Compute the length of the argument. This works on sequences.","title":"AbsInventory"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.AbsPosition","text":"Compute the length of the argument. This works on sequences.","title":"AbsPosition"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.AccountColumn","text":"The account of the posting.","title":"AccountColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.AccountSortKey","text":"Get a string to sort accounts in order taking into account the types.","title":"AccountSortKey"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.AnyMeta","text":"Get metadata from the posting or its parent transaction's metadata if not present.","title":"AnyMeta"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.BalanceColumn","text":"The balance for the posting. These can be summed into inventories.","title":"BalanceColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.CloseDate","text":"Get the date of the close directive of the account.","title":"CloseDate"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Coalesce","text":"Return the first non-null argument","title":"Coalesce"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.ConvertAmount","text":"Coerce an amount to a particular currency.","title":"ConvertAmount"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.ConvertAmountWithDate","text":"Coerce an amount to a particular currency.","title":"ConvertAmountWithDate"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.ConvertInventory","text":"Coerce an inventory to a particular currency.","title":"ConvertInventory"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.ConvertInventoryWithDate","text":"Coerce an inventory to a particular currency.","title":"ConvertInventoryWithDate"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.ConvertPosition","text":"Coerce an amount to a particular currency.","title":"ConvertPosition"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.ConvertPositionWithDate","text":"Coerce an amount to a particular currency.","title":"ConvertPositionWithDate"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.CostCurrencyColumn","text":"The cost currency of the posting.","title":"CostCurrencyColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.CostDateColumn","text":"The cost currency of the posting.","title":"CostDateColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.CostInventory","text":"Get the cost of an inventory.","title":"CostInventory"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.CostLabelColumn","text":"The cost currency of the posting.","title":"CostLabelColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.CostNumberColumn","text":"The number of cost units of the posting.","title":"CostNumberColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.CostPosition","text":"Get the cost of a position.","title":"CostPosition"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Count","text":"Count the number of occurrences of the argument.","title":"Count"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Count.allocate","text":"Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate()","title":"allocate()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Count.initialize","text":"Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = 0","title":"initialize()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Count.update","text":"Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, unused_ontext): store[self.handle] += 1","title":"update()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Currency","text":"Extract the currency from an Amount.","title":"Currency"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.CurrencyColumn","text":"The currency of the posting.","title":"CurrencyColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.CurrencyMeta","text":"Get the metadata dict of the commodity directive of the currency.","title":"CurrencyMeta"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Date","text":"Construct a date with year, month, day arguments","title":"Date"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.DateAdd","text":"Adds/subtracts number of days from the given date","title":"DateAdd"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.DateColumn","text":"The date of the parent transaction for this posting.","title":"DateColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.DateDiff","text":"Calculates the difference (in days) between two dates","title":"DateDiff"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.DateEntryColumn","text":"The date of the directive.","title":"DateEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Day","text":"Extract the day from a date.","title":"Day"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.DayColumn","text":"The day of the date of the parent transaction for this posting.","title":"DayColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.DayEntryColumn","text":"The day of the date of the directive.","title":"DayEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.DescriptionColumn","text":"A combination of the payee + narration for the transaction of this posting.","title":"DescriptionColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.DescriptionEntryColumn","text":"A combination of the payee + narration of the transaction, if present.","title":"DescriptionEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.EntryMeta","text":"Get some metadata key of the parent directive (Transaction).","title":"EntryMeta"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.FileLocationColumn","text":"The filename:lineno where the posting was parsed from or created. If you select this column as the first column, because it renders like errors, Emacs is able to pick those up and you can navigate between an arbitrary list of transactions with next-error and previous-error.","title":"FileLocationColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.FilenameColumn","text":"The filename where the posting was parsed from or created.","title":"FilenameColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.FilenameEntryColumn","text":"The filename where the directive was parsed from or created.","title":"FilenameEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.FilterCurrencyInventory","text":"Filter an inventory to just the specified currency.","title":"FilterCurrencyInventory"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.FilterCurrencyPosition","text":"Filter an inventory to just the specified currency.","title":"FilterCurrencyPosition"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.FilterEntriesEnvironment","text":"An execution context that provides access to attributes on Transactions and other entry types.","title":"FilterEntriesEnvironment"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.FilterPostingsEnvironment","text":"An execution context that provides access to attributes on Postings.","title":"FilterPostingsEnvironment"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.FindFirst","text":"Filter a string sequence by regular expression and return the first match.","title":"FindFirst"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.First","text":"Keep the first of the values seen.","title":"First"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.First.allocate","text":"Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate()","title":"allocate()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.First.initialize","text":"Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = None","title":"initialize()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.First.update","text":"Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): if store[self.handle] is None: value = self.eval_args(context)[0] store[self.handle] = value","title":"update()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.FlagColumn","text":"The flag of the parent transaction for this posting.","title":"FlagColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.FlagEntryColumn","text":"The flag the transaction.","title":"FlagEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.GetItemStr","text":"Get the string value of a dict. The value is always converted to a string.","title":"GetItemStr"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Grep","text":"Match a group against a string and return only the matched portion.","title":"Grep"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.GrepN","text":"Match a pattern with subgroups against a string and return the subgroup at the index","title":"GrepN"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.IdColumn","text":"The unique id of the parent transaction for this posting.","title":"IdColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.IdEntryColumn","text":"Unique id of a directive.","title":"IdEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.JoinStr","text":"Join a sequence of strings to a single comma-separated string.","title":"JoinStr"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Last","text":"Keep the last of the values seen.","title":"Last"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Last.allocate","text":"Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate()","title":"allocate()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Last.initialize","text":"Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = None","title":"initialize()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Last.update","text":"Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] store[self.handle] = value","title":"update()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Leaf","text":"Get the name of the leaf subaccount.","title":"Leaf"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Length","text":"Compute the length of the argument. This works on sequences.","title":"Length"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.LineNoColumn","text":"The line number from the file the posting was parsed from.","title":"LineNoColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.LineNoEntryColumn","text":"The line number from the file the directive was parsed from.","title":"LineNoEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.LinksColumn","text":"The set of links of the parent transaction for this posting.","title":"LinksColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.LinksEntryColumn","text":"The set of links of the transaction.","title":"LinksEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.MatchAccount","text":"A predicate, true if the transaction has at least one posting matching the regular expression argument.","title":"MatchAccount"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Max","text":"Compute the maximum of the values.","title":"Max"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Max.allocate","text":"Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate()","title":"allocate()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Max.initialize","text":"Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = self.dtype()","title":"initialize()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Max.update","text":"Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] if value > store[self.handle]: store[self.handle] = value","title":"update()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.MaxWidth","text":"Convert the argument to a substring. This can be used to ensure maximum width","title":"MaxWidth"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Meta","text":"Get some metadata key of the Posting.","title":"Meta"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Min","text":"Compute the minimum of the values.","title":"Min"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Min.allocate","text":"Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate()","title":"allocate()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Min.initialize","text":"Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = self.dtype()","title":"initialize()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Min.update","text":"Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] if value < store[self.handle]: store[self.handle] = value","title":"update()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Month","text":"Extract the month from a date.","title":"Month"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.MonthColumn","text":"The month of the date of the parent transaction for this posting.","title":"MonthColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.MonthEntryColumn","text":"The month of the date of the directive.","title":"MonthEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.NarrationColumn","text":"The narration of the parent transaction for this posting.","title":"NarrationColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.NarrationEntryColumn","text":"The narration of the transaction.","title":"NarrationEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Number","text":"Extract the number from an Amount.","title":"Number"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.NumberColumn","text":"The number of units of the posting.","title":"NumberColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.OnlyInventory","text":"Get one currency's amount from the inventory.","title":"OnlyInventory"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.OpenDate","text":"Get the date of the open directive of the account.","title":"OpenDate"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.OpenMeta","text":"Get the metadata dict of the open directive of the account.","title":"OpenMeta"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.OtherAccountsColumn","text":"The list of other accounts in the transaction, excluding that of this posting.","title":"OtherAccountsColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Parent","text":"Get the parent name of the account.","title":"Parent"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.ParseDate","text":"Construct a date with year, month, day arguments","title":"ParseDate"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.PayeeColumn","text":"The payee of the parent transaction for this posting.","title":"PayeeColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.PayeeEntryColumn","text":"The payee of the transaction.","title":"PayeeEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.PosSignAmount","text":"Correct sign of an Amount based on the usual balance of associated account.","title":"PosSignAmount"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.PosSignDecimal","text":"Correct sign of an Amount based on the usual balance of associated account.","title":"PosSignDecimal"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.PosSignInventory","text":"Correct sign of an Amount based on the usual balance of associated account.","title":"PosSignInventory"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.PosSignPosition","text":"Correct sign of an Amount based on the usual balance of associated account.","title":"PosSignPosition"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.PositionColumn","text":"The position for the posting. These can be summed into inventories.","title":"PositionColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.PostingFlagColumn","text":"The flag of the posting itself.","title":"PostingFlagColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Price","text":"Fetch a price for something at a particular date","title":"Price"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.PriceColumn","text":"The price attached to the posting.","title":"PriceColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.PriceWithDate","text":"Fetch a price for something at a particular date","title":"PriceWithDate"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Quarter","text":"Extract the quarter from a date.","title":"Quarter"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Root","text":"Get the root name(s) of the account.","title":"Root"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.SafeDiv","text":"A division operation that swallows dbz exceptions and outputs 0 instead.","title":"SafeDiv"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Str","text":"Convert the argument to a string.","title":"Str"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Subst","text":"Substitute leftmost non-overlapping occurrences of pattern by replacement.","title":"Subst"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Sum","text":"Calculate the sum of the numerical argument.","title":"Sum"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Sum.allocate","text":"Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate()","title":"allocate()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Sum.initialize","text":"Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = self.dtype()","title":"initialize()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Sum.update","text":"Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] if value is not None: store[self.handle] += value","title":"update()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.SumAmount","text":"Calculate the sum of the amount. The result is an Inventory.","title":"SumAmount"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.SumAmount.update","text":"Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] store[self.handle].add_amount(value)","title":"update()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.SumBase","text":"","title":"SumBase"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.SumBase.allocate","text":"Allocate handles to store data for a node's aggregate storage. This is called once before beginning aggregations. If you need any kind of per-aggregate storage during the computation phase, get it in this method. Parameters: allocator \u2013 An instance of Allocator, on which you can call allocate() to obtain a handle for a slot to store data on store objects later on. Source code in beancount/query/query_env.py def allocate(self, allocator): self.handle = allocator.allocate()","title":"allocate()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.SumBase.initialize","text":"Initialize this node's aggregate data. If the node is not an aggregate, simply initialize the subnodes. Override this method in the aggregator if you need data for storage. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). Source code in beancount/query/query_env.py def initialize(self, store): store[self.handle] = inventory.Inventory()","title":"initialize()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.SumInventory","text":"Calculate the sum of the inventories. The result is an Inventory.","title":"SumInventory"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.SumInventory.update","text":"Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] store[self.handle].add_inventory(value)","title":"update()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.SumPosition","text":"Calculate the sum of the position. The result is an Inventory.","title":"SumPosition"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.SumPosition.update","text":"Evaluate this node. This is designed to recurse on its children. Parameters: store \u2013 An object indexable by handles appropriated during allocate(). context \u2013 The object to which the evaluation need to apply (see call ). Source code in beancount/query/query_env.py def update(self, store, context): value = self.eval_args(context)[0] store[self.handle].add_position(value)","title":"update()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.TagsColumn","text":"The set of tags of the parent transaction for this posting.","title":"TagsColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.TagsEntryColumn","text":"The set of tags of the transaction.","title":"TagsEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.TargetsEnvironment","text":"An execution context that provides access to attributes on Postings.","title":"TargetsEnvironment"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Today","text":"Today's date","title":"Today"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.TypeColumn","text":"The data type of the parent transaction for this posting.","title":"TypeColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.TypeEntryColumn","text":"The data type of the directive.","title":"TypeEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.UnitsInventory","text":"Get the number of units of an inventory (stripping cost).","title":"UnitsInventory"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.UnitsPosition","text":"Get the number of units of a position (stripping cost).","title":"UnitsPosition"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.ValueInventory","text":"Coerce an inventory to its market value at the current date.","title":"ValueInventory"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.ValueInventoryWithDate","text":"Coerce an inventory to its market value at a particular date.","title":"ValueInventoryWithDate"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.ValuePosition","text":"Convert a position to its cost currency at the market value.","title":"ValuePosition"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.ValuePositionWithDate","text":"Convert a position to its cost currency at the market value of a particular date.","title":"ValuePositionWithDate"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Weekday","text":"Extract a 3-letter weekday from a date.","title":"Weekday"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.WeightColumn","text":"The computed weight used for this posting.","title":"WeightColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.Year","text":"Extract the year from a date.","title":"Year"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.YearColumn","text":"The year of the date of the parent transaction for this posting.","title":"YearColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.YearEntryColumn","text":"The year of the date of the directive.","title":"YearEntryColumn"},{"location":"api_reference/beancount.query.html#beancount.query.query_env.YearMonth","text":"Extract the year and month from a date.","title":"YearMonth"},{"location":"api_reference/beancount.query.html#beancount.query.query_env._Neg","text":"Compute the negative value of the argument. This works on various types.","title":"_Neg"},{"location":"api_reference/beancount.query.html#beancount.query.query_env_test","text":"","title":"query_env_test"},{"location":"api_reference/beancount.query.html#beancount.query.query_env_test.TestEnv","text":"","title":"TestEnv"},{"location":"api_reference/beancount.query.html#beancount.query.query_env_test.TestEnv.test_AnyMeta","text":"2016-11-20 * name: \"TheName\" address: \"1 Wrong Way\" empty: \"NotEmpty\" Assets:Banking 1 USD color: \"Green\" address: \"1 Right Way\" empty: Source code in beancount/query/query_env_test.py @parser.parse_doc() def test_AnyMeta(self, entries, _, options_map): \"\"\" 2016-11-20 * name: \"TheName\" address: \"1 Wrong Way\" empty: \"NotEmpty\" Assets:Banking 1 USD color: \"Green\" address: \"1 Right Way\" empty: \"\"\" rtypes, rrows = query.run_query(entries, options_map, 'SELECT ANY_META(\"name\") as m') self.assertEqual([('TheName',)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT ANY_META(\"color\") as m') self.assertEqual([('Green',)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT ANY_META(\"address\") as m') self.assertEqual([('1 Right Way',)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT ANY_META(\"empty\") as m') self.assertEqual([(None,)], rrows)","title":"test_AnyMeta()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env_test.TestEnv.test_Coalesce","text":"2016-11-20 * Assets:Banking 1 USD Source code in beancount/query/query_env_test.py @parser.parse_doc() def test_Coalesce(self, entries, _, options_map): \"\"\" 2016-11-20 * Assets:Banking 1 USD \"\"\" rtypes, rrows = query.run_query(entries, options_map, 'SELECT COALESCE(account, price) as m') self.assertEqual([('Assets:Banking',)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT COALESCE(price, account) as m') self.assertEqual([('Assets:Banking',)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT COALESCE(price, cost_number) as m') self.assertEqual([(None,)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT COALESCE(narration, account) as m') self.assertEqual([('',)], rrows)","title":"test_Coalesce()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env_test.TestEnv.test_Date","text":"2016-11-20 * \"ok\" Assets:Banking 1 USD Source code in beancount/query/query_env_test.py @parser.parse_doc() def test_Date(self, entries, _, options_map): \"\"\" 2016-11-20 * \"ok\" Assets:Banking 1 USD \"\"\" rtypes, rrows = query.run_query(entries, options_map, 'SELECT date(2020, 1, 2) as m') self.assertEqual([(datetime.date(2020, 1, 2),)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT date(year, month, 1) as m') self.assertEqual([(datetime.date(2016, 11, 1),)], rrows) with self.assertRaisesRegex(ValueError, \"day is out of range for month\"): rtypes, rrows = query.run_query(entries, options_map, 'SELECT date(2020, 2, 32) as m') rtypes, rrows = query.run_query(entries, options_map, 'SELECT date(\"2020-01-02\") as m') self.assertEqual([(datetime.date(2020, 1, 2),)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT date(\"2016/11/1\") as m') self.assertEqual([(datetime.date(2016, 11, 1),)], rrows)","title":"test_Date()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env_test.TestEnv.test_DateDiffAdjust","text":"2016-11-20 * \"ok\" Assets:Banking -1 STOCK { 5 USD, 2016-10-30 } Source code in beancount/query/query_env_test.py @parser.parse_doc() def test_DateDiffAdjust(self, entries, _, options_map): \"\"\" 2016-11-20 * \"ok\" Assets:Banking -1 STOCK { 5 USD, 2016-10-30 } \"\"\" rtypes, rrows = query.run_query(entries, options_map, 'SELECT date_diff(date, cost_date) as m') self.assertEqual([(21,)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT date_diff(cost_date, date) as m') self.assertEqual([(-21,)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT date_add(date, 1) as m') self.assertEqual([(datetime.date(2016, 11, 21),)], rrows) rtypes, rrows = query.run_query(entries, options_map, 'SELECT date_add(date, -1) as m') self.assertEqual([(datetime.date(2016, 11, 19),)], rrows)","title":"test_DateDiffAdjust()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env_test.TestEnv.test_GrepN","text":"2016-11-20 * \"prev match in context next\" Assets:Banking 1 USD Source code in beancount/query/query_env_test.py @parser.parse_doc() def test_GrepN(self, entries, _, options_map): \"\"\" 2016-11-20 * \"prev match in context next\" Assets:Banking 1 USD \"\"\" rtypes, rrows = query.run_query(entries, options_map, ''' SELECT GREPN(\"in\", narration, 0) as m ''') self.assertEqual([('in',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT GREPN(\"match (.*) context\", narration, 1) as m ''') self.assertEqual([('in',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT GREPN(\"(.*) in (.*)\", narration, 2) as m ''') self.assertEqual([('context next',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT GREPN(\"ab(at)hing\", \"abathing\", 1) as m ''') self.assertEqual([('at',)], rrows)","title":"test_GrepN()"},{"location":"api_reference/beancount.query.html#beancount.query.query_env_test.TestEnv.test_Subst","text":"2016-11-20 * \"I love candy\" Assets:Banking -1 USD 2016-11-21 * \"Buy thing thing\" Assets:Cash -1 USD Source code in beancount/query/query_env_test.py @parser.parse_doc() def test_Subst(self, entries, _, options_map): \"\"\" 2016-11-20 * \"I love candy\" Assets:Banking -1 USD 2016-11-21 * \"Buy thing thing\" Assets:Cash -1 USD \"\"\" rtypes, rrows = query.run_query(entries, options_map, ''' SELECT SUBST(\"[Cc]andy\", \"carrots\", narration) as m where date = 2016-11-20 ''') self.assertEqual([('I love carrots',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT SUBST(\"thing\", \"t\", narration) as m where date = 2016-11-21 ''') self.assertEqual([('Buy t t',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT SUBST(\"random\", \"t\", narration) as m where date = 2016-11-21 ''') self.assertEqual([('Buy thing thing',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT SUBST(\"(love)\", \"\\\\1 \\\\1\", narration) as m where date = 2016-11-20 ''') self.assertEqual([('I love love candy',)], rrows) rtypes, rrows = query.run_query(entries, options_map, ''' SELECT SUBST(\"Assets:.*\", \"Savings\", account) as a, str(sum(position)) as p ''') self.assertEqual([('Savings', '(-2 USD)')], rrows)","title":"test_Subst()"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute","text":"Execution of interpreter on data rows.","title":"query_execute"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute.Allocator","text":"A helper class to count slot allocations and return unique handles to them.","title":"Allocator"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute.Allocator.allocate","text":"Allocate a new slot to store row aggregation information. Returns: A unique handle used to index into an row-aggregation store (an integer). Source code in beancount/query/query_execute.py def allocate(self): \"\"\"Allocate a new slot to store row aggregation information. Returns: A unique handle used to index into an row-aggregation store (an integer). \"\"\" handle = self.size self.size += 1 return handle","title":"allocate()"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute.Allocator.create_store","text":"Create a new row-aggregation store suitable to contain all the node allocations. Returns: A store that can accommodate and be indexed by all the allocated slot handles. Source code in beancount/query/query_execute.py def create_store(self): \"\"\"Create a new row-aggregation store suitable to contain all the node allocations. Returns: A store that can accommodate and be indexed by all the allocated slot handles. \"\"\" return [None] * self.size","title":"create_store()"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute.RowContext","text":"A dumb container for information used by a row expression.","title":"RowContext"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute.create_row_context","text":"Create the context container which we will use to evaluate rows. Source code in beancount/query/query_execute.py def create_row_context(entries, options_map): \"\"\"Create the context container which we will use to evaluate rows.\"\"\" context = RowContext() context.balance = inventory.Inventory() # Initialize some global properties for use by some of the accessors. context.options_map = options_map context.account_types = options.get_account_types(options_map) context.open_close_map = getters.get_account_open_close(entries) context.commodity_map = getters.get_commodity_map(entries) context.price_map = prices.build_price_map(entries) return context","title":"create_row_context()"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute.execute_print","text":"Print entries from a print statement specification. Parameters: c_print \u2013 An instance of a compiled EvalPrint statement. entries \u2013 A list of directives. options_map \u2013 A parser's option_map. file \u2013 The output file to print to. Source code in beancount/query/query_execute.py def execute_print(c_print, entries, options_map, file): \"\"\"Print entries from a print statement specification. Args: c_print: An instance of a compiled EvalPrint statement. entries: A list of directives. options_map: A parser's option_map. file: The output file to print to. \"\"\" if c_print and c_print.c_from is not None: context = create_row_context(entries, options_map) entries = filter_entries(c_print.c_from, entries, options_map, context) # Create a context that renders all numbers with their natural # precision, but honors the commas option. This is kept in sync with # {2c694afe3140} to avoid a dependency. dcontext = display_context.DisplayContext() dcontext.set_commas(options_map['dcontext'].commas) printer.print_entries(entries, dcontext, file=file)","title":"execute_print()"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute.execute_query","text":"Given a compiled select statement, execute the query. Parameters: query \u2013 An instance of a query_compile.Query entries \u2013 A list of directives. options_map \u2013 A parser's option_map. Returns: A pair of \u2013 result_types: A list of (name, data-type) item pairs. result_rows: A list of ResultRow tuples of length and types described by 'result_types'. Source code in beancount/query/query_execute.py def execute_query(query, entries, options_map): \"\"\"Given a compiled select statement, execute the query. Args: query: An instance of a query_compile.Query entries: A list of directives. options_map: A parser's option_map. Returns: A pair of: result_types: A list of (name, data-type) item pairs. result_rows: A list of ResultRow tuples of length and types described by 'result_types'. \"\"\" # Figure out the result types that describe what we return. result_types = [(target.name, target.c_expr.dtype) for target in query.c_targets if target.name is not None] # Create a class for each final result. # pylint: disable=invalid-name ResultRow = collections.namedtuple('ResultRow', [target.name for target in query.c_targets if target.name is not None]) # Pre-compute lists of the expressions to evaluate. group_indexes = (set(query.group_indexes) if query.group_indexes is not None else query.group_indexes) # Indexes of the columns for result rows and order rows. result_indexes = [index for index, c_target in enumerate(query.c_targets) if c_target.name] order_indexes = query.order_indexes # Figure out if we need to compute balance. uses_balance = any(uses_balance_column(c_expr) for c_expr in itertools.chain( [c_target.c_expr for c_target in query.c_targets], [query.c_where] if query.c_where else [])) context = create_row_context(entries, options_map) # Filter the entries using the FROM clause. filt_entries = (filter_entries(query.c_from, entries, options_map, context) if query.c_from is not None else entries) # Dispatch between the non-aggregated queries and aggregated queries. c_where = query.c_where schwartz_rows = [] # Precompute a list of expressions to be evaluated. c_target_exprs = [c_target.c_expr for c_target in query.c_targets] if query.group_indexes is None: # This is a non-aggregated query. # Iterate over all the postings once and produce schwartzian rows. for entry in misc_utils.filter_type(filt_entries, data.Transaction): context.entry = entry for posting in entry.postings: context.posting = posting if c_where is None or c_where(context): # Compute the balance. if uses_balance: context.balance.add_position(posting) # Evaluate all the values. values = [c_expr(context) for c_expr in c_target_exprs] # Compute result and sort-key objects. result = ResultRow._make(values[index] for index in result_indexes) sortkey = row_sortkey(order_indexes, values, c_target_exprs) schwartz_rows.append((sortkey, result)) else: # This is an aggregated query. # Precompute lists of non-aggregate and aggregate expressions to # evaluate. For aggregate targets, we hunt down the aggregate # sub-expressions to evaluate, to avoid recursion during iteration. c_nonaggregate_exprs = [] c_aggregate_exprs = [] for index, c_expr in enumerate(c_target_exprs): if index in group_indexes: c_nonaggregate_exprs.append(c_expr) else: _, aggregate_exprs = query_compile.get_columns_and_aggregates(c_expr) c_aggregate_exprs.extend(aggregate_exprs) # Note: it is possible that there are no aggregates to compute here. You could # have all columns be non-aggregates and group-by the entire list of columns. # Pre-allocate handles in aggregation nodes. allocator = Allocator() for c_expr in c_aggregate_exprs: c_expr.allocate(allocator) # Iterate over all the postings to evaluate the aggregates. agg_store = {} for entry in misc_utils.filter_type(filt_entries, data.Transaction): context.entry = entry for posting in entry.postings: context.posting = posting if c_where is None or c_where(context): # Compute the balance. if uses_balance: context.balance.add_position(posting) # Compute the non-aggregate expressions. row_key = tuple(c_expr(context) for c_expr in c_nonaggregate_exprs) # Get an appropriate store for the unique key of this row. try: store = agg_store[row_key] except KeyError: # This is a row; create a new store. store = allocator.create_store() for c_expr in c_aggregate_exprs: c_expr.initialize(store) agg_store[row_key] = store # Update the aggregate expressions. for c_expr in c_aggregate_exprs: c_expr.update(store, context) # Iterate over all the aggregations to produce the schwartzian rows. for key, store in agg_store.items(): key_iter = iter(key) values = [] # Finalize the store. for c_expr in c_aggregate_exprs: c_expr.finalize(store) context.store = store for index, c_expr in enumerate(c_target_exprs): if index in group_indexes: value = next(key_iter) else: value = c_expr(context) values.append(value) # Compute result and sort-key objects. result = ResultRow._make(values[index] for index in result_indexes) sortkey = row_sortkey(order_indexes, values, c_target_exprs) schwartz_rows.append((sortkey, result)) # Order results if requested. if order_indexes is not None: schwartz_rows.sort(key=operator.itemgetter(0), reverse=(query.ordering == 'DESC')) # Extract final results, in sorted order at this point. result_rows = [x[1] for x in schwartz_rows] # Apply distinct. if query.distinct: result_rows = list(misc_utils.uniquify(result_rows)) # Apply limit. if query.limit is not None: result_rows = result_rows[:query.limit] # Flatten inventories if requested. if query.flatten: result_types, result_rows = flatten_results(result_types, result_rows) return (result_types, result_rows)","title":"execute_query()"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute.filter_entries","text":"Filter the entries by the given compiled FROM clause. Parameters: c_from \u2013 A compiled From clause instance. entries \u2013 A list of directives. options_map \u2013 A parser's option_map. context \u2013 A prototype of RowContext to use for evaluation. Returns: A list of filtered entries. Source code in beancount/query/query_execute.py def filter_entries(c_from, entries, options_map, context): \"\"\"Filter the entries by the given compiled FROM clause. Args: c_from: A compiled From clause instance. entries: A list of directives. options_map: A parser's option_map. context: A prototype of RowContext to use for evaluation. Returns: A list of filtered entries. \"\"\" assert c_from is None or isinstance(c_from, query_compile.EvalFrom) assert isinstance(entries, list) context = copy.copy(context) if c_from is None: return entries # Process the OPEN clause. if c_from.open is not None: assert isinstance(c_from.open, datetime.date) open_date = c_from.open entries, index = summarize.open_opt(entries, open_date, options_map) # Process the CLOSE clause. if c_from.close is not None: if isinstance(c_from.close, datetime.date): close_date = c_from.close entries, index = summarize.close_opt(entries, close_date, options_map) elif c_from.close is True: entries, index = summarize.close_opt(entries, None, options_map) # Process the CLEAR clause. if c_from.clear is not None: entries, index = summarize.clear_opt(entries, None, options_map) # Filter the entries with the FROM clause's expression. c_expr = c_from.c_expr if c_expr is not None: # A simple function receives a context; how come close_date() is # accepted in the context of a FROM clause? It shouldn't be. new_entries = [] for entry in entries: context.entry = entry if c_expr(context): new_entries.append(entry) entries = new_entries return entries","title":"filter_entries()"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute.flatten_results","text":"Convert inventories in result types to have a row for each. This routine will expand all result lines with an inventory into a new line for each position. Parameters: result_types \u2013 A list of (name, data-type) item pairs. result_rows \u2013 A list of ResultRow tuples of length and types described by 'result_types'. Returns: result_types \u2013 A list of (name, data-type) item pairs. There should be no Inventory types anymore. result_rows: A list of ResultRow tuples of length and types described by 'result_types'. All inventories from the input should have been converted to Position types. Source code in beancount/query/query_execute.py def flatten_results(result_types, result_rows): \"\"\"Convert inventories in result types to have a row for each. This routine will expand all result lines with an inventory into a new line for each position. Args: result_types: A list of (name, data-type) item pairs. result_rows: A list of ResultRow tuples of length and types described by 'result_types'. Returns: result_types: A list of (name, data-type) item pairs. There should be no Inventory types anymore. result_rows: A list of ResultRow tuples of length and types described by 'result_types'. All inventories from the input should have been converted to Position types. \"\"\" indexes = set(index for index, (name, result_type) in enumerate(result_types) if result_type is inventory.Inventory) if not indexes: return (result_types, result_rows) # pylint: disable=invalid-name ResultRow = type(result_rows[0]) # We have to make at least some conversions. num_columns = len(result_types) output_rows = [] for result_row in result_rows: max_rows = max(len(result_row[icol]) for icol in indexes) for irow in range(max_rows): output_row = [] for icol in range(num_columns): value = result_row[icol] if icol in indexes: value = value[irow] if irow < len(value) else None output_row.append(value) output_rows.append(ResultRow._make(output_row)) # Convert the types. output_types = [(name, (position.Position if result_type is inventory.Inventory else result_type)) for name, result_type in result_types] return output_types, output_rows","title":"flatten_results()"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute.row_sortkey","text":"Generate a sortkey for the given values. Parameters: order_indexes \u2013 The indexes by which the rows should be sorted. values \u2013 The computed values in the row. c_exprs \u2013 The matching c_expr's. Returns: A tuple, the sortkey. Source code in beancount/query/query_execute.py def row_sortkey(order_indexes, values, c_exprs): \"\"\"Generate a sortkey for the given values. Args: order_indexes: The indexes by which the rows should be sorted. values: The computed values in the row. c_exprs: The matching c_expr's. Returns: A tuple, the sortkey. \"\"\" if order_indexes is None: return None key = [] for index in order_indexes: value = values[index] key.append(_MIN_VALUES.get(c_exprs[index].dtype, None) if value is None else value) return tuple(key)","title":"row_sortkey()"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute.uses_balance_column","text":"Return true if the expression accesses the special 'balance' column. Parameters: c_expr \u2013 A compiled expression tree (an EvalNode node). Returns: A boolean, true if the expression contains a BalanceColumn node. Source code in beancount/query/query_execute.py def uses_balance_column(c_expr): \"\"\"Return true if the expression accesses the special 'balance' column. Args: c_expr: A compiled expression tree (an EvalNode node). Returns: A boolean, true if the expression contains a BalanceColumn node. \"\"\" return (isinstance(c_expr, query_env.BalanceColumn) or any(uses_balance_column(c_node) for c_node in c_expr.childnodes()))","title":"uses_balance_column()"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute_test","text":"","title":"query_execute_test"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute_test.QueryBase","text":"","title":"QueryBase"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute_test.QueryBase.compile","text":"Parse a query and compile it. Parameters: bql_string \u2013 An SQL query to be parsed. Returns: A compiled EvalQuery node. Source code in beancount/query/query_execute_test.py def compile(self, bql_string): \"\"\"Parse a query and compile it. Args: bql_string: An SQL query to be parsed. Returns: A compiled EvalQuery node. \"\"\" return qc.compile_select(self.parse(bql_string), self.xcontext_targets, self.xcontext_postings, self.xcontext_entries)","title":"compile()"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute_test.QueryBase.parse","text":"Parse a query. Parameters: bql_string \u2013 An SQL query to be parsed. Returns: A parsed statement (Select() node). Source code in beancount/query/query_execute_test.py def parse(self, bql_string): \"\"\"Parse a query. Args: bql_string: An SQL query to be parsed. Returns: A parsed statement (Select() node). \"\"\" return self.parser.parse(bql_string.strip())","title":"parse()"},{"location":"api_reference/beancount.query.html#beancount.query.query_execute_test.QueryBase.setUp","text":"Hook method for setting up the test fixture before exercising it. Source code in beancount/query/query_execute_test.py def setUp(self): super().setUp() self.parser = query_parser.Parser()","title":"setUp()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser","text":"Parser for Beancount Query Language.","title":"query_parser"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Balances","text":"Balances(summary_func, from_clause, where_clause)","title":"Balances"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Balances.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Balances.__new__","text":"Create new instance of Balances(summary_func, from_clause, where_clause)","title":"__new__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Balances.__repr__","text":"Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Balances._asdict","text":"Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self))","title":"_asdict()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Balances._make","text":"Make a new Balances object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result","title":"_make()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Balances._replace","text":"Return a new Balances object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result","title":"_replace()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Errors","text":"Errors()","title":"Errors"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Errors.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Errors.__new__","text":"Create new instance of Errors()","title":"__new__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Errors.__repr__","text":"Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Errors._asdict","text":"Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self))","title":"_asdict()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Errors._make","text":"Make a new Errors object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result","title":"_make()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Errors._replace","text":"Return a new Errors object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result","title":"_replace()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Explain","text":"Explain(statement,)","title":"Explain"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Explain.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Explain.__new__","text":"Create new instance of Explain(statement,)","title":"__new__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Explain.__repr__","text":"Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Explain._asdict","text":"Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self))","title":"_asdict()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Explain._make","text":"Make a new Explain object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result","title":"_make()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Explain._replace","text":"Return a new Explain object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result","title":"_replace()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Journal","text":"Journal(account, summary_func, from_clause)","title":"Journal"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Journal.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Journal.__new__","text":"Create new instance of Journal(account, summary_func, from_clause)","title":"__new__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Journal.__repr__","text":"Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Journal._asdict","text":"Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self))","title":"_asdict()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Journal._make","text":"Make a new Journal object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result","title":"_make()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Journal._replace","text":"Return a new Journal object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result","title":"_replace()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Lexer","text":"PLY lexer for the Beancount Query Language.","title":"Lexer"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Lexer.t_DATE","text":"(#(\\\"[^\\\"] \\\"|\\'[^\\'] \\')|\\d\\d\\d\\d-\\d\\d-\\d\\d) Source code in beancount/query/query_parser.py def t_DATE(self, token): r\"(\\#(\\\"[^\\\"]*\\\"|\\'[^\\']*\\')|\\d\\d\\d\\d-\\d\\d-\\d\\d)\" if token.value[0] == '#': token.value = dateutil.parser.parse(token.value[2:-1]).date() else: token.value = datetime.datetime.strptime(token.value, '%Y-%m-%d').date() return token","title":"t_DATE()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Lexer.t_DECIMAL","text":"[-+]?([0-9]+.[0-9] |[0-9] .[0-9]+) Source code in beancount/query/query_parser.py def t_DECIMAL(self, token): r\"[-+]?([0-9]+\\.[0-9]*|[0-9]*\\.[0-9]+)\" token.value = D(token.value) return token","title":"t_DECIMAL()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Lexer.t_ID","text":"a-zA-Z * Source code in beancount/query/query_parser.py def t_ID(self, token): \"[a-zA-Z][a-zA-Z_]*\" utoken = token.value.upper() if utoken in self.keywords: token.type = utoken token.value = token.value.upper() else: token.value = token.value.lower() return token","title":"t_ID()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Lexer.t_INTEGER","text":"[-+]?[0-9]+ Source code in beancount/query/query_parser.py def t_INTEGER(self, token): r\"[-+]?[0-9]+\" token.value = int(token.value) return token","title":"t_INTEGER()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Lexer.t_STRING","text":"(\"[^\"] \"|'[^'] ') Source code in beancount/query/query_parser.py def t_STRING(self, token): \"(\\\"[^\\\"]*\\\"|\\'[^\\']*\\')\" token.value = token.value[1:-1] return token","title":"t_STRING()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.ParseError","text":"A parser error.","title":"ParseError"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Parser","text":"PLY parser for the Beancount Query Language's full command syntax.","title":"Parser"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Parser.p_balances_statement","text":"balances_statement : BALANCES summary_func from where Source code in beancount/query/query_parser.py def p_balances_statement(self, p): \"\"\" balances_statement : BALANCES summary_func from where \"\"\" p[0] = Balances(p[2], p[3], p[4])","title":"p_balances_statement()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Parser.p_delimiter","text":"delimiter : SEMI | empty Source code in beancount/query/query_parser.py def p_delimiter(self, p): \"\"\" delimiter : SEMI | empty \"\"\"","title":"p_delimiter()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Parser.p_errors_statement","text":"errors_statement : ERRORS Source code in beancount/query/query_parser.py def p_errors_statement(self, p): \"\"\" errors_statement : ERRORS \"\"\" p[0] = Errors()","title":"p_errors_statement()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Parser.p_explain_statement","text":"top_statement : EXPLAIN statement delimiter Source code in beancount/query/query_parser.py def p_explain_statement(self, p): \"top_statement : EXPLAIN statement delimiter\" p[0] = Explain(p[2])","title":"p_explain_statement()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Parser.p_journal_statement","text":"journal_statement : JOURNAL summary_func from | JOURNAL account summary_func from Source code in beancount/query/query_parser.py def p_journal_statement(self, p): \"\"\" journal_statement : JOURNAL summary_func from | JOURNAL account summary_func from \"\"\" p[0] = Journal(None, p[2], p[3]) if len(p) == 4 else Journal(p[2], p[3], p[4])","title":"p_journal_statement()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Parser.p_print_statement","text":"print_statement : PRINT from Source code in beancount/query/query_parser.py def p_print_statement(self, p): \"\"\" print_statement : PRINT from \"\"\" p[0] = Print(p[2])","title":"p_print_statement()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Parser.p_regular_statement","text":"top_statement : statement delimiter Source code in beancount/query/query_parser.py def p_regular_statement(self, p): \"top_statement : statement delimiter\" p[0] = p[1]","title":"p_regular_statement()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Parser.p_reload_statement","text":"reload_statement : RELOAD Source code in beancount/query/query_parser.py def p_reload_statement(self, p): \"\"\" reload_statement : RELOAD \"\"\" p[0] = Reload()","title":"p_reload_statement()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Parser.p_run_statement","text":"run_statement : RUN ID | RUN STRING | RUN ASTERISK | RUN empty Source code in beancount/query/query_parser.py def p_run_statement(self, p): \"\"\" run_statement : RUN ID | RUN STRING | RUN ASTERISK | RUN empty \"\"\" p[0] = RunCustom(p[2])","title":"p_run_statement()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Parser.p_statement","text":"statement : select_statement | balances_statement | journal_statement | print_statement | run_statement | errors_statement | reload_statement Source code in beancount/query/query_parser.py def p_statement(self, p): \"\"\" statement : select_statement | balances_statement | journal_statement | print_statement | run_statement | errors_statement | reload_statement \"\"\" p[0] = p[1]","title":"p_statement()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Parser.p_summary_func","text":"summary_func : empty | AT ID Source code in beancount/query/query_parser.py def p_summary_func(self, p): \"\"\" summary_func : empty | AT ID \"\"\" p[0] = p[2] if len(p) == 3 else None","title":"p_summary_func()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Print","text":"Print(from_clause,)","title":"Print"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Print.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Print.__new__","text":"Create new instance of Print(from_clause,)","title":"__new__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Print.__repr__","text":"Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Print._asdict","text":"Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self))","title":"_asdict()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Print._make","text":"Make a new Print object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result","title":"_make()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Print._replace","text":"Return a new Print object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result","title":"_replace()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Reload","text":"Reload()","title":"Reload"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Reload.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Reload.__new__","text":"Create new instance of Reload()","title":"__new__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Reload.__repr__","text":"Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Reload._asdict","text":"Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self))","title":"_asdict()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Reload._make","text":"Make a new Reload object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result","title":"_make()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Reload._replace","text":"Return a new Reload object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result","title":"_replace()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.RunCustom","text":"RunCustom(query_name,)","title":"RunCustom"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.RunCustom.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.RunCustom.__new__","text":"Create new instance of RunCustom(query_name,)","title":"__new__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.RunCustom.__repr__","text":"Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.RunCustom._asdict","text":"Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self))","title":"_asdict()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.RunCustom._make","text":"Make a new RunCustom object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result","title":"_make()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.RunCustom._replace","text":"Return a new RunCustom object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result","title":"_replace()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Select","text":"Select(targets, from_clause, where_clause, group_by, order_by, pivot_by, limit, distinct, flatten)","title":"Select"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Select.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/query/query_parser.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Select.__new__","text":"Create new instance of Select(targets, from_clause, where_clause, group_by, order_by, pivot_by, limit, distinct, flatten)","title":"__new__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Select.__repr__","text":"Return a nicely formatted representation string Source code in beancount/query/query_parser.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Select._asdict","text":"Return a new dict which maps field names to their values. Source code in beancount/query/query_parser.py def _asdict(self): 'Return a new dict which maps field names to their values.' return _dict(_zip(self._fields, self))","title":"_asdict()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Select._make","text":"Make a new Select object from a sequence or iterable Source code in beancount/query/query_parser.py @classmethod def _make(cls, iterable): result = tuple_new(cls, iterable) if _len(result) != num_fields: raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result","title":"_make()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.Select._replace","text":"Return a new Select object replacing specified fields with new values Source code in beancount/query/query_parser.py def _replace(self, /, **kwds): result = self._make(_map(kwds.pop, field_names, self)) if kwds: raise ValueError(f'Got unexpected field names: {list(kwds)!r}') return result","title":"_replace()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser","text":"PLY parser for the Beancount Query Language's SELECT statement.","title":"SelectParser"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.handle_comma_separated_list","text":"Handle a list of 0, 1 or more comma-separated values. Parameters: p \u2013 A grammar object. Source code in beancount/query/query_parser.py def handle_comma_separated_list(self, p): \"\"\"Handle a list of 0, 1 or more comma-separated values. Args: p: A grammar object. \"\"\" if len(p) == 2: return [] if p[1] is None else [p[1]] else: return p[1] + [p[3]]","title":"handle_comma_separated_list()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_account","text":"account : STRING Source code in beancount/query/query_parser.py def p_account(self, p): \"\"\" account : STRING \"\"\" p[0] = p[1]","title":"p_account()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_boolean","text":"boolean : TRUE | FALSE Source code in beancount/query/query_parser.py def p_boolean(self, p): \"\"\" boolean : TRUE | FALSE \"\"\" p[0] = (p[1] == 'TRUE')","title":"p_boolean()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_column","text":"column : ID Source code in beancount/query/query_parser.py def p_column(self, p): \"\"\" column : ID \"\"\" p[0] = Column(p[1])","title":"p_column()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_column_list","text":"column_list : column | column_list COMMA column Source code in beancount/query/query_parser.py def p_column_list(self, p): \"\"\" column_list : column | column_list COMMA column \"\"\" p[0] = self.handle_comma_separated_list(p)","title":"p_column_list()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_constant","text":"constant : NULL | boolean | INTEGER | DECIMAL | STRING | DATE Source code in beancount/query/query_parser.py def p_constant(self, p): \"\"\" constant : NULL | boolean | INTEGER | DECIMAL | STRING | DATE \"\"\" p[0] = Constant(p[1] if p[1] != 'NULL' else None)","title":"p_constant()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_distinct","text":"distinct : empty | DISTINCT Source code in beancount/query/query_parser.py def p_distinct(self, p): \"\"\" distinct : empty | DISTINCT \"\"\" p[0] = True if p[1] == 'DISTINCT' else None","title":"p_distinct()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_empty","text":"empty : Source code in beancount/query/query_parser.py def p_empty(self, _): \"\"\" empty : \"\"\"","title":"p_empty()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expr_index","text":"expr_index : expression | INTEGER Source code in beancount/query/query_parser.py def p_expr_index(self, p): \"\"\" expr_index : expression | INTEGER \"\"\" p[0] = p[1]","title":"p_expr_index()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expr_index_list","text":"expr_index_list : expr_index | expr_index_list COMMA expr_index Source code in beancount/query/query_parser.py def p_expr_index_list(self, p): \"\"\" expr_index_list : expr_index | expr_index_list COMMA expr_index \"\"\" p[0] = self.handle_comma_separated_list(p)","title":"p_expr_index_list()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_add","text":"expression : expression PLUS expression Source code in beancount/query/query_parser.py def p_expression_add(self, p): \"expression : expression PLUS expression\" p[0] = Add(p[1], p[3])","title":"p_expression_add()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_and","text":"expression : expression AND expression Source code in beancount/query/query_parser.py def p_expression_and(self, p): \"expression : expression AND expression\" p[0] = And(p[1], p[3])","title":"p_expression_and()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_column","text":"expression : column Source code in beancount/query/query_parser.py def p_expression_column(self, p): \"expression : column\" p[0] = p[1]","title":"p_expression_column()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_constant","text":"expression : constant Source code in beancount/query/query_parser.py def p_expression_constant(self, p): \"expression : constant\" p[0] = p[1]","title":"p_expression_constant()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_contains","text":"expression : expression IN expression Source code in beancount/query/query_parser.py def p_expression_contains(self, p): \"expression : expression IN expression\" p[0] = Contains(p[1], p[3])","title":"p_expression_contains()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_div","text":"expression : expression SLASH expression Source code in beancount/query/query_parser.py def p_expression_div(self, p): \"expression : expression SLASH expression\" p[0] = Div(p[1], p[3])","title":"p_expression_div()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_eq","text":"expression : expression EQ expression Source code in beancount/query/query_parser.py def p_expression_eq(self, p): \"expression : expression EQ expression\" p[0] = Equal(p[1], p[3])","title":"p_expression_eq()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_function","text":"expression : ID LPAREN expression_list_opt RPAREN Source code in beancount/query/query_parser.py def p_expression_function(self, p): \"expression : ID LPAREN expression_list_opt RPAREN\" p[0] = Function(p[1], p[3])","title":"p_expression_function()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_gt","text":"expression : expression GT expression Source code in beancount/query/query_parser.py def p_expression_gt(self, p): \"expression : expression GT expression\" p[0] = Greater(p[1], p[3])","title":"p_expression_gt()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_gte","text":"expression : expression GTE expression Source code in beancount/query/query_parser.py def p_expression_gte(self, p): \"expression : expression GTE expression\" p[0] = GreaterEq(p[1], p[3])","title":"p_expression_gte()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_list","text":"expression_list : expression | expression_list COMMA expression Source code in beancount/query/query_parser.py def p_expression_list(self, p): \"\"\" expression_list : expression | expression_list COMMA expression \"\"\" p[0] = self.handle_comma_separated_list(p)","title":"p_expression_list()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_list_opt","text":"expression_list_opt : empty | expression | expression_list COMMA expression Source code in beancount/query/query_parser.py def p_expression_list_opt(self, p): \"\"\" expression_list_opt : empty | expression | expression_list COMMA expression \"\"\" p[0] = self.handle_comma_separated_list(p)","title":"p_expression_list_opt()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_lt","text":"expression : expression LT expression Source code in beancount/query/query_parser.py def p_expression_lt(self, p): \"expression : expression LT expression\" p[0] = Less(p[1], p[3])","title":"p_expression_lt()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_lte","text":"expression : expression LTE expression Source code in beancount/query/query_parser.py def p_expression_lte(self, p): \"expression : expression LTE expression\" p[0] = LessEq(p[1], p[3])","title":"p_expression_lte()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_match","text":"expression : expression TILDE expression Source code in beancount/query/query_parser.py def p_expression_match(self, p): \"expression : expression TILDE expression\" p[0] = Match(p[1], p[3])","title":"p_expression_match()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_mul","text":"expression : expression ASTERISK expression Source code in beancount/query/query_parser.py def p_expression_mul(self, p): \"expression : expression ASTERISK expression\" p[0] = Mul(p[1], p[3])","title":"p_expression_mul()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_ne","text":"expression : expression NE expression Source code in beancount/query/query_parser.py def p_expression_ne(self, p): \"expression : expression NE expression\" p[0] = Not(Equal(p[1], p[3]))","title":"p_expression_ne()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_not","text":"expression : NOT expression Source code in beancount/query/query_parser.py def p_expression_not(self, p): \"expression : NOT expression\" p[0] = Not(p[2])","title":"p_expression_not()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_or","text":"expression : expression OR expression Source code in beancount/query/query_parser.py def p_expression_or(self, p): \"expression : expression OR expression\" p[0] = Or(p[1], p[3])","title":"p_expression_or()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_paren","text":"expression : LPAREN expression RPAREN Source code in beancount/query/query_parser.py def p_expression_paren(self, p): \"expression : LPAREN expression RPAREN\" p[0] = p[2]","title":"p_expression_paren()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_expression_sub","text":"expression : expression MINUS expression Source code in beancount/query/query_parser.py def p_expression_sub(self, p): \"expression : expression MINUS expression\" p[0] = Sub(p[1], p[3])","title":"p_expression_sub()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_flatten","text":"flatten : empty | FLATTEN Source code in beancount/query/query_parser.py def p_flatten(self, p): \"\"\" flatten : empty | FLATTEN \"\"\" p[0] = True if p[1] == 'FLATTEN' else None","title":"p_flatten()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_from","text":"from : empty | FROM opt_expression opt_open opt_close opt_clear Source code in beancount/query/query_parser.py def p_from(self, p): \"\"\" from : empty | FROM opt_expression opt_open opt_close opt_clear \"\"\" if len(p) != 2: if all(p[i] is None for i in range(2, 6)): raise ParseError(\"Empty FROM expression is not allowed\") p[0] = From(p[2], p[3], p[4], p[5]) else: p[0] = None","title":"p_from()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_from_subselect","text":"from_subselect : from | FROM LPAREN select_statement RPAREN Source code in beancount/query/query_parser.py def p_from_subselect(self, p): \"\"\" from_subselect : from | FROM LPAREN select_statement RPAREN \"\"\" if len(p) == 2: p[0] = p[1] else: p[0] = p[3]","title":"p_from_subselect()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_group_by","text":"group_by : empty | GROUP BY expr_index_list having Source code in beancount/query/query_parser.py def p_group_by(self, p): \"\"\" group_by : empty | GROUP BY expr_index_list having \"\"\" p[0] = GroupBy(p[3], p[4]) if len(p) != 2 else None","title":"p_group_by()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_having","text":"having : empty | HAVING expression Source code in beancount/query/query_parser.py def p_having(self, p): \"\"\" having : empty | HAVING expression \"\"\" p[0] = p[2] if len(p) == 3 else None","title":"p_having()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_limit","text":"limit : empty | LIMIT INTEGER Source code in beancount/query/query_parser.py def p_limit(self, p): \"\"\" limit : empty | LIMIT INTEGER \"\"\" p[0] = p[2] if len(p) == 3 else None","title":"p_limit()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_opt_clear","text":"opt_clear : empty | CLEAR Source code in beancount/query/query_parser.py def p_opt_clear(self, p): \"\"\" opt_clear : empty | CLEAR \"\"\" p[0] = True if (p[1] == 'CLEAR') else None","title":"p_opt_clear()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_opt_close","text":"opt_close : empty | CLOSE | CLOSE ON DATE Source code in beancount/query/query_parser.py def p_opt_close(self, p): \"\"\" opt_close : empty | CLOSE | CLOSE ON DATE \"\"\" p[0] = p[3] if len(p) == 4 else (True if (p[1] == 'CLOSE') else self.default_close_date)","title":"p_opt_close()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_opt_expression","text":"opt_expression : empty | expression Source code in beancount/query/query_parser.py def p_opt_expression(self, p): \"\"\" opt_expression : empty | expression \"\"\" p[0] = p[1]","title":"p_opt_expression()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_opt_open","text":"opt_open : empty | OPEN ON DATE Source code in beancount/query/query_parser.py def p_opt_open(self, p): \"\"\" opt_open : empty | OPEN ON DATE \"\"\" p[0] = p[3] if len(p) == 4 else None","title":"p_opt_open()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_order_by","text":"order_by : empty | ORDER BY expr_index_list ordering Source code in beancount/query/query_parser.py def p_order_by(self, p): \"\"\" order_by : empty | ORDER BY expr_index_list ordering \"\"\" p[0] = None if len(p) == 2 else OrderBy(p[3], p[4])","title":"p_order_by()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_ordering","text":"ordering : empty | ASC | DESC Source code in beancount/query/query_parser.py def p_ordering(self, p): \"\"\" ordering : empty | ASC | DESC \"\"\" p[0] = p[1]","title":"p_ordering()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_pivot_by","text":"pivot_by : empty | PIVOT BY column_list Source code in beancount/query/query_parser.py def p_pivot_by(self, p): \"\"\" pivot_by : empty | PIVOT BY column_list \"\"\" p[0] = PivotBy(p[3]) if len(p) == 4 else None","title":"p_pivot_by()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_select_statement","text":"select_statement : SELECT distinct target_spec from_subselect where group_by order_by pivot_by limit flatten Source code in beancount/query/query_parser.py def p_select_statement(self, p): \"\"\" select_statement : SELECT distinct target_spec from_subselect where \\ group_by order_by pivot_by limit flatten \"\"\" p[0] = Select(p[3], p[4], p[5], p[6], p[7], p[8], p[9], p[2], p[10])","title":"p_select_statement()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_target","text":"target : expression AS ID | expression Source code in beancount/query/query_parser.py def p_target(self, p): \"\"\" target : expression AS ID | expression \"\"\" p[0] = Target(p[1], p[3] if len(p) == 4 else None)","title":"p_target()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_target_list","text":"target_list : target | target_list COMMA target Source code in beancount/query/query_parser.py def p_target_list(self, p): \"\"\" target_list : target | target_list COMMA target \"\"\" p[0] = self.handle_comma_separated_list(p)","title":"p_target_list()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_target_spec","text":"target_spec : ASTERISK | target_list Source code in beancount/query/query_parser.py def p_target_spec(self, p): \"\"\" target_spec : ASTERISK | target_list \"\"\" p[0] = Wildcard() if p[1] == '*' else p[1]","title":"p_target_spec()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.SelectParser.p_where","text":"where : empty | WHERE expression Source code in beancount/query/query_parser.py def p_where(self, p): \"\"\" where : empty | WHERE expression \"\"\" if len(p) == 3: assert p[2], \"Empty WHERE clause is not allowed\" p[0] = p[2]","title":"p_where()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser.get_expression_name","text":"Come up with a reasonable identifier for an expression. Parameters: expr \u2013 An expression node. Source code in beancount/query/query_parser.py def get_expression_name(expr): \"\"\"Come up with a reasonable identifier for an expression. Args: expr: An expression node. \"\"\" if isinstance(expr, Column): return expr.name.lower() elif isinstance(expr, Function): names = [expr.fname.lower()] for operand in expr.operands: names.append(get_expression_name(operand)) return '_'.join(names) elif isinstance(expr, Constant): return 'c{}'.format(re.sub('[^a-z0-9]+', '_', str(expr.value))) elif isinstance(expr, UnaryOp): return '_'.join([type(expr).__name__.lower(), get_expression_name(expr.operand)]) elif isinstance(expr, BinaryOp): return '_'.join([type(expr).__name__.lower(), get_expression_name(expr.left), get_expression_name(expr.right)]) else: assert False, \"Unknown expression type.\"","title":"get_expression_name()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser_test","text":"","title":"query_parser_test"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser_test.QueryParserTestBase","text":"","title":"QueryParserTestBase"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser_test.QueryParserTestBase.assertParse","text":"Assert parsed contents from 'query' is 'expected'. Parameters: expected \u2013 An expected AST to compare against the parsed value. query \u2013 An SQL query to be parsed. debug \u2013 A boolean, if true, print extra debugging information on the console. Exceptions: AssertionError \u2013 If the actual AST does not match the expected one. Source code in beancount/query/query_parser_test.py def assertParse(self, expected, query, debug=False): \"\"\"Assert parsed contents from 'query' is 'expected'. Args: expected: An expected AST to compare against the parsed value. query: An SQL query to be parsed. debug: A boolean, if true, print extra debugging information on the console. Raises: AssertionError: If the actual AST does not match the expected one. \"\"\" actual = self.parse(query) if debug: print() print() print(actual) print() self.assertEqual(expected, actual) return actual","title":"assertParse()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser_test.QueryParserTestBase.parse","text":"Parse one query. Parameters: query \u2013 An SQL query to be parsed. Returns: The AST. Source code in beancount/query/query_parser_test.py def parse(self, query): \"\"\"Parse one query. Args: query: An SQL query to be parsed. Returns: The AST. \"\"\" return self.parser.parse(query.strip())","title":"parse()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser_test.QueryParserTestBase.setUp","text":"Hook method for setting up the test fixture before exercising it. Source code in beancount/query/query_parser_test.py def setUp(self): self.parser = qp.Parser()","title":"setUp()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser_test.TestSelectFromBase","text":"","title":"TestSelectFromBase"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser_test.TestSelectFromBase.setUp","text":"Hook method for setting up the test fixture before exercising it. Source code in beancount/query/query_parser_test.py def setUp(self): super().setUp() self.targets = [qp.Target(qp.Column('a'), None), qp.Target(qp.Column('b'), None)] self.expr = qp.Equal(qp.Column('d'), qp.And( qp.Function('max', [qp.Column('e')]), qp.Constant(17)))","title":"setUp()"},{"location":"api_reference/beancount.query.html#beancount.query.query_parser_test.qSelect","text":"A convenience constructor for writing tests without having to provide all arguments. Source code in beancount/query/query_parser_test.py def qSelect(target_spec=None, from_clause=None, where_clause=None, group_by=None, order_by=None, pivot_by=None, limit=None, distinct=None, flatten=None): \"A convenience constructor for writing tests without having to provide all arguments.\" return qp.Select(target_spec, from_clause, where_clause, group_by, order_by, pivot_by, limit, distinct, flatten)","title":"qSelect()"},{"location":"api_reference/beancount.query.html#beancount.query.shell","text":"An interactive command-line shell interpreter for the Beancount Query Language.","title":"shell"},{"location":"api_reference/beancount.query.html#beancount.query.shell.BQLShell","text":"An interactive shell interpreter for the Beancount query language.","title":"BQLShell"},{"location":"api_reference/beancount.query.html#beancount.query.shell.BQLShell.on_Balances","text":"Select balances of some subset of postings. This command is a convenience and converts into an equivalent Select statement, designed to extract the most sensible list of columns for the register of a list of entries as a table. The general form of a JOURNAL statement loosely follows SQL syntax: BALANCE [FROM_CLAUSE] See the SELECT query help for more details on the FROM clause. Source code in beancount/query/shell.py def on_Balances(self, balance): \"\"\" Select balances of some subset of postings. This command is a convenience and converts into an equivalent Select statement, designed to extract the most sensible list of columns for the register of a list of entries as a table. The general form of a JOURNAL statement loosely follows SQL syntax: BALANCE [FROM_CLAUSE] See the SELECT query help for more details on the FROM clause. \"\"\" return self.on_Select(balance)","title":"on_Balances()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.BQLShell.on_Errors","text":"Print the errors that occurred during parsing. Source code in beancount/query/shell.py def on_Errors(self, errors_statement): \"\"\" Print the errors that occurred during parsing. \"\"\" if self.errors: printer.print_errors(self.errors) else: print('(No errors)', file=self.outfile)","title":"on_Errors()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.BQLShell.on_Explain","text":"Compile and print a compiled statement for debugging. Source code in beancount/query/shell.py def on_Explain(self, explain): \"\"\" Compile and print a compiled statement for debugging. \"\"\" # pylint: disable=invalid-name pr = lambda *args: print(*args, file=self.outfile) pr(\"Parsed statement:\") pr(\" {}\".format(explain.statement)) pr() # Compile the select statement and print it uot. try: query = query_compile.compile(explain.statement, self.env_targets, self.env_postings, self.env_entries) except query_compile.CompilationError as exc: pr(str(exc).rstrip('.')) return pr(\"Compiled query:\") pr(\" {}\".format(query)) pr() pr(\"Targets:\") for c_target in query.c_targets: pr(\" '{}'{}: {}\".format( c_target.name or '(invisible)', ' (aggregate)' if query_compile.is_aggregate(c_target.c_expr) else '', c_target.c_expr.dtype.__name__)) pr()","title":"on_Explain()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.BQLShell.on_Journal","text":"Select a journal of some subset of postings. This command is a convenience and converts into an equivalent Select statement, designed to extract the most sensible list of columns for the register of a list of entries as a table. The general form of a JOURNAL statement loosely follows SQL syntax: JOURNAL [FROM_CLAUSE] See the SELECT query help for more details on the FROM clause. Source code in beancount/query/shell.py def on_Journal(self, journal): \"\"\" Select a journal of some subset of postings. This command is a convenience and converts into an equivalent Select statement, designed to extract the most sensible list of columns for the register of a list of entries as a table. The general form of a JOURNAL statement loosely follows SQL syntax: JOURNAL [FROM_CLAUSE] See the SELECT query help for more details on the FROM clause. \"\"\" return self.on_Select(journal)","title":"on_Journal()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.BQLShell.on_Print","text":"Print entries in Beancount format. The general form of a PRINT statement includes an SQL-like FROM selector: PRINT [FROM ...] Where: from_expr: A logical expression that matches on the attributes of the directives. See SELECT command for details (this FROM expression supports all the same expressions including its OPEN, CLOSE and CLEAR operations). Source code in beancount/query/shell.py def on_Print(self, print_stmt): \"\"\" Print entries in Beancount format. The general form of a PRINT statement includes an SQL-like FROM selector: PRINT [FROM ...] Where: from_expr: A logical expression that matches on the attributes of the directives. See SELECT command for details (this FROM expression supports all the same expressions including its OPEN, CLOSE and CLEAR operations). \"\"\" # Compile the print statement. try: c_print = query_compile.compile(print_stmt, self.env_targets, self.env_postings, self.env_entries) except query_compile.CompilationError as exc: print('ERROR: {}.'.format(str(exc).rstrip('.')), file=self.outfile) return if self.outfile is sys.stdout: query_execute.execute_print(c_print, self.entries, self.options_map, file=self.outfile) else: with self.get_pager() as file: query_execute.execute_print(c_print, self.entries, self.options_map, file)","title":"on_Print()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.BQLShell.on_Reload","text":"Reload the input file without restarting the shell. Source code in beancount/query/shell.py def on_Reload(self, unused_statement=None): \"\"\" Reload the input file without restarting the shell. \"\"\" self.entries, self.errors, self.options_map = self.loadfun() if self.is_interactive: print_statistics(self.entries, self.options_map, self.outfile)","title":"on_Reload()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.BQLShell.on_RunCustom","text":"Run a custom query instead of a SQL command. RUN Where: custom-query-name: Should be the name of a custom query to be defined in the Beancount input file. Source code in beancount/query/shell.py def on_RunCustom(self, run_stmt): \"\"\" Run a custom query instead of a SQL command. RUN Where: custom-query-name: Should be the name of a custom query to be defined in the Beancount input file. \"\"\" custom_query_map = create_custom_query_map(self.entries) name = run_stmt.query_name if name is None: # List the available queries. for name in sorted(custom_query_map): print(name) elif name == \"*\": for name, query in sorted(custom_query_map.items()): print('{}:'.format(name)) self.run_parser(query.query_string, default_close_date=query.date) print() print() else: query = None if name in custom_query_map: query = custom_query_map[name] else: # lookup best query match using name as prefix queries = [q for q in custom_query_map if q.startswith(name)] if len(queries) == 1: name = queries[0] query = custom_query_map[name] if query: statement = self.parser.parse(query.query_string) self.dispatch(statement) else: print(\"ERROR: Query '{}' not found\".format(name))","title":"on_RunCustom()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.BQLShell.on_Select","text":"Extract data from a query on the postings. The general form of a SELECT statement loosely follows SQL syntax, with some mild and idiomatic extensions: SELECT DISTINCT [FROM [OPEN ON ] [CLOSE [ON ]] [CLEAR]] [WHERE ] [GROUP BY ] [ORDER BY [ASC|DESC]] [LIMIT num] Where: targets: A list of desired output attributes from the postings, and expressions on them. Some of the attributes of the parent transaction directive are made available in this context as well. Simple functions (that return a single value per row) and aggregation functions (that return a single value per group) are available. For the complete list of supported columns and functions, see help on \"targets\". You can also provide a wildcard here, which will select a reasonable default set of columns for rendering a journal. from_expr: A logical expression that matches on the attributes of the directives (not postings). This allows you to select a subset of transactions, so the accounting equation is respected for balance reports. For the complete list of supported columns and functions, see help on \"from\". where_expr: A logical expression that matches on the attributes of postings. The available columns are similar to those in the targets clause, without the aggregation functions. OPEN clause: replace all the transactions before the given date by summarizing entries and transfer Income and Expenses balances to Equity. CLOSE clause: Remove all the transactions after the given date and CLEAR: Transfer final Income and Expenses balances to Equity. Source code in beancount/query/shell.py def on_Select(self, statement): \"\"\" Extract data from a query on the postings. The general form of a SELECT statement loosely follows SQL syntax, with some mild and idiomatic extensions: SELECT [DISTINCT] [|*] [FROM [OPEN ON ] [CLOSE [ON ]] [CLEAR]] [WHERE ] [GROUP BY ] [ORDER BY [ASC|DESC]] [LIMIT num] Where: targets: A list of desired output attributes from the postings, and expressions on them. Some of the attributes of the parent transaction directive are made available in this context as well. Simple functions (that return a single value per row) and aggregation functions (that return a single value per group) are available. For the complete list of supported columns and functions, see help on \"targets\". You can also provide a wildcard here, which will select a reasonable default set of columns for rendering a journal. from_expr: A logical expression that matches on the attributes of the directives (not postings). This allows you to select a subset of transactions, so the accounting equation is respected for balance reports. For the complete list of supported columns and functions, see help on \"from\". where_expr: A logical expression that matches on the attributes of postings. The available columns are similar to those in the targets clause, without the aggregation functions. OPEN clause: replace all the transactions before the given date by summarizing entries and transfer Income and Expenses balances to Equity. CLOSE clause: Remove all the transactions after the given date and CLEAR: Transfer final Income and Expenses balances to Equity. \"\"\" # Compile the SELECT statement. try: c_query = query_compile.compile(statement, self.env_targets, self.env_postings, self.env_entries) except query_compile.CompilationError as exc: print('ERROR: {}.'.format(str(exc).rstrip('.')), file=self.outfile) return # Execute it to obtain the result rows. rtypes, rrows = query_execute.execute_query(c_query, self.entries, self.options_map) # Output the resulting rows. if not rrows: print(\"(empty)\", file=self.outfile) else: output_format = self.vars['format'] if output_format == 'text': kwds = dict(boxed=self.vars['boxed'], spaced=self.vars['spaced'], expand=self.vars['expand']) if self.outfile is sys.stdout: with self.get_pager() as file: query_render.render_text(rtypes, rrows, self.options_map['dcontext'], file, **kwds) else: query_render.render_text(rtypes, rrows, self.options_map['dcontext'], self.outfile, **kwds) elif output_format == 'csv': # Numberify CSV output if requested. if self.vars['numberify']: dformat = self.options_map['dcontext'].build() rtypes, rrows = numberify.numberify_results(rtypes, rrows, dformat) query_render.render_csv(rtypes, rrows, self.options_map['dcontext'], self.outfile, expand=self.vars['expand']) else: assert output_format not in _SUPPORTED_FORMATS print(\"Unsupported output format: '{}'.\".format(output_format), file=self.outfile)","title":"on_Select()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell","text":"A usable convenient shell for interpreting commands, with history.","title":"DispatchingShell"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.__init__","text":"Create a shell with history. Parameters: is_interactive \u2013 A boolean, true if this serves an interactive tty. parser \u2013 A command parser. outfile \u2013 An output file object to write communications to. default_format \u2013 A string, the default output format. Source code in beancount/query/shell.py def __init__(self, is_interactive, parser, outfile, default_format, do_numberify): \"\"\"Create a shell with history. Args: is_interactive: A boolean, true if this serves an interactive tty. parser: A command parser. outfile: An output file object to write communications to. default_format: A string, the default output format. \"\"\" super().__init__() if is_interactive and readline is not None: load_history(path.expanduser(HISTORY_FILENAME)) self.is_interactive = is_interactive self.parser = parser self.initialize_vars(default_format, do_numberify) self.add_help() self.outfile = outfile","title":"__init__()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.add_help","text":"Attach help functions for each of the parsed token handlers. Source code in beancount/query/shell.py def add_help(self): \"Attach help functions for each of the parsed token handlers.\" for attrname, func in list(self.__class__.__dict__.items()): match = re.match('on_(.*)', attrname) if not match: continue command_name = match.group(1) setattr(self.__class__, 'help_{}'.format(command_name.lower()), lambda _, fun=func: print(textwrap.dedent(fun.__doc__).strip(), file=self.outfile))","title":"add_help()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.cmdloop","text":"Override cmdloop to handle keyboard interrupts. Source code in beancount/query/shell.py def cmdloop(self): \"\"\"Override cmdloop to handle keyboard interrupts.\"\"\" while True: try: super().cmdloop() break except KeyboardInterrupt: print('\\n(Interrupted)', file=self.outfile)","title":"cmdloop()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.default","text":"Default handling of lines which aren't recognized as native shell commands. Parameters: line \u2013 The string to be parsed. Source code in beancount/query/shell.py def default(self, line): \"\"\"Default handling of lines which aren't recognized as native shell commands. Args: line: The string to be parsed. \"\"\" self.run_parser(line)","title":"default()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.dispatch","text":"Dispatch the given statement to a suitable method. Parameters: statement \u2013 An instance provided by the parser. Returns: Whatever the invoked method happens to return. Source code in beancount/query/shell.py def dispatch(self, statement): \"\"\"Dispatch the given statement to a suitable method. Args: statement: An instance provided by the parser. Returns: Whatever the invoked method happens to return. \"\"\" try: method = getattr(self, 'on_{}'.format(type(statement).__name__)) except AttributeError: print(\"Internal error: statement '{}' is unsupported.\".format(statement), file=self.outfile) else: return method(statement)","title":"dispatch()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.do_EOF","text":"Exit the parser. Source code in beancount/query/shell.py def exit(self, _): \"\"\"Exit the parser.\"\"\" print('exit', file=self.outfile) return 1","title":"do_EOF()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.do_clear","text":"Clear the history. Source code in beancount/query/shell.py def do_clear(self, _): \"Clear the history.\" readline.clear_history()","title":"do_clear()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.do_exit","text":"Exit the parser. Source code in beancount/query/shell.py def exit(self, _): \"\"\"Exit the parser.\"\"\" print('exit', file=self.outfile) return 1","title":"do_exit()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.do_help","text":"Strip superfluous semicolon. Source code in beancount/query/shell.py def do_help(self, command): \"\"\"Strip superfluous semicolon.\"\"\" super().do_help(command.rstrip('; \\t'))","title":"do_help()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.do_history","text":"Print the command-line history statement. Source code in beancount/query/shell.py def do_history(self, _): \"Print the command-line history statement.\" if readline is not None: for index, line in enumerate(get_history(self.max_entries)): print(line, file=self.outfile)","title":"do_history()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.do_lex","text":"Just run the lexer on the following command and print the output. Source code in beancount/query/shell.py def do_lex(self, line): \"Just run the lexer on the following command and print the output.\" try: self.parser.tokenize(line) except query_parser.ParseError as exc: print(exc, file=self.outfile)","title":"do_lex()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.do_parse","text":"Just run the parser on the following command and print the output. Source code in beancount/query/shell.py def do_parse(self, line): \"Just run the parser on the following command and print the output.\" print(\"INPUT: {}\".format(repr(line)), file=self.outfile) try: statement = self.parser.parse(line, True) print(statement, file=self.outfile) except (query_parser.ParseError, query_compile.CompilationError) as exc: print(exc, file=self.outfile) except Exception as exc: traceback.print_exc(file=self.outfile)","title":"do_parse()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.do_quit","text":"Exit the parser. Source code in beancount/query/shell.py def exit(self, _): \"\"\"Exit the parser.\"\"\" print('exit', file=self.outfile) return 1","title":"do_quit()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.do_set","text":"Get/set shell settings variables. Source code in beancount/query/shell.py def do_set(self, line): \"Get/set shell settings variables.\" if not line: for varname, value in sorted(self.vars.items()): print('{}: {}'.format(varname, value), file=self.outfile) else: components = shlex.split(line) varname = components[0] if len(components) == 1: try: value = self.vars[varname] print('{}: {}'.format(varname, value), file=self.outfile) except KeyError: print(\"Variable '{}' does not exist.\".format(varname), file=self.outfile) elif len(components) == 2: value = components[1] try: converted_value = self.vars_types[varname](value) self.vars[varname] = converted_value print('{}: {}'.format(varname, converted_value), file=self.outfile) except KeyError: print(\"Variable '{}' does not exist.\".format(varname), file=self.outfile) else: print(\"Invalid number of arguments.\", file=self.outfile)","title":"do_set()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.do_tokenize","text":"Just run the lexer on the following command and print the output. Source code in beancount/query/shell.py def do_lex(self, line): \"Just run the lexer on the following command and print the output.\" try: self.parser.tokenize(line) except query_parser.ParseError as exc: print(exc, file=self.outfile)","title":"do_tokenize()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.emptyline","text":"Do nothing on an empty line. Source code in beancount/query/shell.py def emptyline(self): \"\"\"Do nothing on an empty line.\"\"\"","title":"emptyline()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.exit","text":"Exit the parser. Source code in beancount/query/shell.py def exit(self, _): \"\"\"Exit the parser.\"\"\" print('exit', file=self.outfile) return 1","title":"exit()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.get_pager","text":"Create and return a context manager to write to, a pager subprocess if required. Returns: A pair of a file object to write to, and a pipe object to wait on (or None if not necessary to wait). Source code in beancount/query/shell.py def get_pager(self): \"\"\"Create and return a context manager to write to, a pager subprocess if required. Returns: A pair of a file object to write to, and a pipe object to wait on (or None if not necessary to wait). \"\"\" if self.is_interactive: return pager.ConditionalPager(self.vars.get('pager', None), minlines=misc_utils.get_screen_height()) else: file = (codecs.getwriter(\"utf-8\")(sys.stdout.buffer) if hasattr(sys.stdout, 'buffer') else sys.stdout) return pager.flush_only(file)","title":"get_pager()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.initialize_vars","text":"Initialize the setting variables of the interactive shell. Source code in beancount/query/shell.py def initialize_vars(self, default_format, do_numberify): \"\"\"Initialize the setting variables of the interactive shell.\"\"\" self.vars_types = { 'pager': str, 'format': str, 'boxed': convert_bool, 'spaced': convert_bool, 'expand': convert_bool, 'numberify': convert_bool, } self.vars = { 'pager': os.environ.get('PAGER', None), 'format': default_format, 'boxed': False, 'spaced': False, 'expand': False, 'numberify': do_numberify, }","title":"initialize_vars()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.DispatchingShell.run_parser","text":"Handle statements via our parser instance and dispatch to appropriate methods. Parameters: line \u2013 The string to be parsed. default_close_date \u2013 A datetimed.date instance, the default close date. Source code in beancount/query/shell.py def run_parser(self, line, default_close_date=None): \"\"\"Handle statements via our parser instance and dispatch to appropriate methods. Args: line: The string to be parsed. default_close_date: A datetimed.date instance, the default close date. \"\"\" try: statement = self.parser.parse(line, default_close_date=default_close_date) self.dispatch(statement) except query_parser.ParseError as exc: print(exc, file=self.outfile) except Exception as exc: traceback.print_exc(file=self.outfile)","title":"run_parser()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.convert_bool","text":"Convert a string to a boolean. Parameters: string \u2013 A string representing a boolean. Returns: The corresponding boolean. Source code in beancount/query/shell.py def convert_bool(string): \"\"\"Convert a string to a boolean. Args: string: A string representing a boolean. Returns: The corresponding boolean. \"\"\" return not string.lower() in ('f', 'false', '0')","title":"convert_bool()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.create_custom_query_map","text":"Extract a mapping of the custom queries from the list of entries. Parameters: entries \u2013 A list of entries. Returns: A map of query-name strings to Query directives. Source code in beancount/query/shell.py def create_custom_query_map(entries): \"\"\"Extract a mapping of the custom queries from the list of entries. Args: entries: A list of entries. Returns: A map of query-name strings to Query directives. \"\"\" query_map = {} for entry in entries: if not isinstance(entry, data.Query): continue if entry.name in query_map: logging.warning(\"Duplicate query: %s\", entry.name) query_map[entry.name] = entry return query_map","title":"create_custom_query_map()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.generate_env_attribute_list","text":"Generate a dictionary of rendered attribute lists for help. Parameters: env \u2013 An instance of an environment. Returns: A dict with keys 'columns', 'functions' and 'aggregates' to rendered and formatted strings. Source code in beancount/query/shell.py def generate_env_attribute_list(env): \"\"\"Generate a dictionary of rendered attribute lists for help. Args: env: An instance of an environment. Returns: A dict with keys 'columns', 'functions' and 'aggregates' to rendered and formatted strings. \"\"\" wrapper = textwrap.TextWrapper(initial_indent=' ', subsequent_indent=' ', drop_whitespace=True, width=80) str_columns = generate_env_attributes( wrapper, env.columns) str_simple = generate_env_attributes( wrapper, env.functions, lambda node: not issubclass(node, query_compile.EvalAggregator)) str_aggregate = generate_env_attributes( wrapper, env.functions, lambda node: issubclass(node, query_compile.EvalAggregator)) return dict(columns=str_columns, functions=str_simple, aggregates=str_aggregate)","title":"generate_env_attribute_list()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.generate_env_attributes","text":"Generate a string of all the help functions of the attributes. Parameters: wrapper \u2013 A TextWrapper instance to format the paragraphs. field_dict \u2013 A dict of the field-names to the node instances, fetch from an environment. filter_pred \u2013 A predicate to filter the desired columns. This is applied to the evaluator node instances. Returns: A formatted multiline string, ready for insertion in a help text. Source code in beancount/query/shell.py def generate_env_attributes(wrapper, field_dict, filter_pred=None): \"\"\"Generate a string of all the help functions of the attributes. Args: wrapper: A TextWrapper instance to format the paragraphs. field_dict: A dict of the field-names to the node instances, fetch from an environment. filter_pred: A predicate to filter the desired columns. This is applied to the evaluator node instances. Returns: A formatted multiline string, ready for insertion in a help text. \"\"\" # Expand the name if its key has argument types. # # FIXME: Render the __intypes__ here nicely instead of the key. flat_items = [] for name, column_cls in field_dict.items(): if isinstance(name, tuple): name = name[0] if issubclass(column_cls, query_compile.EvalFunction): name = name.upper() args = [] for dtypes in column_cls.__intypes__: if isinstance(dtypes, (tuple, list)): arg = '|'.join(dtype.__name__ for dtype in dtypes) else: arg = dtypes.__name__ args.append(arg) name = \"{}({})\".format(name, ','.join(args)) flat_items.append((name, column_cls)) # Render each of the attributes. oss = io.StringIO() for name, column_cls in sorted(flat_items): if filter_pred and not filter_pred(column_cls): continue docstring = column_cls.__doc__ or \"[See class {}]\".format(column_cls.__name__) if issubclass(column_cls, query_compile.EvalColumn): docstring += \" Type: {}.\".format(column_cls().dtype.__name__) # if hasattr(column_cls, '__equivalent__'): # docstring += \" Attribute:{}.\".format(column_cls.__equivalent__) text = re.sub('[ \\t]+', ' ', docstring.strip().replace('\\n', ' ')) doc = \"'{}': {}\".format(name, text) oss.write(wrapper.fill(doc)) oss.write('\\n') return oss.getvalue().rstrip()","title":"generate_env_attributes()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.get_history","text":"Return the history in the readline buffer. Parameters: max_entries \u2013 An integer, the maximum number of entries to return. Returns: A list of string, the previous history of commands. Source code in beancount/query/shell.py def get_history(max_entries): \"\"\"Return the history in the readline buffer. Args: max_entries: An integer, the maximum number of entries to return. Returns: A list of string, the previous history of commands. \"\"\" num_entries = readline.get_current_history_length() assert num_entries >= 0 start = max(0, num_entries - max_entries) return [readline.get_history_item(index+1) for index in range(start, num_entries)]","title":"get_history()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.load_history","text":"Load the shell's past history. Parameters: filename \u2013 A string, the name of the file containing the shell history. Source code in beancount/query/shell.py def load_history(filename): \"\"\"Load the shell's past history. Args: filename: A string, the name of the file containing the shell history. \"\"\" readline.parse_and_bind(\"tab:complete\") if hasattr(readline, \"read_history_file\"): try: readline.read_history_file(filename) except IOError: # Don't error on absent file. pass atexit.register(save_history, filename)","title":"load_history()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.print_statistics","text":"Print summary statistics to stdout. Parameters: entries \u2013 A list of directives. options_map \u2013 An options map. as produced by the parser. outfile \u2013 A file object to write to. Source code in beancount/query/shell.py def print_statistics(entries, options_map, outfile): \"\"\"Print summary statistics to stdout. Args: entries: A list of directives. options_map: An options map. as produced by the parser. outfile: A file object to write to. \"\"\" num_directives, num_transactions, num_postings = summary_statistics(entries) if 'title' in options_map: print('Input file: \"{}\"'.format(options_map['title']), file=outfile) print(\"Ready with {} directives ({} postings in {} transactions).\".format( num_directives, num_postings, num_transactions), file=outfile)","title":"print_statistics()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.save_history","text":"Save the shell history. This should be invoked on exit. Parameters: filename \u2013 A string, the name of the file to save the history to. Source code in beancount/query/shell.py def save_history(filename): \"\"\"Save the shell history. This should be invoked on exit. Args: filename: A string, the name of the file to save the history to. \"\"\" readline.write_history_file(filename)","title":"save_history()"},{"location":"api_reference/beancount.query.html#beancount.query.shell.summary_statistics","text":"Calculate basic summary statistics to output a brief welcome message. Parameters: entries \u2013 A list of directives. Returns: A tuple of three integers, the total number of directives parsed, the total number of transactions and the total number of postings there in. Source code in beancount/query/shell.py def summary_statistics(entries): \"\"\"Calculate basic summary statistics to output a brief welcome message. Args: entries: A list of directives. Returns: A tuple of three integers, the total number of directives parsed, the total number of transactions and the total number of postings there in. \"\"\" num_directives = len(entries) num_transactions = 0 num_postings = 0 for entry in entries: if isinstance(entry, data.Transaction): num_transactions += 1 num_postings += len(entry.postings) return (num_directives, num_transactions, num_postings)","title":"summary_statistics()"},{"location":"api_reference/beancount.query.html#beancount.query.shell_test","text":"","title":"shell_test"},{"location":"api_reference/beancount.query.html#beancount.query.shell_test.TestShell","text":"","title":"TestShell"},{"location":"api_reference/beancount.query.html#beancount.query.shell_test.TestShell.test_success","text":"2013-01-01 open Assets:Account1 2013-01-01 open Assets:Account2 2013-01-01 open Assets:Account3 2013-01-01 open Equity:Unknown 2013-04-05 * Equity:Unknown Assets:Account1 5000 USD 2013-04-05 * Assets:Account1 -3000 USD Assets:Account2 30 BOOG {100 USD} 2013-04-05 * Assets:Account1 -1000 USD Assets:Account3 800 EUR @ 1.25 USD Source code in beancount/query/shell_test.py @test_utils.docfile def test_success(self, filename): \"\"\" 2013-01-01 open Assets:Account1 2013-01-01 open Assets:Account2 2013-01-01 open Assets:Account3 2013-01-01 open Equity:Unknown 2013-04-05 * Equity:Unknown Assets:Account1 5000 USD 2013-04-05 * Assets:Account1 -3000 USD Assets:Account2 30 BOOG {100 USD} 2013-04-05 * Assets:Account1 -1000 USD Assets:Account3 800 EUR @ 1.25 USD \"\"\" with test_utils.capture('stdout', 'stderr') as (stdout, _): test_utils.run_with_args(shell.main, [filename, \"SELECT 1;\"]) self.assertTrue(stdout.getvalue())","title":"test_success()"},{"location":"api_reference/beancount.query.html#beancount.query.shell_test.TestUseCases","text":"Testing all the use cases from the proposal here. I'm hoping to replace reports by these queries instead.","title":"TestUseCases"},{"location":"api_reference/beancount.query.html#beancount.query.shell_test.runshell","text":"Decorate a function to run the shell and return the output. Source code in beancount/query/shell_test.py def runshell(function): \"\"\"Decorate a function to run the shell and return the output.\"\"\" def test_function(self): def loadfun(): return entries, errors, options_map with test_utils.capture('stdout') as stdout: shell_obj = shell.BQLShell(False, loadfun, sys.stdout) shell_obj.on_Reload() shell_obj.onecmd(function.__doc__) return function(self, stdout.getvalue()) return test_function","title":"runshell()"},{"location":"api_reference/beancount.reports.html","text":"beancount.reports \uf0c1 Routines to produce various reports, either to HTML or to text. beancount.reports.balance_reports \uf0c1 Report classes for all reports that display ending balances of accounts. beancount.reports.balance_reports.BalanceSheetReport ( HTMLReport ) \uf0c1 Print out a balance sheet. beancount.reports.balance_reports.BalanceSheetReport.render_real_html(cls, real_root, price_map, price_date, options_map, file) \uf0c1 Wrap an htmldiv into our standard HTML template. Parameters: real_root \u2013 An instance of RealAccount. price_map \u2013 A price database. price_date \u2013 A date for evaluating prices. options_map \u2013 A dict, options as produced by the parser. file \u2013 A file object to write the output to. Source code in beancount/reports/balance_reports.py def render_real_html(cls, real_root, price_map, price_date, options_map, file): \"\"\"Wrap an htmldiv into our standard HTML template. Args: real_root: An instance of RealAccount. price_map: A price database. price_date: A date for evaluating prices. options_map: A dict, options as produced by the parser. file: A file object to write the output to. \"\"\" template = get_html_template() oss = io.StringIO() cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss) file.write(template.format(body=oss.getvalue(), title='')) beancount.reports.balance_reports.BalancesReport ( HTMLReport ) \uf0c1 Print out the trial balance of accounts matching an expression. beancount.reports.balance_reports.BalancesReport.add_args(parser) classmethod \uf0c1 Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/balance_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-e', '--filter-expression', '--expression', '--regexp', action='store', default=None, help=\"Filter expression for which account balances to display.\") parser.add_argument('-c', '--at-cost', '--cost', action='store_true', help=\"Render values at cost, convert the units to cost value\") beancount.reports.balance_reports.BalancesReport.render_real_html(cls, real_root, price_map, price_date, options_map, file) \uf0c1 Wrap an htmldiv into our standard HTML template. Parameters: real_root \u2013 An instance of RealAccount. price_map \u2013 A price database. price_date \u2013 A date for evaluating prices. options_map \u2013 A dict, options as produced by the parser. file \u2013 A file object to write the output to. Source code in beancount/reports/balance_reports.py def render_real_html(cls, real_root, price_map, price_date, options_map, file): \"\"\"Wrap an htmldiv into our standard HTML template. Args: real_root: An instance of RealAccount. price_map: A price database. price_date: A date for evaluating prices. options_map: A dict, options as produced by the parser. file: A file object to write the output to. \"\"\" template = get_html_template() oss = io.StringIO() cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss) file.write(template.format(body=oss.getvalue(), title='')) beancount.reports.balance_reports.IncomeStatementReport ( HTMLReport ) \uf0c1 Print out an income statement. beancount.reports.balance_reports.IncomeStatementReport.render_real_html(cls, real_root, price_map, price_date, options_map, file) \uf0c1 Wrap an htmldiv into our standard HTML template. Parameters: real_root \u2013 An instance of RealAccount. price_map \u2013 A price database. price_date \u2013 A date for evaluating prices. options_map \u2013 A dict, options as produced by the parser. file \u2013 A file object to write the output to. Source code in beancount/reports/balance_reports.py def render_real_html(cls, real_root, price_map, price_date, options_map, file): \"\"\"Wrap an htmldiv into our standard HTML template. Args: real_root: An instance of RealAccount. price_map: A price database. price_date: A date for evaluating prices. options_map: A dict, options as produced by the parser. file: A file object to write the output to. \"\"\" template = get_html_template() oss = io.StringIO() cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss) file.write(template.format(body=oss.getvalue(), title='')) beancount.reports.base \uf0c1 Base class for all reports classes. Each report class should be able to render a filtered list of entries to a variety of formats. Each report has a name, some command-line options, and supports some subset of formats. beancount.reports.base.HTMLReport ( Report ) \uf0c1 A mixin for reports that support forwarding html to htmldiv implementation. beancount.reports.base.RealizationMeta ( type ) \uf0c1 A metaclass for reports that render a realization. The main use of this metaclass is to allow us to create report classes with render_real_*() methods that accept a RealAccount instance as the basis for producing a report. RealAccount can be expensive to build, and may be pre-computed and kept around to generate the various reports related to a particular filter of a subset of transactions, and it would be inconvenient to have to recalculate it every time we need to produce a report. In particular, this is the case for the web interface: the user selects a particular subset of transactions to view, and can then click to the various reports related to this subset of transactions. This is why this is useful. The classes generated with this metaclass respond to the same interface as the regular report classes, so that if invoked from the command-line, it will automatically build the realization from the given set of entries. This metaclass looks at the class' existing render_real_ () methods and generate the corresponding render_ () methods automatically. beancount.reports.base.RealizationMeta.__new__(mcs, name, bases, namespace) special staticmethod \uf0c1 Create and return a new object. See help(type) for accurate signature. Source code in beancount/reports/base.py def __new__(mcs, name, bases, namespace): new_type = super(RealizationMeta, mcs).__new__(mcs, name, bases, namespace) # Go through the methods of the new type and look for render_real() methods. new_methods = {} for attr, value in new_type.__dict__.items(): match = re.match('render_real_(.*)', attr) if not match: continue # Make sure that if an explicit version of render_*() has already # been declared, that we don't override it. render_function_name = 'render_{}'.format(match.group(1)) if render_function_name in new_type.__dict__: continue # Define a render_*() method on the class. def forward_method(self, entries, errors, options_map, file, fwdfunc=value): account_types = options.get_account_types(options_map) real_root = realization.realize(entries, account_types) price_map = prices.build_price_map(entries) # Note: When we forward, use the latest date (None). return fwdfunc(self, real_root, price_map, None, options_map, file) forward_method.__name__ = render_function_name new_methods[render_function_name] = forward_method # Update the type with the newly defined methods.. for mname, mvalue in new_methods.items(): setattr(new_type, mname, mvalue) # Auto-generate other methods if necessary. if hasattr(new_type, 'render_real_htmldiv'): setattr(new_type, 'render_real_html', mcs.render_real_html) return new_type beancount.reports.base.RealizationMeta.render_real_html(cls, real_root, price_map, price_date, options_map, file) \uf0c1 Wrap an htmldiv into our standard HTML template. Parameters: real_root \u2013 An instance of RealAccount. price_map \u2013 A price database. price_date \u2013 A date for evaluating prices. options_map \u2013 A dict, options as produced by the parser. file \u2013 A file object to write the output to. Source code in beancount/reports/base.py def render_real_html(cls, real_root, price_map, price_date, options_map, file): \"\"\"Wrap an htmldiv into our standard HTML template. Args: real_root: An instance of RealAccount. price_map: A price database. price_date: A date for evaluating prices. options_map: A dict, options as produced by the parser. file: A file object to write the output to. \"\"\" template = get_html_template() oss = io.StringIO() cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss) file.write(template.format(body=oss.getvalue(), title='')) beancount.reports.base.Report \uf0c1 Base class for all reports. Attributes: Name Type Description names A list of strings, the various names of this report. The first name is taken to be the authoritative name of the report; the rest are considered aliases. parser The parser for the command's arguments. This is used to raise errors. args An object that contains the values of this command's parsed arguments. beancount.reports.base.Report.__call__(self, entries, errors, options_map, output_format=None, file=None) special \uf0c1 Render a report of filtered entries to any format. This function dispatches to a specific method. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. output_format \u2013 A string, the name of the format. If not specified, use the default format. file \u2013 The file to write the output to. Returns: If no 'file' is provided, return the contents of the report as a string. Exceptions: ReportError \u2013 If the requested format is not supported. Source code in beancount/reports/base.py def render(self, entries, errors, options_map, output_format=None, file=None): \"\"\"Render a report of filtered entries to any format. This function dispatches to a specific method. Args: entries: A list of directives to render. errors: A list of errors that occurred during processing. options_map: A dict of options, as produced by the parser. output_format: A string, the name of the format. If not specified, use the default format. file: The file to write the output to. Returns: If no 'file' is provided, return the contents of the report as a string. Raises: ReportError: If the requested format is not supported. \"\"\" try: render_method = getattr(self, 'render_{}'.format(output_format or self.default_format)) except AttributeError: raise ReportError(\"Unsupported format: '{}'\".format(output_format)) outfile = io.StringIO() if file is None else file result = render_method(entries, errors, options_map, outfile) assert result is None, \"Render method must write to file.\" if file is None: return outfile.getvalue() beancount.reports.base.Report.add_args(parser) classmethod \uf0c1 Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/base.py @classmethod def add_args(cls, parser): \"\"\"Add arguments to parse for this report. Args: parser: An instance of argparse.ArgumentParser. \"\"\" # No-op. beancount.reports.base.Report.from_args(argv=None, **kwds) classmethod \uf0c1 A convenience method used to create an instance from arguments. This creates an instance of the report with default arguments. This is a convenience that may be used for tests. Our actual script uses subparsers and invokes add_args() and creates an appropriate instance directly. Parameters: argv \u2013 A list of strings, command-line arguments to use to construct the report. kwds \u2013 A dict of other keyword arguments to pass to the report's constructor. Returns: A new instance of the report. Source code in beancount/reports/base.py @classmethod def from_args(cls, argv=None, **kwds): \"\"\"A convenience method used to create an instance from arguments. This creates an instance of the report with default arguments. This is a convenience that may be used for tests. Our actual script uses subparsers and invokes add_args() and creates an appropriate instance directly. Args: argv: A list of strings, command-line arguments to use to construct the report. kwds: A dict of other keyword arguments to pass to the report's constructor. Returns: A new instance of the report. \"\"\" parser = version.ArgumentParser() cls.add_args(parser) return cls(parser.parse_args(argv or []), parser, **kwds) beancount.reports.base.Report.get_supported_formats() classmethod \uf0c1 Enumerates the list of supported formats, by inspecting methods of this object. Returns: A list of strings, such as ['html', 'text']. Source code in beancount/reports/base.py @classmethod def get_supported_formats(cls): \"\"\"Enumerates the list of supported formats, by inspecting methods of this object. Returns: A list of strings, such as ['html', 'text']. \"\"\" formats = [] for name in dir(cls): match = re.match('render_([a-z0-9]+)$', name) if match: formats.append(match.group(1)) return sorted(formats) beancount.reports.base.Report.render(self, entries, errors, options_map, output_format=None, file=None) \uf0c1 Render a report of filtered entries to any format. This function dispatches to a specific method. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. output_format \u2013 A string, the name of the format. If not specified, use the default format. file \u2013 The file to write the output to. Returns: If no 'file' is provided, return the contents of the report as a string. Exceptions: ReportError \u2013 If the requested format is not supported. Source code in beancount/reports/base.py def render(self, entries, errors, options_map, output_format=None, file=None): \"\"\"Render a report of filtered entries to any format. This function dispatches to a specific method. Args: entries: A list of directives to render. errors: A list of errors that occurred during processing. options_map: A dict of options, as produced by the parser. output_format: A string, the name of the format. If not specified, use the default format. file: The file to write the output to. Returns: If no 'file' is provided, return the contents of the report as a string. Raises: ReportError: If the requested format is not supported. \"\"\" try: render_method = getattr(self, 'render_{}'.format(output_format or self.default_format)) except AttributeError: raise ReportError(\"Unsupported format: '{}'\".format(output_format)) outfile = io.StringIO() if file is None else file result = render_method(entries, errors, options_map, outfile) assert result is None, \"Render method must write to file.\" if file is None: return outfile.getvalue() beancount.reports.base.ReportError ( Exception ) \uf0c1 Error that occurred during report generation. beancount.reports.base.TableReport ( HTMLReport ) \uf0c1 A base class for reports that supports automatic conversions from Table. beancount.reports.base.TableReport.generate_table(self, entries, errors, options_map) \uf0c1 Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/base.py def generate_table(self, entries, errors, options_map): \"\"\"Render the report to a Table instance. Args: entries: A list of directives to render. errors: A list of errors that occurred during processing. options_map: A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. \"\"\" raise NotImplementedError beancount.reports.base.get_html_template() \uf0c1 Returns our vanilla HTML template for embedding an HTML div. Returns: A string, with a formatting style placeholders \u2013 {title}: for the title of the page. {body}: for the body, where the div goes. Source code in beancount/reports/base.py def get_html_template(): \"\"\"Returns our vanilla HTML template for embedding an HTML div. Returns: A string, with a formatting style placeholders: {title}: for the title of the page. {body}: for the body, where the div goes. \"\"\" with open(path.join(path.dirname(__file__), 'template.html')) as infile: return infile.read() beancount.reports.context \uf0c1 Produce a rendering of the account balances just before and after a particular entry is applied. beancount.reports.context.render_entry_context(entries, options_map, entry) \uf0c1 Render the context before and after a particular transaction is applied. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. entry \u2013 The entry instance which should be rendered. (Note that this object is expected to be in the set of entries, not just structurally equal.) Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. Source code in beancount/reports/context.py def render_entry_context(entries, options_map, entry): \"\"\"Render the context before and after a particular transaction is applied. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. entry: The entry instance which should be rendered. (Note that this object is expected to be in the set of entries, not just structurally equal.) Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. \"\"\" oss = io.StringIO() meta = entry.meta print(\"Hash:{}\".format(compare.hash_entry(entry)), file=oss) print(\"Location: {}:{}\".format(meta[\"filename\"], meta[\"lineno\"]), file=oss) # Get the list of accounts sorted by the order in which they appear in the # closest entry. order = {} if isinstance(entry, data.Transaction): order = {posting.account: index for index, posting in enumerate(entry.postings)} accounts = sorted(getters.get_entry_accounts(entry), key=lambda account: order.get(account, 10000)) # Accumulate the balances of these accounts up to the entry. balance_before, balance_after = interpolate.compute_entry_context(entries, entry) # Create a format line for printing the contents of account balances. max_account_width = max(map(len, accounts)) if accounts else 1 position_line = '{{:1}} {{:{width}}} {{:>49}}'.format(width=max_account_width) # Print the context before. print(file=oss) print(\"------------ Balances before transaction\", file=oss) print(file=oss) before_hashes = set() for account in accounts: positions = balance_before[account].get_positions() for position in positions: before_hashes.add((account, hash(position))) print(position_line.format('', account, str(position)), file=oss) if not positions: print(position_line.format('', account, ''), file=oss) print(file=oss) # Print the entry itself. print(file=oss) print(\"------------ Transaction\", file=oss) print(file=oss) dcontext = options_map['dcontext'] printer.print_entry(entry, dcontext, render_weights=True, file=oss) if isinstance(entry, data.Transaction): print(file=oss) # Print residuals. residual = interpolate.compute_residual(entry.postings) if not residual.is_empty(): # Note: We render the residual at maximum precision, for debugging. print('Residual: {}'.format(residual), file=oss) # Dump the tolerances used. tolerances = interpolate.infer_tolerances(entry.postings, options_map) if tolerances: print('Tolerances: {}'.format( ', '.join('{}={}'.format(key, value) for key, value in sorted(tolerances.items()))), file=oss) # Compute the total cost basis. cost_basis = inventory.Inventory( pos for pos in entry.postings if pos.cost is not None ).reduce(convert.get_cost) if not cost_basis.is_empty(): print('Basis: {}'.format(cost_basis), file=oss) # Print the context after. print(file=oss) print(\"------------ Balances after transaction\", file=oss) print(file=oss) for account in accounts: positions = balance_after[account].get_positions() for position in positions: changed = (account, hash(position)) not in before_hashes print(position_line.format('*' if changed else '', account, str(position)), file=oss) if not positions: print(position_line.format('', account, ''), file=oss) print(file=oss) return oss.getvalue() beancount.reports.context.render_file_context(entries, options_map, filename, lineno) \uf0c1 Render the context before and after a particular transaction is applied. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. filename \u2013 A string, the name of the file from which the transaction was parsed. lineno \u2013 An integer, the line number in the file the transaction was parsed from. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. Source code in beancount/reports/context.py def render_file_context(entries, options_map, filename, lineno): \"\"\"Render the context before and after a particular transaction is applied. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. filename: A string, the name of the file from which the transaction was parsed. lineno: An integer, the line number in the file the transaction was parsed from. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. \"\"\" # Find the closest entry. closest_entry = data.find_closest(entries, filename, lineno) if closest_entry is None: raise SystemExit(\"No entry could be found before {}:{}\".format(filename, lineno)) return render_entry_context(entries, options_map, closest_entry) beancount.reports.convert_reports \uf0c1 Format converter reports. This module contains reports that can convert an input file into other formats, such as Ledger. beancount.reports.convert_reports.HLedgerPrinter ( LedgerPrinter ) \uf0c1 Multi-method for printing directives in HLedger format. beancount.reports.convert_reports.HLedgerReport ( Report ) \uf0c1 Print out the entries in a format that can be parsed by HLedger. beancount.reports.convert_reports.LedgerPrinter \uf0c1 Multi-method for printing directives in Ledger format. beancount.reports.convert_reports.LedgerReport ( Report ) \uf0c1 Print out the entries in a format that can be parsed by Ledger. beancount.reports.convert_reports.postings_by_type(entry) \uf0c1 Split up the postings by simple, at-cost, at-price. Parameters: entry \u2013 An instance of Transaction. Returns: A tuple of simple postings, postings with price conversions, postings held at cost. Source code in beancount/reports/convert_reports.py def postings_by_type(entry): \"\"\"Split up the postings by simple, at-cost, at-price. Args: entry: An instance of Transaction. Returns: A tuple of simple postings, postings with price conversions, postings held at cost. \"\"\" postings_at_cost = [] postings_at_price = [] postings_simple = [] for posting in entry.postings: if posting.cost: accumlator = postings_at_cost elif posting.price: accumlator = postings_at_price else: accumlator = postings_simple accumlator.append(posting) return (postings_simple, postings_at_price, postings_at_cost) beancount.reports.convert_reports.quote(match) \uf0c1 Add quotes around a re.MatchObject. Parameters: match \u2013 A MatchObject from the re module. Returns: A quoted string of the match contents. Source code in beancount/reports/convert_reports.py def quote(match): \"\"\"Add quotes around a re.MatchObject. Args: match: A MatchObject from the re module. Returns: A quoted string of the match contents. \"\"\" currency = match.group(1) return '\"{}\"'.format(currency) if re.search(r'[0-9\\.]', currency) else currency beancount.reports.convert_reports.quote_currency(string) \uf0c1 Quote all the currencies with numbers from the given string. Parameters: string \u2013 A string of text. Returns: A string of text, with the commodity expressions surrounded with quotes. Source code in beancount/reports/convert_reports.py def quote_currency(string): \"\"\"Quote all the currencies with numbers from the given string. Args: string: A string of text. Returns: A string of text, with the commodity expressions surrounded with quotes. \"\"\" return re.sub(r'\\b({})\\b'.format(amount.CURRENCY_RE), quote, string) beancount.reports.convert_reports.split_currency_conversions(entry) \uf0c1 If the transaction has a mix of conversion at cost and a currency conversion, split the transaction into two transactions: one that applies the currency conversion in the same account, and one that uses the other currency without conversion. This is required because Ledger does not appear to be able to grok a transaction like this one: 2014-11-02 * \"Buy some stock with foreign currency funds\" Assets:CA:Investment:HOOL 5 HOOL {520.0 USD} Expenses:Commissions 9.95 USD Assets:CA:Investment:Cash -2939.46 CAD @ 0.8879 USD HISTORICAL NOTE: Adding a price directive on the first posting above makes Ledger accept the transaction. So we will not split the transaction here now. However, since Ledger's treatment of this type of conflict is subject to revision (See http://bugs.ledger-cli.org/show_bug.cgi?id=630), we will keep this code around, it might become useful eventually. See https://groups.google.com/d/msg/ledger-cli/35hA0Dvhom0/WX8gY_5kHy0J for details of the discussion. Parameters: entry \u2013 An instance of Transaction. Returns: A pair of converted \u2013 boolean, true if a conversion was made. entries: A list of the original entry if converted was False, or a list of the split converted entries if True. Source code in beancount/reports/convert_reports.py def split_currency_conversions(entry): \"\"\"If the transaction has a mix of conversion at cost and a currency conversion, split the transaction into two transactions: one that applies the currency conversion in the same account, and one that uses the other currency without conversion. This is required because Ledger does not appear to be able to grok a transaction like this one: 2014-11-02 * \"Buy some stock with foreign currency funds\" Assets:CA:Investment:HOOL 5 HOOL {520.0 USD} Expenses:Commissions 9.95 USD Assets:CA:Investment:Cash -2939.46 CAD @ 0.8879 USD HISTORICAL NOTE: Adding a price directive on the first posting above makes Ledger accept the transaction. So we will not split the transaction here now. However, since Ledger's treatment of this type of conflict is subject to revision (See http://bugs.ledger-cli.org/show_bug.cgi?id=630), we will keep this code around, it might become useful eventually. See https://groups.google.com/d/msg/ledger-cli/35hA0Dvhom0/WX8gY_5kHy0J for details of the discussion. Args: entry: An instance of Transaction. Returns: A pair of converted: boolean, true if a conversion was made. entries: A list of the original entry if converted was False, or a list of the split converted entries if True. \"\"\" assert isinstance(entry, data.Transaction) (postings_simple, postings_at_price, postings_at_cost) = postings_by_type(entry) converted = postings_at_cost and postings_at_price if converted: # Generate a new entry for each currency conversion. new_entries = [] replacement_postings = [] for posting_orig in postings_at_price: weight = convert.get_weight(posting_orig) posting_pos = data.Posting(posting_orig.account, weight, None, None, None, None) posting_neg = data.Posting(posting_orig.account, -weight, None, None, None, None) currency_entry = entry._replace( postings=[posting_orig, posting_neg], narration=entry.narration + ' (Currency conversion)') new_entries.append(currency_entry) replacement_postings.append(posting_pos) converted_entry = entry._replace(postings=( postings_at_cost + postings_simple + replacement_postings)) new_entries.append(converted_entry) else: new_entries = [entry] return converted, new_entries beancount.reports.export_reports \uf0c1 Reports to Export to third-party portfolio sites. beancount.reports.export_reports.ExportEntry ( tuple ) \uf0c1 ExportEntry(symbol, cost_currency, number, cost_number, mutual_fund, memo, holdings) beancount.reports.export_reports.ExportEntry.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/reports/export_reports.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.reports.export_reports.ExportEntry.__new__(_cls, symbol, cost_currency, number, cost_number, mutual_fund, memo, holdings) special staticmethod \uf0c1 Create new instance of ExportEntry(symbol, cost_currency, number, cost_number, mutual_fund, memo, holdings) beancount.reports.export_reports.ExportEntry.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/reports/export_reports.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.reports.export_reports.ExportPortfolioReport ( TableReport ) \uf0c1 Holdings lists that can be exported to external portfolio management software. beancount.reports.export_reports.ExportPortfolioReport.add_args(parser) classmethod \uf0c1 Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/export_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-d', '--debug', action='store_true', help=\"Output position export debugging information on stderr.\") parser.add_argument('-p', '--promiscuous', action='store_true', help=(\"Include title and account names in memos. \" \"Use this if you trust wherever you upload.\")) parser.add_argument('-a', '--aggregate-by-commodity', action='store_true', help=(\"Group the holdings by account. This may help if your \" \"portfolio fails to import and you have many holdings.\")) beancount.reports.export_reports.classify_holdings_for_export(holdings_list, commodities_map) \uf0c1 Figure out what to do for example with each holding. Parameters: holdings_list \u2013 A list of Holding instances to be exported. commodities_map \u2013 A dict of commodity to Commodity instances. Returns: A pair of \u2013 action_holdings: A list of (symbol, holding) for each holding. 'Symbol' is the ticker to use for export, and may be \"CASH\" or \"IGNORE\" for holdings to be converted or ignored. Source code in beancount/reports/export_reports.py def classify_holdings_for_export(holdings_list, commodities_map): \"\"\"Figure out what to do for example with each holding. Args: holdings_list: A list of Holding instances to be exported. commodities_map: A dict of commodity to Commodity instances. Returns: A pair of: action_holdings: A list of (symbol, holding) for each holding. 'Symbol' is the ticker to use for export, and may be \"CASH\" or \"IGNORE\" for holdings to be converted or ignored. \"\"\" # Get the map of commodities to tickers and export meta tags. exports = getters.get_values_meta(commodities_map, FIELD) # Classify the holdings based on their commodities' ticker metadata field. action_holdings = [] for holding in holdings_list: # Get export field and remove (MONEY:...) specifications. export = re.sub(r'\\(.*\\)', '', exports.get(holding.currency, None) or '').strip() if export: if export.upper() == \"CASH\": action_holdings.append(('CASH', holding)) elif export.upper() == \"IGNORE\": action_holdings.append(('IGNORE', holding)) else: action_holdings.append((export, holding)) else: logging.warning((\"Exporting holding using default commodity name '{}'; this \" \"can potentially break the OFX import. Consider providing \" \"'export' metadata for your commodities.\").format( holding.currency)) action_holdings.append((holding.currency, holding)) return action_holdings beancount.reports.export_reports.export_holdings(entries, options_map, promiscuous, aggregate_by_commodity=False) \uf0c1 Compute a list of holdings to export. Holdings that are converted to cash equivalents will receive a currency of \"CASH:\" where is the converted cash currency. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options as provided by the parser. promiscuous \u2013 A boolean, true if we should output a promiscuous memo. aggregate_by_commodity \u2013 A boolean, true if we should group the holdings by account. Returns: A pair of exported \u2013 A list of ExportEntry tuples, one for each exported position. converted: A list of ExportEntry tuples, one for each converted position. These will contain multiple holdings. holdings_ignored: A list of Holding instances that were ignored, either because they were explicitly marked to be ignored, or because we could not convert them to a money vehicle matching the holding's cost-currency. Source code in beancount/reports/export_reports.py def export_holdings(entries, options_map, promiscuous, aggregate_by_commodity=False): \"\"\"Compute a list of holdings to export. Holdings that are converted to cash equivalents will receive a currency of \"CASH:\" where is the converted cash currency. Args: entries: A list of directives. options_map: A dict of options as provided by the parser. promiscuous: A boolean, true if we should output a promiscuous memo. aggregate_by_commodity: A boolean, true if we should group the holdings by account. Returns: A pair of exported: A list of ExportEntry tuples, one for each exported position. converted: A list of ExportEntry tuples, one for each converted position. These will contain multiple holdings. holdings_ignored: A list of Holding instances that were ignored, either because they were explicitly marked to be ignored, or because we could not convert them to a money vehicle matching the holding's cost-currency. \"\"\" # Get the desired list of holdings. holdings_list, price_map = holdings_reports.get_assets_holdings(entries, options_map) commodities_map = getters.get_commodity_map(entries) dcontext = options_map['dcontext'] # Aggregate the holdings, if requested. Google Finance is notoriously # finnicky and if you have many holdings this might help. if aggregate_by_commodity: holdings_list = holdings.aggregate_holdings_by(holdings_list, lambda holding: holding.currency) # Classify all the holdings for export. action_holdings = classify_holdings_for_export(holdings_list, commodities_map) # The lists of exported and converted export entries, and the list of # ignored holdings. exported = [] converted = [] holdings_ignored = [] # Export the holdings with tickers individually. for symbol, holding in action_holdings: if symbol in (\"CASH\", \"IGNORE\"): continue if holding.cost_number is None: assert holding.cost_currency in (None, holding.currency) cost_number = holding.number cost_currency = holding.currency else: cost_number = holding.cost_number cost_currency = holding.cost_currency exported.append( ExportEntry(symbol, cost_currency, holding.number, cost_number, is_mutual_fund(symbol), holding.account if promiscuous else '', [holding])) # Convert all the cash entries to their book and market value by currency. cash_holdings_map = collections.defaultdict(list) for symbol, holding in action_holdings: if symbol != \"CASH\": continue if holding.cost_currency: # Accumulate market and book values. cash_holdings_map[holding.cost_currency].append(holding) else: # We cannot price this... no cost currency. holdings_ignored.append(holding) # Get the money instruments. money_instruments = get_money_instruments(commodities_map) # Convert all the cash values to money instruments, if possible. If not # possible, we'll just have to ignore those values. # Go through all the holdings to convert, and for each of those which aren't # in terms of one of the money instruments, which we can directly add to the # exported portfolio, attempt to convert them into currencies to one of # those in the money instruments. money_values_book = collections.defaultdict(D) money_values_market = collections.defaultdict(D) money_values_holdings = collections.defaultdict(list) for cost_currency, holdings_list in cash_holdings_map.items(): book_value = sum(holding.book_value for holding in holdings_list) market_value = sum(holding.market_value for holding in holdings_list) if cost_currency in money_instruments: # The holding is already in terms of one of the money instruments. money_values_book[cost_currency] += book_value money_values_market[cost_currency] += market_value money_values_holdings[cost_currency].extend(holdings_list) else: # The holding is not in terms of one of the money instruments. # Find the first available price to convert it into one for money_currency in money_instruments: base_quote = (cost_currency, money_currency) _, rate = prices.get_latest_price(price_map, base_quote) if rate is not None: money_values_book[money_currency] += book_value * rate money_values_market[money_currency] += market_value * rate money_values_holdings[money_currency].extend(holdings_list) break else: # We could not convert into any of the money commodities. Ignore # those holdings. holdings_ignored.extend(holdings_list) for money_currency in money_values_book.keys(): book_value = money_values_book[money_currency] market_value = money_values_market[money_currency] holdings_list = money_values_holdings[money_currency] symbol = money_instruments[money_currency] assert isinstance(book_value, Decimal) assert isinstance(market_value, Decimal) converted.append( ExportEntry(symbol, money_currency, dcontext.quantize(market_value, money_currency), dcontext.quantize(book_value / market_value, money_currency), is_mutual_fund(symbol), '', holdings_list)) # Add all ignored holdings to a final list. for symbol, holding in action_holdings: if symbol == \"IGNORE\": holdings_ignored.append(holding) return exported, converted, holdings_ignored beancount.reports.export_reports.get_money_instruments(commodities_map) \uf0c1 Get the money-market stand-ins for cash positions. Parameters: commodities_map \u2013 A map of currency to their corresponding Commodity directives. Returns: A dict of quote currency to the ticker symbol that stands for it, e.g. {'USD' \u2013 'VMMXX'}. Source code in beancount/reports/export_reports.py def get_money_instruments(commodities_map): \"\"\"Get the money-market stand-ins for cash positions. Args: commodities_map: A map of currency to their corresponding Commodity directives. Returns: A dict of quote currency to the ticker symbol that stands for it, e.g. {'USD': 'VMMXX'}. \"\"\" instruments = {} for currency, entry in commodities_map.items(): export = entry.meta.get(FIELD, '') paren_match = re.search(r'\\((.*)\\)', export) if paren_match: match = re.match('MONEY:({})'.format(amount.CURRENCY_RE), paren_match.group(1)) if match: instruments[match.group(1)] = ( re.sub(r'\\(.*\\)', '', export).strip() or currency) else: logging.error(\"Invalid money specification: %s\", export) return instruments beancount.reports.export_reports.get_symbol(sources, prefer='google') \uf0c1 Filter a source specification to some corresponding ticker. Parameters: source \u2013 A comma-separated list of sources as a string, such as \"google/NASDAQ:AAPL,yahoo/AAPL\". Returns: The symbol string. Exceptions: ValueError \u2013 If the sources does not contain a ticker for the google source. Source code in beancount/reports/export_reports.py def get_symbol(sources, prefer='google'): \"\"\"Filter a source specification to some corresponding ticker. Args: source: A comma-separated list of sources as a string, such as \"google/NASDAQ:AAPL,yahoo/AAPL\". Returns: The symbol string. Raises: ValueError: If the sources does not contain a ticker for the google source. \"\"\" # If the ticker is a list of /, extract the symbol # from it. symbol_items = [] for source in map(str.strip, sources.split(',')): match = re.match('([a-zA-Z][a-zA-Z0-9._]+)/(.*)', source) if match: source, symbol = match.groups() else: source, symbol = None, source symbol_items.append((source, symbol)) if not symbol_items: raise ValueError( 'Invalid source \"{}\" does not contain a ticker'.format(sources)) symbol_map = dict(symbol_items) # If not found, return the first symbol in the list of items. return symbol_map.get(prefer, symbol_items[0][1]) beancount.reports.export_reports.is_mutual_fund(ticker) \uf0c1 Return true if the GFinance ticker is for a mutual fund. Parameters: ticker \u2013 A string, the symbol for GFinance. Returns: A boolean, true for mutual funds. Source code in beancount/reports/export_reports.py def is_mutual_fund(ticker): \"\"\"Return true if the GFinance ticker is for a mutual fund. Args: ticker: A string, the symbol for GFinance. Returns: A boolean, true for mutual funds. \"\"\" return bool(re.match('MUTF.*:', ticker)) beancount.reports.export_reports.render_ofx_date(dtime) \uf0c1 Render a datetime to the OFX format. Parameters: dtime \u2013 A datetime.datetime instance. Returns: A string, rendered to milliseconds. Source code in beancount/reports/export_reports.py def render_ofx_date(dtime): \"\"\"Render a datetime to the OFX format. Args: dtime: A datetime.datetime instance. Returns: A string, rendered to milliseconds. \"\"\" return '{}.{:03d}'.format(dtime.strftime('%Y%m%d%H%M%S'), int(dtime.microsecond / 1000)) beancount.reports.gviz \uf0c1 Support for creating Google gviz timeline charts. beancount.reports.gviz.gviz_timeline(time_array, data_array_map, css_id='chart') \uf0c1 Create a HTML rendering of the given arrays. Parameters: time_array \u2013 A sequence of datetime objects. data_array_map \u2013 A dict or list of items of name to sequence of data points. css_id \u2013 A string, the CSS id attribute of the target node. Returns: Javascript code for rendering the chart. (It's up to you to insert the a div with the correct CSS id in your accompanying HTML page.) Source code in beancount/reports/gviz.py def gviz_timeline(time_array, data_array_map, css_id='chart'): \"\"\"Create a HTML rendering of the given arrays. Args: time_array: A sequence of datetime objects. data_array_map: A dict or list of items of name to sequence of data points. css_id: A string, the CSS id attribute of the target node. Returns: Javascript code for rendering the chart. (It's up to you to insert the a div with the correct CSS id in your accompanying HTML page.) \"\"\" # Set the order of the data to be output. if isinstance(data_array_map, dict): data_array_map = list(data_array_map.items()) # Write preamble. oss = io.StringIO() oss.write('\\n') oss.write('\\n') return oss.getvalue() beancount.reports.holdings_reports \uf0c1 Generate reports no holdings. beancount.reports.holdings_reports.CashReport ( TableReport ) \uf0c1 The list of cash holdings (defined as currency = cost-currency). beancount.reports.holdings_reports.CashReport.add_args(parser) classmethod \uf0c1 Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/holdings_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-c', '--currency', action='store', default=None, help=\"Which currency to convert all the holdings to\") parser.add_argument('-i', '--ignored', action='store_true', help=\"Report on ignored holdings instead of included ones\") parser.add_argument('-o', '--operating-only', action='store_true', help=\"Only report on operating currencies\") beancount.reports.holdings_reports.CashReport.generate_table(self, entries, errors, options_map) \uf0c1 Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/holdings_reports.py def generate_table(self, entries, errors, options_map): holdings_list, price_map = get_assets_holdings(entries, options_map) holdings_list_orig = holdings_list # Keep only the holdings where currency is the same as the cost-currency. holdings_list = [holding for holding in holdings_list if (holding.currency == holding.cost_currency or holding.cost_currency is None)] # Keep only those holdings held in one of the operating currencies. if self.args.operating_only: operating_currencies = set(options_map['operating_currency']) holdings_list = [holding for holding in holdings_list if holding.currency in operating_currencies] # Compute the list of ignored holdings and optionally report on them. if self.args.ignored: ignored_holdings = set(holdings_list_orig) - set(holdings_list) holdings_list = ignored_holdings # Convert holdings to a unified currency. if self.args.currency: holdings_list = holdings.convert_to_currency(price_map, self.args.currency, holdings_list) return table.create_table(holdings_list, FIELD_SPEC) beancount.reports.holdings_reports.HoldingsReport ( TableReport ) \uf0c1 The full list of holdings for Asset and Liabilities accounts. beancount.reports.holdings_reports.HoldingsReport.add_args(parser) classmethod \uf0c1 Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/holdings_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-c', '--currency', action='store', default=None, help=\"Which currency to convert all the holdings to\") parser.add_argument('-r', '--relative', action='store_true', help=\"True if we should render as relative values only\") parser.add_argument('-g', '--groupby', '--by', action='store', default=None, choices=cls.aggregations.keys(), help=\"How to group the holdings (default is: don't group)\") beancount.reports.holdings_reports.HoldingsReport.generate_table(self, entries, errors, options_map) \uf0c1 Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/holdings_reports.py def generate_table(self, entries, errors, options_map): keywords = self.aggregations[self.args.groupby] if self.args.groupby else {} return report_holdings(self.args.currency, self.args.relative, entries, options_map, **keywords) beancount.reports.holdings_reports.NetWorthReport ( TableReport ) \uf0c1 Generate a table of total net worth for each operating currency. beancount.reports.holdings_reports.NetWorthReport.generate_table(self, entries, errors, options_map) \uf0c1 Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/holdings_reports.py def generate_table(self, entries, errors, options_map): holdings_list, price_map = get_assets_holdings(entries, options_map) net_worths = [] for currency in options_map['operating_currency']: # Convert holdings to a unified currency. # # Note: It's entirely possible that the price map does not have all # the necessary rate conversions here. The resulting holdings will # simply have no cost when that is the case. We must handle this # gracefully below. currency_holdings_list = holdings.convert_to_currency(price_map, currency, holdings_list) if not currency_holdings_list: continue holdings_list = holdings.aggregate_holdings_by( currency_holdings_list, lambda holding: holding.cost_currency) holdings_list = [holding for holding in holdings_list if holding.currency and holding.cost_currency] # If after conversion there are no valid holdings, skip the currency # altogether. if not holdings_list: continue net_worths.append((currency, holdings_list[0].market_value)) field_spec = [ (0, 'Currency'), (1, 'Net Worth', '{:,.2f}'.format), ] return table.create_table(net_worths, field_spec) beancount.reports.holdings_reports.get_assets_holdings(entries, options_map, currency=None) \uf0c1 Return holdings for all assets and liabilities. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of parsed options. currency \u2013 If specified, a string, the target currency to convert all holding values to. Returns: A list of Holding instances and a price-map. Source code in beancount/reports/holdings_reports.py def get_assets_holdings(entries, options_map, currency=None): \"\"\"Return holdings for all assets and liabilities. Args: entries: A list of directives. options_map: A dict of parsed options. currency: If specified, a string, the target currency to convert all holding values to. Returns: A list of Holding instances and a price-map. \"\"\" # Compute a price map, to perform conversions. price_map = prices.build_price_map(entries) # Get the list of holdings. account_types = options.get_account_types(options_map) holdings_list = holdings.get_final_holdings(entries, (account_types.assets, account_types.liabilities), price_map) # Convert holdings to a unified currency. if currency: holdings_list = holdings.convert_to_currency(price_map, currency, holdings_list) return holdings_list, price_map beancount.reports.holdings_reports.get_holdings_entries(entries, options_map) \uf0c1 Summarizes the entries to list of entries representing the final holdings.. This list includes the latest prices entries as well. This can be used to load a full snapshot of holdings without including the entire history. This is a way of summarizing a balance sheet in a way that filters away history. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of parsed options. Returns: A string, the entries to print out. Source code in beancount/reports/holdings_reports.py def get_holdings_entries(entries, options_map): \"\"\"Summarizes the entries to list of entries representing the final holdings.. This list includes the latest prices entries as well. This can be used to load a full snapshot of holdings without including the entire history. This is a way of summarizing a balance sheet in a way that filters away history. Args: entries: A list of directives. options_map: A dict of parsed options. Returns: A string, the entries to print out. \"\"\" # The entries will be created at the latest date, against an equity account. latest_date = entries[-1].date _, equity_account, _ = options.get_previous_accounts(options_map) # Get all the assets. holdings_list, _ = get_assets_holdings(entries, options_map) # Create synthetic entries for them. holdings_entries = [] for index, holding in enumerate(holdings_list): meta = data.new_metadata('report_holdings_print', index) entry = data.Transaction(meta, latest_date, flags.FLAG_SUMMARIZE, None, \"\", None, None, []) # Convert the holding to a position. pos = holdings.holding_to_position(holding) entry.postings.append( data.Posting(holding.account, pos.units, pos.cost, None, None, None)) cost = -convert.get_cost(pos) entry.postings.append( data.Posting(equity_account, cost, None, None, None, None)) holdings_entries.append(entry) # Get opening directives for all the accounts. used_accounts = {holding.account for holding in holdings_list} open_entries = summarize.get_open_entries(entries, latest_date) used_open_entries = [open_entry for open_entry in open_entries if open_entry.account in used_accounts] # Add an entry for the equity account we're using. meta = data.new_metadata('report_holdings_print', -1) used_open_entries.insert(0, data.Open(meta, latest_date, equity_account, None, None)) # Get the latest price entries. price_entries = prices.get_last_price_entries(entries, None) return used_open_entries + holdings_entries + price_entries beancount.reports.holdings_reports.load_from_csv(fileobj) \uf0c1 Load a list of holdings from a CSV file. Parameters: fileobj \u2013 A file object. Yields: Instances of Holding, as read from the file. Source code in beancount/reports/holdings_reports.py def load_from_csv(fileobj): \"\"\"Load a list of holdings from a CSV file. Args: fileobj: A file object. Yields: Instances of Holding, as read from the file. \"\"\" column_spec = [ ('Account', 'account', None), ('Units', 'number', D), ('Currency', 'currency', None), ('Cost Currency', 'cost_currency', None), ('Average Cost', 'cost_number', D), ('Price', 'price_number', D), ('Book Value', 'book_value', D), ('Market Value', 'market_value', D), ('Price Date', 'price_date', None), ] column_dict = {name: (attr, converter) for name, attr, converter in column_spec} klass = holdings.Holding # Create a set of default values for the namedtuple. defaults_dict = {attr: None for attr in klass._fields} # Start reading the file. reader = csv.reader(fileobj) # Check that the header is readable. header = next(reader) attr_converters = [] for header_name in header: try: attr_converter = column_dict[header_name] attr_converters.append(attr_converter) except KeyError: raise IOError(\"Invalid file contents for holdings\") for line in reader: value_dict = defaults_dict.copy() for (attr, converter), value in zip(attr_converters, line): if converter: value = converter(value) value_dict[attr] = value yield holdings.Holding(**value_dict) beancount.reports.holdings_reports.report_holdings(currency, relative, entries, options_map, aggregation_key=None, sort_key=None) \uf0c1 Generate a detailed list of all holdings. Parameters: currency \u2013 A string, a currency to convert to. If left to None, no conversion is carried out. relative \u2013 A boolean, true if we should reduce this to a relative value. entries \u2013 A list of directives. options_map \u2013 A dict of parsed options. aggregation_key \u2013 A callable use to generate aggregations. sort_key \u2013 A function to use to sort the holdings, if specified. Returns: A Table instance. Source code in beancount/reports/holdings_reports.py def report_holdings(currency, relative, entries, options_map, aggregation_key=None, sort_key=None): \"\"\"Generate a detailed list of all holdings. Args: currency: A string, a currency to convert to. If left to None, no conversion is carried out. relative: A boolean, true if we should reduce this to a relative value. entries: A list of directives. options_map: A dict of parsed options. aggregation_key: A callable use to generate aggregations. sort_key: A function to use to sort the holdings, if specified. Returns: A Table instance. \"\"\" holdings_list, _ = get_assets_holdings(entries, options_map, currency) if aggregation_key: holdings_list = holdings.aggregate_holdings_by(holdings_list, aggregation_key) if relative: holdings_list = holdings.reduce_relative(holdings_list) field_spec = RELATIVE_FIELD_SPEC else: field_spec = FIELD_SPEC if sort_key: holdings_list.sort(key=sort_key, reverse=True) return table.create_table(holdings_list, field_spec) beancount.reports.html_formatter \uf0c1 Base class for HTML formatters. This object encapsulates the rendering of various objects to HTML. You may, and should, derive and override from this object in order to provide links within a web interface. beancount.reports.html_formatter.HTMLFormatter \uf0c1 A trivial formatter object that can be used to format strings as themselves. This mainly defines an interface to implement. beancount.reports.html_formatter.HTMLFormatter.__init__(self, dcontext) special \uf0c1 Create an instance of HTMLFormatter. Parameters: dcontext \u2013 DisplayContext to use to render the numbers. Source code in beancount/reports/html_formatter.py def __init__(self, dcontext): \"\"\"Create an instance of HTMLFormatter. Args: dcontext: DisplayContext to use to render the numbers. \"\"\" self._dformat = dcontext.build( precision=display_context.Precision.MOST_COMMON) beancount.reports.html_formatter.HTMLFormatter.render_account(self, account_name) \uf0c1 Render an account name. Parameters: account_name \u2013 A string, the name of the account to render. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_account(self, account_name): \"\"\"Render an account name. Args: account_name: A string, the name of the account to render. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return account_name beancount.reports.html_formatter.HTMLFormatter.render_amount(self, amount) \uf0c1 Render an amount. Parameters: amount \u2013 An Amount instance. Returns: A string of HTML to be spliced inside a table cell. Source code in beancount/reports/html_formatter.py def render_amount(self, amount): \"\"\"Render an amount. Args: amount: An Amount instance. Returns: A string of HTML to be spliced inside a table cell. \"\"\" return amount.to_string(self._dformat) beancount.reports.html_formatter.HTMLFormatter.render_commodity(self, base_quote) \uf0c1 Render a commodity (base currency / quote currency). This is only used when we want the commodity to link to its prices. Parameters: commodity \u2013 A pair of strings, the base and quote currency names. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_commodity(self, base_quote): \"\"\"Render a commodity (base currency / quote currency). This is only used when we want the commodity to link to its prices. Args: commodity: A pair of strings, the base and quote currency names. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return '{} / {}'.format(*base_quote) beancount.reports.html_formatter.HTMLFormatter.render_context(self, entry) \uf0c1 Render a reference to context around a transaction (maybe as an HTML link). Parameters: entry \u2013 A directive. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_context(self, entry): \"\"\"Render a reference to context around a transaction (maybe as an HTML link). Args: entry: A directive. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return '' beancount.reports.html_formatter.HTMLFormatter.render_doc(self, filename) \uf0c1 Render a document path. Parameters: filename \u2013 A string, the filename for the document. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_doc(self, filename): \"\"\"Render a document path. Args: filename: A string, the filename for the document. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return filename beancount.reports.html_formatter.HTMLFormatter.render_event_type(self, event) \uf0c1 Render an event type. Parameters: event \u2013 A string, the name of the even type. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_event_type(self, event): \"\"\"Render an event type. Args: event: A string, the name of the even type. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return event beancount.reports.html_formatter.HTMLFormatter.render_inventory(self, inv) \uf0c1 Render an inventory. You can use this opportunity to convert the inventory to units or cost or whatever. Parameters: inv \u2013 An Inventory instance. Returns: A string of HTML to be spliced inside a table cell. Source code in beancount/reports/html_formatter.py def render_inventory(self, inv): \"\"\"Render an inventory. You can use this opportunity to convert the inventory to units or cost or whatever. Args: inv: An Inventory instance. Returns: A string of HTML to be spliced inside a table cell. \"\"\" return '
    '.join(position_.to_string(self._dformat) for position_ in sorted(inv)) beancount.reports.html_formatter.HTMLFormatter.render_link(self, link) \uf0c1 Render a transaction link (maybe as an HTML link). Parameters: link \u2013 A string, the name of the link to render. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_link(self, link): \"\"\"Render a transaction link (maybe as an HTML link). Args: link: A string, the name of the link to render. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return link beancount.reports.html_formatter.HTMLFormatter.render_number(self, number, currency) \uf0c1 Render a number for a currency using the formatter's display context. Parameters: number \u2013 A Decimal instance, the number to be rendered. currency \u2013 A string, the commodity the number represent. Returns: A string, the formatted number to render. Source code in beancount/reports/html_formatter.py def render_number(self, number, currency): \"\"\"Render a number for a currency using the formatter's display context. Args: number: A Decimal instance, the number to be rendered. currency: A string, the commodity the number represent. Returns: A string, the formatted number to render. \"\"\" return self._dformat.format(number, currency) beancount.reports.html_formatter.HTMLFormatter.render_source(self, meta) \uf0c1 Render a reference to the source file. Parameters: meta \u2013 A metadata dict object. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_source(self, meta): \"\"\"Render a reference to the source file. Args: meta: A metadata dict object. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return printer.render_source(meta) beancount.reports.journal_html \uf0c1 HTML rendering routines for serving a lists of postings/entries. beancount.reports.journal_html.Row ( tuple ) \uf0c1 Row(entry, leg_postings, rowtype, extra_class, flag, description, links, amount_str, balance_str) beancount.reports.journal_html.Row.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/reports/journal_html.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.reports.journal_html.Row.__new__(_cls, entry, leg_postings, rowtype, extra_class, flag, description, links, amount_str, balance_str) special staticmethod \uf0c1 Create new instance of Row(entry, leg_postings, rowtype, extra_class, flag, description, links, amount_str, balance_str) beancount.reports.journal_html.Row.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/reports/journal_html.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.reports.journal_html.html_entries_table(oss, txn_postings, formatter, render_postings=True) \uf0c1 Render a list of entries into an HTML table, with no running balance. This is appropriate for rendering tables of entries for postings with multiple accounts, whereby computing the running balances makes little sense. (This function returns nothing, it write to oss as a side-effect.) Parameters: oss \u2013 A file object to write the output to. txn_postings \u2013 A list of Posting or directive instances. formatter \u2013 An instance of HTMLFormatter, to be render accounts, inventories, links and docs. render_postings \u2013 A boolean; if true, render the postings as rows under the main transaction row. Source code in beancount/reports/journal_html.py def html_entries_table(oss, txn_postings, formatter, render_postings=True): \"\"\"Render a list of entries into an HTML table, with no running balance. This is appropriate for rendering tables of entries for postings with multiple accounts, whereby computing the running balances makes little sense. (This function returns nothing, it write to oss as a side-effect.) Args: oss: A file object to write the output to. txn_postings: A list of Posting or directive instances. formatter: An instance of HTMLFormatter, to be render accounts, inventories, links and docs. render_postings: A boolean; if true, render the postings as rows under the main transaction row. \"\"\" write = lambda data: (oss.write(data), oss.write('\\n')) write(''' ''') for row in iterate_html_postings(txn_postings, formatter): entry = row.entry description = row.description if row.links: description += render_links(row.links) # Render a row. write(''' '''.format(row.rowtype, row.extra_class, '{}:{}'.format(entry.meta[\"filename\"], entry.meta[\"lineno\"]), formatter.render_context(entry), entry.date, row.flag, description)) if render_postings and isinstance(entry, data.Transaction): for posting in entry.postings: classes = ['Posting'] if posting.flag == flags.FLAG_WARNING: classes.append('warning') write(''' '''.format(' '.join(classes), posting.flag or '', formatter.render_account(posting.account), posting.units or '', posting.cost or '', posting.price or '', convert.get_weight(posting))) write('
    Date F Narration/Payee Amount Cost Price Balance
    {} {} {}
    {} {} {} {} {} {}
    ') beancount.reports.journal_html.html_entries_table_with_balance(oss, txn_postings, formatter, render_postings=True) \uf0c1 Render a list of entries into an HTML table, with a running balance. (This function returns nothing, it write to oss as a side-effect.) Parameters: oss \u2013 A file object to write the output to. txn_postings \u2013 A list of Posting or directive instances. formatter \u2013 An instance of HTMLFormatter, to be render accounts, inventories, links and docs. render_postings \u2013 A boolean; if true, render the postings as rows under the main transaction row. Source code in beancount/reports/journal_html.py def html_entries_table_with_balance(oss, txn_postings, formatter, render_postings=True): \"\"\"Render a list of entries into an HTML table, with a running balance. (This function returns nothing, it write to oss as a side-effect.) Args: oss: A file object to write the output to. txn_postings: A list of Posting or directive instances. formatter: An instance of HTMLFormatter, to be render accounts, inventories, links and docs. render_postings: A boolean; if true, render the postings as rows under the main transaction row. \"\"\" write = lambda data: (oss.write(data), oss.write('\\n')) write(''' ''') for row in iterate_html_postings(txn_postings, formatter): entry = row.entry description = row.description if row.links: description += render_links(row.links) # Render a row. write(''' '''.format(row.rowtype, row.extra_class, '{}:{}'.format(entry.meta[\"filename\"], entry.meta[\"lineno\"]), formatter.render_context(entry), entry.date, row.flag, description, row.amount_str, row.balance_str)) if render_postings and isinstance(entry, data.Transaction): for posting in entry.postings: classes = ['Posting'] if posting.flag == flags.FLAG_WARNING: classes.append('warning') if posting in row.leg_postings: classes.append('leg') write(''' '''.format(' '.join(classes), posting.flag or '', formatter.render_account(posting.account), position.to_string(posting), posting.price or '', convert.get_weight(posting))) write('
    Date F Narration/Payee Position Price Cost Change Balance
    {} {} {} {} {}
    {} {} {} {} {}
    ') beancount.reports.journal_html.iterate_html_postings(txn_postings, formatter) \uf0c1 Iterate through the list of transactions with rendered HTML strings for each cell. This pre-renders all the data for each row to HTML. This is reused by the entries table rendering routines. Parameters: txn_postings \u2013 A list of TxnPosting or directive instances. formatter \u2013 An instance of HTMLFormatter, to be render accounts, inventories, links and docs. Yields: Instances of Row tuples. See above. Source code in beancount/reports/journal_html.py def iterate_html_postings(txn_postings, formatter): \"\"\"Iterate through the list of transactions with rendered HTML strings for each cell. This pre-renders all the data for each row to HTML. This is reused by the entries table rendering routines. Args: txn_postings: A list of TxnPosting or directive instances. formatter: An instance of HTMLFormatter, to be render accounts, inventories, links and docs. Yields: Instances of Row tuples. See above. \"\"\" for entry_line in realization.iterate_with_balance(txn_postings): entry, leg_postings, change, entry_balance = entry_line # Prepare the data to be rendered for this row. balance_str = formatter.render_inventory(entry_balance) rowtype = entry.__class__.__name__ flag = '' extra_class = '' links = None if isinstance(entry, data.Transaction): rowtype = FLAG_ROWTYPES.get(entry.flag, 'Transaction') extra_class = 'warning' if entry.flag == flags.FLAG_WARNING else '' flag = entry.flag description = '{}'.format(entry.narration) if entry.payee: description = ('{}' '|' '{}').format(entry.payee, description) amount_str = formatter.render_inventory(change) if entry.links and formatter: links = [formatter.render_link(link) for link in entry.links] elif isinstance(entry, data.Balance): # Check the balance here and possibly change the rowtype if entry.diff_amount is None: description = 'Balance {} has {}'.format( formatter.render_account(entry.account), entry.amount) else: description = ('Balance in {} fails; ' 'expected = {}, balance = {}, difference = {}').format( formatter.render_account(entry.account), entry.amount, entry_balance.get_currency_units(entry.amount.currency), entry.diff_amount) extra_class = 'fail' amount_str = formatter.render_amount(entry.amount) elif isinstance(entry, (data.Open, data.Close)): description = '{} {}'.format(entry.__class__.__name__, formatter.render_account(entry.account)) amount_str = '' elif isinstance(entry, data.Note): description = '{} {}'.format(entry.__class__.__name__, entry.comment) amount_str = '' balance_str = '' elif isinstance(entry, data.Document): assert path.isabs(entry.filename) description = 'Document for {}: {}'.format( formatter.render_account(entry.account), formatter.render_doc(entry.filename)) amount_str = '' balance_str = '' else: description = entry.__class__.__name__ amount_str = '' balance_str = '' yield Row(entry, leg_postings, rowtype, extra_class, flag, description, links, amount_str, balance_str) beancount.reports.journal_html.render_links(links) \uf0c1 Render Transaction links to HTML. Parameters: links \u2013 A list of set of strings, transaction \"links\" to be rendered. Returns: A string, a snippet of HTML to be rendering somewhere. Source code in beancount/reports/journal_html.py def render_links(links): \"\"\"Render Transaction links to HTML. Args: links: A list of set of strings, transaction \"links\" to be rendered. Returns: A string, a snippet of HTML to be rendering somewhere. \"\"\" return '{}'.format( ''.join('^'.format(link) for link in links)) beancount.reports.journal_reports \uf0c1 Report classes for all reports that display ending journals of accounts. beancount.reports.journal_reports.ConversionsReport ( HTMLReport ) \uf0c1 Print out a report of all conversions. beancount.reports.journal_reports.DocumentsReport ( HTMLReport ) \uf0c1 Print out a report of documents. beancount.reports.journal_reports.JournalReport ( HTMLReport ) \uf0c1 Print out an account register/journal. beancount.reports.journal_reports.JournalReport.add_args(parser) classmethod \uf0c1 Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/journal_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-a', '--account', action='store', default=None, help=\"Account to render\") parser.add_argument('-w', '--width', action='store', type=int, default=0, help=\"The number of characters wide to render the report to\") parser.add_argument('-k', '--precision', action='store', type=int, default=2, help=\"The number of digits to render after the period\") parser.add_argument('-b', '--render-balance', '--balance', action='store_true', help=\"Render a running balance, not just changes\") parser.add_argument('-c', '--at-cost', '--cost', action='store_true', help=\"Render values at cost, convert the units to cost value\") parser.add_argument('-x', '--compact', dest='verbosity', action='store_const', const=journal_text.COMPACT, default=journal_text.NORMAL, help=\"Rendering compactly\") parser.add_argument('-X', '--verbose', dest='verbosity', action='store_const', const=journal_text.VERBOSE, help=\"Rendering verbosely\") beancount.reports.journal_reports.JournalReport.get_postings(self, real_root) \uf0c1 Return the postings corresponding to the account filter option. Parameters: real_root \u2013 A RealAccount node for the root of all accounts. Returns: A list of posting or directive instances. Source code in beancount/reports/journal_reports.py def get_postings(self, real_root): \"\"\"Return the postings corresponding to the account filter option. Args: real_root: A RealAccount node for the root of all accounts. Returns: A list of posting or directive instances. \"\"\" if self.args.account: real_account = realization.get(real_root, self.args.account) if real_account is None: # If the account isn't found, return an empty list of postings. # Note that this used to return the following error. # raise base.ReportError( # \"Invalid account name: {}\".format(self.args.account)) return [] else: real_account = real_root return realization.get_postings(real_account) beancount.reports.journal_reports.JournalReport.render_real_html(cls, real_root, price_map, price_date, options_map, file) \uf0c1 Wrap an htmldiv into our standard HTML template. Parameters: real_root \u2013 An instance of RealAccount. price_map \u2013 A price database. price_date \u2013 A date for evaluating prices. options_map \u2013 A dict, options as produced by the parser. file \u2013 A file object to write the output to. Source code in beancount/reports/journal_reports.py def render_real_html(cls, real_root, price_map, price_date, options_map, file): \"\"\"Wrap an htmldiv into our standard HTML template. Args: real_root: An instance of RealAccount. price_map: A price database. price_date: A date for evaluating prices. options_map: A dict, options as produced by the parser. file: A file object to write the output to. \"\"\" template = get_html_template() oss = io.StringIO() cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss) file.write(template.format(body=oss.getvalue(), title='')) beancount.reports.journal_text \uf0c1 Text rendering routines for serving a lists of postings/entries. beancount.reports.journal_text.AmountColumnSizer \uf0c1 A class that computes minimal sizes for columns of numbers and their currencies. beancount.reports.journal_text.AmountColumnSizer.get_format(self, precision) \uf0c1 Return a format string for the column of numbers. Parameters: precision \u2013 An integer, the number of digits to render after the period. Returns: A new-style Python format string, with PREFIX_number and PREFIX_currency named fields. Source code in beancount/reports/journal_text.py def get_format(self, precision): \"\"\"Return a format string for the column of numbers. Args: precision: An integer, the number of digits to render after the period. Returns: A new-style Python format string, with PREFIX_number and PREFIX_currency named fields. \"\"\" return ('{{0:>{width:d}.{precision:d}f}} {{1:<{currency_width}}}').format( width=1 + self.get_number_width() + 1 + precision, precision=precision, currency_width=self.max_currency_width) beancount.reports.journal_text.AmountColumnSizer.get_generic_format(self, precision) \uf0c1 Return a generic format string for rendering as wide as required. This can be used to render an empty string in-lieu of a number. Parameters: precision \u2013 An integer, the number of digits to render after the period. Returns: A new-style Python format string, with PREFIX_number and PREFIX_currency named fields. Source code in beancount/reports/journal_text.py def get_generic_format(self, precision): \"\"\"Return a generic format string for rendering as wide as required. This can be used to render an empty string in-lieu of a number. Args: precision: An integer, the number of digits to render after the period. Returns: A new-style Python format string, with PREFIX_number and PREFIX_currency named fields. \"\"\" return '{{{prefix}:<{width}}}'.format( prefix=self.prefix, width=1 + self.get_number_width() + 1 + precision + 1 + self.max_currency_width) beancount.reports.journal_text.AmountColumnSizer.get_number_width(self) \uf0c1 Return the width of the integer part of the max number. Returns: An integer, the number of digits required to render the integral part. Source code in beancount/reports/journal_text.py def get_number_width(self): \"\"\"Return the width of the integer part of the max number. Returns: An integer, the number of digits required to render the integral part. \"\"\" return ((math.floor(math.log10(self.max_number)) + 1) if self.max_number > 0 else 1) beancount.reports.journal_text.AmountColumnSizer.update(self, number, currency) \uf0c1 Update the sizer with the given number and currency. Parameters: number \u2013 A Decimal instance. currency \u2013 A string, the currency to render for it. Source code in beancount/reports/journal_text.py def update(self, number, currency): \"\"\"Update the sizer with the given number and currency. Args: number: A Decimal instance. currency: A string, the currency to render for it. \"\"\" abs_number = abs(number) if abs_number > self.max_number: self.max_number = abs_number currency_width = len(currency) if currency_width > self.max_currency_width: self.max_currency_width = currency_width beancount.reports.journal_text.get_entry_text_description(entry) \uf0c1 Return the text of a description. Parameters: entry \u2013 A directive, of any type. Returns: A string to use for the filling the description field in text reports. Source code in beancount/reports/journal_text.py def get_entry_text_description(entry): \"\"\"Return the text of a description. Args: entry: A directive, of any type. Returns: A string to use for the filling the description field in text reports. \"\"\" if isinstance(entry, data.Transaction): description = ' | '.join([field for field in [entry.payee, entry.narration] if field is not None]) elif isinstance(entry, data.Balance): if entry.diff_amount is None: description = 'PASS - In {}'.format(entry.account) else: description = ('FAIL - In {}; ' 'expected = {}, difference = {}').format( entry.account, entry.amount, entry.diff_amount) elif isinstance(entry, (data.Open, data.Close)): description = entry.account elif isinstance(entry, data.Note): description = entry.comment elif isinstance(entry, data.Document): description = entry.filename else: description = '-' return description beancount.reports.journal_text.render_posting(posting, number_format) \uf0c1 Render a posting compactly, for text report rendering. Parameters: posting \u2013 An instance of Posting. Returns: A string, the rendered posting. Source code in beancount/reports/journal_text.py def render_posting(posting, number_format): \"\"\"Render a posting compactly, for text report rendering. Args: posting: An instance of Posting. Returns: A string, the rendered posting. \"\"\" # Note: there's probably no need to redo the work of rendering here... see # if you can't just simply replace this by Position.to_string(). units = posting.units strings = [ posting.flag if posting.flag else ' ', '{:32}'.format(posting.account), number_format.format(units.number, units.currency) ] cost = posting.cost if cost: strings.append('{{{}}}'.format(number_format.format(cost.number, cost.currency).strip())) price = posting.price if price: strings.append('@ {}'.format(number_format.format(price.number, price.currency).strip())) return ' '.join(strings) beancount.reports.journal_text.size_and_render_amounts(postings, at_cost, render_balance) \uf0c1 Iterate through postings and compute sizers and render amounts. Parameters: postings \u2013 A list of Posting or directive instances. at_cost \u2013 A boolean, if true, render the cost value, not the actual. render_balance \u2013 A boolean, if true, renders a running balance column. Source code in beancount/reports/journal_text.py def size_and_render_amounts(postings, at_cost, render_balance): \"\"\"Iterate through postings and compute sizers and render amounts. Args: postings: A list of Posting or directive instances. at_cost: A boolean, if true, render the cost value, not the actual. render_balance: A boolean, if true, renders a running balance column. \"\"\" # Compute the maximum width required to render the change and balance # columns. In order to carry this out, we will pre-compute all the data to # render this and save it for later. change_sizer = AmountColumnSizer('change') balance_sizer = AmountColumnSizer('balance') entry_data = [] for entry_line in realization.iterate_with_balance(postings): entry, leg_postings, change, balance = entry_line # Convert to cost if necessary. (Note that this agglutinates currencies, # so we'd rather do make the conversion at this level (inventory) than # convert the positions or amounts later.) if at_cost: change = change.reduce(convert.get_cost) if render_balance: balance = balance.reduce(convert.get_cost) # Compute the amounts and maximum widths for the change column. change_amounts = [] for position in change.get_positions(): units = position.units change_amounts.append(units) change_sizer.update(units.number, units.currency) # Compute the amounts and maximum widths for the balance column. balance_amounts = [] if render_balance: for position in balance.get_positions(): units = position.units balance_amounts.append(units) balance_sizer.update(units.number, units.currency) entry_data.append((entry, leg_postings, change_amounts, balance_amounts)) return (entry_data, change_sizer, balance_sizer) beancount.reports.journal_text.text_entries_table(oss, postings, width, at_cost, render_balance, precision, verbosity, output_format) \uf0c1 Render a table of postings or directives with an accumulated balance. This function has three verbosity modes for rendering: 1. COMPACT: no separating line, no postings 2. NORMAL: a separating line between entries, no postings 3. VERBOSE: renders all the postings in addition to normal. The output is written to the 'oss' file object. Nothing is returned. Parameters: oss \u2013 A file object to write the output to. postings \u2013 A list of Posting or directive instances. width \u2013 An integer, the width to render the table to. at_cost \u2013 A boolean, if true, render the cost value, not the actual. render_balance \u2013 A boolean, if true, renders a running balance column. precision \u2013 An integer, the number of digits to render after the period. verbosity \u2013 An integer, the verbosity level. See COMPACT, NORMAL, VERBOSE, etc. output_format \u2013 A string, either 'text' or 'csv' for the chosen output format. This routine's inner loop calculations are complex enough it gets reused by both formats. Exceptions: ValueError \u2013 If the width is insufficient to render the description. Source code in beancount/reports/journal_text.py def text_entries_table(oss, postings, width, at_cost, render_balance, precision, verbosity, output_format): \"\"\"Render a table of postings or directives with an accumulated balance. This function has three verbosity modes for rendering: 1. COMPACT: no separating line, no postings 2. NORMAL: a separating line between entries, no postings 3. VERBOSE: renders all the postings in addition to normal. The output is written to the 'oss' file object. Nothing is returned. Args: oss: A file object to write the output to. postings: A list of Posting or directive instances. width: An integer, the width to render the table to. at_cost: A boolean, if true, render the cost value, not the actual. render_balance: A boolean, if true, renders a running balance column. precision: An integer, the number of digits to render after the period. verbosity: An integer, the verbosity level. See COMPACT, NORMAL, VERBOSE, etc. output_format: A string, either 'text' or 'csv' for the chosen output format. This routine's inner loop calculations are complex enough it gets reused by both formats. Raises: ValueError: If the width is insufficient to render the description. \"\"\" assert output_format in (FORMAT_TEXT, FORMAT_CSV) if output_format is FORMAT_CSV: csv_writer = csv.writer(oss) # Render the changes and balances to lists of amounts and precompute sizes. entry_data, change_sizer, balance_sizer = size_and_render_amounts(postings, at_cost, render_balance) # Render an empty line and compute the width the description should be (the # description is the only elastic field). empty_format = '{{date:10}} {{dirtype:5}} {{description}} {}'.format( change_sizer.get_generic_format(precision)) if render_balance: empty_format += ' {}'.format(balance_sizer.get_generic_format(precision)) empty_line = empty_format.format(date='', dirtype='', description='', change='', balance='') description_width = width - len(empty_line) if description_width <= 0: raise ValueError( \"Width not sufficient to render text report ({} chars)\".format(width)) # Establish a format string for the final format of all lines. # pylint: disable=duplicate-string-formatting-argument line_format = '{{date:10}} {{dirtype:5}} {{description:{:d}.{:d}}} {}'.format( description_width, description_width, change_sizer.get_generic_format(precision)) change_format = change_sizer.get_format(precision) if render_balance: line_format += ' {}'.format(balance_sizer.get_generic_format(precision)) balance_format = balance_sizer.get_format(precision) line_format += '\\n' # Iterate over all the pre-computed data. for (entry, leg_postings, change_amounts, balance_amounts) in entry_data: # Render the date. date = entry.date.isoformat() # Get the directive type name. dirtype = TEXT_SHORT_NAME[type(entry)] if isinstance(entry, data.Transaction) and entry.flag: dirtype = entry.flag # Get the description string and split the description line in multiple # lines. description = get_entry_text_description(entry) description_lines = textwrap.wrap(description, width=description_width) # Ensure at least one line is rendered (for zip_longest). if not description_lines: description_lines.append('') # Render all the amounts in the line. for (description, change_amount, balance_amount) in itertools.zip_longest(description_lines, change_amounts, balance_amounts, fillvalue=''): change = (change_format.format(change_amount.number, change_amount.currency) if change_amount else '') balance = (balance_format.format(balance_amount.number, balance_amount.currency) if balance_amount else '') if not description and verbosity >= VERBOSE and leg_postings: description = '..' if output_format is FORMAT_TEXT: oss.write(line_format.format(date=date, dirtype=dirtype, description=description, change=change, balance=balance)) else: change_number, change_currency = '', '' if change: change_number, change_currency = change.split() if render_balance: balance_number, balance_currency = '', '' if balance: balance_number, balance_currency = balance.split() row = (date, dirtype, description, change_number, change_currency, balance_number, balance_currency) else: row = (date, dirtype, description, change_number, change_currency) csv_writer.writerow(row) # Reset the date, directive type and description. Only the first # line renders these; the other lines render only the amounts. if date: date = dirtype = '' if verbosity >= VERBOSE: for posting in leg_postings: posting_str = render_posting(posting, change_format) if len(posting_str) > description_width: posting_str = posting_str[:description_width-3] + '...' if output_format is FORMAT_TEXT: oss.write(line_format.format(date='', dirtype='', description=posting_str, change='', balance='')) else: row = ('', '', posting_str) csv_writer.writerow(row) if verbosity >= NORMAL: oss.write('\\n') beancount.reports.misc_reports \uf0c1 Miscellaneous report classes. beancount.reports.misc_reports.AccountsReport ( Report ) \uf0c1 Print out the list of all accounts. beancount.reports.misc_reports.ActivityReport ( HTMLReport ) \uf0c1 Render the last or recent update activity. beancount.reports.misc_reports.ActivityReport.add_args(parser) classmethod \uf0c1 Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/misc_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-d', '--cutoff', action='store', default=None, type=date_utils.parse_date_liberally, help=\"Cutoff date where we ignore whatever comes after.\") beancount.reports.misc_reports.ActivityReport.render_real_html(cls, real_root, price_map, price_date, options_map, file) \uf0c1 Wrap an htmldiv into our standard HTML template. Parameters: real_root \u2013 An instance of RealAccount. price_map \u2013 A price database. price_date \u2013 A date for evaluating prices. options_map \u2013 A dict, options as produced by the parser. file \u2013 A file object to write the output to. Source code in beancount/reports/misc_reports.py def render_real_html(cls, real_root, price_map, price_date, options_map, file): \"\"\"Wrap an htmldiv into our standard HTML template. Args: real_root: An instance of RealAccount. price_map: A price database. price_date: A date for evaluating prices. options_map: A dict, options as produced by the parser. file: A file object to write the output to. \"\"\" template = get_html_template() oss = io.StringIO() cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss) file.write(template.format(body=oss.getvalue(), title='')) beancount.reports.misc_reports.CurrentEventsReport ( TableReport ) \uf0c1 Produce a table of the current values of all event types. beancount.reports.misc_reports.CurrentEventsReport.generate_table(self, entries, errors, options_map) \uf0c1 Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/misc_reports.py def generate_table(self, entries, errors, options_map): events = {} for entry in entries: if isinstance(entry, data.Event): events[entry.type] = entry.description return table.create_table(list(sorted(events.items())), [(0, \"Type\", self.formatter.render_event_type), (1, \"Description\")]) beancount.reports.misc_reports.ErrorReport ( HTMLReport ) \uf0c1 Report the errors. beancount.reports.misc_reports.EventsReport ( TableReport ) \uf0c1 Produce a table of all the values of a particular event. beancount.reports.misc_reports.EventsReport.add_args(parser) classmethod \uf0c1 Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/misc_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-e', '--expr', action='store', default=None, help=\"A regexp to filer on which events to display.\") beancount.reports.misc_reports.EventsReport.generate_table(self, entries, errors, options_map) \uf0c1 Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/misc_reports.py def generate_table(self, entries, errors, options_map): event_entries = [] for entry in entries: if not isinstance(entry, data.Event): continue if self.args.expr and not re.match(self.args.expr, entry.type): continue event_entries.append(entry) return table.create_table([(entry.date, entry.type, entry.description) for entry in event_entries], [(0, \"Date\", datetime.date.isoformat), (1, \"Type\"), (2, \"Description\")]) beancount.reports.misc_reports.NoopReport ( Report ) \uf0c1 Report nothing. beancount.reports.misc_reports.PrintReport ( Report ) \uf0c1 Print out the entries. beancount.reports.misc_reports.StatsDirectivesReport ( TableReport ) \uf0c1 Render statistics on each directive type, the number of entries by type. beancount.reports.misc_reports.StatsDirectivesReport.generate_table(self, entries, _, __) \uf0c1 Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/misc_reports.py def generate_table(self, entries, _, __): entries_by_type = misc_utils.groupby(lambda entry: type(entry).__name__, entries) nb_entries_by_type = {name: len(entries) for name, entries in entries_by_type.items()} rows = sorted(nb_entries_by_type.items(), key=lambda x: x[1], reverse=True) rows = [(name, str(count)) for (name, count) in rows] rows.append(('~Total~', str(len(entries)))) return table.create_table(rows, [(0, 'Type'), (1, 'Num Entries', '{:>}'.format)]) beancount.reports.misc_reports.StatsPostingsReport ( TableReport ) \uf0c1 Render the number of postings for each account. beancount.reports.misc_reports.StatsPostingsReport.generate_table(self, entries, _, __) \uf0c1 Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/misc_reports.py def generate_table(self, entries, _, __): all_postings = [posting for entry in entries if isinstance(entry, data.Transaction) for posting in entry.postings] postings_by_account = misc_utils.groupby(lambda posting: posting.account, all_postings) nb_postings_by_account = {key: len(postings) for key, postings in postings_by_account.items()} rows = sorted(nb_postings_by_account.items(), key=lambda x: x[1], reverse=True) rows = [(name, str(count)) for (name, count) in rows] rows.append(('~Total~', str(sum(nb_postings_by_account.values())))) return table.create_table(rows, [(0, 'Account'), (1, 'Num Postings', '{:>}'.format)]) beancount.reports.price_reports \uf0c1 Miscellaneous report classes. beancount.reports.price_reports.CommoditiesReport ( TableReport ) \uf0c1 Print out a list of commodities. beancount.reports.price_reports.CommoditiesReport.generate_table(self, entries, errors, options_map) \uf0c1 Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/price_reports.py def generate_table(self, entries, errors, options_map): price_map = prices.build_price_map(entries) return table.create_table([(base_quote,) for base_quote in sorted(price_map.forward_pairs)], [(0, \"Base/Quote\", self.formatter.render_commodity)]) beancount.reports.price_reports.CommodityLifetimes ( TableReport ) \uf0c1 Print out a list of lifetimes of each commodity. beancount.reports.price_reports.CommodityLifetimes.add_args(parser) classmethod \uf0c1 Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/price_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-c', '--compress-days', type=int, action='store', default=None, help=\"The number of unused days to allow for continuous usage.\") beancount.reports.price_reports.CommodityPricesReport ( TableReport ) \uf0c1 Print all the prices for a particular commodity. beancount.reports.price_reports.CommodityPricesReport.add_args(parser) classmethod \uf0c1 Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/price_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-c', '--commodity', '--currency', action='store', default=None, help=\"The commodity pair to display.\") beancount.reports.price_reports.CommodityPricesReport.generate_table(self, entries, errors, options_map) \uf0c1 Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/price_reports.py def generate_table(self, entries, errors, options_map): date_rates = self.get_date_rates(entries) return table.create_table(date_rates, [(0, \"Date\", datetime.date.isoformat), (1, \"Price\", '{:.5f}'.format)]) beancount.reports.price_reports.PriceDBReport ( Report ) \uf0c1 Print out the normalized price entries from the price db. Normalized means that we print prices in the most common (base, quote) order. This can be used to rebuild a prices database without having to share the entire ledger file. Only the forward prices are printed; which (base, quote) pair is selected is selected based on the most common occurrence between (base, quote) and (quote, base). This is done in the price map. beancount.reports.price_reports.PricesReport ( Report ) \uf0c1 Print out the unnormalized price entries that we input. Unnormalized means that we may render both (base,quote) and (quote,base). This can be used to rebuild a prices database without having to share the entire ledger file. Note: this type of report should be removed once we have filtering on directive type, this is simply the 'print' report with type:price. Maybe rename the 'pricedb' report to just 'prices' for simplicity's sake. beancount.reports.price_reports.TickerReport ( TableReport ) \uf0c1 Print a parseable mapping of (base, quote, ticker, name) for all commodities. beancount.reports.price_reports.TickerReport.generate_table(self, entries, errors, options_map) \uf0c1 Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/price_reports.py def generate_table(self, entries, errors, options_map): commodity_map = getters.get_commodity_map(entries) ticker_info = getters.get_values_meta(commodity_map, 'name', 'ticker', 'quote') price_rows = [ (currency, cost_currency, ticker, name) for currency, (name, ticker, cost_currency) in sorted(ticker_info.items()) if ticker] return table.create_table(price_rows, [(0, \"Currency\"), (1, \"Cost-Currency\"), (2, \"Symbol\"), (3, \"Name\")]) beancount.reports.report \uf0c1 Produce various custom implemented reports. beancount.reports.report.ListFormatsAction ( Action ) \uf0c1 An argparse action that prints all supported formats (for each report). beancount.reports.report.ListReportsAction ( Action ) \uf0c1 An argparse action that just prints the list of reports and exits. beancount.reports.report.get_all_reports() \uf0c1 Return all report classes. Returns: A list of all available report classes. Source code in beancount/reports/report.py def get_all_reports(): \"\"\"Return all report classes. Returns: A list of all available report classes. \"\"\" return functools.reduce(operator.add, map(lambda module: module.__reports__, [balance_reports, journal_reports, holdings_reports, export_reports, price_reports, misc_reports, convert_reports])) beancount.reports.report.get_list_report_string(only_report=None) \uf0c1 Return a formatted string for the list of supported reports. Parameters: only_report \u2013 A string, the name of a single report to produce the help for. If not specified, list all the available reports. Returns: A help string, or None, if 'only_report' was provided and is not a valid report name. Source code in beancount/reports/report.py def get_list_report_string(only_report=None): \"\"\"Return a formatted string for the list of supported reports. Args: only_report: A string, the name of a single report to produce the help for. If not specified, list all the available reports. Returns: A help string, or None, if 'only_report' was provided and is not a valid report name. \"\"\" oss = io.StringIO() num_reports = 0 for report_class in get_all_reports(): # Filter the name if only_report and only_report not in report_class.names: continue # Get the textual description. description = textwrap.fill( re.sub(' +', ' ', ' '.join(report_class.__doc__.splitlines())), initial_indent=\" \", subsequent_indent=\" \", width=80) # Get the report's arguments. parser = version.ArgumentParser() report_ = report_class report_class.add_args(parser) # Get the list of supported formats. ## formats = report_class.get_supported_formats() oss.write('{}:\\n{}\\n'.format(','.join(report_.names), description)) num_reports += 1 if not num_reports: return None return oss.getvalue() beancount.reports.table \uf0c1 Table rendering. beancount.reports.table.Table ( tuple ) \uf0c1 Table(columns, header, body) beancount.reports.table.Table.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/reports/table.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.reports.table.Table.__new__(_cls, columns, header, body) special staticmethod \uf0c1 Create new instance of Table(columns, header, body) beancount.reports.table.Table.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/reports/table.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.reports.table.attribute_to_title(fieldname) \uf0c1 Convert programming id into readable field name. Parameters: fieldname \u2013 A string, a programming ids, such as 'book_value'. Returns: A readable string, such as 'Book Value.' Source code in beancount/reports/table.py def attribute_to_title(fieldname): \"\"\"Convert programming id into readable field name. Args: fieldname: A string, a programming ids, such as 'book_value'. Returns: A readable string, such as 'Book Value.' \"\"\" return fieldname.replace('_', ' ').title() beancount.reports.table.compute_table_widths(rows) \uf0c1 Compute the max character widths of a list of rows. Parameters: rows \u2013 A list of rows, which are sequences of strings. Returns: A list of integers, the maximum widths required to render the columns of this table. Exceptions: IndexError \u2013 If the rows are of different lengths. Source code in beancount/reports/table.py def compute_table_widths(rows): \"\"\"Compute the max character widths of a list of rows. Args: rows: A list of rows, which are sequences of strings. Returns: A list of integers, the maximum widths required to render the columns of this table. Raises: IndexError: If the rows are of different lengths. \"\"\" row_iter = iter(rows) first_row = next(row_iter) num_columns = len(first_row) column_widths = [len(cell) for cell in first_row] for row in row_iter: for i, cell in enumerate(row): if not isinstance(cell, str): cell = str(cell) cell_len = len(cell) if cell_len > column_widths[i]: column_widths[i] = cell_len if i+1 != num_columns: raise IndexError(\"Invalid number of rows\") return column_widths beancount.reports.table.create_table(rows, field_spec=None) \uf0c1 Convert a list of tuples to an table report object. Parameters: rows \u2013 A list of tuples. field_spec \u2013 A list of strings, or a list of (FIELDNAME-OR-INDEX, HEADER, FORMATTER-FUNCTION) triplets, that selects a subset of the fields is to be rendered as well as their ordering. If this is a dict, the values are functions to call on the fields to render them. If a function is set to None, we will just call str() on the field. Returns: A Table instance. Source code in beancount/reports/table.py def create_table(rows, field_spec=None): \"\"\"Convert a list of tuples to an table report object. Args: rows: A list of tuples. field_spec: A list of strings, or a list of (FIELDNAME-OR-INDEX, HEADER, FORMATTER-FUNCTION) triplets, that selects a subset of the fields is to be rendered as well as their ordering. If this is a dict, the values are functions to call on the fields to render them. If a function is set to None, we will just call str() on the field. Returns: A Table instance. \"\"\" # Normalize field_spec to a dict. if field_spec is None: namedtuple_class = type(rows[0]) field_spec = [(field, None, None) for field in namedtuple_class._fields] elif isinstance(field_spec, (list, tuple)): new_field_spec = [] for field in field_spec: if isinstance(field, tuple): assert len(field) <= 3, field if len(field) == 1: field = field[0] new_field_spec.append((field, None, None)) elif len(field) == 2: field, header = field new_field_spec.append((field, header, None)) elif len(field) == 3: new_field_spec.append(field) else: if isinstance(field, str): title = attribute_to_title(field) elif isinstance(field, int): title = \"Field {}\".format(field) else: raise ValueError(\"Invalid type for column name\") new_field_spec.append((field, title, None)) field_spec = new_field_spec # Ensure a nicely formatted header. field_spec = [((name, attribute_to_title(name), formatter) if header_ is None else (name, header_, formatter)) for (name, header_, formatter) in field_spec] assert isinstance(field_spec, list), field_spec assert all(len(x) == 3 for x in field_spec), field_spec # Compute the column names. columns = [name for (name, _, __) in field_spec] # Compute the table header. header = [header_column for (_, header_column, __) in field_spec] # Compute the table body. body = [] for row in rows: body_row = [] for name, _, formatter in field_spec: if isinstance(name, str): value = getattr(row, name) elif isinstance(name, int): value = row[name] else: raise ValueError(\"Invalid type for column name\") if value is not None: if formatter is not None: value = formatter(value) else: value = str(value) else: value = '' body_row.append(value) body.append(body_row) return Table(columns, header, body) beancount.reports.table.render_table(table_, output, output_format, css_id=None, css_class=None) \uf0c1 Render the given table to the output file object in the requested format. The table gets written out to the 'output' file. Parameters: table_ \u2013 An instance of Table. output \u2013 A file object you can write to. output_format \u2013 A string, the format to write the table to, either 'csv', 'txt' or 'html'. css_id \u2013 A string, an optional CSS id for the table object (only used for HTML). css_class \u2013 A string, an optional CSS class for the table object (only used for HTML). Source code in beancount/reports/table.py def render_table(table_, output, output_format, css_id=None, css_class=None): \"\"\"Render the given table to the output file object in the requested format. The table gets written out to the 'output' file. Args: table_: An instance of Table. output: A file object you can write to. output_format: A string, the format to write the table to, either 'csv', 'txt' or 'html'. css_id: A string, an optional CSS id for the table object (only used for HTML). css_class: A string, an optional CSS class for the table object (only used for HTML). \"\"\" if output_format in ('txt', 'text'): text = table_to_text(table_, \" \", formats={'*': '>', 'account': '<'}) output.write(text) elif output_format in ('csv',): table_to_csv(table_, file=output) elif output_format in ('htmldiv', 'html'): if output_format == 'html': output.write('\\n') output.write('\\n') output.write('
    \\n'.format(css_id) if css_id else '
    \\n') classes = [css_class] if css_class else None table_to_html(table_, file=output, classes=classes) output.write('
    \\n') if output_format == 'html': output.write('\\n') output.write('\\n') else: raise NotImplementedError(\"Unsupported format: {}\".format(output_format)) beancount.reports.table.table_to_csv(table, file=None, **kwargs) \uf0c1 Render a Table to a CSV file. Parameters: table \u2013 An instance of a Table. file \u2013 A file object to write to. If no object is provided, this function returns a string. **kwargs \u2013 Optional arguments forwarded to csv.writer(). Returns: A string, the rendered table, or None, if a file object is provided to write to. Source code in beancount/reports/table.py def table_to_csv(table, file=None, **kwargs): \"\"\"Render a Table to a CSV file. Args: table: An instance of a Table. file: A file object to write to. If no object is provided, this function returns a string. **kwargs: Optional arguments forwarded to csv.writer(). Returns: A string, the rendered table, or None, if a file object is provided to write to. \"\"\" output_file = file or io.StringIO() writer = csv.writer(output_file, **kwargs) if table.header: writer.writerow(table.header) writer.writerows(table.body) if not file: return output_file.getvalue() beancount.reports.table.table_to_html(table, classes=None, file=None) \uf0c1 Render a Table to HTML. Parameters: table \u2013 An instance of a Table. classes \u2013 A list of string, CSS classes to set on the table. file \u2013 A file object to write to. If no object is provided, this function returns a string. Returns: A string, the rendered table, or None, if a file object is provided to write to. Source code in beancount/reports/table.py def table_to_html(table, classes=None, file=None): \"\"\"Render a Table to HTML. Args: table: An instance of a Table. classes: A list of string, CSS classes to set on the table. file: A file object to write to. If no object is provided, this function returns a string. Returns: A string, the rendered table, or None, if a file object is provided to write to. \"\"\" # Initialize file. oss = io.StringIO() if file is None else file oss.write('\\n'.format(' '.join(classes or []))) # Render header. if table.header: oss.write(' \\n') oss.write(' \\n') for header in table.header: oss.write(' \\n'.format(header)) oss.write(' \\n') oss.write(' \\n') # Render body. oss.write(' \\n') for row in table.body: oss.write(' \\n') for cell in row: oss.write(' \\n'.format(cell)) oss.write(' \\n') oss.write(' \\n') # Render footer. oss.write('
    {}
    {}
    \\n') if file is None: return oss.getvalue() beancount.reports.table.table_to_text(table, column_interspace=' ', formats=None) \uf0c1 Render a Table to ASCII text. Parameters: table \u2013 An instance of a Table. column_interspace \u2013 A string to render between the columns as spacer. formats \u2013 An optional dict of column name to a format character that gets inserted in a format string specified, like this (where '' is): {:}. A key of ' ' will provide a default value, like this, for example: (... formats={' ': '>'}). Returns: A string, the rendered text table. Source code in beancount/reports/table.py def table_to_text(table, column_interspace=\" \", formats=None): \"\"\"Render a Table to ASCII text. Args: table: An instance of a Table. column_interspace: A string to render between the columns as spacer. formats: An optional dict of column name to a format character that gets inserted in a format string specified, like this (where '' is): {:}. A key of '*' will provide a default value, like this, for example: (... formats={'*': '>'}). Returns: A string, the rendered text table. \"\"\" column_widths = compute_table_widths(itertools.chain([table.header], table.body)) # Insert column format chars and compute line formatting string. column_formats = [] if formats: default_format = formats.get('*', None) for column, width in zip(table.columns, column_widths): if column and formats: format_ = formats.get(column, default_format) if format_: column_formats.append(\"{{:{}{:d}}}\".format(format_, width)) else: column_formats.append(\"{{:{:d}}}\".format(width)) else: column_formats.append(\"{{:{:d}}}\".format(width)) line_format = column_interspace.join(column_formats) + \"\\n\" separator = line_format.format(*[('-' * width) for width in column_widths]) # Render the header. oss = io.StringIO() if table.header: oss.write(line_format.format(*table.header)) # Render the body. oss.write(separator) for row in table.body: oss.write(line_format.format(*row)) oss.write(separator) return oss.getvalue() beancount.reports.tree_table \uf0c1 Routines to render an HTML table with a tree of accounts. beancount.reports.tree_table.is_account_active(real_account) \uf0c1 Return true if the account should be rendered. An active account has at least one directive that is not an Open directive. Parameters: real_account \u2013 An instance of RealAccount. Returns: A boolean, true if the account is active, according to the definition above. Source code in beancount/reports/tree_table.py def is_account_active(real_account): \"\"\"Return true if the account should be rendered. An active account has at least one directive that is not an Open directive. Args: real_account: An instance of RealAccount. Returns: A boolean, true if the account is active, according to the definition above. \"\"\" for entry in real_account.txn_postings: if isinstance(entry, data.Open): continue return True return False beancount.reports.tree_table.table_of_balances(real_root, price_map, price_date, operating_currencies, formatter, classes=None) \uf0c1 Render a tree table with the balance of each accounts. Parameters: real_root \u2013 A RealAccount node, the root node to render. price_map \u2013 A prices map, a built by build_price_map. price_date \u2013 A datetime.date instance, the date at which to compute market value. operating_currencies \u2013 A list of strings, the operating currencies to render to their own dedicated columns. formatter \u2013 A object used to render account names and other links. classes \u2013 A list of strings, the CSS classes to attach to the rendered top-level table object. Returns: A string with HTML contents, the rendered table. Source code in beancount/reports/tree_table.py def table_of_balances(real_root, price_map, price_date, operating_currencies, formatter, classes=None): \"\"\"Render a tree table with the balance of each accounts. Args: real_root: A RealAccount node, the root node to render. price_map: A prices map, a built by build_price_map. price_date: A datetime.date instance, the date at which to compute market value. operating_currencies: A list of strings, the operating currencies to render to their own dedicated columns. formatter: A object used to render account names and other links. classes: A list of strings, the CSS classes to attach to the rendered top-level table object. Returns: A string with HTML contents, the rendered table. \"\"\" header = ['Account'] + operating_currencies + ['Other'] # Pre-calculate which accounts should be rendered. real_active = realization.filter(real_root, is_account_active) if real_active: active_set = {real_account.account for real_account in realization.iter_children(real_active)} else: active_set = set() balance_totals = inventory.Inventory() oss = io.StringIO() classes = list(classes) if classes else [] classes.append('fullwidth') for real_account, cells, row_classes in tree_table(oss, real_root, formatter, header, classes): if real_account is TOTALS_LINE: line_balance = balance_totals row_classes.append('totals') else: # Check if this account has had activity; if not, skip rendering it. if (real_account.account not in active_set and not account_types.is_root_account(real_account.account)): continue if real_account.account is None: row_classes.append('parent-node') # For each account line, get the final balance of the account at # latest market value. line_balance = real_account.balance.reduce(convert.get_value, price_map) # Update the total balance for the totals line. balance_totals.add_inventory(line_balance) # Extract all the positions that the user has identified as operating # currencies to their own subinventories. ccy_dict = line_balance.segregate_units(operating_currencies) # FIXME: This little algorithm is inefficient; rewrite it. for currency in operating_currencies: units = ccy_dict[currency].get_currency_units(currency) cells.append(formatter.render_number(units.number, units.currency) if units.number != ZERO else '') # Render all the rest of the inventory in the last cell. if None in ccy_dict: ccy_balance = ccy_dict[None] last_cell = '
    '.join(formatter.render_amount(pos.units) for pos in sorted(ccy_balance)) else: last_cell = '' cells.append(last_cell) return oss.getvalue() beancount.reports.tree_table.tree_table(oss, real_account, formatter, header=None, classes=None) \uf0c1 Generator to a tree of accounts as an HTML table. This yields each real_account object in turn and a list object used to provide the values for the various columns to render. Parameters: oss \u2013 a io.StringIO instance, into which we will render the HTML. real_account \u2013 an instance of a RealAccount node. formatter \u2013 A object used to render account names and other links. header \u2013 a list of header columns to render. The first column is special, and is used for the account name. classes \u2013 a list of CSS class strings to apply to the table element. Returns: A generator of tuples of real_account \u2013 An instance of RealAccount to render as a row cells: A mutable list object to accumulate values for each column you want to render. row_classes: A mutable list object to accumulate css classes that you want to add for the row. You need to append to the given 'cells' object; if you don't append anything, this tells this routine to skip rendering the row. On the very last line, the 'real_account' object will be a special sentinel value to indicate that it is meant to render the totals line: TOTALS_LINE. Source code in beancount/reports/tree_table.py def tree_table(oss, real_account, formatter, header=None, classes=None): \"\"\"Generator to a tree of accounts as an HTML table. This yields each real_account object in turn and a list object used to provide the values for the various columns to render. Args: oss: a io.StringIO instance, into which we will render the HTML. real_account: an instance of a RealAccount node. formatter: A object used to render account names and other links. header: a list of header columns to render. The first column is special, and is used for the account name. classes: a list of CSS class strings to apply to the table element. Returns: A generator of tuples of real_account: An instance of RealAccount to render as a row cells: A mutable list object to accumulate values for each column you want to render. row_classes: A mutable list object to accumulate css classes that you want to add for the row. You need to append to the given 'cells' object; if you don't append anything, this tells this routine to skip rendering the row. On the very last line, the 'real_account' object will be a special sentinel value to indicate that it is meant to render the totals line: TOTALS_LINE. \"\"\" write = lambda data: (oss.write(data), oss.write('\\n')) classes = list(classes) if classes else [] classes.append('tree-table') write(''.format(' '.join(classes) if classes else '')) if header: write('') write('') header_iter = iter(header) write(''.format(next(header_iter))) for column in header_iter: write(''.format(column)) write('') write('') # Note: This code eventually should be reworked to be agnostic regarding # text or HTML output rendering. lines = realization.dump(real_account) # Yield with a None for the final line. lines.append((None, None, TOTALS_LINE)) for first_line, unused_cont_line, real_acc in lines: # Let the caller fill in the data to be rendered by adding it to a list # objects. The caller may return multiple cell values; this will create # multiple columns. cells = [] row_classes = [] yield real_acc, cells, row_classes # If no cells were added, skip the line. If you want to render empty # cells, append empty strings. if not cells: continue # Render the row write(''.format(' '.join(row_classes))) if real_acc is TOTALS_LINE: label = '' else: label = (formatter.render_account(real_acc.account) if formatter else real_acc.account) write(''.format(label)) # Add columns for each value rendered. for cell in cells: write(''.format(cell)) write('') write('
    {}{}
    {}{}
    ')","title":"beancount.reports"},{"location":"api_reference/beancount.reports.html#beancountreports","text":"Routines to produce various reports, either to HTML or to text.","title":"beancount.reports"},{"location":"api_reference/beancount.reports.html#beancount.reports.balance_reports","text":"Report classes for all reports that display ending balances of accounts.","title":"balance_reports"},{"location":"api_reference/beancount.reports.html#beancount.reports.balance_reports.BalanceSheetReport","text":"Print out a balance sheet.","title":"BalanceSheetReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.balance_reports.BalanceSheetReport.render_real_html","text":"Wrap an htmldiv into our standard HTML template. Parameters: real_root \u2013 An instance of RealAccount. price_map \u2013 A price database. price_date \u2013 A date for evaluating prices. options_map \u2013 A dict, options as produced by the parser. file \u2013 A file object to write the output to. Source code in beancount/reports/balance_reports.py def render_real_html(cls, real_root, price_map, price_date, options_map, file): \"\"\"Wrap an htmldiv into our standard HTML template. Args: real_root: An instance of RealAccount. price_map: A price database. price_date: A date for evaluating prices. options_map: A dict, options as produced by the parser. file: A file object to write the output to. \"\"\" template = get_html_template() oss = io.StringIO() cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss) file.write(template.format(body=oss.getvalue(), title=''))","title":"render_real_html()"},{"location":"api_reference/beancount.reports.html#beancount.reports.balance_reports.BalancesReport","text":"Print out the trial balance of accounts matching an expression.","title":"BalancesReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.balance_reports.BalancesReport.add_args","text":"Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/balance_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-e', '--filter-expression', '--expression', '--regexp', action='store', default=None, help=\"Filter expression for which account balances to display.\") parser.add_argument('-c', '--at-cost', '--cost', action='store_true', help=\"Render values at cost, convert the units to cost value\")","title":"add_args()"},{"location":"api_reference/beancount.reports.html#beancount.reports.balance_reports.BalancesReport.render_real_html","text":"Wrap an htmldiv into our standard HTML template. Parameters: real_root \u2013 An instance of RealAccount. price_map \u2013 A price database. price_date \u2013 A date for evaluating prices. options_map \u2013 A dict, options as produced by the parser. file \u2013 A file object to write the output to. Source code in beancount/reports/balance_reports.py def render_real_html(cls, real_root, price_map, price_date, options_map, file): \"\"\"Wrap an htmldiv into our standard HTML template. Args: real_root: An instance of RealAccount. price_map: A price database. price_date: A date for evaluating prices. options_map: A dict, options as produced by the parser. file: A file object to write the output to. \"\"\" template = get_html_template() oss = io.StringIO() cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss) file.write(template.format(body=oss.getvalue(), title=''))","title":"render_real_html()"},{"location":"api_reference/beancount.reports.html#beancount.reports.balance_reports.IncomeStatementReport","text":"Print out an income statement.","title":"IncomeStatementReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.balance_reports.IncomeStatementReport.render_real_html","text":"Wrap an htmldiv into our standard HTML template. Parameters: real_root \u2013 An instance of RealAccount. price_map \u2013 A price database. price_date \u2013 A date for evaluating prices. options_map \u2013 A dict, options as produced by the parser. file \u2013 A file object to write the output to. Source code in beancount/reports/balance_reports.py def render_real_html(cls, real_root, price_map, price_date, options_map, file): \"\"\"Wrap an htmldiv into our standard HTML template. Args: real_root: An instance of RealAccount. price_map: A price database. price_date: A date for evaluating prices. options_map: A dict, options as produced by the parser. file: A file object to write the output to. \"\"\" template = get_html_template() oss = io.StringIO() cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss) file.write(template.format(body=oss.getvalue(), title=''))","title":"render_real_html()"},{"location":"api_reference/beancount.reports.html#beancount.reports.base","text":"Base class for all reports classes. Each report class should be able to render a filtered list of entries to a variety of formats. Each report has a name, some command-line options, and supports some subset of formats.","title":"base"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.HTMLReport","text":"A mixin for reports that support forwarding html to htmldiv implementation.","title":"HTMLReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.RealizationMeta","text":"A metaclass for reports that render a realization. The main use of this metaclass is to allow us to create report classes with render_real_*() methods that accept a RealAccount instance as the basis for producing a report. RealAccount can be expensive to build, and may be pre-computed and kept around to generate the various reports related to a particular filter of a subset of transactions, and it would be inconvenient to have to recalculate it every time we need to produce a report. In particular, this is the case for the web interface: the user selects a particular subset of transactions to view, and can then click to the various reports related to this subset of transactions. This is why this is useful. The classes generated with this metaclass respond to the same interface as the regular report classes, so that if invoked from the command-line, it will automatically build the realization from the given set of entries. This metaclass looks at the class' existing render_real_ () methods and generate the corresponding render_ () methods automatically.","title":"RealizationMeta"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.RealizationMeta.__new__","text":"Create and return a new object. See help(type) for accurate signature. Source code in beancount/reports/base.py def __new__(mcs, name, bases, namespace): new_type = super(RealizationMeta, mcs).__new__(mcs, name, bases, namespace) # Go through the methods of the new type and look for render_real() methods. new_methods = {} for attr, value in new_type.__dict__.items(): match = re.match('render_real_(.*)', attr) if not match: continue # Make sure that if an explicit version of render_*() has already # been declared, that we don't override it. render_function_name = 'render_{}'.format(match.group(1)) if render_function_name in new_type.__dict__: continue # Define a render_*() method on the class. def forward_method(self, entries, errors, options_map, file, fwdfunc=value): account_types = options.get_account_types(options_map) real_root = realization.realize(entries, account_types) price_map = prices.build_price_map(entries) # Note: When we forward, use the latest date (None). return fwdfunc(self, real_root, price_map, None, options_map, file) forward_method.__name__ = render_function_name new_methods[render_function_name] = forward_method # Update the type with the newly defined methods.. for mname, mvalue in new_methods.items(): setattr(new_type, mname, mvalue) # Auto-generate other methods if necessary. if hasattr(new_type, 'render_real_htmldiv'): setattr(new_type, 'render_real_html', mcs.render_real_html) return new_type","title":"__new__()"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.RealizationMeta.render_real_html","text":"Wrap an htmldiv into our standard HTML template. Parameters: real_root \u2013 An instance of RealAccount. price_map \u2013 A price database. price_date \u2013 A date for evaluating prices. options_map \u2013 A dict, options as produced by the parser. file \u2013 A file object to write the output to. Source code in beancount/reports/base.py def render_real_html(cls, real_root, price_map, price_date, options_map, file): \"\"\"Wrap an htmldiv into our standard HTML template. Args: real_root: An instance of RealAccount. price_map: A price database. price_date: A date for evaluating prices. options_map: A dict, options as produced by the parser. file: A file object to write the output to. \"\"\" template = get_html_template() oss = io.StringIO() cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss) file.write(template.format(body=oss.getvalue(), title=''))","title":"render_real_html()"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.Report","text":"Base class for all reports. Attributes: Name Type Description names A list of strings, the various names of this report. The first name is taken to be the authoritative name of the report; the rest are considered aliases. parser The parser for the command's arguments. This is used to raise errors. args An object that contains the values of this command's parsed arguments.","title":"Report"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.Report.__call__","text":"Render a report of filtered entries to any format. This function dispatches to a specific method. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. output_format \u2013 A string, the name of the format. If not specified, use the default format. file \u2013 The file to write the output to. Returns: If no 'file' is provided, return the contents of the report as a string. Exceptions: ReportError \u2013 If the requested format is not supported. Source code in beancount/reports/base.py def render(self, entries, errors, options_map, output_format=None, file=None): \"\"\"Render a report of filtered entries to any format. This function dispatches to a specific method. Args: entries: A list of directives to render. errors: A list of errors that occurred during processing. options_map: A dict of options, as produced by the parser. output_format: A string, the name of the format. If not specified, use the default format. file: The file to write the output to. Returns: If no 'file' is provided, return the contents of the report as a string. Raises: ReportError: If the requested format is not supported. \"\"\" try: render_method = getattr(self, 'render_{}'.format(output_format or self.default_format)) except AttributeError: raise ReportError(\"Unsupported format: '{}'\".format(output_format)) outfile = io.StringIO() if file is None else file result = render_method(entries, errors, options_map, outfile) assert result is None, \"Render method must write to file.\" if file is None: return outfile.getvalue()","title":"__call__()"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.Report.add_args","text":"Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/base.py @classmethod def add_args(cls, parser): \"\"\"Add arguments to parse for this report. Args: parser: An instance of argparse.ArgumentParser. \"\"\" # No-op.","title":"add_args()"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.Report.from_args","text":"A convenience method used to create an instance from arguments. This creates an instance of the report with default arguments. This is a convenience that may be used for tests. Our actual script uses subparsers and invokes add_args() and creates an appropriate instance directly. Parameters: argv \u2013 A list of strings, command-line arguments to use to construct the report. kwds \u2013 A dict of other keyword arguments to pass to the report's constructor. Returns: A new instance of the report. Source code in beancount/reports/base.py @classmethod def from_args(cls, argv=None, **kwds): \"\"\"A convenience method used to create an instance from arguments. This creates an instance of the report with default arguments. This is a convenience that may be used for tests. Our actual script uses subparsers and invokes add_args() and creates an appropriate instance directly. Args: argv: A list of strings, command-line arguments to use to construct the report. kwds: A dict of other keyword arguments to pass to the report's constructor. Returns: A new instance of the report. \"\"\" parser = version.ArgumentParser() cls.add_args(parser) return cls(parser.parse_args(argv or []), parser, **kwds)","title":"from_args()"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.Report.get_supported_formats","text":"Enumerates the list of supported formats, by inspecting methods of this object. Returns: A list of strings, such as ['html', 'text']. Source code in beancount/reports/base.py @classmethod def get_supported_formats(cls): \"\"\"Enumerates the list of supported formats, by inspecting methods of this object. Returns: A list of strings, such as ['html', 'text']. \"\"\" formats = [] for name in dir(cls): match = re.match('render_([a-z0-9]+)$', name) if match: formats.append(match.group(1)) return sorted(formats)","title":"get_supported_formats()"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.Report.render","text":"Render a report of filtered entries to any format. This function dispatches to a specific method. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. output_format \u2013 A string, the name of the format. If not specified, use the default format. file \u2013 The file to write the output to. Returns: If no 'file' is provided, return the contents of the report as a string. Exceptions: ReportError \u2013 If the requested format is not supported. Source code in beancount/reports/base.py def render(self, entries, errors, options_map, output_format=None, file=None): \"\"\"Render a report of filtered entries to any format. This function dispatches to a specific method. Args: entries: A list of directives to render. errors: A list of errors that occurred during processing. options_map: A dict of options, as produced by the parser. output_format: A string, the name of the format. If not specified, use the default format. file: The file to write the output to. Returns: If no 'file' is provided, return the contents of the report as a string. Raises: ReportError: If the requested format is not supported. \"\"\" try: render_method = getattr(self, 'render_{}'.format(output_format or self.default_format)) except AttributeError: raise ReportError(\"Unsupported format: '{}'\".format(output_format)) outfile = io.StringIO() if file is None else file result = render_method(entries, errors, options_map, outfile) assert result is None, \"Render method must write to file.\" if file is None: return outfile.getvalue()","title":"render()"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.ReportError","text":"Error that occurred during report generation.","title":"ReportError"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.TableReport","text":"A base class for reports that supports automatic conversions from Table.","title":"TableReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.TableReport.generate_table","text":"Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/base.py def generate_table(self, entries, errors, options_map): \"\"\"Render the report to a Table instance. Args: entries: A list of directives to render. errors: A list of errors that occurred during processing. options_map: A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. \"\"\" raise NotImplementedError","title":"generate_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.base.get_html_template","text":"Returns our vanilla HTML template for embedding an HTML div. Returns: A string, with a formatting style placeholders \u2013 {title}: for the title of the page. {body}: for the body, where the div goes. Source code in beancount/reports/base.py def get_html_template(): \"\"\"Returns our vanilla HTML template for embedding an HTML div. Returns: A string, with a formatting style placeholders: {title}: for the title of the page. {body}: for the body, where the div goes. \"\"\" with open(path.join(path.dirname(__file__), 'template.html')) as infile: return infile.read()","title":"get_html_template()"},{"location":"api_reference/beancount.reports.html#beancount.reports.context","text":"Produce a rendering of the account balances just before and after a particular entry is applied.","title":"context"},{"location":"api_reference/beancount.reports.html#beancount.reports.context.render_entry_context","text":"Render the context before and after a particular transaction is applied. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. entry \u2013 The entry instance which should be rendered. (Note that this object is expected to be in the set of entries, not just structurally equal.) Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. Source code in beancount/reports/context.py def render_entry_context(entries, options_map, entry): \"\"\"Render the context before and after a particular transaction is applied. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. entry: The entry instance which should be rendered. (Note that this object is expected to be in the set of entries, not just structurally equal.) Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. \"\"\" oss = io.StringIO() meta = entry.meta print(\"Hash:{}\".format(compare.hash_entry(entry)), file=oss) print(\"Location: {}:{}\".format(meta[\"filename\"], meta[\"lineno\"]), file=oss) # Get the list of accounts sorted by the order in which they appear in the # closest entry. order = {} if isinstance(entry, data.Transaction): order = {posting.account: index for index, posting in enumerate(entry.postings)} accounts = sorted(getters.get_entry_accounts(entry), key=lambda account: order.get(account, 10000)) # Accumulate the balances of these accounts up to the entry. balance_before, balance_after = interpolate.compute_entry_context(entries, entry) # Create a format line for printing the contents of account balances. max_account_width = max(map(len, accounts)) if accounts else 1 position_line = '{{:1}} {{:{width}}} {{:>49}}'.format(width=max_account_width) # Print the context before. print(file=oss) print(\"------------ Balances before transaction\", file=oss) print(file=oss) before_hashes = set() for account in accounts: positions = balance_before[account].get_positions() for position in positions: before_hashes.add((account, hash(position))) print(position_line.format('', account, str(position)), file=oss) if not positions: print(position_line.format('', account, ''), file=oss) print(file=oss) # Print the entry itself. print(file=oss) print(\"------------ Transaction\", file=oss) print(file=oss) dcontext = options_map['dcontext'] printer.print_entry(entry, dcontext, render_weights=True, file=oss) if isinstance(entry, data.Transaction): print(file=oss) # Print residuals. residual = interpolate.compute_residual(entry.postings) if not residual.is_empty(): # Note: We render the residual at maximum precision, for debugging. print('Residual: {}'.format(residual), file=oss) # Dump the tolerances used. tolerances = interpolate.infer_tolerances(entry.postings, options_map) if tolerances: print('Tolerances: {}'.format( ', '.join('{}={}'.format(key, value) for key, value in sorted(tolerances.items()))), file=oss) # Compute the total cost basis. cost_basis = inventory.Inventory( pos for pos in entry.postings if pos.cost is not None ).reduce(convert.get_cost) if not cost_basis.is_empty(): print('Basis: {}'.format(cost_basis), file=oss) # Print the context after. print(file=oss) print(\"------------ Balances after transaction\", file=oss) print(file=oss) for account in accounts: positions = balance_after[account].get_positions() for position in positions: changed = (account, hash(position)) not in before_hashes print(position_line.format('*' if changed else '', account, str(position)), file=oss) if not positions: print(position_line.format('', account, ''), file=oss) print(file=oss) return oss.getvalue()","title":"render_entry_context()"},{"location":"api_reference/beancount.reports.html#beancount.reports.context.render_file_context","text":"Render the context before and after a particular transaction is applied. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. filename \u2013 A string, the name of the file from which the transaction was parsed. lineno \u2013 An integer, the line number in the file the transaction was parsed from. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. Source code in beancount/reports/context.py def render_file_context(entries, options_map, filename, lineno): \"\"\"Render the context before and after a particular transaction is applied. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. filename: A string, the name of the file from which the transaction was parsed. lineno: An integer, the line number in the file the transaction was parsed from. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. \"\"\" # Find the closest entry. closest_entry = data.find_closest(entries, filename, lineno) if closest_entry is None: raise SystemExit(\"No entry could be found before {}:{}\".format(filename, lineno)) return render_entry_context(entries, options_map, closest_entry)","title":"render_file_context()"},{"location":"api_reference/beancount.reports.html#beancount.reports.convert_reports","text":"Format converter reports. This module contains reports that can convert an input file into other formats, such as Ledger.","title":"convert_reports"},{"location":"api_reference/beancount.reports.html#beancount.reports.convert_reports.HLedgerPrinter","text":"Multi-method for printing directives in HLedger format.","title":"HLedgerPrinter"},{"location":"api_reference/beancount.reports.html#beancount.reports.convert_reports.HLedgerReport","text":"Print out the entries in a format that can be parsed by HLedger.","title":"HLedgerReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.convert_reports.LedgerPrinter","text":"Multi-method for printing directives in Ledger format.","title":"LedgerPrinter"},{"location":"api_reference/beancount.reports.html#beancount.reports.convert_reports.LedgerReport","text":"Print out the entries in a format that can be parsed by Ledger.","title":"LedgerReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.convert_reports.postings_by_type","text":"Split up the postings by simple, at-cost, at-price. Parameters: entry \u2013 An instance of Transaction. Returns: A tuple of simple postings, postings with price conversions, postings held at cost. Source code in beancount/reports/convert_reports.py def postings_by_type(entry): \"\"\"Split up the postings by simple, at-cost, at-price. Args: entry: An instance of Transaction. Returns: A tuple of simple postings, postings with price conversions, postings held at cost. \"\"\" postings_at_cost = [] postings_at_price = [] postings_simple = [] for posting in entry.postings: if posting.cost: accumlator = postings_at_cost elif posting.price: accumlator = postings_at_price else: accumlator = postings_simple accumlator.append(posting) return (postings_simple, postings_at_price, postings_at_cost)","title":"postings_by_type()"},{"location":"api_reference/beancount.reports.html#beancount.reports.convert_reports.quote","text":"Add quotes around a re.MatchObject. Parameters: match \u2013 A MatchObject from the re module. Returns: A quoted string of the match contents. Source code in beancount/reports/convert_reports.py def quote(match): \"\"\"Add quotes around a re.MatchObject. Args: match: A MatchObject from the re module. Returns: A quoted string of the match contents. \"\"\" currency = match.group(1) return '\"{}\"'.format(currency) if re.search(r'[0-9\\.]', currency) else currency","title":"quote()"},{"location":"api_reference/beancount.reports.html#beancount.reports.convert_reports.quote_currency","text":"Quote all the currencies with numbers from the given string. Parameters: string \u2013 A string of text. Returns: A string of text, with the commodity expressions surrounded with quotes. Source code in beancount/reports/convert_reports.py def quote_currency(string): \"\"\"Quote all the currencies with numbers from the given string. Args: string: A string of text. Returns: A string of text, with the commodity expressions surrounded with quotes. \"\"\" return re.sub(r'\\b({})\\b'.format(amount.CURRENCY_RE), quote, string)","title":"quote_currency()"},{"location":"api_reference/beancount.reports.html#beancount.reports.convert_reports.split_currency_conversions","text":"If the transaction has a mix of conversion at cost and a currency conversion, split the transaction into two transactions: one that applies the currency conversion in the same account, and one that uses the other currency without conversion. This is required because Ledger does not appear to be able to grok a transaction like this one: 2014-11-02 * \"Buy some stock with foreign currency funds\" Assets:CA:Investment:HOOL 5 HOOL {520.0 USD} Expenses:Commissions 9.95 USD Assets:CA:Investment:Cash -2939.46 CAD @ 0.8879 USD HISTORICAL NOTE: Adding a price directive on the first posting above makes Ledger accept the transaction. So we will not split the transaction here now. However, since Ledger's treatment of this type of conflict is subject to revision (See http://bugs.ledger-cli.org/show_bug.cgi?id=630), we will keep this code around, it might become useful eventually. See https://groups.google.com/d/msg/ledger-cli/35hA0Dvhom0/WX8gY_5kHy0J for details of the discussion. Parameters: entry \u2013 An instance of Transaction. Returns: A pair of converted \u2013 boolean, true if a conversion was made. entries: A list of the original entry if converted was False, or a list of the split converted entries if True. Source code in beancount/reports/convert_reports.py def split_currency_conversions(entry): \"\"\"If the transaction has a mix of conversion at cost and a currency conversion, split the transaction into two transactions: one that applies the currency conversion in the same account, and one that uses the other currency without conversion. This is required because Ledger does not appear to be able to grok a transaction like this one: 2014-11-02 * \"Buy some stock with foreign currency funds\" Assets:CA:Investment:HOOL 5 HOOL {520.0 USD} Expenses:Commissions 9.95 USD Assets:CA:Investment:Cash -2939.46 CAD @ 0.8879 USD HISTORICAL NOTE: Adding a price directive on the first posting above makes Ledger accept the transaction. So we will not split the transaction here now. However, since Ledger's treatment of this type of conflict is subject to revision (See http://bugs.ledger-cli.org/show_bug.cgi?id=630), we will keep this code around, it might become useful eventually. See https://groups.google.com/d/msg/ledger-cli/35hA0Dvhom0/WX8gY_5kHy0J for details of the discussion. Args: entry: An instance of Transaction. Returns: A pair of converted: boolean, true if a conversion was made. entries: A list of the original entry if converted was False, or a list of the split converted entries if True. \"\"\" assert isinstance(entry, data.Transaction) (postings_simple, postings_at_price, postings_at_cost) = postings_by_type(entry) converted = postings_at_cost and postings_at_price if converted: # Generate a new entry for each currency conversion. new_entries = [] replacement_postings = [] for posting_orig in postings_at_price: weight = convert.get_weight(posting_orig) posting_pos = data.Posting(posting_orig.account, weight, None, None, None, None) posting_neg = data.Posting(posting_orig.account, -weight, None, None, None, None) currency_entry = entry._replace( postings=[posting_orig, posting_neg], narration=entry.narration + ' (Currency conversion)') new_entries.append(currency_entry) replacement_postings.append(posting_pos) converted_entry = entry._replace(postings=( postings_at_cost + postings_simple + replacement_postings)) new_entries.append(converted_entry) else: new_entries = [entry] return converted, new_entries","title":"split_currency_conversions()"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports","text":"Reports to Export to third-party portfolio sites.","title":"export_reports"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports.ExportEntry","text":"ExportEntry(symbol, cost_currency, number, cost_number, mutual_fund, memo, holdings)","title":"ExportEntry"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports.ExportEntry.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/reports/export_reports.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports.ExportEntry.__new__","text":"Create new instance of ExportEntry(symbol, cost_currency, number, cost_number, mutual_fund, memo, holdings)","title":"__new__()"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports.ExportEntry.__repr__","text":"Return a nicely formatted representation string Source code in beancount/reports/export_reports.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports.ExportPortfolioReport","text":"Holdings lists that can be exported to external portfolio management software.","title":"ExportPortfolioReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports.ExportPortfolioReport.add_args","text":"Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/export_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-d', '--debug', action='store_true', help=\"Output position export debugging information on stderr.\") parser.add_argument('-p', '--promiscuous', action='store_true', help=(\"Include title and account names in memos. \" \"Use this if you trust wherever you upload.\")) parser.add_argument('-a', '--aggregate-by-commodity', action='store_true', help=(\"Group the holdings by account. This may help if your \" \"portfolio fails to import and you have many holdings.\"))","title":"add_args()"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports.classify_holdings_for_export","text":"Figure out what to do for example with each holding. Parameters: holdings_list \u2013 A list of Holding instances to be exported. commodities_map \u2013 A dict of commodity to Commodity instances. Returns: A pair of \u2013 action_holdings: A list of (symbol, holding) for each holding. 'Symbol' is the ticker to use for export, and may be \"CASH\" or \"IGNORE\" for holdings to be converted or ignored. Source code in beancount/reports/export_reports.py def classify_holdings_for_export(holdings_list, commodities_map): \"\"\"Figure out what to do for example with each holding. Args: holdings_list: A list of Holding instances to be exported. commodities_map: A dict of commodity to Commodity instances. Returns: A pair of: action_holdings: A list of (symbol, holding) for each holding. 'Symbol' is the ticker to use for export, and may be \"CASH\" or \"IGNORE\" for holdings to be converted or ignored. \"\"\" # Get the map of commodities to tickers and export meta tags. exports = getters.get_values_meta(commodities_map, FIELD) # Classify the holdings based on their commodities' ticker metadata field. action_holdings = [] for holding in holdings_list: # Get export field and remove (MONEY:...) specifications. export = re.sub(r'\\(.*\\)', '', exports.get(holding.currency, None) or '').strip() if export: if export.upper() == \"CASH\": action_holdings.append(('CASH', holding)) elif export.upper() == \"IGNORE\": action_holdings.append(('IGNORE', holding)) else: action_holdings.append((export, holding)) else: logging.warning((\"Exporting holding using default commodity name '{}'; this \" \"can potentially break the OFX import. Consider providing \" \"'export' metadata for your commodities.\").format( holding.currency)) action_holdings.append((holding.currency, holding)) return action_holdings","title":"classify_holdings_for_export()"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports.export_holdings","text":"Compute a list of holdings to export. Holdings that are converted to cash equivalents will receive a currency of \"CASH:\" where is the converted cash currency. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options as provided by the parser. promiscuous \u2013 A boolean, true if we should output a promiscuous memo. aggregate_by_commodity \u2013 A boolean, true if we should group the holdings by account. Returns: A pair of exported \u2013 A list of ExportEntry tuples, one for each exported position. converted: A list of ExportEntry tuples, one for each converted position. These will contain multiple holdings. holdings_ignored: A list of Holding instances that were ignored, either because they were explicitly marked to be ignored, or because we could not convert them to a money vehicle matching the holding's cost-currency. Source code in beancount/reports/export_reports.py def export_holdings(entries, options_map, promiscuous, aggregate_by_commodity=False): \"\"\"Compute a list of holdings to export. Holdings that are converted to cash equivalents will receive a currency of \"CASH:\" where is the converted cash currency. Args: entries: A list of directives. options_map: A dict of options as provided by the parser. promiscuous: A boolean, true if we should output a promiscuous memo. aggregate_by_commodity: A boolean, true if we should group the holdings by account. Returns: A pair of exported: A list of ExportEntry tuples, one for each exported position. converted: A list of ExportEntry tuples, one for each converted position. These will contain multiple holdings. holdings_ignored: A list of Holding instances that were ignored, either because they were explicitly marked to be ignored, or because we could not convert them to a money vehicle matching the holding's cost-currency. \"\"\" # Get the desired list of holdings. holdings_list, price_map = holdings_reports.get_assets_holdings(entries, options_map) commodities_map = getters.get_commodity_map(entries) dcontext = options_map['dcontext'] # Aggregate the holdings, if requested. Google Finance is notoriously # finnicky and if you have many holdings this might help. if aggregate_by_commodity: holdings_list = holdings.aggregate_holdings_by(holdings_list, lambda holding: holding.currency) # Classify all the holdings for export. action_holdings = classify_holdings_for_export(holdings_list, commodities_map) # The lists of exported and converted export entries, and the list of # ignored holdings. exported = [] converted = [] holdings_ignored = [] # Export the holdings with tickers individually. for symbol, holding in action_holdings: if symbol in (\"CASH\", \"IGNORE\"): continue if holding.cost_number is None: assert holding.cost_currency in (None, holding.currency) cost_number = holding.number cost_currency = holding.currency else: cost_number = holding.cost_number cost_currency = holding.cost_currency exported.append( ExportEntry(symbol, cost_currency, holding.number, cost_number, is_mutual_fund(symbol), holding.account if promiscuous else '', [holding])) # Convert all the cash entries to their book and market value by currency. cash_holdings_map = collections.defaultdict(list) for symbol, holding in action_holdings: if symbol != \"CASH\": continue if holding.cost_currency: # Accumulate market and book values. cash_holdings_map[holding.cost_currency].append(holding) else: # We cannot price this... no cost currency. holdings_ignored.append(holding) # Get the money instruments. money_instruments = get_money_instruments(commodities_map) # Convert all the cash values to money instruments, if possible. If not # possible, we'll just have to ignore those values. # Go through all the holdings to convert, and for each of those which aren't # in terms of one of the money instruments, which we can directly add to the # exported portfolio, attempt to convert them into currencies to one of # those in the money instruments. money_values_book = collections.defaultdict(D) money_values_market = collections.defaultdict(D) money_values_holdings = collections.defaultdict(list) for cost_currency, holdings_list in cash_holdings_map.items(): book_value = sum(holding.book_value for holding in holdings_list) market_value = sum(holding.market_value for holding in holdings_list) if cost_currency in money_instruments: # The holding is already in terms of one of the money instruments. money_values_book[cost_currency] += book_value money_values_market[cost_currency] += market_value money_values_holdings[cost_currency].extend(holdings_list) else: # The holding is not in terms of one of the money instruments. # Find the first available price to convert it into one for money_currency in money_instruments: base_quote = (cost_currency, money_currency) _, rate = prices.get_latest_price(price_map, base_quote) if rate is not None: money_values_book[money_currency] += book_value * rate money_values_market[money_currency] += market_value * rate money_values_holdings[money_currency].extend(holdings_list) break else: # We could not convert into any of the money commodities. Ignore # those holdings. holdings_ignored.extend(holdings_list) for money_currency in money_values_book.keys(): book_value = money_values_book[money_currency] market_value = money_values_market[money_currency] holdings_list = money_values_holdings[money_currency] symbol = money_instruments[money_currency] assert isinstance(book_value, Decimal) assert isinstance(market_value, Decimal) converted.append( ExportEntry(symbol, money_currency, dcontext.quantize(market_value, money_currency), dcontext.quantize(book_value / market_value, money_currency), is_mutual_fund(symbol), '', holdings_list)) # Add all ignored holdings to a final list. for symbol, holding in action_holdings: if symbol == \"IGNORE\": holdings_ignored.append(holding) return exported, converted, holdings_ignored","title":"export_holdings()"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports.get_money_instruments","text":"Get the money-market stand-ins for cash positions. Parameters: commodities_map \u2013 A map of currency to their corresponding Commodity directives. Returns: A dict of quote currency to the ticker symbol that stands for it, e.g. {'USD' \u2013 'VMMXX'}. Source code in beancount/reports/export_reports.py def get_money_instruments(commodities_map): \"\"\"Get the money-market stand-ins for cash positions. Args: commodities_map: A map of currency to their corresponding Commodity directives. Returns: A dict of quote currency to the ticker symbol that stands for it, e.g. {'USD': 'VMMXX'}. \"\"\" instruments = {} for currency, entry in commodities_map.items(): export = entry.meta.get(FIELD, '') paren_match = re.search(r'\\((.*)\\)', export) if paren_match: match = re.match('MONEY:({})'.format(amount.CURRENCY_RE), paren_match.group(1)) if match: instruments[match.group(1)] = ( re.sub(r'\\(.*\\)', '', export).strip() or currency) else: logging.error(\"Invalid money specification: %s\", export) return instruments","title":"get_money_instruments()"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports.get_symbol","text":"Filter a source specification to some corresponding ticker. Parameters: source \u2013 A comma-separated list of sources as a string, such as \"google/NASDAQ:AAPL,yahoo/AAPL\". Returns: The symbol string. Exceptions: ValueError \u2013 If the sources does not contain a ticker for the google source. Source code in beancount/reports/export_reports.py def get_symbol(sources, prefer='google'): \"\"\"Filter a source specification to some corresponding ticker. Args: source: A comma-separated list of sources as a string, such as \"google/NASDAQ:AAPL,yahoo/AAPL\". Returns: The symbol string. Raises: ValueError: If the sources does not contain a ticker for the google source. \"\"\" # If the ticker is a list of /, extract the symbol # from it. symbol_items = [] for source in map(str.strip, sources.split(',')): match = re.match('([a-zA-Z][a-zA-Z0-9._]+)/(.*)', source) if match: source, symbol = match.groups() else: source, symbol = None, source symbol_items.append((source, symbol)) if not symbol_items: raise ValueError( 'Invalid source \"{}\" does not contain a ticker'.format(sources)) symbol_map = dict(symbol_items) # If not found, return the first symbol in the list of items. return symbol_map.get(prefer, symbol_items[0][1])","title":"get_symbol()"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports.is_mutual_fund","text":"Return true if the GFinance ticker is for a mutual fund. Parameters: ticker \u2013 A string, the symbol for GFinance. Returns: A boolean, true for mutual funds. Source code in beancount/reports/export_reports.py def is_mutual_fund(ticker): \"\"\"Return true if the GFinance ticker is for a mutual fund. Args: ticker: A string, the symbol for GFinance. Returns: A boolean, true for mutual funds. \"\"\" return bool(re.match('MUTF.*:', ticker))","title":"is_mutual_fund()"},{"location":"api_reference/beancount.reports.html#beancount.reports.export_reports.render_ofx_date","text":"Render a datetime to the OFX format. Parameters: dtime \u2013 A datetime.datetime instance. Returns: A string, rendered to milliseconds. Source code in beancount/reports/export_reports.py def render_ofx_date(dtime): \"\"\"Render a datetime to the OFX format. Args: dtime: A datetime.datetime instance. Returns: A string, rendered to milliseconds. \"\"\" return '{}.{:03d}'.format(dtime.strftime('%Y%m%d%H%M%S'), int(dtime.microsecond / 1000))","title":"render_ofx_date()"},{"location":"api_reference/beancount.reports.html#beancount.reports.gviz","text":"Support for creating Google gviz timeline charts.","title":"gviz"},{"location":"api_reference/beancount.reports.html#beancount.reports.gviz.gviz_timeline","text":"Create a HTML rendering of the given arrays. Parameters: time_array \u2013 A sequence of datetime objects. data_array_map \u2013 A dict or list of items of name to sequence of data points. css_id \u2013 A string, the CSS id attribute of the target node. Returns: Javascript code for rendering the chart. (It's up to you to insert the a div with the correct CSS id in your accompanying HTML page.) Source code in beancount/reports/gviz.py def gviz_timeline(time_array, data_array_map, css_id='chart'): \"\"\"Create a HTML rendering of the given arrays. Args: time_array: A sequence of datetime objects. data_array_map: A dict or list of items of name to sequence of data points. css_id: A string, the CSS id attribute of the target node. Returns: Javascript code for rendering the chart. (It's up to you to insert the a div with the correct CSS id in your accompanying HTML page.) \"\"\" # Set the order of the data to be output. if isinstance(data_array_map, dict): data_array_map = list(data_array_map.items()) # Write preamble. oss = io.StringIO() oss.write('\\n') oss.write('\\n') return oss.getvalue()","title":"gviz_timeline()"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports","text":"Generate reports no holdings.","title":"holdings_reports"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports.CashReport","text":"The list of cash holdings (defined as currency = cost-currency).","title":"CashReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports.CashReport.add_args","text":"Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/holdings_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-c', '--currency', action='store', default=None, help=\"Which currency to convert all the holdings to\") parser.add_argument('-i', '--ignored', action='store_true', help=\"Report on ignored holdings instead of included ones\") parser.add_argument('-o', '--operating-only', action='store_true', help=\"Only report on operating currencies\")","title":"add_args()"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports.CashReport.generate_table","text":"Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/holdings_reports.py def generate_table(self, entries, errors, options_map): holdings_list, price_map = get_assets_holdings(entries, options_map) holdings_list_orig = holdings_list # Keep only the holdings where currency is the same as the cost-currency. holdings_list = [holding for holding in holdings_list if (holding.currency == holding.cost_currency or holding.cost_currency is None)] # Keep only those holdings held in one of the operating currencies. if self.args.operating_only: operating_currencies = set(options_map['operating_currency']) holdings_list = [holding for holding in holdings_list if holding.currency in operating_currencies] # Compute the list of ignored holdings and optionally report on them. if self.args.ignored: ignored_holdings = set(holdings_list_orig) - set(holdings_list) holdings_list = ignored_holdings # Convert holdings to a unified currency. if self.args.currency: holdings_list = holdings.convert_to_currency(price_map, self.args.currency, holdings_list) return table.create_table(holdings_list, FIELD_SPEC)","title":"generate_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports.HoldingsReport","text":"The full list of holdings for Asset and Liabilities accounts.","title":"HoldingsReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports.HoldingsReport.add_args","text":"Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/holdings_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-c', '--currency', action='store', default=None, help=\"Which currency to convert all the holdings to\") parser.add_argument('-r', '--relative', action='store_true', help=\"True if we should render as relative values only\") parser.add_argument('-g', '--groupby', '--by', action='store', default=None, choices=cls.aggregations.keys(), help=\"How to group the holdings (default is: don't group)\")","title":"add_args()"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports.HoldingsReport.generate_table","text":"Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/holdings_reports.py def generate_table(self, entries, errors, options_map): keywords = self.aggregations[self.args.groupby] if self.args.groupby else {} return report_holdings(self.args.currency, self.args.relative, entries, options_map, **keywords)","title":"generate_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports.NetWorthReport","text":"Generate a table of total net worth for each operating currency.","title":"NetWorthReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports.NetWorthReport.generate_table","text":"Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/holdings_reports.py def generate_table(self, entries, errors, options_map): holdings_list, price_map = get_assets_holdings(entries, options_map) net_worths = [] for currency in options_map['operating_currency']: # Convert holdings to a unified currency. # # Note: It's entirely possible that the price map does not have all # the necessary rate conversions here. The resulting holdings will # simply have no cost when that is the case. We must handle this # gracefully below. currency_holdings_list = holdings.convert_to_currency(price_map, currency, holdings_list) if not currency_holdings_list: continue holdings_list = holdings.aggregate_holdings_by( currency_holdings_list, lambda holding: holding.cost_currency) holdings_list = [holding for holding in holdings_list if holding.currency and holding.cost_currency] # If after conversion there are no valid holdings, skip the currency # altogether. if not holdings_list: continue net_worths.append((currency, holdings_list[0].market_value)) field_spec = [ (0, 'Currency'), (1, 'Net Worth', '{:,.2f}'.format), ] return table.create_table(net_worths, field_spec)","title":"generate_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports.get_assets_holdings","text":"Return holdings for all assets and liabilities. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of parsed options. currency \u2013 If specified, a string, the target currency to convert all holding values to. Returns: A list of Holding instances and a price-map. Source code in beancount/reports/holdings_reports.py def get_assets_holdings(entries, options_map, currency=None): \"\"\"Return holdings for all assets and liabilities. Args: entries: A list of directives. options_map: A dict of parsed options. currency: If specified, a string, the target currency to convert all holding values to. Returns: A list of Holding instances and a price-map. \"\"\" # Compute a price map, to perform conversions. price_map = prices.build_price_map(entries) # Get the list of holdings. account_types = options.get_account_types(options_map) holdings_list = holdings.get_final_holdings(entries, (account_types.assets, account_types.liabilities), price_map) # Convert holdings to a unified currency. if currency: holdings_list = holdings.convert_to_currency(price_map, currency, holdings_list) return holdings_list, price_map","title":"get_assets_holdings()"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports.get_holdings_entries","text":"Summarizes the entries to list of entries representing the final holdings.. This list includes the latest prices entries as well. This can be used to load a full snapshot of holdings without including the entire history. This is a way of summarizing a balance sheet in a way that filters away history. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of parsed options. Returns: A string, the entries to print out. Source code in beancount/reports/holdings_reports.py def get_holdings_entries(entries, options_map): \"\"\"Summarizes the entries to list of entries representing the final holdings.. This list includes the latest prices entries as well. This can be used to load a full snapshot of holdings without including the entire history. This is a way of summarizing a balance sheet in a way that filters away history. Args: entries: A list of directives. options_map: A dict of parsed options. Returns: A string, the entries to print out. \"\"\" # The entries will be created at the latest date, against an equity account. latest_date = entries[-1].date _, equity_account, _ = options.get_previous_accounts(options_map) # Get all the assets. holdings_list, _ = get_assets_holdings(entries, options_map) # Create synthetic entries for them. holdings_entries = [] for index, holding in enumerate(holdings_list): meta = data.new_metadata('report_holdings_print', index) entry = data.Transaction(meta, latest_date, flags.FLAG_SUMMARIZE, None, \"\", None, None, []) # Convert the holding to a position. pos = holdings.holding_to_position(holding) entry.postings.append( data.Posting(holding.account, pos.units, pos.cost, None, None, None)) cost = -convert.get_cost(pos) entry.postings.append( data.Posting(equity_account, cost, None, None, None, None)) holdings_entries.append(entry) # Get opening directives for all the accounts. used_accounts = {holding.account for holding in holdings_list} open_entries = summarize.get_open_entries(entries, latest_date) used_open_entries = [open_entry for open_entry in open_entries if open_entry.account in used_accounts] # Add an entry for the equity account we're using. meta = data.new_metadata('report_holdings_print', -1) used_open_entries.insert(0, data.Open(meta, latest_date, equity_account, None, None)) # Get the latest price entries. price_entries = prices.get_last_price_entries(entries, None) return used_open_entries + holdings_entries + price_entries","title":"get_holdings_entries()"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports.load_from_csv","text":"Load a list of holdings from a CSV file. Parameters: fileobj \u2013 A file object. Yields: Instances of Holding, as read from the file. Source code in beancount/reports/holdings_reports.py def load_from_csv(fileobj): \"\"\"Load a list of holdings from a CSV file. Args: fileobj: A file object. Yields: Instances of Holding, as read from the file. \"\"\" column_spec = [ ('Account', 'account', None), ('Units', 'number', D), ('Currency', 'currency', None), ('Cost Currency', 'cost_currency', None), ('Average Cost', 'cost_number', D), ('Price', 'price_number', D), ('Book Value', 'book_value', D), ('Market Value', 'market_value', D), ('Price Date', 'price_date', None), ] column_dict = {name: (attr, converter) for name, attr, converter in column_spec} klass = holdings.Holding # Create a set of default values for the namedtuple. defaults_dict = {attr: None for attr in klass._fields} # Start reading the file. reader = csv.reader(fileobj) # Check that the header is readable. header = next(reader) attr_converters = [] for header_name in header: try: attr_converter = column_dict[header_name] attr_converters.append(attr_converter) except KeyError: raise IOError(\"Invalid file contents for holdings\") for line in reader: value_dict = defaults_dict.copy() for (attr, converter), value in zip(attr_converters, line): if converter: value = converter(value) value_dict[attr] = value yield holdings.Holding(**value_dict)","title":"load_from_csv()"},{"location":"api_reference/beancount.reports.html#beancount.reports.holdings_reports.report_holdings","text":"Generate a detailed list of all holdings. Parameters: currency \u2013 A string, a currency to convert to. If left to None, no conversion is carried out. relative \u2013 A boolean, true if we should reduce this to a relative value. entries \u2013 A list of directives. options_map \u2013 A dict of parsed options. aggregation_key \u2013 A callable use to generate aggregations. sort_key \u2013 A function to use to sort the holdings, if specified. Returns: A Table instance. Source code in beancount/reports/holdings_reports.py def report_holdings(currency, relative, entries, options_map, aggregation_key=None, sort_key=None): \"\"\"Generate a detailed list of all holdings. Args: currency: A string, a currency to convert to. If left to None, no conversion is carried out. relative: A boolean, true if we should reduce this to a relative value. entries: A list of directives. options_map: A dict of parsed options. aggregation_key: A callable use to generate aggregations. sort_key: A function to use to sort the holdings, if specified. Returns: A Table instance. \"\"\" holdings_list, _ = get_assets_holdings(entries, options_map, currency) if aggregation_key: holdings_list = holdings.aggregate_holdings_by(holdings_list, aggregation_key) if relative: holdings_list = holdings.reduce_relative(holdings_list) field_spec = RELATIVE_FIELD_SPEC else: field_spec = FIELD_SPEC if sort_key: holdings_list.sort(key=sort_key, reverse=True) return table.create_table(holdings_list, field_spec)","title":"report_holdings()"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter","text":"Base class for HTML formatters. This object encapsulates the rendering of various objects to HTML. You may, and should, derive and override from this object in order to provide links within a web interface.","title":"html_formatter"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter.HTMLFormatter","text":"A trivial formatter object that can be used to format strings as themselves. This mainly defines an interface to implement.","title":"HTMLFormatter"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter.HTMLFormatter.__init__","text":"Create an instance of HTMLFormatter. Parameters: dcontext \u2013 DisplayContext to use to render the numbers. Source code in beancount/reports/html_formatter.py def __init__(self, dcontext): \"\"\"Create an instance of HTMLFormatter. Args: dcontext: DisplayContext to use to render the numbers. \"\"\" self._dformat = dcontext.build( precision=display_context.Precision.MOST_COMMON)","title":"__init__()"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter.HTMLFormatter.render_account","text":"Render an account name. Parameters: account_name \u2013 A string, the name of the account to render. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_account(self, account_name): \"\"\"Render an account name. Args: account_name: A string, the name of the account to render. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return account_name","title":"render_account()"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter.HTMLFormatter.render_amount","text":"Render an amount. Parameters: amount \u2013 An Amount instance. Returns: A string of HTML to be spliced inside a table cell. Source code in beancount/reports/html_formatter.py def render_amount(self, amount): \"\"\"Render an amount. Args: amount: An Amount instance. Returns: A string of HTML to be spliced inside a table cell. \"\"\" return amount.to_string(self._dformat)","title":"render_amount()"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter.HTMLFormatter.render_commodity","text":"Render a commodity (base currency / quote currency). This is only used when we want the commodity to link to its prices. Parameters: commodity \u2013 A pair of strings, the base and quote currency names. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_commodity(self, base_quote): \"\"\"Render a commodity (base currency / quote currency). This is only used when we want the commodity to link to its prices. Args: commodity: A pair of strings, the base and quote currency names. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return '{} / {}'.format(*base_quote)","title":"render_commodity()"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter.HTMLFormatter.render_context","text":"Render a reference to context around a transaction (maybe as an HTML link). Parameters: entry \u2013 A directive. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_context(self, entry): \"\"\"Render a reference to context around a transaction (maybe as an HTML link). Args: entry: A directive. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return ''","title":"render_context()"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter.HTMLFormatter.render_doc","text":"Render a document path. Parameters: filename \u2013 A string, the filename for the document. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_doc(self, filename): \"\"\"Render a document path. Args: filename: A string, the filename for the document. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return filename","title":"render_doc()"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter.HTMLFormatter.render_event_type","text":"Render an event type. Parameters: event \u2013 A string, the name of the even type. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_event_type(self, event): \"\"\"Render an event type. Args: event: A string, the name of the even type. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return event","title":"render_event_type()"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter.HTMLFormatter.render_inventory","text":"Render an inventory. You can use this opportunity to convert the inventory to units or cost or whatever. Parameters: inv \u2013 An Inventory instance. Returns: A string of HTML to be spliced inside a table cell. Source code in beancount/reports/html_formatter.py def render_inventory(self, inv): \"\"\"Render an inventory. You can use this opportunity to convert the inventory to units or cost or whatever. Args: inv: An Inventory instance. Returns: A string of HTML to be spliced inside a table cell. \"\"\" return '
    '.join(position_.to_string(self._dformat) for position_ in sorted(inv))","title":"render_inventory()"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter.HTMLFormatter.render_link","text":"Render a transaction link (maybe as an HTML link). Parameters: link \u2013 A string, the name of the link to render. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_link(self, link): \"\"\"Render a transaction link (maybe as an HTML link). Args: link: A string, the name of the link to render. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return link","title":"render_link()"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter.HTMLFormatter.render_number","text":"Render a number for a currency using the formatter's display context. Parameters: number \u2013 A Decimal instance, the number to be rendered. currency \u2013 A string, the commodity the number represent. Returns: A string, the formatted number to render. Source code in beancount/reports/html_formatter.py def render_number(self, number, currency): \"\"\"Render a number for a currency using the formatter's display context. Args: number: A Decimal instance, the number to be rendered. currency: A string, the commodity the number represent. Returns: A string, the formatted number to render. \"\"\" return self._dformat.format(number, currency)","title":"render_number()"},{"location":"api_reference/beancount.reports.html#beancount.reports.html_formatter.HTMLFormatter.render_source","text":"Render a reference to the source file. Parameters: meta \u2013 A metadata dict object. Returns: A string of HTML to be spliced inside an HTML template. Source code in beancount/reports/html_formatter.py def render_source(self, meta): \"\"\"Render a reference to the source file. Args: meta: A metadata dict object. Returns: A string of HTML to be spliced inside an HTML template. \"\"\" return printer.render_source(meta)","title":"render_source()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_html","text":"HTML rendering routines for serving a lists of postings/entries.","title":"journal_html"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_html.Row","text":"Row(entry, leg_postings, rowtype, extra_class, flag, description, links, amount_str, balance_str)","title":"Row"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_html.Row.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/reports/journal_html.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_html.Row.__new__","text":"Create new instance of Row(entry, leg_postings, rowtype, extra_class, flag, description, links, amount_str, balance_str)","title":"__new__()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_html.Row.__repr__","text":"Return a nicely formatted representation string Source code in beancount/reports/journal_html.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_html.html_entries_table","text":"Render a list of entries into an HTML table, with no running balance. This is appropriate for rendering tables of entries for postings with multiple accounts, whereby computing the running balances makes little sense. (This function returns nothing, it write to oss as a side-effect.) Parameters: oss \u2013 A file object to write the output to. txn_postings \u2013 A list of Posting or directive instances. formatter \u2013 An instance of HTMLFormatter, to be render accounts, inventories, links and docs. render_postings \u2013 A boolean; if true, render the postings as rows under the main transaction row. Source code in beancount/reports/journal_html.py def html_entries_table(oss, txn_postings, formatter, render_postings=True): \"\"\"Render a list of entries into an HTML table, with no running balance. This is appropriate for rendering tables of entries for postings with multiple accounts, whereby computing the running balances makes little sense. (This function returns nothing, it write to oss as a side-effect.) Args: oss: A file object to write the output to. txn_postings: A list of Posting or directive instances. formatter: An instance of HTMLFormatter, to be render accounts, inventories, links and docs. render_postings: A boolean; if true, render the postings as rows under the main transaction row. \"\"\" write = lambda data: (oss.write(data), oss.write('\\n')) write(''' ''') for row in iterate_html_postings(txn_postings, formatter): entry = row.entry description = row.description if row.links: description += render_links(row.links) # Render a row. write(''' '''.format(row.rowtype, row.extra_class, '{}:{}'.format(entry.meta[\"filename\"], entry.meta[\"lineno\"]), formatter.render_context(entry), entry.date, row.flag, description)) if render_postings and isinstance(entry, data.Transaction): for posting in entry.postings: classes = ['Posting'] if posting.flag == flags.FLAG_WARNING: classes.append('warning') write(''' '''.format(' '.join(classes), posting.flag or '', formatter.render_account(posting.account), posting.units or '', posting.cost or '', posting.price or '', convert.get_weight(posting))) write('
    Date F Narration/Payee Amount Cost Price Balance
    {} {} {}
    {} {} {} {} {} {}
    ')","title":"html_entries_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_html.html_entries_table_with_balance","text":"Render a list of entries into an HTML table, with a running balance. (This function returns nothing, it write to oss as a side-effect.) Parameters: oss \u2013 A file object to write the output to. txn_postings \u2013 A list of Posting or directive instances. formatter \u2013 An instance of HTMLFormatter, to be render accounts, inventories, links and docs. render_postings \u2013 A boolean; if true, render the postings as rows under the main transaction row. Source code in beancount/reports/journal_html.py def html_entries_table_with_balance(oss, txn_postings, formatter, render_postings=True): \"\"\"Render a list of entries into an HTML table, with a running balance. (This function returns nothing, it write to oss as a side-effect.) Args: oss: A file object to write the output to. txn_postings: A list of Posting or directive instances. formatter: An instance of HTMLFormatter, to be render accounts, inventories, links and docs. render_postings: A boolean; if true, render the postings as rows under the main transaction row. \"\"\" write = lambda data: (oss.write(data), oss.write('\\n')) write(''' ''') for row in iterate_html_postings(txn_postings, formatter): entry = row.entry description = row.description if row.links: description += render_links(row.links) # Render a row. write(''' '''.format(row.rowtype, row.extra_class, '{}:{}'.format(entry.meta[\"filename\"], entry.meta[\"lineno\"]), formatter.render_context(entry), entry.date, row.flag, description, row.amount_str, row.balance_str)) if render_postings and isinstance(entry, data.Transaction): for posting in entry.postings: classes = ['Posting'] if posting.flag == flags.FLAG_WARNING: classes.append('warning') if posting in row.leg_postings: classes.append('leg') write(''' '''.format(' '.join(classes), posting.flag or '', formatter.render_account(posting.account), position.to_string(posting), posting.price or '', convert.get_weight(posting))) write('
    Date F Narration/Payee Position Price Cost Change Balance
    {} {} {} {} {}
    {} {} {} {} {}
    ')","title":"html_entries_table_with_balance()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_html.iterate_html_postings","text":"Iterate through the list of transactions with rendered HTML strings for each cell. This pre-renders all the data for each row to HTML. This is reused by the entries table rendering routines. Parameters: txn_postings \u2013 A list of TxnPosting or directive instances. formatter \u2013 An instance of HTMLFormatter, to be render accounts, inventories, links and docs. Yields: Instances of Row tuples. See above. Source code in beancount/reports/journal_html.py def iterate_html_postings(txn_postings, formatter): \"\"\"Iterate through the list of transactions with rendered HTML strings for each cell. This pre-renders all the data for each row to HTML. This is reused by the entries table rendering routines. Args: txn_postings: A list of TxnPosting or directive instances. formatter: An instance of HTMLFormatter, to be render accounts, inventories, links and docs. Yields: Instances of Row tuples. See above. \"\"\" for entry_line in realization.iterate_with_balance(txn_postings): entry, leg_postings, change, entry_balance = entry_line # Prepare the data to be rendered for this row. balance_str = formatter.render_inventory(entry_balance) rowtype = entry.__class__.__name__ flag = '' extra_class = '' links = None if isinstance(entry, data.Transaction): rowtype = FLAG_ROWTYPES.get(entry.flag, 'Transaction') extra_class = 'warning' if entry.flag == flags.FLAG_WARNING else '' flag = entry.flag description = '{}'.format(entry.narration) if entry.payee: description = ('{}' '|' '{}').format(entry.payee, description) amount_str = formatter.render_inventory(change) if entry.links and formatter: links = [formatter.render_link(link) for link in entry.links] elif isinstance(entry, data.Balance): # Check the balance here and possibly change the rowtype if entry.diff_amount is None: description = 'Balance {} has {}'.format( formatter.render_account(entry.account), entry.amount) else: description = ('Balance in {} fails; ' 'expected = {}, balance = {}, difference = {}').format( formatter.render_account(entry.account), entry.amount, entry_balance.get_currency_units(entry.amount.currency), entry.diff_amount) extra_class = 'fail' amount_str = formatter.render_amount(entry.amount) elif isinstance(entry, (data.Open, data.Close)): description = '{} {}'.format(entry.__class__.__name__, formatter.render_account(entry.account)) amount_str = '' elif isinstance(entry, data.Note): description = '{} {}'.format(entry.__class__.__name__, entry.comment) amount_str = '' balance_str = '' elif isinstance(entry, data.Document): assert path.isabs(entry.filename) description = 'Document for {}: {}'.format( formatter.render_account(entry.account), formatter.render_doc(entry.filename)) amount_str = '' balance_str = '' else: description = entry.__class__.__name__ amount_str = '' balance_str = '' yield Row(entry, leg_postings, rowtype, extra_class, flag, description, links, amount_str, balance_str)","title":"iterate_html_postings()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_html.render_links","text":"Render Transaction links to HTML. Parameters: links \u2013 A list of set of strings, transaction \"links\" to be rendered. Returns: A string, a snippet of HTML to be rendering somewhere. Source code in beancount/reports/journal_html.py def render_links(links): \"\"\"Render Transaction links to HTML. Args: links: A list of set of strings, transaction \"links\" to be rendered. Returns: A string, a snippet of HTML to be rendering somewhere. \"\"\" return '{}'.format( ''.join('^'.format(link) for link in links))","title":"render_links()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_reports","text":"Report classes for all reports that display ending journals of accounts.","title":"journal_reports"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_reports.ConversionsReport","text":"Print out a report of all conversions.","title":"ConversionsReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_reports.DocumentsReport","text":"Print out a report of documents.","title":"DocumentsReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_reports.JournalReport","text":"Print out an account register/journal.","title":"JournalReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_reports.JournalReport.add_args","text":"Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/journal_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-a', '--account', action='store', default=None, help=\"Account to render\") parser.add_argument('-w', '--width', action='store', type=int, default=0, help=\"The number of characters wide to render the report to\") parser.add_argument('-k', '--precision', action='store', type=int, default=2, help=\"The number of digits to render after the period\") parser.add_argument('-b', '--render-balance', '--balance', action='store_true', help=\"Render a running balance, not just changes\") parser.add_argument('-c', '--at-cost', '--cost', action='store_true', help=\"Render values at cost, convert the units to cost value\") parser.add_argument('-x', '--compact', dest='verbosity', action='store_const', const=journal_text.COMPACT, default=journal_text.NORMAL, help=\"Rendering compactly\") parser.add_argument('-X', '--verbose', dest='verbosity', action='store_const', const=journal_text.VERBOSE, help=\"Rendering verbosely\")","title":"add_args()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_reports.JournalReport.get_postings","text":"Return the postings corresponding to the account filter option. Parameters: real_root \u2013 A RealAccount node for the root of all accounts. Returns: A list of posting or directive instances. Source code in beancount/reports/journal_reports.py def get_postings(self, real_root): \"\"\"Return the postings corresponding to the account filter option. Args: real_root: A RealAccount node for the root of all accounts. Returns: A list of posting or directive instances. \"\"\" if self.args.account: real_account = realization.get(real_root, self.args.account) if real_account is None: # If the account isn't found, return an empty list of postings. # Note that this used to return the following error. # raise base.ReportError( # \"Invalid account name: {}\".format(self.args.account)) return [] else: real_account = real_root return realization.get_postings(real_account)","title":"get_postings()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_reports.JournalReport.render_real_html","text":"Wrap an htmldiv into our standard HTML template. Parameters: real_root \u2013 An instance of RealAccount. price_map \u2013 A price database. price_date \u2013 A date for evaluating prices. options_map \u2013 A dict, options as produced by the parser. file \u2013 A file object to write the output to. Source code in beancount/reports/journal_reports.py def render_real_html(cls, real_root, price_map, price_date, options_map, file): \"\"\"Wrap an htmldiv into our standard HTML template. Args: real_root: An instance of RealAccount. price_map: A price database. price_date: A date for evaluating prices. options_map: A dict, options as produced by the parser. file: A file object to write the output to. \"\"\" template = get_html_template() oss = io.StringIO() cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss) file.write(template.format(body=oss.getvalue(), title=''))","title":"render_real_html()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_text","text":"Text rendering routines for serving a lists of postings/entries.","title":"journal_text"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_text.AmountColumnSizer","text":"A class that computes minimal sizes for columns of numbers and their currencies.","title":"AmountColumnSizer"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_text.AmountColumnSizer.get_format","text":"Return a format string for the column of numbers. Parameters: precision \u2013 An integer, the number of digits to render after the period. Returns: A new-style Python format string, with PREFIX_number and PREFIX_currency named fields. Source code in beancount/reports/journal_text.py def get_format(self, precision): \"\"\"Return a format string for the column of numbers. Args: precision: An integer, the number of digits to render after the period. Returns: A new-style Python format string, with PREFIX_number and PREFIX_currency named fields. \"\"\" return ('{{0:>{width:d}.{precision:d}f}} {{1:<{currency_width}}}').format( width=1 + self.get_number_width() + 1 + precision, precision=precision, currency_width=self.max_currency_width)","title":"get_format()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_text.AmountColumnSizer.get_generic_format","text":"Return a generic format string for rendering as wide as required. This can be used to render an empty string in-lieu of a number. Parameters: precision \u2013 An integer, the number of digits to render after the period. Returns: A new-style Python format string, with PREFIX_number and PREFIX_currency named fields. Source code in beancount/reports/journal_text.py def get_generic_format(self, precision): \"\"\"Return a generic format string for rendering as wide as required. This can be used to render an empty string in-lieu of a number. Args: precision: An integer, the number of digits to render after the period. Returns: A new-style Python format string, with PREFIX_number and PREFIX_currency named fields. \"\"\" return '{{{prefix}:<{width}}}'.format( prefix=self.prefix, width=1 + self.get_number_width() + 1 + precision + 1 + self.max_currency_width)","title":"get_generic_format()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_text.AmountColumnSizer.get_number_width","text":"Return the width of the integer part of the max number. Returns: An integer, the number of digits required to render the integral part. Source code in beancount/reports/journal_text.py def get_number_width(self): \"\"\"Return the width of the integer part of the max number. Returns: An integer, the number of digits required to render the integral part. \"\"\" return ((math.floor(math.log10(self.max_number)) + 1) if self.max_number > 0 else 1)","title":"get_number_width()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_text.AmountColumnSizer.update","text":"Update the sizer with the given number and currency. Parameters: number \u2013 A Decimal instance. currency \u2013 A string, the currency to render for it. Source code in beancount/reports/journal_text.py def update(self, number, currency): \"\"\"Update the sizer with the given number and currency. Args: number: A Decimal instance. currency: A string, the currency to render for it. \"\"\" abs_number = abs(number) if abs_number > self.max_number: self.max_number = abs_number currency_width = len(currency) if currency_width > self.max_currency_width: self.max_currency_width = currency_width","title":"update()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_text.get_entry_text_description","text":"Return the text of a description. Parameters: entry \u2013 A directive, of any type. Returns: A string to use for the filling the description field in text reports. Source code in beancount/reports/journal_text.py def get_entry_text_description(entry): \"\"\"Return the text of a description. Args: entry: A directive, of any type. Returns: A string to use for the filling the description field in text reports. \"\"\" if isinstance(entry, data.Transaction): description = ' | '.join([field for field in [entry.payee, entry.narration] if field is not None]) elif isinstance(entry, data.Balance): if entry.diff_amount is None: description = 'PASS - In {}'.format(entry.account) else: description = ('FAIL - In {}; ' 'expected = {}, difference = {}').format( entry.account, entry.amount, entry.diff_amount) elif isinstance(entry, (data.Open, data.Close)): description = entry.account elif isinstance(entry, data.Note): description = entry.comment elif isinstance(entry, data.Document): description = entry.filename else: description = '-' return description","title":"get_entry_text_description()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_text.render_posting","text":"Render a posting compactly, for text report rendering. Parameters: posting \u2013 An instance of Posting. Returns: A string, the rendered posting. Source code in beancount/reports/journal_text.py def render_posting(posting, number_format): \"\"\"Render a posting compactly, for text report rendering. Args: posting: An instance of Posting. Returns: A string, the rendered posting. \"\"\" # Note: there's probably no need to redo the work of rendering here... see # if you can't just simply replace this by Position.to_string(). units = posting.units strings = [ posting.flag if posting.flag else ' ', '{:32}'.format(posting.account), number_format.format(units.number, units.currency) ] cost = posting.cost if cost: strings.append('{{{}}}'.format(number_format.format(cost.number, cost.currency).strip())) price = posting.price if price: strings.append('@ {}'.format(number_format.format(price.number, price.currency).strip())) return ' '.join(strings)","title":"render_posting()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_text.size_and_render_amounts","text":"Iterate through postings and compute sizers and render amounts. Parameters: postings \u2013 A list of Posting or directive instances. at_cost \u2013 A boolean, if true, render the cost value, not the actual. render_balance \u2013 A boolean, if true, renders a running balance column. Source code in beancount/reports/journal_text.py def size_and_render_amounts(postings, at_cost, render_balance): \"\"\"Iterate through postings and compute sizers and render amounts. Args: postings: A list of Posting or directive instances. at_cost: A boolean, if true, render the cost value, not the actual. render_balance: A boolean, if true, renders a running balance column. \"\"\" # Compute the maximum width required to render the change and balance # columns. In order to carry this out, we will pre-compute all the data to # render this and save it for later. change_sizer = AmountColumnSizer('change') balance_sizer = AmountColumnSizer('balance') entry_data = [] for entry_line in realization.iterate_with_balance(postings): entry, leg_postings, change, balance = entry_line # Convert to cost if necessary. (Note that this agglutinates currencies, # so we'd rather do make the conversion at this level (inventory) than # convert the positions or amounts later.) if at_cost: change = change.reduce(convert.get_cost) if render_balance: balance = balance.reduce(convert.get_cost) # Compute the amounts and maximum widths for the change column. change_amounts = [] for position in change.get_positions(): units = position.units change_amounts.append(units) change_sizer.update(units.number, units.currency) # Compute the amounts and maximum widths for the balance column. balance_amounts = [] if render_balance: for position in balance.get_positions(): units = position.units balance_amounts.append(units) balance_sizer.update(units.number, units.currency) entry_data.append((entry, leg_postings, change_amounts, balance_amounts)) return (entry_data, change_sizer, balance_sizer)","title":"size_and_render_amounts()"},{"location":"api_reference/beancount.reports.html#beancount.reports.journal_text.text_entries_table","text":"Render a table of postings or directives with an accumulated balance. This function has three verbosity modes for rendering: 1. COMPACT: no separating line, no postings 2. NORMAL: a separating line between entries, no postings 3. VERBOSE: renders all the postings in addition to normal. The output is written to the 'oss' file object. Nothing is returned. Parameters: oss \u2013 A file object to write the output to. postings \u2013 A list of Posting or directive instances. width \u2013 An integer, the width to render the table to. at_cost \u2013 A boolean, if true, render the cost value, not the actual. render_balance \u2013 A boolean, if true, renders a running balance column. precision \u2013 An integer, the number of digits to render after the period. verbosity \u2013 An integer, the verbosity level. See COMPACT, NORMAL, VERBOSE, etc. output_format \u2013 A string, either 'text' or 'csv' for the chosen output format. This routine's inner loop calculations are complex enough it gets reused by both formats. Exceptions: ValueError \u2013 If the width is insufficient to render the description. Source code in beancount/reports/journal_text.py def text_entries_table(oss, postings, width, at_cost, render_balance, precision, verbosity, output_format): \"\"\"Render a table of postings or directives with an accumulated balance. This function has three verbosity modes for rendering: 1. COMPACT: no separating line, no postings 2. NORMAL: a separating line between entries, no postings 3. VERBOSE: renders all the postings in addition to normal. The output is written to the 'oss' file object. Nothing is returned. Args: oss: A file object to write the output to. postings: A list of Posting or directive instances. width: An integer, the width to render the table to. at_cost: A boolean, if true, render the cost value, not the actual. render_balance: A boolean, if true, renders a running balance column. precision: An integer, the number of digits to render after the period. verbosity: An integer, the verbosity level. See COMPACT, NORMAL, VERBOSE, etc. output_format: A string, either 'text' or 'csv' for the chosen output format. This routine's inner loop calculations are complex enough it gets reused by both formats. Raises: ValueError: If the width is insufficient to render the description. \"\"\" assert output_format in (FORMAT_TEXT, FORMAT_CSV) if output_format is FORMAT_CSV: csv_writer = csv.writer(oss) # Render the changes and balances to lists of amounts and precompute sizes. entry_data, change_sizer, balance_sizer = size_and_render_amounts(postings, at_cost, render_balance) # Render an empty line and compute the width the description should be (the # description is the only elastic field). empty_format = '{{date:10}} {{dirtype:5}} {{description}} {}'.format( change_sizer.get_generic_format(precision)) if render_balance: empty_format += ' {}'.format(balance_sizer.get_generic_format(precision)) empty_line = empty_format.format(date='', dirtype='', description='', change='', balance='') description_width = width - len(empty_line) if description_width <= 0: raise ValueError( \"Width not sufficient to render text report ({} chars)\".format(width)) # Establish a format string for the final format of all lines. # pylint: disable=duplicate-string-formatting-argument line_format = '{{date:10}} {{dirtype:5}} {{description:{:d}.{:d}}} {}'.format( description_width, description_width, change_sizer.get_generic_format(precision)) change_format = change_sizer.get_format(precision) if render_balance: line_format += ' {}'.format(balance_sizer.get_generic_format(precision)) balance_format = balance_sizer.get_format(precision) line_format += '\\n' # Iterate over all the pre-computed data. for (entry, leg_postings, change_amounts, balance_amounts) in entry_data: # Render the date. date = entry.date.isoformat() # Get the directive type name. dirtype = TEXT_SHORT_NAME[type(entry)] if isinstance(entry, data.Transaction) and entry.flag: dirtype = entry.flag # Get the description string and split the description line in multiple # lines. description = get_entry_text_description(entry) description_lines = textwrap.wrap(description, width=description_width) # Ensure at least one line is rendered (for zip_longest). if not description_lines: description_lines.append('') # Render all the amounts in the line. for (description, change_amount, balance_amount) in itertools.zip_longest(description_lines, change_amounts, balance_amounts, fillvalue=''): change = (change_format.format(change_amount.number, change_amount.currency) if change_amount else '') balance = (balance_format.format(balance_amount.number, balance_amount.currency) if balance_amount else '') if not description and verbosity >= VERBOSE and leg_postings: description = '..' if output_format is FORMAT_TEXT: oss.write(line_format.format(date=date, dirtype=dirtype, description=description, change=change, balance=balance)) else: change_number, change_currency = '', '' if change: change_number, change_currency = change.split() if render_balance: balance_number, balance_currency = '', '' if balance: balance_number, balance_currency = balance.split() row = (date, dirtype, description, change_number, change_currency, balance_number, balance_currency) else: row = (date, dirtype, description, change_number, change_currency) csv_writer.writerow(row) # Reset the date, directive type and description. Only the first # line renders these; the other lines render only the amounts. if date: date = dirtype = '' if verbosity >= VERBOSE: for posting in leg_postings: posting_str = render_posting(posting, change_format) if len(posting_str) > description_width: posting_str = posting_str[:description_width-3] + '...' if output_format is FORMAT_TEXT: oss.write(line_format.format(date='', dirtype='', description=posting_str, change='', balance='')) else: row = ('', '', posting_str) csv_writer.writerow(row) if verbosity >= NORMAL: oss.write('\\n')","title":"text_entries_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports","text":"Miscellaneous report classes.","title":"misc_reports"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.AccountsReport","text":"Print out the list of all accounts.","title":"AccountsReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.ActivityReport","text":"Render the last or recent update activity.","title":"ActivityReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.ActivityReport.add_args","text":"Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/misc_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-d', '--cutoff', action='store', default=None, type=date_utils.parse_date_liberally, help=\"Cutoff date where we ignore whatever comes after.\")","title":"add_args()"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.ActivityReport.render_real_html","text":"Wrap an htmldiv into our standard HTML template. Parameters: real_root \u2013 An instance of RealAccount. price_map \u2013 A price database. price_date \u2013 A date for evaluating prices. options_map \u2013 A dict, options as produced by the parser. file \u2013 A file object to write the output to. Source code in beancount/reports/misc_reports.py def render_real_html(cls, real_root, price_map, price_date, options_map, file): \"\"\"Wrap an htmldiv into our standard HTML template. Args: real_root: An instance of RealAccount. price_map: A price database. price_date: A date for evaluating prices. options_map: A dict, options as produced by the parser. file: A file object to write the output to. \"\"\" template = get_html_template() oss = io.StringIO() cls.render_real_htmldiv(real_root, price_map, price_date, options_map, oss) file.write(template.format(body=oss.getvalue(), title=''))","title":"render_real_html()"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.CurrentEventsReport","text":"Produce a table of the current values of all event types.","title":"CurrentEventsReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.CurrentEventsReport.generate_table","text":"Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/misc_reports.py def generate_table(self, entries, errors, options_map): events = {} for entry in entries: if isinstance(entry, data.Event): events[entry.type] = entry.description return table.create_table(list(sorted(events.items())), [(0, \"Type\", self.formatter.render_event_type), (1, \"Description\")])","title":"generate_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.ErrorReport","text":"Report the errors.","title":"ErrorReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.EventsReport","text":"Produce a table of all the values of a particular event.","title":"EventsReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.EventsReport.add_args","text":"Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/misc_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-e', '--expr', action='store', default=None, help=\"A regexp to filer on which events to display.\")","title":"add_args()"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.EventsReport.generate_table","text":"Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/misc_reports.py def generate_table(self, entries, errors, options_map): event_entries = [] for entry in entries: if not isinstance(entry, data.Event): continue if self.args.expr and not re.match(self.args.expr, entry.type): continue event_entries.append(entry) return table.create_table([(entry.date, entry.type, entry.description) for entry in event_entries], [(0, \"Date\", datetime.date.isoformat), (1, \"Type\"), (2, \"Description\")])","title":"generate_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.NoopReport","text":"Report nothing.","title":"NoopReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.PrintReport","text":"Print out the entries.","title":"PrintReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.StatsDirectivesReport","text":"Render statistics on each directive type, the number of entries by type.","title":"StatsDirectivesReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.StatsDirectivesReport.generate_table","text":"Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/misc_reports.py def generate_table(self, entries, _, __): entries_by_type = misc_utils.groupby(lambda entry: type(entry).__name__, entries) nb_entries_by_type = {name: len(entries) for name, entries in entries_by_type.items()} rows = sorted(nb_entries_by_type.items(), key=lambda x: x[1], reverse=True) rows = [(name, str(count)) for (name, count) in rows] rows.append(('~Total~', str(len(entries)))) return table.create_table(rows, [(0, 'Type'), (1, 'Num Entries', '{:>}'.format)])","title":"generate_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.StatsPostingsReport","text":"Render the number of postings for each account.","title":"StatsPostingsReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.misc_reports.StatsPostingsReport.generate_table","text":"Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/misc_reports.py def generate_table(self, entries, _, __): all_postings = [posting for entry in entries if isinstance(entry, data.Transaction) for posting in entry.postings] postings_by_account = misc_utils.groupby(lambda posting: posting.account, all_postings) nb_postings_by_account = {key: len(postings) for key, postings in postings_by_account.items()} rows = sorted(nb_postings_by_account.items(), key=lambda x: x[1], reverse=True) rows = [(name, str(count)) for (name, count) in rows] rows.append(('~Total~', str(sum(nb_postings_by_account.values())))) return table.create_table(rows, [(0, 'Account'), (1, 'Num Postings', '{:>}'.format)])","title":"generate_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.price_reports","text":"Miscellaneous report classes.","title":"price_reports"},{"location":"api_reference/beancount.reports.html#beancount.reports.price_reports.CommoditiesReport","text":"Print out a list of commodities.","title":"CommoditiesReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.price_reports.CommoditiesReport.generate_table","text":"Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/price_reports.py def generate_table(self, entries, errors, options_map): price_map = prices.build_price_map(entries) return table.create_table([(base_quote,) for base_quote in sorted(price_map.forward_pairs)], [(0, \"Base/Quote\", self.formatter.render_commodity)])","title":"generate_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.price_reports.CommodityLifetimes","text":"Print out a list of lifetimes of each commodity.","title":"CommodityLifetimes"},{"location":"api_reference/beancount.reports.html#beancount.reports.price_reports.CommodityLifetimes.add_args","text":"Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/price_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-c', '--compress-days', type=int, action='store', default=None, help=\"The number of unused days to allow for continuous usage.\")","title":"add_args()"},{"location":"api_reference/beancount.reports.html#beancount.reports.price_reports.CommodityPricesReport","text":"Print all the prices for a particular commodity.","title":"CommodityPricesReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.price_reports.CommodityPricesReport.add_args","text":"Add arguments to parse for this report. Parameters: parser \u2013 An instance of argparse.ArgumentParser. Source code in beancount/reports/price_reports.py @classmethod def add_args(cls, parser): parser.add_argument('-c', '--commodity', '--currency', action='store', default=None, help=\"The commodity pair to display.\")","title":"add_args()"},{"location":"api_reference/beancount.reports.html#beancount.reports.price_reports.CommodityPricesReport.generate_table","text":"Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/price_reports.py def generate_table(self, entries, errors, options_map): date_rates = self.get_date_rates(entries) return table.create_table(date_rates, [(0, \"Date\", datetime.date.isoformat), (1, \"Price\", '{:.5f}'.format)])","title":"generate_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.price_reports.PriceDBReport","text":"Print out the normalized price entries from the price db. Normalized means that we print prices in the most common (base, quote) order. This can be used to rebuild a prices database without having to share the entire ledger file. Only the forward prices are printed; which (base, quote) pair is selected is selected based on the most common occurrence between (base, quote) and (quote, base). This is done in the price map.","title":"PriceDBReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.price_reports.PricesReport","text":"Print out the unnormalized price entries that we input. Unnormalized means that we may render both (base,quote) and (quote,base). This can be used to rebuild a prices database without having to share the entire ledger file. Note: this type of report should be removed once we have filtering on directive type, this is simply the 'print' report with type:price. Maybe rename the 'pricedb' report to just 'prices' for simplicity's sake.","title":"PricesReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.price_reports.TickerReport","text":"Print a parseable mapping of (base, quote, ticker, name) for all commodities.","title":"TickerReport"},{"location":"api_reference/beancount.reports.html#beancount.reports.price_reports.TickerReport.generate_table","text":"Render the report to a Table instance. Parameters: entries \u2013 A list of directives to render. errors \u2013 A list of errors that occurred during processing. options_map \u2013 A dict of options, as produced by the parser. Returns: An instance of Table, that will get converted to another format. Source code in beancount/reports/price_reports.py def generate_table(self, entries, errors, options_map): commodity_map = getters.get_commodity_map(entries) ticker_info = getters.get_values_meta(commodity_map, 'name', 'ticker', 'quote') price_rows = [ (currency, cost_currency, ticker, name) for currency, (name, ticker, cost_currency) in sorted(ticker_info.items()) if ticker] return table.create_table(price_rows, [(0, \"Currency\"), (1, \"Cost-Currency\"), (2, \"Symbol\"), (3, \"Name\")])","title":"generate_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.report","text":"Produce various custom implemented reports.","title":"report"},{"location":"api_reference/beancount.reports.html#beancount.reports.report.ListFormatsAction","text":"An argparse action that prints all supported formats (for each report).","title":"ListFormatsAction"},{"location":"api_reference/beancount.reports.html#beancount.reports.report.ListReportsAction","text":"An argparse action that just prints the list of reports and exits.","title":"ListReportsAction"},{"location":"api_reference/beancount.reports.html#beancount.reports.report.get_all_reports","text":"Return all report classes. Returns: A list of all available report classes. Source code in beancount/reports/report.py def get_all_reports(): \"\"\"Return all report classes. Returns: A list of all available report classes. \"\"\" return functools.reduce(operator.add, map(lambda module: module.__reports__, [balance_reports, journal_reports, holdings_reports, export_reports, price_reports, misc_reports, convert_reports]))","title":"get_all_reports()"},{"location":"api_reference/beancount.reports.html#beancount.reports.report.get_list_report_string","text":"Return a formatted string for the list of supported reports. Parameters: only_report \u2013 A string, the name of a single report to produce the help for. If not specified, list all the available reports. Returns: A help string, or None, if 'only_report' was provided and is not a valid report name. Source code in beancount/reports/report.py def get_list_report_string(only_report=None): \"\"\"Return a formatted string for the list of supported reports. Args: only_report: A string, the name of a single report to produce the help for. If not specified, list all the available reports. Returns: A help string, or None, if 'only_report' was provided and is not a valid report name. \"\"\" oss = io.StringIO() num_reports = 0 for report_class in get_all_reports(): # Filter the name if only_report and only_report not in report_class.names: continue # Get the textual description. description = textwrap.fill( re.sub(' +', ' ', ' '.join(report_class.__doc__.splitlines())), initial_indent=\" \", subsequent_indent=\" \", width=80) # Get the report's arguments. parser = version.ArgumentParser() report_ = report_class report_class.add_args(parser) # Get the list of supported formats. ## formats = report_class.get_supported_formats() oss.write('{}:\\n{}\\n'.format(','.join(report_.names), description)) num_reports += 1 if not num_reports: return None return oss.getvalue()","title":"get_list_report_string()"},{"location":"api_reference/beancount.reports.html#beancount.reports.table","text":"Table rendering.","title":"table"},{"location":"api_reference/beancount.reports.html#beancount.reports.table.Table","text":"Table(columns, header, body)","title":"Table"},{"location":"api_reference/beancount.reports.html#beancount.reports.table.Table.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/reports/table.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.reports.html#beancount.reports.table.Table.__new__","text":"Create new instance of Table(columns, header, body)","title":"__new__()"},{"location":"api_reference/beancount.reports.html#beancount.reports.table.Table.__repr__","text":"Return a nicely formatted representation string Source code in beancount/reports/table.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.reports.html#beancount.reports.table.attribute_to_title","text":"Convert programming id into readable field name. Parameters: fieldname \u2013 A string, a programming ids, such as 'book_value'. Returns: A readable string, such as 'Book Value.' Source code in beancount/reports/table.py def attribute_to_title(fieldname): \"\"\"Convert programming id into readable field name. Args: fieldname: A string, a programming ids, such as 'book_value'. Returns: A readable string, such as 'Book Value.' \"\"\" return fieldname.replace('_', ' ').title()","title":"attribute_to_title()"},{"location":"api_reference/beancount.reports.html#beancount.reports.table.compute_table_widths","text":"Compute the max character widths of a list of rows. Parameters: rows \u2013 A list of rows, which are sequences of strings. Returns: A list of integers, the maximum widths required to render the columns of this table. Exceptions: IndexError \u2013 If the rows are of different lengths. Source code in beancount/reports/table.py def compute_table_widths(rows): \"\"\"Compute the max character widths of a list of rows. Args: rows: A list of rows, which are sequences of strings. Returns: A list of integers, the maximum widths required to render the columns of this table. Raises: IndexError: If the rows are of different lengths. \"\"\" row_iter = iter(rows) first_row = next(row_iter) num_columns = len(first_row) column_widths = [len(cell) for cell in first_row] for row in row_iter: for i, cell in enumerate(row): if not isinstance(cell, str): cell = str(cell) cell_len = len(cell) if cell_len > column_widths[i]: column_widths[i] = cell_len if i+1 != num_columns: raise IndexError(\"Invalid number of rows\") return column_widths","title":"compute_table_widths()"},{"location":"api_reference/beancount.reports.html#beancount.reports.table.create_table","text":"Convert a list of tuples to an table report object. Parameters: rows \u2013 A list of tuples. field_spec \u2013 A list of strings, or a list of (FIELDNAME-OR-INDEX, HEADER, FORMATTER-FUNCTION) triplets, that selects a subset of the fields is to be rendered as well as their ordering. If this is a dict, the values are functions to call on the fields to render them. If a function is set to None, we will just call str() on the field. Returns: A Table instance. Source code in beancount/reports/table.py def create_table(rows, field_spec=None): \"\"\"Convert a list of tuples to an table report object. Args: rows: A list of tuples. field_spec: A list of strings, or a list of (FIELDNAME-OR-INDEX, HEADER, FORMATTER-FUNCTION) triplets, that selects a subset of the fields is to be rendered as well as their ordering. If this is a dict, the values are functions to call on the fields to render them. If a function is set to None, we will just call str() on the field. Returns: A Table instance. \"\"\" # Normalize field_spec to a dict. if field_spec is None: namedtuple_class = type(rows[0]) field_spec = [(field, None, None) for field in namedtuple_class._fields] elif isinstance(field_spec, (list, tuple)): new_field_spec = [] for field in field_spec: if isinstance(field, tuple): assert len(field) <= 3, field if len(field) == 1: field = field[0] new_field_spec.append((field, None, None)) elif len(field) == 2: field, header = field new_field_spec.append((field, header, None)) elif len(field) == 3: new_field_spec.append(field) else: if isinstance(field, str): title = attribute_to_title(field) elif isinstance(field, int): title = \"Field {}\".format(field) else: raise ValueError(\"Invalid type for column name\") new_field_spec.append((field, title, None)) field_spec = new_field_spec # Ensure a nicely formatted header. field_spec = [((name, attribute_to_title(name), formatter) if header_ is None else (name, header_, formatter)) for (name, header_, formatter) in field_spec] assert isinstance(field_spec, list), field_spec assert all(len(x) == 3 for x in field_spec), field_spec # Compute the column names. columns = [name for (name, _, __) in field_spec] # Compute the table header. header = [header_column for (_, header_column, __) in field_spec] # Compute the table body. body = [] for row in rows: body_row = [] for name, _, formatter in field_spec: if isinstance(name, str): value = getattr(row, name) elif isinstance(name, int): value = row[name] else: raise ValueError(\"Invalid type for column name\") if value is not None: if formatter is not None: value = formatter(value) else: value = str(value) else: value = '' body_row.append(value) body.append(body_row) return Table(columns, header, body)","title":"create_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.table.render_table","text":"Render the given table to the output file object in the requested format. The table gets written out to the 'output' file. Parameters: table_ \u2013 An instance of Table. output \u2013 A file object you can write to. output_format \u2013 A string, the format to write the table to, either 'csv', 'txt' or 'html'. css_id \u2013 A string, an optional CSS id for the table object (only used for HTML). css_class \u2013 A string, an optional CSS class for the table object (only used for HTML). Source code in beancount/reports/table.py def render_table(table_, output, output_format, css_id=None, css_class=None): \"\"\"Render the given table to the output file object in the requested format. The table gets written out to the 'output' file. Args: table_: An instance of Table. output: A file object you can write to. output_format: A string, the format to write the table to, either 'csv', 'txt' or 'html'. css_id: A string, an optional CSS id for the table object (only used for HTML). css_class: A string, an optional CSS class for the table object (only used for HTML). \"\"\" if output_format in ('txt', 'text'): text = table_to_text(table_, \" \", formats={'*': '>', 'account': '<'}) output.write(text) elif output_format in ('csv',): table_to_csv(table_, file=output) elif output_format in ('htmldiv', 'html'): if output_format == 'html': output.write('\\n') output.write('\\n') output.write('
    \\n'.format(css_id) if css_id else '
    \\n') classes = [css_class] if css_class else None table_to_html(table_, file=output, classes=classes) output.write('
    \\n') if output_format == 'html': output.write('\\n') output.write('\\n') else: raise NotImplementedError(\"Unsupported format: {}\".format(output_format))","title":"render_table()"},{"location":"api_reference/beancount.reports.html#beancount.reports.table.table_to_csv","text":"Render a Table to a CSV file. Parameters: table \u2013 An instance of a Table. file \u2013 A file object to write to. If no object is provided, this function returns a string. **kwargs \u2013 Optional arguments forwarded to csv.writer(). Returns: A string, the rendered table, or None, if a file object is provided to write to. Source code in beancount/reports/table.py def table_to_csv(table, file=None, **kwargs): \"\"\"Render a Table to a CSV file. Args: table: An instance of a Table. file: A file object to write to. If no object is provided, this function returns a string. **kwargs: Optional arguments forwarded to csv.writer(). Returns: A string, the rendered table, or None, if a file object is provided to write to. \"\"\" output_file = file or io.StringIO() writer = csv.writer(output_file, **kwargs) if table.header: writer.writerow(table.header) writer.writerows(table.body) if not file: return output_file.getvalue()","title":"table_to_csv()"},{"location":"api_reference/beancount.reports.html#beancount.reports.table.table_to_html","text":"Render a Table to HTML. Parameters: table \u2013 An instance of a Table. classes \u2013 A list of string, CSS classes to set on the table. file \u2013 A file object to write to. If no object is provided, this function returns a string. Returns: A string, the rendered table, or None, if a file object is provided to write to. Source code in beancount/reports/table.py def table_to_html(table, classes=None, file=None): \"\"\"Render a Table to HTML. Args: table: An instance of a Table. classes: A list of string, CSS classes to set on the table. file: A file object to write to. If no object is provided, this function returns a string. Returns: A string, the rendered table, or None, if a file object is provided to write to. \"\"\" # Initialize file. oss = io.StringIO() if file is None else file oss.write('\\n'.format(' '.join(classes or []))) # Render header. if table.header: oss.write(' \\n') oss.write(' \\n') for header in table.header: oss.write(' \\n'.format(header)) oss.write(' \\n') oss.write(' \\n') # Render body. oss.write(' \\n') for row in table.body: oss.write(' \\n') for cell in row: oss.write(' \\n'.format(cell)) oss.write(' \\n') oss.write(' \\n') # Render footer. oss.write('
    {}
    {}
    \\n') if file is None: return oss.getvalue()","title":"table_to_html()"},{"location":"api_reference/beancount.reports.html#beancount.reports.table.table_to_text","text":"Render a Table to ASCII text. Parameters: table \u2013 An instance of a Table. column_interspace \u2013 A string to render between the columns as spacer. formats \u2013 An optional dict of column name to a format character that gets inserted in a format string specified, like this (where '' is): {:}. A key of ' ' will provide a default value, like this, for example: (... formats={' ': '>'}). Returns: A string, the rendered text table. Source code in beancount/reports/table.py def table_to_text(table, column_interspace=\" \", formats=None): \"\"\"Render a Table to ASCII text. Args: table: An instance of a Table. column_interspace: A string to render between the columns as spacer. formats: An optional dict of column name to a format character that gets inserted in a format string specified, like this (where '' is): {:}. A key of '*' will provide a default value, like this, for example: (... formats={'*': '>'}). Returns: A string, the rendered text table. \"\"\" column_widths = compute_table_widths(itertools.chain([table.header], table.body)) # Insert column format chars and compute line formatting string. column_formats = [] if formats: default_format = formats.get('*', None) for column, width in zip(table.columns, column_widths): if column and formats: format_ = formats.get(column, default_format) if format_: column_formats.append(\"{{:{}{:d}}}\".format(format_, width)) else: column_formats.append(\"{{:{:d}}}\".format(width)) else: column_formats.append(\"{{:{:d}}}\".format(width)) line_format = column_interspace.join(column_formats) + \"\\n\" separator = line_format.format(*[('-' * width) for width in column_widths]) # Render the header. oss = io.StringIO() if table.header: oss.write(line_format.format(*table.header)) # Render the body. oss.write(separator) for row in table.body: oss.write(line_format.format(*row)) oss.write(separator) return oss.getvalue()","title":"table_to_text()"},{"location":"api_reference/beancount.reports.html#beancount.reports.tree_table","text":"Routines to render an HTML table with a tree of accounts.","title":"tree_table"},{"location":"api_reference/beancount.reports.html#beancount.reports.tree_table.is_account_active","text":"Return true if the account should be rendered. An active account has at least one directive that is not an Open directive. Parameters: real_account \u2013 An instance of RealAccount. Returns: A boolean, true if the account is active, according to the definition above. Source code in beancount/reports/tree_table.py def is_account_active(real_account): \"\"\"Return true if the account should be rendered. An active account has at least one directive that is not an Open directive. Args: real_account: An instance of RealAccount. Returns: A boolean, true if the account is active, according to the definition above. \"\"\" for entry in real_account.txn_postings: if isinstance(entry, data.Open): continue return True return False","title":"is_account_active()"},{"location":"api_reference/beancount.reports.html#beancount.reports.tree_table.table_of_balances","text":"Render a tree table with the balance of each accounts. Parameters: real_root \u2013 A RealAccount node, the root node to render. price_map \u2013 A prices map, a built by build_price_map. price_date \u2013 A datetime.date instance, the date at which to compute market value. operating_currencies \u2013 A list of strings, the operating currencies to render to their own dedicated columns. formatter \u2013 A object used to render account names and other links. classes \u2013 A list of strings, the CSS classes to attach to the rendered top-level table object. Returns: A string with HTML contents, the rendered table. Source code in beancount/reports/tree_table.py def table_of_balances(real_root, price_map, price_date, operating_currencies, formatter, classes=None): \"\"\"Render a tree table with the balance of each accounts. Args: real_root: A RealAccount node, the root node to render. price_map: A prices map, a built by build_price_map. price_date: A datetime.date instance, the date at which to compute market value. operating_currencies: A list of strings, the operating currencies to render to their own dedicated columns. formatter: A object used to render account names and other links. classes: A list of strings, the CSS classes to attach to the rendered top-level table object. Returns: A string with HTML contents, the rendered table. \"\"\" header = ['Account'] + operating_currencies + ['Other'] # Pre-calculate which accounts should be rendered. real_active = realization.filter(real_root, is_account_active) if real_active: active_set = {real_account.account for real_account in realization.iter_children(real_active)} else: active_set = set() balance_totals = inventory.Inventory() oss = io.StringIO() classes = list(classes) if classes else [] classes.append('fullwidth') for real_account, cells, row_classes in tree_table(oss, real_root, formatter, header, classes): if real_account is TOTALS_LINE: line_balance = balance_totals row_classes.append('totals') else: # Check if this account has had activity; if not, skip rendering it. if (real_account.account not in active_set and not account_types.is_root_account(real_account.account)): continue if real_account.account is None: row_classes.append('parent-node') # For each account line, get the final balance of the account at # latest market value. line_balance = real_account.balance.reduce(convert.get_value, price_map) # Update the total balance for the totals line. balance_totals.add_inventory(line_balance) # Extract all the positions that the user has identified as operating # currencies to their own subinventories. ccy_dict = line_balance.segregate_units(operating_currencies) # FIXME: This little algorithm is inefficient; rewrite it. for currency in operating_currencies: units = ccy_dict[currency].get_currency_units(currency) cells.append(formatter.render_number(units.number, units.currency) if units.number != ZERO else '') # Render all the rest of the inventory in the last cell. if None in ccy_dict: ccy_balance = ccy_dict[None] last_cell = '
    '.join(formatter.render_amount(pos.units) for pos in sorted(ccy_balance)) else: last_cell = '' cells.append(last_cell) return oss.getvalue()","title":"table_of_balances()"},{"location":"api_reference/beancount.reports.html#beancount.reports.tree_table.tree_table","text":"Generator to a tree of accounts as an HTML table. This yields each real_account object in turn and a list object used to provide the values for the various columns to render. Parameters: oss \u2013 a io.StringIO instance, into which we will render the HTML. real_account \u2013 an instance of a RealAccount node. formatter \u2013 A object used to render account names and other links. header \u2013 a list of header columns to render. The first column is special, and is used for the account name. classes \u2013 a list of CSS class strings to apply to the table element. Returns: A generator of tuples of real_account \u2013 An instance of RealAccount to render as a row cells: A mutable list object to accumulate values for each column you want to render. row_classes: A mutable list object to accumulate css classes that you want to add for the row. You need to append to the given 'cells' object; if you don't append anything, this tells this routine to skip rendering the row. On the very last line, the 'real_account' object will be a special sentinel value to indicate that it is meant to render the totals line: TOTALS_LINE. Source code in beancount/reports/tree_table.py def tree_table(oss, real_account, formatter, header=None, classes=None): \"\"\"Generator to a tree of accounts as an HTML table. This yields each real_account object in turn and a list object used to provide the values for the various columns to render. Args: oss: a io.StringIO instance, into which we will render the HTML. real_account: an instance of a RealAccount node. formatter: A object used to render account names and other links. header: a list of header columns to render. The first column is special, and is used for the account name. classes: a list of CSS class strings to apply to the table element. Returns: A generator of tuples of real_account: An instance of RealAccount to render as a row cells: A mutable list object to accumulate values for each column you want to render. row_classes: A mutable list object to accumulate css classes that you want to add for the row. You need to append to the given 'cells' object; if you don't append anything, this tells this routine to skip rendering the row. On the very last line, the 'real_account' object will be a special sentinel value to indicate that it is meant to render the totals line: TOTALS_LINE. \"\"\" write = lambda data: (oss.write(data), oss.write('\\n')) classes = list(classes) if classes else [] classes.append('tree-table') write(''.format(' '.join(classes) if classes else '')) if header: write('') write('') header_iter = iter(header) write(''.format(next(header_iter))) for column in header_iter: write(''.format(column)) write('') write('') # Note: This code eventually should be reworked to be agnostic regarding # text or HTML output rendering. lines = realization.dump(real_account) # Yield with a None for the final line. lines.append((None, None, TOTALS_LINE)) for first_line, unused_cont_line, real_acc in lines: # Let the caller fill in the data to be rendered by adding it to a list # objects. The caller may return multiple cell values; this will create # multiple columns. cells = [] row_classes = [] yield real_acc, cells, row_classes # If no cells were added, skip the line. If you want to render empty # cells, append empty strings. if not cells: continue # Render the row write(''.format(' '.join(row_classes))) if real_acc is TOTALS_LINE: label = '' else: label = (formatter.render_account(real_acc.account) if formatter else real_acc.account) write(''.format(label)) # Add columns for each value rendered. for cell in cells: write(''.format(cell)) write('') write('
    {}{}
    {}{}
    ')","title":"tree_table()"},{"location":"api_reference/beancount.scripts.html","text":"beancount.scripts \uf0c1 Implementation of the various scripts available from bin. This is structured this way because we want all the significant codes under a single directory, for analysis, grepping and unit testing. beancount.scripts.bake \uf0c1 Bake a Beancount input file's web files to a directory hierarchy. You provide a Beancount filename, an output directory, and this script runs a server and a scraper that puts all the files in the directory, and if your output name has an archive suffix, we automatically the fetched directory contents to the archive and delete them. beancount.scripts.bake.archive(command_template, directory, archive, quiet=False) \uf0c1 Archive the directory to the given tar/gz archive filename. Parameters: command_template \u2013 A string, the command template to format with in order to compute the command to run. directory \u2013 A string, the name of the directory to archive. archive \u2013 A string, the name of the file to output. quiet \u2013 A boolean, True to suppress output. Exceptions: IOError \u2013 if the directory does not exist or if the archive name already Source code in beancount/scripts/bake.py def archive(command_template, directory, archive, quiet=False): \"\"\"Archive the directory to the given tar/gz archive filename. Args: command_template: A string, the command template to format with in order to compute the command to run. directory: A string, the name of the directory to archive. archive: A string, the name of the file to output. quiet: A boolean, True to suppress output. Raises: IOError: if the directory does not exist or if the archive name already exists. \"\"\" directory = path.abspath(directory) archive = path.abspath(archive) if not path.exists(directory): raise IOError(\"Directory to archive '{}' does not exist\".format( directory)) if path.exists(archive): raise IOError(\"Output archive name '{}' already exists\".format( archive)) command = command_template.format(directory=directory, dirname=path.dirname(directory), basename=path.basename(directory), archive=archive) pipe = subprocess.Popen(shlex.split(command), shell=False, cwd=path.dirname(directory), stdout=subprocess.PIPE if quiet else None, stderr=subprocess.PIPE if quiet else None) _, _ = pipe.communicate() if pipe.returncode != 0: raise OSError(\"Archive failure\") beancount.scripts.bake.archive_zip(directory, archive) \uf0c1 Archive the directory to the given tar/gz archive filename. Parameters: directory \u2013 A string, the name of the directory to archive. archive \u2013 A string, the name of the file to output. Source code in beancount/scripts/bake.py def archive_zip(directory, archive): \"\"\"Archive the directory to the given tar/gz archive filename. Args: directory: A string, the name of the directory to archive. archive: A string, the name of the file to output. \"\"\" # Figure out optimal level of compression among the supported ones in this # installation. for spec, compression in [ ('lzma', zipfile.ZIP_LZMA), ('bz2', zipfile.ZIP_BZIP2), ('zlib', zipfile.ZIP_DEFLATED)]: if importlib.util.find_spec(spec): zip_compression = compression break else: # Default is no compression. zip_compression = zipfile.ZIP_STORED with file_utils.chdir(directory), zipfile.ZipFile( archive, 'w', compression=zip_compression) as archfile: for root, dirs, files in os.walk(directory): for filename in files: relpath = path.relpath(path.join(root, filename), directory) archfile.write(relpath) beancount.scripts.bake.bake_to_directory(webargs, output_dir, render_all_pages=True) \uf0c1 Serve and bake a Beancount's web to a directory. Parameters: webargs \u2013 An argparse parsed options object with the web app arguments. output_dir \u2013 A directory name. We don't check here whether it exists or not. quiet \u2013 A boolean, True to suppress web server fetch log. render_all_pages \u2013 If true, fetch the full set of pages, not just the subset that is palatable. Returns: True on success, False otherwise. Source code in beancount/scripts/bake.py def bake_to_directory(webargs, output_dir, render_all_pages=True): \"\"\"Serve and bake a Beancount's web to a directory. Args: webargs: An argparse parsed options object with the web app arguments. output_dir: A directory name. We don't check here whether it exists or not. quiet: A boolean, True to suppress web server fetch log. render_all_pages: If true, fetch the full set of pages, not just the subset that is palatable. Returns: True on success, False otherwise. \"\"\" callback = functools.partial(save_scraped_document, output_dir) if render_all_pages: ignore_regexps = None else: regexps = [ # Skip the context pages, too slow. r'/context/', # Skip the link pages, too slow. r'/link/', # Skip the component pages... too many. r'/view/component/', # Skip served documents. r'/.*/doc/', # Skip monthly pages. r'/view/year/\\d\\d\\d\\d/month/', ] ignore_regexps = '({})'.format('|'.join(regexps)) processed_urls, skipped_urls = web.scrape_webapp(webargs, callback, ignore_regexps) beancount.scripts.bake.normalize_filename(url) \uf0c1 Convert URL paths to filenames. Add .html extension if needed. Parameters: url \u2013 A string, the url to convert. Returns: A string, possibly with an extension appended. Source code in beancount/scripts/bake.py def normalize_filename(url): \"\"\"Convert URL paths to filenames. Add .html extension if needed. Args: url: A string, the url to convert. Returns: A string, possibly with an extension appended. \"\"\" if url.endswith('/'): return path.join(url, 'index.html') elif BINARY_MATCH(url): return url else: return url if url.endswith('.html') else (url + '.html') beancount.scripts.bake.relativize_links(html, current_url) \uf0c1 Make all the links in the contents string relative to an URL. Parameters: html \u2013 An lxml document node. current_url \u2013 A string, the URL of the current page, a path to. a file or a directory. If the path represents a directory, the path ends with a /. Source code in beancount/scripts/bake.py def relativize_links(html, current_url): \"\"\"Make all the links in the contents string relative to an URL. Args: html: An lxml document node. current_url: A string, the URL of the current page, a path to. a file or a directory. If the path represents a directory, the path ends with a /. \"\"\" current_dir = path.dirname(current_url) for element, attribute, link, pos in lxml.html.iterlinks(html): if path.isabs(link): relative_link = path.relpath(normalize_filename(link), current_dir) element.set(attribute, relative_link) beancount.scripts.bake.remove_links(html, targets) \uf0c1 Convert a list of anchors () from an HTML tree to spans (). Parameters: html \u2013 An lxml document node. targets \u2013 A set of string, targets to be removed. Source code in beancount/scripts/bake.py def remove_links(html, targets): \"\"\"Convert a list of anchors () from an HTML tree to spans (). Args: html: An lxml document node. targets: A set of string, targets to be removed. \"\"\" for element, attribute, link, pos in lxml.html.iterlinks(html): if link in targets: del element.attrib[attribute] element.tag = 'span' element.set('class', 'removed-link') beancount.scripts.bake.save_scraped_document(output_dir, url, response, contents, html_root, skipped_urls) \uf0c1 Callback function to process a document being scraped. This converts the document to have relative links and writes out the file to the output directory. Parameters: output_dir \u2013 A string, the output directory to write. url \u2013 A string, the originally requested URL. response \u2013 An http response as per urlopen. contents \u2013 Bytes, the content of a response. html_root \u2013 An lxml root node for the document, optionally. If this is provided, this avoid you having to reprocess it (for performance reasons). skipped_urls \u2013 A set of the links from the file that were skipped. Source code in beancount/scripts/bake.py def save_scraped_document(output_dir, url, response, contents, html_root, skipped_urls): \"\"\"Callback function to process a document being scraped. This converts the document to have relative links and writes out the file to the output directory. Args: output_dir: A string, the output directory to write. url: A string, the originally requested URL. response: An http response as per urlopen. contents: Bytes, the content of a response. html_root: An lxml root node for the document, optionally. If this is provided, this avoid you having to reprocess it (for performance reasons). skipped_urls: A set of the links from the file that were skipped. \"\"\" if response.status != 200: logging.error(\"Invalid status: %s\", response.status) # Ignore directories. if url.endswith('/'): return # Note that we're saving the file under the non-redirected URL, because this # will have to be opened using files and there are no redirects that way. if response.info().get_content_type() == 'text/html': if html_root is None: html_root = lxml.html.document_fromstring(contents) remove_links(html_root, skipped_urls) relativize_links(html_root, url) contents = lxml.html.tostring(html_root, method=\"html\") # Compute output filename and write out the relativized contents. output_filename = path.join(output_dir, normalize_filename(url).lstrip('/')) os.makedirs(path.dirname(output_filename), exist_ok=True) with open(output_filename, 'wb') as outfile: outfile.write(contents) beancount.scripts.check \uf0c1 Parse, check and realize a beancount input file. This also measures the time it takes to run all these steps. beancount.scripts.deps \uf0c1 Check the installation dependencies and report the version numbers of each. This is meant to be used as an error diagnostic tool. beancount.scripts.deps.check_cdecimal() \uf0c1 Check that Python 3.3 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_cdecimal(): \"\"\"Check that Python 3.3 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" # Note: this code mirrors and should be kept in-sync with that at the top of # beancount.core.number. # Try the built-in installation. import decimal if is_fast_decimal(decimal): return ('cdecimal', '{} (built-in)'.format(decimal.__version__), True) # Try an explicitly installed version. try: import cdecimal if is_fast_decimal(cdecimal): return ('cdecimal', getattr(cdecimal, '__version__', 'OKAY'), True) except ImportError: pass # Not found. return ('cdecimal', None, False) beancount.scripts.deps.check_dependencies() \uf0c1 Check the runtime dependencies and report their version numbers. Returns: A list of pairs of (package-name, version-number, sufficient) whereby if a package has not been installed, its 'version-number' will be set to None. Otherwise, it will be a string with the version number in it. 'sufficient' will be True if the version if sufficient for this installation of Beancount. Source code in beancount/scripts/deps.py def check_dependencies(): \"\"\"Check the runtime dependencies and report their version numbers. Returns: A list of pairs of (package-name, version-number, sufficient) whereby if a package has not been installed, its 'version-number' will be set to None. Otherwise, it will be a string with the version number in it. 'sufficient' will be True if the version if sufficient for this installation of Beancount. \"\"\" return [ # Check for a complete installation of Python itself. check_python(), check_cdecimal(), # Modules we really do need installed. check_import('dateutil'), check_import('bottle'), check_import('ply', module_name='ply.yacc', min_version='3.4'), check_import('lxml', module_name='lxml.etree', min_version='3'), # Optionally required to upload data to Google Drive. check_import('googleapiclient'), check_import('oauth2client'), check_import('httplib2'), # Optionally required to support various price source fetchers. check_import('requests', min_version='2.0'), # Optionally required to support imports (identify, extract, file) code. check_python_magic(), check_import('beautifulsoup4', module_name='bs4', min_version='4'), ] beancount.scripts.deps.check_import(package_name, min_version=None, module_name=None) \uf0c1 Check that a particular module name is installed. Parameters: package_name \u2013 A string, the name of the package and module to be imported to verify this works. This should have a version attribute on it. min_version \u2013 If not None, a string, the minimum version number we require. module_name \u2013 The name of the module to import if it differs from the package name. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_import(package_name, min_version=None, module_name=None): \"\"\"Check that a particular module name is installed. Args: package_name: A string, the name of the package and module to be imported to verify this works. This should have a __version__ attribute on it. min_version: If not None, a string, the minimum version number we require. module_name: The name of the module to import if it differs from the package name. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" if module_name is None: module_name = package_name try: __import__(module_name) module = sys.modules[module_name] if min_version is not None: version = module.__version__ assert isinstance(version, str) is_sufficient = (parse_version(version) >= parse_version(min_version) if min_version else True) else: version, is_sufficient = None, True except ImportError: version, is_sufficient = None, False return (package_name, version, is_sufficient) beancount.scripts.deps.check_python() \uf0c1 Check that Python 3.3 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_python(): \"\"\"Check that Python 3.3 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" return ('python3', '.'.join(map(str, sys.version_info[:3])), sys.version_info[:2] >= (3, 3)) beancount.scripts.deps.check_python_magic() \uf0c1 Check that a recent-enough version of python-magic is installed. python-magic is an interface to libmagic, which is used by the 'file' tool and UNIX to identify file types. Note that there are two Python wrappers which provide the 'magic' import: python-magic and filemagic. The former is what we need, which appears to be more recently maintained. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_python_magic(): \"\"\"Check that a recent-enough version of python-magic is installed. python-magic is an interface to libmagic, which is used by the 'file' tool and UNIX to identify file types. Note that there are two Python wrappers which provide the 'magic' import: python-magic and filemagic. The former is what we need, which appears to be more recently maintained. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" try: import magic # Check that python-magic and not filemagic is installed. if not hasattr(magic, 'from_file'): # 'filemagic' is installed; install python-magic. raise ImportError return ('python-magic', 'OK', True) except ImportError: return ('python-magic', None, False) beancount.scripts.deps.is_fast_decimal(decimal_module) \uf0c1 Return true if a fast C decimal implementation is installed. Source code in beancount/scripts/deps.py def is_fast_decimal(decimal_module): \"Return true if a fast C decimal implementation is installed.\" return isinstance(decimal_module.Decimal().sqrt, types.BuiltinFunctionType) beancount.scripts.deps.list_dependencies(file=<_io.TextIOWrapper name='' mode='w' encoding='utf-8'>) \uf0c1 Check the dependencies and produce a listing on the given file. Parameters: file \u2013 A file object to write the output to. Source code in beancount/scripts/deps.py def list_dependencies(file=sys.stderr): \"\"\"Check the dependencies and produce a listing on the given file. Args: file: A file object to write the output to. \"\"\" print(\"Dependencies:\") for package, version, sufficient in check_dependencies(): print(\" {:16}: {} {}\".format( package, version or 'NOT INSTALLED', \"(INSUFFICIENT)\" if version and not sufficient else \"\"), file=file) beancount.scripts.deps.parse_version(version_str) \uf0c1 Parse the version string into a comparable tuple. Source code in beancount/scripts/deps.py def parse_version(version_str: str) -> str: \"\"\"Parse the version string into a comparable tuple.\"\"\" return [int(v) for v in version_str.split('.')] beancount.scripts.directories \uf0c1 Check that document directories mirror a list of accounts correctly. beancount.scripts.directories.ValidateDirectoryError ( Exception ) \uf0c1 A directory validation error. beancount.scripts.directories.validate_directories(entries, document_dirs) \uf0c1 Validate a directory hierarchy against a ledger's account names. Read a ledger's list of account names and check that all the capitalized subdirectory names under the given roots match the account names. Parameters: entries \u2013 A list of directives. document_dirs \u2013 A list of string, the directory roots to walk and validate. Source code in beancount/scripts/directories.py def validate_directories(entries, document_dirs): \"\"\"Validate a directory hierarchy against a ledger's account names. Read a ledger's list of account names and check that all the capitalized subdirectory names under the given roots match the account names. Args: entries: A list of directives. document_dirs: A list of string, the directory roots to walk and validate. \"\"\" # Get the list of accounts declared in the ledge. accounts = getters.get_accounts(entries) # For each of the roots, validate the hierarchy of directories. for document_dir in document_dirs: errors = validate_directory(accounts, document_dir) for error in errors: print(\"ERROR: {}\".format(error)) beancount.scripts.directories.validate_directory(accounts, document_dir) \uf0c1 Check a directory hierarchy against a list of valid accounts. Walk the directory hierarchy, and for all directories with names matching that of accounts (with \":\" replaced with \"/\"), check that they refer to an account name declared in the given list. Parameters: account \u2013 A set or dict of account names. document_dir \u2013 A string, the root directory to walk and validate. Returns: An errors for each invalid directory name found. Source code in beancount/scripts/directories.py def validate_directory(accounts, document_dir): \"\"\"Check a directory hierarchy against a list of valid accounts. Walk the directory hierarchy, and for all directories with names matching that of accounts (with \":\" replaced with \"/\"), check that they refer to an account name declared in the given list. Args: account: A set or dict of account names. document_dir: A string, the root directory to walk and validate. Returns: An errors for each invalid directory name found. \"\"\" # Generate all parent accounts in the account_set we're checking against, so # that parent directories with no corresponding account don't warn. accounts_with_parents = set(accounts) for account_ in accounts: while True: parent = account.parent(account_) if not parent: break if parent in accounts_with_parents: break accounts_with_parents.add(parent) account_ = parent errors = [] for directory, account_name, _, _ in account.walk(document_dir): if account_name not in accounts_with_parents: errors.append(ValidateDirectoryError( \"Invalid directory '{}': no corresponding account '{}'\".format( directory, account_name))) return errors beancount.scripts.doctor \uf0c1 Debugging tool for those finding bugs in Beancount. This tool is able to dump lexer/parser state, and will provide other services in the name of debugging. beancount.scripts.doctor.RenderError ( tuple ) \uf0c1 RenderError(source, message, entry) beancount.scripts.doctor.RenderError.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/doctor.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.scripts.doctor.RenderError.__new__(_cls, source, message, entry) special staticmethod \uf0c1 Create new instance of RenderError(source, message, entry) beancount.scripts.doctor.RenderError.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/scripts/doctor.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.scripts.doctor.do_checkdeps(*unused_args) \uf0c1 Report on the runtime dependencies. Parameters: unused_args \u2013 Ignored. Source code in beancount/scripts/doctor.py def do_deps(*unused_args): \"\"\"Report on the runtime dependencies. Args: unused_args: Ignored. \"\"\" from beancount.scripts import deps deps.list_dependencies(sys.stdout) print('') print('Use \"pip3 install \" to install new packages.') beancount.scripts.doctor.do_context(filename, args) \uf0c1 Describe the context that a particular transaction is applied to. Parameters: filename \u2013 A string, which consists in the filename. args \u2013 A tuple of the rest of arguments. We're expecting the first argument to be a string which contains either a lineno integer or a filename:lineno combination (which can be used if the location is not in the top-level file). Source code in beancount/scripts/doctor.py def do_context(filename, args): \"\"\"Describe the context that a particular transaction is applied to. Args: filename: A string, which consists in the filename. args: A tuple of the rest of arguments. We're expecting the first argument to be a string which contains either a lineno integer or a filename:lineno combination (which can be used if the location is not in the top-level file). \"\"\" from beancount.reports import context from beancount import loader # Check we have the required number of arguments. if len(args) != 1: raise SystemExit(\"Missing line number argument.\") # Load the input files. entries, errors, options_map = loader.load_file(filename) # Parse the arguments, get the line number. match = re.match(r\"(.+):(\\d+)$\", args[0]) if match: search_filename = path.abspath(match.group(1)) lineno = int(match.group(2)) elif re.match(r\"(\\d+)$\", args[0]): # Note: Make sure to use the absolute filename used by the parser to # resolve the file. search_filename = options_map['filename'] lineno = int(args[0]) else: raise SystemExit(\"Invalid format for location.\") str_context = context.render_file_context(entries, options_map, search_filename, lineno) sys.stdout.write(str_context) beancount.scripts.doctor.do_deps(*unused_args) \uf0c1 Report on the runtime dependencies. Parameters: unused_args \u2013 Ignored. Source code in beancount/scripts/doctor.py def do_deps(*unused_args): \"\"\"Report on the runtime dependencies. Args: unused_args: Ignored. \"\"\" from beancount.scripts import deps deps.list_dependencies(sys.stdout) print('') print('Use \"pip3 install \" to install new packages.') beancount.scripts.doctor.do_directories(filename, args) \uf0c1 Validate a directory hierarchy against a ledger's account names. Read a ledger's list of account names and check that all the capitalized subdirectory names under the given roots match the account names. Parameters: filename \u2013 A string, the Beancount input filename. args \u2013 The rest of the arguments provided on the command-line, which in this case will be interpreted as the names of root directories to validate against the accounts in the given ledger. Source code in beancount/scripts/doctor.py def do_directories(filename, args): \"\"\"Validate a directory hierarchy against a ledger's account names. Read a ledger's list of account names and check that all the capitalized subdirectory names under the given roots match the account names. Args: filename: A string, the Beancount input filename. args: The rest of the arguments provided on the command-line, which in this case will be interpreted as the names of root directories to validate against the accounts in the given ledger. \"\"\" from beancount import loader from beancount.scripts import directories entries, _, __ = loader.load_file(filename) directories.validate_directories(entries, args) beancount.scripts.doctor.do_display_context(filename, args) \uf0c1 Print out the precision inferred from the parsed numbers in the input file. Parameters: filename \u2013 A string, which consists in the filename. args \u2013 A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. Source code in beancount/scripts/doctor.py def do_display_context(filename, args): \"\"\"Print out the precision inferred from the parsed numbers in the input file. Args: filename: A string, which consists in the filename. args: A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. \"\"\" from beancount import loader entries, errors, options_map = loader.load_file(filename) dcontext = options_map['dcontext'] sys.stdout.write(str(dcontext)) beancount.scripts.doctor.do_dump_lexer(filename, unused_args) \uf0c1 Dump the lexer output for a Beancount syntax file. Parameters: filename \u2013 A string, the Beancount input filename. Source code in beancount/scripts/doctor.py def do_lex(filename, unused_args): \"\"\"Dump the lexer output for a Beancount syntax file. Args: filename: A string, the Beancount input filename. \"\"\" from beancount.parser import lexer for token, lineno, text, obj in lexer.lex_iter(filename): sys.stdout.write('{:12} {:6d} {}\\n'.format( '(None)' if token is None else token, lineno, repr(text))) beancount.scripts.doctor.do_lex(filename, unused_args) \uf0c1 Dump the lexer output for a Beancount syntax file. Parameters: filename \u2013 A string, the Beancount input filename. Source code in beancount/scripts/doctor.py def do_lex(filename, unused_args): \"\"\"Dump the lexer output for a Beancount syntax file. Args: filename: A string, the Beancount input filename. \"\"\" from beancount.parser import lexer for token, lineno, text, obj in lexer.lex_iter(filename): sys.stdout.write('{:12} {:6d} {}\\n'.format( '(None)' if token is None else token, lineno, repr(text))) beancount.scripts.doctor.do_linked(filename, args) \uf0c1 Print out a list of transactions linked to the one at the given line. Parameters: filename \u2013 A string, which consists in the filename. args \u2013 A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. Source code in beancount/scripts/doctor.py def do_linked(filename, args): \"\"\"Print out a list of transactions linked to the one at the given line. Args: filename: A string, which consists in the filename. args: A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. \"\"\" from beancount.parser import options from beancount.parser import printer from beancount.core import account_types from beancount.core import inventory from beancount.core import data from beancount.core import realization from beancount import loader # Parse the arguments, get the line number. if len(args) != 1: raise SystemExit(\"Missing line number argument.\") lineno = int(args[0]) # Load the input file. entries, errors, options_map = loader.load_file(filename) # Find the closest entry. closest_entry = data.find_closest(entries, options_map['filename'], lineno) # Find its links. if closest_entry is None: raise SystemExit(\"No entry could be found before {}:{}\".format(filename, lineno)) links = (closest_entry.links if isinstance(closest_entry, data.Transaction) else data.EMPTY_SET) if not links: linked_entries = [closest_entry] else: # Find all linked entries. # # Note that there is an option here: You can either just look at the links # on the closest entry, or you can include the links of the linked # transactions as well. Whichever one you want depends on how you use your # links. Best would be to query the user (in Emacs) when there are many # links present. follow_links = True if not follow_links: linked_entries = [entry for entry in entries if (isinstance(entry, data.Transaction) and entry.links and entry.links & links)] else: links = set(links) linked_entries = [] while True: num_linked = len(linked_entries) linked_entries = [entry for entry in entries if (isinstance(entry, data.Transaction) and entry.links and entry.links & links)] if len(linked_entries) == num_linked: break for entry in linked_entries: if entry.links: links.update(entry.links) # Render linked entries (in date order) as errors (for Emacs). errors = [RenderError(entry.meta, '', entry) for entry in linked_entries] printer.print_errors(errors) # Print out balances. real_root = realization.realize(linked_entries) dformat = options_map['dcontext'].build(alignment=display_context.Align.DOT, reserved=2) realization.dump_balances(real_root, dformat, file=sys.stdout) # Print out net income change. acctypes = options.get_account_types(options_map) net_income = inventory.Inventory() for real_node in realization.iter_children(real_root): if account_types.is_income_statement_account(real_node.account, acctypes): net_income.add_inventory(real_node.balance) print() print('Net Income: {}'.format(-net_income)) beancount.scripts.doctor.do_list_options(*unused_args) \uf0c1 Print out a list of the available options. Parameters: unused_args \u2013 Ignored. Source code in beancount/scripts/doctor.py def do_list_options(*unused_args): \"\"\"Print out a list of the available options. Args: unused_args: Ignored. \"\"\" from beancount.parser import options print(options.list_options()) beancount.scripts.doctor.do_missing_open(filename, args) \uf0c1 Print out Open directives that are missing for the given input file. This can be useful during demos in order to quickly generate all the required Open directives without having to type them manually. Parameters: filename \u2013 A string, which consists in the filename. args \u2013 A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. Source code in beancount/scripts/doctor.py def do_missing_open(filename, args): \"\"\"Print out Open directives that are missing for the given input file. This can be useful during demos in order to quickly generate all the required Open directives without having to type them manually. Args: filename: A string, which consists in the filename. args: A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. \"\"\" from beancount.parser import printer from beancount.core import data from beancount.core import getters from beancount import loader entries, errors, options_map = loader.load_file(filename) # Get accounts usage and open directives. first_use_map, _ = getters.get_accounts_use_map(entries) open_close_map = getters.get_account_open_close(entries) new_entries = [] for account, first_use_date in first_use_map.items(): if account not in open_close_map: new_entries.append( data.Open(data.new_metadata(filename, 0), first_use_date, account, None, None)) dcontext = options_map['dcontext'] printer.print_entries(data.sorted(new_entries), dcontext) beancount.scripts.doctor.do_parse(filename, unused_args) \uf0c1 Run the parser in debug mode. Parameters: filename \u2013 A string, the Beancount input filename. Source code in beancount/scripts/doctor.py def do_parse(filename, unused_args): \"\"\"Run the parser in debug mode. Args: filename: A string, the Beancount input filename. \"\"\" from beancount.parser import parser entries, errors, _ = parser.parse_file(filename, yydebug=1) beancount.scripts.doctor.do_print_options(filename, *args) \uf0c1 Print out the actual options parsed from a file. Parameters: unused_args \u2013 Ignored. Source code in beancount/scripts/doctor.py def do_print_options(filename, *args): \"\"\"Print out the actual options parsed from a file. Args: unused_args: Ignored. \"\"\" from beancount import loader _, __, options_map = loader.load_file(filename) for key, value in sorted(options_map.items()): print('{}: {}'.format(key, value)) beancount.scripts.doctor.do_roundtrip(filename, unused_args) \uf0c1 Round-trip test on arbitrary Ledger. Read a Ledger's transactions, print them out, re-read them again and compare them. Both sets of parsed entries should be equal. Both printed files are output to disk, so you can also run diff on them yourself afterwards. Parameters: filename \u2013 A string, the Beancount input filename. Source code in beancount/scripts/doctor.py def do_roundtrip(filename, unused_args): \"\"\"Round-trip test on arbitrary Ledger. Read a Ledger's transactions, print them out, re-read them again and compare them. Both sets of parsed entries should be equal. Both printed files are output to disk, so you can also run diff on them yourself afterwards. Args: filename: A string, the Beancount input filename. \"\"\" from beancount.parser import printer from beancount.core import compare from beancount import loader round1_filename = round2_filename = None try: logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s') logging.info(\"Read the entries\") entries, errors, options_map = loader.load_file(filename) printer.print_errors(errors, file=sys.stderr) logging.info(\"Print them out to a file\") basename, extension = path.splitext(filename) round1_filename = ''.join([basename, '.roundtrip1', extension]) with open(round1_filename, 'w') as outfile: printer.print_entries(entries, file=outfile) logging.info(\"Read the entries from that file\") # Note that we don't want to run any of the auto-generation here, but # parsing now returns incomplete objects and we assume idempotence on a # file that was output from the printer after having been processed, so # it shouldn't add anything new. That is, a processed file printed and # resolve when parsed again should contain the same entries, i.e. # nothing new should be generated. entries_roundtrip, errors, options_map = loader.load_file(round1_filename) # Print out the list of errors from parsing the results. if errors: print(',----------------------------------------------------------------------') printer.print_errors(errors, file=sys.stdout) print('`----------------------------------------------------------------------') logging.info(\"Print what you read to yet another file\") round2_filename = ''.join([basename, '.roundtrip2', extension]) with open(round2_filename, 'w') as outfile: printer.print_entries(entries_roundtrip, file=outfile) logging.info(\"Compare the original entries with the re-read ones\") same, missing1, missing2 = compare.compare_entries(entries, entries_roundtrip) if same: logging.info('Entries are the same. Congratulations.') else: logging.error('Entries differ!') print() print('\\n\\nMissing from original:') for entry in entries: print(entry) print(compare.hash_entry(entry)) print(printer.format_entry(entry)) print() print('\\n\\nMissing from round-trip:') for entry in missing2: print(entry) print(compare.hash_entry(entry)) print(printer.format_entry(entry)) print() finally: for rfilename in (round1_filename, round2_filename): if path.exists(rfilename): os.remove(rfilename) beancount.scripts.doctor.do_validate_html(directory, args) \uf0c1 Validate all the HTML files under a directory hierarchy. Parameters: directory \u2013 A string, the root directory whose contents to validate. args \u2013 A tuple of the rest of arguments. Source code in beancount/scripts/doctor.py def do_validate_html(directory, args): \"\"\"Validate all the HTML files under a directory hierarchy. Args: directory: A string, the root directory whose contents to validate. args: A tuple of the rest of arguments. \"\"\" from beancount.web import scrape files, missing, empty = scrape.validate_local_links_in_dir(directory) logging.info('%d files processed', len(files)) for target in missing: logging.error('Missing %s', target) for target in empty: logging.error('Empty %s', target) beancount.scripts.doctor.get_commands() \uf0c1 Return a list of available commands in this file. Returns: A list of pairs of (command-name string, docstring). Source code in beancount/scripts/doctor.py def get_commands(): \"\"\"Return a list of available commands in this file. Returns: A list of pairs of (command-name string, docstring). \"\"\" commands = [] for attr_name, attr_value in globals().items(): match = re.match('do_(.*)', attr_name) if match: commands.append((match.group(1), misc_utils.first_paragraph(attr_value.__doc__))) return commands beancount.scripts.example \uf0c1 Generate a decently-sized example history, based on some rules. This script is used to generate some meaningful input to Beancount, input that looks as realistic as possible for a moderately complex mock individual. This can also be used as an input generator for a stress test for performance evaluation. beancount.scripts.example.check_non_negative(entries, account, currency) \uf0c1 Check that the balance of the given account never goes negative. Parameters: entries \u2013 A list of directives. account \u2013 An account string, the account to check the balance for. currency \u2013 A string, the currency to check minimums for. Exceptions: AssertionError \u2013 if the balance goes negative. Source code in beancount/scripts/example.py def check_non_negative(entries, account, currency): \"\"\"Check that the balance of the given account never goes negative. Args: entries: A list of directives. account: An account string, the account to check the balance for. currency: A string, the currency to check minimums for. Raises: AssertionError: if the balance goes negative. \"\"\" previous_date = None for txn_posting, balances in postings_for(data.sorted(entries), [account], before=True): balance = balances[account] date = txn_posting.txn.date if date != previous_date: assert all(pos.units.number >= ZERO for pos in balance.get_positions()), ( \"Negative balance: {} at: {}\".format(balance, txn_posting.txn.date)) previous_date = date beancount.scripts.example.compute_trip_dates(date_begin, date_end) \uf0c1 Generate dates at reasonable intervals for trips during the given time period. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Pairs of dates for the trips within the period. Source code in beancount/scripts/example.py def compute_trip_dates(date_begin, date_end): \"\"\"Generate dates at reasonable intervals for trips during the given time period. Args: date_begin: The start date. date_end: The end date. Yields: Pairs of dates for the trips within the period. \"\"\" # Min and max number of days remaining at home. days_at_home = (4*30, 13*30) # Length of trip. days_trip = (8, 22) # Number of days to ensure no trip at the beginning and the end. days_buffer = 21 date_begin += datetime.timedelta(days=days_buffer) date_end -= datetime.timedelta(days=days_buffer) date = date_begin while 1: duration_at_home = datetime.timedelta(days=random.randint(*days_at_home)) duration_trip = datetime.timedelta(days=random.randint(*days_trip)) date_trip_begin = date + duration_at_home date_trip_end = date_trip_begin + duration_trip if date_trip_end >= date_end: break yield (date_trip_begin, date_trip_end) date = date_trip_end beancount.scripts.example.contextualize_file(contents, employer) \uf0c1 Replace generic strings in the generated file with realistic strings. Parameters: contents \u2013 A string, the generic file contents. Returns: A string, the contextualized version. Source code in beancount/scripts/example.py def contextualize_file(contents, employer): \"\"\"Replace generic strings in the generated file with realistic strings. Args: contents: A string, the generic file contents. Returns: A string, the contextualized version. \"\"\" replacements = { 'CC': 'US', 'Bank1': 'BofA', 'Bank1_Institution': 'Bank of America', 'Bank1_Address': '123 America Street, LargeTown, USA', 'Bank1_Phone': '+1.012.345.6789', 'CreditCard1': 'Chase:Slate', 'CreditCard2': 'Amex:BlueCash', 'Employer1': employer, 'Retirement': 'Vanguard', 'Retirement_Institution': 'Vanguard Group', 'Retirement_Address': \"P.O. Box 1110, Valley Forge, PA 19482-1110\", 'Retirement_Phone': \"+1.800.523.1188\", 'Investment': 'ETrade', # Commodities 'CCY': 'USD', 'VACHR': 'VACHR', 'DEFCCY': 'IRAUSD', 'MFUND1': 'VBMPX', 'MFUND2': 'RGAGX', 'STK1': 'ITOT', 'STK2': 'VEA', 'STK3': 'VHT', 'STK4': 'GLD', } new_contents = replace(contents, replacements) return new_contents, replacements beancount.scripts.example.date_iter(date_begin, date_end) \uf0c1 Generate a sequence of dates. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Instances of datetime.date. Source code in beancount/scripts/example.py def date_iter(date_begin, date_end): \"\"\"Generate a sequence of dates. Args: date_begin: The start date. date_end: The end date. Yields: Instances of datetime.date. \"\"\" assert date_begin <= date_end date = date_begin one_day = datetime.timedelta(days=1) while date < date_end: date += one_day yield date beancount.scripts.example.date_random_seq(date_begin, date_end, days_min, days_max) \uf0c1 Generate a sequence of dates with some random increase in days. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. days_min \u2013 The minimum number of days to advance on each iteration. days_max \u2013 The maximum number of days to advance on each iteration. Yields: Instances of datetime.date. Source code in beancount/scripts/example.py def date_random_seq(date_begin, date_end, days_min, days_max): \"\"\"Generate a sequence of dates with some random increase in days. Args: date_begin: The start date. date_end: The end date. days_min: The minimum number of days to advance on each iteration. days_max: The maximum number of days to advance on each iteration. Yields: Instances of datetime.date. \"\"\" assert days_min > 0 assert days_min <= days_max date = date_begin while date < date_end: nb_days_forward = random.randint(days_min, days_max) date += datetime.timedelta(days=nb_days_forward) if date >= date_end: break yield date beancount.scripts.example.delay_dates(date_iter, delay_days_min, delay_days_max) \uf0c1 Delay the dates from the given iterator by some uniformly drawn number of days. Parameters: date_iter \u2013 An iterator of datetime.date instances. delay_days_min \u2013 The minimum amount of advance days for the transaction. delay_days_max \u2013 The maximum amount of advance days for the transaction. Yields: datetime.date instances. Source code in beancount/scripts/example.py def delay_dates(date_iter, delay_days_min, delay_days_max): \"\"\"Delay the dates from the given iterator by some uniformly drawn number of days. Args: date_iter: An iterator of datetime.date instances. delay_days_min: The minimum amount of advance days for the transaction. delay_days_max: The maximum amount of advance days for the transaction. Yields: datetime.date instances. \"\"\" dates = list(date_iter) last_date = dates[-1] last_date = last_date.date() if isinstance(last_date, datetime.datetime) else last_date for dtime in dates: date = dtime.date() if isinstance(dtime, datetime.datetime) else dtime date += datetime.timedelta(days=random.randint(delay_days_min, delay_days_max)) if date >= last_date: break yield date beancount.scripts.example.generate_balance_checks(entries, account, date_iter) \uf0c1 Generate balance check entries to the given frequency. Parameters: entries \u2013 A list of directives that contain all the transactions for the accounts. account \u2013 The name of the account for which to generate. date_iter \u2013 Iterator of dates. We generate balance checks at these dates. Returns: A list of balance check entries. Source code in beancount/scripts/example.py def generate_balance_checks(entries, account, date_iter): \"\"\"Generate balance check entries to the given frequency. Args: entries: A list of directives that contain all the transactions for the accounts. account: The name of the account for which to generate. date_iter: Iterator of dates. We generate balance checks at these dates. Returns: A list of balance check entries. \"\"\" balance_checks = [] date_iter = iter(date_iter) next_date = next(date_iter) with misc_utils.swallow(StopIteration): for txn_posting, balance in postings_for(entries, [account], before=True): while txn_posting.txn.date >= next_date: amount = balance[account].get_currency_units('CCY').number balance_checks.extend(parse(\"\"\" {next_date} balance {account} {amount} CCY \"\"\", **locals())) next_date = next(date_iter) return balance_checks beancount.scripts.example.generate_banking(entries, date_begin, date_end, amount_initial) \uf0c1 Generate a checking account opening. Parameters: entries \u2013 A list of entries which affect this account. date_begin \u2013 A date instance, the beginning date. date_end \u2013 A date instance, the end date. amount_initial \u2013 A Decimal instance, the amount to initialize the checking account with. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_banking(entries, date_begin, date_end, amount_initial): \"\"\"Generate a checking account opening. Args: entries: A list of entries which affect this account. date_begin: A date instance, the beginning date. date_end: A date instance, the end date. amount_initial: A Decimal instance, the amount to initialize the checking account with. Returns: A list of directives. \"\"\" amount_initial_neg = -amount_initial new_entries = parse(\"\"\" {date_begin} open Assets:CC:Bank1 institution: \"Bank1_Institution\" address: \"Bank1_Address\" phone: \"Bank1_Phone\" {date_begin} open Assets:CC:Bank1:Checking CCY account: \"00234-48574897\" ;; {date_begin} open Assets:CC:Bank1:Savings CCY {date_begin} * \"Opening Balance for checking account\" Assets:CC:Bank1:Checking {amount_initial} CCY Equity:Opening-Balances {amount_initial_neg} CCY \"\"\", **locals()) date_balance = date_begin + datetime.timedelta(days=1) account = 'Assets:CC:Bank1:Checking' for txn_posting, balances in postings_for(data.sorted(entries + new_entries), [account], before=True): if txn_posting.txn.date >= date_balance: break amount_balance = balances[account].get_currency_units('CCY').number bal_entries = parse(\"\"\" {date_balance} balance Assets:CC:Bank1:Checking {amount_balance} CCY \"\"\", **locals()) return new_entries + bal_entries beancount.scripts.example.generate_banking_expenses(date_begin, date_end, account, rent_amount) \uf0c1 Generate expenses paid out of a checking account, typically living expenses. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. account \u2013 The checking account to generate expenses to. rent_amount \u2013 The amount of rent. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_banking_expenses(date_begin, date_end, account, rent_amount): \"\"\"Generate expenses paid out of a checking account, typically living expenses. Args: date_begin: The start date. date_end: The end date. account: The checking account to generate expenses to. rent_amount: The amount of rent. Returns: A list of directives. \"\"\" fee_expenses = generate_periodic_expenses( rrule.rrule(rrule.MONTHLY, bymonthday=4, dtstart=date_begin, until=date_end), \"BANK FEES\", \"Monthly bank fee\", account, 'Expenses:Financial:Fees', lambda: D('4.00')) rent_expenses = generate_periodic_expenses( delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 2, 5), \"RiverBank Properties\", \"Paying the rent\", account, 'Expenses:Home:Rent', lambda: random.normalvariate(float(rent_amount), 0)) electricity_expenses = generate_periodic_expenses( delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 7, 8), \"EDISON POWER\", \"\", account, 'Expenses:Home:Electricity', lambda: random.normalvariate(65, 0)) internet_expenses = generate_periodic_expenses( delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 20, 22), \"Wine-Tarner Cable\", \"\", account, 'Expenses:Home:Internet', lambda: random.normalvariate(80, 0.10)) phone_expenses = generate_periodic_expenses( delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 17, 19), \"Verizon Wireless\", \"\", account, 'Expenses:Home:Phone', lambda: random.normalvariate(60, 10)) return data.sorted(fee_expenses + rent_expenses + electricity_expenses + internet_expenses + phone_expenses) beancount.scripts.example.generate_clearing_entries(date_iter, payee, narration, entries, account_clear, account_from) \uf0c1 Generate entries to clear the value of an account. Parameters: date_iter \u2013 An iterator of datetime.date instances. payee \u2013 A string, the payee name to use on the transactions. narration \u2013 A string, the narration to use on the transactions. entries \u2013 A list of entries. account_clear \u2013 The account to clear. account_from \u2013 The source account to clear 'account_clear' from. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_clearing_entries(date_iter, payee, narration, entries, account_clear, account_from): \"\"\"Generate entries to clear the value of an account. Args: date_iter: An iterator of datetime.date instances. payee: A string, the payee name to use on the transactions. narration: A string, the narration to use on the transactions. entries: A list of entries. account_clear: The account to clear. account_from: The source account to clear 'account_clear' from. Returns: A list of directives. \"\"\" # The next date we're looking for. next_date = next(iter(date_iter)) # Iterate over all the postings of the account to clear. new_entries = [] for txn_posting, balances in postings_for(entries, [account_clear]): balance_clear = balances[account_clear] # Check if we need to clear. if next_date <= txn_posting.txn.date: pos_amount = balance_clear.get_currency_units('CCY') neg_amount = -pos_amount new_entries.extend(parse(\"\"\" {next_date} * \"{payee}\" \"{narration}\" {account_clear} {neg_amount.number:.2f} CCY {account_from} {pos_amount.number:.2f} CCY \"\"\", **locals())) balance_clear.add_amount(neg_amount) # Advance to the next date we're looking for. try: next_date = next(iter(date_iter)) except StopIteration: break return new_entries beancount.scripts.example.generate_commodity_entries(date_birth) \uf0c1 Create a list of Commodity entries for all the currencies we're using. Parameters: date_birth \u2013 A datetime.date instance, the date of birth of the user. Returns: A list of Commodity entries for all the commodities in use. Source code in beancount/scripts/example.py def generate_commodity_entries(date_birth): \"\"\"Create a list of Commodity entries for all the currencies we're using. Args: date_birth: A datetime.date instance, the date of birth of the user. Returns: A list of Commodity entries for all the commodities in use. \"\"\" return parse(\"\"\" 1792-01-01 commodity USD name: \"US Dollar\" export: \"CASH\" {date_birth} commodity VACHR name: \"Employer Vacation Hours\" export: \"IGNORE\" {date_birth} commodity IRAUSD name: \"US 401k and IRA Contributions\" export: \"IGNORE\" 2009-05-01 commodity RGAGX name: \"American Funds The Growth Fund of America Class R-6\" export: \"MUTF:RGAGX\" price: \"USD:google/MUTF:RGAGX\" 1995-09-18 commodity VBMPX name: \"Vanguard Total Bond Market Index Fund Institutional Plus Shares\" export: \"MUTF:VBMPX\" price: \"USD:google/MUTF:VBMPX\" 2004-01-20 commodity ITOT name: \"iShares Core S&P Total U.S. Stock Market ETF\" export: \"NYSEARCA:ITOT\" price: \"USD:google/NYSEARCA:ITOT\" 2007-07-20 commodity VEA name: \"Vanguard FTSE Developed Markets ETF\" export: \"NYSEARCA:VEA\" price: \"USD:google/NYSEARCA:VEA\" 2004-01-26 commodity VHT name: \"Vanguard Health Care ETF\" export: \"NYSEARCA:VHT\" price: \"USD:google/NYSEARCA:VHT\" 2004-11-01 commodity GLD name: \"SPDR Gold Trust (ETF)\" export: \"NYSEARCA:GLD\" price: \"USD:google/NYSEARCA:GLD\" 1900-01-01 commodity VMMXX export: \"MUTF:VMMXX (MONEY:USD)\" \"\"\", **locals()) beancount.scripts.example.generate_employment_income(employer_name, employer_address, annual_salary, account_deposit, account_retirement, date_begin, date_end) \uf0c1 Generate bi-weekly entries for payroll salary income. Parameters: employer_name \u2013 A string, the human-readable name of the employer. employer_address \u2013 A string, the address of the employer. annual_salary \u2013 A Decimal, the annual salary of the employee. account_deposit \u2013 An account string, the account to deposit the salary to. account_retirement \u2013 An account string, the account to deposit retirement contributions to. date_begin \u2013 The start date. date_end \u2013 The end date. Returns: A list of directives, including open directives for the account. Source code in beancount/scripts/example.py def generate_employment_income(employer_name, employer_address, annual_salary, account_deposit, account_retirement, date_begin, date_end): \"\"\"Generate bi-weekly entries for payroll salary income. Args: employer_name: A string, the human-readable name of the employer. employer_address: A string, the address of the employer. annual_salary: A Decimal, the annual salary of the employee. account_deposit: An account string, the account to deposit the salary to. account_retirement: An account string, the account to deposit retirement contributions to. date_begin: The start date. date_end: The end date. Returns: A list of directives, including open directives for the account. \"\"\" preamble = parse(\"\"\" {date_begin} event \"employer\" \"{employer_name}, {employer_address}\" {date_begin} open Income:CC:Employer1:Salary CCY ;{date_begin} open Income:CC:Employer1:AnnualBonus CCY {date_begin} open Income:CC:Employer1:GroupTermLife CCY {date_begin} open Income:CC:Employer1:Vacation VACHR {date_begin} open Assets:CC:Employer1:Vacation VACHR {date_begin} open Expenses:Vacation VACHR {date_begin} open Expenses:Health:Life:GroupTermLife {date_begin} open Expenses:Health:Medical:Insurance {date_begin} open Expenses:Health:Dental:Insurance {date_begin} open Expenses:Health:Vision:Insurance ;{date_begin} open Expenses:Vacation:Employer \"\"\", **locals()) date_prev = None contrib_retirement = ZERO contrib_socsec = ZERO biweekly_pay = annual_salary / 26 gross = biweekly_pay medicare = gross * D('0.0231') federal = gross * D('0.2303') state = gross * D('0.0791') city = gross * D('0.0379') sdi = D('1.12') lifeinsurance = D('24.32') dental = D('2.90') medical = D('27.38') vision = D('42.30') fixed = (medicare + federal + state + city + sdi + dental + medical + vision) # Calculate vacation hours per-pay. with decimal.localcontext() as ctx: ctx.prec = 4 vacation_hrs = (ANNUAL_VACATION_DAYS * D('8')) / D('26') transactions = [] for dtime in misc_utils.skipiter( rrule.rrule(rrule.WEEKLY, byweekday=rrule.TH, dtstart=date_begin, until=date_end), 2): date = dtime.date() year = date.year if not date_prev or date_prev.year != date.year: contrib_retirement = RETIREMENT_LIMITS.get(date.year, RETIREMENT_LIMITS[None]) contrib_socsec = D('7000') date_prev = date retirement_uncapped = math.ceil((gross * D('0.25')) / 100) * 100 retirement = min(contrib_retirement, retirement_uncapped) contrib_retirement -= retirement socsec_uncapped = gross * D('0.0610') socsec = min(contrib_socsec, socsec_uncapped) contrib_socsec -= socsec with decimal.localcontext() as ctx: ctx.prec = 6 deposit = (gross - retirement - fixed - socsec) retirement_neg = -retirement gross_neg = -gross lifeinsurance_neg = -lifeinsurance vacation_hrs_neg = -vacation_hrs template = \"\"\" {date} * \"{employer_name}\" \"Payroll\" {account_deposit} {deposit:.2f} CCY {account_retirement} {retirement:.2f} CCY Assets:CC:Federal:PreTax401k {retirement_neg:.2f} DEFCCY Expenses:Taxes:Y{year}:CC:Federal:PreTax401k {retirement:.2f} DEFCCY Income:CC:Employer1:Salary {gross_neg:.2f} CCY Income:CC:Employer1:GroupTermLife {lifeinsurance_neg:.2f} CCY Expenses:Health:Life:GroupTermLife {lifeinsurance:.2f} CCY Expenses:Health:Dental:Insurance {dental} CCY Expenses:Health:Medical:Insurance {medical} CCY Expenses:Health:Vision:Insurance {vision} CCY Expenses:Taxes:Y{year}:CC:Medicare {medicare:.2f} CCY Expenses:Taxes:Y{year}:CC:Federal {federal:.2f} CCY Expenses:Taxes:Y{year}:CC:State {state:.2f} CCY Expenses:Taxes:Y{year}:CC:CityNYC {city:.2f} CCY Expenses:Taxes:Y{year}:CC:SDI {sdi:.2f} CCY Expenses:Taxes:Y{year}:CC:SocSec {socsec:.2f} CCY Assets:CC:Employer1:Vacation {vacation_hrs:.2f} VACHR Income:CC:Employer1:Vacation {vacation_hrs_neg:.2f} VACHR \"\"\" if retirement == ZERO: # Remove retirement lines. template = '\\n'.join(line for line in template.splitlines() if not re.search(r'\\bretirement\\b', line)) transactions.extend(parse(template, **locals())) return preamble + transactions beancount.scripts.example.generate_expense_accounts(date_birth) \uf0c1 Generate directives for expense accounts. Parameters: date_birth \u2013 Birth date of the character. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_expense_accounts(date_birth): \"\"\"Generate directives for expense accounts. Args: date_birth: Birth date of the character. Returns: A list of directives. \"\"\" return parse(\"\"\" {date_birth} open Expenses:Food:Groceries {date_birth} open Expenses:Food:Restaurant {date_birth} open Expenses:Food:Coffee {date_birth} open Expenses:Food:Alcohol {date_birth} open Expenses:Transport:Tram {date_birth} open Expenses:Home:Rent {date_birth} open Expenses:Home:Electricity {date_birth} open Expenses:Home:Internet {date_birth} open Expenses:Home:Phone {date_birth} open Expenses:Financial:Fees {date_birth} open Expenses:Financial:Commissions \"\"\", **locals()) beancount.scripts.example.generate_open_entries(date, accounts, currency=None) \uf0c1 Generate a list of Open entries for the given accounts: Parameters: date \u2013 A datetime.date instance for the open entries. accounts \u2013 A list of account strings. currency \u2013 An optional currency constraint. Returns: A list of Open directives. Source code in beancount/scripts/example.py def generate_open_entries(date, accounts, currency=None): \"\"\"Generate a list of Open entries for the given accounts: Args: date: A datetime.date instance for the open entries. accounts: A list of account strings. currency: An optional currency constraint. Returns: A list of Open directives. \"\"\" assert isinstance(accounts, (list, tuple)) return parse(''.join( '{date} open {account} {currency}\\n'.format(date=date, account=account, currency=currency or '') for account in accounts)) beancount.scripts.example.generate_outgoing_transfers(entries, account, account_out, transfer_minimum, transfer_threshold, transfer_increment) \uf0c1 Generate transfers of accumulated funds out of an account. This monitors the balance of an account and when it is beyond a threshold, generate out transfers form that account to another account. Parameters: entries \u2013 A list of existing entries that affect this account so far. The generated entries will also affect this account. account \u2013 An account string, the account to monitor. account_out \u2013 An account string, the savings account to make transfers to. transfer_minimum \u2013 The minimum amount of funds to always leave in this account after a transfer. transfer_threshold \u2013 The minimum amount of funds to be able to transfer out without breaking the minimum. transfer_increment \u2013 A Decimal, the increment to round transfers to. Returns: A list of new directives, the transfers to add to the given account. Source code in beancount/scripts/example.py def generate_outgoing_transfers(entries, account, account_out, transfer_minimum, transfer_threshold, transfer_increment): \"\"\"Generate transfers of accumulated funds out of an account. This monitors the balance of an account and when it is beyond a threshold, generate out transfers form that account to another account. Args: entries: A list of existing entries that affect this account so far. The generated entries will also affect this account. account: An account string, the account to monitor. account_out: An account string, the savings account to make transfers to. transfer_minimum: The minimum amount of funds to always leave in this account after a transfer. transfer_threshold: The minimum amount of funds to be able to transfer out without breaking the minimum. transfer_increment: A Decimal, the increment to round transfers to. Returns: A list of new directives, the transfers to add to the given account. \"\"\" last_date = entries[-1].date # Reverse the balance amounts taking into account the minimum balance for # all time in the future. amounts = [(balances[account].get_currency_units('CCY').number, txn_posting) for txn_posting, balances in postings_for(entries, [account])] reversed_amounts = [] last_amount, _ = amounts[-1] for current_amount, _ in reversed(amounts): if current_amount < last_amount: reversed_amounts.append(current_amount) last_amount = current_amount else: reversed_amounts.append(last_amount) capped_amounts = reversed(reversed_amounts) # Create transfers outward where the future allows it. new_entries = [] offset_amount = ZERO for current_amount, (_, txn_posting) in zip(capped_amounts, amounts): if txn_posting.txn.date >= last_date: break adjusted_amount = current_amount - offset_amount if adjusted_amount > (transfer_minimum + transfer_threshold): amount_transfer = round_to(adjusted_amount - transfer_minimum, transfer_increment) date = txn_posting.txn.date + datetime.timedelta(days=1) amount_transfer_neg = -amount_transfer new_entries.extend(parse(\"\"\" {date} * \"Transfering accumulated savings to other account\" {account} {amount_transfer_neg:2f} CCY {account_out} {amount_transfer:2f} CCY \"\"\", **locals())) offset_amount += amount_transfer return new_entries beancount.scripts.example.generate_periodic_expenses(date_iter, payee, narration, account_from, account_to, amount_generator) \uf0c1 Generate periodic expense transactions. Parameters: date_iter \u2013 An iterator for dates or datetimes. payee \u2013 A string, the payee name to use on the transactions, or a set of such strings to randomly choose from narration \u2013 A string, the narration to use on the transactions. account_from \u2013 An account string the debited account. account_to \u2013 An account string the credited account. amount_generator \u2013 A callable object to generate variates. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_periodic_expenses(date_iter, payee, narration, account_from, account_to, amount_generator): \"\"\"Generate periodic expense transactions. Args: date_iter: An iterator for dates or datetimes. payee: A string, the payee name to use on the transactions, or a set of such strings to randomly choose from narration: A string, the narration to use on the transactions. account_from: An account string the debited account. account_to: An account string the credited account. amount_generator: A callable object to generate variates. Returns: A list of directives. \"\"\" new_entries = [] for dtime in date_iter: date = dtime.date() if isinstance(dtime, datetime.datetime) else dtime amount = D(amount_generator()) txn_payee = (payee if isinstance(payee, str) else random.choice(payee)) txn_narration = (narration if isinstance(narration, str) else random.choice(narration)) amount_neg = -amount new_entries.extend(parse(\"\"\" {date} * \"{txn_payee}\" \"{txn_narration}\" {account_from} {amount_neg:.2f} CCY {account_to} {amount:.2f} CCY \"\"\", **locals())) return new_entries beancount.scripts.example.generate_prices(date_begin, date_end, currencies, cost_currency) \uf0c1 Generate weekly or monthly price entries for the given currencies. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. currencies \u2013 A list of currency strings to generate prices for. cost_currency \u2013 A string, the cost currency. Returns: A list of Price directives. Source code in beancount/scripts/example.py def generate_prices(date_begin, date_end, currencies, cost_currency): \"\"\"Generate weekly or monthly price entries for the given currencies. Args: date_begin: The start date. date_end: The end date. currencies: A list of currency strings to generate prices for. cost_currency: A string, the cost currency. Returns: A list of Price directives. \"\"\" digits = D('0.01') entries = [] counter = itertools.count() for currency in currencies: start_price = random.uniform(30, 200) growth = random.uniform(0.02, 0.13) # %/year mu = growth * (7 / 365) sigma = random.uniform(0.005, 0.02) # Vol for dtime, price_float in zip(rrule.rrule(rrule.WEEKLY, byweekday=rrule.FR, dtstart=date_begin, until=date_end), price_series(start_price, mu, sigma)): price = D(price_float).quantize(digits) meta = data.new_metadata(generate_prices.__name__, next(counter)) entry = data.Price(meta, dtime.date(), currency, amount.Amount(price, cost_currency)) entries.append(entry) return entries beancount.scripts.example.generate_regular_credit_expenses(date_birth, date_begin, date_end, account_credit, account_checking) \uf0c1 Generate expenses paid out of a credit card account, including payments to the credit card. Parameters: date_birth \u2013 The user's birth date. date_begin \u2013 The start date. date_end \u2013 The end date. account_credit \u2013 The credit card account to generate expenses against. account_checking \u2013 The checking account to generate payments from. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_regular_credit_expenses(date_birth, date_begin, date_end, account_credit, account_checking): \"\"\"Generate expenses paid out of a credit card account, including payments to the credit card. Args: date_birth: The user's birth date. date_begin: The start date. date_end: The end date. account_credit: The credit card account to generate expenses against. account_checking: The checking account to generate payments from. Returns: A list of directives. \"\"\" restaurant_expenses = generate_periodic_expenses( date_random_seq(date_begin, date_end, 1, 5), RESTAURANT_NAMES, RESTAURANT_NARRATIONS, account_credit, 'Expenses:Food:Restaurant', lambda: min(random.lognormvariate(math.log(30), math.log(1.5)), random.randint(200, 220))) groceries_expenses = generate_periodic_expenses( date_random_seq(date_begin, date_end, 5, 20), GROCERIES_NAMES, \"Buying groceries\", account_credit, 'Expenses:Food:Groceries', lambda: min(random.lognormvariate(math.log(80), math.log(1.3)), random.randint(250, 300))) subway_expenses = generate_periodic_expenses( date_random_seq(date_begin, date_end, 27, 33), \"Metro Transport Authority\", \"Tram tickets\", account_credit, 'Expenses:Transport:Tram', lambda: D('120.00')) credit_expenses = data.sorted(restaurant_expenses + groceries_expenses + subway_expenses) # Entries to open accounts. credit_preamble = generate_open_entries(date_birth, [account_credit], 'CCY') return data.sorted(credit_preamble + credit_expenses) beancount.scripts.example.generate_retirement_employer_match(entries, account_invest, account_income) \uf0c1 Generate employer matching contributions into a retirement account. Parameters: entries \u2013 A list of directives that cover the retirement account. account_invest \u2013 The name of the retirement cash account. account_income \u2013 The name of the income account. Returns: A list of new entries generated for employer contributions. Source code in beancount/scripts/example.py def generate_retirement_employer_match(entries, account_invest, account_income): \"\"\"Generate employer matching contributions into a retirement account. Args: entries: A list of directives that cover the retirement account. account_invest: The name of the retirement cash account. account_income: The name of the income account. Returns: A list of new entries generated for employer contributions. \"\"\" match_frac = D('0.50') new_entries = parse(\"\"\" {date} open {account_income} CCY \"\"\", date=entries[0].date, account_income=account_income) for txn_posting, balances in postings_for(entries, [account_invest]): amount = txn_posting.posting.units.number * match_frac amount_neg = -amount date = txn_posting.txn.date + ONE_DAY new_entries.extend(parse(\"\"\" {date} * \"Employer match for contribution\" {account_invest} {amount:.2f} CCY {account_income} {amount_neg:.2f} CCY \"\"\", **locals())) return new_entries beancount.scripts.example.generate_retirement_investments(entries, account, commodities_items, price_map) \uf0c1 Invest money deposited to the given retirement account. Parameters: entries \u2013 A list of directives account \u2013 The root account for all retirement investment sub-accounts. commodities_items \u2013 A list of (commodity, fraction to be invested in) items. price_map \u2013 A dict of prices, as per beancount.core.prices.build_price_map(). Returns: A list of new directives for the given investments. This also generates account opening directives for the desired investment commodities. Source code in beancount/scripts/example.py def generate_retirement_investments(entries, account, commodities_items, price_map): \"\"\"Invest money deposited to the given retirement account. Args: entries: A list of directives account: The root account for all retirement investment sub-accounts. commodities_items: A list of (commodity, fraction to be invested in) items. price_map: A dict of prices, as per beancount.core.prices.build_price_map(). Returns: A list of new directives for the given investments. This also generates account opening directives for the desired investment commodities. \"\"\" open_entries = [] account_cash = join(account, 'Cash') date_origin = entries[0].date open_entries.extend(parse(\"\"\" {date_origin} open {account} CCY institution: \"Retirement_Institution\" address: \"Retirement_Address\" phone: \"Retirement_Phone\" {date_origin} open {account_cash} CCY number: \"882882\" \"\"\", **locals())) for currency, _ in commodities_items: open_entries.extend(parse(\"\"\" {date_origin} open {account}:{currency} {currency} number: \"882882\" \"\"\", **locals())) new_entries = [] for txn_posting, balances in postings_for(entries, [account_cash]): balance = balances[account_cash] amount_to_invest = balance.get_currency_units('CCY').number # Find the date the following Monday, the date to invest. txn_date = txn_posting.txn.date while txn_date.weekday() != calendar.MONDAY: txn_date += ONE_DAY amount_invested = ZERO for commodity, fraction in commodities_items: amount_fraction = amount_to_invest * D(fraction) # Find the price at that date. _, price = prices.get_price(price_map, (commodity, 'CCY'), txn_date) units = (amount_fraction / price).quantize(D('0.001')) amount_cash = (units * price).quantize(D('0.01')) amount_cash_neg = -amount_cash new_entries.extend(parse(\"\"\" {txn_date} * \"Investing {fraction:.0%} of cash in {commodity}\" {account}:{commodity} {units:.3f} {commodity} {{{price:.2f} CCY}} {account}:Cash {amount_cash_neg:.2f} CCY \"\"\", **locals())) balance.add_amount(amount.Amount(-amount_cash, 'CCY')) return data.sorted(open_entries + new_entries) beancount.scripts.example.generate_tax_accounts(year, date_max) \uf0c1 Generate accounts and contribution directives for a particular tax year. Parameters: year \u2013 An integer, the year we're to generate this for. date_max \u2013 The maximum date to produce an entry for. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_tax_accounts(year, date_max): \"\"\"Generate accounts and contribution directives for a particular tax year. Args: year: An integer, the year we're to generate this for. date_max: The maximum date to produce an entry for. Returns: A list of directives. \"\"\" date_year = datetime.date(year, 1, 1) date_filing = (datetime.date(year + 1, 3, 20) + datetime.timedelta(days=random.randint(0, 5))) date_federal = (date_filing + datetime.timedelta(days=random.randint(0, 4))) date_state = (date_filing + datetime.timedelta(days=random.randint(0, 4))) quantum = D('0.01') amount_federal = D(max(random.normalvariate(500, 120), 12)).quantize(quantum) amount_federal_neg = -amount_federal amount_state = D(max(random.normalvariate(300, 100), 10)).quantize(quantum) amount_state_neg = -amount_state amount_payable = -(amount_federal + amount_state) amount_limit = RETIREMENT_LIMITS.get(year, RETIREMENT_LIMITS[None]) amount_limit_neg = -amount_limit entries = parse(\"\"\" ;; Open tax accounts for that year. {date_year} open Expenses:Taxes:Y{year}:CC:Federal:PreTax401k DEFCCY {date_year} open Expenses:Taxes:Y{year}:CC:Medicare CCY {date_year} open Expenses:Taxes:Y{year}:CC:Federal CCY {date_year} open Expenses:Taxes:Y{year}:CC:CityNYC CCY {date_year} open Expenses:Taxes:Y{year}:CC:SDI CCY {date_year} open Expenses:Taxes:Y{year}:CC:State CCY {date_year} open Expenses:Taxes:Y{year}:CC:SocSec CCY ;; Check that the tax amounts have been fully used. {date_year} balance Assets:CC:Federal:PreTax401k 0 DEFCCY {date_year} * \"Allowed contributions for one year\" Income:CC:Federal:PreTax401k {amount_limit_neg} DEFCCY Assets:CC:Federal:PreTax401k {amount_limit} DEFCCY {date_filing} * \"Filing taxes for {year}\" Expenses:Taxes:Y{year}:CC:Federal {amount_federal:.2f} CCY Expenses:Taxes:Y{year}:CC:State {amount_state:.2f} CCY Liabilities:AccountsPayable {amount_payable:.2f} CCY {date_federal} * \"FEDERAL TAXPYMT\" Assets:CC:Bank1:Checking {amount_federal_neg:.2f} CCY Liabilities:AccountsPayable {amount_federal:.2f} CCY {date_state} * \"STATE TAX & FINANC PYMT\" Assets:CC:Bank1:Checking {amount_state_neg:.2f} CCY Liabilities:AccountsPayable {amount_state:.2f} CCY \"\"\", **locals()) return [entry for entry in entries if entry.date < date_max] beancount.scripts.example.generate_tax_preamble(date_birth) \uf0c1 Generate tax declarations not specific to any particular year. Parameters: date_birth \u2013 A date instance, the birth date of the character. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_tax_preamble(date_birth): \"\"\"Generate tax declarations not specific to any particular year. Args: date_birth: A date instance, the birth date of the character. Returns: A list of directives. \"\"\" return parse(\"\"\" ;; Tax accounts not specific to a year. {date_birth} open Income:CC:Federal:PreTax401k DEFCCY {date_birth} open Assets:CC:Federal:PreTax401k DEFCCY \"\"\", **locals()) beancount.scripts.example.generate_taxable_investment(date_begin, date_end, entries, price_map, stocks) \uf0c1 Generate opening directives and transactions for an investment account. Parameters: date_begin \u2013 A date instance, the beginning date. date_end \u2013 A date instance, the end date. entries \u2013 A list of entries that contains at least the transfers to the investment account's cash account. price_map \u2013 A dict of prices, as per beancount.core.prices.build_price_map(). stocks \u2013 A list of strings, the list of commodities to invest in. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_taxable_investment(date_begin, date_end, entries, price_map, stocks): \"\"\"Generate opening directives and transactions for an investment account. Args: date_begin: A date instance, the beginning date. date_end: A date instance, the end date. entries: A list of entries that contains at least the transfers to the investment account's cash account. price_map: A dict of prices, as per beancount.core.prices.build_price_map(). stocks: A list of strings, the list of commodities to invest in. Returns: A list of directives. \"\"\" account = 'Assets:CC:Investment' account_cash = join(account, 'Cash') account_gains = 'Income:CC:Investment:Gains' account_dividends = 'Income:CC:Investment:Dividends' accounts_stocks = ['Assets:CC:Investment:{}'.format(commodity) for commodity in stocks] open_entries = parse(\"\"\" {date_begin} open {account}:Cash CCY {date_begin} open {account_gains} CCY {date_begin} open {account_dividends} CCY \"\"\", **locals()) for stock in stocks: open_entries.extend(parse(\"\"\" {date_begin} open {account}:{stock} {stock} \"\"\", **locals())) # Figure out dates at which dividends should be distributed, near the end of # each quarter. days_to = datetime.timedelta(days=3*90-10) dividend_dates = [] for quarter_begin in iter_quarters(date_begin, date_end): end_of_quarter = quarter_begin + days_to if not (date_begin < end_of_quarter < date_end): continue dividend_dates.append(end_of_quarter) # Iterate over all the dates, but merging in the postings for the cash # account. min_amount = D('1000.00') round_amount = D('100.00') commission = D('8.95') round_units = D('1') frac_invest = D('1.00') frac_dividend = D('0.004') p_daily_buy = 1./15 # days p_daily_sell = 1./90 # days stocks_inventory = inventory.Inventory() new_entries = [] dividend_date_iter = iter(dividend_dates) next_dividend_date = next(dividend_date_iter) for date, balances in iter_dates_with_balance(date_begin, date_end, entries, [account_cash]): # Check if we should insert a dividend. Note that we could not factor # this out because we want to explicitly reinvest the cash dividends and # we also want the dividends to be proportional to the amount of # invested stock, so one feeds on the other and vice-versa. if next_dividend_date and date > next_dividend_date: # Compute the total balances for the stock accounts in order to # create a realistic dividend. total = inventory.Inventory() for account_stock in accounts_stocks: total.add_inventory(balances[account_stock]) # Create an entry offering dividends of 1% of the portfolio. portfolio_cost = total.reduce(convert.get_cost).get_currency_units('CCY').number amount_cash = (frac_dividend * portfolio_cost).quantize(D('0.01')) amount_cash_neg = -amount_cash dividend = parse(\"\"\" {next_dividend_date} * \"Dividends on portfolio\" {account}:Cash {amount_cash:.2f} CCY {account_dividends} {amount_cash_neg:.2f} CCY \"\"\", **locals())[0] new_entries.append(dividend) # Advance the next dividend date. try: next_dividend_date = next(dividend_date_iter) except StopIteration: next_dividend_date = None # If the balance is high, buy with high probability. balance = balances[account_cash] total_cash = balance.get_currency_units('CCY').number assert total_cash >= ZERO, ('Cash balance is negative: {}'.format(total_cash)) invest_cash = total_cash * frac_invest - commission if invest_cash > min_amount: if random.random() < p_daily_buy: commodities = random.sample(stocks, random.randint(1, len(stocks))) lot_amount = round_to(invest_cash / len(commodities), round_amount) invested_amount = ZERO for stock in commodities: # Find the price at that date. _, price = prices.get_price(price_map, (stock, 'CCY'), date) units = round_to((lot_amount / price), round_units) if units <= ZERO: continue amount_cash = -(units * price + commission) # logging.info('Buying %s %s @ %s CCY = %s CCY', # units, stock, price, units * price) buy = parse(\"\"\" {date} * \"Buy shares of {stock}\" {account}:Cash {amount_cash:.2f} CCY {account}:{stock} {units:.0f} {stock} {{{price:.2f} CCY}} Expenses:Financial:Commissions {commission:.2f} CCY \"\"\", **locals())[0] new_entries.append(buy) account_stock = ':'.join([account, stock]) balances[account_cash].add_position(buy.postings[0]) balances[account_stock].add_position(buy.postings[1]) stocks_inventory.add_position(buy.postings[1]) # Don't sell on days you buy. continue # Otherwise, sell with low probability. if not stocks_inventory.is_empty() and random.random() < p_daily_sell: # Choose the lot with the highest gain or highest loss. gains = [] for position in stocks_inventory.get_positions(): base_quote = (position.units.currency, position.cost.currency) _, price = prices.get_price(price_map, base_quote, date) if price == position.cost.number: continue # Skip lots without movement. market_value = position.units.number * price book_value = convert.get_cost(position).number gain = market_value - book_value gains.append((gain, market_value, price, position)) if not gains: continue # Sell either biggest winner or biggest loser. biggest = bool(random.random() < 0.5) lot_tuple = sorted(gains)[0 if biggest else -1] gain, market_value, price, sell_position = lot_tuple #logging.info('Selling {} for {}'.format(sell_position, market_value)) sell_position = -sell_position stock = sell_position.units.currency amount_cash = market_value - commission amount_gain = -gain sell = parse(\"\"\" {date} * \"Sell shares of {stock}\" {account}:{stock} {sell_position} @ {price:.2f} CCY {account}:Cash {amount_cash:.2f} CCY Expenses:Financial:Commissions {commission:.2f} CCY {account_gains} {amount_gain:.2f} CCY \"\"\", **locals())[0] new_entries.append(sell) balances[account_cash].add_position(sell.postings[1]) stocks_inventory.add_position(sell.postings[0]) continue return open_entries + new_entries beancount.scripts.example.generate_trip_entries(date_begin, date_end, tag, config, trip_city, home_city, account_credit) \uf0c1 Generate more dense expenses for a trip. Parameters: date_begin \u2013 A datetime.date instance, the beginning of the trip. date_end \u2013 A datetime.date instance, the end of the trip. tag \u2013 A string, the name of the tag. config \u2013 A list of (payee name, account name, (mu, 3sigma)), where mu is the mean of the prices to generate and 3sigma is 3 times the standard deviation. trip_city \u2013 A string, the capitalized name of the destination city. home_city \u2013 A string, the name of the home city. account_credit \u2013 A string, the name of the credit card account to pay the expenses from. Returns: A list of entries for the trip, all tagged with the given tag. Source code in beancount/scripts/example.py def generate_trip_entries(date_begin, date_end, tag, config, trip_city, home_city, account_credit): \"\"\"Generate more dense expenses for a trip. Args: date_begin: A datetime.date instance, the beginning of the trip. date_end: A datetime.date instance, the end of the trip. tag: A string, the name of the tag. config: A list of (payee name, account name, (mu, 3sigma)), where mu is the mean of the prices to generate and 3sigma is 3 times the standard deviation. trip_city: A string, the capitalized name of the destination city. home_city: A string, the name of the home city. account_credit: A string, the name of the credit card account to pay the expenses from. Returns: A list of entries for the trip, all tagged with the given tag. \"\"\" p_day_generate = 0.3 new_entries = [] for date in date_iter(date_begin, date_end): for payee, account_expense, (mu, sigma3) in config: if random.random() < p_day_generate: amount = random.normalvariate(mu, sigma3 / 3.) amount_neg = -amount new_entries.extend(parse(\"\"\" {date} * \"{payee}\" \"\" #{tag} {account_credit} {amount_neg:.2f} CCY {account_expense} {amount:.2f} CCY \"\"\", **locals())) # Consume the vacation days. vacation_hrs = (date_end - date_begin).days * 8 # hrs/day new_entries.extend(parse(\"\"\" {date_end} * \"Consume vacation days\" Assets:CC:Employer1:Vacation -{vacation_hrs:.2f} VACHR Expenses:Vacation {vacation_hrs:.2f} VACHR \"\"\", **locals())) # Generate events for the trip. new_entries.extend(parse(\"\"\" {date_begin} event \"location\" \"{trip_city}\" {date_end} event \"location\" \"{home_city}\" \"\"\", **locals())) return new_entries beancount.scripts.example.get_minimum_balance(entries, account, currency) \uf0c1 Compute the minimum balance of the given account according to the entries history. Parameters: entries \u2013 A list of directives. account \u2013 An account string. currency \u2013 A currency string, for which we want to compute the minimum. Returns: A Decimal number, the minimum amount throughout the history of this account. Source code in beancount/scripts/example.py def get_minimum_balance(entries, account, currency): \"\"\"Compute the minimum balance of the given account according to the entries history. Args: entries: A list of directives. account: An account string. currency: A currency string, for which we want to compute the minimum. Returns: A Decimal number, the minimum amount throughout the history of this account. \"\"\" min_amount = ZERO for _, balances in postings_for(entries, [account]): balance = balances[account] current = balance.get_currency_units(currency).number if current < min_amount: min_amount = current return min_amount beancount.scripts.example.iter_dates_with_balance(date_begin, date_end, entries, accounts) \uf0c1 Iterate over dates, including the balances of the postings iterator. Parameters: postings_iter \u2013 An iterator of postings as per postings_for(). date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Pairs of (data, balances) objects, with date: A datetime.date instance balances: An Inventory object, representing the current balance. You can modify the inventory object to feed back changes in the balance. Source code in beancount/scripts/example.py def iter_dates_with_balance(date_begin, date_end, entries, accounts): \"\"\"Iterate over dates, including the balances of the postings iterator. Args: postings_iter: An iterator of postings as per postings_for(). date_begin: The start date. date_end: The end date. Yields: Pairs of (data, balances) objects, with date: A datetime.date instance balances: An Inventory object, representing the current balance. You can modify the inventory object to feed back changes in the balance. \"\"\" balances = collections.defaultdict(inventory.Inventory) merged_txn_postings = iter(merge_postings(entries, accounts)) txn_posting = next(merged_txn_postings, None) for date in date_iter(date_begin, date_end): while txn_posting and txn_posting.txn.date == date: posting = txn_posting.posting balances[posting.account].add_position(posting) txn_posting = next(merged_txn_postings, None) yield date, balances beancount.scripts.example.iter_quarters(date_begin, date_end) \uf0c1 Iterate over all quarters between begin and end dates. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Instances of datetime.date at the beginning of the quarters. This will include the quarter of the beginning date and of the end date. Source code in beancount/scripts/example.py def iter_quarters(date_begin, date_end): \"\"\"Iterate over all quarters between begin and end dates. Args: date_begin: The start date. date_end: The end date. Yields: Instances of datetime.date at the beginning of the quarters. This will include the quarter of the beginning date and of the end date. \"\"\" quarter = (date_begin.year, (date_begin.month-1)//3) quarter_last = (date_end.year, (date_end.month-1)//3) assert quarter <= quarter_last while True: year, trimester = quarter yield datetime.date(year, trimester*3 + 1, 1) if quarter == quarter_last: break trimester = (trimester + 1) % 4 if trimester == 0: year += 1 quarter = (year, trimester) beancount.scripts.example.merge_postings(entries, accounts) \uf0c1 Merge all the postings from the given account names. Parameters: entries \u2013 A list of directives. accounts \u2013 A list of account strings to get the balances for. Yields: A list of TxnPosting's for all the accounts, in sorted order. Source code in beancount/scripts/example.py def merge_postings(entries, accounts): \"\"\"Merge all the postings from the given account names. Args: entries: A list of directives. accounts: A list of account strings to get the balances for. Yields: A list of TxnPosting's for all the accounts, in sorted order. \"\"\" real_root = realization.realize(entries) merged_postings = [] for account in accounts: real_account = realization.get(real_root, account) if real_account is None: continue merged_postings.extend(txn_posting for txn_posting in real_account.txn_postings if isinstance(txn_posting, data.TxnPosting)) merged_postings.sort(key=lambda txn_posting: txn_posting.txn.date) return merged_postings beancount.scripts.example.parse(input_string, **replacements) \uf0c1 Parse some input string and assert no errors. This parse function does not just create the object, it also triggers local interpolation to fill in the missing amounts. Parameters: input_string \u2013 Beancount input text. **replacements \u2013 A dict of keywords to replace to their values. Returns: A list of directive objects. Source code in beancount/scripts/example.py def parse(input_string, **replacements): \"\"\"Parse some input string and assert no errors. This parse function does not just create the object, it also triggers local interpolation to fill in the missing amounts. Args: input_string: Beancount input text. **replacements: A dict of keywords to replace to their values. Returns: A list of directive objects. \"\"\" if replacements: class IgnoreFormatter(string.Formatter): def check_unused_args(self, used_args, args, kwargs): pass formatter = IgnoreFormatter() formatted_string = formatter.format(input_string, **replacements) else: formatted_string = input_string entries, errors, options_map = parser.parse_string(textwrap.dedent(formatted_string)) if errors: printer.print_errors(errors, file=sys.stderr) raise ValueError(\"Parsed text has errors\") # Interpolation. entries, unused_balance_errors = booking.book(entries, options_map) return data.sorted(entries) beancount.scripts.example.postings_for(entries, accounts, before=False) \uf0c1 Realize the entries and get the list of postings for the given accounts. All the non-Posting directives are already filtered out. Parameters: entries \u2013 A list of directives. accounts \u2013 A list of account strings to get the balances for. before \u2013 A boolean, if true, yield the balance before the position is applied. The default is to yield the balance after applying the position. Yields: Tuples of: posting: An instance of TxnPosting balances: A dict of Inventory balances for the given accounts after applying the posting. These inventory objects can be mutated to adjust the balance due to generated transactions to be applied later. Source code in beancount/scripts/example.py def postings_for(entries, accounts, before=False): \"\"\"Realize the entries and get the list of postings for the given accounts. All the non-Posting directives are already filtered out. Args: entries: A list of directives. accounts: A list of account strings to get the balances for. before: A boolean, if true, yield the balance before the position is applied. The default is to yield the balance after applying the position. Yields: Tuples of: posting: An instance of TxnPosting balances: A dict of Inventory balances for the given accounts _after_ applying the posting. These inventory objects can be mutated to adjust the balance due to generated transactions to be applied later. \"\"\" assert isinstance(accounts, list) merged_txn_postings = merge_postings(entries, accounts) balances = collections.defaultdict(inventory.Inventory) for txn_posting in merged_txn_postings: if before: yield txn_posting, balances posting = txn_posting.posting balances[posting.account].add_position(posting) if not before: yield txn_posting, balances beancount.scripts.example.price_series(start, mu, sigma) \uf0c1 Generate a price series based on a simple stochastic model. Parameters: start \u2013 The beginning value. mu \u2013 The per-step drift, in units of value. sigma \u2013 Volatility of the changes. Yields: Floats, at each step. Source code in beancount/scripts/example.py def price_series(start, mu, sigma): \"\"\"Generate a price series based on a simple stochastic model. Args: start: The beginning value. mu: The per-step drift, in units of value. sigma: Volatility of the changes. Yields: Floats, at each step. \"\"\" value = start while 1: yield value value += random.normalvariate(mu, sigma) * value beancount.scripts.example.replace(string, replacements, strip=False) \uf0c1 Apply word-boundaried regular expression replacements to an indented string. Parameters: string \u2013 Some input template string. replacements \u2013 A dict of regexp to replacement value. strip \u2013 A boolean, true if we should strip the input. Returns: The input string with the replacements applied to it, with the indentation removed. Source code in beancount/scripts/example.py def replace(string, replacements, strip=False): \"\"\"Apply word-boundaried regular expression replacements to an indented string. Args: string: Some input template string. replacements: A dict of regexp to replacement value. strip: A boolean, true if we should strip the input. Returns: The input string with the replacements applied to it, with the indentation removed. \"\"\" output = textwrap.dedent(string) if strip: output = output.strip() for from_, to_ in replacements.items(): if not isinstance(to_, str) and not callable(to_): to_ = str(to_) output = re.sub(r'\\b{}\\b'.format(from_), to_, output) return output beancount.scripts.example.validate_output(contents, positive_accounts, currency) \uf0c1 Check that the output file validates. Parameters: contents \u2013 A string, the output file. positive_accounts \u2013 A list of strings, account names to check for non-negative balances. currency \u2013 A string, the currency to check minimums for. Exceptions: AssertionError \u2013 If the output does not validate. Source code in beancount/scripts/example.py def validate_output(contents, positive_accounts, currency): \"\"\"Check that the output file validates. Args: contents: A string, the output file. positive_accounts: A list of strings, account names to check for non-negative balances. currency: A string, the currency to check minimums for. Raises: AssertionError: If the output does not validate. \"\"\" loaded_entries, _, _ = loader.load_string( contents, log_errors=sys.stderr, extra_validations=validation.HARDCORE_VALIDATIONS) # Sanity checks: Check that the checking balance never goes below zero. for account in positive_accounts: check_non_negative(loaded_entries, account, currency) beancount.scripts.example.write_example_file(date_birth, date_begin, date_end, reformat, file) \uf0c1 Generate the example file. Parameters: date_birth \u2013 A datetime.date instance, the birth date of our character. date_begin \u2013 A datetime.date instance, the beginning date at which to generate transactions. date_end \u2013 A datetime.date instance, the end date at which to generate transactions. reformat \u2013 A boolean, true if we should apply global reformatting to this file. file \u2013 A file object, where to write out the output. Source code in beancount/scripts/example.py def write_example_file(date_birth, date_begin, date_end, reformat, file): \"\"\"Generate the example file. Args: date_birth: A datetime.date instance, the birth date of our character. date_begin: A datetime.date instance, the beginning date at which to generate transactions. date_end: A datetime.date instance, the end date at which to generate transactions. reformat: A boolean, true if we should apply global reformatting to this file. file: A file object, where to write out the output. \"\"\" # The following code entirely writes out the output to generic names, such # as \"Employer1\", \"Bank1\", and \"CCY\" (for principal currency). Those names # are purposely chosen to be unique, and only near the very end do we make # renamings to more specific and realistic names. # Name of the checking account. account_opening = 'Equity:Opening-Balances' account_payable = 'Liabilities:AccountsPayable' account_checking = 'Assets:CC:Bank1:Checking' account_credit = 'Liabilities:CC:CreditCard1' account_retirement = 'Assets:CC:Retirement' account_investing = 'Assets:CC:Investment:Cash' # Commodities. commodity_entries = generate_commodity_entries(date_birth) # Estimate the rent. rent_amount = round_to(ANNUAL_SALARY / RENT_DIVISOR, RENT_INCREMENT) # Get a random employer. employer_name, employer_address = random.choice(EMPLOYERS) logging.info(\"Generating Salary Employment Income\") income_entries = generate_employment_income(employer_name, employer_address, ANNUAL_SALARY, account_checking, join(account_retirement, 'Cash'), date_begin, date_end) logging.info(\"Generating Expenses from Banking Accounts\") banking_expenses = generate_banking_expenses(date_begin, date_end, account_checking, rent_amount) logging.info(\"Generating Regular Expenses via Credit Card\") credit_regular_entries = generate_regular_credit_expenses( date_birth, date_begin, date_end, account_credit, account_checking) logging.info(\"Generating Credit Card Expenses for Trips\") trip_entries = [] destinations = sorted(TRIP_DESTINATIONS.items()) destinations.extend(destinations) random.shuffle(destinations) for (date_trip_begin, date_trip_end), (destination_name, config) in zip( compute_trip_dates(date_begin, date_end), destinations): # Compute a suitable tag. tag = 'trip-{}-{}'.format(destination_name.lower().replace(' ', '-'), date_trip_begin.year) #logging.info(\"%s -- %s %s\", tag, date_trip_begin, date_trip_end) # Remove regular entries during this trip. credit_regular_entries = [entry for entry in credit_regular_entries if not(date_trip_begin <= entry.date < date_trip_end)] # Generate entries for the trip. this_trip_entries = generate_trip_entries( date_trip_begin, date_trip_end, tag, config, destination_name.replace('-', ' ').title(), HOME_NAME, account_credit) trip_entries.extend(this_trip_entries) logging.info(\"Generating Credit Card Payment Entries\") credit_payments = generate_clearing_entries( delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end, bymonthday=7), 0, 4), \"CreditCard1\", \"Paying off credit card\", credit_regular_entries, account_credit, account_checking) credit_entries = credit_regular_entries + trip_entries + credit_payments logging.info(\"Generating Tax Filings and Payments\") tax_preamble = generate_tax_preamble(date_birth) # Figure out all the years we need tax accounts for. years = set() for account_name in getters.get_accounts(income_entries): match = re.match(r'Expenses:Taxes:Y(\\d\\d\\d\\d)', account_name) if match: years.add(int(match.group(1))) taxes = [(year, generate_tax_accounts(year, date_end)) for year in sorted(years)] tax_entries = tax_preamble + functools.reduce(operator.add, (entries for _, entries in taxes)) logging.info(\"Generating Opening of Banking Accounts\") # Open banking accounts and gift the checking account with a balance that # will offset all the amounts to ensure a positive balance throughout its # lifetime. entries_for_banking = data.sorted(income_entries + banking_expenses + credit_entries + tax_entries) minimum = get_minimum_balance(entries_for_banking, account_checking, 'CCY') banking_entries = generate_banking(entries_for_banking, date_begin, date_end, max(-minimum, ZERO)) logging.info(\"Generating Transfers to Investment Account\") banking_transfers = generate_outgoing_transfers( data.sorted(income_entries + banking_entries + banking_expenses + credit_entries + tax_entries), account_checking, account_investing, transfer_minimum=D('200'), transfer_threshold=D('3000'), transfer_increment=D('500')) logging.info(\"Generating Prices\") # Generate price entries for investment currencies and create a price map to # use for later for generating investment transactions. funds_allocation = {'MFUND1': 0.40, 'MFUND2': 0.60} stocks = ['STK1', 'STK2', 'STK3', 'STK4'] price_entries = generate_prices(date_begin, date_end, sorted(funds_allocation.keys()) + stocks, 'CCY') price_map = prices.build_price_map(price_entries) logging.info(\"Generating Employer Match Contribution\") account_match = 'Income:US:Employer1:Match401k' retirement_match = generate_retirement_employer_match(income_entries, join(account_retirement, 'Cash'), account_match) logging.info(\"Generating Retirement Investments\") retirement_entries = generate_retirement_investments( income_entries + retirement_match, account_retirement, sorted(funds_allocation.items()), price_map) logging.info(\"Generating Taxes Investments\") investment_entries = generate_taxable_investment(date_begin, date_end, banking_transfers, price_map, stocks) logging.info(\"Generating Expense Accounts\") expense_accounts_entries = generate_expense_accounts(date_birth) logging.info(\"Generating Equity Accounts\") equity_entries = generate_open_entries(date_birth, [account_opening, account_payable]) logging.info(\"Generating Balance Checks\") credit_checks = generate_balance_checks(credit_entries, account_credit, date_random_seq(date_begin, date_end, 20, 30)) banking_checks = generate_balance_checks(data.sorted(income_entries + banking_entries + banking_expenses + banking_transfers + credit_entries + tax_entries), account_checking, date_random_seq(date_begin, date_end, 20, 30)) logging.info(\"Outputting and Formatting Entries\") dcontext = display_context.DisplayContext() default_int_digits = 8 for currency, precision in {'USD': 2, 'CAD': 2, 'VACHR':0, 'IRAUSD': 2, 'VBMPX': 3, 'RGAGX': 3, 'ITOT': 0, 'VEA': 0, 'VHT': 0, 'GLD': 0}.items(): int_digits = default_int_digits if precision > 0: int_digits += 1 + precision dcontext.update(D('{{:0{}.{}f}}'.format(int_digits, precision).format(0)), currency) output = io.StringIO() def output_section(title, entries): output.write('\\n\\n\\n{}\\n\\n'.format(title)) printer.print_entries(data.sorted(entries), dcontext, file=output) output.write(FILE_PREAMBLE.format(**locals())) output_section('* Commodities', commodity_entries) output_section('* Equity Accounts', equity_entries) output_section('* Banking', data.sorted(banking_entries + banking_expenses + banking_transfers + banking_checks)) output_section('* Credit-Cards', data.sorted(credit_entries + credit_checks)) output_section('* Taxable Investments', investment_entries) output_section('* Retirement Investments', data.sorted(retirement_entries + retirement_match)) output_section('* Sources of Income', income_entries) output_section('* Taxes', tax_preamble) for year, entries in taxes: output_section('** Tax Year {}'.format(year), entries) output_section('* Expenses', expense_accounts_entries) output_section('* Prices', price_entries) output_section('* Cash', []) logging.info(\"Contextualizing to Realistic Names\") contents, replacements = contextualize_file(output.getvalue(), employer_name) if reformat: contents = format.align_beancount(contents) logging.info(\"Writing contents\") file.write(contents) logging.info(\"Validating Results\") validate_output(contents, [replace(account, replacements) for account in [account_checking]], replace('CCY', replacements)) beancount.scripts.format \uf0c1 Align a beancount/ledger input file's numbers. This reformats at beancount or ledger input file so that the amounts in the postings are all aligned to the same column. The currency should match. Note: this does not parse the Beancount ledger. It simply uses regular expressions and text manipulations to do its work. beancount.scripts.format.align_beancount(contents, prefix_width=None, num_width=None, currency_column=None) \uf0c1 Reformat Beancount input to align all the numbers at the same column. Parameters: contents \u2013 A string, Beancount input syntax to reformat. prefix_width \u2013 An integer, the width in characters to render the account name to. If this is not specified, a good value is selected automatically from the contents of the file. num_width \u2013 An integer, the width to render each number. If this is not specified, a good value is selected automatically from the contents of the file. currency_column \u2013 An integer, the column at which to align the currencies. If given, this overrides the other options. Returns: A string, reformatted Beancount input with all the number aligned. No other changes than whitespace changes should be present between that return value and the input contents. Source code in beancount/scripts/format.py def align_beancount(contents, prefix_width=None, num_width=None, currency_column=None): \"\"\"Reformat Beancount input to align all the numbers at the same column. Args: contents: A string, Beancount input syntax to reformat. prefix_width: An integer, the width in characters to render the account name to. If this is not specified, a good value is selected automatically from the contents of the file. num_width: An integer, the width to render each number. If this is not specified, a good value is selected automatically from the contents of the file. currency_column: An integer, the column at which to align the currencies. If given, this overrides the other options. Returns: A string, reformatted Beancount input with all the number aligned. No other changes than whitespace changes should be present between that return value and the input contents. \"\"\" # Find all lines that have a number in them and calculate the maximum length # of the stripped prefix and the number. match_pairs = [] for line in contents.splitlines(): match = re.match( r'([^\";]*?)\\s+([-+]?\\s*[\\d,]+(?:\\.\\d*)?)\\s+({}\\b.*)'.format(amount.CURRENCY_RE), line) if match: prefix, number, rest = match.groups() match_pairs.append((prefix, number, rest)) else: match_pairs.append((line, None, None)) # Normalize whitespace before lines that has some indent and an account # name. norm_match_pairs = normalize_indent_whitespace(match_pairs) if currency_column: output = io.StringIO() for prefix, number, rest in norm_match_pairs: if number is None: output.write(prefix) else: num_of_spaces = currency_column - len(prefix) - len(number) - 4 spaces = ' ' * num_of_spaces output.write(prefix + spaces + ' ' + number + ' ' + rest) output.write('\\n') return output.getvalue() # Compute the maximum widths. filtered_pairs = [(prefix, number) for prefix, number, _ in match_pairs if number is not None] if filtered_pairs: max_prefix_width = max(len(prefix) for prefix, _ in filtered_pairs) max_num_width = max(len(number) for _, number in filtered_pairs) else: max_prefix_width = 0 max_num_width = 0 # Use user-supplied overrides, if available if prefix_width: max_prefix_width = prefix_width if num_width: max_num_width = num_width # Create a format that will admit the maximum width of all prefixes equally. line_format = '{{:<{prefix_width}}} {{:>{num_width}}} {{}}'.format( prefix_width=max_prefix_width, num_width=max_num_width) # Process each line to an output buffer. output = io.StringIO() for prefix, number, rest in norm_match_pairs: if number is None: output.write(prefix) else: output.write(line_format.format(prefix.rstrip(), number, rest)) output.write('\\n') formatted_contents = output.getvalue() # Ensure that the file before and after have only whitespace differences. # This is a sanity check, to make really sure we never change anything but whitespace, # so it's safe. # open('/tmp/before', 'w').write(re.sub(r'[ \\t]+', ' ', contents)) # open('/tmp/after', 'w').write(re.sub(r'[ \\t]+', ' ', formatted_contents)) old_stripped = re.sub(r'[ \\t\\n]+', ' ', contents.rstrip()) new_stripped = re.sub(r'[ \\t\\n]+', ' ', formatted_contents.rstrip()) assert (old_stripped == new_stripped), (old_stripped, new_stripped) return formatted_contents beancount.scripts.format.compute_most_frequent(iterable) \uf0c1 Compute the frequencies of the given elements and return the most frequent. Parameters: iterable \u2013 A collection of hashable elements. Returns: The most frequent element. If there are no elements in the iterable, return None. Source code in beancount/scripts/format.py def compute_most_frequent(iterable): \"\"\"Compute the frequencies of the given elements and return the most frequent. Args: iterable: A collection of hashable elements. Returns: The most frequent element. If there are no elements in the iterable, return None. \"\"\" frequencies = collections.Counter(iterable) if not frequencies: return None counts = sorted((count, element) for element, count in frequencies.items()) # Note: In case of a tie, this chooses the longest width. # We could eventually make this an option. return counts[-1][1] beancount.scripts.format.normalize_indent_whitespace(match_pairs) \uf0c1 Normalize whitespace before lines that has some indent and an account name. Parameters: match_pairs \u2013 A list of (prefix, number, rest) tuples. Returns: Another list of (prefix, number, rest) tuples, where prefix may have been adjusted with a different whitespace prefix. Source code in beancount/scripts/format.py def normalize_indent_whitespace(match_pairs): \"\"\"Normalize whitespace before lines that has some indent and an account name. Args: match_pairs: A list of (prefix, number, rest) tuples. Returns: Another list of (prefix, number, rest) tuples, where prefix may have been adjusted with a different whitespace prefix. \"\"\" # Compute most frequent account name prefix. match_posting = re.compile(r'([ \\t]+)({}.*)'.format(account.ACCOUNT_RE)).match width = compute_most_frequent( len(match.group(1)) for match in (match_posting(prefix) for prefix, _, _ in match_pairs) if match is not None) norm_format = ' ' * (width or 0) + '{}' # Make the necessary adjustments. adjusted_pairs = [] for tup in match_pairs: prefix, number, rest = tup match = match_posting(prefix) if match is not None: tup = (norm_format.format(match.group(2)), number, rest) adjusted_pairs.append(tup) return adjusted_pairs beancount.scripts.sql \uf0c1 Convert a Beancount ledger into an SQL database. beancount.scripts.sql.BalanceWriter ( DirectiveWriter ) \uf0c1 beancount.scripts.sql.BalanceWriter.type ( tuple ) \uf0c1 Balance(meta, date, account, amount, tolerance, diff_amount) beancount.scripts.sql.BalanceWriter.type.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.scripts.sql.BalanceWriter.type.__new__(_cls, meta, date, account, amount, tolerance, diff_amount) special staticmethod \uf0c1 Create new instance of Balance(meta, date, account, amount, tolerance, diff_amount) beancount.scripts.sql.BalanceWriter.type.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.scripts.sql.BalanceWriter.get_detail(self, entry) \uf0c1 Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.account, entry.amount.number, entry.amount.currency, entry.diff_amount.currency if entry.diff_amount else None, entry.diff_amount.currency if entry.diff_amount else None) beancount.scripts.sql.CloseWriter ( DirectiveWriter ) \uf0c1 beancount.scripts.sql.CloseWriter.type ( tuple ) \uf0c1 Close(meta, date, account) beancount.scripts.sql.CloseWriter.type.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.scripts.sql.CloseWriter.type.__new__(_cls, meta, date, account) special staticmethod \uf0c1 Create new instance of Close(meta, date, account) beancount.scripts.sql.CloseWriter.type.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.scripts.sql.CloseWriter.get_detail(self, entry) \uf0c1 Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.account,) beancount.scripts.sql.DirectiveWriter \uf0c1 A base class for writers of directives. This is used to factor out code for all the simple directives types (all types except Transaction). beancount.scripts.sql.DirectiveWriter.__call__(self, connection, entries) special \uf0c1 Create a table for a directives. Parameters: connection \u2013 A DBAPI-2.0 Connection object. entries \u2013 A list of directives. Source code in beancount/scripts/sql.py def __call__(self, connection, entries): \"\"\"Create a table for a directives. Args: connection: A DBAPI-2.0 Connection object. entries: A list of directives. \"\"\" with connection: columns_text = ','.join(self.columns.strip().splitlines()) connection.execute(\"\"\" CREATE TABLE {name}_detail ( id INTEGER PRIMARY KEY, {columns} ); \"\"\".format(name=self.name, columns=columns_text)) connection.execute(\"\"\" CREATE VIEW {name} AS SELECT * FROM entry JOIN {name}_detail USING (id); \"\"\".format(name=self.name)) with connection: for eid, entry in enumerate(entries): if not isinstance(entry, self.type): continue # Store common data. connection.execute(\"\"\" INSERT INTO entry VALUES (?, ?, ?, ?, ?); \"\"\", (eid, entry.date, self.name, entry.meta[\"filename\"], entry.meta[\"lineno\"])) # Store detail data. detail_data = self.get_detail(entry) row_data = (eid,) + detail_data query = \"\"\" INSERT INTO {name}_detail VALUES ({placeholder}); \"\"\".format(name=self.name, placeholder=','.join(['?'] * (1 + len(detail_data)))) connection.execute(query, row_data) beancount.scripts.sql.DirectiveWriter.get_detail(self, entry) \uf0c1 Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): \"\"\"Provide data to store for details table. Args: entry: An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. \"\"\" raise NotImplementedError beancount.scripts.sql.DocumentWriter ( DirectiveWriter ) \uf0c1 beancount.scripts.sql.DocumentWriter.type ( tuple ) \uf0c1 Document(meta, date, account, filename, tags, links) beancount.scripts.sql.DocumentWriter.type.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.scripts.sql.DocumentWriter.type.__new__(_cls, meta, date, account, filename, tags, links) special staticmethod \uf0c1 Create new instance of Document(meta, date, account, filename, tags, links) beancount.scripts.sql.DocumentWriter.type.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.scripts.sql.DocumentWriter.get_detail(self, entry) \uf0c1 Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.account, entry.filename) beancount.scripts.sql.EventWriter ( DirectiveWriter ) \uf0c1 beancount.scripts.sql.EventWriter.type ( tuple ) \uf0c1 Event(meta, date, type, description) beancount.scripts.sql.EventWriter.type.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.scripts.sql.EventWriter.type.__new__(_cls, meta, date, type, description) special staticmethod \uf0c1 Create new instance of Event(meta, date, type, description) beancount.scripts.sql.EventWriter.type.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.scripts.sql.EventWriter.get_detail(self, entry) \uf0c1 Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.type, entry.description) beancount.scripts.sql.NoteWriter ( DirectiveWriter ) \uf0c1 beancount.scripts.sql.NoteWriter.type ( tuple ) \uf0c1 Note(meta, date, account, comment) beancount.scripts.sql.NoteWriter.type.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.scripts.sql.NoteWriter.type.__new__(_cls, meta, date, account, comment) special staticmethod \uf0c1 Create new instance of Note(meta, date, account, comment) beancount.scripts.sql.NoteWriter.type.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.scripts.sql.NoteWriter.get_detail(self, entry) \uf0c1 Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.account, entry.comment) beancount.scripts.sql.OpenWriter ( DirectiveWriter ) \uf0c1 beancount.scripts.sql.OpenWriter.type ( tuple ) \uf0c1 Open(meta, date, account, currencies, booking) beancount.scripts.sql.OpenWriter.type.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.scripts.sql.OpenWriter.type.__new__(_cls, meta, date, account, currencies, booking) special staticmethod \uf0c1 Create new instance of Open(meta, date, account, currencies, booking) beancount.scripts.sql.OpenWriter.type.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.scripts.sql.OpenWriter.get_detail(self, entry) \uf0c1 Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.account, ','.join(entry.currencies or [])) beancount.scripts.sql.PadWriter ( DirectiveWriter ) \uf0c1 beancount.scripts.sql.PadWriter.type ( tuple ) \uf0c1 Pad(meta, date, account, source_account) beancount.scripts.sql.PadWriter.type.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.scripts.sql.PadWriter.type.__new__(_cls, meta, date, account, source_account) special staticmethod \uf0c1 Create new instance of Pad(meta, date, account, source_account) beancount.scripts.sql.PadWriter.type.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.scripts.sql.PadWriter.get_detail(self, entry) \uf0c1 Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.account, entry.source_account) beancount.scripts.sql.PriceWriter ( DirectiveWriter ) \uf0c1 beancount.scripts.sql.PriceWriter.type ( tuple ) \uf0c1 Price(meta, date, currency, amount) beancount.scripts.sql.PriceWriter.type.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.scripts.sql.PriceWriter.type.__new__(_cls, meta, date, currency, amount) special staticmethod \uf0c1 Create new instance of Price(meta, date, currency, amount) beancount.scripts.sql.PriceWriter.type.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.scripts.sql.PriceWriter.get_detail(self, entry) \uf0c1 Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.currency, entry.amount.number, entry.amount.currency) beancount.scripts.sql.QueryWriter ( DirectiveWriter ) \uf0c1 beancount.scripts.sql.QueryWriter.type ( tuple ) \uf0c1 Query(meta, date, name, query_string) beancount.scripts.sql.QueryWriter.type.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.scripts.sql.QueryWriter.type.__new__(_cls, meta, date, name, query_string) special staticmethod \uf0c1 Create new instance of Query(meta, date, name, query_string) beancount.scripts.sql.QueryWriter.type.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.scripts.sql.QueryWriter.get_detail(self, entry) \uf0c1 Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.name, entry.query_string) beancount.scripts.sql.adapt_decimal(number) \uf0c1 Adapt a Decimal instance to a string for creating queries. Parameters: number \u2013 An instance of Decimal. Returns: A string. Source code in beancount/scripts/sql.py def adapt_decimal(number): \"\"\"Adapt a Decimal instance to a string for creating queries. Args: number: An instance of Decimal. Returns: A string. \"\"\" return str(number) beancount.scripts.sql.convert_decimal(string) \uf0c1 Convert a Decimal string to a Decimal instance. Parameters: string \u2013 A decimal number in a string. Returns: An instance of Decimal. Source code in beancount/scripts/sql.py def convert_decimal(string): \"\"\"Convert a Decimal string to a Decimal instance. Args: string: A decimal number in a string. Returns: An instance of Decimal. \"\"\" return Decimal(string) beancount.scripts.sql.output_common(connection, unused_entries) \uf0c1 Create a table of common data for all entries. Parameters: connection \u2013 A DBAPI-2.0 Connection object. entries \u2013 A list of directives. Source code in beancount/scripts/sql.py def output_common(connection, unused_entries): \"\"\"Create a table of common data for all entries. Args: connection: A DBAPI-2.0 Connection object. entries: A list of directives. \"\"\" with connection: connection.execute(\"\"\" CREATE TABLE entry ( id INTEGER PRIMARY KEY, date DATE, type CHARACTER(8), source_filename STRING, source_lineno INTEGER ); \"\"\") beancount.scripts.sql.output_transactions(connection, entries) \uf0c1 Create a table for transactions and fill in the data. Parameters: connection \u2013 A DBAPI-2.0 Connection object. entries \u2013 A list of directives. Source code in beancount/scripts/sql.py def output_transactions(connection, entries): \"\"\"Create a table for transactions and fill in the data. Args: connection: A DBAPI-2.0 Connection object. entries: A list of directives. \"\"\" with connection: connection.execute(\"\"\" CREATE TABLE transactions_detail ( id INTEGER PRIMARY KEY, flag CHARACTER(1), payee VARCHAR, narration VARCHAR, tags VARCHAR, -- Comma-separated links VARCHAR -- Comma-separated ); \"\"\") connection.execute(\"\"\" CREATE VIEW transactions AS SELECT * FROM entry JOIN transactions_detail USING (id); \"\"\") connection.execute(\"\"\" CREATE TABLE postings ( posting_id INTEGER PRIMARY KEY, id INTEGER, flag CHARACTER(1), account VARCHAR, number DECIMAL(16, 6), currency CHARACTER(10), cost_number DECIMAL(16, 6), cost_currency CHARACTER(10), cost_date DATE, cost_label VARCHAR, price_number DECIMAL(16, 6), price_currency CHARACTER(10), FOREIGN KEY(id) REFERENCES entries(id) ); \"\"\") postings_count = iter(itertools.count()) with connection: for eid, entry in enumerate(entries): if not isinstance(entry, data.Transaction): continue connection.execute(\"\"\" insert into entry values (?, ?, ?, ?, ?); \"\"\", (eid, entry.date, 'txn', entry.meta[\"filename\"], entry.meta[\"lineno\"])) connection.execute(\"\"\" insert into transactions_detail values (?, ?, ?, ?, ?, ?); \"\"\", (eid, entry.flag, entry.payee, entry.narration, ','.join(entry.tags or ()), ','.join(entry.links or ()))) for posting in entry.postings: pid = next(postings_count) units = posting.units cost = posting.cost price = posting.price connection.execute(\"\"\" INSERT INTO postings VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); \"\"\", (pid, eid, posting.flag, posting.account, units.number, units.currency, cost.number if cost else None, cost.currency if cost else None, cost.date if cost else None, cost.label if cost else None, price.number if price else None, price.currency if price else None)) beancount.scripts.sql.setup_decimal_support() \uf0c1 Setup sqlite3 to support conversions to/from Decimal numbers. Source code in beancount/scripts/sql.py def setup_decimal_support(): \"\"\"Setup sqlite3 to support conversions to/from Decimal numbers. \"\"\" dbapi.register_adapter(Decimal, adapt_decimal) dbapi.register_converter(\"decimal\", convert_decimal) beancount.scripts.tutorial \uf0c1 Write output files for the tutorial commands.","title":"beancount.scripts"},{"location":"api_reference/beancount.scripts.html#beancountscripts","text":"Implementation of the various scripts available from bin. This is structured this way because we want all the significant codes under a single directory, for analysis, grepping and unit testing.","title":"beancount.scripts"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.bake","text":"Bake a Beancount input file's web files to a directory hierarchy. You provide a Beancount filename, an output directory, and this script runs a server and a scraper that puts all the files in the directory, and if your output name has an archive suffix, we automatically the fetched directory contents to the archive and delete them.","title":"bake"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.bake.archive","text":"Archive the directory to the given tar/gz archive filename. Parameters: command_template \u2013 A string, the command template to format with in order to compute the command to run. directory \u2013 A string, the name of the directory to archive. archive \u2013 A string, the name of the file to output. quiet \u2013 A boolean, True to suppress output. Exceptions: IOError \u2013 if the directory does not exist or if the archive name already Source code in beancount/scripts/bake.py def archive(command_template, directory, archive, quiet=False): \"\"\"Archive the directory to the given tar/gz archive filename. Args: command_template: A string, the command template to format with in order to compute the command to run. directory: A string, the name of the directory to archive. archive: A string, the name of the file to output. quiet: A boolean, True to suppress output. Raises: IOError: if the directory does not exist or if the archive name already exists. \"\"\" directory = path.abspath(directory) archive = path.abspath(archive) if not path.exists(directory): raise IOError(\"Directory to archive '{}' does not exist\".format( directory)) if path.exists(archive): raise IOError(\"Output archive name '{}' already exists\".format( archive)) command = command_template.format(directory=directory, dirname=path.dirname(directory), basename=path.basename(directory), archive=archive) pipe = subprocess.Popen(shlex.split(command), shell=False, cwd=path.dirname(directory), stdout=subprocess.PIPE if quiet else None, stderr=subprocess.PIPE if quiet else None) _, _ = pipe.communicate() if pipe.returncode != 0: raise OSError(\"Archive failure\")","title":"archive()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.bake.archive_zip","text":"Archive the directory to the given tar/gz archive filename. Parameters: directory \u2013 A string, the name of the directory to archive. archive \u2013 A string, the name of the file to output. Source code in beancount/scripts/bake.py def archive_zip(directory, archive): \"\"\"Archive the directory to the given tar/gz archive filename. Args: directory: A string, the name of the directory to archive. archive: A string, the name of the file to output. \"\"\" # Figure out optimal level of compression among the supported ones in this # installation. for spec, compression in [ ('lzma', zipfile.ZIP_LZMA), ('bz2', zipfile.ZIP_BZIP2), ('zlib', zipfile.ZIP_DEFLATED)]: if importlib.util.find_spec(spec): zip_compression = compression break else: # Default is no compression. zip_compression = zipfile.ZIP_STORED with file_utils.chdir(directory), zipfile.ZipFile( archive, 'w', compression=zip_compression) as archfile: for root, dirs, files in os.walk(directory): for filename in files: relpath = path.relpath(path.join(root, filename), directory) archfile.write(relpath)","title":"archive_zip()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.bake.bake_to_directory","text":"Serve and bake a Beancount's web to a directory. Parameters: webargs \u2013 An argparse parsed options object with the web app arguments. output_dir \u2013 A directory name. We don't check here whether it exists or not. quiet \u2013 A boolean, True to suppress web server fetch log. render_all_pages \u2013 If true, fetch the full set of pages, not just the subset that is palatable. Returns: True on success, False otherwise. Source code in beancount/scripts/bake.py def bake_to_directory(webargs, output_dir, render_all_pages=True): \"\"\"Serve and bake a Beancount's web to a directory. Args: webargs: An argparse parsed options object with the web app arguments. output_dir: A directory name. We don't check here whether it exists or not. quiet: A boolean, True to suppress web server fetch log. render_all_pages: If true, fetch the full set of pages, not just the subset that is palatable. Returns: True on success, False otherwise. \"\"\" callback = functools.partial(save_scraped_document, output_dir) if render_all_pages: ignore_regexps = None else: regexps = [ # Skip the context pages, too slow. r'/context/', # Skip the link pages, too slow. r'/link/', # Skip the component pages... too many. r'/view/component/', # Skip served documents. r'/.*/doc/', # Skip monthly pages. r'/view/year/\\d\\d\\d\\d/month/', ] ignore_regexps = '({})'.format('|'.join(regexps)) processed_urls, skipped_urls = web.scrape_webapp(webargs, callback, ignore_regexps)","title":"bake_to_directory()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.bake.normalize_filename","text":"Convert URL paths to filenames. Add .html extension if needed. Parameters: url \u2013 A string, the url to convert. Returns: A string, possibly with an extension appended. Source code in beancount/scripts/bake.py def normalize_filename(url): \"\"\"Convert URL paths to filenames. Add .html extension if needed. Args: url: A string, the url to convert. Returns: A string, possibly with an extension appended. \"\"\" if url.endswith('/'): return path.join(url, 'index.html') elif BINARY_MATCH(url): return url else: return url if url.endswith('.html') else (url + '.html')","title":"normalize_filename()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.bake.relativize_links","text":"Make all the links in the contents string relative to an URL. Parameters: html \u2013 An lxml document node. current_url \u2013 A string, the URL of the current page, a path to. a file or a directory. If the path represents a directory, the path ends with a /. Source code in beancount/scripts/bake.py def relativize_links(html, current_url): \"\"\"Make all the links in the contents string relative to an URL. Args: html: An lxml document node. current_url: A string, the URL of the current page, a path to. a file or a directory. If the path represents a directory, the path ends with a /. \"\"\" current_dir = path.dirname(current_url) for element, attribute, link, pos in lxml.html.iterlinks(html): if path.isabs(link): relative_link = path.relpath(normalize_filename(link), current_dir) element.set(attribute, relative_link)","title":"relativize_links()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.bake.remove_links","text":"Convert a list of anchors () from an HTML tree to spans (). Parameters: html \u2013 An lxml document node. targets \u2013 A set of string, targets to be removed. Source code in beancount/scripts/bake.py def remove_links(html, targets): \"\"\"Convert a list of anchors () from an HTML tree to spans (). Args: html: An lxml document node. targets: A set of string, targets to be removed. \"\"\" for element, attribute, link, pos in lxml.html.iterlinks(html): if link in targets: del element.attrib[attribute] element.tag = 'span' element.set('class', 'removed-link')","title":"remove_links()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.bake.save_scraped_document","text":"Callback function to process a document being scraped. This converts the document to have relative links and writes out the file to the output directory. Parameters: output_dir \u2013 A string, the output directory to write. url \u2013 A string, the originally requested URL. response \u2013 An http response as per urlopen. contents \u2013 Bytes, the content of a response. html_root \u2013 An lxml root node for the document, optionally. If this is provided, this avoid you having to reprocess it (for performance reasons). skipped_urls \u2013 A set of the links from the file that were skipped. Source code in beancount/scripts/bake.py def save_scraped_document(output_dir, url, response, contents, html_root, skipped_urls): \"\"\"Callback function to process a document being scraped. This converts the document to have relative links and writes out the file to the output directory. Args: output_dir: A string, the output directory to write. url: A string, the originally requested URL. response: An http response as per urlopen. contents: Bytes, the content of a response. html_root: An lxml root node for the document, optionally. If this is provided, this avoid you having to reprocess it (for performance reasons). skipped_urls: A set of the links from the file that were skipped. \"\"\" if response.status != 200: logging.error(\"Invalid status: %s\", response.status) # Ignore directories. if url.endswith('/'): return # Note that we're saving the file under the non-redirected URL, because this # will have to be opened using files and there are no redirects that way. if response.info().get_content_type() == 'text/html': if html_root is None: html_root = lxml.html.document_fromstring(contents) remove_links(html_root, skipped_urls) relativize_links(html_root, url) contents = lxml.html.tostring(html_root, method=\"html\") # Compute output filename and write out the relativized contents. output_filename = path.join(output_dir, normalize_filename(url).lstrip('/')) os.makedirs(path.dirname(output_filename), exist_ok=True) with open(output_filename, 'wb') as outfile: outfile.write(contents)","title":"save_scraped_document()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.check","text":"Parse, check and realize a beancount input file. This also measures the time it takes to run all these steps.","title":"check"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps","text":"Check the installation dependencies and report the version numbers of each. This is meant to be used as an error diagnostic tool.","title":"deps"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.check_cdecimal","text":"Check that Python 3.3 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_cdecimal(): \"\"\"Check that Python 3.3 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" # Note: this code mirrors and should be kept in-sync with that at the top of # beancount.core.number. # Try the built-in installation. import decimal if is_fast_decimal(decimal): return ('cdecimal', '{} (built-in)'.format(decimal.__version__), True) # Try an explicitly installed version. try: import cdecimal if is_fast_decimal(cdecimal): return ('cdecimal', getattr(cdecimal, '__version__', 'OKAY'), True) except ImportError: pass # Not found. return ('cdecimal', None, False)","title":"check_cdecimal()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.check_dependencies","text":"Check the runtime dependencies and report their version numbers. Returns: A list of pairs of (package-name, version-number, sufficient) whereby if a package has not been installed, its 'version-number' will be set to None. Otherwise, it will be a string with the version number in it. 'sufficient' will be True if the version if sufficient for this installation of Beancount. Source code in beancount/scripts/deps.py def check_dependencies(): \"\"\"Check the runtime dependencies and report their version numbers. Returns: A list of pairs of (package-name, version-number, sufficient) whereby if a package has not been installed, its 'version-number' will be set to None. Otherwise, it will be a string with the version number in it. 'sufficient' will be True if the version if sufficient for this installation of Beancount. \"\"\" return [ # Check for a complete installation of Python itself. check_python(), check_cdecimal(), # Modules we really do need installed. check_import('dateutil'), check_import('bottle'), check_import('ply', module_name='ply.yacc', min_version='3.4'), check_import('lxml', module_name='lxml.etree', min_version='3'), # Optionally required to upload data to Google Drive. check_import('googleapiclient'), check_import('oauth2client'), check_import('httplib2'), # Optionally required to support various price source fetchers. check_import('requests', min_version='2.0'), # Optionally required to support imports (identify, extract, file) code. check_python_magic(), check_import('beautifulsoup4', module_name='bs4', min_version='4'), ]","title":"check_dependencies()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.check_import","text":"Check that a particular module name is installed. Parameters: package_name \u2013 A string, the name of the package and module to be imported to verify this works. This should have a version attribute on it. min_version \u2013 If not None, a string, the minimum version number we require. module_name \u2013 The name of the module to import if it differs from the package name. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_import(package_name, min_version=None, module_name=None): \"\"\"Check that a particular module name is installed. Args: package_name: A string, the name of the package and module to be imported to verify this works. This should have a __version__ attribute on it. min_version: If not None, a string, the minimum version number we require. module_name: The name of the module to import if it differs from the package name. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" if module_name is None: module_name = package_name try: __import__(module_name) module = sys.modules[module_name] if min_version is not None: version = module.__version__ assert isinstance(version, str) is_sufficient = (parse_version(version) >= parse_version(min_version) if min_version else True) else: version, is_sufficient = None, True except ImportError: version, is_sufficient = None, False return (package_name, version, is_sufficient)","title":"check_import()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.check_python","text":"Check that Python 3.3 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_python(): \"\"\"Check that Python 3.3 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" return ('python3', '.'.join(map(str, sys.version_info[:3])), sys.version_info[:2] >= (3, 3))","title":"check_python()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.check_python_magic","text":"Check that a recent-enough version of python-magic is installed. python-magic is an interface to libmagic, which is used by the 'file' tool and UNIX to identify file types. Note that there are two Python wrappers which provide the 'magic' import: python-magic and filemagic. The former is what we need, which appears to be more recently maintained. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_python_magic(): \"\"\"Check that a recent-enough version of python-magic is installed. python-magic is an interface to libmagic, which is used by the 'file' tool and UNIX to identify file types. Note that there are two Python wrappers which provide the 'magic' import: python-magic and filemagic. The former is what we need, which appears to be more recently maintained. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" try: import magic # Check that python-magic and not filemagic is installed. if not hasattr(magic, 'from_file'): # 'filemagic' is installed; install python-magic. raise ImportError return ('python-magic', 'OK', True) except ImportError: return ('python-magic', None, False)","title":"check_python_magic()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.is_fast_decimal","text":"Return true if a fast C decimal implementation is installed. Source code in beancount/scripts/deps.py def is_fast_decimal(decimal_module): \"Return true if a fast C decimal implementation is installed.\" return isinstance(decimal_module.Decimal().sqrt, types.BuiltinFunctionType)","title":"is_fast_decimal()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.list_dependencies","text":"Check the dependencies and produce a listing on the given file. Parameters: file \u2013 A file object to write the output to. Source code in beancount/scripts/deps.py def list_dependencies(file=sys.stderr): \"\"\"Check the dependencies and produce a listing on the given file. Args: file: A file object to write the output to. \"\"\" print(\"Dependencies:\") for package, version, sufficient in check_dependencies(): print(\" {:16}: {} {}\".format( package, version or 'NOT INSTALLED', \"(INSUFFICIENT)\" if version and not sufficient else \"\"), file=file)","title":"list_dependencies()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.parse_version","text":"Parse the version string into a comparable tuple. Source code in beancount/scripts/deps.py def parse_version(version_str: str) -> str: \"\"\"Parse the version string into a comparable tuple.\"\"\" return [int(v) for v in version_str.split('.')]","title":"parse_version()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.directories","text":"Check that document directories mirror a list of accounts correctly.","title":"directories"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.directories.ValidateDirectoryError","text":"A directory validation error.","title":"ValidateDirectoryError"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.directories.validate_directories","text":"Validate a directory hierarchy against a ledger's account names. Read a ledger's list of account names and check that all the capitalized subdirectory names under the given roots match the account names. Parameters: entries \u2013 A list of directives. document_dirs \u2013 A list of string, the directory roots to walk and validate. Source code in beancount/scripts/directories.py def validate_directories(entries, document_dirs): \"\"\"Validate a directory hierarchy against a ledger's account names. Read a ledger's list of account names and check that all the capitalized subdirectory names under the given roots match the account names. Args: entries: A list of directives. document_dirs: A list of string, the directory roots to walk and validate. \"\"\" # Get the list of accounts declared in the ledge. accounts = getters.get_accounts(entries) # For each of the roots, validate the hierarchy of directories. for document_dir in document_dirs: errors = validate_directory(accounts, document_dir) for error in errors: print(\"ERROR: {}\".format(error))","title":"validate_directories()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.directories.validate_directory","text":"Check a directory hierarchy against a list of valid accounts. Walk the directory hierarchy, and for all directories with names matching that of accounts (with \":\" replaced with \"/\"), check that they refer to an account name declared in the given list. Parameters: account \u2013 A set or dict of account names. document_dir \u2013 A string, the root directory to walk and validate. Returns: An errors for each invalid directory name found. Source code in beancount/scripts/directories.py def validate_directory(accounts, document_dir): \"\"\"Check a directory hierarchy against a list of valid accounts. Walk the directory hierarchy, and for all directories with names matching that of accounts (with \":\" replaced with \"/\"), check that they refer to an account name declared in the given list. Args: account: A set or dict of account names. document_dir: A string, the root directory to walk and validate. Returns: An errors for each invalid directory name found. \"\"\" # Generate all parent accounts in the account_set we're checking against, so # that parent directories with no corresponding account don't warn. accounts_with_parents = set(accounts) for account_ in accounts: while True: parent = account.parent(account_) if not parent: break if parent in accounts_with_parents: break accounts_with_parents.add(parent) account_ = parent errors = [] for directory, account_name, _, _ in account.walk(document_dir): if account_name not in accounts_with_parents: errors.append(ValidateDirectoryError( \"Invalid directory '{}': no corresponding account '{}'\".format( directory, account_name))) return errors","title":"validate_directory()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor","text":"Debugging tool for those finding bugs in Beancount. This tool is able to dump lexer/parser state, and will provide other services in the name of debugging.","title":"doctor"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.RenderError","text":"RenderError(source, message, entry)","title":"RenderError"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.RenderError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/doctor.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.RenderError.__new__","text":"Create new instance of RenderError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.RenderError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/scripts/doctor.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_checkdeps","text":"Report on the runtime dependencies. Parameters: unused_args \u2013 Ignored. Source code in beancount/scripts/doctor.py def do_deps(*unused_args): \"\"\"Report on the runtime dependencies. Args: unused_args: Ignored. \"\"\" from beancount.scripts import deps deps.list_dependencies(sys.stdout) print('') print('Use \"pip3 install \" to install new packages.')","title":"do_checkdeps()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_context","text":"Describe the context that a particular transaction is applied to. Parameters: filename \u2013 A string, which consists in the filename. args \u2013 A tuple of the rest of arguments. We're expecting the first argument to be a string which contains either a lineno integer or a filename:lineno combination (which can be used if the location is not in the top-level file). Source code in beancount/scripts/doctor.py def do_context(filename, args): \"\"\"Describe the context that a particular transaction is applied to. Args: filename: A string, which consists in the filename. args: A tuple of the rest of arguments. We're expecting the first argument to be a string which contains either a lineno integer or a filename:lineno combination (which can be used if the location is not in the top-level file). \"\"\" from beancount.reports import context from beancount import loader # Check we have the required number of arguments. if len(args) != 1: raise SystemExit(\"Missing line number argument.\") # Load the input files. entries, errors, options_map = loader.load_file(filename) # Parse the arguments, get the line number. match = re.match(r\"(.+):(\\d+)$\", args[0]) if match: search_filename = path.abspath(match.group(1)) lineno = int(match.group(2)) elif re.match(r\"(\\d+)$\", args[0]): # Note: Make sure to use the absolute filename used by the parser to # resolve the file. search_filename = options_map['filename'] lineno = int(args[0]) else: raise SystemExit(\"Invalid format for location.\") str_context = context.render_file_context(entries, options_map, search_filename, lineno) sys.stdout.write(str_context)","title":"do_context()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_deps","text":"Report on the runtime dependencies. Parameters: unused_args \u2013 Ignored. Source code in beancount/scripts/doctor.py def do_deps(*unused_args): \"\"\"Report on the runtime dependencies. Args: unused_args: Ignored. \"\"\" from beancount.scripts import deps deps.list_dependencies(sys.stdout) print('') print('Use \"pip3 install \" to install new packages.')","title":"do_deps()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_directories","text":"Validate a directory hierarchy against a ledger's account names. Read a ledger's list of account names and check that all the capitalized subdirectory names under the given roots match the account names. Parameters: filename \u2013 A string, the Beancount input filename. args \u2013 The rest of the arguments provided on the command-line, which in this case will be interpreted as the names of root directories to validate against the accounts in the given ledger. Source code in beancount/scripts/doctor.py def do_directories(filename, args): \"\"\"Validate a directory hierarchy against a ledger's account names. Read a ledger's list of account names and check that all the capitalized subdirectory names under the given roots match the account names. Args: filename: A string, the Beancount input filename. args: The rest of the arguments provided on the command-line, which in this case will be interpreted as the names of root directories to validate against the accounts in the given ledger. \"\"\" from beancount import loader from beancount.scripts import directories entries, _, __ = loader.load_file(filename) directories.validate_directories(entries, args)","title":"do_directories()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_display_context","text":"Print out the precision inferred from the parsed numbers in the input file. Parameters: filename \u2013 A string, which consists in the filename. args \u2013 A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. Source code in beancount/scripts/doctor.py def do_display_context(filename, args): \"\"\"Print out the precision inferred from the parsed numbers in the input file. Args: filename: A string, which consists in the filename. args: A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. \"\"\" from beancount import loader entries, errors, options_map = loader.load_file(filename) dcontext = options_map['dcontext'] sys.stdout.write(str(dcontext))","title":"do_display_context()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_dump_lexer","text":"Dump the lexer output for a Beancount syntax file. Parameters: filename \u2013 A string, the Beancount input filename. Source code in beancount/scripts/doctor.py def do_lex(filename, unused_args): \"\"\"Dump the lexer output for a Beancount syntax file. Args: filename: A string, the Beancount input filename. \"\"\" from beancount.parser import lexer for token, lineno, text, obj in lexer.lex_iter(filename): sys.stdout.write('{:12} {:6d} {}\\n'.format( '(None)' if token is None else token, lineno, repr(text)))","title":"do_dump_lexer()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_lex","text":"Dump the lexer output for a Beancount syntax file. Parameters: filename \u2013 A string, the Beancount input filename. Source code in beancount/scripts/doctor.py def do_lex(filename, unused_args): \"\"\"Dump the lexer output for a Beancount syntax file. Args: filename: A string, the Beancount input filename. \"\"\" from beancount.parser import lexer for token, lineno, text, obj in lexer.lex_iter(filename): sys.stdout.write('{:12} {:6d} {}\\n'.format( '(None)' if token is None else token, lineno, repr(text)))","title":"do_lex()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_linked","text":"Print out a list of transactions linked to the one at the given line. Parameters: filename \u2013 A string, which consists in the filename. args \u2013 A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. Source code in beancount/scripts/doctor.py def do_linked(filename, args): \"\"\"Print out a list of transactions linked to the one at the given line. Args: filename: A string, which consists in the filename. args: A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. \"\"\" from beancount.parser import options from beancount.parser import printer from beancount.core import account_types from beancount.core import inventory from beancount.core import data from beancount.core import realization from beancount import loader # Parse the arguments, get the line number. if len(args) != 1: raise SystemExit(\"Missing line number argument.\") lineno = int(args[0]) # Load the input file. entries, errors, options_map = loader.load_file(filename) # Find the closest entry. closest_entry = data.find_closest(entries, options_map['filename'], lineno) # Find its links. if closest_entry is None: raise SystemExit(\"No entry could be found before {}:{}\".format(filename, lineno)) links = (closest_entry.links if isinstance(closest_entry, data.Transaction) else data.EMPTY_SET) if not links: linked_entries = [closest_entry] else: # Find all linked entries. # # Note that there is an option here: You can either just look at the links # on the closest entry, or you can include the links of the linked # transactions as well. Whichever one you want depends on how you use your # links. Best would be to query the user (in Emacs) when there are many # links present. follow_links = True if not follow_links: linked_entries = [entry for entry in entries if (isinstance(entry, data.Transaction) and entry.links and entry.links & links)] else: links = set(links) linked_entries = [] while True: num_linked = len(linked_entries) linked_entries = [entry for entry in entries if (isinstance(entry, data.Transaction) and entry.links and entry.links & links)] if len(linked_entries) == num_linked: break for entry in linked_entries: if entry.links: links.update(entry.links) # Render linked entries (in date order) as errors (for Emacs). errors = [RenderError(entry.meta, '', entry) for entry in linked_entries] printer.print_errors(errors) # Print out balances. real_root = realization.realize(linked_entries) dformat = options_map['dcontext'].build(alignment=display_context.Align.DOT, reserved=2) realization.dump_balances(real_root, dformat, file=sys.stdout) # Print out net income change. acctypes = options.get_account_types(options_map) net_income = inventory.Inventory() for real_node in realization.iter_children(real_root): if account_types.is_income_statement_account(real_node.account, acctypes): net_income.add_inventory(real_node.balance) print() print('Net Income: {}'.format(-net_income))","title":"do_linked()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_list_options","text":"Print out a list of the available options. Parameters: unused_args \u2013 Ignored. Source code in beancount/scripts/doctor.py def do_list_options(*unused_args): \"\"\"Print out a list of the available options. Args: unused_args: Ignored. \"\"\" from beancount.parser import options print(options.list_options())","title":"do_list_options()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_missing_open","text":"Print out Open directives that are missing for the given input file. This can be useful during demos in order to quickly generate all the required Open directives without having to type them manually. Parameters: filename \u2013 A string, which consists in the filename. args \u2013 A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. Source code in beancount/scripts/doctor.py def do_missing_open(filename, args): \"\"\"Print out Open directives that are missing for the given input file. This can be useful during demos in order to quickly generate all the required Open directives without having to type them manually. Args: filename: A string, which consists in the filename. args: A tuple of the rest of arguments. We're expecting the first argument to be an integer as a string. \"\"\" from beancount.parser import printer from beancount.core import data from beancount.core import getters from beancount import loader entries, errors, options_map = loader.load_file(filename) # Get accounts usage and open directives. first_use_map, _ = getters.get_accounts_use_map(entries) open_close_map = getters.get_account_open_close(entries) new_entries = [] for account, first_use_date in first_use_map.items(): if account not in open_close_map: new_entries.append( data.Open(data.new_metadata(filename, 0), first_use_date, account, None, None)) dcontext = options_map['dcontext'] printer.print_entries(data.sorted(new_entries), dcontext)","title":"do_missing_open()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_parse","text":"Run the parser in debug mode. Parameters: filename \u2013 A string, the Beancount input filename. Source code in beancount/scripts/doctor.py def do_parse(filename, unused_args): \"\"\"Run the parser in debug mode. Args: filename: A string, the Beancount input filename. \"\"\" from beancount.parser import parser entries, errors, _ = parser.parse_file(filename, yydebug=1)","title":"do_parse()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_print_options","text":"Print out the actual options parsed from a file. Parameters: unused_args \u2013 Ignored. Source code in beancount/scripts/doctor.py def do_print_options(filename, *args): \"\"\"Print out the actual options parsed from a file. Args: unused_args: Ignored. \"\"\" from beancount import loader _, __, options_map = loader.load_file(filename) for key, value in sorted(options_map.items()): print('{}: {}'.format(key, value))","title":"do_print_options()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_roundtrip","text":"Round-trip test on arbitrary Ledger. Read a Ledger's transactions, print them out, re-read them again and compare them. Both sets of parsed entries should be equal. Both printed files are output to disk, so you can also run diff on them yourself afterwards. Parameters: filename \u2013 A string, the Beancount input filename. Source code in beancount/scripts/doctor.py def do_roundtrip(filename, unused_args): \"\"\"Round-trip test on arbitrary Ledger. Read a Ledger's transactions, print them out, re-read them again and compare them. Both sets of parsed entries should be equal. Both printed files are output to disk, so you can also run diff on them yourself afterwards. Args: filename: A string, the Beancount input filename. \"\"\" from beancount.parser import printer from beancount.core import compare from beancount import loader round1_filename = round2_filename = None try: logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s') logging.info(\"Read the entries\") entries, errors, options_map = loader.load_file(filename) printer.print_errors(errors, file=sys.stderr) logging.info(\"Print them out to a file\") basename, extension = path.splitext(filename) round1_filename = ''.join([basename, '.roundtrip1', extension]) with open(round1_filename, 'w') as outfile: printer.print_entries(entries, file=outfile) logging.info(\"Read the entries from that file\") # Note that we don't want to run any of the auto-generation here, but # parsing now returns incomplete objects and we assume idempotence on a # file that was output from the printer after having been processed, so # it shouldn't add anything new. That is, a processed file printed and # resolve when parsed again should contain the same entries, i.e. # nothing new should be generated. entries_roundtrip, errors, options_map = loader.load_file(round1_filename) # Print out the list of errors from parsing the results. if errors: print(',----------------------------------------------------------------------') printer.print_errors(errors, file=sys.stdout) print('`----------------------------------------------------------------------') logging.info(\"Print what you read to yet another file\") round2_filename = ''.join([basename, '.roundtrip2', extension]) with open(round2_filename, 'w') as outfile: printer.print_entries(entries_roundtrip, file=outfile) logging.info(\"Compare the original entries with the re-read ones\") same, missing1, missing2 = compare.compare_entries(entries, entries_roundtrip) if same: logging.info('Entries are the same. Congratulations.') else: logging.error('Entries differ!') print() print('\\n\\nMissing from original:') for entry in entries: print(entry) print(compare.hash_entry(entry)) print(printer.format_entry(entry)) print() print('\\n\\nMissing from round-trip:') for entry in missing2: print(entry) print(compare.hash_entry(entry)) print(printer.format_entry(entry)) print() finally: for rfilename in (round1_filename, round2_filename): if path.exists(rfilename): os.remove(rfilename)","title":"do_roundtrip()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.do_validate_html","text":"Validate all the HTML files under a directory hierarchy. Parameters: directory \u2013 A string, the root directory whose contents to validate. args \u2013 A tuple of the rest of arguments. Source code in beancount/scripts/doctor.py def do_validate_html(directory, args): \"\"\"Validate all the HTML files under a directory hierarchy. Args: directory: A string, the root directory whose contents to validate. args: A tuple of the rest of arguments. \"\"\" from beancount.web import scrape files, missing, empty = scrape.validate_local_links_in_dir(directory) logging.info('%d files processed', len(files)) for target in missing: logging.error('Missing %s', target) for target in empty: logging.error('Empty %s', target)","title":"do_validate_html()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.get_commands","text":"Return a list of available commands in this file. Returns: A list of pairs of (command-name string, docstring). Source code in beancount/scripts/doctor.py def get_commands(): \"\"\"Return a list of available commands in this file. Returns: A list of pairs of (command-name string, docstring). \"\"\" commands = [] for attr_name, attr_value in globals().items(): match = re.match('do_(.*)', attr_name) if match: commands.append((match.group(1), misc_utils.first_paragraph(attr_value.__doc__))) return commands","title":"get_commands()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example","text":"Generate a decently-sized example history, based on some rules. This script is used to generate some meaningful input to Beancount, input that looks as realistic as possible for a moderately complex mock individual. This can also be used as an input generator for a stress test for performance evaluation.","title":"example"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.check_non_negative","text":"Check that the balance of the given account never goes negative. Parameters: entries \u2013 A list of directives. account \u2013 An account string, the account to check the balance for. currency \u2013 A string, the currency to check minimums for. Exceptions: AssertionError \u2013 if the balance goes negative. Source code in beancount/scripts/example.py def check_non_negative(entries, account, currency): \"\"\"Check that the balance of the given account never goes negative. Args: entries: A list of directives. account: An account string, the account to check the balance for. currency: A string, the currency to check minimums for. Raises: AssertionError: if the balance goes negative. \"\"\" previous_date = None for txn_posting, balances in postings_for(data.sorted(entries), [account], before=True): balance = balances[account] date = txn_posting.txn.date if date != previous_date: assert all(pos.units.number >= ZERO for pos in balance.get_positions()), ( \"Negative balance: {} at: {}\".format(balance, txn_posting.txn.date)) previous_date = date","title":"check_non_negative()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.compute_trip_dates","text":"Generate dates at reasonable intervals for trips during the given time period. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Pairs of dates for the trips within the period. Source code in beancount/scripts/example.py def compute_trip_dates(date_begin, date_end): \"\"\"Generate dates at reasonable intervals for trips during the given time period. Args: date_begin: The start date. date_end: The end date. Yields: Pairs of dates for the trips within the period. \"\"\" # Min and max number of days remaining at home. days_at_home = (4*30, 13*30) # Length of trip. days_trip = (8, 22) # Number of days to ensure no trip at the beginning and the end. days_buffer = 21 date_begin += datetime.timedelta(days=days_buffer) date_end -= datetime.timedelta(days=days_buffer) date = date_begin while 1: duration_at_home = datetime.timedelta(days=random.randint(*days_at_home)) duration_trip = datetime.timedelta(days=random.randint(*days_trip)) date_trip_begin = date + duration_at_home date_trip_end = date_trip_begin + duration_trip if date_trip_end >= date_end: break yield (date_trip_begin, date_trip_end) date = date_trip_end","title":"compute_trip_dates()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.contextualize_file","text":"Replace generic strings in the generated file with realistic strings. Parameters: contents \u2013 A string, the generic file contents. Returns: A string, the contextualized version. Source code in beancount/scripts/example.py def contextualize_file(contents, employer): \"\"\"Replace generic strings in the generated file with realistic strings. Args: contents: A string, the generic file contents. Returns: A string, the contextualized version. \"\"\" replacements = { 'CC': 'US', 'Bank1': 'BofA', 'Bank1_Institution': 'Bank of America', 'Bank1_Address': '123 America Street, LargeTown, USA', 'Bank1_Phone': '+1.012.345.6789', 'CreditCard1': 'Chase:Slate', 'CreditCard2': 'Amex:BlueCash', 'Employer1': employer, 'Retirement': 'Vanguard', 'Retirement_Institution': 'Vanguard Group', 'Retirement_Address': \"P.O. Box 1110, Valley Forge, PA 19482-1110\", 'Retirement_Phone': \"+1.800.523.1188\", 'Investment': 'ETrade', # Commodities 'CCY': 'USD', 'VACHR': 'VACHR', 'DEFCCY': 'IRAUSD', 'MFUND1': 'VBMPX', 'MFUND2': 'RGAGX', 'STK1': 'ITOT', 'STK2': 'VEA', 'STK3': 'VHT', 'STK4': 'GLD', } new_contents = replace(contents, replacements) return new_contents, replacements","title":"contextualize_file()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.date_iter","text":"Generate a sequence of dates. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Instances of datetime.date. Source code in beancount/scripts/example.py def date_iter(date_begin, date_end): \"\"\"Generate a sequence of dates. Args: date_begin: The start date. date_end: The end date. Yields: Instances of datetime.date. \"\"\" assert date_begin <= date_end date = date_begin one_day = datetime.timedelta(days=1) while date < date_end: date += one_day yield date","title":"date_iter()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.date_random_seq","text":"Generate a sequence of dates with some random increase in days. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. days_min \u2013 The minimum number of days to advance on each iteration. days_max \u2013 The maximum number of days to advance on each iteration. Yields: Instances of datetime.date. Source code in beancount/scripts/example.py def date_random_seq(date_begin, date_end, days_min, days_max): \"\"\"Generate a sequence of dates with some random increase in days. Args: date_begin: The start date. date_end: The end date. days_min: The minimum number of days to advance on each iteration. days_max: The maximum number of days to advance on each iteration. Yields: Instances of datetime.date. \"\"\" assert days_min > 0 assert days_min <= days_max date = date_begin while date < date_end: nb_days_forward = random.randint(days_min, days_max) date += datetime.timedelta(days=nb_days_forward) if date >= date_end: break yield date","title":"date_random_seq()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.delay_dates","text":"Delay the dates from the given iterator by some uniformly drawn number of days. Parameters: date_iter \u2013 An iterator of datetime.date instances. delay_days_min \u2013 The minimum amount of advance days for the transaction. delay_days_max \u2013 The maximum amount of advance days for the transaction. Yields: datetime.date instances. Source code in beancount/scripts/example.py def delay_dates(date_iter, delay_days_min, delay_days_max): \"\"\"Delay the dates from the given iterator by some uniformly drawn number of days. Args: date_iter: An iterator of datetime.date instances. delay_days_min: The minimum amount of advance days for the transaction. delay_days_max: The maximum amount of advance days for the transaction. Yields: datetime.date instances. \"\"\" dates = list(date_iter) last_date = dates[-1] last_date = last_date.date() if isinstance(last_date, datetime.datetime) else last_date for dtime in dates: date = dtime.date() if isinstance(dtime, datetime.datetime) else dtime date += datetime.timedelta(days=random.randint(delay_days_min, delay_days_max)) if date >= last_date: break yield date","title":"delay_dates()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_balance_checks","text":"Generate balance check entries to the given frequency. Parameters: entries \u2013 A list of directives that contain all the transactions for the accounts. account \u2013 The name of the account for which to generate. date_iter \u2013 Iterator of dates. We generate balance checks at these dates. Returns: A list of balance check entries. Source code in beancount/scripts/example.py def generate_balance_checks(entries, account, date_iter): \"\"\"Generate balance check entries to the given frequency. Args: entries: A list of directives that contain all the transactions for the accounts. account: The name of the account for which to generate. date_iter: Iterator of dates. We generate balance checks at these dates. Returns: A list of balance check entries. \"\"\" balance_checks = [] date_iter = iter(date_iter) next_date = next(date_iter) with misc_utils.swallow(StopIteration): for txn_posting, balance in postings_for(entries, [account], before=True): while txn_posting.txn.date >= next_date: amount = balance[account].get_currency_units('CCY').number balance_checks.extend(parse(\"\"\" {next_date} balance {account} {amount} CCY \"\"\", **locals())) next_date = next(date_iter) return balance_checks","title":"generate_balance_checks()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_banking","text":"Generate a checking account opening. Parameters: entries \u2013 A list of entries which affect this account. date_begin \u2013 A date instance, the beginning date. date_end \u2013 A date instance, the end date. amount_initial \u2013 A Decimal instance, the amount to initialize the checking account with. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_banking(entries, date_begin, date_end, amount_initial): \"\"\"Generate a checking account opening. Args: entries: A list of entries which affect this account. date_begin: A date instance, the beginning date. date_end: A date instance, the end date. amount_initial: A Decimal instance, the amount to initialize the checking account with. Returns: A list of directives. \"\"\" amount_initial_neg = -amount_initial new_entries = parse(\"\"\" {date_begin} open Assets:CC:Bank1 institution: \"Bank1_Institution\" address: \"Bank1_Address\" phone: \"Bank1_Phone\" {date_begin} open Assets:CC:Bank1:Checking CCY account: \"00234-48574897\" ;; {date_begin} open Assets:CC:Bank1:Savings CCY {date_begin} * \"Opening Balance for checking account\" Assets:CC:Bank1:Checking {amount_initial} CCY Equity:Opening-Balances {amount_initial_neg} CCY \"\"\", **locals()) date_balance = date_begin + datetime.timedelta(days=1) account = 'Assets:CC:Bank1:Checking' for txn_posting, balances in postings_for(data.sorted(entries + new_entries), [account], before=True): if txn_posting.txn.date >= date_balance: break amount_balance = balances[account].get_currency_units('CCY').number bal_entries = parse(\"\"\" {date_balance} balance Assets:CC:Bank1:Checking {amount_balance} CCY \"\"\", **locals()) return new_entries + bal_entries","title":"generate_banking()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_banking_expenses","text":"Generate expenses paid out of a checking account, typically living expenses. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. account \u2013 The checking account to generate expenses to. rent_amount \u2013 The amount of rent. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_banking_expenses(date_begin, date_end, account, rent_amount): \"\"\"Generate expenses paid out of a checking account, typically living expenses. Args: date_begin: The start date. date_end: The end date. account: The checking account to generate expenses to. rent_amount: The amount of rent. Returns: A list of directives. \"\"\" fee_expenses = generate_periodic_expenses( rrule.rrule(rrule.MONTHLY, bymonthday=4, dtstart=date_begin, until=date_end), \"BANK FEES\", \"Monthly bank fee\", account, 'Expenses:Financial:Fees', lambda: D('4.00')) rent_expenses = generate_periodic_expenses( delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 2, 5), \"RiverBank Properties\", \"Paying the rent\", account, 'Expenses:Home:Rent', lambda: random.normalvariate(float(rent_amount), 0)) electricity_expenses = generate_periodic_expenses( delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 7, 8), \"EDISON POWER\", \"\", account, 'Expenses:Home:Electricity', lambda: random.normalvariate(65, 0)) internet_expenses = generate_periodic_expenses( delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 20, 22), \"Wine-Tarner Cable\", \"\", account, 'Expenses:Home:Internet', lambda: random.normalvariate(80, 0.10)) phone_expenses = generate_periodic_expenses( delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end), 17, 19), \"Verizon Wireless\", \"\", account, 'Expenses:Home:Phone', lambda: random.normalvariate(60, 10)) return data.sorted(fee_expenses + rent_expenses + electricity_expenses + internet_expenses + phone_expenses)","title":"generate_banking_expenses()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_clearing_entries","text":"Generate entries to clear the value of an account. Parameters: date_iter \u2013 An iterator of datetime.date instances. payee \u2013 A string, the payee name to use on the transactions. narration \u2013 A string, the narration to use on the transactions. entries \u2013 A list of entries. account_clear \u2013 The account to clear. account_from \u2013 The source account to clear 'account_clear' from. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_clearing_entries(date_iter, payee, narration, entries, account_clear, account_from): \"\"\"Generate entries to clear the value of an account. Args: date_iter: An iterator of datetime.date instances. payee: A string, the payee name to use on the transactions. narration: A string, the narration to use on the transactions. entries: A list of entries. account_clear: The account to clear. account_from: The source account to clear 'account_clear' from. Returns: A list of directives. \"\"\" # The next date we're looking for. next_date = next(iter(date_iter)) # Iterate over all the postings of the account to clear. new_entries = [] for txn_posting, balances in postings_for(entries, [account_clear]): balance_clear = balances[account_clear] # Check if we need to clear. if next_date <= txn_posting.txn.date: pos_amount = balance_clear.get_currency_units('CCY') neg_amount = -pos_amount new_entries.extend(parse(\"\"\" {next_date} * \"{payee}\" \"{narration}\" {account_clear} {neg_amount.number:.2f} CCY {account_from} {pos_amount.number:.2f} CCY \"\"\", **locals())) balance_clear.add_amount(neg_amount) # Advance to the next date we're looking for. try: next_date = next(iter(date_iter)) except StopIteration: break return new_entries","title":"generate_clearing_entries()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_commodity_entries","text":"Create a list of Commodity entries for all the currencies we're using. Parameters: date_birth \u2013 A datetime.date instance, the date of birth of the user. Returns: A list of Commodity entries for all the commodities in use. Source code in beancount/scripts/example.py def generate_commodity_entries(date_birth): \"\"\"Create a list of Commodity entries for all the currencies we're using. Args: date_birth: A datetime.date instance, the date of birth of the user. Returns: A list of Commodity entries for all the commodities in use. \"\"\" return parse(\"\"\" 1792-01-01 commodity USD name: \"US Dollar\" export: \"CASH\" {date_birth} commodity VACHR name: \"Employer Vacation Hours\" export: \"IGNORE\" {date_birth} commodity IRAUSD name: \"US 401k and IRA Contributions\" export: \"IGNORE\" 2009-05-01 commodity RGAGX name: \"American Funds The Growth Fund of America Class R-6\" export: \"MUTF:RGAGX\" price: \"USD:google/MUTF:RGAGX\" 1995-09-18 commodity VBMPX name: \"Vanguard Total Bond Market Index Fund Institutional Plus Shares\" export: \"MUTF:VBMPX\" price: \"USD:google/MUTF:VBMPX\" 2004-01-20 commodity ITOT name: \"iShares Core S&P Total U.S. Stock Market ETF\" export: \"NYSEARCA:ITOT\" price: \"USD:google/NYSEARCA:ITOT\" 2007-07-20 commodity VEA name: \"Vanguard FTSE Developed Markets ETF\" export: \"NYSEARCA:VEA\" price: \"USD:google/NYSEARCA:VEA\" 2004-01-26 commodity VHT name: \"Vanguard Health Care ETF\" export: \"NYSEARCA:VHT\" price: \"USD:google/NYSEARCA:VHT\" 2004-11-01 commodity GLD name: \"SPDR Gold Trust (ETF)\" export: \"NYSEARCA:GLD\" price: \"USD:google/NYSEARCA:GLD\" 1900-01-01 commodity VMMXX export: \"MUTF:VMMXX (MONEY:USD)\" \"\"\", **locals())","title":"generate_commodity_entries()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_employment_income","text":"Generate bi-weekly entries for payroll salary income. Parameters: employer_name \u2013 A string, the human-readable name of the employer. employer_address \u2013 A string, the address of the employer. annual_salary \u2013 A Decimal, the annual salary of the employee. account_deposit \u2013 An account string, the account to deposit the salary to. account_retirement \u2013 An account string, the account to deposit retirement contributions to. date_begin \u2013 The start date. date_end \u2013 The end date. Returns: A list of directives, including open directives for the account. Source code in beancount/scripts/example.py def generate_employment_income(employer_name, employer_address, annual_salary, account_deposit, account_retirement, date_begin, date_end): \"\"\"Generate bi-weekly entries for payroll salary income. Args: employer_name: A string, the human-readable name of the employer. employer_address: A string, the address of the employer. annual_salary: A Decimal, the annual salary of the employee. account_deposit: An account string, the account to deposit the salary to. account_retirement: An account string, the account to deposit retirement contributions to. date_begin: The start date. date_end: The end date. Returns: A list of directives, including open directives for the account. \"\"\" preamble = parse(\"\"\" {date_begin} event \"employer\" \"{employer_name}, {employer_address}\" {date_begin} open Income:CC:Employer1:Salary CCY ;{date_begin} open Income:CC:Employer1:AnnualBonus CCY {date_begin} open Income:CC:Employer1:GroupTermLife CCY {date_begin} open Income:CC:Employer1:Vacation VACHR {date_begin} open Assets:CC:Employer1:Vacation VACHR {date_begin} open Expenses:Vacation VACHR {date_begin} open Expenses:Health:Life:GroupTermLife {date_begin} open Expenses:Health:Medical:Insurance {date_begin} open Expenses:Health:Dental:Insurance {date_begin} open Expenses:Health:Vision:Insurance ;{date_begin} open Expenses:Vacation:Employer \"\"\", **locals()) date_prev = None contrib_retirement = ZERO contrib_socsec = ZERO biweekly_pay = annual_salary / 26 gross = biweekly_pay medicare = gross * D('0.0231') federal = gross * D('0.2303') state = gross * D('0.0791') city = gross * D('0.0379') sdi = D('1.12') lifeinsurance = D('24.32') dental = D('2.90') medical = D('27.38') vision = D('42.30') fixed = (medicare + federal + state + city + sdi + dental + medical + vision) # Calculate vacation hours per-pay. with decimal.localcontext() as ctx: ctx.prec = 4 vacation_hrs = (ANNUAL_VACATION_DAYS * D('8')) / D('26') transactions = [] for dtime in misc_utils.skipiter( rrule.rrule(rrule.WEEKLY, byweekday=rrule.TH, dtstart=date_begin, until=date_end), 2): date = dtime.date() year = date.year if not date_prev or date_prev.year != date.year: contrib_retirement = RETIREMENT_LIMITS.get(date.year, RETIREMENT_LIMITS[None]) contrib_socsec = D('7000') date_prev = date retirement_uncapped = math.ceil((gross * D('0.25')) / 100) * 100 retirement = min(contrib_retirement, retirement_uncapped) contrib_retirement -= retirement socsec_uncapped = gross * D('0.0610') socsec = min(contrib_socsec, socsec_uncapped) contrib_socsec -= socsec with decimal.localcontext() as ctx: ctx.prec = 6 deposit = (gross - retirement - fixed - socsec) retirement_neg = -retirement gross_neg = -gross lifeinsurance_neg = -lifeinsurance vacation_hrs_neg = -vacation_hrs template = \"\"\" {date} * \"{employer_name}\" \"Payroll\" {account_deposit} {deposit:.2f} CCY {account_retirement} {retirement:.2f} CCY Assets:CC:Federal:PreTax401k {retirement_neg:.2f} DEFCCY Expenses:Taxes:Y{year}:CC:Federal:PreTax401k {retirement:.2f} DEFCCY Income:CC:Employer1:Salary {gross_neg:.2f} CCY Income:CC:Employer1:GroupTermLife {lifeinsurance_neg:.2f} CCY Expenses:Health:Life:GroupTermLife {lifeinsurance:.2f} CCY Expenses:Health:Dental:Insurance {dental} CCY Expenses:Health:Medical:Insurance {medical} CCY Expenses:Health:Vision:Insurance {vision} CCY Expenses:Taxes:Y{year}:CC:Medicare {medicare:.2f} CCY Expenses:Taxes:Y{year}:CC:Federal {federal:.2f} CCY Expenses:Taxes:Y{year}:CC:State {state:.2f} CCY Expenses:Taxes:Y{year}:CC:CityNYC {city:.2f} CCY Expenses:Taxes:Y{year}:CC:SDI {sdi:.2f} CCY Expenses:Taxes:Y{year}:CC:SocSec {socsec:.2f} CCY Assets:CC:Employer1:Vacation {vacation_hrs:.2f} VACHR Income:CC:Employer1:Vacation {vacation_hrs_neg:.2f} VACHR \"\"\" if retirement == ZERO: # Remove retirement lines. template = '\\n'.join(line for line in template.splitlines() if not re.search(r'\\bretirement\\b', line)) transactions.extend(parse(template, **locals())) return preamble + transactions","title":"generate_employment_income()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_expense_accounts","text":"Generate directives for expense accounts. Parameters: date_birth \u2013 Birth date of the character. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_expense_accounts(date_birth): \"\"\"Generate directives for expense accounts. Args: date_birth: Birth date of the character. Returns: A list of directives. \"\"\" return parse(\"\"\" {date_birth} open Expenses:Food:Groceries {date_birth} open Expenses:Food:Restaurant {date_birth} open Expenses:Food:Coffee {date_birth} open Expenses:Food:Alcohol {date_birth} open Expenses:Transport:Tram {date_birth} open Expenses:Home:Rent {date_birth} open Expenses:Home:Electricity {date_birth} open Expenses:Home:Internet {date_birth} open Expenses:Home:Phone {date_birth} open Expenses:Financial:Fees {date_birth} open Expenses:Financial:Commissions \"\"\", **locals())","title":"generate_expense_accounts()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_open_entries","text":"Generate a list of Open entries for the given accounts: Parameters: date \u2013 A datetime.date instance for the open entries. accounts \u2013 A list of account strings. currency \u2013 An optional currency constraint. Returns: A list of Open directives. Source code in beancount/scripts/example.py def generate_open_entries(date, accounts, currency=None): \"\"\"Generate a list of Open entries for the given accounts: Args: date: A datetime.date instance for the open entries. accounts: A list of account strings. currency: An optional currency constraint. Returns: A list of Open directives. \"\"\" assert isinstance(accounts, (list, tuple)) return parse(''.join( '{date} open {account} {currency}\\n'.format(date=date, account=account, currency=currency or '') for account in accounts))","title":"generate_open_entries()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_outgoing_transfers","text":"Generate transfers of accumulated funds out of an account. This monitors the balance of an account and when it is beyond a threshold, generate out transfers form that account to another account. Parameters: entries \u2013 A list of existing entries that affect this account so far. The generated entries will also affect this account. account \u2013 An account string, the account to monitor. account_out \u2013 An account string, the savings account to make transfers to. transfer_minimum \u2013 The minimum amount of funds to always leave in this account after a transfer. transfer_threshold \u2013 The minimum amount of funds to be able to transfer out without breaking the minimum. transfer_increment \u2013 A Decimal, the increment to round transfers to. Returns: A list of new directives, the transfers to add to the given account. Source code in beancount/scripts/example.py def generate_outgoing_transfers(entries, account, account_out, transfer_minimum, transfer_threshold, transfer_increment): \"\"\"Generate transfers of accumulated funds out of an account. This monitors the balance of an account and when it is beyond a threshold, generate out transfers form that account to another account. Args: entries: A list of existing entries that affect this account so far. The generated entries will also affect this account. account: An account string, the account to monitor. account_out: An account string, the savings account to make transfers to. transfer_minimum: The minimum amount of funds to always leave in this account after a transfer. transfer_threshold: The minimum amount of funds to be able to transfer out without breaking the minimum. transfer_increment: A Decimal, the increment to round transfers to. Returns: A list of new directives, the transfers to add to the given account. \"\"\" last_date = entries[-1].date # Reverse the balance amounts taking into account the minimum balance for # all time in the future. amounts = [(balances[account].get_currency_units('CCY').number, txn_posting) for txn_posting, balances in postings_for(entries, [account])] reversed_amounts = [] last_amount, _ = amounts[-1] for current_amount, _ in reversed(amounts): if current_amount < last_amount: reversed_amounts.append(current_amount) last_amount = current_amount else: reversed_amounts.append(last_amount) capped_amounts = reversed(reversed_amounts) # Create transfers outward where the future allows it. new_entries = [] offset_amount = ZERO for current_amount, (_, txn_posting) in zip(capped_amounts, amounts): if txn_posting.txn.date >= last_date: break adjusted_amount = current_amount - offset_amount if adjusted_amount > (transfer_minimum + transfer_threshold): amount_transfer = round_to(adjusted_amount - transfer_minimum, transfer_increment) date = txn_posting.txn.date + datetime.timedelta(days=1) amount_transfer_neg = -amount_transfer new_entries.extend(parse(\"\"\" {date} * \"Transfering accumulated savings to other account\" {account} {amount_transfer_neg:2f} CCY {account_out} {amount_transfer:2f} CCY \"\"\", **locals())) offset_amount += amount_transfer return new_entries","title":"generate_outgoing_transfers()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_periodic_expenses","text":"Generate periodic expense transactions. Parameters: date_iter \u2013 An iterator for dates or datetimes. payee \u2013 A string, the payee name to use on the transactions, or a set of such strings to randomly choose from narration \u2013 A string, the narration to use on the transactions. account_from \u2013 An account string the debited account. account_to \u2013 An account string the credited account. amount_generator \u2013 A callable object to generate variates. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_periodic_expenses(date_iter, payee, narration, account_from, account_to, amount_generator): \"\"\"Generate periodic expense transactions. Args: date_iter: An iterator for dates or datetimes. payee: A string, the payee name to use on the transactions, or a set of such strings to randomly choose from narration: A string, the narration to use on the transactions. account_from: An account string the debited account. account_to: An account string the credited account. amount_generator: A callable object to generate variates. Returns: A list of directives. \"\"\" new_entries = [] for dtime in date_iter: date = dtime.date() if isinstance(dtime, datetime.datetime) else dtime amount = D(amount_generator()) txn_payee = (payee if isinstance(payee, str) else random.choice(payee)) txn_narration = (narration if isinstance(narration, str) else random.choice(narration)) amount_neg = -amount new_entries.extend(parse(\"\"\" {date} * \"{txn_payee}\" \"{txn_narration}\" {account_from} {amount_neg:.2f} CCY {account_to} {amount:.2f} CCY \"\"\", **locals())) return new_entries","title":"generate_periodic_expenses()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_prices","text":"Generate weekly or monthly price entries for the given currencies. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. currencies \u2013 A list of currency strings to generate prices for. cost_currency \u2013 A string, the cost currency. Returns: A list of Price directives. Source code in beancount/scripts/example.py def generate_prices(date_begin, date_end, currencies, cost_currency): \"\"\"Generate weekly or monthly price entries for the given currencies. Args: date_begin: The start date. date_end: The end date. currencies: A list of currency strings to generate prices for. cost_currency: A string, the cost currency. Returns: A list of Price directives. \"\"\" digits = D('0.01') entries = [] counter = itertools.count() for currency in currencies: start_price = random.uniform(30, 200) growth = random.uniform(0.02, 0.13) # %/year mu = growth * (7 / 365) sigma = random.uniform(0.005, 0.02) # Vol for dtime, price_float in zip(rrule.rrule(rrule.WEEKLY, byweekday=rrule.FR, dtstart=date_begin, until=date_end), price_series(start_price, mu, sigma)): price = D(price_float).quantize(digits) meta = data.new_metadata(generate_prices.__name__, next(counter)) entry = data.Price(meta, dtime.date(), currency, amount.Amount(price, cost_currency)) entries.append(entry) return entries","title":"generate_prices()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_regular_credit_expenses","text":"Generate expenses paid out of a credit card account, including payments to the credit card. Parameters: date_birth \u2013 The user's birth date. date_begin \u2013 The start date. date_end \u2013 The end date. account_credit \u2013 The credit card account to generate expenses against. account_checking \u2013 The checking account to generate payments from. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_regular_credit_expenses(date_birth, date_begin, date_end, account_credit, account_checking): \"\"\"Generate expenses paid out of a credit card account, including payments to the credit card. Args: date_birth: The user's birth date. date_begin: The start date. date_end: The end date. account_credit: The credit card account to generate expenses against. account_checking: The checking account to generate payments from. Returns: A list of directives. \"\"\" restaurant_expenses = generate_periodic_expenses( date_random_seq(date_begin, date_end, 1, 5), RESTAURANT_NAMES, RESTAURANT_NARRATIONS, account_credit, 'Expenses:Food:Restaurant', lambda: min(random.lognormvariate(math.log(30), math.log(1.5)), random.randint(200, 220))) groceries_expenses = generate_periodic_expenses( date_random_seq(date_begin, date_end, 5, 20), GROCERIES_NAMES, \"Buying groceries\", account_credit, 'Expenses:Food:Groceries', lambda: min(random.lognormvariate(math.log(80), math.log(1.3)), random.randint(250, 300))) subway_expenses = generate_periodic_expenses( date_random_seq(date_begin, date_end, 27, 33), \"Metro Transport Authority\", \"Tram tickets\", account_credit, 'Expenses:Transport:Tram', lambda: D('120.00')) credit_expenses = data.sorted(restaurant_expenses + groceries_expenses + subway_expenses) # Entries to open accounts. credit_preamble = generate_open_entries(date_birth, [account_credit], 'CCY') return data.sorted(credit_preamble + credit_expenses)","title":"generate_regular_credit_expenses()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_retirement_employer_match","text":"Generate employer matching contributions into a retirement account. Parameters: entries \u2013 A list of directives that cover the retirement account. account_invest \u2013 The name of the retirement cash account. account_income \u2013 The name of the income account. Returns: A list of new entries generated for employer contributions. Source code in beancount/scripts/example.py def generate_retirement_employer_match(entries, account_invest, account_income): \"\"\"Generate employer matching contributions into a retirement account. Args: entries: A list of directives that cover the retirement account. account_invest: The name of the retirement cash account. account_income: The name of the income account. Returns: A list of new entries generated for employer contributions. \"\"\" match_frac = D('0.50') new_entries = parse(\"\"\" {date} open {account_income} CCY \"\"\", date=entries[0].date, account_income=account_income) for txn_posting, balances in postings_for(entries, [account_invest]): amount = txn_posting.posting.units.number * match_frac amount_neg = -amount date = txn_posting.txn.date + ONE_DAY new_entries.extend(parse(\"\"\" {date} * \"Employer match for contribution\" {account_invest} {amount:.2f} CCY {account_income} {amount_neg:.2f} CCY \"\"\", **locals())) return new_entries","title":"generate_retirement_employer_match()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_retirement_investments","text":"Invest money deposited to the given retirement account. Parameters: entries \u2013 A list of directives account \u2013 The root account for all retirement investment sub-accounts. commodities_items \u2013 A list of (commodity, fraction to be invested in) items. price_map \u2013 A dict of prices, as per beancount.core.prices.build_price_map(). Returns: A list of new directives for the given investments. This also generates account opening directives for the desired investment commodities. Source code in beancount/scripts/example.py def generate_retirement_investments(entries, account, commodities_items, price_map): \"\"\"Invest money deposited to the given retirement account. Args: entries: A list of directives account: The root account for all retirement investment sub-accounts. commodities_items: A list of (commodity, fraction to be invested in) items. price_map: A dict of prices, as per beancount.core.prices.build_price_map(). Returns: A list of new directives for the given investments. This also generates account opening directives for the desired investment commodities. \"\"\" open_entries = [] account_cash = join(account, 'Cash') date_origin = entries[0].date open_entries.extend(parse(\"\"\" {date_origin} open {account} CCY institution: \"Retirement_Institution\" address: \"Retirement_Address\" phone: \"Retirement_Phone\" {date_origin} open {account_cash} CCY number: \"882882\" \"\"\", **locals())) for currency, _ in commodities_items: open_entries.extend(parse(\"\"\" {date_origin} open {account}:{currency} {currency} number: \"882882\" \"\"\", **locals())) new_entries = [] for txn_posting, balances in postings_for(entries, [account_cash]): balance = balances[account_cash] amount_to_invest = balance.get_currency_units('CCY').number # Find the date the following Monday, the date to invest. txn_date = txn_posting.txn.date while txn_date.weekday() != calendar.MONDAY: txn_date += ONE_DAY amount_invested = ZERO for commodity, fraction in commodities_items: amount_fraction = amount_to_invest * D(fraction) # Find the price at that date. _, price = prices.get_price(price_map, (commodity, 'CCY'), txn_date) units = (amount_fraction / price).quantize(D('0.001')) amount_cash = (units * price).quantize(D('0.01')) amount_cash_neg = -amount_cash new_entries.extend(parse(\"\"\" {txn_date} * \"Investing {fraction:.0%} of cash in {commodity}\" {account}:{commodity} {units:.3f} {commodity} {{{price:.2f} CCY}} {account}:Cash {amount_cash_neg:.2f} CCY \"\"\", **locals())) balance.add_amount(amount.Amount(-amount_cash, 'CCY')) return data.sorted(open_entries + new_entries)","title":"generate_retirement_investments()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_tax_accounts","text":"Generate accounts and contribution directives for a particular tax year. Parameters: year \u2013 An integer, the year we're to generate this for. date_max \u2013 The maximum date to produce an entry for. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_tax_accounts(year, date_max): \"\"\"Generate accounts and contribution directives for a particular tax year. Args: year: An integer, the year we're to generate this for. date_max: The maximum date to produce an entry for. Returns: A list of directives. \"\"\" date_year = datetime.date(year, 1, 1) date_filing = (datetime.date(year + 1, 3, 20) + datetime.timedelta(days=random.randint(0, 5))) date_federal = (date_filing + datetime.timedelta(days=random.randint(0, 4))) date_state = (date_filing + datetime.timedelta(days=random.randint(0, 4))) quantum = D('0.01') amount_federal = D(max(random.normalvariate(500, 120), 12)).quantize(quantum) amount_federal_neg = -amount_federal amount_state = D(max(random.normalvariate(300, 100), 10)).quantize(quantum) amount_state_neg = -amount_state amount_payable = -(amount_federal + amount_state) amount_limit = RETIREMENT_LIMITS.get(year, RETIREMENT_LIMITS[None]) amount_limit_neg = -amount_limit entries = parse(\"\"\" ;; Open tax accounts for that year. {date_year} open Expenses:Taxes:Y{year}:CC:Federal:PreTax401k DEFCCY {date_year} open Expenses:Taxes:Y{year}:CC:Medicare CCY {date_year} open Expenses:Taxes:Y{year}:CC:Federal CCY {date_year} open Expenses:Taxes:Y{year}:CC:CityNYC CCY {date_year} open Expenses:Taxes:Y{year}:CC:SDI CCY {date_year} open Expenses:Taxes:Y{year}:CC:State CCY {date_year} open Expenses:Taxes:Y{year}:CC:SocSec CCY ;; Check that the tax amounts have been fully used. {date_year} balance Assets:CC:Federal:PreTax401k 0 DEFCCY {date_year} * \"Allowed contributions for one year\" Income:CC:Federal:PreTax401k {amount_limit_neg} DEFCCY Assets:CC:Federal:PreTax401k {amount_limit} DEFCCY {date_filing} * \"Filing taxes for {year}\" Expenses:Taxes:Y{year}:CC:Federal {amount_federal:.2f} CCY Expenses:Taxes:Y{year}:CC:State {amount_state:.2f} CCY Liabilities:AccountsPayable {amount_payable:.2f} CCY {date_federal} * \"FEDERAL TAXPYMT\" Assets:CC:Bank1:Checking {amount_federal_neg:.2f} CCY Liabilities:AccountsPayable {amount_federal:.2f} CCY {date_state} * \"STATE TAX & FINANC PYMT\" Assets:CC:Bank1:Checking {amount_state_neg:.2f} CCY Liabilities:AccountsPayable {amount_state:.2f} CCY \"\"\", **locals()) return [entry for entry in entries if entry.date < date_max]","title":"generate_tax_accounts()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_tax_preamble","text":"Generate tax declarations not specific to any particular year. Parameters: date_birth \u2013 A date instance, the birth date of the character. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_tax_preamble(date_birth): \"\"\"Generate tax declarations not specific to any particular year. Args: date_birth: A date instance, the birth date of the character. Returns: A list of directives. \"\"\" return parse(\"\"\" ;; Tax accounts not specific to a year. {date_birth} open Income:CC:Federal:PreTax401k DEFCCY {date_birth} open Assets:CC:Federal:PreTax401k DEFCCY \"\"\", **locals())","title":"generate_tax_preamble()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_taxable_investment","text":"Generate opening directives and transactions for an investment account. Parameters: date_begin \u2013 A date instance, the beginning date. date_end \u2013 A date instance, the end date. entries \u2013 A list of entries that contains at least the transfers to the investment account's cash account. price_map \u2013 A dict of prices, as per beancount.core.prices.build_price_map(). stocks \u2013 A list of strings, the list of commodities to invest in. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_taxable_investment(date_begin, date_end, entries, price_map, stocks): \"\"\"Generate opening directives and transactions for an investment account. Args: date_begin: A date instance, the beginning date. date_end: A date instance, the end date. entries: A list of entries that contains at least the transfers to the investment account's cash account. price_map: A dict of prices, as per beancount.core.prices.build_price_map(). stocks: A list of strings, the list of commodities to invest in. Returns: A list of directives. \"\"\" account = 'Assets:CC:Investment' account_cash = join(account, 'Cash') account_gains = 'Income:CC:Investment:Gains' account_dividends = 'Income:CC:Investment:Dividends' accounts_stocks = ['Assets:CC:Investment:{}'.format(commodity) for commodity in stocks] open_entries = parse(\"\"\" {date_begin} open {account}:Cash CCY {date_begin} open {account_gains} CCY {date_begin} open {account_dividends} CCY \"\"\", **locals()) for stock in stocks: open_entries.extend(parse(\"\"\" {date_begin} open {account}:{stock} {stock} \"\"\", **locals())) # Figure out dates at which dividends should be distributed, near the end of # each quarter. days_to = datetime.timedelta(days=3*90-10) dividend_dates = [] for quarter_begin in iter_quarters(date_begin, date_end): end_of_quarter = quarter_begin + days_to if not (date_begin < end_of_quarter < date_end): continue dividend_dates.append(end_of_quarter) # Iterate over all the dates, but merging in the postings for the cash # account. min_amount = D('1000.00') round_amount = D('100.00') commission = D('8.95') round_units = D('1') frac_invest = D('1.00') frac_dividend = D('0.004') p_daily_buy = 1./15 # days p_daily_sell = 1./90 # days stocks_inventory = inventory.Inventory() new_entries = [] dividend_date_iter = iter(dividend_dates) next_dividend_date = next(dividend_date_iter) for date, balances in iter_dates_with_balance(date_begin, date_end, entries, [account_cash]): # Check if we should insert a dividend. Note that we could not factor # this out because we want to explicitly reinvest the cash dividends and # we also want the dividends to be proportional to the amount of # invested stock, so one feeds on the other and vice-versa. if next_dividend_date and date > next_dividend_date: # Compute the total balances for the stock accounts in order to # create a realistic dividend. total = inventory.Inventory() for account_stock in accounts_stocks: total.add_inventory(balances[account_stock]) # Create an entry offering dividends of 1% of the portfolio. portfolio_cost = total.reduce(convert.get_cost).get_currency_units('CCY').number amount_cash = (frac_dividend * portfolio_cost).quantize(D('0.01')) amount_cash_neg = -amount_cash dividend = parse(\"\"\" {next_dividend_date} * \"Dividends on portfolio\" {account}:Cash {amount_cash:.2f} CCY {account_dividends} {amount_cash_neg:.2f} CCY \"\"\", **locals())[0] new_entries.append(dividend) # Advance the next dividend date. try: next_dividend_date = next(dividend_date_iter) except StopIteration: next_dividend_date = None # If the balance is high, buy with high probability. balance = balances[account_cash] total_cash = balance.get_currency_units('CCY').number assert total_cash >= ZERO, ('Cash balance is negative: {}'.format(total_cash)) invest_cash = total_cash * frac_invest - commission if invest_cash > min_amount: if random.random() < p_daily_buy: commodities = random.sample(stocks, random.randint(1, len(stocks))) lot_amount = round_to(invest_cash / len(commodities), round_amount) invested_amount = ZERO for stock in commodities: # Find the price at that date. _, price = prices.get_price(price_map, (stock, 'CCY'), date) units = round_to((lot_amount / price), round_units) if units <= ZERO: continue amount_cash = -(units * price + commission) # logging.info('Buying %s %s @ %s CCY = %s CCY', # units, stock, price, units * price) buy = parse(\"\"\" {date} * \"Buy shares of {stock}\" {account}:Cash {amount_cash:.2f} CCY {account}:{stock} {units:.0f} {stock} {{{price:.2f} CCY}} Expenses:Financial:Commissions {commission:.2f} CCY \"\"\", **locals())[0] new_entries.append(buy) account_stock = ':'.join([account, stock]) balances[account_cash].add_position(buy.postings[0]) balances[account_stock].add_position(buy.postings[1]) stocks_inventory.add_position(buy.postings[1]) # Don't sell on days you buy. continue # Otherwise, sell with low probability. if not stocks_inventory.is_empty() and random.random() < p_daily_sell: # Choose the lot with the highest gain or highest loss. gains = [] for position in stocks_inventory.get_positions(): base_quote = (position.units.currency, position.cost.currency) _, price = prices.get_price(price_map, base_quote, date) if price == position.cost.number: continue # Skip lots without movement. market_value = position.units.number * price book_value = convert.get_cost(position).number gain = market_value - book_value gains.append((gain, market_value, price, position)) if not gains: continue # Sell either biggest winner or biggest loser. biggest = bool(random.random() < 0.5) lot_tuple = sorted(gains)[0 if biggest else -1] gain, market_value, price, sell_position = lot_tuple #logging.info('Selling {} for {}'.format(sell_position, market_value)) sell_position = -sell_position stock = sell_position.units.currency amount_cash = market_value - commission amount_gain = -gain sell = parse(\"\"\" {date} * \"Sell shares of {stock}\" {account}:{stock} {sell_position} @ {price:.2f} CCY {account}:Cash {amount_cash:.2f} CCY Expenses:Financial:Commissions {commission:.2f} CCY {account_gains} {amount_gain:.2f} CCY \"\"\", **locals())[0] new_entries.append(sell) balances[account_cash].add_position(sell.postings[1]) stocks_inventory.add_position(sell.postings[0]) continue return open_entries + new_entries","title":"generate_taxable_investment()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_trip_entries","text":"Generate more dense expenses for a trip. Parameters: date_begin \u2013 A datetime.date instance, the beginning of the trip. date_end \u2013 A datetime.date instance, the end of the trip. tag \u2013 A string, the name of the tag. config \u2013 A list of (payee name, account name, (mu, 3sigma)), where mu is the mean of the prices to generate and 3sigma is 3 times the standard deviation. trip_city \u2013 A string, the capitalized name of the destination city. home_city \u2013 A string, the name of the home city. account_credit \u2013 A string, the name of the credit card account to pay the expenses from. Returns: A list of entries for the trip, all tagged with the given tag. Source code in beancount/scripts/example.py def generate_trip_entries(date_begin, date_end, tag, config, trip_city, home_city, account_credit): \"\"\"Generate more dense expenses for a trip. Args: date_begin: A datetime.date instance, the beginning of the trip. date_end: A datetime.date instance, the end of the trip. tag: A string, the name of the tag. config: A list of (payee name, account name, (mu, 3sigma)), where mu is the mean of the prices to generate and 3sigma is 3 times the standard deviation. trip_city: A string, the capitalized name of the destination city. home_city: A string, the name of the home city. account_credit: A string, the name of the credit card account to pay the expenses from. Returns: A list of entries for the trip, all tagged with the given tag. \"\"\" p_day_generate = 0.3 new_entries = [] for date in date_iter(date_begin, date_end): for payee, account_expense, (mu, sigma3) in config: if random.random() < p_day_generate: amount = random.normalvariate(mu, sigma3 / 3.) amount_neg = -amount new_entries.extend(parse(\"\"\" {date} * \"{payee}\" \"\" #{tag} {account_credit} {amount_neg:.2f} CCY {account_expense} {amount:.2f} CCY \"\"\", **locals())) # Consume the vacation days. vacation_hrs = (date_end - date_begin).days * 8 # hrs/day new_entries.extend(parse(\"\"\" {date_end} * \"Consume vacation days\" Assets:CC:Employer1:Vacation -{vacation_hrs:.2f} VACHR Expenses:Vacation {vacation_hrs:.2f} VACHR \"\"\", **locals())) # Generate events for the trip. new_entries.extend(parse(\"\"\" {date_begin} event \"location\" \"{trip_city}\" {date_end} event \"location\" \"{home_city}\" \"\"\", **locals())) return new_entries","title":"generate_trip_entries()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.get_minimum_balance","text":"Compute the minimum balance of the given account according to the entries history. Parameters: entries \u2013 A list of directives. account \u2013 An account string. currency \u2013 A currency string, for which we want to compute the minimum. Returns: A Decimal number, the minimum amount throughout the history of this account. Source code in beancount/scripts/example.py def get_minimum_balance(entries, account, currency): \"\"\"Compute the minimum balance of the given account according to the entries history. Args: entries: A list of directives. account: An account string. currency: A currency string, for which we want to compute the minimum. Returns: A Decimal number, the minimum amount throughout the history of this account. \"\"\" min_amount = ZERO for _, balances in postings_for(entries, [account]): balance = balances[account] current = balance.get_currency_units(currency).number if current < min_amount: min_amount = current return min_amount","title":"get_minimum_balance()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.iter_dates_with_balance","text":"Iterate over dates, including the balances of the postings iterator. Parameters: postings_iter \u2013 An iterator of postings as per postings_for(). date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Pairs of (data, balances) objects, with date: A datetime.date instance balances: An Inventory object, representing the current balance. You can modify the inventory object to feed back changes in the balance. Source code in beancount/scripts/example.py def iter_dates_with_balance(date_begin, date_end, entries, accounts): \"\"\"Iterate over dates, including the balances of the postings iterator. Args: postings_iter: An iterator of postings as per postings_for(). date_begin: The start date. date_end: The end date. Yields: Pairs of (data, balances) objects, with date: A datetime.date instance balances: An Inventory object, representing the current balance. You can modify the inventory object to feed back changes in the balance. \"\"\" balances = collections.defaultdict(inventory.Inventory) merged_txn_postings = iter(merge_postings(entries, accounts)) txn_posting = next(merged_txn_postings, None) for date in date_iter(date_begin, date_end): while txn_posting and txn_posting.txn.date == date: posting = txn_posting.posting balances[posting.account].add_position(posting) txn_posting = next(merged_txn_postings, None) yield date, balances","title":"iter_dates_with_balance()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.iter_quarters","text":"Iterate over all quarters between begin and end dates. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Instances of datetime.date at the beginning of the quarters. This will include the quarter of the beginning date and of the end date. Source code in beancount/scripts/example.py def iter_quarters(date_begin, date_end): \"\"\"Iterate over all quarters between begin and end dates. Args: date_begin: The start date. date_end: The end date. Yields: Instances of datetime.date at the beginning of the quarters. This will include the quarter of the beginning date and of the end date. \"\"\" quarter = (date_begin.year, (date_begin.month-1)//3) quarter_last = (date_end.year, (date_end.month-1)//3) assert quarter <= quarter_last while True: year, trimester = quarter yield datetime.date(year, trimester*3 + 1, 1) if quarter == quarter_last: break trimester = (trimester + 1) % 4 if trimester == 0: year += 1 quarter = (year, trimester)","title":"iter_quarters()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.merge_postings","text":"Merge all the postings from the given account names. Parameters: entries \u2013 A list of directives. accounts \u2013 A list of account strings to get the balances for. Yields: A list of TxnPosting's for all the accounts, in sorted order. Source code in beancount/scripts/example.py def merge_postings(entries, accounts): \"\"\"Merge all the postings from the given account names. Args: entries: A list of directives. accounts: A list of account strings to get the balances for. Yields: A list of TxnPosting's for all the accounts, in sorted order. \"\"\" real_root = realization.realize(entries) merged_postings = [] for account in accounts: real_account = realization.get(real_root, account) if real_account is None: continue merged_postings.extend(txn_posting for txn_posting in real_account.txn_postings if isinstance(txn_posting, data.TxnPosting)) merged_postings.sort(key=lambda txn_posting: txn_posting.txn.date) return merged_postings","title":"merge_postings()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.parse","text":"Parse some input string and assert no errors. This parse function does not just create the object, it also triggers local interpolation to fill in the missing amounts. Parameters: input_string \u2013 Beancount input text. **replacements \u2013 A dict of keywords to replace to their values. Returns: A list of directive objects. Source code in beancount/scripts/example.py def parse(input_string, **replacements): \"\"\"Parse some input string and assert no errors. This parse function does not just create the object, it also triggers local interpolation to fill in the missing amounts. Args: input_string: Beancount input text. **replacements: A dict of keywords to replace to their values. Returns: A list of directive objects. \"\"\" if replacements: class IgnoreFormatter(string.Formatter): def check_unused_args(self, used_args, args, kwargs): pass formatter = IgnoreFormatter() formatted_string = formatter.format(input_string, **replacements) else: formatted_string = input_string entries, errors, options_map = parser.parse_string(textwrap.dedent(formatted_string)) if errors: printer.print_errors(errors, file=sys.stderr) raise ValueError(\"Parsed text has errors\") # Interpolation. entries, unused_balance_errors = booking.book(entries, options_map) return data.sorted(entries)","title":"parse()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.postings_for","text":"Realize the entries and get the list of postings for the given accounts. All the non-Posting directives are already filtered out. Parameters: entries \u2013 A list of directives. accounts \u2013 A list of account strings to get the balances for. before \u2013 A boolean, if true, yield the balance before the position is applied. The default is to yield the balance after applying the position. Yields: Tuples of: posting: An instance of TxnPosting balances: A dict of Inventory balances for the given accounts after applying the posting. These inventory objects can be mutated to adjust the balance due to generated transactions to be applied later. Source code in beancount/scripts/example.py def postings_for(entries, accounts, before=False): \"\"\"Realize the entries and get the list of postings for the given accounts. All the non-Posting directives are already filtered out. Args: entries: A list of directives. accounts: A list of account strings to get the balances for. before: A boolean, if true, yield the balance before the position is applied. The default is to yield the balance after applying the position. Yields: Tuples of: posting: An instance of TxnPosting balances: A dict of Inventory balances for the given accounts _after_ applying the posting. These inventory objects can be mutated to adjust the balance due to generated transactions to be applied later. \"\"\" assert isinstance(accounts, list) merged_txn_postings = merge_postings(entries, accounts) balances = collections.defaultdict(inventory.Inventory) for txn_posting in merged_txn_postings: if before: yield txn_posting, balances posting = txn_posting.posting balances[posting.account].add_position(posting) if not before: yield txn_posting, balances","title":"postings_for()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.price_series","text":"Generate a price series based on a simple stochastic model. Parameters: start \u2013 The beginning value. mu \u2013 The per-step drift, in units of value. sigma \u2013 Volatility of the changes. Yields: Floats, at each step. Source code in beancount/scripts/example.py def price_series(start, mu, sigma): \"\"\"Generate a price series based on a simple stochastic model. Args: start: The beginning value. mu: The per-step drift, in units of value. sigma: Volatility of the changes. Yields: Floats, at each step. \"\"\" value = start while 1: yield value value += random.normalvariate(mu, sigma) * value","title":"price_series()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.replace","text":"Apply word-boundaried regular expression replacements to an indented string. Parameters: string \u2013 Some input template string. replacements \u2013 A dict of regexp to replacement value. strip \u2013 A boolean, true if we should strip the input. Returns: The input string with the replacements applied to it, with the indentation removed. Source code in beancount/scripts/example.py def replace(string, replacements, strip=False): \"\"\"Apply word-boundaried regular expression replacements to an indented string. Args: string: Some input template string. replacements: A dict of regexp to replacement value. strip: A boolean, true if we should strip the input. Returns: The input string with the replacements applied to it, with the indentation removed. \"\"\" output = textwrap.dedent(string) if strip: output = output.strip() for from_, to_ in replacements.items(): if not isinstance(to_, str) and not callable(to_): to_ = str(to_) output = re.sub(r'\\b{}\\b'.format(from_), to_, output) return output","title":"replace()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.validate_output","text":"Check that the output file validates. Parameters: contents \u2013 A string, the output file. positive_accounts \u2013 A list of strings, account names to check for non-negative balances. currency \u2013 A string, the currency to check minimums for. Exceptions: AssertionError \u2013 If the output does not validate. Source code in beancount/scripts/example.py def validate_output(contents, positive_accounts, currency): \"\"\"Check that the output file validates. Args: contents: A string, the output file. positive_accounts: A list of strings, account names to check for non-negative balances. currency: A string, the currency to check minimums for. Raises: AssertionError: If the output does not validate. \"\"\" loaded_entries, _, _ = loader.load_string( contents, log_errors=sys.stderr, extra_validations=validation.HARDCORE_VALIDATIONS) # Sanity checks: Check that the checking balance never goes below zero. for account in positive_accounts: check_non_negative(loaded_entries, account, currency)","title":"validate_output()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.write_example_file","text":"Generate the example file. Parameters: date_birth \u2013 A datetime.date instance, the birth date of our character. date_begin \u2013 A datetime.date instance, the beginning date at which to generate transactions. date_end \u2013 A datetime.date instance, the end date at which to generate transactions. reformat \u2013 A boolean, true if we should apply global reformatting to this file. file \u2013 A file object, where to write out the output. Source code in beancount/scripts/example.py def write_example_file(date_birth, date_begin, date_end, reformat, file): \"\"\"Generate the example file. Args: date_birth: A datetime.date instance, the birth date of our character. date_begin: A datetime.date instance, the beginning date at which to generate transactions. date_end: A datetime.date instance, the end date at which to generate transactions. reformat: A boolean, true if we should apply global reformatting to this file. file: A file object, where to write out the output. \"\"\" # The following code entirely writes out the output to generic names, such # as \"Employer1\", \"Bank1\", and \"CCY\" (for principal currency). Those names # are purposely chosen to be unique, and only near the very end do we make # renamings to more specific and realistic names. # Name of the checking account. account_opening = 'Equity:Opening-Balances' account_payable = 'Liabilities:AccountsPayable' account_checking = 'Assets:CC:Bank1:Checking' account_credit = 'Liabilities:CC:CreditCard1' account_retirement = 'Assets:CC:Retirement' account_investing = 'Assets:CC:Investment:Cash' # Commodities. commodity_entries = generate_commodity_entries(date_birth) # Estimate the rent. rent_amount = round_to(ANNUAL_SALARY / RENT_DIVISOR, RENT_INCREMENT) # Get a random employer. employer_name, employer_address = random.choice(EMPLOYERS) logging.info(\"Generating Salary Employment Income\") income_entries = generate_employment_income(employer_name, employer_address, ANNUAL_SALARY, account_checking, join(account_retirement, 'Cash'), date_begin, date_end) logging.info(\"Generating Expenses from Banking Accounts\") banking_expenses = generate_banking_expenses(date_begin, date_end, account_checking, rent_amount) logging.info(\"Generating Regular Expenses via Credit Card\") credit_regular_entries = generate_regular_credit_expenses( date_birth, date_begin, date_end, account_credit, account_checking) logging.info(\"Generating Credit Card Expenses for Trips\") trip_entries = [] destinations = sorted(TRIP_DESTINATIONS.items()) destinations.extend(destinations) random.shuffle(destinations) for (date_trip_begin, date_trip_end), (destination_name, config) in zip( compute_trip_dates(date_begin, date_end), destinations): # Compute a suitable tag. tag = 'trip-{}-{}'.format(destination_name.lower().replace(' ', '-'), date_trip_begin.year) #logging.info(\"%s -- %s %s\", tag, date_trip_begin, date_trip_end) # Remove regular entries during this trip. credit_regular_entries = [entry for entry in credit_regular_entries if not(date_trip_begin <= entry.date < date_trip_end)] # Generate entries for the trip. this_trip_entries = generate_trip_entries( date_trip_begin, date_trip_end, tag, config, destination_name.replace('-', ' ').title(), HOME_NAME, account_credit) trip_entries.extend(this_trip_entries) logging.info(\"Generating Credit Card Payment Entries\") credit_payments = generate_clearing_entries( delay_dates(rrule.rrule(rrule.MONTHLY, dtstart=date_begin, until=date_end, bymonthday=7), 0, 4), \"CreditCard1\", \"Paying off credit card\", credit_regular_entries, account_credit, account_checking) credit_entries = credit_regular_entries + trip_entries + credit_payments logging.info(\"Generating Tax Filings and Payments\") tax_preamble = generate_tax_preamble(date_birth) # Figure out all the years we need tax accounts for. years = set() for account_name in getters.get_accounts(income_entries): match = re.match(r'Expenses:Taxes:Y(\\d\\d\\d\\d)', account_name) if match: years.add(int(match.group(1))) taxes = [(year, generate_tax_accounts(year, date_end)) for year in sorted(years)] tax_entries = tax_preamble + functools.reduce(operator.add, (entries for _, entries in taxes)) logging.info(\"Generating Opening of Banking Accounts\") # Open banking accounts and gift the checking account with a balance that # will offset all the amounts to ensure a positive balance throughout its # lifetime. entries_for_banking = data.sorted(income_entries + banking_expenses + credit_entries + tax_entries) minimum = get_minimum_balance(entries_for_banking, account_checking, 'CCY') banking_entries = generate_banking(entries_for_banking, date_begin, date_end, max(-minimum, ZERO)) logging.info(\"Generating Transfers to Investment Account\") banking_transfers = generate_outgoing_transfers( data.sorted(income_entries + banking_entries + banking_expenses + credit_entries + tax_entries), account_checking, account_investing, transfer_minimum=D('200'), transfer_threshold=D('3000'), transfer_increment=D('500')) logging.info(\"Generating Prices\") # Generate price entries for investment currencies and create a price map to # use for later for generating investment transactions. funds_allocation = {'MFUND1': 0.40, 'MFUND2': 0.60} stocks = ['STK1', 'STK2', 'STK3', 'STK4'] price_entries = generate_prices(date_begin, date_end, sorted(funds_allocation.keys()) + stocks, 'CCY') price_map = prices.build_price_map(price_entries) logging.info(\"Generating Employer Match Contribution\") account_match = 'Income:US:Employer1:Match401k' retirement_match = generate_retirement_employer_match(income_entries, join(account_retirement, 'Cash'), account_match) logging.info(\"Generating Retirement Investments\") retirement_entries = generate_retirement_investments( income_entries + retirement_match, account_retirement, sorted(funds_allocation.items()), price_map) logging.info(\"Generating Taxes Investments\") investment_entries = generate_taxable_investment(date_begin, date_end, banking_transfers, price_map, stocks) logging.info(\"Generating Expense Accounts\") expense_accounts_entries = generate_expense_accounts(date_birth) logging.info(\"Generating Equity Accounts\") equity_entries = generate_open_entries(date_birth, [account_opening, account_payable]) logging.info(\"Generating Balance Checks\") credit_checks = generate_balance_checks(credit_entries, account_credit, date_random_seq(date_begin, date_end, 20, 30)) banking_checks = generate_balance_checks(data.sorted(income_entries + banking_entries + banking_expenses + banking_transfers + credit_entries + tax_entries), account_checking, date_random_seq(date_begin, date_end, 20, 30)) logging.info(\"Outputting and Formatting Entries\") dcontext = display_context.DisplayContext() default_int_digits = 8 for currency, precision in {'USD': 2, 'CAD': 2, 'VACHR':0, 'IRAUSD': 2, 'VBMPX': 3, 'RGAGX': 3, 'ITOT': 0, 'VEA': 0, 'VHT': 0, 'GLD': 0}.items(): int_digits = default_int_digits if precision > 0: int_digits += 1 + precision dcontext.update(D('{{:0{}.{}f}}'.format(int_digits, precision).format(0)), currency) output = io.StringIO() def output_section(title, entries): output.write('\\n\\n\\n{}\\n\\n'.format(title)) printer.print_entries(data.sorted(entries), dcontext, file=output) output.write(FILE_PREAMBLE.format(**locals())) output_section('* Commodities', commodity_entries) output_section('* Equity Accounts', equity_entries) output_section('* Banking', data.sorted(banking_entries + banking_expenses + banking_transfers + banking_checks)) output_section('* Credit-Cards', data.sorted(credit_entries + credit_checks)) output_section('* Taxable Investments', investment_entries) output_section('* Retirement Investments', data.sorted(retirement_entries + retirement_match)) output_section('* Sources of Income', income_entries) output_section('* Taxes', tax_preamble) for year, entries in taxes: output_section('** Tax Year {}'.format(year), entries) output_section('* Expenses', expense_accounts_entries) output_section('* Prices', price_entries) output_section('* Cash', []) logging.info(\"Contextualizing to Realistic Names\") contents, replacements = contextualize_file(output.getvalue(), employer_name) if reformat: contents = format.align_beancount(contents) logging.info(\"Writing contents\") file.write(contents) logging.info(\"Validating Results\") validate_output(contents, [replace(account, replacements) for account in [account_checking]], replace('CCY', replacements))","title":"write_example_file()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.format","text":"Align a beancount/ledger input file's numbers. This reformats at beancount or ledger input file so that the amounts in the postings are all aligned to the same column. The currency should match. Note: this does not parse the Beancount ledger. It simply uses regular expressions and text manipulations to do its work.","title":"format"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.format.align_beancount","text":"Reformat Beancount input to align all the numbers at the same column. Parameters: contents \u2013 A string, Beancount input syntax to reformat. prefix_width \u2013 An integer, the width in characters to render the account name to. If this is not specified, a good value is selected automatically from the contents of the file. num_width \u2013 An integer, the width to render each number. If this is not specified, a good value is selected automatically from the contents of the file. currency_column \u2013 An integer, the column at which to align the currencies. If given, this overrides the other options. Returns: A string, reformatted Beancount input with all the number aligned. No other changes than whitespace changes should be present between that return value and the input contents. Source code in beancount/scripts/format.py def align_beancount(contents, prefix_width=None, num_width=None, currency_column=None): \"\"\"Reformat Beancount input to align all the numbers at the same column. Args: contents: A string, Beancount input syntax to reformat. prefix_width: An integer, the width in characters to render the account name to. If this is not specified, a good value is selected automatically from the contents of the file. num_width: An integer, the width to render each number. If this is not specified, a good value is selected automatically from the contents of the file. currency_column: An integer, the column at which to align the currencies. If given, this overrides the other options. Returns: A string, reformatted Beancount input with all the number aligned. No other changes than whitespace changes should be present between that return value and the input contents. \"\"\" # Find all lines that have a number in them and calculate the maximum length # of the stripped prefix and the number. match_pairs = [] for line in contents.splitlines(): match = re.match( r'([^\";]*?)\\s+([-+]?\\s*[\\d,]+(?:\\.\\d*)?)\\s+({}\\b.*)'.format(amount.CURRENCY_RE), line) if match: prefix, number, rest = match.groups() match_pairs.append((prefix, number, rest)) else: match_pairs.append((line, None, None)) # Normalize whitespace before lines that has some indent and an account # name. norm_match_pairs = normalize_indent_whitespace(match_pairs) if currency_column: output = io.StringIO() for prefix, number, rest in norm_match_pairs: if number is None: output.write(prefix) else: num_of_spaces = currency_column - len(prefix) - len(number) - 4 spaces = ' ' * num_of_spaces output.write(prefix + spaces + ' ' + number + ' ' + rest) output.write('\\n') return output.getvalue() # Compute the maximum widths. filtered_pairs = [(prefix, number) for prefix, number, _ in match_pairs if number is not None] if filtered_pairs: max_prefix_width = max(len(prefix) for prefix, _ in filtered_pairs) max_num_width = max(len(number) for _, number in filtered_pairs) else: max_prefix_width = 0 max_num_width = 0 # Use user-supplied overrides, if available if prefix_width: max_prefix_width = prefix_width if num_width: max_num_width = num_width # Create a format that will admit the maximum width of all prefixes equally. line_format = '{{:<{prefix_width}}} {{:>{num_width}}} {{}}'.format( prefix_width=max_prefix_width, num_width=max_num_width) # Process each line to an output buffer. output = io.StringIO() for prefix, number, rest in norm_match_pairs: if number is None: output.write(prefix) else: output.write(line_format.format(prefix.rstrip(), number, rest)) output.write('\\n') formatted_contents = output.getvalue() # Ensure that the file before and after have only whitespace differences. # This is a sanity check, to make really sure we never change anything but whitespace, # so it's safe. # open('/tmp/before', 'w').write(re.sub(r'[ \\t]+', ' ', contents)) # open('/tmp/after', 'w').write(re.sub(r'[ \\t]+', ' ', formatted_contents)) old_stripped = re.sub(r'[ \\t\\n]+', ' ', contents.rstrip()) new_stripped = re.sub(r'[ \\t\\n]+', ' ', formatted_contents.rstrip()) assert (old_stripped == new_stripped), (old_stripped, new_stripped) return formatted_contents","title":"align_beancount()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.format.compute_most_frequent","text":"Compute the frequencies of the given elements and return the most frequent. Parameters: iterable \u2013 A collection of hashable elements. Returns: The most frequent element. If there are no elements in the iterable, return None. Source code in beancount/scripts/format.py def compute_most_frequent(iterable): \"\"\"Compute the frequencies of the given elements and return the most frequent. Args: iterable: A collection of hashable elements. Returns: The most frequent element. If there are no elements in the iterable, return None. \"\"\" frequencies = collections.Counter(iterable) if not frequencies: return None counts = sorted((count, element) for element, count in frequencies.items()) # Note: In case of a tie, this chooses the longest width. # We could eventually make this an option. return counts[-1][1]","title":"compute_most_frequent()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.format.normalize_indent_whitespace","text":"Normalize whitespace before lines that has some indent and an account name. Parameters: match_pairs \u2013 A list of (prefix, number, rest) tuples. Returns: Another list of (prefix, number, rest) tuples, where prefix may have been adjusted with a different whitespace prefix. Source code in beancount/scripts/format.py def normalize_indent_whitespace(match_pairs): \"\"\"Normalize whitespace before lines that has some indent and an account name. Args: match_pairs: A list of (prefix, number, rest) tuples. Returns: Another list of (prefix, number, rest) tuples, where prefix may have been adjusted with a different whitespace prefix. \"\"\" # Compute most frequent account name prefix. match_posting = re.compile(r'([ \\t]+)({}.*)'.format(account.ACCOUNT_RE)).match width = compute_most_frequent( len(match.group(1)) for match in (match_posting(prefix) for prefix, _, _ in match_pairs) if match is not None) norm_format = ' ' * (width or 0) + '{}' # Make the necessary adjustments. adjusted_pairs = [] for tup in match_pairs: prefix, number, rest = tup match = match_posting(prefix) if match is not None: tup = (norm_format.format(match.group(2)), number, rest) adjusted_pairs.append(tup) return adjusted_pairs","title":"normalize_indent_whitespace()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql","text":"Convert a Beancount ledger into an SQL database.","title":"sql"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.BalanceWriter","text":"","title":"BalanceWriter"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.BalanceWriter.type","text":"Balance(meta, date, account, amount, tolerance, diff_amount)","title":"type"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.BalanceWriter.type.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.BalanceWriter.type.__new__","text":"Create new instance of Balance(meta, date, account, amount, tolerance, diff_amount)","title":"__new__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.BalanceWriter.type.__repr__","text":"Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.BalanceWriter.get_detail","text":"Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.account, entry.amount.number, entry.amount.currency, entry.diff_amount.currency if entry.diff_amount else None, entry.diff_amount.currency if entry.diff_amount else None)","title":"get_detail()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.CloseWriter","text":"","title":"CloseWriter"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.CloseWriter.type","text":"Close(meta, date, account)","title":"type"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.CloseWriter.type.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.CloseWriter.type.__new__","text":"Create new instance of Close(meta, date, account)","title":"__new__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.CloseWriter.type.__repr__","text":"Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.CloseWriter.get_detail","text":"Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.account,)","title":"get_detail()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.DirectiveWriter","text":"A base class for writers of directives. This is used to factor out code for all the simple directives types (all types except Transaction).","title":"DirectiveWriter"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.DirectiveWriter.__call__","text":"Create a table for a directives. Parameters: connection \u2013 A DBAPI-2.0 Connection object. entries \u2013 A list of directives. Source code in beancount/scripts/sql.py def __call__(self, connection, entries): \"\"\"Create a table for a directives. Args: connection: A DBAPI-2.0 Connection object. entries: A list of directives. \"\"\" with connection: columns_text = ','.join(self.columns.strip().splitlines()) connection.execute(\"\"\" CREATE TABLE {name}_detail ( id INTEGER PRIMARY KEY, {columns} ); \"\"\".format(name=self.name, columns=columns_text)) connection.execute(\"\"\" CREATE VIEW {name} AS SELECT * FROM entry JOIN {name}_detail USING (id); \"\"\".format(name=self.name)) with connection: for eid, entry in enumerate(entries): if not isinstance(entry, self.type): continue # Store common data. connection.execute(\"\"\" INSERT INTO entry VALUES (?, ?, ?, ?, ?); \"\"\", (eid, entry.date, self.name, entry.meta[\"filename\"], entry.meta[\"lineno\"])) # Store detail data. detail_data = self.get_detail(entry) row_data = (eid,) + detail_data query = \"\"\" INSERT INTO {name}_detail VALUES ({placeholder}); \"\"\".format(name=self.name, placeholder=','.join(['?'] * (1 + len(detail_data)))) connection.execute(query, row_data)","title":"__call__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.DirectiveWriter.get_detail","text":"Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): \"\"\"Provide data to store for details table. Args: entry: An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. \"\"\" raise NotImplementedError","title":"get_detail()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.DocumentWriter","text":"","title":"DocumentWriter"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.DocumentWriter.type","text":"Document(meta, date, account, filename, tags, links)","title":"type"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.DocumentWriter.type.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.DocumentWriter.type.__new__","text":"Create new instance of Document(meta, date, account, filename, tags, links)","title":"__new__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.DocumentWriter.type.__repr__","text":"Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.DocumentWriter.get_detail","text":"Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.account, entry.filename)","title":"get_detail()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.EventWriter","text":"","title":"EventWriter"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.EventWriter.type","text":"Event(meta, date, type, description)","title":"type"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.EventWriter.type.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.EventWriter.type.__new__","text":"Create new instance of Event(meta, date, type, description)","title":"__new__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.EventWriter.type.__repr__","text":"Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.EventWriter.get_detail","text":"Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.type, entry.description)","title":"get_detail()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.NoteWriter","text":"","title":"NoteWriter"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.NoteWriter.type","text":"Note(meta, date, account, comment)","title":"type"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.NoteWriter.type.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.NoteWriter.type.__new__","text":"Create new instance of Note(meta, date, account, comment)","title":"__new__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.NoteWriter.type.__repr__","text":"Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.NoteWriter.get_detail","text":"Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.account, entry.comment)","title":"get_detail()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.OpenWriter","text":"","title":"OpenWriter"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.OpenWriter.type","text":"Open(meta, date, account, currencies, booking)","title":"type"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.OpenWriter.type.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.OpenWriter.type.__new__","text":"Create new instance of Open(meta, date, account, currencies, booking)","title":"__new__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.OpenWriter.type.__repr__","text":"Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.OpenWriter.get_detail","text":"Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.account, ','.join(entry.currencies or []))","title":"get_detail()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.PadWriter","text":"","title":"PadWriter"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.PadWriter.type","text":"Pad(meta, date, account, source_account)","title":"type"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.PadWriter.type.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.PadWriter.type.__new__","text":"Create new instance of Pad(meta, date, account, source_account)","title":"__new__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.PadWriter.type.__repr__","text":"Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.PadWriter.get_detail","text":"Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.account, entry.source_account)","title":"get_detail()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.PriceWriter","text":"","title":"PriceWriter"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.PriceWriter.type","text":"Price(meta, date, currency, amount)","title":"type"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.PriceWriter.type.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.PriceWriter.type.__new__","text":"Create new instance of Price(meta, date, currency, amount)","title":"__new__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.PriceWriter.type.__repr__","text":"Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.PriceWriter.get_detail","text":"Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.currency, entry.amount.number, entry.amount.currency)","title":"get_detail()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.QueryWriter","text":"","title":"QueryWriter"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.QueryWriter.type","text":"Query(meta, date, name, query_string)","title":"type"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.QueryWriter.type.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/sql.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.QueryWriter.type.__new__","text":"Create new instance of Query(meta, date, name, query_string)","title":"__new__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.QueryWriter.type.__repr__","text":"Return a nicely formatted representation string Source code in beancount/scripts/sql.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.QueryWriter.get_detail","text":"Provide data to store for details table. Parameters: entry \u2013 An instance of the desired directive. Returns: A tuple of the values corresponding to the columns declared in the 'columns' attribute. Source code in beancount/scripts/sql.py def get_detail(self, entry): return (entry.name, entry.query_string)","title":"get_detail()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.adapt_decimal","text":"Adapt a Decimal instance to a string for creating queries. Parameters: number \u2013 An instance of Decimal. Returns: A string. Source code in beancount/scripts/sql.py def adapt_decimal(number): \"\"\"Adapt a Decimal instance to a string for creating queries. Args: number: An instance of Decimal. Returns: A string. \"\"\" return str(number)","title":"adapt_decimal()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.convert_decimal","text":"Convert a Decimal string to a Decimal instance. Parameters: string \u2013 A decimal number in a string. Returns: An instance of Decimal. Source code in beancount/scripts/sql.py def convert_decimal(string): \"\"\"Convert a Decimal string to a Decimal instance. Args: string: A decimal number in a string. Returns: An instance of Decimal. \"\"\" return Decimal(string)","title":"convert_decimal()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.output_common","text":"Create a table of common data for all entries. Parameters: connection \u2013 A DBAPI-2.0 Connection object. entries \u2013 A list of directives. Source code in beancount/scripts/sql.py def output_common(connection, unused_entries): \"\"\"Create a table of common data for all entries. Args: connection: A DBAPI-2.0 Connection object. entries: A list of directives. \"\"\" with connection: connection.execute(\"\"\" CREATE TABLE entry ( id INTEGER PRIMARY KEY, date DATE, type CHARACTER(8), source_filename STRING, source_lineno INTEGER ); \"\"\")","title":"output_common()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.output_transactions","text":"Create a table for transactions and fill in the data. Parameters: connection \u2013 A DBAPI-2.0 Connection object. entries \u2013 A list of directives. Source code in beancount/scripts/sql.py def output_transactions(connection, entries): \"\"\"Create a table for transactions and fill in the data. Args: connection: A DBAPI-2.0 Connection object. entries: A list of directives. \"\"\" with connection: connection.execute(\"\"\" CREATE TABLE transactions_detail ( id INTEGER PRIMARY KEY, flag CHARACTER(1), payee VARCHAR, narration VARCHAR, tags VARCHAR, -- Comma-separated links VARCHAR -- Comma-separated ); \"\"\") connection.execute(\"\"\" CREATE VIEW transactions AS SELECT * FROM entry JOIN transactions_detail USING (id); \"\"\") connection.execute(\"\"\" CREATE TABLE postings ( posting_id INTEGER PRIMARY KEY, id INTEGER, flag CHARACTER(1), account VARCHAR, number DECIMAL(16, 6), currency CHARACTER(10), cost_number DECIMAL(16, 6), cost_currency CHARACTER(10), cost_date DATE, cost_label VARCHAR, price_number DECIMAL(16, 6), price_currency CHARACTER(10), FOREIGN KEY(id) REFERENCES entries(id) ); \"\"\") postings_count = iter(itertools.count()) with connection: for eid, entry in enumerate(entries): if not isinstance(entry, data.Transaction): continue connection.execute(\"\"\" insert into entry values (?, ?, ?, ?, ?); \"\"\", (eid, entry.date, 'txn', entry.meta[\"filename\"], entry.meta[\"lineno\"])) connection.execute(\"\"\" insert into transactions_detail values (?, ?, ?, ?, ?, ?); \"\"\", (eid, entry.flag, entry.payee, entry.narration, ','.join(entry.tags or ()), ','.join(entry.links or ()))) for posting in entry.postings: pid = next(postings_count) units = posting.units cost = posting.cost price = posting.price connection.execute(\"\"\" INSERT INTO postings VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); \"\"\", (pid, eid, posting.flag, posting.account, units.number, units.currency, cost.number if cost else None, cost.currency if cost else None, cost.date if cost else None, cost.label if cost else None, price.number if price else None, price.currency if price else None))","title":"output_transactions()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.sql.setup_decimal_support","text":"Setup sqlite3 to support conversions to/from Decimal numbers. Source code in beancount/scripts/sql.py def setup_decimal_support(): \"\"\"Setup sqlite3 to support conversions to/from Decimal numbers. \"\"\" dbapi.register_adapter(Decimal, adapt_decimal) dbapi.register_converter(\"decimal\", convert_decimal)","title":"setup_decimal_support()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.tutorial","text":"Write output files for the tutorial commands.","title":"tutorial"},{"location":"api_reference/beancount.tools.html","text":"beancount.tools \uf0c1 Standalone tools that aren't linked to Beancount but that are useful with it. The beancount.scripts package contains the implementation of scripts which invoke the Beancount library code. This beancount.tools package implements other tools which aren't directly invoking Beancount library code and which could be theoretically copied and used independently. However, these are to be distributed with Beancount and in order to maintain all the source code together they are put in this package and invokes from stubs under beancount/bin/, just like the other scripts. beancount.tools.treeify \uf0c1 Identify a column of text that contains hierarchical id and treeify that column. This script will inspect a text file and attempt to find a vertically left-aligned column of text that contains identifiers with multiple components, such as \"Assets:US:Bank:Checking\", and replace those by a tree-like structure rendered in ASCII, inserting new empty lines where necessary to create the tree. Note: If your paths have spaces in them, this will not work. Space is used as a delimiter to detect the end of a column. You can customize the delimiter with an option. beancount.tools.treeify.Node ( list ) \uf0c1 A node with a name attribute, a list of line numbers and a list of children (from its parent class). beancount.tools.treeify.Node.__repr__(self) special \uf0c1 Return str(self). Source code in beancount/tools/treeify.py def __str__(self): return ''.format(self.name, [node.name for node in self]) beancount.tools.treeify.create_tree(column_matches, regexp_split) \uf0c1 Build up a tree from a list of matches. Parameters: column_matches \u2013 A list of (line-number, name) pairs. regexp_split \u2013 A regular expression string, to use for splitting the names of components. Returns: An instance of Node, the root node of the created tree. Source code in beancount/tools/treeify.py def create_tree(column_matches, regexp_split): \"\"\"Build up a tree from a list of matches. Args: column_matches: A list of (line-number, name) pairs. regexp_split: A regular expression string, to use for splitting the names of components. Returns: An instance of Node, the root node of the created tree. \"\"\" root = Node('') for no, name in column_matches: parts = re.split(regexp_split, name) node = root for part in parts: last_node = node[-1] if node else None if last_node is None or last_node.name != part: last_node = Node(part) node.append(last_node) node = last_node node.nos.append(no) return root beancount.tools.treeify.dump_tree(node, file=<_io.StringIO object at 0x78e865153580>, prefix='') \uf0c1 Render a tree as a tree. Parameters: node \u2013 An instance of Node. file \u2013 A file object to write to. prefix \u2013 A prefix string for each of the lines of the children. Source code in beancount/tools/treeify.py def dump_tree(node, file=sys.stdout, prefix=''): \"\"\"Render a tree as a tree. Args: node: An instance of Node. file: A file object to write to. prefix: A prefix string for each of the lines of the children. \"\"\" file.write(prefix) file.write(node.name) file.write('\\n') for child in node: dump_tree(child, file, prefix + '... ') beancount.tools.treeify.enum_tree_by_input_line_num(tree_lines) \uf0c1 Accumulate the lines of a tree until a line number is found. Parameters: tree_lines \u2013 A list of lines as returned by render_tree. Yields: Pairs of (line number, list of (line, node)). Source code in beancount/tools/treeify.py def enum_tree_by_input_line_num(tree_lines): \"\"\"Accumulate the lines of a tree until a line number is found. Args: tree_lines: A list of lines as returned by render_tree. Yields: Pairs of (line number, list of (line, node)). \"\"\" pending = [] for first_line, cont_line, node in tree_lines: if not node.nos: pending.append((first_line, node)) else: line = first_line for no in node.nos: pending.append((line, node)) line = cont_line yield (no, pending) pending = [] if pending: yield (None, pending) beancount.tools.treeify.find_column(lines, pattern, delimiter) \uf0c1 Find a valid column with hierarchical data in the text lines. Parameters: lines \u2013 A list of strings, the contents of the input. pattern \u2013 A regular expression for the hierarchical entries. delimiter \u2013 A regular expression that dictates how we detect the end of a column. Normally this is a single space. If the patterns contain spaces, you will need to increase this. Returns: A tuple of matches \u2013 A list of (line-number, name) tuples where 'name' is the hierarchical string to treeify and line-number is an integer, the line number where this applies. left: An integer, the leftmost column. right: An integer, the rightmost column. Note that not all line numbers may be present, so you may need to skip some. However, they are in guaranteed in sorted order. Source code in beancount/tools/treeify.py def find_column(lines, pattern, delimiter): \"\"\"Find a valid column with hierarchical data in the text lines. Args: lines: A list of strings, the contents of the input. pattern: A regular expression for the hierarchical entries. delimiter: A regular expression that dictates how we detect the end of a column. Normally this is a single space. If the patterns contain spaces, you will need to increase this. Returns: A tuple of matches: A list of (line-number, name) tuples where 'name' is the hierarchical string to treeify and line-number is an integer, the line number where this applies. left: An integer, the leftmost column. right: An integer, the rightmost column. Note that not all line numbers may be present, so you may need to skip some. However, they are in guaranteed in sorted order. \"\"\" # A mapping of the line beginning position to its match object. beginnings = collections.defaultdict(list) pattern_and_whitespace = \"({})(?P{}.|$)\".format(pattern, delimiter) for no, line in enumerate(lines): for match in re.finditer(pattern_and_whitespace, line): beginnings[match.start()].append((no, line, match)) # For each potential column found, verify that it is valid. A valid column # will have the maximum of its content text not overlap with any of the # following text. We assume that a column will have been formatted to full # width and that no text following the line overlap with the column, even in # its trailing whitespace. # # In other words, the following example is a violation because \"10,990.74\" # overlaps with the end of \"Insurance\" and so this would not be recognized # as a valid column: # # Expenses:Food:Restaurant 10,990.74 USD # Expenses:Health:Dental:Insurance 208.80 USD # for leftmost_column, column_matches in sorted(beginnings.items()): # Compute the location of the rightmost column of text. rightmost_column = max(match.end(1) for _, _, match in column_matches) # Compute the leftmost location of the content following the column text # and past its whitespace. following_column = min(match.end() if match.group('ws') else 10000 for _, _, match in column_matches) if rightmost_column < following_column: # We process only the very first match. return_matches = [(no, match.group(1).rstrip()) for no, _, match in column_matches] return return_matches, leftmost_column, rightmost_column beancount.tools.treeify.render_tree(root) \uf0c1 Render a tree of nodes. Returns: A list of tuples of (first_line, continuation_line, node) where first_line \u2013 A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. node: The Node instance which corresponds to this line. and an integer, the width of the new columns. Source code in beancount/tools/treeify.py def render_tree(root): \"\"\"Render a tree of nodes. Returns: A list of tuples of (first_line, continuation_line, node) where first_line: A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. node: The Node instance which corresponds to this line. and an integer, the width of the new columns. \"\"\" # Compute all the lines ahead of time in order to calculate the width. lines = [] # Start with the root node. We push the constant prefix before this node, # the account name, and the RealAccount instance. We will maintain a stack # of children nodes to render. stack = [('', root.name, root, True)] while stack: prefix, name, node, is_last = stack.pop(-1) if node is root: # For the root node, we don't want to render any prefix. first = cont = '' else: # Compute the string that precedes the name directly and the one below # that for the continuation lines. # | # @@@ Bank1 <---------------- # @@@ | # | |-- Checking if is_last: first = prefix + PREFIX_LEAF_1 cont = prefix + PREFIX_LEAF_C else: first = prefix + PREFIX_CHILD_1 cont = prefix + PREFIX_CHILD_C # Compute the name to render for continuation lines. # | # |-- Bank1 # | @@@ <---------------- # | |-- Checking if len(node) > 0: cont_name = PREFIX_CHILD_C else: cont_name = PREFIX_LEAF_C # Add a line for this account. if not (node is root and not name): lines.append((first + name, cont + cont_name, node)) # Push the children onto the stack, being careful with ordering and # marking the last node as such. if node: child_items = reversed(node) child_iter = iter(child_items) child_node = next(child_iter) stack.append((cont, child_node.name, child_node, True)) for child_node in child_iter: stack.append((cont, child_node.name, child_node, False)) if not lines: return lines # Compute the maximum width of the lines and convert all of them to the same # maximal width. This makes it easy on the client. max_width = max(len(first_line) for first_line, _, __ in lines) line_format = '{{:{width}}}'.format(width=max_width) return [(line_format.format(first_line), line_format.format(cont_line), node) for (first_line, cont_line, node) in lines], max_width beancount.tools.treeify_test \uf0c1 Unit tests for treeify tool. beancount.tools.treeify_test.TestTreeifyBase ( TestCase ) \uf0c1 beancount.tools.treeify_test.TestTreeifyBase.treeify(self, string, expect_errors=False, options=None) \uf0c1 Run treeify on the given string and assert no errors. Parameters: string \u2013 A string, the input contents to run on. expect_errors \u2013 A boolean, true if we should expect there to be errors. options \u2013 An optional list of options for the subprogram. Returns: The converted output string. This fails the test if there were any errors. Source code in beancount/tools/treeify_test.py def treeify(self, string, expect_errors=False, options=None): \"\"\"Run treeify on the given string and assert no errors. Args: string: A string, the input contents to run on. expect_errors: A boolean, true if we should expect there to be errors. options: An optional list of options for the subprogram. Returns: The converted output string. This fails the test if there were any errors. \"\"\" returncode, output, errors = treeify(string, options) actual_errors = returncode != 0 or bool(errors) if actual_errors != expect_errors: if expect_errors: self.fail(\"Missing expected errors\") else: self.fail(\"Unexpected errors: {}\".format(errors)) return output beancount.tools.treeify_test.TestTreeifyBase.treeify_equal(self, string, expected, expect_errors=False, options=None) \uf0c1 Assert an expected treeification result. Parameters: string \u2013 An expected input contents string to treeify. expected \u2013 A string, the expected treeified output. expect_errors \u2013 A boolean, true if we should expect there to be errors. options \u2013 An optional list of options for the subprogram. Returns: The actual output string. This fails the test if there the expected output differed from the actual. Source code in beancount/tools/treeify_test.py def treeify_equal(self, string, expected, expect_errors=False, options=None): \"\"\"Assert an expected treeification result. Args: string: An expected input contents string to treeify. expected: A string, the expected treeified output. expect_errors: A boolean, true if we should expect there to be errors. options: An optional list of options for the subprogram. Returns: The actual output string. This fails the test if there the expected output differed from the actual. \"\"\" input_ = textwrap.dedent(string) output = self.treeify(input_, expect_errors, options) expected = textwrap.dedent(expected) if DEBUG: print('-(input)----------------------------------') print(input_) print('-(output)---------------------------------') print(output) print('-(expected)-------------------------------') print(expected) print('------------------------------------------') self.assertEqual(expected, output) return output beancount.tools.treeify_test.treeify(string, options=None) \uf0c1 Run treeify on the string. Parameters: string \u2013 The input string to feed treeify. options \u2013 An optional list of options for the subprogram. Returns: The treeified string. Source code in beancount/tools/treeify_test.py def treeify(string, options=None): \"\"\"Run treeify on the string. Args: string: The input string to feed treeify. options: An optional list of options for the subprogram. Returns: The treeified string. \"\"\" pipe = subprocess.Popen([PROGRAM] + (options or []), shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, errors = pipe.communicate(string.encode('utf-8')) return (pipe.returncode, output.decode('utf-8') if output else '', errors.decode('utf-8') if errors else '')","title":"beancount.tools"},{"location":"api_reference/beancount.tools.html#beancounttools","text":"Standalone tools that aren't linked to Beancount but that are useful with it. The beancount.scripts package contains the implementation of scripts which invoke the Beancount library code. This beancount.tools package implements other tools which aren't directly invoking Beancount library code and which could be theoretically copied and used independently. However, these are to be distributed with Beancount and in order to maintain all the source code together they are put in this package and invokes from stubs under beancount/bin/, just like the other scripts.","title":"beancount.tools"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify","text":"Identify a column of text that contains hierarchical id and treeify that column. This script will inspect a text file and attempt to find a vertically left-aligned column of text that contains identifiers with multiple components, such as \"Assets:US:Bank:Checking\", and replace those by a tree-like structure rendered in ASCII, inserting new empty lines where necessary to create the tree. Note: If your paths have spaces in them, this will not work. Space is used as a delimiter to detect the end of a column. You can customize the delimiter with an option.","title":"treeify"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.Node","text":"A node with a name attribute, a list of line numbers and a list of children (from its parent class).","title":"Node"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.Node.__repr__","text":"Return str(self). Source code in beancount/tools/treeify.py def __str__(self): return ''.format(self.name, [node.name for node in self])","title":"__repr__()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.create_tree","text":"Build up a tree from a list of matches. Parameters: column_matches \u2013 A list of (line-number, name) pairs. regexp_split \u2013 A regular expression string, to use for splitting the names of components. Returns: An instance of Node, the root node of the created tree. Source code in beancount/tools/treeify.py def create_tree(column_matches, regexp_split): \"\"\"Build up a tree from a list of matches. Args: column_matches: A list of (line-number, name) pairs. regexp_split: A regular expression string, to use for splitting the names of components. Returns: An instance of Node, the root node of the created tree. \"\"\" root = Node('') for no, name in column_matches: parts = re.split(regexp_split, name) node = root for part in parts: last_node = node[-1] if node else None if last_node is None or last_node.name != part: last_node = Node(part) node.append(last_node) node = last_node node.nos.append(no) return root","title":"create_tree()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.dump_tree","text":"Render a tree as a tree. Parameters: node \u2013 An instance of Node. file \u2013 A file object to write to. prefix \u2013 A prefix string for each of the lines of the children. Source code in beancount/tools/treeify.py def dump_tree(node, file=sys.stdout, prefix=''): \"\"\"Render a tree as a tree. Args: node: An instance of Node. file: A file object to write to. prefix: A prefix string for each of the lines of the children. \"\"\" file.write(prefix) file.write(node.name) file.write('\\n') for child in node: dump_tree(child, file, prefix + '... ')","title":"dump_tree()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.enum_tree_by_input_line_num","text":"Accumulate the lines of a tree until a line number is found. Parameters: tree_lines \u2013 A list of lines as returned by render_tree. Yields: Pairs of (line number, list of (line, node)). Source code in beancount/tools/treeify.py def enum_tree_by_input_line_num(tree_lines): \"\"\"Accumulate the lines of a tree until a line number is found. Args: tree_lines: A list of lines as returned by render_tree. Yields: Pairs of (line number, list of (line, node)). \"\"\" pending = [] for first_line, cont_line, node in tree_lines: if not node.nos: pending.append((first_line, node)) else: line = first_line for no in node.nos: pending.append((line, node)) line = cont_line yield (no, pending) pending = [] if pending: yield (None, pending)","title":"enum_tree_by_input_line_num()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.find_column","text":"Find a valid column with hierarchical data in the text lines. Parameters: lines \u2013 A list of strings, the contents of the input. pattern \u2013 A regular expression for the hierarchical entries. delimiter \u2013 A regular expression that dictates how we detect the end of a column. Normally this is a single space. If the patterns contain spaces, you will need to increase this. Returns: A tuple of matches \u2013 A list of (line-number, name) tuples where 'name' is the hierarchical string to treeify and line-number is an integer, the line number where this applies. left: An integer, the leftmost column. right: An integer, the rightmost column. Note that not all line numbers may be present, so you may need to skip some. However, they are in guaranteed in sorted order. Source code in beancount/tools/treeify.py def find_column(lines, pattern, delimiter): \"\"\"Find a valid column with hierarchical data in the text lines. Args: lines: A list of strings, the contents of the input. pattern: A regular expression for the hierarchical entries. delimiter: A regular expression that dictates how we detect the end of a column. Normally this is a single space. If the patterns contain spaces, you will need to increase this. Returns: A tuple of matches: A list of (line-number, name) tuples where 'name' is the hierarchical string to treeify and line-number is an integer, the line number where this applies. left: An integer, the leftmost column. right: An integer, the rightmost column. Note that not all line numbers may be present, so you may need to skip some. However, they are in guaranteed in sorted order. \"\"\" # A mapping of the line beginning position to its match object. beginnings = collections.defaultdict(list) pattern_and_whitespace = \"({})(?P{}.|$)\".format(pattern, delimiter) for no, line in enumerate(lines): for match in re.finditer(pattern_and_whitespace, line): beginnings[match.start()].append((no, line, match)) # For each potential column found, verify that it is valid. A valid column # will have the maximum of its content text not overlap with any of the # following text. We assume that a column will have been formatted to full # width and that no text following the line overlap with the column, even in # its trailing whitespace. # # In other words, the following example is a violation because \"10,990.74\" # overlaps with the end of \"Insurance\" and so this would not be recognized # as a valid column: # # Expenses:Food:Restaurant 10,990.74 USD # Expenses:Health:Dental:Insurance 208.80 USD # for leftmost_column, column_matches in sorted(beginnings.items()): # Compute the location of the rightmost column of text. rightmost_column = max(match.end(1) for _, _, match in column_matches) # Compute the leftmost location of the content following the column text # and past its whitespace. following_column = min(match.end() if match.group('ws') else 10000 for _, _, match in column_matches) if rightmost_column < following_column: # We process only the very first match. return_matches = [(no, match.group(1).rstrip()) for no, _, match in column_matches] return return_matches, leftmost_column, rightmost_column","title":"find_column()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.render_tree","text":"Render a tree of nodes. Returns: A list of tuples of (first_line, continuation_line, node) where first_line \u2013 A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. node: The Node instance which corresponds to this line. and an integer, the width of the new columns. Source code in beancount/tools/treeify.py def render_tree(root): \"\"\"Render a tree of nodes. Returns: A list of tuples of (first_line, continuation_line, node) where first_line: A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. node: The Node instance which corresponds to this line. and an integer, the width of the new columns. \"\"\" # Compute all the lines ahead of time in order to calculate the width. lines = [] # Start with the root node. We push the constant prefix before this node, # the account name, and the RealAccount instance. We will maintain a stack # of children nodes to render. stack = [('', root.name, root, True)] while stack: prefix, name, node, is_last = stack.pop(-1) if node is root: # For the root node, we don't want to render any prefix. first = cont = '' else: # Compute the string that precedes the name directly and the one below # that for the continuation lines. # | # @@@ Bank1 <---------------- # @@@ | # | |-- Checking if is_last: first = prefix + PREFIX_LEAF_1 cont = prefix + PREFIX_LEAF_C else: first = prefix + PREFIX_CHILD_1 cont = prefix + PREFIX_CHILD_C # Compute the name to render for continuation lines. # | # |-- Bank1 # | @@@ <---------------- # | |-- Checking if len(node) > 0: cont_name = PREFIX_CHILD_C else: cont_name = PREFIX_LEAF_C # Add a line for this account. if not (node is root and not name): lines.append((first + name, cont + cont_name, node)) # Push the children onto the stack, being careful with ordering and # marking the last node as such. if node: child_items = reversed(node) child_iter = iter(child_items) child_node = next(child_iter) stack.append((cont, child_node.name, child_node, True)) for child_node in child_iter: stack.append((cont, child_node.name, child_node, False)) if not lines: return lines # Compute the maximum width of the lines and convert all of them to the same # maximal width. This makes it easy on the client. max_width = max(len(first_line) for first_line, _, __ in lines) line_format = '{{:{width}}}'.format(width=max_width) return [(line_format.format(first_line), line_format.format(cont_line), node) for (first_line, cont_line, node) in lines], max_width","title":"render_tree()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify_test","text":"Unit tests for treeify tool.","title":"treeify_test"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify_test.TestTreeifyBase","text":"","title":"TestTreeifyBase"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify_test.TestTreeifyBase.treeify","text":"Run treeify on the given string and assert no errors. Parameters: string \u2013 A string, the input contents to run on. expect_errors \u2013 A boolean, true if we should expect there to be errors. options \u2013 An optional list of options for the subprogram. Returns: The converted output string. This fails the test if there were any errors. Source code in beancount/tools/treeify_test.py def treeify(self, string, expect_errors=False, options=None): \"\"\"Run treeify on the given string and assert no errors. Args: string: A string, the input contents to run on. expect_errors: A boolean, true if we should expect there to be errors. options: An optional list of options for the subprogram. Returns: The converted output string. This fails the test if there were any errors. \"\"\" returncode, output, errors = treeify(string, options) actual_errors = returncode != 0 or bool(errors) if actual_errors != expect_errors: if expect_errors: self.fail(\"Missing expected errors\") else: self.fail(\"Unexpected errors: {}\".format(errors)) return output","title":"treeify()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify_test.TestTreeifyBase.treeify_equal","text":"Assert an expected treeification result. Parameters: string \u2013 An expected input contents string to treeify. expected \u2013 A string, the expected treeified output. expect_errors \u2013 A boolean, true if we should expect there to be errors. options \u2013 An optional list of options for the subprogram. Returns: The actual output string. This fails the test if there the expected output differed from the actual. Source code in beancount/tools/treeify_test.py def treeify_equal(self, string, expected, expect_errors=False, options=None): \"\"\"Assert an expected treeification result. Args: string: An expected input contents string to treeify. expected: A string, the expected treeified output. expect_errors: A boolean, true if we should expect there to be errors. options: An optional list of options for the subprogram. Returns: The actual output string. This fails the test if there the expected output differed from the actual. \"\"\" input_ = textwrap.dedent(string) output = self.treeify(input_, expect_errors, options) expected = textwrap.dedent(expected) if DEBUG: print('-(input)----------------------------------') print(input_) print('-(output)---------------------------------') print(output) print('-(expected)-------------------------------') print(expected) print('------------------------------------------') self.assertEqual(expected, output) return output","title":"treeify_equal()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify_test.treeify","text":"Run treeify on the string. Parameters: string \u2013 The input string to feed treeify. options \u2013 An optional list of options for the subprogram. Returns: The treeified string. Source code in beancount/tools/treeify_test.py def treeify(string, options=None): \"\"\"Run treeify on the string. Args: string: The input string to feed treeify. options: An optional list of options for the subprogram. Returns: The treeified string. \"\"\" pipe = subprocess.Popen([PROGRAM] + (options or []), shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, errors = pipe.communicate(string.encode('utf-8')) return (pipe.returncode, output.decode('utf-8') if output else '', errors.decode('utf-8') if errors else '')","title":"treeify()"},{"location":"api_reference/beancount.utils.html","text":"beancount.utils \uf0c1 Generic utility packages and functions. beancount.utils.bisect_key \uf0c1 A version of bisect that accepts a custom key function, like the sorting ones do. beancount.utils.bisect_key.bisect_left_with_key(sequence, value, key=None) \uf0c1 Find the last element before the given value in a sorted list. Parameters: sequence \u2013 A sorted sequence of elements. value \u2013 The value to search for. key \u2013 An optional function used to extract the value from the elements of sequence. Returns: Return the index. May return None. Source code in beancount/utils/bisect_key.py def bisect_left_with_key(sequence, value, key=None): \"\"\"Find the last element before the given value in a sorted list. Args: sequence: A sorted sequence of elements. value: The value to search for. key: An optional function used to extract the value from the elements of sequence. Returns: Return the index. May return None. \"\"\" # pylint: disable=invalid-name if key is None: key = lambda x: x # Identity. lo = 0 hi = len(sequence) while lo < hi: mid = (lo + hi) // 2 if key(sequence[mid]) < value: lo = mid + 1 else: hi = mid return lo beancount.utils.bisect_key.bisect_right_with_key(a, x, key, lo=0, hi=None) \uf0c1 Like bisect.bisect_right, but with a key lookup parameter. Parameters: a \u2013 The list to search in. x \u2013 The element to search for. key \u2013 A function, to extract the value from the list. lo \u2013 The smallest index to search. hi \u2013 The largest index to search. Returns: As in bisect.bisect_right, an element from list 'a'. Source code in beancount/utils/bisect_key.py def bisect_right_with_key(a, x, key, lo=0, hi=None): \"\"\"Like bisect.bisect_right, but with a key lookup parameter. Args: a: The list to search in. x: The element to search for. key: A function, to extract the value from the list. lo: The smallest index to search. hi: The largest index to search. Returns: As in bisect.bisect_right, an element from list 'a'. \"\"\" # pylint: disable=invalid-name if lo < 0: raise ValueError('lo must be non-negative') if hi is None: hi = len(a) while lo < hi: mid = (lo+hi)//2 if x < key(a[mid]): hi = mid else: lo = mid+1 return lo beancount.utils.csv_utils \uf0c1 Utilities for reading and writing CSV files. beancount.utils.csv_utils.as_rows(string) \uf0c1 Split a string as rows of a CSV file. Parameters: string \u2013 A string to be split, the contents of a CSV file. Returns: Lists of lists of strings. Source code in beancount/utils/csv_utils.py def as_rows(string): \"\"\"Split a string as rows of a CSV file. Args: string: A string to be split, the contents of a CSV file. Returns: Lists of lists of strings. \"\"\" return list(csv.reader(io.StringIO(textwrap.dedent(string)))) beancount.utils.csv_utils.csv_clean_header(header_row) \uf0c1 Create a new class for the following rows from the header line. This both normalizes the header line and assign Parameters: header_row \u2013 A list of strings, the row with header titles. Returns: A list of strings, with possibly modified (cleaned) row titles, of the same lengths. Source code in beancount/utils/csv_utils.py def csv_clean_header(header_row): \"\"\"Create a new class for the following rows from the header line. This both normalizes the header line and assign Args: header_row: A list of strings, the row with header titles. Returns: A list of strings, with possibly modified (cleaned) row titles, of the same lengths. \"\"\" fieldnames = [] for index, column in enumerate(header_row): field = column.lower() field = re.sub(r'\\bp/l\\b', 'pnl', field) field = re.sub('[^a-z0-9]', '_', field) field = field.strip(' _') field = re.sub('__+', '_', field) if not field: field = 'col{}'.format(index) assert field not in fieldnames, field fieldnames.append(field) return fieldnames beancount.utils.csv_utils.csv_dict_reader(fileobj, **kw) \uf0c1 Read a CSV file yielding normalized dictionary fields. This is basically an alternative constructor for csv.DictReader that normalized the field names. Parameters: fileobj \u2013 A file object to be read. **kw \u2013 Optional arguments forwarded to csv.DictReader. Returns: A csv.DictReader object. Source code in beancount/utils/csv_utils.py def csv_dict_reader(fileobj, **kw): \"\"\"Read a CSV file yielding normalized dictionary fields. This is basically an alternative constructor for csv.DictReader that normalized the field names. Args: fileobj: A file object to be read. **kw: Optional arguments forwarded to csv.DictReader. Returns: A csv.DictReader object. \"\"\" reader = csv.DictReader(fileobj, **kw) reader.fieldnames = csv_clean_header(reader.fieldnames) return reader beancount.utils.csv_utils.csv_split_sections(rows) \uf0c1 Given rows, split them in at empty lines. This is useful for structured CSV files with multiple sections. Parameters: rows \u2013 A list of rows, which are themselves lists of strings. Returns: A list of sections, which are lists of rows, which are lists of strings. Source code in beancount/utils/csv_utils.py def csv_split_sections(rows): \"\"\"Given rows, split them in at empty lines. This is useful for structured CSV files with multiple sections. Args: rows: A list of rows, which are themselves lists of strings. Returns: A list of sections, which are lists of rows, which are lists of strings. \"\"\" sections = [] current_section = [] for row in rows: if row: current_section.append(row) else: sections.append(current_section) current_section = [] if current_section: sections.append(current_section) return sections beancount.utils.csv_utils.csv_split_sections_with_titles(rows) \uf0c1 Given a list of rows, split their sections. If the sections have single column titles, consume those lines as their names and return a mapping of section names. This is useful for CSV files with multiple sections, where the separator is a title. We use this to separate the multiple tables within the CSV files. Parameters: rows \u2013 A list of rows (list-of-strings). Returns: A list of lists of rows (list-of-strings). Source code in beancount/utils/csv_utils.py def csv_split_sections_with_titles(rows): \"\"\"Given a list of rows, split their sections. If the sections have single column titles, consume those lines as their names and return a mapping of section names. This is useful for CSV files with multiple sections, where the separator is a title. We use this to separate the multiple tables within the CSV files. Args: rows: A list of rows (list-of-strings). Returns: A list of lists of rows (list-of-strings). \"\"\" sections_map = {} for index, section in enumerate(csv_split_sections(rows)): # Skip too short sections, cannot possibly be a title. if len(section) < 2: continue if len(section[0]) == 1 and len(section[1]) != 1: name = section[0][0] section = section[1:] else: name = 'Section {}'.format(index) sections_map[name] = section return sections_map beancount.utils.csv_utils.csv_tuple_reader(fileobj, **kw) \uf0c1 Read a CSV file yielding namedtuple instances. The CSV file must have a header line. Parameters: fileobj \u2013 A file object to be read. **kw \u2013 Optional arguments forwarded to csv.DictReader. Yields: Nametuple instances, one for each row. Source code in beancount/utils/csv_utils.py def csv_tuple_reader(fileobj, **kw): \"\"\"Read a CSV file yielding namedtuple instances. The CSV file must have a header line. Args: fileobj: A file object to be read. **kw: Optional arguments forwarded to csv.DictReader. Yields: Nametuple instances, one for each row. \"\"\" reader = csv.reader(fileobj, **kw) ireader = iter(reader) fieldnames = csv_clean_header(next(ireader)) Tuple = collections.namedtuple('Row', fieldnames) for row in ireader: try: yield Tuple(*row) except TypeError: # If there's an error, it's usually from a line that has a 'END OF # LINE' marker at the end, or some comment line. assert len(row) in (0, 1) beancount.utils.csv_utils.iter_sections(fileobj, separating_predicate=None) \uf0c1 For a given file object, yield file-like objects for each of the sections contained therein. A section is defined as a list of lines that don't match the predicate. For example, if you want to split by empty lines, provide a predicate that will be true given an empty line, which will cause a new section to be begun. Parameters: fileobj \u2013 A file object to read from. separating_predicate \u2013 A boolean predicate that is true on separating lines. Yields: A list of lines that you can use to iterate. Source code in beancount/utils/csv_utils.py def iter_sections(fileobj, separating_predicate=None): \"\"\"For a given file object, yield file-like objects for each of the sections contained therein. A section is defined as a list of lines that don't match the predicate. For example, if you want to split by empty lines, provide a predicate that will be true given an empty line, which will cause a new section to be begun. Args: fileobj: A file object to read from. separating_predicate: A boolean predicate that is true on separating lines. Yields: A list of lines that you can use to iterate. \"\"\" if separating_predicate is None: separating_predicate = lambda line: bool(line.strip()) lineiter = iter(fileobj) for line in lineiter: if separating_predicate(line): iterator = itertools.chain((line,), iter_until_empty(lineiter, separating_predicate)) yield iterator for _ in iterator: pass beancount.utils.csv_utils.iter_until_empty(iterator, separating_predicate=None) \uf0c1 An iterator of lines that will stop at the first empty line. Parameters: iterator \u2013 An iterator of lines. separating_predicate \u2013 A boolean predicate that is true on separating lines. Yields: Non-empty lines. EOF when we hit an empty line. Source code in beancount/utils/csv_utils.py def iter_until_empty(iterator, separating_predicate=None): \"\"\"An iterator of lines that will stop at the first empty line. Args: iterator: An iterator of lines. separating_predicate: A boolean predicate that is true on separating lines. Yields: Non-empty lines. EOF when we hit an empty line. \"\"\" if separating_predicate is None: separating_predicate = lambda line: bool(line.strip()) for line in iterator: if not separating_predicate(line): break yield line beancount.utils.date_utils \uf0c1 Parse the date from various formats. beancount.utils.date_utils.intimezone(tz_value) \uf0c1 Temporarily reset the value of TZ. This is used for testing. Parameters: tz_value ( str ) \u2013 The value of TZ to set for the duration of this context. Returns: A contextmanager in the given timezone locale. Source code in beancount/utils/date_utils.py @contextlib.contextmanager def intimezone(tz_value: str): \"\"\"Temporarily reset the value of TZ. This is used for testing. Args: tz_value: The value of TZ to set for the duration of this context. Returns: A contextmanager in the given timezone locale. \"\"\" tz_old = os.environ.get('TZ', None) os.environ['TZ'] = tz_value time.tzset() try: yield finally: if tz_old is None: del os.environ['TZ'] else: os.environ['TZ'] = tz_old time.tzset() beancount.utils.date_utils.iter_dates(start_date, end_date) \uf0c1 Yield all the dates between 'start_date' and 'end_date'. Parameters: start_date \u2013 An instance of datetime.date. end_date \u2013 An instance of datetime.date. Yields: Instances of datetime.date. Source code in beancount/utils/date_utils.py def iter_dates(start_date, end_date): \"\"\"Yield all the dates between 'start_date' and 'end_date'. Args: start_date: An instance of datetime.date. end_date: An instance of datetime.date. Yields: Instances of datetime.date. \"\"\" oneday = datetime.timedelta(days=1) date = start_date while date < end_date: yield date date += oneday beancount.utils.date_utils.next_month(date) \uf0c1 Compute the date at the beginning of the following month from the given date. Parameters: date \u2013 A datetime.date instance. Returns: A datetime.date instance, the first day of the month following 'date'. Source code in beancount/utils/date_utils.py def next_month(date): \"\"\"Compute the date at the beginning of the following month from the given date. Args: date: A datetime.date instance. Returns: A datetime.date instance, the first day of the month following 'date'. \"\"\" # Compute the date at the beginning of the following month. year = date.year month = date.month + 1 if date.month == 12: year += 1 month = 1 return datetime.date(year, month, 1) beancount.utils.date_utils.parse_date_liberally(string, parse_kwargs_dict=None) \uf0c1 Parse arbitrary strings to dates. This function is intended to support liberal inputs, so that we can use it in accepting user-specified dates on command-line scripts. Parameters: string \u2013 A string to parse. parse_kwargs_dict \u2013 Dict of kwargs to pass to dateutil parser. Returns: A datetime.date object. Source code in beancount/utils/date_utils.py def parse_date_liberally(string, parse_kwargs_dict=None): \"\"\"Parse arbitrary strings to dates. This function is intended to support liberal inputs, so that we can use it in accepting user-specified dates on command-line scripts. Args: string: A string to parse. parse_kwargs_dict: Dict of kwargs to pass to dateutil parser. Returns: A datetime.date object. \"\"\" # At the moment, rely on the most excellent dateutil. if parse_kwargs_dict is None: parse_kwargs_dict = {} return dateutil.parser.parse(string, **parse_kwargs_dict).date() beancount.utils.date_utils.render_ofx_date(dtime) \uf0c1 Render a datetime to the OFX format. Parameters: dtime \u2013 A datetime.datetime instance. Returns: A string, rendered to milliseconds. Source code in beancount/utils/date_utils.py def render_ofx_date(dtime): \"\"\"Render a datetime to the OFX format. Args: dtime: A datetime.datetime instance. Returns: A string, rendered to milliseconds. \"\"\" return '{}.{:03d}'.format(dtime.strftime('%Y%m%d%H%M%S'), int(dtime.microsecond / 1000)) beancount.utils.defdict \uf0c1 An instance of collections.defaultdict whose factory accepts a key. Note: This really ought to be an enhancement to Python itself. I should bother adding this in eventually. beancount.utils.defdict.DefaultDictWithKey ( defaultdict ) \uf0c1 A version of defaultdict whose factory accepts the key as an argument. Note: collections.defaultdict would be improved by supporting this directly, this is a common occurrence. beancount.utils.defdict.ImmutableDictWithDefault ( dict ) \uf0c1 An immutable dict which returns a default value for missing keys. This differs from a defaultdict in that it does not insert a missing default value when one is materialized (from a missing fetch), and furthermore, the set method is make unavailable to prevent mutation beyond construction. beancount.utils.defdict.ImmutableDictWithDefault.__setitem__(self, key, value) special \uf0c1 Disallow mutating the dict in the usual way. Source code in beancount/utils/defdict.py def __setitem__(self, key, value): \"\"\"Disallow mutating the dict in the usual way.\"\"\" raise NotImplementedError beancount.utils.defdict.ImmutableDictWithDefault.get(self, key, _=None) \uf0c1 Return the value for key if key is in the dictionary, else default. Source code in beancount/utils/defdict.py def get(self, key, _=None): return self.__getitem__(key) beancount.utils.encryption \uf0c1 Support for encrypted tests. beancount.utils.encryption.is_encrypted_file(filename) \uf0c1 Return true if the given filename contains an encrypted file. Parameters: filename \u2013 A path string. Returns: A boolean, true if the file contains an encrypted file. Source code in beancount/utils/encryption.py def is_encrypted_file(filename): \"\"\"Return true if the given filename contains an encrypted file. Args: filename: A path string. Returns: A boolean, true if the file contains an encrypted file. \"\"\" _, ext = path.splitext(filename) if ext == '.gpg': return True if ext == '.asc': with open(filename) as encfile: head = encfile.read(1024) if re.search('--BEGIN PGP MESSAGE--', head): return True return False beancount.utils.encryption.is_gpg_installed() \uf0c1 Return true if GPG 1.4.x or 2.x are installed, which is what we use and support. Source code in beancount/utils/encryption.py def is_gpg_installed(): \"\"\"Return true if GPG 1.4.x or 2.x are installed, which is what we use and support.\"\"\" try: pipe = subprocess.Popen(['gpg', '--version'], shell=0, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = pipe.communicate() version_text = out.decode('utf8') return pipe.returncode == 0 and re.match(r'gpg \\(GnuPG\\) (1\\.4|2)\\.', version_text) except OSError: return False beancount.utils.encryption.read_encrypted_file(filename) \uf0c1 Decrypt and read an encrypted file without temporary storage. Parameters: filename \u2013 A string, the path to the encrypted file. Returns: A string, the contents of the file. Exceptions: OSError \u2013 If we could not properly decrypt the file. Source code in beancount/utils/encryption.py def read_encrypted_file(filename): \"\"\"Decrypt and read an encrypted file without temporary storage. Args: filename: A string, the path to the encrypted file. Returns: A string, the contents of the file. Raises: OSError: If we could not properly decrypt the file. \"\"\" command = ['gpg', '--batch', '--decrypt', path.realpath(filename)] pipe = subprocess.Popen(command, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) contents, errors = pipe.communicate() if pipe.returncode != 0: raise OSError(\"Could not decrypt file ({}): {}\".format(pipe.returncode, errors.decode('utf8'))) return contents.decode('utf-8') beancount.utils.file_type \uf0c1 Code that can guess a MIME type for a filename. This attempts to identify the mime-type of a file suing The built-in mimetypes library, then python-magic (if available), and finally some custom mappers for datatypes used in financial downloads, such as Quicken files. beancount.utils.file_type.guess_file_type(filename) \uf0c1 Attempt to guess the type of the input file. Parameters: filename \u2013 A string, the name of the file to guess the type for. Returns: A suitable mimetype string, or None if we could not guess. Source code in beancount/utils/file_type.py def guess_file_type(filename): \"\"\"Attempt to guess the type of the input file. Args: filename: A string, the name of the file to guess the type for. Returns: A suitable mimetype string, or None if we could not guess. \"\"\" # Try the standard mimetypes association. filetype, _ = mimetypes.guess_type(filename, False) if filetype: return filetype # Try out some extra types that only we know about. for regexp, mimetype in EXTRA_FILE_TYPES: if regexp.match(filename): return mimetype # Try out libmagic, if it is installed. if magic: filetype = magic.from_file(filename, mime=True) if isinstance(filetype, bytes): filetype = filetype.decode('utf8') return filetype else: raise ValueError((\"Could not identify the type of file '{}'; \" \"try installing python-magic\").format(filename)) beancount.utils.file_utils \uf0c1 File utilities. beancount.utils.file_utils.chdir(directory) \uf0c1 Temporarily chdir to the given directory. Parameters: directory \u2013 The directory to switch do. Returns: A context manager which restores the cwd after running. Source code in beancount/utils/file_utils.py @contextlib.contextmanager def chdir(directory): \"\"\"Temporarily chdir to the given directory. Args: directory: The directory to switch do. Returns: A context manager which restores the cwd after running. \"\"\" cwd = os.getcwd() os.chdir(directory) try: yield cwd finally: os.chdir(cwd) beancount.utils.file_utils.find_files(fords, ignore_dirs=('.hg', '.svn', '.git'), ignore_files=('.DS_Store',)) \uf0c1 Enumerate the files under the given directories, stably. Invalid file or directory names will be logged to the error log. Parameters: fords \u2013 A list of strings, file or directory names. ignore_dirs \u2013 A list of strings, filenames or directories to be ignored. Yields: Strings, full filenames from the given roots. Source code in beancount/utils/file_utils.py def find_files(fords, ignore_dirs=('.hg', '.svn', '.git'), ignore_files=('.DS_Store',)): \"\"\"Enumerate the files under the given directories, stably. Invalid file or directory names will be logged to the error log. Args: fords: A list of strings, file or directory names. ignore_dirs: A list of strings, filenames or directories to be ignored. Yields: Strings, full filenames from the given roots. \"\"\" if isinstance(fords, str): fords = [fords] assert isinstance(fords, (list, tuple)) for ford in fords: if path.isdir(ford): for root, dirs, filenames in os.walk(ford): dirs[:] = sorted(dirname for dirname in dirs if dirname not in ignore_dirs) for filename in sorted(filenames): if filename in ignore_files: continue yield path.join(root, filename) elif path.isfile(ford) or path.islink(ford): yield ford elif not path.exists(ford): logging.error(\"File or directory '{}' does not exist.\".format(ford)) beancount.utils.file_utils.guess_file_format(filename, default=None) \uf0c1 Guess the file format from the filename. Parameters: filename \u2013 A string, the name of the file. This can be None. Returns: A string, the extension of the format, without a leading period. Source code in beancount/utils/file_utils.py def guess_file_format(filename, default=None): \"\"\"Guess the file format from the filename. Args: filename: A string, the name of the file. This can be None. Returns: A string, the extension of the format, without a leading period. \"\"\" if filename: if filename.endswith('.txt') or filename.endswith('.text'): format = 'text' elif filename.endswith('.csv'): format = 'csv' elif filename.endswith('.html') or filename.endswith('.xhtml'): format = 'html' else: format = default else: format = default return format beancount.utils.file_utils.path_greedy_split(filename) \uf0c1 Split a path, returning the longest possible extension. Parameters: filename \u2013 A string, the filename to split. Returns: A pair of basename, extension (which includes the leading period). Source code in beancount/utils/file_utils.py def path_greedy_split(filename): \"\"\"Split a path, returning the longest possible extension. Args: filename: A string, the filename to split. Returns: A pair of basename, extension (which includes the leading period). \"\"\" basename = path.basename(filename) index = basename.find('.') if index == -1: extension = None else: extension = basename[index:] basename = basename[:index] return (path.join(path.dirname(filename), basename), extension) beancount.utils.file_utils.touch_file(filename, *otherfiles) \uf0c1 Touch a file and wait until its timestamp has been changed. Parameters: filename \u2013 A string path, the name of the file to touch. otherfiles \u2013 A list of other files to ensure the timestamp is beyond of. Source code in beancount/utils/file_utils.py def touch_file(filename, *otherfiles): \"\"\"Touch a file and wait until its timestamp has been changed. Args: filename: A string path, the name of the file to touch. otherfiles: A list of other files to ensure the timestamp is beyond of. \"\"\" # Note: You could set os.stat_float_times() but then the main function would # have to set that up as well. It doesn't help so much, however, since # filesystems tend to have low resolutions, e.g. one second. orig_mtime_ns = max(os.stat(minfile).st_mtime_ns for minfile in (filename,) + otherfiles) delay_secs = 0.05 while True: with open(filename, 'a'): os.utime(filename) time.sleep(delay_secs) new_stat = os.stat(filename) if new_stat.st_mtime_ns > orig_mtime_ns: break beancount.utils.import_utils \uf0c1 Utilities for importing symbols programmatically. beancount.utils.import_utils.import_symbol(dotted_name) \uf0c1 Import a symbol in an arbitrary module. Parameters: dotted_name \u2013 A dotted path to a symbol. Returns: The object referenced by the given name. Exceptions: ImportError \u2013 If the module not not be imported. AttributeError \u2013 If the symbol could not be found in the module. Source code in beancount/utils/import_utils.py def import_symbol(dotted_name): \"\"\"Import a symbol in an arbitrary module. Args: dotted_name: A dotted path to a symbol. Returns: The object referenced by the given name. Raises: ImportError: If the module not not be imported. AttributeError: If the symbol could not be found in the module. \"\"\" comps = dotted_name.split('.') module_name = '.'.join(comps[:-1]) symbol_name = comps[-1] module = importlib.import_module(module_name) return getattr(module, symbol_name) beancount.utils.invariants \uf0c1 Functions to register auxiliary functions on a class' methods to check for invariants. This is intended to be used in a test, whereby your test will setup a class to automatically run invariant verification functions before and after each function call, to ensure some extra sanity checks that wouldn't be used in non-tests. Example: Instrument the Inventory class with the check_inventory_invariants() function. def setUp(module): instrument_invariants(Inventory, check_inventory_invariants, check_inventory_invariants) def tearDown(module): uninstrument_invariants(Inventory) beancount.utils.invariants.instrument_invariants(klass, prefun, postfun) \uf0c1 Instrument the class 'klass' with pre/post invariant checker functions. Parameters: klass \u2013 A class object, whose methods to be instrumented. prefun \u2013 A function that checks invariants pre-call. postfun \u2013 A function that checks invariants pre-call. Source code in beancount/utils/invariants.py def instrument_invariants(klass, prefun, postfun): \"\"\"Instrument the class 'klass' with pre/post invariant checker functions. Args: klass: A class object, whose methods to be instrumented. prefun: A function that checks invariants pre-call. postfun: A function that checks invariants pre-call. \"\"\" instrumented = {} for attrname, object_ in klass.__dict__.items(): if attrname.startswith('_'): continue if not isinstance(object_, types.FunctionType): continue instrumented[attrname] = object_ setattr(klass, attrname, invariant_check(object_, prefun, postfun)) klass.__instrumented = instrumented beancount.utils.invariants.invariant_check(method, prefun, postfun) \uf0c1 Decorate a method with the pre/post invariant checkers. Parameters: method \u2013 An unbound method to instrument. prefun \u2013 A function that checks invariants pre-call. postfun \u2013 A function that checks invariants post-call. Returns: An unbound method, decorated. Source code in beancount/utils/invariants.py def invariant_check(method, prefun, postfun): \"\"\"Decorate a method with the pre/post invariant checkers. Args: method: An unbound method to instrument. prefun: A function that checks invariants pre-call. postfun: A function that checks invariants post-call. Returns: An unbound method, decorated. \"\"\" reentrant = [] def new_method(self, *args, **kw): reentrant.append(None) if len(reentrant) == 1: prefun(self) result = method(self, *args, **kw) if len(reentrant) == 1: postfun(self) reentrant.pop() return result return new_method beancount.utils.invariants.uninstrument_invariants(klass) \uf0c1 Undo the instrumentation for invariants. Parameters: klass \u2013 A class object, whose methods to be uninstrumented. Source code in beancount/utils/invariants.py def uninstrument_invariants(klass): \"\"\"Undo the instrumentation for invariants. Args: klass: A class object, whose methods to be uninstrumented. \"\"\" instrumented = getattr(klass, '__instrumented', None) if instrumented: for attrname, object_ in instrumented.items(): setattr(klass, attrname, object_) del klass.__instrumented beancount.utils.memo \uf0c1 Memoization utilities. beancount.utils.memo.memoize_recent_fileobj(function, cache_filename, expiration=None) \uf0c1 Memoize recent calls to the given function which returns a file object. The results of the cache expire after some time. Parameters: function \u2013 A callable object. cache_filename \u2013 A string, the path to the database file to cache to. expiration \u2013 The time during which the results will be kept valid. Use 'None' to never expire the cache (this is the default). Returns: A memoized version of the function. Source code in beancount/utils/memo.py def memoize_recent_fileobj(function, cache_filename, expiration=None): \"\"\"Memoize recent calls to the given function which returns a file object. The results of the cache expire after some time. Args: function: A callable object. cache_filename: A string, the path to the database file to cache to. expiration: The time during which the results will be kept valid. Use 'None' to never expire the cache (this is the default). Returns: A memoized version of the function. \"\"\" urlcache = shelve.open(cache_filename, 'c') urlcache.lock = threading.Lock() # Note: 'shelve' is not thread-safe. @functools.wraps(function) def memoized(*args, **kw): # Encode the arguments, including a date string in order to invalidate # results over some time. md5 = hashlib.md5() md5.update(str(args).encode('utf-8')) md5.update(str(sorted(kw.items())).encode('utf-8')) hash_ = md5.hexdigest() time_now = now() try: with urlcache.lock: time_orig, contents = urlcache[hash_] if expiration is not None and (time_now - time_orig) > expiration: raise KeyError except KeyError: fileobj = function(*args, **kw) if fileobj: contents = fileobj.read() with urlcache.lock: urlcache[hash_] = (time_now, contents) else: contents = None return io.BytesIO(contents) if contents else None return memoized beancount.utils.memo.now() \uf0c1 Indirection on datetime.datetime.now() for testing. Source code in beancount/utils/memo.py def now(): \"Indirection on datetime.datetime.now() for testing.\" return datetime.datetime.now() beancount.utils.misc_utils \uf0c1 Generic utility packages and functions. beancount.utils.misc_utils.LineFileProxy \uf0c1 A file object that will delegate writing full lines to another logging function. This may be used for writing data to a logging level without having to worry about lines. beancount.utils.misc_utils.LineFileProxy.__init__(self, line_writer, prefix=None, write_newlines=False) special \uf0c1 Construct a new line delegator file object proxy. Parameters: line_writer \u2013 A callable function, used to write to the delegated output. prefix \u2013 An optional string, the prefix to insert before every line. write_newlines \u2013 A boolean, true if we should output the newline characters. Source code in beancount/utils/misc_utils.py def __init__(self, line_writer, prefix=None, write_newlines=False): \"\"\"Construct a new line delegator file object proxy. Args: line_writer: A callable function, used to write to the delegated output. prefix: An optional string, the prefix to insert before every line. write_newlines: A boolean, true if we should output the newline characters. \"\"\" self.line_writer = line_writer self.prefix = prefix self.write_newlines = write_newlines self.data = [] beancount.utils.misc_utils.LineFileProxy.close(self) \uf0c1 Close the line delegator. Source code in beancount/utils/misc_utils.py def close(self): \"\"\"Close the line delegator.\"\"\" self.flush() beancount.utils.misc_utils.LineFileProxy.flush(self) \uf0c1 Flush the data to the line writer. Source code in beancount/utils/misc_utils.py def flush(self): \"\"\"Flush the data to the line writer.\"\"\" data = ''.join(self.data) if data: lines = data.splitlines() self.data = [lines.pop(-1)] if data[-1] != '\\n' else [] for line in lines: if self.prefix: line = self.prefix + line if self.write_newlines: line += '\\n' self.line_writer(line) beancount.utils.misc_utils.LineFileProxy.write(self, data) \uf0c1 Write some string data to the output. Parameters: data \u2013 A string, with or without newlines. Source code in beancount/utils/misc_utils.py def write(self, data): \"\"\"Write some string data to the output. Args: data: A string, with or without newlines. \"\"\" if '\\n' in data: self.data.append(data) self.flush() else: self.data.append(data) beancount.utils.misc_utils.TypeComparable \uf0c1 A base class whose equality comparison includes comparing the type of the instance itself. beancount.utils.misc_utils.box(name=None, file=None) \uf0c1 A context manager that prints out a box around a block. This is useful for printing out stuff from tests in a way that is readable. Parameters: name \u2013 A string, the name of the box to use. file \u2013 The file object to print to. Yields: None. Source code in beancount/utils/misc_utils.py @contextlib.contextmanager def box(name=None, file=None): \"\"\"A context manager that prints out a box around a block. This is useful for printing out stuff from tests in a way that is readable. Args: name: A string, the name of the box to use. file: The file object to print to. Yields: None. \"\"\" file = file or sys.stdout file.write('\\n') if name: header = ',--------({})--------\\n'.format(name) footer = '`{}\\n'.format('-' * (len(header)-2)) else: header = ',----------------\\n' footer = '`----------------\\n' file.write(header) yield file.write(footer) file.flush() beancount.utils.misc_utils.cmptuple(name, attributes) \uf0c1 Manufacture a comparable namedtuple class, similar to collections.namedtuple. A comparable named tuple is a tuple which compares to False if contents are equal but the data types are different. We define this to supplement collections.namedtuple because by default a namedtuple disregards the type and we want to make precise comparisons for tests. Parameters: name \u2013 The given name of the class. attributes \u2013 A string or tuple of strings, with the names of the attributes. Returns: A new namedtuple-derived type that compares False with other tuples with same contents. Source code in beancount/utils/misc_utils.py def cmptuple(name, attributes): \"\"\"Manufacture a comparable namedtuple class, similar to collections.namedtuple. A comparable named tuple is a tuple which compares to False if contents are equal but the data types are different. We define this to supplement collections.namedtuple because by default a namedtuple disregards the type and we want to make precise comparisons for tests. Args: name: The given name of the class. attributes: A string or tuple of strings, with the names of the attributes. Returns: A new namedtuple-derived type that compares False with other tuples with same contents. \"\"\" base = collections.namedtuple('_{}'.format(name), attributes) return type(name, (TypeComparable, base,), {}) beancount.utils.misc_utils.compute_unique_clean_ids(strings) \uf0c1 Given a sequence of strings, reduce them to corresponding ids without any funny characters and insure that the list of ids is unique. Yields pairs of (id, string) for the result. Parameters: strings \u2013 A list of strings. Returns: A list of (id, string) pairs. Source code in beancount/utils/misc_utils.py def compute_unique_clean_ids(strings): \"\"\"Given a sequence of strings, reduce them to corresponding ids without any funny characters and insure that the list of ids is unique. Yields pairs of (id, string) for the result. Args: strings: A list of strings. Returns: A list of (id, string) pairs. \"\"\" string_set = set(strings) # Try multiple methods until we get one that has no collisions. for regexp, replacement in [(r'[^A-Za-z0-9.-]', '_'), (r'[^A-Za-z0-9_]', ''),]: seen = set() idmap = {} mre = re.compile(regexp) for string in string_set: id_ = mre.sub(replacement, string) if id_ in seen: break # Collision. seen.add(id_) idmap[id_] = string else: break else: return None # Could not find a unique mapping. return idmap beancount.utils.misc_utils.deprecated(message) \uf0c1 A decorator generator to mark functions as deprecated and log a warning. Source code in beancount/utils/misc_utils.py def deprecated(message): \"\"\"A decorator generator to mark functions as deprecated and log a warning.\"\"\" def decorator(func): @functools.wraps(func) def new_func(*args, **kwargs): warnings.warn(\"Call to deprecated function {}: {}\".format(func.__name__, message), category=DeprecationWarning, stacklevel=2) return func(*args, **kwargs) return new_func return decorator beancount.utils.misc_utils.dictmap(mdict, keyfun=None, valfun=None) \uf0c1 Map a dictionary's value. Parameters: mdict \u2013 A dict. key \u2013 A callable to apply to the keys. value \u2013 A callable to apply to the values. Source code in beancount/utils/misc_utils.py def dictmap(mdict, keyfun=None, valfun=None): \"\"\"Map a dictionary's value. Args: mdict: A dict. key: A callable to apply to the keys. value: A callable to apply to the values. \"\"\" if keyfun is None: keyfun = lambda x: x if valfun is None: valfun = lambda x: x return {keyfun(key): valfun(val) for key, val in mdict.items()} beancount.utils.misc_utils.escape_string(string) \uf0c1 Escape quotes and backslashes in payee and narration. Parameters: string \u2013 Any string. Returns. The input string, with offending characters replaced. Source code in beancount/utils/misc_utils.py def escape_string(string): \"\"\"Escape quotes and backslashes in payee and narration. Args: string: Any string. Returns. The input string, with offending characters replaced. \"\"\" return string.replace('\\\\', r'\\\\')\\ .replace('\"', r'\\\"') beancount.utils.misc_utils.filter_type(elist, types) \uf0c1 Filter the given list to yield only instances of the given types. Parameters: elist \u2013 A sequence of elements. types \u2013 A sequence of types to include in the output list. Yields: Each element, if it is an instance of 'types'. Source code in beancount/utils/misc_utils.py def filter_type(elist, types): \"\"\"Filter the given list to yield only instances of the given types. Args: elist: A sequence of elements. types: A sequence of types to include in the output list. Yields: Each element, if it is an instance of 'types'. \"\"\" for element in elist: if not isinstance(element, types): continue yield element beancount.utils.misc_utils.first_paragraph(docstring) \uf0c1 Return the first sentence of a docstring. The sentence has to be delimited by an empty line. Parameters: docstring \u2013 A doc string. Returns: A string with just the first sentence on a single line. Source code in beancount/utils/misc_utils.py def first_paragraph(docstring): \"\"\"Return the first sentence of a docstring. The sentence has to be delimited by an empty line. Args: docstring: A doc string. Returns: A string with just the first sentence on a single line. \"\"\" lines = [] for line in docstring.strip().splitlines(): if not line: break lines.append(line.rstrip()) return ' '.join(lines) beancount.utils.misc_utils.get_screen_height() \uf0c1 Return the height of the terminal that runs this program. Returns: An integer, the number of characters the screen is high. Return 0 if the terminal cannot be initialized. Source code in beancount/utils/misc_utils.py def get_screen_height(): \"\"\"Return the height of the terminal that runs this program. Returns: An integer, the number of characters the screen is high. Return 0 if the terminal cannot be initialized. \"\"\" return _get_screen_value('lines', 0) beancount.utils.misc_utils.get_screen_width() \uf0c1 Return the width of the terminal that runs this program. Returns: An integer, the number of characters the screen is wide. Return 0 if the terminal cannot be initialized. Source code in beancount/utils/misc_utils.py def get_screen_width(): \"\"\"Return the width of the terminal that runs this program. Returns: An integer, the number of characters the screen is wide. Return 0 if the terminal cannot be initialized. \"\"\" return _get_screen_value('cols', 0) beancount.utils.misc_utils.get_tuple_values(ntuple, predicate, memo=None) \uf0c1 Return all members referred to by this namedtuple instance that satisfy the given predicate. This function also works recursively on its members which are lists or tuples, and so it can be used for Transaction instances. Parameters: ntuple \u2013 A tuple or namedtuple. predicate \u2013 A predicate function that returns true if an attribute is to be output. memo \u2013 An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. Source code in beancount/utils/misc_utils.py def get_tuple_values(ntuple, predicate, memo=None): \"\"\"Return all members referred to by this namedtuple instance that satisfy the given predicate. This function also works recursively on its members which are lists or tuples, and so it can be used for Transaction instances. Args: ntuple: A tuple or namedtuple. predicate: A predicate function that returns true if an attribute is to be output. memo: An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. \"\"\" if memo is None: memo = set() id_ntuple = id(ntuple) if id_ntuple in memo: return memo.add(id_ntuple) if predicate(ntuple): yield for attribute in ntuple: if predicate(attribute): yield attribute if isinstance(attribute, (list, tuple)): for value in get_tuple_values(attribute, predicate, memo): yield value beancount.utils.misc_utils.groupby(keyfun, elements) \uf0c1 Group the elements as a dict of lists, where the key is computed using the function 'keyfun'. Parameters: keyfun \u2013 A callable, used to obtain the group key from each element. elements \u2013 An iterable of the elements to group. Returns: A dict of key to list of sequences. Source code in beancount/utils/misc_utils.py def groupby(keyfun, elements): \"\"\"Group the elements as a dict of lists, where the key is computed using the function 'keyfun'. Args: keyfun: A callable, used to obtain the group key from each element. elements: An iterable of the elements to group. Returns: A dict of key to list of sequences. \"\"\" # Note: We could allow a custom aggregation function. Another option is # provide another method to reduce the list values of a dict, but that can # be accomplished using a dict comprehension. grouped = defaultdict(list) for element in elements: grouped[keyfun(element)].append(element) return grouped beancount.utils.misc_utils.idify(string) \uf0c1 Replace characters objectionable for a filename with underscores. Parameters: string \u2013 Any string. Returns: The input string, with offending characters replaced. Source code in beancount/utils/misc_utils.py def idify(string): \"\"\"Replace characters objectionable for a filename with underscores. Args: string: Any string. Returns: The input string, with offending characters replaced. \"\"\" for sfrom, sto in [(r'[ \\(\\)]+', '_'), (r'_*\\._*', '.')]: string = re.sub(sfrom, sto, string) string = string.strip('_') return string beancount.utils.misc_utils.import_curses() \uf0c1 Try to import the 'curses' module. (This is used here in order to override for tests.) Returns: The curses module, if it was possible to import it. Exceptions: ImportError \u2013 If the module could not be imported. Source code in beancount/utils/misc_utils.py def import_curses(): \"\"\"Try to import the 'curses' module. (This is used here in order to override for tests.) Returns: The curses module, if it was possible to import it. Raises: ImportError: If the module could not be imported. \"\"\" # Note: There's a recipe for getting terminal size on Windows here, without # curses, I should probably implement that at some point: # https://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window # Also, consider just using 'blessings' instead, which provides this across # multiple platforms. # pylint: disable=import-outside-toplevel import curses return curses beancount.utils.misc_utils.is_sorted(iterable, key= at 0x78e868b45bc0>, cmp= at 0x78e868b45c60>) \uf0c1 Return true if the sequence is sorted. Parameters: iterable \u2013 An iterable sequence. key \u2013 A function to extract the quantity by which to sort. cmp \u2013 A function that compares two elements of a sequence. Returns: A boolean, true if the sequence is sorted. Source code in beancount/utils/misc_utils.py def is_sorted(iterable, key=lambda x: x, cmp=lambda x, y: x <= y): \"\"\"Return true if the sequence is sorted. Args: iterable: An iterable sequence. key: A function to extract the quantity by which to sort. cmp: A function that compares two elements of a sequence. Returns: A boolean, true if the sequence is sorted. \"\"\" iterator = map(key, iterable) prev = next(iterator) for element in iterator: if not cmp(prev, element): return False prev = element return True beancount.utils.misc_utils.log_time(operation_name, log_timings, indent=0) \uf0c1 A context manager that times the block and logs it to info level. Parameters: operation_name \u2013 A string, a label for the name of the operation. log_timings \u2013 A function to write log messages to. If left to None, no timings are written (this becomes a no-op). indent \u2013 An integer, the indentation level for the format of the timing line. This is useful if you're logging timing to a hierarchy of operations. Yields: The start time of the operation. Source code in beancount/utils/misc_utils.py @contextlib.contextmanager def log_time(operation_name, log_timings, indent=0): \"\"\"A context manager that times the block and logs it to info level. Args: operation_name: A string, a label for the name of the operation. log_timings: A function to write log messages to. If left to None, no timings are written (this becomes a no-op). indent: An integer, the indentation level for the format of the timing line. This is useful if you're logging timing to a hierarchy of operations. Yields: The start time of the operation. \"\"\" time1 = time() yield time1 time2 = time() if log_timings: log_timings(\"Operation: {:48} Time: {}{:6.0f} ms\".format( \"'{}'\".format(operation_name), ' '*indent, (time2 - time1) * 1000)) beancount.utils.misc_utils.longest(seq) \uf0c1 Return the longest of the given subsequences. Parameters: seq \u2013 An iterable sequence of lists. Returns: The longest list from the sequence. Source code in beancount/utils/misc_utils.py def longest(seq): \"\"\"Return the longest of the given subsequences. Args: seq: An iterable sequence of lists. Returns: The longest list from the sequence. \"\"\" longest, length = None, -1 for element in seq: len_element = len(element) if len_element > length: longest, length = element, len_element return longest beancount.utils.misc_utils.map_namedtuple_attributes(attributes, mapper, object_) \uf0c1 Map the value of the named attributes of object by mapper. Parameters: attributes \u2013 A sequence of string, the attribute names to map. mapper \u2013 A callable that accepts the value of a field and returns the new value. object_ \u2013 Some namedtuple object with attributes on it. Returns: A new instance of the same namedtuple with the named fields mapped by mapper. Source code in beancount/utils/misc_utils.py def map_namedtuple_attributes(attributes, mapper, object_): \"\"\"Map the value of the named attributes of object by mapper. Args: attributes: A sequence of string, the attribute names to map. mapper: A callable that accepts the value of a field and returns the new value. object_: Some namedtuple object with attributes on it. Returns: A new instance of the same namedtuple with the named fields mapped by mapper. \"\"\" return object_._replace(**{attribute: mapper(getattr(object_, attribute)) for attribute in attributes}) beancount.utils.misc_utils.replace_namedtuple_values(ntuple, predicate, mapper, memo=None) \uf0c1 Recurse through all the members of namedtuples and lists, and for members that match the given predicate, run them through the given mapper. Parameters: ntuple \u2013 A namedtuple instance. predicate \u2013 A predicate function that returns true if an attribute is to be output. mapper \u2013 A callable, that will accept a single argument and return its replacement value. memo \u2013 An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. Source code in beancount/utils/misc_utils.py def replace_namedtuple_values(ntuple, predicate, mapper, memo=None): \"\"\"Recurse through all the members of namedtuples and lists, and for members that match the given predicate, run them through the given mapper. Args: ntuple: A namedtuple instance. predicate: A predicate function that returns true if an attribute is to be output. mapper: A callable, that will accept a single argument and return its replacement value. memo: An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. \"\"\" if memo is None: memo = set() id_ntuple = id(ntuple) if id_ntuple in memo: return None memo.add(id_ntuple) # pylint: disable=unidiomatic-typecheck if not (type(ntuple) is not tuple and isinstance(ntuple, tuple)): return ntuple replacements = {} for attribute_name, attribute in zip(ntuple._fields, ntuple): if predicate(attribute): replacements[attribute_name] = mapper(attribute) elif type(attribute) is not tuple and isinstance(attribute, tuple): replacements[attribute_name] = replace_namedtuple_values( attribute, predicate, mapper, memo) elif type(attribute) in (list, tuple): replacements[attribute_name] = [ replace_namedtuple_values(member, predicate, mapper, memo) for member in attribute] return ntuple._replace(**replacements) beancount.utils.misc_utils.skipiter(iterable, num_skip) \uf0c1 Skip some elements from an iterator. Parameters: iterable \u2013 An iterator. num_skip \u2013 The number of elements in the period. Yields: Elements from the iterable, with num_skip elements skipped. For example, skipiter(range(10), 3) yields [0, 3, 6, 9]. Source code in beancount/utils/misc_utils.py def skipiter(iterable, num_skip): \"\"\"Skip some elements from an iterator. Args: iterable: An iterator. num_skip: The number of elements in the period. Yields: Elements from the iterable, with num_skip elements skipped. For example, skipiter(range(10), 3) yields [0, 3, 6, 9]. \"\"\" assert num_skip > 0 sit = iter(iterable) while 1: try: value = next(sit) except StopIteration: return yield value for _ in range(num_skip-1): try: next(sit) except StopIteration: return beancount.utils.misc_utils.sorted_uniquify(iterable, keyfunc=None, last=False) \uf0c1 Given a sequence of elements, sort and remove duplicates of the given key. Keep either the first or the last (by key) element of a sequence of key-identical elements. This does not maintain the ordering of the original elements, they are returned sorted (by key) instead. Parameters: iterable \u2013 An iterable sequence. keyfunc \u2013 A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last \u2013 A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. Source code in beancount/utils/misc_utils.py def sorted_uniquify(iterable, keyfunc=None, last=False): \"\"\"Given a sequence of elements, sort and remove duplicates of the given key. Keep either the first or the last (by key) element of a sequence of key-identical elements. This does _not_ maintain the ordering of the original elements, they are returned sorted (by key) instead. Args: iterable: An iterable sequence. keyfunc: A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last: A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. \"\"\" if keyfunc is None: keyfunc = lambda x: x if last: prev_obj = UNSET prev_key = UNSET for obj in sorted(iterable, key=keyfunc): key = keyfunc(obj) if key != prev_key and prev_obj is not UNSET: yield prev_obj prev_obj = obj prev_key = key if prev_obj is not UNSET: yield prev_obj else: prev_key = UNSET for obj in sorted(iterable, key=keyfunc): key = keyfunc(obj) if key != prev_key: yield obj prev_key = key beancount.utils.misc_utils.staticvar(varname, initial_value) \uf0c1 Returns a decorator that defines a Python function attribute. This is used to simulate a static function variable in Python. Parameters: varname \u2013 A string, the name of the variable to define. initial_value \u2013 The value to initialize the variable to. Returns: A function decorator. Source code in beancount/utils/misc_utils.py def staticvar(varname, initial_value): \"\"\"Returns a decorator that defines a Python function attribute. This is used to simulate a static function variable in Python. Args: varname: A string, the name of the variable to define. initial_value: The value to initialize the variable to. Returns: A function decorator. \"\"\" def deco(fun): setattr(fun, varname, initial_value) return fun return deco beancount.utils.misc_utils.swallow(*exception_types) \uf0c1 Catch and ignore certain exceptions. Parameters: exception_types \u2013 A tuple of exception classes to ignore. Yields: None. Source code in beancount/utils/misc_utils.py @contextlib.contextmanager def swallow(*exception_types): \"\"\"Catch and ignore certain exceptions. Args: exception_types: A tuple of exception classes to ignore. Yields: None. \"\"\" try: yield except Exception as exc: if not isinstance(exc, exception_types): raise beancount.utils.misc_utils.uniquify(iterable, keyfunc=None, last=False) \uf0c1 Given a sequence of elements, remove duplicates of the given key. Keep either the first or the last element of a sequence of key-identical elements. Order is maintained as much as possible. This does maintain the ordering of the original elements, they are returned in the same order as the original elements. Parameters: iterable \u2013 An iterable sequence. keyfunc \u2013 A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last \u2013 A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. Source code in beancount/utils/misc_utils.py def uniquify(iterable, keyfunc=None, last=False): \"\"\"Given a sequence of elements, remove duplicates of the given key. Keep either the first or the last element of a sequence of key-identical elements. Order is maintained as much as possible. This does maintain the ordering of the original elements, they are returned in the same order as the original elements. Args: iterable: An iterable sequence. keyfunc: A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last: A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. \"\"\" if keyfunc is None: keyfunc = lambda x: x seen = set() if last: unique_reversed_list = [] for obj in reversed(iterable): key = keyfunc(obj) if key not in seen: seen.add(key) unique_reversed_list.append(obj) yield from reversed(unique_reversed_list) else: for obj in iterable: key = keyfunc(obj) if key not in seen: seen.add(key) yield obj beancount.utils.net_utils \uf0c1 Network utilities. beancount.utils.net_utils.retrying_urlopen(url, timeout=5, max_retry=5) \uf0c1 Open and download the given URL, retrying if it times out. Parameters: url \u2013 A string, the URL to fetch. timeout \u2013 A timeout after which to stop waiting for a response and return an error. max_retry \u2013 The maximum number of times to retry. Returns: The contents of the fetched URL. Source code in beancount/utils/net_utils.py def retrying_urlopen(url, timeout=5, max_retry=5): \"\"\"Open and download the given URL, retrying if it times out. Args: url: A string, the URL to fetch. timeout: A timeout after which to stop waiting for a response and return an error. max_retry: The maximum number of times to retry. Returns: The contents of the fetched URL. \"\"\" for _ in range(max_retry): logging.debug(\"Reading %s\", url) try: response = request.urlopen(url, timeout=timeout) if response: break except error.URLError: return None if response and response.getcode() != 200: return None return response beancount.utils.pager \uf0c1 Code to write output to a pager. This module contains an object accumulates lines up to a minimum and then decides whether to flush them to the original output directly if under the threshold (no pager) or creates a pager and flushes the lines to it if above the threshold and then forwards all future lines to it. The purpose of this object is to pipe output to a pager only if the number of lines to be printed exceeds a minimum number of lines. The contextmanager is intended to be used to pipe output to a pager and wait on the pager to complete before continuing. Simply write to the file object and upon exit we close the file object. This also silences broken pipe errors triggered by the user exiting the sub-process, and recovers from a failing pager command by just using stdout. beancount.utils.pager.ConditionalPager \uf0c1 A proxy file for a pager that only creates a pager after a minimum number of lines has been printed to it. beancount.utils.pager.ConditionalPager.__enter__(self) special \uf0c1 Initialize the context manager and return this instance as it. Source code in beancount/utils/pager.py def __enter__(self): \"\"\"Initialize the context manager and return this instance as it.\"\"\" # The file and pipe object we're writing to. This gets set after the # number of accumulated lines reaches the threshold. if self.minlines: self.file = None self.pipe = None else: self.file, self.pipe = create_pager(self.command, self.default_file) # Lines accumulated before the threshold. self.accumulated_data = [] self.accumulated_lines = 0 # Return this object to be used as the context manager itself. return self beancount.utils.pager.ConditionalPager.__exit__(self, type, value, unused_traceback) special \uf0c1 Context manager exit. This flushes the output to our output file. Parameters: type \u2013 Optional exception type, as per context managers. value \u2013 Optional exception value, as per context managers. unused_traceback \u2013 Optional trace. Source code in beancount/utils/pager.py def __exit__(self, type, value, unused_traceback): \"\"\"Context manager exit. This flushes the output to our output file. Args: type: Optional exception type, as per context managers. value: Optional exception value, as per context managers. unused_traceback: Optional trace. \"\"\" try: if self.file: # Flush the output file and close it. self.file.flush() else: # Oops... we never reached the threshold. Flush the accumulated # output to the file. self.flush_accumulated(self.default_file) # Wait for the subprocess (if we have one). if self.pipe: self.file.close() self.pipe.wait() # Absorb broken pipes that may occur on flush or close above. except BrokenPipeError: return True # Absorb broken pipes. if isinstance(value, BrokenPipeError): return True elif value: raise beancount.utils.pager.ConditionalPager.__init__(self, command, minlines=None) special \uf0c1 Create a conditional pager. Parameters: command \u2013 A string, the shell command to run as a pager. minlines \u2013 If set, the number of lines under which you should not bother starting a pager. This avoids kicking off a pager if the screen is high enough to render the contents. If the value is unset, always starts a pager (which is fine behavior too). Source code in beancount/utils/pager.py def __init__(self, command, minlines=None): \"\"\"Create a conditional pager. Args: command: A string, the shell command to run as a pager. minlines: If set, the number of lines under which you should not bother starting a pager. This avoids kicking off a pager if the screen is high enough to render the contents. If the value is unset, always starts a pager (which is fine behavior too). \"\"\" self.command = command self.minlines = minlines self.default_file = (codecs.getwriter(\"utf-8\")(sys.stdout.buffer) if hasattr(sys.stdout, 'buffer') else sys.stdout) beancount.utils.pager.ConditionalPager.flush_accumulated(self, file) \uf0c1 Flush the existing lines to the newly created pager. This also disabled the accumulator. Parameters: file \u2013 A file object to flush the accumulated data to. Source code in beancount/utils/pager.py def flush_accumulated(self, file): \"\"\"Flush the existing lines to the newly created pager. This also disabled the accumulator. Args: file: A file object to flush the accumulated data to. \"\"\" if self.accumulated_data: write = file.write for data in self.accumulated_data: write(data) self.accumulated_data = None self.accumulated_lines = None beancount.utils.pager.ConditionalPager.write(self, data) \uf0c1 Write the data out. Overridden from the file object interface. Parameters: data \u2013 A string, data to write to the output. Source code in beancount/utils/pager.py def write(self, data): \"\"\"Write the data out. Overridden from the file object interface. Args: data: A string, data to write to the output. \"\"\" if self.file is None: # Accumulate the new lines. self.accumulated_lines += data.count('\\n') self.accumulated_data.append(data) # If we've reached the threshold, create a file. if self.accumulated_lines > self.minlines: self.file, self.pipe = create_pager(self.command, self.default_file) self.flush_accumulated(self.file) else: # We've already created a pager subprocess... flush the lines to it. self.file.write(data) # try: # except BrokenPipeError: # # Make sure we don't barf on __exit__(). # self.file = self.pipe = None # raise beancount.utils.pager.create_pager(command, file) \uf0c1 Try to create and return a pager subprocess. Parameters: command \u2013 A string, the shell command to run as a pager. file \u2013 The file object for the pager write to. This is also used as a default if we failed to create the pager subprocess. Returns: A pair of (file, pipe), a file object and an optional subprocess.Popen instance to wait on. The pipe instance may be set to None if we failed to create a subprocess. Source code in beancount/utils/pager.py def create_pager(command, file): \"\"\"Try to create and return a pager subprocess. Args: command: A string, the shell command to run as a pager. file: The file object for the pager write to. This is also used as a default if we failed to create the pager subprocess. Returns: A pair of (file, pipe), a file object and an optional subprocess.Popen instance to wait on. The pipe instance may be set to None if we failed to create a subprocess. \"\"\" if command is None: command = os.environ.get('PAGER', DEFAULT_PAGER) if not command: command = DEFAULT_PAGER pipe = None # In case of using 'less', make sure the charset is set properly. In theory # you could override this by setting PAGER to \"LESSCHARSET=utf-8 less\" but # this shouldn't affect other programs and is unlikely to cause problems, so # we set it here to make default behavior work for most people (we always # write UTF-8). env = os.environ.copy() env['LESSCHARSET'] = \"utf-8\" try: pipe = subprocess.Popen(command, shell=True, stdin=subprocess.PIPE, stdout=file, env=env) except OSError as exc: logging.error(\"Invalid pager: {}\".format(exc)) else: stdin_wrapper = io.TextIOWrapper(pipe.stdin, 'utf-8') file = stdin_wrapper return file, pipe beancount.utils.pager.flush_only(fileobj) \uf0c1 A contextmanager around a file object that does not close the file. This is used to return a context manager on a file object but not close it. We flush it instead. This is useful in order to provide an alternative to a pager class as above. Parameters: fileobj \u2013 A file object, to remain open after running the context manager. Yields: A context manager that yields this object. Source code in beancount/utils/pager.py @contextlib.contextmanager def flush_only(fileobj): \"\"\"A contextmanager around a file object that does not close the file. This is used to return a context manager on a file object but not close it. We flush it instead. This is useful in order to provide an alternative to a pager class as above. Args: fileobj: A file object, to remain open after running the context manager. Yields: A context manager that yields this object. \"\"\" try: yield fileobj finally: fileobj.flush() beancount.utils.regexp_utils \uf0c1 Regular expression helpers. beancount.utils.regexp_utils.re_replace_unicode(regexp) \uf0c1 Substitute Unicode Properties in regular expressions with their corresponding expanded ranges. Parameters: regexp \u2013 A regular expression string. Returns: Return the regular expression with Unicode Properties substituted. Source code in beancount/utils/regexp_utils.py def re_replace_unicode(regexp): \"\"\"Substitute Unicode Properties in regular expressions with their corresponding expanded ranges. Args: regexp: A regular expression string. Returns: Return the regular expression with Unicode Properties substituted. \"\"\" for category, rangestr in UNICODE_RANGES.items(): regexp = regexp.replace(r'\\p{' + category + '}', rangestr) return regexp beancount.utils.snoop \uf0c1 Text manipulation utilities. beancount.utils.snoop.Snoop \uf0c1 A snooper callable that just saves the returned values of a function. This is particularly useful for re.match and re.search in conditionals, e.g.:: snoop = Snoop() ... if snoop(re.match(r\"(\\d+)-(\\d+)-(\\d+)\", text)): year, month, date = snoop.value.group(1, 2, 3) Attributes: Name Type Description value The last value snooped from a function call. history If 'maxlen' was specified, the last few values that were snooped. beancount.utils.snoop.Snoop.__call__(self, value) special \uf0c1 Save a value to the snooper. This is meant to wrap a function call. Parameters: value \u2013 The value to push/save. Returns: Value itself. Source code in beancount/utils/snoop.py def __call__(self, value): \"\"\"Save a value to the snooper. This is meant to wrap a function call. Args: value: The value to push/save. Returns: Value itself. \"\"\" self.value = value if self.history is not None: self.history.append(value) return value beancount.utils.snoop.Snoop.__getattr__(self, attr) special \uf0c1 Forward the attribute to the value. Parameters: attr \u2013 A string, the name of the attribute. Returns: The value of the attribute. Source code in beancount/utils/snoop.py def __getattr__(self, attr): \"\"\"Forward the attribute to the value. Args: attr: A string, the name of the attribute. Returns: The value of the attribute. \"\"\" return getattr(self.value, attr) beancount.utils.snoop.Snoop.__init__(self, maxlen=None) special \uf0c1 Create a new snooper. Parameters: maxlen \u2013 If specified, an integer, which enables the saving of that Source code in beancount/utils/snoop.py def __init__(self, maxlen=None): \"\"\"Create a new snooper. Args: maxlen: If specified, an integer, which enables the saving of that number of last values in the history attribute. \"\"\" self.value = None self.history = (collections.deque(maxlen=maxlen) if maxlen else None) beancount.utils.snoop.snoopify(function) \uf0c1 Decorate a function as snoopable. This is meant to reassign existing functions to a snoopable version of them. For example, if you wanted 're.match' to be automatically snoopable, just decorate it like this: re.match = snoopify(re.match) and then you can just call 're.match' in a conditional and then access 're.match.value' to get to the last returned value. Source code in beancount/utils/snoop.py def snoopify(function): \"\"\"Decorate a function as snoopable. This is meant to reassign existing functions to a snoopable version of them. For example, if you wanted 're.match' to be automatically snoopable, just decorate it like this: re.match = snoopify(re.match) and then you can just call 're.match' in a conditional and then access 're.match.value' to get to the last returned value. \"\"\" @functools.wraps(function) def wrapper(*args, **kw): value = function(*args, **kw) wrapper.value = value return value wrapper.value = None return wrapper beancount.utils.test_utils \uf0c1 Support utilities for testing scripts. beancount.utils.test_utils.RCall ( tuple ) \uf0c1 RCall(args, kwargs, return_value) beancount.utils.test_utils.RCall.__getnewargs__(self) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/utils/test_utils.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self) beancount.utils.test_utils.RCall.__new__(_cls, args, kwargs, return_value) special staticmethod \uf0c1 Create new instance of RCall(args, kwargs, return_value) beancount.utils.test_utils.RCall.__repr__(self) special \uf0c1 Return a nicely formatted representation string Source code in beancount/utils/test_utils.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self beancount.utils.test_utils.TestCase ( TestCase ) \uf0c1 beancount.utils.test_utils.TestCase.assertLines(self, text1, text2, message=None) \uf0c1 Compare the lines of text1 and text2, ignoring whitespace. Parameters: text1 \u2013 A string, the expected text. text2 \u2013 A string, the actual text. message \u2013 An optional string message in case the assertion fails. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/utils/test_utils.py def assertLines(self, text1, text2, message=None): \"\"\"Compare the lines of text1 and text2, ignoring whitespace. Args: text1: A string, the expected text. text2: A string, the actual text. message: An optional string message in case the assertion fails. Raises: AssertionError: If the exception fails. \"\"\" clean_text1 = textwrap.dedent(text1.strip()) clean_text2 = textwrap.dedent(text2.strip()) lines1 = [line.strip() for line in clean_text1.splitlines()] lines2 = [line.strip() for line in clean_text2.splitlines()] # Compress all space longer than 4 spaces to exactly 4. # This affords us to be even looser. lines1 = [re.sub(' [ \\t]*', ' ', line) for line in lines1] lines2 = [re.sub(' [ \\t]*', ' ', line) for line in lines2] self.assertEqual(lines1, lines2, message) beancount.utils.test_utils.TestCase.assertOutput(self, expected_text) \uf0c1 Expect text printed to stdout. Parameters: expected_text \u2013 A string, the text that should have been printed to stdout. Exceptions: AssertionError \u2013 If the text differs. Source code in beancount/utils/test_utils.py @contextlib.contextmanager def assertOutput(self, expected_text): \"\"\"Expect text printed to stdout. Args: expected_text: A string, the text that should have been printed to stdout. Raises: AssertionError: If the text differs. \"\"\" with capture() as oss: yield oss self.assertLines(textwrap.dedent(expected_text), oss.getvalue()) beancount.utils.test_utils.call_command(command) \uf0c1 Run the script with a subprocess. Parameters: script_args \u2013 A list of strings, the arguments to the subprocess, including the script name. Returns: A triplet of (return code integer, stdout ext, stderr text). Source code in beancount/utils/test_utils.py def call_command(command): \"\"\"Run the script with a subprocess. Args: script_args: A list of strings, the arguments to the subprocess, including the script name. Returns: A triplet of (return code integer, stdout ext, stderr text). \"\"\" assert isinstance(command, list), command p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() return p.returncode, stdout.decode(), stderr.decode() beancount.utils.test_utils.capture(*attributes) \uf0c1 A context manager that captures what's printed to stdout. Parameters: *attributes \u2013 A tuple of strings, the name of the sys attributes to override with StringIO instances. Yields: A StringIO string accumulator. Source code in beancount/utils/test_utils.py def capture(*attributes): \"\"\"A context manager that captures what's printed to stdout. Args: *attributes: A tuple of strings, the name of the sys attributes to override with StringIO instances. Yields: A StringIO string accumulator. \"\"\" if not attributes: attributes = 'stdout' elif len(attributes) == 1: attributes = attributes[0] return patch(sys, attributes, io.StringIO) beancount.utils.test_utils.create_temporary_files(root, contents_map) \uf0c1 Create a number of temporary files under 'root'. This routine is used to initialize the contents of multiple files under a temporary directory. Parameters: root \u2013 A string, the name of the directory under which to create the files. contents_map \u2013 A dict of relative filenames to their contents. The content strings will be automatically dedented for convenience. In addition, the string 'ROOT' in the contents will be automatically replaced by the root directory name. Source code in beancount/utils/test_utils.py def create_temporary_files(root, contents_map): \"\"\"Create a number of temporary files under 'root'. This routine is used to initialize the contents of multiple files under a temporary directory. Args: root: A string, the name of the directory under which to create the files. contents_map: A dict of relative filenames to their contents. The content strings will be automatically dedented for convenience. In addition, the string 'ROOT' in the contents will be automatically replaced by the root directory name. \"\"\" os.makedirs(root, exist_ok=True) for relative_filename, contents in contents_map.items(): assert not path.isabs(relative_filename) filename = path.join(root, relative_filename) os.makedirs(path.dirname(filename), exist_ok=True) clean_contents = textwrap.dedent(contents.replace('{root}', root)) with open(filename, 'w') as f: f.write(clean_contents) beancount.utils.test_utils.docfile(function, **kwargs) \uf0c1 A decorator that write the function's docstring to a temporary file and calls the decorated function with the temporary filename. This is useful for writing tests. Parameters: function \u2013 A function to decorate. Returns: The decorated function. Source code in beancount/utils/test_utils.py def docfile(function, **kwargs): \"\"\"A decorator that write the function's docstring to a temporary file and calls the decorated function with the temporary filename. This is useful for writing tests. Args: function: A function to decorate. Returns: The decorated function. \"\"\" @functools.wraps(function) def new_function(self): allowed = ('buffering', 'encoding', 'newline', 'dir', 'prefix', 'suffix') if any([key not in allowed for key in kwargs]): raise ValueError(\"Invalid kwarg to docfile_extra\") with tempfile.NamedTemporaryFile('w', **kwargs) as file: text = function.__doc__ file.write(textwrap.dedent(text)) file.flush() return function(self, file.name) new_function.__doc__ = None return new_function beancount.utils.test_utils.docfile_extra(**kwargs) \uf0c1 A decorator identical to @docfile, but it also takes kwargs for the temporary file, Kwargs: e.g. buffering, encoding, newline, dir, prefix, and suffix. Returns: docfile Source code in beancount/utils/test_utils.py def docfile_extra(**kwargs): \"\"\" A decorator identical to @docfile, but it also takes kwargs for the temporary file, Kwargs: e.g. buffering, encoding, newline, dir, prefix, and suffix. Returns: docfile \"\"\" return functools.partial(docfile, **kwargs) beancount.utils.test_utils.environ(varname, newvalue) \uf0c1 A context manager which pushes varname's value and restores it later. Parameters: varname \u2013 A string, the environ variable name. newvalue \u2013 A string, the desired value. Source code in beancount/utils/test_utils.py @contextlib.contextmanager def environ(varname, newvalue): \"\"\"A context manager which pushes varname's value and restores it later. Args: varname: A string, the environ variable name. newvalue: A string, the desired value. \"\"\" oldvalue = os.environ.get(varname, None) os.environ[varname] = newvalue yield if oldvalue is not None: os.environ[varname] = oldvalue else: del os.environ[varname] beancount.utils.test_utils.find_python_lib() \uf0c1 Return the path to the root of the Python libraries. Returns: A string, the root directory. Source code in beancount/utils/test_utils.py def find_python_lib(): \"\"\"Return the path to the root of the Python libraries. Returns: A string, the root directory. \"\"\" return path.dirname(path.dirname(path.dirname(__file__))) beancount.utils.test_utils.find_repository_root(filename=None) \uf0c1 Return the path to the repository root. Parameters: filename \u2013 A string, the name of a file within the repository. Returns: A string, the root directory. Source code in beancount/utils/test_utils.py def find_repository_root(filename=None): \"\"\"Return the path to the repository root. Args: filename: A string, the name of a file within the repository. Returns: A string, the root directory. \"\"\" if filename is None: filename = __file__ # Support root directory under Bazel. match = re.match(r\"(.*\\.runfiles/beancount)/\", filename) if match: return match.group(1) while not all(path.exists(path.join(filename, sigfile)) for sigfile in ('PKG-INFO', 'COPYING', 'README.rst')): prev_filename = filename filename = path.dirname(filename) if prev_filename == filename: raise ValueError(\"Failed to find the root directory.\") return filename beancount.utils.test_utils.make_failing_importer(*removed_module_names) \uf0c1 Make an importer that raise an ImportError for some modules. Use it like this: @mock.patch('builtins. import ', make_failing_importer('setuptools')) def test_... Parameters: removed_module_name \u2013 The name of the module import that should raise an exception. Returns: A decorated test decorator. Source code in beancount/utils/test_utils.py def make_failing_importer(*removed_module_names): \"\"\"Make an importer that raise an ImportError for some modules. Use it like this: @mock.patch('builtins.__import__', make_failing_importer('setuptools')) def test_... Args: removed_module_name: The name of the module import that should raise an exception. Returns: A decorated test decorator. \"\"\" def failing_import(name, *args, **kwargs): if name in removed_module_names: raise ImportError(\"Could not import {}\".format(name)) return builtins.__import__(name, *args, **kwargs) return failing_import beancount.utils.test_utils.nottest(func) \uf0c1 Make the given function not testable. Source code in beancount/utils/test_utils.py def nottest(func): \"Make the given function not testable.\" func.__test__ = False return func beancount.utils.test_utils.patch(obj, attributes, replacement_type) \uf0c1 A context manager that temporarily patches an object's attributes. All attributes in 'attributes' are saved and replaced by new instances of type 'replacement_type'. Parameters: obj \u2013 The object to patch up. attributes \u2013 A string or a sequence of strings, the names of attributes to replace. replacement_type \u2013 A callable to build replacement objects. Yields: An instance of a list of sequences of 'replacement_type'. Source code in beancount/utils/test_utils.py @contextlib.contextmanager def patch(obj, attributes, replacement_type): \"\"\"A context manager that temporarily patches an object's attributes. All attributes in 'attributes' are saved and replaced by new instances of type 'replacement_type'. Args: obj: The object to patch up. attributes: A string or a sequence of strings, the names of attributes to replace. replacement_type: A callable to build replacement objects. Yields: An instance of a list of sequences of 'replacement_type'. \"\"\" single = isinstance(attributes, str) if single: attributes = [attributes] saved = [] replacements = [] for attribute in attributes: replacement = replacement_type() replacements.append(replacement) saved.append(getattr(obj, attribute)) setattr(obj, attribute, replacement) yield replacements[0] if single else replacements for attribute, saved_attr in zip(attributes, saved): setattr(obj, attribute, saved_attr) beancount.utils.test_utils.record(fun) \uf0c1 Decorates the function to intercept and record all calls and return values. Parameters: fun \u2013 A callable to be decorated. Returns: A wrapper function with a .calls attribute, a list of RCall instances. Source code in beancount/utils/test_utils.py def record(fun): \"\"\"Decorates the function to intercept and record all calls and return values. Args: fun: A callable to be decorated. Returns: A wrapper function with a .calls attribute, a list of RCall instances. \"\"\" @functools.wraps(fun) def wrapped(*args, **kw): return_value = fun(*args, **kw) wrapped.calls.append(RCall(args, kw, return_value)) return return_value wrapped.calls = [] return wrapped beancount.utils.test_utils.run_with_args(function, args) \uf0c1 Run the given function with sys.argv set to argv. The first argument is automatically inferred to be where the function object was defined. sys.argv is restored after the function is called. Parameters: function \u2013 A function object to call with no arguments. argv \u2013 A list of arguments, excluding the script name, to be temporarily set on sys.argv. Returns: The return value of the function run. Source code in beancount/utils/test_utils.py def run_with_args(function, args): \"\"\"Run the given function with sys.argv set to argv. The first argument is automatically inferred to be where the function object was defined. sys.argv is restored after the function is called. Args: function: A function object to call with no arguments. argv: A list of arguments, excluding the script name, to be temporarily set on sys.argv. Returns: The return value of the function run. \"\"\" saved_argv = sys.argv saved_handlers = logging.root.handlers try: module = sys.modules[function.__module__] sys.argv = [module.__file__] + args logging.root.handlers = [] return function() finally: sys.argv = saved_argv logging.root.handlers = saved_handlers beancount.utils.test_utils.search_words(words, line) \uf0c1 Search for a sequence of words in a line. Parameters: words \u2013 A list of strings, the words to look for, or a space-separated string. line \u2013 A string, the line to search into. Returns: A MatchObject, or None. Source code in beancount/utils/test_utils.py def search_words(words, line): \"\"\"Search for a sequence of words in a line. Args: words: A list of strings, the words to look for, or a space-separated string. line: A string, the line to search into. Returns: A MatchObject, or None. \"\"\" if isinstance(words, str): words = words.split() return re.search('.*'.join(r'\\b{}\\b'.format(word) for word in words), line) beancount.utils.test_utils.skipIfRaises(*exc_types) \uf0c1 A context manager (or decorator) that skips a test if an exception is raised. Yields: Nothing, for you to execute the function code. Exceptions: SkipTest \u2013 if the test raised the expected exception. Source code in beancount/utils/test_utils.py @contextlib.contextmanager def skipIfRaises(*exc_types): \"\"\"A context manager (or decorator) that skips a test if an exception is raised. Args: exc_type Yields: Nothing, for you to execute the function code. Raises: SkipTest: if the test raised the expected exception. \"\"\" try: yield except exc_types as exception: raise unittest.SkipTest(exception) beancount.utils.test_utils.subprocess_env() \uf0c1 Return a dict to use as environment for running subprocesses. Returns: A string, the root directory. Source code in beancount/utils/test_utils.py def subprocess_env(): \"\"\"Return a dict to use as environment for running subprocesses. Returns: A string, the root directory. \"\"\" # Ensure we have locations to invoke our Python executable and our # runnable binaries in the test environment to run subprocesses. binpath = ':'.join([ path.dirname(sys.executable), path.join(find_repository_root(__file__), 'bin'), os.environ.get('PATH', '').strip(':')]).strip(':') return {'PATH': binpath, 'PYTHONPATH': find_python_lib()} beancount.utils.test_utils.tempdir(delete=True, **kw) \uf0c1 A context manager that creates a temporary directory and deletes its contents unconditionally once done. Parameters: delete \u2013 A boolean, true if we want to delete the directory after running. **kw \u2013 Keyword arguments for mkdtemp. Yields: A string, the name of the temporary directory created. Source code in beancount/utils/test_utils.py @contextlib.contextmanager def tempdir(delete=True, **kw): \"\"\"A context manager that creates a temporary directory and deletes its contents unconditionally once done. Args: delete: A boolean, true if we want to delete the directory after running. **kw: Keyword arguments for mkdtemp. Yields: A string, the name of the temporary directory created. \"\"\" tempdir = tempfile.mkdtemp(prefix=\"beancount-test-tmpdir.\", **kw) try: yield tempdir finally: if delete: shutil.rmtree(tempdir, ignore_errors=True) beancount.utils.text_utils \uf0c1 Text manipulation utilities. beancount.utils.text_utils.entitize_ampersand(filename) \uf0c1 Convert unescaped ampersand characters (&) to XML entities. This is used to fix code that has been programmed by bad developers who didn't think about escaping the entities in their strings. file does not contain Parameters: filename \u2013 A string, the name of the file to convert. Returns: A self-destructing NamedTemporaryFile object that has been flushed and which you may read to obtain the fixed contents. Source code in beancount/utils/text_utils.py def entitize_ampersand(filename): \"\"\"Convert unescaped ampersand characters (&) to XML entities. This is used to fix code that has been programmed by bad developers who didn't think about escaping the entities in their strings. file does not contain Args: filename: A string, the name of the file to convert. Returns: A self-destructing NamedTemporaryFile object that has been flushed and which you may read to obtain the fixed contents. \"\"\" tidy_file = tempfile.NamedTemporaryFile(suffix='.xls', mode='w', delete=False) with open(filename) as infile: contents = infile.read() new_contents = re.sub('&([^;&]{12})', '&\\\\1', contents) tidy_file.write(new_contents) tidy_file.flush() return tidy_file beancount.utils.text_utils.replace_number(match) \uf0c1 Replace a single number matched from text into X'es. 'match' is a MatchObject from a regular expressions match. (Use this with re.sub()). Parameters: match \u2013 A MatchObject. Returns: A replacement string, consisting only of X'es. Source code in beancount/utils/text_utils.py def replace_number(match): \"\"\"Replace a single number matched from text into X'es. 'match' is a MatchObject from a regular expressions match. (Use this with re.sub()). Args: match: A MatchObject. Returns: A replacement string, consisting only of X'es. \"\"\" return re.sub('[0-9]', 'X', match.group(1)) + match.group(2) beancount.utils.text_utils.replace_numbers(text) \uf0c1 Replace all numbers found within text. Note that this is a heuristic used to filter out private numbers from web pages in incognito mode and thus may not be perfect. It should let through numbers which are part of URLs. Parameters: text \u2013 An input string object. Returns: A string, with relevant numbers hopefully replaced with X'es. Source code in beancount/utils/text_utils.py def replace_numbers(text): \"\"\"Replace all numbers found within text. Note that this is a heuristic used to filter out private numbers from web pages in incognito mode and thus may not be perfect. It should let through numbers which are part of URLs. Args: text: An input string object. Returns: A string, with relevant numbers hopefully replaced with X'es. \"\"\" return re.sub(r'\\b([0-9,]+(?:\\.[0-9]*)?)\\b([ \\t<]+[^0-9,.]|$)', replace_number, text) beancount.utils.version \uf0c1 Implement common options across all programs. beancount.utils.version.ArgumentParser(*args, **kwargs) \uf0c1 Add a standard --version option to an ArgumentParser. Parameters: *args \u2013 Arguments for the parser. *kwargs \u2013 Keyword arguments for the parser. Returns: An instance of ArgumentParser, with our default options set. Source code in beancount/utils/version.py def ArgumentParser(*args, **kwargs): \"\"\"Add a standard --version option to an ArgumentParser. Args: *args: Arguments for the parser. *kwargs: Keyword arguments for the parser. Returns: An instance of ArgumentParser, with our default options set. \"\"\" parser = argparse.ArgumentParser(*args, **kwargs) parser.add_argument('--version', '-V', action='version', version=compute_version_string( beancount.__version__, _parser.__vc_changeset__, _parser.__vc_timestamp__)) return parser beancount.utils.version.compute_version_string(version, changeset, timestamp) \uf0c1 Compute a version string from the changeset and timestamp baked in the parser. Parameters: version \u2013 A string, the version number. changeset \u2013 A string, a version control string identifying the commit of the version. timestamp \u2013 An integer, the UNIX epoch timestamp of the changeset. Returns: A human-readable string for the version. Source code in beancount/utils/version.py def compute_version_string(version, changeset, timestamp): \"\"\"Compute a version string from the changeset and timestamp baked in the parser. Args: version: A string, the version number. changeset: A string, a version control string identifying the commit of the version. timestamp: An integer, the UNIX epoch timestamp of the changeset. Returns: A human-readable string for the version. \"\"\" # Shorten changeset. if changeset: if re.match('hg:', changeset): changeset = changeset[:15] elif re.match('git:', changeset): changeset = changeset[:12] # Convert timestamp to a date. date = None if timestamp > 0: date = datetime.datetime.utcfromtimestamp(timestamp).date() version = 'Beancount {}'.format(version) if changeset or date: version = '{} ({})'.format( version, '; '.join(map(str, filter(None, [changeset, date])))) return version","title":"beancount.utils"},{"location":"api_reference/beancount.utils.html#beancountutils","text":"Generic utility packages and functions.","title":"beancount.utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.bisect_key","text":"A version of bisect that accepts a custom key function, like the sorting ones do.","title":"bisect_key"},{"location":"api_reference/beancount.utils.html#beancount.utils.bisect_key.bisect_left_with_key","text":"Find the last element before the given value in a sorted list. Parameters: sequence \u2013 A sorted sequence of elements. value \u2013 The value to search for. key \u2013 An optional function used to extract the value from the elements of sequence. Returns: Return the index. May return None. Source code in beancount/utils/bisect_key.py def bisect_left_with_key(sequence, value, key=None): \"\"\"Find the last element before the given value in a sorted list. Args: sequence: A sorted sequence of elements. value: The value to search for. key: An optional function used to extract the value from the elements of sequence. Returns: Return the index. May return None. \"\"\" # pylint: disable=invalid-name if key is None: key = lambda x: x # Identity. lo = 0 hi = len(sequence) while lo < hi: mid = (lo + hi) // 2 if key(sequence[mid]) < value: lo = mid + 1 else: hi = mid return lo","title":"bisect_left_with_key()"},{"location":"api_reference/beancount.utils.html#beancount.utils.bisect_key.bisect_right_with_key","text":"Like bisect.bisect_right, but with a key lookup parameter. Parameters: a \u2013 The list to search in. x \u2013 The element to search for. key \u2013 A function, to extract the value from the list. lo \u2013 The smallest index to search. hi \u2013 The largest index to search. Returns: As in bisect.bisect_right, an element from list 'a'. Source code in beancount/utils/bisect_key.py def bisect_right_with_key(a, x, key, lo=0, hi=None): \"\"\"Like bisect.bisect_right, but with a key lookup parameter. Args: a: The list to search in. x: The element to search for. key: A function, to extract the value from the list. lo: The smallest index to search. hi: The largest index to search. Returns: As in bisect.bisect_right, an element from list 'a'. \"\"\" # pylint: disable=invalid-name if lo < 0: raise ValueError('lo must be non-negative') if hi is None: hi = len(a) while lo < hi: mid = (lo+hi)//2 if x < key(a[mid]): hi = mid else: lo = mid+1 return lo","title":"bisect_right_with_key()"},{"location":"api_reference/beancount.utils.html#beancount.utils.csv_utils","text":"Utilities for reading and writing CSV files.","title":"csv_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.csv_utils.as_rows","text":"Split a string as rows of a CSV file. Parameters: string \u2013 A string to be split, the contents of a CSV file. Returns: Lists of lists of strings. Source code in beancount/utils/csv_utils.py def as_rows(string): \"\"\"Split a string as rows of a CSV file. Args: string: A string to be split, the contents of a CSV file. Returns: Lists of lists of strings. \"\"\" return list(csv.reader(io.StringIO(textwrap.dedent(string))))","title":"as_rows()"},{"location":"api_reference/beancount.utils.html#beancount.utils.csv_utils.csv_clean_header","text":"Create a new class for the following rows from the header line. This both normalizes the header line and assign Parameters: header_row \u2013 A list of strings, the row with header titles. Returns: A list of strings, with possibly modified (cleaned) row titles, of the same lengths. Source code in beancount/utils/csv_utils.py def csv_clean_header(header_row): \"\"\"Create a new class for the following rows from the header line. This both normalizes the header line and assign Args: header_row: A list of strings, the row with header titles. Returns: A list of strings, with possibly modified (cleaned) row titles, of the same lengths. \"\"\" fieldnames = [] for index, column in enumerate(header_row): field = column.lower() field = re.sub(r'\\bp/l\\b', 'pnl', field) field = re.sub('[^a-z0-9]', '_', field) field = field.strip(' _') field = re.sub('__+', '_', field) if not field: field = 'col{}'.format(index) assert field not in fieldnames, field fieldnames.append(field) return fieldnames","title":"csv_clean_header()"},{"location":"api_reference/beancount.utils.html#beancount.utils.csv_utils.csv_dict_reader","text":"Read a CSV file yielding normalized dictionary fields. This is basically an alternative constructor for csv.DictReader that normalized the field names. Parameters: fileobj \u2013 A file object to be read. **kw \u2013 Optional arguments forwarded to csv.DictReader. Returns: A csv.DictReader object. Source code in beancount/utils/csv_utils.py def csv_dict_reader(fileobj, **kw): \"\"\"Read a CSV file yielding normalized dictionary fields. This is basically an alternative constructor for csv.DictReader that normalized the field names. Args: fileobj: A file object to be read. **kw: Optional arguments forwarded to csv.DictReader. Returns: A csv.DictReader object. \"\"\" reader = csv.DictReader(fileobj, **kw) reader.fieldnames = csv_clean_header(reader.fieldnames) return reader","title":"csv_dict_reader()"},{"location":"api_reference/beancount.utils.html#beancount.utils.csv_utils.csv_split_sections","text":"Given rows, split them in at empty lines. This is useful for structured CSV files with multiple sections. Parameters: rows \u2013 A list of rows, which are themselves lists of strings. Returns: A list of sections, which are lists of rows, which are lists of strings. Source code in beancount/utils/csv_utils.py def csv_split_sections(rows): \"\"\"Given rows, split them in at empty lines. This is useful for structured CSV files with multiple sections. Args: rows: A list of rows, which are themselves lists of strings. Returns: A list of sections, which are lists of rows, which are lists of strings. \"\"\" sections = [] current_section = [] for row in rows: if row: current_section.append(row) else: sections.append(current_section) current_section = [] if current_section: sections.append(current_section) return sections","title":"csv_split_sections()"},{"location":"api_reference/beancount.utils.html#beancount.utils.csv_utils.csv_split_sections_with_titles","text":"Given a list of rows, split their sections. If the sections have single column titles, consume those lines as their names and return a mapping of section names. This is useful for CSV files with multiple sections, where the separator is a title. We use this to separate the multiple tables within the CSV files. Parameters: rows \u2013 A list of rows (list-of-strings). Returns: A list of lists of rows (list-of-strings). Source code in beancount/utils/csv_utils.py def csv_split_sections_with_titles(rows): \"\"\"Given a list of rows, split their sections. If the sections have single column titles, consume those lines as their names and return a mapping of section names. This is useful for CSV files with multiple sections, where the separator is a title. We use this to separate the multiple tables within the CSV files. Args: rows: A list of rows (list-of-strings). Returns: A list of lists of rows (list-of-strings). \"\"\" sections_map = {} for index, section in enumerate(csv_split_sections(rows)): # Skip too short sections, cannot possibly be a title. if len(section) < 2: continue if len(section[0]) == 1 and len(section[1]) != 1: name = section[0][0] section = section[1:] else: name = 'Section {}'.format(index) sections_map[name] = section return sections_map","title":"csv_split_sections_with_titles()"},{"location":"api_reference/beancount.utils.html#beancount.utils.csv_utils.csv_tuple_reader","text":"Read a CSV file yielding namedtuple instances. The CSV file must have a header line. Parameters: fileobj \u2013 A file object to be read. **kw \u2013 Optional arguments forwarded to csv.DictReader. Yields: Nametuple instances, one for each row. Source code in beancount/utils/csv_utils.py def csv_tuple_reader(fileobj, **kw): \"\"\"Read a CSV file yielding namedtuple instances. The CSV file must have a header line. Args: fileobj: A file object to be read. **kw: Optional arguments forwarded to csv.DictReader. Yields: Nametuple instances, one for each row. \"\"\" reader = csv.reader(fileobj, **kw) ireader = iter(reader) fieldnames = csv_clean_header(next(ireader)) Tuple = collections.namedtuple('Row', fieldnames) for row in ireader: try: yield Tuple(*row) except TypeError: # If there's an error, it's usually from a line that has a 'END OF # LINE' marker at the end, or some comment line. assert len(row) in (0, 1)","title":"csv_tuple_reader()"},{"location":"api_reference/beancount.utils.html#beancount.utils.csv_utils.iter_sections","text":"For a given file object, yield file-like objects for each of the sections contained therein. A section is defined as a list of lines that don't match the predicate. For example, if you want to split by empty lines, provide a predicate that will be true given an empty line, which will cause a new section to be begun. Parameters: fileobj \u2013 A file object to read from. separating_predicate \u2013 A boolean predicate that is true on separating lines. Yields: A list of lines that you can use to iterate. Source code in beancount/utils/csv_utils.py def iter_sections(fileobj, separating_predicate=None): \"\"\"For a given file object, yield file-like objects for each of the sections contained therein. A section is defined as a list of lines that don't match the predicate. For example, if you want to split by empty lines, provide a predicate that will be true given an empty line, which will cause a new section to be begun. Args: fileobj: A file object to read from. separating_predicate: A boolean predicate that is true on separating lines. Yields: A list of lines that you can use to iterate. \"\"\" if separating_predicate is None: separating_predicate = lambda line: bool(line.strip()) lineiter = iter(fileobj) for line in lineiter: if separating_predicate(line): iterator = itertools.chain((line,), iter_until_empty(lineiter, separating_predicate)) yield iterator for _ in iterator: pass","title":"iter_sections()"},{"location":"api_reference/beancount.utils.html#beancount.utils.csv_utils.iter_until_empty","text":"An iterator of lines that will stop at the first empty line. Parameters: iterator \u2013 An iterator of lines. separating_predicate \u2013 A boolean predicate that is true on separating lines. Yields: Non-empty lines. EOF when we hit an empty line. Source code in beancount/utils/csv_utils.py def iter_until_empty(iterator, separating_predicate=None): \"\"\"An iterator of lines that will stop at the first empty line. Args: iterator: An iterator of lines. separating_predicate: A boolean predicate that is true on separating lines. Yields: Non-empty lines. EOF when we hit an empty line. \"\"\" if separating_predicate is None: separating_predicate = lambda line: bool(line.strip()) for line in iterator: if not separating_predicate(line): break yield line","title":"iter_until_empty()"},{"location":"api_reference/beancount.utils.html#beancount.utils.date_utils","text":"Parse the date from various formats.","title":"date_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.date_utils.intimezone","text":"Temporarily reset the value of TZ. This is used for testing. Parameters: tz_value ( str ) \u2013 The value of TZ to set for the duration of this context. Returns: A contextmanager in the given timezone locale. Source code in beancount/utils/date_utils.py @contextlib.contextmanager def intimezone(tz_value: str): \"\"\"Temporarily reset the value of TZ. This is used for testing. Args: tz_value: The value of TZ to set for the duration of this context. Returns: A contextmanager in the given timezone locale. \"\"\" tz_old = os.environ.get('TZ', None) os.environ['TZ'] = tz_value time.tzset() try: yield finally: if tz_old is None: del os.environ['TZ'] else: os.environ['TZ'] = tz_old time.tzset()","title":"intimezone()"},{"location":"api_reference/beancount.utils.html#beancount.utils.date_utils.iter_dates","text":"Yield all the dates between 'start_date' and 'end_date'. Parameters: start_date \u2013 An instance of datetime.date. end_date \u2013 An instance of datetime.date. Yields: Instances of datetime.date. Source code in beancount/utils/date_utils.py def iter_dates(start_date, end_date): \"\"\"Yield all the dates between 'start_date' and 'end_date'. Args: start_date: An instance of datetime.date. end_date: An instance of datetime.date. Yields: Instances of datetime.date. \"\"\" oneday = datetime.timedelta(days=1) date = start_date while date < end_date: yield date date += oneday","title":"iter_dates()"},{"location":"api_reference/beancount.utils.html#beancount.utils.date_utils.next_month","text":"Compute the date at the beginning of the following month from the given date. Parameters: date \u2013 A datetime.date instance. Returns: A datetime.date instance, the first day of the month following 'date'. Source code in beancount/utils/date_utils.py def next_month(date): \"\"\"Compute the date at the beginning of the following month from the given date. Args: date: A datetime.date instance. Returns: A datetime.date instance, the first day of the month following 'date'. \"\"\" # Compute the date at the beginning of the following month. year = date.year month = date.month + 1 if date.month == 12: year += 1 month = 1 return datetime.date(year, month, 1)","title":"next_month()"},{"location":"api_reference/beancount.utils.html#beancount.utils.date_utils.parse_date_liberally","text":"Parse arbitrary strings to dates. This function is intended to support liberal inputs, so that we can use it in accepting user-specified dates on command-line scripts. Parameters: string \u2013 A string to parse. parse_kwargs_dict \u2013 Dict of kwargs to pass to dateutil parser. Returns: A datetime.date object. Source code in beancount/utils/date_utils.py def parse_date_liberally(string, parse_kwargs_dict=None): \"\"\"Parse arbitrary strings to dates. This function is intended to support liberal inputs, so that we can use it in accepting user-specified dates on command-line scripts. Args: string: A string to parse. parse_kwargs_dict: Dict of kwargs to pass to dateutil parser. Returns: A datetime.date object. \"\"\" # At the moment, rely on the most excellent dateutil. if parse_kwargs_dict is None: parse_kwargs_dict = {} return dateutil.parser.parse(string, **parse_kwargs_dict).date()","title":"parse_date_liberally()"},{"location":"api_reference/beancount.utils.html#beancount.utils.date_utils.render_ofx_date","text":"Render a datetime to the OFX format. Parameters: dtime \u2013 A datetime.datetime instance. Returns: A string, rendered to milliseconds. Source code in beancount/utils/date_utils.py def render_ofx_date(dtime): \"\"\"Render a datetime to the OFX format. Args: dtime: A datetime.datetime instance. Returns: A string, rendered to milliseconds. \"\"\" return '{}.{:03d}'.format(dtime.strftime('%Y%m%d%H%M%S'), int(dtime.microsecond / 1000))","title":"render_ofx_date()"},{"location":"api_reference/beancount.utils.html#beancount.utils.defdict","text":"An instance of collections.defaultdict whose factory accepts a key. Note: This really ought to be an enhancement to Python itself. I should bother adding this in eventually.","title":"defdict"},{"location":"api_reference/beancount.utils.html#beancount.utils.defdict.DefaultDictWithKey","text":"A version of defaultdict whose factory accepts the key as an argument. Note: collections.defaultdict would be improved by supporting this directly, this is a common occurrence.","title":"DefaultDictWithKey"},{"location":"api_reference/beancount.utils.html#beancount.utils.defdict.ImmutableDictWithDefault","text":"An immutable dict which returns a default value for missing keys. This differs from a defaultdict in that it does not insert a missing default value when one is materialized (from a missing fetch), and furthermore, the set method is make unavailable to prevent mutation beyond construction.","title":"ImmutableDictWithDefault"},{"location":"api_reference/beancount.utils.html#beancount.utils.defdict.ImmutableDictWithDefault.__setitem__","text":"Disallow mutating the dict in the usual way. Source code in beancount/utils/defdict.py def __setitem__(self, key, value): \"\"\"Disallow mutating the dict in the usual way.\"\"\" raise NotImplementedError","title":"__setitem__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.defdict.ImmutableDictWithDefault.get","text":"Return the value for key if key is in the dictionary, else default. Source code in beancount/utils/defdict.py def get(self, key, _=None): return self.__getitem__(key)","title":"get()"},{"location":"api_reference/beancount.utils.html#beancount.utils.encryption","text":"Support for encrypted tests.","title":"encryption"},{"location":"api_reference/beancount.utils.html#beancount.utils.encryption.is_encrypted_file","text":"Return true if the given filename contains an encrypted file. Parameters: filename \u2013 A path string. Returns: A boolean, true if the file contains an encrypted file. Source code in beancount/utils/encryption.py def is_encrypted_file(filename): \"\"\"Return true if the given filename contains an encrypted file. Args: filename: A path string. Returns: A boolean, true if the file contains an encrypted file. \"\"\" _, ext = path.splitext(filename) if ext == '.gpg': return True if ext == '.asc': with open(filename) as encfile: head = encfile.read(1024) if re.search('--BEGIN PGP MESSAGE--', head): return True return False","title":"is_encrypted_file()"},{"location":"api_reference/beancount.utils.html#beancount.utils.encryption.is_gpg_installed","text":"Return true if GPG 1.4.x or 2.x are installed, which is what we use and support. Source code in beancount/utils/encryption.py def is_gpg_installed(): \"\"\"Return true if GPG 1.4.x or 2.x are installed, which is what we use and support.\"\"\" try: pipe = subprocess.Popen(['gpg', '--version'], shell=0, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = pipe.communicate() version_text = out.decode('utf8') return pipe.returncode == 0 and re.match(r'gpg \\(GnuPG\\) (1\\.4|2)\\.', version_text) except OSError: return False","title":"is_gpg_installed()"},{"location":"api_reference/beancount.utils.html#beancount.utils.encryption.read_encrypted_file","text":"Decrypt and read an encrypted file without temporary storage. Parameters: filename \u2013 A string, the path to the encrypted file. Returns: A string, the contents of the file. Exceptions: OSError \u2013 If we could not properly decrypt the file. Source code in beancount/utils/encryption.py def read_encrypted_file(filename): \"\"\"Decrypt and read an encrypted file without temporary storage. Args: filename: A string, the path to the encrypted file. Returns: A string, the contents of the file. Raises: OSError: If we could not properly decrypt the file. \"\"\" command = ['gpg', '--batch', '--decrypt', path.realpath(filename)] pipe = subprocess.Popen(command, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) contents, errors = pipe.communicate() if pipe.returncode != 0: raise OSError(\"Could not decrypt file ({}): {}\".format(pipe.returncode, errors.decode('utf8'))) return contents.decode('utf-8')","title":"read_encrypted_file()"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_type","text":"Code that can guess a MIME type for a filename. This attempts to identify the mime-type of a file suing The built-in mimetypes library, then python-magic (if available), and finally some custom mappers for datatypes used in financial downloads, such as Quicken files.","title":"file_type"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_type.guess_file_type","text":"Attempt to guess the type of the input file. Parameters: filename \u2013 A string, the name of the file to guess the type for. Returns: A suitable mimetype string, or None if we could not guess. Source code in beancount/utils/file_type.py def guess_file_type(filename): \"\"\"Attempt to guess the type of the input file. Args: filename: A string, the name of the file to guess the type for. Returns: A suitable mimetype string, or None if we could not guess. \"\"\" # Try the standard mimetypes association. filetype, _ = mimetypes.guess_type(filename, False) if filetype: return filetype # Try out some extra types that only we know about. for regexp, mimetype in EXTRA_FILE_TYPES: if regexp.match(filename): return mimetype # Try out libmagic, if it is installed. if magic: filetype = magic.from_file(filename, mime=True) if isinstance(filetype, bytes): filetype = filetype.decode('utf8') return filetype else: raise ValueError((\"Could not identify the type of file '{}'; \" \"try installing python-magic\").format(filename))","title":"guess_file_type()"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_utils","text":"File utilities.","title":"file_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_utils.chdir","text":"Temporarily chdir to the given directory. Parameters: directory \u2013 The directory to switch do. Returns: A context manager which restores the cwd after running. Source code in beancount/utils/file_utils.py @contextlib.contextmanager def chdir(directory): \"\"\"Temporarily chdir to the given directory. Args: directory: The directory to switch do. Returns: A context manager which restores the cwd after running. \"\"\" cwd = os.getcwd() os.chdir(directory) try: yield cwd finally: os.chdir(cwd)","title":"chdir()"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_utils.find_files","text":"Enumerate the files under the given directories, stably. Invalid file or directory names will be logged to the error log. Parameters: fords \u2013 A list of strings, file or directory names. ignore_dirs \u2013 A list of strings, filenames or directories to be ignored. Yields: Strings, full filenames from the given roots. Source code in beancount/utils/file_utils.py def find_files(fords, ignore_dirs=('.hg', '.svn', '.git'), ignore_files=('.DS_Store',)): \"\"\"Enumerate the files under the given directories, stably. Invalid file or directory names will be logged to the error log. Args: fords: A list of strings, file or directory names. ignore_dirs: A list of strings, filenames or directories to be ignored. Yields: Strings, full filenames from the given roots. \"\"\" if isinstance(fords, str): fords = [fords] assert isinstance(fords, (list, tuple)) for ford in fords: if path.isdir(ford): for root, dirs, filenames in os.walk(ford): dirs[:] = sorted(dirname for dirname in dirs if dirname not in ignore_dirs) for filename in sorted(filenames): if filename in ignore_files: continue yield path.join(root, filename) elif path.isfile(ford) or path.islink(ford): yield ford elif not path.exists(ford): logging.error(\"File or directory '{}' does not exist.\".format(ford))","title":"find_files()"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_utils.guess_file_format","text":"Guess the file format from the filename. Parameters: filename \u2013 A string, the name of the file. This can be None. Returns: A string, the extension of the format, without a leading period. Source code in beancount/utils/file_utils.py def guess_file_format(filename, default=None): \"\"\"Guess the file format from the filename. Args: filename: A string, the name of the file. This can be None. Returns: A string, the extension of the format, without a leading period. \"\"\" if filename: if filename.endswith('.txt') or filename.endswith('.text'): format = 'text' elif filename.endswith('.csv'): format = 'csv' elif filename.endswith('.html') or filename.endswith('.xhtml'): format = 'html' else: format = default else: format = default return format","title":"guess_file_format()"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_utils.path_greedy_split","text":"Split a path, returning the longest possible extension. Parameters: filename \u2013 A string, the filename to split. Returns: A pair of basename, extension (which includes the leading period). Source code in beancount/utils/file_utils.py def path_greedy_split(filename): \"\"\"Split a path, returning the longest possible extension. Args: filename: A string, the filename to split. Returns: A pair of basename, extension (which includes the leading period). \"\"\" basename = path.basename(filename) index = basename.find('.') if index == -1: extension = None else: extension = basename[index:] basename = basename[:index] return (path.join(path.dirname(filename), basename), extension)","title":"path_greedy_split()"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_utils.touch_file","text":"Touch a file and wait until its timestamp has been changed. Parameters: filename \u2013 A string path, the name of the file to touch. otherfiles \u2013 A list of other files to ensure the timestamp is beyond of. Source code in beancount/utils/file_utils.py def touch_file(filename, *otherfiles): \"\"\"Touch a file and wait until its timestamp has been changed. Args: filename: A string path, the name of the file to touch. otherfiles: A list of other files to ensure the timestamp is beyond of. \"\"\" # Note: You could set os.stat_float_times() but then the main function would # have to set that up as well. It doesn't help so much, however, since # filesystems tend to have low resolutions, e.g. one second. orig_mtime_ns = max(os.stat(minfile).st_mtime_ns for minfile in (filename,) + otherfiles) delay_secs = 0.05 while True: with open(filename, 'a'): os.utime(filename) time.sleep(delay_secs) new_stat = os.stat(filename) if new_stat.st_mtime_ns > orig_mtime_ns: break","title":"touch_file()"},{"location":"api_reference/beancount.utils.html#beancount.utils.import_utils","text":"Utilities for importing symbols programmatically.","title":"import_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.import_utils.import_symbol","text":"Import a symbol in an arbitrary module. Parameters: dotted_name \u2013 A dotted path to a symbol. Returns: The object referenced by the given name. Exceptions: ImportError \u2013 If the module not not be imported. AttributeError \u2013 If the symbol could not be found in the module. Source code in beancount/utils/import_utils.py def import_symbol(dotted_name): \"\"\"Import a symbol in an arbitrary module. Args: dotted_name: A dotted path to a symbol. Returns: The object referenced by the given name. Raises: ImportError: If the module not not be imported. AttributeError: If the symbol could not be found in the module. \"\"\" comps = dotted_name.split('.') module_name = '.'.join(comps[:-1]) symbol_name = comps[-1] module = importlib.import_module(module_name) return getattr(module, symbol_name)","title":"import_symbol()"},{"location":"api_reference/beancount.utils.html#beancount.utils.invariants","text":"Functions to register auxiliary functions on a class' methods to check for invariants. This is intended to be used in a test, whereby your test will setup a class to automatically run invariant verification functions before and after each function call, to ensure some extra sanity checks that wouldn't be used in non-tests. Example: Instrument the Inventory class with the check_inventory_invariants() function. def setUp(module): instrument_invariants(Inventory, check_inventory_invariants, check_inventory_invariants) def tearDown(module): uninstrument_invariants(Inventory)","title":"invariants"},{"location":"api_reference/beancount.utils.html#beancount.utils.invariants.instrument_invariants","text":"Instrument the class 'klass' with pre/post invariant checker functions. Parameters: klass \u2013 A class object, whose methods to be instrumented. prefun \u2013 A function that checks invariants pre-call. postfun \u2013 A function that checks invariants pre-call. Source code in beancount/utils/invariants.py def instrument_invariants(klass, prefun, postfun): \"\"\"Instrument the class 'klass' with pre/post invariant checker functions. Args: klass: A class object, whose methods to be instrumented. prefun: A function that checks invariants pre-call. postfun: A function that checks invariants pre-call. \"\"\" instrumented = {} for attrname, object_ in klass.__dict__.items(): if attrname.startswith('_'): continue if not isinstance(object_, types.FunctionType): continue instrumented[attrname] = object_ setattr(klass, attrname, invariant_check(object_, prefun, postfun)) klass.__instrumented = instrumented","title":"instrument_invariants()"},{"location":"api_reference/beancount.utils.html#beancount.utils.invariants.invariant_check","text":"Decorate a method with the pre/post invariant checkers. Parameters: method \u2013 An unbound method to instrument. prefun \u2013 A function that checks invariants pre-call. postfun \u2013 A function that checks invariants post-call. Returns: An unbound method, decorated. Source code in beancount/utils/invariants.py def invariant_check(method, prefun, postfun): \"\"\"Decorate a method with the pre/post invariant checkers. Args: method: An unbound method to instrument. prefun: A function that checks invariants pre-call. postfun: A function that checks invariants post-call. Returns: An unbound method, decorated. \"\"\" reentrant = [] def new_method(self, *args, **kw): reentrant.append(None) if len(reentrant) == 1: prefun(self) result = method(self, *args, **kw) if len(reentrant) == 1: postfun(self) reentrant.pop() return result return new_method","title":"invariant_check()"},{"location":"api_reference/beancount.utils.html#beancount.utils.invariants.uninstrument_invariants","text":"Undo the instrumentation for invariants. Parameters: klass \u2013 A class object, whose methods to be uninstrumented. Source code in beancount/utils/invariants.py def uninstrument_invariants(klass): \"\"\"Undo the instrumentation for invariants. Args: klass: A class object, whose methods to be uninstrumented. \"\"\" instrumented = getattr(klass, '__instrumented', None) if instrumented: for attrname, object_ in instrumented.items(): setattr(klass, attrname, object_) del klass.__instrumented","title":"uninstrument_invariants()"},{"location":"api_reference/beancount.utils.html#beancount.utils.memo","text":"Memoization utilities.","title":"memo"},{"location":"api_reference/beancount.utils.html#beancount.utils.memo.memoize_recent_fileobj","text":"Memoize recent calls to the given function which returns a file object. The results of the cache expire after some time. Parameters: function \u2013 A callable object. cache_filename \u2013 A string, the path to the database file to cache to. expiration \u2013 The time during which the results will be kept valid. Use 'None' to never expire the cache (this is the default). Returns: A memoized version of the function. Source code in beancount/utils/memo.py def memoize_recent_fileobj(function, cache_filename, expiration=None): \"\"\"Memoize recent calls to the given function which returns a file object. The results of the cache expire after some time. Args: function: A callable object. cache_filename: A string, the path to the database file to cache to. expiration: The time during which the results will be kept valid. Use 'None' to never expire the cache (this is the default). Returns: A memoized version of the function. \"\"\" urlcache = shelve.open(cache_filename, 'c') urlcache.lock = threading.Lock() # Note: 'shelve' is not thread-safe. @functools.wraps(function) def memoized(*args, **kw): # Encode the arguments, including a date string in order to invalidate # results over some time. md5 = hashlib.md5() md5.update(str(args).encode('utf-8')) md5.update(str(sorted(kw.items())).encode('utf-8')) hash_ = md5.hexdigest() time_now = now() try: with urlcache.lock: time_orig, contents = urlcache[hash_] if expiration is not None and (time_now - time_orig) > expiration: raise KeyError except KeyError: fileobj = function(*args, **kw) if fileobj: contents = fileobj.read() with urlcache.lock: urlcache[hash_] = (time_now, contents) else: contents = None return io.BytesIO(contents) if contents else None return memoized","title":"memoize_recent_fileobj()"},{"location":"api_reference/beancount.utils.html#beancount.utils.memo.now","text":"Indirection on datetime.datetime.now() for testing. Source code in beancount/utils/memo.py def now(): \"Indirection on datetime.datetime.now() for testing.\" return datetime.datetime.now()","title":"now()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils","text":"Generic utility packages and functions.","title":"misc_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.LineFileProxy","text":"A file object that will delegate writing full lines to another logging function. This may be used for writing data to a logging level without having to worry about lines.","title":"LineFileProxy"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.LineFileProxy.__init__","text":"Construct a new line delegator file object proxy. Parameters: line_writer \u2013 A callable function, used to write to the delegated output. prefix \u2013 An optional string, the prefix to insert before every line. write_newlines \u2013 A boolean, true if we should output the newline characters. Source code in beancount/utils/misc_utils.py def __init__(self, line_writer, prefix=None, write_newlines=False): \"\"\"Construct a new line delegator file object proxy. Args: line_writer: A callable function, used to write to the delegated output. prefix: An optional string, the prefix to insert before every line. write_newlines: A boolean, true if we should output the newline characters. \"\"\" self.line_writer = line_writer self.prefix = prefix self.write_newlines = write_newlines self.data = []","title":"__init__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.LineFileProxy.close","text":"Close the line delegator. Source code in beancount/utils/misc_utils.py def close(self): \"\"\"Close the line delegator.\"\"\" self.flush()","title":"close()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.LineFileProxy.flush","text":"Flush the data to the line writer. Source code in beancount/utils/misc_utils.py def flush(self): \"\"\"Flush the data to the line writer.\"\"\" data = ''.join(self.data) if data: lines = data.splitlines() self.data = [lines.pop(-1)] if data[-1] != '\\n' else [] for line in lines: if self.prefix: line = self.prefix + line if self.write_newlines: line += '\\n' self.line_writer(line)","title":"flush()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.LineFileProxy.write","text":"Write some string data to the output. Parameters: data \u2013 A string, with or without newlines. Source code in beancount/utils/misc_utils.py def write(self, data): \"\"\"Write some string data to the output. Args: data: A string, with or without newlines. \"\"\" if '\\n' in data: self.data.append(data) self.flush() else: self.data.append(data)","title":"write()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.TypeComparable","text":"A base class whose equality comparison includes comparing the type of the instance itself.","title":"TypeComparable"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.box","text":"A context manager that prints out a box around a block. This is useful for printing out stuff from tests in a way that is readable. Parameters: name \u2013 A string, the name of the box to use. file \u2013 The file object to print to. Yields: None. Source code in beancount/utils/misc_utils.py @contextlib.contextmanager def box(name=None, file=None): \"\"\"A context manager that prints out a box around a block. This is useful for printing out stuff from tests in a way that is readable. Args: name: A string, the name of the box to use. file: The file object to print to. Yields: None. \"\"\" file = file or sys.stdout file.write('\\n') if name: header = ',--------({})--------\\n'.format(name) footer = '`{}\\n'.format('-' * (len(header)-2)) else: header = ',----------------\\n' footer = '`----------------\\n' file.write(header) yield file.write(footer) file.flush()","title":"box()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.cmptuple","text":"Manufacture a comparable namedtuple class, similar to collections.namedtuple. A comparable named tuple is a tuple which compares to False if contents are equal but the data types are different. We define this to supplement collections.namedtuple because by default a namedtuple disregards the type and we want to make precise comparisons for tests. Parameters: name \u2013 The given name of the class. attributes \u2013 A string or tuple of strings, with the names of the attributes. Returns: A new namedtuple-derived type that compares False with other tuples with same contents. Source code in beancount/utils/misc_utils.py def cmptuple(name, attributes): \"\"\"Manufacture a comparable namedtuple class, similar to collections.namedtuple. A comparable named tuple is a tuple which compares to False if contents are equal but the data types are different. We define this to supplement collections.namedtuple because by default a namedtuple disregards the type and we want to make precise comparisons for tests. Args: name: The given name of the class. attributes: A string or tuple of strings, with the names of the attributes. Returns: A new namedtuple-derived type that compares False with other tuples with same contents. \"\"\" base = collections.namedtuple('_{}'.format(name), attributes) return type(name, (TypeComparable, base,), {})","title":"cmptuple()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.compute_unique_clean_ids","text":"Given a sequence of strings, reduce them to corresponding ids without any funny characters and insure that the list of ids is unique. Yields pairs of (id, string) for the result. Parameters: strings \u2013 A list of strings. Returns: A list of (id, string) pairs. Source code in beancount/utils/misc_utils.py def compute_unique_clean_ids(strings): \"\"\"Given a sequence of strings, reduce them to corresponding ids without any funny characters and insure that the list of ids is unique. Yields pairs of (id, string) for the result. Args: strings: A list of strings. Returns: A list of (id, string) pairs. \"\"\" string_set = set(strings) # Try multiple methods until we get one that has no collisions. for regexp, replacement in [(r'[^A-Za-z0-9.-]', '_'), (r'[^A-Za-z0-9_]', ''),]: seen = set() idmap = {} mre = re.compile(regexp) for string in string_set: id_ = mre.sub(replacement, string) if id_ in seen: break # Collision. seen.add(id_) idmap[id_] = string else: break else: return None # Could not find a unique mapping. return idmap","title":"compute_unique_clean_ids()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.deprecated","text":"A decorator generator to mark functions as deprecated and log a warning. Source code in beancount/utils/misc_utils.py def deprecated(message): \"\"\"A decorator generator to mark functions as deprecated and log a warning.\"\"\" def decorator(func): @functools.wraps(func) def new_func(*args, **kwargs): warnings.warn(\"Call to deprecated function {}: {}\".format(func.__name__, message), category=DeprecationWarning, stacklevel=2) return func(*args, **kwargs) return new_func return decorator","title":"deprecated()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.dictmap","text":"Map a dictionary's value. Parameters: mdict \u2013 A dict. key \u2013 A callable to apply to the keys. value \u2013 A callable to apply to the values. Source code in beancount/utils/misc_utils.py def dictmap(mdict, keyfun=None, valfun=None): \"\"\"Map a dictionary's value. Args: mdict: A dict. key: A callable to apply to the keys. value: A callable to apply to the values. \"\"\" if keyfun is None: keyfun = lambda x: x if valfun is None: valfun = lambda x: x return {keyfun(key): valfun(val) for key, val in mdict.items()}","title":"dictmap()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.escape_string","text":"Escape quotes and backslashes in payee and narration. Parameters: string \u2013 Any string. Returns. The input string, with offending characters replaced. Source code in beancount/utils/misc_utils.py def escape_string(string): \"\"\"Escape quotes and backslashes in payee and narration. Args: string: Any string. Returns. The input string, with offending characters replaced. \"\"\" return string.replace('\\\\', r'\\\\')\\ .replace('\"', r'\\\"')","title":"escape_string()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.filter_type","text":"Filter the given list to yield only instances of the given types. Parameters: elist \u2013 A sequence of elements. types \u2013 A sequence of types to include in the output list. Yields: Each element, if it is an instance of 'types'. Source code in beancount/utils/misc_utils.py def filter_type(elist, types): \"\"\"Filter the given list to yield only instances of the given types. Args: elist: A sequence of elements. types: A sequence of types to include in the output list. Yields: Each element, if it is an instance of 'types'. \"\"\" for element in elist: if not isinstance(element, types): continue yield element","title":"filter_type()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.first_paragraph","text":"Return the first sentence of a docstring. The sentence has to be delimited by an empty line. Parameters: docstring \u2013 A doc string. Returns: A string with just the first sentence on a single line. Source code in beancount/utils/misc_utils.py def first_paragraph(docstring): \"\"\"Return the first sentence of a docstring. The sentence has to be delimited by an empty line. Args: docstring: A doc string. Returns: A string with just the first sentence on a single line. \"\"\" lines = [] for line in docstring.strip().splitlines(): if not line: break lines.append(line.rstrip()) return ' '.join(lines)","title":"first_paragraph()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.get_screen_height","text":"Return the height of the terminal that runs this program. Returns: An integer, the number of characters the screen is high. Return 0 if the terminal cannot be initialized. Source code in beancount/utils/misc_utils.py def get_screen_height(): \"\"\"Return the height of the terminal that runs this program. Returns: An integer, the number of characters the screen is high. Return 0 if the terminal cannot be initialized. \"\"\" return _get_screen_value('lines', 0)","title":"get_screen_height()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.get_screen_width","text":"Return the width of the terminal that runs this program. Returns: An integer, the number of characters the screen is wide. Return 0 if the terminal cannot be initialized. Source code in beancount/utils/misc_utils.py def get_screen_width(): \"\"\"Return the width of the terminal that runs this program. Returns: An integer, the number of characters the screen is wide. Return 0 if the terminal cannot be initialized. \"\"\" return _get_screen_value('cols', 0)","title":"get_screen_width()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.get_tuple_values","text":"Return all members referred to by this namedtuple instance that satisfy the given predicate. This function also works recursively on its members which are lists or tuples, and so it can be used for Transaction instances. Parameters: ntuple \u2013 A tuple or namedtuple. predicate \u2013 A predicate function that returns true if an attribute is to be output. memo \u2013 An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. Source code in beancount/utils/misc_utils.py def get_tuple_values(ntuple, predicate, memo=None): \"\"\"Return all members referred to by this namedtuple instance that satisfy the given predicate. This function also works recursively on its members which are lists or tuples, and so it can be used for Transaction instances. Args: ntuple: A tuple or namedtuple. predicate: A predicate function that returns true if an attribute is to be output. memo: An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. \"\"\" if memo is None: memo = set() id_ntuple = id(ntuple) if id_ntuple in memo: return memo.add(id_ntuple) if predicate(ntuple): yield for attribute in ntuple: if predicate(attribute): yield attribute if isinstance(attribute, (list, tuple)): for value in get_tuple_values(attribute, predicate, memo): yield value","title":"get_tuple_values()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.groupby","text":"Group the elements as a dict of lists, where the key is computed using the function 'keyfun'. Parameters: keyfun \u2013 A callable, used to obtain the group key from each element. elements \u2013 An iterable of the elements to group. Returns: A dict of key to list of sequences. Source code in beancount/utils/misc_utils.py def groupby(keyfun, elements): \"\"\"Group the elements as a dict of lists, where the key is computed using the function 'keyfun'. Args: keyfun: A callable, used to obtain the group key from each element. elements: An iterable of the elements to group. Returns: A dict of key to list of sequences. \"\"\" # Note: We could allow a custom aggregation function. Another option is # provide another method to reduce the list values of a dict, but that can # be accomplished using a dict comprehension. grouped = defaultdict(list) for element in elements: grouped[keyfun(element)].append(element) return grouped","title":"groupby()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.idify","text":"Replace characters objectionable for a filename with underscores. Parameters: string \u2013 Any string. Returns: The input string, with offending characters replaced. Source code in beancount/utils/misc_utils.py def idify(string): \"\"\"Replace characters objectionable for a filename with underscores. Args: string: Any string. Returns: The input string, with offending characters replaced. \"\"\" for sfrom, sto in [(r'[ \\(\\)]+', '_'), (r'_*\\._*', '.')]: string = re.sub(sfrom, sto, string) string = string.strip('_') return string","title":"idify()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.import_curses","text":"Try to import the 'curses' module. (This is used here in order to override for tests.) Returns: The curses module, if it was possible to import it. Exceptions: ImportError \u2013 If the module could not be imported. Source code in beancount/utils/misc_utils.py def import_curses(): \"\"\"Try to import the 'curses' module. (This is used here in order to override for tests.) Returns: The curses module, if it was possible to import it. Raises: ImportError: If the module could not be imported. \"\"\" # Note: There's a recipe for getting terminal size on Windows here, without # curses, I should probably implement that at some point: # https://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window # Also, consider just using 'blessings' instead, which provides this across # multiple platforms. # pylint: disable=import-outside-toplevel import curses return curses","title":"import_curses()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.is_sorted","text":"Return true if the sequence is sorted. Parameters: iterable \u2013 An iterable sequence. key \u2013 A function to extract the quantity by which to sort. cmp \u2013 A function that compares two elements of a sequence. Returns: A boolean, true if the sequence is sorted. Source code in beancount/utils/misc_utils.py def is_sorted(iterable, key=lambda x: x, cmp=lambda x, y: x <= y): \"\"\"Return true if the sequence is sorted. Args: iterable: An iterable sequence. key: A function to extract the quantity by which to sort. cmp: A function that compares two elements of a sequence. Returns: A boolean, true if the sequence is sorted. \"\"\" iterator = map(key, iterable) prev = next(iterator) for element in iterator: if not cmp(prev, element): return False prev = element return True","title":"is_sorted()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.log_time","text":"A context manager that times the block and logs it to info level. Parameters: operation_name \u2013 A string, a label for the name of the operation. log_timings \u2013 A function to write log messages to. If left to None, no timings are written (this becomes a no-op). indent \u2013 An integer, the indentation level for the format of the timing line. This is useful if you're logging timing to a hierarchy of operations. Yields: The start time of the operation. Source code in beancount/utils/misc_utils.py @contextlib.contextmanager def log_time(operation_name, log_timings, indent=0): \"\"\"A context manager that times the block and logs it to info level. Args: operation_name: A string, a label for the name of the operation. log_timings: A function to write log messages to. If left to None, no timings are written (this becomes a no-op). indent: An integer, the indentation level for the format of the timing line. This is useful if you're logging timing to a hierarchy of operations. Yields: The start time of the operation. \"\"\" time1 = time() yield time1 time2 = time() if log_timings: log_timings(\"Operation: {:48} Time: {}{:6.0f} ms\".format( \"'{}'\".format(operation_name), ' '*indent, (time2 - time1) * 1000))","title":"log_time()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.longest","text":"Return the longest of the given subsequences. Parameters: seq \u2013 An iterable sequence of lists. Returns: The longest list from the sequence. Source code in beancount/utils/misc_utils.py def longest(seq): \"\"\"Return the longest of the given subsequences. Args: seq: An iterable sequence of lists. Returns: The longest list from the sequence. \"\"\" longest, length = None, -1 for element in seq: len_element = len(element) if len_element > length: longest, length = element, len_element return longest","title":"longest()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.map_namedtuple_attributes","text":"Map the value of the named attributes of object by mapper. Parameters: attributes \u2013 A sequence of string, the attribute names to map. mapper \u2013 A callable that accepts the value of a field and returns the new value. object_ \u2013 Some namedtuple object with attributes on it. Returns: A new instance of the same namedtuple with the named fields mapped by mapper. Source code in beancount/utils/misc_utils.py def map_namedtuple_attributes(attributes, mapper, object_): \"\"\"Map the value of the named attributes of object by mapper. Args: attributes: A sequence of string, the attribute names to map. mapper: A callable that accepts the value of a field and returns the new value. object_: Some namedtuple object with attributes on it. Returns: A new instance of the same namedtuple with the named fields mapped by mapper. \"\"\" return object_._replace(**{attribute: mapper(getattr(object_, attribute)) for attribute in attributes})","title":"map_namedtuple_attributes()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.replace_namedtuple_values","text":"Recurse through all the members of namedtuples and lists, and for members that match the given predicate, run them through the given mapper. Parameters: ntuple \u2013 A namedtuple instance. predicate \u2013 A predicate function that returns true if an attribute is to be output. mapper \u2013 A callable, that will accept a single argument and return its replacement value. memo \u2013 An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. Source code in beancount/utils/misc_utils.py def replace_namedtuple_values(ntuple, predicate, mapper, memo=None): \"\"\"Recurse through all the members of namedtuples and lists, and for members that match the given predicate, run them through the given mapper. Args: ntuple: A namedtuple instance. predicate: A predicate function that returns true if an attribute is to be output. mapper: A callable, that will accept a single argument and return its replacement value. memo: An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. \"\"\" if memo is None: memo = set() id_ntuple = id(ntuple) if id_ntuple in memo: return None memo.add(id_ntuple) # pylint: disable=unidiomatic-typecheck if not (type(ntuple) is not tuple and isinstance(ntuple, tuple)): return ntuple replacements = {} for attribute_name, attribute in zip(ntuple._fields, ntuple): if predicate(attribute): replacements[attribute_name] = mapper(attribute) elif type(attribute) is not tuple and isinstance(attribute, tuple): replacements[attribute_name] = replace_namedtuple_values( attribute, predicate, mapper, memo) elif type(attribute) in (list, tuple): replacements[attribute_name] = [ replace_namedtuple_values(member, predicate, mapper, memo) for member in attribute] return ntuple._replace(**replacements)","title":"replace_namedtuple_values()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.skipiter","text":"Skip some elements from an iterator. Parameters: iterable \u2013 An iterator. num_skip \u2013 The number of elements in the period. Yields: Elements from the iterable, with num_skip elements skipped. For example, skipiter(range(10), 3) yields [0, 3, 6, 9]. Source code in beancount/utils/misc_utils.py def skipiter(iterable, num_skip): \"\"\"Skip some elements from an iterator. Args: iterable: An iterator. num_skip: The number of elements in the period. Yields: Elements from the iterable, with num_skip elements skipped. For example, skipiter(range(10), 3) yields [0, 3, 6, 9]. \"\"\" assert num_skip > 0 sit = iter(iterable) while 1: try: value = next(sit) except StopIteration: return yield value for _ in range(num_skip-1): try: next(sit) except StopIteration: return","title":"skipiter()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.sorted_uniquify","text":"Given a sequence of elements, sort and remove duplicates of the given key. Keep either the first or the last (by key) element of a sequence of key-identical elements. This does not maintain the ordering of the original elements, they are returned sorted (by key) instead. Parameters: iterable \u2013 An iterable sequence. keyfunc \u2013 A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last \u2013 A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. Source code in beancount/utils/misc_utils.py def sorted_uniquify(iterable, keyfunc=None, last=False): \"\"\"Given a sequence of elements, sort and remove duplicates of the given key. Keep either the first or the last (by key) element of a sequence of key-identical elements. This does _not_ maintain the ordering of the original elements, they are returned sorted (by key) instead. Args: iterable: An iterable sequence. keyfunc: A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last: A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. \"\"\" if keyfunc is None: keyfunc = lambda x: x if last: prev_obj = UNSET prev_key = UNSET for obj in sorted(iterable, key=keyfunc): key = keyfunc(obj) if key != prev_key and prev_obj is not UNSET: yield prev_obj prev_obj = obj prev_key = key if prev_obj is not UNSET: yield prev_obj else: prev_key = UNSET for obj in sorted(iterable, key=keyfunc): key = keyfunc(obj) if key != prev_key: yield obj prev_key = key","title":"sorted_uniquify()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.staticvar","text":"Returns a decorator that defines a Python function attribute. This is used to simulate a static function variable in Python. Parameters: varname \u2013 A string, the name of the variable to define. initial_value \u2013 The value to initialize the variable to. Returns: A function decorator. Source code in beancount/utils/misc_utils.py def staticvar(varname, initial_value): \"\"\"Returns a decorator that defines a Python function attribute. This is used to simulate a static function variable in Python. Args: varname: A string, the name of the variable to define. initial_value: The value to initialize the variable to. Returns: A function decorator. \"\"\" def deco(fun): setattr(fun, varname, initial_value) return fun return deco","title":"staticvar()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.swallow","text":"Catch and ignore certain exceptions. Parameters: exception_types \u2013 A tuple of exception classes to ignore. Yields: None. Source code in beancount/utils/misc_utils.py @contextlib.contextmanager def swallow(*exception_types): \"\"\"Catch and ignore certain exceptions. Args: exception_types: A tuple of exception classes to ignore. Yields: None. \"\"\" try: yield except Exception as exc: if not isinstance(exc, exception_types): raise","title":"swallow()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.uniquify","text":"Given a sequence of elements, remove duplicates of the given key. Keep either the first or the last element of a sequence of key-identical elements. Order is maintained as much as possible. This does maintain the ordering of the original elements, they are returned in the same order as the original elements. Parameters: iterable \u2013 An iterable sequence. keyfunc \u2013 A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last \u2013 A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. Source code in beancount/utils/misc_utils.py def uniquify(iterable, keyfunc=None, last=False): \"\"\"Given a sequence of elements, remove duplicates of the given key. Keep either the first or the last element of a sequence of key-identical elements. Order is maintained as much as possible. This does maintain the ordering of the original elements, they are returned in the same order as the original elements. Args: iterable: An iterable sequence. keyfunc: A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last: A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. \"\"\" if keyfunc is None: keyfunc = lambda x: x seen = set() if last: unique_reversed_list = [] for obj in reversed(iterable): key = keyfunc(obj) if key not in seen: seen.add(key) unique_reversed_list.append(obj) yield from reversed(unique_reversed_list) else: for obj in iterable: key = keyfunc(obj) if key not in seen: seen.add(key) yield obj","title":"uniquify()"},{"location":"api_reference/beancount.utils.html#beancount.utils.net_utils","text":"Network utilities.","title":"net_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.net_utils.retrying_urlopen","text":"Open and download the given URL, retrying if it times out. Parameters: url \u2013 A string, the URL to fetch. timeout \u2013 A timeout after which to stop waiting for a response and return an error. max_retry \u2013 The maximum number of times to retry. Returns: The contents of the fetched URL. Source code in beancount/utils/net_utils.py def retrying_urlopen(url, timeout=5, max_retry=5): \"\"\"Open and download the given URL, retrying if it times out. Args: url: A string, the URL to fetch. timeout: A timeout after which to stop waiting for a response and return an error. max_retry: The maximum number of times to retry. Returns: The contents of the fetched URL. \"\"\" for _ in range(max_retry): logging.debug(\"Reading %s\", url) try: response = request.urlopen(url, timeout=timeout) if response: break except error.URLError: return None if response and response.getcode() != 200: return None return response","title":"retrying_urlopen()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager","text":"Code to write output to a pager. This module contains an object accumulates lines up to a minimum and then decides whether to flush them to the original output directly if under the threshold (no pager) or creates a pager and flushes the lines to it if above the threshold and then forwards all future lines to it. The purpose of this object is to pipe output to a pager only if the number of lines to be printed exceeds a minimum number of lines. The contextmanager is intended to be used to pipe output to a pager and wait on the pager to complete before continuing. Simply write to the file object and upon exit we close the file object. This also silences broken pipe errors triggered by the user exiting the sub-process, and recovers from a failing pager command by just using stdout.","title":"pager"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.ConditionalPager","text":"A proxy file for a pager that only creates a pager after a minimum number of lines has been printed to it.","title":"ConditionalPager"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.ConditionalPager.__enter__","text":"Initialize the context manager and return this instance as it. Source code in beancount/utils/pager.py def __enter__(self): \"\"\"Initialize the context manager and return this instance as it.\"\"\" # The file and pipe object we're writing to. This gets set after the # number of accumulated lines reaches the threshold. if self.minlines: self.file = None self.pipe = None else: self.file, self.pipe = create_pager(self.command, self.default_file) # Lines accumulated before the threshold. self.accumulated_data = [] self.accumulated_lines = 0 # Return this object to be used as the context manager itself. return self","title":"__enter__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.ConditionalPager.__exit__","text":"Context manager exit. This flushes the output to our output file. Parameters: type \u2013 Optional exception type, as per context managers. value \u2013 Optional exception value, as per context managers. unused_traceback \u2013 Optional trace. Source code in beancount/utils/pager.py def __exit__(self, type, value, unused_traceback): \"\"\"Context manager exit. This flushes the output to our output file. Args: type: Optional exception type, as per context managers. value: Optional exception value, as per context managers. unused_traceback: Optional trace. \"\"\" try: if self.file: # Flush the output file and close it. self.file.flush() else: # Oops... we never reached the threshold. Flush the accumulated # output to the file. self.flush_accumulated(self.default_file) # Wait for the subprocess (if we have one). if self.pipe: self.file.close() self.pipe.wait() # Absorb broken pipes that may occur on flush or close above. except BrokenPipeError: return True # Absorb broken pipes. if isinstance(value, BrokenPipeError): return True elif value: raise","title":"__exit__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.ConditionalPager.__init__","text":"Create a conditional pager. Parameters: command \u2013 A string, the shell command to run as a pager. minlines \u2013 If set, the number of lines under which you should not bother starting a pager. This avoids kicking off a pager if the screen is high enough to render the contents. If the value is unset, always starts a pager (which is fine behavior too). Source code in beancount/utils/pager.py def __init__(self, command, minlines=None): \"\"\"Create a conditional pager. Args: command: A string, the shell command to run as a pager. minlines: If set, the number of lines under which you should not bother starting a pager. This avoids kicking off a pager if the screen is high enough to render the contents. If the value is unset, always starts a pager (which is fine behavior too). \"\"\" self.command = command self.minlines = minlines self.default_file = (codecs.getwriter(\"utf-8\")(sys.stdout.buffer) if hasattr(sys.stdout, 'buffer') else sys.stdout)","title":"__init__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.ConditionalPager.flush_accumulated","text":"Flush the existing lines to the newly created pager. This also disabled the accumulator. Parameters: file \u2013 A file object to flush the accumulated data to. Source code in beancount/utils/pager.py def flush_accumulated(self, file): \"\"\"Flush the existing lines to the newly created pager. This also disabled the accumulator. Args: file: A file object to flush the accumulated data to. \"\"\" if self.accumulated_data: write = file.write for data in self.accumulated_data: write(data) self.accumulated_data = None self.accumulated_lines = None","title":"flush_accumulated()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.ConditionalPager.write","text":"Write the data out. Overridden from the file object interface. Parameters: data \u2013 A string, data to write to the output. Source code in beancount/utils/pager.py def write(self, data): \"\"\"Write the data out. Overridden from the file object interface. Args: data: A string, data to write to the output. \"\"\" if self.file is None: # Accumulate the new lines. self.accumulated_lines += data.count('\\n') self.accumulated_data.append(data) # If we've reached the threshold, create a file. if self.accumulated_lines > self.minlines: self.file, self.pipe = create_pager(self.command, self.default_file) self.flush_accumulated(self.file) else: # We've already created a pager subprocess... flush the lines to it. self.file.write(data) # try: # except BrokenPipeError: # # Make sure we don't barf on __exit__(). # self.file = self.pipe = None # raise","title":"write()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.create_pager","text":"Try to create and return a pager subprocess. Parameters: command \u2013 A string, the shell command to run as a pager. file \u2013 The file object for the pager write to. This is also used as a default if we failed to create the pager subprocess. Returns: A pair of (file, pipe), a file object and an optional subprocess.Popen instance to wait on. The pipe instance may be set to None if we failed to create a subprocess. Source code in beancount/utils/pager.py def create_pager(command, file): \"\"\"Try to create and return a pager subprocess. Args: command: A string, the shell command to run as a pager. file: The file object for the pager write to. This is also used as a default if we failed to create the pager subprocess. Returns: A pair of (file, pipe), a file object and an optional subprocess.Popen instance to wait on. The pipe instance may be set to None if we failed to create a subprocess. \"\"\" if command is None: command = os.environ.get('PAGER', DEFAULT_PAGER) if not command: command = DEFAULT_PAGER pipe = None # In case of using 'less', make sure the charset is set properly. In theory # you could override this by setting PAGER to \"LESSCHARSET=utf-8 less\" but # this shouldn't affect other programs and is unlikely to cause problems, so # we set it here to make default behavior work for most people (we always # write UTF-8). env = os.environ.copy() env['LESSCHARSET'] = \"utf-8\" try: pipe = subprocess.Popen(command, shell=True, stdin=subprocess.PIPE, stdout=file, env=env) except OSError as exc: logging.error(\"Invalid pager: {}\".format(exc)) else: stdin_wrapper = io.TextIOWrapper(pipe.stdin, 'utf-8') file = stdin_wrapper return file, pipe","title":"create_pager()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.flush_only","text":"A contextmanager around a file object that does not close the file. This is used to return a context manager on a file object but not close it. We flush it instead. This is useful in order to provide an alternative to a pager class as above. Parameters: fileobj \u2013 A file object, to remain open after running the context manager. Yields: A context manager that yields this object. Source code in beancount/utils/pager.py @contextlib.contextmanager def flush_only(fileobj): \"\"\"A contextmanager around a file object that does not close the file. This is used to return a context manager on a file object but not close it. We flush it instead. This is useful in order to provide an alternative to a pager class as above. Args: fileobj: A file object, to remain open after running the context manager. Yields: A context manager that yields this object. \"\"\" try: yield fileobj finally: fileobj.flush()","title":"flush_only()"},{"location":"api_reference/beancount.utils.html#beancount.utils.regexp_utils","text":"Regular expression helpers.","title":"regexp_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.regexp_utils.re_replace_unicode","text":"Substitute Unicode Properties in regular expressions with their corresponding expanded ranges. Parameters: regexp \u2013 A regular expression string. Returns: Return the regular expression with Unicode Properties substituted. Source code in beancount/utils/regexp_utils.py def re_replace_unicode(regexp): \"\"\"Substitute Unicode Properties in regular expressions with their corresponding expanded ranges. Args: regexp: A regular expression string. Returns: Return the regular expression with Unicode Properties substituted. \"\"\" for category, rangestr in UNICODE_RANGES.items(): regexp = regexp.replace(r'\\p{' + category + '}', rangestr) return regexp","title":"re_replace_unicode()"},{"location":"api_reference/beancount.utils.html#beancount.utils.snoop","text":"Text manipulation utilities.","title":"snoop"},{"location":"api_reference/beancount.utils.html#beancount.utils.snoop.Snoop","text":"A snooper callable that just saves the returned values of a function. This is particularly useful for re.match and re.search in conditionals, e.g.:: snoop = Snoop() ... if snoop(re.match(r\"(\\d+)-(\\d+)-(\\d+)\", text)): year, month, date = snoop.value.group(1, 2, 3) Attributes: Name Type Description value The last value snooped from a function call. history If 'maxlen' was specified, the last few values that were snooped.","title":"Snoop"},{"location":"api_reference/beancount.utils.html#beancount.utils.snoop.Snoop.__call__","text":"Save a value to the snooper. This is meant to wrap a function call. Parameters: value \u2013 The value to push/save. Returns: Value itself. Source code in beancount/utils/snoop.py def __call__(self, value): \"\"\"Save a value to the snooper. This is meant to wrap a function call. Args: value: The value to push/save. Returns: Value itself. \"\"\" self.value = value if self.history is not None: self.history.append(value) return value","title":"__call__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.snoop.Snoop.__getattr__","text":"Forward the attribute to the value. Parameters: attr \u2013 A string, the name of the attribute. Returns: The value of the attribute. Source code in beancount/utils/snoop.py def __getattr__(self, attr): \"\"\"Forward the attribute to the value. Args: attr: A string, the name of the attribute. Returns: The value of the attribute. \"\"\" return getattr(self.value, attr)","title":"__getattr__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.snoop.Snoop.__init__","text":"Create a new snooper. Parameters: maxlen \u2013 If specified, an integer, which enables the saving of that Source code in beancount/utils/snoop.py def __init__(self, maxlen=None): \"\"\"Create a new snooper. Args: maxlen: If specified, an integer, which enables the saving of that number of last values in the history attribute. \"\"\" self.value = None self.history = (collections.deque(maxlen=maxlen) if maxlen else None)","title":"__init__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.snoop.snoopify","text":"Decorate a function as snoopable. This is meant to reassign existing functions to a snoopable version of them. For example, if you wanted 're.match' to be automatically snoopable, just decorate it like this: re.match = snoopify(re.match) and then you can just call 're.match' in a conditional and then access 're.match.value' to get to the last returned value. Source code in beancount/utils/snoop.py def snoopify(function): \"\"\"Decorate a function as snoopable. This is meant to reassign existing functions to a snoopable version of them. For example, if you wanted 're.match' to be automatically snoopable, just decorate it like this: re.match = snoopify(re.match) and then you can just call 're.match' in a conditional and then access 're.match.value' to get to the last returned value. \"\"\" @functools.wraps(function) def wrapper(*args, **kw): value = function(*args, **kw) wrapper.value = value return value wrapper.value = None return wrapper","title":"snoopify()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils","text":"Support utilities for testing scripts.","title":"test_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.RCall","text":"RCall(args, kwargs, return_value)","title":"RCall"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.RCall.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/utils/test_utils.py def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple(self)","title":"__getnewargs__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.RCall.__new__","text":"Create new instance of RCall(args, kwargs, return_value)","title":"__new__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.RCall.__repr__","text":"Return a nicely formatted representation string Source code in beancount/utils/test_utils.py def __repr__(self): 'Return a nicely formatted representation string' return self.__class__.__name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.TestCase","text":"","title":"TestCase"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.TestCase.assertLines","text":"Compare the lines of text1 and text2, ignoring whitespace. Parameters: text1 \u2013 A string, the expected text. text2 \u2013 A string, the actual text. message \u2013 An optional string message in case the assertion fails. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/utils/test_utils.py def assertLines(self, text1, text2, message=None): \"\"\"Compare the lines of text1 and text2, ignoring whitespace. Args: text1: A string, the expected text. text2: A string, the actual text. message: An optional string message in case the assertion fails. Raises: AssertionError: If the exception fails. \"\"\" clean_text1 = textwrap.dedent(text1.strip()) clean_text2 = textwrap.dedent(text2.strip()) lines1 = [line.strip() for line in clean_text1.splitlines()] lines2 = [line.strip() for line in clean_text2.splitlines()] # Compress all space longer than 4 spaces to exactly 4. # This affords us to be even looser. lines1 = [re.sub(' [ \\t]*', ' ', line) for line in lines1] lines2 = [re.sub(' [ \\t]*', ' ', line) for line in lines2] self.assertEqual(lines1, lines2, message)","title":"assertLines()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.TestCase.assertOutput","text":"Expect text printed to stdout. Parameters: expected_text \u2013 A string, the text that should have been printed to stdout. Exceptions: AssertionError \u2013 If the text differs. Source code in beancount/utils/test_utils.py @contextlib.contextmanager def assertOutput(self, expected_text): \"\"\"Expect text printed to stdout. Args: expected_text: A string, the text that should have been printed to stdout. Raises: AssertionError: If the text differs. \"\"\" with capture() as oss: yield oss self.assertLines(textwrap.dedent(expected_text), oss.getvalue())","title":"assertOutput()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.call_command","text":"Run the script with a subprocess. Parameters: script_args \u2013 A list of strings, the arguments to the subprocess, including the script name. Returns: A triplet of (return code integer, stdout ext, stderr text). Source code in beancount/utils/test_utils.py def call_command(command): \"\"\"Run the script with a subprocess. Args: script_args: A list of strings, the arguments to the subprocess, including the script name. Returns: A triplet of (return code integer, stdout ext, stderr text). \"\"\" assert isinstance(command, list), command p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() return p.returncode, stdout.decode(), stderr.decode()","title":"call_command()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.capture","text":"A context manager that captures what's printed to stdout. Parameters: *attributes \u2013 A tuple of strings, the name of the sys attributes to override with StringIO instances. Yields: A StringIO string accumulator. Source code in beancount/utils/test_utils.py def capture(*attributes): \"\"\"A context manager that captures what's printed to stdout. Args: *attributes: A tuple of strings, the name of the sys attributes to override with StringIO instances. Yields: A StringIO string accumulator. \"\"\" if not attributes: attributes = 'stdout' elif len(attributes) == 1: attributes = attributes[0] return patch(sys, attributes, io.StringIO)","title":"capture()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.create_temporary_files","text":"Create a number of temporary files under 'root'. This routine is used to initialize the contents of multiple files under a temporary directory. Parameters: root \u2013 A string, the name of the directory under which to create the files. contents_map \u2013 A dict of relative filenames to their contents. The content strings will be automatically dedented for convenience. In addition, the string 'ROOT' in the contents will be automatically replaced by the root directory name. Source code in beancount/utils/test_utils.py def create_temporary_files(root, contents_map): \"\"\"Create a number of temporary files under 'root'. This routine is used to initialize the contents of multiple files under a temporary directory. Args: root: A string, the name of the directory under which to create the files. contents_map: A dict of relative filenames to their contents. The content strings will be automatically dedented for convenience. In addition, the string 'ROOT' in the contents will be automatically replaced by the root directory name. \"\"\" os.makedirs(root, exist_ok=True) for relative_filename, contents in contents_map.items(): assert not path.isabs(relative_filename) filename = path.join(root, relative_filename) os.makedirs(path.dirname(filename), exist_ok=True) clean_contents = textwrap.dedent(contents.replace('{root}', root)) with open(filename, 'w') as f: f.write(clean_contents)","title":"create_temporary_files()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.docfile","text":"A decorator that write the function's docstring to a temporary file and calls the decorated function with the temporary filename. This is useful for writing tests. Parameters: function \u2013 A function to decorate. Returns: The decorated function. Source code in beancount/utils/test_utils.py def docfile(function, **kwargs): \"\"\"A decorator that write the function's docstring to a temporary file and calls the decorated function with the temporary filename. This is useful for writing tests. Args: function: A function to decorate. Returns: The decorated function. \"\"\" @functools.wraps(function) def new_function(self): allowed = ('buffering', 'encoding', 'newline', 'dir', 'prefix', 'suffix') if any([key not in allowed for key in kwargs]): raise ValueError(\"Invalid kwarg to docfile_extra\") with tempfile.NamedTemporaryFile('w', **kwargs) as file: text = function.__doc__ file.write(textwrap.dedent(text)) file.flush() return function(self, file.name) new_function.__doc__ = None return new_function","title":"docfile()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.docfile_extra","text":"A decorator identical to @docfile, but it also takes kwargs for the temporary file, Kwargs: e.g. buffering, encoding, newline, dir, prefix, and suffix. Returns: docfile Source code in beancount/utils/test_utils.py def docfile_extra(**kwargs): \"\"\" A decorator identical to @docfile, but it also takes kwargs for the temporary file, Kwargs: e.g. buffering, encoding, newline, dir, prefix, and suffix. Returns: docfile \"\"\" return functools.partial(docfile, **kwargs)","title":"docfile_extra()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.environ","text":"A context manager which pushes varname's value and restores it later. Parameters: varname \u2013 A string, the environ variable name. newvalue \u2013 A string, the desired value. Source code in beancount/utils/test_utils.py @contextlib.contextmanager def environ(varname, newvalue): \"\"\"A context manager which pushes varname's value and restores it later. Args: varname: A string, the environ variable name. newvalue: A string, the desired value. \"\"\" oldvalue = os.environ.get(varname, None) os.environ[varname] = newvalue yield if oldvalue is not None: os.environ[varname] = oldvalue else: del os.environ[varname]","title":"environ()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.find_python_lib","text":"Return the path to the root of the Python libraries. Returns: A string, the root directory. Source code in beancount/utils/test_utils.py def find_python_lib(): \"\"\"Return the path to the root of the Python libraries. Returns: A string, the root directory. \"\"\" return path.dirname(path.dirname(path.dirname(__file__)))","title":"find_python_lib()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.find_repository_root","text":"Return the path to the repository root. Parameters: filename \u2013 A string, the name of a file within the repository. Returns: A string, the root directory. Source code in beancount/utils/test_utils.py def find_repository_root(filename=None): \"\"\"Return the path to the repository root. Args: filename: A string, the name of a file within the repository. Returns: A string, the root directory. \"\"\" if filename is None: filename = __file__ # Support root directory under Bazel. match = re.match(r\"(.*\\.runfiles/beancount)/\", filename) if match: return match.group(1) while not all(path.exists(path.join(filename, sigfile)) for sigfile in ('PKG-INFO', 'COPYING', 'README.rst')): prev_filename = filename filename = path.dirname(filename) if prev_filename == filename: raise ValueError(\"Failed to find the root directory.\") return filename","title":"find_repository_root()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.make_failing_importer","text":"Make an importer that raise an ImportError for some modules. Use it like this: @mock.patch('builtins. import ', make_failing_importer('setuptools')) def test_... Parameters: removed_module_name \u2013 The name of the module import that should raise an exception. Returns: A decorated test decorator. Source code in beancount/utils/test_utils.py def make_failing_importer(*removed_module_names): \"\"\"Make an importer that raise an ImportError for some modules. Use it like this: @mock.patch('builtins.__import__', make_failing_importer('setuptools')) def test_... Args: removed_module_name: The name of the module import that should raise an exception. Returns: A decorated test decorator. \"\"\" def failing_import(name, *args, **kwargs): if name in removed_module_names: raise ImportError(\"Could not import {}\".format(name)) return builtins.__import__(name, *args, **kwargs) return failing_import","title":"make_failing_importer()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.nottest","text":"Make the given function not testable. Source code in beancount/utils/test_utils.py def nottest(func): \"Make the given function not testable.\" func.__test__ = False return func","title":"nottest()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.patch","text":"A context manager that temporarily patches an object's attributes. All attributes in 'attributes' are saved and replaced by new instances of type 'replacement_type'. Parameters: obj \u2013 The object to patch up. attributes \u2013 A string or a sequence of strings, the names of attributes to replace. replacement_type \u2013 A callable to build replacement objects. Yields: An instance of a list of sequences of 'replacement_type'. Source code in beancount/utils/test_utils.py @contextlib.contextmanager def patch(obj, attributes, replacement_type): \"\"\"A context manager that temporarily patches an object's attributes. All attributes in 'attributes' are saved and replaced by new instances of type 'replacement_type'. Args: obj: The object to patch up. attributes: A string or a sequence of strings, the names of attributes to replace. replacement_type: A callable to build replacement objects. Yields: An instance of a list of sequences of 'replacement_type'. \"\"\" single = isinstance(attributes, str) if single: attributes = [attributes] saved = [] replacements = [] for attribute in attributes: replacement = replacement_type() replacements.append(replacement) saved.append(getattr(obj, attribute)) setattr(obj, attribute, replacement) yield replacements[0] if single else replacements for attribute, saved_attr in zip(attributes, saved): setattr(obj, attribute, saved_attr)","title":"patch()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.record","text":"Decorates the function to intercept and record all calls and return values. Parameters: fun \u2013 A callable to be decorated. Returns: A wrapper function with a .calls attribute, a list of RCall instances. Source code in beancount/utils/test_utils.py def record(fun): \"\"\"Decorates the function to intercept and record all calls and return values. Args: fun: A callable to be decorated. Returns: A wrapper function with a .calls attribute, a list of RCall instances. \"\"\" @functools.wraps(fun) def wrapped(*args, **kw): return_value = fun(*args, **kw) wrapped.calls.append(RCall(args, kw, return_value)) return return_value wrapped.calls = [] return wrapped","title":"record()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.run_with_args","text":"Run the given function with sys.argv set to argv. The first argument is automatically inferred to be where the function object was defined. sys.argv is restored after the function is called. Parameters: function \u2013 A function object to call with no arguments. argv \u2013 A list of arguments, excluding the script name, to be temporarily set on sys.argv. Returns: The return value of the function run. Source code in beancount/utils/test_utils.py def run_with_args(function, args): \"\"\"Run the given function with sys.argv set to argv. The first argument is automatically inferred to be where the function object was defined. sys.argv is restored after the function is called. Args: function: A function object to call with no arguments. argv: A list of arguments, excluding the script name, to be temporarily set on sys.argv. Returns: The return value of the function run. \"\"\" saved_argv = sys.argv saved_handlers = logging.root.handlers try: module = sys.modules[function.__module__] sys.argv = [module.__file__] + args logging.root.handlers = [] return function() finally: sys.argv = saved_argv logging.root.handlers = saved_handlers","title":"run_with_args()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.search_words","text":"Search for a sequence of words in a line. Parameters: words \u2013 A list of strings, the words to look for, or a space-separated string. line \u2013 A string, the line to search into. Returns: A MatchObject, or None. Source code in beancount/utils/test_utils.py def search_words(words, line): \"\"\"Search for a sequence of words in a line. Args: words: A list of strings, the words to look for, or a space-separated string. line: A string, the line to search into. Returns: A MatchObject, or None. \"\"\" if isinstance(words, str): words = words.split() return re.search('.*'.join(r'\\b{}\\b'.format(word) for word in words), line)","title":"search_words()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.skipIfRaises","text":"A context manager (or decorator) that skips a test if an exception is raised. Yields: Nothing, for you to execute the function code. Exceptions: SkipTest \u2013 if the test raised the expected exception. Source code in beancount/utils/test_utils.py @contextlib.contextmanager def skipIfRaises(*exc_types): \"\"\"A context manager (or decorator) that skips a test if an exception is raised. Args: exc_type Yields: Nothing, for you to execute the function code. Raises: SkipTest: if the test raised the expected exception. \"\"\" try: yield except exc_types as exception: raise unittest.SkipTest(exception)","title":"skipIfRaises()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.subprocess_env","text":"Return a dict to use as environment for running subprocesses. Returns: A string, the root directory. Source code in beancount/utils/test_utils.py def subprocess_env(): \"\"\"Return a dict to use as environment for running subprocesses. Returns: A string, the root directory. \"\"\" # Ensure we have locations to invoke our Python executable and our # runnable binaries in the test environment to run subprocesses. binpath = ':'.join([ path.dirname(sys.executable), path.join(find_repository_root(__file__), 'bin'), os.environ.get('PATH', '').strip(':')]).strip(':') return {'PATH': binpath, 'PYTHONPATH': find_python_lib()}","title":"subprocess_env()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.tempdir","text":"A context manager that creates a temporary directory and deletes its contents unconditionally once done. Parameters: delete \u2013 A boolean, true if we want to delete the directory after running. **kw \u2013 Keyword arguments for mkdtemp. Yields: A string, the name of the temporary directory created. Source code in beancount/utils/test_utils.py @contextlib.contextmanager def tempdir(delete=True, **kw): \"\"\"A context manager that creates a temporary directory and deletes its contents unconditionally once done. Args: delete: A boolean, true if we want to delete the directory after running. **kw: Keyword arguments for mkdtemp. Yields: A string, the name of the temporary directory created. \"\"\" tempdir = tempfile.mkdtemp(prefix=\"beancount-test-tmpdir.\", **kw) try: yield tempdir finally: if delete: shutil.rmtree(tempdir, ignore_errors=True)","title":"tempdir()"},{"location":"api_reference/beancount.utils.html#beancount.utils.text_utils","text":"Text manipulation utilities.","title":"text_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.text_utils.entitize_ampersand","text":"Convert unescaped ampersand characters (&) to XML entities. This is used to fix code that has been programmed by bad developers who didn't think about escaping the entities in their strings. file does not contain Parameters: filename \u2013 A string, the name of the file to convert. Returns: A self-destructing NamedTemporaryFile object that has been flushed and which you may read to obtain the fixed contents. Source code in beancount/utils/text_utils.py def entitize_ampersand(filename): \"\"\"Convert unescaped ampersand characters (&) to XML entities. This is used to fix code that has been programmed by bad developers who didn't think about escaping the entities in their strings. file does not contain Args: filename: A string, the name of the file to convert. Returns: A self-destructing NamedTemporaryFile object that has been flushed and which you may read to obtain the fixed contents. \"\"\" tidy_file = tempfile.NamedTemporaryFile(suffix='.xls', mode='w', delete=False) with open(filename) as infile: contents = infile.read() new_contents = re.sub('&([^;&]{12})', '&\\\\1', contents) tidy_file.write(new_contents) tidy_file.flush() return tidy_file","title":"entitize_ampersand()"},{"location":"api_reference/beancount.utils.html#beancount.utils.text_utils.replace_number","text":"Replace a single number matched from text into X'es. 'match' is a MatchObject from a regular expressions match. (Use this with re.sub()). Parameters: match \u2013 A MatchObject. Returns: A replacement string, consisting only of X'es. Source code in beancount/utils/text_utils.py def replace_number(match): \"\"\"Replace a single number matched from text into X'es. 'match' is a MatchObject from a regular expressions match. (Use this with re.sub()). Args: match: A MatchObject. Returns: A replacement string, consisting only of X'es. \"\"\" return re.sub('[0-9]', 'X', match.group(1)) + match.group(2)","title":"replace_number()"},{"location":"api_reference/beancount.utils.html#beancount.utils.text_utils.replace_numbers","text":"Replace all numbers found within text. Note that this is a heuristic used to filter out private numbers from web pages in incognito mode and thus may not be perfect. It should let through numbers which are part of URLs. Parameters: text \u2013 An input string object. Returns: A string, with relevant numbers hopefully replaced with X'es. Source code in beancount/utils/text_utils.py def replace_numbers(text): \"\"\"Replace all numbers found within text. Note that this is a heuristic used to filter out private numbers from web pages in incognito mode and thus may not be perfect. It should let through numbers which are part of URLs. Args: text: An input string object. Returns: A string, with relevant numbers hopefully replaced with X'es. \"\"\" return re.sub(r'\\b([0-9,]+(?:\\.[0-9]*)?)\\b([ \\t<]+[^0-9,.]|$)', replace_number, text)","title":"replace_numbers()"},{"location":"api_reference/beancount.utils.html#beancount.utils.version","text":"Implement common options across all programs.","title":"version"},{"location":"api_reference/beancount.utils.html#beancount.utils.version.ArgumentParser","text":"Add a standard --version option to an ArgumentParser. Parameters: *args \u2013 Arguments for the parser. *kwargs \u2013 Keyword arguments for the parser. Returns: An instance of ArgumentParser, with our default options set. Source code in beancount/utils/version.py def ArgumentParser(*args, **kwargs): \"\"\"Add a standard --version option to an ArgumentParser. Args: *args: Arguments for the parser. *kwargs: Keyword arguments for the parser. Returns: An instance of ArgumentParser, with our default options set. \"\"\" parser = argparse.ArgumentParser(*args, **kwargs) parser.add_argument('--version', '-V', action='version', version=compute_version_string( beancount.__version__, _parser.__vc_changeset__, _parser.__vc_timestamp__)) return parser","title":"ArgumentParser()"},{"location":"api_reference/beancount.utils.html#beancount.utils.version.compute_version_string","text":"Compute a version string from the changeset and timestamp baked in the parser. Parameters: version \u2013 A string, the version number. changeset \u2013 A string, a version control string identifying the commit of the version. timestamp \u2013 An integer, the UNIX epoch timestamp of the changeset. Returns: A human-readable string for the version. Source code in beancount/utils/version.py def compute_version_string(version, changeset, timestamp): \"\"\"Compute a version string from the changeset and timestamp baked in the parser. Args: version: A string, the version number. changeset: A string, a version control string identifying the commit of the version. timestamp: An integer, the UNIX epoch timestamp of the changeset. Returns: A human-readable string for the version. \"\"\" # Shorten changeset. if changeset: if re.match('hg:', changeset): changeset = changeset[:15] elif re.match('git:', changeset): changeset = changeset[:12] # Convert timestamp to a date. date = None if timestamp > 0: date = datetime.datetime.utcfromtimestamp(timestamp).date() version = 'Beancount {}'.format(version) if changeset or date: version = '{} ({})'.format( version, '; '.join(map(str, filter(None, [changeset, date])))) return version","title":"compute_version_string()"},{"location":"api_reference/beancount.web.html","text":"beancount.web \uf0c1 Web server reporting front-end. beancount.web.bottle_utils \uf0c1 Bottle utilities, mostly helpers to do mounts on top of dynamic routes. beancount.web.bottle_utils.AttrMapper \uf0c1 A URL mapper that allows attribute access for view-links. This is used in templates. beancount.web.bottle_utils.AttrMapper.__init__(self, mapper_function) special \uf0c1 Constructor for an attribute mapper. Parameters: mapper_function \u2013 A function to apply on attribute lookup, and upon calling .build(). Source code in beancount/web/bottle_utils.py def __init__(self, mapper_function): \"\"\"Constructor for an attribute mapper. Args: mapper_function: A function to apply on attribute lookup, and upon calling .build(). \"\"\" self.mapper_function = mapper_function beancount.web.bottle_utils.internal_redirect(app, path_depth) \uf0c1 A version of bottle's mountpoint_wrapper() that we call explicitly. Bottle supports a mount() method that allows on to install an application on a subpath of the main application. However, it does this on a fixed path. We want to manually intercept the lazy creation or fetching of a view and call for a redirect explicitly (via bottle's mountpoint_wrapper() function). However, this function is hidden within the scope of a the Bottle.mount() method; if it were defined globally we would just use it, but it is not. So we copy if here. This is directly lifted from Bottle.mount() and edited minimally. Parameters: app \u2013 A Bottle instance. path_depth \u2013 The number of request path components to skip for the mount. For example, if our subapplication is mount on /view/all, then the path depth is 2. Returns: A Bottle HTTPResponse object. Exceptions: Exception \u2013 Any exception, depending on the callback. Source code in beancount/web/bottle_utils.py def internal_redirect(app, path_depth): \"\"\"A version of bottle's mountpoint_wrapper() that we call explicitly. Bottle supports a mount() method that allows on to install an application on a subpath of the main application. However, it does this on a fixed path. We want to manually intercept the lazy creation or fetching of a view and call for a redirect explicitly (via bottle's mountpoint_wrapper() function). However, this function is hidden within the scope of a the Bottle.mount() method; if it were defined globally we would just use it, but it is not. So we copy if here. This is directly lifted from Bottle.mount() and edited minimally. Args: app: A Bottle instance. path_depth: The number of request path components to skip for the mount. For example, if our subapplication is mount on /view/all, then the path depth is 2. Returns: A Bottle HTTPResponse object. Raises: Exception: Any exception, depending on the callback. \"\"\" # pylint: disable=invalid-name try: request.path_shift(path_depth) rs = bottle.HTTPResponse([]) def start_response(status, headerlist, exc_info=None): if exc_info: try: _raise(*exc_info) finally: exc_info = None rs.status = status for name, value in headerlist: rs.add_header(name, value) return rs.body.append body = app(request.environ, start_response) if body and rs.body: body = itertools.chain(rs.body, body) rs.body = body or rs.body return rs finally: request.path_shift(-path_depth) beancount.web.scrape \uf0c1 beancount.web.scrape.iterlinks(html, html_path) \uf0c1 Find links targets in HTML text. This deals with both absolute and relative links, and it external links to external sites. Parameters: html \u2013 An lxml document node. html_path \u2013 The URL of the document node. Yields: URL strings, where found. Source code in beancount/web/scrape.py def iterlinks(html, html_path): \"\"\"Find links targets in HTML text. This deals with both absolute and relative links, and it external links to external sites. Args: html: An lxml document node. html_path: The URL of the document node. Yields: URL strings, where found. \"\"\" html_dir = path.dirname(html_path) for element, attribute, link, pos in lxml.html.iterlinks(html): url = urllib.parse.urlparse(link) if url.scheme or url.netloc: continue # Skip external urls. link = url.path if not link: continue if not path.isabs(link): link = path.join(html_dir, link) yield link beancount.web.scrape.scrape_urls(url_format, callback, ignore_regexp=None) \uf0c1 Recursively scrape pages from a web address. Parameters: url_format \u2013 The pattern for building links from relative paths. callback \u2013 A callback function to invoke on each page to validate it. The function is called with the response and the url as arguments. This function should trigger an error on failure (via an exception). ignore_regexp \u2013 A regular expression string, the urls to ignore. Returns: A set of all the processed URLs and a set of all the skipped URLs. Source code in beancount/web/scrape.py def scrape_urls(url_format, callback, ignore_regexp=None): \"\"\"Recursively scrape pages from a web address. Args: url_format: The pattern for building links from relative paths. callback: A callback function to invoke on each page to validate it. The function is called with the response and the url as arguments. This function should trigger an error on failure (via an exception). ignore_regexp: A regular expression string, the urls to ignore. Returns: A set of all the processed URLs and a set of all the skipped URLs. \"\"\" # The set of all URLs seen so far. seen = set() # The list of all URLs to process. We use a list here so we have # reproducible order if we repeat the test. process_list = [\"/\"] # A set of all the URLs processed and skipped everywhere. all_processed_urls = set() all_skipped_urls = set() # Loop over all URLs remaining to process. while process_list: url = process_list.pop() logging.debug(\"Processing: %s\", url) all_processed_urls.add(url) # Fetch the URL and check its return status. response = urllib.request.urlopen(url_format.format(url)) # Generate errors on redirects. redirected_url = urllib.parse.urlparse(response.geturl()).path if redirected_url != url: logging.error(\"Redirected: %s -> %s\", url, redirected_url) # Read the contents. This can only be done once. response_contents = response.read() skipped_urls = set() content_type = response.info().get_content_type() if content_type == 'text/html': # Process all the links in the page and register all the unseen links to # be processed. html_root = lxml.html.document_fromstring(response_contents) for link in iterlinks(html_root, url): # Skip URLs to be ignored. if ignore_regexp and re.match(ignore_regexp, link): logging.debug(\"Skipping: %s\", link) skipped_urls.add(link) all_skipped_urls.add(link) continue # Check if link has already been seen. if link in seen: logging.debug('Seen: \"%s\"', link) continue # Schedule the link for scraping. logging.debug('Scheduling: \"%s\"', link) process_list.append(link) seen.add(link) else: html_root = None # Call back for processing. callback(url, response, response_contents, html_root, skipped_urls) return all_processed_urls, all_skipped_urls beancount.web.scrape.validate_local_links(filename) \uf0c1 Open and parse the given HTML filename and verify all local targets exist. This checks that all the files pointed to by the file we're processing are files that exist on disk. This can be used to validate that a baked output does not have links to files that do not exist, that all the links are valid. Parameters: filename \u2013 A string, the name of the HTML file to process. Returns: A pair of \u2013 missing: A set of strings, the names of links to files that do not exist. empty: A boolean, true if this file is empty. Source code in beancount/web/scrape.py def validate_local_links(filename): \"\"\"Open and parse the given HTML filename and verify all local targets exist. This checks that all the files pointed to by the file we're processing are files that exist on disk. This can be used to validate that a baked output does not have links to files that do not exist, that all the links are valid. Args: filename: A string, the name of the HTML file to process. Returns: A pair of: missing: A set of strings, the names of links to files that do not exist. empty: A boolean, true if this file is empty. \"\"\" filedir = path.dirname(filename) contents = open(filename, 'rb').read() empty = len(contents) == 0 missing = set() if not empty: html = lxml.html.document_fromstring(contents) if html is not None: for element, attribute, link, pos in lxml.html.iterlinks(html): urlpath = urllib.parse.urlparse(link) if urlpath.scheme or urlpath.netloc: continue if path.isabs(urlpath.path): continue target = path.normpath(path.join(filedir, urlpath.path)) if not path.exists(target): missing.add(target) return missing, empty beancount.web.scrape.validate_local_links_in_dir(directory) \uf0c1 Find all the files under the given directory and validate all their links. Parameters: directory \u2013 A string, the root directory whose files to process. Returns: A tuple of \u2013 files: A list of all the filenames found and processed. missing: A set of strings, the names of links to files that do not exist. empty: A boolean, true if this file is empty. Source code in beancount/web/scrape.py def validate_local_links_in_dir(directory): \"\"\"Find all the files under the given directory and validate all their links. Args: directory: A string, the root directory whose files to process. Returns: A tuple of: files: A list of all the filenames found and processed. missing: A set of strings, the names of links to files that do not exist. empty: A boolean, true if this file is empty. \"\"\" logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s') allfiles = [] missing, empty = set(), set() for root, dirs, files in os.walk(directory): for filename in files: afilename = path.join(root, filename) allfiles.append(afilename) logging.info(\"Validating: '%s'\", afilename) missing, is_empty = validate_local_links(afilename) if is_empty: empty.add(afilename) return allfiles, missing, empty beancount.web.views \uf0c1 Views are filters on the global list of entries, which produces a subset of entries. beancount.web.views.AllView ( View ) \uf0c1 A view that includes all the entries, unmodified. beancount.web.views.AllView.apply_filter(self, entries, options_map) \uf0c1 Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries, options_map): return (entries, None, None) beancount.web.views.ComponentView ( View ) \uf0c1 A view that includes transactions with at least one posting with an account that includes a given component. beancount.web.views.ComponentView.__init__(self, entries, options_map, title, component) special \uf0c1 Create a view clamped to one year. Note: this is the only view where the entries are summarized and clamped. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. title \u2013 A string, the title of this view. component \u2013 A string, the name of an account component to include. Source code in beancount/web/views.py def __init__(self, entries, options_map, title, component): \"\"\"Create a view clamped to one year. Note: this is the only view where the entries are summarized and clamped. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. title: A string, the title of this view. component: A string, the name of an account component to include. \"\"\" assert isinstance(component, str) self.component = component View.__init__(self, entries, options_map, title) beancount.web.views.ComponentView.apply_filter(self, entries, options_map) \uf0c1 Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries, options_map): component = self.component component_entries = [entry for entry in entries if data.has_entry_account_component(entry, component)] return component_entries, None, None beancount.web.views.EmptyView ( View ) \uf0c1 An empty view, for testing. beancount.web.views.EmptyView.__init__(self, entries, options_map, title, *args, **kw) special \uf0c1 Create an empty view. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. title \u2013 A string, the title of this view. *args \u2013 Ignored. **kw \u2013 Ignored. Source code in beancount/web/views.py def __init__(self, entries, options_map, title, *args, **kw): \"\"\"Create an empty view. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. title: A string, the title of this view. *args: Ignored. **kw: Ignored. \"\"\" View.__init__(self, entries, options_map, title) beancount.web.views.EmptyView.apply_filter(self, _, __) \uf0c1 Return the list of entries unmodified. Source code in beancount/web/views.py def apply_filter(self, _, __): \"Return the list of entries unmodified.\" return ([], None, None) beancount.web.views.MonthView ( View ) \uf0c1 A view of the entries for a single month. beancount.web.views.MonthView.__init__(self, entries, options_map, title, year, month) special \uf0c1 Create a view clamped to one month. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. title \u2013 A string, the title of this view. year \u2013 An integer, the year of period. month \u2013 An integer, the month to be used as year end. Source code in beancount/web/views.py def __init__(self, entries, options_map, title, year, month): \"\"\"Create a view clamped to one month. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. title: A string, the title of this view. year: An integer, the year of period. month: An integer, the month to be used as year end. \"\"\" self.year = year self.month = month View.__init__(self, entries, options_map, title) self.monthly = MonthNavigation.FULL beancount.web.views.MonthView.apply_filter(self, entries, options_map) \uf0c1 Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries, options_map): # Clamp to the desired period. begin_date = datetime.date(self.year, self.month, 1) end_date = date_utils.next_month(begin_date) with misc_utils.log_time('clamp', logging.info): entries, index = summarize.clamp_opt(entries, begin_date, end_date, options_map) return entries, index, end_date beancount.web.views.PayeeView ( View ) \uf0c1 A view that includes entries with some specific payee. beancount.web.views.PayeeView.__init__(self, entries, options_map, title, payee) special \uf0c1 Create a view clamped to one year. Note: this is the only view where the entries are summarized and clamped. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. title \u2013 A string, the title of this view. payee \u2013 A string, the payee whose transactions to include. Source code in beancount/web/views.py def __init__(self, entries, options_map, title, payee): \"\"\"Create a view clamped to one year. Note: this is the only view where the entries are summarized and clamped. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. title: A string, the title of this view. payee: A string, the payee whose transactions to include. \"\"\" assert isinstance(payee, str) self.payee = payee View.__init__(self, entries, options_map, title) beancount.web.views.PayeeView.apply_filter(self, entries, options_map) \uf0c1 Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries, options_map): payee = self.payee payee_entries = [entry for entry in entries if isinstance(entry, data.Transaction) and (entry.payee == payee)] return payee_entries, None, None beancount.web.views.TagView ( View ) \uf0c1 A view that includes only entries some specific tags. beancount.web.views.TagView.__init__(self, entries, options_map, title, tags) special \uf0c1 Create a view with only entries tagged with the given tags. Note: this is the only view where the entries are summarized and clamped. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. title \u2013 A string, the title of this view. tags \u2013 A set of strings, the tags to include. Entries with at least one of these tags will be included in the output. Source code in beancount/web/views.py def __init__(self, entries, options_map, title, tags): \"\"\"Create a view with only entries tagged with the given tags. Note: this is the only view where the entries are summarized and clamped. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. title: A string, the title of this view. tags: A set of strings, the tags to include. Entries with at least one of these tags will be included in the output. \"\"\" assert isinstance(tags, (set, frozenset, list, tuple)) self.tags = tags View.__init__(self, entries, options_map, title) beancount.web.views.TagView.apply_filter(self, entries, options_map) \uf0c1 Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries, options_map): tags = self.tags tagged_entries = [ entry for entry in entries if isinstance(entry, data.Transaction) and entry.tags and (entry.tags & tags)] return tagged_entries, None, None beancount.web.views.View \uf0c1 A container for filtering a subset of entries and realizing that for display. beancount.web.views.View.__init__(self, all_entries, options_map, title) special \uf0c1 Build a View instance. Parameters: all_entries \u2013 The full list of directives as output from the loader. options_map \u2013 The options dict, as output by the parser. title \u2013 A string, the title of this view to render. Source code in beancount/web/views.py def __init__(self, all_entries, options_map, title): \"\"\"Build a View instance. Args: all_entries: The full list of directives as output from the loader. options_map: The options dict, as output by the parser. title: A string, the title of this view to render. \"\"\" # A reference to the full list of padded entries. self.all_entries = all_entries # List of filtered entries for this view, and index at the beginning # of the period transactions, past the opening balances. These are # computed in _initialize(). self.entries = None self.opening_entries = None self.closing_entries = None # Title. self.title = title # Realization of the filtered entries to display. These are computed in # _initialize(). self.real_accounts = None self.opening_real_accounts = None self.closing_real_accounts = None # Monthly navigation style. self.monthly = MonthNavigation.NONE # Realize now, we don't need to do this lazily because we create these # view objects on-demand and cache them. self._initialize(options_map) beancount.web.views.View.apply_filter(self, entries) \uf0c1 Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries): \"\"\"Filter the list of entries. This is used to obtain the filtered list of entries. Args: entries: A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. \"\"\" raise NotImplementedError beancount.web.views.YearView ( View ) \uf0c1 A view of the entries for a single year. beancount.web.views.YearView.__init__(self, entries, options_map, title, year, first_month=1) special \uf0c1 Create a view clamped to one year. Note: this is the only view where the entries are summarized and clamped. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. title \u2013 A string, the title of this view. year \u2013 An integer, the year of the exercise period. first_month \u2013 The calendar month (starting with 1) with which the year opens. Source code in beancount/web/views.py def __init__(self, entries, options_map, title, year, first_month=1): \"\"\"Create a view clamped to one year. Note: this is the only view where the entries are summarized and clamped. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. title: A string, the title of this view. year: An integer, the year of the exercise period. first_month: The calendar month (starting with 1) with which the year opens. \"\"\" self.year = year self.first_month = first_month if not (1 <= first_month <= 12): raise ValueError(\"Invalid month: {}\".format(first_month)) View.__init__(self, entries, options_map, title) self.monthly = MonthNavigation.COMPACT beancount.web.views.YearView.apply_filter(self, entries, options_map) \uf0c1 Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries, options_map): # Clamp to the desired period. begin_date = datetime.date(self.year, self.first_month, 1) end_date = datetime.date(self.year+1, self.first_month, 1) with misc_utils.log_time('clamp', logging.info): entries, index = summarize.clamp_opt(entries, begin_date, end_date, options_map) return entries, index, end_date beancount.web.web \uf0c1 Web server for Beancount ledgers. This uses the Bottle single-file micro web framework (with no plugins). beancount.web.web.HTMLFormatter ( HTMLFormatter ) \uf0c1 A formatter object that can be used to render accounts links. Attributes: Name Type Description build_url A function used to render links to a Bottle application. leafonly a boolean, if true, render only the name of the leaf nodes. beancount.web.web.HTMLFormatter.build_global(self, *args, **kwds) \uf0c1 Render to global application. Source code in beancount/web/web.py def build_global(self, *args, **kwds): \"Render to global application.\" return app.router.build(*args, **kwds) beancount.web.web.HTMLFormatter.render_account(self, account_name) \uf0c1 See base class. Source code in beancount/web/web.py def render_account(self, account_name): \"\"\"See base class.\"\"\" if self.view_links: if self.leaf_only: # Calculate the number of components to figure out the indent to # render at. components = account.split(account_name) indent = '{:.1f}'.format(len(components) * self.EMS_PER_COMPONENT) anchor = '{}'.format( self.build_url('journal', account_name=self.account_xform.render(account_name)), account.leaf(account_name)) return '{}'.format( indent, anchor) else: anchor = '{}'.format( self.build_url('journal', account_name=self.account_xform.render(account_name)), account_name) return '{}'.format(anchor) else: if self.leaf_only: # Calculate the number of components to figure out the indent to # render at. components = account.split(account_name) indent = '{:.1f}'.format(len(components) * self.EMS_PER_COMPONENT) account_name = account.leaf(account_name) return '{}'.format( indent, account_name) else: return '{}'.format(account_name) beancount.web.web.HTMLFormatter.render_commodity(self, base_quote) \uf0c1 See base class. Source code in beancount/web/web.py def render_commodity(self, base_quote): \"\"\"See base class.\"\"\" base, quote = base_quote return '{} / {}'.format( self.build_url('prices', base=base, quote=quote), base, quote) beancount.web.web.HTMLFormatter.render_context(self, entry) \uf0c1 See base class. Source code in beancount/web/web.py def render_context(self, entry): \"\"\"See base class.\"\"\" # Note: rendering to global application. # Note(2): we could avoid rendering links to summarizing and transfer # entries which are not going to be found. return self.build_global('context', ehash=compare.hash_entry(entry)) beancount.web.web.HTMLFormatter.render_doc(self, filename) \uf0c1 See base class. Source code in beancount/web/web.py def render_doc(self, filename): \"\"\"See base class.\"\"\" return '{}'.format( self.build_url('doc', filename=filename.lstrip('/')), path.basename(filename)) beancount.web.web.HTMLFormatter.render_event_type(self, event) \uf0c1 See base class. Source code in beancount/web/web.py def render_event_type(self, event): \"\"\"See base class.\"\"\" return '{}'.format(self.build_url('event', event=event), event) beancount.web.web.HTMLFormatter.render_inventory(self, inv) \uf0c1 Override this formatter to convert the inventory to units only. Source code in beancount/web/web.py def render_inventory(self, inv): \"\"\"Override this formatter to convert the inventory to units only.\"\"\" return super().render_inventory(inv.reduce(convert.get_units)) beancount.web.web.HTMLFormatter.render_link(self, link) \uf0c1 See base class. Source code in beancount/web/web.py def render_link(self, link): \"\"\"See base class.\"\"\" # Note: rendering to global application. return self.build_global('link', link=link) beancount.web.web.HTMLFormatter.render_source(self, source) \uf0c1 See base class. Source code in beancount/web/web.py def render_source(self, source): \"\"\"See base class.\"\"\" return '{}#{}'.format(app.get_url('source'), source['lineno']) beancount.web.web.activity() \uf0c1 Render the update activity. Source code in beancount/web/web.py @viewapp.route('/activity', name='activity') def activity(): \"Render the update activity.\" return render_global( pagetitle=\"Update Activity\", contents=render_real_report(misc_reports.ActivityReport, request.view.real_accounts, app.price_map, request.view.price_date, leaf_only=False)) beancount.web.web.auto_reload_input_file(callback) \uf0c1 A plugin that automatically reloads the input file if it changed since the last page was loaded. Source code in beancount/web/web.py def auto_reload_input_file(callback): \"\"\"A plugin that automatically reloads the input file if it changed since the last page was loaded.\"\"\" def wrapper(*posargs, **kwargs): filename = app.args.filename if loader.needs_refresh(app.options): logging.info('Reloading...') # Save the source for later, to render. with open(filename, encoding='utf8') as f: app.source = f.read() # Parse the beancount file. entries, errors, options_map = loader.load_file(filename) # Print out the list of errors. if errors: # pylint: disable=unsupported-assignment-operation request.params['render_overlay'] = True print(',----------------------------------------------------------------') printer.print_errors(errors, file=sys.stdout) print('`----------------------------------------------------------------') # Save globals in the global app. app.entries = entries app.errors = errors app.options = options_map app.account_types = options.get_account_types(options_map) # Pre-compute the price database. app.price_map = prices.build_price_map(entries) # Pre-compute the list of active years. app.active_years = list(getters.get_active_years(entries)) # Reset the view cache. app.views.clear() else: # For now, the overlay is a link to the errors page. Always render # it on the right when there are errors. if app.errors: # pylint: disable=unsupported-assignment-operation request.params['render_overlay'] = True return callback(*posargs, **kwargs) return wrapper beancount.web.web.balsheet() \uf0c1 Balance sheet. Source code in beancount/web/web.py @viewapp.route('/balsheet', name='balsheet') def balsheet(): \"Balance sheet.\" return render_view(pagetitle=\"Balance Sheet\", contents=render_real_report(balance_reports.BalanceSheetReport, request.view.closing_real_accounts, app.price_map, request.view.price_date, leaf_only=True)) beancount.web.web.commodities() \uf0c1 Render a list commodities with list their prices page. Source code in beancount/web/web.py @viewapp.route('/commodities', name='commodities') def commodities(): \"Render a list commodities with list their prices page.\" html_table = render_report(price_reports.CommoditiesReport, request.view.entries, [], css_id='price-index') return render_view( pagetitle=\"Commodities\", contents=html_table) beancount.web.web.context_(ehash=None) \uf0c1 Render the before & after context around a transaction entry. Source code in beancount/web/web.py @app.route('/context/', name='context') def context_(ehash=None): \"Render the before & after context around a transaction entry.\" matching_entries = [entry for entry in app.entries if ehash == compare.hash_entry(entry)] oss = io.StringIO() if len(matching_entries) == 0: print(\"ERROR: Could not find matching entry for '{}'\".format(ehash), file=oss) elif len(matching_entries) > 1: print(\"ERROR: Ambiguous entries for '{}'\".format(ehash), file=oss) print(file=oss) dcontext = app.options['dcontext'] printer.print_entries(matching_entries, dcontext, file=oss) else: entry = matching_entries[0] # Render the context. oss.write(\"
    \\n\") oss.write(context.render_entry_context(app.entries, app.options, entry)) oss.write(\"
    \\n\") # Render the filelinks. if FILELINK_PROTOCOL: meta = entry.meta uri = FILELINK_PROTOCOL.format(filename=meta.get('filename'), lineno=meta.get('lineno')) oss.write(''.format(uri, 'Open')) return render_global( pagetitle=\"Context: {}\".format(ehash), contents=oss.getvalue()) beancount.web.web.conversions_() \uf0c1 Render the list of transactions with conversions. Source code in beancount/web/web.py @viewapp.route('/conversions', name='conversions') def conversions_(): \"Render the list of transactions with conversions.\" return render_view( pagetitle=\"Conversions\", contents=render_report(journal_reports.ConversionsReport, request.view.entries, leaf_only=False)) beancount.web.web.doc(filename=None) \uf0c1 Serve static filenames for documents directives. Source code in beancount/web/web.py @app.route('/doc/', name=doc_name) def doc(filename=None): \"Serve static filenames for documents directives.\" filename = '/' + filename # Check that there is a document directive that has this filename. # This is for security; we don't want to be able to serve just any file. for entry in misc_utils.filter_type(app.entries, data.Document): if entry.filename == filename: break else: raise bottle.HTTPError(404, \"Not found\") # Just serve the file ourselves. return bottle.static_file(path.basename(filename), path.dirname(filename)) beancount.web.web.documents() \uf0c1 Render a tree with all the documents found. Source code in beancount/web/web.py @viewapp.route('/documents', name='documents') def documents(): \"Render a tree with all the documents found.\" return render_view( pagetitle=\"Documents\", contents=render_report(journal_reports.DocumentsReport, request.view.entries, leaf_only=False)) beancount.web.web.errors() \uf0c1 Report error encountered during parsing, checking and realization. Source code in beancount/web/web.py @app.route('/errors', name='errors') def errors(): \"Report error encountered during parsing, checking and realization.\" return render_global( pagetitle=\"Errors\", contents=render_report(misc_reports.ErrorReport, [], leaf_only=False)) beancount.web.web.event(event=None) \uf0c1 Render all values of a particular event. Source code in beancount/web/web.py @viewapp.route(r'/event/', name='event') def event(event=None): \"Render all values of a particular event.\" if not event: bottle.redirect(app.get_url('event_index')) return render_view( pagetitle=\"Event: {}\".format(event), contents=render_report(misc_reports.EventsReport, app.entries, ['--expr', event])) beancount.web.web.event_index() \uf0c1 Render the latest values of all events and an index. Source code in beancount/web/web.py @viewapp.route('/event', name='event_index') def event_index(): \"Render the latest values of all events and an index.\" return render_view( pagetitle=\"Events Index\", contents=render_report(misc_reports.CurrentEventsReport, request.view.entries)) beancount.web.web.get_all_view(app) \uf0c1 Return a view of all transactions. Returns: An instance of AllView, that covers all transactions. Source code in beancount/web/web.py def get_all_view(app): \"\"\"Return a view of all transactions. Returns: An instance of AllView, that covers all transactions. \"\"\" return views.AllView(app.entries, app.options, 'All Transactions') beancount.web.web.handle_view(path_depth) \uf0c1 A decorator for handlers which create views lazily. If you decorate a method with this, the wrapper does the redirect handling and your method is just a factory for a View instance, which is cached. Parameters: path_depth \u2013 An integer, the number of components that form the view id. Returns: A decorator function, used to wrap view handlers. Source code in beancount/web/web.py def handle_view(path_depth): \"\"\"A decorator for handlers which create views lazily. If you decorate a method with this, the wrapper does the redirect handling and your method is just a factory for a View instance, which is cached. Args: path_depth: An integer, the number of components that form the view id. Returns: A decorator function, used to wrap view handlers. \"\"\" def view_populator(callback): def wrapper(*args, **kwargs): components = request.path.split('/') viewid = '/'.join(components[:path_depth+1]) try: # Try fetching the view from the cache. view = app.views[viewid] except KeyError: # We need to create the view. view = app.views[viewid] = callback(*args, **kwargs) # Save the view for the subrequest and redirect. populate_view() # picks this up and saves it in request.view. request.environ['VIEW'] = view return bottle_utils.internal_redirect(viewapp, path_depth) return wrapper return view_populator beancount.web.web.holdings_() \uf0c1 Render a detailed table of all holdings. Source code in beancount/web/web.py @viewapp.route('/equity/holdings', name='holdings') def holdings_(): \"Render a detailed table of all holdings.\" html_table = render_report(holdings_reports.HoldingsReport, request.view.entries, [], css_class='holdings detail-table sortable', center=True) return render_view( pagetitle=\"Holdings\", contents=html_table, scripts='') beancount.web.web.holdings_byaccount() \uf0c1 Render a table of holdings by account. Source code in beancount/web/web.py @viewapp.route('/equity/holdings_byaccount', name='holdings_byaccount') def holdings_byaccount(): \"Render a table of holdings by account.\" html_table = render_report(holdings_reports.HoldingsReport, request.view.entries, ['--by', 'account'], css_class='holdings detail-table sortable', center=True) return render_view( pagetitle=\"Holdings by Account\", contents=html_table, scripts='') beancount.web.web.holdings_bycommodity() \uf0c1 Render a table of holdings by commodity. Source code in beancount/web/web.py @viewapp.route('/equity/holdings_bycommodity', name='holdings_bycommodity') def holdings_bycommodity(): \"Render a table of holdings by commodity.\" html_table = render_report(holdings_reports.HoldingsReport, request.view.entries, ['--by', 'commodity'], css_class='holdings detail-table sortable', center=True) return render_view( pagetitle=\"Holdings by Commodity\", contents=html_table, scripts='') beancount.web.web.holdings_bycurrency() \uf0c1 Render a table of holdings by currency. Source code in beancount/web/web.py @viewapp.route('/equity/holdings_bycurrency', name='holdings_bycurrency') def holdings_bycurrency(): \"Render a table of holdings by currency.\" html_table = render_report(holdings_reports.HoldingsReport, request.view.entries, ['--by', 'currency'], css_class='holdings detail-table sortable', center=True) return render_view( pagetitle=\"Holdings by Currency\", contents=html_table, scripts='') beancount.web.web.holdings_byrootaccount() \uf0c1 Render a table of holdings by account. Source code in beancount/web/web.py @viewapp.route('/equity/holdings_byrootaccount', name='holdings_byrootaccount') def holdings_byrootaccount(): \"Render a table of holdings by account.\" html_table = render_report(holdings_reports.HoldingsReport, request.view.entries, ['--by', 'root-account'], css_class='holdings detail-table sortable', center=True) return render_view( pagetitle=\"Holdings by Account\", contents=html_table, scripts='') beancount.web.web.incognito(callback) \uf0c1 A plugin that converts all numbers rendered into X's, in order to hide the actual values in the ledger. This is used for doing public demos using my real ledger, where I don't necessarily want to share the detail of my financial life with the viewers but when I still want an interesting ledger, with enough detail that looks realistic. Source code in beancount/web/web.py def incognito(callback): \"\"\"A plugin that converts all numbers rendered into X's, in order to hide the actual values in the ledger. This is used for doing public demos using my real ledger, where I don't necessarily want to share the detail of my financial life with the viewers but when I still want an interesting ledger, with enough detail that looks realistic.\"\"\" def wrapper(*posargs, **kwargs): contents = callback(*posargs, **kwargs) # pylint: disable=bad-continuation if (response.content_type in ('text/html', '') and isinstance(contents, str)): contents = text_utils.replace_numbers(contents) return contents return wrapper beancount.web.web.income() \uf0c1 Income statement. Source code in beancount/web/web.py @viewapp.route('/income', name='income') def income(): \"Income statement.\" return render_view(pagetitle=\"Income Statement\", contents=render_real_report(balance_reports.IncomeStatementReport, request.view.real_accounts, app.price_map, request.view.price_date, leaf_only=True)) beancount.web.web.index() \uf0c1 Index of all pages, so that navigation need not have all links. Source code in beancount/web/web.py @viewapp.route('/index', name='index') def index(): \"Index of all pages, so that navigation need not have all links.\" oss = io.StringIO() oss.write('
      \\n') for title, page in [ (\"Balances\", \"trial\"), (\"Balance Sheet\", \"balsheet\"), (\"Opening Balances\", \"openbal\"), (\"Income Statement\", \"income\"), (\"General Journal\", \"journal_all\"), (\"Conversions\", \"conversions\"), (\"Documents\", \"documents\"), (\"Holdings (Full Detail)\", \"holdings\"), (\"Holdings by Account\", \"holdings_byaccount\"), (\"Holdings by Root Account\", \"holdings_byrootaccount\"), (\"Holdings by Commodity\", \"holdings_bycommodity\"), (\"Holdings by Currency\", \"holdings_bycurrency\"), (\"Net Worth\", \"networth\"), (\"Commodities\", \"commodities\"), (\"Events\", \"event_index\"), (\"Activity/Update\", \"activity\"), (\"Statistics (Types)\", \"stats_types\"), (\"Statistics (Postings)\", \"stats_postings\"), ]: oss.write('
    • {}
    • \\n'.format( request.app.get_url(page), title)) oss.write('
    \\n') return render_view( pagetitle=\"Index\", contents=oss.getvalue()) beancount.web.web.journal_(account_name=None) \uf0c1 A list of all the entries for this account realization. Source code in beancount/web/web.py @viewapp.route('/journal/', name='journal') def journal_(account_name=None): \"A list of all the entries for this account realization.\" account_name = app.account_xform.parse(account_name) # Figure out which account to render this from. real_accounts = request.view.real_accounts if account_name: if account_name and account_types.is_balance_sheet_account(account_name, app.account_types): real_accounts = request.view.closing_real_accounts # Render the report. args = [] if account_name: args.append('--account={}'.format(account_name)) render_postings = request.params.get('postings', True) if isinstance(render_postings, str): render_postings = render_postings.lower() in ('1', 'true') if render_postings: args.append('--verbose') try: html_journal = render_real_report(journal_reports.JournalReport, real_accounts, app.price_map, request.view.price_date, args, leaf_only=False) except KeyError as e: raise bottle.HTTPError(404, '{}'.format(e)) return render_view(pagetitle='{}'.format(account_name or 'General Ledger (All Accounts)'), contents=html_journal) beancount.web.web.journal_all() \uf0c1 A list of all the entries in this realization. Source code in beancount/web/web.py @viewapp.route('/journal/all', name='journal_all') def journal_all(): \"A list of all the entries in this realization.\" bottle.redirect(request.app.get_url('journal', account_name='')) beancount.web.web.link(link=None) \uf0c1 Serve journals for links. Source code in beancount/web/web.py @app.route('/link/', name='link') def link(link=None): \"Serve journals for links.\" linked_entries = basicops.filter_link(link, app.entries) oss = io.StringIO() formatter = HTMLFormatter(app.options['dcontext'], request.app.get_url, False, app.account_xform, view_links=False) journal_html.html_entries_table_with_balance(oss, linked_entries, formatter) return render_global( pagetitle=\"Link: {}\".format(link), contents=oss.getvalue()) beancount.web.web.main() \uf0c1 Main web service runner. This runs the event loop and blocks indefinitely. Source code in beancount/web/web.py def main(): \"\"\"Main web service runner. This runs the event loop and blocks indefinitely.\"\"\" argparser = version.ArgumentParser(description=__doc__.strip()) add_web_arguments(argparser) args = argparser.parse_args() run_app(args) beancount.web.web.month_request(year, month) \uf0c1 Render a URL to a particular month of the context's request. Source code in beancount/web/web.py def month_request(year, month): \"\"\"Render a URL to a particular month of the context's request. \"\"\" if year < app.active_years[0] or year > app.active_years[-1]: return '' month = list(calendar.month_abbr).index(month) month = \"{:0>2d}\".format(month) return app.router.build('month', year=year, month=month, path=request.path[1:]) beancount.web.web.networth() \uf0c1 Render a table of the net worth for this filter. Source code in beancount/web/web.py @viewapp.route('/equity/networth', name='networth') def networth(): \"Render a table of the net worth for this filter.\" html_table = render_report(holdings_reports.NetWorthReport, request.view.entries) return render_view( pagetitle=\"Net Worth\", contents=html_table) beancount.web.web.openbal() \uf0c1 Opening balances. Source code in beancount/web/web.py @viewapp.route('/openbal', name='openbal') def openbal(): \"Opening balances.\" return render_view(pagetitle=\"Opening Balances\", contents=render_real_report(balance_reports.BalanceSheetReport, request.view.opening_real_accounts, app.price_map, request.view.price_date, leaf_only=True)) beancount.web.web.populate_view(callback) \uf0c1 A plugin that will populate the request with the current view instance. Parameters: callback \u2013 A continuation function to call to handle the request. Returns: A function to call to install view-specific parameters on the request. Source code in beancount/web/web.py def populate_view(callback): \"\"\"A plugin that will populate the request with the current view instance. Args: callback: A continuation function to call to handle the request. Returns: A function to call to install view-specific parameters on the request. \"\"\" def wrapper(*args, **kwargs): request.view = request.environ['VIEW'] return callback(*args, **kwargs) return wrapper beancount.web.web.prices_values(base=None, quote=None) \uf0c1 Render all the values for a particular price pair. Source code in beancount/web/web.py @viewapp.route('/prices' r'/' r'/', name='prices') def prices_values(base=None, quote=None): \"Render all the values for a particular price pair.\" html_table = render_report(price_reports.CommodityPricesReport, request.view.entries, ['--commodity', '{}/{}'.format(base, quote)], css_id='price-index') return render_view( pagetitle=\"Price: {} / {}\".format(base, quote), contents=html_table) beancount.web.web.render_global(*args, **kw) \uf0c1 Render the title and contents in our standard template for a global page. Parameters: *args \u2013 A tuple of values for the HTML template. *kw \u2013 A dict of optional values for the HTML template. Returns: An HTML string of the rendered template. Source code in beancount/web/web.py def render_global(*args, **kw): \"\"\"Render the title and contents in our standard template for a global page. Args: *args: A tuple of values for the HTML template. *kw: A dict of optional values for the HTML template. Returns: An HTML string of the rendered template. \"\"\" response.content_type = 'text/html' kw['A'] = A # Application mapper kw['V'] = V # View mapper kw['title'] = app.options['title'] kw['view_title'] = '' kw['navigation'] = GLOBAL_NAVIGATION kw['scripts'] = kw.get('scripts', '') kw['overlay'] = ( render_overlay('
  • Errors
  • '.format( app.router.build('errors'))) if request.params.pop('render_overlay', True) else '') return template.render(*args, **kw) beancount.web.web.render_overlay(contents) \uf0c1 Render an overlay of the navigation with the current errors. This is used to bring up new errors on any page when they occur. Returns: A string of HTML for the contents of the errors overlay. Source code in beancount/web/web.py def render_overlay(contents): \"\"\"Render an overlay of the navigation with the current errors. This is used to bring up new errors on any page when they occur. Returns: A string of HTML for the contents of the errors overlay. \"\"\" return '''
      {}
    '''.format(contents) # It would be nice to have a fancy overlay here, that automatically appears # after parsing if there are errors and that automatically smoothly fades # out. beancount.web.web.render_real_report(report_class, real_root, price_map, price_date, args=None, leaf_only=False) \uf0c1 Instantiate a report and rendering it to a string. This is intended to be called in the context of a Bottle view app request (it uses 'request'). Parameters: report_class \u2013 A class, the type of the report to render. real_root \u2013 An instance of RealAccount to render. price_map \u2013 A price map as built by build_price_map(). price_date \u2013 The date at which to evaluate the prices. args \u2013 A list of strings, the arguments to initialize the report with. leaf_only \u2013 A boolean, whether to render the leaf names only. Returns: A string, the rendered report. Source code in beancount/web/web.py def render_real_report(report_class, real_root, price_map, price_date, args=None, leaf_only=False): # pylint: disable=too-many-arguments \"\"\"Instantiate a report and rendering it to a string. This is intended to be called in the context of a Bottle view app request (it uses 'request'). Args: report_class: A class, the type of the report to render. real_root: An instance of RealAccount to render. price_map: A price map as built by build_price_map(). price_date: The date at which to evaluate the prices. args: A list of strings, the arguments to initialize the report with. leaf_only: A boolean, whether to render the leaf names only. Returns: A string, the rendered report. \"\"\" formatter = HTMLFormatter(app.options['dcontext'], request.app.get_url, leaf_only, app.account_xform) oss = io.StringIO() report_ = report_class.from_args(args, formatter=formatter) report_.render_real_htmldiv(real_root, price_map, price_date, app.options, oss) return oss.getvalue() beancount.web.web.render_report(report_class, entries, args=None, css_id=None, css_class=None, center=False, leaf_only=True) \uf0c1 Instantiate a report and rendering it to a string. Parameters: report_class \u2013 A class, the type of the report to render. real_root \u2013 An instance of RealAccount to render. args \u2013 A list of strings, the arguments to initialize the report with. css_id \u2013 An optional string, the CSS id for the div to render. css_class \u2013 An optional string, the CSS class for the div to render. center \u2013 A boolean flag, if true, wrap the results in a
    tag. leaf_only \u2013 A boolean, whether to render the leaf names only. Returns: A string, the rendered report. Source code in beancount/web/web.py def render_report(report_class, entries, args=None, css_id=None, css_class=None, center=False, leaf_only=True): \"\"\"Instantiate a report and rendering it to a string. Args: report_class: A class, the type of the report to render. real_root: An instance of RealAccount to render. args: A list of strings, the arguments to initialize the report with. css_id: An optional string, the CSS id for the div to render. css_class: An optional string, the CSS class for the div to render. center: A boolean flag, if true, wrap the results in a
    tag. leaf_only: A boolean, whether to render the leaf names only. Returns: A string, the rendered report. \"\"\" formatter = HTMLFormatter(app.options['dcontext'], request.app.get_url, leaf_only, app.account_xform) oss = io.StringIO() if center: oss.write('
    \\n') report_ = report_class.from_args(args, formatter=formatter, css_id=css_id, css_class=css_class) report_.render_htmldiv(entries, app.errors, app.options, oss) if center: oss.write('
    \\n') return oss.getvalue() beancount.web.web.render_view(*args, **kw) \uf0c1 Render the title and contents in our standard template for a view page. Parameters: *args \u2013 A tuple of values for the HTML template. *kw \u2013 A dict of optional values for the HTML template. Returns: An HTML string of the rendered template. Source code in beancount/web/web.py def render_view(*args, **kw): \"\"\"Render the title and contents in our standard template for a view page. Args: *args: A tuple of values for the HTML template. *kw: A dict of optional values for the HTML template. Returns: An HTML string of the rendered template. \"\"\" response.content_type = 'text/html' kw['A'] = A # Application mapper kw['V'] = V # View mapper kw['title'] = app.options['title'] kw['view_title'] = ' - ' + request.view.title overlays = [] if request.params.pop('render_overlay', False): overlays.append( '
  • Errors
  • '.format(app.router.build('errors'))) # Render navigation, with monthly navigation option. oss = io.StringIO() oss.write(APP_NAVIGATION.render(A=A, V=V, view_title=request.view.title)) if request.view.monthly is views.MonthNavigation.COMPACT: overlays.append( '
  • Monthly
  • '.format(M.Jan)) elif request.view.monthly is views.MonthNavigation.FULL: annual = app.router.build('year', path=DEFAULT_VIEW_REDIRECT, year=request.view.year) oss.write(APP_NAVIGATION_MONTHLY_FULL.render(M=M, Mp=Mp, Mn=Mn, V=V, annual=annual)) kw['navigation'] = oss.getvalue() kw['overlay'] = render_overlay(' '.join(overlays)) kw['scripts'] = kw.get('scripts', '') return template.render(*args, **kw) beancount.web.web.root() \uf0c1 Redirect the root page to the home page. Source code in beancount/web/web.py @app.route('/', name='root') def root(): \"Redirect the root page to the home page.\" bottle.redirect(app.get_url('toc')) beancount.web.web.scrape_webapp(webargs, callback, ignore_regexp) \uf0c1 Run a web server on a Beancount file and scrape it. This is the main entry point of this module. Parameters: webargs \u2013 An argparse.Namespace container of the arguments provided in web.add_web_arguments(). callback \u2013 A callback function to invoke on each page to validate it. The function is called with the response and the url as arguments. This function should trigger an error on failure (via an exception). ignore_regexp \u2013 A regular expression string, the urls to ignore. Returns: A set of all the processed URLs and a set of all the skipped URLs. Source code in beancount/web/web.py def scrape_webapp(webargs, callback, ignore_regexp): \"\"\"Run a web server on a Beancount file and scrape it. This is the main entry point of this module. Args: webargs: An argparse.Namespace container of the arguments provided in web.add_web_arguments(). callback: A callback function to invoke on each page to validate it. The function is called with the response and the url as arguments. This function should trigger an error on failure (via an exception). ignore_regexp: A regular expression string, the urls to ignore. Returns: A set of all the processed URLs and a set of all the skipped URLs. \"\"\" url_format = 'http://localhost:{}{{}}'.format(webargs.port) thread = thread_server_start(webargs) # Skips: # - Docs cannot be read for external files. # # - Components views... well there are just too many, makes the tests # impossibly slow. Just keep the A's so some are covered. url_lists = scrape.scrape_urls(url_format, callback, ignore_regexp) thread_server_shutdown(thread) return url_lists beancount.web.web.setup_monkey_patch_for_server_shutdown() \uf0c1 Setup globals to steal access to the server reference. This is required to initiate shutdown, unfortunately. (Bottle could easily remedy that.) Source code in beancount/web/web.py def setup_monkey_patch_for_server_shutdown(): \"\"\"Setup globals to steal access to the server reference. This is required to initiate shutdown, unfortunately. (Bottle could easily remedy that.)\"\"\" # Save the original function. # pylint: disable=import-outside-toplevel from wsgiref.simple_server import make_server # Create a decorator that will save the server upon start. def stealing_make_server(*args, **kw): global server server = make_server(*args, **kw) return server # Patch up wsgiref itself with the decorated function. import wsgiref.simple_server wsgiref.simple_server.make_server = stealing_make_server beancount.web.web.shutdown() \uf0c1 Request for the server to shutdown. Source code in beancount/web/web.py def shutdown(): \"\"\"Request for the server to shutdown.\"\"\" server.shutdown() beancount.web.web.source() \uf0c1 Render the source file, allowing scrolling at a specific line. Source code in beancount/web/web.py @app.route('/source', name='source') def source(): \"Render the source file, allowing scrolling at a specific line.\" contents = io.StringIO() if app.args.no_source: contents.write(\"Source hidden.\") else: contents.write('
    ') for i, line in enumerate(app.source.splitlines()): lineno = i+1 contents.write( '
    {lineno} {line}
    \\n'.format( lineno=lineno, line=line.rstrip())) contents.write('
    ') return render_global( pagetitle=\"Source\", contents=contents.getvalue() ) beancount.web.web.stats_postings() \uf0c1 Compute and render statistics about the input file. Source code in beancount/web/web.py @viewapp.route('/stats_postings', name='stats_postings') def stats_postings(): \"Compute and render statistics about the input file.\" return render_view( pagetitle=\"Postings Statistics\", contents=render_report(misc_reports.StatsPostingsReport, request.view.entries)) beancount.web.web.stats_types() \uf0c1 Compute and render statistics about the input file. Source code in beancount/web/web.py @viewapp.route('/stats_types', name='stats_types') def stats_types(): \"Compute and render statistics about the input file.\" return render_view( pagetitle=\"Directives Statistics\", contents=render_report(misc_reports.StatsDirectivesReport, request.view.entries)) beancount.web.web.style() \uf0c1 Stylesheet for the entire document. Source code in beancount/web/web.py @app.route('/resources/web.css', name='style') def style(): \"Stylesheet for the entire document.\" response.content_type = 'text/css' if app.args.debug: with open(path.join(path.dirname(__file__), 'web.css')) as f: global STYLE; STYLE = f.read() return STYLE beancount.web.web.thread_server_shutdown(thread) \uf0c1 Shutdown the server running in the given thread. Unfortunately, in the meantime this has a side-effect on all servers. This returns after waiting that the thread has stopped. Parameters: thread \u2013 A threading.Thread instance. Source code in beancount/web/web.py def thread_server_shutdown(thread): \"\"\"Shutdown the server running in the given thread. Unfortunately, in the meantime this has a side-effect on all servers. This returns after waiting that the thread has stopped. Args: thread: A threading.Thread instance. \"\"\" # Clean shutdown: request to stop, then join the thread. # Note that because we daemonize, we could forego this elegant detail. shutdown() thread.join() beancount.web.web.thread_server_start(web_args, **kwargs) \uf0c1 Start a server in a new thread. Parameters: argparse_args \u2013 An argparse parsed options object, with all the options from add_web_arguments(). Returns: A new Thread instance. Source code in beancount/web/web.py def thread_server_start(web_args, **kwargs): \"\"\"Start a server in a new thread. Args: argparse_args: An argparse parsed options object, with all the options from add_web_arguments(). Returns: A new Thread instance. \"\"\" thread = threading.Thread( target=run_app, args=(web_args,), kwargs=kwargs) thread.daemon = True # Automatically exit if the process comes dwn. thread.start() # Ensure the server has at least started before running the scraper. wait_ready() time.sleep(0.1) return thread beancount.web.web.trial() \uf0c1 Trial balance / Chart of Accounts. Source code in beancount/web/web.py @viewapp.route('/trial', name='trial') def trial(): \"Trial balance / Chart of Accounts.\" return render_view( pagetitle=\"Trial Balance\", contents=render_real_report(balance_reports.BalancesReport, request.view.real_accounts, app.price_map, request.view.price_date, leaf_only=True)) beancount.web.web.url_restrict_generator(url_prefix) \uf0c1 Restrict to only a single prefix. Parameters: url_prefix \u2013 A string, a URL prefix to restrict to. callback \u2013 The function to wrap. Returns: A handler decorator. Source code in beancount/web/web.py def url_restrict_generator(url_prefix): \"\"\"Restrict to only a single prefix. Args: url_prefix: A string, a URL prefix to restrict to. callback: The function to wrap. Returns: A handler decorator. \"\"\" # A list of URLs that should always be accepted, even when restricted. allowed_regexps = [re.compile(regexp).match for regexp in ['/resources', '/favicon.ico', '/index', '/errors', '/source', '/context', '/third_party']] def url_restrict_handler(callback): def wrapper(*args, **kwargs): if (any(match(request.path) for match in allowed_regexps) or request.path.startswith(url_prefix)): return callback(*args, **kwargs) if request.path == '/': bottle.redirect(url_prefix) # Note: we issue a \"202 Accepted\" status in order to satisfy bean-bake, # we want to distinguish between an actual 404 error and this case. raise bottle.HTTPError(202, \"URLs restricted to '{}'\".format(url_prefix)) return wrapper return url_restrict_handler beancount.web.web.wait_ready() \uf0c1 Wait until the 'server' global has been set. This tells us the server is running. Source code in beancount/web/web.py def wait_ready(): \"\"\"Wait until the 'server' global has been set. This tells us the server is running. \"\"\" while server is None: time.sleep(0.05)","title":"beancount.web"},{"location":"api_reference/beancount.web.html#beancountweb","text":"Web server reporting front-end.","title":"beancount.web"},{"location":"api_reference/beancount.web.html#beancount.web.bottle_utils","text":"Bottle utilities, mostly helpers to do mounts on top of dynamic routes.","title":"bottle_utils"},{"location":"api_reference/beancount.web.html#beancount.web.bottle_utils.AttrMapper","text":"A URL mapper that allows attribute access for view-links. This is used in templates.","title":"AttrMapper"},{"location":"api_reference/beancount.web.html#beancount.web.bottle_utils.AttrMapper.__init__","text":"Constructor for an attribute mapper. Parameters: mapper_function \u2013 A function to apply on attribute lookup, and upon calling .build(). Source code in beancount/web/bottle_utils.py def __init__(self, mapper_function): \"\"\"Constructor for an attribute mapper. Args: mapper_function: A function to apply on attribute lookup, and upon calling .build(). \"\"\" self.mapper_function = mapper_function","title":"__init__()"},{"location":"api_reference/beancount.web.html#beancount.web.bottle_utils.internal_redirect","text":"A version of bottle's mountpoint_wrapper() that we call explicitly. Bottle supports a mount() method that allows on to install an application on a subpath of the main application. However, it does this on a fixed path. We want to manually intercept the lazy creation or fetching of a view and call for a redirect explicitly (via bottle's mountpoint_wrapper() function). However, this function is hidden within the scope of a the Bottle.mount() method; if it were defined globally we would just use it, but it is not. So we copy if here. This is directly lifted from Bottle.mount() and edited minimally. Parameters: app \u2013 A Bottle instance. path_depth \u2013 The number of request path components to skip for the mount. For example, if our subapplication is mount on /view/all, then the path depth is 2. Returns: A Bottle HTTPResponse object. Exceptions: Exception \u2013 Any exception, depending on the callback. Source code in beancount/web/bottle_utils.py def internal_redirect(app, path_depth): \"\"\"A version of bottle's mountpoint_wrapper() that we call explicitly. Bottle supports a mount() method that allows on to install an application on a subpath of the main application. However, it does this on a fixed path. We want to manually intercept the lazy creation or fetching of a view and call for a redirect explicitly (via bottle's mountpoint_wrapper() function). However, this function is hidden within the scope of a the Bottle.mount() method; if it were defined globally we would just use it, but it is not. So we copy if here. This is directly lifted from Bottle.mount() and edited minimally. Args: app: A Bottle instance. path_depth: The number of request path components to skip for the mount. For example, if our subapplication is mount on /view/all, then the path depth is 2. Returns: A Bottle HTTPResponse object. Raises: Exception: Any exception, depending on the callback. \"\"\" # pylint: disable=invalid-name try: request.path_shift(path_depth) rs = bottle.HTTPResponse([]) def start_response(status, headerlist, exc_info=None): if exc_info: try: _raise(*exc_info) finally: exc_info = None rs.status = status for name, value in headerlist: rs.add_header(name, value) return rs.body.append body = app(request.environ, start_response) if body and rs.body: body = itertools.chain(rs.body, body) rs.body = body or rs.body return rs finally: request.path_shift(-path_depth)","title":"internal_redirect()"},{"location":"api_reference/beancount.web.html#beancount.web.scrape","text":"","title":"scrape"},{"location":"api_reference/beancount.web.html#beancount.web.scrape.iterlinks","text":"Find links targets in HTML text. This deals with both absolute and relative links, and it external links to external sites. Parameters: html \u2013 An lxml document node. html_path \u2013 The URL of the document node. Yields: URL strings, where found. Source code in beancount/web/scrape.py def iterlinks(html, html_path): \"\"\"Find links targets in HTML text. This deals with both absolute and relative links, and it external links to external sites. Args: html: An lxml document node. html_path: The URL of the document node. Yields: URL strings, where found. \"\"\" html_dir = path.dirname(html_path) for element, attribute, link, pos in lxml.html.iterlinks(html): url = urllib.parse.urlparse(link) if url.scheme or url.netloc: continue # Skip external urls. link = url.path if not link: continue if not path.isabs(link): link = path.join(html_dir, link) yield link","title":"iterlinks()"},{"location":"api_reference/beancount.web.html#beancount.web.scrape.scrape_urls","text":"Recursively scrape pages from a web address. Parameters: url_format \u2013 The pattern for building links from relative paths. callback \u2013 A callback function to invoke on each page to validate it. The function is called with the response and the url as arguments. This function should trigger an error on failure (via an exception). ignore_regexp \u2013 A regular expression string, the urls to ignore. Returns: A set of all the processed URLs and a set of all the skipped URLs. Source code in beancount/web/scrape.py def scrape_urls(url_format, callback, ignore_regexp=None): \"\"\"Recursively scrape pages from a web address. Args: url_format: The pattern for building links from relative paths. callback: A callback function to invoke on each page to validate it. The function is called with the response and the url as arguments. This function should trigger an error on failure (via an exception). ignore_regexp: A regular expression string, the urls to ignore. Returns: A set of all the processed URLs and a set of all the skipped URLs. \"\"\" # The set of all URLs seen so far. seen = set() # The list of all URLs to process. We use a list here so we have # reproducible order if we repeat the test. process_list = [\"/\"] # A set of all the URLs processed and skipped everywhere. all_processed_urls = set() all_skipped_urls = set() # Loop over all URLs remaining to process. while process_list: url = process_list.pop() logging.debug(\"Processing: %s\", url) all_processed_urls.add(url) # Fetch the URL and check its return status. response = urllib.request.urlopen(url_format.format(url)) # Generate errors on redirects. redirected_url = urllib.parse.urlparse(response.geturl()).path if redirected_url != url: logging.error(\"Redirected: %s -> %s\", url, redirected_url) # Read the contents. This can only be done once. response_contents = response.read() skipped_urls = set() content_type = response.info().get_content_type() if content_type == 'text/html': # Process all the links in the page and register all the unseen links to # be processed. html_root = lxml.html.document_fromstring(response_contents) for link in iterlinks(html_root, url): # Skip URLs to be ignored. if ignore_regexp and re.match(ignore_regexp, link): logging.debug(\"Skipping: %s\", link) skipped_urls.add(link) all_skipped_urls.add(link) continue # Check if link has already been seen. if link in seen: logging.debug('Seen: \"%s\"', link) continue # Schedule the link for scraping. logging.debug('Scheduling: \"%s\"', link) process_list.append(link) seen.add(link) else: html_root = None # Call back for processing. callback(url, response, response_contents, html_root, skipped_urls) return all_processed_urls, all_skipped_urls","title":"scrape_urls()"},{"location":"api_reference/beancount.web.html#beancount.web.scrape.validate_local_links","text":"Open and parse the given HTML filename and verify all local targets exist. This checks that all the files pointed to by the file we're processing are files that exist on disk. This can be used to validate that a baked output does not have links to files that do not exist, that all the links are valid. Parameters: filename \u2013 A string, the name of the HTML file to process. Returns: A pair of \u2013 missing: A set of strings, the names of links to files that do not exist. empty: A boolean, true if this file is empty. Source code in beancount/web/scrape.py def validate_local_links(filename): \"\"\"Open and parse the given HTML filename and verify all local targets exist. This checks that all the files pointed to by the file we're processing are files that exist on disk. This can be used to validate that a baked output does not have links to files that do not exist, that all the links are valid. Args: filename: A string, the name of the HTML file to process. Returns: A pair of: missing: A set of strings, the names of links to files that do not exist. empty: A boolean, true if this file is empty. \"\"\" filedir = path.dirname(filename) contents = open(filename, 'rb').read() empty = len(contents) == 0 missing = set() if not empty: html = lxml.html.document_fromstring(contents) if html is not None: for element, attribute, link, pos in lxml.html.iterlinks(html): urlpath = urllib.parse.urlparse(link) if urlpath.scheme or urlpath.netloc: continue if path.isabs(urlpath.path): continue target = path.normpath(path.join(filedir, urlpath.path)) if not path.exists(target): missing.add(target) return missing, empty","title":"validate_local_links()"},{"location":"api_reference/beancount.web.html#beancount.web.scrape.validate_local_links_in_dir","text":"Find all the files under the given directory and validate all their links. Parameters: directory \u2013 A string, the root directory whose files to process. Returns: A tuple of \u2013 files: A list of all the filenames found and processed. missing: A set of strings, the names of links to files that do not exist. empty: A boolean, true if this file is empty. Source code in beancount/web/scrape.py def validate_local_links_in_dir(directory): \"\"\"Find all the files under the given directory and validate all their links. Args: directory: A string, the root directory whose files to process. Returns: A tuple of: files: A list of all the filenames found and processed. missing: A set of strings, the names of links to files that do not exist. empty: A boolean, true if this file is empty. \"\"\" logging.basicConfig(level=logging.INFO, format='%(levelname)-8s: %(message)s') allfiles = [] missing, empty = set(), set() for root, dirs, files in os.walk(directory): for filename in files: afilename = path.join(root, filename) allfiles.append(afilename) logging.info(\"Validating: '%s'\", afilename) missing, is_empty = validate_local_links(afilename) if is_empty: empty.add(afilename) return allfiles, missing, empty","title":"validate_local_links_in_dir()"},{"location":"api_reference/beancount.web.html#beancount.web.views","text":"Views are filters on the global list of entries, which produces a subset of entries.","title":"views"},{"location":"api_reference/beancount.web.html#beancount.web.views.AllView","text":"A view that includes all the entries, unmodified.","title":"AllView"},{"location":"api_reference/beancount.web.html#beancount.web.views.AllView.apply_filter","text":"Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries, options_map): return (entries, None, None)","title":"apply_filter()"},{"location":"api_reference/beancount.web.html#beancount.web.views.ComponentView","text":"A view that includes transactions with at least one posting with an account that includes a given component.","title":"ComponentView"},{"location":"api_reference/beancount.web.html#beancount.web.views.ComponentView.__init__","text":"Create a view clamped to one year. Note: this is the only view where the entries are summarized and clamped. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. title \u2013 A string, the title of this view. component \u2013 A string, the name of an account component to include. Source code in beancount/web/views.py def __init__(self, entries, options_map, title, component): \"\"\"Create a view clamped to one year. Note: this is the only view where the entries are summarized and clamped. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. title: A string, the title of this view. component: A string, the name of an account component to include. \"\"\" assert isinstance(component, str) self.component = component View.__init__(self, entries, options_map, title)","title":"__init__()"},{"location":"api_reference/beancount.web.html#beancount.web.views.ComponentView.apply_filter","text":"Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries, options_map): component = self.component component_entries = [entry for entry in entries if data.has_entry_account_component(entry, component)] return component_entries, None, None","title":"apply_filter()"},{"location":"api_reference/beancount.web.html#beancount.web.views.EmptyView","text":"An empty view, for testing.","title":"EmptyView"},{"location":"api_reference/beancount.web.html#beancount.web.views.EmptyView.__init__","text":"Create an empty view. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. title \u2013 A string, the title of this view. *args \u2013 Ignored. **kw \u2013 Ignored. Source code in beancount/web/views.py def __init__(self, entries, options_map, title, *args, **kw): \"\"\"Create an empty view. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. title: A string, the title of this view. *args: Ignored. **kw: Ignored. \"\"\" View.__init__(self, entries, options_map, title)","title":"__init__()"},{"location":"api_reference/beancount.web.html#beancount.web.views.EmptyView.apply_filter","text":"Return the list of entries unmodified. Source code in beancount/web/views.py def apply_filter(self, _, __): \"Return the list of entries unmodified.\" return ([], None, None)","title":"apply_filter()"},{"location":"api_reference/beancount.web.html#beancount.web.views.MonthView","text":"A view of the entries for a single month.","title":"MonthView"},{"location":"api_reference/beancount.web.html#beancount.web.views.MonthView.__init__","text":"Create a view clamped to one month. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. title \u2013 A string, the title of this view. year \u2013 An integer, the year of period. month \u2013 An integer, the month to be used as year end. Source code in beancount/web/views.py def __init__(self, entries, options_map, title, year, month): \"\"\"Create a view clamped to one month. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. title: A string, the title of this view. year: An integer, the year of period. month: An integer, the month to be used as year end. \"\"\" self.year = year self.month = month View.__init__(self, entries, options_map, title) self.monthly = MonthNavigation.FULL","title":"__init__()"},{"location":"api_reference/beancount.web.html#beancount.web.views.MonthView.apply_filter","text":"Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries, options_map): # Clamp to the desired period. begin_date = datetime.date(self.year, self.month, 1) end_date = date_utils.next_month(begin_date) with misc_utils.log_time('clamp', logging.info): entries, index = summarize.clamp_opt(entries, begin_date, end_date, options_map) return entries, index, end_date","title":"apply_filter()"},{"location":"api_reference/beancount.web.html#beancount.web.views.PayeeView","text":"A view that includes entries with some specific payee.","title":"PayeeView"},{"location":"api_reference/beancount.web.html#beancount.web.views.PayeeView.__init__","text":"Create a view clamped to one year. Note: this is the only view where the entries are summarized and clamped. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. title \u2013 A string, the title of this view. payee \u2013 A string, the payee whose transactions to include. Source code in beancount/web/views.py def __init__(self, entries, options_map, title, payee): \"\"\"Create a view clamped to one year. Note: this is the only view where the entries are summarized and clamped. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. title: A string, the title of this view. payee: A string, the payee whose transactions to include. \"\"\" assert isinstance(payee, str) self.payee = payee View.__init__(self, entries, options_map, title)","title":"__init__()"},{"location":"api_reference/beancount.web.html#beancount.web.views.PayeeView.apply_filter","text":"Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries, options_map): payee = self.payee payee_entries = [entry for entry in entries if isinstance(entry, data.Transaction) and (entry.payee == payee)] return payee_entries, None, None","title":"apply_filter()"},{"location":"api_reference/beancount.web.html#beancount.web.views.TagView","text":"A view that includes only entries some specific tags.","title":"TagView"},{"location":"api_reference/beancount.web.html#beancount.web.views.TagView.__init__","text":"Create a view with only entries tagged with the given tags. Note: this is the only view where the entries are summarized and clamped. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. title \u2013 A string, the title of this view. tags \u2013 A set of strings, the tags to include. Entries with at least one of these tags will be included in the output. Source code in beancount/web/views.py def __init__(self, entries, options_map, title, tags): \"\"\"Create a view with only entries tagged with the given tags. Note: this is the only view where the entries are summarized and clamped. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. title: A string, the title of this view. tags: A set of strings, the tags to include. Entries with at least one of these tags will be included in the output. \"\"\" assert isinstance(tags, (set, frozenset, list, tuple)) self.tags = tags View.__init__(self, entries, options_map, title)","title":"__init__()"},{"location":"api_reference/beancount.web.html#beancount.web.views.TagView.apply_filter","text":"Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries, options_map): tags = self.tags tagged_entries = [ entry for entry in entries if isinstance(entry, data.Transaction) and entry.tags and (entry.tags & tags)] return tagged_entries, None, None","title":"apply_filter()"},{"location":"api_reference/beancount.web.html#beancount.web.views.View","text":"A container for filtering a subset of entries and realizing that for display.","title":"View"},{"location":"api_reference/beancount.web.html#beancount.web.views.View.__init__","text":"Build a View instance. Parameters: all_entries \u2013 The full list of directives as output from the loader. options_map \u2013 The options dict, as output by the parser. title \u2013 A string, the title of this view to render. Source code in beancount/web/views.py def __init__(self, all_entries, options_map, title): \"\"\"Build a View instance. Args: all_entries: The full list of directives as output from the loader. options_map: The options dict, as output by the parser. title: A string, the title of this view to render. \"\"\" # A reference to the full list of padded entries. self.all_entries = all_entries # List of filtered entries for this view, and index at the beginning # of the period transactions, past the opening balances. These are # computed in _initialize(). self.entries = None self.opening_entries = None self.closing_entries = None # Title. self.title = title # Realization of the filtered entries to display. These are computed in # _initialize(). self.real_accounts = None self.opening_real_accounts = None self.closing_real_accounts = None # Monthly navigation style. self.monthly = MonthNavigation.NONE # Realize now, we don't need to do this lazily because we create these # view objects on-demand and cache them. self._initialize(options_map)","title":"__init__()"},{"location":"api_reference/beancount.web.html#beancount.web.views.View.apply_filter","text":"Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries): \"\"\"Filter the list of entries. This is used to obtain the filtered list of entries. Args: entries: A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. \"\"\" raise NotImplementedError","title":"apply_filter()"},{"location":"api_reference/beancount.web.html#beancount.web.views.YearView","text":"A view of the entries for a single year.","title":"YearView"},{"location":"api_reference/beancount.web.html#beancount.web.views.YearView.__init__","text":"Create a view clamped to one year. Note: this is the only view where the entries are summarized and clamped. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. title \u2013 A string, the title of this view. year \u2013 An integer, the year of the exercise period. first_month \u2013 The calendar month (starting with 1) with which the year opens. Source code in beancount/web/views.py def __init__(self, entries, options_map, title, year, first_month=1): \"\"\"Create a view clamped to one year. Note: this is the only view where the entries are summarized and clamped. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. title: A string, the title of this view. year: An integer, the year of the exercise period. first_month: The calendar month (starting with 1) with which the year opens. \"\"\" self.year = year self.first_month = first_month if not (1 <= first_month <= 12): raise ValueError(\"Invalid month: {}\".format(first_month)) View.__init__(self, entries, options_map, title) self.monthly = MonthNavigation.COMPACT","title":"__init__()"},{"location":"api_reference/beancount.web.html#beancount.web.views.YearView.apply_filter","text":"Filter the list of entries. This is used to obtain the filtered list of entries. Parameters: entries \u2013 A list of directives to filter. Returns: A pair of 1. a list of filtered entries, and 2. an integer, the index at which the beginning of the entries for the period begin, one directive past the opening balances/initialization entries. 3. a datetime.date instance, the date at which to evaluate the value of assets held at cost. Source code in beancount/web/views.py def apply_filter(self, entries, options_map): # Clamp to the desired period. begin_date = datetime.date(self.year, self.first_month, 1) end_date = datetime.date(self.year+1, self.first_month, 1) with misc_utils.log_time('clamp', logging.info): entries, index = summarize.clamp_opt(entries, begin_date, end_date, options_map) return entries, index, end_date","title":"apply_filter()"},{"location":"api_reference/beancount.web.html#beancount.web.web","text":"Web server for Beancount ledgers. This uses the Bottle single-file micro web framework (with no plugins).","title":"web"},{"location":"api_reference/beancount.web.html#beancount.web.web.HTMLFormatter","text":"A formatter object that can be used to render accounts links. Attributes: Name Type Description build_url A function used to render links to a Bottle application. leafonly a boolean, if true, render only the name of the leaf nodes.","title":"HTMLFormatter"},{"location":"api_reference/beancount.web.html#beancount.web.web.HTMLFormatter.build_global","text":"Render to global application. Source code in beancount/web/web.py def build_global(self, *args, **kwds): \"Render to global application.\" return app.router.build(*args, **kwds)","title":"build_global()"},{"location":"api_reference/beancount.web.html#beancount.web.web.HTMLFormatter.render_account","text":"See base class. Source code in beancount/web/web.py def render_account(self, account_name): \"\"\"See base class.\"\"\" if self.view_links: if self.leaf_only: # Calculate the number of components to figure out the indent to # render at. components = account.split(account_name) indent = '{:.1f}'.format(len(components) * self.EMS_PER_COMPONENT) anchor = '{}'.format( self.build_url('journal', account_name=self.account_xform.render(account_name)), account.leaf(account_name)) return '{}'.format( indent, anchor) else: anchor = '{}'.format( self.build_url('journal', account_name=self.account_xform.render(account_name)), account_name) return '{}'.format(anchor) else: if self.leaf_only: # Calculate the number of components to figure out the indent to # render at. components = account.split(account_name) indent = '{:.1f}'.format(len(components) * self.EMS_PER_COMPONENT) account_name = account.leaf(account_name) return '{}'.format( indent, account_name) else: return '{}'.format(account_name)","title":"render_account()"},{"location":"api_reference/beancount.web.html#beancount.web.web.HTMLFormatter.render_commodity","text":"See base class. Source code in beancount/web/web.py def render_commodity(self, base_quote): \"\"\"See base class.\"\"\" base, quote = base_quote return '{} / {}'.format( self.build_url('prices', base=base, quote=quote), base, quote)","title":"render_commodity()"},{"location":"api_reference/beancount.web.html#beancount.web.web.HTMLFormatter.render_context","text":"See base class. Source code in beancount/web/web.py def render_context(self, entry): \"\"\"See base class.\"\"\" # Note: rendering to global application. # Note(2): we could avoid rendering links to summarizing and transfer # entries which are not going to be found. return self.build_global('context', ehash=compare.hash_entry(entry))","title":"render_context()"},{"location":"api_reference/beancount.web.html#beancount.web.web.HTMLFormatter.render_doc","text":"See base class. Source code in beancount/web/web.py def render_doc(self, filename): \"\"\"See base class.\"\"\" return '{}'.format( self.build_url('doc', filename=filename.lstrip('/')), path.basename(filename))","title":"render_doc()"},{"location":"api_reference/beancount.web.html#beancount.web.web.HTMLFormatter.render_event_type","text":"See base class. Source code in beancount/web/web.py def render_event_type(self, event): \"\"\"See base class.\"\"\" return '{}'.format(self.build_url('event', event=event), event)","title":"render_event_type()"},{"location":"api_reference/beancount.web.html#beancount.web.web.HTMLFormatter.render_inventory","text":"Override this formatter to convert the inventory to units only. Source code in beancount/web/web.py def render_inventory(self, inv): \"\"\"Override this formatter to convert the inventory to units only.\"\"\" return super().render_inventory(inv.reduce(convert.get_units))","title":"render_inventory()"},{"location":"api_reference/beancount.web.html#beancount.web.web.HTMLFormatter.render_link","text":"See base class. Source code in beancount/web/web.py def render_link(self, link): \"\"\"See base class.\"\"\" # Note: rendering to global application. return self.build_global('link', link=link)","title":"render_link()"},{"location":"api_reference/beancount.web.html#beancount.web.web.HTMLFormatter.render_source","text":"See base class. Source code in beancount/web/web.py def render_source(self, source): \"\"\"See base class.\"\"\" return '{}#{}'.format(app.get_url('source'), source['lineno'])","title":"render_source()"},{"location":"api_reference/beancount.web.html#beancount.web.web.activity","text":"Render the update activity. Source code in beancount/web/web.py @viewapp.route('/activity', name='activity') def activity(): \"Render the update activity.\" return render_global( pagetitle=\"Update Activity\", contents=render_real_report(misc_reports.ActivityReport, request.view.real_accounts, app.price_map, request.view.price_date, leaf_only=False))","title":"activity()"},{"location":"api_reference/beancount.web.html#beancount.web.web.auto_reload_input_file","text":"A plugin that automatically reloads the input file if it changed since the last page was loaded. Source code in beancount/web/web.py def auto_reload_input_file(callback): \"\"\"A plugin that automatically reloads the input file if it changed since the last page was loaded.\"\"\" def wrapper(*posargs, **kwargs): filename = app.args.filename if loader.needs_refresh(app.options): logging.info('Reloading...') # Save the source for later, to render. with open(filename, encoding='utf8') as f: app.source = f.read() # Parse the beancount file. entries, errors, options_map = loader.load_file(filename) # Print out the list of errors. if errors: # pylint: disable=unsupported-assignment-operation request.params['render_overlay'] = True print(',----------------------------------------------------------------') printer.print_errors(errors, file=sys.stdout) print('`----------------------------------------------------------------') # Save globals in the global app. app.entries = entries app.errors = errors app.options = options_map app.account_types = options.get_account_types(options_map) # Pre-compute the price database. app.price_map = prices.build_price_map(entries) # Pre-compute the list of active years. app.active_years = list(getters.get_active_years(entries)) # Reset the view cache. app.views.clear() else: # For now, the overlay is a link to the errors page. Always render # it on the right when there are errors. if app.errors: # pylint: disable=unsupported-assignment-operation request.params['render_overlay'] = True return callback(*posargs, **kwargs) return wrapper","title":"auto_reload_input_file()"},{"location":"api_reference/beancount.web.html#beancount.web.web.balsheet","text":"Balance sheet. Source code in beancount/web/web.py @viewapp.route('/balsheet', name='balsheet') def balsheet(): \"Balance sheet.\" return render_view(pagetitle=\"Balance Sheet\", contents=render_real_report(balance_reports.BalanceSheetReport, request.view.closing_real_accounts, app.price_map, request.view.price_date, leaf_only=True))","title":"balsheet()"},{"location":"api_reference/beancount.web.html#beancount.web.web.commodities","text":"Render a list commodities with list their prices page. Source code in beancount/web/web.py @viewapp.route('/commodities', name='commodities') def commodities(): \"Render a list commodities with list their prices page.\" html_table = render_report(price_reports.CommoditiesReport, request.view.entries, [], css_id='price-index') return render_view( pagetitle=\"Commodities\", contents=html_table)","title":"commodities()"},{"location":"api_reference/beancount.web.html#beancount.web.web.context_","text":"Render the before & after context around a transaction entry. Source code in beancount/web/web.py @app.route('/context/', name='context') def context_(ehash=None): \"Render the before & after context around a transaction entry.\" matching_entries = [entry for entry in app.entries if ehash == compare.hash_entry(entry)] oss = io.StringIO() if len(matching_entries) == 0: print(\"ERROR: Could not find matching entry for '{}'\".format(ehash), file=oss) elif len(matching_entries) > 1: print(\"ERROR: Ambiguous entries for '{}'\".format(ehash), file=oss) print(file=oss) dcontext = app.options['dcontext'] printer.print_entries(matching_entries, dcontext, file=oss) else: entry = matching_entries[0] # Render the context. oss.write(\"
    \\n\") oss.write(context.render_entry_context(app.entries, app.options, entry)) oss.write(\"
    \\n\") # Render the filelinks. if FILELINK_PROTOCOL: meta = entry.meta uri = FILELINK_PROTOCOL.format(filename=meta.get('filename'), lineno=meta.get('lineno')) oss.write(''.format(uri, 'Open')) return render_global( pagetitle=\"Context: {}\".format(ehash), contents=oss.getvalue())","title":"context_()"},{"location":"api_reference/beancount.web.html#beancount.web.web.conversions_","text":"Render the list of transactions with conversions. Source code in beancount/web/web.py @viewapp.route('/conversions', name='conversions') def conversions_(): \"Render the list of transactions with conversions.\" return render_view( pagetitle=\"Conversions\", contents=render_report(journal_reports.ConversionsReport, request.view.entries, leaf_only=False))","title":"conversions_()"},{"location":"api_reference/beancount.web.html#beancount.web.web.doc","text":"Serve static filenames for documents directives. Source code in beancount/web/web.py @app.route('/doc/', name=doc_name) def doc(filename=None): \"Serve static filenames for documents directives.\" filename = '/' + filename # Check that there is a document directive that has this filename. # This is for security; we don't want to be able to serve just any file. for entry in misc_utils.filter_type(app.entries, data.Document): if entry.filename == filename: break else: raise bottle.HTTPError(404, \"Not found\") # Just serve the file ourselves. return bottle.static_file(path.basename(filename), path.dirname(filename))","title":"doc()"},{"location":"api_reference/beancount.web.html#beancount.web.web.documents","text":"Render a tree with all the documents found. Source code in beancount/web/web.py @viewapp.route('/documents', name='documents') def documents(): \"Render a tree with all the documents found.\" return render_view( pagetitle=\"Documents\", contents=render_report(journal_reports.DocumentsReport, request.view.entries, leaf_only=False))","title":"documents()"},{"location":"api_reference/beancount.web.html#beancount.web.web.errors","text":"Report error encountered during parsing, checking and realization. Source code in beancount/web/web.py @app.route('/errors', name='errors') def errors(): \"Report error encountered during parsing, checking and realization.\" return render_global( pagetitle=\"Errors\", contents=render_report(misc_reports.ErrorReport, [], leaf_only=False))","title":"errors()"},{"location":"api_reference/beancount.web.html#beancount.web.web.event","text":"Render all values of a particular event. Source code in beancount/web/web.py @viewapp.route(r'/event/', name='event') def event(event=None): \"Render all values of a particular event.\" if not event: bottle.redirect(app.get_url('event_index')) return render_view( pagetitle=\"Event: {}\".format(event), contents=render_report(misc_reports.EventsReport, app.entries, ['--expr', event]))","title":"event()"},{"location":"api_reference/beancount.web.html#beancount.web.web.event_index","text":"Render the latest values of all events and an index. Source code in beancount/web/web.py @viewapp.route('/event', name='event_index') def event_index(): \"Render the latest values of all events and an index.\" return render_view( pagetitle=\"Events Index\", contents=render_report(misc_reports.CurrentEventsReport, request.view.entries))","title":"event_index()"},{"location":"api_reference/beancount.web.html#beancount.web.web.get_all_view","text":"Return a view of all transactions. Returns: An instance of AllView, that covers all transactions. Source code in beancount/web/web.py def get_all_view(app): \"\"\"Return a view of all transactions. Returns: An instance of AllView, that covers all transactions. \"\"\" return views.AllView(app.entries, app.options, 'All Transactions')","title":"get_all_view()"},{"location":"api_reference/beancount.web.html#beancount.web.web.handle_view","text":"A decorator for handlers which create views lazily. If you decorate a method with this, the wrapper does the redirect handling and your method is just a factory for a View instance, which is cached. Parameters: path_depth \u2013 An integer, the number of components that form the view id. Returns: A decorator function, used to wrap view handlers. Source code in beancount/web/web.py def handle_view(path_depth): \"\"\"A decorator for handlers which create views lazily. If you decorate a method with this, the wrapper does the redirect handling and your method is just a factory for a View instance, which is cached. Args: path_depth: An integer, the number of components that form the view id. Returns: A decorator function, used to wrap view handlers. \"\"\" def view_populator(callback): def wrapper(*args, **kwargs): components = request.path.split('/') viewid = '/'.join(components[:path_depth+1]) try: # Try fetching the view from the cache. view = app.views[viewid] except KeyError: # We need to create the view. view = app.views[viewid] = callback(*args, **kwargs) # Save the view for the subrequest and redirect. populate_view() # picks this up and saves it in request.view. request.environ['VIEW'] = view return bottle_utils.internal_redirect(viewapp, path_depth) return wrapper return view_populator","title":"handle_view()"},{"location":"api_reference/beancount.web.html#beancount.web.web.holdings_","text":"Render a detailed table of all holdings. Source code in beancount/web/web.py @viewapp.route('/equity/holdings', name='holdings') def holdings_(): \"Render a detailed table of all holdings.\" html_table = render_report(holdings_reports.HoldingsReport, request.view.entries, [], css_class='holdings detail-table sortable', center=True) return render_view( pagetitle=\"Holdings\", contents=html_table, scripts='')","title":"holdings_()"},{"location":"api_reference/beancount.web.html#beancount.web.web.holdings_byaccount","text":"Render a table of holdings by account. Source code in beancount/web/web.py @viewapp.route('/equity/holdings_byaccount', name='holdings_byaccount') def holdings_byaccount(): \"Render a table of holdings by account.\" html_table = render_report(holdings_reports.HoldingsReport, request.view.entries, ['--by', 'account'], css_class='holdings detail-table sortable', center=True) return render_view( pagetitle=\"Holdings by Account\", contents=html_table, scripts='')","title":"holdings_byaccount()"},{"location":"api_reference/beancount.web.html#beancount.web.web.holdings_bycommodity","text":"Render a table of holdings by commodity. Source code in beancount/web/web.py @viewapp.route('/equity/holdings_bycommodity', name='holdings_bycommodity') def holdings_bycommodity(): \"Render a table of holdings by commodity.\" html_table = render_report(holdings_reports.HoldingsReport, request.view.entries, ['--by', 'commodity'], css_class='holdings detail-table sortable', center=True) return render_view( pagetitle=\"Holdings by Commodity\", contents=html_table, scripts='')","title":"holdings_bycommodity()"},{"location":"api_reference/beancount.web.html#beancount.web.web.holdings_bycurrency","text":"Render a table of holdings by currency. Source code in beancount/web/web.py @viewapp.route('/equity/holdings_bycurrency', name='holdings_bycurrency') def holdings_bycurrency(): \"Render a table of holdings by currency.\" html_table = render_report(holdings_reports.HoldingsReport, request.view.entries, ['--by', 'currency'], css_class='holdings detail-table sortable', center=True) return render_view( pagetitle=\"Holdings by Currency\", contents=html_table, scripts='')","title":"holdings_bycurrency()"},{"location":"api_reference/beancount.web.html#beancount.web.web.holdings_byrootaccount","text":"Render a table of holdings by account. Source code in beancount/web/web.py @viewapp.route('/equity/holdings_byrootaccount', name='holdings_byrootaccount') def holdings_byrootaccount(): \"Render a table of holdings by account.\" html_table = render_report(holdings_reports.HoldingsReport, request.view.entries, ['--by', 'root-account'], css_class='holdings detail-table sortable', center=True) return render_view( pagetitle=\"Holdings by Account\", contents=html_table, scripts='')","title":"holdings_byrootaccount()"},{"location":"api_reference/beancount.web.html#beancount.web.web.incognito","text":"A plugin that converts all numbers rendered into X's, in order to hide the actual values in the ledger. This is used for doing public demos using my real ledger, where I don't necessarily want to share the detail of my financial life with the viewers but when I still want an interesting ledger, with enough detail that looks realistic. Source code in beancount/web/web.py def incognito(callback): \"\"\"A plugin that converts all numbers rendered into X's, in order to hide the actual values in the ledger. This is used for doing public demos using my real ledger, where I don't necessarily want to share the detail of my financial life with the viewers but when I still want an interesting ledger, with enough detail that looks realistic.\"\"\" def wrapper(*posargs, **kwargs): contents = callback(*posargs, **kwargs) # pylint: disable=bad-continuation if (response.content_type in ('text/html', '') and isinstance(contents, str)): contents = text_utils.replace_numbers(contents) return contents return wrapper","title":"incognito()"},{"location":"api_reference/beancount.web.html#beancount.web.web.income","text":"Income statement. Source code in beancount/web/web.py @viewapp.route('/income', name='income') def income(): \"Income statement.\" return render_view(pagetitle=\"Income Statement\", contents=render_real_report(balance_reports.IncomeStatementReport, request.view.real_accounts, app.price_map, request.view.price_date, leaf_only=True))","title":"income()"},{"location":"api_reference/beancount.web.html#beancount.web.web.index","text":"Index of all pages, so that navigation need not have all links. Source code in beancount/web/web.py @viewapp.route('/index', name='index') def index(): \"Index of all pages, so that navigation need not have all links.\" oss = io.StringIO() oss.write('
      \\n') for title, page in [ (\"Balances\", \"trial\"), (\"Balance Sheet\", \"balsheet\"), (\"Opening Balances\", \"openbal\"), (\"Income Statement\", \"income\"), (\"General Journal\", \"journal_all\"), (\"Conversions\", \"conversions\"), (\"Documents\", \"documents\"), (\"Holdings (Full Detail)\", \"holdings\"), (\"Holdings by Account\", \"holdings_byaccount\"), (\"Holdings by Root Account\", \"holdings_byrootaccount\"), (\"Holdings by Commodity\", \"holdings_bycommodity\"), (\"Holdings by Currency\", \"holdings_bycurrency\"), (\"Net Worth\", \"networth\"), (\"Commodities\", \"commodities\"), (\"Events\", \"event_index\"), (\"Activity/Update\", \"activity\"), (\"Statistics (Types)\", \"stats_types\"), (\"Statistics (Postings)\", \"stats_postings\"), ]: oss.write('
    • {}
    • \\n'.format( request.app.get_url(page), title)) oss.write('
    \\n') return render_view( pagetitle=\"Index\", contents=oss.getvalue())","title":"index()"},{"location":"api_reference/beancount.web.html#beancount.web.web.journal_","text":"A list of all the entries for this account realization. Source code in beancount/web/web.py @viewapp.route('/journal/', name='journal') def journal_(account_name=None): \"A list of all the entries for this account realization.\" account_name = app.account_xform.parse(account_name) # Figure out which account to render this from. real_accounts = request.view.real_accounts if account_name: if account_name and account_types.is_balance_sheet_account(account_name, app.account_types): real_accounts = request.view.closing_real_accounts # Render the report. args = [] if account_name: args.append('--account={}'.format(account_name)) render_postings = request.params.get('postings', True) if isinstance(render_postings, str): render_postings = render_postings.lower() in ('1', 'true') if render_postings: args.append('--verbose') try: html_journal = render_real_report(journal_reports.JournalReport, real_accounts, app.price_map, request.view.price_date, args, leaf_only=False) except KeyError as e: raise bottle.HTTPError(404, '{}'.format(e)) return render_view(pagetitle='{}'.format(account_name or 'General Ledger (All Accounts)'), contents=html_journal)","title":"journal_()"},{"location":"api_reference/beancount.web.html#beancount.web.web.journal_all","text":"A list of all the entries in this realization. Source code in beancount/web/web.py @viewapp.route('/journal/all', name='journal_all') def journal_all(): \"A list of all the entries in this realization.\" bottle.redirect(request.app.get_url('journal', account_name=''))","title":"journal_all()"},{"location":"api_reference/beancount.web.html#beancount.web.web.link","text":"Serve journals for links. Source code in beancount/web/web.py @app.route('/link/', name='link') def link(link=None): \"Serve journals for links.\" linked_entries = basicops.filter_link(link, app.entries) oss = io.StringIO() formatter = HTMLFormatter(app.options['dcontext'], request.app.get_url, False, app.account_xform, view_links=False) journal_html.html_entries_table_with_balance(oss, linked_entries, formatter) return render_global( pagetitle=\"Link: {}\".format(link), contents=oss.getvalue())","title":"link()"},{"location":"api_reference/beancount.web.html#beancount.web.web.main","text":"Main web service runner. This runs the event loop and blocks indefinitely. Source code in beancount/web/web.py def main(): \"\"\"Main web service runner. This runs the event loop and blocks indefinitely.\"\"\" argparser = version.ArgumentParser(description=__doc__.strip()) add_web_arguments(argparser) args = argparser.parse_args() run_app(args)","title":"main()"},{"location":"api_reference/beancount.web.html#beancount.web.web.month_request","text":"Render a URL to a particular month of the context's request. Source code in beancount/web/web.py def month_request(year, month): \"\"\"Render a URL to a particular month of the context's request. \"\"\" if year < app.active_years[0] or year > app.active_years[-1]: return '' month = list(calendar.month_abbr).index(month) month = \"{:0>2d}\".format(month) return app.router.build('month', year=year, month=month, path=request.path[1:])","title":"month_request()"},{"location":"api_reference/beancount.web.html#beancount.web.web.networth","text":"Render a table of the net worth for this filter. Source code in beancount/web/web.py @viewapp.route('/equity/networth', name='networth') def networth(): \"Render a table of the net worth for this filter.\" html_table = render_report(holdings_reports.NetWorthReport, request.view.entries) return render_view( pagetitle=\"Net Worth\", contents=html_table)","title":"networth()"},{"location":"api_reference/beancount.web.html#beancount.web.web.openbal","text":"Opening balances. Source code in beancount/web/web.py @viewapp.route('/openbal', name='openbal') def openbal(): \"Opening balances.\" return render_view(pagetitle=\"Opening Balances\", contents=render_real_report(balance_reports.BalanceSheetReport, request.view.opening_real_accounts, app.price_map, request.view.price_date, leaf_only=True))","title":"openbal()"},{"location":"api_reference/beancount.web.html#beancount.web.web.populate_view","text":"A plugin that will populate the request with the current view instance. Parameters: callback \u2013 A continuation function to call to handle the request. Returns: A function to call to install view-specific parameters on the request. Source code in beancount/web/web.py def populate_view(callback): \"\"\"A plugin that will populate the request with the current view instance. Args: callback: A continuation function to call to handle the request. Returns: A function to call to install view-specific parameters on the request. \"\"\" def wrapper(*args, **kwargs): request.view = request.environ['VIEW'] return callback(*args, **kwargs) return wrapper","title":"populate_view()"},{"location":"api_reference/beancount.web.html#beancount.web.web.prices_values","text":"Render all the values for a particular price pair. Source code in beancount/web/web.py @viewapp.route('/prices' r'/' r'/', name='prices') def prices_values(base=None, quote=None): \"Render all the values for a particular price pair.\" html_table = render_report(price_reports.CommodityPricesReport, request.view.entries, ['--commodity', '{}/{}'.format(base, quote)], css_id='price-index') return render_view( pagetitle=\"Price: {} / {}\".format(base, quote), contents=html_table)","title":"prices_values()"},{"location":"api_reference/beancount.web.html#beancount.web.web.render_global","text":"Render the title and contents in our standard template for a global page. Parameters: *args \u2013 A tuple of values for the HTML template. *kw \u2013 A dict of optional values for the HTML template. Returns: An HTML string of the rendered template. Source code in beancount/web/web.py def render_global(*args, **kw): \"\"\"Render the title and contents in our standard template for a global page. Args: *args: A tuple of values for the HTML template. *kw: A dict of optional values for the HTML template. Returns: An HTML string of the rendered template. \"\"\" response.content_type = 'text/html' kw['A'] = A # Application mapper kw['V'] = V # View mapper kw['title'] = app.options['title'] kw['view_title'] = '' kw['navigation'] = GLOBAL_NAVIGATION kw['scripts'] = kw.get('scripts', '') kw['overlay'] = ( render_overlay('
  • Errors
  • '.format( app.router.build('errors'))) if request.params.pop('render_overlay', True) else '') return template.render(*args, **kw)","title":"render_global()"},{"location":"api_reference/beancount.web.html#beancount.web.web.render_overlay","text":"Render an overlay of the navigation with the current errors. This is used to bring up new errors on any page when they occur. Returns: A string of HTML for the contents of the errors overlay. Source code in beancount/web/web.py def render_overlay(contents): \"\"\"Render an overlay of the navigation with the current errors. This is used to bring up new errors on any page when they occur. Returns: A string of HTML for the contents of the errors overlay. \"\"\" return '''
      {}
    '''.format(contents) # It would be nice to have a fancy overlay here, that automatically appears # after parsing if there are errors and that automatically smoothly fades # out.","title":"render_overlay()"},{"location":"api_reference/beancount.web.html#beancount.web.web.render_real_report","text":"Instantiate a report and rendering it to a string. This is intended to be called in the context of a Bottle view app request (it uses 'request'). Parameters: report_class \u2013 A class, the type of the report to render. real_root \u2013 An instance of RealAccount to render. price_map \u2013 A price map as built by build_price_map(). price_date \u2013 The date at which to evaluate the prices. args \u2013 A list of strings, the arguments to initialize the report with. leaf_only \u2013 A boolean, whether to render the leaf names only. Returns: A string, the rendered report. Source code in beancount/web/web.py def render_real_report(report_class, real_root, price_map, price_date, args=None, leaf_only=False): # pylint: disable=too-many-arguments \"\"\"Instantiate a report and rendering it to a string. This is intended to be called in the context of a Bottle view app request (it uses 'request'). Args: report_class: A class, the type of the report to render. real_root: An instance of RealAccount to render. price_map: A price map as built by build_price_map(). price_date: The date at which to evaluate the prices. args: A list of strings, the arguments to initialize the report with. leaf_only: A boolean, whether to render the leaf names only. Returns: A string, the rendered report. \"\"\" formatter = HTMLFormatter(app.options['dcontext'], request.app.get_url, leaf_only, app.account_xform) oss = io.StringIO() report_ = report_class.from_args(args, formatter=formatter) report_.render_real_htmldiv(real_root, price_map, price_date, app.options, oss) return oss.getvalue()","title":"render_real_report()"},{"location":"api_reference/beancount.web.html#beancount.web.web.render_report","text":"Instantiate a report and rendering it to a string. Parameters: report_class \u2013 A class, the type of the report to render. real_root \u2013 An instance of RealAccount to render. args \u2013 A list of strings, the arguments to initialize the report with. css_id \u2013 An optional string, the CSS id for the div to render. css_class \u2013 An optional string, the CSS class for the div to render. center \u2013 A boolean flag, if true, wrap the results in a
    tag. leaf_only \u2013 A boolean, whether to render the leaf names only. Returns: A string, the rendered report. Source code in beancount/web/web.py def render_report(report_class, entries, args=None, css_id=None, css_class=None, center=False, leaf_only=True): \"\"\"Instantiate a report and rendering it to a string. Args: report_class: A class, the type of the report to render. real_root: An instance of RealAccount to render. args: A list of strings, the arguments to initialize the report with. css_id: An optional string, the CSS id for the div to render. css_class: An optional string, the CSS class for the div to render. center: A boolean flag, if true, wrap the results in a
    tag. leaf_only: A boolean, whether to render the leaf names only. Returns: A string, the rendered report. \"\"\" formatter = HTMLFormatter(app.options['dcontext'], request.app.get_url, leaf_only, app.account_xform) oss = io.StringIO() if center: oss.write('
    \\n') report_ = report_class.from_args(args, formatter=formatter, css_id=css_id, css_class=css_class) report_.render_htmldiv(entries, app.errors, app.options, oss) if center: oss.write('
    \\n') return oss.getvalue()","title":"render_report()"},{"location":"api_reference/beancount.web.html#beancount.web.web.render_view","text":"Render the title and contents in our standard template for a view page. Parameters: *args \u2013 A tuple of values for the HTML template. *kw \u2013 A dict of optional values for the HTML template. Returns: An HTML string of the rendered template. Source code in beancount/web/web.py def render_view(*args, **kw): \"\"\"Render the title and contents in our standard template for a view page. Args: *args: A tuple of values for the HTML template. *kw: A dict of optional values for the HTML template. Returns: An HTML string of the rendered template. \"\"\" response.content_type = 'text/html' kw['A'] = A # Application mapper kw['V'] = V # View mapper kw['title'] = app.options['title'] kw['view_title'] = ' - ' + request.view.title overlays = [] if request.params.pop('render_overlay', False): overlays.append( '
  • Errors
  • '.format(app.router.build('errors'))) # Render navigation, with monthly navigation option. oss = io.StringIO() oss.write(APP_NAVIGATION.render(A=A, V=V, view_title=request.view.title)) if request.view.monthly is views.MonthNavigation.COMPACT: overlays.append( '
  • Monthly
  • '.format(M.Jan)) elif request.view.monthly is views.MonthNavigation.FULL: annual = app.router.build('year', path=DEFAULT_VIEW_REDIRECT, year=request.view.year) oss.write(APP_NAVIGATION_MONTHLY_FULL.render(M=M, Mp=Mp, Mn=Mn, V=V, annual=annual)) kw['navigation'] = oss.getvalue() kw['overlay'] = render_overlay(' '.join(overlays)) kw['scripts'] = kw.get('scripts', '') return template.render(*args, **kw)","title":"render_view()"},{"location":"api_reference/beancount.web.html#beancount.web.web.root","text":"Redirect the root page to the home page. Source code in beancount/web/web.py @app.route('/', name='root') def root(): \"Redirect the root page to the home page.\" bottle.redirect(app.get_url('toc'))","title":"root()"},{"location":"api_reference/beancount.web.html#beancount.web.web.scrape_webapp","text":"Run a web server on a Beancount file and scrape it. This is the main entry point of this module. Parameters: webargs \u2013 An argparse.Namespace container of the arguments provided in web.add_web_arguments(). callback \u2013 A callback function to invoke on each page to validate it. The function is called with the response and the url as arguments. This function should trigger an error on failure (via an exception). ignore_regexp \u2013 A regular expression string, the urls to ignore. Returns: A set of all the processed URLs and a set of all the skipped URLs. Source code in beancount/web/web.py def scrape_webapp(webargs, callback, ignore_regexp): \"\"\"Run a web server on a Beancount file and scrape it. This is the main entry point of this module. Args: webargs: An argparse.Namespace container of the arguments provided in web.add_web_arguments(). callback: A callback function to invoke on each page to validate it. The function is called with the response and the url as arguments. This function should trigger an error on failure (via an exception). ignore_regexp: A regular expression string, the urls to ignore. Returns: A set of all the processed URLs and a set of all the skipped URLs. \"\"\" url_format = 'http://localhost:{}{{}}'.format(webargs.port) thread = thread_server_start(webargs) # Skips: # - Docs cannot be read for external files. # # - Components views... well there are just too many, makes the tests # impossibly slow. Just keep the A's so some are covered. url_lists = scrape.scrape_urls(url_format, callback, ignore_regexp) thread_server_shutdown(thread) return url_lists","title":"scrape_webapp()"},{"location":"api_reference/beancount.web.html#beancount.web.web.setup_monkey_patch_for_server_shutdown","text":"Setup globals to steal access to the server reference. This is required to initiate shutdown, unfortunately. (Bottle could easily remedy that.) Source code in beancount/web/web.py def setup_monkey_patch_for_server_shutdown(): \"\"\"Setup globals to steal access to the server reference. This is required to initiate shutdown, unfortunately. (Bottle could easily remedy that.)\"\"\" # Save the original function. # pylint: disable=import-outside-toplevel from wsgiref.simple_server import make_server # Create a decorator that will save the server upon start. def stealing_make_server(*args, **kw): global server server = make_server(*args, **kw) return server # Patch up wsgiref itself with the decorated function. import wsgiref.simple_server wsgiref.simple_server.make_server = stealing_make_server","title":"setup_monkey_patch_for_server_shutdown()"},{"location":"api_reference/beancount.web.html#beancount.web.web.shutdown","text":"Request for the server to shutdown. Source code in beancount/web/web.py def shutdown(): \"\"\"Request for the server to shutdown.\"\"\" server.shutdown()","title":"shutdown()"},{"location":"api_reference/beancount.web.html#beancount.web.web.source","text":"Render the source file, allowing scrolling at a specific line. Source code in beancount/web/web.py @app.route('/source', name='source') def source(): \"Render the source file, allowing scrolling at a specific line.\" contents = io.StringIO() if app.args.no_source: contents.write(\"Source hidden.\") else: contents.write('
    ') for i, line in enumerate(app.source.splitlines()): lineno = i+1 contents.write( '
    {lineno} {line}
    \\n'.format( lineno=lineno, line=line.rstrip())) contents.write('
    ') return render_global( pagetitle=\"Source\", contents=contents.getvalue() )","title":"source()"},{"location":"api_reference/beancount.web.html#beancount.web.web.stats_postings","text":"Compute and render statistics about the input file. Source code in beancount/web/web.py @viewapp.route('/stats_postings', name='stats_postings') def stats_postings(): \"Compute and render statistics about the input file.\" return render_view( pagetitle=\"Postings Statistics\", contents=render_report(misc_reports.StatsPostingsReport, request.view.entries))","title":"stats_postings()"},{"location":"api_reference/beancount.web.html#beancount.web.web.stats_types","text":"Compute and render statistics about the input file. Source code in beancount/web/web.py @viewapp.route('/stats_types', name='stats_types') def stats_types(): \"Compute and render statistics about the input file.\" return render_view( pagetitle=\"Directives Statistics\", contents=render_report(misc_reports.StatsDirectivesReport, request.view.entries))","title":"stats_types()"},{"location":"api_reference/beancount.web.html#beancount.web.web.style","text":"Stylesheet for the entire document. Source code in beancount/web/web.py @app.route('/resources/web.css', name='style') def style(): \"Stylesheet for the entire document.\" response.content_type = 'text/css' if app.args.debug: with open(path.join(path.dirname(__file__), 'web.css')) as f: global STYLE; STYLE = f.read() return STYLE","title":"style()"},{"location":"api_reference/beancount.web.html#beancount.web.web.thread_server_shutdown","text":"Shutdown the server running in the given thread. Unfortunately, in the meantime this has a side-effect on all servers. This returns after waiting that the thread has stopped. Parameters: thread \u2013 A threading.Thread instance. Source code in beancount/web/web.py def thread_server_shutdown(thread): \"\"\"Shutdown the server running in the given thread. Unfortunately, in the meantime this has a side-effect on all servers. This returns after waiting that the thread has stopped. Args: thread: A threading.Thread instance. \"\"\" # Clean shutdown: request to stop, then join the thread. # Note that because we daemonize, we could forego this elegant detail. shutdown() thread.join()","title":"thread_server_shutdown()"},{"location":"api_reference/beancount.web.html#beancount.web.web.thread_server_start","text":"Start a server in a new thread. Parameters: argparse_args \u2013 An argparse parsed options object, with all the options from add_web_arguments(). Returns: A new Thread instance. Source code in beancount/web/web.py def thread_server_start(web_args, **kwargs): \"\"\"Start a server in a new thread. Args: argparse_args: An argparse parsed options object, with all the options from add_web_arguments(). Returns: A new Thread instance. \"\"\" thread = threading.Thread( target=run_app, args=(web_args,), kwargs=kwargs) thread.daemon = True # Automatically exit if the process comes dwn. thread.start() # Ensure the server has at least started before running the scraper. wait_ready() time.sleep(0.1) return thread","title":"thread_server_start()"},{"location":"api_reference/beancount.web.html#beancount.web.web.trial","text":"Trial balance / Chart of Accounts. Source code in beancount/web/web.py @viewapp.route('/trial', name='trial') def trial(): \"Trial balance / Chart of Accounts.\" return render_view( pagetitle=\"Trial Balance\", contents=render_real_report(balance_reports.BalancesReport, request.view.real_accounts, app.price_map, request.view.price_date, leaf_only=True))","title":"trial()"},{"location":"api_reference/beancount.web.html#beancount.web.web.url_restrict_generator","text":"Restrict to only a single prefix. Parameters: url_prefix \u2013 A string, a URL prefix to restrict to. callback \u2013 The function to wrap. Returns: A handler decorator. Source code in beancount/web/web.py def url_restrict_generator(url_prefix): \"\"\"Restrict to only a single prefix. Args: url_prefix: A string, a URL prefix to restrict to. callback: The function to wrap. Returns: A handler decorator. \"\"\" # A list of URLs that should always be accepted, even when restricted. allowed_regexps = [re.compile(regexp).match for regexp in ['/resources', '/favicon.ico', '/index', '/errors', '/source', '/context', '/third_party']] def url_restrict_handler(callback): def wrapper(*args, **kwargs): if (any(match(request.path) for match in allowed_regexps) or request.path.startswith(url_prefix)): return callback(*args, **kwargs) if request.path == '/': bottle.redirect(url_prefix) # Note: we issue a \"202 Accepted\" status in order to satisfy bean-bake, # we want to distinguish between an actual 404 error and this case. raise bottle.HTTPError(202, \"URLs restricted to '{}'\".format(url_prefix)) return wrapper return url_restrict_handler","title":"url_restrict_generator()"},{"location":"api_reference/beancount.web.html#beancount.web.web.wait_ready","text":"Wait until the 'server' global has been set. This tells us the server is running. Source code in beancount/web/web.py def wait_ready(): \"\"\"Wait until the 'server' global has been set. This tells us the server is running. \"\"\" while server is None: time.sleep(0.05)","title":"wait_ready()"}]} \ No newline at end of file +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"index.html","text":"Beancount User's Manual \uf0c1 This is the top-level page for all documentation related to Beancount. http://furius.ca/beancount/doc/index Documentation for Users \uf0c1 Command-line Accounting in Context : A motivational document that explains what command-line accounting is, why I do it, with a simple example of how I do it. Read this as an introduction. The Double-Entry Counting Method : A gentle introduction to the double-entry method, in terms compatible with the assumptions and simplifications that command-line accounting systems make. Installing Beancount : Instructions for download and installation on your computer, and software release information. Running Beancount and Generating Reports : How to run the Beancount executables and generate reports with it. This contains technical information for Beancount users. Getting Started with Beancount : A text that explains the basics of how to create, initialize and organize your input file, and the process of declaring accounts and adding padding directives. Beancount Language Syntax : A full description of the language syntax with examples. This is the main reference for the Beancount language. Beancount Options Reference : A description and explanation of all the possible option values. Precision & Tolerances : Transactions and Balance assertions tolerance small amounts of imprecision which are inferred from the context. This document explains how this works. Beancount Query Language : A high-level overview of the bean-query command-line client tool that allows you to extract various tables of data from your Beancount ledger file. Beancount Cheat Sheet : A single page \u201ccheat sheet\u201d that summarizes the Beancount syntax. How Inventories Work : An explanation of inventories, their representation, and the detail of how lots are matched to positions held in an inventory. This is preliminary reading for the trading doc. Exporting Your Portfolio : How to export your portfolio to external portfolio tracking websites. Tutorial & Example : A realistic example ledger file that contains a few years of a hypothetical Beancount user's financial life. This can be used to kick the tires on Beancount and see what reports or its web interface look like. This can give a quick idea of what you can get out of using Beancount. Beancount Mailing-list : Questions and discussions specific to Beancount occur there. Of related interest is also the Ledger-CLI Forum which contains lots of more general discussions and acts as a meta-list for command-line accounting topics. Beancount History and Credits: A description of the development history of Beancount and a shout out to its contributors. A Comparison of Beancount and Ledger & HLedger : A qualitative feature comparison between CLI accounting systems. Fetching Prices in Beancount : How to fetch and maintain a price database for your assets using Beancount\u2019s tools. Importing External Data : A description of the process of importing data from external files that can be downloaded from financial institutions and the tools and libraries that Beancount provides to automate some of this. Cookbook & Examples \uf0c1 These documents are examples of using the double-entry method to carry out specific tasks. Use these documents to develop an intuition for how to structure your accounts. Command-line Accounting Cookbook : Various examples of how to book many different kinds of financial transactions. This is undoubtedly the best way to build an intuition for how to best use the double-entry method. Trading with Beancount : An explanation of trading P/L and worked examples of how to deal with various investing and trading scenarios with Beancount. This is a complement to the cookbook. Stock Vesting in Beancount : An example that shows how to deal with restricted stock units and vesting events typical of those offered by technology companies & startups. Sharing Expenses with Beancount : A realistic and detailed example of using the double-entry method for sharing expenses for a trip or project with other people. How We Share Expenses : A more involved description of a continuous system for (a) sharing expenses between partners when both are contributing and (b) sharing expenses to a specific project (our son) who has a ledger of his own. This describes our real system. Health care Expenses (incomplete): A not-quite finished document explaining how to handle the sequence of health care expenses for in and out of network providers in the USA. Tracking Medical Claims : An example structure tracking out-of-network psychotherapy sessions with out-of-pocket payments, a medical claim, and an associated HSA repayment for the uncovered portion. Calculating Portolio Returns : How to compute portfolio returns from a Beancount ledger. This describes work done in an experimental script and the process that was involved in extracting the correct data for it. Documentation for Developers \uf0c1 Beancount Scripting & Plugins : A guide to writing scripts that load your ledger contents in memory and process the directives, and how to write plugins that transform them. Beancount Design Doc : Information about the program\u2019s architecture and design choices, code conventions, invariants and methodology. Read this if you want to get a deeper understanding. LedgerHub Design Doc : The design and architecture of the importing tools and library implemented in beancount.ingest . This used to be a separate project called LedgerHub (now defunct), whose useful parts have been eventually folded into Beancount. This is the somewhat dated original design doc. Source Code : The official repository of the Beancount source code lives at Github since May 2020. (From 2008 to May 2020 it was hosted at Bitbucket). External Contributions : A list of plugins, importers and other codes that build on Beancount\u2019s libraries that other people have made and shared. Enhancement Proposals & Discussions \uf0c1 I occasionally write proposals for enhancements in order to organize and summarize my thoughts before moving forward, and solicit feedback from other users. This is good material to find out what features I\u2019m planning to add, how things work in detail, or compare the differences with other similar software such as Ledger or HLedger. A Proposal for an Improvement on Inventory Booking : A proposal in which I outline the current state of affairs in Ledger and Beancount regarding inventory booking, and a better method for doing it that will support average cost booking and calculating capital gains without commissions. Settlement Dates in Beancount : A discussion of how settlement or auxiliary dates could be used in Beancount, and how they are used in Ledger. Balance Assertions in Beancount : A summary of the different semantics for making balance assertions in Beancount and Ledger and a proposal to extend Beancount\u2019s syntax to support more complex assertions. Fund Accounting with Beancount : A discussion of fund accounting and how best to go about using Beancount to track multiple funds in a single ledger. Rounding & Precision in Beancount : A discussion of important rounding and precision issues for balance checks and a proposal for a better method to infer required precision. ( Implemented ) External Links \uf0c1 Documents, links, blog entries, writings, etc. about Beancount written by other authors.] Beancount Source Code Documentation (Dominik Aumayr): Sphinx-generated source code documentation of the Beancount codebase. The code to produce this is located here . Beancount ou la comptabilit\u00e9 pour les hackers (Cyril Deguet): An overview blog entry (in french). V3 Documentation \uf0c1 As of summer 2020, a rewrite to C++ is underway. These are the documents related to that. Beancount V3 : Goals & Design: Motivation, goals and design for version 3 rewrite to C++. Beancount V3: Changes from V2 : A list of current changes presently done in v3 (on the \"master\" branch) whose documentation hasn't made it to the v2 documentation set. Installing Beancount (v3) : Installation procedure for v3. Beancount V3: Dependencies : Software dependencies for building version 3. Beangulp : The renewed ingestion framework for v3. About this Documentation \uf0c1 You may have noticed that I\u2019m using Google Docs . I realize that this is unusual for an open source project. If you take offense to this , so you know, I do like text formats too: in graduate school I used LaTeX extensively, and for the last decade I was in love with the reStructuredText format, I even wrote Emacs\u2019 support for it. But something happened around 2013: Google Docs became good enough to write solid technical documentation and I\u2019ve begun enjoying its revision and commenting facilities extensively: I am hooked, I love it. For users to be able to suggest a correction or place a comment in context is an incredibly useful feature that helps improve the quality of my writing a lot more than patches or comments on a mailing-list. It also gives users a chance to point out passages that need improvement. I have already received orders of magnitude more feedback on documentation than on any of my other projects; it works. It also looks really good , and this is helping motivate me to write more and better. I think maybe I\u2019m falling in love again with WYSIWYG... Don\u2019t get me wrong: LaTeX and no-markup formats are great, but the world is changing and it is more important to me to have community collaboration and a living document than a crufty TeX file slowly aging in my repository. My aim is to produce quality text that is easy to read, that prints nicely, and most especially these days, that can render well on a mobile device or a tablet so you can read it anywhere. I\u2019m also able to edit it while I\u2019m on the go, which is fun for jotting down ideas or making corrections. Finally, Google Docs has an API that provides access to the doc, so should it be needed, I could eventually download the meta-data and convert it all to another format. I don\u2019t like everything about it, but what it offers, I really do like. If you want to leave comments, I\u2019d appreciate it if you can log into your Google account while you read, so that your name appears next to your comment. Thank you! \u2014 Martin Blais","title":"Index"},{"location":"index.html#beancount-users-manual","text":"This is the top-level page for all documentation related to Beancount. http://furius.ca/beancount/doc/index","title":"Beancount User's Manual"},{"location":"index.html#documentation-for-users","text":"Command-line Accounting in Context : A motivational document that explains what command-line accounting is, why I do it, with a simple example of how I do it. Read this as an introduction. The Double-Entry Counting Method : A gentle introduction to the double-entry method, in terms compatible with the assumptions and simplifications that command-line accounting systems make. Installing Beancount : Instructions for download and installation on your computer, and software release information. Running Beancount and Generating Reports : How to run the Beancount executables and generate reports with it. This contains technical information for Beancount users. Getting Started with Beancount : A text that explains the basics of how to create, initialize and organize your input file, and the process of declaring accounts and adding padding directives. Beancount Language Syntax : A full description of the language syntax with examples. This is the main reference for the Beancount language. Beancount Options Reference : A description and explanation of all the possible option values. Precision & Tolerances : Transactions and Balance assertions tolerance small amounts of imprecision which are inferred from the context. This document explains how this works. Beancount Query Language : A high-level overview of the bean-query command-line client tool that allows you to extract various tables of data from your Beancount ledger file. Beancount Cheat Sheet : A single page \u201ccheat sheet\u201d that summarizes the Beancount syntax. How Inventories Work : An explanation of inventories, their representation, and the detail of how lots are matched to positions held in an inventory. This is preliminary reading for the trading doc. Exporting Your Portfolio : How to export your portfolio to external portfolio tracking websites. Tutorial & Example : A realistic example ledger file that contains a few years of a hypothetical Beancount user's financial life. This can be used to kick the tires on Beancount and see what reports or its web interface look like. This can give a quick idea of what you can get out of using Beancount. Beancount Mailing-list : Questions and discussions specific to Beancount occur there. Of related interest is also the Ledger-CLI Forum which contains lots of more general discussions and acts as a meta-list for command-line accounting topics. Beancount History and Credits: A description of the development history of Beancount and a shout out to its contributors. A Comparison of Beancount and Ledger & HLedger : A qualitative feature comparison between CLI accounting systems. Fetching Prices in Beancount : How to fetch and maintain a price database for your assets using Beancount\u2019s tools. Importing External Data : A description of the process of importing data from external files that can be downloaded from financial institutions and the tools and libraries that Beancount provides to automate some of this.","title":"Documentation for Users"},{"location":"index.html#cookbook-examples","text":"These documents are examples of using the double-entry method to carry out specific tasks. Use these documents to develop an intuition for how to structure your accounts. Command-line Accounting Cookbook : Various examples of how to book many different kinds of financial transactions. This is undoubtedly the best way to build an intuition for how to best use the double-entry method. Trading with Beancount : An explanation of trading P/L and worked examples of how to deal with various investing and trading scenarios with Beancount. This is a complement to the cookbook. Stock Vesting in Beancount : An example that shows how to deal with restricted stock units and vesting events typical of those offered by technology companies & startups. Sharing Expenses with Beancount : A realistic and detailed example of using the double-entry method for sharing expenses for a trip or project with other people. How We Share Expenses : A more involved description of a continuous system for (a) sharing expenses between partners when both are contributing and (b) sharing expenses to a specific project (our son) who has a ledger of his own. This describes our real system. Health care Expenses (incomplete): A not-quite finished document explaining how to handle the sequence of health care expenses for in and out of network providers in the USA. Tracking Medical Claims : An example structure tracking out-of-network psychotherapy sessions with out-of-pocket payments, a medical claim, and an associated HSA repayment for the uncovered portion. Calculating Portolio Returns : How to compute portfolio returns from a Beancount ledger. This describes work done in an experimental script and the process that was involved in extracting the correct data for it.","title":"Cookbook & Examples"},{"location":"index.html#documentation-for-developers","text":"Beancount Scripting & Plugins : A guide to writing scripts that load your ledger contents in memory and process the directives, and how to write plugins that transform them. Beancount Design Doc : Information about the program\u2019s architecture and design choices, code conventions, invariants and methodology. Read this if you want to get a deeper understanding. LedgerHub Design Doc : The design and architecture of the importing tools and library implemented in beancount.ingest . This used to be a separate project called LedgerHub (now defunct), whose useful parts have been eventually folded into Beancount. This is the somewhat dated original design doc. Source Code : The official repository of the Beancount source code lives at Github since May 2020. (From 2008 to May 2020 it was hosted at Bitbucket). External Contributions : A list of plugins, importers and other codes that build on Beancount\u2019s libraries that other people have made and shared.","title":"Documentation for Developers"},{"location":"index.html#enhancement-proposals-discussions","text":"I occasionally write proposals for enhancements in order to organize and summarize my thoughts before moving forward, and solicit feedback from other users. This is good material to find out what features I\u2019m planning to add, how things work in detail, or compare the differences with other similar software such as Ledger or HLedger. A Proposal for an Improvement on Inventory Booking : A proposal in which I outline the current state of affairs in Ledger and Beancount regarding inventory booking, and a better method for doing it that will support average cost booking and calculating capital gains without commissions. Settlement Dates in Beancount : A discussion of how settlement or auxiliary dates could be used in Beancount, and how they are used in Ledger. Balance Assertions in Beancount : A summary of the different semantics for making balance assertions in Beancount and Ledger and a proposal to extend Beancount\u2019s syntax to support more complex assertions. Fund Accounting with Beancount : A discussion of fund accounting and how best to go about using Beancount to track multiple funds in a single ledger. Rounding & Precision in Beancount : A discussion of important rounding and precision issues for balance checks and a proposal for a better method to infer required precision. ( Implemented )","title":"Enhancement Proposals & Discussions"},{"location":"index.html#external-links","text":"Documents, links, blog entries, writings, etc. about Beancount written by other authors.] Beancount Source Code Documentation (Dominik Aumayr): Sphinx-generated source code documentation of the Beancount codebase. The code to produce this is located here . Beancount ou la comptabilit\u00e9 pour les hackers (Cyril Deguet): An overview blog entry (in french).","title":"External Links"},{"location":"index.html#v3-documentation","text":"As of summer 2020, a rewrite to C++ is underway. These are the documents related to that. Beancount V3 : Goals & Design: Motivation, goals and design for version 3 rewrite to C++. Beancount V3: Changes from V2 : A list of current changes presently done in v3 (on the \"master\" branch) whose documentation hasn't made it to the v2 documentation set. Installing Beancount (v3) : Installation procedure for v3. Beancount V3: Dependencies : Software dependencies for building version 3. Beangulp : The renewed ingestion framework for v3.","title":"V3 Documentation"},{"location":"index.html#about-this-documentation","text":"You may have noticed that I\u2019m using Google Docs . I realize that this is unusual for an open source project. If you take offense to this , so you know, I do like text formats too: in graduate school I used LaTeX extensively, and for the last decade I was in love with the reStructuredText format, I even wrote Emacs\u2019 support for it. But something happened around 2013: Google Docs became good enough to write solid technical documentation and I\u2019ve begun enjoying its revision and commenting facilities extensively: I am hooked, I love it. For users to be able to suggest a correction or place a comment in context is an incredibly useful feature that helps improve the quality of my writing a lot more than patches or comments on a mailing-list. It also gives users a chance to point out passages that need improvement. I have already received orders of magnitude more feedback on documentation than on any of my other projects; it works. It also looks really good , and this is helping motivate me to write more and better. I think maybe I\u2019m falling in love again with WYSIWYG... Don\u2019t get me wrong: LaTeX and no-markup formats are great, but the world is changing and it is more important to me to have community collaboration and a living document than a crufty TeX file slowly aging in my repository. My aim is to produce quality text that is easy to read, that prints nicely, and most especially these days, that can render well on a mobile device or a tablet so you can read it anywhere. I\u2019m also able to edit it while I\u2019m on the go, which is fun for jotting down ideas or making corrections. Finally, Google Docs has an API that provides access to the doc, so should it be needed, I could eventually download the meta-data and convert it all to another format. I don\u2019t like everything about it, but what it offers, I really do like. If you want to leave comments, I\u2019d appreciate it if you can log into your Google account while you read, so that your name appears next to your comment. Thank you! \u2014 Martin Blais","title":"About this Documentation"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html","text":"A Comparison of Beancount and Ledger \uf0c1 Martin Blais , September 2014 http://furius.ca/beancount/doc/comparison The question of how Beancount differs from Ledger & HLedger has come up a few times on mailing-lists and in private emails. This document highlights key differences between these systems, as they differ sharply in their design and implementations. Keep in mind that this document is written from the perspective of Beancount and as its author, reflects my own biased views for what the design of a CLI accounting system should be. My purpose here is not to shoot down other systems, but rather to highlight material differences to help newcomers understand how these systems vary in their operation and capabilities, and perhaps to stimulate a fruitful discussion about design choices with the other developers. Philosophical Differences \uf0c1 (See this thread .) First, Ledger is optimistic. It assumes it's easy to input correct data by a user. My experience with data entry of the kind we're doing is that it's impossible to do this right without many automated checks. Sign errors on unasserted accounts are very common, for instance. In contrast, Beancount is highly pessimistic. It assumes the user is unreliable. It imposes a number of constraints on the input. For instance, if you added a share of AAPL at $100 to an empty account it won't let you remove a share of AAPL at $101 from it; you just don't have one. It doesn't assume the user is able or should be relied upon to input transactions in the correct order (dated assertions instead of file-order assertions). It optionally checks that proceeds match sale price (sellgains plugin). And it allows you to place extra constraints on your chart of accounts, e.g. a plugin that refuses postings to non-leaf accounts, or that refuses more than one commodity per account, or that requires you declare all accounts with Open directives; choose your level of pedanticity a-la-carte. It adds more automated cross-checks than the double-entry method provides. After all, cross-checking is why we choose to use the DE method in the first place, why not go hardcore on checking for correctness? Beancount should appeal to anyone who does not trust themselves too much. And because of this, it does not provide support for unbalanced/virtual postings; it's not a shortcoming, it's on purpose. Secondly, there's a design ethos difference. As is evidenced in the user manual, Ledger provides a myriad of options. This surely will be appealing to many, but to me it seems it has grown into a very complicated monolithic tool. How these options interact and some of the semantic consequences of many of these options are confusing and very subtle. Beancount offers a minimalistic approach: while there are some small number of options , it tries really hard not to have them. And those options that do affect the semantics of transactions always appear in the input file (nothing on the command-line) and are distinct from the options of particular tools. Loading a file always results in the same stream of transactions, regardless of the reporting tool that will consume them. The only command-line options present are those which affect the particular behavior of the reporting tool invoked; those never change the semantics of the stream itself. Thirdly, Beancount embraces stream processing to a deeper extent. Its loader creates a single ordered list of directives, and all the directives share some common attributes (a name, a date, metadata). This is all the data. Directives that are considered \"grammar\" in Ledger are defined as ordinary directive objects, e.g. \"Open\" is nothing special in Beancount and does nothing by itself. It's simply used in some routines that apply constraints (an account appears, has an Open directive been witnessed prior?) or that might want to hang per-account metadata to them. Prices are also specified as directives and are embedded in the stream, and can be generated in this way. All internal operations are defined as processing and outputting a stream of directives. This makes it possible to allow a user to insert their own code inside the processing pipeline to carry out arbitrary transformations on this stream of directives\u2014anything is possible, unlimited by the particular semantics of an expression language. It's a mechanism that allows users to build new features by writing a short add-on in Python, which gets run at the core of Beancount, not an API to access its data at the edges. If anything, Beancount's own internal processing will evolve towards doing less and less and moving all the work to these plugins, perhaps even to the extent of allowing plugins to declare the directive types (with the exception of Transaction objects). It is evolving into a shallow driver that just puts together a processing pipeline to produce a stream of directives, with a handy library and functional operations. Specific Differences \uf0c1 Inventory Booking & Cost Basis Treatment \uf0c1 Beancount applies strict rules regarding reductions in positions from inventories tracking the contents of its accounts. This means that you can only take out of an account something that you placed in it previously (in time), or an error will be generated. This is enforced for units \u201cheld at cost\u201d (e.g., stock shares), to ensure that no cost basis can ever leak from accounts, we can detect errors in data entry for trades (which are all too common), and we are able to correctly calculate capital gains. In contrast, Ledger does not implement inventory booking checks over time: all lots are simply accumulated regardless of the previous contents of an inventory (there is no distinction between lot addition vs. reduction). In Beancount, reductions to the contents of an inventory are required to match particular lots with a specified cost basis. In Ledger, the output is slightly misleading about this: in order to simplify the reporting output the user may specify one of a few types of lot merging algorithms. By default, the sum of units of all lots is printed, but by using these options you can tell the reporting generation to consider the cost basis (what it calls \u201cprices\u201d) and/or the dates the lots were created at in which case it will report the set of lots with distinct cost bases and/or dates individually. You can select which type of merging occurs via command-line options, e.g. --lot-dates. Most importantly, this means that it is legal in Ledger to remove from an account a lot that was never added to it. This results in a mix of long and short lots, which do not accurately represent the actual changes that occur in accounts. However, it trivially allows for average cost basis reporting. I believe that this is not only confusing, but also an incorrect treatment of account inventories and have argued it can lead to leakage and incorrect calculations. More details and an example are available here . Furthermore, until recently Ledger was not using cost basis to balance postings in a way that correctly computes capital gains. For this reason I suspect Ledger users have probably not used it to compute and compare their gains against the values reported by their broker. In order for Beancount to detect such errors meaningfully and implement a strict matching discipline when reducing lots, it turns out that the only constraint it needs to apply is that a particular account not be allowed to simultaneously hold long and short positions of the same commodity. For example, all it has to do is enforce that you could not hold a lot of 1 GOOG units and a lot of -1 GOOG units simultaneously in the same inventory (regardless of their acquisition cost). Any reduction of a particular set of units is detected as a \u201clot reduction\u201d and a search is found for a lot that matches the specification of the reducing posting, normally, a search for a lot held at the same cost as the posting specifies. Enforcing this constraint is not of much concern in practice, as there are no cases where you would want both long and short positions in the same account, but if you ever needed this, you could simply resort to using separate accounts to hold your long and short positions (it is already recommended that you use a sub-account to track your positions in any one commodity anyway, this would be quite natural). Finally, Beancount is implementing a proposal that significantly extends the scope of its inventory booking: the syntax will allow a loose specification for lot reductions that makes it easy for users to write postings where there is no ambiguity, as well as supporting booking lots at their average cost as is common in Canada and in all tax-deferred accounts across the world. It will also provide a new syntax for specifying cost per unit and total cost at the same time, a feature which will make it possible to correctly track capital gains without commissions . Currency Conversions \uf0c1 Beancount makes an important semantic distinction between simple currency conversions and conversions of commodities to be held at cost, i.e., for which we want to track the cost basis. For example, converting 20,000 USD to 22,000 CAD is a currency conversion (e.g., between banks), and after inserting the resulting CAD units in the destination account, they are not considered \u201cCAD at a cost basis of 1.1 USD each,\u201d they are simply left as CAD units in the account, with no record of the rate which was used to convert them. This models accurately how the real world operates. On the other hand, converting 5,000 USD into 10 units of GOOG shares with a cost basis of 500 USD each is treated as a distinct operation from a currency conversion: the resulting GOOG units have a particular cost basis attached to them, a memory of the price per unit is kept on the inventory lot (as well as the date) and strict rules are applied to the reduction of such lots (what I call \u201cbooking\u201d) as mentioned previously. This is used to calculate and report capital gains, and most importantly, it detects a lot of errors in data entry. In contrast, Ledger does not distinguish between these two types of conversions . Ledger treats currency conversions the same way as for the conversion of commodities held at cost. The reason that this is not causing as many headaches as one might intuit, is because there is no inventory booking - all lots at whichever conversion rate they occur accumulate in accounts, with positive or negative values - and for simple commodities (e.g. currencies, such as dollars), netting the total amount of units without considering the cost of each unit provides the correct answer. Inspecting the full list of an account\u2019s inventory lots is provided as an option (See Ledger\u2019s \u201c--lot-dates\u201d) and is not the default way to render account balances. I suspect few users make use of the feature: if you did render the list of lots for real-world accounts in which many currency conversions occurred in the past, you would observe a large number of irrelevant lots. I think the cost basis of currency conversions would be best elided instead. HLedger does not parse cost basis syntax and as such does not recognize it. Isolation of Inputs \uf0c1 Beancount reads its entire input only from the text file you provide for it. This isolation is by design. There is no linkage to external data formats nor online services, such as fetchers for historical price values. Fetching and converting external data is disparate enough that I feel it should be the province of separate projects. These problem domains also segment very well and quite naturally: Beancount provides an isolated core which allows you to ingest all the transactional data and derive reports of various aggregations from it, and its syntax is the hinge that connects it to external transaction repositories or price databases. It isolates itself from the ugly details of external sources of data in this way. There are too many external formats for downloadable files that contains transactional information to be able to cover all of them. The data files you can obtain from most institutions are provided in various formats: OFX, Quicken, XLS or CSV, and looking at their contents makes it glaringly obvious that the programmers who built the codes that outputs them did not pay much attention to detail or the standard definition of their format; it\u2019s quite an ugly affair. Those files are almost always very messy, and the details of that messiness varies over time as well as these files evolve. Fetching historical or current price information is a similarly annoying task. While Yahoo and Google Finance are able to provide some basic level of price data for common stocks on US exchanges, when you need to fetch information for instruments traded on foreign exchanges, or instruments typically not traded on exchanges, such as mutual funds, either the data is not available, or if it is, you need to figure out what ticker symbol they decided to map it to, there are few standards for this. You must manually sign the ticker. Finally, it is entirely possible that you want to manage instruments for which there is no existing external price source, so it is necessary that your bookkeeping software provide a mechanism whereby you can manually input price values (both Beancount and Ledger provide a way). The same declaration mechanism is used for caching historical price data, so that Beancount need not require the network at all. Most users will want to write their own scripts for import, but some libraries exist: the beancount.ingest library (within Beancount) provides a framework to automate the identification, extraction of transactions from and filing of downloadable files for various institutions. See its design doc for details. In comparison, Ledger and HLedger support rudimentary conversions of transactions from CSV files as well as automated fetching of current prices that uses an external script you are meant to provide ( getquote ). CSV import is far insufficient for real world usage if you are to track all your accounts, so this needs to be extended. Hooking into an external script is the right thing to do, but Beancount favors taking a strong stance about this issue and instead not to provide code that would trigger any kind of network access nor support any external format as input. In Beancount you are meant to integrate price updates on your own, perhaps with your own script, and maybe bring in the data via an include file. (But if you don't like this, you could also write your own plugin module that could fetch live prices.) Language Syntax \uf0c1 Beancount\u2019s syntax is somewhat simpler and quite a bit more restrictive than Ledger\u2019s. For its 2.0 version, the Beancount syntax was redesigned to be easily specifiable to a parser generator by a grammar. The tokens were simplified in order to make tokenization unambiguous. For example, Currencies must be entirely in capital letters (allowing numbers and some special characters, like \u201c_\u201d or \u201c-\u201d). Currency symbols (such as $ or \u20ac) are not supported. (On the other hand, currencies with numbers require quoting in Ledger.) Account names do not admit spaces (though you can use dashes), and must have at least two components, separated by colons. Description strings must be quoted, like this: \u201cAMEX PMNT\u201d. No freestyle text as strings is supported anymore. Dates are only parsed from ISO8601 format, that is, \u201cYYYY-MM-DD\u201d. Tags must begin with \u201c#\u201d, and links with \u201c^\u201d. Apart from the tag stack, all context information has been removed. There is no account alias, for example, nor is there a notion of \u201capply\u201d as in Ledger (see \u201capply root\u201d and \u201capply tag\u201d). It requires a bit more verbose input\u2014full account names\u2014and so assumes that you have account name completion setup in your editor. As a side effect, these changes make the input syntax look a bit more like a programming language. These restrictions may annoy some users, but overall they make the task of parsing the contents of a ledger simpler and the resulting simplicity will pave the way for people to more easily write parsers in other languages. (Some subtleties remain around parsing indentation, which is meaningful in the syntax, but should be easily addressable in all contexts by building a custom lexer.) Due to its looser and more user-friendly syntax, Ledger uses a custom parser. If you need to parse its contents from another language, the best approach is probably to create bindings into its source code or to use it to export the contents of a ledger to XML and then parse that (which works well). I suspect the parsing method might be reviewed in the next version of Ledger, because using a parser generator is liberating for experimentation. Order Independence \uf0c1 Beancount offers a guarantee that the ordering of directives in an input file is irrelevant to the outcome of its computations. You should be able to organize your input file and reorder any declaration as is most convenient for you without having to worry about how the software will make its calculations. Not even directives that declare accounts (\u201cOpen\u201d) are required to appear before these accounts get used in the file. All directives are parsed and then basically stably sorted before any calculation or validation occurs. This also makes it trivial to implement inclusions of multiple files (you can just concatenate the files if you want). In contrast, Ledger processes the amounts on its postings as it parses the input file. In terms of implementation, this has the advantage that only a single pass is needed to check all the assertions and balances, whereas in Beancount, numerous passes are made on the entire list of directives (this has not been much of a problem in Beancount, however, because even a realistically large number of transactions is rather modest for our computers; most of Beancount\u2019s processing time is due to parsing and numerical calculations). An unfortunate side-effect of Ledger\u2019s method of calculation is that a user must be careful with the order in which the transactions appear in the file. This can be treacherous and difficult to understand when editing a very large input file. This difference is particularly visible in balance assertions . Ledger\u2019s balance assertions are attached to the postings of transaction directives and calculated in file order (I call these \u201cfile assertions\u201d). Beancount\u2019s balance assertions are separate directives that are applied at the beginning of the date for which they are declared, regardless of their position in the file (call these \u201cdated assertions\u201d). I believe that dated assertions are more useful and less error-prone, as they do not depend on the ordering of declarations. On the other hand, Ledger-style file assertions naturally support balance checks on intra-day balances without having to specify the time on transactions, which is impossible with dated assertions. For this reason, a proposal has been written up to consider implementing file assertions in Beancount (in addition to its dated assertions). This will probably be carried out as a plugin. Ledger does not support dated assertions. Account Types \uf0c1 Beancount accounts must have a particular type from one of five categories: Assets, Liabilities, Income, Expenses and Equity. Ledger accounts are not constrained in this way, you can define any root account you desire and there is no requirement to identify an account as belonging to one of these categories. This reflects the more liberal approach of Ledger: its design aims to be a more general \u201ccalculator\u201d for anything you want. No account types are enforced or used by the system. In my experience, I haven\u2019t seen any case where I could not classify one of my accounts in one of those categories. For the more exotic commodities, e.g., \u201cUS dollars allowable to contribute to an IRA\u201d, it might require a bit of imagination to understand which account fits which category, but there is a logic to it: if the account has an absolute value that we care about, then it is an Assets or Liabilities account; if we care only about the transitional values, or the value accumulated during a time period, then it should be an Income or Expenses account. If the sign is positive, then it should be an Assets or Expenses account; conversely, if the sign is negative, it should be a Liabilities or Income account. Equity accounts are almost never used explicitly, and are defined and used by Beancount itself to transfer opening balances, retained earnings and net income to the balance sheet report for a particular reporting period (any period you choose). This principle makes it easy to resolve which type an account should have. I have data from 2008 to 2014 and have been able to represent everything I ever wanted using these five categories. Also, I don\u2019t think asking the user to categorize their accounts in this way is limiting in any way; it just requires a bit of foresight. The reason for requiring these account types is that it allows us to carry out logic based on their types. We can isolate the Income and Expenses accounts and derive an income statement and a single net income value. We can then transfer retained earnings (income from before the period under consideration) and net income (income during the period) to Equity accounts and draw up a balance sheet. We can generate lists of holdings which automatically exclude income and expenses, to compute net worth, value per account, etc. In addition, we can use account types to identify external flows to a group of accounts and compute the correct return on investments for these accounts in a way that can be compared with a target allocation\u2019s market return (note: not integrated yet, but prototyped and spec\u2019ed out, it works). The bottom line is that having account types is a useful attribute, so we enforce that you should choose a type for each account. The absence of account types is probably also why Ledger does not provide a balance sheet or income statement reports, only a trial balance. The advantage is an apparently looser and more permissive naming structure. But also note that requiring types does not itself cause any difference in calculations between the two systems, you can still accumulate any kind of \"beans\" you may want in these accounts, it is no less general. The type is only extra information that Beancount\u2019s reporting makes use of. Transactions Must Balance \uf0c1 Beancount transactions are required to balance, period. I make no compromise in this, there is no way out. The benefit of this is that the sum of the balance amounts of the postings of any subset of transactions is always precisely zero (and I check for it). Ledger allows the user two special kinds of postings: Virtual postings : These are postings input with parentheses around them, and they are not considered in the transaction balance\u2019s sum, you can put any value in them without causing an error. Balanced virtual postings : These postings are input with square brackets around them. These are less permissive: the set of postings in square brackets is enforced to balance within itself. The second case can be shown to be equivalent to two transactions: a transaction with the set of regular postings, and a separate transaction with only the balanced virtual ones (this causes no problem). The first case is the problematic one: in attempting to solve accounting problems, beginning users routinely revert to them as a cop-out instead of modeling their problem with the double-entry method. It is apparent that most adopters of CLI accounting systems are computer scientists and not accounting professionals, and as we are all learning how to create our charts of accounts and fill up our ledgers, we are making mistakes. Oftentimes it is truly not obvious how to solve these problems; it simply requires experience. But the fact is that in 8 years\u2019 worth of usage, there isn\u2019t a single case I have come across that truly required having virtual postings. The first version of Beancount used to have support for virtual postings, but I\u2019ve managed to remove all of them over time. I was always able to come up with a better set of accounts, or to use an imaginary currency that would allow me to track whatever I needed to track. And it has always resulted in a better solution with unexpected and sometimes elegant side-effects. But these systems have to be easy to use, so how do we address this problem? The mailing-list is a good place to begin and ask questions, where people share information regarding how they have solved similar problems (there aren\u2019t that many \u201caccounting problems\u201d per-se in the first place). I am also in the process of documenting all the solutions that I have come up with to solve my own accounting problems in the Beancount Cookbook , basically everything I\u2019ve learned so far; this is work in progress. I hope for this evolving document to become a helpful reference to guide others in coming up with solutions that fit the double-entry accounting framework, and to provide ample examples that will act as templates for others to replicate, to fit in their own data. In the words of Ledger\u2019s author: \u201cIf people don't want to use them [virtual accounts], that's fine. But Ledger is not an accounting tool; it's a tool that may be used to do accounting. As such, I believe virtual accounts serve a role that others with non-accounting problems may wish to fill.\u201d I respectfully beg to differ. Therefore Beancount takes a more radical stance and explicitly avoids supporting virtual postings. If you feel strongly that you should need them, you should use Ledger. Numbers and Precision of Operations \uf0c1 Beancount, Ledger and HLedger all differ in how they represent their numbers internally, and in how they handle the precision of balance checks for a transaction\u2019s postings. First, about how number are represented: Ledger uses rational numbers in an attempt to maintain the full precision of numbers resulting from mathematical operations. This works, but I believe this is perhaps not the most appropriate choice . The great majority of the cases where operations occur involve the conversion from a number of units and a price or a cost to the total value of an account\u2019s posted change (e.g., units x cost = total cost). Our task in representing transactional information is the replication of operations that take place mostly in institutions. These operations always involve the rounding of numbers for units and currencies (banks do apply stochastic rounding), and the correct numbers to be used from the perspective of these institutions, and from the perspective of the government, are indeed the rounded numbers themselves. It is a not a question of mathematical purity, but one of practicality, and our system should do the same that banks do. Therefore, I think that we should always post the rounded numbers to accounts. Using rational numbers is not a limitation in that sense, but we must be careful to store rounded numbers where it matters. I think the approach implemented by Ledger is to keep as much of the original precision as possible. Beancount chooses a decimal number representation to store the numbers parsed from the input with the same precision they are written as. This method suffers from the same problem as using rational numbers does in that the result of mathematical operations between the decimal numbers will currently be stored with their full precision (albeit in decimal). Admittedly, I have yet to apply explicit quantization where required, which would be the correct thing to do. A scheme has to be devised to infer suitable precisions for automatically quantizing the numbers after operations. The decimal representation provides natural opportunities for rounding after operations, and it is a suitable choice for this, implementations even typically provide a context for the precision to take place. Also note that it will never be required to store numbers to an infinite precision: the institutions never do it themselves. HLedger, oddly enough, selects \u201cdouble\u201d fractional binary representation for its prices . This is an unfortunate choice, a worse one than using a precise representation: fractional decimal numbers input by the user are never represented precisely by their corresponding binary form. So all the numbers are incorrect but \u201cclose enough\u201d that it works overall, and the only way to display a clean final result is by rounding to a suitable number of digits at the time of rendering a report. One could argue that the large number of digits provided by a 64-bit double representation is unlikely to cause significant errors given the quantity of operations we make\u2026 but binary rounding error could potentially accumulate, and the numbers are basically all incorrectly stored internally, rounded to their closest binary relative. Given that our task is accounting, why not just represent them correctly? Secondly, when checking that the postings of a transaction balance to zero, with all three systems it is necessary to allow for some tolerance on those amounts. This need is clear when you consider that inputting numbers in a text file implies a limited decimal representation. For example, if you\u2019re going to multiply a number of units and a cost, say both written down with 2 fractional digits, you might end up with a number that has 4 fractional digits, and then you need to compare that result with a cash amount that would typically be entered with only 2 fractional digits. You need to allow for some looseness somehow. The systems differ in how they choose that tolerance: Ledger attempts to automatically derive the precision to use for its balance checks by using recently parsed context (in file order). The precision to be used is that of the last value parsed for the particular commodity under consideration. This can be problematic: it can lead to unnecessary side-effects between transactions which can be difficult to debug . HLedger, on the other hand, uses global precision settings. The whole file is processed first, then the precisions are derived from the most precise numbers seen in the entire input file. At the moment, Beancount uses a constant value for the tolerance used in its balance checking algorithm (0.005 of any unit). This is weak and should, at the very least, be commodity-dependent, if not also dependent on the particular account in which the commodity is used. Ultimately, it depends on the numbers of digits used to represent the particular postings. We have a proposal en route to fix this. I am planning to fix this: Beancount will eventually derive its precision using a method entirely local to each transaction, perhaps with a global value for defaults (this is still in the works - Oct 2014 ). Half of the most precise digit will be the tolerance. This will be derived similarly to HLedger\u2019s method, but for each transaction separately. This will allow the user to use an arbitrary precision, simply by inserting more digits in the input. Only fractional digits will be used to derive precision. No global effect implied by transactions will be applied. No transaction should ever affect any other transaction\u2019s balancing context. Rounding error will be optionally accumulated to an Equity account if you want to monitor it . As for automatically quantizing the numbers resulting from operations, I still need to figure out an automatic method for doing so. Filtering at the Transactional Level \uf0c1 Another area where the systems differ is in that Beancount will not support filtering at the postings level but only at the transaction level. That is, when applying filters to the input data, even filtering that applies predicates to postings, only sets of complete transactions will be produced. This is carried out in order to produce sets of postings whose sum balances to exactly zero. We will not ignore only some postings of a transaction. We will nevertheless allow reports to cull out arbitrary subsets of accounts once all balances have been computed. Ledger filtering works at either the transactional or postings level . I think this is confusing. I don\u2019t really understand why it works this way or how it actually works. (Note that this is not a very relevant point at this moment, because I have yet to implement custom arbitrary filtering; the only filtering available at the moment are \u201cviews\u201d you can obtain through the web interface, but I will soon provide a simple logical expression language to apply custom filters to the parsed transactions as in Ledger, as I think it\u2019s a powerful feature. Until recently, most reports were only rendered through a web interface and it wasn\u2019t as needed as it is now that Beancount implements console reports.) Extension Mechanisms \uf0c1 Both Beancount and Ledger provide mechanisms for writing scripts against their corpus of data. These mechanisms are all useful but different. Ledger provides A custom expression language which it interprets to produce reports A command to export contents to XML A command to export contents to LISP A library of Python bindings which provides access to its C++ data structures (\u2026 others?) (Note: due to its dependencies, C++ features being used and build system, I have found it difficult to build Ledger on my vanilla Ubuntu machine or Mac OS computer. Building it with the Python bindings is even more difficult. If you have the patience, the time and that\u2019s not a problem for you, great, but if you can find a pre-packaged version I would recommend using that instead.) Beancount provides A native Python plugin system whereby you may specify lists of Python modules to import and call to filter and transform the parsed directives to implement new features; An easy loader function that allows you to access the internal data structures resulting from parsing and processing a Beancount ledger. This is also a native Python library. So basically, you must write Python to extend Beancount. I\u2019m planning to provide output to XML and SQL as well (due to the simple data structures I\u2019m using these will be both very simple to implement). Moreover, using Beancount\u2019s own \u201cprinter\u201d module produces text that is guaranteed to parse back into exactly the same data structure (Beancount guarantees round-tripping of its syntax and its data structures). One advantage is that the plugins system allows you to perform in-stream arbitrary transformations of the list of directives produced, and this is a great way to prototype new functionality and easier syntax. For example, you can allocate a special tag that will trigger some arbitrary transformation on such tagged transactions. And these modules don\u2019t have to be integrated with Beancount: they can just live anywhere on your own PYTHONPATH, so you can experiment without having to contribute new features upstream or patch the source code. (Note: Beancount is implemented in Python 3, so you might have to install a recent version in order to work with it, such as Python-3.4, if you don\u2019t have it. At this point, Python 3 is becoming pretty widespread, so I don\u2019t see this as a problem, but you might be using an older OS.) Automated Transactions via Plugins \uf0c1 Ledger provides a special syntax to automatically insert postings on existing transactions , based on some matching criteria. The syntax allows the user to access some of a posting\u2019s data, such as amount and account name. The syntax is specialized to also allow the application of transaction and posting \u201ctags.\u201d Beancount allows you to do the same thing and more via its plugin extension mechanism. Plugins that you write are able to completely modify, create or even delete any object and attribute of any object in the parsed flow of transactions, allowing you to carry out as much automation and summarization as you like. This does not require a special syntax - you simply work in Python, with access to all its features - and you can piggyback your automation on top of existing syntax. Some examples are provided under beancount.plugins.* . There is an argument to be made, however, for the availability of a quick and easy method for specifying the most common case of just adding some postings. I\u2019m not entirely convinced yet, but Beancount may acquire this eventually (you could easily prototype this now in a plugin file if you wanted). No Support for Time or Effective Dates \uf0c1 Beancount does not represent the intra-day time of transactions, its granularity is a day. Ledger allows you to specify the time of transactions down to seconds precision. I choose to limit its scope in the interest of simplicity, and I also think there are few use cases for supporting intra-day operations. Note that while Beancount\u2019s maximum resolution is one day, when it sorts the directives it will maintain the relative order of all transactions that occurs within one day, so it is possible to represent multiple transactions occurring on the same day in Beancount and still do correct inventory bookings. But I believe that if you were to do day-trading you would need a more specialized system to compute intra-day returns and do technical analysis and intraday P/L calculations. Beancount is not suited for those (a lot of other features would be needed if that was its scope). Ledger also has support for effective dates which are essentially an alternative date for the transaction. Reporting features allow Ledger users to use the main date or the alternative date. I used to have this feature in Beancount and I removed it, mainly because I did not want to introduce reporting options, and to handle two dates, say a transaction date and a settlement date, I wanted to enforce that at any point in time all transactions would balance. I also never made much use of it which indicates it was probably futile. Handling postings to occur at different points in time would have created imbalances, or I would have had to come up with a solution that involved \u201climbo\u201d or \u201ctransfer\u201d accounts. I preferred to just remove the feature: in 8 years of data, I have always been able to fudge the dates to make everything balance. This is not a big issue. Note that handling those split transaction and merging them together will be handled; a proposal is underway. Documents \uf0c1 Beancount offers support for integrating a directory hierarchy of documents with the contents of a ledger\u2019s chart of accounts. You can provide a directory path and Beancount will automatically find and create corresponding Document directives for filenames that begin with a date in directories that mirror account names, and attach those documents to those accounts. And given a bit of configuration, the bean-file tool can automatically file downloaded documents to such a directory hierarchy. Ledger\u2019s binding of documents to transactions works by generic meta-data. Users can attach arbitrary key-value pairs to their transactions, and those can be filenames. Beyond that, there is no particular support for document organization. Simpler and More Strict \uf0c1 Finally, Beancount has a generally simpler input syntax than Ledger. There are very few command-line options\u2014and this is on purpose, I want to localize all the input within the file\u2014and the directive syntax is more homogeneous: all transactions begin with a date and a keyword. If the argument of simplicity appeals to you, you might prefer to work with Beancount. I feel that the number of options offered in Ledger is daunting, and I could not claim to understand all of the possible ways they might interact with each other. If this does not worry you, you might prefer to use Ledger. It is also more strict than Ledger. Certain kinds of Beancount inputs are not valid. Any transaction in an account needs to have an open directive to initiate the account, for example (though some of these constraints can be relaxed via optional plugins). If you maintain a Beancount ledger, you can expect to have to normalize it to fix a number of common errors being reported. I view this as a good thing: it detects many potential problems and applies a number of strict constraints to its input which allows us to make reasonable assumptions later on when we process the stream of directives. If you don\u2019t care about precision or detecting potential mistakes, Ledger will allow you to be more liberal. On the other hand if you care to produce a precise and flawless account of transactions, Beancount offers more support in its validation of your inputs. Web Interface \uf0c1 Beancount has a built-in web interface, and there is an external project called Fava which significantly improves on the same theme. This is the default mode for browsing reports. I believe HLedger also has a web interface as well. Missing Features \uf0c1 Beancount generally attempts to minimize the number of features it provides. This is in contrast with Ledger\u2019s implementation, which has received a substantial amount of feature addition in order to experiment with double-entry bookkeeping. There are a large number of options. This reflects a difference in approach: I believe that there is a small core of essential features to be identified and that forward progress is made when we are able to minimize and remove any feature that is not strictly necessary. My goal is to provide the smallest possible kernel of features that will allow one to carry out the full spectrum of bookkeeping activities, and to make it possible for users to extend the system to automate repetitive syntax. But here is a list of features that Beancount does not support that are supported in Ledger, and that I think would be nice to have eventually. The list below is unlikely to be exhaustive. Console Output \uf0c1 Beancount\u2019s original implementation focused on providing a web view for all of its contents. During the 2.0 rewrite I began implementing some console/text outputs, mainly because I want to be able for reports to be exportable to share with others. I have a trial balance view (like Ledger\u2019s \u201cbal\u201d report) but for now the journal view isn\u2019t implemented. Ledger, on the other hand, has always focused on console reports. I\u2019ll make all the reports in Beancount support output to text format first thing after the initial release, as I\u2019m increasingly enjoying text reports. Use bean-query --list-formats to view current status of this. Filtering Language \uf0c1 Beancount does not yet have a filtering language. Until recently, its web interface was the main mode for rendering reports and exploring the contents of its ledger, and it provided limited subsets of transactions in the form of \u201cviews\u201d, e.g., per-year, per-tag, etc. Having a filtering language in particular allows one to do away with many sub-accounts. I want to simplify my chart of accounts so I need this. I\u2019m working on adding a simple logical expression language to do arbitrary filters on the set of Beancount transactions. This is straightforward to implement and a high priority. No Meta-data \uf0c1 Beancount does not currently support meta-data. Ledger users routinely make liberal use of metadata. This has been identified as a powerful feature and a prototype has already been implemented. Meta-data will be supported on any directive type as well as on any posting. A dictionary of key-value pairs will be attached to each of these objects. Supported values will include strings, dates, numbers, currencies and amounts. So far the plan is to restrict Beancount\u2019s own code to make no specific use of meta-data, on purpose. The meta-data will be strictly made available for user-plugins and custom user scripts to make use of. No Arithmetic Expressions \uf0c1 Beancount does not support arbitrary expression evaluation in the syntax, in the places where numbers are allowed in the input. Ledger does. I have had no use for these yet, but I have no particular reason against adding this, I just haven\u2019t implemented it, as I don\u2019t need it myself. I think an implementation would be straightforward and very low risk, a simple change to the parser and there is already a callback for numbers. I think there are many legitimate uses for it. Limited Support for Unicode \uf0c1 Beancount supports UTF8 or and other encodings in strings only (that is, for input that is in quotes). For example, you can enter payees and narrations with non-ASCII characters, but not account names (which aren\u2019t in quotes). Ledger supports other encodings over the entire file. The reason for the lack of a more general encoding support in Beancount is the current limitation of tokenizer tools. I\u2019ve been using GNU flex to implement my lexer and it does not support arbitrary encodings. I just need to write a better lexer and make that work with Bison , it\u2019s not a difficult task. I will eventually write my own lexer manually\u2014this has other advantages\u2014and will write it to support Unicode (Python 3 has full support for this, so all that is required is to modify the lexer, which is one simple compilation unit). This is a relatively easy and isolated task to implement. No Forecasting or Periodic Transactions \uf0c1 Beancount has no support for generating periodic transactions for forecasting, though there is a plugin provided that implements a simplistic form of it to be used as an example for how plugins work (see beancount.plugins.forecast ). Ledger supports periodic transaction generation . I do want to add this to core Beancount eventually, but I want to define the semantics very clearly before I do. Updating one\u2019s ledger is essentially the process of copying and replicating transactional data that happens somewhere else. I don\u2019t believe that regular transactions are that \u201cregular\u201d in reality; in my experience, there is always some amount of small variations in real transactions that makes it impossible to automatically generate a series of transactions by a generator in a way that would allow the user to forgo updating them one-by-one. What it is useful for in my view, is for generating tentative future transactions. I feel strongly that those transactions should be limited not to straddle reconciled history, and that reconciled history should replace any kind of automatically generated transactions. I have some fairly complete ideas for implementing this feature, but I\u2019m not using forecasting myself at the moment, so it\u2019s on the backburner. While in theory you can forecast using Ledger\u2019s periodic transactions, to precisely represent your account history you will likely need to adjust the beginning dates of those transactions every time you append new transactions to your accounts and replace forecasted ones. I don\u2019t find the current semantics of automated transactions in Ledger to be very useful beyond creating an approximation of account contents. (In the meantime, given the ease of extending Beancount with plugins, I suggest you begin experimenting with forecast transactions on your own for now, and if we can derive a generic way to create them, I\u2019d be open to merging that into the main code.)","title":"A Comparison of Beancount and Ledger Hledger"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#a-comparison-of-beancount-and-ledger","text":"Martin Blais , September 2014 http://furius.ca/beancount/doc/comparison The question of how Beancount differs from Ledger & HLedger has come up a few times on mailing-lists and in private emails. This document highlights key differences between these systems, as they differ sharply in their design and implementations. Keep in mind that this document is written from the perspective of Beancount and as its author, reflects my own biased views for what the design of a CLI accounting system should be. My purpose here is not to shoot down other systems, but rather to highlight material differences to help newcomers understand how these systems vary in their operation and capabilities, and perhaps to stimulate a fruitful discussion about design choices with the other developers.","title":"A Comparison of Beancount and Ledger"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#philosophical-differences","text":"(See this thread .) First, Ledger is optimistic. It assumes it's easy to input correct data by a user. My experience with data entry of the kind we're doing is that it's impossible to do this right without many automated checks. Sign errors on unasserted accounts are very common, for instance. In contrast, Beancount is highly pessimistic. It assumes the user is unreliable. It imposes a number of constraints on the input. For instance, if you added a share of AAPL at $100 to an empty account it won't let you remove a share of AAPL at $101 from it; you just don't have one. It doesn't assume the user is able or should be relied upon to input transactions in the correct order (dated assertions instead of file-order assertions). It optionally checks that proceeds match sale price (sellgains plugin). And it allows you to place extra constraints on your chart of accounts, e.g. a plugin that refuses postings to non-leaf accounts, or that refuses more than one commodity per account, or that requires you declare all accounts with Open directives; choose your level of pedanticity a-la-carte. It adds more automated cross-checks than the double-entry method provides. After all, cross-checking is why we choose to use the DE method in the first place, why not go hardcore on checking for correctness? Beancount should appeal to anyone who does not trust themselves too much. And because of this, it does not provide support for unbalanced/virtual postings; it's not a shortcoming, it's on purpose. Secondly, there's a design ethos difference. As is evidenced in the user manual, Ledger provides a myriad of options. This surely will be appealing to many, but to me it seems it has grown into a very complicated monolithic tool. How these options interact and some of the semantic consequences of many of these options are confusing and very subtle. Beancount offers a minimalistic approach: while there are some small number of options , it tries really hard not to have them. And those options that do affect the semantics of transactions always appear in the input file (nothing on the command-line) and are distinct from the options of particular tools. Loading a file always results in the same stream of transactions, regardless of the reporting tool that will consume them. The only command-line options present are those which affect the particular behavior of the reporting tool invoked; those never change the semantics of the stream itself. Thirdly, Beancount embraces stream processing to a deeper extent. Its loader creates a single ordered list of directives, and all the directives share some common attributes (a name, a date, metadata). This is all the data. Directives that are considered \"grammar\" in Ledger are defined as ordinary directive objects, e.g. \"Open\" is nothing special in Beancount and does nothing by itself. It's simply used in some routines that apply constraints (an account appears, has an Open directive been witnessed prior?) or that might want to hang per-account metadata to them. Prices are also specified as directives and are embedded in the stream, and can be generated in this way. All internal operations are defined as processing and outputting a stream of directives. This makes it possible to allow a user to insert their own code inside the processing pipeline to carry out arbitrary transformations on this stream of directives\u2014anything is possible, unlimited by the particular semantics of an expression language. It's a mechanism that allows users to build new features by writing a short add-on in Python, which gets run at the core of Beancount, not an API to access its data at the edges. If anything, Beancount's own internal processing will evolve towards doing less and less and moving all the work to these plugins, perhaps even to the extent of allowing plugins to declare the directive types (with the exception of Transaction objects). It is evolving into a shallow driver that just puts together a processing pipeline to produce a stream of directives, with a handy library and functional operations.","title":"Philosophical Differences"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#specific-differences","text":"","title":"Specific Differences"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#inventory-booking-cost-basis-treatment","text":"Beancount applies strict rules regarding reductions in positions from inventories tracking the contents of its accounts. This means that you can only take out of an account something that you placed in it previously (in time), or an error will be generated. This is enforced for units \u201cheld at cost\u201d (e.g., stock shares), to ensure that no cost basis can ever leak from accounts, we can detect errors in data entry for trades (which are all too common), and we are able to correctly calculate capital gains. In contrast, Ledger does not implement inventory booking checks over time: all lots are simply accumulated regardless of the previous contents of an inventory (there is no distinction between lot addition vs. reduction). In Beancount, reductions to the contents of an inventory are required to match particular lots with a specified cost basis. In Ledger, the output is slightly misleading about this: in order to simplify the reporting output the user may specify one of a few types of lot merging algorithms. By default, the sum of units of all lots is printed, but by using these options you can tell the reporting generation to consider the cost basis (what it calls \u201cprices\u201d) and/or the dates the lots were created at in which case it will report the set of lots with distinct cost bases and/or dates individually. You can select which type of merging occurs via command-line options, e.g. --lot-dates. Most importantly, this means that it is legal in Ledger to remove from an account a lot that was never added to it. This results in a mix of long and short lots, which do not accurately represent the actual changes that occur in accounts. However, it trivially allows for average cost basis reporting. I believe that this is not only confusing, but also an incorrect treatment of account inventories and have argued it can lead to leakage and incorrect calculations. More details and an example are available here . Furthermore, until recently Ledger was not using cost basis to balance postings in a way that correctly computes capital gains. For this reason I suspect Ledger users have probably not used it to compute and compare their gains against the values reported by their broker. In order for Beancount to detect such errors meaningfully and implement a strict matching discipline when reducing lots, it turns out that the only constraint it needs to apply is that a particular account not be allowed to simultaneously hold long and short positions of the same commodity. For example, all it has to do is enforce that you could not hold a lot of 1 GOOG units and a lot of -1 GOOG units simultaneously in the same inventory (regardless of their acquisition cost). Any reduction of a particular set of units is detected as a \u201clot reduction\u201d and a search is found for a lot that matches the specification of the reducing posting, normally, a search for a lot held at the same cost as the posting specifies. Enforcing this constraint is not of much concern in practice, as there are no cases where you would want both long and short positions in the same account, but if you ever needed this, you could simply resort to using separate accounts to hold your long and short positions (it is already recommended that you use a sub-account to track your positions in any one commodity anyway, this would be quite natural). Finally, Beancount is implementing a proposal that significantly extends the scope of its inventory booking: the syntax will allow a loose specification for lot reductions that makes it easy for users to write postings where there is no ambiguity, as well as supporting booking lots at their average cost as is common in Canada and in all tax-deferred accounts across the world. It will also provide a new syntax for specifying cost per unit and total cost at the same time, a feature which will make it possible to correctly track capital gains without commissions .","title":"Inventory Booking & Cost Basis Treatment"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#currency-conversions","text":"Beancount makes an important semantic distinction between simple currency conversions and conversions of commodities to be held at cost, i.e., for which we want to track the cost basis. For example, converting 20,000 USD to 22,000 CAD is a currency conversion (e.g., between banks), and after inserting the resulting CAD units in the destination account, they are not considered \u201cCAD at a cost basis of 1.1 USD each,\u201d they are simply left as CAD units in the account, with no record of the rate which was used to convert them. This models accurately how the real world operates. On the other hand, converting 5,000 USD into 10 units of GOOG shares with a cost basis of 500 USD each is treated as a distinct operation from a currency conversion: the resulting GOOG units have a particular cost basis attached to them, a memory of the price per unit is kept on the inventory lot (as well as the date) and strict rules are applied to the reduction of such lots (what I call \u201cbooking\u201d) as mentioned previously. This is used to calculate and report capital gains, and most importantly, it detects a lot of errors in data entry. In contrast, Ledger does not distinguish between these two types of conversions . Ledger treats currency conversions the same way as for the conversion of commodities held at cost. The reason that this is not causing as many headaches as one might intuit, is because there is no inventory booking - all lots at whichever conversion rate they occur accumulate in accounts, with positive or negative values - and for simple commodities (e.g. currencies, such as dollars), netting the total amount of units without considering the cost of each unit provides the correct answer. Inspecting the full list of an account\u2019s inventory lots is provided as an option (See Ledger\u2019s \u201c--lot-dates\u201d) and is not the default way to render account balances. I suspect few users make use of the feature: if you did render the list of lots for real-world accounts in which many currency conversions occurred in the past, you would observe a large number of irrelevant lots. I think the cost basis of currency conversions would be best elided instead. HLedger does not parse cost basis syntax and as such does not recognize it.","title":"Currency Conversions"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#isolation-of-inputs","text":"Beancount reads its entire input only from the text file you provide for it. This isolation is by design. There is no linkage to external data formats nor online services, such as fetchers for historical price values. Fetching and converting external data is disparate enough that I feel it should be the province of separate projects. These problem domains also segment very well and quite naturally: Beancount provides an isolated core which allows you to ingest all the transactional data and derive reports of various aggregations from it, and its syntax is the hinge that connects it to external transaction repositories or price databases. It isolates itself from the ugly details of external sources of data in this way. There are too many external formats for downloadable files that contains transactional information to be able to cover all of them. The data files you can obtain from most institutions are provided in various formats: OFX, Quicken, XLS or CSV, and looking at their contents makes it glaringly obvious that the programmers who built the codes that outputs them did not pay much attention to detail or the standard definition of their format; it\u2019s quite an ugly affair. Those files are almost always very messy, and the details of that messiness varies over time as well as these files evolve. Fetching historical or current price information is a similarly annoying task. While Yahoo and Google Finance are able to provide some basic level of price data for common stocks on US exchanges, when you need to fetch information for instruments traded on foreign exchanges, or instruments typically not traded on exchanges, such as mutual funds, either the data is not available, or if it is, you need to figure out what ticker symbol they decided to map it to, there are few standards for this. You must manually sign the ticker. Finally, it is entirely possible that you want to manage instruments for which there is no existing external price source, so it is necessary that your bookkeeping software provide a mechanism whereby you can manually input price values (both Beancount and Ledger provide a way). The same declaration mechanism is used for caching historical price data, so that Beancount need not require the network at all. Most users will want to write their own scripts for import, but some libraries exist: the beancount.ingest library (within Beancount) provides a framework to automate the identification, extraction of transactions from and filing of downloadable files for various institutions. See its design doc for details. In comparison, Ledger and HLedger support rudimentary conversions of transactions from CSV files as well as automated fetching of current prices that uses an external script you are meant to provide ( getquote ). CSV import is far insufficient for real world usage if you are to track all your accounts, so this needs to be extended. Hooking into an external script is the right thing to do, but Beancount favors taking a strong stance about this issue and instead not to provide code that would trigger any kind of network access nor support any external format as input. In Beancount you are meant to integrate price updates on your own, perhaps with your own script, and maybe bring in the data via an include file. (But if you don't like this, you could also write your own plugin module that could fetch live prices.)","title":"Isolation of Inputs"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#language-syntax","text":"Beancount\u2019s syntax is somewhat simpler and quite a bit more restrictive than Ledger\u2019s. For its 2.0 version, the Beancount syntax was redesigned to be easily specifiable to a parser generator by a grammar. The tokens were simplified in order to make tokenization unambiguous. For example, Currencies must be entirely in capital letters (allowing numbers and some special characters, like \u201c_\u201d or \u201c-\u201d). Currency symbols (such as $ or \u20ac) are not supported. (On the other hand, currencies with numbers require quoting in Ledger.) Account names do not admit spaces (though you can use dashes), and must have at least two components, separated by colons. Description strings must be quoted, like this: \u201cAMEX PMNT\u201d. No freestyle text as strings is supported anymore. Dates are only parsed from ISO8601 format, that is, \u201cYYYY-MM-DD\u201d. Tags must begin with \u201c#\u201d, and links with \u201c^\u201d. Apart from the tag stack, all context information has been removed. There is no account alias, for example, nor is there a notion of \u201capply\u201d as in Ledger (see \u201capply root\u201d and \u201capply tag\u201d). It requires a bit more verbose input\u2014full account names\u2014and so assumes that you have account name completion setup in your editor. As a side effect, these changes make the input syntax look a bit more like a programming language. These restrictions may annoy some users, but overall they make the task of parsing the contents of a ledger simpler and the resulting simplicity will pave the way for people to more easily write parsers in other languages. (Some subtleties remain around parsing indentation, which is meaningful in the syntax, but should be easily addressable in all contexts by building a custom lexer.) Due to its looser and more user-friendly syntax, Ledger uses a custom parser. If you need to parse its contents from another language, the best approach is probably to create bindings into its source code or to use it to export the contents of a ledger to XML and then parse that (which works well). I suspect the parsing method might be reviewed in the next version of Ledger, because using a parser generator is liberating for experimentation.","title":"Language Syntax"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#order-independence","text":"Beancount offers a guarantee that the ordering of directives in an input file is irrelevant to the outcome of its computations. You should be able to organize your input file and reorder any declaration as is most convenient for you without having to worry about how the software will make its calculations. Not even directives that declare accounts (\u201cOpen\u201d) are required to appear before these accounts get used in the file. All directives are parsed and then basically stably sorted before any calculation or validation occurs. This also makes it trivial to implement inclusions of multiple files (you can just concatenate the files if you want). In contrast, Ledger processes the amounts on its postings as it parses the input file. In terms of implementation, this has the advantage that only a single pass is needed to check all the assertions and balances, whereas in Beancount, numerous passes are made on the entire list of directives (this has not been much of a problem in Beancount, however, because even a realistically large number of transactions is rather modest for our computers; most of Beancount\u2019s processing time is due to parsing and numerical calculations). An unfortunate side-effect of Ledger\u2019s method of calculation is that a user must be careful with the order in which the transactions appear in the file. This can be treacherous and difficult to understand when editing a very large input file. This difference is particularly visible in balance assertions . Ledger\u2019s balance assertions are attached to the postings of transaction directives and calculated in file order (I call these \u201cfile assertions\u201d). Beancount\u2019s balance assertions are separate directives that are applied at the beginning of the date for which they are declared, regardless of their position in the file (call these \u201cdated assertions\u201d). I believe that dated assertions are more useful and less error-prone, as they do not depend on the ordering of declarations. On the other hand, Ledger-style file assertions naturally support balance checks on intra-day balances without having to specify the time on transactions, which is impossible with dated assertions. For this reason, a proposal has been written up to consider implementing file assertions in Beancount (in addition to its dated assertions). This will probably be carried out as a plugin. Ledger does not support dated assertions.","title":"Order Independence"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#account-types","text":"Beancount accounts must have a particular type from one of five categories: Assets, Liabilities, Income, Expenses and Equity. Ledger accounts are not constrained in this way, you can define any root account you desire and there is no requirement to identify an account as belonging to one of these categories. This reflects the more liberal approach of Ledger: its design aims to be a more general \u201ccalculator\u201d for anything you want. No account types are enforced or used by the system. In my experience, I haven\u2019t seen any case where I could not classify one of my accounts in one of those categories. For the more exotic commodities, e.g., \u201cUS dollars allowable to contribute to an IRA\u201d, it might require a bit of imagination to understand which account fits which category, but there is a logic to it: if the account has an absolute value that we care about, then it is an Assets or Liabilities account; if we care only about the transitional values, or the value accumulated during a time period, then it should be an Income or Expenses account. If the sign is positive, then it should be an Assets or Expenses account; conversely, if the sign is negative, it should be a Liabilities or Income account. Equity accounts are almost never used explicitly, and are defined and used by Beancount itself to transfer opening balances, retained earnings and net income to the balance sheet report for a particular reporting period (any period you choose). This principle makes it easy to resolve which type an account should have. I have data from 2008 to 2014 and have been able to represent everything I ever wanted using these five categories. Also, I don\u2019t think asking the user to categorize their accounts in this way is limiting in any way; it just requires a bit of foresight. The reason for requiring these account types is that it allows us to carry out logic based on their types. We can isolate the Income and Expenses accounts and derive an income statement and a single net income value. We can then transfer retained earnings (income from before the period under consideration) and net income (income during the period) to Equity accounts and draw up a balance sheet. We can generate lists of holdings which automatically exclude income and expenses, to compute net worth, value per account, etc. In addition, we can use account types to identify external flows to a group of accounts and compute the correct return on investments for these accounts in a way that can be compared with a target allocation\u2019s market return (note: not integrated yet, but prototyped and spec\u2019ed out, it works). The bottom line is that having account types is a useful attribute, so we enforce that you should choose a type for each account. The absence of account types is probably also why Ledger does not provide a balance sheet or income statement reports, only a trial balance. The advantage is an apparently looser and more permissive naming structure. But also note that requiring types does not itself cause any difference in calculations between the two systems, you can still accumulate any kind of \"beans\" you may want in these accounts, it is no less general. The type is only extra information that Beancount\u2019s reporting makes use of.","title":"Account Types"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#transactions-must-balance","text":"Beancount transactions are required to balance, period. I make no compromise in this, there is no way out. The benefit of this is that the sum of the balance amounts of the postings of any subset of transactions is always precisely zero (and I check for it). Ledger allows the user two special kinds of postings: Virtual postings : These are postings input with parentheses around them, and they are not considered in the transaction balance\u2019s sum, you can put any value in them without causing an error. Balanced virtual postings : These postings are input with square brackets around them. These are less permissive: the set of postings in square brackets is enforced to balance within itself. The second case can be shown to be equivalent to two transactions: a transaction with the set of regular postings, and a separate transaction with only the balanced virtual ones (this causes no problem). The first case is the problematic one: in attempting to solve accounting problems, beginning users routinely revert to them as a cop-out instead of modeling their problem with the double-entry method. It is apparent that most adopters of CLI accounting systems are computer scientists and not accounting professionals, and as we are all learning how to create our charts of accounts and fill up our ledgers, we are making mistakes. Oftentimes it is truly not obvious how to solve these problems; it simply requires experience. But the fact is that in 8 years\u2019 worth of usage, there isn\u2019t a single case I have come across that truly required having virtual postings. The first version of Beancount used to have support for virtual postings, but I\u2019ve managed to remove all of them over time. I was always able to come up with a better set of accounts, or to use an imaginary currency that would allow me to track whatever I needed to track. And it has always resulted in a better solution with unexpected and sometimes elegant side-effects. But these systems have to be easy to use, so how do we address this problem? The mailing-list is a good place to begin and ask questions, where people share information regarding how they have solved similar problems (there aren\u2019t that many \u201caccounting problems\u201d per-se in the first place). I am also in the process of documenting all the solutions that I have come up with to solve my own accounting problems in the Beancount Cookbook , basically everything I\u2019ve learned so far; this is work in progress. I hope for this evolving document to become a helpful reference to guide others in coming up with solutions that fit the double-entry accounting framework, and to provide ample examples that will act as templates for others to replicate, to fit in their own data. In the words of Ledger\u2019s author: \u201cIf people don't want to use them [virtual accounts], that's fine. But Ledger is not an accounting tool; it's a tool that may be used to do accounting. As such, I believe virtual accounts serve a role that others with non-accounting problems may wish to fill.\u201d I respectfully beg to differ. Therefore Beancount takes a more radical stance and explicitly avoids supporting virtual postings. If you feel strongly that you should need them, you should use Ledger.","title":"Transactions Must Balance"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#numbers-and-precision-of-operations","text":"Beancount, Ledger and HLedger all differ in how they represent their numbers internally, and in how they handle the precision of balance checks for a transaction\u2019s postings. First, about how number are represented: Ledger uses rational numbers in an attempt to maintain the full precision of numbers resulting from mathematical operations. This works, but I believe this is perhaps not the most appropriate choice . The great majority of the cases where operations occur involve the conversion from a number of units and a price or a cost to the total value of an account\u2019s posted change (e.g., units x cost = total cost). Our task in representing transactional information is the replication of operations that take place mostly in institutions. These operations always involve the rounding of numbers for units and currencies (banks do apply stochastic rounding), and the correct numbers to be used from the perspective of these institutions, and from the perspective of the government, are indeed the rounded numbers themselves. It is a not a question of mathematical purity, but one of practicality, and our system should do the same that banks do. Therefore, I think that we should always post the rounded numbers to accounts. Using rational numbers is not a limitation in that sense, but we must be careful to store rounded numbers where it matters. I think the approach implemented by Ledger is to keep as much of the original precision as possible. Beancount chooses a decimal number representation to store the numbers parsed from the input with the same precision they are written as. This method suffers from the same problem as using rational numbers does in that the result of mathematical operations between the decimal numbers will currently be stored with their full precision (albeit in decimal). Admittedly, I have yet to apply explicit quantization where required, which would be the correct thing to do. A scheme has to be devised to infer suitable precisions for automatically quantizing the numbers after operations. The decimal representation provides natural opportunities for rounding after operations, and it is a suitable choice for this, implementations even typically provide a context for the precision to take place. Also note that it will never be required to store numbers to an infinite precision: the institutions never do it themselves. HLedger, oddly enough, selects \u201cdouble\u201d fractional binary representation for its prices . This is an unfortunate choice, a worse one than using a precise representation: fractional decimal numbers input by the user are never represented precisely by their corresponding binary form. So all the numbers are incorrect but \u201cclose enough\u201d that it works overall, and the only way to display a clean final result is by rounding to a suitable number of digits at the time of rendering a report. One could argue that the large number of digits provided by a 64-bit double representation is unlikely to cause significant errors given the quantity of operations we make\u2026 but binary rounding error could potentially accumulate, and the numbers are basically all incorrectly stored internally, rounded to their closest binary relative. Given that our task is accounting, why not just represent them correctly? Secondly, when checking that the postings of a transaction balance to zero, with all three systems it is necessary to allow for some tolerance on those amounts. This need is clear when you consider that inputting numbers in a text file implies a limited decimal representation. For example, if you\u2019re going to multiply a number of units and a cost, say both written down with 2 fractional digits, you might end up with a number that has 4 fractional digits, and then you need to compare that result with a cash amount that would typically be entered with only 2 fractional digits. You need to allow for some looseness somehow. The systems differ in how they choose that tolerance: Ledger attempts to automatically derive the precision to use for its balance checks by using recently parsed context (in file order). The precision to be used is that of the last value parsed for the particular commodity under consideration. This can be problematic: it can lead to unnecessary side-effects between transactions which can be difficult to debug . HLedger, on the other hand, uses global precision settings. The whole file is processed first, then the precisions are derived from the most precise numbers seen in the entire input file. At the moment, Beancount uses a constant value for the tolerance used in its balance checking algorithm (0.005 of any unit). This is weak and should, at the very least, be commodity-dependent, if not also dependent on the particular account in which the commodity is used. Ultimately, it depends on the numbers of digits used to represent the particular postings. We have a proposal en route to fix this. I am planning to fix this: Beancount will eventually derive its precision using a method entirely local to each transaction, perhaps with a global value for defaults (this is still in the works - Oct 2014 ). Half of the most precise digit will be the tolerance. This will be derived similarly to HLedger\u2019s method, but for each transaction separately. This will allow the user to use an arbitrary precision, simply by inserting more digits in the input. Only fractional digits will be used to derive precision. No global effect implied by transactions will be applied. No transaction should ever affect any other transaction\u2019s balancing context. Rounding error will be optionally accumulated to an Equity account if you want to monitor it . As for automatically quantizing the numbers resulting from operations, I still need to figure out an automatic method for doing so.","title":"Numbers and Precision of Operations"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#filtering-at-the-transactional-level","text":"Another area where the systems differ is in that Beancount will not support filtering at the postings level but only at the transaction level. That is, when applying filters to the input data, even filtering that applies predicates to postings, only sets of complete transactions will be produced. This is carried out in order to produce sets of postings whose sum balances to exactly zero. We will not ignore only some postings of a transaction. We will nevertheless allow reports to cull out arbitrary subsets of accounts once all balances have been computed. Ledger filtering works at either the transactional or postings level . I think this is confusing. I don\u2019t really understand why it works this way or how it actually works. (Note that this is not a very relevant point at this moment, because I have yet to implement custom arbitrary filtering; the only filtering available at the moment are \u201cviews\u201d you can obtain through the web interface, but I will soon provide a simple logical expression language to apply custom filters to the parsed transactions as in Ledger, as I think it\u2019s a powerful feature. Until recently, most reports were only rendered through a web interface and it wasn\u2019t as needed as it is now that Beancount implements console reports.)","title":"Filtering at the Transactional Level"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#extension-mechanisms","text":"Both Beancount and Ledger provide mechanisms for writing scripts against their corpus of data. These mechanisms are all useful but different. Ledger provides A custom expression language which it interprets to produce reports A command to export contents to XML A command to export contents to LISP A library of Python bindings which provides access to its C++ data structures (\u2026 others?) (Note: due to its dependencies, C++ features being used and build system, I have found it difficult to build Ledger on my vanilla Ubuntu machine or Mac OS computer. Building it with the Python bindings is even more difficult. If you have the patience, the time and that\u2019s not a problem for you, great, but if you can find a pre-packaged version I would recommend using that instead.) Beancount provides A native Python plugin system whereby you may specify lists of Python modules to import and call to filter and transform the parsed directives to implement new features; An easy loader function that allows you to access the internal data structures resulting from parsing and processing a Beancount ledger. This is also a native Python library. So basically, you must write Python to extend Beancount. I\u2019m planning to provide output to XML and SQL as well (due to the simple data structures I\u2019m using these will be both very simple to implement). Moreover, using Beancount\u2019s own \u201cprinter\u201d module produces text that is guaranteed to parse back into exactly the same data structure (Beancount guarantees round-tripping of its syntax and its data structures). One advantage is that the plugins system allows you to perform in-stream arbitrary transformations of the list of directives produced, and this is a great way to prototype new functionality and easier syntax. For example, you can allocate a special tag that will trigger some arbitrary transformation on such tagged transactions. And these modules don\u2019t have to be integrated with Beancount: they can just live anywhere on your own PYTHONPATH, so you can experiment without having to contribute new features upstream or patch the source code. (Note: Beancount is implemented in Python 3, so you might have to install a recent version in order to work with it, such as Python-3.4, if you don\u2019t have it. At this point, Python 3 is becoming pretty widespread, so I don\u2019t see this as a problem, but you might be using an older OS.)","title":"Extension Mechanisms"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#automated-transactions-via-plugins","text":"Ledger provides a special syntax to automatically insert postings on existing transactions , based on some matching criteria. The syntax allows the user to access some of a posting\u2019s data, such as amount and account name. The syntax is specialized to also allow the application of transaction and posting \u201ctags.\u201d Beancount allows you to do the same thing and more via its plugin extension mechanism. Plugins that you write are able to completely modify, create or even delete any object and attribute of any object in the parsed flow of transactions, allowing you to carry out as much automation and summarization as you like. This does not require a special syntax - you simply work in Python, with access to all its features - and you can piggyback your automation on top of existing syntax. Some examples are provided under beancount.plugins.* . There is an argument to be made, however, for the availability of a quick and easy method for specifying the most common case of just adding some postings. I\u2019m not entirely convinced yet, but Beancount may acquire this eventually (you could easily prototype this now in a plugin file if you wanted).","title":"Automated Transactions via Plugins"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#no-support-for-time-or-effective-dates","text":"Beancount does not represent the intra-day time of transactions, its granularity is a day. Ledger allows you to specify the time of transactions down to seconds precision. I choose to limit its scope in the interest of simplicity, and I also think there are few use cases for supporting intra-day operations. Note that while Beancount\u2019s maximum resolution is one day, when it sorts the directives it will maintain the relative order of all transactions that occurs within one day, so it is possible to represent multiple transactions occurring on the same day in Beancount and still do correct inventory bookings. But I believe that if you were to do day-trading you would need a more specialized system to compute intra-day returns and do technical analysis and intraday P/L calculations. Beancount is not suited for those (a lot of other features would be needed if that was its scope). Ledger also has support for effective dates which are essentially an alternative date for the transaction. Reporting features allow Ledger users to use the main date or the alternative date. I used to have this feature in Beancount and I removed it, mainly because I did not want to introduce reporting options, and to handle two dates, say a transaction date and a settlement date, I wanted to enforce that at any point in time all transactions would balance. I also never made much use of it which indicates it was probably futile. Handling postings to occur at different points in time would have created imbalances, or I would have had to come up with a solution that involved \u201climbo\u201d or \u201ctransfer\u201d accounts. I preferred to just remove the feature: in 8 years of data, I have always been able to fudge the dates to make everything balance. This is not a big issue. Note that handling those split transaction and merging them together will be handled; a proposal is underway.","title":"No Support for Time or Effective Dates"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#documents","text":"Beancount offers support for integrating a directory hierarchy of documents with the contents of a ledger\u2019s chart of accounts. You can provide a directory path and Beancount will automatically find and create corresponding Document directives for filenames that begin with a date in directories that mirror account names, and attach those documents to those accounts. And given a bit of configuration, the bean-file tool can automatically file downloaded documents to such a directory hierarchy. Ledger\u2019s binding of documents to transactions works by generic meta-data. Users can attach arbitrary key-value pairs to their transactions, and those can be filenames. Beyond that, there is no particular support for document organization.","title":"Documents"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#simpler-and-more-strict","text":"Finally, Beancount has a generally simpler input syntax than Ledger. There are very few command-line options\u2014and this is on purpose, I want to localize all the input within the file\u2014and the directive syntax is more homogeneous: all transactions begin with a date and a keyword. If the argument of simplicity appeals to you, you might prefer to work with Beancount. I feel that the number of options offered in Ledger is daunting, and I could not claim to understand all of the possible ways they might interact with each other. If this does not worry you, you might prefer to use Ledger. It is also more strict than Ledger. Certain kinds of Beancount inputs are not valid. Any transaction in an account needs to have an open directive to initiate the account, for example (though some of these constraints can be relaxed via optional plugins). If you maintain a Beancount ledger, you can expect to have to normalize it to fix a number of common errors being reported. I view this as a good thing: it detects many potential problems and applies a number of strict constraints to its input which allows us to make reasonable assumptions later on when we process the stream of directives. If you don\u2019t care about precision or detecting potential mistakes, Ledger will allow you to be more liberal. On the other hand if you care to produce a precise and flawless account of transactions, Beancount offers more support in its validation of your inputs.","title":"Simpler and More Strict"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#web-interface","text":"Beancount has a built-in web interface, and there is an external project called Fava which significantly improves on the same theme. This is the default mode for browsing reports. I believe HLedger also has a web interface as well.","title":"Web Interface"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#missing-features","text":"Beancount generally attempts to minimize the number of features it provides. This is in contrast with Ledger\u2019s implementation, which has received a substantial amount of feature addition in order to experiment with double-entry bookkeeping. There are a large number of options. This reflects a difference in approach: I believe that there is a small core of essential features to be identified and that forward progress is made when we are able to minimize and remove any feature that is not strictly necessary. My goal is to provide the smallest possible kernel of features that will allow one to carry out the full spectrum of bookkeeping activities, and to make it possible for users to extend the system to automate repetitive syntax. But here is a list of features that Beancount does not support that are supported in Ledger, and that I think would be nice to have eventually. The list below is unlikely to be exhaustive.","title":"Missing Features"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#console-output","text":"Beancount\u2019s original implementation focused on providing a web view for all of its contents. During the 2.0 rewrite I began implementing some console/text outputs, mainly because I want to be able for reports to be exportable to share with others. I have a trial balance view (like Ledger\u2019s \u201cbal\u201d report) but for now the journal view isn\u2019t implemented. Ledger, on the other hand, has always focused on console reports. I\u2019ll make all the reports in Beancount support output to text format first thing after the initial release, as I\u2019m increasingly enjoying text reports. Use bean-query --list-formats to view current status of this.","title":"Console Output"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#filtering-language","text":"Beancount does not yet have a filtering language. Until recently, its web interface was the main mode for rendering reports and exploring the contents of its ledger, and it provided limited subsets of transactions in the form of \u201cviews\u201d, e.g., per-year, per-tag, etc. Having a filtering language in particular allows one to do away with many sub-accounts. I want to simplify my chart of accounts so I need this. I\u2019m working on adding a simple logical expression language to do arbitrary filters on the set of Beancount transactions. This is straightforward to implement and a high priority.","title":"Filtering Language"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#no-meta-data","text":"Beancount does not currently support meta-data. Ledger users routinely make liberal use of metadata. This has been identified as a powerful feature and a prototype has already been implemented. Meta-data will be supported on any directive type as well as on any posting. A dictionary of key-value pairs will be attached to each of these objects. Supported values will include strings, dates, numbers, currencies and amounts. So far the plan is to restrict Beancount\u2019s own code to make no specific use of meta-data, on purpose. The meta-data will be strictly made available for user-plugins and custom user scripts to make use of.","title":"No Meta-data"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#no-arithmetic-expressions","text":"Beancount does not support arbitrary expression evaluation in the syntax, in the places where numbers are allowed in the input. Ledger does. I have had no use for these yet, but I have no particular reason against adding this, I just haven\u2019t implemented it, as I don\u2019t need it myself. I think an implementation would be straightforward and very low risk, a simple change to the parser and there is already a callback for numbers. I think there are many legitimate uses for it.","title":"No Arithmetic Expressions"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#limited-support-for-unicode","text":"Beancount supports UTF8 or and other encodings in strings only (that is, for input that is in quotes). For example, you can enter payees and narrations with non-ASCII characters, but not account names (which aren\u2019t in quotes). Ledger supports other encodings over the entire file. The reason for the lack of a more general encoding support in Beancount is the current limitation of tokenizer tools. I\u2019ve been using GNU flex to implement my lexer and it does not support arbitrary encodings. I just need to write a better lexer and make that work with Bison , it\u2019s not a difficult task. I will eventually write my own lexer manually\u2014this has other advantages\u2014and will write it to support Unicode (Python 3 has full support for this, so all that is required is to modify the lexer, which is one simple compilation unit). This is a relatively easy and isolated task to implement.","title":"Limited Support for Unicode"},{"location":"a_comparison_of_beancount_and_ledger_hledger.html#no-forecasting-or-periodic-transactions","text":"Beancount has no support for generating periodic transactions for forecasting, though there is a plugin provided that implements a simplistic form of it to be used as an example for how plugins work (see beancount.plugins.forecast ). Ledger supports periodic transaction generation . I do want to add this to core Beancount eventually, but I want to define the semantics very clearly before I do. Updating one\u2019s ledger is essentially the process of copying and replicating transactional data that happens somewhere else. I don\u2019t believe that regular transactions are that \u201cregular\u201d in reality; in my experience, there is always some amount of small variations in real transactions that makes it impossible to automatically generate a series of transactions by a generator in a way that would allow the user to forgo updating them one-by-one. What it is useful for in my view, is for generating tentative future transactions. I feel strongly that those transactions should be limited not to straddle reconciled history, and that reconciled history should replace any kind of automatically generated transactions. I have some fairly complete ideas for implementing this feature, but I\u2019m not using forecasting myself at the moment, so it\u2019s on the backburner. While in theory you can forecast using Ledger\u2019s periodic transactions, to precisely represent your account history you will likely need to adjust the beginning dates of those transactions every time you append new transactions to your accounts and replace forecasted ones. I don\u2019t find the current semantics of automated transactions in Ledger to be very useful beyond creating an approximation of account contents. (In the meantime, given the ease of extending Beancount with plugins, I suggest you begin experimenting with forecast transactions on your own for now, and if we can derive a generic way to create them, I\u2019d be open to merging that into the main code.)","title":"No Forecasting or Periodic Transactions"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html","text":"Inventory Booking \uf0c1 A Proposal for an Improvement on Command-Line Bookkeeping Martin Blais, June 2014 http://furius.ca/beancount/doc/proposal-booking Motivation Problem Description Lot Date Average Cost Basis Capital Gains Sans Commissions Cost Basis Adjustments Dealing with Stock Splits Previous Solutions Shortcomings in Ledger Shortcomings in Beancount Requirements Debugging Tools Explicit vs. Implicit Booking Reduction Design Proposal Inventory Input Syntax & Filtering Algorithm Implicit Booking Methods Dates Inserted by Default Matching with No Information Reducing Multiple Lots Examples No Conflict Explicit Selection By Cost Explicit Selection By Date Explicit Selection By Label Explicit Selection By Combination Not Enough Units Redundant Selection of Same Lot Automatic Price Extrapolation Average Cost Booking Future Work Implementation Note Conclusion Motivation \uf0c1 The problem of \u201cinventory booking,\u201d that is, selecting which of an inventory\u2019s trade lots to reduce when closing a position, is a tricky one. So far, in the command-line accounting community, relatively little progress has been made in supporting the flexibility to deal with many common real-life cases. This document offers a discussion of the current state of affairs, describes the common use cases we would like to be able to solve, identifies a set of requirements for a better booking method, and proposes a syntax and implementation design to support these requirements, along with clearly defined semantics for the booking method. Problem Description \uf0c1 The problem under consideration is the problem of deciding, for a double-entry transaction that intends to reduce the size of a position at a particular point in time in a particular account, which of the account inventory lots contained at that point should be reduced. This should be specified using a simple data entry syntax that minimizes the burden on the user. For example, one could enter a position in HOOL stock by buying two lots of it at different points in time: 2014-02-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {500 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD 2014-02-15 * \"Second trade\" Assets:Investments:Stock 8 HOOL {510 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD We will call the two sets of shares \u201ctrade lots\u201d and assume that the underlying system that counts them is keeping track of each of their cost and date of acquisition separately. The question here is, if we were to sell some of this stock, which of the shares should we select? Those from the first trade, or those from the second? This is an important decision, because it has an impact on capital gains, and thus on taxes. (The account of capital gains is described in more detail in the \u201cStock Trading\u201d section of the Beancount cookbook if you need an explanation of this.) Depending on our financial situation, this year\u2019s trading history, and the unrealized gains that a trade would realize, sometimes we may want to select one lot over another. By now, most discount brokers even let you select your specific lot when you place a trade that reduces or closes a position. It is important to emphasize here the two directions of inventory booking: Augmenting a position. This is the process of creating a new trading lot, or adding to an existing trading lot (if the cost and other attributes are identical). This is easy, and amounts to adding a record to some sort of mapping that describes an account\u2019s balance (I call this an \"inventory\"). Reducing a position. This is generally where booking complications take place, and the problem is essentially to figure out which lot of an existing position to remove units from. Most of this document is dedicated to the second direction. Lot Date \uf0c1 Even if the cost of each lot is identical, the acquisition date of the position you\u2019re intending to close matters, because different taxation rules may apply, for example, in the US, positions held for more than 12 months are subject to a significantly lower tax rate (\u201cthe long-term capital gains rate\u201d). For example, if you entered your position like this: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {300 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD 2014-02-15 * \"Second trade at same price\" Assets:Investments:Stock 8 HOOL {300 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD and we are booking a trade for 2014-03-01, selecting the first lot would result in a long-term (low) gain, because two years have passed since acquisition (position held from 2012-05-01 to 2014-03-01) while booking against the second would result in a short-term (high) capital gains tax. So it\u2019s not just a matter of the cost of the lots. Our systems don't aim to file taxes automatically at this point but we would like to enter our data in a way that eventually allows this kind of reporting to take place. Note that this discussion assumes that you were able to decide which of the lots you could trade against. There are several taxation rules that may apply, and in some cases, depending on which country you live in, you may not have a choice, the government may dictate how you are meant to report your gains. In some cases you may even need to be able to apply multiple booking methods to the same account (e.g., if an account is subject to a taxation claim from multiple countries). We will ignore this exotic case in this document, as it is quite rare. Average Cost Basis \uf0c1 Things get more complicated for tax-sheltered accounts. Because there are no tax consequences to closing positions in these accounts, brokers will normally choose to account for the book value of your positions as if they were a single lot. That is, the cost of each share is computed using the weighted average of the cost of the position you are holding. This can equivalently be calculated by summing the total cost of each position and then dividing by the total number of shares. To continue with our first example: 10 HOOL x 500 USD/HOOL = 5000 USD 8 HOOL x 510 USD/HOOL = 4080 USD Cost basis = 5000 USD + 4080 USD = 9080 USD Total number of shares = 10 HOOL + 8 HOOL = 18 HOOL Cost basis per share = 9080 USD / 18 HOOL = 504.44 USD/HOOL So if you were to close some of your position and sell some shares, you would do so at a cost of 504.44 USD/HOOL. The gain you would realize would be relative to this cost; for example if you sold 5 shares at 520 USD/HOOL, your gain would be calculated like this: (520 USD/HOOL - 504.44 USD/HOOL) x 5 HOOL = 77.78 USD This type of booking method is made evident by two kinds of financial events that you might witness occur in these types of accounts: Fees may be withdrawn by selling some shares reported at their current market price. You will see these if you hold a retirement account at Vanguard in the USA. The broker takes a fixed amount of dollars per quarter (5$, with my employer's plan) that is backed out in terms of a small, fractional number of shares of each fund to sell. That transaction is oblivious to capital gains: they simply figure out how many shares to sell to cover their fee and don\u2019t report a gain to you. You have to assume it doesn\u2019t matter. Most people don't track their gains in non-taxable accounts but freaks like us may want to compute the return nonetheless. Cost basis readjustments may occur spontaneously. This is rare, but I have seen this once or twice in the case of mutual funds in a Canadian tax-deferred account: based on some undisclosed details of their trading activity, the fund management has had to issue an adjustment to the cost basis, which you are meant to apply to your positions. You get an email telling you how much the adjustment was for. 99% of people probably don\u2019t blink, don't understand and ignore this message\u2026 perhaps rightly so, as it has no impact on their taxes, but if you want to account for your trading returns in that account, you do need to count it. So we need to be able to add or remove to the cost basis of existing positions. Note that the average cost of your positions changes on every trade and needs to get recalculated every time that you add to an existing position. A problem with this is that if you track the cost per share, these multiple recalculations may result in some errors if you store the amounts using a fixed numerical representation (typically decimal numbers). An alternative method to track the cost in our data structures, one that is more stable numerically, is to simply keep track of the total cost instead of the cost-per-share. Also note that using fractional numbers does not provide a sufficient answer to this problem: in practice, the broker may report a specific cost, one that is rounded to specific number of decimals, and we may want to be able to book our reducing trade using that reported number rather than maintain the idealized amount that a fractional representation would. Both using a fixed decimal or a fractional representation pose problems. We will largely ignore those problems here. In summary: we need to be able to support book against lots at their average cost, and we need to be able to adjust the cost basis of an existing position. I\u2019ve been thinking about a solution to implement this that would not force an account to be marked with a special state. I think we can do this using only inventory manipulations. All we need is a small change to the syntax that allows the user to indicate to the system that it should book against the average cost of all positions. Using the first example above, acquiring the shares would take the same form as previously, that is after buying two lots of 10 and 8 HOOL units, we end up with an inventory that holds two lots, one of 10 HOOL {500 USD} and one of 8 HOOL {510 USD}. However, when it\u2019s time to sell, the following syntax would be used (this is an old idea I\u2019ve been meaning to implement for a while): 2014-02-01 * \"Selling 5 shares at market price 550 USD\" Assets:Investments:Stock -5 HOOL {*} Assets:Investments:Cash 2759.95 USD Expenses:Commissions 9.95 USD Income:Investments:CapitalGains When the \u201c * \u201d is encountered in lieu of the cost, like this, it would: Merge all the lots together and recalculate the average price per share (504.44 USD) Book against this merged inventory, reducing the resulting lot. After the transaction, we would end up with a single position of 13 HOOL {504.44 USD). Adding to this position at a different price would create a new lot, and those would get merged again the next time a reduction occurs at average cost. We do not need to merge lots until there is a reduction. Apart from concerns of accumulating rounding error, this solution is correct mathematically, and maintains the property that accounts aren\u2019t coerced into any specific booking method\u2014a mix of methods could be used on every reduction. This is a nice property, even if not strictly necessary, and even if we want to be able to just specify a \u201cdefault\u201d booking method to use per account and just stick with it throughout. It\u2019s always nice to have the flexibility to support exceptions, because in real life, they sometimes occur. Capital Gains Sans Commissions \uf0c1 We would like to be able to support the automatic calculation of capital gains without the commissions. This problem is described in much detail in the Commissions section of the trading documentation and in a thread on the mailing-list . The essence of the complication that occurs is that one cannot simply subtract the commissions incurred during the reporting period from the gains that include commissions, because the commissions that were incurred to acquire the position must be pro-rated to the shares of the position that are sold. The simplest and most common way to implement this is to include the costs of acquiring the position into the cost basis of the position itself, and deduct the selling costs from the market value when a position is reduced. Whatever new design we come up with must allow us to count these adjusted gains as this is essential to various individuals' situations. In Beancount, I have figured out a solution for this problem, which luckily enough involves only an automated transformation of a transaction flagged by the presence of a special flag on its postings... if and only if I can find a way to specify which lot to book against without having to specify its cost, because once the cost of commissions gets folded into the cost of the position, the adjusted cost does not show up anywhere in the input file, the user would have to calculate it manually, which is unreasonable. In the solution I\u2019m proposing, the following transactions would be transformed automatically, triggered by the presence of the \u201cC\u201d flag: 2014-02-10 * \"Buy\" Assets:US:Invest:Cash -5009.95 USD C Expenses:Commissions 9.95 USD Assets:US:Invest:HOOL 10.00 HOOL {500 USD / aa2ba9695cc7} 2014-04-10 * \"Sell #1\" Assets:US:Invest:HOOL -4.00 HOOL {aa2ba9695cc7} C Expenses:Commissions 9.95 USD Assets:US:Invest:Cash 2110.05 USD Income:US:Invest:Gains 2014-05-10 * \"Sell #2\" Assets:US:Invest:HOOL -6.00 HOOL {aa2ba9695cc7} C Expenses:Commissions 9.95 USD Assets:US:Invest:Cash 3230.05 USD Income:US:Invest:Gains They would be automatically transformed by a plugin into the following and replace the original transactions above: 2014-02-10 * \"Buy\" Assets:US:Invest:Cash -5009.95 USD X Expenses:Commissions 9.95 USD X Income:US:Invest:Rebates -9.95 USD X Assets:US:Invest:HOOL 10.00 HOOL {500.995 USD / aa2ba9695cc7} 2014-04-10 * \"Sell #1\" X Assets:US:Invest:HOOL -4.00 HOOL {aa2ba9695cc7} X Expenses:Commissions 9.95 USD X Income:US:Invest:Rebates -9.95 USD Assets:US:Invest:Cash 2110.05 USD Income:US:Invest:Gains ; Should be (530-500)*4 - 9.95*(4/10) - 9.95 = ; 106.07 USD 2014-05-10 * \"Sell #2\" X Assets:US:Invest:HOOL -6.00 HOOL {aa2ba9695cc7} X Expenses:Commissions 9.95 USD X Income:US:Invest:Rebates -9.95 USD Assets:US:Invest:Cash 3230.05 USD Income:US:Invest:Gains ; Should be (540-500)*6 - 9.95*(6/10) - 9.95 = ; 224.08 USD The \u201cX\u201d flag would just mark the postings that have been transformed, so maybe they can be rendered with a special color or attribute later on. The rebates account is included as a separate counter just to make the transaction balance. The reason I need to relax the disambiguation of trading lots is that the user needs to be able to specify the matching leg without having to specify the cost of the position, because at the point where the position is reduced (2014-04-10), there is no way to figure out what the cost of the original lot was. Just to be clear, in the example above, this means that if all the information have is 4 HOOL and 500 USD, there is no way to back out a cost of 500.995 USD that could specify a match with the opening trade lot, because that happened with 10 HOOLs. So we need to be more explicit about booking. In the example above, I\u2019m selecting the matching lot \u201cby label,\u201d that is, the user has the option to provide a unique lot identifier in the cost specification, and that can later on be used to disambiguate which lot we want to book a reduction/sale against. The example above uses the string \u201c aa2ba9695cc7 \u201d in this way. (An alternative solution to this problem would involve keep track of both the original cost (without commissions), and the actual cost (with commissions), and then finding the lot against the former, but using the latter to balance the transaction. This idea is to allow the user to keep using the cost of a position to select the lot, but I\u2019m not even sure if that is possible, in the presence of various changes to the inventory. More thought is required on this matter.) Cost Basis Adjustments \uf0c1 In order to adjust the cost basis, one can replace the contents of an account explicitly like this: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500 USD} Assets:US:Invest:HOOL 10.00 HOOL {510 USD} Income:US:Invest:Gains This method works well and lets the system automatically compute the gains. But it would be nice to support making adjustments in price units against the total cost of the position, for example, \u201cadd 340.51 USD to the cost basis of this position\u201d. The problem is that the adjusted cost per share now needs to be computed by the user\u2026 it\u2019s inconvenient. Here\u2019s one way we could support this well, however: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500 USD} Assets:US:Invest:HOOL 10.00 HOOL {} Income:US:Invest:Gains -340.51 USD If all the legs are fully specified - they have a calculatable balance - we allow a single price to be elided. This solves this problem well. Dealing with Stock Splits \uf0c1 Accounting for stock splits creates some complications in terms of booking. In general, the problem is that we need to deal with changes in the meaning of a commodity over time. For example, you could do this in Beancount right now and it works: 2014-01-04 * \"Buy some HOOL\" Assets:Investments:Stock 10 HOOL {1000.00 USD} Assets:Investments:Cash 2014-04-17 * \"2:1 split into Class A and Class B shares\" Assets:Investments:Stock -10 HOOL {1000.00 USD} Assets:Investments:Stock 10 HOOL {500.00 USD} Assets:Investments:Stock 10 HOOLL {500.00 USD} One problem with splitting lots this way is that the price and meaning of a HOOL unit before and after the split differs, but let\u2019s just assume we\u2019re okay with that for now (this can be solved by relabeling the commodity by using another name, ideally in a way that is not visible to the user - we have a pending discussion elsewhere ). The issue that concerns booking is, when you sell that position, which cost do you use? The inventory will contain positions at 500 USD, so that\u2019s what you should be using, but is it obvious to the user? We can assume this can be learned with a recipe . Now, what if we automatically attach the transaction date? Does it get reset on the split and now you would have to use 2014-04-17? If so, we could not automatically inspect the list of trades to determine whether this is a long-term vs. short-term trade. We need to somehow preserve some of the attributes of the original event that augmented this position, which includes the original trade date (not the split date) and the original user-specified label on the position, if one was provided. Forcing a Single Method per Account \uf0c1 For accounts that will use the average booking method, it may be useful to allow specifying that an account should only use this booking method . The idea is to avoid data entry errors when we know that an account is only able to use this method. One way to ensure this is to automatically aggregate inventory lots when augmenting a position. This ensures that at any point in time, there is only a single lot for each commodity. If we associated an inventory booking type to each account, we could define a special one that also triggers aggregation upon the addition of a position. Another approach would be to not enforce these aggregations, but to provide a plugin that would check that for those accounts that are marked as using the average cost inventory booking method by default, that only such bookings actually take place. Previous Solutions \uf0c1 This section reviews existing implementations of booking methods in command-line bookkeeping systems and highlights specific shortcomings that motivate why we need to define a new method. Shortcomings in Ledger \uf0c1 (Please note: I\u2019m not deeply familiar with the finer details of the inner workings of Ledger; I inferred its behavior from building test examples and reading the docs. If I got any of this wrong, please do let me know by leaving a comment.) Ledger\u2019s approach to booking is quite liberal. Internally, Ledger does not distinguish between conversions held at cost and regular price conversions : all conversions are assumed held at cost, and the only place trading lots appear is at reporting time (using its --lots option). Its \u201c {} \u201d cost syntax is meant to be used to disambiguate lots on a position reduction , not to be used when acquiring a position. The cost of acquiring a position is specified using the \u201c @ \u201d price notation: 2014/05/01 * Buy some stock (Ledger syntax) Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash This will result in an inventory of 10 HOOL {500 USD}, that is, the cost is always stored in the inventory that holds the position: $ ledger -f l1.lgr bal --lots 10 HOOL {USD500} [2014/05/01] USD-5000 Assets:Investments USD-5000 Cash 10 HOOL {USD500} [2014/05/01] Stock -------------------- 10 HOOL {USD500} [2014/05/01] USD-5000 There is no distinction between conversions at cost and without cost\u2014all conversions are tracked as if \u201cat cost.\u201d As we will see in the next section, this behaviour is distinct from Beancount\u2019s semantics, which requires a conversion held at cost to be specified using the \u201c {} \u201d notation for both its augmenting and reducing postings, and distinguishes between positions held at cost and price conversions. The advantage of the Ledger method is a simpler input mechanism, but it leads to confusing outcomes if you accumulate many conversions of many types of currencies in the same account. An inventory can easily become fragmented in its lot composition. Consider what happens, for instance, if you convert in various directions between 5 different currencies\u2026 you may easily end up with USD held in CAD, JPY, EUR and GBP and vice-versa\u2026 any possible combinations of those are possible. For instance, the following currency conversions will maintain their original cost against their converted currencies: 2014/05/01 Transfer from Canada Assets:CA:Checking -500 CAD Assets:US:Checking 400 USD @ 1.25 CAD 2014/05/15 Transfers from Europe Assets:UK:Checking -200 GBP Assets:US:Checking 500 USD @ 0.4 GBP 2014/05/21 Transfers from Europe Assets:DE:Checking -100 EUR Assets:US:Checking 133.33 USD @ 0.75 EUR This results in the following inventory in the Assets:US:Checking account: $ ledger -f l2.lgr bal --lots US:Checking 500.00 USD {0.4 GBP} [2014/05/15] 133.33 USD {0.75 EUR} [2014/05/21] 400.00 USD {1.25 CAD} [2014/05/01] Assets:US:Checking The currencies are treated like stock. But nobody thinks of their currency balances like this , one normally thinks of currencies held in an account as units in and of themselves, not in terms of their price related to some other currency. In my view, this is confusing. After these conversions, I just think of the balance of that account as 1033.33 USD. A possible rebuttal to this observation is that most of the time a user does not care about inventory lots for accounts with just currencies, so they just happen not print them out. Out of sight, out of mind. But there is no way to distinguish when the rendering routine should render lots or not. If instructed to produce a report, a balance routine should ideally only render lots only for some accounts (e.g., investing accounts, where the cost of units matters) and not for others (e.g. accounts with currencies that were deposited sometimes as a result of conversions). Moreover, when I render a balance sheet, for units held at cost we need to report the book values rather than the number of shares. But for currencies, the currency units themselves need to get reported, always. If you don\u2019t distinguish between the two cases, how do you know which to render? I think the answer is to select which view you want to render using the options. The problem is that neither provides a single unified view that does the correct thing for both types of commodities. The user should not have to specify options to view partially incorrect views in one style or the other, this should be automatic and depend on whether we care about the cost of those units. This gets handled correctly if you distinguish between the two kinds of conversions. But perhaps most importantly, this method does not particularly address the conversion problem : it is still possible to create money out of thin air by making conversions back and forth at different rates: 2014/05/01 Transfer to Canada Assets:US:Checking -40000 USD Assets:CA:Checking 50000 CAD @ 0.80 USD 2014/05/15 Transfer from Canada Assets:CA:Checking -50000 CAD @ 0.85 USD Assets:US:Checking 42500 USD This results in a trial balance with just 2500 USD: $ ledger -f l3.lgr bal 2500 USD Assets:US:Checking This is not a desirable outcome. We should require that the trial balance always sum to zero (except for virtual transactions). After all, this is the aggregate realization of the elegance of the double-entry method: because all transactions sum to zero, the total sum of any subgroup of transactions also sums to zero\u2026 except it doesn\u2019t. In my view, any balance sheet that gets rendered should comply with the accounting equation, precisely. In my explorations with Ledger, I was hoping that by virtue of its inventory tracking method it would somehow naturally deal with these conversions automatically and thus provide a good justification for keeping track of all units at cost, but I witness the same problem showing up. (The conversions issue has been a real bitch of a problem to figure out a solution for in Beancount, but it is working, and I think that the same solution could be applied to Ledger to adjust these sums, without changing its current booking method: automatically insert a special conversion entry at strategic points - when a balance sheet is rendered.) In the end, I have not found that tracking costs for every transaction would appear to have an advantage over forgetting the costs for price conversions, the conversions problem is independent of this. I\u2019d love to hear more support in favour of this design choice. Now, because all conversions maintain their cost, Ledger needs to be quite liberal about booking, because it would be inconvenient to force the user to specify the original rate of conversion for each currency, in the case of commonly occurring currency conversions. This has an unfortunate impact on those conversions which we do want to hold at cost: arbitrary lot reductions are allowed, that is, reductions against lots that do not exist. For example, the following trade does not trigger an error in Ledger: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash 2014/05/15 Sale of an impossible lot Assets:Investments:Stock -10 HOOL {505 USD} @ 510 USD Assets:Investments:Cash This results in an inventory with a long position of 10 HOOL and a short position of 10 HOOL (at a different cost): $ ledger -f l4.lgr bal --lots stock 10 HOOL {USD500} [2014/05/01] -10 HOOL {USD505} Assets:Investments:Stock I think that the reduction of a 505 USD position of HOOL should not have been allowed; Beancount treats this as an error by choice. Ledger clearly appears to support booking against an existing inventory, so surely the intention was to be able to reduce against existing positions. The following transactions do result in an empty inventory, for instance: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash 2014/05/15 Sale of matching lot Assets:Investments:Stock -10 HOOL {500 USD} [2014/05/01] @ 510 USD Assets:Investments:Cash With output: $ ledger -f l5.lgr bal --lots USD100 Equity:Capital Gains As you see, the HOOL position has been eliminated. (Don\u2019t mind the auto-inserted equity entry in the output, I believe this will be fixed in a pending patch .) However, both the date and the cost must be specified for this to work. The following transactions results in a similar problematic long + short position inventory as mentioned above: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash 2014/05/15 Sale of matching lot? Assets:Investments:Stock -10 HOOL {500 USD} @ 510 USD Assets:Investments:Cash With output: $ ledger -f l6.lgr bal --lots 10 HOOL {USD500} [2014/05/01] -10 HOOL {USD500} Assets:Investments:Stock USD100 Equity:Capital Gains -------------------- 10 HOOL {USD500} [2014/05/01] -10 HOOL {USD500} USD100 This is a bit surprising, I expected the lots to book against each other. I suspect this may be an unreported bug, and not intended behaviour. Finally, the \u201c {} \u201d cost syntax is allowed be used on augmenting legs as well. The documentation points to these methods being equivalent . It results in an inventory lot that does not have a date associated with it, but the other leg is not converted to cost: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL {500 USD} Assets:Investments:Cash With output: $ ledger -f l7.lgr bal --lots 0 Assets:Investments -10 HOOL {USD500} Cash 10 HOOL {USD500} Stock -------------------- 0 I probably just don\u2019t understand how this is meant to be used; in Beancount the automatically computed leg would have posted a -5000 USD amount to the Cash account, not shares. What I think is going on, is that Ledger (probably) accumulates all the lots without attempting to match them together to try to make reductions, and then at reporting time - and only then - it matches the lots together based on some reporting options: Group lots by both cost/price and date, using --lots Group lots by cost/price only, using --lot-prices Group lots by date only, using --lot-dates It is not entirely obvious from the documentation how that works, but the behavior is consistent with this. (If this is correct, I believe that to be a problem with its design: the mechanism by which an inventory reduction is booked to an existing set of lots should definitely not be a reporting feature. It needs to occur before processing reports, so that a single and definitive history of inventory bookings can be realized. If variants in bookings are supported - and I don\u2019t think this is a good idea - they should be supported before the reporting phase. In my view, altering inventory booking strategies from command-line options is a bad idea, the strategies in place should be fully defined by the language itself.) Shortcomings in Beancount \uf0c1 Beancount also has various shortcomings, though different ones. In contrast to Ledger, Beancount disambiguates between currency conversions and conversions \u201cheld at cost\u201d: 2014-05-01 * \"Convert some Loonies to Franklins\" Assets:CA:Checking -6000 CAD Assets:Investments:Cash 5000 USD @ 1.2 CAD ; Currency conversion 2014-05-01 * \"Buy some stock\" Assets:Investments:Stock 10 HOOL {500 USD} ; Held at cost Assets:Investments:Cash In the first transaction, the Assets:Investment:Cash account results in an inventory of 5000 USD, with no cost associated to it. However, after the second transaction the Assets:Investment:Stock account has an inventory of 10 HOOL with a cost of 500 USD associated to it. This is perhaps a little bit more complex, and requires more knowledge from the user: there are two kinds of conversions and he has to understand and distinguish between these two cases, and this is not obvious for newcomers to the system. Some user education is required (I\u2019ll admit it would help if I wrote more documentation). Beancount\u2019s method is also less liberal, it requires a strict application of lot reduction. That is, if you enter a transaction that reduces a lot that does not exist, it will output an error. The motivation for this is to make it difficult for the user to make a mistake in data entry. Any reduction of a position in an inventory has to match against exactly one lot. There are a few downsides that result from this choice: It requires the users to always find the cost of the lot to match against. This means that automating the import of transactions requires manual intervention from the user, as he has to go search in the ledger to insert the matching lot. (Note that theoretically the import code could load up the ledger contents to list its holdings, and if unambiguous, could look for the cost itself and insert it in the output. But this makes the import code dependent on the user\u2019s ledger, which most import codes aren't doing). This is an important step, as finding the correct cost is required in order to correctly compute capital gains, but a more flexible method is desired, one that allows the user to be a bit more lazy and not have to put the cost of the existing position when the choice is obvious, e.g., when there is a single lot of that unit to match against. I\u2019d like to relax this method somewhat. The strict requirement to reduce against an existing lot also means that both long and short positions of the same commodities \u201cheld at cost\u201d in the same account cannot exist. This is not much of a problem in practice, however, because short positions are quite rare\u2014few individuals engage in them\u2014and the user could always create separate accounts to hold short positions if needed. In fact, it is already customary to create a dedicated subaccount for each commodity in an investment account, as it naturally organizes trading activity nicely (reports a bit of a mess otherwise, it\u2019s nice to see the total position value grouped by stock, since they offer exposure to different market characteristics). Different accounts should work well, as long as we treat the signs correctly in reductions, i.e., buying stock against an existing short position is seen as a position reduction (the signs are just reversed). Another problem with the Beancount booking method is in how it matches against lots with dates. For example, if you were to try to disambiguate between two lots at the same price, the most obvious input would not work: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {500 USD} Assets:Investments:Cash 2014-02-15 * \"Second trade at very same price\" Assets:Investments:Stock 8 HOOL {500 USD} Assets:Investments:Cash 2014-06-30 * \"Trying to sell\" Assets:Investments:Stock -5 HOOL {500 USD / 2012-05-01} Assets:Investments:Cash This would report a booking error. This is confusing. In this case Beancount, in its desire to be strict, applies strict matching against the inventory lots, and attempting to match units of a lot of (HOOL, 500 USD, 2012-05-01) with units of (HOOL, 500 USD, None ) simply fails. Note that the lot-date syntax is accepted by Beancount on both augmenting and reducing legs, so the \u201csolution\u201d is that the user is forced to specifically provide the lot-date on acquiring the position. This would work, for instance: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {500 USD / 2012-05-01} Assets:Investments:Cash 2014-02-15 * \"Second trade at very same price\" Assets:Investments:Stock 8 HOOL {500 USD} Assets:Investments:Cash 2014-06-30 * \"Trying to sell\" Assets:Investments:Stock -5 HOOL {500 USD / 2012-05-01} Assets:Investments:Cash The inconvenience here is that when the user entered the first trade, he could not predict that he would enter another trade at the same price in the future and typically would not have inserted the lot-date. More likely than not, the user had to go back and revisit that transaction in order to disambiguate this. We can do better. This is a shortcoming of its implementation, which reflects the fact that position reduction was initially regarded as an exact matching problem rather than a fuzzy matching one\u2014it wasn\u2019t clear to me how to handle this safely at the time. This needs to be fixed after this proposal, and what I\u2019m doing with this document is very much a process of clarifying for myself what I want this fuzzy matching to mean, and to ensure that I come up with an unambiguous design that is easy to enter data for. Ledger always automatically attaches the transaction date to the inventory lot, and in my view this is the correct thing to do (below we propose attaching more information as well). Context \uf0c1 [Updated on Nov\u20192014] It is important to distinguish between two types of booking: Booking. The strict type of booking that occurs when we are considering the entire list of transactions. We will call this the \u201cbooking\u201d process, and it should occur just once. This process should be strict: the failure of correctly book a reduction to an existing lot should trigger a fatal error. Inventory Aggregation. When we are considering a subset of entries, it is possible that entries that add some lots are removed, and that other entries that reduce or remove those lots are kept. This creates a situation in which it is necessary to support aggregations over time of changes to an inventory that may not admit a matching lot. If we are to support arbitrary subsets of transactions being aggregated, we must take this into account. This aggregation process should never lead to an error. Let\u2019s take an example to justify why we need the non-booking summarization. Let\u2019s say we have a stock purchase and sale: 2013-11-03 * \"Buy\" Assets:Investments:VEA 10 AAPL {37.45 USD} ;; (A) Assets:Investments:Cash -370.45 USD 2014-08-09 * \"Sell\" Assets:Investments:VEA -5 AAPL {37.45 USD} Assets:Investments:Cash 194.40 USD Income:Investments:PnL If we filter \u201cby year\u201d and only want to view transactions that occurred in 2014, AND we don\u2019t \u201cclose\u201d the previous year, that is, we do not create opening balances entries that would deposit the lots in the account at the beginning of the year - this could happen if we filter by tag, or by some other combination of conditions - then the (A) lot is not present in the inventory to be debited from. We must somehow accommodate a -5 AAPL at that point. When reporting, the user could decide how to summarize and \u201cmatch up as best it can\u201d those legs, but converting them to units or cost, or convert them to a single lot at average-cost. The booking process, however, should admit no such laxity. It should be strict and trigger an error if a lot cannot be matched. This would only be applied on the total set of transactions, and only once, never on any subset. The purpose of the booking stage should be threefold: Match against existing lots and issue errors when that is impossible. Do this, using the method specified by the user. Replace all partially specified lots by a full lot specification, the lot that was matched during the booking process. Insert links on transactions that form a trade: a buy and corresponding sells should be linked together. This essentially identifies trades, and can then be used for reporting later on. (Trades at average cost will require special consideration.) We can view Ledger\u2019s method as having an aggregation method only, lacking a booking stage. The particular method for aggregation is chosen using --lots or --lot-dates or --lot-prices. This would indicate that a \u201cbooking\u201d stage could be tacked on to Ledger without changing much of its general structure: it could be inserted as a pre- or post- process, but it would require Ledger to sort the transactions by date, something it does not currently do (it runs a single pass over its data). Beancount has had both methods for a while, but the separate nature of these two operations has not been explicitly stated so far\u2014instead, the inventory supported an optional flag to raise an error when booking was impossible. Also, inventory aggregation has not been thought as requiring much customization, it was meant to be unified to the booking process. It is now clear that they are two distinct algorithms, and that a few different methods for aggregating can be relevant (though they do not function the same as Ledger\u2019s do. Note: you can view the inventory aggregation method as a GROUP BY within an Inventory object\u2019s lots: which columns do you group over? This requires use cases). In any case, fully specified lots should match against each other by default: we need to support this as the degenerate case to use for simple currencies (not held at cost)... we would not want to accumulate all changes in the same currency as separate lots in an inventory, they need to cross each other as soon as they are applied. We want to change this in order to make the code clearer: there should be a separate \u201cbooking\u201d stage, provided by a plugin , which resolves the partially specific lot reduction using the algorithm outlined in this document, and a separate method for inventory aggregation should not bother with any of those details. In fact, the inventory aggregation could potentially simply become an accumulated list of lots, and the summarization of them could take place a posteriori, with some conceptual similarly to when Ledger applies its aggregation. Just using a simple aggregation becomes more relevant once we begin entering more specific data about lots, such as always having an acquisition date, a label, and possibly even a link to the entry that created the lot. In order to trigger summarization sensibly, functions to convert and summarize accumulated inventories could be provided in the shell, such as \u201cUNITS(inventory)\u201d. Precision for Interpolation \uf0c1 The interpolation capabilities will be extended to cover eliding a single number, any number, in any subset of postings whose \u201cweights\u201d resolve to a particular currency (e.g., \u201call postings with weights in USD\u201d). The interpolation needs to occur at a particular precision. The interpolated number should be quantized automatically, and the number of fractional digits to quantize it to should be automatically inferred from the DisplayContext which is itself derived from the input file. Either the most common or the maximum number of digits witnessed in the file should be used. For a use case, see this thread : On Mon, Nov 24, 2014 at 12:33 PM, ELI wrote: Regarding the last piece on which there seems to be a misunderstanding between you and I, let me provide an standalone example, outside the context of the plugin. Since Vanguard only makes three decimal places of precision available me, I need to have the actual share count calculated from the price and transaction amount. For example, I have a transaction on my activities page with these details: Shares Transacted: 0.000 Share Price: 48.62 Amount: 0.02 The only way to enter this transaction and have it balance would be to manually calculate the Shares Transacted with four decimal places of precision. My preferred way of entering this would be to enter the Share Price and Amount and have Beancount calculate Shares Transacted to a precision associated with my Vanguard account. Additionally, as metadata, I'd record \"Shares Transacted: 0.000\" as history of \"what the statement said\". Maybe you can give me your thoughts on how such a case would be addressed with planned Beancount features or as a plugin I could right? What does the downloadable file contain? 0.000 or 0.0004 or even 0.00041? What this is, most likely, is the quarterly fee that they take, which they price in terms of shares. If this is your Vanguard account, and you sum all the similar such transactions around it, you should observe that it sums to 5$ per quarter. I would log it like this: 2014-11-23 * \"Quarterly fee\" Assets:US:Vanguard:VIIPX -0.00041 VIIPX {* 48.62 USD} Expenses:Financial:Fees 0.02 USD The \"*\" will be required to merge all the lots into a single one (average cost booking) because the 48.62 USD they declare is _not_ one of your lots, but rather just the price of the asset that happened to be there on the day they took the fee. In other words, they take their fee in terms of units of each asset, priced at the current price, deducting against the cost basis of all your lots together (it doesn't matter which because this is a pre-tax account, so no capital gains are reported). Now you're asking, how could I avoid calculating 0.00041 myself? My first answer would be: let's first see if the OFX download from Vanguard includes the precision. That would solve it. I believe it does (I checked my own history of downloads and I have some numbers in there with 4 fractional digits). My second answer would be that - if you don't have the number of shares from the file - after I implement the much needed new inventory booking proposal ( http://furius.ca/beancount/doc/proposal-booking ), the interpolation capabilities will be vastly extended, such that eliding the number of shares would even be possible, like this: 2014-11-23 * \"Quarterly fee\" Assets:US:Vanguard:VIIPX VIIPX {* 48.62 USD} Expenses:Financial:Fees 0.02 USD Now this by itself still would not solve the problem: this would store 0.000411353353 which is limited at 12 fractional digits because of the default context. So that's incorrect. What would need to be done to deal with this is to infer the most common number of digits used on units of VIIPX and to quantize the result to that number of digits (I think this could be safe enough). The number of digits appearing in the file for each currency is already tracked in a DisplayContext object that is stored in the options_map from the parser. I'll have to take that into account in the inventory booking proposal. I'm adding this to the proposal. Requirements \uf0c1 The previous sections introduce the general problem of booking, and point out important shortcomings in both the Beancount and Ledger implementations. This section will present a set of desires for a new and improved booking mechanism for command-line bookkeeping software implementations. Here are reasonable requirements for a new method: Explicit inventory lot selection needs to be supported. The user should be able to indicate precisely which of the lots to reduce a position against, per-transaction. An account or commodity should not have to have a consistent method applied across all its trades. Explicit inventory lot selection should support partial matching , that is, in cases where the lots to match against are ambiguous, the user should be able to supply only minimal information to disambiguate the lots, e.g., the date, or a label, or the cost, or combinations of these. For instance, if only a single lot is available to match against, the user should be able to specify the cost as \u201c {} \u201d, telling the system to book at cost, but essentially saying: \u201cbook against any lot.\u201d This should trigger an error only if there are multiple lots. A variety of informations should be supported to specify an existing lot, and they should be combinable: The cost of acquisition of the position; The date the position was acquired at; A user-provided label. Cases where insufficient information is provided for explicit lot selection is available should admit for a default booking method to be invoked. The user should be able to specify globally and per-account what the default booking method should be. This paves the way for implicit lot selection . A degenerate method should be available that kicks off an error and requires the user to be explicit. Average cost booking needs to be supported, as it is quite common in retirement or tax-sheltered accounts. This is the default method used in Canadian investment accounts. Cost basis adjustments need to be supported as well. The problem should be able to be specified by providing a specific lot (or all of a commodity\u2019s lots at average cost) and a dollar amount to adjust the position by. Stock splits need to be able to maintain some of the original attributes of the position , specifically, the original trade date and the user-specified label, if one has been provided. This should allow a common syntax to specify an original trading lot when reducing a position after a split. Reducing a position needs to always book against an existing lot . I\u2019m preserving the Beancount behavior here, which I\u2019ve argued for previously, as it works quite well and is a great method to avoid data entry mistakes. This naturally defines an invariant for inventories: all positions of a particular commodity held at cost should be of the same sign in one inventory (this can be used as a sanity check). Bookings that change the sign of a number of units should raise an error , unless an explicit exception is requested (and I\u2019m not even convinced that we need it). Note again that bookings only occur for units held at cost, so currency conversions are unaffected by this requirement. Beancount has had this feature for a while, and it has proved useful to detect errors. For example, if an inventory has a position of 8 HOOL {500 USD} you attempt to post a change of -10 units to this lot, the resulting number of units is now negative: -2. This should indicate user error. The only use case I can think for allowing this is the trading of futures spreads and currencies, which would naturally be reported in the same account (a short position in currencies is not regarded as a different instrument in the same way that a short position would); this is the only reason to provide an exception, and I suspect that 99% of users will not need it.] Debugging Tools \uf0c1 Since this is a non-trivial but greatly important part of the process of entering trading data, we should provide tools that list in detail the running balance for an inventory, including all of the detail of its lots. Booking errors being reported should include sufficient context, that is: The transaction with offending posting The offending posting The current state of the inventory before the posting is applied, with all its lots The implicit booking method that is in effect to disambiguate between lots A detailed reason why the booking failed Explicit vs. Implicit Booking Reduction \uf0c1 Another question that may come up in the design of a new booking method is whether we require the user to be explicit about whether he thinks this is an addition to a position, or a reduction. This could be done, for instance, by requiring a slightly different syntax for the cost, for example \u201c {} \u201d would indicate an addition and \u201c [] \u201d a reduction. I\u2019m not convinced that it is necessary to make that distinction, maybe the extra burden on the user is not worth it, but it might be a way to cross-check an expectation against the actual calculations that occur. I\u2019ll drop the idea for now. Design Proposal \uf0c1 This section presents a concrete description of a design that fulfills the previously introduced requirements. We hope to keep this as simple as possible. Inventory \uf0c1 An inventory is simply a list of lot descriptors. Each inventory lot will be defined by: UNITS, (CURRENCY, COST-UNITS, COST-CURRENCY, DATE, LABEL) Fields: UNITS: the number of units of that lot that are held CURRENCY: the type of commodity held, a string COST-UNITS: the number in the price of that lot COST-CURRENCY: the pricing commodity of the cost DATE: the acquisition date of the lot LABEL: an arbitrary user-specified string used to identify this lot If this represents a lot held at cost, after this processing, only the LABEL field should be optionally with a NULL value. Anyone writing code against this inventory could expect that all other values are filled in with non-NULL values (or are otherwise all NULL). Input Syntax & Filtering \uf0c1 The input syntax should allow the specification of cost as any combination of the following informations: The cost per unit , as an amount, such as \u201c 500 USD \u201d A total cost , to be automatically divided by the number of units, like this: {... +9.95 USD} . You should be able to use either cost per unit, total cost, or even combine the two, like this: {500 + 9.95 USD} . This is useful to enter commissions. This syntax also replaces the previous {{ ... }} syntax. The lot-date , as a YYYY-MM-DD date, such as \u201c 2014-06-20 \u201d A label , which is any non-numerical identifier, such as first-apple or some random uuid like \u201c aa2ba9695cc7 \u201d A special marker \u201c * \u201d that indicates to book at the average cost of the inventory balance These following postings should all be valid syntax: ... Assets:Investments:Stock 10 HOOL {500 USD} ; cost Assets:Investments:Stock 10 HOOL {339999615d7a} ; label Assets:Investments:Stock 10 HOOL {2014-05-01} ; lot-date Assets:Investments:Stock 10 HOOL {} ; no information ... Combinations Assets:Investments:Stock 10 HOOL {500 USD, 2014-05-01} Assets:Investments:Stock 10 HOOL {2014-05-01, 339999615d7a} Algorithm \uf0c1 All postings should be processed in a separate step after parsing, in the order of their date, against a running inventory balance for each account. The cost amount should become fully determined at this stage, and if we fail to resolve a cost, an error should be raised and the transaction deleted from the flow of directives (after the program loudly complaining about it). When processing an entry, we should match and filter all inventory lots against all the filters that are provided by the user. In general, after filtering the lots with the user restrictions: If the set of lots is empty, an error should be raised; If there is a single lot, this lot should be selected (success case); If there are multiple lots, the default implicitly booking method for the corresponding account should be invoked. Implicit Booking Methods \uf0c1 If there are multiple matching lots to choose from during booking, the following implicit booking methods could be invoked: STRICT: Select the only lot of the inventory. If there are more than one lots, raise an error. FIFO: Select the lot with the earliest date. LIFO: Select the lot with the latest date. AVERAGE: Book this transaction at average cost. AVERAGE_ONLY: Like AVERAGE but also trigger an aggregation on an addition to a position. This can be used to enforce that the only booking method for all lots is at the average cost. NONE: Don\u2019t perform any inventory booking on this account. Allow a mix of lots for the same commodity or positive and negative numbers in the inventory. (This essentially devolves to the Ledger method of booking.) This method would only get invoked if disambiguation between multiple lots is required, after filtering the lots against the expression provided by the user. The STRICT method is basically the degenerate disambiguation which issues an error if there is any ambiguity and should be the default. There should be a default method that applies to all accounts. The default value should be overridable. In Beancount, we would add a new \u201coption\u201d to allow the user to change this: option \"booking_method\" \"FIFO\" The default value used in a particular account should be specifiable as well, because it is a common case that the method varies by account, and is fixed within the account. In Beancount, this would probably become part of the open directive, something like this: 2003-02-17 open Assets:Ameritrade:HOOL HOOL booking:FIFO (I\u2019m not sure about the syntax.) Resolving Same Date Ambiguity \uf0c1 If the automatic booking method gets invoked to resolve an ambiguous lot reduction, e.g. FIFO, if there are multiple lots at the same date, something needs to be done to resolve which of the lots is to be chosen. The line number at which the transaction appears should be selected. For example, in the following case, a WIDGET Of 8 GBP would be selected: 2014-10-15 * \"buy widgets\" Assets:Inventory 10 WIDGET {} ;; Price inferred to 8 GBP/widget Assets:Cash -80 GBP 2014-10-15 * \"buy another widget\" Assets:Inventory 1 WIDGET {} ;; Price inferred to 9 GBP/widget Assets:Cash -9 GBP 2014-10-16 * \"sell a widget\" Assets:Cash 11 GBP Assets:Inventory -1 WIDGET {} ;; Ambiguous lot Dates Inserted by Default \uf0c1 By default, if an explicit date is not specified in a cost, the date of the transaction that holds the posting should be attached to the trading lot automatically. This date is overridable so that stock splits may be implemented by a transformation to a transaction like this: 2014-01-04 * \"Buy some HOOL\" Assets:Investments:Stock 10 HOOL {1000.00 USD} Assets:Investments:Cash 2014-04-17 * \"2:1 split into Class A and Class B shares\" Assets:Investments:Stock -10 HOOL {1000.00 USD, 2014-01-04} ; reducing Assets:Investments:Stock 10 HOOL {500.00 USD, 2014-01-04} ; augment Assets:Investments:Stock 10 HOOLL {500.00 USD, 2014-01-04} ; augment Matching with No Information \uf0c1 Supplying no information for the cost should be supported and is sometimes useful: in the case of augmenting a position, if all the other legs have values specified, we should be able to automatically infer the cost of the units being traded: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {} Assets:Investments:Cash -5009.95 USD Expenses:Commissions 9.95 USD In the case of reducing a position\u2014and we can figure that out whether that is the case by looking at the inventory at the point of applying the transaction to its account balance\u2014an empty specification should trigger the default booking method. If the method is STRICT, for instance, this would select a lot only if there is a single lot available (the choice is unambiguous), which is probably a common case if one trades infrequently. Other methods will apply as they are defined. Note that the user still has to specify a cost of \u201c {} \u201d in order to inform us that this posting has to be considered \u201cheld at cost.\u201d This is important to disambiguate from a price conversion with no associated cost. Reducing Multiple Lots \uf0c1 If a single trade needs to close multiple existing lots of an inventory, this can dealt with trivially by inserting one posting for each lot. I think this is a totally reasonable requirement. This could represent a single trade, for instance, if your brokerage allows it: 2012-05-01 * \"Closing my position\" Assets:Investments:Stock -10 HOOL {500 USD} Assets:Investments:Stock -12 HOOL {510 USD} Assets:Investments:Cash 12000.00 USD Income:Investments:Gains Note: If the cost basis specification for the lot matches multiple lots of the inventory and the result is unambiguous, the lots will be correctly selected. For example, if the ante-inventory contains just those two lots (22 HOOL in total), you should be able to have a single reducing posting like this: 2012-05-01 * \"Closing my position\" Assets:Investments:Stock -22 HOOL {} Assets:Investments:Cash 12000.00 USD Income:Investments:Gains and they will both be included. On the other hand, if the result is ambiguous (for example, if you have more than these two lots) the booking strategy for that account would be invoked. By default, this strategy will be \"STRICT\" which will generate an error, but if this account's strategy is set to \"FIFO\" (or is not set and the default global strategy is \"FIFO\"), the FIFO lots would be automatically selected for you, as per your wish. Lot Basis Modification \uf0c1 Another idea is to support the modification of a specific lot\u2019s cost basis in a single leg. The way this could work, is by specifying the number of units to modify, and the \u201c+ number currency\u201d syntax would be used to adjust the cost basis by a specific number, like this: 2012-05-01 * \"Adjust cost basis by 250 USD for 5 of the 500 USD units\" Assets:Investments:Stock ~5 HOOL {500 + 250 USD} If the number of units is smaller than the total number of units in the lot, split out the lot before applying the cost basis adjustment. I\u2019m not certain I like this. Not specifying the size could also adjust the entire position (I\u2019m not sure if this makes sense as it departs from the current syntax significantly): 2012-05-01 * \"Adjust cost basis by 250 USD for the 500 USD units\" Assets:Investments:Stock HOOL {500 + 250 USD} In any case, all the fields other than the total cost adjustment would be used to select which lot to adjust. Tag Reuse \uf0c1 We will have to be careful in the inventory booking to warn on reuse of lot labels. Labels should be unique. Examples \uf0c1 Nothing speaks more clearly than concrete examples. If otherwise unspecified, we are assuming that the booking method on the Assets:Investments:Stock account is STRICT . No Conflict \uf0c1 Given the following inventory lots: 21, (HOOL, 500, USD, 2012-05-01, null) 22, (AAPL, 380, USD, 2012-06-01, null) This booking should succeed: 2013-05-01 * Assets:Investments:Stock -10 HOOL {} ... This booking should fail (because the amount is not one of the valid lots): 2013-05-01 * Assets:Investments:Stock -10 HOOL {520 USD} ... This one too should fail (no currency matching lot): 2013-05-01 * Assets:Investments:Stock -10 MSFT {80 USD} ... So should this one (invalid date when a date is specified): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD, 2010-01-01} ... Explicit Selection By Cost \uf0c1 Given the following inventory: 21, (HOOL, 500, USD, 2012-05-01, null) 32, (HOOL, 500, USD, 2012-06-01, \u201cabc\u201d) 25, (HOOL, 510, USD, 2012-06-01, null) This booking should succeed: 2013-05-01 * Assets:Investments:Stock -10 HOOL {510 USD} ... This booking should fail if the stock account\u2019s method is STRICT (ambiguous): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD} ... This booking should succeed if the stock account\u2019s method is FIFO, booking against the lot at 2012-05-01 (earliest): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD} ... Explicit Selection By Date \uf0c1 Given the same inventory as previously, this booking should succeed: 2013-05-01 * Assets:Investments:Stock -10 HOOL {2012-05-01} ... This booking should fail if the method is STRICT (ambiguous) 2013-05-01 * Assets:Investments:Stock -10 HOOL {2012-06-01} ... Explicit Selection By Label \uf0c1 This booking should succeed, because there is a single lot with the \u201cabc\u201c label: 2013-05-01 * Assets:Investments:Stock -10 HOOL {abc} ... If multiple lots have the same label, ambiguous cases may occur; with this inventory, for example: 32, (HOOL, 500, USD, 2012-06-01, \u201cabc\u201d) 31, (HOOL, 510, USD, 2012-07-01, \u201cabc\u201d) The same booking should fail. Explicit Selection By Combination \uf0c1 With the initial inventory, this booking should succeed (unambiguous): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD, 2012-06-01} ... There is only one lot at a cost of 500 USD and at an acquisition date of 2012-06-01. Not Enough Units \uf0c1 The following booking would be unambiguous, but would fail, because there aren\u2019t enough units of the lot in the inventory to subtract from: 2013-05-01 * Assets:Investments:Stock -33 HOOL {500 USD, 2012-06-01} \u2026 Redundant Selection of Same Lot \uf0c1 If two separate postings select the same inventory lot in one transaction, it should be able to work: 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD, 2012-06-01} Assets:Investments:Stock -10 HOOL {abc} ... Of course, if there are not enough shares, it should fail: 2013-05-01 * Assets:Investments:Stock -20 HOOL {500 USD, 2012-06-01} Assets:Investments:Stock -20 HOOL {abc} ... Automatic Price Extrapolation \uf0c1 If all the postings of a transaction can have their balance computed, we allow a single price to be automatically calculated - this should work: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500.00 USD} Assets:US:Invest:HOOL 10.00 HOOL {} Income:US:Invest:Gains -340.51 USD And result in the equivalent of: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500.00 USD} Assets:US:Invest:HOOL 10.00 HOOL {534.51 USD} Income:US:Invest:Gains -340.51 USD Of course, preserving the original date of the lot should work too, so this should work as well: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500.00 USD} Assets:US:Invest:HOOL 10.00 HOOL {2014-02-04} Income:US:Invest:Gains -340.51 USD Average Cost Booking \uf0c1 Augmenting lots with the average cost syntax should fail: 2014-03-15 * \"Buying at average cost, what does this mean?\" Assets:US:Invest:Stock 10.00 HOOL {*} Income:US:Invest:Gains -5000.00 USD Reducing should work, this is the main use case: 2014-03-15 * \"Buying a first lot\" Assets:US:Invest:Stock 10.00 HOOL {500.00 USD} Assert:US:Invest:Cash -5000.00 USD 2014-04-15 * \"Buying a second lot\" Assets:US:Invest:Stock 10.00 HOOL {510.00 USD} Assets:US:Invest:Cash -5100.00 USD 2014-04-28 * \"Obtaining a dividend in stock\" Assets:US:Invest:Stock 1.00 HOOL {520.00 USD} Income:US:Invest:Dividends -520.00 USD 2014-05-20 * \"Sell some stock at average cost\" Assets:US:Invest:Stock -8.00 HOOL {*} Assets:US:Invest:Cash 4240.00 USD Income:US:Invest:Gains ; Automatically calculated: -194.29 USD The effect would be to aggregate the 10 HOOL {500.00 USD} lot, the 10 HOOL {510.00 USD} lot, and the 1.00 HOOL {520.00 USD} lot, together into a single lot of 21 HOOL {505.714285 USD}, and to remove 8 HOOL from this lot, which is 8 HOOL x 505.714285 USD/HOOL = 4045.71 USD. We receive 4240.00 USD, which allows us to automatically compute the capital gain: 4240.00 - 4045.71 = 194.29 USD. After the reduction, a single lot of 13 HOOL {505.714285 USD} remains. It should also work if we have multiple currencies in the account, that is adding the following transaction to the above should not make it faile: 2014-04-15 * \"Buying another stock\" Assets:US:Invest:Stock 15.00 AAPL {300.00 USD} Assets:US:Invest:Cash -4500.00 USD However it should fail if we have the same currency priced in distinct cost currencies in the same account: 2014-03-15 * \"Buying a first lot\" Assets:US:Invest:Stock 10.00 HOOL {500.00 USD} Assert:US:Invest:Cash -5000.00 USD 2014-04-15 * \"Buying a second lot\" Assets:US:Invest:Stock 10.00 HOOL {623.00 CAD} Assets:US:Invest:Cash -62300.00 CAD 2014-05-20 * \"Sell some stock at average cost\" Assets:US:Invest:Stock -8.00 HOOL {*} ; Which HOOL, USD or CAD? Assets:US:Invest:Cash 4240.00 USD Income:US:Invest:Gains But of course, this never happens in practice, so I\u2019m not too concerned. We could potentially support specifying the cost-currency to resolve this case, that is, replacing the sell leg with this: 2014-05-20 * \"Sell some stock at average cost\" Assets:US:Invest:Stock -8.00 HOOL {* USD} Assets:US:Invest:Cash 4240.00 USD Income:US:Invest:Gains I\u2019m quite sure it wouldn\u2019t be useful, but we could go the extra mile and be as general as we can possibly be. Future Work \uf0c1 This proposal does not yet deal with cost basis re-adjustments! We need to way to be able to add or remove a fixed dollar amount to a position\u2019s cost basis, that is, from (1) a lot identifier and (2) a cost-currency amount, we should be able to update the cost of that lot. The transaction still has to balance. Here are some ideas what this might look like: 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock 10.00 HOOL {500.00 USD + 230.00 USD} Income:US:Invest:Gains -230.00 USD The resulting inventory would be 10 HOOL at 523.00 USD per share, and the rest of the original lots, less 10 HOOL at 500.00 USD per share (there might be some of that remaining, that\u2019s fine). This is the equivalent of 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock -10.00 HOOL {500.00 USD} Assets:US:Invest:Stock 10.00 HOOL {523.00 USD} Income:US:Invest:Gains -230.00 USD except you did not have to carry out the calculation. This should therefore be implemented as a transformation. However, this is not super useful, because this is already supported... adjustments are usually performed on lots at average cost, and this is where the problem lie: you\u2019d have to make the calculation yourself! Here\u2019s a relevant use case: 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock 10.00 HOOL {* + 230.00 USD} Income:US:Invest:Gains -230.00 USD This would first average all the lots of HOOL together and recompute the cost basis with the new amount. This should work: 2014-03-15 * \"Buying a first lot\" Assets:US:Invest:Stock 5.00 HOOL {500.00 USD} Assert:US:Invest:Cash -2500.00 USD 2014-04-15 * \"Buying a second lot\" Assets:US:Invest:Stock 5.00 HOOL {520.00 USD} Assets:US:Invest:Cash -2600.00 USD 2014-05-20 * \"Adjust cost basis of the Hooli units\" Assets:US:Invest:Stock 10.00 HOOL {* + 230.00 USD} Income:US:Invest:Gains -230.00 USD Notice how we still have to specify an amount of HOOL shares to be repriced. I like this, but I\u2019m wondering if we should allow specifying \u201call the units\u201d like this (this would be a more radical change to the syntax): 2014-05-20 * \"Adjust cost basis of the Hooli units\" Assets:US:Invest:Stock * HOOL {* + 230.00 USD} Income:US:Invest:Gains -230.00 USD Perhaps the best way to auto-compute cost basis adjustments would be via powerful interpolation, like this: 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock -10.00 HOOL {500 USD} ; reduce Assets:US:Invest:Stock 10.00 HOOL {} ; augment, interpolated Income:US:Invest:Gains -230.00 USD See the Smarter Elision document for more details on a proposal. Inter-Account Booking \uf0c1 Some tax laws require a user to book according to a specific method, and this might apply between all accounts. This means that some sort of transfer needs to be applied in order to handle this correctly. See the separate document for detail. TODO - complete this with more tests for average cost booking! Implementation Notes \uf0c1 Separate Parsing & Interpolation \uf0c1 In order to implement a syntax for reduction that does not specify the cost, we need to make changes to the parser. Because there may be no costs on the posting, it becomes impossible to perform balance checks at parsing time. Therefore, we will need to postpone balance checks to a stage after parsing. This is reasonable and in a way nice: it is best if the Beancount parser does not output many complex errors at that stage. A new \u201cpost-parse\u201d stage will be added, right before running the plugins. Conclusion \uf0c1 This document is work-in-progress. I\u2019d really love to get some feedback in order to improve the suggested method, which is why I put this down in words instead of just writing the code. I think a better semantic can be reached by iterating on this design to produce something that works better in all systems, and this can be used as documentation later on for developers to understand why it is designed this way. Your feedback is much appreciated.","title":"A Proposal for an Improvement on Inventory Booking"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#inventory-booking","text":"A Proposal for an Improvement on Command-Line Bookkeeping Martin Blais, June 2014 http://furius.ca/beancount/doc/proposal-booking Motivation Problem Description Lot Date Average Cost Basis Capital Gains Sans Commissions Cost Basis Adjustments Dealing with Stock Splits Previous Solutions Shortcomings in Ledger Shortcomings in Beancount Requirements Debugging Tools Explicit vs. Implicit Booking Reduction Design Proposal Inventory Input Syntax & Filtering Algorithm Implicit Booking Methods Dates Inserted by Default Matching with No Information Reducing Multiple Lots Examples No Conflict Explicit Selection By Cost Explicit Selection By Date Explicit Selection By Label Explicit Selection By Combination Not Enough Units Redundant Selection of Same Lot Automatic Price Extrapolation Average Cost Booking Future Work Implementation Note Conclusion","title":"Inventory Booking"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#motivation","text":"The problem of \u201cinventory booking,\u201d that is, selecting which of an inventory\u2019s trade lots to reduce when closing a position, is a tricky one. So far, in the command-line accounting community, relatively little progress has been made in supporting the flexibility to deal with many common real-life cases. This document offers a discussion of the current state of affairs, describes the common use cases we would like to be able to solve, identifies a set of requirements for a better booking method, and proposes a syntax and implementation design to support these requirements, along with clearly defined semantics for the booking method.","title":"Motivation"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#problem-description","text":"The problem under consideration is the problem of deciding, for a double-entry transaction that intends to reduce the size of a position at a particular point in time in a particular account, which of the account inventory lots contained at that point should be reduced. This should be specified using a simple data entry syntax that minimizes the burden on the user. For example, one could enter a position in HOOL stock by buying two lots of it at different points in time: 2014-02-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {500 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD 2014-02-15 * \"Second trade\" Assets:Investments:Stock 8 HOOL {510 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD We will call the two sets of shares \u201ctrade lots\u201d and assume that the underlying system that counts them is keeping track of each of their cost and date of acquisition separately. The question here is, if we were to sell some of this stock, which of the shares should we select? Those from the first trade, or those from the second? This is an important decision, because it has an impact on capital gains, and thus on taxes. (The account of capital gains is described in more detail in the \u201cStock Trading\u201d section of the Beancount cookbook if you need an explanation of this.) Depending on our financial situation, this year\u2019s trading history, and the unrealized gains that a trade would realize, sometimes we may want to select one lot over another. By now, most discount brokers even let you select your specific lot when you place a trade that reduces or closes a position. It is important to emphasize here the two directions of inventory booking: Augmenting a position. This is the process of creating a new trading lot, or adding to an existing trading lot (if the cost and other attributes are identical). This is easy, and amounts to adding a record to some sort of mapping that describes an account\u2019s balance (I call this an \"inventory\"). Reducing a position. This is generally where booking complications take place, and the problem is essentially to figure out which lot of an existing position to remove units from. Most of this document is dedicated to the second direction.","title":"Problem Description"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#lot-date","text":"Even if the cost of each lot is identical, the acquisition date of the position you\u2019re intending to close matters, because different taxation rules may apply, for example, in the US, positions held for more than 12 months are subject to a significantly lower tax rate (\u201cthe long-term capital gains rate\u201d). For example, if you entered your position like this: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {300 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD 2014-02-15 * \"Second trade at same price\" Assets:Investments:Stock 8 HOOL {300 USD} Assets:Investments:Cash Expenses:Commissions 9.95 USD and we are booking a trade for 2014-03-01, selecting the first lot would result in a long-term (low) gain, because two years have passed since acquisition (position held from 2012-05-01 to 2014-03-01) while booking against the second would result in a short-term (high) capital gains tax. So it\u2019s not just a matter of the cost of the lots. Our systems don't aim to file taxes automatically at this point but we would like to enter our data in a way that eventually allows this kind of reporting to take place. Note that this discussion assumes that you were able to decide which of the lots you could trade against. There are several taxation rules that may apply, and in some cases, depending on which country you live in, you may not have a choice, the government may dictate how you are meant to report your gains. In some cases you may even need to be able to apply multiple booking methods to the same account (e.g., if an account is subject to a taxation claim from multiple countries). We will ignore this exotic case in this document, as it is quite rare.","title":"Lot Date"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#average-cost-basis","text":"Things get more complicated for tax-sheltered accounts. Because there are no tax consequences to closing positions in these accounts, brokers will normally choose to account for the book value of your positions as if they were a single lot. That is, the cost of each share is computed using the weighted average of the cost of the position you are holding. This can equivalently be calculated by summing the total cost of each position and then dividing by the total number of shares. To continue with our first example: 10 HOOL x 500 USD/HOOL = 5000 USD 8 HOOL x 510 USD/HOOL = 4080 USD Cost basis = 5000 USD + 4080 USD = 9080 USD Total number of shares = 10 HOOL + 8 HOOL = 18 HOOL Cost basis per share = 9080 USD / 18 HOOL = 504.44 USD/HOOL So if you were to close some of your position and sell some shares, you would do so at a cost of 504.44 USD/HOOL. The gain you would realize would be relative to this cost; for example if you sold 5 shares at 520 USD/HOOL, your gain would be calculated like this: (520 USD/HOOL - 504.44 USD/HOOL) x 5 HOOL = 77.78 USD This type of booking method is made evident by two kinds of financial events that you might witness occur in these types of accounts: Fees may be withdrawn by selling some shares reported at their current market price. You will see these if you hold a retirement account at Vanguard in the USA. The broker takes a fixed amount of dollars per quarter (5$, with my employer's plan) that is backed out in terms of a small, fractional number of shares of each fund to sell. That transaction is oblivious to capital gains: they simply figure out how many shares to sell to cover their fee and don\u2019t report a gain to you. You have to assume it doesn\u2019t matter. Most people don't track their gains in non-taxable accounts but freaks like us may want to compute the return nonetheless. Cost basis readjustments may occur spontaneously. This is rare, but I have seen this once or twice in the case of mutual funds in a Canadian tax-deferred account: based on some undisclosed details of their trading activity, the fund management has had to issue an adjustment to the cost basis, which you are meant to apply to your positions. You get an email telling you how much the adjustment was for. 99% of people probably don\u2019t blink, don't understand and ignore this message\u2026 perhaps rightly so, as it has no impact on their taxes, but if you want to account for your trading returns in that account, you do need to count it. So we need to be able to add or remove to the cost basis of existing positions. Note that the average cost of your positions changes on every trade and needs to get recalculated every time that you add to an existing position. A problem with this is that if you track the cost per share, these multiple recalculations may result in some errors if you store the amounts using a fixed numerical representation (typically decimal numbers). An alternative method to track the cost in our data structures, one that is more stable numerically, is to simply keep track of the total cost instead of the cost-per-share. Also note that using fractional numbers does not provide a sufficient answer to this problem: in practice, the broker may report a specific cost, one that is rounded to specific number of decimals, and we may want to be able to book our reducing trade using that reported number rather than maintain the idealized amount that a fractional representation would. Both using a fixed decimal or a fractional representation pose problems. We will largely ignore those problems here. In summary: we need to be able to support book against lots at their average cost, and we need to be able to adjust the cost basis of an existing position. I\u2019ve been thinking about a solution to implement this that would not force an account to be marked with a special state. I think we can do this using only inventory manipulations. All we need is a small change to the syntax that allows the user to indicate to the system that it should book against the average cost of all positions. Using the first example above, acquiring the shares would take the same form as previously, that is after buying two lots of 10 and 8 HOOL units, we end up with an inventory that holds two lots, one of 10 HOOL {500 USD} and one of 8 HOOL {510 USD}. However, when it\u2019s time to sell, the following syntax would be used (this is an old idea I\u2019ve been meaning to implement for a while): 2014-02-01 * \"Selling 5 shares at market price 550 USD\" Assets:Investments:Stock -5 HOOL {*} Assets:Investments:Cash 2759.95 USD Expenses:Commissions 9.95 USD Income:Investments:CapitalGains When the \u201c * \u201d is encountered in lieu of the cost, like this, it would: Merge all the lots together and recalculate the average price per share (504.44 USD) Book against this merged inventory, reducing the resulting lot. After the transaction, we would end up with a single position of 13 HOOL {504.44 USD). Adding to this position at a different price would create a new lot, and those would get merged again the next time a reduction occurs at average cost. We do not need to merge lots until there is a reduction. Apart from concerns of accumulating rounding error, this solution is correct mathematically, and maintains the property that accounts aren\u2019t coerced into any specific booking method\u2014a mix of methods could be used on every reduction. This is a nice property, even if not strictly necessary, and even if we want to be able to just specify a \u201cdefault\u201d booking method to use per account and just stick with it throughout. It\u2019s always nice to have the flexibility to support exceptions, because in real life, they sometimes occur.","title":"Average Cost Basis"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#capital-gains-sans-commissions","text":"We would like to be able to support the automatic calculation of capital gains without the commissions. This problem is described in much detail in the Commissions section of the trading documentation and in a thread on the mailing-list . The essence of the complication that occurs is that one cannot simply subtract the commissions incurred during the reporting period from the gains that include commissions, because the commissions that were incurred to acquire the position must be pro-rated to the shares of the position that are sold. The simplest and most common way to implement this is to include the costs of acquiring the position into the cost basis of the position itself, and deduct the selling costs from the market value when a position is reduced. Whatever new design we come up with must allow us to count these adjusted gains as this is essential to various individuals' situations. In Beancount, I have figured out a solution for this problem, which luckily enough involves only an automated transformation of a transaction flagged by the presence of a special flag on its postings... if and only if I can find a way to specify which lot to book against without having to specify its cost, because once the cost of commissions gets folded into the cost of the position, the adjusted cost does not show up anywhere in the input file, the user would have to calculate it manually, which is unreasonable. In the solution I\u2019m proposing, the following transactions would be transformed automatically, triggered by the presence of the \u201cC\u201d flag: 2014-02-10 * \"Buy\" Assets:US:Invest:Cash -5009.95 USD C Expenses:Commissions 9.95 USD Assets:US:Invest:HOOL 10.00 HOOL {500 USD / aa2ba9695cc7} 2014-04-10 * \"Sell #1\" Assets:US:Invest:HOOL -4.00 HOOL {aa2ba9695cc7} C Expenses:Commissions 9.95 USD Assets:US:Invest:Cash 2110.05 USD Income:US:Invest:Gains 2014-05-10 * \"Sell #2\" Assets:US:Invest:HOOL -6.00 HOOL {aa2ba9695cc7} C Expenses:Commissions 9.95 USD Assets:US:Invest:Cash 3230.05 USD Income:US:Invest:Gains They would be automatically transformed by a plugin into the following and replace the original transactions above: 2014-02-10 * \"Buy\" Assets:US:Invest:Cash -5009.95 USD X Expenses:Commissions 9.95 USD X Income:US:Invest:Rebates -9.95 USD X Assets:US:Invest:HOOL 10.00 HOOL {500.995 USD / aa2ba9695cc7} 2014-04-10 * \"Sell #1\" X Assets:US:Invest:HOOL -4.00 HOOL {aa2ba9695cc7} X Expenses:Commissions 9.95 USD X Income:US:Invest:Rebates -9.95 USD Assets:US:Invest:Cash 2110.05 USD Income:US:Invest:Gains ; Should be (530-500)*4 - 9.95*(4/10) - 9.95 = ; 106.07 USD 2014-05-10 * \"Sell #2\" X Assets:US:Invest:HOOL -6.00 HOOL {aa2ba9695cc7} X Expenses:Commissions 9.95 USD X Income:US:Invest:Rebates -9.95 USD Assets:US:Invest:Cash 3230.05 USD Income:US:Invest:Gains ; Should be (540-500)*6 - 9.95*(6/10) - 9.95 = ; 224.08 USD The \u201cX\u201d flag would just mark the postings that have been transformed, so maybe they can be rendered with a special color or attribute later on. The rebates account is included as a separate counter just to make the transaction balance. The reason I need to relax the disambiguation of trading lots is that the user needs to be able to specify the matching leg without having to specify the cost of the position, because at the point where the position is reduced (2014-04-10), there is no way to figure out what the cost of the original lot was. Just to be clear, in the example above, this means that if all the information have is 4 HOOL and 500 USD, there is no way to back out a cost of 500.995 USD that could specify a match with the opening trade lot, because that happened with 10 HOOLs. So we need to be more explicit about booking. In the example above, I\u2019m selecting the matching lot \u201cby label,\u201d that is, the user has the option to provide a unique lot identifier in the cost specification, and that can later on be used to disambiguate which lot we want to book a reduction/sale against. The example above uses the string \u201c aa2ba9695cc7 \u201d in this way. (An alternative solution to this problem would involve keep track of both the original cost (without commissions), and the actual cost (with commissions), and then finding the lot against the former, but using the latter to balance the transaction. This idea is to allow the user to keep using the cost of a position to select the lot, but I\u2019m not even sure if that is possible, in the presence of various changes to the inventory. More thought is required on this matter.)","title":"Capital Gains Sans Commissions"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#cost-basis-adjustments","text":"In order to adjust the cost basis, one can replace the contents of an account explicitly like this: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500 USD} Assets:US:Invest:HOOL 10.00 HOOL {510 USD} Income:US:Invest:Gains This method works well and lets the system automatically compute the gains. But it would be nice to support making adjustments in price units against the total cost of the position, for example, \u201cadd 340.51 USD to the cost basis of this position\u201d. The problem is that the adjusted cost per share now needs to be computed by the user\u2026 it\u2019s inconvenient. Here\u2019s one way we could support this well, however: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500 USD} Assets:US:Invest:HOOL 10.00 HOOL {} Income:US:Invest:Gains -340.51 USD If all the legs are fully specified - they have a calculatable balance - we allow a single price to be elided. This solves this problem well.","title":"Cost Basis Adjustments"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#dealing-with-stock-splits","text":"Accounting for stock splits creates some complications in terms of booking. In general, the problem is that we need to deal with changes in the meaning of a commodity over time. For example, you could do this in Beancount right now and it works: 2014-01-04 * \"Buy some HOOL\" Assets:Investments:Stock 10 HOOL {1000.00 USD} Assets:Investments:Cash 2014-04-17 * \"2:1 split into Class A and Class B shares\" Assets:Investments:Stock -10 HOOL {1000.00 USD} Assets:Investments:Stock 10 HOOL {500.00 USD} Assets:Investments:Stock 10 HOOLL {500.00 USD} One problem with splitting lots this way is that the price and meaning of a HOOL unit before and after the split differs, but let\u2019s just assume we\u2019re okay with that for now (this can be solved by relabeling the commodity by using another name, ideally in a way that is not visible to the user - we have a pending discussion elsewhere ). The issue that concerns booking is, when you sell that position, which cost do you use? The inventory will contain positions at 500 USD, so that\u2019s what you should be using, but is it obvious to the user? We can assume this can be learned with a recipe . Now, what if we automatically attach the transaction date? Does it get reset on the split and now you would have to use 2014-04-17? If so, we could not automatically inspect the list of trades to determine whether this is a long-term vs. short-term trade. We need to somehow preserve some of the attributes of the original event that augmented this position, which includes the original trade date (not the split date) and the original user-specified label on the position, if one was provided.","title":"Dealing with Stock Splits"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#forcing-a-single-method-per-account","text":"For accounts that will use the average booking method, it may be useful to allow specifying that an account should only use this booking method . The idea is to avoid data entry errors when we know that an account is only able to use this method. One way to ensure this is to automatically aggregate inventory lots when augmenting a position. This ensures that at any point in time, there is only a single lot for each commodity. If we associated an inventory booking type to each account, we could define a special one that also triggers aggregation upon the addition of a position. Another approach would be to not enforce these aggregations, but to provide a plugin that would check that for those accounts that are marked as using the average cost inventory booking method by default, that only such bookings actually take place.","title":"Forcing a Single Method per Account"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#previous-solutions","text":"This section reviews existing implementations of booking methods in command-line bookkeeping systems and highlights specific shortcomings that motivate why we need to define a new method.","title":"Previous Solutions"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#shortcomings-in-ledger","text":"(Please note: I\u2019m not deeply familiar with the finer details of the inner workings of Ledger; I inferred its behavior from building test examples and reading the docs. If I got any of this wrong, please do let me know by leaving a comment.) Ledger\u2019s approach to booking is quite liberal. Internally, Ledger does not distinguish between conversions held at cost and regular price conversions : all conversions are assumed held at cost, and the only place trading lots appear is at reporting time (using its --lots option). Its \u201c {} \u201d cost syntax is meant to be used to disambiguate lots on a position reduction , not to be used when acquiring a position. The cost of acquiring a position is specified using the \u201c @ \u201d price notation: 2014/05/01 * Buy some stock (Ledger syntax) Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash This will result in an inventory of 10 HOOL {500 USD}, that is, the cost is always stored in the inventory that holds the position: $ ledger -f l1.lgr bal --lots 10 HOOL {USD500} [2014/05/01] USD-5000 Assets:Investments USD-5000 Cash 10 HOOL {USD500} [2014/05/01] Stock -------------------- 10 HOOL {USD500} [2014/05/01] USD-5000 There is no distinction between conversions at cost and without cost\u2014all conversions are tracked as if \u201cat cost.\u201d As we will see in the next section, this behaviour is distinct from Beancount\u2019s semantics, which requires a conversion held at cost to be specified using the \u201c {} \u201d notation for both its augmenting and reducing postings, and distinguishes between positions held at cost and price conversions. The advantage of the Ledger method is a simpler input mechanism, but it leads to confusing outcomes if you accumulate many conversions of many types of currencies in the same account. An inventory can easily become fragmented in its lot composition. Consider what happens, for instance, if you convert in various directions between 5 different currencies\u2026 you may easily end up with USD held in CAD, JPY, EUR and GBP and vice-versa\u2026 any possible combinations of those are possible. For instance, the following currency conversions will maintain their original cost against their converted currencies: 2014/05/01 Transfer from Canada Assets:CA:Checking -500 CAD Assets:US:Checking 400 USD @ 1.25 CAD 2014/05/15 Transfers from Europe Assets:UK:Checking -200 GBP Assets:US:Checking 500 USD @ 0.4 GBP 2014/05/21 Transfers from Europe Assets:DE:Checking -100 EUR Assets:US:Checking 133.33 USD @ 0.75 EUR This results in the following inventory in the Assets:US:Checking account: $ ledger -f l2.lgr bal --lots US:Checking 500.00 USD {0.4 GBP} [2014/05/15] 133.33 USD {0.75 EUR} [2014/05/21] 400.00 USD {1.25 CAD} [2014/05/01] Assets:US:Checking The currencies are treated like stock. But nobody thinks of their currency balances like this , one normally thinks of currencies held in an account as units in and of themselves, not in terms of their price related to some other currency. In my view, this is confusing. After these conversions, I just think of the balance of that account as 1033.33 USD. A possible rebuttal to this observation is that most of the time a user does not care about inventory lots for accounts with just currencies, so they just happen not print them out. Out of sight, out of mind. But there is no way to distinguish when the rendering routine should render lots or not. If instructed to produce a report, a balance routine should ideally only render lots only for some accounts (e.g., investing accounts, where the cost of units matters) and not for others (e.g. accounts with currencies that were deposited sometimes as a result of conversions). Moreover, when I render a balance sheet, for units held at cost we need to report the book values rather than the number of shares. But for currencies, the currency units themselves need to get reported, always. If you don\u2019t distinguish between the two cases, how do you know which to render? I think the answer is to select which view you want to render using the options. The problem is that neither provides a single unified view that does the correct thing for both types of commodities. The user should not have to specify options to view partially incorrect views in one style or the other, this should be automatic and depend on whether we care about the cost of those units. This gets handled correctly if you distinguish between the two kinds of conversions. But perhaps most importantly, this method does not particularly address the conversion problem : it is still possible to create money out of thin air by making conversions back and forth at different rates: 2014/05/01 Transfer to Canada Assets:US:Checking -40000 USD Assets:CA:Checking 50000 CAD @ 0.80 USD 2014/05/15 Transfer from Canada Assets:CA:Checking -50000 CAD @ 0.85 USD Assets:US:Checking 42500 USD This results in a trial balance with just 2500 USD: $ ledger -f l3.lgr bal 2500 USD Assets:US:Checking This is not a desirable outcome. We should require that the trial balance always sum to zero (except for virtual transactions). After all, this is the aggregate realization of the elegance of the double-entry method: because all transactions sum to zero, the total sum of any subgroup of transactions also sums to zero\u2026 except it doesn\u2019t. In my view, any balance sheet that gets rendered should comply with the accounting equation, precisely. In my explorations with Ledger, I was hoping that by virtue of its inventory tracking method it would somehow naturally deal with these conversions automatically and thus provide a good justification for keeping track of all units at cost, but I witness the same problem showing up. (The conversions issue has been a real bitch of a problem to figure out a solution for in Beancount, but it is working, and I think that the same solution could be applied to Ledger to adjust these sums, without changing its current booking method: automatically insert a special conversion entry at strategic points - when a balance sheet is rendered.) In the end, I have not found that tracking costs for every transaction would appear to have an advantage over forgetting the costs for price conversions, the conversions problem is independent of this. I\u2019d love to hear more support in favour of this design choice. Now, because all conversions maintain their cost, Ledger needs to be quite liberal about booking, because it would be inconvenient to force the user to specify the original rate of conversion for each currency, in the case of commonly occurring currency conversions. This has an unfortunate impact on those conversions which we do want to hold at cost: arbitrary lot reductions are allowed, that is, reductions against lots that do not exist. For example, the following trade does not trigger an error in Ledger: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash 2014/05/15 Sale of an impossible lot Assets:Investments:Stock -10 HOOL {505 USD} @ 510 USD Assets:Investments:Cash This results in an inventory with a long position of 10 HOOL and a short position of 10 HOOL (at a different cost): $ ledger -f l4.lgr bal --lots stock 10 HOOL {USD500} [2014/05/01] -10 HOOL {USD505} Assets:Investments:Stock I think that the reduction of a 505 USD position of HOOL should not have been allowed; Beancount treats this as an error by choice. Ledger clearly appears to support booking against an existing inventory, so surely the intention was to be able to reduce against existing positions. The following transactions do result in an empty inventory, for instance: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash 2014/05/15 Sale of matching lot Assets:Investments:Stock -10 HOOL {500 USD} [2014/05/01] @ 510 USD Assets:Investments:Cash With output: $ ledger -f l5.lgr bal --lots USD100 Equity:Capital Gains As you see, the HOOL position has been eliminated. (Don\u2019t mind the auto-inserted equity entry in the output, I believe this will be fixed in a pending patch .) However, both the date and the cost must be specified for this to work. The following transactions results in a similar problematic long + short position inventory as mentioned above: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL @ 500 USD Assets:Investments:Cash 2014/05/15 Sale of matching lot? Assets:Investments:Stock -10 HOOL {500 USD} @ 510 USD Assets:Investments:Cash With output: $ ledger -f l6.lgr bal --lots 10 HOOL {USD500} [2014/05/01] -10 HOOL {USD500} Assets:Investments:Stock USD100 Equity:Capital Gains -------------------- 10 HOOL {USD500} [2014/05/01] -10 HOOL {USD500} USD100 This is a bit surprising, I expected the lots to book against each other. I suspect this may be an unreported bug, and not intended behaviour. Finally, the \u201c {} \u201d cost syntax is allowed be used on augmenting legs as well. The documentation points to these methods being equivalent . It results in an inventory lot that does not have a date associated with it, but the other leg is not converted to cost: 2014/05/01 Enter a position Assets:Investments:Stock 10 HOOL {500 USD} Assets:Investments:Cash With output: $ ledger -f l7.lgr bal --lots 0 Assets:Investments -10 HOOL {USD500} Cash 10 HOOL {USD500} Stock -------------------- 0 I probably just don\u2019t understand how this is meant to be used; in Beancount the automatically computed leg would have posted a -5000 USD amount to the Cash account, not shares. What I think is going on, is that Ledger (probably) accumulates all the lots without attempting to match them together to try to make reductions, and then at reporting time - and only then - it matches the lots together based on some reporting options: Group lots by both cost/price and date, using --lots Group lots by cost/price only, using --lot-prices Group lots by date only, using --lot-dates It is not entirely obvious from the documentation how that works, but the behavior is consistent with this. (If this is correct, I believe that to be a problem with its design: the mechanism by which an inventory reduction is booked to an existing set of lots should definitely not be a reporting feature. It needs to occur before processing reports, so that a single and definitive history of inventory bookings can be realized. If variants in bookings are supported - and I don\u2019t think this is a good idea - they should be supported before the reporting phase. In my view, altering inventory booking strategies from command-line options is a bad idea, the strategies in place should be fully defined by the language itself.)","title":"Shortcomings in Ledger"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#shortcomings-in-beancount","text":"Beancount also has various shortcomings, though different ones. In contrast to Ledger, Beancount disambiguates between currency conversions and conversions \u201cheld at cost\u201d: 2014-05-01 * \"Convert some Loonies to Franklins\" Assets:CA:Checking -6000 CAD Assets:Investments:Cash 5000 USD @ 1.2 CAD ; Currency conversion 2014-05-01 * \"Buy some stock\" Assets:Investments:Stock 10 HOOL {500 USD} ; Held at cost Assets:Investments:Cash In the first transaction, the Assets:Investment:Cash account results in an inventory of 5000 USD, with no cost associated to it. However, after the second transaction the Assets:Investment:Stock account has an inventory of 10 HOOL with a cost of 500 USD associated to it. This is perhaps a little bit more complex, and requires more knowledge from the user: there are two kinds of conversions and he has to understand and distinguish between these two cases, and this is not obvious for newcomers to the system. Some user education is required (I\u2019ll admit it would help if I wrote more documentation). Beancount\u2019s method is also less liberal, it requires a strict application of lot reduction. That is, if you enter a transaction that reduces a lot that does not exist, it will output an error. The motivation for this is to make it difficult for the user to make a mistake in data entry. Any reduction of a position in an inventory has to match against exactly one lot. There are a few downsides that result from this choice: It requires the users to always find the cost of the lot to match against. This means that automating the import of transactions requires manual intervention from the user, as he has to go search in the ledger to insert the matching lot. (Note that theoretically the import code could load up the ledger contents to list its holdings, and if unambiguous, could look for the cost itself and insert it in the output. But this makes the import code dependent on the user\u2019s ledger, which most import codes aren't doing). This is an important step, as finding the correct cost is required in order to correctly compute capital gains, but a more flexible method is desired, one that allows the user to be a bit more lazy and not have to put the cost of the existing position when the choice is obvious, e.g., when there is a single lot of that unit to match against. I\u2019d like to relax this method somewhat. The strict requirement to reduce against an existing lot also means that both long and short positions of the same commodities \u201cheld at cost\u201d in the same account cannot exist. This is not much of a problem in practice, however, because short positions are quite rare\u2014few individuals engage in them\u2014and the user could always create separate accounts to hold short positions if needed. In fact, it is already customary to create a dedicated subaccount for each commodity in an investment account, as it naturally organizes trading activity nicely (reports a bit of a mess otherwise, it\u2019s nice to see the total position value grouped by stock, since they offer exposure to different market characteristics). Different accounts should work well, as long as we treat the signs correctly in reductions, i.e., buying stock against an existing short position is seen as a position reduction (the signs are just reversed). Another problem with the Beancount booking method is in how it matches against lots with dates. For example, if you were to try to disambiguate between two lots at the same price, the most obvious input would not work: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {500 USD} Assets:Investments:Cash 2014-02-15 * \"Second trade at very same price\" Assets:Investments:Stock 8 HOOL {500 USD} Assets:Investments:Cash 2014-06-30 * \"Trying to sell\" Assets:Investments:Stock -5 HOOL {500 USD / 2012-05-01} Assets:Investments:Cash This would report a booking error. This is confusing. In this case Beancount, in its desire to be strict, applies strict matching against the inventory lots, and attempting to match units of a lot of (HOOL, 500 USD, 2012-05-01) with units of (HOOL, 500 USD, None ) simply fails. Note that the lot-date syntax is accepted by Beancount on both augmenting and reducing legs, so the \u201csolution\u201d is that the user is forced to specifically provide the lot-date on acquiring the position. This would work, for instance: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {500 USD / 2012-05-01} Assets:Investments:Cash 2014-02-15 * \"Second trade at very same price\" Assets:Investments:Stock 8 HOOL {500 USD} Assets:Investments:Cash 2014-06-30 * \"Trying to sell\" Assets:Investments:Stock -5 HOOL {500 USD / 2012-05-01} Assets:Investments:Cash The inconvenience here is that when the user entered the first trade, he could not predict that he would enter another trade at the same price in the future and typically would not have inserted the lot-date. More likely than not, the user had to go back and revisit that transaction in order to disambiguate this. We can do better. This is a shortcoming of its implementation, which reflects the fact that position reduction was initially regarded as an exact matching problem rather than a fuzzy matching one\u2014it wasn\u2019t clear to me how to handle this safely at the time. This needs to be fixed after this proposal, and what I\u2019m doing with this document is very much a process of clarifying for myself what I want this fuzzy matching to mean, and to ensure that I come up with an unambiguous design that is easy to enter data for. Ledger always automatically attaches the transaction date to the inventory lot, and in my view this is the correct thing to do (below we propose attaching more information as well).","title":"Shortcomings in Beancount"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#context","text":"[Updated on Nov\u20192014] It is important to distinguish between two types of booking: Booking. The strict type of booking that occurs when we are considering the entire list of transactions. We will call this the \u201cbooking\u201d process, and it should occur just once. This process should be strict: the failure of correctly book a reduction to an existing lot should trigger a fatal error. Inventory Aggregation. When we are considering a subset of entries, it is possible that entries that add some lots are removed, and that other entries that reduce or remove those lots are kept. This creates a situation in which it is necessary to support aggregations over time of changes to an inventory that may not admit a matching lot. If we are to support arbitrary subsets of transactions being aggregated, we must take this into account. This aggregation process should never lead to an error. Let\u2019s take an example to justify why we need the non-booking summarization. Let\u2019s say we have a stock purchase and sale: 2013-11-03 * \"Buy\" Assets:Investments:VEA 10 AAPL {37.45 USD} ;; (A) Assets:Investments:Cash -370.45 USD 2014-08-09 * \"Sell\" Assets:Investments:VEA -5 AAPL {37.45 USD} Assets:Investments:Cash 194.40 USD Income:Investments:PnL If we filter \u201cby year\u201d and only want to view transactions that occurred in 2014, AND we don\u2019t \u201cclose\u201d the previous year, that is, we do not create opening balances entries that would deposit the lots in the account at the beginning of the year - this could happen if we filter by tag, or by some other combination of conditions - then the (A) lot is not present in the inventory to be debited from. We must somehow accommodate a -5 AAPL at that point. When reporting, the user could decide how to summarize and \u201cmatch up as best it can\u201d those legs, but converting them to units or cost, or convert them to a single lot at average-cost. The booking process, however, should admit no such laxity. It should be strict and trigger an error if a lot cannot be matched. This would only be applied on the total set of transactions, and only once, never on any subset. The purpose of the booking stage should be threefold: Match against existing lots and issue errors when that is impossible. Do this, using the method specified by the user. Replace all partially specified lots by a full lot specification, the lot that was matched during the booking process. Insert links on transactions that form a trade: a buy and corresponding sells should be linked together. This essentially identifies trades, and can then be used for reporting later on. (Trades at average cost will require special consideration.) We can view Ledger\u2019s method as having an aggregation method only, lacking a booking stage. The particular method for aggregation is chosen using --lots or --lot-dates or --lot-prices. This would indicate that a \u201cbooking\u201d stage could be tacked on to Ledger without changing much of its general structure: it could be inserted as a pre- or post- process, but it would require Ledger to sort the transactions by date, something it does not currently do (it runs a single pass over its data). Beancount has had both methods for a while, but the separate nature of these two operations has not been explicitly stated so far\u2014instead, the inventory supported an optional flag to raise an error when booking was impossible. Also, inventory aggregation has not been thought as requiring much customization, it was meant to be unified to the booking process. It is now clear that they are two distinct algorithms, and that a few different methods for aggregating can be relevant (though they do not function the same as Ledger\u2019s do. Note: you can view the inventory aggregation method as a GROUP BY within an Inventory object\u2019s lots: which columns do you group over? This requires use cases). In any case, fully specified lots should match against each other by default: we need to support this as the degenerate case to use for simple currencies (not held at cost)... we would not want to accumulate all changes in the same currency as separate lots in an inventory, they need to cross each other as soon as they are applied. We want to change this in order to make the code clearer: there should be a separate \u201cbooking\u201d stage, provided by a plugin , which resolves the partially specific lot reduction using the algorithm outlined in this document, and a separate method for inventory aggregation should not bother with any of those details. In fact, the inventory aggregation could potentially simply become an accumulated list of lots, and the summarization of them could take place a posteriori, with some conceptual similarly to when Ledger applies its aggregation. Just using a simple aggregation becomes more relevant once we begin entering more specific data about lots, such as always having an acquisition date, a label, and possibly even a link to the entry that created the lot. In order to trigger summarization sensibly, functions to convert and summarize accumulated inventories could be provided in the shell, such as \u201cUNITS(inventory)\u201d.","title":"Context"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#precision-for-interpolation","text":"The interpolation capabilities will be extended to cover eliding a single number, any number, in any subset of postings whose \u201cweights\u201d resolve to a particular currency (e.g., \u201call postings with weights in USD\u201d). The interpolation needs to occur at a particular precision. The interpolated number should be quantized automatically, and the number of fractional digits to quantize it to should be automatically inferred from the DisplayContext which is itself derived from the input file. Either the most common or the maximum number of digits witnessed in the file should be used. For a use case, see this thread : On Mon, Nov 24, 2014 at 12:33 PM, ELI wrote: Regarding the last piece on which there seems to be a misunderstanding between you and I, let me provide an standalone example, outside the context of the plugin. Since Vanguard only makes three decimal places of precision available me, I need to have the actual share count calculated from the price and transaction amount. For example, I have a transaction on my activities page with these details: Shares Transacted: 0.000 Share Price: 48.62 Amount: 0.02 The only way to enter this transaction and have it balance would be to manually calculate the Shares Transacted with four decimal places of precision. My preferred way of entering this would be to enter the Share Price and Amount and have Beancount calculate Shares Transacted to a precision associated with my Vanguard account. Additionally, as metadata, I'd record \"Shares Transacted: 0.000\" as history of \"what the statement said\". Maybe you can give me your thoughts on how such a case would be addressed with planned Beancount features or as a plugin I could right? What does the downloadable file contain? 0.000 or 0.0004 or even 0.00041? What this is, most likely, is the quarterly fee that they take, which they price in terms of shares. If this is your Vanguard account, and you sum all the similar such transactions around it, you should observe that it sums to 5$ per quarter. I would log it like this: 2014-11-23 * \"Quarterly fee\" Assets:US:Vanguard:VIIPX -0.00041 VIIPX {* 48.62 USD} Expenses:Financial:Fees 0.02 USD The \"*\" will be required to merge all the lots into a single one (average cost booking) because the 48.62 USD they declare is _not_ one of your lots, but rather just the price of the asset that happened to be there on the day they took the fee. In other words, they take their fee in terms of units of each asset, priced at the current price, deducting against the cost basis of all your lots together (it doesn't matter which because this is a pre-tax account, so no capital gains are reported). Now you're asking, how could I avoid calculating 0.00041 myself? My first answer would be: let's first see if the OFX download from Vanguard includes the precision. That would solve it. I believe it does (I checked my own history of downloads and I have some numbers in there with 4 fractional digits). My second answer would be that - if you don't have the number of shares from the file - after I implement the much needed new inventory booking proposal ( http://furius.ca/beancount/doc/proposal-booking ), the interpolation capabilities will be vastly extended, such that eliding the number of shares would even be possible, like this: 2014-11-23 * \"Quarterly fee\" Assets:US:Vanguard:VIIPX VIIPX {* 48.62 USD} Expenses:Financial:Fees 0.02 USD Now this by itself still would not solve the problem: this would store 0.000411353353 which is limited at 12 fractional digits because of the default context. So that's incorrect. What would need to be done to deal with this is to infer the most common number of digits used on units of VIIPX and to quantize the result to that number of digits (I think this could be safe enough). The number of digits appearing in the file for each currency is already tracked in a DisplayContext object that is stored in the options_map from the parser. I'll have to take that into account in the inventory booking proposal. I'm adding this to the proposal.","title":"Precision for Interpolation"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#requirements","text":"The previous sections introduce the general problem of booking, and point out important shortcomings in both the Beancount and Ledger implementations. This section will present a set of desires for a new and improved booking mechanism for command-line bookkeeping software implementations. Here are reasonable requirements for a new method: Explicit inventory lot selection needs to be supported. The user should be able to indicate precisely which of the lots to reduce a position against, per-transaction. An account or commodity should not have to have a consistent method applied across all its trades. Explicit inventory lot selection should support partial matching , that is, in cases where the lots to match against are ambiguous, the user should be able to supply only minimal information to disambiguate the lots, e.g., the date, or a label, or the cost, or combinations of these. For instance, if only a single lot is available to match against, the user should be able to specify the cost as \u201c {} \u201d, telling the system to book at cost, but essentially saying: \u201cbook against any lot.\u201d This should trigger an error only if there are multiple lots. A variety of informations should be supported to specify an existing lot, and they should be combinable: The cost of acquisition of the position; The date the position was acquired at; A user-provided label. Cases where insufficient information is provided for explicit lot selection is available should admit for a default booking method to be invoked. The user should be able to specify globally and per-account what the default booking method should be. This paves the way for implicit lot selection . A degenerate method should be available that kicks off an error and requires the user to be explicit. Average cost booking needs to be supported, as it is quite common in retirement or tax-sheltered accounts. This is the default method used in Canadian investment accounts. Cost basis adjustments need to be supported as well. The problem should be able to be specified by providing a specific lot (or all of a commodity\u2019s lots at average cost) and a dollar amount to adjust the position by. Stock splits need to be able to maintain some of the original attributes of the position , specifically, the original trade date and the user-specified label, if one has been provided. This should allow a common syntax to specify an original trading lot when reducing a position after a split. Reducing a position needs to always book against an existing lot . I\u2019m preserving the Beancount behavior here, which I\u2019ve argued for previously, as it works quite well and is a great method to avoid data entry mistakes. This naturally defines an invariant for inventories: all positions of a particular commodity held at cost should be of the same sign in one inventory (this can be used as a sanity check). Bookings that change the sign of a number of units should raise an error , unless an explicit exception is requested (and I\u2019m not even convinced that we need it). Note again that bookings only occur for units held at cost, so currency conversions are unaffected by this requirement. Beancount has had this feature for a while, and it has proved useful to detect errors. For example, if an inventory has a position of 8 HOOL {500 USD} you attempt to post a change of -10 units to this lot, the resulting number of units is now negative: -2. This should indicate user error. The only use case I can think for allowing this is the trading of futures spreads and currencies, which would naturally be reported in the same account (a short position in currencies is not regarded as a different instrument in the same way that a short position would); this is the only reason to provide an exception, and I suspect that 99% of users will not need it.]","title":"Requirements"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#debugging-tools","text":"Since this is a non-trivial but greatly important part of the process of entering trading data, we should provide tools that list in detail the running balance for an inventory, including all of the detail of its lots. Booking errors being reported should include sufficient context, that is: The transaction with offending posting The offending posting The current state of the inventory before the posting is applied, with all its lots The implicit booking method that is in effect to disambiguate between lots A detailed reason why the booking failed","title":"Debugging Tools"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#explicit-vs-implicit-booking-reduction","text":"Another question that may come up in the design of a new booking method is whether we require the user to be explicit about whether he thinks this is an addition to a position, or a reduction. This could be done, for instance, by requiring a slightly different syntax for the cost, for example \u201c {} \u201d would indicate an addition and \u201c [] \u201d a reduction. I\u2019m not convinced that it is necessary to make that distinction, maybe the extra burden on the user is not worth it, but it might be a way to cross-check an expectation against the actual calculations that occur. I\u2019ll drop the idea for now.","title":"Explicit vs. Implicit Booking Reduction"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#design-proposal","text":"This section presents a concrete description of a design that fulfills the previously introduced requirements. We hope to keep this as simple as possible.","title":"Design Proposal"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#inventory","text":"An inventory is simply a list of lot descriptors. Each inventory lot will be defined by: UNITS, (CURRENCY, COST-UNITS, COST-CURRENCY, DATE, LABEL) Fields: UNITS: the number of units of that lot that are held CURRENCY: the type of commodity held, a string COST-UNITS: the number in the price of that lot COST-CURRENCY: the pricing commodity of the cost DATE: the acquisition date of the lot LABEL: an arbitrary user-specified string used to identify this lot If this represents a lot held at cost, after this processing, only the LABEL field should be optionally with a NULL value. Anyone writing code against this inventory could expect that all other values are filled in with non-NULL values (or are otherwise all NULL).","title":"Inventory"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#input-syntax-filtering","text":"The input syntax should allow the specification of cost as any combination of the following informations: The cost per unit , as an amount, such as \u201c 500 USD \u201d A total cost , to be automatically divided by the number of units, like this: {... +9.95 USD} . You should be able to use either cost per unit, total cost, or even combine the two, like this: {500 + 9.95 USD} . This is useful to enter commissions. This syntax also replaces the previous {{ ... }} syntax. The lot-date , as a YYYY-MM-DD date, such as \u201c 2014-06-20 \u201d A label , which is any non-numerical identifier, such as first-apple or some random uuid like \u201c aa2ba9695cc7 \u201d A special marker \u201c * \u201d that indicates to book at the average cost of the inventory balance These following postings should all be valid syntax: ... Assets:Investments:Stock 10 HOOL {500 USD} ; cost Assets:Investments:Stock 10 HOOL {339999615d7a} ; label Assets:Investments:Stock 10 HOOL {2014-05-01} ; lot-date Assets:Investments:Stock 10 HOOL {} ; no information ... Combinations Assets:Investments:Stock 10 HOOL {500 USD, 2014-05-01} Assets:Investments:Stock 10 HOOL {2014-05-01, 339999615d7a}","title":"Input Syntax & Filtering"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#algorithm","text":"All postings should be processed in a separate step after parsing, in the order of their date, against a running inventory balance for each account. The cost amount should become fully determined at this stage, and if we fail to resolve a cost, an error should be raised and the transaction deleted from the flow of directives (after the program loudly complaining about it). When processing an entry, we should match and filter all inventory lots against all the filters that are provided by the user. In general, after filtering the lots with the user restrictions: If the set of lots is empty, an error should be raised; If there is a single lot, this lot should be selected (success case); If there are multiple lots, the default implicitly booking method for the corresponding account should be invoked.","title":"Algorithm"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#implicit-booking-methods","text":"If there are multiple matching lots to choose from during booking, the following implicit booking methods could be invoked: STRICT: Select the only lot of the inventory. If there are more than one lots, raise an error. FIFO: Select the lot with the earliest date. LIFO: Select the lot with the latest date. AVERAGE: Book this transaction at average cost. AVERAGE_ONLY: Like AVERAGE but also trigger an aggregation on an addition to a position. This can be used to enforce that the only booking method for all lots is at the average cost. NONE: Don\u2019t perform any inventory booking on this account. Allow a mix of lots for the same commodity or positive and negative numbers in the inventory. (This essentially devolves to the Ledger method of booking.) This method would only get invoked if disambiguation between multiple lots is required, after filtering the lots against the expression provided by the user. The STRICT method is basically the degenerate disambiguation which issues an error if there is any ambiguity and should be the default. There should be a default method that applies to all accounts. The default value should be overridable. In Beancount, we would add a new \u201coption\u201d to allow the user to change this: option \"booking_method\" \"FIFO\" The default value used in a particular account should be specifiable as well, because it is a common case that the method varies by account, and is fixed within the account. In Beancount, this would probably become part of the open directive, something like this: 2003-02-17 open Assets:Ameritrade:HOOL HOOL booking:FIFO (I\u2019m not sure about the syntax.)","title":"Implicit Booking Methods"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#resolving-same-date-ambiguity","text":"If the automatic booking method gets invoked to resolve an ambiguous lot reduction, e.g. FIFO, if there are multiple lots at the same date, something needs to be done to resolve which of the lots is to be chosen. The line number at which the transaction appears should be selected. For example, in the following case, a WIDGET Of 8 GBP would be selected: 2014-10-15 * \"buy widgets\" Assets:Inventory 10 WIDGET {} ;; Price inferred to 8 GBP/widget Assets:Cash -80 GBP 2014-10-15 * \"buy another widget\" Assets:Inventory 1 WIDGET {} ;; Price inferred to 9 GBP/widget Assets:Cash -9 GBP 2014-10-16 * \"sell a widget\" Assets:Cash 11 GBP Assets:Inventory -1 WIDGET {} ;; Ambiguous lot","title":"Resolving Same Date Ambiguity"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#dates-inserted-by-default","text":"By default, if an explicit date is not specified in a cost, the date of the transaction that holds the posting should be attached to the trading lot automatically. This date is overridable so that stock splits may be implemented by a transformation to a transaction like this: 2014-01-04 * \"Buy some HOOL\" Assets:Investments:Stock 10 HOOL {1000.00 USD} Assets:Investments:Cash 2014-04-17 * \"2:1 split into Class A and Class B shares\" Assets:Investments:Stock -10 HOOL {1000.00 USD, 2014-01-04} ; reducing Assets:Investments:Stock 10 HOOL {500.00 USD, 2014-01-04} ; augment Assets:Investments:Stock 10 HOOLL {500.00 USD, 2014-01-04} ; augment","title":"Dates Inserted by Default"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#matching-with-no-information","text":"Supplying no information for the cost should be supported and is sometimes useful: in the case of augmenting a position, if all the other legs have values specified, we should be able to automatically infer the cost of the units being traded: 2012-05-01 * \"First trade\" Assets:Investments:Stock 10 HOOL {} Assets:Investments:Cash -5009.95 USD Expenses:Commissions 9.95 USD In the case of reducing a position\u2014and we can figure that out whether that is the case by looking at the inventory at the point of applying the transaction to its account balance\u2014an empty specification should trigger the default booking method. If the method is STRICT, for instance, this would select a lot only if there is a single lot available (the choice is unambiguous), which is probably a common case if one trades infrequently. Other methods will apply as they are defined. Note that the user still has to specify a cost of \u201c {} \u201d in order to inform us that this posting has to be considered \u201cheld at cost.\u201d This is important to disambiguate from a price conversion with no associated cost.","title":"Matching with No Information"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#reducing-multiple-lots","text":"If a single trade needs to close multiple existing lots of an inventory, this can dealt with trivially by inserting one posting for each lot. I think this is a totally reasonable requirement. This could represent a single trade, for instance, if your brokerage allows it: 2012-05-01 * \"Closing my position\" Assets:Investments:Stock -10 HOOL {500 USD} Assets:Investments:Stock -12 HOOL {510 USD} Assets:Investments:Cash 12000.00 USD Income:Investments:Gains Note: If the cost basis specification for the lot matches multiple lots of the inventory and the result is unambiguous, the lots will be correctly selected. For example, if the ante-inventory contains just those two lots (22 HOOL in total), you should be able to have a single reducing posting like this: 2012-05-01 * \"Closing my position\" Assets:Investments:Stock -22 HOOL {} Assets:Investments:Cash 12000.00 USD Income:Investments:Gains and they will both be included. On the other hand, if the result is ambiguous (for example, if you have more than these two lots) the booking strategy for that account would be invoked. By default, this strategy will be \"STRICT\" which will generate an error, but if this account's strategy is set to \"FIFO\" (or is not set and the default global strategy is \"FIFO\"), the FIFO lots would be automatically selected for you, as per your wish.","title":"Reducing Multiple Lots"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#lot-basis-modification","text":"Another idea is to support the modification of a specific lot\u2019s cost basis in a single leg. The way this could work, is by specifying the number of units to modify, and the \u201c+ number currency\u201d syntax would be used to adjust the cost basis by a specific number, like this: 2012-05-01 * \"Adjust cost basis by 250 USD for 5 of the 500 USD units\" Assets:Investments:Stock ~5 HOOL {500 + 250 USD} If the number of units is smaller than the total number of units in the lot, split out the lot before applying the cost basis adjustment. I\u2019m not certain I like this. Not specifying the size could also adjust the entire position (I\u2019m not sure if this makes sense as it departs from the current syntax significantly): 2012-05-01 * \"Adjust cost basis by 250 USD for the 500 USD units\" Assets:Investments:Stock HOOL {500 + 250 USD} In any case, all the fields other than the total cost adjustment would be used to select which lot to adjust.","title":"Lot Basis Modification"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#tag-reuse","text":"We will have to be careful in the inventory booking to warn on reuse of lot labels. Labels should be unique.","title":"Tag Reuse"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#examples","text":"Nothing speaks more clearly than concrete examples. If otherwise unspecified, we are assuming that the booking method on the Assets:Investments:Stock account is STRICT .","title":"Examples"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#no-conflict","text":"Given the following inventory lots: 21, (HOOL, 500, USD, 2012-05-01, null) 22, (AAPL, 380, USD, 2012-06-01, null) This booking should succeed: 2013-05-01 * Assets:Investments:Stock -10 HOOL {} ... This booking should fail (because the amount is not one of the valid lots): 2013-05-01 * Assets:Investments:Stock -10 HOOL {520 USD} ... This one too should fail (no currency matching lot): 2013-05-01 * Assets:Investments:Stock -10 MSFT {80 USD} ... So should this one (invalid date when a date is specified): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD, 2010-01-01} ...","title":"No Conflict"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#explicit-selection-by-cost","text":"Given the following inventory: 21, (HOOL, 500, USD, 2012-05-01, null) 32, (HOOL, 500, USD, 2012-06-01, \u201cabc\u201d) 25, (HOOL, 510, USD, 2012-06-01, null) This booking should succeed: 2013-05-01 * Assets:Investments:Stock -10 HOOL {510 USD} ... This booking should fail if the stock account\u2019s method is STRICT (ambiguous): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD} ... This booking should succeed if the stock account\u2019s method is FIFO, booking against the lot at 2012-05-01 (earliest): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD} ...","title":"Explicit Selection By Cost"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#explicit-selection-by-date","text":"Given the same inventory as previously, this booking should succeed: 2013-05-01 * Assets:Investments:Stock -10 HOOL {2012-05-01} ... This booking should fail if the method is STRICT (ambiguous) 2013-05-01 * Assets:Investments:Stock -10 HOOL {2012-06-01} ...","title":"Explicit Selection By Date"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#explicit-selection-by-label","text":"This booking should succeed, because there is a single lot with the \u201cabc\u201c label: 2013-05-01 * Assets:Investments:Stock -10 HOOL {abc} ... If multiple lots have the same label, ambiguous cases may occur; with this inventory, for example: 32, (HOOL, 500, USD, 2012-06-01, \u201cabc\u201d) 31, (HOOL, 510, USD, 2012-07-01, \u201cabc\u201d) The same booking should fail.","title":"Explicit Selection By Label"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#explicit-selection-by-combination","text":"With the initial inventory, this booking should succeed (unambiguous): 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD, 2012-06-01} ... There is only one lot at a cost of 500 USD and at an acquisition date of 2012-06-01.","title":"Explicit Selection By Combination"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#not-enough-units","text":"The following booking would be unambiguous, but would fail, because there aren\u2019t enough units of the lot in the inventory to subtract from: 2013-05-01 * Assets:Investments:Stock -33 HOOL {500 USD, 2012-06-01} \u2026","title":"Not Enough Units"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#redundant-selection-of-same-lot","text":"If two separate postings select the same inventory lot in one transaction, it should be able to work: 2013-05-01 * Assets:Investments:Stock -10 HOOL {500 USD, 2012-06-01} Assets:Investments:Stock -10 HOOL {abc} ... Of course, if there are not enough shares, it should fail: 2013-05-01 * Assets:Investments:Stock -20 HOOL {500 USD, 2012-06-01} Assets:Investments:Stock -20 HOOL {abc} ...","title":"Redundant Selection of Same Lot"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#automatic-price-extrapolation","text":"If all the postings of a transaction can have their balance computed, we allow a single price to be automatically calculated - this should work: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500.00 USD} Assets:US:Invest:HOOL 10.00 HOOL {} Income:US:Invest:Gains -340.51 USD And result in the equivalent of: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500.00 USD} Assets:US:Invest:HOOL 10.00 HOOL {534.51 USD} Income:US:Invest:Gains -340.51 USD Of course, preserving the original date of the lot should work too, so this should work as well: 2014-03-15 * \"Adjust cost basis from 500 USD to 510 USD\" Assets:US:Invest:HOOL -10.00 HOOL {500.00 USD} Assets:US:Invest:HOOL 10.00 HOOL {2014-02-04} Income:US:Invest:Gains -340.51 USD","title":"Automatic Price Extrapolation"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#average-cost-booking","text":"Augmenting lots with the average cost syntax should fail: 2014-03-15 * \"Buying at average cost, what does this mean?\" Assets:US:Invest:Stock 10.00 HOOL {*} Income:US:Invest:Gains -5000.00 USD Reducing should work, this is the main use case: 2014-03-15 * \"Buying a first lot\" Assets:US:Invest:Stock 10.00 HOOL {500.00 USD} Assert:US:Invest:Cash -5000.00 USD 2014-04-15 * \"Buying a second lot\" Assets:US:Invest:Stock 10.00 HOOL {510.00 USD} Assets:US:Invest:Cash -5100.00 USD 2014-04-28 * \"Obtaining a dividend in stock\" Assets:US:Invest:Stock 1.00 HOOL {520.00 USD} Income:US:Invest:Dividends -520.00 USD 2014-05-20 * \"Sell some stock at average cost\" Assets:US:Invest:Stock -8.00 HOOL {*} Assets:US:Invest:Cash 4240.00 USD Income:US:Invest:Gains ; Automatically calculated: -194.29 USD The effect would be to aggregate the 10 HOOL {500.00 USD} lot, the 10 HOOL {510.00 USD} lot, and the 1.00 HOOL {520.00 USD} lot, together into a single lot of 21 HOOL {505.714285 USD}, and to remove 8 HOOL from this lot, which is 8 HOOL x 505.714285 USD/HOOL = 4045.71 USD. We receive 4240.00 USD, which allows us to automatically compute the capital gain: 4240.00 - 4045.71 = 194.29 USD. After the reduction, a single lot of 13 HOOL {505.714285 USD} remains. It should also work if we have multiple currencies in the account, that is adding the following transaction to the above should not make it faile: 2014-04-15 * \"Buying another stock\" Assets:US:Invest:Stock 15.00 AAPL {300.00 USD} Assets:US:Invest:Cash -4500.00 USD However it should fail if we have the same currency priced in distinct cost currencies in the same account: 2014-03-15 * \"Buying a first lot\" Assets:US:Invest:Stock 10.00 HOOL {500.00 USD} Assert:US:Invest:Cash -5000.00 USD 2014-04-15 * \"Buying a second lot\" Assets:US:Invest:Stock 10.00 HOOL {623.00 CAD} Assets:US:Invest:Cash -62300.00 CAD 2014-05-20 * \"Sell some stock at average cost\" Assets:US:Invest:Stock -8.00 HOOL {*} ; Which HOOL, USD or CAD? Assets:US:Invest:Cash 4240.00 USD Income:US:Invest:Gains But of course, this never happens in practice, so I\u2019m not too concerned. We could potentially support specifying the cost-currency to resolve this case, that is, replacing the sell leg with this: 2014-05-20 * \"Sell some stock at average cost\" Assets:US:Invest:Stock -8.00 HOOL {* USD} Assets:US:Invest:Cash 4240.00 USD Income:US:Invest:Gains I\u2019m quite sure it wouldn\u2019t be useful, but we could go the extra mile and be as general as we can possibly be.","title":"Average Cost Booking"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#future-work","text":"This proposal does not yet deal with cost basis re-adjustments! We need to way to be able to add or remove a fixed dollar amount to a position\u2019s cost basis, that is, from (1) a lot identifier and (2) a cost-currency amount, we should be able to update the cost of that lot. The transaction still has to balance. Here are some ideas what this might look like: 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock 10.00 HOOL {500.00 USD + 230.00 USD} Income:US:Invest:Gains -230.00 USD The resulting inventory would be 10 HOOL at 523.00 USD per share, and the rest of the original lots, less 10 HOOL at 500.00 USD per share (there might be some of that remaining, that\u2019s fine). This is the equivalent of 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock -10.00 HOOL {500.00 USD} Assets:US:Invest:Stock 10.00 HOOL {523.00 USD} Income:US:Invest:Gains -230.00 USD except you did not have to carry out the calculation. This should therefore be implemented as a transformation. However, this is not super useful, because this is already supported... adjustments are usually performed on lots at average cost, and this is where the problem lie: you\u2019d have to make the calculation yourself! Here\u2019s a relevant use case: 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock 10.00 HOOL {* + 230.00 USD} Income:US:Invest:Gains -230.00 USD This would first average all the lots of HOOL together and recompute the cost basis with the new amount. This should work: 2014-03-15 * \"Buying a first lot\" Assets:US:Invest:Stock 5.00 HOOL {500.00 USD} Assert:US:Invest:Cash -2500.00 USD 2014-04-15 * \"Buying a second lot\" Assets:US:Invest:Stock 5.00 HOOL {520.00 USD} Assets:US:Invest:Cash -2600.00 USD 2014-05-20 * \"Adjust cost basis of the Hooli units\" Assets:US:Invest:Stock 10.00 HOOL {* + 230.00 USD} Income:US:Invest:Gains -230.00 USD Notice how we still have to specify an amount of HOOL shares to be repriced. I like this, but I\u2019m wondering if we should allow specifying \u201call the units\u201d like this (this would be a more radical change to the syntax): 2014-05-20 * \"Adjust cost basis of the Hooli units\" Assets:US:Invest:Stock * HOOL {* + 230.00 USD} Income:US:Invest:Gains -230.00 USD Perhaps the best way to auto-compute cost basis adjustments would be via powerful interpolation, like this: 2014-05-20 * \"Adjust cost basis of a specific lot\" Assets:US:Invest:Stock -10.00 HOOL {500 USD} ; reduce Assets:US:Invest:Stock 10.00 HOOL {} ; augment, interpolated Income:US:Invest:Gains -230.00 USD See the Smarter Elision document for more details on a proposal.","title":"Future Work"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#inter-account-booking","text":"Some tax laws require a user to book according to a specific method, and this might apply between all accounts. This means that some sort of transfer needs to be applied in order to handle this correctly. See the separate document for detail. TODO - complete this with more tests for average cost booking!","title":"Inter-Account Booking"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#implementation-notes","text":"","title":"Implementation Notes"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#separate-parsing-interpolation","text":"In order to implement a syntax for reduction that does not specify the cost, we need to make changes to the parser. Because there may be no costs on the posting, it becomes impossible to perform balance checks at parsing time. Therefore, we will need to postpone balance checks to a stage after parsing. This is reasonable and in a way nice: it is best if the Beancount parser does not output many complex errors at that stage. A new \u201cpost-parse\u201d stage will be added, right before running the plugins.","title":"Separate Parsing & Interpolation"},{"location":"a_proposal_for_an_improvement_on_inventory_booking.html#conclusion","text":"This document is work-in-progress. I\u2019d really love to get some feedback in order to improve the suggested method, which is why I put this down in words instead of just writing the code. I think a better semantic can be reached by iterating on this design to produce something that works better in all systems, and this can be used as documentation later on for developers to understand why it is designed this way. Your feedback is much appreciated.","title":"Conclusion"},{"location":"balance_assertions_in_beancount.html","text":"Balance Assertions in Beancount \uf0c1 Martin Blais, July 2014 http://furius.ca/beancount/doc/proposal-balance This document summarizes the different kinds of semantics for balance assertions in all command-line accounting systems and proposes new syntax for total and file-order running assertions in Beancount. Motivation Partial vs. Complete Assertions File vs. Date Assertions Ordering & Ambiguity Intra-Day Assertions Beginning vs. End of Day Status Proposal File Assertions Complete Assertions Motivation \uf0c1 Both Beancount and Ledger implement balance assertions. These provide the system with checkpoints it can use to verify the integrity of the data entry 1 . Traditional accounting principles state that a user may never change the past\u2014correcting the past involves inserting new entries to undo past mistakes 2 \u2014but we in the command-line accounting community take issue with that: we want to remain able to reconstruct the past and more importantly, to correct past mistakes at the site of their original data entry. Yes, we are dilettantes but we are bent on simplifying the process of bookkeeping by challenging existing concepts and think that this is perfectly reasonable, as long as it does not break known balances. These known balances are what we provide via balance assertions. Another way to look at these balance assertions, is that they are simply the bottom line amounts reported on various account statements, as exemplified in the figure below. Following this thread , we established that there were differing interpretations of balance assertions in the current versions of Ledger (3.0) and Beancount (2.0b). Partial vs. Complete Assertions \uf0c1 An assertion in Beancount currently looks like this: 2012-02-04 balance Assets:CA:Bank:Checking 417.61 CAD The meaning of this directive is: \u201cplease assert that the inventory of account \u2018Assets:CA:Bank:Checking\u2019 contains exactly 417.61 units of CAD at the end of February 3, 2012\u201d (so it\u2019s dated on the 4th because in Beancount balance assertions are specified at the beginning of the date). It says nothing about other commodities in the account\u2019s inventory. For example, if the account contains units of \u201cUSD\u201d, those are unchecked. We will call this interpretation a partial balance assertion or single-commodity assertion . An alternative assertion would be exhaustive: \u201cplease assert that the inventory of account \u2018Assets:CA:Bank:Checking\u2019 contains only 417.61 units of CAD at the end of February 3, 2012.\u201d We call this a complete balance assertion . In order to work, this second type of assertion would have to support the specification of the full contents of an inventory. This is currently not supported. Further note that we are not asserting the cost basis of an inventory, just the number of units. File vs. Date Assertions \uf0c1 There are two differing interpretations and implementations of the running balances for assertions: Beancount first sorts all the directives and verifies the balance at the beginning of the date of the directive. In the previous example, that is \u201c the balance before any transactions on 2012-02-04 are applied.\u201d We will call this date assertions or date-based assertions . Ledger keeps a running balance of each account\u2019s inventory during its parsing phase and performs the check at the site of the assertion in the file. We will call this file assertions or file-order, or file-based assertions . This kind of assertion has no date associated with it (this is slightly misleading in Ledger because of the way assertions are specified, as attached to a transaction\u2019s posting, which appears to imply that they occur on the transaction date, but they don\u2019t, they strictly apply to the file location). Ordering & Ambiguity \uf0c1 An important difference between those two types of assertions is that file-based assertions are not order-independent. For example, take the following input file: ;; Credit card account 2014/05/01 opening Liabilities:CreditCard $-1000.00 Expenses:Opening-Balances 2014/05/12 dinner Liabilities:CreditCard $-74.20 Expenses:Restaurant ;; Checking account 2014/06/05 salary Assets:Checking $4082.87 Income:Salary 2014/06/05 cc payment Assets:Checking $-1074.20 = $3008.67 Liabilities:CreditCard = $0 If you move the credit card payment up to the credit card account section, the same set of transactions fails: ;; Credit card account 2014/05/01 opening Liabilities:CreditCard $-1000.00 Expenses:Opening-Balances 2014/05/12 dinner Liabilities:CreditCard $-74.20 Expenses:Restaurant 2014/06/05 cc payment Assets:Checking $-1074.20 = $3008.67 Liabilities:CreditCard = $0 ;; Checking account 2014/06/05 salary Assets:Checking $4082.87 Income:Salary This subtle problem could be difficult for beginners to understand. Moving the file assertion to its own, undated directive might be more indicative syntax of its semantics, something that would look like this: balance Assets:Checking = $3008.67 The absence of a date indicates that the check is not applied at a particular point in time. Intra-Day Assertions \uf0c1 On the other hand, date-based assertions, because of their order-independence, preclude intra-day assertions, that is, a balance assertion that occurs between two postings on the same account during the same day. Beancount does not support intra-day assertions at the moment. Note despite this shortcoming, this has not been much of a problem, because it is often possible to fudge the date where necessary by adding or removing a day, or even just skipping an assertion where it would be impossible (skipping assertions is not a big deal, as they are optional and their purpose is just to provide certainty. As long as you have one that appears some date after the skipped one, it isn\u2019t much of an issue). It would be nice to find a solution to intra-day assertions for date-based assertions, however. One interesting idea would be to extend the semantics to apply the balance in file order within the set of all transactions directly before and after the balance check that occur on the same date as the balance check, for example, this would balance: 2013-05-05 balance Assets:Checking 100 USD 2013-05-20 * \"Interest payment\" Assets:Checking 12.01 USD Income:Interest 2013-05-20 balance Assets:Checking 112.01 USD 2013-05-20 * \"Check deposit\" Assets:Checking 731.73 USD Assets:Receivable The spell would be broken as soon as a directive would appear at a different date. Another idea would be to always sort the balance assertions in file-order as the second sort key (after the date) and apply them as such. I\u2019m not sure this would be easy to understand though. Beginning vs. End of Day \uf0c1 Finally, just for completeness, it is worth mentioning that date assertions have to have well-defined semantics regarding when they apply during the day. In Beancount, they currently apply at the beginning of the day. It might be worthwhile to provide an alternate version of date-based assertions that applies at the end of the day, e.g. \u201cbalance_end\u201d. Beancount v1 used to have this (\u201ccheck_end\u201d) but it was removed in the v2 rewrite, as it wasn\u2019t clear it would be really needed. The simplicity of a single meaning for balance assertions is nice too. Status \uf0c1 Ledger 3.0 currently supports only partial file-order assertions, on transactions. Beancount 2.0 currently supports only partial date-based assertions at the beginning of the day. Proposal \uf0c1 I propose the following improvements to Beancount\u2019s balance assertions. File Assertions \uf0c1 File assertions should be provided as a plugin. They would look like this: 2012-02-03 file_balance Assets:CA:Bank:Checking 417.61 CAD Ideally, in order to make it clear that they apply strictly in file order, they would not have a date, something like this: file_balance Assets:CA:Bank:Checking 417.61 CAD But this breaks the regularity of the syntax for all other directives. It also complicates an otherwise very regular and simple parser just that much more\u2026 all other directives begin with a date and a word, and all other lines are pretty much ignored. It would be a bit of a departure from this. Finally, it would still be nice to have a date just to insert those directives somewhere in the rendered journal. So I\u2019m considering keeping a date for it. If you decide to use those odd assertions, you should know what they mean. I also don\u2019t like the idea of attaching assertions to transactions; transaction syntax is already busy enough, it is calling to remain simple. This should be a standalone directive that few people use. In order to implement this, the plugin would simply resort all the directives according to their file location only (using the fileloc attribute of entries), disregarding the date, and recompute the running balances top to bottom while applying the checks. This can entirely be done via post-processing, like date-based assertions, without disturbing any of the other processing. Moreover, another advantage of doing this in a plugin is that people who don\u2019t use this directive won\u2019t have to pay the cost of calculating these inventories. Complete Assertions \uf0c1 Complete assertions should be supported in Beancount by the current balance assertion directive. They aren\u2019t very important but are potentially useful. Possible syntax ideas: 2012-02-03 balance* Assets:CA:Bank:Checking 417.61 CAD, 162 USD 2012-02-03 balance Assets:CA:Bank:Checking = 417.61 CAD, 162 USD 2012-02-03 balance full Assets:CA:Bank:Checking 417.61 CAD, 162 USD 2012-02-03 balance Assets:CA:Bank:Checking 417.61 CAD Assets:CA:Bank:Checking 162 USD I\u2019m still undecided which is best. So far it seems a matter of taste. As far as we know, the notion of inputting an explicit expected amount is unique to command-line accounting systems. Other systems \u201creconcile\u201d by freezing changes in the past. \u21a9 There are multiple reasons for this. First, in pre-computer times, accounting was done using books, and recomputing running balances manually would have involved making multiple annoying corrections to a book. This must have been incredibly inconvenient, and inserting correcting entries at the current time is a lot easier. Secondly, if your accounting balances are used to file taxes, changing some of the balances retroactively makes it difficult to go back and check the detail of reported amounts in case of an audit. This problem also applies to our context, but whether a past correction should be allowed is a choice that depends on the context and the particular account, and we leave it up to the user to decide whether it should be allowed. \u21a9","title":"Balance Assertions in Beancount"},{"location":"balance_assertions_in_beancount.html#balance-assertions-in-beancount","text":"Martin Blais, July 2014 http://furius.ca/beancount/doc/proposal-balance This document summarizes the different kinds of semantics for balance assertions in all command-line accounting systems and proposes new syntax for total and file-order running assertions in Beancount. Motivation Partial vs. Complete Assertions File vs. Date Assertions Ordering & Ambiguity Intra-Day Assertions Beginning vs. End of Day Status Proposal File Assertions Complete Assertions","title":"Balance Assertions in Beancount"},{"location":"balance_assertions_in_beancount.html#motivation","text":"Both Beancount and Ledger implement balance assertions. These provide the system with checkpoints it can use to verify the integrity of the data entry 1 . Traditional accounting principles state that a user may never change the past\u2014correcting the past involves inserting new entries to undo past mistakes 2 \u2014but we in the command-line accounting community take issue with that: we want to remain able to reconstruct the past and more importantly, to correct past mistakes at the site of their original data entry. Yes, we are dilettantes but we are bent on simplifying the process of bookkeeping by challenging existing concepts and think that this is perfectly reasonable, as long as it does not break known balances. These known balances are what we provide via balance assertions. Another way to look at these balance assertions, is that they are simply the bottom line amounts reported on various account statements, as exemplified in the figure below. Following this thread , we established that there were differing interpretations of balance assertions in the current versions of Ledger (3.0) and Beancount (2.0b).","title":"Motivation"},{"location":"balance_assertions_in_beancount.html#partial-vs-complete-assertions","text":"An assertion in Beancount currently looks like this: 2012-02-04 balance Assets:CA:Bank:Checking 417.61 CAD The meaning of this directive is: \u201cplease assert that the inventory of account \u2018Assets:CA:Bank:Checking\u2019 contains exactly 417.61 units of CAD at the end of February 3, 2012\u201d (so it\u2019s dated on the 4th because in Beancount balance assertions are specified at the beginning of the date). It says nothing about other commodities in the account\u2019s inventory. For example, if the account contains units of \u201cUSD\u201d, those are unchecked. We will call this interpretation a partial balance assertion or single-commodity assertion . An alternative assertion would be exhaustive: \u201cplease assert that the inventory of account \u2018Assets:CA:Bank:Checking\u2019 contains only 417.61 units of CAD at the end of February 3, 2012.\u201d We call this a complete balance assertion . In order to work, this second type of assertion would have to support the specification of the full contents of an inventory. This is currently not supported. Further note that we are not asserting the cost basis of an inventory, just the number of units.","title":"Partial vs. Complete Assertions"},{"location":"balance_assertions_in_beancount.html#file-vs-date-assertions","text":"There are two differing interpretations and implementations of the running balances for assertions: Beancount first sorts all the directives and verifies the balance at the beginning of the date of the directive. In the previous example, that is \u201c the balance before any transactions on 2012-02-04 are applied.\u201d We will call this date assertions or date-based assertions . Ledger keeps a running balance of each account\u2019s inventory during its parsing phase and performs the check at the site of the assertion in the file. We will call this file assertions or file-order, or file-based assertions . This kind of assertion has no date associated with it (this is slightly misleading in Ledger because of the way assertions are specified, as attached to a transaction\u2019s posting, which appears to imply that they occur on the transaction date, but they don\u2019t, they strictly apply to the file location).","title":"File vs. Date Assertions"},{"location":"balance_assertions_in_beancount.html#ordering-ambiguity","text":"An important difference between those two types of assertions is that file-based assertions are not order-independent. For example, take the following input file: ;; Credit card account 2014/05/01 opening Liabilities:CreditCard $-1000.00 Expenses:Opening-Balances 2014/05/12 dinner Liabilities:CreditCard $-74.20 Expenses:Restaurant ;; Checking account 2014/06/05 salary Assets:Checking $4082.87 Income:Salary 2014/06/05 cc payment Assets:Checking $-1074.20 = $3008.67 Liabilities:CreditCard = $0 If you move the credit card payment up to the credit card account section, the same set of transactions fails: ;; Credit card account 2014/05/01 opening Liabilities:CreditCard $-1000.00 Expenses:Opening-Balances 2014/05/12 dinner Liabilities:CreditCard $-74.20 Expenses:Restaurant 2014/06/05 cc payment Assets:Checking $-1074.20 = $3008.67 Liabilities:CreditCard = $0 ;; Checking account 2014/06/05 salary Assets:Checking $4082.87 Income:Salary This subtle problem could be difficult for beginners to understand. Moving the file assertion to its own, undated directive might be more indicative syntax of its semantics, something that would look like this: balance Assets:Checking = $3008.67 The absence of a date indicates that the check is not applied at a particular point in time.","title":"Ordering & Ambiguity"},{"location":"balance_assertions_in_beancount.html#intra-day-assertions","text":"On the other hand, date-based assertions, because of their order-independence, preclude intra-day assertions, that is, a balance assertion that occurs between two postings on the same account during the same day. Beancount does not support intra-day assertions at the moment. Note despite this shortcoming, this has not been much of a problem, because it is often possible to fudge the date where necessary by adding or removing a day, or even just skipping an assertion where it would be impossible (skipping assertions is not a big deal, as they are optional and their purpose is just to provide certainty. As long as you have one that appears some date after the skipped one, it isn\u2019t much of an issue). It would be nice to find a solution to intra-day assertions for date-based assertions, however. One interesting idea would be to extend the semantics to apply the balance in file order within the set of all transactions directly before and after the balance check that occur on the same date as the balance check, for example, this would balance: 2013-05-05 balance Assets:Checking 100 USD 2013-05-20 * \"Interest payment\" Assets:Checking 12.01 USD Income:Interest 2013-05-20 balance Assets:Checking 112.01 USD 2013-05-20 * \"Check deposit\" Assets:Checking 731.73 USD Assets:Receivable The spell would be broken as soon as a directive would appear at a different date. Another idea would be to always sort the balance assertions in file-order as the second sort key (after the date) and apply them as such. I\u2019m not sure this would be easy to understand though.","title":"Intra-Day Assertions"},{"location":"balance_assertions_in_beancount.html#beginning-vs-end-of-day","text":"Finally, just for completeness, it is worth mentioning that date assertions have to have well-defined semantics regarding when they apply during the day. In Beancount, they currently apply at the beginning of the day. It might be worthwhile to provide an alternate version of date-based assertions that applies at the end of the day, e.g. \u201cbalance_end\u201d. Beancount v1 used to have this (\u201ccheck_end\u201d) but it was removed in the v2 rewrite, as it wasn\u2019t clear it would be really needed. The simplicity of a single meaning for balance assertions is nice too.","title":"Beginning vs. End of Day"},{"location":"balance_assertions_in_beancount.html#status","text":"Ledger 3.0 currently supports only partial file-order assertions, on transactions. Beancount 2.0 currently supports only partial date-based assertions at the beginning of the day.","title":"Status"},{"location":"balance_assertions_in_beancount.html#proposal","text":"I propose the following improvements to Beancount\u2019s balance assertions.","title":"Proposal"},{"location":"balance_assertions_in_beancount.html#file-assertions","text":"File assertions should be provided as a plugin. They would look like this: 2012-02-03 file_balance Assets:CA:Bank:Checking 417.61 CAD Ideally, in order to make it clear that they apply strictly in file order, they would not have a date, something like this: file_balance Assets:CA:Bank:Checking 417.61 CAD But this breaks the regularity of the syntax for all other directives. It also complicates an otherwise very regular and simple parser just that much more\u2026 all other directives begin with a date and a word, and all other lines are pretty much ignored. It would be a bit of a departure from this. Finally, it would still be nice to have a date just to insert those directives somewhere in the rendered journal. So I\u2019m considering keeping a date for it. If you decide to use those odd assertions, you should know what they mean. I also don\u2019t like the idea of attaching assertions to transactions; transaction syntax is already busy enough, it is calling to remain simple. This should be a standalone directive that few people use. In order to implement this, the plugin would simply resort all the directives according to their file location only (using the fileloc attribute of entries), disregarding the date, and recompute the running balances top to bottom while applying the checks. This can entirely be done via post-processing, like date-based assertions, without disturbing any of the other processing. Moreover, another advantage of doing this in a plugin is that people who don\u2019t use this directive won\u2019t have to pay the cost of calculating these inventories.","title":"File Assertions"},{"location":"balance_assertions_in_beancount.html#complete-assertions","text":"Complete assertions should be supported in Beancount by the current balance assertion directive. They aren\u2019t very important but are potentially useful. Possible syntax ideas: 2012-02-03 balance* Assets:CA:Bank:Checking 417.61 CAD, 162 USD 2012-02-03 balance Assets:CA:Bank:Checking = 417.61 CAD, 162 USD 2012-02-03 balance full Assets:CA:Bank:Checking 417.61 CAD, 162 USD 2012-02-03 balance Assets:CA:Bank:Checking 417.61 CAD Assets:CA:Bank:Checking 162 USD I\u2019m still undecided which is best. So far it seems a matter of taste. As far as we know, the notion of inputting an explicit expected amount is unique to command-line accounting systems. Other systems \u201creconcile\u201d by freezing changes in the past. \u21a9 There are multiple reasons for this. First, in pre-computer times, accounting was done using books, and recomputing running balances manually would have involved making multiple annoying corrections to a book. This must have been incredibly inconvenient, and inserting correcting entries at the current time is a lot easier. Secondly, if your accounting balances are used to file taxes, changing some of the balances retroactively makes it difficult to go back and check the detail of reported amounts in case of an audit. This problem also applies to our context, but whether a past correction should be allowed is a choice that depends on the context and the particular account, and we leave it up to the user to decide whether it should be allowed. \u21a9","title":"Complete Assertions"},{"location":"beancount_cheat_sheet.html","text":"Beancount Syntax Cheat Sheet \uf0c1 Example Account Name: Assets:US:BofA:Checking Account Types Assets Liabilities Income Expenses Equity + - - + - Commodities All in CAPS: USD, EUR, CAD, AUD GOOG, AAPL, RBF1005 HOME_MAYST, AIRMILES HOURS Directives General syntax: YYYY-MM-DD Opening & Closing Accounts 2001-05-29 open Expenses:Restaurant 2001-05-29 open Assets:Checking USD,EUR ; Currency constraints 2015-04-23 close Assets:Checking Declaring Commodities This is optional; use this only if you want to attach metadata by currency. 1998-07-22 commodity AAPL name: \"Apple Computer Inc.\" Prices Use many times to fill historical price database: 2015-04-30 price AAPL 125.15 USD 2015-05-30 price AAPL 130.28 USD Notes 2013-03-20 note Assets:Checking \"Called to ask about rebate\" Documents 2013-03-20 document Assets:Checking \"path/to/statement.pdf\" Transactions 2015-05-30 * \"Some narration about this transaction\" Liabilities:CreditCard -101.23 USD Expenses:Restaurant 101.23 USD 2015-05-30 ! \"Cable Co\" \"Phone Bill\" #tag \u02c6link id: \"TW378743437\" ; Meta-data Expenses:Home:Phone 87.45 USD Assets:Checking ; You may leave one amount out Postings ... 123.45 USD Simple ... 10 GOOG {502.12 USD} With per-unit cost ... 10 GOOG {{5021.20 USD}} With total cost ... 10 GOOG {502.12 # 9.95 USD} With both costs ... 1000.00 USD @ 1.10 CAD With per-unit price ... 10 GOOG {502.12 USD} @ 1.10 CAD With cost & price ... 10 GOOG {502.12 USD, 2014-05-12 } With date ! ... 123.45 USD ... With flag Balance Assertions and Padding Asserts the amount for only the given currency: 2015-06-01 balance Liabilities:CreditCard -634.30 USD Automatic insertion of transaction to fulfill the following assertion: YYYY-MM-DD pad Assets:Checking Equity:Opening-Balances Events YYYY-MM-DD event \"location\" \"New York, USA\" YYYY-MM-DD event \"address\" \"123 May Street\" Options option \"title\" \"My Personal Ledger\" See this doc for the full list of supported options. Other pushtag #trip-to-peru ... poptag #trip-to-peru ; Comments begin with a semi-colon","title":"Beancount Cheat Sheet"},{"location":"beancount_cheat_sheet.html#beancount-syntax-cheat-sheet","text":"Example Account Name: Assets:US:BofA:Checking","title":"Beancount Syntax Cheat Sheet"},{"location":"beancount_design_doc.html","text":"Beancount Design Doc \uf0c1 Martin Blais ( blais@furius.ca ) http://furius.ca/beancount/doc/design-doc A guide for developers to understand Beancount\u2019s internals. I hope for this document to provide a map of the main objects used in the source code to make it easy to write scripts and plugins on top of Beancount or even extend it. Introduction Invariants Isolation of Inputs Order-Independence All Transactions Must Balance Account Have Types Account Lifetimes & Open Directives Supports Dates Only (and No Time) Metadata is for User Data Overview of the Codebase Core Data Structures Number Commodity Amount Lot Position Inventory Account Flag Posting About Tuples & Mutability Summary Directives Common Properties Transactions Flag Payee & Narration Tags Links Postings Balancing Postings Elision of Amounts Stream Processing Stream Invariants Loader & Processing Order Loader Output Parser Implementation Two Stages of Parsing: Incomplete Entries The Printer Uniqueness & Hashing Display Context Realization The Web Interface Reports vs. Web Client-Side JavaScript The Query Interface Design Principles Minimize Configurability Favor Code over DSLs File Format or Input Language? Grammar via Parser Generator Future Work Tagged Strings Errors Cleanup Types Conclusion Introduction \uf0c1 This document describes the principles behind the design of Beancount and a high-level overview of its codebase, data structures, algorithms, implementation and methodology. This is not a user's manual; if you are interested in just using Beancount, see the associated User's Manual and all the other documents available here . However, if you just want to understand more deeply how Beancount works this document should be very helpful. This should also be of interest to developers. This is a place where I wrote about a lot of the ideas behind Beancount that did not find any other venue. Expect some random thoughts that aren\u2019t important for users. Normally one writes a design document before writing the software. I did not do that. But I figured it would still be worthwhile to spell out some of the ideas that led to this design here. I hope someone finds this useful. Invariants \uf0c1 Isolation of Inputs \uf0c1 Beancount should accept input only from the text file you provide to its tools. In particular, it should not contact any external networked service or open any \u201cglobal\u201d files, even just a cache of historical prices or otherwise. This isolation is by design. Isolating the input to a single source makes it easier to debug, reproduce and isolate problems when they occur. This is a nice property for the tool. In addition, fetching and converting external data is very messy. External data formats can be notoriously bad, and they are too numerous to handle all of them (handling just the most common subset would beg the question of where to include the implementation of new converters). Most importantly, the problems of representing the data vs. that of fetching and converting it segment from each other very well naturally: Beancount provides a core which allows you to ingest all the transactional data and derive reports from it, and its syntax is the hinge that connects it to these external repositories of transactions or prices. It isolates itself from the ugly details of external sources of data in this way. Order-Independence \uf0c1 Beancount offers a guarantee that the ordering of its directives in an input file is irrelevant to the outcome of its computations. You should be able to organize your input text and reorder any declaration as is most convenient for you, without having to worry about how the software will make its calculations. This also makes it trivial to implement inclusions of multiple files: you can just concatenate the files if you want, and you can process them in any order. Not even directives that declare accounts or commodities are required to appear before these accounts get used in the file. All directives are parsed and then basically sorted before any calculation or validation occurs (a minor detail is that the line number is used as a secondary key in the sorting, so that the re-ordering is stable). Correspondingly, all the non-directive grammar that is in the language is limited to either setting values with global impact (\u201c option \u201d, \u201c plugin \u201d) or syntax-related conveniences (\u201c pushtag \u201d, \u201c poptag \u201d). There is an exception: Options may have effects during the processing of the file and should appear at the beginning. At the moment, Beancount does not enforce this, e.g., by performing multiple passes of parsing, but it probably should. What\u2019s more, options in included files are currently ignored. You should put all your options in the top-level ledger file. All Transactions Must Balance \uf0c1 All transactions must balance by \u201cweight.\u201d There is no exception. Beancount transactions are required to have balanced, period. In particular, there is no allowance for exceptions such as the \u201cvirtual postings\u201d that can be seen in Ledger. The first implementation of Beancount allowed such postings. As a result, I often used them to \u201cresolve\u201d issues that I did not know how to model well otherwise. When I rewrote Beancount, I set out to convert my entire input file to avoid using these, and over time I succeeded in doing this and learned a lot of new things in the process. I have since become convinced that virtual postings are wholly unnecessary, and that making them available only provides a crutch upon which a lot of users will latch instead of learning to model transfers using balanced transactions. I have yet to come across a sufficiently convincing case for them 1 . This provides the property that any subsets of transactions will sum up to zero. This is a nice property to have, it means we can generate balance sheets at any point in time. Even when we eventually support settlement dates or per-posting dates , we will split up transactions in a way that does not break this invariant. Accounts Have Types \uf0c1 Beancount accounts should be constrained to be of one of five types: Assets, Liabilities, Income, Expenses and Equity. The rationale behind this is to realize that all matters of counting can be characterized by being either permanent (Assets, Liabilities) or transitory (Income, Expenses), and that the great majority of accounts have a usual balance sign: positive (Assets, Expenses) or negative (Liabilities, Income). Given a quantity to accumulate, we can always select one of these four types of labels for it. Enforcing this makes it possible to implement reports of accumulated values for permanent accounts (balance sheet) and reports of changes of periods of time (income statement). Although it is technically possible to explicitly book postings against Equity accounts the usual purpose of items in that category is to account for past changes on the balance sheet (net/retained income or opening balances). These concepts of time and sign generalize beyond that of traditional accounting. Not having them raises difficult and unnecessary questions about how to handle calculating these types of reports. We simply require that you label your accounts into this model. I\u2019ll admit that this requires a bit of practice and forethought, but the end result is a structure that easily allows us to build commonly expected outputs. Account Lifetimes & Open Directives \uf0c1 Accounts have lifetimes; an account opens at a particular date and optionally closes at another. This is directed by the Open and Close directives. All accounts are required to have a corresponding Open directive in the stream. By default, this is enforced by spitting out an error when posting to an account whose Open directive hasn\u2019t been seen in the stream. You can automate the generation of these directives by using the \u201cauto_accounts\u201d plugin, but in any case, the stream of entries will always contain the Open directives. This is an assumption that one should be able to rely upon when writing scripts. (Eventually a similar constraint will be applied for Commodity directives so that the stream always includes one before it is used; and they should be auto-generated as well. This is not the case right now [June 2015].) Supports Dates Only (and No Time) \uf0c1 Beancount does not represent time, only dates. The minimal time interval is one day. This is for simplicity\u2019s sake. The situations where time would be necessary to disambiguate account balances exist, but in practice they are rare enough that their presence does not justify the added complexity of handling time values and providing syntax to deal with it. If you have a use case whereby times are required, you may use metadata to add it in, or more likely, you should probably write custom software for it. This is outside the scope of Beancount by choice. Metadata is for User Data \uf0c1 By default, Beancount will not make assumptions about metadata fields and values. Metadata is reserved for private usage by the user and for plugins. The Beancount core does not read metadata and change its processing based on it. However, plugins may define special metadata values that affect what they produce. That being said, there are a few special metadata fields produced by the Beancount processing: filename: A string, the name of the file the transaction was read from (or the plugin that created it) lineno: An integer, the line number from the file where the transaction was read from. tolerances : A dict of currency to Decimal, the tolerance values used in balancing the transaction. residual (on postings): A boolean, true if the posting has been added to automatically absorb rounding error. There may be a few more produced in the future but in any case, the core should not read any metadata and affect its behavior as a consequence. (I should probably create a central registry or location where all the special values can be found in one place.) Overview of the Codebase \uf0c1 All source code lives under a \u201c beancount \u201d Python package. It consists of several packages with well-defined roles, the dependencies between which are enforced strictly. Beancount source code packages and approximate dependencies between them. At the bottom, we have a beancount.core package which contains the data structures used to represent transactions and other directives in memory. This contains code for accounts, amounts, lots, positions, and inventories. It also contains commonly used routines for processing streams of directives. One level up, the beancount.parser package contains code to parse the Beancount language and a printer that can produce the corresponding text given a stream of directives. It should be possible to convert between text and data structure and round-trip this process without loss. At the root of the main package is a beancount.loader module that coordinates everything that has to be done to load a file in memory. This is the main entry point for anyone writing a script for Beancount, it always starts with loading a stream of entries. This calls the parser, runs some processing code, imports the plugins and runs them in order to produce the final stream of directives which is used to produce reports. The beancount.plugins package contains implementations of various plugins. Each of these plugin modules are independent from each other. Consult the docstrings in the source code to figure out what each of these does. Some of these are prototypes of ideas. There is a beancount.ops package which contains some high-level processing code and some of the default code that always runs by default that is implemented as plugins as well (like padding using the Pad directive). This package needs to be shuffled a bit in order to clarify its role. On top of this, reporting code calls modules from those packages. There are four packages which contain reporting code, corresponding to the Beancount reporting tools: beancount.reports : This package contains code specialized to produce different kinds of outputs (invoked by bean-report). In general I\u2019d like to avoid defining custom routines to produce desired outputs and use the SQL query tool to express grouping & aggregation instead, but I think there will always be a need for at least some of these. The reports are defined in a class hierarchy with a common interface and you should be able to extend them. Each report supports some subset of a number of output formats. beancount.query : This package contains the implementation of a query language and interactive shell (invoked by bean-query) that allows you to group and aggregate positions using an SQL-like DSL. This essentially implements processing over an in-memory table of Posting objects, with functions defined over Amount, Position and Inventory objects. beancount.web : This package contains all the source code for the default web interface (invoked by bean-web). This is a simple Bottle application that serves many of the reports from beancount.report to HTML format, running on your machine locally. The web interface provides simple views and access to your data. (It stands to be improved greatly, it\u2019s in no way perfect.) beancount.projects : This package contains various custom scripts for particular applications that I\u2019ve wanted to share and distribute. Wherever possible, I aim to fold these into reports. There are no scripts to invoke these; you should use \u201c python3 -m beancount.projects. \u201d to run them. There is a beancount.scripts package that contains the \u201cmain\u201d programs for all the scripts under the bin directory . Those scripts are simple launchers that import the corresponding file under beancount.scripts. This allows us to keep all the source code under a single directory and that makes it easier to run linters and other code health checkers on the code\u2014it\u2019s all in one place. Finally, there is a beancount.utils package which is there to contain generic reusable random bits of utility code. And there is a relatively unimportant beancount.docs package that contains code used by the author just to produce and maintain this documentation (code that connects to Google Drive). Enforcing the dependency relationships between those packages is done by a custom script . Core Data Structures \uf0c1 This section describes the basic data structures that are used as building blocks to represent directives. Where possible I will describe the data structure in conceptual terms. (Note for Ledger users : This section introduces some terminology for Beancount; look here if you\u2019re interested to contrast it with concepts and terminology found in Ledger.) Number \uf0c1 Numbers are represented using decimal objects, which are perfectly suited for this. The great majority of numbers entered in accounting systems are naturally decimal numbers and binary representations involve representational errors which cause many problems for display and in precision. Rational numbers avoid this problem, but they do not carry the limited amount of precision that the user intended in the input. We must deal with tolerances explicitly. Therefore, all numbers should use Python\u2019s \u201c decimal.Decimal \u201d objects. Conveniently, Python v3.x supports a C implementation of decimal types natively (in its standard library; this used to be an external \u201ccdecimal\u201d package to install but it has been integrated in the C/Python interpreter). The default constructor for decimal objects does not support some of the input syntax we want to allow, such as commas in the integer part of numbers (e.g., \u201c278 , 401.35 USD\u201d) or initialization from an empty string. These are important cases to handle. So I provide a special constructor to accommodate these inputs: beancount.core.number.D() . This is the only method that should be used to create decimal objects: from beancount.core.number import D \u2026 number = D(\"2,402.21\") I like to import the \u201c D \u201d symbol by itself (and not use number.D ). All the number-related code is located under beancount.core.number . Some number constants have been defined as well: ZERO , ONE , and HALF . Use those instead of explicitly constructed numbers (such as D(\"1\") ) as it makes it easier to grep for such initializations. Commodity \uf0c1 A commodity , or currency (I use both names interchangeably in the code and documentation) is a string that represents a kind of \u201cthing\u201d that can be stored in accounts. In the implementation, it is represented as a Python str object (there is no module with currency manipulation code). These strings may only contain capital letters and numbers and some special characters (see the lexer code). Beancount does not predefine any currency names or categories\u2014all currencies are created equal. Really. It genuinely does not know anything special about dollars or euros or yen or anything else. The only place in the source code where there is a reference to those is in the tests. There is no support for syntax using \u201c$\u201d or other currency symbols; I understand that some users may want this syntax, but in the name of keeping the parser very simple and consistent I choose not to provide that option. Moreover, Beancount does not make a distinction between commodities which represent \"money\" and others that do not (such as shares, hours, etc.). These are treated the same throughout the system. It also does not have the concept of a \"home\" currency 2 ; it's a genuinely multi-currency system. Currencies need not be defined explicitly in the input file format; you can just start using them in the file and they will be recognized as such by their unique syntax (the lexer recognizes and emits currency tokens). However, you may declare some using a Commodity directive. This is only provided so that a per-commodity entity exists upon which the user can attach some metadata, and some reports and plugins are able to find and use that metadata. Account \uf0c1 An account is basically just the name of a bucket associated with a posting and is represented as a simple string (a Python str object). Accounts are detected and tokenized by the lexer and have names with at least two words separated by a colon (\":\"). Accounts don\u2019t have a corresponding object type; we just refer to them by their unique name string (like filenames in Python). When per-account attributes are needed, we can extract the Open directives from the stream of entries and find the one corresponding to the particular account we\u2019re looking for. Similar to Python\u2019s os.path module, there are some routines to manipulate account names in beancount.core.account and the functions are named similarly to those of the os.path module. The first component of an account\u2019s name is limited to one of five types: Assets Liabilities Equity Income Expenses The names of the account types as read in the input syntax may be customized with some \"option\" directives, so that you can change those names to another language, or even just rename \"Income\" to \"Revenue\" if you prefer that. The beancount.core.account_types module contains some helpers to deal with these. Note that the set of account names forms an implicit hierarchy. For example, the names: Assets:US:TDBank:Checking Assets:US:TDBank:Savings implicitly defines a tree of nodes with parent nodes \"Assets\", \"US\", \"TDBank\" with two leaf nodes \"Checking\" and \"Savings\". This implicit tree is never realized during processing, but there is a Python module that allows one to do this easily (see beancount.core.realization ) and create linearized renderings of the tree. Flag \uf0c1 A \u201cflag\u201d is a single-character string that may be associated with Transactions and Postings to indicate whether they are assumed to be correct (\"reconciled\") or flagged as suspicious. The typical value used on transaction instances is the character \u201c*\u201d. On Postings, it is usually left absent (and set to a None). Amount \uf0c1 An Amount is the combination of a number and an associated currency, conceptually: Amount = (Number, Currency) Amount instances are used to represent a quantity of a particular currency (the \u201cunits\u201d) and the price on a posting. A class is defined in beancount.core.amount as a simple tuple-like object. Functions exist to perform simple math operations directly on instances of Amount. You can also create Amount instance using amount.from_string() , for example: value = amount.from_string(\"201.32 USD\") Cost \uf0c1 A Cost object represents the cost basis for a particular lot of a commodity. Conceptually, it is Cost = (Number, Currency, Date, Label) The number and currency is that of the cost itself, not of the commodity. For example, if you bought 40 shares of AAPL stock at 56.78 USD, the Number is a \u201c56.78\u201d decimal and the Currency is \u201cUSD.\u201d For example: Cost(Decimal(\"56.78\"), \"USD\", date(2012, 3, 5), \"lot15\") The Date is the acquisition date of the corresponding lot (a datetime.date object). This is automatically attached to the Cost object when a posting augments an inventory\u2014the Transaction\u2019s date is automatically attached to the Cost object\u2014or if the input syntax provides an explicit date override. The Label can be any string. It is provided as a convenience for a user to refer to a particular lot or disambiguate similar lots. On a Cost object, the number, currency and date attributes are always set. If the label is unset, it has a value of \u201cNone.\u201d CostSpec \uf0c1 In the input syntax, we allow users to provide as little information as necessary in order to disambiguate between the lots contained in the inventory prior to posting. The data provided filters the set of matching lots to an unambiguous choice, or to a subset from which an automated booking algorithm will apply (e.g., \u201cFIFO\u201d). In addition, we allow the user to provide either the per-unit cost and/or the total-cost. This convenience is useful to let Beancount automatically compute the per-unit cost from a total of proceeds. CostSpec = (Number-per-unit, Number-total, Currency, Date, Label, Merge) Since any of the input elements may be omitted, any of the attributes of a CostSpec may be left to None. If a number is missing and necessary to be filled in, the special value \u201cMISSING\u201d will be set on it. The Merge attribute it used to record a user request to merge all the input lots before applying the transaction and to merge them after. This is the method used for all transactions posted to an account with the \u201cAVERAGE\u201d booking method. Position \uf0c1 A position represents some units of a particular commodity held at cost. It consists simply in Position = (Units, Cost) Units is an instance of Amount , and Cost is an instance of Cost , or a null value if the commodity is not held at cost. Inventories contain lists of Position instances. See its definition in beancount.core.position . Posting \uf0c1 Each Transaction directive is composed of multiple Postings (I often informally refer to these in the code and on the mailing-list as the \u201clegs\u201d of a transaction). Each of these postings is associated with an account, a position and an optional price and flag: Posting = (Account, Units, Cost-or-CostSpec, Price, Flag, Metadata) As you can see, a Posting embeds its Position instance 3 . The Units is an Amount , and the \u2018cost\u2019 attribute refers to either a Cost or a CostSpec instance (the parser outputs Posting instances with an CostSpec attribute which is resolved to a Cost instance by the booking process; see How Inventories Work for details). The Price is an instance of Amount or a null value. It is used to declare a currency conversion for balancing the transaction, or the current price of a position held at cost. It is the Amount that appears next to a \u201c@\u201d in the input. Flags on postings are relatively rare; users will normally find it sufficient to flag an entire transaction instead of a specific posting. The flag is usually left to None; if set, it is a single-character string. The Posting type is defined in beancount.core.data , along with all the directive types. Inventory \uf0c1 An Inventory is a container for an account\u2019s balance in various lots of commodities. It is essentially a list of Position instances with suitable operations defined on it. Conceptually you may think of it as a mapping with unique keys: Inventory = [Position1, Position2, Position3, \u2026 , PositionN] Generally, the combination of a position\u2019s ( Units.Currency, Cost) is kept unique in the list, like the key of a mapping. Positions for equal values of currency and cost are merged together by summing up their Units.Number and keeping a single position for it. And simple positions are mixed in the list with positions held at cost. The Inventory is one of the most important and oft-used object in Beancount\u2019s implementation, because it is used to sum the balance of one or more accounts over time. It is also the place where the inventory reduction algorithms get applied to, and traces of that mechanism can be found there. The \u201c How Inventories Work \u201d document provides the full detail of that process. For testing, you can create initialized instances of Inventory using inventory.from_string() . All the inventory code is written in beancount.core.inventory . About Tuples & Mutability \uf0c1 Despite the program being written in a language which does not make mutability \u201cdifficult by default\u201d, I designed the software to avoid mutability in most places. Python provides a \u201c collections.namedtuple \u201d factory that makes up new record types whose attributes cannot be overridden. Well\u2026 this is only partially true: mutable attributes of immutable tuples can be modified. Python does not provide very strong mechanisms to enforce this property. Regardless, functional programming is not so much an attribute of the language used to implement our programs than of the guarantees we build into its structure. A language that supports strong guarantees helps to enforce this. But if, even by just using a set of conventions, we manage to mostly avoid mutability, we have a mostly functional program that avoids most of the pitfalls that occur from unpredictable mutations and our code is that much easier to maintain. Programs with no particular regard for where mutation occurs are most difficult to maintain. By avoiding most of the mutation in a functional approach, we avoid most of those problems. Most of the data structures used in Beancount are namedtuples , which make the modification of their attributes inconvenient on purpose. Most of the code will attempt to avoid mutation, except for local state (within a function) or in a narrow scope that we can easily wrap our head around. Where mutation is possible, by convention I try to avoid it by default. When we do mutation, I try to document the effects. I avoid the creation of classes which mutate their internal state, except for a few cases (e.g. the web application). I prefer functions to objects and where I define classes I mostly avoid inheritance. These properties are especially true for all the small objects: Amount, Lot, Position, Posting objects, and all the directives types from beancount.core.data . On the other hand, the Inventory object is nearly always used as an accumulator and does allow the modification of its internal state (it would require a special, persistent data structure to avoid this). You have to be careful how you share access to Inventory objects\u2026 and modify them, if you ever do. Finally, the loader produces lists of directives which are all simple namedtuple objects. These lists form the main application state. I\u2019ve avoided placing these inside some special container and instead pass them around explicitly, on purpose. Instead of having some sort of big \u201capplication\u201d object, I\u2019ve trimmed down all the fat and all your state is represented in two things: A dated and sorted list of directives which can be the subject of stream processing and a list of constant read-only option values. I think this is simpler. I credit my ability to make wide-ranging changes to a mid-size Python codebase to the adoption of these principles. I would love to have types in order to safeguard against another class of potential errors, and I plan to experiment with Python 3.5\u2019s upcoming typing module . Summary \uf0c1 The following diagram explains how these objects relate to each other, starting from a Posting. For example, to access the number of units of a postings you could use posting.units.number For the cost currency: posting.cost.currency You can print out the tuples in Python to figure out their structure. Previous Design \uf0c1 For the sake of preservation, if you go back in time in the repository, the structure of postings was deeper and more complex. The new design reflects a flatter and simpler version of it. Here is what the old design used to look like: Directives \uf0c1 The main production from loading a Beancount input file is a list of directives . I also call these entries interchangeably in the codebase and in the documents. There are directives of various types: Transaction Balance & Pad Open & Close Commodity Note Event Price Document In a typical Beancount input file, the great majority of entries will be Transactions and some Balance assertions. There will also be Open & perhaps some Close entries. Everything else is optional. Since these combined with a map of option values form the entire application state, you should be able to feed those entries to functions that will produce reports. The system is built around this idea of processing a stream of directives to extract all contents and produce reports, which are essentially different flavors of filtering and aggregations of values attached to this stream of directives. Common Properties \uf0c1 Some properties are in common to all the directives: Date. A datetime.date object. This is useful and consistent. For a Transaction, that\u2019s the date at which it occurs. For an Open or Close directive, that\u2019s the date at which the account was opened or closed. For a Pad directive, that\u2019s the date at which to insert a transaction. For a Note or Document, it is the date at which to insert the note in the stream of postings of that account. For an Event, it is the date at which it occurs. For a Commodity directive which essentially provides a per-commodity hanging point for commodity-related metadata, the date isn\u2019t as meaningful; I choose to date those on the day the commodity was created. Meta . All the directives have metadata attribute (a Python dict object). The purpose of metadata is to allow the user to hang any kind of ancillary data they want and then use this in scripts or queries. Posting instances also have a metadata attribute. Transactions \uf0c1 The Transaction directive is the subject of Beancount and is by far the most common directive found in input files and is worth of special attention. The function of a bookkeeping system is to organize the Postings attached to Transactions in various ways. All the other types of entries occupy supporting roles in our system. A Transaction has the following additional fields. Flag \uf0c1 The single-character flag is usually there to replace the \u201ctxn\u201d keyword (Transaction is the only directive which allows being entered without entering its keyword). I\u2019ve been considering changing the syntax definition somewhat to allow not entering the flag nor the keyword, because I\u2019d like to eventually support that. Right now, either the flag or keyword is required. The flag attribute may be set to None. Payee & Narration \uf0c1 The narration field is a user-provided description of the transaction, such as \"Dinner with Mary-Ann.\" You can put any information in this string. It shows up in the journal report. Oftentimes it is used to enrich the transaction with context that cannot be imported automatically, such as \"transfer from savings account to pay for car repairs.\" The payee name is optional, and exists to describe the entity with which the transaction is conducted, such as \"Whole Foods Market\" or \"Shell.\" Note that I want to be able to produce reports for all transactions associated with a particular payee, so it would be nice to enter consistent payee names. The problem with this is that the effort to do this right is sometimes too great. Either better tools or looser matching over names is required in order to make this work. The input syntax also allows only a single string to be provided. By default this becomes the narration, but I\u2019ve found that in practice it can be useful to have just the payee. It\u2019s just a matter of convenience. At the moment, if you want to enter just the payee string you need to append an empty narration string. This should be revisited at some point. Tags \uf0c1 Tags are sets of strings that can be used to group sets of transactions (or set to None if there are no tags). A view of this subset of transactions can then be generated, including all the usual reports (balance sheet, income statement, journals, etc.). You can tag transactions for a variety of purposes; here are some examples: All transactions during a particular travel might be tagged to later summarize your trip expenses. Those transactions will usually occur around the same date and therefore there is convenient syntax used to automatically tag many transactions that are declared together in a file. Transactions related to a particular project to be accomplished. I took some classes in an online program once, and tagged all related expenses to it. I use this to track all my immigration-related expenses for a particular stage (e.g. green card). Declaring a group of expenses to be paid back by an employer (or your own company) later on. Expenses related to moving between locations. Typical tag names that I use for tags look like #trip-israel-2012 , #conference-siggraph , and #course-cfa . In general, tags are useful where adding a sub-accounts won't do. This is often the case where a group of related expenses are of differing types, and so they would not belong to a single account. Given the right support from the query tools, they could eventually be subsumed by metadata\u2014I have been considering converting tags into metadata keys with a boolean value of True, in order to remove unnecessary complexity. Links \uf0c1 Links are a unique set of strings or None , and in practice will be usually empty for most transactions. They differ from tags only in purpose. Links are there to chain together a list of related transactions and there are tools used to list, navigate and balance a subset of transactions linked together. They are a way for a transaction to refer to other transactions. They are not meant to be used for summarizing. Examples include: transaction-ids from trading accounts (these often provide an id to a \"related\" or \"other\" transaction); account entries associated with related corrections, for example a reversed fee following a phone call could be linked to the original invalid fee from several weeks before; the purchase and sale of a home, and related acquisition expenses. In contrast to tags, their strings are most often unique numbers produced by the importers. No views are produced for links; only a journal of a particular links transactions can be produced and a rendered transaction should be accompanied by an actual \"link\" icon you can click on to view all the other related transactions. Postings \uf0c1 A list of Postings are attached to the Transaction object. Technically this list object is mutable but in practice we try not to modify it. A Posting can ever only be part of a single Transaction. Sometimes different postings of the same transaction will settle at different dates in their respective accounts, so eventually we may allow a posting to have its own date to override the transaction's date, to be used as documentation; in the simplest version, we enforce all transactions to occur punctually, which is simple to understand and was not found to be a significant problem in practice. Eventually we might implement this by implicitly converting Transactions with multiple dates into multiple Transactions and using some sort of transfer account to hold the pending balance in-between dates. See the associated proposal for details. Balancing Postings \uf0c1 The fundamental principle of double-entry book-keeping is enforced in each of the Transaction entries: the sum of all postings must be zero. This section describes the specific way in which we do this, and is the key piece of logic that allow the entire system to balance nicely. This is also one of the few places in this system where calculations go beyond simple filtering and aggregation. As we saw earlier, postings are associated with A position, which is a number of units of a lot (which itself may or may not have a cost) An optional conversion price Given this, how do we balance a transaction? Some terminology is in order: for this example posting: Assets:Investment:HOOL -50 HOOL {700 USD} @ 920 USD Units. The number of units is the number of the position, or \u201c50\u201d. Currency. The commodity of the lot, that is, \u201cHOOL\u201d. Cost. The cost of this position is the number of units times the per-unit cost in the cost currency, that is 50 x 700 = 35000 USD. (Total) Price. The price of a unit is the attached price amount, 920 USD. The total price of a position is its number of units times the conversion price, in this example 50 x 920 = 46000 USD. Weight. The amount used to balance each posting in the transaction: If the posting has an associated cost, we calculate the cost of the position (regardless of whether a price is present or not); If the posting has no associated cost but has a conversion price, we convert the position into its total price; Finally, if the posting has no associated cost nor conversion price, the number of units of the lot are used directly. Balancing is really simple: each of the Posting's positions are first converted into their weight. These amounts are then grouped together by currency, and the final sum for each currency is asserted to be close to zero, that is, within a small amount of tolerance (as inferred by a combination of by the numbers in the input and the options ). The following example is one of case (2), with a price conversion and a regular leg with neither a cost nor a price (case 3): 2013-07-22 * \"Wired money to foreign account\" Assets:Investment:HOOL -35350 CAD @ 1.01 USD ;; -35000 USD (2) Assets:Investment:Cash 35000 USD ;; 35000 USD (3) ;;------------------ ;; 0 USD In the next example, the posting for the first leg has a cost, the second posting has neither: 2013-07-22 * \"Bought some investment\" Assets:Investment:HOOL 50 HOOL {700 USD} ;; 35000 USD (1) Assets:Investment:Cash -35000 USD ;; -35000 USD (3) ;;------------------ ;; 0 USD Here's a more complex example with both a price and a cost; the price here is ignored for the purpose of balance and is used only to enter a data-point in the internal price database: 2013-07-22 * \"Sold some investment\" Assets:Investment:HOOL -50 HOOL {700 USD} @ 920 USD ;; -35000 USD (1) Assets:Investment:Cash 46000 USD ;; 46000 USD (3) Income:CapitalGains -11000 USD ;; -11000 USD (3) ;;------------------ ;; 0 USD Finally, here's an example with multiple commodities: 2013-07-05 * \"COMPANY INC PAYROLL\" Assets:US:TD:Checking 4585.38 USD Income:US:Company:GroupTermLife -25.38 USD Income:US:Company:Salary -5000.00 USD Assets:US:Vanguard:Cash 540.00 USD Assets:US:Federal:IRAContrib -540.00 IRAUSD Expenses:Taxes:US:Federal:IRAContrib 540.00 IRAUSD Assets:US:Company:Vacation 4.62 VACHR Income:US:Company:Vacation -4.62 VACHR Here there are three groups of weights to balance: USD: (4485.38) + (-25.38) + (-5000) + (540) ~= 0 USD IRAUSD: (540.00) + (-540.00) ~= 0 IRAUSD VACHR: (4.62) + (-4.62) ~= 0 VACHR Elision of Amounts \uf0c1 Transactions allow for at most one posting to be elided and automatically set; if an amount was elided, the final balance of all the other postings is attributed to the balance. With the inventory booking proposal , it should be extended to be able to handle more cases of elision. An interesting idea would be to allow the elided postings to at least specify the currency they're using. This would allow the user to elide multiple postings, like this: 2013-07-05 * \"COMPANY INC PAYROLL\" Assets:US:TD:Checking USD Income:US:Company:GroupTermLife -25.38 USD Income:US:Company:Salary -5000.00 USD Assets:US:Vanguard:Cash 540.00 USD Assets:US:Federal:IRAContrib -540.00 IRAUSD Expenses:Taxes:US:Federal:IRAContrib IRAUSD Assets:US:Company:Vacation 4.62 VACHR Income:US:Company:Vacation VACHR Stream Processing \uf0c1 An important by-product of representing all state using a single stream of directives is that most of the operations in Beancount can be implemented by simple functions that accept the list of directives as input and output a modified list of directives. For example, The \u201cPad\u201d directive is implemented by processing the stream of transactions, accumulating balances, inserting a new padding transaction after the Pad directive when a balance assertion fails. Summarization operations (open books, close books, clear net income to equity) are all implemented this way, as functional operators on the list of streams. For example, opening the books at a particular date is implemented by summing all balances before a certain date and replacing those transactions by new transactions that pull the final balance from an Equity account. Closing the books only involves moving the balances of income statement account to Equity and truncation the list of transactions to that date. This is very elegant\u2014the reporting code can be oblivious to the fact that summarization has occurred. Prices on postings held with a cost are normally ignored. The \u201cimplicit prices\u201d option is implemented using a plugin which works its magic by processing the stream of operations and inserting otherwise banal Price directives automatically when such a posting is found. Many types of verifications are also implemented this way; the sellgains plugin verifies that non-income balances check against the converted price of postings disregarding the cost. This one does not insert any new directives, but may generate new errors. These are just some examples. I\u2019m aiming to make most operations work this way. This design has proved to be elegant, but also surprisingly flexible and it has given birth to the plugins system available in Beancount; you might be surprised to learn that I had not originally thought to provide a plugins system... it just emerged over time teasing abstractions and trying to avoid state in my program. I am considering experimenting with weaving the errors within the stream of directives by providing a new \u201cError\u201d directive that could be inserted by stream processing functions. I\u2019m not sure whether this would make anything simpler yet, it\u2019s just an idea at this point [July 2015]. Stream Invariants \uf0c1 The stream of directives comes with a few guarantees you can rest upon: All the directives are ordered by date. Because many dates have multiple directives, in order to have a stable sorting order the line number in the file is used as a secondary sort key. All uses of an account is preceded in the stream by a corresponding Open entry. If the input did not provide one, an Open directive is automatically synthesized. All Balance directives precede any other directive on a particular day. This is enforced in order to make the processing of balance assertions straightforward and to emphasize that their semantics is to occur at the beginning of the day. Loader & Processing Order \uf0c1 The process of loading a list of entries from an input file is the heart of the project. It is important to understand it in order to understand how Beancount works. Refer to the diagram below. It consists of A parsing step that reads in all the input files and produces a stream of potentially incomplete directives. The processing of this stream of entries by built-in and user-specified plugins. This step potentially produces new error objects. A final validation step that ensures the invariants have not been broken by the plugins. The beancount.loader module orchestrates this processing. In the code and documents, I\u2019m careful to distinguish between the terms \u201cparsing\u201d and \u201cloading\u201d carefully. These two concepts are distinct. \u201cParsing\u201d is only a part of \u201cloading.\u201d The user-specified plugins are run in the order they are provided in the input files. Raw mode. Some users have expressed a desire for more control over which built-in plugins are run and in which order they are to be run, and so enabling the \u201craw\u201d option disables the automatic loading of all built-in plugins (you have to replace those with explicit \u201cplugin\u201d directives if you want to do that). I\u2019m not sure if this is going to be useful yet, but it allows for tighter control over the loading process for experimentation. Loader Output \uf0c1 The parser and the loader produce three lists: entries: A list of directive tuples from beancount.core.data. This is the stream of data which should consists mostly of Transaction and Balance instances. As much as possible, all data transformations are performed by inserting or removing entries from this list. Throughout the code and documents, I refer to these as entries or directives interchangeably. errors: A list of error objects. In Beancount I don\u2019t use exceptions to report errors; rather, all the functions produce error objects and they are displayed at the most appropriate time. (These deserve a base common type but for now the convention they respect is that they all provide a source (filename, line-no), message and entry attributes.) options_map: A dict of the options provided in the file and derived during parsing. Though this is a mutable object, we never modify it once produced by the parser. Parser Implementation \uf0c1 The Beancount parser is a mix of C and Python 3 code. This is the reason you need to compile anything in the first place. The parser is implemented in C using a flex tokenizer and a Bison parser generator, mainly for performance reasons. I chose the C language and these crufty old GNU tools because they are portable and will work everywhere. There are better parser generators out there, but they would either require my users to install more compiler tools or exotic languages. The C language is a great common denominator. I want Beancount to work everywhere and to be easy to install. I want this to make it easy on others, but I also want this for myself, because at some point I\u2019ll be working on other projects and the less dependencies I have the lighter it will be to maintain aging software. Just looking ahead. (That\u2019s also why I chose to use Python instead of some of the other languages I\u2019m more naturally attracted to these days (Clojure, Go, ML); I need Beancount to work... when I\u2019m counting the beans I don\u2019t have time to debug problems. Python is careful about its changes and it will be around for a long long time as is.) There is a lexer file lexer.l written in flex and a Bison grammar in grammar.y. These tools are used to generate the corresponding C source code (lexer.h/.c and grammar.h/.c). These, along with some hand-written C code that defines some interface functions for the module (parser.h/.c), are compiled into an extension module (_parser.so). Eventually we could consider creating a small dependency rule in setup.py to invoke flex and Bison automatically but at the moment, in order to minimize the installation burden, I check the generated source code in the repository (lexer.h/c and grammar.h/c). The interaction between the Python and C code works like this: You import beancount.parser.parser and invoke either parse_file() or parse_string(). This uses code in grammar.py to create a Builder object which essentially provides callbacks in Python to process grammar rules. parser.py calls a C function in the extension module with the Builder object. That C code sets up flex to be able to read the input file or string then hands over control to the parser by calling the generated C function \u201cyyparse()\u201d. yyparse() is the parser and will fetch input tokens from the lexer C code using successive calls to \u201cyylex()\u201d and attempt to reduce rules. When a rule or token is successfully reduced, the C code invokes callback methods on the Builder object you provided. The parser\u2019s rules communicate between each other by passing around \u201cPyObject*\u201d instances, so that code has to be a little careful about correctly handling reference counting. This allows me to run Python code on rule reduction, which makes it really easy to customize parser behaviour yet maintain the performance of the grammar rule processing in C. Note that the grammar.py Builder derives from a similar Builder class defined in lexer.py, so that lexer tokens recognized calls methods defined in the lexer.py file to create them and parser rule correspondingly calls methods from grammar.py\u2019s Builder. This isolation is not only clean, it also makes it possible to just use the lexer to tokenize a file from Python (without parsing) for testing the tokenizer; see the tests where this is used if you\u2019re curious how this works. I just came up with this; I haven\u2019t seen this done anywhere else. I tried it and the faster parser made a huge difference compared to using PLY so I stuck with that. I quite like the pattern, it\u2019s a good compromise that offers the flexibility of a parser generator yet allows me to handle the rules using Python. Eventually I\u2019d like to move just some of the most important callbacks to C code in order to make this a great deal faster (I haven\u2019t done any real performance optimizations yet). This has worked well so far but for one thing: There are several limitations inherent in the flex tokenizer that have proved problematic. In particular, in order to recognize transaction postings using indent I have had to tokenize the whitespace that occurs at the beginning of a line. Also, single-characters in the input should be parsed as flags and at the moment this is limited to a small subset of characters. I\u2019d like to eventually write a custom lexer with some lookahead capabilities to better deal with these problems (this is easy to do). Two Stages of Parsing: Incomplete Entries \uf0c1 At the moment, the parser produces transactions which may or may not balance. The validation stage which runs after the plugins carries out the balance assertions. This degree of freedom is allowed to provide the opportunity for users to enter incomplete lists of postings and for corresponding plugins to automate some data entry and insert completing postings. However, the postings on the transactions are always complete objects, with all their expected attributes set. For example, a posting which has its amount elided in the input syntax will have a filled in position by the time it comes out of the parser. We will have to loosen this property when we implement the inventory booking proposal , because we want to support the elision of numbers whose values depend on the accumulated state of previous transactions. Here is an obvious example. A posting like this 2015-04-03 * \"Sell stock\" Assets:Investments:AAPL -10 AAPL {} ... should succeed if at the beginning of April 3rd the account contains exactly 10 units of AAPL, in either a single or multiple lots. There is no ambiguity: \u201csell all the AAPL,\u201d this says. However, the fact of its unambiguity assumes that we\u2019ve computed the inventory balance of that account up until April 3rd\u2026 So the parser will need to be split into two phases: A simple parsing step that produces potentially incomplete entries with missing amounts A separate step for the interpolation which will have available the inventory balances of each account as inputs. This second step is where the booking algorithms (e.g., FIFO) will be invoked from. See the diagram above for reference. Once implemented, everything else should be the same. The Printer \uf0c1 In the same package as the parser lives a printer. This isolates all the functionality that deals with the Beancount \u201clanguage\u201d in the beancount.parser package: the parser converts from input syntax to data structure and the printer does the reverse. No code outside this package should concern itself with the Beancount syntax. At some point I decided to make sure that the printer was able to round-trip through the parser, that is, given a stream of entries produced by the loader, you should be able to convert those to text input and parse them back in and the resulting set of entries should be the same (outputting the re-read input back to text should produce the same text), e.g., Note that the reverse isn\u2019t necessarily true: reading an input file and processing it through the loader potentially synthesizes a lot of entries (thanks to the plugins), so printing it back may not produce the same input file (even ignoring reordering and white space concerns). This is a nice property to have. Among other things, it allows us to use the parser to easily create tests with expected outputs. While there is a test that attempts to protect this property, some of the recent changes break it partially: Metadata round-tripping hasn\u2019t been tested super well. In particular, some of the metadata fields need to be ignored (e.g., filename and lineno). Some directives contain derived data, e.g. the Balance directive has a \u201cdiff_amount\u201d field that contains the difference when there is a failure in asserting a balance. This is used to report errors more easily. I probably need to remove these exceptions at some point because it is the only one of its kind (I could replace it with an inserted \u201cError\u201d directive). This probably needs to get refined a bit at some point with a more complete test (it\u2019s not far as it is). Uniqueness & Hashing \uf0c1 In order to make it possible to compare directives quickly, we support unique hashing of all directives, that is, from each directive we should be able to produce a short and unique id. We can then use these ids to perform set inclusion/exclusion/comparison tests for our unit tests. We provide a base test case class with assertion methods that use this capability . This feature is used liberally throughout our test suites. This is also used to detect and remove duplicates. This feature is optional and enabled by the beancount.plugins.noduplicates plugin. Note that the hashing of directives currently does not include user meta-data . Display Context \uf0c1 Number are input with different numbers of fractional digits: 500 -> 0 fractional digits 520.23 -> 2 fractional digits 1.2357 -> 4 fractional digits 31.462 -> 3 fractional digits Often, a number used for the same currency is input with a different number of digits. Given this variation in the input, a question that arises is how to render the numbers consistently in reports? Consider that: We would like that the user not to have to specify the number of fractional digits to use for rendering by default. Rendering numbers may differ depending on the context: most of the time we want to render numbers using their most commonly seen number of digits, rounding if necessary, but when we are rendering conversion rates we would like to render numbers using the maximum number of digits ever seen. The number of digits used tends to vary with the commodity they represent. For example, US dollars will usually render with two digits of precision; the number of units of a mutual fund such as RGAGX might be recorded by your broker with 3 digits of precision. Japanese Yen will usually be specified in integer units. We want to render text report which need to align numbers with varying number of fractional digits together, aligning them by their period or rightmost digits, and possibly rendering a currency thereafter. In order to deal with this thorny problem, I built a kind of accumulator which is used to record all numbers seen from some input and tally statistics about the precisions witnessed for each currency. I call this a DisplayContext . From this object one can request to build a DisplayFormatter object which can be used to render numbers in a particular way. I refer to those objects using the variable names dcontext and dformat throughout the code. The parser automatically creates a DisplayContext object and feeds it all the numbers it sees during parsing. The object is available in the options_map produced by the loader. Realization \uf0c1 It occurs often that one needs to compute the final balances of a list of filtered transactions, and to report on them in a hierarchical account structure. See diagram below. \uf0c1 For the purpose of creating hierarchical renderings, I provide a process called a \u201c realization .\u201d A realization is a tree of nodes, each of which contains An account name, A list of postings and other entries to that account, and The final balance for that account, A mapping of sub-account names to child nodes. The nodes are of type \u201cRealAccount,\u201d which is also a dict of its children. All variables of type RealAccount in the codebase are by convention prefixed by \u201creal_*\u201d. The realization module provides functions to iterate and produce hierarchical reports on these trees of balances, and the reporting routines all use these. For example, here is a bit of code that will dump a tree of balances of your accounts to the console: import sys from beancount import loader from beancount.core import realization entries, errors, options_map = loader.load_file(\"filename.beancount\") real_root = realization.realize(entries) realization.dump_balances(real_root, file=sys.stdout) The Web Interface \uf0c1 Before the appearance of the SQL syntax to filter and aggregate postings, the only report produced by Beancount was the web interface provided by bean-web. As such, bean-web evolved to be good enough for most usage and general reports. Reports vs. Web \uf0c1 One of the important characteristics of bean-web is that it should just be a thin dispatching shell that serves reports generated from the beancount.reports layer. It used to contain the report rendering code itself, but at some point I began to factor out all the reporting code to a separate package in order to produce reports to other formats, such as text reports and output to CSV. This is mostly finished, but at this time [July 2015] some reports remain that only support HTML output. This is why. Client-Side JavaScript \uf0c1 I would like to eventually include a lot more client-side scripting in the web interface. However, I don\u2019t think I\u2019ll be able to work on this for a while, at least not until all the proposals for changes to the core are complete (e.g., inventory booking improvements, settlement splitting and merging, etc.). If you\u2019d like to contribute to Beancount, improving bean-web or creating your own visualizations would be a great venue. The Query Interface \uf0c1 The current query interface is the result of a prototype . It has not yet been subjected to the amount of testing and refinement as the rest of the codebase. I\u2019ve been experimenting with it and have a lot of ideas for improving the SQL language and the kinds of outputs that can be produced from it. I think of it as \u201c70% done\u201d. However, it works, and though some of the reports can be a little clunky to specify, it produces useful results. It will be the subject of a complete rewrite at some point and when I do, I will keep the current implementation for a little while so that existing scripts don\u2019t just break; I\u2019ll implement a v2 of the shell. Design Principles \uf0c1 Minimize Configurability \uf0c1 First, Beancount should have as few options as possible. Second, command-line programs should have no options that pertain to the processing and semantic of the input file (options that affect the output or that pertain to the script itself are fine). The goal is to avoid feature creep. Just a few options opens the possibility of them interacting in complex ways and raises a lot of questions. In this project, instead of providing options to customize every possible thing, I choose to bias the design towards providing the best behavior by default, even if that means less behavior. This is the Apple approach to design. I don\u2019t usually favor this approach for my projects, but I chose it for this particular one. What I\u2019ve learned in the process is how far you can get and still provide much of the functionality you originally set out for. Too many command-line options makes a program difficult to use. I think Ledger suffers from this, for instance. I can never remember options that I don\u2019t use regularly, so I prefer to just design without them. Options that affect semantics should live in the file itself (and should be minimized as well). Options provided when running a process should be only to affect this process. Having less options also makes the software much easier to refactor. The main obstacle to evolving a library of software is the number of complex interactions between the codes. Guaranteeing a well-defined set of invariants makes it much easier to later on split up functionality or group it differently. So.. by default I will resist making changes that aren't generic or that would not work for most users. On the other hand, large-scale changes that would generalize well and that require little configurability are more likely to see implementation. Favor Code over DSLs \uf0c1 Beancount provides a simple syntax, a parser and printer for it, and a library of very simple data record types to allow a user to easily write their scripts to process their data, taking advantage of the multitude of libraries available from the Python environment. Should the built-in querying tools fail to suffice, I want it to be trivially easy for someone to build what they need on top of Beancount. This also means that the tools provided by Beancount don\u2019t have to support all the features everyone might ever possibly want. Beancount\u2019s strength should be representational coherence and simplicity rather than the richness of its reporting features. Creating new kinds of reports should be easy; changing the internals should be hard. (That said, it does provide an evolving palette of querying utilities that should be sufficient for most users.) In contrast, the implementation of the Ledger system provides high-level operations that are invoked as \"expressions\" defined in a domain-specific language, expressions which are either provided on the command-line or in the input file itself. For the user, this expression language is yet another thing to learn, and it\u2019s yet another thing that might involve limitations requiring expansion and work\u2026 I prefer to avoid DSLs if possible and use Python\u2019s well understood semantics instead, though this is not always sensible. File Format or Input Language? \uf0c1 One may wonder whether Beancount\u2019s input syntax should be a computer language or just a data format. The problem we're trying to solve is essentially that of making it easy for a human being to create a transactions-to-postings-to-accounts-&-positions data structure. What is the difference between a data file format and a simple declarative computer language? One distinction is the intended writer of the file. Is it a human? Or is it a computer? The input files are meant to be manicured by a human, at the very least briefly eyeballed. While we are trying to automate as much of this process as possible via importing code\u2014well, the unpleasant bits, anyway\u2014we do want to ensure that we review all the new transactions that we're adding to the ledger in order to ensure correctness. If the intended writer is a human one could file the input under the computer language rubric (most data formats are designed to be written by computers). We can also judge it by the power of its semantics. Beancount\u2019s language is not one designed to be used to perform computation, that is, you cannot define new \u201cBeancount functions\u201d in it. But it has data types. In this regard, it is more like a file format. Just something to think about, especially in the context of adding new semantics. Grammar via Parser Generator \uf0c1 The grammar of its language should be compatible with the use of commonly used parser generator tools. It should be possible to parse the language with a simple lexer and some grammar input file. Beancount\u2019s original syntax was modeled after the syntax of Ledger. However, in its attempt to make the amount of markup minimal, that syntax is difficult to parse with regular tools. By making simple changes to the grammar, e.g. adding tokens for strings and accounts and commodities, the Beancount v2 reimplementation made it possible to use a flex/bison parser generator. This has important advantages: It makes it really easy to make incremental changes to the grammar. Using a custom parser without a well-defined grammar can make it quite difficult to prototype syntax changes. It makes it much easier to implement a parser in another computer language. I\u2019d like to eventually be able to parse Beancount input files in other languages. The data structures are simple enough that it should be easy to reimplement the core in a different language. It\u2019s just a lot less code to write and maintain. (Eventually I\u2019d like to create a file format to generate parsers in multiple languages from a single input. That will be done later, once all the major features are implemented.) Future Work \uf0c1 Tagged Strings \uf0c1 At the moment, account names, tags, links and commodities are represented as simple Python strings. I\u2019ve tried to keep things simple. At some point I\u2019d like to refine this a bit and create a specialization of strings for each of these types. I\u2019d need to assess the performance impact of doing that beforehand. I\u2019m not entirely sure how it could benefit functionality yet. Errors Cleanup \uf0c1 I\u2019ve been thinking about removing the separate \u201cerrors\u201d list and integrating it into the stream of entries as automatically produced \u201cError\u201d entries. This has the advantage that the errors could be rendered as part of the rest of the stream, but it means we have to process the whole list of entries any time we need to print and find errors (probably not a big deal, given how rarely we output errors). Conclusion \uf0c1 This document is bound to evolve. If you have questions about the internals, please post them to the mailing-list . References \uf0c1 Nothing in Beancount is inspired from the following docs, but you may find them interesting as well: Accounting for Computer Scientists I have been considering the addition of a very constrained form of non-balancing postings that would preserve the property of transactions otherwise being balanced, whereby the extra postings would not be able to interact (or sum with) regular balancing postings. \u21a9 There is an option called operating_currency but it is only used to provide good defaults for building reports, never in the processing of transactions. It is used to tell the reporting code which commodities should be broken out to have their own columns of balances, for example. \u21a9 In previous version of Beancount this was not true, the Posting used to have a \u2018position\u2019 attribute and composed it. I prefer the flattened design, as many of the functions apply to either a Position or a Posting. Think of a Posting as deriving from a Position, though, technically it does not. \u21a9","title":"Beancount Design Doc"},{"location":"beancount_design_doc.html#beancount-design-doc","text":"Martin Blais ( blais@furius.ca ) http://furius.ca/beancount/doc/design-doc A guide for developers to understand Beancount\u2019s internals. I hope for this document to provide a map of the main objects used in the source code to make it easy to write scripts and plugins on top of Beancount or even extend it. Introduction Invariants Isolation of Inputs Order-Independence All Transactions Must Balance Account Have Types Account Lifetimes & Open Directives Supports Dates Only (and No Time) Metadata is for User Data Overview of the Codebase Core Data Structures Number Commodity Amount Lot Position Inventory Account Flag Posting About Tuples & Mutability Summary Directives Common Properties Transactions Flag Payee & Narration Tags Links Postings Balancing Postings Elision of Amounts Stream Processing Stream Invariants Loader & Processing Order Loader Output Parser Implementation Two Stages of Parsing: Incomplete Entries The Printer Uniqueness & Hashing Display Context Realization The Web Interface Reports vs. Web Client-Side JavaScript The Query Interface Design Principles Minimize Configurability Favor Code over DSLs File Format or Input Language? Grammar via Parser Generator Future Work Tagged Strings Errors Cleanup Types Conclusion","title":"Beancount Design Doc"},{"location":"beancount_design_doc.html#introduction","text":"This document describes the principles behind the design of Beancount and a high-level overview of its codebase, data structures, algorithms, implementation and methodology. This is not a user's manual; if you are interested in just using Beancount, see the associated User's Manual and all the other documents available here . However, if you just want to understand more deeply how Beancount works this document should be very helpful. This should also be of interest to developers. This is a place where I wrote about a lot of the ideas behind Beancount that did not find any other venue. Expect some random thoughts that aren\u2019t important for users. Normally one writes a design document before writing the software. I did not do that. But I figured it would still be worthwhile to spell out some of the ideas that led to this design here. I hope someone finds this useful.","title":"Introduction"},{"location":"beancount_design_doc.html#invariants","text":"","title":"Invariants"},{"location":"beancount_design_doc.html#isolation-of-inputs","text":"Beancount should accept input only from the text file you provide to its tools. In particular, it should not contact any external networked service or open any \u201cglobal\u201d files, even just a cache of historical prices or otherwise. This isolation is by design. Isolating the input to a single source makes it easier to debug, reproduce and isolate problems when they occur. This is a nice property for the tool. In addition, fetching and converting external data is very messy. External data formats can be notoriously bad, and they are too numerous to handle all of them (handling just the most common subset would beg the question of where to include the implementation of new converters). Most importantly, the problems of representing the data vs. that of fetching and converting it segment from each other very well naturally: Beancount provides a core which allows you to ingest all the transactional data and derive reports from it, and its syntax is the hinge that connects it to these external repositories of transactions or prices. It isolates itself from the ugly details of external sources of data in this way.","title":"Isolation of Inputs"},{"location":"beancount_design_doc.html#order-independence","text":"Beancount offers a guarantee that the ordering of its directives in an input file is irrelevant to the outcome of its computations. You should be able to organize your input text and reorder any declaration as is most convenient for you, without having to worry about how the software will make its calculations. This also makes it trivial to implement inclusions of multiple files: you can just concatenate the files if you want, and you can process them in any order. Not even directives that declare accounts or commodities are required to appear before these accounts get used in the file. All directives are parsed and then basically sorted before any calculation or validation occurs (a minor detail is that the line number is used as a secondary key in the sorting, so that the re-ordering is stable). Correspondingly, all the non-directive grammar that is in the language is limited to either setting values with global impact (\u201c option \u201d, \u201c plugin \u201d) or syntax-related conveniences (\u201c pushtag \u201d, \u201c poptag \u201d). There is an exception: Options may have effects during the processing of the file and should appear at the beginning. At the moment, Beancount does not enforce this, e.g., by performing multiple passes of parsing, but it probably should. What\u2019s more, options in included files are currently ignored. You should put all your options in the top-level ledger file.","title":"Order-Independence"},{"location":"beancount_design_doc.html#all-transactions-must-balance","text":"All transactions must balance by \u201cweight.\u201d There is no exception. Beancount transactions are required to have balanced, period. In particular, there is no allowance for exceptions such as the \u201cvirtual postings\u201d that can be seen in Ledger. The first implementation of Beancount allowed such postings. As a result, I often used them to \u201cresolve\u201d issues that I did not know how to model well otherwise. When I rewrote Beancount, I set out to convert my entire input file to avoid using these, and over time I succeeded in doing this and learned a lot of new things in the process. I have since become convinced that virtual postings are wholly unnecessary, and that making them available only provides a crutch upon which a lot of users will latch instead of learning to model transfers using balanced transactions. I have yet to come across a sufficiently convincing case for them 1 . This provides the property that any subsets of transactions will sum up to zero. This is a nice property to have, it means we can generate balance sheets at any point in time. Even when we eventually support settlement dates or per-posting dates , we will split up transactions in a way that does not break this invariant.","title":"All Transactions Must Balance"},{"location":"beancount_design_doc.html#accounts-have-types","text":"Beancount accounts should be constrained to be of one of five types: Assets, Liabilities, Income, Expenses and Equity. The rationale behind this is to realize that all matters of counting can be characterized by being either permanent (Assets, Liabilities) or transitory (Income, Expenses), and that the great majority of accounts have a usual balance sign: positive (Assets, Expenses) or negative (Liabilities, Income). Given a quantity to accumulate, we can always select one of these four types of labels for it. Enforcing this makes it possible to implement reports of accumulated values for permanent accounts (balance sheet) and reports of changes of periods of time (income statement). Although it is technically possible to explicitly book postings against Equity accounts the usual purpose of items in that category is to account for past changes on the balance sheet (net/retained income or opening balances). These concepts of time and sign generalize beyond that of traditional accounting. Not having them raises difficult and unnecessary questions about how to handle calculating these types of reports. We simply require that you label your accounts into this model. I\u2019ll admit that this requires a bit of practice and forethought, but the end result is a structure that easily allows us to build commonly expected outputs.","title":"Accounts Have Types"},{"location":"beancount_design_doc.html#account-lifetimes-open-directives","text":"Accounts have lifetimes; an account opens at a particular date and optionally closes at another. This is directed by the Open and Close directives. All accounts are required to have a corresponding Open directive in the stream. By default, this is enforced by spitting out an error when posting to an account whose Open directive hasn\u2019t been seen in the stream. You can automate the generation of these directives by using the \u201cauto_accounts\u201d plugin, but in any case, the stream of entries will always contain the Open directives. This is an assumption that one should be able to rely upon when writing scripts. (Eventually a similar constraint will be applied for Commodity directives so that the stream always includes one before it is used; and they should be auto-generated as well. This is not the case right now [June 2015].)","title":"Account Lifetimes & Open Directives"},{"location":"beancount_design_doc.html#supports-dates-only-and-no-time","text":"Beancount does not represent time, only dates. The minimal time interval is one day. This is for simplicity\u2019s sake. The situations where time would be necessary to disambiguate account balances exist, but in practice they are rare enough that their presence does not justify the added complexity of handling time values and providing syntax to deal with it. If you have a use case whereby times are required, you may use metadata to add it in, or more likely, you should probably write custom software for it. This is outside the scope of Beancount by choice.","title":"Supports Dates Only (and No Time)"},{"location":"beancount_design_doc.html#metadata-is-for-user-data","text":"By default, Beancount will not make assumptions about metadata fields and values. Metadata is reserved for private usage by the user and for plugins. The Beancount core does not read metadata and change its processing based on it. However, plugins may define special metadata values that affect what they produce. That being said, there are a few special metadata fields produced by the Beancount processing: filename: A string, the name of the file the transaction was read from (or the plugin that created it) lineno: An integer, the line number from the file where the transaction was read from. tolerances : A dict of currency to Decimal, the tolerance values used in balancing the transaction. residual (on postings): A boolean, true if the posting has been added to automatically absorb rounding error. There may be a few more produced in the future but in any case, the core should not read any metadata and affect its behavior as a consequence. (I should probably create a central registry or location where all the special values can be found in one place.)","title":"Metadata is for User Data"},{"location":"beancount_design_doc.html#overview-of-the-codebase","text":"All source code lives under a \u201c beancount \u201d Python package. It consists of several packages with well-defined roles, the dependencies between which are enforced strictly. Beancount source code packages and approximate dependencies between them. At the bottom, we have a beancount.core package which contains the data structures used to represent transactions and other directives in memory. This contains code for accounts, amounts, lots, positions, and inventories. It also contains commonly used routines for processing streams of directives. One level up, the beancount.parser package contains code to parse the Beancount language and a printer that can produce the corresponding text given a stream of directives. It should be possible to convert between text and data structure and round-trip this process without loss. At the root of the main package is a beancount.loader module that coordinates everything that has to be done to load a file in memory. This is the main entry point for anyone writing a script for Beancount, it always starts with loading a stream of entries. This calls the parser, runs some processing code, imports the plugins and runs them in order to produce the final stream of directives which is used to produce reports. The beancount.plugins package contains implementations of various plugins. Each of these plugin modules are independent from each other. Consult the docstrings in the source code to figure out what each of these does. Some of these are prototypes of ideas. There is a beancount.ops package which contains some high-level processing code and some of the default code that always runs by default that is implemented as plugins as well (like padding using the Pad directive). This package needs to be shuffled a bit in order to clarify its role. On top of this, reporting code calls modules from those packages. There are four packages which contain reporting code, corresponding to the Beancount reporting tools: beancount.reports : This package contains code specialized to produce different kinds of outputs (invoked by bean-report). In general I\u2019d like to avoid defining custom routines to produce desired outputs and use the SQL query tool to express grouping & aggregation instead, but I think there will always be a need for at least some of these. The reports are defined in a class hierarchy with a common interface and you should be able to extend them. Each report supports some subset of a number of output formats. beancount.query : This package contains the implementation of a query language and interactive shell (invoked by bean-query) that allows you to group and aggregate positions using an SQL-like DSL. This essentially implements processing over an in-memory table of Posting objects, with functions defined over Amount, Position and Inventory objects. beancount.web : This package contains all the source code for the default web interface (invoked by bean-web). This is a simple Bottle application that serves many of the reports from beancount.report to HTML format, running on your machine locally. The web interface provides simple views and access to your data. (It stands to be improved greatly, it\u2019s in no way perfect.) beancount.projects : This package contains various custom scripts for particular applications that I\u2019ve wanted to share and distribute. Wherever possible, I aim to fold these into reports. There are no scripts to invoke these; you should use \u201c python3 -m beancount.projects. \u201d to run them. There is a beancount.scripts package that contains the \u201cmain\u201d programs for all the scripts under the bin directory . Those scripts are simple launchers that import the corresponding file under beancount.scripts. This allows us to keep all the source code under a single directory and that makes it easier to run linters and other code health checkers on the code\u2014it\u2019s all in one place. Finally, there is a beancount.utils package which is there to contain generic reusable random bits of utility code. And there is a relatively unimportant beancount.docs package that contains code used by the author just to produce and maintain this documentation (code that connects to Google Drive). Enforcing the dependency relationships between those packages is done by a custom script .","title":"Overview of the Codebase"},{"location":"beancount_design_doc.html#core-data-structures","text":"This section describes the basic data structures that are used as building blocks to represent directives. Where possible I will describe the data structure in conceptual terms. (Note for Ledger users : This section introduces some terminology for Beancount; look here if you\u2019re interested to contrast it with concepts and terminology found in Ledger.)","title":"Core Data Structures"},{"location":"beancount_design_doc.html#number","text":"Numbers are represented using decimal objects, which are perfectly suited for this. The great majority of numbers entered in accounting systems are naturally decimal numbers and binary representations involve representational errors which cause many problems for display and in precision. Rational numbers avoid this problem, but they do not carry the limited amount of precision that the user intended in the input. We must deal with tolerances explicitly. Therefore, all numbers should use Python\u2019s \u201c decimal.Decimal \u201d objects. Conveniently, Python v3.x supports a C implementation of decimal types natively (in its standard library; this used to be an external \u201ccdecimal\u201d package to install but it has been integrated in the C/Python interpreter). The default constructor for decimal objects does not support some of the input syntax we want to allow, such as commas in the integer part of numbers (e.g., \u201c278 , 401.35 USD\u201d) or initialization from an empty string. These are important cases to handle. So I provide a special constructor to accommodate these inputs: beancount.core.number.D() . This is the only method that should be used to create decimal objects: from beancount.core.number import D \u2026 number = D(\"2,402.21\") I like to import the \u201c D \u201d symbol by itself (and not use number.D ). All the number-related code is located under beancount.core.number . Some number constants have been defined as well: ZERO , ONE , and HALF . Use those instead of explicitly constructed numbers (such as D(\"1\") ) as it makes it easier to grep for such initializations.","title":"Number"},{"location":"beancount_design_doc.html#commodity","text":"A commodity , or currency (I use both names interchangeably in the code and documentation) is a string that represents a kind of \u201cthing\u201d that can be stored in accounts. In the implementation, it is represented as a Python str object (there is no module with currency manipulation code). These strings may only contain capital letters and numbers and some special characters (see the lexer code). Beancount does not predefine any currency names or categories\u2014all currencies are created equal. Really. It genuinely does not know anything special about dollars or euros or yen or anything else. The only place in the source code where there is a reference to those is in the tests. There is no support for syntax using \u201c$\u201d or other currency symbols; I understand that some users may want this syntax, but in the name of keeping the parser very simple and consistent I choose not to provide that option. Moreover, Beancount does not make a distinction between commodities which represent \"money\" and others that do not (such as shares, hours, etc.). These are treated the same throughout the system. It also does not have the concept of a \"home\" currency 2 ; it's a genuinely multi-currency system. Currencies need not be defined explicitly in the input file format; you can just start using them in the file and they will be recognized as such by their unique syntax (the lexer recognizes and emits currency tokens). However, you may declare some using a Commodity directive. This is only provided so that a per-commodity entity exists upon which the user can attach some metadata, and some reports and plugins are able to find and use that metadata.","title":"Commodity"},{"location":"beancount_design_doc.html#account","text":"An account is basically just the name of a bucket associated with a posting and is represented as a simple string (a Python str object). Accounts are detected and tokenized by the lexer and have names with at least two words separated by a colon (\":\"). Accounts don\u2019t have a corresponding object type; we just refer to them by their unique name string (like filenames in Python). When per-account attributes are needed, we can extract the Open directives from the stream of entries and find the one corresponding to the particular account we\u2019re looking for. Similar to Python\u2019s os.path module, there are some routines to manipulate account names in beancount.core.account and the functions are named similarly to those of the os.path module. The first component of an account\u2019s name is limited to one of five types: Assets Liabilities Equity Income Expenses The names of the account types as read in the input syntax may be customized with some \"option\" directives, so that you can change those names to another language, or even just rename \"Income\" to \"Revenue\" if you prefer that. The beancount.core.account_types module contains some helpers to deal with these. Note that the set of account names forms an implicit hierarchy. For example, the names: Assets:US:TDBank:Checking Assets:US:TDBank:Savings implicitly defines a tree of nodes with parent nodes \"Assets\", \"US\", \"TDBank\" with two leaf nodes \"Checking\" and \"Savings\". This implicit tree is never realized during processing, but there is a Python module that allows one to do this easily (see beancount.core.realization ) and create linearized renderings of the tree.","title":"Account"},{"location":"beancount_design_doc.html#flag","text":"A \u201cflag\u201d is a single-character string that may be associated with Transactions and Postings to indicate whether they are assumed to be correct (\"reconciled\") or flagged as suspicious. The typical value used on transaction instances is the character \u201c*\u201d. On Postings, it is usually left absent (and set to a None).","title":"Flag"},{"location":"beancount_design_doc.html#amount","text":"An Amount is the combination of a number and an associated currency, conceptually: Amount = (Number, Currency) Amount instances are used to represent a quantity of a particular currency (the \u201cunits\u201d) and the price on a posting. A class is defined in beancount.core.amount as a simple tuple-like object. Functions exist to perform simple math operations directly on instances of Amount. You can also create Amount instance using amount.from_string() , for example: value = amount.from_string(\"201.32 USD\")","title":"Amount"},{"location":"beancount_design_doc.html#cost","text":"A Cost object represents the cost basis for a particular lot of a commodity. Conceptually, it is Cost = (Number, Currency, Date, Label) The number and currency is that of the cost itself, not of the commodity. For example, if you bought 40 shares of AAPL stock at 56.78 USD, the Number is a \u201c56.78\u201d decimal and the Currency is \u201cUSD.\u201d For example: Cost(Decimal(\"56.78\"), \"USD\", date(2012, 3, 5), \"lot15\") The Date is the acquisition date of the corresponding lot (a datetime.date object). This is automatically attached to the Cost object when a posting augments an inventory\u2014the Transaction\u2019s date is automatically attached to the Cost object\u2014or if the input syntax provides an explicit date override. The Label can be any string. It is provided as a convenience for a user to refer to a particular lot or disambiguate similar lots. On a Cost object, the number, currency and date attributes are always set. If the label is unset, it has a value of \u201cNone.\u201d","title":"Cost"},{"location":"beancount_design_doc.html#costspec","text":"In the input syntax, we allow users to provide as little information as necessary in order to disambiguate between the lots contained in the inventory prior to posting. The data provided filters the set of matching lots to an unambiguous choice, or to a subset from which an automated booking algorithm will apply (e.g., \u201cFIFO\u201d). In addition, we allow the user to provide either the per-unit cost and/or the total-cost. This convenience is useful to let Beancount automatically compute the per-unit cost from a total of proceeds. CostSpec = (Number-per-unit, Number-total, Currency, Date, Label, Merge) Since any of the input elements may be omitted, any of the attributes of a CostSpec may be left to None. If a number is missing and necessary to be filled in, the special value \u201cMISSING\u201d will be set on it. The Merge attribute it used to record a user request to merge all the input lots before applying the transaction and to merge them after. This is the method used for all transactions posted to an account with the \u201cAVERAGE\u201d booking method.","title":"CostSpec"},{"location":"beancount_design_doc.html#position","text":"A position represents some units of a particular commodity held at cost. It consists simply in Position = (Units, Cost) Units is an instance of Amount , and Cost is an instance of Cost , or a null value if the commodity is not held at cost. Inventories contain lists of Position instances. See its definition in beancount.core.position .","title":"Position"},{"location":"beancount_design_doc.html#posting","text":"Each Transaction directive is composed of multiple Postings (I often informally refer to these in the code and on the mailing-list as the \u201clegs\u201d of a transaction). Each of these postings is associated with an account, a position and an optional price and flag: Posting = (Account, Units, Cost-or-CostSpec, Price, Flag, Metadata) As you can see, a Posting embeds its Position instance 3 . The Units is an Amount , and the \u2018cost\u2019 attribute refers to either a Cost or a CostSpec instance (the parser outputs Posting instances with an CostSpec attribute which is resolved to a Cost instance by the booking process; see How Inventories Work for details). The Price is an instance of Amount or a null value. It is used to declare a currency conversion for balancing the transaction, or the current price of a position held at cost. It is the Amount that appears next to a \u201c@\u201d in the input. Flags on postings are relatively rare; users will normally find it sufficient to flag an entire transaction instead of a specific posting. The flag is usually left to None; if set, it is a single-character string. The Posting type is defined in beancount.core.data , along with all the directive types.","title":"Posting"},{"location":"beancount_design_doc.html#inventory","text":"An Inventory is a container for an account\u2019s balance in various lots of commodities. It is essentially a list of Position instances with suitable operations defined on it. Conceptually you may think of it as a mapping with unique keys: Inventory = [Position1, Position2, Position3, \u2026 , PositionN] Generally, the combination of a position\u2019s ( Units.Currency, Cost) is kept unique in the list, like the key of a mapping. Positions for equal values of currency and cost are merged together by summing up their Units.Number and keeping a single position for it. And simple positions are mixed in the list with positions held at cost. The Inventory is one of the most important and oft-used object in Beancount\u2019s implementation, because it is used to sum the balance of one or more accounts over time. It is also the place where the inventory reduction algorithms get applied to, and traces of that mechanism can be found there. The \u201c How Inventories Work \u201d document provides the full detail of that process. For testing, you can create initialized instances of Inventory using inventory.from_string() . All the inventory code is written in beancount.core.inventory .","title":"Inventory"},{"location":"beancount_design_doc.html#about-tuples-mutability","text":"Despite the program being written in a language which does not make mutability \u201cdifficult by default\u201d, I designed the software to avoid mutability in most places. Python provides a \u201c collections.namedtuple \u201d factory that makes up new record types whose attributes cannot be overridden. Well\u2026 this is only partially true: mutable attributes of immutable tuples can be modified. Python does not provide very strong mechanisms to enforce this property. Regardless, functional programming is not so much an attribute of the language used to implement our programs than of the guarantees we build into its structure. A language that supports strong guarantees helps to enforce this. But if, even by just using a set of conventions, we manage to mostly avoid mutability, we have a mostly functional program that avoids most of the pitfalls that occur from unpredictable mutations and our code is that much easier to maintain. Programs with no particular regard for where mutation occurs are most difficult to maintain. By avoiding most of the mutation in a functional approach, we avoid most of those problems. Most of the data structures used in Beancount are namedtuples , which make the modification of their attributes inconvenient on purpose. Most of the code will attempt to avoid mutation, except for local state (within a function) or in a narrow scope that we can easily wrap our head around. Where mutation is possible, by convention I try to avoid it by default. When we do mutation, I try to document the effects. I avoid the creation of classes which mutate their internal state, except for a few cases (e.g. the web application). I prefer functions to objects and where I define classes I mostly avoid inheritance. These properties are especially true for all the small objects: Amount, Lot, Position, Posting objects, and all the directives types from beancount.core.data . On the other hand, the Inventory object is nearly always used as an accumulator and does allow the modification of its internal state (it would require a special, persistent data structure to avoid this). You have to be careful how you share access to Inventory objects\u2026 and modify them, if you ever do. Finally, the loader produces lists of directives which are all simple namedtuple objects. These lists form the main application state. I\u2019ve avoided placing these inside some special container and instead pass them around explicitly, on purpose. Instead of having some sort of big \u201capplication\u201d object, I\u2019ve trimmed down all the fat and all your state is represented in two things: A dated and sorted list of directives which can be the subject of stream processing and a list of constant read-only option values. I think this is simpler. I credit my ability to make wide-ranging changes to a mid-size Python codebase to the adoption of these principles. I would love to have types in order to safeguard against another class of potential errors, and I plan to experiment with Python 3.5\u2019s upcoming typing module .","title":"About Tuples & Mutability"},{"location":"beancount_design_doc.html#summary","text":"The following diagram explains how these objects relate to each other, starting from a Posting. For example, to access the number of units of a postings you could use posting.units.number For the cost currency: posting.cost.currency You can print out the tuples in Python to figure out their structure.","title":"Summary"},{"location":"beancount_design_doc.html#previous-design","text":"For the sake of preservation, if you go back in time in the repository, the structure of postings was deeper and more complex. The new design reflects a flatter and simpler version of it. Here is what the old design used to look like:","title":"Previous Design"},{"location":"beancount_design_doc.html#directives","text":"The main production from loading a Beancount input file is a list of directives . I also call these entries interchangeably in the codebase and in the documents. There are directives of various types: Transaction Balance & Pad Open & Close Commodity Note Event Price Document In a typical Beancount input file, the great majority of entries will be Transactions and some Balance assertions. There will also be Open & perhaps some Close entries. Everything else is optional. Since these combined with a map of option values form the entire application state, you should be able to feed those entries to functions that will produce reports. The system is built around this idea of processing a stream of directives to extract all contents and produce reports, which are essentially different flavors of filtering and aggregations of values attached to this stream of directives.","title":"Directives"},{"location":"beancount_design_doc.html#common-properties","text":"Some properties are in common to all the directives: Date. A datetime.date object. This is useful and consistent. For a Transaction, that\u2019s the date at which it occurs. For an Open or Close directive, that\u2019s the date at which the account was opened or closed. For a Pad directive, that\u2019s the date at which to insert a transaction. For a Note or Document, it is the date at which to insert the note in the stream of postings of that account. For an Event, it is the date at which it occurs. For a Commodity directive which essentially provides a per-commodity hanging point for commodity-related metadata, the date isn\u2019t as meaningful; I choose to date those on the day the commodity was created. Meta . All the directives have metadata attribute (a Python dict object). The purpose of metadata is to allow the user to hang any kind of ancillary data they want and then use this in scripts or queries. Posting instances also have a metadata attribute.","title":"Common Properties"},{"location":"beancount_design_doc.html#transactions","text":"The Transaction directive is the subject of Beancount and is by far the most common directive found in input files and is worth of special attention. The function of a bookkeeping system is to organize the Postings attached to Transactions in various ways. All the other types of entries occupy supporting roles in our system. A Transaction has the following additional fields.","title":"Transactions"},{"location":"beancount_design_doc.html#flag_1","text":"The single-character flag is usually there to replace the \u201ctxn\u201d keyword (Transaction is the only directive which allows being entered without entering its keyword). I\u2019ve been considering changing the syntax definition somewhat to allow not entering the flag nor the keyword, because I\u2019d like to eventually support that. Right now, either the flag or keyword is required. The flag attribute may be set to None.","title":"Flag"},{"location":"beancount_design_doc.html#payee-narration","text":"The narration field is a user-provided description of the transaction, such as \"Dinner with Mary-Ann.\" You can put any information in this string. It shows up in the journal report. Oftentimes it is used to enrich the transaction with context that cannot be imported automatically, such as \"transfer from savings account to pay for car repairs.\" The payee name is optional, and exists to describe the entity with which the transaction is conducted, such as \"Whole Foods Market\" or \"Shell.\" Note that I want to be able to produce reports for all transactions associated with a particular payee, so it would be nice to enter consistent payee names. The problem with this is that the effort to do this right is sometimes too great. Either better tools or looser matching over names is required in order to make this work. The input syntax also allows only a single string to be provided. By default this becomes the narration, but I\u2019ve found that in practice it can be useful to have just the payee. It\u2019s just a matter of convenience. At the moment, if you want to enter just the payee string you need to append an empty narration string. This should be revisited at some point.","title":"Payee & Narration"},{"location":"beancount_design_doc.html#tags","text":"Tags are sets of strings that can be used to group sets of transactions (or set to None if there are no tags). A view of this subset of transactions can then be generated, including all the usual reports (balance sheet, income statement, journals, etc.). You can tag transactions for a variety of purposes; here are some examples: All transactions during a particular travel might be tagged to later summarize your trip expenses. Those transactions will usually occur around the same date and therefore there is convenient syntax used to automatically tag many transactions that are declared together in a file. Transactions related to a particular project to be accomplished. I took some classes in an online program once, and tagged all related expenses to it. I use this to track all my immigration-related expenses for a particular stage (e.g. green card). Declaring a group of expenses to be paid back by an employer (or your own company) later on. Expenses related to moving between locations. Typical tag names that I use for tags look like #trip-israel-2012 , #conference-siggraph , and #course-cfa . In general, tags are useful where adding a sub-accounts won't do. This is often the case where a group of related expenses are of differing types, and so they would not belong to a single account. Given the right support from the query tools, they could eventually be subsumed by metadata\u2014I have been considering converting tags into metadata keys with a boolean value of True, in order to remove unnecessary complexity.","title":"Tags"},{"location":"beancount_design_doc.html#links","text":"Links are a unique set of strings or None , and in practice will be usually empty for most transactions. They differ from tags only in purpose. Links are there to chain together a list of related transactions and there are tools used to list, navigate and balance a subset of transactions linked together. They are a way for a transaction to refer to other transactions. They are not meant to be used for summarizing. Examples include: transaction-ids from trading accounts (these often provide an id to a \"related\" or \"other\" transaction); account entries associated with related corrections, for example a reversed fee following a phone call could be linked to the original invalid fee from several weeks before; the purchase and sale of a home, and related acquisition expenses. In contrast to tags, their strings are most often unique numbers produced by the importers. No views are produced for links; only a journal of a particular links transactions can be produced and a rendered transaction should be accompanied by an actual \"link\" icon you can click on to view all the other related transactions.","title":"Links"},{"location":"beancount_design_doc.html#postings","text":"A list of Postings are attached to the Transaction object. Technically this list object is mutable but in practice we try not to modify it. A Posting can ever only be part of a single Transaction. Sometimes different postings of the same transaction will settle at different dates in their respective accounts, so eventually we may allow a posting to have its own date to override the transaction's date, to be used as documentation; in the simplest version, we enforce all transactions to occur punctually, which is simple to understand and was not found to be a significant problem in practice. Eventually we might implement this by implicitly converting Transactions with multiple dates into multiple Transactions and using some sort of transfer account to hold the pending balance in-between dates. See the associated proposal for details.","title":"Postings"},{"location":"beancount_design_doc.html#balancing-postings","text":"The fundamental principle of double-entry book-keeping is enforced in each of the Transaction entries: the sum of all postings must be zero. This section describes the specific way in which we do this, and is the key piece of logic that allow the entire system to balance nicely. This is also one of the few places in this system where calculations go beyond simple filtering and aggregation. As we saw earlier, postings are associated with A position, which is a number of units of a lot (which itself may or may not have a cost) An optional conversion price Given this, how do we balance a transaction? Some terminology is in order: for this example posting: Assets:Investment:HOOL -50 HOOL {700 USD} @ 920 USD Units. The number of units is the number of the position, or \u201c50\u201d. Currency. The commodity of the lot, that is, \u201cHOOL\u201d. Cost. The cost of this position is the number of units times the per-unit cost in the cost currency, that is 50 x 700 = 35000 USD. (Total) Price. The price of a unit is the attached price amount, 920 USD. The total price of a position is its number of units times the conversion price, in this example 50 x 920 = 46000 USD. Weight. The amount used to balance each posting in the transaction: If the posting has an associated cost, we calculate the cost of the position (regardless of whether a price is present or not); If the posting has no associated cost but has a conversion price, we convert the position into its total price; Finally, if the posting has no associated cost nor conversion price, the number of units of the lot are used directly. Balancing is really simple: each of the Posting's positions are first converted into their weight. These amounts are then grouped together by currency, and the final sum for each currency is asserted to be close to zero, that is, within a small amount of tolerance (as inferred by a combination of by the numbers in the input and the options ). The following example is one of case (2), with a price conversion and a regular leg with neither a cost nor a price (case 3): 2013-07-22 * \"Wired money to foreign account\" Assets:Investment:HOOL -35350 CAD @ 1.01 USD ;; -35000 USD (2) Assets:Investment:Cash 35000 USD ;; 35000 USD (3) ;;------------------ ;; 0 USD In the next example, the posting for the first leg has a cost, the second posting has neither: 2013-07-22 * \"Bought some investment\" Assets:Investment:HOOL 50 HOOL {700 USD} ;; 35000 USD (1) Assets:Investment:Cash -35000 USD ;; -35000 USD (3) ;;------------------ ;; 0 USD Here's a more complex example with both a price and a cost; the price here is ignored for the purpose of balance and is used only to enter a data-point in the internal price database: 2013-07-22 * \"Sold some investment\" Assets:Investment:HOOL -50 HOOL {700 USD} @ 920 USD ;; -35000 USD (1) Assets:Investment:Cash 46000 USD ;; 46000 USD (3) Income:CapitalGains -11000 USD ;; -11000 USD (3) ;;------------------ ;; 0 USD Finally, here's an example with multiple commodities: 2013-07-05 * \"COMPANY INC PAYROLL\" Assets:US:TD:Checking 4585.38 USD Income:US:Company:GroupTermLife -25.38 USD Income:US:Company:Salary -5000.00 USD Assets:US:Vanguard:Cash 540.00 USD Assets:US:Federal:IRAContrib -540.00 IRAUSD Expenses:Taxes:US:Federal:IRAContrib 540.00 IRAUSD Assets:US:Company:Vacation 4.62 VACHR Income:US:Company:Vacation -4.62 VACHR Here there are three groups of weights to balance: USD: (4485.38) + (-25.38) + (-5000) + (540) ~= 0 USD IRAUSD: (540.00) + (-540.00) ~= 0 IRAUSD VACHR: (4.62) + (-4.62) ~= 0 VACHR","title":"Balancing Postings"},{"location":"beancount_design_doc.html#elision-of-amounts","text":"Transactions allow for at most one posting to be elided and automatically set; if an amount was elided, the final balance of all the other postings is attributed to the balance. With the inventory booking proposal , it should be extended to be able to handle more cases of elision. An interesting idea would be to allow the elided postings to at least specify the currency they're using. This would allow the user to elide multiple postings, like this: 2013-07-05 * \"COMPANY INC PAYROLL\" Assets:US:TD:Checking USD Income:US:Company:GroupTermLife -25.38 USD Income:US:Company:Salary -5000.00 USD Assets:US:Vanguard:Cash 540.00 USD Assets:US:Federal:IRAContrib -540.00 IRAUSD Expenses:Taxes:US:Federal:IRAContrib IRAUSD Assets:US:Company:Vacation 4.62 VACHR Income:US:Company:Vacation VACHR","title":"Elision of Amounts"},{"location":"beancount_design_doc.html#stream-processing","text":"An important by-product of representing all state using a single stream of directives is that most of the operations in Beancount can be implemented by simple functions that accept the list of directives as input and output a modified list of directives. For example, The \u201cPad\u201d directive is implemented by processing the stream of transactions, accumulating balances, inserting a new padding transaction after the Pad directive when a balance assertion fails. Summarization operations (open books, close books, clear net income to equity) are all implemented this way, as functional operators on the list of streams. For example, opening the books at a particular date is implemented by summing all balances before a certain date and replacing those transactions by new transactions that pull the final balance from an Equity account. Closing the books only involves moving the balances of income statement account to Equity and truncation the list of transactions to that date. This is very elegant\u2014the reporting code can be oblivious to the fact that summarization has occurred. Prices on postings held with a cost are normally ignored. The \u201cimplicit prices\u201d option is implemented using a plugin which works its magic by processing the stream of operations and inserting otherwise banal Price directives automatically when such a posting is found. Many types of verifications are also implemented this way; the sellgains plugin verifies that non-income balances check against the converted price of postings disregarding the cost. This one does not insert any new directives, but may generate new errors. These are just some examples. I\u2019m aiming to make most operations work this way. This design has proved to be elegant, but also surprisingly flexible and it has given birth to the plugins system available in Beancount; you might be surprised to learn that I had not originally thought to provide a plugins system... it just emerged over time teasing abstractions and trying to avoid state in my program. I am considering experimenting with weaving the errors within the stream of directives by providing a new \u201cError\u201d directive that could be inserted by stream processing functions. I\u2019m not sure whether this would make anything simpler yet, it\u2019s just an idea at this point [July 2015].","title":"Stream Processing"},{"location":"beancount_design_doc.html#stream-invariants","text":"The stream of directives comes with a few guarantees you can rest upon: All the directives are ordered by date. Because many dates have multiple directives, in order to have a stable sorting order the line number in the file is used as a secondary sort key. All uses of an account is preceded in the stream by a corresponding Open entry. If the input did not provide one, an Open directive is automatically synthesized. All Balance directives precede any other directive on a particular day. This is enforced in order to make the processing of balance assertions straightforward and to emphasize that their semantics is to occur at the beginning of the day.","title":"Stream Invariants"},{"location":"beancount_design_doc.html#loader-processing-order","text":"The process of loading a list of entries from an input file is the heart of the project. It is important to understand it in order to understand how Beancount works. Refer to the diagram below. It consists of A parsing step that reads in all the input files and produces a stream of potentially incomplete directives. The processing of this stream of entries by built-in and user-specified plugins. This step potentially produces new error objects. A final validation step that ensures the invariants have not been broken by the plugins. The beancount.loader module orchestrates this processing. In the code and documents, I\u2019m careful to distinguish between the terms \u201cparsing\u201d and \u201cloading\u201d carefully. These two concepts are distinct. \u201cParsing\u201d is only a part of \u201cloading.\u201d The user-specified plugins are run in the order they are provided in the input files. Raw mode. Some users have expressed a desire for more control over which built-in plugins are run and in which order they are to be run, and so enabling the \u201craw\u201d option disables the automatic loading of all built-in plugins (you have to replace those with explicit \u201cplugin\u201d directives if you want to do that). I\u2019m not sure if this is going to be useful yet, but it allows for tighter control over the loading process for experimentation.","title":"Loader & Processing Order"},{"location":"beancount_design_doc.html#loader-output","text":"The parser and the loader produce three lists: entries: A list of directive tuples from beancount.core.data. This is the stream of data which should consists mostly of Transaction and Balance instances. As much as possible, all data transformations are performed by inserting or removing entries from this list. Throughout the code and documents, I refer to these as entries or directives interchangeably. errors: A list of error objects. In Beancount I don\u2019t use exceptions to report errors; rather, all the functions produce error objects and they are displayed at the most appropriate time. (These deserve a base common type but for now the convention they respect is that they all provide a source (filename, line-no), message and entry attributes.) options_map: A dict of the options provided in the file and derived during parsing. Though this is a mutable object, we never modify it once produced by the parser.","title":"Loader Output"},{"location":"beancount_design_doc.html#parser-implementation","text":"The Beancount parser is a mix of C and Python 3 code. This is the reason you need to compile anything in the first place. The parser is implemented in C using a flex tokenizer and a Bison parser generator, mainly for performance reasons. I chose the C language and these crufty old GNU tools because they are portable and will work everywhere. There are better parser generators out there, but they would either require my users to install more compiler tools or exotic languages. The C language is a great common denominator. I want Beancount to work everywhere and to be easy to install. I want this to make it easy on others, but I also want this for myself, because at some point I\u2019ll be working on other projects and the less dependencies I have the lighter it will be to maintain aging software. Just looking ahead. (That\u2019s also why I chose to use Python instead of some of the other languages I\u2019m more naturally attracted to these days (Clojure, Go, ML); I need Beancount to work... when I\u2019m counting the beans I don\u2019t have time to debug problems. Python is careful about its changes and it will be around for a long long time as is.) There is a lexer file lexer.l written in flex and a Bison grammar in grammar.y. These tools are used to generate the corresponding C source code (lexer.h/.c and grammar.h/.c). These, along with some hand-written C code that defines some interface functions for the module (parser.h/.c), are compiled into an extension module (_parser.so). Eventually we could consider creating a small dependency rule in setup.py to invoke flex and Bison automatically but at the moment, in order to minimize the installation burden, I check the generated source code in the repository (lexer.h/c and grammar.h/c). The interaction between the Python and C code works like this: You import beancount.parser.parser and invoke either parse_file() or parse_string(). This uses code in grammar.py to create a Builder object which essentially provides callbacks in Python to process grammar rules. parser.py calls a C function in the extension module with the Builder object. That C code sets up flex to be able to read the input file or string then hands over control to the parser by calling the generated C function \u201cyyparse()\u201d. yyparse() is the parser and will fetch input tokens from the lexer C code using successive calls to \u201cyylex()\u201d and attempt to reduce rules. When a rule or token is successfully reduced, the C code invokes callback methods on the Builder object you provided. The parser\u2019s rules communicate between each other by passing around \u201cPyObject*\u201d instances, so that code has to be a little careful about correctly handling reference counting. This allows me to run Python code on rule reduction, which makes it really easy to customize parser behaviour yet maintain the performance of the grammar rule processing in C. Note that the grammar.py Builder derives from a similar Builder class defined in lexer.py, so that lexer tokens recognized calls methods defined in the lexer.py file to create them and parser rule correspondingly calls methods from grammar.py\u2019s Builder. This isolation is not only clean, it also makes it possible to just use the lexer to tokenize a file from Python (without parsing) for testing the tokenizer; see the tests where this is used if you\u2019re curious how this works. I just came up with this; I haven\u2019t seen this done anywhere else. I tried it and the faster parser made a huge difference compared to using PLY so I stuck with that. I quite like the pattern, it\u2019s a good compromise that offers the flexibility of a parser generator yet allows me to handle the rules using Python. Eventually I\u2019d like to move just some of the most important callbacks to C code in order to make this a great deal faster (I haven\u2019t done any real performance optimizations yet). This has worked well so far but for one thing: There are several limitations inherent in the flex tokenizer that have proved problematic. In particular, in order to recognize transaction postings using indent I have had to tokenize the whitespace that occurs at the beginning of a line. Also, single-characters in the input should be parsed as flags and at the moment this is limited to a small subset of characters. I\u2019d like to eventually write a custom lexer with some lookahead capabilities to better deal with these problems (this is easy to do).","title":"Parser Implementation"},{"location":"beancount_design_doc.html#two-stages-of-parsing-incomplete-entries","text":"At the moment, the parser produces transactions which may or may not balance. The validation stage which runs after the plugins carries out the balance assertions. This degree of freedom is allowed to provide the opportunity for users to enter incomplete lists of postings and for corresponding plugins to automate some data entry and insert completing postings. However, the postings on the transactions are always complete objects, with all their expected attributes set. For example, a posting which has its amount elided in the input syntax will have a filled in position by the time it comes out of the parser. We will have to loosen this property when we implement the inventory booking proposal , because we want to support the elision of numbers whose values depend on the accumulated state of previous transactions. Here is an obvious example. A posting like this 2015-04-03 * \"Sell stock\" Assets:Investments:AAPL -10 AAPL {} ... should succeed if at the beginning of April 3rd the account contains exactly 10 units of AAPL, in either a single or multiple lots. There is no ambiguity: \u201csell all the AAPL,\u201d this says. However, the fact of its unambiguity assumes that we\u2019ve computed the inventory balance of that account up until April 3rd\u2026 So the parser will need to be split into two phases: A simple parsing step that produces potentially incomplete entries with missing amounts A separate step for the interpolation which will have available the inventory balances of each account as inputs. This second step is where the booking algorithms (e.g., FIFO) will be invoked from. See the diagram above for reference. Once implemented, everything else should be the same.","title":"Two Stages of Parsing: Incomplete Entries"},{"location":"beancount_design_doc.html#the-printer","text":"In the same package as the parser lives a printer. This isolates all the functionality that deals with the Beancount \u201clanguage\u201d in the beancount.parser package: the parser converts from input syntax to data structure and the printer does the reverse. No code outside this package should concern itself with the Beancount syntax. At some point I decided to make sure that the printer was able to round-trip through the parser, that is, given a stream of entries produced by the loader, you should be able to convert those to text input and parse them back in and the resulting set of entries should be the same (outputting the re-read input back to text should produce the same text), e.g., Note that the reverse isn\u2019t necessarily true: reading an input file and processing it through the loader potentially synthesizes a lot of entries (thanks to the plugins), so printing it back may not produce the same input file (even ignoring reordering and white space concerns). This is a nice property to have. Among other things, it allows us to use the parser to easily create tests with expected outputs. While there is a test that attempts to protect this property, some of the recent changes break it partially: Metadata round-tripping hasn\u2019t been tested super well. In particular, some of the metadata fields need to be ignored (e.g., filename and lineno). Some directives contain derived data, e.g. the Balance directive has a \u201cdiff_amount\u201d field that contains the difference when there is a failure in asserting a balance. This is used to report errors more easily. I probably need to remove these exceptions at some point because it is the only one of its kind (I could replace it with an inserted \u201cError\u201d directive). This probably needs to get refined a bit at some point with a more complete test (it\u2019s not far as it is).","title":"The Printer"},{"location":"beancount_design_doc.html#uniqueness-hashing","text":"In order to make it possible to compare directives quickly, we support unique hashing of all directives, that is, from each directive we should be able to produce a short and unique id. We can then use these ids to perform set inclusion/exclusion/comparison tests for our unit tests. We provide a base test case class with assertion methods that use this capability . This feature is used liberally throughout our test suites. This is also used to detect and remove duplicates. This feature is optional and enabled by the beancount.plugins.noduplicates plugin. Note that the hashing of directives currently does not include user meta-data .","title":"Uniqueness & Hashing"},{"location":"beancount_design_doc.html#display-context","text":"Number are input with different numbers of fractional digits: 500 -> 0 fractional digits 520.23 -> 2 fractional digits 1.2357 -> 4 fractional digits 31.462 -> 3 fractional digits Often, a number used for the same currency is input with a different number of digits. Given this variation in the input, a question that arises is how to render the numbers consistently in reports? Consider that: We would like that the user not to have to specify the number of fractional digits to use for rendering by default. Rendering numbers may differ depending on the context: most of the time we want to render numbers using their most commonly seen number of digits, rounding if necessary, but when we are rendering conversion rates we would like to render numbers using the maximum number of digits ever seen. The number of digits used tends to vary with the commodity they represent. For example, US dollars will usually render with two digits of precision; the number of units of a mutual fund such as RGAGX might be recorded by your broker with 3 digits of precision. Japanese Yen will usually be specified in integer units. We want to render text report which need to align numbers with varying number of fractional digits together, aligning them by their period or rightmost digits, and possibly rendering a currency thereafter. In order to deal with this thorny problem, I built a kind of accumulator which is used to record all numbers seen from some input and tally statistics about the precisions witnessed for each currency. I call this a DisplayContext . From this object one can request to build a DisplayFormatter object which can be used to render numbers in a particular way. I refer to those objects using the variable names dcontext and dformat throughout the code. The parser automatically creates a DisplayContext object and feeds it all the numbers it sees during parsing. The object is available in the options_map produced by the loader.","title":"Display Context"},{"location":"beancount_design_doc.html#realization","text":"It occurs often that one needs to compute the final balances of a list of filtered transactions, and to report on them in a hierarchical account structure. See diagram below.","title":"Realization"},{"location":"beancount_design_doc.html#_1","text":"For the purpose of creating hierarchical renderings, I provide a process called a \u201c realization .\u201d A realization is a tree of nodes, each of which contains An account name, A list of postings and other entries to that account, and The final balance for that account, A mapping of sub-account names to child nodes. The nodes are of type \u201cRealAccount,\u201d which is also a dict of its children. All variables of type RealAccount in the codebase are by convention prefixed by \u201creal_*\u201d. The realization module provides functions to iterate and produce hierarchical reports on these trees of balances, and the reporting routines all use these. For example, here is a bit of code that will dump a tree of balances of your accounts to the console: import sys from beancount import loader from beancount.core import realization entries, errors, options_map = loader.load_file(\"filename.beancount\") real_root = realization.realize(entries) realization.dump_balances(real_root, file=sys.stdout)","title":""},{"location":"beancount_design_doc.html#the-web-interface","text":"Before the appearance of the SQL syntax to filter and aggregate postings, the only report produced by Beancount was the web interface provided by bean-web. As such, bean-web evolved to be good enough for most usage and general reports.","title":"The Web Interface"},{"location":"beancount_design_doc.html#reports-vs-web","text":"One of the important characteristics of bean-web is that it should just be a thin dispatching shell that serves reports generated from the beancount.reports layer. It used to contain the report rendering code itself, but at some point I began to factor out all the reporting code to a separate package in order to produce reports to other formats, such as text reports and output to CSV. This is mostly finished, but at this time [July 2015] some reports remain that only support HTML output. This is why.","title":"Reports vs. Web"},{"location":"beancount_design_doc.html#client-side-javascript","text":"I would like to eventually include a lot more client-side scripting in the web interface. However, I don\u2019t think I\u2019ll be able to work on this for a while, at least not until all the proposals for changes to the core are complete (e.g., inventory booking improvements, settlement splitting and merging, etc.). If you\u2019d like to contribute to Beancount, improving bean-web or creating your own visualizations would be a great venue.","title":"Client-Side JavaScript"},{"location":"beancount_design_doc.html#the-query-interface","text":"The current query interface is the result of a prototype . It has not yet been subjected to the amount of testing and refinement as the rest of the codebase. I\u2019ve been experimenting with it and have a lot of ideas for improving the SQL language and the kinds of outputs that can be produced from it. I think of it as \u201c70% done\u201d. However, it works, and though some of the reports can be a little clunky to specify, it produces useful results. It will be the subject of a complete rewrite at some point and when I do, I will keep the current implementation for a little while so that existing scripts don\u2019t just break; I\u2019ll implement a v2 of the shell.","title":"The Query Interface"},{"location":"beancount_design_doc.html#design-principles","text":"","title":"Design Principles"},{"location":"beancount_design_doc.html#minimize-configurability","text":"First, Beancount should have as few options as possible. Second, command-line programs should have no options that pertain to the processing and semantic of the input file (options that affect the output or that pertain to the script itself are fine). The goal is to avoid feature creep. Just a few options opens the possibility of them interacting in complex ways and raises a lot of questions. In this project, instead of providing options to customize every possible thing, I choose to bias the design towards providing the best behavior by default, even if that means less behavior. This is the Apple approach to design. I don\u2019t usually favor this approach for my projects, but I chose it for this particular one. What I\u2019ve learned in the process is how far you can get and still provide much of the functionality you originally set out for. Too many command-line options makes a program difficult to use. I think Ledger suffers from this, for instance. I can never remember options that I don\u2019t use regularly, so I prefer to just design without them. Options that affect semantics should live in the file itself (and should be minimized as well). Options provided when running a process should be only to affect this process. Having less options also makes the software much easier to refactor. The main obstacle to evolving a library of software is the number of complex interactions between the codes. Guaranteeing a well-defined set of invariants makes it much easier to later on split up functionality or group it differently. So.. by default I will resist making changes that aren't generic or that would not work for most users. On the other hand, large-scale changes that would generalize well and that require little configurability are more likely to see implementation.","title":"Minimize Configurability"},{"location":"beancount_design_doc.html#favor-code-over-dsls","text":"Beancount provides a simple syntax, a parser and printer for it, and a library of very simple data record types to allow a user to easily write their scripts to process their data, taking advantage of the multitude of libraries available from the Python environment. Should the built-in querying tools fail to suffice, I want it to be trivially easy for someone to build what they need on top of Beancount. This also means that the tools provided by Beancount don\u2019t have to support all the features everyone might ever possibly want. Beancount\u2019s strength should be representational coherence and simplicity rather than the richness of its reporting features. Creating new kinds of reports should be easy; changing the internals should be hard. (That said, it does provide an evolving palette of querying utilities that should be sufficient for most users.) In contrast, the implementation of the Ledger system provides high-level operations that are invoked as \"expressions\" defined in a domain-specific language, expressions which are either provided on the command-line or in the input file itself. For the user, this expression language is yet another thing to learn, and it\u2019s yet another thing that might involve limitations requiring expansion and work\u2026 I prefer to avoid DSLs if possible and use Python\u2019s well understood semantics instead, though this is not always sensible.","title":"Favor Code over DSLs"},{"location":"beancount_design_doc.html#file-format-or-input-language","text":"One may wonder whether Beancount\u2019s input syntax should be a computer language or just a data format. The problem we're trying to solve is essentially that of making it easy for a human being to create a transactions-to-postings-to-accounts-&-positions data structure. What is the difference between a data file format and a simple declarative computer language? One distinction is the intended writer of the file. Is it a human? Or is it a computer? The input files are meant to be manicured by a human, at the very least briefly eyeballed. While we are trying to automate as much of this process as possible via importing code\u2014well, the unpleasant bits, anyway\u2014we do want to ensure that we review all the new transactions that we're adding to the ledger in order to ensure correctness. If the intended writer is a human one could file the input under the computer language rubric (most data formats are designed to be written by computers). We can also judge it by the power of its semantics. Beancount\u2019s language is not one designed to be used to perform computation, that is, you cannot define new \u201cBeancount functions\u201d in it. But it has data types. In this regard, it is more like a file format. Just something to think about, especially in the context of adding new semantics.","title":"File Format or Input Language?"},{"location":"beancount_design_doc.html#grammar-via-parser-generator","text":"The grammar of its language should be compatible with the use of commonly used parser generator tools. It should be possible to parse the language with a simple lexer and some grammar input file. Beancount\u2019s original syntax was modeled after the syntax of Ledger. However, in its attempt to make the amount of markup minimal, that syntax is difficult to parse with regular tools. By making simple changes to the grammar, e.g. adding tokens for strings and accounts and commodities, the Beancount v2 reimplementation made it possible to use a flex/bison parser generator. This has important advantages: It makes it really easy to make incremental changes to the grammar. Using a custom parser without a well-defined grammar can make it quite difficult to prototype syntax changes. It makes it much easier to implement a parser in another computer language. I\u2019d like to eventually be able to parse Beancount input files in other languages. The data structures are simple enough that it should be easy to reimplement the core in a different language. It\u2019s just a lot less code to write and maintain. (Eventually I\u2019d like to create a file format to generate parsers in multiple languages from a single input. That will be done later, once all the major features are implemented.)","title":"Grammar via Parser Generator"},{"location":"beancount_design_doc.html#future-work","text":"","title":"Future Work"},{"location":"beancount_design_doc.html#tagged-strings","text":"At the moment, account names, tags, links and commodities are represented as simple Python strings. I\u2019ve tried to keep things simple. At some point I\u2019d like to refine this a bit and create a specialization of strings for each of these types. I\u2019d need to assess the performance impact of doing that beforehand. I\u2019m not entirely sure how it could benefit functionality yet.","title":"Tagged Strings"},{"location":"beancount_design_doc.html#errors-cleanup","text":"I\u2019ve been thinking about removing the separate \u201cerrors\u201d list and integrating it into the stream of entries as automatically produced \u201cError\u201d entries. This has the advantage that the errors could be rendered as part of the rest of the stream, but it means we have to process the whole list of entries any time we need to print and find errors (probably not a big deal, given how rarely we output errors).","title":"Errors Cleanup"},{"location":"beancount_design_doc.html#conclusion","text":"This document is bound to evolve. If you have questions about the internals, please post them to the mailing-list .","title":"Conclusion"},{"location":"beancount_design_doc.html#references","text":"Nothing in Beancount is inspired from the following docs, but you may find them interesting as well: Accounting for Computer Scientists I have been considering the addition of a very constrained form of non-balancing postings that would preserve the property of transactions otherwise being balanced, whereby the extra postings would not be able to interact (or sum with) regular balancing postings. \u21a9 There is an option called operating_currency but it is only used to provide good defaults for building reports, never in the processing of transactions. It is used to tell the reporting code which commodities should be broken out to have their own columns of balances, for example. \u21a9 In previous version of Beancount this was not true, the Posting used to have a \u2018position\u2019 attribute and composed it. I prefer the flattened design, as many of the functions apply to either a Position or a Posting. Think of a Posting as deriving from a Position, though, technically it does not. \u21a9","title":"References"},{"location":"beancount_history_and_credits.html","text":"Beancount History and Credits \uf0c1 Martin Blais , July 2014 http://furius.ca/beancount/doc/history A history of the development of Beancount and credits for contributors.. History of Beancount \uf0c1 John Wiegley's Ledger was the inspiration for the first version of Beancount. His system is where much of the original ideas for this system came from. When I first learned about double-entry bookkeeping and realized that it could be the perfect method to solve many of the tracking problems I was having in counting various installments for my company, and after a quick disappointment in the solutions that were available at the time (including GnuCash, which I could break very easily), I was quickly led to the Ledger website. There, John laid out his vision for a text-based system, in particular, the idea of doing away with credits and debits and just the signs, and the basics of a convenient input syntax which allows you to omit the amount of one of the postings. I got really excited and had various enthusiastic discussions with him about Ledger and how I wanted to use it. There was some cross-pollination of ideas and John was very receptive to proposals for adding new features. I was so intensely curious about bookkeeping that I began writing a Python interface to Ledger. But in the end I found myself rewriting the entire thing in Python\u2013not for dislike of Ledger but rather because it was simple enough that I could do most of it in little time, and immediately add some features I thought would be useful. One reason for doing so was that instead of parsing the input file every time and generating one report to the console, I would parse it once and then serve the various reports from the in-memory database of transactions, requested via a web page. Therefore, I did not need processing speed, so having to use C++ for performance reasons was not necessary anymore, and I chose to just stick with a dynamic language, which allowed me to add many features quickly. This became Beancount version 1, which stood on its own and evolved its own set of experimental features. My dream was to be able to quickly and easily manipulate these transaction objects to get various views and breakdowns of the data. I don't think the first implementation pushed the limits far enough, however; the code was substandard, to be honest\u2014I wrote it really quickly\u2014and making modifications to the system was awkward. In particular, the way I originally implemented the tracking of capital gains was inelegant and involved some manual counting. I was unhappy with this, but it worked. It was also using ugly ad-hoc parser code in order to remain reasonably compatible with Ledger syntax\u2014I thought it would be interesting to be able to share some common input syntax and use either system for validation and maybe even to convert between them\u2013and that made me wary of making modifications to the syntax to evolve new features, so it stabilized for a few years and I lost interest in adding new features. But it was correct and it worked, mostly, so I used the system continuously from 2008 to 2012 to manage my own personal finances, my company's finances, and joint property with my wife, with detailed transactions; this was great. I learned a lot about how to keep books during that time (the cookbook document is meant to help you do the same). In the summer of 2013, I had an epiphany and realized a correct and generalizable way to implement capital gains, how to merge the tracking of positions held at a cost and regular positions, and a set of simple rules for carrying out operations on them sensibly (the design of how inventories work). I also saw a better way to factor out the internal data structures, and decided to break from the constraint of compatibility with Ledger and redesign the input syntax in order to parse the input language using a lex/yacc generator, which would allow me to easily evolve the input syntax without having to deal with parsing issues, and to create ports to other languages more easily. In the process, a much simpler and more consistent syntax emerged, and in a fit of sweat and a few intense weekends I re-implemented the entire thing from scratch, without even looking at the previous version, clean-room. Beancount version 2 was born, much better than the last, modular, and easy to extend with plugins. The result is what I think is an elegant design involving a small set of objects, a design that could easily be a basis for reimplementation in other computer languages. This is described in the accompanying design doc, for those who would have an interest in having a go at it (this would be welcome and I'm expecting this will happen). While Ledger remains an interesting project bubbling with ideas for expressing the problem of bookkeeping, the second version of Beancount proposes a simpler design that leaves out features that are not strictly necessary and aims at maximum usability through a simple web interface and a very small set of command-line options. Since I had more than 5 years worth of real-world usage experience with the first version, I set a goal for myself to remove all the features that I thought weren't actually useful and introduced unnecessary complexity (like virtual transactions, allowing accounts not in the five types, etc.), and to simplify the system as much as possible without compromising on its usability. The resulting language and software were much simpler to use and understand, the resulting data structures are much simpler to use, the processing more \u201cfunctional\u201d in nature, and the internals of Beancount are very modular. I converted all my 6 years worth of input data\u2013thanks to some very simple Python scripts to manipulate the file\u2013and began using the new version exclusively. It is now in a fully functional state. Ledger's syntax implements many features that trigger a lot of implicitly-defined behaviour; I find this confusing and some of the reasons for this are documented in the many improvement proposals. I don\u2019t like command-line options. In contrast, Beancount's design provides a less expressive, lower-level syntax but one that closely matches the generated in-memory data structure, and that is hopefully more explicit in that way. I think both projects have strengths and weaknesses. Despite its popularity, the latest version of Ledger remains with a number of shortcomings in my view. In particular, reductions in positions are not booked at the moment they occur, but rather they appear to simply accumulate and get matched only at display time, using different methods depending on the command-line options. Therefore, it is possible in Ledger to hold positive and negative lots of the same commodity in an account simultaneously. I believe that this can lead to confusing and even incorrect accounting for trading lots. I think this is still being figured out by the Ledger look-alikes community and that they will eventually converge to the same solution I have, or perhaps even figure out a better solution. In the redesign, I separated out configuration directives that I had used for importing and over many iterations eventually figured out an elegant way to mostly automate imports and automatically detect the various input files and convert them into my input syntax. The result is the LedgerHub design doc . LedgerHub is currently implemented and I\u2019m using it exclusively, but has insufficient testing for me to stamp a public release [July 2014]. You are welcome to try it out for yourself. In June and July 2014, I decided to dump seven years\u2019 worth of thinking about command-line accounting in a set of Google Docs and this now forms the basis for the current documentation of Beancount. I hope to be evolving it gradually from here on. Chronology \uf0c1 Ledger was begun in August 2003 http://web.archive.org/web/*/http://www.newartisans.com/ledger/ Beancount was begun in 2008 http://web.archive.org/web/*/furius.ca/beancount HLedger was also begun in 2008 https://github.com/simonmichael/hledger/graphs/contributors?from=2007-01-21&to=2014-09-08&type=c Credits \uf0c1 So far I\u2019ve been contributing all the code to Beancount. Some users have made significant contributions in other ways: Daniel Clemente has been reporting all the issues he came across while using Beancount to manage his company and personal finances. His relentless perseverance and attention to detail has helped me put a focus on fixing the rough corners of Beancount that I knew to avoid myself. After many years of prodding, my old friend Filippo Tampieri has finally decided to convert his trading history in Beancount format. He has contributed a number of sophisticated reviews of my documentation and is working on adding various methods for evaluating returns on assets.","title":"Beancount History and Credits"},{"location":"beancount_history_and_credits.html#beancount-history-and-credits","text":"Martin Blais , July 2014 http://furius.ca/beancount/doc/history A history of the development of Beancount and credits for contributors..","title":"Beancount History and Credits"},{"location":"beancount_history_and_credits.html#history-of-beancount","text":"John Wiegley's Ledger was the inspiration for the first version of Beancount. His system is where much of the original ideas for this system came from. When I first learned about double-entry bookkeeping and realized that it could be the perfect method to solve many of the tracking problems I was having in counting various installments for my company, and after a quick disappointment in the solutions that were available at the time (including GnuCash, which I could break very easily), I was quickly led to the Ledger website. There, John laid out his vision for a text-based system, in particular, the idea of doing away with credits and debits and just the signs, and the basics of a convenient input syntax which allows you to omit the amount of one of the postings. I got really excited and had various enthusiastic discussions with him about Ledger and how I wanted to use it. There was some cross-pollination of ideas and John was very receptive to proposals for adding new features. I was so intensely curious about bookkeeping that I began writing a Python interface to Ledger. But in the end I found myself rewriting the entire thing in Python\u2013not for dislike of Ledger but rather because it was simple enough that I could do most of it in little time, and immediately add some features I thought would be useful. One reason for doing so was that instead of parsing the input file every time and generating one report to the console, I would parse it once and then serve the various reports from the in-memory database of transactions, requested via a web page. Therefore, I did not need processing speed, so having to use C++ for performance reasons was not necessary anymore, and I chose to just stick with a dynamic language, which allowed me to add many features quickly. This became Beancount version 1, which stood on its own and evolved its own set of experimental features. My dream was to be able to quickly and easily manipulate these transaction objects to get various views and breakdowns of the data. I don't think the first implementation pushed the limits far enough, however; the code was substandard, to be honest\u2014I wrote it really quickly\u2014and making modifications to the system was awkward. In particular, the way I originally implemented the tracking of capital gains was inelegant and involved some manual counting. I was unhappy with this, but it worked. It was also using ugly ad-hoc parser code in order to remain reasonably compatible with Ledger syntax\u2014I thought it would be interesting to be able to share some common input syntax and use either system for validation and maybe even to convert between them\u2013and that made me wary of making modifications to the syntax to evolve new features, so it stabilized for a few years and I lost interest in adding new features. But it was correct and it worked, mostly, so I used the system continuously from 2008 to 2012 to manage my own personal finances, my company's finances, and joint property with my wife, with detailed transactions; this was great. I learned a lot about how to keep books during that time (the cookbook document is meant to help you do the same). In the summer of 2013, I had an epiphany and realized a correct and generalizable way to implement capital gains, how to merge the tracking of positions held at a cost and regular positions, and a set of simple rules for carrying out operations on them sensibly (the design of how inventories work). I also saw a better way to factor out the internal data structures, and decided to break from the constraint of compatibility with Ledger and redesign the input syntax in order to parse the input language using a lex/yacc generator, which would allow me to easily evolve the input syntax without having to deal with parsing issues, and to create ports to other languages more easily. In the process, a much simpler and more consistent syntax emerged, and in a fit of sweat and a few intense weekends I re-implemented the entire thing from scratch, without even looking at the previous version, clean-room. Beancount version 2 was born, much better than the last, modular, and easy to extend with plugins. The result is what I think is an elegant design involving a small set of objects, a design that could easily be a basis for reimplementation in other computer languages. This is described in the accompanying design doc, for those who would have an interest in having a go at it (this would be welcome and I'm expecting this will happen). While Ledger remains an interesting project bubbling with ideas for expressing the problem of bookkeeping, the second version of Beancount proposes a simpler design that leaves out features that are not strictly necessary and aims at maximum usability through a simple web interface and a very small set of command-line options. Since I had more than 5 years worth of real-world usage experience with the first version, I set a goal for myself to remove all the features that I thought weren't actually useful and introduced unnecessary complexity (like virtual transactions, allowing accounts not in the five types, etc.), and to simplify the system as much as possible without compromising on its usability. The resulting language and software were much simpler to use and understand, the resulting data structures are much simpler to use, the processing more \u201cfunctional\u201d in nature, and the internals of Beancount are very modular. I converted all my 6 years worth of input data\u2013thanks to some very simple Python scripts to manipulate the file\u2013and began using the new version exclusively. It is now in a fully functional state. Ledger's syntax implements many features that trigger a lot of implicitly-defined behaviour; I find this confusing and some of the reasons for this are documented in the many improvement proposals. I don\u2019t like command-line options. In contrast, Beancount's design provides a less expressive, lower-level syntax but one that closely matches the generated in-memory data structure, and that is hopefully more explicit in that way. I think both projects have strengths and weaknesses. Despite its popularity, the latest version of Ledger remains with a number of shortcomings in my view. In particular, reductions in positions are not booked at the moment they occur, but rather they appear to simply accumulate and get matched only at display time, using different methods depending on the command-line options. Therefore, it is possible in Ledger to hold positive and negative lots of the same commodity in an account simultaneously. I believe that this can lead to confusing and even incorrect accounting for trading lots. I think this is still being figured out by the Ledger look-alikes community and that they will eventually converge to the same solution I have, or perhaps even figure out a better solution. In the redesign, I separated out configuration directives that I had used for importing and over many iterations eventually figured out an elegant way to mostly automate imports and automatically detect the various input files and convert them into my input syntax. The result is the LedgerHub design doc . LedgerHub is currently implemented and I\u2019m using it exclusively, but has insufficient testing for me to stamp a public release [July 2014]. You are welcome to try it out for yourself. In June and July 2014, I decided to dump seven years\u2019 worth of thinking about command-line accounting in a set of Google Docs and this now forms the basis for the current documentation of Beancount. I hope to be evolving it gradually from here on.","title":"History of Beancount"},{"location":"beancount_history_and_credits.html#chronology","text":"Ledger was begun in August 2003 http://web.archive.org/web/*/http://www.newartisans.com/ledger/ Beancount was begun in 2008 http://web.archive.org/web/*/furius.ca/beancount HLedger was also begun in 2008 https://github.com/simonmichael/hledger/graphs/contributors?from=2007-01-21&to=2014-09-08&type=c","title":"Chronology"},{"location":"beancount_history_and_credits.html#credits","text":"So far I\u2019ve been contributing all the code to Beancount. Some users have made significant contributions in other ways: Daniel Clemente has been reporting all the issues he came across while using Beancount to manage his company and personal finances. His relentless perseverance and attention to detail has helped me put a focus on fixing the rough corners of Beancount that I knew to avoid myself. After many years of prodding, my old friend Filippo Tampieri has finally decided to convert his trading history in Beancount format. He has contributed a number of sophisticated reviews of my documentation and is working on adding various methods for evaluating returns on assets.","title":"Credits"},{"location":"beancount_language_syntax.html","text":"Beancount Language Syntax \uf0c1 Martin Blais , Updated: April 2016 http://furius.ca/beancount/doc/syntax Introduction Syntax Overview Directives Ordering of Directives Accounts Commodities / Currencies Strings Comments Directives Open Close Commodity Transactions Metadata Payee & Narration Costs and Prices Balancing Rule - The \u201cweight\u201d of postings Reducing Positions Amount Interpolation Tags The Tag Stack Links Balance Assertions Multiple Commodities Lots Are Aggregated Checks on Parent Accounts Before Close Pad Unused Pad Directives Commodities Cost Basis Multiple Paddings Notes Documents Documents from a Directory Prices Prices from Postings Prices on the Same Day Events Query Custom Metadata Options Operating Currencies Plugins Includes What\u2019s Next? Introduction \uf0c1 This is a user's manual to the language of Beancount, the command-line double-entry bookkeeping system. Beancount defines a computer language that allows you to enter financial transactions in a text file and extract various reports from it. It is a generic counting tool that works with multiple currencies, commodities held at cost (e.g., stocks), and even allows you to track unusual things, like vacation hours, air miles and rewards points, and anything else you might want to count, even beans. This document provides an introduction to Beancount\u2019s syntax and some of the technical details needed for one to understand how it carries out its calculations. This document does not provide an introduction to the double-entry method , a motivation , nor examples and guidelines for entering transactions in your input file, nor how to run the tools . These subjects have their own dedicated documents , and it is recommended that you have had a look at those before diving into this user\u2019s manual. This manual covers the technical details for using Beancount. Syntax Overview \uf0c1 Directives \uf0c1 Beancount is a declarative language. The input consists of a text file containing mainly a list of directives , or entries (we use these terms interchangeably in the code and documentation); there is also syntax for defining various options . Each directive begins with an associated date , which determines the point in time at which the directive will apply, and its type , which defines which kind of event this directive represents. All the directives begin with a syntax that looks like this: YYYY-MM-DD \u2026 where YYYY is the year, MM is the numerical month, and DD the numerical date. All digits are required, for example, the 7th of May 2007 should be \u201c2007-05-07\u201d, including its zeros. Beancount supports the international ISO 8601 standard format for dates, with dashes (e.g., \u201c2014-02-03\u201d), or the same ordering with slashes (e.g., \u201c2014/02/03\u201d). Here are some example directives, just to give you an idea of the aesthetics: 2014-02-03 open Assets:US:BofA:Checking 2014-04-10 note Assets:US:BofA:Checking \"Called to confirm wire transfer.\" 2014-05-02 balance Assets:US:BofA:Checking 154.20 USD The end product of a parsed input file is a simple list of these entries, in a data structure. All operations in Beancount are performed on these entries. Each particular directive type is documented in a section below. Ordering of Directives \uf0c1 The order of declaration of the directives is not important. In fact, the entries are re-sorted chronologically after parsing and before being processed. This is an important feature of the language, because it makes it possible for you to organize your input file any way you like without having to worry about affecting the meaning of the directives. Except for transactions, each directive is assumed to occur at the beginning of each day. For example, you could declare an account being opened on the same day as its first transaction: 2014-02-03 open Assets:US:BofA:Checking 2014-02-03 * \"Initial deposit\" Assets:US:BofA:Checking 100 USD Assets:Cash -100 USD However, if you hypothetically closed that account immediately, you could not declare it closed on the same day, you would have to fudge the date forward by declaring the close on 2/4: 2014-02-04 close Assets:US:BofA:Checking This also explains why balance assertions are verified before any transactions that occur on the same date. This is for consistency. Accounts \uf0c1 Beancount accumulates commodities in accounts. The names of these accounts do not have to be declared before being used in the file, they are recognized as \u201caccounts\u201d by virtue of their syntax alone 1 . An account name is a colon-separated list of capitalized words which begin with a letter, and whose first word must be one of five account types: Assets Liabilities Equity Income Expenses Each component of the account names begin with a capital letter or a number and are followed by letters, numbers or dash (-) characters. All other characters are disallowed. Here are some realistic example account names: Assets:US:BofA:Checking Liabilities:CA:RBC:CreditCard Equity:Retained-Earnings Income:US:Acme:Salary Expenses:Food:Groceries The set of all names of accounts seen in an input file implicitly define a hierarchy of accounts (sometimes called a chart-of-accounts ), similarly to how files are organized in a file system. For example, the following account names: Assets:US:BofA:Checking Assets:US:BofA:Savings Assets:US:Vanguard:Cash Assets:US:Vanguard:RGAGX Assets:Receivables implicitly declare a tree of accounts that looks like this: `-- Assets |-- Receivables `-- US |-- BofA | |-- Checking | `-- Savings `-- Vanguard |-- Cash `-- RGAGX We would say that \u201c Assets:US:BofA \u201d is the parent account of \u201c Assets:US:BofA:Checking \u201d, and that the latter is a child account of the former. Commodities / Currencies \uf0c1 Accounts contain currencies , which we sometimes also call commodities (we use both terms interchangeably). Like account names, currency names are recognized by their syntax, though, unlike account names, they need not be declared before being used). The syntax for a currency is a word all in capital letters, like these: USD CAD EUR MSFT IBM AIRMILE (Technically, a currency name may be up to 24 characters long, and it must start with a capital letter, must end with with a capital letter or number, and its other characters must only be capital letters, numbers, or punctuation limited to these characters: \u201c'._-\u201d (apostrophe, period, underscore, dash.) The first three might evoke real world currencies to you (US dollars, Canadian dollars, Euros); the next two, stock ticker names (Microsoft and IBM). And the last: rewards points (airmiles). Beancount knows of no such thing; from its perspective all of these instruments are treated similarly. There is no built-in notion of any previously existing currency. These currency names are just names of \u201cthings\u201d that can be put in accounts and accumulated in inventories associated with these accounts. There is something elegant about the fact that there is no \u201cspecial\u201d currency unit, that all commodities are treated equally the same: Beancount is inherently a multi-currency system. You will appreciate this if, like many of us, you are an expat and your life is divided between two or three continents. You can handle an international ledger of accounts without any problems. And your use of currencies can get quite creative: you can create a currency for your home, for example (e.g. MYLOFT ), a currency to count accumulated vacation hours ( VACHR ), or a currency to count potential contributions to your retirement accounts allowed annually ( IRAUSD ). You can actually solve many problems this way. The cookbook describes many such concrete examples. Beancount does not support the dollar sign syntax, e.g., \u201c$120.00\u201d. You should always use names for currencies in your input file. This makes the input more regular and is a design choice. For monetary units, I suggest that you use the standard ISO 4217 currency code as a guideline; these quickly become familiar. However, as detailed above, you may include some other characters in currency names, like underscores (_), dashes (-), periods (.), or apostrophes (\u2018), but no spaces. Finally, you will notice that there exists a \u201c commodity \u201d directive that can be used to declare currencies. It is entirely optional: currencies come into being as you use them. The purpose of the directive is simply to attach metadata to it. Strings \uf0c1 Whenever we need to insert some free text as part of an entry, it should be surrounded by double-quotes. This applies to the payee and narration fields, mainly; basically anything that\u2019s not a date, a number, a currency, an account name. Strings may be split over multiple lines. (Strings with multiple lines will include their newline characters and those need to be handled accordingly when rendering.) Comments \uf0c1 The Beancount input file isn\u2019t intended to contain only your directives: you can be liberal in placing comments and headers in it to organize your file. Any text on a line after the character \u201c;\u201d is ignored, text like this: ; I paid and left the taxi, forgot to take change, it was cold. 2015-01-01 * \"Taxi home from concert in Brooklyn\" Assets:Cash -20 USD ; inline comment Expenses:Taxi You can use one or more \u201c;\u201d characters if you like. Prepend on all lines if you want to enter a larger comment text. If you prefer to have the comment text parsed in and rendered in your journals, see the Note directive elsewhere in this document. Any line that does not begin as a valid Beancount syntax directive (e.g. with a date) is silently ignored. This way you can insert markup to organize your file for various outline modes, such as org-mode in Emacs. For example, you could organize your input file by institution like this and fold & unfold each of the sections independently,: * Banking ** Bank of America 2003-01-05 open Assets:US:BofA:Checking 2003-01-05 open Assets:US:BofA:Savings ;; Transactions follow \u2026 ** TD Bank 2006-03-15 open Assets:US:TD:Cash ;; More transactions follow \u2026 The unmatching lines are simply ignored. Note to visiting Ledger users: In Ledger, \u201c;\u201d is used both for marking comments and for attaching \u201cLedger tags\u201d (Beancount metadata) to postings. This is not the case in Beancount. In Beancount comments are always just comments. Metadata has its own separate syntax. Directives \uf0c1 For a quick reference & overview of directive syntax, please consult the Syntax Cheat Sheet . Open \uf0c1 All accounts need to be declared \u201copen\u201d in order to accept amounts posted to them. You do this by writing a directive that looks like this: 2014-05-01 open Liabilities:CreditCard:CapitalOne USD The general format of the Open directive is: YYYY-MM-DD open Account [ConstraintCurrency,...] [\"BookingMethod\"] The comma-separated optional list of constraint currencies enforces that all changes posted to this account are in units of one of the declared currencies. Specifying a currency constraint is recommended: the more constraints you provide Beancount with, the less likely you will be to make data entry mistakes because if you do, it will warn you about it. Each account should be declared \u201copened\u201d at a particular date that precedes (or is the same as) the date of the first transaction that posts an amount to that account. Just to be clear: an Open directive does not have to appear before transactions in the file , but rather, the date of the Open directive must precede the date of postings to that account. The order of declarations in the file is not important. So for example, this is a legal input file: 2014-05-05 * \"Using my new credit card\" Liabilities:CreditCard:CapitalOne -37.45 USD Expenses:Restaurant 2014-05-01 open Liabilities:CreditCard:CapitalOne USD 1990-01-01 open Expenses:Restaurant Another optional declaration for opening accounts is the \u201cbooking method\u201d, which is the algorithm that will be invoked if a reducing lot leads to an ambiguous choice of matching lots from the inventory (0, 2 or more lots match). Currently, the possible values it can take are: STRICT : The lot specification has to match exactly one lot. This is the default method. If this booking method is invoked, it will simply raise an error. This ensures that your input file explicitly selects all matching lots. NONE : No lot matching is performed. Lots of any price will be accepted. A mix of positive and negative numbers of lots for the same currency is allowed. (This is similar to how Ledger treats matching\u2026 it ignores it.) Close \uf0c1 Similarly to the Open directive, there is a Close directive that can be used to tell Beancount that an account has become inactive, for example: ; Closing credit card after fraud was detected. 2016-11-28 close Liabilities:CreditCard:CapitalOne The general format of the Close directive is: YYYY-MM-DD close Account This directive is used in a couple of ways: An error message is raised if you post amounts to that account after its closing date (it's a sanity check). This helps avoid mistakes in data entry. It helps the reporting code figure out which accounts are still active and filter out closed accounts outside of the reporting period. This is especially useful as your ledger accumulates much data over time, as there will be accounts that stop existing and which you just don't want to see in reports for years that follow their closure. Note that a Close directive does not currently generate an implicit zero balance check. You may want to add one just before the closing date to ensure that the account is correctly closed with empty contents. At the moment, once an account is closed, you cannot reopen it after that date. (Though you can, of course, delete or comment-out the directive that closed it.) Finally, there are utility functions in the code that allow you to establish which accounts are open on a particular date. I strongly recommend that you close accounts when they actually close in reality, it will keep your ledger more tidy. Commodity \uf0c1 There is a \u201cCommodity\u201d directive that can be used to declare currencies, financial instruments, commodities (different names for the same thing in Beancount): 1867-07-01 commodity CAD The general format of the Commodity directive is: YYYY-MM-DD commodity Currency This directive is a late arrival, and is entirely optional: you can use commodities without having to really declare them this way. The purpose of this directive is to attach commodity-specific metadata fields on it, so that it can be gathered by plugins later on. For example, you might want to provide a long descriptive name for each commodity, such as \u201cSwiss Franc\u201d for commodity \u201cCHF\u201d, or \u201cHooli Corporation Class C Shares\u201d for \u201cHOOL\u201d, like this: 1867-07-01 commodity CAD name: \"Canadian Dollar\" asset-class: \"cash\" 2012-01-01 commodity HOOL name: \"Hooli Corporation Class C Shares\" asset-class: \"stock\" For example, a plugin could then gather the metadata attribute names and perform some aggregations per asset class. You can use any date for a commodity\u2026 but a relevant date is the date at which it was created or introduced, e.g. Canadian dollars were first introduced in 1867, ILS (new Israeli Shekels) are in use since 1986-01-01. For a company, the date the corporation was formed and shares created could be a good date. Since the main purpose of this directive is to collect per-commodity information, the particular date you choose doesn\u2019t matter all that much. It is an error to declare the same commodity twice. Transactions \uf0c1 Transactions are the most common type of directives that occur in a ledger. They are slightly different to the other ones, because they can be followed by a list of postings. Here is an example: 2014-05-05 txn \"Cafe Mogador\" \"Lamb tagine with wine\" Liabilities:CreditCard:CapitalOne -37.45 USD Expenses:Restaurant As for all the other directives, a transaction directive begins with a date in the YYYY-MM-DD format and is followed by the directive name, in this case, \u201c txn \u201d. However, because transactions are the raison d\u2019\u00eatre for our double-entry system and as such are by far the most common type of directive that should appear in a Beancount input file, we make a special case and allow the user to elide the \u201ctxn\u201d keyword and just use a flag instead of it: 2014-05-05 * \"Cafe Mogador\" \"Lamb tagine with wine\" Liabilities:CreditCard:CapitalOne -37.45 USD Expenses:Restaurant A flag is used to indicate the status of a transaction, and the particular meaning of the flag is yours to define. We recommend using the following interpretation for them: *: Completed transaction, known amounts, \u201cthis looks correct.\u201d !: Incomplete transaction, needs confirmation or revision, \u201cthis looks incorrect.\u201d In the case of the first example using \u201ctxn\u201d to leave the transaction unflagged, the default flag (\u201c*\u201d) will be set on the transaction object. (I nearly always use the \u201c*\u201d variant and never the keyword one, it is mainly there for consistency with all the other directive formats.) You can also attach flags to the postings themselves, if you want to flag one of the transaction\u2019s legs in particular: 2014-05-05 * \"Transfer from Savings account\" Assets:MyBank:Checking -400.00 USD ! Assets:MyBank:Savings This is useful in the intermediate stage of de-duping transactions (see Getting Started document for more details). The general format of a Transaction directive is: YYYY-MM-DD [txn|Flag] [[Payee] Narration] [Flag] Account Amount [{Cost}] [@ Price] [Flag] Account Amount [{Cost}] [@ Price] ... The lines that follow the first line are for \u201cPostings.\u201d You can attach as many postings as you want to a transaction. For example, a salary entry might look like this: 2014-03-19 * \"Acme Corp\" \"Bi-monthly salary payment\" Assets:MyBank:Checking 3062.68 USD ; Direct deposit Income:AcmeCorp:Salary -4615.38 USD ; Gross salary Expenses:Taxes:TY2014:Federal 920.53 USD ; Federal taxes Expenses:Taxes:TY2014:SocSec 286.15 USD ; Social security Expenses:Taxes:TY2014:Medicare 66.92 USD ; Medicare Expenses:Taxes:TY2014:StateNY 277.90 USD ; New York taxes Expenses:Taxes:TY2014:SDI 1.20 USD ; Disability insurance The Amount in \u201cPostings\u201d can also be an arithmetic expression using ( ) * / - + . For example, 2014-10-05 * \"Costco\" \"Shopping for birthday\" Liabilities:CreditCard:CapitalOne -45.00 USD Assets:AccountsReceivable:John ((40.00/3) + 5) USD Assets:AccountsReceivable:Michael 40.00/3 USD Expenses:Shopping The crucial and only constraint on postings is that the sum of their balance amounts must be zero. This is explained in full detail below. Metadata \uf0c1 It\u2019s also possible to attach metadata to the transaction and/or any of its postings, so the fully general format is: YYYY-MM-DD [txn|Flag] [[Payee] Narration] [Key: Value] ... [Flag] Account Amount [{Cost}] [@ Price] [Key: Value] ... [Flag] Account Amount [{Cost}] [@ Price] [Key: Value] ... ... See the dedicated section on metadata below. Payee & Narration \uf0c1 A transaction may have an optional \u201cpayee\u201d and/or a \u201cnarration.\u201d In the first example above, the payee is \u201cCafe Mogador\u201d and the narration is \u201cLamb tagine with wine\u201d. The payee is a string that represents an external entity that is involved in the transaction. Payees are sometimes useful on transactions that post amounts to Expense accounts, whereby the account accumulates a category of expenses from multiple businesses. A good example is \u201c Expenses:Restaurant \u201d, which will include all postings for the various restaurants one might visit. A narration is a description of the transaction that you write. It can be a comment about the context, the person who accompanied you, some note about the product you bought... whatever you want it to be. It\u2019s for you to insert whatever you like. I like to insert notes when I reconcile, it\u2019s quick and I can refer to my notes later on, for example, to answer the question \u201cWhat was the name of that great little sushi place I visited with Andreas on the West side last winter?\u201d If you place a single string on a transaction line, it becomes its narration: 2014-05-05 * \"Lamb tagine with wine\" \u2026 If you want to set just a payee, put an empty narration string: 2014-05-05 * \"Cafe Mogador\" \"\" \u2026 For legacy reasons, a pipe symbol (\u201c|\u201d) is accepted between those strings (but this will be removed at some point in the future): 2014-05-05 * \"Cafe Mogador\" | \"\" \u2026 You may also leave out either (but you must provide a flag): 2014-05-05 * \u2026 Note for Ledger users. Ledger does not have separate narration and payee fields, it has only one field, which is referred to by the \u201cPayee\u201d metadata tag, and the narration ends up in a saved comment (a \u201cpersistent note\u201d). In Beancount, a Transaction object simply has two fields: payee and narration, where payee just happens to have an empty value a lot of the time. For a deeper discussion of how and when to use payees or not, see Payees, Subaccounts, and Assets . Costs and Prices \uf0c1 Postings represent a single amount being deposited to or withdrawn from an account. The simplest type of posting includes only its amount: 2012-11-03 * \"Transfer to pay credit card\" Assets:MyBank:Checking -400.00 USD Liabilities:CreditCard 400.00 USD If you converted the amount from another currency, you must provide a conversion rate to balance the transaction (see next section). This is done by attaching a \u201cprice\u201d to the posting, which is the rate of conversion: 2012-11-03 * \"Transfer to account in Canada\" Assets:MyBank:Checking -400.00 USD @ 1.09 CAD Assets:FR:SocGen:Checking 436.01 CAD You could also use the \u201c@@\u201d syntax to specify the total cost: 2012-11-03 * \"Transfer to account in Canada\" Assets:MyBank:Checking -400.00 USD @@ 436.01 CAD Assets:FR:SocGen:Checking 436.01 CAD Beancount will automatically compute the per-unit price, that is 1.090025 CAD (note that the precision will differ between the last two examples). After the transaction, we are not interested in keeping track of the exchange rate for the units of USD deposited into the account; those units of \u201cUSD\u201d are simply deposited. In a sense, the rate at which they were converted at has been forgotten. However, some commodities that you deposit in accounts must be \u201cheld at cost.\u201d This happens when you want to keep track of the cost basis of those commodities. The typical use case is investing, for example when you deposit shares of a stock in an account. What you want in that circumstance is for the acquisition cost of the commodities you deposit to be attached to the units, such that when you remove those units later on (when you sell), you should be able to identify by cost which of those units to remove, in order to control the effect of taxes (and avoid errors). You can imagine that for each of the units in the account there is an attached cost basis. This will allow us to automatically compute capital gains later. In order to specify that a posting to an account is to be held at a specific cost, include the cost in curly brackets: 2014-02-11 * \"Bought shares of S&P 500\" Assets:ETrade:IVV 10 IVV {183.07 USD} Assets:ETrade:Cash -1830.70 USD This is the subject of a deeper discussion. Refer to \u201c How Inventories Work \u201d and \u201c Trading with Beancount \u201d documents for an in-depth discussion of these topics. Finally, you can include both a cost and a price on a posting: 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 The price will only be used to insert a price entry in the prices database (see Price section below). This is discussed in more details in the Balancing Postings section of this document. Important Note. Amounts specified as either per-share or total prices or costs are always unsigned . It is an error to use a negative sign or a negative cost and Beancount will raise an error if you attempt to do so. Balancing Rule - The \u201cweight\u201d of postings \uf0c1 A crucial aspect of the double-entry method is ensuring that the sum of all the amounts on its postings equals ZERO, in all currencies. This is the central, non-negotiable condition that engenders the accounting equation, and makes it possible to filter any subset of transactions and drawing balance sheets that balance to zero. But with different types of units, the previously introduced price conversions and units \u201cheld at cost\u201d, what does it all mean? It\u2019s simple: we have devised a simple and clear rule for obtaining an amount and a currency from each posting, to be used for balancing them together. We call this the \u201cweight\u201d of a posting, or the balancing amount. Here is a short example of weights derived from postings using the four possible types of cost/price combinations: YYYY-MM-DD Account 10.00 USD -> 10.00 USD Account 10.00 CAD @ 1.01 USD -> 10.10 USD Account 10 SOME {2.02 USD} -> 20.20 USD Account 10 SOME {2.02 USD} @ 2.50 USD -> 20.20 USD Here is the explanation of how it is calculated: If the posting has only an amount and no cost, the balance amount is just the amount and currency on that posting. Using the first example from the previous section, the amount is -400.00 USD, and that is balanced against the 400.00 USD of the second leg. If the posting has only a price , the price is multiplied by the number of units and the price currency is used. In the second example from the preceding section, that is -400.00 USD x 1.09 CAD(/USD) = -436.00 CAD, and that is balanced against the other posting of 436.00 CAD 2 . If the posting has a cost , the cost is multiplied by the number of units and the cost currency is used. In the third example from the preceding section, that is 10 IVV x 183.08 USD(/IVV) = 1830.70 USD. That is balanced against the cash leg of -1830.70 USD, so all is good. Finally, if a posting has both a cost and a price , we simply ignore the price. This optional price is used later on to generate an entry in the in-memory prices database, but it is not used in balancing at all. With this rule, you should be able to easily balance all your transactions. Moreover, this rule makes it possible to let Beancount automatically calculate capital gains for you (see Trading with Beancount for details). Reducing Positions \uf0c1 When you post a reduction to a position in an account, the reduction must always match an existing lot. For example, if an account holds 3200 USD and a transaction posts a -1200 USD change to that account, the 1200 USD match against the existing 3200 USD, and the result is a single position of 2000 USD. This also works for negative values. For example, if an account has a -1300 USD balance and you post a +2000 USD change to it, you obtain a 700 USD balance. A change posted to an account, regardless of the account type, can result in a positive or negative balance; there are no limitations on the balances of simple commodity amounts (that is, those with no cost associated to them). For example, while Assets accounts normally have a positive balance and Liabilities accounts usually a negative one, you can legally credit an Assets account to a negative balance, or debit a Liabilities account to a positive balance. This is because in the real world these things do happen: you might write a check too many and obtain temporary credit from your bank\u2019s checking account (usually along with an outrageous \u201coverdraft\u201d fee), or pay that credit card balance twice by mistake. For commodities held at cost, the cost specification of the posting must match one of the lots held in the inventory before the transaction is applied. The list of lots is gathered, and matched against the specification in the {...} part of the posting. For example, if you provide a cost, only those lots whose cost match that will remain. If you provide a date, only those lots which match that date will remain. And you can use a label as well. If you provide a cost and a date, both of these are matched against the list of candidate lots to reduce. This is essentially a filter on the list of lots. If the filtered list results in a single lot, that lot is chosen to be reduced. If the list results in multiple lots, but the total amount being reduced equals the total amount in the lots, all those lots are reduced by that posting. For example, if in the past you had the following transactions: 2014-02-11 * \"Bought shares of S&P 500\" Assets:ETrade:IVV 20 IVV {183.07 USD, \"ref-001\"} \u2026 2014-03-22 * \"Bought shares of S&P 500\" Assets:ETrade:IVV 15 IVV {187.12 USD} \u2026 Each of the following reductions would be unambiguous: 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {183.07 USD} \u2026 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {2014-02-11} \u2026 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {\"ref-001\"} \u2026 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -35 IVV {} \u2026 However, the following would be ambiguous: 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {} \u2026 If multiple lots match against the reducing posting and their number is not the total number, we are in a situation of ambiguous matches . What happens then, is that the account\u2019s booking method is invoked. There are multiple booking methods, but by default, all accounts are set to use the \u201cSTRICT\u201d booking method. This method simply issues an error in an ambiguous situation. You may set the account\u2019s booking method to \u201cFIFO\u201d to instruct Beancount to select the oldest of the lots. Or \u201cLIFO\u201d for the latest (youngest) of the lots. This will automatically select all the necessary matching lots to fulfill the reduction. PLEASE NOTE! Requiring the dates to match will be dealt with more sensibly in the near future. See A Proposal for an Improvement on Inventory Booking for details of this upcoming change. For such postings, a change that results in a negative number of units is usually impossible. Beancount does not currently allow holding a negative number of a commodity held at cost. For example, an input with just this transaction will fail: 2014-05-23 * Assets:Investments:MSFT -10 MSFT {43.40 USD} Assets:Investments:Cash 434.00 USD If it did not, this would result in a balance of -10 units of MSFT. On the other hand, if the account had a balance of 12 units of MSFT held at 43.40 USD on 5/23, the transaction would book just fine, reducing the existing 12 units to 2. Most often, the error that will occur is that the account will be holding a balance of 10 or more units of MSFT at a different cost , and the user will specify an incorrect value for the cost. For instance, if the account had a positive balance of 20 MSFT {42.10 USD}, the transaction above would still fail, because there aren\u2019t 10 or more units of MSFT at 43.40 USD to remove from. This constraint is enforced for a few reasons: Mistakes in data entry for stock units are not uncommon, they have an important negative impact on the correctness of your Ledger\u2014the amounts are usually large\u2014and they will typically trigger an error from this constraint. Therefore, the error check is a useful way to detect these types of errors. Negative numbers of units held at cost are fairly rare. Chances are you don\u2019t need them at all. Exceptions include: short sales of stock, holding spreads on futures contracts, and depending on how you account for them, short positions in currency trading. This is why this check is enabled by default. PLEASE NOTE! In a future version of Beancount, we will relax this constraint somewhat. We will allow an account to hold a negative number of units of a commodity if and only if there are no other units of that commodity held in the account. Either that, or we will allow you to mark an account as having no such constraints at all. The purpose is to allow the account of short positions in commodities. The only blocking factor is this constraint. For more details of the inventory booking algorithm, see the How Inventories Work document. Amount Interpolation \uf0c1 Beancount is able to fill in some of the details of a transaction automatically. You can currently elide the amount of at most one posting within a transaction: 2012-11-03 * \"Transfer to pay credit card\" Assets:MyBank:Checking -400.00 USD Liabilities:CreditCard In the example above, the amount of the credit card posting has been elided. It is automatically calculated by Beancount at 400.00 USD to balance the transaction. This also works with multiple postings, and with postings with costs: 2014-07-11 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -10 IVV {183.07 USD} Assets:ETrade:Cash 1979.90 USD Income:ETrade:CapitalGains In this case, the units of IVV are sold at a higher price ($197.90) than they were bought for ($183.07). The cash first posting has a weight of -10 x 183.07 = -1830.70 and the second posting a straightforward $1979.90. The last posting will be ascribed the difference, that is, a balance of -149.20 USD , which is to say, a gain of $149.20. When calculating the amount to be balanced, the same balance amounts that are used to check that the transaction balances to zero are used to fill in the missing amounts. For example, the following would not trigger an error: 2014-07-11 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -10 IVV {183.07 USD} @ 197.90 USD Assets:ETrade:Cash The cash account would receive 1830.70 USD automatically, because the balance amount of the IVV posting is -1830.70 USD (if a posting has both a cost and a price, the cost basis is always used and the optional price ignored). While this is accepted and correct from a balancing perspective, this would be incomplete from an accounting perspective: the capital gain on a sale needs to be accounted for separately and besides, the amount deposited to the cash account if you did as above would fall short of the real deposit (1979.00 USD) and hopefully a subsequent balance assertion in the cash account would indicate this oversight by triggering an error. Finally, this also works when the balance includes multiple commodities: 2014-07-12 * \"Uncle Bob gave me his foreign currency collection!\" Income:Gifts -117.00 ILS Income:Gifts -3000.00 INR Income:Gifts -800.00 JPY Assets:ForeignCash Multiple postings (one for each commodity required to balance) will be inserted to replace the elided one. Tags \uf0c1 Transactions can be tagged with arbitrary strings: 2014-04-23 * \"Flight to Berlin\" #berlin-trip-2014 Expenses:Flights -1230.27 USD Liabilities:CreditCard This is similar to the popular idea of \u201chash tagging\u201d on Twitter and such. These tags essentially allow you to mark a subset of transactions. They can then be used as a filter to generate reports on only this subset of transactions. They have numerous uses. I like to use them to mark all my trips. Multiple tags can be specified as well: 2014-04-23 * \"Flight to Berlin\" #berlin-trip-2014 #germany Expenses:Flights -1230.27 USD Liabilities:CreditCard (If you want to store key-value pairs on directives, see the section on metadata below.) The Tag Stack \uf0c1 Oftentimes multiple transactions related to a single tag will be entered consecutively in a file. As a convenience, the parser can automatically tag transactions within a block of text. How this works is simple: the parser has a \u201cstack\u201d of current tags which it applies to all transactions as it reads them one-by-one. You can push and pop tags onto/from this stack, like this: pushtag #berlin-trip-2014 2014-04-23 * \"Flight to Berlin\" Expenses:Flights -1230.27 USD Liabilities:CreditCard poptag #berlin-trip-2014 This way, you can also push multiple tags onto a long, consecutive set of transactions without having to type them all in. Links \uf0c1 Transactions can also be linked together. You may think of the link as a special kind of tag that can be used to group together a set of financially related transactions over time. For example you may use links to group together transactions that are each related with a specific invoice. This allows to track payments (or write-offs) associated with the invoice: 2014-02-05 * \"Invoice for January\" ^invoice-pepe-studios-jan14 Income:Clients:PepeStudios -8450.00 USD Assets:AccountsReceivable 2014-02-20 * \"Check deposit - payment from Pepe\" ^invoice-pepe-studios-jan14 Assets:BofA:Checking 8450.00 USD Assets:AccountsReceivable Or track multiple transfers related to a single nefarious purpose: 2014-02-05 * \"Moving money to Isle of Man\" ^transfers-offshore-17 Assets:WellsFargo:Savings -40000.00 USD Assets:WellsFargo:Checking 40000.00 USD 2014-02-09 * \"Wire to FX broker\" ^transfers-offshore-17 Assets:WellsFargo:Checking -40025.00 USD Expenses:Fees:WireTransfers 25.00 USD Assets:OANDA:USDollar 40000.00 2014-03-16 * \"Conversion to offshore beans\" ^transfers-offshore-17 Assets:OANDA:USDollar -40000.00 USD Assets:OANDA:GBPounds 23391.81 GBP @ 1.71 USD 2014-03-16 * \"God save the Queen (and taxes)\" ^transfers-offshore-17 Assets:OANDA:GBPounds -23391.81 GBP Expenses:Fees:WireTransfers 15.00 GBP Assets:Brittania:PrivateBanking 23376.81 GBP Linked transactions can be rendered by the web interface in their own dedicated journal, regardless of the current view/filtered set of transactions (the list of links is a global page). Balance Assertions \uf0c1 A balance assertion is a way for you to input your statement balance into the flow of transactions. It tells Beancount to verify that the number of units of a particular commodity in some account should equal some expected value at some point in time. For instance, this 2014-12-26 balance Liabilities:US:CreditCard -3492.02 USD says \u201cCheck that the number of USD units in account \u201c Liabilities:US:CreditCard \u201d on the morning of December 26th, 2014 is -3492.02 USD.\u201d When processing the list of entries, if Beancount encounters a different balance than this for USD it will report an error. If no error is reported, you should have some confidence that the list of transactions that precedes it in this account is highly likely to be correct. This is useful in practice, because in many cases some transactions can get imported separately from the accounts of each of their postings (see the de-duping problem ). This can result in you booking the same transaction twice without noticing, and regularly inserting a balance assertion will catch that problem every time. The general format of the Balance directive is: YYYY-MM-DD balance Account Amount Note that a balance assertion, like all other non-transaction directives, applies at the beginning of its date (i.e., midnight at the start of day). Just imagine that the balance check occurs right after midnight on that day. Balance assertions only make sense on balance sheet accounts (Assets and Liabilities). Because the postings on Income and Expenses accounts are only interesting because of their transient value, i.e., for these accounts we\u2019re interested in sums of changes over a period of time (not the absolute value), it makes little sense to use a balance assertion on income statement accounts. Also, take note that each account benefits from an implicit balance assertion that the account is empty after it is opened at the date of its Open directive. You do not need to explicitly assert a zero balance when opening accounts. Multiple Commodities \uf0c1 A Beancount account may contain more than one commodity (although in practice, you will find that this does not occur often, it is sensible to create dedicated sub-accounts to hold each commodity, for example, holding a portfolio of stocks). A balance assertion applies only to the commodity of the assertion; it leaves the other commodities in the balance unchecked. If you want to check multiple commodities, use multiple balance assertions, like this: ; Check cash balances from wallet 2014-08-09 balance Assets:Cash 562.00 USD 2014-08-09 balance Assets:Cash 210.00 CAD 2014-08-09 balance Assets:Cash 60.00 EUR There is currently no way to exhaustively check the full list of commodities in an account ( a proposal is underway ). Note that in this example if an exhaustive check really matters to you, you could circumvent by defining a subaccount of the cash account to segregate each commodity separately, like this Assets:Cash:USD , Assets:Cash:CAD . Lots Are Aggregated \uf0c1 The balance assertion applies to the sum of units of a particular commody, irrespective of their cost. For example, if you hold three lots of the same commodity in an account, for example, 5 HOOL {500 USD} and 6 HOOL {510 USD}, the following balance check should succeed: 2014-08-09 balance Assets:Investing:HOOL 11 HOOL All the lots are aggregated together and you can verify their number of units. Checks on Parent Accounts \uf0c1 Balance assertions may be performed on parent accounts, and will include the balances of theirs and their sub-accounts: 2014-01-01 open Assets:Investing 2014-01-01 open Assets:Investing:Apple AAPL 2014-01-01 open Assets:Investing:Amazon AMZN 2014-01-01 open Assets:Investing:Microsoft MSFT 2014-01-01 open Equity:Opening-Balances 2014-06-01 * Assets:Investing:Apple 5 AAPL {578.23 USD} Assets:Investing:Amazon 5 AMZN {346.20 USD} Assets:Investing:Microsoft 5 MSFT {42.09 USD} Equity:Opening-Balances 2014-07-13 balance Assets:Investing 5 AAPL 2014-07-13 balance Assets:Investing 5 AMZN 2014-07-13 balance Assets:Investing 5 MSFT Note that this does require that a parent account have been declared as Open, in order to be legitimately used in the balance assertions directive. Before Close \uf0c1 It is useful to insert a balance assertion for 0 units just before closing an account, just to make sure its contents are empty as you close it. The Close directive does not insert that for you automatically (we may eventually build a plug-in for it). Local Tolerance \uf0c1 It's pretty common that sometimes one needs to override the tolerance on the balance check to loosen it on that balance assertion. This can be done using a local tolerance amount off of the balance amount, like this: 2013-09-20 balance Assets:Investing:Funds 319.020 ~ 0.002 RGAGX Pad \uf0c1 A padding directive automatically inserts a transaction that will make the subsequent balance assertion succeed, if it is needed. It inserts the difference needed to fulfill that balance assertion. (What \u201crubber space\u201d is in LaTeX, Pad directives are to balances in Beancount.) Note that by \u201csubsequent,\u201d I mean in date order , not in the order of the declarations in the file. This is the conceptual equivalent of a transaction that will automatically expand or contract to fill the difference between two balance assertions over time. It looks like this: 2014-06-01 pad Assets:BofA:Checking Equity:Opening-Balances The general format of the Pad directive is: YYYY-MM-DD pad Account AccountPad The first account is the account to credit the automatically calculated amount to. This is the account that should have a balance assertion following it (if the account does not have a balance assertion, the pad entry is benign and does nothing). The second account is for the other leg of the transaction, it is the source where the funds will come from, and this is almost always some Equity account. The reason for this is that this directive is generally used for initializing the balances of new accounts, to save us from having to either insert such a directive manually, or from having to enter the full past history of transactions that will bring the account to its current balance. Here is a realistic example usage scenario: ; Account was opened way back in the past. 2002-01-17 open Assets:US:BofA:Checking 2002-01-17 pad Assets:US:BofA:Checking Equity:Opening-Balances 2014-07-09 balance Assets:US:BofA:Checking 987.34 USD This will result in the following transaction being inserted right after the Pad directive, on the same date: 2002-01-17 P \"(Padding inserted for balance of 987.34 USD)\" Assets:US:BofA:Checking 987.34 USD Equity:Opening-Balances -987.34 USD This is a normal transaction\u2014you will see it appear in the rendered journals along with the other ones. (Note the special \u201cP\u201d flag, which can be used by scripts to find these.) Observe that without balance assertions, Pad directives make no sense. Therefore, like balance assertions, they are normally only used on balance sheet accounts (Assets and Liabilities). You could also insert Pad entries between balance assertions, it works too. For example: 2014-07-09 balance Assets:US:BofA:Checking 987.34 USD \u2026 more transactions\u2026 2014-08-08 pad Assets:US:BofA:Checking Equity:Opening-Balances 2014-08-09 balance Assets:US:BofA:Checking 1137.23 USD Without intervening transactions, this would insert the following padding transaction: 2014-08-08 P \"(Padding inserted for balance of 1137.23 USD)\" Assets:US:BofA:Checking 149.89 USD Equity:Opening-Balances -149.89 USD In case that\u2019s not obvious, 149.89 USD is the difference between 1137.23 USD and 987.34 USD. If there were more intervening transactions posting amounts to the checking account, the amount would automatically have been adjusted to make the second assertion pass. Unused Pad Directives \uf0c1 You may not currently leave unused Pad directives in your input file. They will trigger an error: 2014-01-01 open Assets:US:BofA:Checking 2014-02-01 pad Assets:US:BofA:Checking Equity:Opening-Balances 2014-06-01 * \"Initializing account\" Assets:US:BofA:Checking 212.00 USD Equity:Opening-Balances 2014-07-09 balance Assets:US:BofA:Checking 212.00 USD (Being this strict is a matter for debate, I suppose, and it could eventually be moved to an optional plugin.) Commodities \uf0c1 Note that the Pad directive does not specify any commodities at all. All commodities with corresponding balance assertions in the account are affected. For instance, the following code would have a padding directive insert a transaction with separate postings for USD and CAD: 2002-01-17 open Assets:Cash 2002-01-17 pad Assets:Cash Equity:Opening-Balances 2014-07-09 balance Assets:Cash 987.34 USD 2014-07-09 balance Assets:Cash 236.24 CAD If the account contained other commodities that aren\u2019t balance asserted, no posting would be inserted for those. Cost Basis \uf0c1 At the moment, Pad directives do not work with accounts holding positions held at cost. The directive is really only useful for cash accounts. (This is mainly because balance assertions do not yet allow specifying a cost basis to assert. It is possible that in the future we decide to support asserting the total cost basis, and that point we could consider supporting padding with cost basis.) Multiple Paddings \uf0c1 You cannot currently insert multiple padding entries for the same account and commodity: 2002-01-17 open Assets:Cash 2002-02-01 pad Assets:Cash Equity:Opening-Balances 2002-03-01 pad Assets:Cash Equity:Opening-Balances 2014-04-19 balance Assets:Cash 987.34 USD (There is a proposal pending to allow this and spread the padding amount evenly among all the intervening Pad directives, but it is as of yet unimplemented.) Notes \uf0c1 A Note directive is simply used to attach a dated comment to the journal of a particular account, like this: 2013-11-03 note Liabilities:CreditCard \"Called about fraudulent card.\" When you render the journal, the note should be rendered in context. This can be useful to record facts and claims associated with a financial event. I often use this to record snippets of information that would otherwise not make their way to a transaction. The general format of the Note directive is: YYYY-MM-DD note Account Description The description string may be split among multiple lines. Documents \uf0c1 A Document directive can be used to attach an external file to the journal of an account: 2013-11-03 document Liabilities:CreditCard \"/home/joe/stmts/apr-2014.pdf\" The filename gets rendered as a browser link in the journals of the web interface for the corresponding account and you should be able to click on it to view the contents of the file itself. This is useful to integrate account statements and other downloads into the flow of accounts, so that they\u2019re easily accessible from a few clicks. Scripts could also be written to obtain this list of documents from an account name and do something with them. The general format of the Document directive is: YYYY-MM-DD document Account PathToDocument Documents from a Directory \uf0c1 A more convenient way to create these entries is to use a special option to specify directories that contain a hierarchy of sub-directories that mirrors that of the chart of accounts. For example, the Document directive shown in the previous section could have been created from a directory hierarchy that looks like this: stmts `-- Liabilities `-- CreditCard `-- 2014-04-27.apr-2014.pdf By simply specifying the root directory as an option (note the absence of a trailing slash): option \"documents\" \"/home/joe/stmts\" The files that will be picked up are those that begin with a date as above, in the YYYY-MM-DD format. You may specify this option multiple times if you have many such document archives. In the past I have used one directory for each year (if you scan all your documents, the directory can grow to a large size, scanned documents tend to be large files). Organizing your electronic document statements and scans using the hierarchy of your ledger\u2019s accounts is a fantastic way to organize them and establish a clear, unambiguous place to find these documents later on, when they\u2019re needed. Prices \uf0c1 Beancount sometimes creates an in-memory data store of prices for each commodity, that is used for various reasons. In particular, it is used to report unrealized gains on account holdings. Price directives can be used to provide data points for this database. A Price directive establishes the rate of exchange between one commodity (the base currency) and another (the quote currency): 2014-07-09 price HOOL 579.18 USD This directive says: \u201cThe price of one unit of HOOL on July 9th, 2014 was 579.18 USD.\u201d Price entries for currency exchange rates work the same way: 2014-07-09 price USD 1.08 CAD The general format of the Price directive is: YYYY-MM-DD price Commodity Price Remember that Beancount knows nothing about what HOOL, USD or CAD are. They are opaque \u201cthings.\u201d You attach meaning to them. So setting a price per hour for your vacation hours is perfectly valid and useful too, if you account for your unused vacations on such terms: 2014-07-09 price VACHR 38.46 USD ; Equiv. $80,000 year Prices from Postings \uf0c1 If you use the beancount.plugins.implicit_prices plugin, every time a Posting appears that has a cost or an optional price declared, it will use that cost or price to automatically synthesize a Price directive. For example, this transaction: 2014-05-23 * Assets:Investments:MSFT -10 MSFT {43.40 USD} Assets:Investments:Cash 434.00 USD automatically becomes this after parsing: 2014-05-23 * Assets:Investments:MSFT -10 MSFT {43.40 USD} Assets:Investments:Cash 434.00 USD 2014-05-23 price MSFT 43.40 USD This is convenient and if you enable it, will probably be where most of the price points in your ledger\u2019s price database will come from. You can print a table of the parsed prices from a ledger (it is just another type of report). Prices on the Same Day \uf0c1 Notice that there is no notion of time; Beancount is not designed to solve problems for intra-day traders, though of course, it certainly is able to handle multiple trades per day. It just stores its prices per day. (Generally, if you need many features that require a notion of intra-day time, you\u2019re better off using another system, this is not the scope of a bookkeeping system.) When multiple Price directives do exist for the same day, the last one to appear in the file will be selected for inclusion in the Price database. Events \uf0c1 Event directives 3 are used to track the value of some variable of your choice over time. For example, your location: 2014-07-09 event \"location\" \"Paris, France\" The above directive says: \u201cChange the value of the \u2018location\u2019 event to \u2018Paris, France\u2019 as of the 9th of July 2014 and onwards.\u201d A particular event type only has a single value per day. The general format of the Event directive is: YYYY-MM-DD event Name Value The event\u2019s Name string does not have to be declared anywhere, it just begins to exist the first time you use the directive. The Value string can be anything you like; it has no prescribed structure. How to use these is best explained by providing examples: Location : You can use events for tracking the city or country you\u2019re in. This is sensible usage within a ledger because the nature of your expenses are heavily correlated to where you are. It\u2019s convenient: if you use Beancount regularly, it is very little effort to add this bit of information to your file. I usually have a \u201ccash daybook\u201d section where I put miscellaneous cash expenses, and that\u2019s where I enter those directives. Address : If you move around a lot, it\u2019s useful to keep a record of your past home addresses. This is sometimes requested on government forms for immigration. The Green Card application process in the US, for instance, Employer : You can record your date of employment and departure for each job this way. Then you can count the number of days you worked there. Trading window : If you\u2019re an employee of a public company, you can record the dates that you\u2019re allowed to trade its stock. This can then be used to ensure you did not trade that stock when the window is closed. Events are often used to report on numbers of days. For example, in the province of Quebec (Canada) you are insured for free health care coverage if \u201c you spend 183 days of the calendar year or more, excluding trips of 21 days or less .\u201d If you travel abroad a lot, you could easily write a script to warn you of remaining day allocations to avoid losing your coverage. Many expats are in similar situations. In the same vein, the IRS recognizes US immigrants as \u201cresident alien\u201d\u2014and thus subject to filing taxes, yay!\u2014if you pass the Substantial Presence Test , which is defined as \u201cbeing physically present in the US on at least 31 days during the current year, and 193 days during the 3 year period that includes the current year and the 2 years immediately before that, counting: all the days you were present in the current year, and \u2153 of the days of the year before that, and \u2159 of the year preceding that one.\u201d Ouch\u2026 my head hurts. This gets complicated. Armed with your location over time, you could report it automatically. Events will also be usable in the filtering language, to specify non-contiguous periods of time. For example, if you are tracking your location using Event directives, you could produce reports for transactions that occur only when you are in a specific location, e.g., \u201cShow me my expenses on all my trips to Germany,\u201d or \u201cGive me a list of payees for restaurant transactions when I\u2019m in Montreal.\u201d PLEASE NOTE! Filters haven\u2019t been implemented yet. Also, reports on events have not yet been re-implemented in Beancount 2.0. They will be reintroduced again soon, as well as filtering. It is worth noticing that the Price and Event directives are the only ones not associated to an account. Query \uf0c1 It can be convenient to be able to associate SQL queries in a Beancount file to be able to run these as a report automatically. This is still an early development / experimental directive. In any case, you can insert queries in the stream of transactions like this: 2014-07-09 query \"france-balances\" \" SELECT account, sum(position) WHERE \u2018trip-france-2014\u2019 in tags\" The grammar is YYYY-MM-DD query Name SqlContents Each query has a name, which will probably be used to invoke its running as a report type. Also, the date of the query is intended to be the date at which the query is intended to be run for, that is, transactions following it should be ignored. If you\u2019re familiar with the SQL syntax, it\u2019s an implicit CLOSE. Custom \uf0c1 The long-term plan for Beancount is to allow plugins and external clients to define their own directive types, to be declared and validated by the Beancount input language parser. In the meantime, a generic directive is provided for clients to prototype new features, e.g., budgeting. 2014-07-09 custom \"budget\" \"...\" TRUE 45.30 USD The grammar for this directive is flexible: YYYY-MM-DD custom TypeName Value1 ... The first argument is a string and is intended to be unique to your directive. Think of this as the type of your directive. Following it, you can put an arbitrary list of strings, dates, booleans, amounts, and numbers. Note that there is no validation that checks that the number and types of arguments following the TypeName is consistent for a particular type. (See this thread for the origin story around this feature.) Metadata \uf0c1 You may attach arbitrary data to each of your entries and postings. The syntax for it looks like this: 2013-03-14 open Assets:BTrade:HOOLI category: \"taxable\" 2013-08-26 * \"Buying some shares of Hooli\" statement: \"confirmation-826453.pdf\" Assets:BTrade:HOOLI 10 HOOL @ {498.45 USD} decision: \"scheduled\" Assets:BTrade:Cash In this example, a \u201ccategory\u201d attribute is attached to an account\u2019s Open directive, a \u201cstatement\u201d attribute is attached to a Transaction directive (with a string value that represents a filename) and a \u201cdecision\u201d attribute has been attached to the first posting of the transaction (the additional indentation from the posting is not strictly necessary but it helps with readability). Metadata can be attached to any directive type. Keys must begin with a lowercase character from a-z and may contain (uppercase or lowercase) letters, numbers, dashes and underscores. Moreover, the values can be any of the following data types: Strings Accounts Currency Dates (datetime.date) Tags Numbers (Decimal) Amount (beancount.core.amount.Amount) There are two ways in which this data can be used: Query tools that come with Beancount (such as bean-query) will allow you to make use of the metadata values for filtering and aggregation. You can access and use the metadata in custom scripts. The metadata values are accessible as a \u201c .meta \u201d attribute on all directives and is a Python dict. There are no special meanings attached to particular attributes, these are intended for users to define. However, all directives are guaranteed to contain a \u2018 filename \u2019 (string) and a \u2018 lineno \u2019 (integer) attribute which reflect the location they were created from. Finally, attributes without a value will be parsed and have a value of 'None'. If an attribute is repeated multiple times, only the first value for this attribute will be parsed and retained and the following values ignored. Options \uf0c1 The great majority of a Beancount input file consists in directives, as seen in the previous section. However, there are a few global options that can be set in the input file as well, by using a special undated \u201coption\u201d directive: option \"title\" \"Ed\u2019s Personal Ledger\" The general format of the Option directive is: option Name Value where Name and Value are both strings. Note that depending the option, the effect may be to set a single value, or add to a list of existing values. In other words, some options are lists. There are three ways to view the list of options: In this document , which I update regularly. To view the definitive list of options supported by your installed version, use the following command: bean-doctor list-options Finally, you can peek at the source code as well. Operating Currencies \uf0c1 One notable option is \u201c operating_currency \u201d. By default Beancount does not treat any of the commodities any different from each other. In particular, it doesn\u2019t know that there\u2019s anything special about the most common of commodities one uses: their currencies. For example, if you live in New Zealand, you\u2019re going to have an overwhelming number of NZD commodities in your transactions. But useful reports try to reduce all non-currency commodities into one of the main currencies used. Also, it\u2019s useful to break out the currency units into their own dedicated columns. This may also be useful for exporting in order to avoid having to specify the units for that column and import to a spreadsheet with numbers you can process. For this reason, you are able to declare the most common currencies you use in an option: option \"operating_currency\" \"USD\" You may declare more than one. In any case, this option is only ever used by reporting code, it never changes the behavior of Beancount\u2019s processing or semantics. Plugins \uf0c1 In order to load plugin Python modules, use the dedicated \u201cplugin\u201d directive: plugin \"beancount.plugins.module_name\" The name of a plugin should be the name of a Python module in your PYTHONPATH. Those modules will be imported by the Beancount loader and run on the list of parsed entries in order for the plugins to transform the entries or output errors. This allows you to integrate some of your code within Beancount, making arbitrary transformations on the entries. See Scripting & Plugins for details. Plugins also optionally accept some configuration parameters. These can be provided by an optional final string argument, like this: plugin \"beancount.plugins.module_name\" \"configuration data\" The general format of the Option directive is: plugin ModuleName StringConfig The format of the configuration data is plugin-dependent. At the moment, an arbitrary string is passed provided to the plugin. See the plugins\u2019 documentation for specific detail on what it can accept. Also see the \u201cplugin processing mode\u201d option which affects the list of built-in plugins that get run. Includes \uf0c1 Include directives are supported. This allows you to split up large input files into multiple files. The syntax looks like this: include \"path/to/include/file.beancount\" The general format is include Filename The specified path can be an absolute or a relative filename. If the filename is relative, it is relative to the including filename\u2019s directory. This makes it easy to put relative includes in a hierarchy of directories that can be placed under source control and checked out anywhere. Include directives are not processed strictly (as in C, for example). The include directives are accumulated by the Beancount parser and processed separately by the loader. This is possible because the order of declarations of a Beancount input file is not relevant. However, for the moment, options are parsed per-file. The options-map that is kept for post-parse processing is the options-map returned for the top-level file. This is probably subject to review in the future. What\u2019s Next? \uf0c1 This document described all the possible syntax of the Beancount language. If you haven\u2019t written any Beancount input yet, you can head to the Getting Started guide, or browse through a list of practical use cases in the Command-line Accounting Cookbook . Note that there exists an \u201cOpen\u201d directive that is used to provide the start date of each account. That can be located anywhere in the file, it does not have to appear in the file somewhere before you use an account name. You can just start using account names in transactions right away, though all account names that receive postings to them will eventually have to have a corresponding Open directive with a date that precedes all transactions posted to the account in the input file. \u21a9 Note that this is valid whether the price is specified as a per-unit price with the @ syntax or as a total price using the @@ syntax. \u21a9 I really dislike the name \u201cevent\u201d for this directive. I\u2019ve been trying to find a better alternative, so far without success. The name \u201cregister\u201d might be more appropriate, as it resembles that of a processor\u2019s register, but that could be confused with an account register report. \u201cvariable\u201d might work, but somehow that just sounds too computer-sciency and out of context. If you can think of something better, please make a suggestion and I\u2019ll seriously entertain a complete rename (with legacy support for \u201cevent\u201d). \u21a9","title":"Beancount Language Syntax"},{"location":"beancount_language_syntax.html#beancount-language-syntax","text":"Martin Blais , Updated: April 2016 http://furius.ca/beancount/doc/syntax Introduction Syntax Overview Directives Ordering of Directives Accounts Commodities / Currencies Strings Comments Directives Open Close Commodity Transactions Metadata Payee & Narration Costs and Prices Balancing Rule - The \u201cweight\u201d of postings Reducing Positions Amount Interpolation Tags The Tag Stack Links Balance Assertions Multiple Commodities Lots Are Aggregated Checks on Parent Accounts Before Close Pad Unused Pad Directives Commodities Cost Basis Multiple Paddings Notes Documents Documents from a Directory Prices Prices from Postings Prices on the Same Day Events Query Custom Metadata Options Operating Currencies Plugins Includes What\u2019s Next?","title":"Beancount Language Syntax"},{"location":"beancount_language_syntax.html#introduction","text":"This is a user's manual to the language of Beancount, the command-line double-entry bookkeeping system. Beancount defines a computer language that allows you to enter financial transactions in a text file and extract various reports from it. It is a generic counting tool that works with multiple currencies, commodities held at cost (e.g., stocks), and even allows you to track unusual things, like vacation hours, air miles and rewards points, and anything else you might want to count, even beans. This document provides an introduction to Beancount\u2019s syntax and some of the technical details needed for one to understand how it carries out its calculations. This document does not provide an introduction to the double-entry method , a motivation , nor examples and guidelines for entering transactions in your input file, nor how to run the tools . These subjects have their own dedicated documents , and it is recommended that you have had a look at those before diving into this user\u2019s manual. This manual covers the technical details for using Beancount.","title":"Introduction"},{"location":"beancount_language_syntax.html#syntax-overview","text":"","title":"Syntax Overview"},{"location":"beancount_language_syntax.html#directives","text":"Beancount is a declarative language. The input consists of a text file containing mainly a list of directives , or entries (we use these terms interchangeably in the code and documentation); there is also syntax for defining various options . Each directive begins with an associated date , which determines the point in time at which the directive will apply, and its type , which defines which kind of event this directive represents. All the directives begin with a syntax that looks like this: YYYY-MM-DD \u2026 where YYYY is the year, MM is the numerical month, and DD the numerical date. All digits are required, for example, the 7th of May 2007 should be \u201c2007-05-07\u201d, including its zeros. Beancount supports the international ISO 8601 standard format for dates, with dashes (e.g., \u201c2014-02-03\u201d), or the same ordering with slashes (e.g., \u201c2014/02/03\u201d). Here are some example directives, just to give you an idea of the aesthetics: 2014-02-03 open Assets:US:BofA:Checking 2014-04-10 note Assets:US:BofA:Checking \"Called to confirm wire transfer.\" 2014-05-02 balance Assets:US:BofA:Checking 154.20 USD The end product of a parsed input file is a simple list of these entries, in a data structure. All operations in Beancount are performed on these entries. Each particular directive type is documented in a section below.","title":"Directives"},{"location":"beancount_language_syntax.html#ordering-of-directives","text":"The order of declaration of the directives is not important. In fact, the entries are re-sorted chronologically after parsing and before being processed. This is an important feature of the language, because it makes it possible for you to organize your input file any way you like without having to worry about affecting the meaning of the directives. Except for transactions, each directive is assumed to occur at the beginning of each day. For example, you could declare an account being opened on the same day as its first transaction: 2014-02-03 open Assets:US:BofA:Checking 2014-02-03 * \"Initial deposit\" Assets:US:BofA:Checking 100 USD Assets:Cash -100 USD However, if you hypothetically closed that account immediately, you could not declare it closed on the same day, you would have to fudge the date forward by declaring the close on 2/4: 2014-02-04 close Assets:US:BofA:Checking This also explains why balance assertions are verified before any transactions that occur on the same date. This is for consistency.","title":"Ordering of Directives"},{"location":"beancount_language_syntax.html#accounts","text":"Beancount accumulates commodities in accounts. The names of these accounts do not have to be declared before being used in the file, they are recognized as \u201caccounts\u201d by virtue of their syntax alone 1 . An account name is a colon-separated list of capitalized words which begin with a letter, and whose first word must be one of five account types: Assets Liabilities Equity Income Expenses Each component of the account names begin with a capital letter or a number and are followed by letters, numbers or dash (-) characters. All other characters are disallowed. Here are some realistic example account names: Assets:US:BofA:Checking Liabilities:CA:RBC:CreditCard Equity:Retained-Earnings Income:US:Acme:Salary Expenses:Food:Groceries The set of all names of accounts seen in an input file implicitly define a hierarchy of accounts (sometimes called a chart-of-accounts ), similarly to how files are organized in a file system. For example, the following account names: Assets:US:BofA:Checking Assets:US:BofA:Savings Assets:US:Vanguard:Cash Assets:US:Vanguard:RGAGX Assets:Receivables implicitly declare a tree of accounts that looks like this: `-- Assets |-- Receivables `-- US |-- BofA | |-- Checking | `-- Savings `-- Vanguard |-- Cash `-- RGAGX We would say that \u201c Assets:US:BofA \u201d is the parent account of \u201c Assets:US:BofA:Checking \u201d, and that the latter is a child account of the former.","title":"Accounts"},{"location":"beancount_language_syntax.html#commodities-currencies","text":"Accounts contain currencies , which we sometimes also call commodities (we use both terms interchangeably). Like account names, currency names are recognized by their syntax, though, unlike account names, they need not be declared before being used). The syntax for a currency is a word all in capital letters, like these: USD CAD EUR MSFT IBM AIRMILE (Technically, a currency name may be up to 24 characters long, and it must start with a capital letter, must end with with a capital letter or number, and its other characters must only be capital letters, numbers, or punctuation limited to these characters: \u201c'._-\u201d (apostrophe, period, underscore, dash.) The first three might evoke real world currencies to you (US dollars, Canadian dollars, Euros); the next two, stock ticker names (Microsoft and IBM). And the last: rewards points (airmiles). Beancount knows of no such thing; from its perspective all of these instruments are treated similarly. There is no built-in notion of any previously existing currency. These currency names are just names of \u201cthings\u201d that can be put in accounts and accumulated in inventories associated with these accounts. There is something elegant about the fact that there is no \u201cspecial\u201d currency unit, that all commodities are treated equally the same: Beancount is inherently a multi-currency system. You will appreciate this if, like many of us, you are an expat and your life is divided between two or three continents. You can handle an international ledger of accounts without any problems. And your use of currencies can get quite creative: you can create a currency for your home, for example (e.g. MYLOFT ), a currency to count accumulated vacation hours ( VACHR ), or a currency to count potential contributions to your retirement accounts allowed annually ( IRAUSD ). You can actually solve many problems this way. The cookbook describes many such concrete examples. Beancount does not support the dollar sign syntax, e.g., \u201c$120.00\u201d. You should always use names for currencies in your input file. This makes the input more regular and is a design choice. For monetary units, I suggest that you use the standard ISO 4217 currency code as a guideline; these quickly become familiar. However, as detailed above, you may include some other characters in currency names, like underscores (_), dashes (-), periods (.), or apostrophes (\u2018), but no spaces. Finally, you will notice that there exists a \u201c commodity \u201d directive that can be used to declare currencies. It is entirely optional: currencies come into being as you use them. The purpose of the directive is simply to attach metadata to it.","title":"Commodities / Currencies"},{"location":"beancount_language_syntax.html#strings","text":"Whenever we need to insert some free text as part of an entry, it should be surrounded by double-quotes. This applies to the payee and narration fields, mainly; basically anything that\u2019s not a date, a number, a currency, an account name. Strings may be split over multiple lines. (Strings with multiple lines will include their newline characters and those need to be handled accordingly when rendering.)","title":"Strings"},{"location":"beancount_language_syntax.html#comments","text":"The Beancount input file isn\u2019t intended to contain only your directives: you can be liberal in placing comments and headers in it to organize your file. Any text on a line after the character \u201c;\u201d is ignored, text like this: ; I paid and left the taxi, forgot to take change, it was cold. 2015-01-01 * \"Taxi home from concert in Brooklyn\" Assets:Cash -20 USD ; inline comment Expenses:Taxi You can use one or more \u201c;\u201d characters if you like. Prepend on all lines if you want to enter a larger comment text. If you prefer to have the comment text parsed in and rendered in your journals, see the Note directive elsewhere in this document. Any line that does not begin as a valid Beancount syntax directive (e.g. with a date) is silently ignored. This way you can insert markup to organize your file for various outline modes, such as org-mode in Emacs. For example, you could organize your input file by institution like this and fold & unfold each of the sections independently,: * Banking ** Bank of America 2003-01-05 open Assets:US:BofA:Checking 2003-01-05 open Assets:US:BofA:Savings ;; Transactions follow \u2026 ** TD Bank 2006-03-15 open Assets:US:TD:Cash ;; More transactions follow \u2026 The unmatching lines are simply ignored. Note to visiting Ledger users: In Ledger, \u201c;\u201d is used both for marking comments and for attaching \u201cLedger tags\u201d (Beancount metadata) to postings. This is not the case in Beancount. In Beancount comments are always just comments. Metadata has its own separate syntax.","title":"Comments"},{"location":"beancount_language_syntax.html#directives_1","text":"For a quick reference & overview of directive syntax, please consult the Syntax Cheat Sheet .","title":"Directives"},{"location":"beancount_language_syntax.html#open","text":"All accounts need to be declared \u201copen\u201d in order to accept amounts posted to them. You do this by writing a directive that looks like this: 2014-05-01 open Liabilities:CreditCard:CapitalOne USD The general format of the Open directive is: YYYY-MM-DD open Account [ConstraintCurrency,...] [\"BookingMethod\"] The comma-separated optional list of constraint currencies enforces that all changes posted to this account are in units of one of the declared currencies. Specifying a currency constraint is recommended: the more constraints you provide Beancount with, the less likely you will be to make data entry mistakes because if you do, it will warn you about it. Each account should be declared \u201copened\u201d at a particular date that precedes (or is the same as) the date of the first transaction that posts an amount to that account. Just to be clear: an Open directive does not have to appear before transactions in the file , but rather, the date of the Open directive must precede the date of postings to that account. The order of declarations in the file is not important. So for example, this is a legal input file: 2014-05-05 * \"Using my new credit card\" Liabilities:CreditCard:CapitalOne -37.45 USD Expenses:Restaurant 2014-05-01 open Liabilities:CreditCard:CapitalOne USD 1990-01-01 open Expenses:Restaurant Another optional declaration for opening accounts is the \u201cbooking method\u201d, which is the algorithm that will be invoked if a reducing lot leads to an ambiguous choice of matching lots from the inventory (0, 2 or more lots match). Currently, the possible values it can take are: STRICT : The lot specification has to match exactly one lot. This is the default method. If this booking method is invoked, it will simply raise an error. This ensures that your input file explicitly selects all matching lots. NONE : No lot matching is performed. Lots of any price will be accepted. A mix of positive and negative numbers of lots for the same currency is allowed. (This is similar to how Ledger treats matching\u2026 it ignores it.)","title":"Open"},{"location":"beancount_language_syntax.html#close","text":"Similarly to the Open directive, there is a Close directive that can be used to tell Beancount that an account has become inactive, for example: ; Closing credit card after fraud was detected. 2016-11-28 close Liabilities:CreditCard:CapitalOne The general format of the Close directive is: YYYY-MM-DD close Account This directive is used in a couple of ways: An error message is raised if you post amounts to that account after its closing date (it's a sanity check). This helps avoid mistakes in data entry. It helps the reporting code figure out which accounts are still active and filter out closed accounts outside of the reporting period. This is especially useful as your ledger accumulates much data over time, as there will be accounts that stop existing and which you just don't want to see in reports for years that follow their closure. Note that a Close directive does not currently generate an implicit zero balance check. You may want to add one just before the closing date to ensure that the account is correctly closed with empty contents. At the moment, once an account is closed, you cannot reopen it after that date. (Though you can, of course, delete or comment-out the directive that closed it.) Finally, there are utility functions in the code that allow you to establish which accounts are open on a particular date. I strongly recommend that you close accounts when they actually close in reality, it will keep your ledger more tidy.","title":"Close"},{"location":"beancount_language_syntax.html#commodity","text":"There is a \u201cCommodity\u201d directive that can be used to declare currencies, financial instruments, commodities (different names for the same thing in Beancount): 1867-07-01 commodity CAD The general format of the Commodity directive is: YYYY-MM-DD commodity Currency This directive is a late arrival, and is entirely optional: you can use commodities without having to really declare them this way. The purpose of this directive is to attach commodity-specific metadata fields on it, so that it can be gathered by plugins later on. For example, you might want to provide a long descriptive name for each commodity, such as \u201cSwiss Franc\u201d for commodity \u201cCHF\u201d, or \u201cHooli Corporation Class C Shares\u201d for \u201cHOOL\u201d, like this: 1867-07-01 commodity CAD name: \"Canadian Dollar\" asset-class: \"cash\" 2012-01-01 commodity HOOL name: \"Hooli Corporation Class C Shares\" asset-class: \"stock\" For example, a plugin could then gather the metadata attribute names and perform some aggregations per asset class. You can use any date for a commodity\u2026 but a relevant date is the date at which it was created or introduced, e.g. Canadian dollars were first introduced in 1867, ILS (new Israeli Shekels) are in use since 1986-01-01. For a company, the date the corporation was formed and shares created could be a good date. Since the main purpose of this directive is to collect per-commodity information, the particular date you choose doesn\u2019t matter all that much. It is an error to declare the same commodity twice.","title":"Commodity"},{"location":"beancount_language_syntax.html#transactions","text":"Transactions are the most common type of directives that occur in a ledger. They are slightly different to the other ones, because they can be followed by a list of postings. Here is an example: 2014-05-05 txn \"Cafe Mogador\" \"Lamb tagine with wine\" Liabilities:CreditCard:CapitalOne -37.45 USD Expenses:Restaurant As for all the other directives, a transaction directive begins with a date in the YYYY-MM-DD format and is followed by the directive name, in this case, \u201c txn \u201d. However, because transactions are the raison d\u2019\u00eatre for our double-entry system and as such are by far the most common type of directive that should appear in a Beancount input file, we make a special case and allow the user to elide the \u201ctxn\u201d keyword and just use a flag instead of it: 2014-05-05 * \"Cafe Mogador\" \"Lamb tagine with wine\" Liabilities:CreditCard:CapitalOne -37.45 USD Expenses:Restaurant A flag is used to indicate the status of a transaction, and the particular meaning of the flag is yours to define. We recommend using the following interpretation for them: *: Completed transaction, known amounts, \u201cthis looks correct.\u201d !: Incomplete transaction, needs confirmation or revision, \u201cthis looks incorrect.\u201d In the case of the first example using \u201ctxn\u201d to leave the transaction unflagged, the default flag (\u201c*\u201d) will be set on the transaction object. (I nearly always use the \u201c*\u201d variant and never the keyword one, it is mainly there for consistency with all the other directive formats.) You can also attach flags to the postings themselves, if you want to flag one of the transaction\u2019s legs in particular: 2014-05-05 * \"Transfer from Savings account\" Assets:MyBank:Checking -400.00 USD ! Assets:MyBank:Savings This is useful in the intermediate stage of de-duping transactions (see Getting Started document for more details). The general format of a Transaction directive is: YYYY-MM-DD [txn|Flag] [[Payee] Narration] [Flag] Account Amount [{Cost}] [@ Price] [Flag] Account Amount [{Cost}] [@ Price] ... The lines that follow the first line are for \u201cPostings.\u201d You can attach as many postings as you want to a transaction. For example, a salary entry might look like this: 2014-03-19 * \"Acme Corp\" \"Bi-monthly salary payment\" Assets:MyBank:Checking 3062.68 USD ; Direct deposit Income:AcmeCorp:Salary -4615.38 USD ; Gross salary Expenses:Taxes:TY2014:Federal 920.53 USD ; Federal taxes Expenses:Taxes:TY2014:SocSec 286.15 USD ; Social security Expenses:Taxes:TY2014:Medicare 66.92 USD ; Medicare Expenses:Taxes:TY2014:StateNY 277.90 USD ; New York taxes Expenses:Taxes:TY2014:SDI 1.20 USD ; Disability insurance The Amount in \u201cPostings\u201d can also be an arithmetic expression using ( ) * / - + . For example, 2014-10-05 * \"Costco\" \"Shopping for birthday\" Liabilities:CreditCard:CapitalOne -45.00 USD Assets:AccountsReceivable:John ((40.00/3) + 5) USD Assets:AccountsReceivable:Michael 40.00/3 USD Expenses:Shopping The crucial and only constraint on postings is that the sum of their balance amounts must be zero. This is explained in full detail below.","title":"Transactions"},{"location":"beancount_language_syntax.html#metadata","text":"It\u2019s also possible to attach metadata to the transaction and/or any of its postings, so the fully general format is: YYYY-MM-DD [txn|Flag] [[Payee] Narration] [Key: Value] ... [Flag] Account Amount [{Cost}] [@ Price] [Key: Value] ... [Flag] Account Amount [{Cost}] [@ Price] [Key: Value] ... ... See the dedicated section on metadata below.","title":"Metadata"},{"location":"beancount_language_syntax.html#payee-narration","text":"A transaction may have an optional \u201cpayee\u201d and/or a \u201cnarration.\u201d In the first example above, the payee is \u201cCafe Mogador\u201d and the narration is \u201cLamb tagine with wine\u201d. The payee is a string that represents an external entity that is involved in the transaction. Payees are sometimes useful on transactions that post amounts to Expense accounts, whereby the account accumulates a category of expenses from multiple businesses. A good example is \u201c Expenses:Restaurant \u201d, which will include all postings for the various restaurants one might visit. A narration is a description of the transaction that you write. It can be a comment about the context, the person who accompanied you, some note about the product you bought... whatever you want it to be. It\u2019s for you to insert whatever you like. I like to insert notes when I reconcile, it\u2019s quick and I can refer to my notes later on, for example, to answer the question \u201cWhat was the name of that great little sushi place I visited with Andreas on the West side last winter?\u201d If you place a single string on a transaction line, it becomes its narration: 2014-05-05 * \"Lamb tagine with wine\" \u2026 If you want to set just a payee, put an empty narration string: 2014-05-05 * \"Cafe Mogador\" \"\" \u2026 For legacy reasons, a pipe symbol (\u201c|\u201d) is accepted between those strings (but this will be removed at some point in the future): 2014-05-05 * \"Cafe Mogador\" | \"\" \u2026 You may also leave out either (but you must provide a flag): 2014-05-05 * \u2026 Note for Ledger users. Ledger does not have separate narration and payee fields, it has only one field, which is referred to by the \u201cPayee\u201d metadata tag, and the narration ends up in a saved comment (a \u201cpersistent note\u201d). In Beancount, a Transaction object simply has two fields: payee and narration, where payee just happens to have an empty value a lot of the time. For a deeper discussion of how and when to use payees or not, see Payees, Subaccounts, and Assets .","title":"Payee & Narration"},{"location":"beancount_language_syntax.html#costs-and-prices","text":"Postings represent a single amount being deposited to or withdrawn from an account. The simplest type of posting includes only its amount: 2012-11-03 * \"Transfer to pay credit card\" Assets:MyBank:Checking -400.00 USD Liabilities:CreditCard 400.00 USD If you converted the amount from another currency, you must provide a conversion rate to balance the transaction (see next section). This is done by attaching a \u201cprice\u201d to the posting, which is the rate of conversion: 2012-11-03 * \"Transfer to account in Canada\" Assets:MyBank:Checking -400.00 USD @ 1.09 CAD Assets:FR:SocGen:Checking 436.01 CAD You could also use the \u201c@@\u201d syntax to specify the total cost: 2012-11-03 * \"Transfer to account in Canada\" Assets:MyBank:Checking -400.00 USD @@ 436.01 CAD Assets:FR:SocGen:Checking 436.01 CAD Beancount will automatically compute the per-unit price, that is 1.090025 CAD (note that the precision will differ between the last two examples). After the transaction, we are not interested in keeping track of the exchange rate for the units of USD deposited into the account; those units of \u201cUSD\u201d are simply deposited. In a sense, the rate at which they were converted at has been forgotten. However, some commodities that you deposit in accounts must be \u201cheld at cost.\u201d This happens when you want to keep track of the cost basis of those commodities. The typical use case is investing, for example when you deposit shares of a stock in an account. What you want in that circumstance is for the acquisition cost of the commodities you deposit to be attached to the units, such that when you remove those units later on (when you sell), you should be able to identify by cost which of those units to remove, in order to control the effect of taxes (and avoid errors). You can imagine that for each of the units in the account there is an attached cost basis. This will allow us to automatically compute capital gains later. In order to specify that a posting to an account is to be held at a specific cost, include the cost in curly brackets: 2014-02-11 * \"Bought shares of S&P 500\" Assets:ETrade:IVV 10 IVV {183.07 USD} Assets:ETrade:Cash -1830.70 USD This is the subject of a deeper discussion. Refer to \u201c How Inventories Work \u201d and \u201c Trading with Beancount \u201d documents for an in-depth discussion of these topics. Finally, you can include both a cost and a price on a posting: 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 The price will only be used to insert a price entry in the prices database (see Price section below). This is discussed in more details in the Balancing Postings section of this document. Important Note. Amounts specified as either per-share or total prices or costs are always unsigned . It is an error to use a negative sign or a negative cost and Beancount will raise an error if you attempt to do so.","title":"Costs and Prices"},{"location":"beancount_language_syntax.html#balancing-rule-the-weight-of-postings","text":"A crucial aspect of the double-entry method is ensuring that the sum of all the amounts on its postings equals ZERO, in all currencies. This is the central, non-negotiable condition that engenders the accounting equation, and makes it possible to filter any subset of transactions and drawing balance sheets that balance to zero. But with different types of units, the previously introduced price conversions and units \u201cheld at cost\u201d, what does it all mean? It\u2019s simple: we have devised a simple and clear rule for obtaining an amount and a currency from each posting, to be used for balancing them together. We call this the \u201cweight\u201d of a posting, or the balancing amount. Here is a short example of weights derived from postings using the four possible types of cost/price combinations: YYYY-MM-DD Account 10.00 USD -> 10.00 USD Account 10.00 CAD @ 1.01 USD -> 10.10 USD Account 10 SOME {2.02 USD} -> 20.20 USD Account 10 SOME {2.02 USD} @ 2.50 USD -> 20.20 USD Here is the explanation of how it is calculated: If the posting has only an amount and no cost, the balance amount is just the amount and currency on that posting. Using the first example from the previous section, the amount is -400.00 USD, and that is balanced against the 400.00 USD of the second leg. If the posting has only a price , the price is multiplied by the number of units and the price currency is used. In the second example from the preceding section, that is -400.00 USD x 1.09 CAD(/USD) = -436.00 CAD, and that is balanced against the other posting of 436.00 CAD 2 . If the posting has a cost , the cost is multiplied by the number of units and the cost currency is used. In the third example from the preceding section, that is 10 IVV x 183.08 USD(/IVV) = 1830.70 USD. That is balanced against the cash leg of -1830.70 USD, so all is good. Finally, if a posting has both a cost and a price , we simply ignore the price. This optional price is used later on to generate an entry in the in-memory prices database, but it is not used in balancing at all. With this rule, you should be able to easily balance all your transactions. Moreover, this rule makes it possible to let Beancount automatically calculate capital gains for you (see Trading with Beancount for details).","title":"Balancing Rule - The \u201cweight\u201d of postings"},{"location":"beancount_language_syntax.html#reducing-positions","text":"When you post a reduction to a position in an account, the reduction must always match an existing lot. For example, if an account holds 3200 USD and a transaction posts a -1200 USD change to that account, the 1200 USD match against the existing 3200 USD, and the result is a single position of 2000 USD. This also works for negative values. For example, if an account has a -1300 USD balance and you post a +2000 USD change to it, you obtain a 700 USD balance. A change posted to an account, regardless of the account type, can result in a positive or negative balance; there are no limitations on the balances of simple commodity amounts (that is, those with no cost associated to them). For example, while Assets accounts normally have a positive balance and Liabilities accounts usually a negative one, you can legally credit an Assets account to a negative balance, or debit a Liabilities account to a positive balance. This is because in the real world these things do happen: you might write a check too many and obtain temporary credit from your bank\u2019s checking account (usually along with an outrageous \u201coverdraft\u201d fee), or pay that credit card balance twice by mistake. For commodities held at cost, the cost specification of the posting must match one of the lots held in the inventory before the transaction is applied. The list of lots is gathered, and matched against the specification in the {...} part of the posting. For example, if you provide a cost, only those lots whose cost match that will remain. If you provide a date, only those lots which match that date will remain. And you can use a label as well. If you provide a cost and a date, both of these are matched against the list of candidate lots to reduce. This is essentially a filter on the list of lots. If the filtered list results in a single lot, that lot is chosen to be reduced. If the list results in multiple lots, but the total amount being reduced equals the total amount in the lots, all those lots are reduced by that posting. For example, if in the past you had the following transactions: 2014-02-11 * \"Bought shares of S&P 500\" Assets:ETrade:IVV 20 IVV {183.07 USD, \"ref-001\"} \u2026 2014-03-22 * \"Bought shares of S&P 500\" Assets:ETrade:IVV 15 IVV {187.12 USD} \u2026 Each of the following reductions would be unambiguous: 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {183.07 USD} \u2026 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {2014-02-11} \u2026 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {\"ref-001\"} \u2026 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -35 IVV {} \u2026 However, the following would be ambiguous: 2014-05-01 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -20 IVV {} \u2026 If multiple lots match against the reducing posting and their number is not the total number, we are in a situation of ambiguous matches . What happens then, is that the account\u2019s booking method is invoked. There are multiple booking methods, but by default, all accounts are set to use the \u201cSTRICT\u201d booking method. This method simply issues an error in an ambiguous situation. You may set the account\u2019s booking method to \u201cFIFO\u201d to instruct Beancount to select the oldest of the lots. Or \u201cLIFO\u201d for the latest (youngest) of the lots. This will automatically select all the necessary matching lots to fulfill the reduction. PLEASE NOTE! Requiring the dates to match will be dealt with more sensibly in the near future. See A Proposal for an Improvement on Inventory Booking for details of this upcoming change. For such postings, a change that results in a negative number of units is usually impossible. Beancount does not currently allow holding a negative number of a commodity held at cost. For example, an input with just this transaction will fail: 2014-05-23 * Assets:Investments:MSFT -10 MSFT {43.40 USD} Assets:Investments:Cash 434.00 USD If it did not, this would result in a balance of -10 units of MSFT. On the other hand, if the account had a balance of 12 units of MSFT held at 43.40 USD on 5/23, the transaction would book just fine, reducing the existing 12 units to 2. Most often, the error that will occur is that the account will be holding a balance of 10 or more units of MSFT at a different cost , and the user will specify an incorrect value for the cost. For instance, if the account had a positive balance of 20 MSFT {42.10 USD}, the transaction above would still fail, because there aren\u2019t 10 or more units of MSFT at 43.40 USD to remove from. This constraint is enforced for a few reasons: Mistakes in data entry for stock units are not uncommon, they have an important negative impact on the correctness of your Ledger\u2014the amounts are usually large\u2014and they will typically trigger an error from this constraint. Therefore, the error check is a useful way to detect these types of errors. Negative numbers of units held at cost are fairly rare. Chances are you don\u2019t need them at all. Exceptions include: short sales of stock, holding spreads on futures contracts, and depending on how you account for them, short positions in currency trading. This is why this check is enabled by default. PLEASE NOTE! In a future version of Beancount, we will relax this constraint somewhat. We will allow an account to hold a negative number of units of a commodity if and only if there are no other units of that commodity held in the account. Either that, or we will allow you to mark an account as having no such constraints at all. The purpose is to allow the account of short positions in commodities. The only blocking factor is this constraint. For more details of the inventory booking algorithm, see the How Inventories Work document.","title":"Reducing Positions"},{"location":"beancount_language_syntax.html#amount-interpolation","text":"Beancount is able to fill in some of the details of a transaction automatically. You can currently elide the amount of at most one posting within a transaction: 2012-11-03 * \"Transfer to pay credit card\" Assets:MyBank:Checking -400.00 USD Liabilities:CreditCard In the example above, the amount of the credit card posting has been elided. It is automatically calculated by Beancount at 400.00 USD to balance the transaction. This also works with multiple postings, and with postings with costs: 2014-07-11 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -10 IVV {183.07 USD} Assets:ETrade:Cash 1979.90 USD Income:ETrade:CapitalGains In this case, the units of IVV are sold at a higher price ($197.90) than they were bought for ($183.07). The cash first posting has a weight of -10 x 183.07 = -1830.70 and the second posting a straightforward $1979.90. The last posting will be ascribed the difference, that is, a balance of -149.20 USD , which is to say, a gain of $149.20. When calculating the amount to be balanced, the same balance amounts that are used to check that the transaction balances to zero are used to fill in the missing amounts. For example, the following would not trigger an error: 2014-07-11 * \"Sold shares of S&P 500\" Assets:ETrade:IVV -10 IVV {183.07 USD} @ 197.90 USD Assets:ETrade:Cash The cash account would receive 1830.70 USD automatically, because the balance amount of the IVV posting is -1830.70 USD (if a posting has both a cost and a price, the cost basis is always used and the optional price ignored). While this is accepted and correct from a balancing perspective, this would be incomplete from an accounting perspective: the capital gain on a sale needs to be accounted for separately and besides, the amount deposited to the cash account if you did as above would fall short of the real deposit (1979.00 USD) and hopefully a subsequent balance assertion in the cash account would indicate this oversight by triggering an error. Finally, this also works when the balance includes multiple commodities: 2014-07-12 * \"Uncle Bob gave me his foreign currency collection!\" Income:Gifts -117.00 ILS Income:Gifts -3000.00 INR Income:Gifts -800.00 JPY Assets:ForeignCash Multiple postings (one for each commodity required to balance) will be inserted to replace the elided one.","title":"Amount Interpolation"},{"location":"beancount_language_syntax.html#tags","text":"Transactions can be tagged with arbitrary strings: 2014-04-23 * \"Flight to Berlin\" #berlin-trip-2014 Expenses:Flights -1230.27 USD Liabilities:CreditCard This is similar to the popular idea of \u201chash tagging\u201d on Twitter and such. These tags essentially allow you to mark a subset of transactions. They can then be used as a filter to generate reports on only this subset of transactions. They have numerous uses. I like to use them to mark all my trips. Multiple tags can be specified as well: 2014-04-23 * \"Flight to Berlin\" #berlin-trip-2014 #germany Expenses:Flights -1230.27 USD Liabilities:CreditCard (If you want to store key-value pairs on directives, see the section on metadata below.)","title":"Tags"},{"location":"beancount_language_syntax.html#the-tag-stack","text":"Oftentimes multiple transactions related to a single tag will be entered consecutively in a file. As a convenience, the parser can automatically tag transactions within a block of text. How this works is simple: the parser has a \u201cstack\u201d of current tags which it applies to all transactions as it reads them one-by-one. You can push and pop tags onto/from this stack, like this: pushtag #berlin-trip-2014 2014-04-23 * \"Flight to Berlin\" Expenses:Flights -1230.27 USD Liabilities:CreditCard poptag #berlin-trip-2014 This way, you can also push multiple tags onto a long, consecutive set of transactions without having to type them all in.","title":"The Tag Stack"},{"location":"beancount_language_syntax.html#links","text":"Transactions can also be linked together. You may think of the link as a special kind of tag that can be used to group together a set of financially related transactions over time. For example you may use links to group together transactions that are each related with a specific invoice. This allows to track payments (or write-offs) associated with the invoice: 2014-02-05 * \"Invoice for January\" ^invoice-pepe-studios-jan14 Income:Clients:PepeStudios -8450.00 USD Assets:AccountsReceivable 2014-02-20 * \"Check deposit - payment from Pepe\" ^invoice-pepe-studios-jan14 Assets:BofA:Checking 8450.00 USD Assets:AccountsReceivable Or track multiple transfers related to a single nefarious purpose: 2014-02-05 * \"Moving money to Isle of Man\" ^transfers-offshore-17 Assets:WellsFargo:Savings -40000.00 USD Assets:WellsFargo:Checking 40000.00 USD 2014-02-09 * \"Wire to FX broker\" ^transfers-offshore-17 Assets:WellsFargo:Checking -40025.00 USD Expenses:Fees:WireTransfers 25.00 USD Assets:OANDA:USDollar 40000.00 2014-03-16 * \"Conversion to offshore beans\" ^transfers-offshore-17 Assets:OANDA:USDollar -40000.00 USD Assets:OANDA:GBPounds 23391.81 GBP @ 1.71 USD 2014-03-16 * \"God save the Queen (and taxes)\" ^transfers-offshore-17 Assets:OANDA:GBPounds -23391.81 GBP Expenses:Fees:WireTransfers 15.00 GBP Assets:Brittania:PrivateBanking 23376.81 GBP Linked transactions can be rendered by the web interface in their own dedicated journal, regardless of the current view/filtered set of transactions (the list of links is a global page).","title":"Links"},{"location":"beancount_language_syntax.html#balance-assertions","text":"A balance assertion is a way for you to input your statement balance into the flow of transactions. It tells Beancount to verify that the number of units of a particular commodity in some account should equal some expected value at some point in time. For instance, this 2014-12-26 balance Liabilities:US:CreditCard -3492.02 USD says \u201cCheck that the number of USD units in account \u201c Liabilities:US:CreditCard \u201d on the morning of December 26th, 2014 is -3492.02 USD.\u201d When processing the list of entries, if Beancount encounters a different balance than this for USD it will report an error. If no error is reported, you should have some confidence that the list of transactions that precedes it in this account is highly likely to be correct. This is useful in practice, because in many cases some transactions can get imported separately from the accounts of each of their postings (see the de-duping problem ). This can result in you booking the same transaction twice without noticing, and regularly inserting a balance assertion will catch that problem every time. The general format of the Balance directive is: YYYY-MM-DD balance Account Amount Note that a balance assertion, like all other non-transaction directives, applies at the beginning of its date (i.e., midnight at the start of day). Just imagine that the balance check occurs right after midnight on that day. Balance assertions only make sense on balance sheet accounts (Assets and Liabilities). Because the postings on Income and Expenses accounts are only interesting because of their transient value, i.e., for these accounts we\u2019re interested in sums of changes over a period of time (not the absolute value), it makes little sense to use a balance assertion on income statement accounts. Also, take note that each account benefits from an implicit balance assertion that the account is empty after it is opened at the date of its Open directive. You do not need to explicitly assert a zero balance when opening accounts.","title":"Balance Assertions"},{"location":"beancount_language_syntax.html#multiple-commodities","text":"A Beancount account may contain more than one commodity (although in practice, you will find that this does not occur often, it is sensible to create dedicated sub-accounts to hold each commodity, for example, holding a portfolio of stocks). A balance assertion applies only to the commodity of the assertion; it leaves the other commodities in the balance unchecked. If you want to check multiple commodities, use multiple balance assertions, like this: ; Check cash balances from wallet 2014-08-09 balance Assets:Cash 562.00 USD 2014-08-09 balance Assets:Cash 210.00 CAD 2014-08-09 balance Assets:Cash 60.00 EUR There is currently no way to exhaustively check the full list of commodities in an account ( a proposal is underway ). Note that in this example if an exhaustive check really matters to you, you could circumvent by defining a subaccount of the cash account to segregate each commodity separately, like this Assets:Cash:USD , Assets:Cash:CAD .","title":"Multiple Commodities"},{"location":"beancount_language_syntax.html#lots-are-aggregated","text":"The balance assertion applies to the sum of units of a particular commody, irrespective of their cost. For example, if you hold three lots of the same commodity in an account, for example, 5 HOOL {500 USD} and 6 HOOL {510 USD}, the following balance check should succeed: 2014-08-09 balance Assets:Investing:HOOL 11 HOOL All the lots are aggregated together and you can verify their number of units.","title":"Lots Are Aggregated"},{"location":"beancount_language_syntax.html#checks-on-parent-accounts","text":"Balance assertions may be performed on parent accounts, and will include the balances of theirs and their sub-accounts: 2014-01-01 open Assets:Investing 2014-01-01 open Assets:Investing:Apple AAPL 2014-01-01 open Assets:Investing:Amazon AMZN 2014-01-01 open Assets:Investing:Microsoft MSFT 2014-01-01 open Equity:Opening-Balances 2014-06-01 * Assets:Investing:Apple 5 AAPL {578.23 USD} Assets:Investing:Amazon 5 AMZN {346.20 USD} Assets:Investing:Microsoft 5 MSFT {42.09 USD} Equity:Opening-Balances 2014-07-13 balance Assets:Investing 5 AAPL 2014-07-13 balance Assets:Investing 5 AMZN 2014-07-13 balance Assets:Investing 5 MSFT Note that this does require that a parent account have been declared as Open, in order to be legitimately used in the balance assertions directive.","title":"Checks on Parent Accounts"},{"location":"beancount_language_syntax.html#before-close","text":"It is useful to insert a balance assertion for 0 units just before closing an account, just to make sure its contents are empty as you close it. The Close directive does not insert that for you automatically (we may eventually build a plug-in for it).","title":"Before Close"},{"location":"beancount_language_syntax.html#local-tolerance","text":"It's pretty common that sometimes one needs to override the tolerance on the balance check to loosen it on that balance assertion. This can be done using a local tolerance amount off of the balance amount, like this: 2013-09-20 balance Assets:Investing:Funds 319.020 ~ 0.002 RGAGX","title":"Local Tolerance"},{"location":"beancount_language_syntax.html#pad","text":"A padding directive automatically inserts a transaction that will make the subsequent balance assertion succeed, if it is needed. It inserts the difference needed to fulfill that balance assertion. (What \u201crubber space\u201d is in LaTeX, Pad directives are to balances in Beancount.) Note that by \u201csubsequent,\u201d I mean in date order , not in the order of the declarations in the file. This is the conceptual equivalent of a transaction that will automatically expand or contract to fill the difference between two balance assertions over time. It looks like this: 2014-06-01 pad Assets:BofA:Checking Equity:Opening-Balances The general format of the Pad directive is: YYYY-MM-DD pad Account AccountPad The first account is the account to credit the automatically calculated amount to. This is the account that should have a balance assertion following it (if the account does not have a balance assertion, the pad entry is benign and does nothing). The second account is for the other leg of the transaction, it is the source where the funds will come from, and this is almost always some Equity account. The reason for this is that this directive is generally used for initializing the balances of new accounts, to save us from having to either insert such a directive manually, or from having to enter the full past history of transactions that will bring the account to its current balance. Here is a realistic example usage scenario: ; Account was opened way back in the past. 2002-01-17 open Assets:US:BofA:Checking 2002-01-17 pad Assets:US:BofA:Checking Equity:Opening-Balances 2014-07-09 balance Assets:US:BofA:Checking 987.34 USD This will result in the following transaction being inserted right after the Pad directive, on the same date: 2002-01-17 P \"(Padding inserted for balance of 987.34 USD)\" Assets:US:BofA:Checking 987.34 USD Equity:Opening-Balances -987.34 USD This is a normal transaction\u2014you will see it appear in the rendered journals along with the other ones. (Note the special \u201cP\u201d flag, which can be used by scripts to find these.) Observe that without balance assertions, Pad directives make no sense. Therefore, like balance assertions, they are normally only used on balance sheet accounts (Assets and Liabilities). You could also insert Pad entries between balance assertions, it works too. For example: 2014-07-09 balance Assets:US:BofA:Checking 987.34 USD \u2026 more transactions\u2026 2014-08-08 pad Assets:US:BofA:Checking Equity:Opening-Balances 2014-08-09 balance Assets:US:BofA:Checking 1137.23 USD Without intervening transactions, this would insert the following padding transaction: 2014-08-08 P \"(Padding inserted for balance of 1137.23 USD)\" Assets:US:BofA:Checking 149.89 USD Equity:Opening-Balances -149.89 USD In case that\u2019s not obvious, 149.89 USD is the difference between 1137.23 USD and 987.34 USD. If there were more intervening transactions posting amounts to the checking account, the amount would automatically have been adjusted to make the second assertion pass.","title":"Pad"},{"location":"beancount_language_syntax.html#unused-pad-directives","text":"You may not currently leave unused Pad directives in your input file. They will trigger an error: 2014-01-01 open Assets:US:BofA:Checking 2014-02-01 pad Assets:US:BofA:Checking Equity:Opening-Balances 2014-06-01 * \"Initializing account\" Assets:US:BofA:Checking 212.00 USD Equity:Opening-Balances 2014-07-09 balance Assets:US:BofA:Checking 212.00 USD (Being this strict is a matter for debate, I suppose, and it could eventually be moved to an optional plugin.)","title":"Unused Pad Directives"},{"location":"beancount_language_syntax.html#commodities","text":"Note that the Pad directive does not specify any commodities at all. All commodities with corresponding balance assertions in the account are affected. For instance, the following code would have a padding directive insert a transaction with separate postings for USD and CAD: 2002-01-17 open Assets:Cash 2002-01-17 pad Assets:Cash Equity:Opening-Balances 2014-07-09 balance Assets:Cash 987.34 USD 2014-07-09 balance Assets:Cash 236.24 CAD If the account contained other commodities that aren\u2019t balance asserted, no posting would be inserted for those.","title":"Commodities"},{"location":"beancount_language_syntax.html#cost-basis","text":"At the moment, Pad directives do not work with accounts holding positions held at cost. The directive is really only useful for cash accounts. (This is mainly because balance assertions do not yet allow specifying a cost basis to assert. It is possible that in the future we decide to support asserting the total cost basis, and that point we could consider supporting padding with cost basis.)","title":"Cost Basis"},{"location":"beancount_language_syntax.html#multiple-paddings","text":"You cannot currently insert multiple padding entries for the same account and commodity: 2002-01-17 open Assets:Cash 2002-02-01 pad Assets:Cash Equity:Opening-Balances 2002-03-01 pad Assets:Cash Equity:Opening-Balances 2014-04-19 balance Assets:Cash 987.34 USD (There is a proposal pending to allow this and spread the padding amount evenly among all the intervening Pad directives, but it is as of yet unimplemented.)","title":"Multiple Paddings"},{"location":"beancount_language_syntax.html#notes","text":"A Note directive is simply used to attach a dated comment to the journal of a particular account, like this: 2013-11-03 note Liabilities:CreditCard \"Called about fraudulent card.\" When you render the journal, the note should be rendered in context. This can be useful to record facts and claims associated with a financial event. I often use this to record snippets of information that would otherwise not make their way to a transaction. The general format of the Note directive is: YYYY-MM-DD note Account Description The description string may be split among multiple lines.","title":"Notes"},{"location":"beancount_language_syntax.html#documents","text":"A Document directive can be used to attach an external file to the journal of an account: 2013-11-03 document Liabilities:CreditCard \"/home/joe/stmts/apr-2014.pdf\" The filename gets rendered as a browser link in the journals of the web interface for the corresponding account and you should be able to click on it to view the contents of the file itself. This is useful to integrate account statements and other downloads into the flow of accounts, so that they\u2019re easily accessible from a few clicks. Scripts could also be written to obtain this list of documents from an account name and do something with them. The general format of the Document directive is: YYYY-MM-DD document Account PathToDocument","title":"Documents"},{"location":"beancount_language_syntax.html#documents-from-a-directory","text":"A more convenient way to create these entries is to use a special option to specify directories that contain a hierarchy of sub-directories that mirrors that of the chart of accounts. For example, the Document directive shown in the previous section could have been created from a directory hierarchy that looks like this: stmts `-- Liabilities `-- CreditCard `-- 2014-04-27.apr-2014.pdf By simply specifying the root directory as an option (note the absence of a trailing slash): option \"documents\" \"/home/joe/stmts\" The files that will be picked up are those that begin with a date as above, in the YYYY-MM-DD format. You may specify this option multiple times if you have many such document archives. In the past I have used one directory for each year (if you scan all your documents, the directory can grow to a large size, scanned documents tend to be large files). Organizing your electronic document statements and scans using the hierarchy of your ledger\u2019s accounts is a fantastic way to organize them and establish a clear, unambiguous place to find these documents later on, when they\u2019re needed.","title":"Documents from a Directory"},{"location":"beancount_language_syntax.html#prices","text":"Beancount sometimes creates an in-memory data store of prices for each commodity, that is used for various reasons. In particular, it is used to report unrealized gains on account holdings. Price directives can be used to provide data points for this database. A Price directive establishes the rate of exchange between one commodity (the base currency) and another (the quote currency): 2014-07-09 price HOOL 579.18 USD This directive says: \u201cThe price of one unit of HOOL on July 9th, 2014 was 579.18 USD.\u201d Price entries for currency exchange rates work the same way: 2014-07-09 price USD 1.08 CAD The general format of the Price directive is: YYYY-MM-DD price Commodity Price Remember that Beancount knows nothing about what HOOL, USD or CAD are. They are opaque \u201cthings.\u201d You attach meaning to them. So setting a price per hour for your vacation hours is perfectly valid and useful too, if you account for your unused vacations on such terms: 2014-07-09 price VACHR 38.46 USD ; Equiv. $80,000 year","title":"Prices"},{"location":"beancount_language_syntax.html#prices-from-postings","text":"If you use the beancount.plugins.implicit_prices plugin, every time a Posting appears that has a cost or an optional price declared, it will use that cost or price to automatically synthesize a Price directive. For example, this transaction: 2014-05-23 * Assets:Investments:MSFT -10 MSFT {43.40 USD} Assets:Investments:Cash 434.00 USD automatically becomes this after parsing: 2014-05-23 * Assets:Investments:MSFT -10 MSFT {43.40 USD} Assets:Investments:Cash 434.00 USD 2014-05-23 price MSFT 43.40 USD This is convenient and if you enable it, will probably be where most of the price points in your ledger\u2019s price database will come from. You can print a table of the parsed prices from a ledger (it is just another type of report).","title":"Prices from Postings"},{"location":"beancount_language_syntax.html#prices-on-the-same-day","text":"Notice that there is no notion of time; Beancount is not designed to solve problems for intra-day traders, though of course, it certainly is able to handle multiple trades per day. It just stores its prices per day. (Generally, if you need many features that require a notion of intra-day time, you\u2019re better off using another system, this is not the scope of a bookkeeping system.) When multiple Price directives do exist for the same day, the last one to appear in the file will be selected for inclusion in the Price database.","title":"Prices on the Same Day"},{"location":"beancount_language_syntax.html#events","text":"Event directives 3 are used to track the value of some variable of your choice over time. For example, your location: 2014-07-09 event \"location\" \"Paris, France\" The above directive says: \u201cChange the value of the \u2018location\u2019 event to \u2018Paris, France\u2019 as of the 9th of July 2014 and onwards.\u201d A particular event type only has a single value per day. The general format of the Event directive is: YYYY-MM-DD event Name Value The event\u2019s Name string does not have to be declared anywhere, it just begins to exist the first time you use the directive. The Value string can be anything you like; it has no prescribed structure. How to use these is best explained by providing examples: Location : You can use events for tracking the city or country you\u2019re in. This is sensible usage within a ledger because the nature of your expenses are heavily correlated to where you are. It\u2019s convenient: if you use Beancount regularly, it is very little effort to add this bit of information to your file. I usually have a \u201ccash daybook\u201d section where I put miscellaneous cash expenses, and that\u2019s where I enter those directives. Address : If you move around a lot, it\u2019s useful to keep a record of your past home addresses. This is sometimes requested on government forms for immigration. The Green Card application process in the US, for instance, Employer : You can record your date of employment and departure for each job this way. Then you can count the number of days you worked there. Trading window : If you\u2019re an employee of a public company, you can record the dates that you\u2019re allowed to trade its stock. This can then be used to ensure you did not trade that stock when the window is closed. Events are often used to report on numbers of days. For example, in the province of Quebec (Canada) you are insured for free health care coverage if \u201c you spend 183 days of the calendar year or more, excluding trips of 21 days or less .\u201d If you travel abroad a lot, you could easily write a script to warn you of remaining day allocations to avoid losing your coverage. Many expats are in similar situations. In the same vein, the IRS recognizes US immigrants as \u201cresident alien\u201d\u2014and thus subject to filing taxes, yay!\u2014if you pass the Substantial Presence Test , which is defined as \u201cbeing physically present in the US on at least 31 days during the current year, and 193 days during the 3 year period that includes the current year and the 2 years immediately before that, counting: all the days you were present in the current year, and \u2153 of the days of the year before that, and \u2159 of the year preceding that one.\u201d Ouch\u2026 my head hurts. This gets complicated. Armed with your location over time, you could report it automatically. Events will also be usable in the filtering language, to specify non-contiguous periods of time. For example, if you are tracking your location using Event directives, you could produce reports for transactions that occur only when you are in a specific location, e.g., \u201cShow me my expenses on all my trips to Germany,\u201d or \u201cGive me a list of payees for restaurant transactions when I\u2019m in Montreal.\u201d PLEASE NOTE! Filters haven\u2019t been implemented yet. Also, reports on events have not yet been re-implemented in Beancount 2.0. They will be reintroduced again soon, as well as filtering. It is worth noticing that the Price and Event directives are the only ones not associated to an account.","title":"Events"},{"location":"beancount_language_syntax.html#query","text":"It can be convenient to be able to associate SQL queries in a Beancount file to be able to run these as a report automatically. This is still an early development / experimental directive. In any case, you can insert queries in the stream of transactions like this: 2014-07-09 query \"france-balances\" \" SELECT account, sum(position) WHERE \u2018trip-france-2014\u2019 in tags\" The grammar is YYYY-MM-DD query Name SqlContents Each query has a name, which will probably be used to invoke its running as a report type. Also, the date of the query is intended to be the date at which the query is intended to be run for, that is, transactions following it should be ignored. If you\u2019re familiar with the SQL syntax, it\u2019s an implicit CLOSE.","title":"Query"},{"location":"beancount_language_syntax.html#custom","text":"The long-term plan for Beancount is to allow plugins and external clients to define their own directive types, to be declared and validated by the Beancount input language parser. In the meantime, a generic directive is provided for clients to prototype new features, e.g., budgeting. 2014-07-09 custom \"budget\" \"...\" TRUE 45.30 USD The grammar for this directive is flexible: YYYY-MM-DD custom TypeName Value1 ... The first argument is a string and is intended to be unique to your directive. Think of this as the type of your directive. Following it, you can put an arbitrary list of strings, dates, booleans, amounts, and numbers. Note that there is no validation that checks that the number and types of arguments following the TypeName is consistent for a particular type. (See this thread for the origin story around this feature.)","title":"Custom"},{"location":"beancount_language_syntax.html#metadata_1","text":"You may attach arbitrary data to each of your entries and postings. The syntax for it looks like this: 2013-03-14 open Assets:BTrade:HOOLI category: \"taxable\" 2013-08-26 * \"Buying some shares of Hooli\" statement: \"confirmation-826453.pdf\" Assets:BTrade:HOOLI 10 HOOL @ {498.45 USD} decision: \"scheduled\" Assets:BTrade:Cash In this example, a \u201ccategory\u201d attribute is attached to an account\u2019s Open directive, a \u201cstatement\u201d attribute is attached to a Transaction directive (with a string value that represents a filename) and a \u201cdecision\u201d attribute has been attached to the first posting of the transaction (the additional indentation from the posting is not strictly necessary but it helps with readability). Metadata can be attached to any directive type. Keys must begin with a lowercase character from a-z and may contain (uppercase or lowercase) letters, numbers, dashes and underscores. Moreover, the values can be any of the following data types: Strings Accounts Currency Dates (datetime.date) Tags Numbers (Decimal) Amount (beancount.core.amount.Amount) There are two ways in which this data can be used: Query tools that come with Beancount (such as bean-query) will allow you to make use of the metadata values for filtering and aggregation. You can access and use the metadata in custom scripts. The metadata values are accessible as a \u201c .meta \u201d attribute on all directives and is a Python dict. There are no special meanings attached to particular attributes, these are intended for users to define. However, all directives are guaranteed to contain a \u2018 filename \u2019 (string) and a \u2018 lineno \u2019 (integer) attribute which reflect the location they were created from. Finally, attributes without a value will be parsed and have a value of 'None'. If an attribute is repeated multiple times, only the first value for this attribute will be parsed and retained and the following values ignored.","title":"Metadata"},{"location":"beancount_language_syntax.html#options","text":"The great majority of a Beancount input file consists in directives, as seen in the previous section. However, there are a few global options that can be set in the input file as well, by using a special undated \u201coption\u201d directive: option \"title\" \"Ed\u2019s Personal Ledger\" The general format of the Option directive is: option Name Value where Name and Value are both strings. Note that depending the option, the effect may be to set a single value, or add to a list of existing values. In other words, some options are lists. There are three ways to view the list of options: In this document , which I update regularly. To view the definitive list of options supported by your installed version, use the following command: bean-doctor list-options Finally, you can peek at the source code as well.","title":"Options"},{"location":"beancount_language_syntax.html#operating-currencies","text":"One notable option is \u201c operating_currency \u201d. By default Beancount does not treat any of the commodities any different from each other. In particular, it doesn\u2019t know that there\u2019s anything special about the most common of commodities one uses: their currencies. For example, if you live in New Zealand, you\u2019re going to have an overwhelming number of NZD commodities in your transactions. But useful reports try to reduce all non-currency commodities into one of the main currencies used. Also, it\u2019s useful to break out the currency units into their own dedicated columns. This may also be useful for exporting in order to avoid having to specify the units for that column and import to a spreadsheet with numbers you can process. For this reason, you are able to declare the most common currencies you use in an option: option \"operating_currency\" \"USD\" You may declare more than one. In any case, this option is only ever used by reporting code, it never changes the behavior of Beancount\u2019s processing or semantics.","title":"Operating Currencies"},{"location":"beancount_language_syntax.html#plugins","text":"In order to load plugin Python modules, use the dedicated \u201cplugin\u201d directive: plugin \"beancount.plugins.module_name\" The name of a plugin should be the name of a Python module in your PYTHONPATH. Those modules will be imported by the Beancount loader and run on the list of parsed entries in order for the plugins to transform the entries or output errors. This allows you to integrate some of your code within Beancount, making arbitrary transformations on the entries. See Scripting & Plugins for details. Plugins also optionally accept some configuration parameters. These can be provided by an optional final string argument, like this: plugin \"beancount.plugins.module_name\" \"configuration data\" The general format of the Option directive is: plugin ModuleName StringConfig The format of the configuration data is plugin-dependent. At the moment, an arbitrary string is passed provided to the plugin. See the plugins\u2019 documentation for specific detail on what it can accept. Also see the \u201cplugin processing mode\u201d option which affects the list of built-in plugins that get run.","title":"Plugins"},{"location":"beancount_language_syntax.html#includes","text":"Include directives are supported. This allows you to split up large input files into multiple files. The syntax looks like this: include \"path/to/include/file.beancount\" The general format is include Filename The specified path can be an absolute or a relative filename. If the filename is relative, it is relative to the including filename\u2019s directory. This makes it easy to put relative includes in a hierarchy of directories that can be placed under source control and checked out anywhere. Include directives are not processed strictly (as in C, for example). The include directives are accumulated by the Beancount parser and processed separately by the loader. This is possible because the order of declarations of a Beancount input file is not relevant. However, for the moment, options are parsed per-file. The options-map that is kept for post-parse processing is the options-map returned for the top-level file. This is probably subject to review in the future.","title":"Includes"},{"location":"beancount_language_syntax.html#whats-next","text":"This document described all the possible syntax of the Beancount language. If you haven\u2019t written any Beancount input yet, you can head to the Getting Started guide, or browse through a list of practical use cases in the Command-line Accounting Cookbook . Note that there exists an \u201cOpen\u201d directive that is used to provide the start date of each account. That can be located anywhere in the file, it does not have to appear in the file somewhere before you use an account name. You can just start using account names in transactions right away, though all account names that receive postings to them will eventually have to have a corresponding Open directive with a date that precedes all transactions posted to the account in the input file. \u21a9 Note that this is valid whether the price is specified as a per-unit price with the @ syntax or as a total price using the @@ syntax. \u21a9 I really dislike the name \u201cevent\u201d for this directive. I\u2019ve been trying to find a better alternative, so far without success. The name \u201cregister\u201d might be more appropriate, as it resembles that of a processor\u2019s register, but that could be confused with an account register report. \u201cvariable\u201d might work, but somehow that just sounds too computer-sciency and out of context. If you can think of something better, please make a suggestion and I\u2019ll seriously entertain a complete rename (with legacy support for \u201cevent\u201d). \u21a9","title":"What\u2019s Next?"},{"location":"beancount_options_reference.html","text":"option \"title\" \"Joe Smith's Personal Ledger\" The title of this ledger / input file. This shows up at the top of every page. option \"name_assets\" \"Assets\" option \"name_liabilities\" \"Liabilities\" option \"name_equity\" \"Equity\" option \"name_income\" \"Income\" option \"name_expenses\" \"Expenses\" Root names of every account. This can be used to customize your category names, so that if you prefer \"Revenue\" over \"Income\" or \"Capital\" over \"Equity\", you can set them here. The account names in your input files must match, and the parser will validate these. You should place these options at the beginning of your file, because they affect how the parser recognizes account names. option \"account_previous_balances\" \"Opening-Balances\" Leaf name of the equity account used for summarizing previous transactions into opening balances. option \"account_previous_earnings\" \"Earnings:Previous\" Leaf name of the equity account used for transferring previous retained earnings from income and expenses accrued before the beginning of the exercise into the balance sheet. option \"account_previous_conversions\" \"Conversions:Previous\" Leaf name of the equity account used for inserting conversions that will zero out remaining amounts due to transfers before the opening date. This will essentially \"fixup\" the basic accounting equation due to the errors that priced conversions introduce. option \"account_current_earnings\" \"Earnings:Current\" Leaf name of the equity account used for transferring current retained earnings from income and expenses accrued during the current exercise into the balance sheet. This is most often called \"Net Income\". option \"account_current_conversions\" \"Conversions:Current\" Leaf name of the equity account used for inserting conversions that will zero out remaining amounts due to transfers during the exercise period. option \"account_rounding\" \"Rounding\" The name of an account to be used to post to and accumulate rounding error. This is unset and this feature is disabled by default; setting this value to an account name will automatically enable the addition of postings on all transactions that have a residual amount. option \"conversion_currency\" \"NOTHING\" The imaginary currency used to convert all units for conversions at a degenerate rate of zero. This can be any currency name that isn't used in the rest of the ledger. Choose something unique that makes sense in your language. option \"inferred_tolerance_default\" \"CHF:0.01\" option \"default_tolerance\" \"CHF:0.01\" THIS OPTION IS DEPRECATED: This option has been renamed to 'inferred_tolerance_default' Mappings of currency to the tolerance used when it cannot be inferred automatically. The tolerance at hand is the one used for verifying (1) that transactions balance, (2) explicit balance checks from 'balance' directives balance, and (3) in the precision used for padding (from the 'pad' directive). The values must be strings in the following format: : for example, 'USD:0.005'. By default, the tolerance used for currencies without an inferred value is zero (which means infinite precision). As a special case, this value, that is, the fallabck value used for all currencies without an explicit default can be overridden using the '*' currency, like this: '*:0.5'. Used by itself, this last example sets the fallabck tolerance as '0.5' for all currencies. (Note: The new value of this option is \"inferred_tolerance_default\"; it renames the option which used to be called \"default_tolerance\". The latter name was confusing.) For detailed documentation about how precision is handled, see this doc: http://furius.ca/beancount/doc/tolerances (This option may be supplied multiple times.) option \"inferred_tolerance_multiplier\" \"1.1\" A multiplier for inferred tolerance values. When the tolerance values aren't specified explicitly via the 'inferred_tolerance_default' option, the tolerance is inferred from the numbers in the input file. For example, if a transaction has posting with a value like '32.424 CAD', the tolerance for CAD will be inferred to be 0.001 times some multiplier. This is the muliplier value. We normally assume that the institution we're reproducing this posting from applies rounding, and so the default value for the multiplier is 0.5, that is, half of the smallest digit encountered. You can customize this multiplier by changing this option, typically expanding it to account for amounts slightly beyond the usual tolerance, for example, if you deal with institutions with bad of unexpected rounding behaviour. For detailed documentation about how precision is handled, see this doc: http://furius.ca/beancount/doc/tolerances option \"infer_tolerance_from_cost\" \"True\" Enable a feature that expands the maximum tolerance inferred on transactions to include values on cost currencies inferred by postings held at-cost or converted at price. Those postings can imply a tolerance value by multiplying the smallest digit of the unit by the cost or price value and taking half of that value. For example, if a posting has an amount of \"2.345 RGAGX {45.00 USD}\" attached to it, it implies a tolerance of 0.001 x 45.00 * M = 0.045 USD (where M is the inferred_tolerance_multiplier) and this is added to the mix to enlarge the tolerance allowed for units of USD on that transaction. All the normally inferred tolerances (see http://furius.ca/beancount/doc/tolerances) are still taken into account. Enabling this flag only makes the tolerances potentially wider. option \"tolerance\" \"0.015\" THIS OPTION IS DEPRECATED: The 'tolerance' option has been deprecated and has no effect. The tolerance allowed for balance checks and padding directives. In the real world, rounding occurs in various places, and we need to allow a small (but very small) amount of tolerance in checking the balance of transactions and in requiring padding entries to be auto- inserted. This is the tolerance amount, which you can override. option \"use_legacy_fixed_tolerances\" \"True\" Restore the legacy fixed handling of tolerances. Balance and Pad directives have a fixed tolerance of 0.015 units, and Transactions balance at 0.005 units. For any units. This is intended as a way for people to revert the behavior of Beancount to ease the transition to the new inferred tolerance logic. See http://furius.ca/beancount/doc/tolerances for more details. option \"documents\" \"/path/to/your/documents/archive\" A list of directory roots, relative to the CWD, which should be searched for document files. For the document files to be automatically found they must have the following filename format: YYYY-MM-DD.(.*) (This option may be supplied multiple times.) option \"operating_currency\" \"USD\" A list of currencies that we single out during reporting and create dedicated columns for. This is used to indicate the main currencies that you work with in real life. (Refrain from listing all the possible currencies here, this is not what it is made for; just list the very principal currencies you use daily only.) Because our system is agnostic to any unit definition that occurs in the input file, we use this to display these values in table cells without their associated unit strings. This allows you to import the numbers in a spreadsheet (e.g, \"101.00 USD\" does not get parsed by a spreadsheet import, but \"101.00\" does). If you need to enter a list of operating currencies, you may input this option multiple times, that is, you repeat the entire directive once for each desired operating currency. (This option may be supplied multiple times.) option \"render_commas\" \"TRUE\" A boolean, true if the number formatting routines should output commas as thousand separators in numbers. option \"plugin_processing_mode\" \"raw\" A string that defines which set of plugins is to be run by the loader: if the mode is \"default\", a preset list of plugins are automatically run before any user plugin. If the mode is \"raw\", no preset plugins are run at all, only user plugins are run (the user should explicitly load the desired list of plugins by using the 'plugin' option. This is useful in case the user wants full control over the ordering in which the plugins are run). option \"plugin\" \"beancount.plugins.module_name\" THIS OPTION IS DEPRECATED: The 'plugin' option is deprecated; it should be replaced by the 'plugin' directive A list of Python modules containing transformation functions to run the entries through after parsing. The parser reads the entries as they are, transforms them through a list of standard functions, such as balance checks and inserting padding entries, and then hands the entries over to those plugins to add more auto-generated goodies. The list is a list of pairs/tuples, in the format (plugin-name, plugin-configuration). The plugin-name should be the name of a Python module to import, and within the module we expect a special '__plugins__' attribute that should list the name of transform functions to run the entries through. The plugin-configuration argument is an optional string to be provided by the user. Each function accepts a pair of (entries, options_map) and should return a pair of (new entries, error instances). If a plugin configuration is provided, it is provided as an extra argument to the plugin function. Errors should not be printed out the output, they will be converted to strings by the loader and displayed as dictated by the output medium. (This option may be supplied multiple times.) option \"long_string_maxlines\" \"64\" The number of lines beyond which a multi-line string will trigger a overly long line warning. This warning is meant to help detect a dangling quote by warning users of unexpectedly long strings. option \"experiment_explicit_tolerances\" \"True\" Enable an EXPERIMENTAL feature that supports an explicit tolerance value on Balance assertions. If enabled, the balance amount supports a tolerance in the input, with this syntax: ~ , for example, \"532.23 ~ 0.001 USD\". See the document on tolerances for more details: http://furius.ca/beancount/doc/tolerances WARNING: This feature may go away at any time. It is an exploration to see if it is truly useful. We may be able to do without. option \"booking_method\" \"SIMPLE\" The booking method to apply, for interpolation and for matching lot specifications to the available lots in an inventory at the moment of the transaction. Values may be 'SIMPLE' for the original method used in Beancount, or 'FULL' for the newer method that does fuzzy matching against the inventory and allows multiple amounts to be interpolated (see http://furius.ca/beancount/doc/proposal-booking for details).","title":"Beancount Options Reference"},{"location":"beancount_query_language.html","text":"Beancount Query Language \uf0c1 Martin Blais, January 2015 http://furius.ca/beancount/doc/query Introduction \uf0c1 The purpose of Beancount is to allow the user to create an accurate and error-free representation of financial transactions, typically those occurring in a user or in an institution\u2019s associated set of accounts, to then extract various reports from this list of transactions. Beancount provides a few tools to extract reports from the corpus of transactions: custom reports (using the Beancount bean-report tool), a web interface (using the bean-web tool) and the ability for the user to write their own scripts to output anything they want. The repository of financial transactions is always read from the text file input, but once parsed and loaded in memory, extracting information from Beancount could be carried out much like you would another database, that is, instead of using custom code to generate output from the data structures, a query language could be compiled and run over the relatively regular list of transactions. In practice, you could flatten out the list of postings to an external SQL database and make queries using that database\u2019s own tools, but the results of this approach are quite disappointing, mainly due to the lack of operations on inventories which is the basis of balancing rules in Beancount. By providing a slightly specialized query engine that takes advantage of the structure of the double-entry transactions we can easily generate custom reports specific to accounting purposes. This document describes our specialized SQL-like query client. It assumes you have at least a passing knowledge of SQL syntax. If not, you may want to first read something about it. Motivation \uf0c1 So one might ask: Why create another SQL client? Why not output the data to an SQLite database and allow the user to use that SQL client? Well, we have done that (see the bean-sql script which converts your Beancount ledger into an SQLite database) and the results are not great. Writing queries is painful and carrying out operations on lots that are held at cost is difficult. By taking advantage of a few aspects of our in-memory data structures, we can do better. So Beancount comes with its own SQL-like query client called \u201c bean-query \u201d. The clients implements the following \u201cextras\u201d that are essential to Beancount: It allows to easily filter at two levels simultaneously: You can filter whole transactions, which has the benefit of respecting the accounting equation, and then, usually for presentation purposes, you can also filter at the postings level. The client supports the semantics of inventory booking implemented in Beancount. It also supports aggregation functions on inventory objects and rendering functions (e.g., COST() to render the cost of an inventory instead of its contents). The client allows you to flatten multiple lots into separate postings to produce lists of holdings each with their associated cost basis. Transactions can be summarized in a manner useful to produce balance sheets and income statements. For example, our SQL variant explicitly supports a \u201cclose\u201d operation with an effect similar to closing the year, which inserts transactions to clear income statement accounts to equity and removes past history. See this post as well for a similar answer. Warning & Caveat \uf0c1 Approximately 70% of the features desired in the original design doc were implemented in the query language in late 2014. More work will be needed to cover the full feature set, but the current iteration supports most of the use cases covered by Ledger, and I suspect by Beancount users. More feedback is desired on the current version before moving on, and I would like to move forward improving some of the more fundamental aspects of Beancount (namely, inventory booking) before spending more time on the query language. It is functional as it is, but a second revision will be made later on, informed by user feedback and prolonged use. Therefore, a first release of the query language has been merged in the default stable branch. This document presents this first iteration on the Beancount query language. Making Queries \uf0c1 The custom query client that we provide is called bean-query . Run it on your ledger file, like this: $ bean-query myfile.beancount Input file: \"My Ledger\u2019s Title\" Ready with 13996 directives (21112 postings in 8833 transactions). beancount> _ This launches the query tool in interactive mode, where you can enter multiple commands on the dataset loaded in memory. bean-query parses the input file, spits out a few basic statistics about your ledger, and provides a command prompt for you to enter query commands. You can type \u201c help \u201d here to view the list of available commands. If any errors in your ledger are incurred, they are printed before the prompt. To suppress error printing, run the tool with the \u201cno-errors\u201d option: $ bean-query -q myfile.beancount Batch Mode Queries \uf0c1 If you\u2019d like to run queries directly from the command-line, without an interactive prompt, you can provide the query directly following your filename: $ bean-query myfile.beancount 'balances from year = 2014' account balance ---------------------------------------------------------------------- \u2026 \u2026 All the interactive commands are supported. \uf0c1 Shell Variables \uf0c1 The interactive shell has a few \u201c set \u201d variables that you can customize to change some of the behavior of the shell. These are like environment variables. Type the \u201c set \u201d command to see the list of available variables and their current value. The variables are: format (string): The output format. Currently, only \u201ctext\u201d is supported. boxed (boolean): Whether we should draw a box around the output table. spaced (boolean): Whether to insert an empty line between every result row. This is only relevant because postings with multiple lots may require multiple lines to be rendered, and inserting an empty line helps delineate those as separate. pager (string): The name of the pager program to pipe multi-page output to when the output is larger than the screen. The initial value is copied from the PAGER environment variable. expand (boolean): If true, expand columns that render to lists on multiple rows. Transactions and Postings \uf0c1 The structure of transactions and entries can be explained by the following simplified diagram: \uf0c1 The contents of a ledger is parsed into a list of directives, most of which are \u201cTransaction\u201d objects which contain two or more \u201cPosting\u201d objects. Postings are always linked only to a single transaction (they are never shared between transactions). Each posting refers to its parent transaction but has a unique account name, amount and associated lot (possibly with a cost), a price and some other attributes. The parent transaction itself contains a few useful attributes as well, such as a date, the name of a payee, a narration string, a flag, links, tags, etc. If we ignore the list of directives other than transactions, you can view the dataset as a single table of all postings joined with their parent transaction. It is mainly on this joined table of postings that we want to perform filtering and aggregation operations. However, because of the double-entry bookkeeping constraint, that is, the sum of amounts on postings attached to a transaction is zero, it is also quite useful to perform filtering operations at the transaction level. Because any isolated transaction has a total impact of zero on the global balance, any subset of transactions will also respect the accounting equation (Assets + Liabilities + Equity + Income + Expenses = 0), and producing balance sheets and income statements on subset of transactions provides meaningful views, for example, \u201call asset changes and expenses incurred during a trip to the Bahamas\u201d which could be selected by a tag. For this reason, we modify the SQL SELECT syntax to provide a two-level filtering syntax: since we have a single table of data, we replace the table name in FROM by a filtering expression which applies over transactions, and the WHERE clause applies to data pulled from the resulting list of postings: SELECT , , \u2026 FROM WHERE ; Both filtering expressions are optional. If no filtering expressions are provided, all postings will be enumerated over. Note that since the transactions are always filtered in date order, the results will be processed and returned in this order by default. Posting Data Columns \uf0c1 The list of targets refers to attributes of postings or of their parent transaction. The same list of \u201ccolumns\u201d is made available in the , to filter by posting attributes. For example, you could write the following query: SELECT date, narration, account, position WHERE account ~ \u201c.*:Vacation\u201d AND year >= 2014; Here, the \u201cdate\u201d, \u201cyear\u201d and \u201cnarration\u201d columns refer to attributes of the parent transaction, the \u201caccount\u201d and \u201cposition\u201d columns refer to attributes of the posting itself. You may name targets explicitly with the familiar AS operator: SELECT last(date) as last_date, cost(sum(position)) as cost; The full list of posting columns and functions available on them is best viewed by querying your actual client using \u201c help targets \u201c or \u201c help where \u201d, which prints out the list and data type of each available data column. You may also refer to the following diagram of the structure of a Posting object for the correspondence between the columns and the data structure attributes. Entry Data Columns \uf0c1 A different list of column names is available on the of the FROM clause. These columns refer to attributes of the Transaction objects. This clause is intended to filter whole transactions (i.e., all their postings or none at all). Available attributes include the date, transaction flag, the optional payee, narration, set of tags and links. Use the \u201c help from \u201d command to find the complete list of columns and functions available in this clause. A Beancount input file consists of many different types of entries, not just transactions. Some of these other types of entries (such as Open, Close, Balance, etc.) may also provide attributes that can be accessed from the FROM clause. This is embryonic at this point. (It\u2019s unclear yet how these will be used in the future, but I suspect we will find some interesting applications for them eventually. The FROM clause provides access to the type of the data entry via column \u201c type \u201d. It\u2019s still an exploration how much we can make pragmatic use of the SQL language for other types of directives.) The \u201cid\u201d Column A special column exists that identifies each transaction uniquely: \u201c id \u201d. It is a unique hash automatically computed from the transaction and should be stable between runs. SELECT DISTINCT id; This hash is derived from the contents of the transaction object itself (if you change something about the transaction, e.g. you edit the narration, the id will change). You can print and select using this column. It can be used for debugging, e.g. PRINT FROM id = '8e7c47250d040ae2b85de580dd4f5c2a'; The \u201cbalance\u201d Column One common desired output is a journal of entries over time (also called a \u201cregister\u201d in Ledger): SELECT date, account, position WHERE account ~ \"Chase:Slate\"; For this type of report, it is convenient to also render a column of the cumulative balance of the selected postings rows. Access to the previous row is not a standard SQL feature, so we get a little creative and provide a special column called \u201c balance \u201d which is automatically calculated based on the previous selected rows: SELECT date, account, position, balance WHERE account ~ \"Chase:Slate\"; This provides the ability to render typical account statements such as those mailed to you by a bank. Output might look like this: $ bean-query $T \"select date, account, position, balance where account ~ 'Expenses:Food:Restaurant';\" date account position balance ---------- ------------------------ --------- ---------- 2012-01-02 Expenses:Food:Restaurant 31.02 USD 31.02 USD 2012-01-04 Expenses:Food:Restaurant 25.33 USD 56.35 USD 2012-01-08 Expenses:Food:Restaurant 67.88 USD 124.23 USD 2012-01-09 Expenses:Food:Restaurant 35.28 USD 159.51 USD 2012-01-14 Expenses:Food:Restaurant 25.84 USD 185.35 USD 2012-01-17 Expenses:Food:Restaurant 36.73 USD 222.08 USD 2012-01-21 Expenses:Food:Restaurant 28.11 USD 250.19 USD 2012-01-22 Expenses:Food:Restaurant 21.12 USD 271.31 USD Wildcard Targets \uf0c1 Using a wildcard as the target list (\u201c*\u201d) select a good default list of columns: SELECT * FROM year = 2014; To view the actual list of columns selected, you can use the EXPLAIN prefix: EXPLAIN SELECT * FROM year = 2014; Data Types \uf0c1 The data attributes extracted from the postings or transactions have particular types. Most of the data types are regular types as provided by the underlying Python implementation language, types such as String (Python str) Date (a datetime.date instance). You can parse a date with the #\"...\" syntax; this uses Python\u2019s dateutil module and is pretty liberal in the formats it accepts. Integer (Python int) Boolean (Python bool object), as TRUE , FALSE Number (a decimal.Decimal object) Set of Strings (a Python set of str objects) Null objects ( NULL ) Positions and Inventories \uf0c1 However, one reason that our SQL-like client exists in the first place is for its ability to carry out aggregation operations on inventories of positions, the data structures at the core of Beancount, that implements its balancing semantics. Internally, Beancount defines Position and Inventory objects and is able to aggregate them together in an instance of Inventory. On each Posting, the \u201cposition\u201d column extracts an object of type Position, which when summed over produces an instance of Inventory. The shell is able to display those appropriately. More specifically, Inventory objects can contain multiple different lots of holdings, and each of these will get rendered on a separate line. Quantities of Positions and Inventories \uf0c1 Objects of type Position are rendered in their full detail by default, including not just their number and currency, but the details of their lot. Inventories consist of a list of lots, and as such are rendered similarly, as a list of positions (one per line) each with their full detail by default. This is generally too much detail. The shell provides functions that allow the user to summarize the positions into one of the various derived quantities. The types of derived quantities are: \u201c raw \u201d: render the position in its full detail, including cost and lot date \u201c units \u201d: render just the number and currency of the position \u201c cost \u201d: render the total cost of the position, that is the number of units x the per-unit cost \u201c weight \u201d: render the amount that is used to balance the postings of a transaction. The main distinction between cost and weight is for postings with a price conversion. \u201c value \u201d: render the amount at the market value of the last entry rendered. Functions with the same names are available to operate on position or inventory objects. For example, one could generate a table of final balances for each account like this: SELECT account, units(sum(position)), cost(sum(position)) GROUP BY 1; Refer to the table below for explicit examples of each type of posting and how it would get converted and rendered. posting raw (full detail) units cost weight market Simple 50.00 USD 50.00 USD 50.00 USD 50.00 USD 50.00 USD With Price Conversion 50.00 USD @ 1.35 CAD 50.00 USD 50.00 USD 67.50 CAD 50.00 USD Held at Cost 50 VEA {1.35 CAD} 50 VEA 67.50 CAD 67.50 CAD 67.50 CAD Held at Cost with Price 50 VEA {1.35 CAD} @ 1.45 CAD 50 VEA 67.50 CAD 67.50 CAD 72.50 CAD Operators \uf0c1 Common comparison and logical operators are provided to operate on the available data columns: = (equality), != (inequality) < (less than), <= (less than or equal) (greater than), >= (greater than or equal) AND (logical conjunction) OR (logical disjunction) NOT (logical negation) IN (set membership) We also provide a regular expression search operator into a string object: ~ (search regexp) At the moment, matching groups are ignored. You can use string, number and integer constants with those operators, and parentheses to explicitly state precedence. You can use the #\u201d...\u201d literal syntax to input dates (valid contents for the string are pretty liberal, it supports anything Python\u2019s dateutil.parser supports). Here is an example query that uses a few of these: SELECT date, payee WHERE account ~ 'Expenses:Food:Restaurant' AND 'trip-new-york' IN tags AND NOT payee = 'Uncle Boons' Unlike SQL, bean-query does not implement three-valued logic for NULL . This means that e.g. the expression NULL = NULL yields TRUE instead of NULL , which simplifies things, but may come as a surprise to veteran SQL users. Simple Functions \uf0c1 The shell provides a list of simple function that operate on a single data column and return a new value. These functions operate on particular types. The shell implements rudimentary type verification and should be able to warn you on incompatible types. Some example functions follow: COST(Inventory), COST(Position): Return an Amount, the cost of the position or inventory. UNITS(Inventory), UNITS(Position): Return the units of the position or inventory. DAY(date), MONTH(date), YEAR(date): Return an integer, the day, month or year of the posting or entry\u2019s date. LENGTH(list): Computes the length of a list or a set, e.g. on tags. PARENT(account-string): Returns the name of the parent account. These are just examples; for the complete list, see \u201c help targets \u201d, \u201c help where \u201d, \u201c help from \u201d. Note that it is exceedingly easy to add new functions to this list. As of December 2014, we are just beginning using the shell widely and we expect to be adding new functions as needed. If you need a function, please add a comment here or log a ticket and we will consider adding it to the list (we understand that the current list is limited). I intend to be liberal about adding new functions; as long as they have generic application, I don\u2019t think it should be a problem. Otherwise, I may be able to provide a mechanism for user to register new functions as part of Python plugins that could live outside the Beancount codebase. Aggregate Functions \uf0c1 Some functions operate on more than a single row. These functions aggregate and summarize the multiple values for the data column that they operate on. A prototypical usage of such a function is to sum the positions in an inventory: SELECT account, sum(position) WHERE account ~ 'Income' GROUP BY account; If a query target has at least one aggregating function, the query becomes an aggregated query (see relevant section for details). Note that you cannot use aggregation functions in the FROM or WHERE clauses. Examples of aggregate functions include: COUNT(...): Computes the number of postings selected (an integer). FIRST(...), LAST(...): Returns first or last value seen. MIN(...), MAX(...): Computes the minimum or maximum value seen. SUM(...): Sums up the values of each set. This works on amounts, positions, inventories, numbers, etc. As for simple functions, this is just a starting list. We will be adding more as needed. Use \u201c help targets \u201d to access the full list of available aggregate functions. Note: You cannot filter (using a WHERE clause) the results of aggregation functions; this requires the implementation offering a HAVING clause, and at the moment, HAVING filtering is not yet implemented. Simple vs. Aggregated Queries \uf0c1 There are two types of queries: Simple queries , which produce a row of results for each posting that matches the restricts in the WHERE clause. Aggregate queries , which produce a row of results for each group of postings that match the restricts in the WHERE clause. A query is \u201caggregate\u201d if it has at least one aggregate function in its list of targets. In order to identify the aggregation keys, all the non-aggregate columns have to be flagged using the GROUP BY clause, like this: SELECT payee, account, COST(SUM(position)), LAST(date) GROUP BY payee, account; You may also use the positional order of the targets to declare the group key, like this: SELECT payee, account, COST(SUM(position)), LAST(date) GROUP BY 1, 2; Furthermore, if you name your targets, you can use the explicit target names: SELECT payee, account as acc, COST(SUM(position)), LAST(date) GROUP BY 1, acc; This should all feel familiar if you have preliminary knowledge of SQL. Finally, because we implement a limited version of SQL, and that the simple columns must always be specified, omitting the GROUP BY clause should also eventually work and we should group by those columns implicitly, as a convenience. Distinct \uf0c1 There is a post-filtering phase that supports uniquifying result rows. You can trigger this unique filter with the DISTINCT flag after SELECT , as is common in SQL, e.g. SELECT DISTINCT account; Controlling Results \uf0c1 Order By \uf0c1 Analogous to the GROUP BY clause is an ORDER BY clause that controls the final ordering of the result rows: SELECT \u2026 GROUP BY account, payee ORDER BY payee, date; The clause is optional. If you do not specify it, the default order of iteration of selected postings is used to output the results (that is, the order of transactions-sorted by date- and then their postings). As in SQL, you may reverse the order of sorting by a DESC suffix (the default is the same as specifying ASC ): SELECT \u2026 GROUP BY account, payee ORDER BY payee, date DESC; Limit \uf0c1 Our query language also supports a LIMIT clause to interrupt output row generation: SELECT \u2026 LIMIT 100; This would output the first 100 result rows and then stop. While this is a common clause present in the SQL language, in the context of double-entry bookkeeping it is not very useful: we always have relatively small datasets to work from. Nevertheless, we provide it for completeness. Format \uf0c1 For SELECT , JOURNAL and BALANCES queries, the output format is a table of text by default. We support CSV output. ( We could easily add support for XLS or Google Sheets output.) However, for PRINT queries, the output format is Beancount input text format. Statement Operators \uf0c1 The shell provides a few operators designed to facilitate the generation of balance sheets and income statements. The particular methodology used to define these operations should be described in detail in the \u201c introduction to double-entry bookkeeping \u201d document that accompanies Beancount and is mostly located in the source code in the summarize module. These special operators are provided on the FROM clause that is made available on the various forms of query commands in the shell. These further transform the set of entries selected by the FROM expression at the transaction levels (not postings). Please note that these are not from standard SQL; these are extensions provided by this shell language only. Opening a Period \uf0c1 Opening an exercise period at a particular date replaces all entries before that date by summarization entries that book the expected balance against an Equity \u201copening balances\u201d account and implicitly clears the income and expenses to zero by transferring their balances to an Equity \u201cprevious earnings\u201d account (see beancount.ops.summarize.open() for implementation details). It is invoked like this: SELECT \u2026 FROM OPEN ON \u2026 For example: SELECT * FROM has_account(\"Invest\") OPEN ON 2014-01-01; If you want, you can view just the inserted summarization entries like this: PRINT FROM flag = \"S\" AND account ~ \"Invest\" OPEN ON 2014-01-01; Closing a Period \uf0c1 Closing an exercise period involves mainly truncating all entries that come after the given date and ensuring that currency conversions are correctly corrected for (see beancount.ops.summarize.close() for implementation details). It is invoked like this: SELECT \u2026 FROM CLOSE [ON ] \u2026 For example: SELECT * FROM has_account(\"Invest\") CLOSE ON 2015-04-01; Note that the closing date should be one day after the last transaction you would like to include (this is in line with the convention we use everywhere in Beancount whereby starting dates are inclusive and ending dates exclusive). The closing date is optional. If the date is not specified, the date one day beyond the date of the last entry is used. Closing a period leaves the Income and Expenses accounts as they are, that is, their balances are not cleared to zero to Equity. This is because closing is also used to produce final balances for income statements. \u201cClearing\u201d, as described in the next section, is only needed for balance sheets. Clearing Income & Expenses \uf0c1 In order to produce a balance sheet, we need to transfer final balances of the Income and Expenses to an Equity \u201ccurrent earnings\u201d account (sometimes called \u201cretained earnings\u201d or \u201cnet income\u201d; you can select the specific account name to use using options in the input file). The resulting balances of income statement accounts should be zero (see beancount.ops.summarize.clear() for implementation details). You can clear like this: SELECT \u2026 FROM CLEAR \u2026 For example: SELECT * FROM has_account(\"Invest\") CLOSE ON 2015-04-01 CLEAR; This is a statement suitable to produce a list of accounts to build a balance sheet. The \u201c Equity:Earnings:Current \u201d (by default) will contain the net income accumulated during the preceding period. No balances for the Income nor Expenses accounts should appear in the output. Example Statements \uf0c1 The statement operators of course may be combined. For instance, if you wanted to output data for an income statement for year 2013, you could issue the following statement: SELECT account, sum(position) FROM OPEN ON 2013-01-01 CLOSE ON 2014-01-01 WHERE account ~ \"Income|Expenses\" GROUP BY 1 ORDER BY 1; This would produce a list of balances for Income and Expenses accounts. To generate a balance sheet, you would add the CLEAR option and select the other accounts: SELECT account, sum(position) FROM OPEN ON 2013-01-01 CLOSE ON 2014-01-01 CLEAR WHERE not account ~ \"Income|Expenses\" GROUP BY 1 ORDER BY 1; Note that if you added the CLEAR operator to the statement of income statement accounts, all the balances would show at zero because of the inserted transactions that move those balances to the Equity net income account at the end of the period. It is relevant to notice that the examples above do not filter the transactions any further. If you are selecting a subset of transactions you may want to leave the accounts unopened, unclosed and uncleared because applying only some of the transactions on top of the opening balances of Assets and Liabilities accounts will not produce correct balances for those accounts. It would be more useful to leave them all opened, and to interpret the balances of the balance sheet accounts as the changes in those accounts for the subset of transactions selected. For example, if you selected all transactions from a trip (using a tag), you would obtain a list of changes in Expenses (and possibly Income) tagged as being included in this trip, and the Assets and Liabilities accounts would show where the funds for those Expenses came from. Consult the \u201cintroduction to double-entry method\u201d document for a pictorial representation of this. (Granted, this is probably worth a dedicated document and I might produce one at some point.) Example Fetching Cost Basis \uf0c1 \u201c... is there currently an easy way to determine what my cost basis is for an account on a given date (other than manually adding up UNITS * COST for every contribution, which is kind of a pain)? I'm trying to estimate the tax implications of potential stock sales.\u201d [Question from Matthew Harris] For a detailed report of share movements: SELECT account, currency, position, COST(position) WHERE year <= 2015 AND account ~ \"Assets:US:Schwab\" AND currency != \"USD\" Or this for the sum total of the cost bases: SELECT sum(cost(position)) WHERE year <= 2015 AND account ~ \"Assets:US:Schwab\" AND currency != \"USD\" High-Level Shortcuts \uf0c1 There are two types of queries that are very common for accounting applications: journals and balances reports. While we have explicit implementations of such reports that can be produced using the bean-report tool, we are also able to synthesize good approximations of such reports using SELECT statements. This section describes a few additional selection commands that translate directly into SELECT statements and which are then run with the same query code. These are intended as convenient shortcuts. Selecting Journals \uf0c1 A common type of query is one that generates a linear journal of entries (Ledger calls this a \u201cregister\u201d). This roughly corresponds to an account statement, but with our language, such a statement can be generated for any subset of postings. You can generate a journal with the following syntax: JOURNAL [AT ] [FROM \u2026] The regular expression account-regexp is used to select which subset of accounts to generate a journal for. The optional \u201cAT \u201d clause is used to specify an aggregation function for the amounts rendered (typically UNITS or COST ). The FROM clause follows the same rules as for the SELECT statement and is optional. Here is an example journal-generating query: JOURNAL \"Invest\" AT COST FROM HAS_ACCOUNT(\"Assets:US\"); Selecting Balances \uf0c1 The other most common type of report is a table of the balances of various accounts at a particular date. This can be viewed as a SELECT query aggregating positions grouping by account. You can generate a balances report with the following syntax: BALANCES [AT ] [FROM \u2026] The optional \u201cAT \u201d clause is used to specify an aggregation function for the balances rendered (usually UNITS or COST ). The FROM clause follows the same rules as for the SELECT statement and is optional. To generate your balances at a particular date, close your set of entries using the \u201c FROM\u2026 CLOSE ON \u201d form described above. Observe that typical balance sheets and income statements seen in an accounting context are subsets of tables of balances such as reported by this query. An income statement reports on just the transactions that appears during a period of time, and a balance sheet summarizes transactions before its reporting before and clears the income & expenses accumulated during the period to an equity account. Then some minor reformatting is carried out. Please consult the introduction document on double-entry bookkeeping for more details, and the section above that discusses the \u201copen\u201d, \u201cclose\u201d and \u201cclear\u201d operations. We will also be providing a separate text processing tool that can accept balance reports and reformat them in a two-column format similar to that you would see balance sheets and income statements. Print \uf0c1 It can be useful to generate output in Beancount format, so that subsets of transactions can be saved to files, for example. The shell provides that ability via the PRINT command: PRINT [FROM \u2026] The FROM clause obeys the usual semantics as described elsewhere in this document. The resulting filtered stream of Beancount entries is then printed out on the output in Beancount syntax. In particular, just running the \u201c PRINT \u201d command will spit out the parsed and loaded contents of a Beancount file. You can use this for troubleshooting if needed, or to expand transactions generated from a plugin you may be in the process of developing. Debugging / Explain \uf0c1 If you\u2019re having trouble getting a particular statement to compile and run,. you can prefix any query statement with the EXPLAIN modifier, e.g.: EXPLAIN SELECT \u2026 This will not run the statement, but rather print out the intermediate AST and compiled representation as well as the list of computed statements. This can be useful to report bugs on the mailing-list. Also, this shows you the translated form of the JOURNAL and BALANCES statements. Future Features \uf0c1 The following list of features were planned for the first release but I\u2019ve decided to make a first cut without them. I\u2019ll be adding those during a revision. Flattening Inventories \uf0c1 If you provide the FLATTEN option after a query, it tells the query engine to flatten inventories with multiple lots into separate rows for each lot. For example, if you have an inventory balance with the following contents: 3 AAPL {102.34 USD} 4 AAPL {104.53 USD} 5 AAPL {106.23 USD} Using the following query: SELECT account, sum(position) GROUP BY account; It should return a single row of results, rendered over three lines. However, adding the option: SELECT account, sum(position) GROUP BY account FLATTEN; This should return three separate rows, with all the selected attributes, as if there were that many postings. Sub-Selects \uf0c1 The ability to select from the result of another SELECT is not currently supported, but the internals of the query code are prepared to do so. More Information \uf0c1 This document attempts to provide a good high-level summary of the features supported in our query language. However, should you find you need more information, you may take a look at the original proposal , or consult the source code under the beancount.query directory. In particular, the parser will provide insight into the specifics of the syntax, and the environments will shed some light on the supported data columns and functions. Feel free to rummage in the source code and ask questions on the mailing-list. Appendix \uf0c1 Future Features \uf0c1 This section documents ideas for features to be implemented in a future version. Pivot By \uf0c1 A special PIVOT BY clause will eventually be usable to convert the output from a one-dimensional list of results to a two-dimensional table. For example, the following query: SELECT account, YEAR(date) AS year, SUM(COST(position)) AS balance WHERE account ~ 'Expenses:Food' AND currency = 'USD' AND year >= 2012 GROUP BY 1,2 ORDER BY 1,2; Might generate the following table of results: account year balance ------------------------ ---- ----------- Expenses:Food:Alcohol 2012 57.91 USD Expenses:Food:Alcohol 2013 33.45 USD Expenses:Food:Coffee 2012 42.07 USD Expenses:Food:Coffee 2013 124.69 USD Expenses:Food:Coffee 2014 38.74 USD Expenses:Food:Groceries 2012 2172.97 USD Expenses:Food:Groceries 2013 2161.90 USD Expenses:Food:Groceries 2014 2072.36 USD Expenses:Food:Restaurant 2012 4310.60 USD Expenses:Food:Restaurant 2013 5053.61 USD Expenses:Food:Restaurant 2014 4209.06 USD If you add a PIVOT clause to the query, like this: \u2026 PIVOT BY account, year; You would get a table like this: account/year 2012 2013 2014 ------------------------ ----------- ----------- ----------- Expenses:Food:Alcohol 57.91 USD 33.45 USD Expenses:Food:Coffee 42.07 USD 124.69 USD 38.74 USD Expenses:Food:Groceries 2172.97 USD 2161.90 USD 2072.36 USD Expenses:Food:Restaurant 4310.60 USD 5053.61 USD 4209.06 USD If your table has more than three columns, the non-pivoted column would get expanded horizontally, like this: SELECT account, YEAR(date), SUM(COST(position)) AS balance, LAST(date) AS updated WHERE account ~ 'Expenses:Food' AND currency = 'USD' AND year >= 2012 GROUP BY 1,2 ORDER BY 1, 2; You would get a table like this: account year balance updated ------------------------ ---- ----------- ---------- Expenses:Food:Alcohol 2012 57.91 USD 2012-07-17 Expenses:Food:Alcohol 2013 33.45 USD 2013-12-13 Expenses:Food:Coffee 2012 42.07 USD 2012-07-19 Expenses:Food:Coffee 2013 124.69 USD 2013-12-16 Expenses:Food:Coffee 2014 38.74 USD 2014-09-21 Expenses:Food:Groceries 2012 2172.97 USD 2012-12-30 Expenses:Food:Groceries 2013 2161.90 USD 2013-12-31 Expenses:Food:Groceries 2014 2072.36 USD 2014-11-20 Expenses:Food:Restaurant 2012 4310.60 USD 2012-12-30 Expenses:Food:Restaurant 2013 5053.61 USD 2013-12-29 Expenses:Food:Restaurant 2014 4209.06 USD 2014-11-28 Pivoting, this would generate this table: account/balance,updated 2012/balanc 2012/updat 2013/balanc 2013/updat 2014/balanc 2014/updat ------------------------ ----------- ---------- ----------- ---------- ----------- ---------- Expenses:Food:Alcohol 57.91 USD 2012-07-17 33.45 USD 2013-12-13 Expenses:Food:Coffee 42.07 USD 2012-07-19 124.69 USD 2013-12-16 38.74 USD 2014-09-21 Expenses:Food:Groceries 2172.97 USD 2012-12-30 2161.90 USD 2013-12-31 2072.36 USD 2014-11-20 Expenses:Food:Restaurant 4310.60 USD 2012-12-30 5053.61 USD 2013-12-29 4209.06 USD 2014-11-28","title":"Beancount Query Language"},{"location":"beancount_query_language.html#beancount-query-language","text":"Martin Blais, January 2015 http://furius.ca/beancount/doc/query","title":"Beancount Query Language"},{"location":"beancount_query_language.html#introduction","text":"The purpose of Beancount is to allow the user to create an accurate and error-free representation of financial transactions, typically those occurring in a user or in an institution\u2019s associated set of accounts, to then extract various reports from this list of transactions. Beancount provides a few tools to extract reports from the corpus of transactions: custom reports (using the Beancount bean-report tool), a web interface (using the bean-web tool) and the ability for the user to write their own scripts to output anything they want. The repository of financial transactions is always read from the text file input, but once parsed and loaded in memory, extracting information from Beancount could be carried out much like you would another database, that is, instead of using custom code to generate output from the data structures, a query language could be compiled and run over the relatively regular list of transactions. In practice, you could flatten out the list of postings to an external SQL database and make queries using that database\u2019s own tools, but the results of this approach are quite disappointing, mainly due to the lack of operations on inventories which is the basis of balancing rules in Beancount. By providing a slightly specialized query engine that takes advantage of the structure of the double-entry transactions we can easily generate custom reports specific to accounting purposes. This document describes our specialized SQL-like query client. It assumes you have at least a passing knowledge of SQL syntax. If not, you may want to first read something about it.","title":"Introduction"},{"location":"beancount_query_language.html#motivation","text":"So one might ask: Why create another SQL client? Why not output the data to an SQLite database and allow the user to use that SQL client? Well, we have done that (see the bean-sql script which converts your Beancount ledger into an SQLite database) and the results are not great. Writing queries is painful and carrying out operations on lots that are held at cost is difficult. By taking advantage of a few aspects of our in-memory data structures, we can do better. So Beancount comes with its own SQL-like query client called \u201c bean-query \u201d. The clients implements the following \u201cextras\u201d that are essential to Beancount: It allows to easily filter at two levels simultaneously: You can filter whole transactions, which has the benefit of respecting the accounting equation, and then, usually for presentation purposes, you can also filter at the postings level. The client supports the semantics of inventory booking implemented in Beancount. It also supports aggregation functions on inventory objects and rendering functions (e.g., COST() to render the cost of an inventory instead of its contents). The client allows you to flatten multiple lots into separate postings to produce lists of holdings each with their associated cost basis. Transactions can be summarized in a manner useful to produce balance sheets and income statements. For example, our SQL variant explicitly supports a \u201cclose\u201d operation with an effect similar to closing the year, which inserts transactions to clear income statement accounts to equity and removes past history. See this post as well for a similar answer.","title":"Motivation"},{"location":"beancount_query_language.html#warning-caveat","text":"Approximately 70% of the features desired in the original design doc were implemented in the query language in late 2014. More work will be needed to cover the full feature set, but the current iteration supports most of the use cases covered by Ledger, and I suspect by Beancount users. More feedback is desired on the current version before moving on, and I would like to move forward improving some of the more fundamental aspects of Beancount (namely, inventory booking) before spending more time on the query language. It is functional as it is, but a second revision will be made later on, informed by user feedback and prolonged use. Therefore, a first release of the query language has been merged in the default stable branch. This document presents this first iteration on the Beancount query language.","title":"Warning & Caveat"},{"location":"beancount_query_language.html#making-queries","text":"The custom query client that we provide is called bean-query . Run it on your ledger file, like this: $ bean-query myfile.beancount Input file: \"My Ledger\u2019s Title\" Ready with 13996 directives (21112 postings in 8833 transactions). beancount> _ This launches the query tool in interactive mode, where you can enter multiple commands on the dataset loaded in memory. bean-query parses the input file, spits out a few basic statistics about your ledger, and provides a command prompt for you to enter query commands. You can type \u201c help \u201d here to view the list of available commands. If any errors in your ledger are incurred, they are printed before the prompt. To suppress error printing, run the tool with the \u201cno-errors\u201d option: $ bean-query -q myfile.beancount","title":"Making Queries"},{"location":"beancount_query_language.html#batch-mode-queries","text":"If you\u2019d like to run queries directly from the command-line, without an interactive prompt, you can provide the query directly following your filename: $ bean-query myfile.beancount 'balances from year = 2014' account balance ---------------------------------------------------------------------- \u2026 \u2026","title":"Batch Mode Queries"},{"location":"beancount_query_language.html#all-the-interactive-commands-are-supported","text":"","title":"All the interactive commands are supported."},{"location":"beancount_query_language.html#shell-variables","text":"The interactive shell has a few \u201c set \u201d variables that you can customize to change some of the behavior of the shell. These are like environment variables. Type the \u201c set \u201d command to see the list of available variables and their current value. The variables are: format (string): The output format. Currently, only \u201ctext\u201d is supported. boxed (boolean): Whether we should draw a box around the output table. spaced (boolean): Whether to insert an empty line between every result row. This is only relevant because postings with multiple lots may require multiple lines to be rendered, and inserting an empty line helps delineate those as separate. pager (string): The name of the pager program to pipe multi-page output to when the output is larger than the screen. The initial value is copied from the PAGER environment variable. expand (boolean): If true, expand columns that render to lists on multiple rows.","title":"Shell Variables"},{"location":"beancount_query_language.html#transactions-and-postings","text":"The structure of transactions and entries can be explained by the following simplified diagram:","title":"Transactions and Postings"},{"location":"beancount_query_language.html#_1","text":"The contents of a ledger is parsed into a list of directives, most of which are \u201cTransaction\u201d objects which contain two or more \u201cPosting\u201d objects. Postings are always linked only to a single transaction (they are never shared between transactions). Each posting refers to its parent transaction but has a unique account name, amount and associated lot (possibly with a cost), a price and some other attributes. The parent transaction itself contains a few useful attributes as well, such as a date, the name of a payee, a narration string, a flag, links, tags, etc. If we ignore the list of directives other than transactions, you can view the dataset as a single table of all postings joined with their parent transaction. It is mainly on this joined table of postings that we want to perform filtering and aggregation operations. However, because of the double-entry bookkeeping constraint, that is, the sum of amounts on postings attached to a transaction is zero, it is also quite useful to perform filtering operations at the transaction level. Because any isolated transaction has a total impact of zero on the global balance, any subset of transactions will also respect the accounting equation (Assets + Liabilities + Equity + Income + Expenses = 0), and producing balance sheets and income statements on subset of transactions provides meaningful views, for example, \u201call asset changes and expenses incurred during a trip to the Bahamas\u201d which could be selected by a tag. For this reason, we modify the SQL SELECT syntax to provide a two-level filtering syntax: since we have a single table of data, we replace the table name in FROM by a filtering expression which applies over transactions, and the WHERE clause applies to data pulled from the resulting list of postings: SELECT , , \u2026 FROM WHERE ; Both filtering expressions are optional. If no filtering expressions are provided, all postings will be enumerated over. Note that since the transactions are always filtered in date order, the results will be processed and returned in this order by default.","title":""},{"location":"beancount_query_language.html#posting-data-columns","text":"The list of targets refers to attributes of postings or of their parent transaction. The same list of \u201ccolumns\u201d is made available in the , to filter by posting attributes. For example, you could write the following query: SELECT date, narration, account, position WHERE account ~ \u201c.*:Vacation\u201d AND year >= 2014; Here, the \u201cdate\u201d, \u201cyear\u201d and \u201cnarration\u201d columns refer to attributes of the parent transaction, the \u201caccount\u201d and \u201cposition\u201d columns refer to attributes of the posting itself. You may name targets explicitly with the familiar AS operator: SELECT last(date) as last_date, cost(sum(position)) as cost; The full list of posting columns and functions available on them is best viewed by querying your actual client using \u201c help targets \u201c or \u201c help where \u201d, which prints out the list and data type of each available data column. You may also refer to the following diagram of the structure of a Posting object for the correspondence between the columns and the data structure attributes.","title":"Posting Data Columns"},{"location":"beancount_query_language.html#entry-data-columns","text":"A different list of column names is available on the of the FROM clause. These columns refer to attributes of the Transaction objects. This clause is intended to filter whole transactions (i.e., all their postings or none at all). Available attributes include the date, transaction flag, the optional payee, narration, set of tags and links. Use the \u201c help from \u201d command to find the complete list of columns and functions available in this clause. A Beancount input file consists of many different types of entries, not just transactions. Some of these other types of entries (such as Open, Close, Balance, etc.) may also provide attributes that can be accessed from the FROM clause. This is embryonic at this point. (It\u2019s unclear yet how these will be used in the future, but I suspect we will find some interesting applications for them eventually. The FROM clause provides access to the type of the data entry via column \u201c type \u201d. It\u2019s still an exploration how much we can make pragmatic use of the SQL language for other types of directives.) The \u201cid\u201d Column A special column exists that identifies each transaction uniquely: \u201c id \u201d. It is a unique hash automatically computed from the transaction and should be stable between runs. SELECT DISTINCT id; This hash is derived from the contents of the transaction object itself (if you change something about the transaction, e.g. you edit the narration, the id will change). You can print and select using this column. It can be used for debugging, e.g. PRINT FROM id = '8e7c47250d040ae2b85de580dd4f5c2a'; The \u201cbalance\u201d Column One common desired output is a journal of entries over time (also called a \u201cregister\u201d in Ledger): SELECT date, account, position WHERE account ~ \"Chase:Slate\"; For this type of report, it is convenient to also render a column of the cumulative balance of the selected postings rows. Access to the previous row is not a standard SQL feature, so we get a little creative and provide a special column called \u201c balance \u201d which is automatically calculated based on the previous selected rows: SELECT date, account, position, balance WHERE account ~ \"Chase:Slate\"; This provides the ability to render typical account statements such as those mailed to you by a bank. Output might look like this: $ bean-query $T \"select date, account, position, balance where account ~ 'Expenses:Food:Restaurant';\" date account position balance ---------- ------------------------ --------- ---------- 2012-01-02 Expenses:Food:Restaurant 31.02 USD 31.02 USD 2012-01-04 Expenses:Food:Restaurant 25.33 USD 56.35 USD 2012-01-08 Expenses:Food:Restaurant 67.88 USD 124.23 USD 2012-01-09 Expenses:Food:Restaurant 35.28 USD 159.51 USD 2012-01-14 Expenses:Food:Restaurant 25.84 USD 185.35 USD 2012-01-17 Expenses:Food:Restaurant 36.73 USD 222.08 USD 2012-01-21 Expenses:Food:Restaurant 28.11 USD 250.19 USD 2012-01-22 Expenses:Food:Restaurant 21.12 USD 271.31 USD","title":"Entry Data Columns"},{"location":"beancount_query_language.html#wildcard-targets","text":"Using a wildcard as the target list (\u201c*\u201d) select a good default list of columns: SELECT * FROM year = 2014; To view the actual list of columns selected, you can use the EXPLAIN prefix: EXPLAIN SELECT * FROM year = 2014;","title":"Wildcard Targets"},{"location":"beancount_query_language.html#data-types","text":"The data attributes extracted from the postings or transactions have particular types. Most of the data types are regular types as provided by the underlying Python implementation language, types such as String (Python str) Date (a datetime.date instance). You can parse a date with the #\"...\" syntax; this uses Python\u2019s dateutil module and is pretty liberal in the formats it accepts. Integer (Python int) Boolean (Python bool object), as TRUE , FALSE Number (a decimal.Decimal object) Set of Strings (a Python set of str objects) Null objects ( NULL )","title":"Data Types"},{"location":"beancount_query_language.html#positions-and-inventories","text":"However, one reason that our SQL-like client exists in the first place is for its ability to carry out aggregation operations on inventories of positions, the data structures at the core of Beancount, that implements its balancing semantics. Internally, Beancount defines Position and Inventory objects and is able to aggregate them together in an instance of Inventory. On each Posting, the \u201cposition\u201d column extracts an object of type Position, which when summed over produces an instance of Inventory. The shell is able to display those appropriately. More specifically, Inventory objects can contain multiple different lots of holdings, and each of these will get rendered on a separate line.","title":"Positions and Inventories"},{"location":"beancount_query_language.html#quantities-of-positions-and-inventories","text":"Objects of type Position are rendered in their full detail by default, including not just their number and currency, but the details of their lot. Inventories consist of a list of lots, and as such are rendered similarly, as a list of positions (one per line) each with their full detail by default. This is generally too much detail. The shell provides functions that allow the user to summarize the positions into one of the various derived quantities. The types of derived quantities are: \u201c raw \u201d: render the position in its full detail, including cost and lot date \u201c units \u201d: render just the number and currency of the position \u201c cost \u201d: render the total cost of the position, that is the number of units x the per-unit cost \u201c weight \u201d: render the amount that is used to balance the postings of a transaction. The main distinction between cost and weight is for postings with a price conversion. \u201c value \u201d: render the amount at the market value of the last entry rendered. Functions with the same names are available to operate on position or inventory objects. For example, one could generate a table of final balances for each account like this: SELECT account, units(sum(position)), cost(sum(position)) GROUP BY 1; Refer to the table below for explicit examples of each type of posting and how it would get converted and rendered. posting raw (full detail) units cost weight market Simple 50.00 USD 50.00 USD 50.00 USD 50.00 USD 50.00 USD With Price Conversion 50.00 USD @ 1.35 CAD 50.00 USD 50.00 USD 67.50 CAD 50.00 USD Held at Cost 50 VEA {1.35 CAD} 50 VEA 67.50 CAD 67.50 CAD 67.50 CAD Held at Cost with Price 50 VEA {1.35 CAD} @ 1.45 CAD 50 VEA 67.50 CAD 67.50 CAD 72.50 CAD","title":"Quantities of Positions and Inventories"},{"location":"beancount_query_language.html#operators","text":"Common comparison and logical operators are provided to operate on the available data columns: = (equality), != (inequality) < (less than), <= (less than or equal) (greater than), >= (greater than or equal) AND (logical conjunction) OR (logical disjunction) NOT (logical negation) IN (set membership) We also provide a regular expression search operator into a string object: ~ (search regexp) At the moment, matching groups are ignored. You can use string, number and integer constants with those operators, and parentheses to explicitly state precedence. You can use the #\u201d...\u201d literal syntax to input dates (valid contents for the string are pretty liberal, it supports anything Python\u2019s dateutil.parser supports). Here is an example query that uses a few of these: SELECT date, payee WHERE account ~ 'Expenses:Food:Restaurant' AND 'trip-new-york' IN tags AND NOT payee = 'Uncle Boons' Unlike SQL, bean-query does not implement three-valued logic for NULL . This means that e.g. the expression NULL = NULL yields TRUE instead of NULL , which simplifies things, but may come as a surprise to veteran SQL users.","title":"Operators"},{"location":"beancount_query_language.html#simple-functions","text":"The shell provides a list of simple function that operate on a single data column and return a new value. These functions operate on particular types. The shell implements rudimentary type verification and should be able to warn you on incompatible types. Some example functions follow: COST(Inventory), COST(Position): Return an Amount, the cost of the position or inventory. UNITS(Inventory), UNITS(Position): Return the units of the position or inventory. DAY(date), MONTH(date), YEAR(date): Return an integer, the day, month or year of the posting or entry\u2019s date. LENGTH(list): Computes the length of a list or a set, e.g. on tags. PARENT(account-string): Returns the name of the parent account. These are just examples; for the complete list, see \u201c help targets \u201d, \u201c help where \u201d, \u201c help from \u201d. Note that it is exceedingly easy to add new functions to this list. As of December 2014, we are just beginning using the shell widely and we expect to be adding new functions as needed. If you need a function, please add a comment here or log a ticket and we will consider adding it to the list (we understand that the current list is limited). I intend to be liberal about adding new functions; as long as they have generic application, I don\u2019t think it should be a problem. Otherwise, I may be able to provide a mechanism for user to register new functions as part of Python plugins that could live outside the Beancount codebase.","title":"Simple Functions"},{"location":"beancount_query_language.html#aggregate-functions","text":"Some functions operate on more than a single row. These functions aggregate and summarize the multiple values for the data column that they operate on. A prototypical usage of such a function is to sum the positions in an inventory: SELECT account, sum(position) WHERE account ~ 'Income' GROUP BY account; If a query target has at least one aggregating function, the query becomes an aggregated query (see relevant section for details). Note that you cannot use aggregation functions in the FROM or WHERE clauses. Examples of aggregate functions include: COUNT(...): Computes the number of postings selected (an integer). FIRST(...), LAST(...): Returns first or last value seen. MIN(...), MAX(...): Computes the minimum or maximum value seen. SUM(...): Sums up the values of each set. This works on amounts, positions, inventories, numbers, etc. As for simple functions, this is just a starting list. We will be adding more as needed. Use \u201c help targets \u201d to access the full list of available aggregate functions. Note: You cannot filter (using a WHERE clause) the results of aggregation functions; this requires the implementation offering a HAVING clause, and at the moment, HAVING filtering is not yet implemented.","title":"Aggregate Functions"},{"location":"beancount_query_language.html#simple-vs-aggregated-queries","text":"There are two types of queries: Simple queries , which produce a row of results for each posting that matches the restricts in the WHERE clause. Aggregate queries , which produce a row of results for each group of postings that match the restricts in the WHERE clause. A query is \u201caggregate\u201d if it has at least one aggregate function in its list of targets. In order to identify the aggregation keys, all the non-aggregate columns have to be flagged using the GROUP BY clause, like this: SELECT payee, account, COST(SUM(position)), LAST(date) GROUP BY payee, account; You may also use the positional order of the targets to declare the group key, like this: SELECT payee, account, COST(SUM(position)), LAST(date) GROUP BY 1, 2; Furthermore, if you name your targets, you can use the explicit target names: SELECT payee, account as acc, COST(SUM(position)), LAST(date) GROUP BY 1, acc; This should all feel familiar if you have preliminary knowledge of SQL. Finally, because we implement a limited version of SQL, and that the simple columns must always be specified, omitting the GROUP BY clause should also eventually work and we should group by those columns implicitly, as a convenience.","title":"Simple vs. Aggregated Queries"},{"location":"beancount_query_language.html#distinct","text":"There is a post-filtering phase that supports uniquifying result rows. You can trigger this unique filter with the DISTINCT flag after SELECT , as is common in SQL, e.g. SELECT DISTINCT account;","title":"Distinct"},{"location":"beancount_query_language.html#controlling-results","text":"","title":"Controlling Results"},{"location":"beancount_query_language.html#order-by","text":"Analogous to the GROUP BY clause is an ORDER BY clause that controls the final ordering of the result rows: SELECT \u2026 GROUP BY account, payee ORDER BY payee, date; The clause is optional. If you do not specify it, the default order of iteration of selected postings is used to output the results (that is, the order of transactions-sorted by date- and then their postings). As in SQL, you may reverse the order of sorting by a DESC suffix (the default is the same as specifying ASC ): SELECT \u2026 GROUP BY account, payee ORDER BY payee, date DESC;","title":"Order By"},{"location":"beancount_query_language.html#limit","text":"Our query language also supports a LIMIT clause to interrupt output row generation: SELECT \u2026 LIMIT 100; This would output the first 100 result rows and then stop. While this is a common clause present in the SQL language, in the context of double-entry bookkeeping it is not very useful: we always have relatively small datasets to work from. Nevertheless, we provide it for completeness.","title":"Limit"},{"location":"beancount_query_language.html#format","text":"For SELECT , JOURNAL and BALANCES queries, the output format is a table of text by default. We support CSV output. ( We could easily add support for XLS or Google Sheets output.) However, for PRINT queries, the output format is Beancount input text format.","title":"Format"},{"location":"beancount_query_language.html#statement-operators","text":"The shell provides a few operators designed to facilitate the generation of balance sheets and income statements. The particular methodology used to define these operations should be described in detail in the \u201c introduction to double-entry bookkeeping \u201d document that accompanies Beancount and is mostly located in the source code in the summarize module. These special operators are provided on the FROM clause that is made available on the various forms of query commands in the shell. These further transform the set of entries selected by the FROM expression at the transaction levels (not postings). Please note that these are not from standard SQL; these are extensions provided by this shell language only.","title":"Statement Operators"},{"location":"beancount_query_language.html#opening-a-period","text":"Opening an exercise period at a particular date replaces all entries before that date by summarization entries that book the expected balance against an Equity \u201copening balances\u201d account and implicitly clears the income and expenses to zero by transferring their balances to an Equity \u201cprevious earnings\u201d account (see beancount.ops.summarize.open() for implementation details). It is invoked like this: SELECT \u2026 FROM OPEN ON \u2026 For example: SELECT * FROM has_account(\"Invest\") OPEN ON 2014-01-01; If you want, you can view just the inserted summarization entries like this: PRINT FROM flag = \"S\" AND account ~ \"Invest\" OPEN ON 2014-01-01;","title":"Opening a Period"},{"location":"beancount_query_language.html#closing-a-period","text":"Closing an exercise period involves mainly truncating all entries that come after the given date and ensuring that currency conversions are correctly corrected for (see beancount.ops.summarize.close() for implementation details). It is invoked like this: SELECT \u2026 FROM CLOSE [ON ] \u2026 For example: SELECT * FROM has_account(\"Invest\") CLOSE ON 2015-04-01; Note that the closing date should be one day after the last transaction you would like to include (this is in line with the convention we use everywhere in Beancount whereby starting dates are inclusive and ending dates exclusive). The closing date is optional. If the date is not specified, the date one day beyond the date of the last entry is used. Closing a period leaves the Income and Expenses accounts as they are, that is, their balances are not cleared to zero to Equity. This is because closing is also used to produce final balances for income statements. \u201cClearing\u201d, as described in the next section, is only needed for balance sheets.","title":"Closing a Period"},{"location":"beancount_query_language.html#clearing-income-expenses","text":"In order to produce a balance sheet, we need to transfer final balances of the Income and Expenses to an Equity \u201ccurrent earnings\u201d account (sometimes called \u201cretained earnings\u201d or \u201cnet income\u201d; you can select the specific account name to use using options in the input file). The resulting balances of income statement accounts should be zero (see beancount.ops.summarize.clear() for implementation details). You can clear like this: SELECT \u2026 FROM CLEAR \u2026 For example: SELECT * FROM has_account(\"Invest\") CLOSE ON 2015-04-01 CLEAR; This is a statement suitable to produce a list of accounts to build a balance sheet. The \u201c Equity:Earnings:Current \u201d (by default) will contain the net income accumulated during the preceding period. No balances for the Income nor Expenses accounts should appear in the output.","title":"Clearing Income & Expenses"},{"location":"beancount_query_language.html#example-statements","text":"The statement operators of course may be combined. For instance, if you wanted to output data for an income statement for year 2013, you could issue the following statement: SELECT account, sum(position) FROM OPEN ON 2013-01-01 CLOSE ON 2014-01-01 WHERE account ~ \"Income|Expenses\" GROUP BY 1 ORDER BY 1; This would produce a list of balances for Income and Expenses accounts. To generate a balance sheet, you would add the CLEAR option and select the other accounts: SELECT account, sum(position) FROM OPEN ON 2013-01-01 CLOSE ON 2014-01-01 CLEAR WHERE not account ~ \"Income|Expenses\" GROUP BY 1 ORDER BY 1; Note that if you added the CLEAR operator to the statement of income statement accounts, all the balances would show at zero because of the inserted transactions that move those balances to the Equity net income account at the end of the period. It is relevant to notice that the examples above do not filter the transactions any further. If you are selecting a subset of transactions you may want to leave the accounts unopened, unclosed and uncleared because applying only some of the transactions on top of the opening balances of Assets and Liabilities accounts will not produce correct balances for those accounts. It would be more useful to leave them all opened, and to interpret the balances of the balance sheet accounts as the changes in those accounts for the subset of transactions selected. For example, if you selected all transactions from a trip (using a tag), you would obtain a list of changes in Expenses (and possibly Income) tagged as being included in this trip, and the Assets and Liabilities accounts would show where the funds for those Expenses came from. Consult the \u201cintroduction to double-entry method\u201d document for a pictorial representation of this. (Granted, this is probably worth a dedicated document and I might produce one at some point.)","title":"Example Statements"},{"location":"beancount_query_language.html#example-fetching-cost-basis","text":"\u201c... is there currently an easy way to determine what my cost basis is for an account on a given date (other than manually adding up UNITS * COST for every contribution, which is kind of a pain)? I'm trying to estimate the tax implications of potential stock sales.\u201d [Question from Matthew Harris] For a detailed report of share movements: SELECT account, currency, position, COST(position) WHERE year <= 2015 AND account ~ \"Assets:US:Schwab\" AND currency != \"USD\" Or this for the sum total of the cost bases: SELECT sum(cost(position)) WHERE year <= 2015 AND account ~ \"Assets:US:Schwab\" AND currency != \"USD\"","title":"Example Fetching Cost Basis"},{"location":"beancount_query_language.html#high-level-shortcuts","text":"There are two types of queries that are very common for accounting applications: journals and balances reports. While we have explicit implementations of such reports that can be produced using the bean-report tool, we are also able to synthesize good approximations of such reports using SELECT statements. This section describes a few additional selection commands that translate directly into SELECT statements and which are then run with the same query code. These are intended as convenient shortcuts.","title":"High-Level Shortcuts"},{"location":"beancount_query_language.html#selecting-journals","text":"A common type of query is one that generates a linear journal of entries (Ledger calls this a \u201cregister\u201d). This roughly corresponds to an account statement, but with our language, such a statement can be generated for any subset of postings. You can generate a journal with the following syntax: JOURNAL [AT ] [FROM \u2026] The regular expression account-regexp is used to select which subset of accounts to generate a journal for. The optional \u201cAT \u201d clause is used to specify an aggregation function for the amounts rendered (typically UNITS or COST ). The FROM clause follows the same rules as for the SELECT statement and is optional. Here is an example journal-generating query: JOURNAL \"Invest\" AT COST FROM HAS_ACCOUNT(\"Assets:US\");","title":"Selecting Journals"},{"location":"beancount_query_language.html#selecting-balances","text":"The other most common type of report is a table of the balances of various accounts at a particular date. This can be viewed as a SELECT query aggregating positions grouping by account. You can generate a balances report with the following syntax: BALANCES [AT ] [FROM \u2026] The optional \u201cAT \u201d clause is used to specify an aggregation function for the balances rendered (usually UNITS or COST ). The FROM clause follows the same rules as for the SELECT statement and is optional. To generate your balances at a particular date, close your set of entries using the \u201c FROM\u2026 CLOSE ON \u201d form described above. Observe that typical balance sheets and income statements seen in an accounting context are subsets of tables of balances such as reported by this query. An income statement reports on just the transactions that appears during a period of time, and a balance sheet summarizes transactions before its reporting before and clears the income & expenses accumulated during the period to an equity account. Then some minor reformatting is carried out. Please consult the introduction document on double-entry bookkeeping for more details, and the section above that discusses the \u201copen\u201d, \u201cclose\u201d and \u201cclear\u201d operations. We will also be providing a separate text processing tool that can accept balance reports and reformat them in a two-column format similar to that you would see balance sheets and income statements.","title":"Selecting Balances"},{"location":"beancount_query_language.html#print","text":"It can be useful to generate output in Beancount format, so that subsets of transactions can be saved to files, for example. The shell provides that ability via the PRINT command: PRINT [FROM \u2026] The FROM clause obeys the usual semantics as described elsewhere in this document. The resulting filtered stream of Beancount entries is then printed out on the output in Beancount syntax. In particular, just running the \u201c PRINT \u201d command will spit out the parsed and loaded contents of a Beancount file. You can use this for troubleshooting if needed, or to expand transactions generated from a plugin you may be in the process of developing.","title":"Print"},{"location":"beancount_query_language.html#debugging-explain","text":"If you\u2019re having trouble getting a particular statement to compile and run,. you can prefix any query statement with the EXPLAIN modifier, e.g.: EXPLAIN SELECT \u2026 This will not run the statement, but rather print out the intermediate AST and compiled representation as well as the list of computed statements. This can be useful to report bugs on the mailing-list. Also, this shows you the translated form of the JOURNAL and BALANCES statements.","title":"Debugging / Explain"},{"location":"beancount_query_language.html#future-features","text":"The following list of features were planned for the first release but I\u2019ve decided to make a first cut without them. I\u2019ll be adding those during a revision.","title":"Future Features"},{"location":"beancount_query_language.html#flattening-inventories","text":"If you provide the FLATTEN option after a query, it tells the query engine to flatten inventories with multiple lots into separate rows for each lot. For example, if you have an inventory balance with the following contents: 3 AAPL {102.34 USD} 4 AAPL {104.53 USD} 5 AAPL {106.23 USD} Using the following query: SELECT account, sum(position) GROUP BY account; It should return a single row of results, rendered over three lines. However, adding the option: SELECT account, sum(position) GROUP BY account FLATTEN; This should return three separate rows, with all the selected attributes, as if there were that many postings.","title":"Flattening Inventories"},{"location":"beancount_query_language.html#sub-selects","text":"The ability to select from the result of another SELECT is not currently supported, but the internals of the query code are prepared to do so.","title":"Sub-Selects"},{"location":"beancount_query_language.html#more-information","text":"This document attempts to provide a good high-level summary of the features supported in our query language. However, should you find you need more information, you may take a look at the original proposal , or consult the source code under the beancount.query directory. In particular, the parser will provide insight into the specifics of the syntax, and the environments will shed some light on the supported data columns and functions. Feel free to rummage in the source code and ask questions on the mailing-list.","title":"More Information"},{"location":"beancount_query_language.html#appendix","text":"","title":"Appendix"},{"location":"beancount_query_language.html#future-features_1","text":"This section documents ideas for features to be implemented in a future version.","title":"Future Features"},{"location":"beancount_query_language.html#pivot-by","text":"A special PIVOT BY clause will eventually be usable to convert the output from a one-dimensional list of results to a two-dimensional table. For example, the following query: SELECT account, YEAR(date) AS year, SUM(COST(position)) AS balance WHERE account ~ 'Expenses:Food' AND currency = 'USD' AND year >= 2012 GROUP BY 1,2 ORDER BY 1,2; Might generate the following table of results: account year balance ------------------------ ---- ----------- Expenses:Food:Alcohol 2012 57.91 USD Expenses:Food:Alcohol 2013 33.45 USD Expenses:Food:Coffee 2012 42.07 USD Expenses:Food:Coffee 2013 124.69 USD Expenses:Food:Coffee 2014 38.74 USD Expenses:Food:Groceries 2012 2172.97 USD Expenses:Food:Groceries 2013 2161.90 USD Expenses:Food:Groceries 2014 2072.36 USD Expenses:Food:Restaurant 2012 4310.60 USD Expenses:Food:Restaurant 2013 5053.61 USD Expenses:Food:Restaurant 2014 4209.06 USD If you add a PIVOT clause to the query, like this: \u2026 PIVOT BY account, year; You would get a table like this: account/year 2012 2013 2014 ------------------------ ----------- ----------- ----------- Expenses:Food:Alcohol 57.91 USD 33.45 USD Expenses:Food:Coffee 42.07 USD 124.69 USD 38.74 USD Expenses:Food:Groceries 2172.97 USD 2161.90 USD 2072.36 USD Expenses:Food:Restaurant 4310.60 USD 5053.61 USD 4209.06 USD If your table has more than three columns, the non-pivoted column would get expanded horizontally, like this: SELECT account, YEAR(date), SUM(COST(position)) AS balance, LAST(date) AS updated WHERE account ~ 'Expenses:Food' AND currency = 'USD' AND year >= 2012 GROUP BY 1,2 ORDER BY 1, 2; You would get a table like this: account year balance updated ------------------------ ---- ----------- ---------- Expenses:Food:Alcohol 2012 57.91 USD 2012-07-17 Expenses:Food:Alcohol 2013 33.45 USD 2013-12-13 Expenses:Food:Coffee 2012 42.07 USD 2012-07-19 Expenses:Food:Coffee 2013 124.69 USD 2013-12-16 Expenses:Food:Coffee 2014 38.74 USD 2014-09-21 Expenses:Food:Groceries 2012 2172.97 USD 2012-12-30 Expenses:Food:Groceries 2013 2161.90 USD 2013-12-31 Expenses:Food:Groceries 2014 2072.36 USD 2014-11-20 Expenses:Food:Restaurant 2012 4310.60 USD 2012-12-30 Expenses:Food:Restaurant 2013 5053.61 USD 2013-12-29 Expenses:Food:Restaurant 2014 4209.06 USD 2014-11-28 Pivoting, this would generate this table: account/balance,updated 2012/balanc 2012/updat 2013/balanc 2013/updat 2014/balanc 2014/updat ------------------------ ----------- ---------- ----------- ---------- ----------- ---------- Expenses:Food:Alcohol 57.91 USD 2012-07-17 33.45 USD 2013-12-13 Expenses:Food:Coffee 42.07 USD 2012-07-19 124.69 USD 2013-12-16 38.74 USD 2014-09-21 Expenses:Food:Groceries 2172.97 USD 2012-12-30 2161.90 USD 2013-12-31 2072.36 USD 2014-11-20 Expenses:Food:Restaurant 4310.60 USD 2012-12-30 5053.61 USD 2013-12-29 4209.06 USD 2014-11-28","title":"Pivot By"},{"location":"beancount_scripting_plugins.html","text":"Beancount Scripting & Plugins \uf0c1 Martin Blais , July 2014 http://furius.ca/beancount/doc/scripting Introduction Load Pipeline Writing Plug-ins Writing Scripts Loading from File Loading from String Printing Errors Printing Entries & Round-Tripping Going Further Introduction \uf0c1 This document provides examples and guidelines on how to write scripts that use the contents of your ledger. It also provides information on how to write your own \u201cplugins,\u201d which are just Python functions that you can configure to transform your transactions or synthesize ones programmatically. These are the main two methods for extending Beancount\u2019s features and for writing your own custom reports. You simply use Python to do this. Load Pipeline \uf0c1 You need to know a little bit about how Beancount processes its input files. Internally, the single point of entry to load an input file is the beancount.loader.load_file() function, which accepts an input file and carries out a list of transformation steps, as in this diagram: The stages of loading are as follows: Parser. Run the input file through the parser. The output of this stage is entries: A list of tuples (defined in beancount.core.data) corresponding to each directive exactly as it appeared in the file, and sorted by date and line number. Moreover, Transaction directives that occur on the same date as other directives are always guaranteed to be sorted after them. This prepares the entries for processing. This list of entries will get transformed and refined by the various subsequent stages. options_map: A Python dict of the option values from the input file. See beancount.parser.options for details. Once created, this will never be modified thereafter. errors: A list of error objects, if any occurred. At every stage, new errors generated are collected. Process plugins. For each plugin, load the plugin module and call its functions with the list of entries and the options_map from the previous stage, replacing the current list by the ones returned by the plugin. This effectively allows the plugin to filter the entries. The list of plugins to run is composed of a set of default plugin modules that implement some of the built-in features of Beancount, followed by the list provided by the user from the \u201cplugin\u201d options in the input file. Validation. Run the resulting entries through a validation stage, to ensure that directives synthesized or modified by the plugins conform to some invariants that the codebase depends on. This mainly exists to generate errors. The list of entries generated by this pipeline are of the various types defined in beancount.core.data , and in a typical input file, most of them will be of type Transaction . Beancount\u2019s own filtering and reporting programs directly process those, and so can you too. These entries are dumb read-only objects (Python namedtuples ) and have no methods that modify their contents explicitly. All processing within Beancount is performed functional-style by processing lists of entries that are assumed immutable 1 . The list of user plugins to run is part of the load stage because that allows programs that monitor the file for changes to reload it and reapply the same list of plugins. It also allows the author of the input file to selectively enable various optional features that way. Writing Plug-ins \uf0c1 As you saw in the previous section, loading a Beancount file essentially produces a list of directives. Many syntax extensions can be carried out by transforming the list of directives into a new list in the plug-ins processing stage. Here are some examples of transformations that you might want to carry out on some of the directives: Add some postings automatically Link some transactions with a common tag Synthesize new transactions Remove or replace some sets of transactions Modify the various fields There is no limit to what you can do, as long as the entries your plugin produces fulfill certain constraints (all postings balance, all data types are as expected). A plugin is added to the input file via the option syntax , for example, like this: plugin \"accounting.wash_sales\" With this directive, the loader will attempt to import the accounting.wash_sales Python module (the code must be Python-3.3 or above), look for a special __plugins__ attribute which should be a sequence of functions to run, and then run those functions. For running the plugins, see the Executing Plugins section below. As an example, you would place code like this in a \u201c accounting/wash_sales.py \u201d file: __plugins__ = ['wash_sales'] def wash_sales(entries, options_map): errors = [] for entry in entries: print(type(entry)) return entries, errors This is a minimal example which does not modify the entries and prints them on the console. In practice, to do something useful, you would modify some of the entries in the list and output them. You then invoke the usual tools provided by Beancount on your input file. The various filters and reports will then operate on the list of entries output by your plugin. Refer to the source code in beancount.core for details and examples of how to manipulate entries. Plugin Configuration \uf0c1 Some plugins will require configuration. In order to provide a plugin some data specific to your file, you can provide a configuration string: plugin \"accounting.wash_sales\" \"days=31\" The plugin function will then receive an extra parameter, the configuration string. It is up to the plugin itself to define how it gets interpreted. Writing Scripts \uf0c1 If you need to produce some custom analysis or visualization that cannot be achieved using the built-in filtering and reporting capabilities, you can just write a script that loads the directives explicitly. This gives you control over the flow of the program and you can do anything you want. Loading from File \uf0c1 You can simply call the beancount.loader.load_file() loader function yourself. Here is an example minimal script: #!/usr/bin/env python3 from beancount import loader filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename) \u2026 At this point you can process the entries as you like, print them out, generate HTML, call out to Python libraries, etc. (I recommend that you use best programming practices and use docstrings on your script and a main function; the script above is meant to be minimal). Once again, refer to the source code in beancount.core for details and examples of how to manipulate entries. Loading from String \uf0c1 You can also parse a string directly. Use beancount.loader.load_string() : #!/usr/bin/env python3 from beancount import loader entries, errors, options = loader.load_string(\"\"\" 2014-02-02 open Assets:TestAccount USD \u2026 \"\"\") The stdlib textwrap.dedent function comes in handy if you want to indent the Beancount directives and have it automatically remove indentation. For a source of many examples, see the various tests in the Beancount source code. Printing Errors \uf0c1 By default, the loader will not print any errors upon loading; we prefer loading not to have any side-effect by default. You can provide an optional argument to print errors, which is the function to call to write error strings: #!/usr/bin/env python3 import sys from beancount import loader filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename, log_errors=sys.stderr) \u2026 Or if you prefer to do it yourself explicitly, you can call the beancount.parser.printer.print_errors() helper function: #!/usr/bin/env python3 from beancount import loader from beancount.parser import printer filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename) printer.print_errors(errors) \u2026 Printing Entries & Round-Tripping \uf0c1 Printing namedtuple entries directly will output some readable though relatively poorly formatted output. It\u2019s best to use the beancount.parser.printer.print_entry() utility function to print out an entry in a readable way: #!/usr/bin/env python3 from beancount import loader from beancount.parser import printer filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename) for entry in entries: printer.print_entry(entry) In particular, Beancount offers the guarantee that the output of the printer should always be parseable and should result in the same data structure when read back in. (It should be considered a bug if that is not the case.) See the beancount.parser.printer module source code for more utility functions. Executing Plugins \uf0c1 All that is required for the plug-in module to be found, is that it must be present in your PYTHONPATH environment variable (you need to make sure that the relevant __init__.py files exist for import). It can live in your own code: you don\u2019t have to modify Beancount itself. There is also an option, which can be added to your beancount file: option \"insert_pythonpath\" \"True\" This will add the folder which contains the beancount file to the PYTHONPATH . The result is that you can place the plugins along the beancount file and have them execute when you use this file. Here is a brief example, using the wash_sales.py plugin we wrote above. Your beancount file would include the following lines: option \"insert_pythonpath\" \"True\" plugin \"wash_sales\" The Python file wash_sales.py would be stored in the same folder as the .beancount file. Going Further \uf0c1 To understand how to manipulate entries, you should refer to the source code, and probably learn more about the following modules: beancount.core.data beancount.core.account beancount.core.number beancount.core.amount beancount.core.position beancount.core.inventory Refer to the Design Doc for more details. Enjoy! Technically, Python does not prevent the modifications of namedtuple attributes that are themselves mutable such as lists and sets, but in practice, by convention, once an entry is created we never modify it in any way. Avoiding side-effects and using a functional style provides benefits in any language. \u21a9","title":"Beancount Scripting Plugins"},{"location":"beancount_scripting_plugins.html#beancount-scripting-plugins","text":"Martin Blais , July 2014 http://furius.ca/beancount/doc/scripting Introduction Load Pipeline Writing Plug-ins Writing Scripts Loading from File Loading from String Printing Errors Printing Entries & Round-Tripping Going Further","title":"Beancount Scripting & Plugins"},{"location":"beancount_scripting_plugins.html#introduction","text":"This document provides examples and guidelines on how to write scripts that use the contents of your ledger. It also provides information on how to write your own \u201cplugins,\u201d which are just Python functions that you can configure to transform your transactions or synthesize ones programmatically. These are the main two methods for extending Beancount\u2019s features and for writing your own custom reports. You simply use Python to do this.","title":"Introduction"},{"location":"beancount_scripting_plugins.html#load-pipeline","text":"You need to know a little bit about how Beancount processes its input files. Internally, the single point of entry to load an input file is the beancount.loader.load_file() function, which accepts an input file and carries out a list of transformation steps, as in this diagram: The stages of loading are as follows: Parser. Run the input file through the parser. The output of this stage is entries: A list of tuples (defined in beancount.core.data) corresponding to each directive exactly as it appeared in the file, and sorted by date and line number. Moreover, Transaction directives that occur on the same date as other directives are always guaranteed to be sorted after them. This prepares the entries for processing. This list of entries will get transformed and refined by the various subsequent stages. options_map: A Python dict of the option values from the input file. See beancount.parser.options for details. Once created, this will never be modified thereafter. errors: A list of error objects, if any occurred. At every stage, new errors generated are collected. Process plugins. For each plugin, load the plugin module and call its functions with the list of entries and the options_map from the previous stage, replacing the current list by the ones returned by the plugin. This effectively allows the plugin to filter the entries. The list of plugins to run is composed of a set of default plugin modules that implement some of the built-in features of Beancount, followed by the list provided by the user from the \u201cplugin\u201d options in the input file. Validation. Run the resulting entries through a validation stage, to ensure that directives synthesized or modified by the plugins conform to some invariants that the codebase depends on. This mainly exists to generate errors. The list of entries generated by this pipeline are of the various types defined in beancount.core.data , and in a typical input file, most of them will be of type Transaction . Beancount\u2019s own filtering and reporting programs directly process those, and so can you too. These entries are dumb read-only objects (Python namedtuples ) and have no methods that modify their contents explicitly. All processing within Beancount is performed functional-style by processing lists of entries that are assumed immutable 1 . The list of user plugins to run is part of the load stage because that allows programs that monitor the file for changes to reload it and reapply the same list of plugins. It also allows the author of the input file to selectively enable various optional features that way.","title":"Load Pipeline"},{"location":"beancount_scripting_plugins.html#writing-plug-ins","text":"As you saw in the previous section, loading a Beancount file essentially produces a list of directives. Many syntax extensions can be carried out by transforming the list of directives into a new list in the plug-ins processing stage. Here are some examples of transformations that you might want to carry out on some of the directives: Add some postings automatically Link some transactions with a common tag Synthesize new transactions Remove or replace some sets of transactions Modify the various fields There is no limit to what you can do, as long as the entries your plugin produces fulfill certain constraints (all postings balance, all data types are as expected). A plugin is added to the input file via the option syntax , for example, like this: plugin \"accounting.wash_sales\" With this directive, the loader will attempt to import the accounting.wash_sales Python module (the code must be Python-3.3 or above), look for a special __plugins__ attribute which should be a sequence of functions to run, and then run those functions. For running the plugins, see the Executing Plugins section below. As an example, you would place code like this in a \u201c accounting/wash_sales.py \u201d file: __plugins__ = ['wash_sales'] def wash_sales(entries, options_map): errors = [] for entry in entries: print(type(entry)) return entries, errors This is a minimal example which does not modify the entries and prints them on the console. In practice, to do something useful, you would modify some of the entries in the list and output them. You then invoke the usual tools provided by Beancount on your input file. The various filters and reports will then operate on the list of entries output by your plugin. Refer to the source code in beancount.core for details and examples of how to manipulate entries.","title":"Writing Plug-ins"},{"location":"beancount_scripting_plugins.html#plugin-configuration","text":"Some plugins will require configuration. In order to provide a plugin some data specific to your file, you can provide a configuration string: plugin \"accounting.wash_sales\" \"days=31\" The plugin function will then receive an extra parameter, the configuration string. It is up to the plugin itself to define how it gets interpreted.","title":"Plugin Configuration"},{"location":"beancount_scripting_plugins.html#writing-scripts","text":"If you need to produce some custom analysis or visualization that cannot be achieved using the built-in filtering and reporting capabilities, you can just write a script that loads the directives explicitly. This gives you control over the flow of the program and you can do anything you want.","title":"Writing Scripts"},{"location":"beancount_scripting_plugins.html#loading-from-file","text":"You can simply call the beancount.loader.load_file() loader function yourself. Here is an example minimal script: #!/usr/bin/env python3 from beancount import loader filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename) \u2026 At this point you can process the entries as you like, print them out, generate HTML, call out to Python libraries, etc. (I recommend that you use best programming practices and use docstrings on your script and a main function; the script above is meant to be minimal). Once again, refer to the source code in beancount.core for details and examples of how to manipulate entries.","title":"Loading from File"},{"location":"beancount_scripting_plugins.html#loading-from-string","text":"You can also parse a string directly. Use beancount.loader.load_string() : #!/usr/bin/env python3 from beancount import loader entries, errors, options = loader.load_string(\"\"\" 2014-02-02 open Assets:TestAccount USD \u2026 \"\"\") The stdlib textwrap.dedent function comes in handy if you want to indent the Beancount directives and have it automatically remove indentation. For a source of many examples, see the various tests in the Beancount source code.","title":"Loading from String"},{"location":"beancount_scripting_plugins.html#printing-errors","text":"By default, the loader will not print any errors upon loading; we prefer loading not to have any side-effect by default. You can provide an optional argument to print errors, which is the function to call to write error strings: #!/usr/bin/env python3 import sys from beancount import loader filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename, log_errors=sys.stderr) \u2026 Or if you prefer to do it yourself explicitly, you can call the beancount.parser.printer.print_errors() helper function: #!/usr/bin/env python3 from beancount import loader from beancount.parser import printer filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename) printer.print_errors(errors) \u2026","title":"Printing Errors"},{"location":"beancount_scripting_plugins.html#printing-entries-round-tripping","text":"Printing namedtuple entries directly will output some readable though relatively poorly formatted output. It\u2019s best to use the beancount.parser.printer.print_entry() utility function to print out an entry in a readable way: #!/usr/bin/env python3 from beancount import loader from beancount.parser import printer filename = \"/path/to/my/input.beancount\" entries, errors, options = loader.load_file(filename) for entry in entries: printer.print_entry(entry) In particular, Beancount offers the guarantee that the output of the printer should always be parseable and should result in the same data structure when read back in. (It should be considered a bug if that is not the case.) See the beancount.parser.printer module source code for more utility functions.","title":"Printing Entries & Round-Tripping"},{"location":"beancount_scripting_plugins.html#executing-plugins","text":"All that is required for the plug-in module to be found, is that it must be present in your PYTHONPATH environment variable (you need to make sure that the relevant __init__.py files exist for import). It can live in your own code: you don\u2019t have to modify Beancount itself. There is also an option, which can be added to your beancount file: option \"insert_pythonpath\" \"True\" This will add the folder which contains the beancount file to the PYTHONPATH . The result is that you can place the plugins along the beancount file and have them execute when you use this file. Here is a brief example, using the wash_sales.py plugin we wrote above. Your beancount file would include the following lines: option \"insert_pythonpath\" \"True\" plugin \"wash_sales\" The Python file wash_sales.py would be stored in the same folder as the .beancount file.","title":"Executing Plugins"},{"location":"beancount_scripting_plugins.html#going-further","text":"To understand how to manipulate entries, you should refer to the source code, and probably learn more about the following modules: beancount.core.data beancount.core.account beancount.core.number beancount.core.amount beancount.core.position beancount.core.inventory Refer to the Design Doc for more details. Enjoy! Technically, Python does not prevent the modifications of namedtuple attributes that are themselves mutable such as lists and sets, but in practice, by convention, once an entry is created we never modify it in any way. Avoiding side-effects and using a functional style provides benefits in any language. \u21a9","title":"Going Further"},{"location":"beancount_v3.html","text":"Beancount Vnext: Goals & Design \uf0c1 Martin Blais , July 2020 http://furius.ca/beancount/doc/Vnext Motivation \uf0c1 It's time to give Beancount a refresh and to put down a concrete plan for what the next iteration of it ought to be. I've had these thoughts in the back of my mind for a long while\u2014at least a year\u2014and I'm putting these in writing partly to share a vision of what the product we all use to organize our finances can become, partly to solicit feedback, and partly to organize my thoughts and to prioritize well on the stuff that matters. Current status. The current state of Beancount is that development has been static for a while now, for a number of reasons. The software is in a state that's far from perfect (and I'll be enumerating the main problems in this document) but I've been resisting making too many changes in order to provide myself and others a really stable base to work from. More importantly, while I used to be able to spend a significant amount of weekend time on its development, life changes and a focus on my career of late has made it difficult for me to justify or find the extra time (it has been 10 years after all). A multitude of ideas have aged to this TODO file but it's too detailed to grok and a bit of a dump, this document should be more useful. Why rewrites happen. When I wrote version 2 of Beancount (a full rewrite of the first version), it was because of a confluence of ideas for improving my first draft; I resisted for a while, but eventually it made so much sense to me that it became simply impossible not to write it. Many of the ideas driving the redesign at the time are still axioms in today's design: removing order dependence, normalizing the syntax to be well-defined with a BNF grammar, converting custom processing to a sequence of plugins off of a simple stream of directives, the current design of booking selection and how cost basis works, and all the directives beyond \"Transaction\". These ideas largely shape what a lot of people like about using Beancount today. Goals. Now is the time for yet another wave of evolution for Beancount, and similarly, a set of new ideas I'm going to lay down in this document form as potent a change as the v1 to v2 transition. The vision I have for Vnext will simplify Beancount, by factoring into simpler, more isolated, more reusable, better defined parts, and not merely by adding new features on top of what's there. In many ways, Vnext will be a distillation of the current system. It will also make space to finally implement some of the core features most often desired by users. And those changes will enhance some organizational aspects: allow for more contributions, and also trim down the part that I'm handling myself to less code, so I can more effectively focus on just the core features. Current Problems \uf0c1 Performance \uf0c1 My personal ledger, and I know that the ledgers of many users, are simply too large to process instantly. My current file takes 6 seconds on the souped-up NUC I use for a desktop at home\u2014but that's just too long. I'm really quite attached to the idea of processing the entire set of inputs every time, instead of forcing users to cut-up their ledgers into multiple files with \"closing\" transitions at arbitrary points in the year, but I really do want that \"instant\" feeling that you get when you run two-letter UNIX programs, that it runs in well under half a second . It makes it a lot more interactive and fun to use. C++ rewrite. One of the reasons for the slow performance right now is the fact that Beancount is implemented in Python, even at the level of the parser (C code calling back into a Python driver). An obvious solution is to rewrite the core of the software in a language closer to the metal, and that will be C++. I'm selecting C++ for its control and because the current slate of tools around it is mature and widespread enough that it should be easy for most to build without too many problems, and I can leverage C libraries that I will need. Using a functional language could have been fun but many of the libraries I want simply would not be available or it would be too difficult for mere mortals to build. Simple, portable C++. It's important to mention that the C++ code I have in mind is not in the style of template-heavy modern C++ code you'd find in something like Boost. Rather, it's a lot more like the conservative \"almost C without exceptions\" subset of C++ that Google uses , with a base on Abseil-Cpp (for example and flavor, see tips ). The reasons for this are stability and portability, and while this rewrite is for faster performance, I believe that it will not be necessary to pull template tricks to make it run fast enough; just a straightforward port to avoid the Python runtime will likely be sufficient. Above all I want to keep the new code simple and \"functional-ish\" as much as possible (no classes if I can avoid it), relying on a trusted set of stable dependencies , built hermetically using the Bazel build tool. Python API. It's also important that the Python API remains for plugins and scripts, and that the full suite of unit tests be carried over to the newer version of the code. After all, the ability to write custom scripts using all that personal finance data is one of the most attractive features of the text-based approach. Code beyond the new core implementation will remain in Python, and existing code built on top of the Python API should be very easily portable to Vnext. This can be achieved by exposing the directives with wrappers written in pybind11. Other languages. The resolved output of the Beancount core will be a stream of protocol buffer objects, so processing from other languages (e.g., Go) will have first-class support. Processing model. An additional algorithmic improvement to performance, should it be necessary, would be to define plugins processing in terms of iterator functions that cascade and interleave the processing of the directive stream without making entirely disjoint passes over the full list of directives. While the freedom to have each plugin process all the directives on their own has been instrumental in keeping the system free of synchronized application state and has allowed me to isolate behavior of the plugins from each other, there are opportunities to join together multiple transformations in a single pass. Fewer passes = faster. Intermediate Parsed Data vs. Final List of Directives \uf0c1 In Beancount v2, I was never too careful to clearly distinguish between The list of directives coming out of the parser , missing interpolation and booking, using position.CostSpec instead of position.Cost on each lot, and The resolved and booked list of directives with booking algorithms applied to select matching lots, and interpolated values filled in, as well as transformations having been applied by the various plugins. These two lists of directives are really quite distinct in purpose, though they share many common data structures, and for the most part, the first list appears mostly in the parser module. There have been cases where it was confusing, even to me, which of the lists I was manipulating. Part of the reason is due to how I'm using mostly the same Python data structures for both, that allow me to bend the rules on typing. Perhaps more importantly is that because plugins run after booking and interpolation, and are required to put out fully interpolated and booked transactions, a plugin that wants to extend transactions that would run as invalid in the input syntax is difficult. See #541 for an example. The next iteration will see both the intermediate parser production and final resolved list of directives implemented as protocol buffer messages, with strictly distinct data types. This will replace beancount.core.data . The distinction between these two streams will be made very clear, and I will try to hide the former as much as possible. The goal is to avoid plugin writers to ever even see the intermediate list. It should become a hidden detail of the implementation of the core. Furthermore, there may be two types of plugins: a plugin that runs on the uninterpolated, unbooked output of the parser, and a plugin that runs on the resolved and booked stream. This would allow more creative use of partial input that might be invalid under the limitations of interpolation and booking. Updates: We could convert the plugin system to one that runs at booking/interpolation time. We should attempt to make the booking/interpolation atomic, in that one could write a Python loop with an accumulator and invoke it independently, so that in theory, booking could be used in the importers (like I do for Ameritrade). Make Rewriting the Input First Class \uf0c1 Added in Dec 2020 after comments and #586 . A number of frequently asked questions have to do with how to process the input data itself. Usually, a new user will attempt to load the contents of the ledger, modify the data structures, and print to update their file, not realizing that the printer includes all the interpolations, booking data, and modifications from plugins, so this cannot work. However, since we're rewriting the parser and ensuring a clean separation between intermediate ASI-like data and processed and finalized directives, we can implement a special printer for the AST intermediate data, so that users could run just the parser, modify the intermediate directives, and print them back out, perhaps losing just some of the formatting and whitespace. This formatting loss can be leveraged to reimplement bean-format more naturally: the output of that printer should always be formatted neatly. This would avoid users having to write ad-hoc parsers on their input file, sed-like conversions, and so on. They could do it properly by modifying the data structure instead. What's more, in order for this to work accurately, we'd have to delay processing of the arithmetic operations post-parsing, so that we can render them back out. This offers another advantage: if we process the calculations after parsing, we can afford to provide an option to let the user specify the precision configuration to use for mpdecimal. I really like that idea, because it avoids hard-coding calculation precision and better defines the outcome of these options, potentially opening the door to a more rational way to remove extra digits that often get rendered out. Finally, if a nice library function can be made to process transactions in-place and output them back out, preserving all comments around them, this can become another way\u2014perhaps the preferential way\u2014for us to clean payees and somesuch. At the moment, the solution is to write a plugin that will clean up the data, but the input file remains a bit of a mess. Making it easy to automatically clean up the input file is an appealing alternative and potentially will add an important new dimension to the Beancount workflow. I want to make all changes necessary to make this possible and well supported (I'm imagining my ledger file all cleaned up right now and it's appealing). I think it's not very much work, it involves: Storing begin/end line information on everything. Adding AST constructs for representing arithmetic calculations. Adding comments parsing to the renderer. Implementing a new renderer that can reproduce the AST, including handling missing data. Implementing a library to make modification of a file in-place as easy as writing plugins, while preserving all non-directive data in the file as is. Contributions \uf0c1 For most of the development of Beancount, I've been pretty reluctant to accept contributions. It has been a closely held pet project of mine since it has so much impact on my personal financial arrangements and I dread unplanned breakage. The main reservations I've had over contributions are two-fold: Not enough testing. Proposed changes that did not include enough testing, or none at all, sometimes even the kind testing that would prevent basic breakage. When I'd accept some proposals and commit to writing the tests myself it could sometimes take me down the rabbit hole for hours (if not days). This wasn't practical. Cascading design impact. Some of the proposals did not take into account broader design considerations that would affect other parts of the code, which I may not have communicated or documented well. I've had to reject some ideas in the interest of keeping things coherent and \"tight.\" Part of this is my fault: putting a high bar on contributions hasn't allowed potential contributors to acquire enough context and familiarity with the codebase to make changes compatible with its design. Allowing more contributions. Starting in Vnext I'd like to restructure the project so that more people are able to get involved directly, and to closely work 1-on-1 with some contributors on particular features. Clearly Beancount benefits from direct input from more people. The recent move to GitHub only compounds the urgency for making that easier. To this effect, I'd like to implement a few strategies: Break down the code in parts. This is a natural evolution that has been seen in many other projects; I'm no Linus nor Guido, but like their respective projects, as they grew in popularity, the original authors narrowed their focus on the core part and let other people expand on adjacent but also important functionality. I'd like to focus more of my time only on core functionality that will impact things like support for settlement dates, currency accounts, split transactions, trade reporting, etc. Letting other people deal with adding or updating price sources, making improvements to the ingestion framework, and making beautiful renderings and presentations of the data would be ideal. In time, I may eventually break down these libraries to separate repositories with looser contribution guidelines and/or add ACLs for others to push directly to those repos. Acquire \"lieutenants.\" I need to leave more space for trusted and frequent contributors to chip in more liberally. For example, Martin Michlmayr now has direct edit access to most documents and has been making numerous helpful contributions and updates to the docs. Kirill Goncharov's conversion of the documentation out of Google Docs is simply beautiful. RedStreet and many others are regularly pitching in answers on the mailing-list. Stefano Zacchiroli and Martin have built a standalone conversion tool from Ledger. Daniele Nicolodi is proposing some low-level changes to the scanner and parser. And of course, Dominik Aumayr and Jakob Schnitzer continue developing the Fava project adjacent to Beancount. There are many more people, there is a slowly-but-surely growing list of familiar recurring names. The question in my mind is: Is there a way to communicate with regular faces so that we're aligned in terms of design and can coordinate our efforts in the same direction? Does the newly acquired familiarity with video-conference meetings (thanks to the coronavirus crisis) afford us a new channel for coordination that wasn't there before? The Python community has thrived in no small part due to the annual face-to-face interactions between its participants and a number of core developers being in the same place for some time. Can we achieve the same thing online, as we are a smaller community? If so, better coordination may make it easier to accept proposed changes. I wonder if I should propose a monthly \"team\" meeting. 20% projects. I should provide a list of \"20% projects\" that are well aligned with the direction of the project for others to take up if they want to, and add profuse guidance and detail of downstream side-effects from those proposed features. The idea is to make it possible for newcomers to contribute changes that are likely to fit well and be easily integrated and accepted on the codebase.: Proposals. Beancount's equivalents of Python's \" PEPs \" are essentially the Google Docs proposal documents I started from threads where many others comment and add suggestions. A central list of those should be shared to a folder, identified as such, and allow others to write similar proposals. Maybe a little more structure and discipline around those would be useful. Restructuring the Code \uf0c1 At the very coarse level, the code restructuring for Vnext looks like this: C++ core, parser, and built-in plugins. The Beancount core, parser, booking algorithm and plugins get rewritten in simple C++, outputting its parsed and booked contents as a stream of protobuf objects. Query engine. Beancount query/SQL gets forked to a separate project operating on arbitrary data schemas, with a much broader scope than Beancount. See section below. The rest. Most of the rest gets cut up into separate projects or at least, at first, in a distinct place within the repository (until the core is rewritten). Note that because the core outputs the stream of directives as proto objects, any language supported by protobufs should be able to read those. This extends the reach of Beancount. Here's a simplified diagram showing how this might look: Here is a detailed breakdown of the various parts of the codebase today and what I think will happen to them: Core. This is the part of Beancount's code which will get rewritten in C++ and output a sequence of messages to a stream of directives. I'll continue keeping a tight focus on that part with a conservative eye toward stability, but in Vnext will be adding desired new capabilities that have been lacking so far as described in the next section of this document. The core will include the following packages: beancount/core beancount/ops beancount/parser beancount/utils beancount/loader.py beancount/plugins (some, see below) beancount/utils (most) Query. The query language will be factored out into a completely separate repo with a broader application domain (and hooks for customizing for Beancount). I suspect that over time that project will acquire a much broader range of contributors, many of which will not even be Beancount users. This includes the code from these packages: beancount/query beancount/tools Prices. This is a simple library and tool that helps users fetch prices from external sources. This should definitely move to another repo and I'd welcome a new owner building a competing solution. People are sending me patches for new price sources and I have too little time to maintain them over time, as the upstream sources change or even disappear. This requires very little from Beancount itself (in theory you could just print() the directives for output, without even loading library code) but I think the Beancount core should include and functions to enumerate a list of required date/instrument pairs at a particular date from a given ledger (and I'm happy to support that). Note that the internal price database core will remain in the core, because it's needed there. The affected packages are: beancount/prices Improvements should be made to this library after it moves out of the Beancount repository: we should isolate the Beancount code to just a few modules, and turn the scope of this project to something larger than Beancount: it's three things, really: a) an up-to-date Python library of price fetchers with unit tests and maintained by the community (i.e., when sources break, we update the library) and a common API interface (needs to be improved from what's there TBH, the API should support fetching time series in a single call); b) an accompanying command-line tool (currency \"bean-price\") for fetching those prices from the command-line. This requires the specification of \"a price in a particular currency from a particular source\" as a string. I'd like to improve that spec to make the USD: prefix optional, and maybe eliminate the chain of prices in the spec, which hasn't found much use in practice and move that upstream. c) Make the interfaces to fetch ledger-related information (e.g., list of missing/required prices and lists of instruments) onto modules: beancount v2, beancount Vnext, ledger, hledger, and rendering output formats to any of these. In other words, this library should be able to fetch prices even if Beancount isn't installed. To turn this project into something that can run independent of beancount. Ingest. The importers library will probably move to another repo and eventually could even find another owner. I think the most interesting part of it has been the establishment of clear phases: the identify, extract and file tasks, and a regression testing framework which works on real input files checking against expected converted outputs, which has worked well to minimize the pain of upgrading importers when they break (as they do break regularly, is a SNAFU). In the past I've had to pull some tricks to make command-line tools provided by the project support an input configuration as Python code but also possible to integrate in a script; I will remove the generic programs and users will be required to turn their configuration itself into a script that will just provide subcommands when run; the change will be very easy for existing users: it will require only a single line-of-code at the bottom of their existing files. The Bazel build may add some minor difficulties in loading a Python extension module built from within a Bazel workspace from an otherwise machine-wide Python installation, but I'm confident we'll figure it out. I'd also be happy for someone else to eventually take ownership of this framework, as long as the basic functionality and API remains stable. The example csv and ofx importers should be removed from it and live in their own repos, perhaps: ofx. the OFX importer should be replaced by something using ofxtools (the one I built is pretty bad), and the CSV importer really needs a thorough rewrite with lots of unit testing for the large list of options it now supports (these tests are sorely missing). csv. Furthermore, I think the CSV importer could be enhanced to be smarter and more flexible, to automatically detect from the column headers and inferred data types in the files which column should convert into which field. I'm not going to do that (I don't have time). Someone with the urge to make the ultimate automatic CSV parser ought to create a separate repository for that. The affected packages are: beancount/ingest : could eventually move to another repo. beancount/ingest/importers: someone could revive a repository of importer implementations, like what LedgerHub once aimed to become, and swallow those codes. See this document for details on what's to happen with the ingestion code. Custom reports and bean-web should be removed: the underlying bottle library seems unmaintained at this point, Fava subsumes bean-web, and I never liked the custom reports code anyway (they're a pain to modify). I never use them myself anymore (other than through bean-web). I really think it's possible to replace those with filters on top enhanced SQL query results. The conversion to Ledger and HLedger from Beancount now seems largely useless, I'm not sure anyone's using those. I'll probably move these to another repo, where they would eventually rot, or if someone cares, adopt them and maintain or evolve them. beancount/web : will be deleted or moved to another repo. beancount/reports : will be deleted or moved to another repo. Note that this includes deprecating beancount/scripts/bake , which depends heavily on bean-web. I have no substitute for bean-bake, but I think I'd like to eventually build something better, a tool that would directly render a user-provided list of specific SQL queries to PDF files and collate them, something you can print. Jupyter notebook support. A replacement for the lightweight interface bean-web used to provide could be Jupyter Notebook integration of the query engine, so that users can run SQL queries from cells and have them rendered as tables, or perhaps a super light web application which only supports rendering general SQL queries to tables. Built-in Plugins. Beancount provides a list of internal plugins under beancount/plugins . It's not indicated clearly, but there have evolved two groups of plugins in there: stable plugins used by the core, and experimental plugins showcasing ideas, which are often incomplete implementations of something that was proposed from a thread on the mailing-list. The former group will be ported to C++, and the latter group should probably move to another location with much looser acceptance constraints. First, there are \"meta-plugins\" which only include groups of other plugins: Only one of those should remain, and maybe be enabled by default (making Beancount pedantic by default): auto pedantic The following plugins should remain in the core and be ported to C++: auto_accounts check_closing check_commodity close_tree commodity_attr check_average_cost coherent_cost currency_accounts implicit_prices leafonly noduplicates nounused onecommodity sellgains unique_prices The following are the experimental implementations of ideas that should move to a dedicated repo where other people can chip in other plugin implementations: book_conversions divert_expenses exclude_tag fill_account fix_payees forecast ira_contribs mark_unverified merge_meta split_expenses tag_pending unrealized Because it's a really common occurrence, the new transfer_lots plugin should be part of the built-in ones. Projects. The beancount/projects directory contains the export script and a project to produce data for a will. The will script will be moved outside the core of Beancount, I'm not sure anyone's using that. Maybe the new external plugins repo could include that script and other scripts I shared under /experimental. The export script should be grouped together with beancount/scripts/sql and other methods to send / share data outside of a ledger; these could remain in the core (I'm using the export script regularly to sync my aggregates and stock exposure to a Google Sheets doc which reflects intraday changes). Scripts. Some of the scripts are completely unrelated to Beancount, they are companions. The scrape validator. The sheets upload. The treeify tool. These should be moved elsewhere. One of the advantages of having all the code in the same repo is that it makes it possible to synchronize API changes across the entire codebase with a single commit. As such, I may keep some of the codes in the same repo until the new C++ core has stabilized, and properly separate them only when Vnext releases. Universal Lightweight Query Engine (ulque) \uf0c1 The SQL query engine for Beancount was initially a prototype but has grown to become the main way to get data out of it. I've been pretty liberal about adding functionality to it when needed and it's time to clean this up and consider a more polished solution. In Vnext, the query/SQL code gets eventually forked to a separate project (and repo) operating on arbitrary data schemas (via protobufs as a common description for various sources of data) and has support for Beancount integration. Imagine if you could automatically infer a schema from an arbitrary CSV file, and run operations on it, either as a Python library function or as a standalone tool. Furthermore, this tool will support sources and/or sinks to/from Google Sheets, XLS spreadsheets, containers of binary streams of serialized protos, tables from HTML web pages, PDF files, directories of files, and many more. This is going to be a data analysis tool with a scope closer to that of the Pandas library rather than an accounting-focused project, but also a universal converter tool, that will include the functionality of the upload-to-sheets script (which will get removed). One of the lessons from the SQL query engine in Beancount is that with just a little bit of post-processing (such as treeify ), we can do most of the operations in Beancount (journals, balance sheet & income statements) as queries with filters and aggregations. The tool will be made extensible in the ways required to add some of the idiosyncrasies required by Beancount, which are: Native support for a Decimal type . The addition of custom types for aggregators with the semantics of beancount.core.Inventory/Position/Amount . The ability to automatically generate a dynamic column rendering a line-by-line aggregation of another column (or set thereof), that is, a \"balance\" column . The ability to render a \" bottom line \" of aggregates at the end of the results table. Functions for splitting of aggregated columns , for amounts and inventories into multiple columns (e..g, \"123.00 USD\" becomes two columns: (123.00, \"USD\") to be processable in spreadsheets, and also for splitting debits and credits to their own columns. In particular, printing multiple lots accumulated in an account should be made natural from the SQL query, replacing the \"flatten\" feature by a more standard splitting off an array type. Moreover, broadening the focus with a new project definition will make a change to testing it thoroughly (the current one is still in a bit of a prototype stage and does not have nearly the amount of required tests), and also include data type validation (no more exceptions at runtime), by implementing a typed SQL translator. I'll document this elsewhere. This is a much bigger project, but I suspect with the broader scope, it will be easier to test and take on a life of its own. I'm preparing a design doc on this. API Rework \uf0c1 I write a lot of custom scripts, and there are a number of things that bother me about today's Beancount API, which I want to radically improve: Consolidate symbols under \"bn\". The internal API calls for importing the symbols from each package separately, but now that I'll have split off the ingestion and reporting code, all of the public API, or at least the majority of the commonly used objects in the core should be available from a single package, a bit like numpy: import beancount as bn \u2026 bn.Inventory(...) bn.Amount(...) bn.Transaction(...) # etc. I'd like for \"bn\" to become the de-facto two-letter import on top of which we write all the scripts. Default values in constructors. The namedtuple containers are mighty fine, but their constructors never had optional arguments, and it's always a bit of a dance to create those containers with a ton of \"None\" options. I never liked it. We'll make this tidy in the next iteration. No API documentation. While there is a substantial amount of documentation around the project, there is no documentation showing people how to use the Python API, e.g. how to accumulate balances, how to create and use a realization tree, how to pull information out of an accumulated inventory object, etc. I think that documenting some of the most common operations will go a long way towards empowering people to make the most out of Beancount. Some of these operations include: Accumulating lots of an inventory and printing them. Converting to market value, and making corresponding account adjustments. \u2026. add more \u2026 Exposed, usable booking. Booking will be a simple loop that can be invoked from Python with an entry and some accumulated state. Moreover, the Inventory object should begin to implement some of the lower-level operations required for booking, such that iterating over a set of postings and doing e.g., average booking, can be done via method calls on an Inventory object. Inventory should take a more prominent place in the API. Data types. Well defined data types should be provided for all objects to make liberal use of the typing module over all new code. Maybe create a module called \"bn.types\" but they should be available directly from \"bn.*\" so that there is a single short-named import. Terminology. I'd like to stop using \"entries\" and consolidate over the name \"directives\" in Vnext. Realization. I've been using a collections.defaultdict(Inventory) and a \"realization\" interchangeably. Both of these are mappings from an account name (or some other key) to an Inventory state object. I'd like to unify both of these constructs into the realization and make it into a commonly used object, with some helper methods. Parser Rewrite \uf0c1 Since we will now depend on C++, the parser will get to be rewritten. Worry not: the input syntax will remain the same or at least compatible with the existing v2 parser. What will change is: Unicode UTF-8 support. The lexer will get rewritten with RE/flex instead of GNU flex. This scanner generator supports Unicode natively and any of the input tokens will support UTF-8 syntax. This should include account names, an oft-requested feature. Flags. The current scanner limits our ability to support any flag and supports only a small list of them. I think the list has proved sufficient for use, but since I'll be putting some work into a new scanner I'm hoping to clean up that story and support a broader, better defined subset of single-letter flags for transactions. Time. The parser will parse and provide a time field, in addition to the date. The time may be used as an extra key in sorting directives. The details for this are yet to be determined, but this is requested often enough at the very minimum the parser will output it as metadata, and at best, it may become a first-class feature. Caching. The pickle cache will be removed. Until very recently , there weren't great options for disabling it (env vars) and I'd rather remove the only two environment variables that Beancount honors as a side-effect. Since the C++ code should be fast enough, hopefully a cache will not be needed. Tags & links. In practice, those two features occupy a very similar role as that of metadata (used to filter transactions). I'm contemplating unseating the special place taken by tags and links in the favor of turning those into metadata; the input syntax would not be removed, but instead the values would be merged into the metadata fields. I'm not 100% sure yet about doing this and open for discussion. Furthermore, the parser should be made to accept #tag and ^link where metadata is declared today, which would be convenient syntax. Finally, users have expressed a desire for tags on postings. We should contemplate that. Plugins configuration as protos. The options for the various plugins have been loosely defined as eval'ed Python code. This is pretty loose and doesn't provide a great opportunity for plugins to do validation nor document their expected inputs. I'd like to formalize plugin configuration syntax a bit, by supporting text-formatted protos in the input syntax (for a message type which would be provided by the plugins themselves). Parser in C++. The parser will be rewritten in C++. In the process of writing Vnext, I'll try to maintain a single grammar for both for as long as possible by calling out to a C++ driver interface, which will have two distinct implementations: one for the V2 version calling into Python, and one for the Vnext parser generating protos. In the process I may be porting the lexer and grammar Python implementation to C, as discussed in this ticket . Better includes. Current includes fail to recognize options that aren't in the top-level file. This caused many surprises in the past and should be fixed. At the minimum, an error should be raised. Code Quality Improvements \uf0c1 Rename \"augmentation\" and \"reduction\" to \"opening\" and \"closing\" everywhere. This is just more common terminology and will be more familiar and understandable to people outside of our context. Type annotations. The use of mypy or pytype with type annotations in Python 3 is by now a very common sight, and works quite well. As part of Vnext, all of the core libraries will be modified to include type annotations and the build should be running pytype automatically. I'll need to add this to our Bazel rules (Google doesn't currently provide external support for this). While doing this, I may relax some of the Args/Returns documentation convention, because in many cases (but not all) the type annotations are sufficient to get a good interpretation of a function's API. PyLint in build. Similarly, the linter should be run as an integral part of the build. I'd like to find a way to selectively and explicitly have to disable it during development, but otherwise be set up such that lint errors would be equivalent to build failures. Flexible constructors for Python API. The types generated by collections.namedtuple() or typing.NamedTuple don't offer flexible constructors with named parameters. I think all codes that create transaction objects today would benefit from having constructors with default values, and I'll be providing those to create corresponding proto objects. Tolerances & Precision \uf0c1 The story around how precision and tolerances are dealt with hasn't been great, for two reasons: Explicit tolerance option. I've tried to design the tolerance (used for balancing transactions) to be automatic and automatically inferred from statistics from the numbers in the input. The results aren't great. In Vnext I aim to provide an explicit option for setting the tolerance per currency. Precision. There are various places where numbers get rendered in v2: the reports code, the SQL query, and debugging scripts, and the way precision is set hasn't been used consistently. The precision also needs to be explicitly settable by the user. Rounding. There is another quantity that's used during interpolation: the precision used to round calculated numbers. Moreover, there is a need to distinguish between the precision and tolerances for numbers when used as prices vs. when used as units (see here ). One way is to store the display context per currency PAIR, not per currency itself. The distinction between these quantities hasn't been documented well; I'll keep in mind to clearly annotate those codes in Vnext and add suitable docs for this. Mostly the precision will be a rendering concern and a quantity that will be relevant for the new universal SQL query tool. Some prior design documentation exists here . Core Improvements \uf0c1 Some desiderata of new features are discussed below. These are all relevant to the core. Note that the changes should not interfere with current usage much, if at all. I expect that v2 users will be largely unaffected and won't have to change their ledger files. Booking Rules Redesign \uf0c1 Main document One of the current problems with booking is that entering an augmenting leg and a reducing leg have to be different by nature. The augmentation leg has to provide the cost basis via {...} syntax, and the reducing leg has to enter the price in the annotation and not in the cost basis. For example: 2021-02-24 * \"BOT +1 /NQH21:XCME 1/20 FEB 21 (EOM) /QNEG21:XCME 13100 CALL @143.75\" Assets:US:Ameritrade:Futures:Options 1 QNEG21C13100 {2875.00 USD} contract: 143.75 USD ... 2021-02-24 * \"SOLD -1 /NQH21:XCME 1/20 FEB 21 (EOM) /QNEG21:XCME 13100 CALL @149.00\" Assets:US:Ameritrade:Futures:Options -1 QNEG21C13100 {} @ 2980.00 USD contract: 149.00 USD ... Notice how the selling transaction has to be written down differently from the perspective of the user. The thing is, this makes it difficult from the perspective of the importer writer. It also ties the required syntax with the state of the inventory it's applied to, as it assumes something about this inventory. Moreover, this makes it difficult to write an importer that would handle a crossing of the absolute position, like this: 2021-02-19 * \"BOT +1 /NQH21:XCME @13593.00\" Assets:US:Ameritrade:Futures:Contracts 1 NQH21 {271860.00 USD} contract: 13593.00 USD Assets:US:Ameritrade:Futures:Margin -271860.00 USD Expenses:Financial:Commissions 2.25 USD Expenses:Financial:Fees 1.25 USD Assets:US:Ameritrade:Futures:Cash -3.50 USD 2021-02-19 * \"SOLD -2 /NQH21:XCME @13590.75\" Assets:US:Ameritrade:Futures:Contracts -2 NQH21 {271815.00 USD} contract: 13590.75 USD Assets:US:Ameritrade:Futures:Margin 543630.00 USD Income:US:Ameritrade:Futures:PnL 45.00 USD Expenses:Financial:Commissions 4.50 USD Expenses:Financial:Fees 2.50 USD Assets:US:Ameritrade:Futures:Cash -52.00 USD The issue here is that we're crossing the flat line, in other words, we go from long one to short one. There are only two ways to do that properly right now: Disable booking and use the cost only, as per above. This is not great \u2014 booking is terribly useful. Track the position in your importer and separate the reducing and augmenting legs: 2021-02-19 * \"SOLD -2 /NQH21:XCME @13590.75\" Assets:US:Ameritrade:Futures:Contracts -1 NQH21 {} @ 271815.00 USD Assets:US:Ameritrade:Futures:Contracts -1 NQH21 {271815.00 USD} Both solutions aren't great. So I came up with something new: a complete reevaluation of how the syntax is to be interpreted. In fact, it's a simplification. What we can do is the following: use only the price annotation syntax for both augmentation and reduction and currency conversions, with a new booking rule \u2014 Match lots without cost basis in priority. If the lots have no cost basis, the weight of this posting is simply the converted amount, as before. If a match has been made against a lot with cost basis, the weight of this posting is that implied by the matched lots. Make the {...} used solely for disambiguating lots to match, and nothing else. If you have unambiguous matches, or a flexible booking strategy, e.g. FIFO, you'd pretty much never have to use the cost matching reduction. With this, the futures transaction above would simply use the @ price annotation syntax for both transactions. It would Make importers substantially simpler to write Supports futures naturally Be backward compatible with existing inputs for both currency conversions and investments. It is also more generally consistent and friendlier to Ledger users, without sacrificing any of the tighter constraints Beancount provides. I think it's even simpler to think about. Furthermore, this: Assets:US:Ameritrade:Futures:Contracts -1 NQH21 {} @ 271815.00 USD would be interpreted as \"match this lot, but only those with a cost basis attached to them.\" One question that remains is to decide whether an augmentation \u2014 now written down simply with @ price annotation \u2014 would store the cost basis in the inventory or not. I think we could make this determination per-commodity, or per-account. This would impose a new constraint: a commodity (or \"in an account\") would always be stored with cost basis, or not. Posting vs. Settlement Dates \uf0c1 When you import a transaction between multiple accounts within a single ledger, e.g. a credit card payment from one's checking account, the dates at which the transaction posts in each account may differ. One side is called the \"transaction date\" or \"posting date\" and the other side the \"settlement date.\" Where the money lives in between is somewhere in limbo (well in practice there is no money at all, just differences in accounting between institutions, things are never reflected instantly). One of the major shortcomings of the current core code is that the ability to insert a single transaction with postings at different dates is missing. Users are recommended to select a single date and fudge the other one. Some prior discussion on this topic exists here . Unfortunately, this method makes it impossible to represent the precise posting history on at least one of the two accounts. A good solution needs to be provided in Vnext, because this is a very common problem and I'd like to provide a system that allows you to precisely mirror your actual account history. The automatic insertion of transfer accounts to hold the commodities can be implemented as a feature, and it should live in the core. One possible idea would be to allow optional posting dates, like this: 2020-01-19 * \"ONLINE PAYMENT - THANK YOU\" \"\" Assets:US:BofA:Checking -2397.72 USD 2020-01-21 Liabilities:US:Amex:BlueCash 2397.72 USD which would result in two transactions behind the scenes, like this: 2020-01-19 * \"ONLINE PAYMENT - THANK YOU\" \"\" Assets:US:BofA:Checking -2397.72 USD Equity:Transfer 2020-01-21 * \"ONLINE PAYMENT - THANK YOU\" \"\" Liabilities:US:Amex:BlueCash 2397.72 USD Equity:Transfer The lack of symmetry here raises the question of whether we should allow a transaction without a date or not: * \"ONLINE PAYMENT - THANK YOU\" \"\" 2020-01-19 Assets:US:BofA:Checking -2397.72 USD 2020-01-21 Liabilities:US:Amex:BlueCash 2397.72 USD I think we can figure this out and the first solution is very doable. Input Split Transactions Some users like to organize their inputs in different files, or in different sections that strictly contain all of an account's transactions in order. This is related in spirit to the posting and settlement dates problem: at the moment the user is required to choose one of the two locations to insert their transaction. This should not be necessary. We should provide a mechanism that would allow users to insert the halves of a transaction into two different locations in their file, and a robust merging mechanism that would ensure that the two related transactions have been matched and merged (so that no unmerged half remains) and otherwise report errors clearly. The two halves could look like this: 2020-01-19 * \"ONLINE PAYMENT - THANK YOU\" \"\" Assets:US:BofA:Checking -2397.72 USD \u2026 2020-01-21 * \"AMEX EPAYMENT ACH PMT; DEBIT\" Liabilities:US:Amex:BlueCash 2397.72 USD The matching could be done via explicit insertion of special links, or by heuristics to match all such related transactions (perhaps declaring valid account pairs, thresholding on date differences and exactly matching amounts). When impossible to match, an error should be raised. Those merged transactions should be checked for balancing. Note how each of the transactions has a differing date; this would integrate with the transfer account solution proposed in the previous section. I haven't designed something yet, but this should be easy to implement and should be provided as a core feature, since it's so closely related to the input syntax. Currency Accounts instead of a Single Conversion \uf0c1 The current implementation of multiple currency transactions relies on a special \"conversion transaction\" that is automatically inserted at reporting time (when closing the year) to account for the sum total of imbalances between currencies. The goal of this transaction is to ensure that if you just sum up all the postings in the book, the result is purely an empty inventory (and not some residual amount of profit or loss incurred during currency exchange across different rates \u2014 note that we're talking only of the @price syntax, not investments). This is a bit of a kludge (the transaction itself does not balance, it converts to zero amounts of a fictional currency in order to keep itself quietly passing the balance test). What's more, its actual value is dependent on a subset of filtered transactions being summed up so it's a reporting-level construct, see here . There exists a method for dealing with multiple currencies without compromising on the hermeticity of individual transactions, described online, here . Using that method, you can filter any subset of transactions and summing them up will cleanly cancel out all lots. You don't need to insert any extra weight to fix up the balance. Also, you can explicitly book profits against the accrued gains in the currency accounts and zero them out and take advantage of this when you report them (and track them over time). The downside is that any currency conversion would see extra postings being inserted, etc. 2020-06-02 * \"Bought document camera\" Expenses:Work:Conference 59.98 EUR @ USD Liabilities:CreditCard -87.54 USD Equity:CurrencyAccounts:EUR -59.98 EUR Equity:CurrencyAccounts:USD 87.54 USD The problem is that it's a pain to use this method manually, it requires too much extra input. It's possible to have Beancount do that for us behind the scenes, completely automatically. I coded a proof-of-concept implementation here , but it's incomplete . In Vnext: The insertion of the kludgey conversions transactions should be removed. The currency accounts should become the norm. The fact that the two streams of directives will be very clearly separated should help, by distinguishing even more clearly between the parsing representation and the fully booked one, which will show these extra legs on transactions The prototype should be completed and issues fixed completely (not that much work involved). Strict Payees \uf0c1 I'm not sure if this makes sense yet, but I'd like to clean up the mess that payee strings are today. Payees are free-form, and if the user does not take care to clean them up\u2014and I'm one of those who doesn't\u2014the memos from imported sources are messy. It could be interesting to create a new directive to declare payee names ahead of time and an optional model that would require payees to be found in the list of declared payee names. Payees would have to have open and close dates, dates which would define the valid duration of the relationship with that payee (thereby adding more error verification capability). Price Inference from Database \uf0c1 Interpolation from price database. One of the oft-requested features is the ability to automatically interpolate prices from the internal price database history. I think that this should be doable unambiguously and deterministically and added to the interpolation algorithm. Price validation. Since a lot of the conversions at price (i.e., using \"@\") are inferred by leaving out one number, we should validate that the effective price is within some tolerance of a pre-existing price point near the date. This would provide yet another level of checking. Quantizing Operators \uf0c1 Another useful addition to the syntax would be operators that automatically quantize their result to a precision that depends on the particular target currency. For example, 1970-01-01 * \"coffee\" Expenses:Food:Net 2.13 / 1.19 EUR Expenses:Food:Taxes 2.13 / 1.19 * 0.19 EUR ; for example to calculate tax Assets:Cash That would become: 1970-01-01 * \"coffee\" Expenses:Food:Net 1.789915966386555 EUR Expenses:Food:Taxes 0.340084033613445 EUR Assets:Cash If instead an operator like this were provided, it would fix the issue: 1970-01-01 * \"coffee\" Expenses:Food:Net 2.13 /. 1.19 EUR Expenses:Food:Taxes (2.13 / 1.19 * 0.19). EUR Assets:Cash Or somesuch. Or maybe we'll want to add an option such that every evaluation of an arithmetic expression is automatically quantized as such. Constraints System & Budgeting \uf0c1 Beancount does not support budgeting constraints explicitly, but I think it would be possible to extend the balance assertion semantics to cover this. The current balance assertions check (a) a single commodity, and (b) that the amount is precisely equal to an expected one. Balance assertions should be extended to support inequalities, e.g., 2020-06-02 balance Liabilities:CreditCard > 1000.00 USD and perhaps we could check for the total inventory like this 2020-06-02 balanceall Assets:Cash 200.00 USD, 300.00 CAD I'd be curious to hear what would work best from users who do budgeting and design a minimalistic expression language to support that use case (though I'd try to keep it as simple as possible to avoid feature creep). Also, if the syntax is getting changed, adding a syntax that allows checking multiple currencies at once, and possibly a complete assertion that checks there aren't other commodities in the account could also make sense. Average Cost Booking \uf0c1 Average cost booking has been discussed and a good solution sketched out a very long time ago. Vnext should sport that method natively; a lot of users want to have this feature for dealing with their tax-deferred accounts. It takes a bit of work to handle the precision of the various automated conversions right. The way it would work is by automatically merging all related lots of the same commodity on a reduction, and optionally on an augmentation. Some constraints may be required (e.g. only a single commodity in that account). Trade Matching & Reporting \uf0c1 A few core tasks related to P/L and trading still need to be implemented. Trade list. A problem that I've really been wanting to solve for a very long time but never found the time for is to save crumbs from the booking process so that a correct list of trade pairs could be easily extracted from the list of directives. I wrote some notes here and here a long time ago. Essentially, for each booked reduction, insert a reference to the corresponding augmenting posting. I've prototyped this as metadata but it should be made something more official. A single linear scan can pick up these references, build a mapping and recover the (buy, sell) pairs to produce a table of trades. There's a precedent I once wrote in a plugin . Needless to say, producing a list of trades is a pretty basic function that Beancount does not provide out of the box today; it really should. Right now users write their own scripts. This needs to be supported out-of-the-box. Harmonize balance and gains validation. Checking that transactions balance and that income from gains balance with a transaction's prices (the sellgains plugin) are done in completely separate places. Those two codes occupy similar roles, and should be implemented next to each other. Commissions in P/L. Properly counting profits & losses by taking off the fraction of buying commission of an original lot and the selling commission into account is not possible at the moment. I think it could be done with a plugin that moves some of the (computed) income leg into a separate negative income account to do this properly for reporting purposes. Self-Reductions \uf0c1 Currently the application of reductions operates on the inventory preceding the transaction. This prevents the common case of self-reductions, and both I and some users have come across this problem before, e.g. this recent thread ( ticket ). This comes off as unintuitive to some users and ought to have a better solution than requiring splitting of transactions. Since we're rewriting the booking code entirely in Vnext, contemplate a new definition that would provide a well-defined behavior in this case. I remember from prior experiments attempting to implement this that it wasn't a trivial thing to define. Revisit. This would be a nice improvement. Stock Splits \uf0c1 Some discussion and perhaps a strategy for handling stock splits should be devised in Vnext. Right now, Beancount ignores the issue. At the minimum this could be just adding the information to the price database. See this document for more details. Multipliers \uf0c1 Options have a standard contract size of 100. Futures have a contract size that depends on the particular instrument (e.g., /NQ with a multiplier of 20). I've been handling this for options by multiplying the units by 100, and for futures by multiplying the contract size by the per-contract multipliers (ditto for options on futures). I do this in the importers. For options, it works and it isn't too bad (e.g. positions of -300 instead of -3), but for futures, it's ugly. The result is not representative of the actual transaction. I'd like to add a per-currency multiplier, as well as a global dictionary of regexps-to-multiplier to apply, and for this to be applied everywhere consistently. One challenge is that everywhere there's a cost or price calculation, this has to be applied. In the current version, those are just multiplications so in many parts of the codebase these haven't been wrapped up in a function that could easily be modified. This needs to happen in a big rewrite \u2014 this is the opportunity to do this. Example here. Returns Calculations \uf0c1 If you look at investment brokers out there, no one calculates returns correctly. Brokers provide one of two features: No cash transfers. Total value of account today vs. total value of account at some point in the past (i.e., account inception or beginning of the year). This isn't very useful because they never account for the addition or removal of cash to the account. For example, say you open an account with $100,000 and invest, and mid-year add another $20,000, say the original investments are now worth $95,000, the report would show a gain of $15,000, whereas you really incurred a loss. Better brokers like Vanguard will show a plot that includes two overlaid bars, one with cash added and profit overlaid, like this: Lack of interest or dividends. Other brokers will report P/L over time from the investments, but they fail to account for actual interest or dividends received (they only look at the price of the underlying) so that's not useful for bonds or for stocks with significant dividends, or when grouping them, they fail to account for the addition of new positions over time. Counterfactual performance. Finally, all of them fail to compare your actual annualized performance with that of a benchmark portfolio with equivalent cash infusions. For example, instead of your actual investments made, compare with the performance you would have obtained if you had invested in some standardized portfolio of investments over that particular time period, given the actual historical prices of those instruments. Ideally one should be able to define any alternative portfolio to compare against using their particular cash transfers. More fancy analyses aren't even contemplated, e.g., what would have been the impact of changing my rebalancing strategy (or actually implementing a more strict one)? There are well known methods for both time-based and value-based returns reporting. The right thing to do is to extract a time-series of cash flows and compute the annualized or IRR returns, or value-weights. I started this work at some point and ran against some difficulties and eventually removed it. The results remain here. I'd really love to build this, and eventually perhaps this could grow into its own project, with associated support functions in the Beancount core. This will possibly be a project of its own, but this requires similar support for enumerating instruments and price sources as that which is needed for fetching prices, as well as functions for isolating cash flows for specific subsets of accounts; these should probably live in the core. UPDATE September 2020: This has mostly been implemented. See this document for details. Unsigned Debits and Credits \uf0c1 A useful idea that's nearly trivial to implement is to allow users to input all positive unit numbers and to automatically flip the signs on input, and to output them all as positive numbers as well splitting them up between Debit and Credit columns. This would make Beancount's rendering a lot easier to grok for people with an existing background in accounting. This feature will introduce no complexity, easy to add for free. See TODO here . Holdings \uf0c1 One of the libraries I built at some point was this notion of a \"holding\", here . At the time I wasn't sure if that data would contain much more than what's in a posting, but as it turns out, representing holdings as just the posting is good enough, all you need is the market value, and compute the total values from the units is trivial. In fact, I've haven't been using that code for years, I've been using the export script instead, which writes out a table that gets uploaded to a Google Sheets doc. This proves to me aggregating the positions in an Inventory is plenty sufficient in practice, along with a mapping of latest market prices. I'm going to delete that code. It's only been used in the reports code anyway, which will be removed anyway, and in the experimental \" unrealized gains \" plug, which was only a proof-of-concept that convinced me booking such gains as transactions is not a good idea and which will get removed and live in a separate experimental repository anyway. Tooling for Debugging \uf0c1 Context should recover erroneous transactions. One of the major annoyances of error recovery is that if a transaction involves some types of errors, the postings aren't produced in the stream of directives. This problem is related to the lack of clarity between the merely parsed data structure and the fully resolved one. In particular, this means that the \"bean-doctor context\" debugging command often cannot provide useful context around a failing transaction. This really needs to be fixed, to improve debuggability. Document debugging tools. In general, I should write a better exposition of how to use the various transaction debugging tools; a lot of the questions on the mailing-list would disappear if users knew better how to leverage those. Interactive context in Emacs. If the performance allows it, we could build an Emacs mode which renders the context around a partially written transaction, including inventories of the affected accounts before and after the transaction, as well as interpolated values, to a different buffer updated interactively. This would make it much more fun to input data and provide immediate feedback about the newly inserted transaction. Documentation Improvements \uf0c1 Remove dependency on furius.ca. The current Google Docs based documentation links to other documents via a global redirect (the definition is found here ). While it does not happen often that my web server goes down (perhaps a few times per year), when it does it takes a few days to rectify the situation. That server is hosted graciously in the company of some friends of mine. Kirill has proved that it would be possible to replace all the links to redirects on github, that would look like this: beancount.github.io/ instead of furius.ca/beancount/doc/ . In order to do this, I'll need to run a script using the Docs API on all the Google Docs to change them automatically. Conclusion \uf0c1 There are other items in the TODO file . These are just the main, big issues that I think matter the most and I'd like to address them in a Vnext rewrite. Development branches will look like this: v2 : Current master will be branched to \"v2\", which will track the stable current version. That branch will build with both the current setup.py system and Bazel. Fixes will be implemented on that branch where possible, and merged to Vnext. master : Current master will become Vnext. Only the Bazel build will be supported on that branch. Any comments appreciated. Appendix \uf0c1 More core ideas for Vnext that came about during discussions after the fact. Customizable Booking \uf0c1 For transfer lots with cost basis\u2026 an idea would be to create a new kind of hook, one that is registered from a plugin, e.g. a callback of yours invoked by the booking code itself, and whose results applied to a transaction are immediately reflected on the state of the affected inventories. Maybe this is the right place to provide custom algorithms so that their impact is affecting the subsequent inventories correctly and immediately. Now, imagine generalizing this further to provide and implement all of the current booking mechanisms that are currently built in the core. Call this \"customizable booking.\" ( thread ). Ugly Little Things \uf0c1 print_entry() uses buffering that makes it impossible to use regular print() interspersed with the regular stdout without providing file= option. Fix this, make this regular instead, that's just annoying, just print to regular stdout. The default format for __str__ for inventories puts () around the rendering. When there's a single position, that looks like a negative number. That's dumb. Use {} instead, or something else. Add a flag to bean-check to make it run --auto plugins by default. This is great for imported files, which may not have a complete ledger to feed in. Incremental Booking/ Beancount Server / Emacs Companion \uf0c1 In order to make recomputation fast, the idea of creating a standalone \"Beancount server\" starts to make sense. The expensive part of the Beancout calculations on a large file is the booking and interpolation. The key to making things fast is thus to keep all the original unprocessed transactions in memory along with the booked and interpolated ones, and on a change, reparse the modified files and scan all the transactions, updating only the ones whose accounts have been affected. This could be problematic in theory: some plugins may rely on non-local effects in a way that affects what they output. I believe in practice it would work 99% of the time. But I think it may be worth a prototype. On the other hand, Vnext may turn out to be fast enough recomputing everything from scratch every single time (my own file went from 4s -> 0.3ms for the parsing stage of the largest file), so maybe this is irrelevant overall. Such a server would be a perfect companion to a running Emacs. We could build an Emacs mode which communicates with the server. Tags & Links Merge with MetaData \uf0c1 TODO(blais): Add colon syntax","title":"Goals & Design"},{"location":"beancount_v3.html#beancount-vnext-goals-design","text":"Martin Blais , July 2020 http://furius.ca/beancount/doc/Vnext","title":"Beancount Vnext: Goals & Design"},{"location":"beancount_v3.html#motivation","text":"It's time to give Beancount a refresh and to put down a concrete plan for what the next iteration of it ought to be. I've had these thoughts in the back of my mind for a long while\u2014at least a year\u2014and I'm putting these in writing partly to share a vision of what the product we all use to organize our finances can become, partly to solicit feedback, and partly to organize my thoughts and to prioritize well on the stuff that matters. Current status. The current state of Beancount is that development has been static for a while now, for a number of reasons. The software is in a state that's far from perfect (and I'll be enumerating the main problems in this document) but I've been resisting making too many changes in order to provide myself and others a really stable base to work from. More importantly, while I used to be able to spend a significant amount of weekend time on its development, life changes and a focus on my career of late has made it difficult for me to justify or find the extra time (it has been 10 years after all). A multitude of ideas have aged to this TODO file but it's too detailed to grok and a bit of a dump, this document should be more useful. Why rewrites happen. When I wrote version 2 of Beancount (a full rewrite of the first version), it was because of a confluence of ideas for improving my first draft; I resisted for a while, but eventually it made so much sense to me that it became simply impossible not to write it. Many of the ideas driving the redesign at the time are still axioms in today's design: removing order dependence, normalizing the syntax to be well-defined with a BNF grammar, converting custom processing to a sequence of plugins off of a simple stream of directives, the current design of booking selection and how cost basis works, and all the directives beyond \"Transaction\". These ideas largely shape what a lot of people like about using Beancount today. Goals. Now is the time for yet another wave of evolution for Beancount, and similarly, a set of new ideas I'm going to lay down in this document form as potent a change as the v1 to v2 transition. The vision I have for Vnext will simplify Beancount, by factoring into simpler, more isolated, more reusable, better defined parts, and not merely by adding new features on top of what's there. In many ways, Vnext will be a distillation of the current system. It will also make space to finally implement some of the core features most often desired by users. And those changes will enhance some organizational aspects: allow for more contributions, and also trim down the part that I'm handling myself to less code, so I can more effectively focus on just the core features.","title":"Motivation"},{"location":"beancount_v3.html#current-problems","text":"","title":"Current Problems"},{"location":"beancount_v3.html#performance","text":"My personal ledger, and I know that the ledgers of many users, are simply too large to process instantly. My current file takes 6 seconds on the souped-up NUC I use for a desktop at home\u2014but that's just too long. I'm really quite attached to the idea of processing the entire set of inputs every time, instead of forcing users to cut-up their ledgers into multiple files with \"closing\" transitions at arbitrary points in the year, but I really do want that \"instant\" feeling that you get when you run two-letter UNIX programs, that it runs in well under half a second . It makes it a lot more interactive and fun to use. C++ rewrite. One of the reasons for the slow performance right now is the fact that Beancount is implemented in Python, even at the level of the parser (C code calling back into a Python driver). An obvious solution is to rewrite the core of the software in a language closer to the metal, and that will be C++. I'm selecting C++ for its control and because the current slate of tools around it is mature and widespread enough that it should be easy for most to build without too many problems, and I can leverage C libraries that I will need. Using a functional language could have been fun but many of the libraries I want simply would not be available or it would be too difficult for mere mortals to build. Simple, portable C++. It's important to mention that the C++ code I have in mind is not in the style of template-heavy modern C++ code you'd find in something like Boost. Rather, it's a lot more like the conservative \"almost C without exceptions\" subset of C++ that Google uses , with a base on Abseil-Cpp (for example and flavor, see tips ). The reasons for this are stability and portability, and while this rewrite is for faster performance, I believe that it will not be necessary to pull template tricks to make it run fast enough; just a straightforward port to avoid the Python runtime will likely be sufficient. Above all I want to keep the new code simple and \"functional-ish\" as much as possible (no classes if I can avoid it), relying on a trusted set of stable dependencies , built hermetically using the Bazel build tool. Python API. It's also important that the Python API remains for plugins and scripts, and that the full suite of unit tests be carried over to the newer version of the code. After all, the ability to write custom scripts using all that personal finance data is one of the most attractive features of the text-based approach. Code beyond the new core implementation will remain in Python, and existing code built on top of the Python API should be very easily portable to Vnext. This can be achieved by exposing the directives with wrappers written in pybind11. Other languages. The resolved output of the Beancount core will be a stream of protocol buffer objects, so processing from other languages (e.g., Go) will have first-class support. Processing model. An additional algorithmic improvement to performance, should it be necessary, would be to define plugins processing in terms of iterator functions that cascade and interleave the processing of the directive stream without making entirely disjoint passes over the full list of directives. While the freedom to have each plugin process all the directives on their own has been instrumental in keeping the system free of synchronized application state and has allowed me to isolate behavior of the plugins from each other, there are opportunities to join together multiple transformations in a single pass. Fewer passes = faster.","title":"Performance"},{"location":"beancount_v3.html#intermediate-parsed-data-vs-final-list-of-directives","text":"In Beancount v2, I was never too careful to clearly distinguish between The list of directives coming out of the parser , missing interpolation and booking, using position.CostSpec instead of position.Cost on each lot, and The resolved and booked list of directives with booking algorithms applied to select matching lots, and interpolated values filled in, as well as transformations having been applied by the various plugins. These two lists of directives are really quite distinct in purpose, though they share many common data structures, and for the most part, the first list appears mostly in the parser module. There have been cases where it was confusing, even to me, which of the lists I was manipulating. Part of the reason is due to how I'm using mostly the same Python data structures for both, that allow me to bend the rules on typing. Perhaps more importantly is that because plugins run after booking and interpolation, and are required to put out fully interpolated and booked transactions, a plugin that wants to extend transactions that would run as invalid in the input syntax is difficult. See #541 for an example. The next iteration will see both the intermediate parser production and final resolved list of directives implemented as protocol buffer messages, with strictly distinct data types. This will replace beancount.core.data . The distinction between these two streams will be made very clear, and I will try to hide the former as much as possible. The goal is to avoid plugin writers to ever even see the intermediate list. It should become a hidden detail of the implementation of the core. Furthermore, there may be two types of plugins: a plugin that runs on the uninterpolated, unbooked output of the parser, and a plugin that runs on the resolved and booked stream. This would allow more creative use of partial input that might be invalid under the limitations of interpolation and booking. Updates: We could convert the plugin system to one that runs at booking/interpolation time. We should attempt to make the booking/interpolation atomic, in that one could write a Python loop with an accumulator and invoke it independently, so that in theory, booking could be used in the importers (like I do for Ameritrade).","title":"Intermediate Parsed Data vs. Final List of Directives"},{"location":"beancount_v3.html#make-rewriting-the-input-first-class","text":"Added in Dec 2020 after comments and #586 . A number of frequently asked questions have to do with how to process the input data itself. Usually, a new user will attempt to load the contents of the ledger, modify the data structures, and print to update their file, not realizing that the printer includes all the interpolations, booking data, and modifications from plugins, so this cannot work. However, since we're rewriting the parser and ensuring a clean separation between intermediate ASI-like data and processed and finalized directives, we can implement a special printer for the AST intermediate data, so that users could run just the parser, modify the intermediate directives, and print them back out, perhaps losing just some of the formatting and whitespace. This formatting loss can be leveraged to reimplement bean-format more naturally: the output of that printer should always be formatted neatly. This would avoid users having to write ad-hoc parsers on their input file, sed-like conversions, and so on. They could do it properly by modifying the data structure instead. What's more, in order for this to work accurately, we'd have to delay processing of the arithmetic operations post-parsing, so that we can render them back out. This offers another advantage: if we process the calculations after parsing, we can afford to provide an option to let the user specify the precision configuration to use for mpdecimal. I really like that idea, because it avoids hard-coding calculation precision and better defines the outcome of these options, potentially opening the door to a more rational way to remove extra digits that often get rendered out. Finally, if a nice library function can be made to process transactions in-place and output them back out, preserving all comments around them, this can become another way\u2014perhaps the preferential way\u2014for us to clean payees and somesuch. At the moment, the solution is to write a plugin that will clean up the data, but the input file remains a bit of a mess. Making it easy to automatically clean up the input file is an appealing alternative and potentially will add an important new dimension to the Beancount workflow. I want to make all changes necessary to make this possible and well supported (I'm imagining my ledger file all cleaned up right now and it's appealing). I think it's not very much work, it involves: Storing begin/end line information on everything. Adding AST constructs for representing arithmetic calculations. Adding comments parsing to the renderer. Implementing a new renderer that can reproduce the AST, including handling missing data. Implementing a library to make modification of a file in-place as easy as writing plugins, while preserving all non-directive data in the file as is.","title":"Make Rewriting the Input First Class"},{"location":"beancount_v3.html#contributions","text":"For most of the development of Beancount, I've been pretty reluctant to accept contributions. It has been a closely held pet project of mine since it has so much impact on my personal financial arrangements and I dread unplanned breakage. The main reservations I've had over contributions are two-fold: Not enough testing. Proposed changes that did not include enough testing, or none at all, sometimes even the kind testing that would prevent basic breakage. When I'd accept some proposals and commit to writing the tests myself it could sometimes take me down the rabbit hole for hours (if not days). This wasn't practical. Cascading design impact. Some of the proposals did not take into account broader design considerations that would affect other parts of the code, which I may not have communicated or documented well. I've had to reject some ideas in the interest of keeping things coherent and \"tight.\" Part of this is my fault: putting a high bar on contributions hasn't allowed potential contributors to acquire enough context and familiarity with the codebase to make changes compatible with its design. Allowing more contributions. Starting in Vnext I'd like to restructure the project so that more people are able to get involved directly, and to closely work 1-on-1 with some contributors on particular features. Clearly Beancount benefits from direct input from more people. The recent move to GitHub only compounds the urgency for making that easier. To this effect, I'd like to implement a few strategies: Break down the code in parts. This is a natural evolution that has been seen in many other projects; I'm no Linus nor Guido, but like their respective projects, as they grew in popularity, the original authors narrowed their focus on the core part and let other people expand on adjacent but also important functionality. I'd like to focus more of my time only on core functionality that will impact things like support for settlement dates, currency accounts, split transactions, trade reporting, etc. Letting other people deal with adding or updating price sources, making improvements to the ingestion framework, and making beautiful renderings and presentations of the data would be ideal. In time, I may eventually break down these libraries to separate repositories with looser contribution guidelines and/or add ACLs for others to push directly to those repos. Acquire \"lieutenants.\" I need to leave more space for trusted and frequent contributors to chip in more liberally. For example, Martin Michlmayr now has direct edit access to most documents and has been making numerous helpful contributions and updates to the docs. Kirill Goncharov's conversion of the documentation out of Google Docs is simply beautiful. RedStreet and many others are regularly pitching in answers on the mailing-list. Stefano Zacchiroli and Martin have built a standalone conversion tool from Ledger. Daniele Nicolodi is proposing some low-level changes to the scanner and parser. And of course, Dominik Aumayr and Jakob Schnitzer continue developing the Fava project adjacent to Beancount. There are many more people, there is a slowly-but-surely growing list of familiar recurring names. The question in my mind is: Is there a way to communicate with regular faces so that we're aligned in terms of design and can coordinate our efforts in the same direction? Does the newly acquired familiarity with video-conference meetings (thanks to the coronavirus crisis) afford us a new channel for coordination that wasn't there before? The Python community has thrived in no small part due to the annual face-to-face interactions between its participants and a number of core developers being in the same place for some time. Can we achieve the same thing online, as we are a smaller community? If so, better coordination may make it easier to accept proposed changes. I wonder if I should propose a monthly \"team\" meeting. 20% projects. I should provide a list of \"20% projects\" that are well aligned with the direction of the project for others to take up if they want to, and add profuse guidance and detail of downstream side-effects from those proposed features. The idea is to make it possible for newcomers to contribute changes that are likely to fit well and be easily integrated and accepted on the codebase.: Proposals. Beancount's equivalents of Python's \" PEPs \" are essentially the Google Docs proposal documents I started from threads where many others comment and add suggestions. A central list of those should be shared to a folder, identified as such, and allow others to write similar proposals. Maybe a little more structure and discipline around those would be useful.","title":"Contributions"},{"location":"beancount_v3.html#restructuring-the-code","text":"At the very coarse level, the code restructuring for Vnext looks like this: C++ core, parser, and built-in plugins. The Beancount core, parser, booking algorithm and plugins get rewritten in simple C++, outputting its parsed and booked contents as a stream of protobuf objects. Query engine. Beancount query/SQL gets forked to a separate project operating on arbitrary data schemas, with a much broader scope than Beancount. See section below. The rest. Most of the rest gets cut up into separate projects or at least, at first, in a distinct place within the repository (until the core is rewritten). Note that because the core outputs the stream of directives as proto objects, any language supported by protobufs should be able to read those. This extends the reach of Beancount. Here's a simplified diagram showing how this might look: Here is a detailed breakdown of the various parts of the codebase today and what I think will happen to them: Core. This is the part of Beancount's code which will get rewritten in C++ and output a sequence of messages to a stream of directives. I'll continue keeping a tight focus on that part with a conservative eye toward stability, but in Vnext will be adding desired new capabilities that have been lacking so far as described in the next section of this document. The core will include the following packages: beancount/core beancount/ops beancount/parser beancount/utils beancount/loader.py beancount/plugins (some, see below) beancount/utils (most) Query. The query language will be factored out into a completely separate repo with a broader application domain (and hooks for customizing for Beancount). I suspect that over time that project will acquire a much broader range of contributors, many of which will not even be Beancount users. This includes the code from these packages: beancount/query beancount/tools Prices. This is a simple library and tool that helps users fetch prices from external sources. This should definitely move to another repo and I'd welcome a new owner building a competing solution. People are sending me patches for new price sources and I have too little time to maintain them over time, as the upstream sources change or even disappear. This requires very little from Beancount itself (in theory you could just print() the directives for output, without even loading library code) but I think the Beancount core should include and functions to enumerate a list of required date/instrument pairs at a particular date from a given ledger (and I'm happy to support that). Note that the internal price database core will remain in the core, because it's needed there. The affected packages are: beancount/prices Improvements should be made to this library after it moves out of the Beancount repository: we should isolate the Beancount code to just a few modules, and turn the scope of this project to something larger than Beancount: it's three things, really: a) an up-to-date Python library of price fetchers with unit tests and maintained by the community (i.e., when sources break, we update the library) and a common API interface (needs to be improved from what's there TBH, the API should support fetching time series in a single call); b) an accompanying command-line tool (currency \"bean-price\") for fetching those prices from the command-line. This requires the specification of \"a price in a particular currency from a particular source\" as a string. I'd like to improve that spec to make the USD: prefix optional, and maybe eliminate the chain of prices in the spec, which hasn't found much use in practice and move that upstream. c) Make the interfaces to fetch ledger-related information (e.g., list of missing/required prices and lists of instruments) onto modules: beancount v2, beancount Vnext, ledger, hledger, and rendering output formats to any of these. In other words, this library should be able to fetch prices even if Beancount isn't installed. To turn this project into something that can run independent of beancount. Ingest. The importers library will probably move to another repo and eventually could even find another owner. I think the most interesting part of it has been the establishment of clear phases: the identify, extract and file tasks, and a regression testing framework which works on real input files checking against expected converted outputs, which has worked well to minimize the pain of upgrading importers when they break (as they do break regularly, is a SNAFU). In the past I've had to pull some tricks to make command-line tools provided by the project support an input configuration as Python code but also possible to integrate in a script; I will remove the generic programs and users will be required to turn their configuration itself into a script that will just provide subcommands when run; the change will be very easy for existing users: it will require only a single line-of-code at the bottom of their existing files. The Bazel build may add some minor difficulties in loading a Python extension module built from within a Bazel workspace from an otherwise machine-wide Python installation, but I'm confident we'll figure it out. I'd also be happy for someone else to eventually take ownership of this framework, as long as the basic functionality and API remains stable. The example csv and ofx importers should be removed from it and live in their own repos, perhaps: ofx. the OFX importer should be replaced by something using ofxtools (the one I built is pretty bad), and the CSV importer really needs a thorough rewrite with lots of unit testing for the large list of options it now supports (these tests are sorely missing). csv. Furthermore, I think the CSV importer could be enhanced to be smarter and more flexible, to automatically detect from the column headers and inferred data types in the files which column should convert into which field. I'm not going to do that (I don't have time). Someone with the urge to make the ultimate automatic CSV parser ought to create a separate repository for that. The affected packages are: beancount/ingest : could eventually move to another repo. beancount/ingest/importers: someone could revive a repository of importer implementations, like what LedgerHub once aimed to become, and swallow those codes. See this document for details on what's to happen with the ingestion code. Custom reports and bean-web should be removed: the underlying bottle library seems unmaintained at this point, Fava subsumes bean-web, and I never liked the custom reports code anyway (they're a pain to modify). I never use them myself anymore (other than through bean-web). I really think it's possible to replace those with filters on top enhanced SQL query results. The conversion to Ledger and HLedger from Beancount now seems largely useless, I'm not sure anyone's using those. I'll probably move these to another repo, where they would eventually rot, or if someone cares, adopt them and maintain or evolve them. beancount/web : will be deleted or moved to another repo. beancount/reports : will be deleted or moved to another repo. Note that this includes deprecating beancount/scripts/bake , which depends heavily on bean-web. I have no substitute for bean-bake, but I think I'd like to eventually build something better, a tool that would directly render a user-provided list of specific SQL queries to PDF files and collate them, something you can print. Jupyter notebook support. A replacement for the lightweight interface bean-web used to provide could be Jupyter Notebook integration of the query engine, so that users can run SQL queries from cells and have them rendered as tables, or perhaps a super light web application which only supports rendering general SQL queries to tables. Built-in Plugins. Beancount provides a list of internal plugins under beancount/plugins . It's not indicated clearly, but there have evolved two groups of plugins in there: stable plugins used by the core, and experimental plugins showcasing ideas, which are often incomplete implementations of something that was proposed from a thread on the mailing-list. The former group will be ported to C++, and the latter group should probably move to another location with much looser acceptance constraints. First, there are \"meta-plugins\" which only include groups of other plugins: Only one of those should remain, and maybe be enabled by default (making Beancount pedantic by default): auto pedantic The following plugins should remain in the core and be ported to C++: auto_accounts check_closing check_commodity close_tree commodity_attr check_average_cost coherent_cost currency_accounts implicit_prices leafonly noduplicates nounused onecommodity sellgains unique_prices The following are the experimental implementations of ideas that should move to a dedicated repo where other people can chip in other plugin implementations: book_conversions divert_expenses exclude_tag fill_account fix_payees forecast ira_contribs mark_unverified merge_meta split_expenses tag_pending unrealized Because it's a really common occurrence, the new transfer_lots plugin should be part of the built-in ones. Projects. The beancount/projects directory contains the export script and a project to produce data for a will. The will script will be moved outside the core of Beancount, I'm not sure anyone's using that. Maybe the new external plugins repo could include that script and other scripts I shared under /experimental. The export script should be grouped together with beancount/scripts/sql and other methods to send / share data outside of a ledger; these could remain in the core (I'm using the export script regularly to sync my aggregates and stock exposure to a Google Sheets doc which reflects intraday changes). Scripts. Some of the scripts are completely unrelated to Beancount, they are companions. The scrape validator. The sheets upload. The treeify tool. These should be moved elsewhere. One of the advantages of having all the code in the same repo is that it makes it possible to synchronize API changes across the entire codebase with a single commit. As such, I may keep some of the codes in the same repo until the new C++ core has stabilized, and properly separate them only when Vnext releases.","title":"Restructuring the Code"},{"location":"beancount_v3.html#universal-lightweight-query-engine-ulque","text":"The SQL query engine for Beancount was initially a prototype but has grown to become the main way to get data out of it. I've been pretty liberal about adding functionality to it when needed and it's time to clean this up and consider a more polished solution. In Vnext, the query/SQL code gets eventually forked to a separate project (and repo) operating on arbitrary data schemas (via protobufs as a common description for various sources of data) and has support for Beancount integration. Imagine if you could automatically infer a schema from an arbitrary CSV file, and run operations on it, either as a Python library function or as a standalone tool. Furthermore, this tool will support sources and/or sinks to/from Google Sheets, XLS spreadsheets, containers of binary streams of serialized protos, tables from HTML web pages, PDF files, directories of files, and many more. This is going to be a data analysis tool with a scope closer to that of the Pandas library rather than an accounting-focused project, but also a universal converter tool, that will include the functionality of the upload-to-sheets script (which will get removed). One of the lessons from the SQL query engine in Beancount is that with just a little bit of post-processing (such as treeify ), we can do most of the operations in Beancount (journals, balance sheet & income statements) as queries with filters and aggregations. The tool will be made extensible in the ways required to add some of the idiosyncrasies required by Beancount, which are: Native support for a Decimal type . The addition of custom types for aggregators with the semantics of beancount.core.Inventory/Position/Amount . The ability to automatically generate a dynamic column rendering a line-by-line aggregation of another column (or set thereof), that is, a \"balance\" column . The ability to render a \" bottom line \" of aggregates at the end of the results table. Functions for splitting of aggregated columns , for amounts and inventories into multiple columns (e..g, \"123.00 USD\" becomes two columns: (123.00, \"USD\") to be processable in spreadsheets, and also for splitting debits and credits to their own columns. In particular, printing multiple lots accumulated in an account should be made natural from the SQL query, replacing the \"flatten\" feature by a more standard splitting off an array type. Moreover, broadening the focus with a new project definition will make a change to testing it thoroughly (the current one is still in a bit of a prototype stage and does not have nearly the amount of required tests), and also include data type validation (no more exceptions at runtime), by implementing a typed SQL translator. I'll document this elsewhere. This is a much bigger project, but I suspect with the broader scope, it will be easier to test and take on a life of its own. I'm preparing a design doc on this.","title":"Universal Lightweight Query Engine (ulque)"},{"location":"beancount_v3.html#api-rework","text":"I write a lot of custom scripts, and there are a number of things that bother me about today's Beancount API, which I want to radically improve: Consolidate symbols under \"bn\". The internal API calls for importing the symbols from each package separately, but now that I'll have split off the ingestion and reporting code, all of the public API, or at least the majority of the commonly used objects in the core should be available from a single package, a bit like numpy: import beancount as bn \u2026 bn.Inventory(...) bn.Amount(...) bn.Transaction(...) # etc. I'd like for \"bn\" to become the de-facto two-letter import on top of which we write all the scripts. Default values in constructors. The namedtuple containers are mighty fine, but their constructors never had optional arguments, and it's always a bit of a dance to create those containers with a ton of \"None\" options. I never liked it. We'll make this tidy in the next iteration. No API documentation. While there is a substantial amount of documentation around the project, there is no documentation showing people how to use the Python API, e.g. how to accumulate balances, how to create and use a realization tree, how to pull information out of an accumulated inventory object, etc. I think that documenting some of the most common operations will go a long way towards empowering people to make the most out of Beancount. Some of these operations include: Accumulating lots of an inventory and printing them. Converting to market value, and making corresponding account adjustments. \u2026. add more \u2026 Exposed, usable booking. Booking will be a simple loop that can be invoked from Python with an entry and some accumulated state. Moreover, the Inventory object should begin to implement some of the lower-level operations required for booking, such that iterating over a set of postings and doing e.g., average booking, can be done via method calls on an Inventory object. Inventory should take a more prominent place in the API. Data types. Well defined data types should be provided for all objects to make liberal use of the typing module over all new code. Maybe create a module called \"bn.types\" but they should be available directly from \"bn.*\" so that there is a single short-named import. Terminology. I'd like to stop using \"entries\" and consolidate over the name \"directives\" in Vnext. Realization. I've been using a collections.defaultdict(Inventory) and a \"realization\" interchangeably. Both of these are mappings from an account name (or some other key) to an Inventory state object. I'd like to unify both of these constructs into the realization and make it into a commonly used object, with some helper methods.","title":"API Rework"},{"location":"beancount_v3.html#parser-rewrite","text":"Since we will now depend on C++, the parser will get to be rewritten. Worry not: the input syntax will remain the same or at least compatible with the existing v2 parser. What will change is: Unicode UTF-8 support. The lexer will get rewritten with RE/flex instead of GNU flex. This scanner generator supports Unicode natively and any of the input tokens will support UTF-8 syntax. This should include account names, an oft-requested feature. Flags. The current scanner limits our ability to support any flag and supports only a small list of them. I think the list has proved sufficient for use, but since I'll be putting some work into a new scanner I'm hoping to clean up that story and support a broader, better defined subset of single-letter flags for transactions. Time. The parser will parse and provide a time field, in addition to the date. The time may be used as an extra key in sorting directives. The details for this are yet to be determined, but this is requested often enough at the very minimum the parser will output it as metadata, and at best, it may become a first-class feature. Caching. The pickle cache will be removed. Until very recently , there weren't great options for disabling it (env vars) and I'd rather remove the only two environment variables that Beancount honors as a side-effect. Since the C++ code should be fast enough, hopefully a cache will not be needed. Tags & links. In practice, those two features occupy a very similar role as that of metadata (used to filter transactions). I'm contemplating unseating the special place taken by tags and links in the favor of turning those into metadata; the input syntax would not be removed, but instead the values would be merged into the metadata fields. I'm not 100% sure yet about doing this and open for discussion. Furthermore, the parser should be made to accept #tag and ^link where metadata is declared today, which would be convenient syntax. Finally, users have expressed a desire for tags on postings. We should contemplate that. Plugins configuration as protos. The options for the various plugins have been loosely defined as eval'ed Python code. This is pretty loose and doesn't provide a great opportunity for plugins to do validation nor document their expected inputs. I'd like to formalize plugin configuration syntax a bit, by supporting text-formatted protos in the input syntax (for a message type which would be provided by the plugins themselves). Parser in C++. The parser will be rewritten in C++. In the process of writing Vnext, I'll try to maintain a single grammar for both for as long as possible by calling out to a C++ driver interface, which will have two distinct implementations: one for the V2 version calling into Python, and one for the Vnext parser generating protos. In the process I may be porting the lexer and grammar Python implementation to C, as discussed in this ticket . Better includes. Current includes fail to recognize options that aren't in the top-level file. This caused many surprises in the past and should be fixed. At the minimum, an error should be raised.","title":"Parser Rewrite"},{"location":"beancount_v3.html#code-quality-improvements","text":"Rename \"augmentation\" and \"reduction\" to \"opening\" and \"closing\" everywhere. This is just more common terminology and will be more familiar and understandable to people outside of our context. Type annotations. The use of mypy or pytype with type annotations in Python 3 is by now a very common sight, and works quite well. As part of Vnext, all of the core libraries will be modified to include type annotations and the build should be running pytype automatically. I'll need to add this to our Bazel rules (Google doesn't currently provide external support for this). While doing this, I may relax some of the Args/Returns documentation convention, because in many cases (but not all) the type annotations are sufficient to get a good interpretation of a function's API. PyLint in build. Similarly, the linter should be run as an integral part of the build. I'd like to find a way to selectively and explicitly have to disable it during development, but otherwise be set up such that lint errors would be equivalent to build failures. Flexible constructors for Python API. The types generated by collections.namedtuple() or typing.NamedTuple don't offer flexible constructors with named parameters. I think all codes that create transaction objects today would benefit from having constructors with default values, and I'll be providing those to create corresponding proto objects.","title":"Code Quality Improvements"},{"location":"beancount_v3.html#tolerances-precision","text":"The story around how precision and tolerances are dealt with hasn't been great, for two reasons: Explicit tolerance option. I've tried to design the tolerance (used for balancing transactions) to be automatic and automatically inferred from statistics from the numbers in the input. The results aren't great. In Vnext I aim to provide an explicit option for setting the tolerance per currency. Precision. There are various places where numbers get rendered in v2: the reports code, the SQL query, and debugging scripts, and the way precision is set hasn't been used consistently. The precision also needs to be explicitly settable by the user. Rounding. There is another quantity that's used during interpolation: the precision used to round calculated numbers. Moreover, there is a need to distinguish between the precision and tolerances for numbers when used as prices vs. when used as units (see here ). One way is to store the display context per currency PAIR, not per currency itself. The distinction between these quantities hasn't been documented well; I'll keep in mind to clearly annotate those codes in Vnext and add suitable docs for this. Mostly the precision will be a rendering concern and a quantity that will be relevant for the new universal SQL query tool. Some prior design documentation exists here .","title":"Tolerances & Precision"},{"location":"beancount_v3.html#core-improvements","text":"Some desiderata of new features are discussed below. These are all relevant to the core. Note that the changes should not interfere with current usage much, if at all. I expect that v2 users will be largely unaffected and won't have to change their ledger files.","title":"Core Improvements"},{"location":"beancount_v3.html#booking-rules-redesign","text":"Main document One of the current problems with booking is that entering an augmenting leg and a reducing leg have to be different by nature. The augmentation leg has to provide the cost basis via {...} syntax, and the reducing leg has to enter the price in the annotation and not in the cost basis. For example: 2021-02-24 * \"BOT +1 /NQH21:XCME 1/20 FEB 21 (EOM) /QNEG21:XCME 13100 CALL @143.75\" Assets:US:Ameritrade:Futures:Options 1 QNEG21C13100 {2875.00 USD} contract: 143.75 USD ... 2021-02-24 * \"SOLD -1 /NQH21:XCME 1/20 FEB 21 (EOM) /QNEG21:XCME 13100 CALL @149.00\" Assets:US:Ameritrade:Futures:Options -1 QNEG21C13100 {} @ 2980.00 USD contract: 149.00 USD ... Notice how the selling transaction has to be written down differently from the perspective of the user. The thing is, this makes it difficult from the perspective of the importer writer. It also ties the required syntax with the state of the inventory it's applied to, as it assumes something about this inventory. Moreover, this makes it difficult to write an importer that would handle a crossing of the absolute position, like this: 2021-02-19 * \"BOT +1 /NQH21:XCME @13593.00\" Assets:US:Ameritrade:Futures:Contracts 1 NQH21 {271860.00 USD} contract: 13593.00 USD Assets:US:Ameritrade:Futures:Margin -271860.00 USD Expenses:Financial:Commissions 2.25 USD Expenses:Financial:Fees 1.25 USD Assets:US:Ameritrade:Futures:Cash -3.50 USD 2021-02-19 * \"SOLD -2 /NQH21:XCME @13590.75\" Assets:US:Ameritrade:Futures:Contracts -2 NQH21 {271815.00 USD} contract: 13590.75 USD Assets:US:Ameritrade:Futures:Margin 543630.00 USD Income:US:Ameritrade:Futures:PnL 45.00 USD Expenses:Financial:Commissions 4.50 USD Expenses:Financial:Fees 2.50 USD Assets:US:Ameritrade:Futures:Cash -52.00 USD The issue here is that we're crossing the flat line, in other words, we go from long one to short one. There are only two ways to do that properly right now: Disable booking and use the cost only, as per above. This is not great \u2014 booking is terribly useful. Track the position in your importer and separate the reducing and augmenting legs: 2021-02-19 * \"SOLD -2 /NQH21:XCME @13590.75\" Assets:US:Ameritrade:Futures:Contracts -1 NQH21 {} @ 271815.00 USD Assets:US:Ameritrade:Futures:Contracts -1 NQH21 {271815.00 USD} Both solutions aren't great. So I came up with something new: a complete reevaluation of how the syntax is to be interpreted. In fact, it's a simplification. What we can do is the following: use only the price annotation syntax for both augmentation and reduction and currency conversions, with a new booking rule \u2014 Match lots without cost basis in priority. If the lots have no cost basis, the weight of this posting is simply the converted amount, as before. If a match has been made against a lot with cost basis, the weight of this posting is that implied by the matched lots. Make the {...} used solely for disambiguating lots to match, and nothing else. If you have unambiguous matches, or a flexible booking strategy, e.g. FIFO, you'd pretty much never have to use the cost matching reduction. With this, the futures transaction above would simply use the @ price annotation syntax for both transactions. It would Make importers substantially simpler to write Supports futures naturally Be backward compatible with existing inputs for both currency conversions and investments. It is also more generally consistent and friendlier to Ledger users, without sacrificing any of the tighter constraints Beancount provides. I think it's even simpler to think about. Furthermore, this: Assets:US:Ameritrade:Futures:Contracts -1 NQH21 {} @ 271815.00 USD would be interpreted as \"match this lot, but only those with a cost basis attached to them.\" One question that remains is to decide whether an augmentation \u2014 now written down simply with @ price annotation \u2014 would store the cost basis in the inventory or not. I think we could make this determination per-commodity, or per-account. This would impose a new constraint: a commodity (or \"in an account\") would always be stored with cost basis, or not.","title":"Booking Rules Redesign"},{"location":"beancount_v3.html#posting-vs-settlement-dates","text":"When you import a transaction between multiple accounts within a single ledger, e.g. a credit card payment from one's checking account, the dates at which the transaction posts in each account may differ. One side is called the \"transaction date\" or \"posting date\" and the other side the \"settlement date.\" Where the money lives in between is somewhere in limbo (well in practice there is no money at all, just differences in accounting between institutions, things are never reflected instantly). One of the major shortcomings of the current core code is that the ability to insert a single transaction with postings at different dates is missing. Users are recommended to select a single date and fudge the other one. Some prior discussion on this topic exists here . Unfortunately, this method makes it impossible to represent the precise posting history on at least one of the two accounts. A good solution needs to be provided in Vnext, because this is a very common problem and I'd like to provide a system that allows you to precisely mirror your actual account history. The automatic insertion of transfer accounts to hold the commodities can be implemented as a feature, and it should live in the core. One possible idea would be to allow optional posting dates, like this: 2020-01-19 * \"ONLINE PAYMENT - THANK YOU\" \"\" Assets:US:BofA:Checking -2397.72 USD 2020-01-21 Liabilities:US:Amex:BlueCash 2397.72 USD which would result in two transactions behind the scenes, like this: 2020-01-19 * \"ONLINE PAYMENT - THANK YOU\" \"\" Assets:US:BofA:Checking -2397.72 USD Equity:Transfer 2020-01-21 * \"ONLINE PAYMENT - THANK YOU\" \"\" Liabilities:US:Amex:BlueCash 2397.72 USD Equity:Transfer The lack of symmetry here raises the question of whether we should allow a transaction without a date or not: * \"ONLINE PAYMENT - THANK YOU\" \"\" 2020-01-19 Assets:US:BofA:Checking -2397.72 USD 2020-01-21 Liabilities:US:Amex:BlueCash 2397.72 USD I think we can figure this out and the first solution is very doable. Input Split Transactions Some users like to organize their inputs in different files, or in different sections that strictly contain all of an account's transactions in order. This is related in spirit to the posting and settlement dates problem: at the moment the user is required to choose one of the two locations to insert their transaction. This should not be necessary. We should provide a mechanism that would allow users to insert the halves of a transaction into two different locations in their file, and a robust merging mechanism that would ensure that the two related transactions have been matched and merged (so that no unmerged half remains) and otherwise report errors clearly. The two halves could look like this: 2020-01-19 * \"ONLINE PAYMENT - THANK YOU\" \"\" Assets:US:BofA:Checking -2397.72 USD \u2026 2020-01-21 * \"AMEX EPAYMENT ACH PMT; DEBIT\" Liabilities:US:Amex:BlueCash 2397.72 USD The matching could be done via explicit insertion of special links, or by heuristics to match all such related transactions (perhaps declaring valid account pairs, thresholding on date differences and exactly matching amounts). When impossible to match, an error should be raised. Those merged transactions should be checked for balancing. Note how each of the transactions has a differing date; this would integrate with the transfer account solution proposed in the previous section. I haven't designed something yet, but this should be easy to implement and should be provided as a core feature, since it's so closely related to the input syntax.","title":"Posting vs. Settlement Dates"},{"location":"beancount_v3.html#currency-accounts-instead-of-a-single-conversion","text":"The current implementation of multiple currency transactions relies on a special \"conversion transaction\" that is automatically inserted at reporting time (when closing the year) to account for the sum total of imbalances between currencies. The goal of this transaction is to ensure that if you just sum up all the postings in the book, the result is purely an empty inventory (and not some residual amount of profit or loss incurred during currency exchange across different rates \u2014 note that we're talking only of the @price syntax, not investments). This is a bit of a kludge (the transaction itself does not balance, it converts to zero amounts of a fictional currency in order to keep itself quietly passing the balance test). What's more, its actual value is dependent on a subset of filtered transactions being summed up so it's a reporting-level construct, see here . There exists a method for dealing with multiple currencies without compromising on the hermeticity of individual transactions, described online, here . Using that method, you can filter any subset of transactions and summing them up will cleanly cancel out all lots. You don't need to insert any extra weight to fix up the balance. Also, you can explicitly book profits against the accrued gains in the currency accounts and zero them out and take advantage of this when you report them (and track them over time). The downside is that any currency conversion would see extra postings being inserted, etc. 2020-06-02 * \"Bought document camera\" Expenses:Work:Conference 59.98 EUR @ USD Liabilities:CreditCard -87.54 USD Equity:CurrencyAccounts:EUR -59.98 EUR Equity:CurrencyAccounts:USD 87.54 USD The problem is that it's a pain to use this method manually, it requires too much extra input. It's possible to have Beancount do that for us behind the scenes, completely automatically. I coded a proof-of-concept implementation here , but it's incomplete . In Vnext: The insertion of the kludgey conversions transactions should be removed. The currency accounts should become the norm. The fact that the two streams of directives will be very clearly separated should help, by distinguishing even more clearly between the parsing representation and the fully booked one, which will show these extra legs on transactions The prototype should be completed and issues fixed completely (not that much work involved).","title":"Currency Accounts instead of a Single Conversion"},{"location":"beancount_v3.html#strict-payees","text":"I'm not sure if this makes sense yet, but I'd like to clean up the mess that payee strings are today. Payees are free-form, and if the user does not take care to clean them up\u2014and I'm one of those who doesn't\u2014the memos from imported sources are messy. It could be interesting to create a new directive to declare payee names ahead of time and an optional model that would require payees to be found in the list of declared payee names. Payees would have to have open and close dates, dates which would define the valid duration of the relationship with that payee (thereby adding more error verification capability).","title":"Strict Payees"},{"location":"beancount_v3.html#price-inference-from-database","text":"Interpolation from price database. One of the oft-requested features is the ability to automatically interpolate prices from the internal price database history. I think that this should be doable unambiguously and deterministically and added to the interpolation algorithm. Price validation. Since a lot of the conversions at price (i.e., using \"@\") are inferred by leaving out one number, we should validate that the effective price is within some tolerance of a pre-existing price point near the date. This would provide yet another level of checking.","title":"Price Inference from Database"},{"location":"beancount_v3.html#quantizing-operators","text":"Another useful addition to the syntax would be operators that automatically quantize their result to a precision that depends on the particular target currency. For example, 1970-01-01 * \"coffee\" Expenses:Food:Net 2.13 / 1.19 EUR Expenses:Food:Taxes 2.13 / 1.19 * 0.19 EUR ; for example to calculate tax Assets:Cash That would become: 1970-01-01 * \"coffee\" Expenses:Food:Net 1.789915966386555 EUR Expenses:Food:Taxes 0.340084033613445 EUR Assets:Cash If instead an operator like this were provided, it would fix the issue: 1970-01-01 * \"coffee\" Expenses:Food:Net 2.13 /. 1.19 EUR Expenses:Food:Taxes (2.13 / 1.19 * 0.19). EUR Assets:Cash Or somesuch. Or maybe we'll want to add an option such that every evaluation of an arithmetic expression is automatically quantized as such.","title":"Quantizing Operators"},{"location":"beancount_v3.html#constraints-system-budgeting","text":"Beancount does not support budgeting constraints explicitly, but I think it would be possible to extend the balance assertion semantics to cover this. The current balance assertions check (a) a single commodity, and (b) that the amount is precisely equal to an expected one. Balance assertions should be extended to support inequalities, e.g., 2020-06-02 balance Liabilities:CreditCard > 1000.00 USD and perhaps we could check for the total inventory like this 2020-06-02 balanceall Assets:Cash 200.00 USD, 300.00 CAD I'd be curious to hear what would work best from users who do budgeting and design a minimalistic expression language to support that use case (though I'd try to keep it as simple as possible to avoid feature creep). Also, if the syntax is getting changed, adding a syntax that allows checking multiple currencies at once, and possibly a complete assertion that checks there aren't other commodities in the account could also make sense.","title":"Constraints System & Budgeting"},{"location":"beancount_v3.html#average-cost-booking","text":"Average cost booking has been discussed and a good solution sketched out a very long time ago. Vnext should sport that method natively; a lot of users want to have this feature for dealing with their tax-deferred accounts. It takes a bit of work to handle the precision of the various automated conversions right. The way it would work is by automatically merging all related lots of the same commodity on a reduction, and optionally on an augmentation. Some constraints may be required (e.g. only a single commodity in that account).","title":"Average Cost Booking"},{"location":"beancount_v3.html#trade-matching-reporting","text":"A few core tasks related to P/L and trading still need to be implemented. Trade list. A problem that I've really been wanting to solve for a very long time but never found the time for is to save crumbs from the booking process so that a correct list of trade pairs could be easily extracted from the list of directives. I wrote some notes here and here a long time ago. Essentially, for each booked reduction, insert a reference to the corresponding augmenting posting. I've prototyped this as metadata but it should be made something more official. A single linear scan can pick up these references, build a mapping and recover the (buy, sell) pairs to produce a table of trades. There's a precedent I once wrote in a plugin . Needless to say, producing a list of trades is a pretty basic function that Beancount does not provide out of the box today; it really should. Right now users write their own scripts. This needs to be supported out-of-the-box. Harmonize balance and gains validation. Checking that transactions balance and that income from gains balance with a transaction's prices (the sellgains plugin) are done in completely separate places. Those two codes occupy similar roles, and should be implemented next to each other. Commissions in P/L. Properly counting profits & losses by taking off the fraction of buying commission of an original lot and the selling commission into account is not possible at the moment. I think it could be done with a plugin that moves some of the (computed) income leg into a separate negative income account to do this properly for reporting purposes.","title":"Trade Matching & Reporting"},{"location":"beancount_v3.html#self-reductions","text":"Currently the application of reductions operates on the inventory preceding the transaction. This prevents the common case of self-reductions, and both I and some users have come across this problem before, e.g. this recent thread ( ticket ). This comes off as unintuitive to some users and ought to have a better solution than requiring splitting of transactions. Since we're rewriting the booking code entirely in Vnext, contemplate a new definition that would provide a well-defined behavior in this case. I remember from prior experiments attempting to implement this that it wasn't a trivial thing to define. Revisit. This would be a nice improvement.","title":"Self-Reductions"},{"location":"beancount_v3.html#stock-splits","text":"Some discussion and perhaps a strategy for handling stock splits should be devised in Vnext. Right now, Beancount ignores the issue. At the minimum this could be just adding the information to the price database. See this document for more details.","title":"Stock Splits"},{"location":"beancount_v3.html#multipliers","text":"Options have a standard contract size of 100. Futures have a contract size that depends on the particular instrument (e.g., /NQ with a multiplier of 20). I've been handling this for options by multiplying the units by 100, and for futures by multiplying the contract size by the per-contract multipliers (ditto for options on futures). I do this in the importers. For options, it works and it isn't too bad (e.g. positions of -300 instead of -3), but for futures, it's ugly. The result is not representative of the actual transaction. I'd like to add a per-currency multiplier, as well as a global dictionary of regexps-to-multiplier to apply, and for this to be applied everywhere consistently. One challenge is that everywhere there's a cost or price calculation, this has to be applied. In the current version, those are just multiplications so in many parts of the codebase these haven't been wrapped up in a function that could easily be modified. This needs to happen in a big rewrite \u2014 this is the opportunity to do this. Example here.","title":"Multipliers"},{"location":"beancount_v3.html#returns-calculations","text":"If you look at investment brokers out there, no one calculates returns correctly. Brokers provide one of two features: No cash transfers. Total value of account today vs. total value of account at some point in the past (i.e., account inception or beginning of the year). This isn't very useful because they never account for the addition or removal of cash to the account. For example, say you open an account with $100,000 and invest, and mid-year add another $20,000, say the original investments are now worth $95,000, the report would show a gain of $15,000, whereas you really incurred a loss. Better brokers like Vanguard will show a plot that includes two overlaid bars, one with cash added and profit overlaid, like this: Lack of interest or dividends. Other brokers will report P/L over time from the investments, but they fail to account for actual interest or dividends received (they only look at the price of the underlying) so that's not useful for bonds or for stocks with significant dividends, or when grouping them, they fail to account for the addition of new positions over time. Counterfactual performance. Finally, all of them fail to compare your actual annualized performance with that of a benchmark portfolio with equivalent cash infusions. For example, instead of your actual investments made, compare with the performance you would have obtained if you had invested in some standardized portfolio of investments over that particular time period, given the actual historical prices of those instruments. Ideally one should be able to define any alternative portfolio to compare against using their particular cash transfers. More fancy analyses aren't even contemplated, e.g., what would have been the impact of changing my rebalancing strategy (or actually implementing a more strict one)? There are well known methods for both time-based and value-based returns reporting. The right thing to do is to extract a time-series of cash flows and compute the annualized or IRR returns, or value-weights. I started this work at some point and ran against some difficulties and eventually removed it. The results remain here. I'd really love to build this, and eventually perhaps this could grow into its own project, with associated support functions in the Beancount core. This will possibly be a project of its own, but this requires similar support for enumerating instruments and price sources as that which is needed for fetching prices, as well as functions for isolating cash flows for specific subsets of accounts; these should probably live in the core. UPDATE September 2020: This has mostly been implemented. See this document for details.","title":"Returns Calculations"},{"location":"beancount_v3.html#unsigned-debits-and-credits","text":"A useful idea that's nearly trivial to implement is to allow users to input all positive unit numbers and to automatically flip the signs on input, and to output them all as positive numbers as well splitting them up between Debit and Credit columns. This would make Beancount's rendering a lot easier to grok for people with an existing background in accounting. This feature will introduce no complexity, easy to add for free. See TODO here .","title":"Unsigned Debits and Credits"},{"location":"beancount_v3.html#holdings","text":"One of the libraries I built at some point was this notion of a \"holding\", here . At the time I wasn't sure if that data would contain much more than what's in a posting, but as it turns out, representing holdings as just the posting is good enough, all you need is the market value, and compute the total values from the units is trivial. In fact, I've haven't been using that code for years, I've been using the export script instead, which writes out a table that gets uploaded to a Google Sheets doc. This proves to me aggregating the positions in an Inventory is plenty sufficient in practice, along with a mapping of latest market prices. I'm going to delete that code. It's only been used in the reports code anyway, which will be removed anyway, and in the experimental \" unrealized gains \" plug, which was only a proof-of-concept that convinced me booking such gains as transactions is not a good idea and which will get removed and live in a separate experimental repository anyway.","title":"Holdings"},{"location":"beancount_v3.html#tooling-for-debugging","text":"Context should recover erroneous transactions. One of the major annoyances of error recovery is that if a transaction involves some types of errors, the postings aren't produced in the stream of directives. This problem is related to the lack of clarity between the merely parsed data structure and the fully resolved one. In particular, this means that the \"bean-doctor context\" debugging command often cannot provide useful context around a failing transaction. This really needs to be fixed, to improve debuggability. Document debugging tools. In general, I should write a better exposition of how to use the various transaction debugging tools; a lot of the questions on the mailing-list would disappear if users knew better how to leverage those. Interactive context in Emacs. If the performance allows it, we could build an Emacs mode which renders the context around a partially written transaction, including inventories of the affected accounts before and after the transaction, as well as interpolated values, to a different buffer updated interactively. This would make it much more fun to input data and provide immediate feedback about the newly inserted transaction.","title":"Tooling for Debugging"},{"location":"beancount_v3.html#documentation-improvements","text":"Remove dependency on furius.ca. The current Google Docs based documentation links to other documents via a global redirect (the definition is found here ). While it does not happen often that my web server goes down (perhaps a few times per year), when it does it takes a few days to rectify the situation. That server is hosted graciously in the company of some friends of mine. Kirill has proved that it would be possible to replace all the links to redirects on github, that would look like this: beancount.github.io/ instead of furius.ca/beancount/doc/ . In order to do this, I'll need to run a script using the Docs API on all the Google Docs to change them automatically.","title":"Documentation Improvements"},{"location":"beancount_v3.html#conclusion","text":"There are other items in the TODO file . These are just the main, big issues that I think matter the most and I'd like to address them in a Vnext rewrite. Development branches will look like this: v2 : Current master will be branched to \"v2\", which will track the stable current version. That branch will build with both the current setup.py system and Bazel. Fixes will be implemented on that branch where possible, and merged to Vnext. master : Current master will become Vnext. Only the Bazel build will be supported on that branch. Any comments appreciated.","title":"Conclusion"},{"location":"beancount_v3.html#appendix","text":"More core ideas for Vnext that came about during discussions after the fact.","title":"Appendix"},{"location":"beancount_v3.html#customizable-booking","text":"For transfer lots with cost basis\u2026 an idea would be to create a new kind of hook, one that is registered from a plugin, e.g. a callback of yours invoked by the booking code itself, and whose results applied to a transaction are immediately reflected on the state of the affected inventories. Maybe this is the right place to provide custom algorithms so that their impact is affecting the subsequent inventories correctly and immediately. Now, imagine generalizing this further to provide and implement all of the current booking mechanisms that are currently built in the core. Call this \"customizable booking.\" ( thread ).","title":"Customizable Booking"},{"location":"beancount_v3.html#ugly-little-things","text":"print_entry() uses buffering that makes it impossible to use regular print() interspersed with the regular stdout without providing file= option. Fix this, make this regular instead, that's just annoying, just print to regular stdout. The default format for __str__ for inventories puts () around the rendering. When there's a single position, that looks like a negative number. That's dumb. Use {} instead, or something else. Add a flag to bean-check to make it run --auto plugins by default. This is great for imported files, which may not have a complete ledger to feed in.","title":"Ugly Little Things"},{"location":"beancount_v3.html#incremental-booking-beancount-server-emacs-companion","text":"In order to make recomputation fast, the idea of creating a standalone \"Beancount server\" starts to make sense. The expensive part of the Beancout calculations on a large file is the booking and interpolation. The key to making things fast is thus to keep all the original unprocessed transactions in memory along with the booked and interpolated ones, and on a change, reparse the modified files and scan all the transactions, updating only the ones whose accounts have been affected. This could be problematic in theory: some plugins may rely on non-local effects in a way that affects what they output. I believe in practice it would work 99% of the time. But I think it may be worth a prototype. On the other hand, Vnext may turn out to be fast enough recomputing everything from scratch every single time (my own file went from 4s -> 0.3ms for the parsing stage of the largest file), so maybe this is irrelevant overall. Such a server would be a perfect companion to a running Emacs. We could build an Emacs mode which communicates with the server.","title":"Incremental Booking/ Beancount Server / Emacs Companion"},{"location":"beancount_v3.html#tags-links-merge-with-metadata","text":"TODO(blais): Add colon syntax","title":"Tags & Links Merge with MetaData"},{"location":"beancount_v3_dependencies.html","text":"Beancount C++ version: Dependencies \uf0c1 Martin Blais , June 2020 Beancount is going to get rewritten in C++, here is the set of dependencies I've tested and that I'm comfortable maintaining in the long run: Base environment \uf0c1 Bazel build ( https://github.com/bazelbuild/bazel ): The Google build system is the most stable approach to build that I know of, much better than SCons and certainly much better than CMake. It allows you to pin down a specific set of dependencies by explicitly referencing other repositories and git repos at specific commit revisions (including non-released ones), and the sometimes annoying constraints it imposes results in hermetically reproducible builds like no other build system can do. This minimizes surprises and hopefully the number of platform-dependent portability issues. It also minimizes the amount of pre-installed packages we assume your system has (e.g. it'll download and compile its own Bison, for example). It runs fast, computes the minimal set of tests and targets to rebuild, and is highly configurable. The downside of choosing Bazel is the same of other Google-issued open source projects: the original version of that product is internal and as a result there are a lot of strange idiosyncrasies to deal with (e.g. //external, the @bazel_tools repo, etc.), many of which are poorly documented outside the company and with a good number of unresolved tickets. However, at this stage I've already managed to create a working build with most of the dependencies described in this section. C++14 with GCC and Clang/LLVM : Both compilers will be supported. Clang provides a much better front-end and stdlib implementation but is a little slower to build. GCC is more commonly present in the wild but the error messages are\u2026 well, we all got used to this I suppose. Note that despite requiring C++14, I will refrain from using exotic features of the language (including classes). There may be questions about Windows support. Abseil-Cpp base library ( https://github.com/abseil/abseil-cpp ): The base library of functions is issued from Google's own gigantic codebase and has been battle-hardened and tested like no other\u2014this is what the Google products run on. This provides a most stable API to work with (it's unlikely to change much given how much code depends on it), one which complements stdc++ well, and whose existing contact surfaces are bound to remain pretty static. It's simpler and more stable than Boost, and doesn't offer a myriad of libraries we're not going to need anyway (plus, I love Titus' approach to C++).. This fills in a lot of the basic string manipulation functions you get for free in Python but crave in C++ (e.g. absl::StrCat). Google Test ( https://github.com/google/googletest ): This is the widely used C++ testing framework I'm already familiar with, which supports matchers and mocking. Data representation \uf0c1 Protocol Buffers ( https://github.com/protocolbuffers/protobuf ): I will maintain a functional style in this C++ rewrite and I need a replacement for Python's nametuples to represent directives. This means creating a lot of simple naked structured data that will need to be created dynamically from within tests (there's a good text-format parser) and also serialized to disk as the boundary between the core and query language will become a file of protobuf messages. Protobuf provides a good hierarchical data structure with repeated fields that is supported in many languages (this opens the door potentially to plugins written in e.g., Go), and it's possible to provide Python bindings for them. It will also become the interface between the Beancount's core and the input to the query language. We will be using proto3 with version >=3.12 in order to have support for optional presence fields (null values). Riegeli (https://github.com/google/riegeli) : An efficient and compressed binary format for storing sequences of protobuf messages to files. I think the Beancount core will output this; it's compact and reads fast. It's also another Googlething that ought to receive more attention than it does and supports both C++ and Python and protobufs. mpdecimal ( https://www.bytereef.org/mpdecimal/ ) : This is the same C-level library used by Python's implementation of Decimal numbers. Using this library will allow to easily operate between the C++ core and Python's runtime. I need to represent decimal numbers in C++ memory with minimal functionality and reasonably small range (BigNum classes are typically more than what we need). We don't need much of the scope for decimal\u2026. basic arithmetic operations + quantizing, mainly. There are other libraries out there: GMP , decNumber . There is some information on this thread: ( https://stackoverflow.com/questions/14096026/c-decimal-data-types . For on-disk representation, I will need a protobuf message definition for those, and I'm thinking of defining a union of string (nice to read but lots of conversions from string to decimal) with some more efficient exponent + mantissa decimal equivalent. Parser \uf0c1 RE/flex lexer ( https://github.com/Genivia/RE-flex ): This modern regexp-based scanner generator supports Unicode natively and is very fast and well documented. It provides a great alternative to the aging GNU flex which made it difficult to support non-ASCII characters outside of string literals (i.e.., for account names). I've had success using it on other projects. Many users want account names in their home language; this will make it easy to provide a UTF-8 parser for the entire file. GNU Bison ( https://git.savannah.gnu.org/git/bison.git ): We will stick with GNU Bison, but instead use the C++ complete modes it supports. I'm hesitating continuing with this parser generator as it's showing its age but it's pretty stable and I can't quite justify the extra work to upgrade to ANTLR. We will have to pull some tricks to support the same grammar for generating C code for v2 and C++ code for the next version; the parser code could be provided with a dispatch table of functions, which would be static C functions in v2, and methods in a C++ version. Some of the generation parameters (% directives) will be different (see here for an example). I nternational Components for Unicode (ICU) ( https://github.com/unicode-org/icu.git ): This is the standard library to depend on for Unicode support. Our C++ will not use std::wstring/std::wchar, but rather regular std::string and function calls to this library where necessary. Python \uf0c1 Python3 (https://www.python.org/) : Not much to say. I will keep using the latest version. Python is a tank of an extension language and no plans to change that. pybind11 ( https://github.com/pybind/pybind11 ): I want to provide a Python API nearly identical to the current one in Beancount, or better (which means simpler). One of the requirements I've had is to make it cheap to pass a list of protobuf objects for the directives to a Python callback, without copying (serializing and deserializing) between C++ and Python\u2014for plugins. I've investigated multiple libraries to interoperate between Python and C++: Cython, CLIF, SWIG, etc. and serialization is a problem (see this partial solution ). The one that seems to have the most momentum at the moment is pybind11, a pure header library which is an evolution from Boost::Python, that offers the most control over the generated API. It also works well with protocol buffer targets built with fast_cpp_protos: only pointers are passed through, so plugins passing in and out the full list of directives should be possible. I also happen to be familiar with Boost::Python having used it 20 years ago, it's really quite similar actually (but does away with the rest of Boost). Type annotations , PyType (or MyPy ?): I've already been compliant to a custom configuration of PyLint for Python but the codebase does not use the increasingly ubiquitous type annotations . In the rewritten subset of the code that will remain, I'd like to have all functions annotated and to replace the sometimes redundant Args/Returns docstrings with a more free-form documentation (the types may be sufficient to avoid the formalism of Args/Returns blocks). I'll have to see how this affects the auto-generated docs . An important addition is that I want to start not only annotating, but running one of the type checkers automatically as part of the build. I'm already familiar with Google's pytype, but perhaps mypy is a good alternative. In any case, the only hurdle for that is to craft Bazel rules that invoke these automatically across the entire codebase, as part of py_library() and py_binary() rules. I'll also attempt to make pylint run in the same way (as part of the build) with a custom flag to disable it during development, instead of having a separate lint target. Subpar ( https://github.com/google/subpar ): It's not clear to me yet how to perform a pip-compatible setup.py for a Bazel build, but surely we can find a way to build wheels for PyPI using the binaries built by Bazel. For packaging a self-contained binary of Python + extensions, the \"subpar\" Bazel rules is supposed to handle that. However, at the moment it does not support C extensions .","title":"Dependencies"},{"location":"beancount_v3_dependencies.html#beancount-c-version-dependencies","text":"Martin Blais , June 2020 Beancount is going to get rewritten in C++, here is the set of dependencies I've tested and that I'm comfortable maintaining in the long run:","title":"Beancount C++ version: Dependencies"},{"location":"beancount_v3_dependencies.html#base-environment","text":"Bazel build ( https://github.com/bazelbuild/bazel ): The Google build system is the most stable approach to build that I know of, much better than SCons and certainly much better than CMake. It allows you to pin down a specific set of dependencies by explicitly referencing other repositories and git repos at specific commit revisions (including non-released ones), and the sometimes annoying constraints it imposes results in hermetically reproducible builds like no other build system can do. This minimizes surprises and hopefully the number of platform-dependent portability issues. It also minimizes the amount of pre-installed packages we assume your system has (e.g. it'll download and compile its own Bison, for example). It runs fast, computes the minimal set of tests and targets to rebuild, and is highly configurable. The downside of choosing Bazel is the same of other Google-issued open source projects: the original version of that product is internal and as a result there are a lot of strange idiosyncrasies to deal with (e.g. //external, the @bazel_tools repo, etc.), many of which are poorly documented outside the company and with a good number of unresolved tickets. However, at this stage I've already managed to create a working build with most of the dependencies described in this section. C++14 with GCC and Clang/LLVM : Both compilers will be supported. Clang provides a much better front-end and stdlib implementation but is a little slower to build. GCC is more commonly present in the wild but the error messages are\u2026 well, we all got used to this I suppose. Note that despite requiring C++14, I will refrain from using exotic features of the language (including classes). There may be questions about Windows support. Abseil-Cpp base library ( https://github.com/abseil/abseil-cpp ): The base library of functions is issued from Google's own gigantic codebase and has been battle-hardened and tested like no other\u2014this is what the Google products run on. This provides a most stable API to work with (it's unlikely to change much given how much code depends on it), one which complements stdc++ well, and whose existing contact surfaces are bound to remain pretty static. It's simpler and more stable than Boost, and doesn't offer a myriad of libraries we're not going to need anyway (plus, I love Titus' approach to C++).. This fills in a lot of the basic string manipulation functions you get for free in Python but crave in C++ (e.g. absl::StrCat). Google Test ( https://github.com/google/googletest ): This is the widely used C++ testing framework I'm already familiar with, which supports matchers and mocking.","title":"Base environment"},{"location":"beancount_v3_dependencies.html#data-representation","text":"Protocol Buffers ( https://github.com/protocolbuffers/protobuf ): I will maintain a functional style in this C++ rewrite and I need a replacement for Python's nametuples to represent directives. This means creating a lot of simple naked structured data that will need to be created dynamically from within tests (there's a good text-format parser) and also serialized to disk as the boundary between the core and query language will become a file of protobuf messages. Protobuf provides a good hierarchical data structure with repeated fields that is supported in many languages (this opens the door potentially to plugins written in e.g., Go), and it's possible to provide Python bindings for them. It will also become the interface between the Beancount's core and the input to the query language. We will be using proto3 with version >=3.12 in order to have support for optional presence fields (null values). Riegeli (https://github.com/google/riegeli) : An efficient and compressed binary format for storing sequences of protobuf messages to files. I think the Beancount core will output this; it's compact and reads fast. It's also another Googlething that ought to receive more attention than it does and supports both C++ and Python and protobufs. mpdecimal ( https://www.bytereef.org/mpdecimal/ ) : This is the same C-level library used by Python's implementation of Decimal numbers. Using this library will allow to easily operate between the C++ core and Python's runtime. I need to represent decimal numbers in C++ memory with minimal functionality and reasonably small range (BigNum classes are typically more than what we need). We don't need much of the scope for decimal\u2026. basic arithmetic operations + quantizing, mainly. There are other libraries out there: GMP , decNumber . There is some information on this thread: ( https://stackoverflow.com/questions/14096026/c-decimal-data-types . For on-disk representation, I will need a protobuf message definition for those, and I'm thinking of defining a union of string (nice to read but lots of conversions from string to decimal) with some more efficient exponent + mantissa decimal equivalent.","title":"Data representation"},{"location":"beancount_v3_dependencies.html#parser","text":"RE/flex lexer ( https://github.com/Genivia/RE-flex ): This modern regexp-based scanner generator supports Unicode natively and is very fast and well documented. It provides a great alternative to the aging GNU flex which made it difficult to support non-ASCII characters outside of string literals (i.e.., for account names). I've had success using it on other projects. Many users want account names in their home language; this will make it easy to provide a UTF-8 parser for the entire file. GNU Bison ( https://git.savannah.gnu.org/git/bison.git ): We will stick with GNU Bison, but instead use the C++ complete modes it supports. I'm hesitating continuing with this parser generator as it's showing its age but it's pretty stable and I can't quite justify the extra work to upgrade to ANTLR. We will have to pull some tricks to support the same grammar for generating C code for v2 and C++ code for the next version; the parser code could be provided with a dispatch table of functions, which would be static C functions in v2, and methods in a C++ version. Some of the generation parameters (% directives) will be different (see here for an example). I nternational Components for Unicode (ICU) ( https://github.com/unicode-org/icu.git ): This is the standard library to depend on for Unicode support. Our C++ will not use std::wstring/std::wchar, but rather regular std::string and function calls to this library where necessary.","title":"Parser"},{"location":"beancount_v3_dependencies.html#python","text":"Python3 (https://www.python.org/) : Not much to say. I will keep using the latest version. Python is a tank of an extension language and no plans to change that. pybind11 ( https://github.com/pybind/pybind11 ): I want to provide a Python API nearly identical to the current one in Beancount, or better (which means simpler). One of the requirements I've had is to make it cheap to pass a list of protobuf objects for the directives to a Python callback, without copying (serializing and deserializing) between C++ and Python\u2014for plugins. I've investigated multiple libraries to interoperate between Python and C++: Cython, CLIF, SWIG, etc. and serialization is a problem (see this partial solution ). The one that seems to have the most momentum at the moment is pybind11, a pure header library which is an evolution from Boost::Python, that offers the most control over the generated API. It also works well with protocol buffer targets built with fast_cpp_protos: only pointers are passed through, so plugins passing in and out the full list of directives should be possible. I also happen to be familiar with Boost::Python having used it 20 years ago, it's really quite similar actually (but does away with the rest of Boost). Type annotations , PyType (or MyPy ?): I've already been compliant to a custom configuration of PyLint for Python but the codebase does not use the increasingly ubiquitous type annotations . In the rewritten subset of the code that will remain, I'd like to have all functions annotated and to replace the sometimes redundant Args/Returns docstrings with a more free-form documentation (the types may be sufficient to avoid the formalism of Args/Returns blocks). I'll have to see how this affects the auto-generated docs . An important addition is that I want to start not only annotating, but running one of the type checkers automatically as part of the build. I'm already familiar with Google's pytype, but perhaps mypy is a good alternative. In any case, the only hurdle for that is to craft Bazel rules that invoke these automatically across the entire codebase, as part of py_library() and py_binary() rules. I'll also attempt to make pylint run in the same way (as part of the build) with a custom flag to disable it during development, instead of having a separate lint target. Subpar ( https://github.com/google/subpar ): It's not clear to me yet how to perform a pip-compatible setup.py for a Bazel build, but surely we can find a way to build wheels for PyPI using the binaries built by Bazel. For packaging a self-contained binary of Python + extensions, the \"subpar\" Bazel rules is supposed to handle that. However, at the moment it does not support C extensions .","title":"Python"},{"location":"beangulp.html","text":"Beangulp \uf0c1 Martin Blais , Jan 2021 Importing data for Beancount has been supported originally by the LedgerHub project along with a library of importers, then reintegrated as a pure framework library (with some examples) in the Beancount repo as beancount.ingest, and now we're splitting up that repository again and will factor out the importing framework to another repo for easier maintenance and evolution. This document lays out some of the changes desired in this new version. New Repo \uf0c1 The new repository will be located at http://github.com/beancount/beangulp Beangulp will target compatibility with the latest beancount release from the v3 branch only. Beancount v3 is expected to evolve rapidly at the beginning, thus, to make the life of early adopters less painful, careful use of version number increments and versioned dependencies should be employed. Ideally, Beangulp should depend on the current minor version of Beancount only, for example, if Beancount 3.0.0 is released, Beangulp should declare install_requires: beancount >3.0, <3.1 See setuptools doc and PEP-440 . Status \uf0c1 As of Jan 2022, most of this proposal is complete and implemented. (Thanks to Daniele Nicolodi for doing most of the work.) Changes \uf0c1 Library Only \uf0c1 The current implementation allows one to use the bean-identify, bean-extract and bean-file tools on a \"config file\" which is evaluated Python, or create a script and call a single endpoint that will implement the subcommands. In order to make this work, a really convoluted trampoline is used to bounce the evaluation to the same code. I'll admit it trumps even me who wrote it whenever I have to go in there and edit that code. It also makes it inconvenient to let users add custom before/after customizations to their import process. The next version will support only (2). The bean-identify, bean-extract, bean-file programs will all be removed. The user will be expected to write their own script. One File \uf0c1 Right now, each importer consists of two files: the implementation file and an associated test file, e.g. soandso_bank.py soandso_bank_test.py The test file is small and usually calls out to a library function to find model files and expected outputs. Since there's hardly any real test code, we'd like to be able to have a single Python file that contains its test invocation. A new endpoint will be added to define the tests in the importer implementation. Self-Running \uf0c1 Moreover, that new function should also be the same one as that which is used to put together the configuration script. In other words, an importer's main() function should be equivalent to an importer's invocation with a single configured importer, one that is configured with the configuration used for testing. This will allow users to just \"run the importer\" on a specific set of files without having to define a configuration, by using the test configuration, like this: soandso_bank.py extract ~/Downloads/transactions.csv Having this option makes it super convenient for people to share the one file and test it out immediately, without having to create a configuration nor a main program. I don\u2019t think there is an easy clean way to implement this other than having something like if __name__ == \u2018__main__\u2019: main = Ingest([SoAndSoBankImporter()]) main() in the importer definition file. This should work right now without changes. Although, it is often the case now that importers require a bit of configuration to work (I am not sure, I don\u2019t use any of the importers distributed with Beancount or widely used). Test S ubcommand & Generate \uf0c1 Since the importers are runnable and define their test cases for pytest to run over, we should also add a subcommand \"test\" to complete \"identify\", \"extract\" and \"file\". That venue is also a great place to replace the --generate option which required ugly injection of the pytestconfig, and instead, implement it ourselves and add a second subcommand: \"genexpect\" to generate the expected file for testing. The interface becomes: soandso_bank.py identify ~/Downloads/ soandso_bank.py extract ~/Downloads/ soandso_bank.py file ~/Downloads/ ~/documents soandso_bank.py test ~/documents/testdocs soandso_bank.py generate ~/Downloads/transactions.csv This way we can remove the pytestconfig dep injection and also simplify the logic of unit tests, which had to handle both the check vs. generate scenarios. This should result in simpler code. One Expected File \uf0c1 Expected outputs from an importer are stored in multiple files with suffixes .extract, .file_name, .file_date, etc. If we had all the output in one file, the \"genexpect\" subcommand could generate everything to stdout. This is convenient. myimporter.py myimporter.beancount Leverage the Beancount syntax to store the expected values for \"file_name()\", \"file_date()\" and other outputs. Store those in Event or Custom directives and have the testing code assert on them. The new contents of a test directory should be simple pairs of (a) original downloaded file and (b) expected output, containing the transactions but also all the other method outputs. Duplicates Identification \uf0c1 This has never really worked properly. I think if this was implemented separately based on each importer \u2014 in other words, letting each importer define how to identify duplicates, e.g., if a unique transaction ID can be assumed having been inserted as a link to disambiguate \u2014 we could do this a lot cleaner. It would be ideal if each importer could specify duplicate id detection, in the importer. It could call on a more general but less reliable method, and that code should live in Beangulp. CSV Utils \uf0c1 I have a lot of really convenient utilities for slicing and dicing CSV files from ugly CSV downloads. CSV downloads often are used to house multiple tables and types of data, and a higher-level of processing is often needed on top of these files. I have code like this spread all over. This deserves a nice library. What's more, I have a nice table processing library under the Baskets project, which hasn't been given the proper quality treatment yet. Clean up my little table library and merge it with all the CSV utils. Put this in beangulp, or even contemplate making this its own project, including a CSV importer. \"CSV world.\" Not sure this matters as much after discovering petl. We could depend on petl. Caching \uf0c1 When running conversion jobs on large files, it would be nice to have a cache and avoid running those more than once. The (small) converted data could be cached and loaded back up in order to avoid running the expensive conversion more than once. One difficulty is that the conversions required to be run depend on the importers configuration, and each importer is unaware of the other ones. All the command-line arguments and at least the head of the file contents should be hashed in. This library could be pretty independent from Beancount. API Changes \uf0c1 \"file_date()\" is not clear; \"get_filing_date()\" would be. The extra argument on extract() is irregular compared to all the other methods. Find a better way? I'm not 100% sure I like my little memoizing file wrapper (\"cache\") with cache. Replace it with a disk-only one. Automatic Insertion \uf0c1 A really convenient and easily built feature that the new code should have is the automatic insertions of the extracted output to an existing ledger, before the point of a special string token in the file. Make this part of the library, as an alternative for storing the output of the importer, e.g. $ ./myimport.py extract | bean-insert ledger.beancount Amex This could also be a flag to \"extract\" $ ./myimport.py extract -f ledger.beancount -F Amex","title":"Beangulp"},{"location":"beangulp.html#beangulp","text":"Martin Blais , Jan 2021 Importing data for Beancount has been supported originally by the LedgerHub project along with a library of importers, then reintegrated as a pure framework library (with some examples) in the Beancount repo as beancount.ingest, and now we're splitting up that repository again and will factor out the importing framework to another repo for easier maintenance and evolution. This document lays out some of the changes desired in this new version.","title":"Beangulp"},{"location":"beangulp.html#new-repo","text":"The new repository will be located at http://github.com/beancount/beangulp Beangulp will target compatibility with the latest beancount release from the v3 branch only. Beancount v3 is expected to evolve rapidly at the beginning, thus, to make the life of early adopters less painful, careful use of version number increments and versioned dependencies should be employed. Ideally, Beangulp should depend on the current minor version of Beancount only, for example, if Beancount 3.0.0 is released, Beangulp should declare install_requires: beancount >3.0, <3.1 See setuptools doc and PEP-440 .","title":"New Repo"},{"location":"beangulp.html#status","text":"As of Jan 2022, most of this proposal is complete and implemented. (Thanks to Daniele Nicolodi for doing most of the work.)","title":"Status"},{"location":"beangulp.html#changes","text":"","title":"Changes"},{"location":"beangulp.html#library-only","text":"The current implementation allows one to use the bean-identify, bean-extract and bean-file tools on a \"config file\" which is evaluated Python, or create a script and call a single endpoint that will implement the subcommands. In order to make this work, a really convoluted trampoline is used to bounce the evaluation to the same code. I'll admit it trumps even me who wrote it whenever I have to go in there and edit that code. It also makes it inconvenient to let users add custom before/after customizations to their import process. The next version will support only (2). The bean-identify, bean-extract, bean-file programs will all be removed. The user will be expected to write their own script.","title":"Library Only"},{"location":"beangulp.html#one-file","text":"Right now, each importer consists of two files: the implementation file and an associated test file, e.g. soandso_bank.py soandso_bank_test.py The test file is small and usually calls out to a library function to find model files and expected outputs. Since there's hardly any real test code, we'd like to be able to have a single Python file that contains its test invocation. A new endpoint will be added to define the tests in the importer implementation.","title":"One File"},{"location":"beangulp.html#self-running","text":"Moreover, that new function should also be the same one as that which is used to put together the configuration script. In other words, an importer's main() function should be equivalent to an importer's invocation with a single configured importer, one that is configured with the configuration used for testing. This will allow users to just \"run the importer\" on a specific set of files without having to define a configuration, by using the test configuration, like this: soandso_bank.py extract ~/Downloads/transactions.csv Having this option makes it super convenient for people to share the one file and test it out immediately, without having to create a configuration nor a main program. I don\u2019t think there is an easy clean way to implement this other than having something like if __name__ == \u2018__main__\u2019: main = Ingest([SoAndSoBankImporter()]) main() in the importer definition file. This should work right now without changes. Although, it is often the case now that importers require a bit of configuration to work (I am not sure, I don\u2019t use any of the importers distributed with Beancount or widely used).","title":"Self-Running"},{"location":"beangulp.html#test-subcommand-generate","text":"Since the importers are runnable and define their test cases for pytest to run over, we should also add a subcommand \"test\" to complete \"identify\", \"extract\" and \"file\". That venue is also a great place to replace the --generate option which required ugly injection of the pytestconfig, and instead, implement it ourselves and add a second subcommand: \"genexpect\" to generate the expected file for testing. The interface becomes: soandso_bank.py identify ~/Downloads/ soandso_bank.py extract ~/Downloads/ soandso_bank.py file ~/Downloads/ ~/documents soandso_bank.py test ~/documents/testdocs soandso_bank.py generate ~/Downloads/transactions.csv This way we can remove the pytestconfig dep injection and also simplify the logic of unit tests, which had to handle both the check vs. generate scenarios. This should result in simpler code.","title":"Test Subcommand & Generate"},{"location":"beangulp.html#one-expected-file","text":"Expected outputs from an importer are stored in multiple files with suffixes .extract, .file_name, .file_date, etc. If we had all the output in one file, the \"genexpect\" subcommand could generate everything to stdout. This is convenient. myimporter.py myimporter.beancount Leverage the Beancount syntax to store the expected values for \"file_name()\", \"file_date()\" and other outputs. Store those in Event or Custom directives and have the testing code assert on them. The new contents of a test directory should be simple pairs of (a) original downloaded file and (b) expected output, containing the transactions but also all the other method outputs.","title":"One Expected File"},{"location":"beangulp.html#duplicates-identification","text":"This has never really worked properly. I think if this was implemented separately based on each importer \u2014 in other words, letting each importer define how to identify duplicates, e.g., if a unique transaction ID can be assumed having been inserted as a link to disambiguate \u2014 we could do this a lot cleaner. It would be ideal if each importer could specify duplicate id detection, in the importer. It could call on a more general but less reliable method, and that code should live in Beangulp.","title":"Duplicates Identification"},{"location":"beangulp.html#csv-utils","text":"I have a lot of really convenient utilities for slicing and dicing CSV files from ugly CSV downloads. CSV downloads often are used to house multiple tables and types of data, and a higher-level of processing is often needed on top of these files. I have code like this spread all over. This deserves a nice library. What's more, I have a nice table processing library under the Baskets project, which hasn't been given the proper quality treatment yet. Clean up my little table library and merge it with all the CSV utils. Put this in beangulp, or even contemplate making this its own project, including a CSV importer. \"CSV world.\" Not sure this matters as much after discovering petl. We could depend on petl.","title":"CSV Utils"},{"location":"beangulp.html#caching","text":"When running conversion jobs on large files, it would be nice to have a cache and avoid running those more than once. The (small) converted data could be cached and loaded back up in order to avoid running the expensive conversion more than once. One difficulty is that the conversions required to be run depend on the importers configuration, and each importer is unaware of the other ones. All the command-line arguments and at least the head of the file contents should be hashed in. This library could be pretty independent from Beancount.","title":"Caching"},{"location":"beangulp.html#api-changes","text":"\"file_date()\" is not clear; \"get_filing_date()\" would be. The extra argument on extract() is irregular compared to all the other methods. Find a better way? I'm not 100% sure I like my little memoizing file wrapper (\"cache\") with cache. Replace it with a disk-only one.","title":"API Changes"},{"location":"beangulp.html#automatic-insertion","text":"A really convenient and easily built feature that the new code should have is the automatic insertions of the extracted output to an existing ledger, before the point of a special string token in the file. Make this part of the library, as an alternative for storing the output of the importer, e.g. $ ./myimport.py extract | bean-insert ledger.beancount Amex This could also be a flag to \"extract\" $ ./myimport.py extract -f ledger.beancount -F Amex","title":"Automatic Insertion"},{"location":"calculating_portolio_returns.html","text":"Calculating Portfolio Returns \uf0c1 Martin Blais , Sept 2020 http://furius.ca/beancount/doc/returns This document describes how to compute portfolio returns from a Beancount ledger. Motivation \uf0c1 You will be surprised to find that discount brokers typically do not provide accurate and complete returns calculations for your investments based on your specific cash flows. They tend to report other measures of performance: Change in value. The simplest they provide is a snapshot of the account value at the beginning and end of the period (or year). The problem with this method is that it does not reflect your infusions or removal of cash as such, nor your changes in positions. For example, if you had an account with $50,000 at the beginning of the year and you've added in $30,000 in August, reporting a difference of $37,000 at the end of December is just not useful (you'd have to mentally discount for new invested cash during the year, and what if you want to break it down and compare the returns from different instruments in the account?). Underlying performance. They might report the growth of the underlying asset in isolation, disregarding your specific positions. It's not very useful to say \"HDV grew by 8.2% over the last year\" if you invested only (or even mostly) in the second half of the year. What I'd like is to know how much my specific investments grew, given my specific changes in positions and timings. In other words, how did I do? No dividends. Another issue is that performance is typically only reporting capital appreciation due to the change in price of the investment. Ideally one would like to break down the performance between capital appreciation and dividends and returns for each of those components, as well as overall performance, so you can compare returns from stocks and bonds to each other. You can sometimes find out that information from the Yield field in the summary, but for uneven distributions, this won't help me find out what my actual returns from dividends were, over any period. Commissions and management expenses. Some brokers charge management expenses on a monthly basis, based on the value of the account. I would like to know what my portfolio return is net of those expenses. I want accurate returns reporting, based on my actual cash flows and changes of positions over time. If I maintain your investment information in a Beancount ledger, in theory it contains all the data I need in order to compute your true returns, based on the specific timings of my own savings (cash infusions) and which positions I held at which time. It's just not in the simplest format required to do it\u2014 Beancount transactions are much more flexible than one might want and a simpler series of cash flows needs to be extracted from it. This document explains how I finally did this from my own Ledger. And how we might generalize this to yours, based on some simple rules. Most of this text is dedicated to the pedestrian details of extracting the right data. The source code can be found here . In addition, a fair and honest comparison to other investments scenarios should be possible, based on those same cash flows. For instance, you should be able to produce data that looks like \"My investments in ZZZ have returned 8.2%, 1.1% of which were from dividends, and if I'd invested in a 60/40 portfolio of broad stocks and bonds it would have returned 7.2% instead.\" In other words, I want to assess my performance relative to a number of common alternatives. (Finally, note that if all you need is a snapshot of your current positions, that's already handled by the export script .) History \uf0c1 In 2014, I made a brief attempt to break down information from my ledger to do this. At the time I got bogged down in details when some of the time-series I was extracting weren't producing what looked like sensible results (some with outliers). I got too detailed too fast. Sometimes it's better to just get the whole job done and come back for the details. I hadn't logged enough debugging information and I didn't have enough confidence in its output to use it. I never actually finished the work at the time, and eventually moved the scripts to experiments and shrugged. \"Later.\" In August 2020, I sat down to do this again, this time with a less ambitious goal of just getting a good approximation and producing lots of debug output, boiling down extraction to pull out just the cash flows and to get the job complete over all my accounts, even if it meant making some adjustments to my input file. It turned out to be the right decision: I managed to complete the task, and this document presents my journey, the method, its assumptions, quirks, and some results. Overview of Method \uf0c1 The method I'll use is to extract cash flow information for each investment. What I call \" an investment \" in this context is a kind of financial instrument , e.g., shares of \"VTI\" for the Vanguard Total Stock Market ETF, invested in a particular account , e.g. a Vanguard 401k. A list of cash flows is a dated list of positive or negative currency being invested or withdrawn as proceeds to/from that investment. So I have a list of cash flows per account, and each account records only one financial instrument. For reporting I'll group those per-account series in logical units of \" reports \", for example lumping together ones for the same instrument bought in different accounts / brokers, or different instruments that represent the same underlying by merging their cash flows. Or even to group them by \"strategy\". Basically, by merging the cash flows of multiple accounts, I have a generic way to group any subsets of accounts and compute their returns. Using the cash flows, I will then run a simple root finding routine to calculate the average annual rate those flows would have to grow in order to result in their final market value. This provides me with overall returns. This is similar to calculating the Internal Rate of Return . I will do that for the entire time series, but also for sub-intervals within the time series to compute, e.g. calendar returns (i.e., each year or quarter) or cumulative returns for trailing periods. Since cash flows are flagged as dividends or not, I can separate the returns from appreciation from the returns from dividends. Reports with plots are produced for each of the groups. Here's a diagram that shows how the \"configure\", \"compute_returns\" and \"download_prices\" scripts work together: These will be further detailed below. Configuration \uf0c1 First, a configuration needs to be created to define a list of investments, and groups of those, for which reports will be produced. This configuration is provided as a text-formatted protocol buffer message. Investment. An investment corresponds to a particular instrument stored in a particular account. It also involves other transactions that don't directly involve that particular account. We want to provide a few set of account names: Asset account. The name of the account holding the commodities for that investment. Matching accounts. A list of additional accounts which will select transactions with postings to them. Cash accounts. A list of accounts external to the investment, which will be used to flag inflows and outflows to and from the investment. Report. While we can compute returns for each investment, the more general and useful case is to compute returns on groups of them. A report is simply a list of investment names. A PDF file will be produced for each report. Here's an example of 6 investments and 3 reports: investments { # Accounts at BTrade. investment { currency: \"GLD\" asset_account: \"Assets:US:BTrade:GLD\" cash_accounts: \"Assets:US:BTrade:Cash\" } investment { currency: \"AMZN\" asset_account: \"Assets:US:BTrade:AMZN\" cash_accounts: \"Assets:US:BTrade:Cash\" } investment { currency: \"QQQ\" asset_account: \"Assets:US:BTrade:QQQ\" dividend_accounts: \"Income:US:BTrade:QQQ:Dividend\" cash_accounts: \"Assets:US:BTrade:Cash\" } # Accounts at IBKR. investment { currency: \"IAU\" asset_account: \"Assets:US:IBKR:IAU\" cash_accounts: \"Assets:US:IBKR:Cash\" } investment { currency: \"SLV\" asset_account: \"Assets:US:IBKR:SLV\" cash_accounts: \"Assets:US:IBKR:Cash\" } # Accounts at Schwab. investment { currency: \"GOOG\" asset_account: \"Assets:US:Schwab:GOOG\" cash_accounts: \"Assets:US:Schwab:Cash\" cash_accounts: \"Assets:AccountsReceivable\" cash_accounts: \"Assets:US:GoogleInc:GSURefund\" } } groups { group { name: \"strategy.gold\" investment: \"Assets:US:BTrade:GLD\" investment: \"Assets:US:IBKR:IAU\" } group { name: \"strategy.tech\" investment: \"Assets:US:BTrade:QQQ\" investment: \"Assets:US:BTrade:FB\" investment: \"Assets:US:Schwab:GOOG\" } group { name: \"all\" investment: \"Assets:US:*\" currency: \"USD\" } group { name: \"accounts.BTrade\" investment: \"Assets:US:BTrade:*\" currency: \"USD\" } } Different reports can include the same investment. References to accounts and investment names support simple UNIX-style globbing patterns in the configuration. These are expanded to full account names at runtime and stored in the output. A \" configure.py \" script can automatically infer a working basic configuration from an existing Beancount ledger. A report will be generated for each unique instrument and the same metadata fields honored by the \"export\" script (\"assetcls\", \"strategy\") will also generate reports. I recommend you run this on your ledger and then custom tailor the configuration manually. Finding Accounts \uf0c1 This script needs to figure out the list of available investments to report on. By convention I keep pairs of dedicated leaf accounts for each commodity type in my ledger, one to contain the actual positions (assets) and one to receive dividends, e.g., for \"VTI\" held in broker \"BTrade\", accounts like 2012-03-01 open Assets:US:BTrade:VTI VTI 2012-03-01 open Income:US:BTrade:VTI:Dividend USD This has two consequences: (a) it makes it easy to find the list of accounts that contain investments (any account with a leaf account name that is also one of the commodities found in the ledger), and (b) it nicely isolates all the activity related to each of those investments by finding all the transactions with a posting to that account . I recommend you follow the same convention in your chart of accounts. The result is a list of accounts like \"Assets:US:BTrade:VTI\", specific to each (instrument, institution). I further filter down this list to the subset of accounts which were still open up to 15 years ago (I close my accounts\u2014using Beancount's Close directive\u2014when they're done). In my particular case, I didn't have much savings back then, and there's no point in bothering to do the work to normalize those crumbs in my investing history for that far back. Extracting Cash Flow Data \uf0c1 This section describes the various steps I took to extract relevant data from my ledger. Extracting Relevant Transactions \uf0c1 For each of the identified asset accounts, we want to pull out from the ledger's transactions the list of transactions affecting that account. We simply run through the entire ledger's transactions keeping transactions with at least one posting to the investment's asset account, dividend income accounts, or to any of the other \"match accounts\" defined for it. For instance, transactions for cash dividend payments will not show an asset posting so if dividends are paid; a typical dividend payment would contain only the dividend income posting and a cash posting (for the deposit): 2019-11-27 * \"Dividend\" Income:US:BTrade:VTI:Dividend -123.45 USD Assets:US:BTrade:Cash 123.45 USD So it is necessary to include the dividend account in the configuration to include those transactions, because they typically do not involve the asset account. Account Categorization \uf0c1 The next step is to generalize transactions to templates : we record the full set of accounts involved in the extracted transactions of each investment, and assign them to general categories based on their role in the transaction. For example, if I inspect my \"VTI\" trades, I will encounter the following accounts: Assets:US:BTrade:Cash Assets:US:BTrade:VTI Expenses:Financial:Commissions Income:US:BTrade:VTI:Dividend I map each account to one of several generic categories (I could probably simplify this now): ASSET # The account holding the commodity. CASH # Cash accounts, employer matches, contributions. DIVIDEND # Dividend income account. EXPENSES # Commissions, fees and other expenses. INCOME # Non-dividend income, P/L, gains, or other. OTHERASSET # Other assets than the primary asset for this investment. OTHER # Any other account. Like this: 'Assets:US:BTrade:Cash': CASH 'Assets:US:BTrade:VTI': ASSET 'Expenses:Financial:Commissions': EXPENSES 'Income:US:BTrade:VTI:Dividend': DIVIDEND In this way, I can compare similar transactions to each other across instruments and extract information from them with the same code. For example, a transaction that involves a dividend account and a cash account is a cash dividend payment, and I can write a generic handler to extract the cash flows for this. The categorization was originally prototyped with a set of ad-hoc rules, but the configuration now serves to provide the categorization. Note: In the process of doing this, I noticed many irregularities in how I named my accounts. For example, I used \":Dividend\" and \":Dividends\" sometimes. I went through my ledger and had to make some changes to name accounts coherently, and iterated until all my accounts were categorized correctly. You may have to review some of your data entry as well. Handling Transactions using the Signature \uf0c1 Using the account-category mappings from the previous section, I was able to derive a unique \"signature\" for each transaction. For example, a transaction like this: 2020-03-12 * \"(DOI) ORDINARY DIVIDEND\" Income:US:BTrade:VTI:Dividend -1312.31 USD Assets:US:BTrade:Cash 1312.31 USD would have a signature of CASH_DIVIDEND which is hopefully always a dividend payment. Beancount has a pretty flexible syntax and does not enforce that your transactions follow particular templates like this, so it wasn't clear to me when I started this project what patterns I would find in my ledger of 12 years of ad-hoc data entry\u2026 I wasn't certain that this categorization and these signatures would be sufficient to correctly handle a correct conversion to cash flows. So I had my script produce two sets of files for debugging: Investment details. A file for each investment , rendering a list of all the transactions that were extracted for it, decorated with metadata showing the categorizations inferred on each posting, as well as a categorization map of all the accounts encountered. I inspected these files visually to ensure that the account/patterns from the configuration were extracting the full and correct set of transactions involved in that investment. Signature transactions. A file for each unique signature , with the full list of transactions matching that signature across all investments. By inspecting these files, I made sure that all the transactions matching the same signature were indeed playing the same role, so that a single handler per signature is sufficient. At this point, I had a limited list of unique signatures, each with clear unique roles: ASSET_CASH : Purchase or sale ASSET_CASH_EXPENSES : Purchase or sale with commission ASSET_CASH_INCOME : Purchase or sale with profit ASSET_CASH_INCOME_EXPENSES : Purchase or sale with commission and profit ASSET_EXPENSES : Fee paid from liquidation ASSET_INCOME : Cost basis adjustment (with P/L) ASSET_INCOME_EXPENSES : Fee from liquidation (with P/L) ASSET : Stock splits ASSET_DIVIDEND : Dividend reinvested CASH_DIVIDEND : Dividend payment CASH_INCOME_DIVIDEND : Dividend payment and gains distribution ASSET_OTHERASSET : Exchange of stock/symbol \u2026 Note that the specific list really depends on the particular contents of your ledger and you should inspect the files produced for correctness. I then wrote specific handlers to produce the cash flows corresponding to each of those transaction signatures, reasoning about each of those cases in isolation. This allowed me to correctly produce a full list of cash flows per investment. Note: In practice I encountered 3 or 4 more signatures types that were a bit exotic and by fixing up my ledger I managed to either correct or break apart these transactions to equivalent but simpler ones. In particular, one of my importers was lumping together trades occurring on the same day, and I went back and fixed it and those transactions manually. The ASSET_OTHERASSET signature, in particular, is an exchange of stock (Google -> GOOG,GOOGL). Doing something like this brings up idiosyncrasies in your bookkeeping technique. Being consistent and using fewer templates is helpful. It would be a valuable idea for an accompanying plugin to restrict the possible set of templates to a select few, so that data entry is constrained to work well with this returns production code. Generalizing Production of Cash Flows \uf0c1 After inspecting each of my signature handlers, I tried to generalize them to a single unified handler that would work across all transactions. It turns out that, at least with my existing ledger's transactions, it's possible. Essentially, recording inflows or outflows to cash accounts or other assets is sufficient. In a transaction like this: 2013-09-18 * \"Buy shares of HOOL\" Assets:US:BTrade:Cash -818.55 USD flow: CASH Assets:US:BTrade:HOOL 8 HOOL {101.20 USD} flow: ASSET Expenses:Financial:Commissions 8.95 USD flow: EXPENSES The \"CASH\" posting is a sufficient incoming cash flow, so we record -818.55 USD. In a cash dividend payment: 2013-12-17 * \"Cash Dividend payment\" Assets:US:BTrade:Cash 38.50 USD flow: CASH Income:US:BTrade:HOOL:Dividends -38.50 USD flow: DIVIDEND Similarly the 38.50 is a sufficient outgoing cash flow, so we record +38.50 USD. On the other hand a reinvested asset dividend, as you would find in some mutual funds, does not generate any cash flow; it simply remains in the investment and increases its total value: 2013-12-30 * \"Reinvested dividend\" Assets:US:BTrade:HOOL 0.356 {103.41} USD flow: ASSET Income:US:BTrade:HOOL:Dividends -36.81 USD flow: DIVIDEND This rule seems sufficient to handle all the contents of my ledger correctly. In the end, I implemented both methods: I use the general rule to produce the list of cash flows, but I also call out to the explicit handlers and cross-check that the extracted cash flows are identical, just to be sure. This is enabled by a flag in compute_returns.py ( --check-explicit-flows ). This forces me to ensure that I've analyzed all the possible transaction templates. Note: If in using this script you find cases from your ledger that aren't handled by using a series of cash accounts, please let me know (on the mailing-list). Cash Flows \uf0c1 The handlers described in the previous section each produced a list of cash flows per transaction, and together for the account, they are essentially a list of: (Date, Amount, IsDividend) Now, this is a simpler model to work from. For each account, we now have a sorted series of dated cash flows. Note that Amount includes its cost currency (I have both USD and CAD), IsDividend is a flag identifying the cash flow as being a dividend payment or not (to compute returns without the dividends). These series of cash flows can be easily merged between accounts, and truncated over time by inserting initial or final cash flows corresponding to the market value at those dates. Rendered, they might look like this (because of the scale, rendering the log brings up detail that is otherwise difficult to see; dividends are rendered in green): Note that since many transactions do not generate cash flows, the list of cash flows of an investment is insufficient by itself to compute the value of the investment over time. When truncating for a time interval, the market value of the investment is derived using the list of transactions. Finally, the list of cash flows for each group of investments reported can be trivially merged by concatenating them. Computing Returns \uf0c1 Calculating the Average Growth Rate \uf0c1 For each series of cash flows, the cash flows are merged together. I use scipy.optimize.fsolve to calculate the rate that satisfies net present value: c f i /(1\u2005+\u2005 r ) t i = 0 where cf i are the signed cash flow amounts and t i are the times from today for each cash flow (in years). We solve for r . To compute the returns without dividends, we just exclude cash flows returned from dividends. The difference tells us how much of our returns was due solely to dividend income. It's important to note that if the corresponding positions are still invested, you have to insert a final negative cash flow for the market value at the latest date to zero it out. You're essentially simulating a sale. If significant transaction costs are going to be involved, you might want to simulate those as well (e.g. if you're doing this for a home, in particular). Here's the beauty in this: nowhere was the underlying price used , except for marking the current position value to market value. We did not read any external measure of returns. These returns are computed from cash going in and coming out . These are actual realized returns. They do not lie. Intervals. To compute calendar returns, e.g., returns for years 2016, 2017, 2018, 2019, 2020, I truncate the cash flows to keep only those inside the interval, e.g. 2018-01-01 to 2018-12-31 for year 2018, and if there was an existing position at the beginning of the interval, insert a negative cash flow at the start of the series equivalent to the market value at those dates. I do the same thing at the end of the interval, with a positive cash flow, as described previously. Ideally I'd like to look at different sets of intervals: Lifetime of investment. Total returns over the entire lifetime of the positions, to boil it all down to a single number. Calendar. Annual or quarterly returns over the last 10 or 15 years, to witness variation in returns over time. Cumulative. Cumulative returns over the last 10 or 15 years, aligned on calendar periods, to get a sense of whether things are improving or worsening, and how well my strategies are doing in more recent periods (e.g. last 3 years). High frequency cumulative. Cumulative returns over the last 12 months, aligned with monthly or weekly dates, to assess the impact of decisions in the short-term. These can be rendered in tables, and investments can be compared to each other in that way. Filling in Missing Price Points \uf0c1 The calculations of returns over various intervals require marking positions to market value at the beginning and end of the interval dates. Beancount being designed hermetically, i.e., it does not fetch any price externally, only uses what's in the ledger, its price database lookup will automatically produce the last available price point and its recording date before the requested date. Depending on your recording discipline, some of those prices might be out-of-date and introduce inaccuracies. This is especially important since the amounts converted at the end of periods (i.e. to estimate the value of current positions) can be large and meaningfully influence even the lifetime returns number. So it's important to have relatively fresh price points in the ledger's price database. Now the question is, given a changing set of positions over time, for a given set of interval dates, which price entries are required to produce accurate results? Because this depends heavily on the particular inputs of the returns script, in order to solve this problem I simply wrapped the price database with a facade that collects all the (instrument, date) pairs for requested conversions during the production of the reports, and filter those down by some age threshold (e.g., price not older than 3 days). These are essentially the price points missing from the file. At the end of the script, I output these to a file with Price directives, and another program (download_prices.py) can read that file and fetch historical rates for those. It produces updated rates which you can paste to your ledger file as a one-off adjustment and then recompute more accurate returns. Pulling data from Yahoo! Finance worked for 90% of my positions, but some of my older instruments were quite old or even retired, or not available (e.g., some retirement funds), so I had to find them by browsing and manually entering some of these price points (I had something like 30\u2026 no big deal). Rolling windows. One important point is that going forward, it will be easier to align reporting intervals to some calendar-based interval (e.g., monthly), so that I don't have to regenerate price data every time I produce my returns. Aligning to months is probably good enough for my time horizon. Stock Splits. Beancount does not explicitly make adjustments of prices for stocks that split, so your price source should return the pre-split price of the stock at the time if you are recording it as such. You should be able to check your price time series for errors using Fava. Currency Conversion \uf0c1 Another important detail is that each investment has its own quote currency. I used to live in Canada, and some of my older investments are denominated in CAD. So the question arises: do I compute the returns in local (CAD) currency, or in terms of my reference currency (USD)? It's convenient that Beancount's Inventory object has functions that can easily perform those conversions where needed. And since the cash flows I extracted are stored using Beancount's Amount object, I already have the quote currencies correctly in my extracted dataset. In any group, if all the instruments have the same quote currency, I report returns in that currency. If the group includes a mix of quote currencies, I further convert everything to USD (so I get USD returns). Reporting \uf0c1 Grouping Accounts \uf0c1 Returns are calculated jointly for each group of accounts in each \"report\", as defined in the configuration. Here are some example groupings that are sensible to define: Same instrument in different accounts. If you buy the same stock in different accounts, it makes sense to report on the returns for that stock jointly, across all your accounts. Same underlying. Some instruments represent the same stock, e.g. GOOG and GOOGL (different share class, same company). Also, IAU and GLD (Gold) are different ETFs whose values are both derived from physical gold reserves (located in bank basements in London). Same asset class. Instruments from the same asset class, e.g. \"metals\", which would include IAU, GLD, SLV, COPX, etc., or \"REITs\", which would include VNQ, VGSLX, etc. Or \"all stocks\" vs. \"all bonds\". By Strategy. In my portfolio investment method, I have a multi-headed approach where I define specific broad strategies and then select a list of instruments to implement it. For instance, I have a \"tech sector\" strategy, which includes FAANG companies. Or a \"growth stock\" strategy, which might include different indexes like VUG, IWO and RGAGX. I can report how well those strategies are doing, across brokers. Or geographically, \"developed APAC\", including EWY, EWT, EWS, EWA. By Broker. I can report returns by broker or broker account. In particular, this can be an easy way to separate realized profits by tax treatment (e.g., 401k is tax-deferred). Asset type. Comparing all index funds to all managed funds (e.g. mutual funds). Note that different reports can include the same investments. Groupings aren't exclusive. You define what makes most sense for your situation. For reference, I use more than 20 reporting groups. Running the Code \uf0c1 Simply call ./experiments/returns/compute_returns.py to generate all reports and debugging files, where is in the format shown in \u201cConfiguration\u201d . It's a little slow \u2014 some performance improvements are possible \u2014 but if you supply a list of report names after the final argument, only those investments and reports will be processed, so you can iterate faster that way. See flags with --help for details, and config.proto for the configuration input documentation. Results Rendered \uf0c1 For each reporting group, I currently produce: A plot of the cash flows over time , and a smaller plot of log (cash flows). Dividend payments are typically dwarfed by cash flows for the principal, so the log chart allows me to see timings. This is mainly used to get an overview of activity over time, and for debugging. A plot of cumulative value, where I render two curves: Cumulative cash flows , with a growth curve matching that returns I regressed over. The result should be a plot with gentle slope between cash flows (corresponding to the total returns growth), with a final drop to zero. Market value over time : A curve of the mark-to-market value of the portfolio over time. This allows me to make some sense of calendar returns, by witnessing how the asset value moves based on price. A table of total returns , returns without dividends, and returns from dividends only. A table of calendar returns for each year, also broken down by total, ex-dividends, dividends-only. (I'll probably render this as a plot in the future.) A table of trailing cumulative returns . This is going to get refined and augmented as I'm actively working on this code [September 2020]. Example \uf0c1 Here's an example report, for a subset of accounts with a \"growth\" focus, held at different brokers. I produce one of these for each reporting group. (I greyed out parts for privacy.) Interpretation Gotchas \uf0c1 A few notes are in order: All rates are annualized . This makes it easy to compare numbers to each other, but it also means that positions held for a short amount of time will produce numbers that are unrealistic for long term extrapolation. In particular, new positions entered only a few months ago may be subject to high growth or a big drop, both of which when extrapolated to an entire year may show eye-popping percentages. Do keep this in mind, especially when looking at recent positions added to your portfolio. Taxes aren't factored in. Returns from taxable accounts and tax-deferred accounts should be evaluated differently and if the difference in tax is large, they can't be compared so readily. Do keep in mind that in most countries gains are only taxed on realization (sale) so in effect, long held investments behave more or less like tax-deferred ones. Just don't sell so much. This is a great advantage in holding broadly diversified ETFs (and usually unquantified, as people's attention is still overly focused on those benefits from registered accounts, e.g., 401k plan). Cost basis. Note that nowhere in our calculations was the cost basis used or factored in, so don't confuse it with market value. The cost basis is only useful for tax-related effects. Other Instruments Types \uf0c1 Note that computing returns in this manner isn't limited to only stocks and bonds. Using the same methodology, we can include other types of instruments: P2P Lending \uf0c1 I've used LendingClub before all the hedge funds got involved to pick up the low-hanging fruit, and eventually let all the bonds I'd invested in expire. It was easy to apply the same methodology to compute returns from those investments. Originally, I had the discipline to record this investment from monthly PDF statements using transactions like this: 2016-10-31 * \"2016-10-31.Monthly_Statement.pdf\" Assets:US:LendingClub:FundsLent -451.52 LENCLUB {1 USD} Assets:US:LendingClub:Cash 451.52 USD Income:US:LendingClub:LoanInterest -21.68 USD Income:US:LendingClub:Recoveries -5.92 USD Expenses:Financial:Fees 1.08 USD ;; Recovery fees Expenses:Financial:Fees 4.71 USD ;; Service fees Expenses:Financial:Fees 0.45 USD ;; Collection fees Assets:US:LendingClub:Cash Assets:US:LendingClub:FundsLent -23.05 LENCLUB {1 USD} Income:US:LendingClub:ChargedOff 23.05 USD And later on, after the principal bonds expired, transactions like this: 2018-11-30 * \"2018-11-30.Monthly_Statement.pdf\" Income:US:LendingClub:Recoveries -2.73 USD Expenses:Financial:Fees 0.49 USD ;; Recovery fees Assets:US:LendingClub:Cash All it took to compute returns is this configuration for the investment: investment { currency: \"LENCLUB\" asset_account: \"Assets:US:LendingClub:FundsLent\" match_accounts: \"Income:US:LendingClub:Recoveries\" cash_accounts: \"Assets:US:LendingClub:Cash\" } Note that the match account configuration was necessary to pick up crumbs from later transactions with only recoveries (and no posting to the asset account). For what it's worth, I was able to generate a 6.75% return over this investment. Meh. Real Estate \uf0c1 Cash flows can be extracted from all transactions for a home, so you can compute the returns for all the money poured into your home, as if it was done purely for investment purposes. Usually buying a home is done for other reasons \u2014 stability for children, the ability to make your own improvements, the forced savings involved in regular principal payments, and oftentimes just a sense of \"having a home\" \u2014 but in the vast majority of the cases, home ownership is more of a cost center and returns would have been better invested elsewhere (see this book for a great expos\u00e9 of the pros & cons). Personally, I have better things to do than fix toilets and worry about leaky windows in the winter so I went back to renting, but I went through the experience once and it was quite worthwhile, as a learning experience but also to experience the \"joy\" of having my own place. Through the exercise is useful to calculate how much having your own home is actually costing you, and how much you might have made by putting the very same cash flows into the market instead. It's a little more involved, because, You will have to have discipline to segregate expenses you wouldn't have had if you'd have rented to accounts specific for your home. You will need to account for equivalent rent received that you didn't have to pay because you lived in the home. If you still own the home, you will have to simulate a substantial agent fee and other closing costs (by inserting a transaction). Most places have a large tax break when selling a property (sometimes the full capital appreciation), so that really needs to be factored in. It's not always simple to calculate, especially if you rented your property for some of its ownership lifetime, in which case you might only be able to deduct a part of the capital gains. For some, there is great value in the optionality of being able to move easily and at low cost (e..g, accepting a new job at the other side of the country) and that personal value will be difficult to estimate. This will require me to do more categorization work, and it will be best documented as a separate project, though using the same code. I did manage to create a simple configuration and extract a 5%-ish number out of it, but I think I'll need a bit more work to declare victory. More to come. Options \uf0c1 I sometimes hedge some of my portfolio downrisk with long-dated OTM PUT positions. I've sold OTM PUTs and CALLs in the near term to finance it. The method I described for stocks in this document works equally well for options, that is, extracting cash flows from cash. The main differences are that: Currency names. Instrument names are specific to each contract\u2014they include the expiration date and strike price\u2014so I don't store the options to an account whose name includes the instrument name at the leaf. I just use a generic leaf account name, such as \"Options\" or \"Hedging\" which I select when I enter/exit positions. Prices for options. Prices for options aren't as easily fetchable programmatically. I'm having to use a private API for this. Perhaps more importantly, declining theta, varying vol and non-linearity near the strike means I do need to have to have pretty recent price estimates in my ledger in order to compute returns on unrealized gains/losses. I think the effect is strong enough that I'd want to eventually have some code to always update the prices just before generating the reports. Future Work \uf0c1 This section describes desired future improvements on the returns code, which I'm likely to implement, and the corresponding challenges. Note: As I'm writing this in September 2020, I'm actively working on this code and probably will continue over the next few months' weekends. It's possible this document falls slightly out-of-date and that some of the proposals below become implemented. Refer to the source code for the latest. Relative Size over Time \uf0c1 Another useful bit of data I could easily add to reports is a stack plot of market values over time, as well as relative fractions of each investment in a report group. This would be useful for monitoring growth of particular instruments in a group to help with rebalancing. Comparison Against a Benchmark \uf0c1 One of the important barometers of success in running your own portfolio is a comparison to an equivalent investment of capital in a simple portfolio of diversified stocks and bonds rebalanced regularly. After all, if my returns aren't beating that, in a normal environment, one could make the argument I shouldn't bother with a more complicated allocation. Assuming that my access to capital is the same (which it is, because in this project I'm looking at savings from income), I could simply replace my cash flows by cash flows to this simulated portfolio, in other words, use the same timings to simulate buying other assets. I would have to exclude the dividend payments because those are specific to the particular instruments I used, and at the same time generate simulated dividends from similarly sized positions in the benchmark portfolio. It should be possible to do this without modifying my ledger. One issue is that I will require a set of accurate prices for the benchmark, at the dates of my historical cash flows. Like my script already does for aging price points, this could easily be stored in a file for fetching. Perhaps more complicated is the fact that Beancount does not currently support a database of per-share dividend distributions. This could be added without introducing new syntax by attaching and honoring metadata on Price directives, such as 2020-09-01 price LQD 136.16 USD distribution: 0.295 USD Overall, it wouldn't be too difficult to implement this. Including Uninvested Cash \uf0c1 One of the problems I'm facing in investing my own portfolio is the lack of discipline around timely investing of available cash, especially when times are uncertain and difficult decisions have to be made. In order to account for the drag on performance from that, I should include an \"investment\" that's purely reflecting the total amount of uninvested cash over time. Because this varies a lot, a good approximation can be obtained by sampling total amount of investable cash every month and synthesizing cash flows from differences. If that cash flow series gets included as part of a portfolio, it'll suitably drag its returns by diluting them. After-Tax Value \uf0c1 At the moment I export all my holdings to a Google Sheets doc using the export script , and from that, break it down between long-term and short-term positions vs. pre-tax, after-tax, Roth, and taxable buckets. From those 8 aggregates, I remove estimated taxes and report a single \"estimated after-tax net worth\" and corresponding tax liability. This is a rough estimate. The returns report is however much more detailed, and I could simulate tax payments not only on liquidation, but also at the end of each year (from dividends and sales). I have all the lot dates on each position held so I can automatically figure out short-term vs. long-term lots. Inflation Adjustments \uf0c1 The market value amounts reported on the returns charts and calendar returns do not account for inflation. Over long periods of time, those can make an important difference in returns. It would be useful to discount the returns over time using annual estimates for the CPI (or some other estimate of inflation; you could even make up your own, from the expenses in your ledger), so that we can look at a curve of real growth and not just nominal growth. Sales Commission \uf0c1 The configuration could easily be improved to let the user specify expected commissions on sales of investments, either in absolute or relative (%) amounts. This would be used to mark positions with more realistic liquidation values. This could make a difference on investments with either small amounts or large commissions (i.e., working through a regular broker, or on real estate). Risk Estimation & Beta \uf0c1 A perhaps more advanced topic would be to compute an estimate of the variance from the specific portfolio composition in order to calculate and report some measurement of risk, such as Sharpe Ratio . This would require a sufficient number of regularly spaced price points. Variation of the measures over time could be fun too, as well as calculating the current portfolio's specific sensitivity to market as a whole (beta). Conclusion \uf0c1 I had expected it would be possible to produce a clear picture of returns from Beancount data, and having done it I'm more satisfied with the level of detail and clarity I was to produce from my ledger than I had expected. This goes well beyond plotting of net worth over time, this actually works really well, and I can use it to compare the performance of different investments fairly. I hope at least some Beancount users will be able to run it on their ledgers and I'm looking forward to hearing some feedback from those who set it up. Perhaps most importantly, I was very surprised to see results on my own portfolio returns. I'm one of those people who would normally shrug and guess an underwhelming ballpark number if asked what I thought my returns were, such as \"meh, 6% or something, not super happy.\" Doing this work was originally driven by not having the right answer to this question. It turns out, over the last 15 years, that I've generated an almost 12% average annual return, and 14% annual return in the last 5 years. I haven't yet made the benchmark comparison, and for sure these numbers should be put side-by-side with the market for a fair assessment. Nevertheless, going through the exercise has provided me with renewed confidence and hope about the future, and I hope the clarity it will bring to other Beancount users' own investments will be similarly enlightening.","title":"Calculating Portfolio Returns"},{"location":"calculating_portolio_returns.html#calculating-portfolio-returns","text":"Martin Blais , Sept 2020 http://furius.ca/beancount/doc/returns This document describes how to compute portfolio returns from a Beancount ledger.","title":"Calculating Portfolio Returns"},{"location":"calculating_portolio_returns.html#motivation","text":"You will be surprised to find that discount brokers typically do not provide accurate and complete returns calculations for your investments based on your specific cash flows. They tend to report other measures of performance: Change in value. The simplest they provide is a snapshot of the account value at the beginning and end of the period (or year). The problem with this method is that it does not reflect your infusions or removal of cash as such, nor your changes in positions. For example, if you had an account with $50,000 at the beginning of the year and you've added in $30,000 in August, reporting a difference of $37,000 at the end of December is just not useful (you'd have to mentally discount for new invested cash during the year, and what if you want to break it down and compare the returns from different instruments in the account?). Underlying performance. They might report the growth of the underlying asset in isolation, disregarding your specific positions. It's not very useful to say \"HDV grew by 8.2% over the last year\" if you invested only (or even mostly) in the second half of the year. What I'd like is to know how much my specific investments grew, given my specific changes in positions and timings. In other words, how did I do? No dividends. Another issue is that performance is typically only reporting capital appreciation due to the change in price of the investment. Ideally one would like to break down the performance between capital appreciation and dividends and returns for each of those components, as well as overall performance, so you can compare returns from stocks and bonds to each other. You can sometimes find out that information from the Yield field in the summary, but for uneven distributions, this won't help me find out what my actual returns from dividends were, over any period. Commissions and management expenses. Some brokers charge management expenses on a monthly basis, based on the value of the account. I would like to know what my portfolio return is net of those expenses. I want accurate returns reporting, based on my actual cash flows and changes of positions over time. If I maintain your investment information in a Beancount ledger, in theory it contains all the data I need in order to compute your true returns, based on the specific timings of my own savings (cash infusions) and which positions I held at which time. It's just not in the simplest format required to do it\u2014 Beancount transactions are much more flexible than one might want and a simpler series of cash flows needs to be extracted from it. This document explains how I finally did this from my own Ledger. And how we might generalize this to yours, based on some simple rules. Most of this text is dedicated to the pedestrian details of extracting the right data. The source code can be found here . In addition, a fair and honest comparison to other investments scenarios should be possible, based on those same cash flows. For instance, you should be able to produce data that looks like \"My investments in ZZZ have returned 8.2%, 1.1% of which were from dividends, and if I'd invested in a 60/40 portfolio of broad stocks and bonds it would have returned 7.2% instead.\" In other words, I want to assess my performance relative to a number of common alternatives. (Finally, note that if all you need is a snapshot of your current positions, that's already handled by the export script .)","title":"Motivation"},{"location":"calculating_portolio_returns.html#history","text":"In 2014, I made a brief attempt to break down information from my ledger to do this. At the time I got bogged down in details when some of the time-series I was extracting weren't producing what looked like sensible results (some with outliers). I got too detailed too fast. Sometimes it's better to just get the whole job done and come back for the details. I hadn't logged enough debugging information and I didn't have enough confidence in its output to use it. I never actually finished the work at the time, and eventually moved the scripts to experiments and shrugged. \"Later.\" In August 2020, I sat down to do this again, this time with a less ambitious goal of just getting a good approximation and producing lots of debug output, boiling down extraction to pull out just the cash flows and to get the job complete over all my accounts, even if it meant making some adjustments to my input file. It turned out to be the right decision: I managed to complete the task, and this document presents my journey, the method, its assumptions, quirks, and some results.","title":"History"},{"location":"calculating_portolio_returns.html#overview-of-method","text":"The method I'll use is to extract cash flow information for each investment. What I call \" an investment \" in this context is a kind of financial instrument , e.g., shares of \"VTI\" for the Vanguard Total Stock Market ETF, invested in a particular account , e.g. a Vanguard 401k. A list of cash flows is a dated list of positive or negative currency being invested or withdrawn as proceeds to/from that investment. So I have a list of cash flows per account, and each account records only one financial instrument. For reporting I'll group those per-account series in logical units of \" reports \", for example lumping together ones for the same instrument bought in different accounts / brokers, or different instruments that represent the same underlying by merging their cash flows. Or even to group them by \"strategy\". Basically, by merging the cash flows of multiple accounts, I have a generic way to group any subsets of accounts and compute their returns. Using the cash flows, I will then run a simple root finding routine to calculate the average annual rate those flows would have to grow in order to result in their final market value. This provides me with overall returns. This is similar to calculating the Internal Rate of Return . I will do that for the entire time series, but also for sub-intervals within the time series to compute, e.g. calendar returns (i.e., each year or quarter) or cumulative returns for trailing periods. Since cash flows are flagged as dividends or not, I can separate the returns from appreciation from the returns from dividends. Reports with plots are produced for each of the groups. Here's a diagram that shows how the \"configure\", \"compute_returns\" and \"download_prices\" scripts work together: These will be further detailed below.","title":"Overview of Method"},{"location":"calculating_portolio_returns.html#configuration","text":"First, a configuration needs to be created to define a list of investments, and groups of those, for which reports will be produced. This configuration is provided as a text-formatted protocol buffer message. Investment. An investment corresponds to a particular instrument stored in a particular account. It also involves other transactions that don't directly involve that particular account. We want to provide a few set of account names: Asset account. The name of the account holding the commodities for that investment. Matching accounts. A list of additional accounts which will select transactions with postings to them. Cash accounts. A list of accounts external to the investment, which will be used to flag inflows and outflows to and from the investment. Report. While we can compute returns for each investment, the more general and useful case is to compute returns on groups of them. A report is simply a list of investment names. A PDF file will be produced for each report. Here's an example of 6 investments and 3 reports: investments { # Accounts at BTrade. investment { currency: \"GLD\" asset_account: \"Assets:US:BTrade:GLD\" cash_accounts: \"Assets:US:BTrade:Cash\" } investment { currency: \"AMZN\" asset_account: \"Assets:US:BTrade:AMZN\" cash_accounts: \"Assets:US:BTrade:Cash\" } investment { currency: \"QQQ\" asset_account: \"Assets:US:BTrade:QQQ\" dividend_accounts: \"Income:US:BTrade:QQQ:Dividend\" cash_accounts: \"Assets:US:BTrade:Cash\" } # Accounts at IBKR. investment { currency: \"IAU\" asset_account: \"Assets:US:IBKR:IAU\" cash_accounts: \"Assets:US:IBKR:Cash\" } investment { currency: \"SLV\" asset_account: \"Assets:US:IBKR:SLV\" cash_accounts: \"Assets:US:IBKR:Cash\" } # Accounts at Schwab. investment { currency: \"GOOG\" asset_account: \"Assets:US:Schwab:GOOG\" cash_accounts: \"Assets:US:Schwab:Cash\" cash_accounts: \"Assets:AccountsReceivable\" cash_accounts: \"Assets:US:GoogleInc:GSURefund\" } } groups { group { name: \"strategy.gold\" investment: \"Assets:US:BTrade:GLD\" investment: \"Assets:US:IBKR:IAU\" } group { name: \"strategy.tech\" investment: \"Assets:US:BTrade:QQQ\" investment: \"Assets:US:BTrade:FB\" investment: \"Assets:US:Schwab:GOOG\" } group { name: \"all\" investment: \"Assets:US:*\" currency: \"USD\" } group { name: \"accounts.BTrade\" investment: \"Assets:US:BTrade:*\" currency: \"USD\" } } Different reports can include the same investment. References to accounts and investment names support simple UNIX-style globbing patterns in the configuration. These are expanded to full account names at runtime and stored in the output. A \" configure.py \" script can automatically infer a working basic configuration from an existing Beancount ledger. A report will be generated for each unique instrument and the same metadata fields honored by the \"export\" script (\"assetcls\", \"strategy\") will also generate reports. I recommend you run this on your ledger and then custom tailor the configuration manually.","title":"Configuration"},{"location":"calculating_portolio_returns.html#finding-accounts","text":"This script needs to figure out the list of available investments to report on. By convention I keep pairs of dedicated leaf accounts for each commodity type in my ledger, one to contain the actual positions (assets) and one to receive dividends, e.g., for \"VTI\" held in broker \"BTrade\", accounts like 2012-03-01 open Assets:US:BTrade:VTI VTI 2012-03-01 open Income:US:BTrade:VTI:Dividend USD This has two consequences: (a) it makes it easy to find the list of accounts that contain investments (any account with a leaf account name that is also one of the commodities found in the ledger), and (b) it nicely isolates all the activity related to each of those investments by finding all the transactions with a posting to that account . I recommend you follow the same convention in your chart of accounts. The result is a list of accounts like \"Assets:US:BTrade:VTI\", specific to each (instrument, institution). I further filter down this list to the subset of accounts which were still open up to 15 years ago (I close my accounts\u2014using Beancount's Close directive\u2014when they're done). In my particular case, I didn't have much savings back then, and there's no point in bothering to do the work to normalize those crumbs in my investing history for that far back.","title":"Finding Accounts"},{"location":"calculating_portolio_returns.html#extracting-cash-flow-data","text":"This section describes the various steps I took to extract relevant data from my ledger.","title":"Extracting Cash Flow Data"},{"location":"calculating_portolio_returns.html#extracting-relevant-transactions","text":"For each of the identified asset accounts, we want to pull out from the ledger's transactions the list of transactions affecting that account. We simply run through the entire ledger's transactions keeping transactions with at least one posting to the investment's asset account, dividend income accounts, or to any of the other \"match accounts\" defined for it. For instance, transactions for cash dividend payments will not show an asset posting so if dividends are paid; a typical dividend payment would contain only the dividend income posting and a cash posting (for the deposit): 2019-11-27 * \"Dividend\" Income:US:BTrade:VTI:Dividend -123.45 USD Assets:US:BTrade:Cash 123.45 USD So it is necessary to include the dividend account in the configuration to include those transactions, because they typically do not involve the asset account.","title":"Extracting Relevant Transactions"},{"location":"calculating_portolio_returns.html#account-categorization","text":"The next step is to generalize transactions to templates : we record the full set of accounts involved in the extracted transactions of each investment, and assign them to general categories based on their role in the transaction. For example, if I inspect my \"VTI\" trades, I will encounter the following accounts: Assets:US:BTrade:Cash Assets:US:BTrade:VTI Expenses:Financial:Commissions Income:US:BTrade:VTI:Dividend I map each account to one of several generic categories (I could probably simplify this now): ASSET # The account holding the commodity. CASH # Cash accounts, employer matches, contributions. DIVIDEND # Dividend income account. EXPENSES # Commissions, fees and other expenses. INCOME # Non-dividend income, P/L, gains, or other. OTHERASSET # Other assets than the primary asset for this investment. OTHER # Any other account. Like this: 'Assets:US:BTrade:Cash': CASH 'Assets:US:BTrade:VTI': ASSET 'Expenses:Financial:Commissions': EXPENSES 'Income:US:BTrade:VTI:Dividend': DIVIDEND In this way, I can compare similar transactions to each other across instruments and extract information from them with the same code. For example, a transaction that involves a dividend account and a cash account is a cash dividend payment, and I can write a generic handler to extract the cash flows for this. The categorization was originally prototyped with a set of ad-hoc rules, but the configuration now serves to provide the categorization. Note: In the process of doing this, I noticed many irregularities in how I named my accounts. For example, I used \":Dividend\" and \":Dividends\" sometimes. I went through my ledger and had to make some changes to name accounts coherently, and iterated until all my accounts were categorized correctly. You may have to review some of your data entry as well.","title":"Account Categorization"},{"location":"calculating_portolio_returns.html#handling-transactions-using-the-signature","text":"Using the account-category mappings from the previous section, I was able to derive a unique \"signature\" for each transaction. For example, a transaction like this: 2020-03-12 * \"(DOI) ORDINARY DIVIDEND\" Income:US:BTrade:VTI:Dividend -1312.31 USD Assets:US:BTrade:Cash 1312.31 USD would have a signature of CASH_DIVIDEND which is hopefully always a dividend payment. Beancount has a pretty flexible syntax and does not enforce that your transactions follow particular templates like this, so it wasn't clear to me when I started this project what patterns I would find in my ledger of 12 years of ad-hoc data entry\u2026 I wasn't certain that this categorization and these signatures would be sufficient to correctly handle a correct conversion to cash flows. So I had my script produce two sets of files for debugging: Investment details. A file for each investment , rendering a list of all the transactions that were extracted for it, decorated with metadata showing the categorizations inferred on each posting, as well as a categorization map of all the accounts encountered. I inspected these files visually to ensure that the account/patterns from the configuration were extracting the full and correct set of transactions involved in that investment. Signature transactions. A file for each unique signature , with the full list of transactions matching that signature across all investments. By inspecting these files, I made sure that all the transactions matching the same signature were indeed playing the same role, so that a single handler per signature is sufficient. At this point, I had a limited list of unique signatures, each with clear unique roles: ASSET_CASH : Purchase or sale ASSET_CASH_EXPENSES : Purchase or sale with commission ASSET_CASH_INCOME : Purchase or sale with profit ASSET_CASH_INCOME_EXPENSES : Purchase or sale with commission and profit ASSET_EXPENSES : Fee paid from liquidation ASSET_INCOME : Cost basis adjustment (with P/L) ASSET_INCOME_EXPENSES : Fee from liquidation (with P/L) ASSET : Stock splits ASSET_DIVIDEND : Dividend reinvested CASH_DIVIDEND : Dividend payment CASH_INCOME_DIVIDEND : Dividend payment and gains distribution ASSET_OTHERASSET : Exchange of stock/symbol \u2026 Note that the specific list really depends on the particular contents of your ledger and you should inspect the files produced for correctness. I then wrote specific handlers to produce the cash flows corresponding to each of those transaction signatures, reasoning about each of those cases in isolation. This allowed me to correctly produce a full list of cash flows per investment. Note: In practice I encountered 3 or 4 more signatures types that were a bit exotic and by fixing up my ledger I managed to either correct or break apart these transactions to equivalent but simpler ones. In particular, one of my importers was lumping together trades occurring on the same day, and I went back and fixed it and those transactions manually. The ASSET_OTHERASSET signature, in particular, is an exchange of stock (Google -> GOOG,GOOGL). Doing something like this brings up idiosyncrasies in your bookkeeping technique. Being consistent and using fewer templates is helpful. It would be a valuable idea for an accompanying plugin to restrict the possible set of templates to a select few, so that data entry is constrained to work well with this returns production code.","title":"Handling Transactions using the Signature"},{"location":"calculating_portolio_returns.html#generalizing-production-of-cash-flows","text":"After inspecting each of my signature handlers, I tried to generalize them to a single unified handler that would work across all transactions. It turns out that, at least with my existing ledger's transactions, it's possible. Essentially, recording inflows or outflows to cash accounts or other assets is sufficient. In a transaction like this: 2013-09-18 * \"Buy shares of HOOL\" Assets:US:BTrade:Cash -818.55 USD flow: CASH Assets:US:BTrade:HOOL 8 HOOL {101.20 USD} flow: ASSET Expenses:Financial:Commissions 8.95 USD flow: EXPENSES The \"CASH\" posting is a sufficient incoming cash flow, so we record -818.55 USD. In a cash dividend payment: 2013-12-17 * \"Cash Dividend payment\" Assets:US:BTrade:Cash 38.50 USD flow: CASH Income:US:BTrade:HOOL:Dividends -38.50 USD flow: DIVIDEND Similarly the 38.50 is a sufficient outgoing cash flow, so we record +38.50 USD. On the other hand a reinvested asset dividend, as you would find in some mutual funds, does not generate any cash flow; it simply remains in the investment and increases its total value: 2013-12-30 * \"Reinvested dividend\" Assets:US:BTrade:HOOL 0.356 {103.41} USD flow: ASSET Income:US:BTrade:HOOL:Dividends -36.81 USD flow: DIVIDEND This rule seems sufficient to handle all the contents of my ledger correctly. In the end, I implemented both methods: I use the general rule to produce the list of cash flows, but I also call out to the explicit handlers and cross-check that the extracted cash flows are identical, just to be sure. This is enabled by a flag in compute_returns.py ( --check-explicit-flows ). This forces me to ensure that I've analyzed all the possible transaction templates. Note: If in using this script you find cases from your ledger that aren't handled by using a series of cash accounts, please let me know (on the mailing-list).","title":"Generalizing Production of Cash Flows"},{"location":"calculating_portolio_returns.html#cash-flows","text":"The handlers described in the previous section each produced a list of cash flows per transaction, and together for the account, they are essentially a list of: (Date, Amount, IsDividend) Now, this is a simpler model to work from. For each account, we now have a sorted series of dated cash flows. Note that Amount includes its cost currency (I have both USD and CAD), IsDividend is a flag identifying the cash flow as being a dividend payment or not (to compute returns without the dividends). These series of cash flows can be easily merged between accounts, and truncated over time by inserting initial or final cash flows corresponding to the market value at those dates. Rendered, they might look like this (because of the scale, rendering the log brings up detail that is otherwise difficult to see; dividends are rendered in green): Note that since many transactions do not generate cash flows, the list of cash flows of an investment is insufficient by itself to compute the value of the investment over time. When truncating for a time interval, the market value of the investment is derived using the list of transactions. Finally, the list of cash flows for each group of investments reported can be trivially merged by concatenating them.","title":"Cash Flows"},{"location":"calculating_portolio_returns.html#computing-returns","text":"","title":"Computing Returns"},{"location":"calculating_portolio_returns.html#calculating-the-average-growth-rate","text":"For each series of cash flows, the cash flows are merged together. I use scipy.optimize.fsolve to calculate the rate that satisfies net present value: c f i /(1\u2005+\u2005 r ) t i = 0 where cf i are the signed cash flow amounts and t i are the times from today for each cash flow (in years). We solve for r . To compute the returns without dividends, we just exclude cash flows returned from dividends. The difference tells us how much of our returns was due solely to dividend income. It's important to note that if the corresponding positions are still invested, you have to insert a final negative cash flow for the market value at the latest date to zero it out. You're essentially simulating a sale. If significant transaction costs are going to be involved, you might want to simulate those as well (e.g. if you're doing this for a home, in particular). Here's the beauty in this: nowhere was the underlying price used , except for marking the current position value to market value. We did not read any external measure of returns. These returns are computed from cash going in and coming out . These are actual realized returns. They do not lie. Intervals. To compute calendar returns, e.g., returns for years 2016, 2017, 2018, 2019, 2020, I truncate the cash flows to keep only those inside the interval, e.g. 2018-01-01 to 2018-12-31 for year 2018, and if there was an existing position at the beginning of the interval, insert a negative cash flow at the start of the series equivalent to the market value at those dates. I do the same thing at the end of the interval, with a positive cash flow, as described previously. Ideally I'd like to look at different sets of intervals: Lifetime of investment. Total returns over the entire lifetime of the positions, to boil it all down to a single number. Calendar. Annual or quarterly returns over the last 10 or 15 years, to witness variation in returns over time. Cumulative. Cumulative returns over the last 10 or 15 years, aligned on calendar periods, to get a sense of whether things are improving or worsening, and how well my strategies are doing in more recent periods (e.g. last 3 years). High frequency cumulative. Cumulative returns over the last 12 months, aligned with monthly or weekly dates, to assess the impact of decisions in the short-term. These can be rendered in tables, and investments can be compared to each other in that way.","title":"Calculating the Average Growth Rate"},{"location":"calculating_portolio_returns.html#filling-in-missing-price-points","text":"The calculations of returns over various intervals require marking positions to market value at the beginning and end of the interval dates. Beancount being designed hermetically, i.e., it does not fetch any price externally, only uses what's in the ledger, its price database lookup will automatically produce the last available price point and its recording date before the requested date. Depending on your recording discipline, some of those prices might be out-of-date and introduce inaccuracies. This is especially important since the amounts converted at the end of periods (i.e. to estimate the value of current positions) can be large and meaningfully influence even the lifetime returns number. So it's important to have relatively fresh price points in the ledger's price database. Now the question is, given a changing set of positions over time, for a given set of interval dates, which price entries are required to produce accurate results? Because this depends heavily on the particular inputs of the returns script, in order to solve this problem I simply wrapped the price database with a facade that collects all the (instrument, date) pairs for requested conversions during the production of the reports, and filter those down by some age threshold (e.g., price not older than 3 days). These are essentially the price points missing from the file. At the end of the script, I output these to a file with Price directives, and another program (download_prices.py) can read that file and fetch historical rates for those. It produces updated rates which you can paste to your ledger file as a one-off adjustment and then recompute more accurate returns. Pulling data from Yahoo! Finance worked for 90% of my positions, but some of my older instruments were quite old or even retired, or not available (e.g., some retirement funds), so I had to find them by browsing and manually entering some of these price points (I had something like 30\u2026 no big deal). Rolling windows. One important point is that going forward, it will be easier to align reporting intervals to some calendar-based interval (e.g., monthly), so that I don't have to regenerate price data every time I produce my returns. Aligning to months is probably good enough for my time horizon. Stock Splits. Beancount does not explicitly make adjustments of prices for stocks that split, so your price source should return the pre-split price of the stock at the time if you are recording it as such. You should be able to check your price time series for errors using Fava.","title":"Filling in Missing Price Points"},{"location":"calculating_portolio_returns.html#currency-conversion","text":"Another important detail is that each investment has its own quote currency. I used to live in Canada, and some of my older investments are denominated in CAD. So the question arises: do I compute the returns in local (CAD) currency, or in terms of my reference currency (USD)? It's convenient that Beancount's Inventory object has functions that can easily perform those conversions where needed. And since the cash flows I extracted are stored using Beancount's Amount object, I already have the quote currencies correctly in my extracted dataset. In any group, if all the instruments have the same quote currency, I report returns in that currency. If the group includes a mix of quote currencies, I further convert everything to USD (so I get USD returns).","title":"Currency Conversion"},{"location":"calculating_portolio_returns.html#reporting","text":"","title":"Reporting"},{"location":"calculating_portolio_returns.html#grouping-accounts","text":"Returns are calculated jointly for each group of accounts in each \"report\", as defined in the configuration. Here are some example groupings that are sensible to define: Same instrument in different accounts. If you buy the same stock in different accounts, it makes sense to report on the returns for that stock jointly, across all your accounts. Same underlying. Some instruments represent the same stock, e.g. GOOG and GOOGL (different share class, same company). Also, IAU and GLD (Gold) are different ETFs whose values are both derived from physical gold reserves (located in bank basements in London). Same asset class. Instruments from the same asset class, e.g. \"metals\", which would include IAU, GLD, SLV, COPX, etc., or \"REITs\", which would include VNQ, VGSLX, etc. Or \"all stocks\" vs. \"all bonds\". By Strategy. In my portfolio investment method, I have a multi-headed approach where I define specific broad strategies and then select a list of instruments to implement it. For instance, I have a \"tech sector\" strategy, which includes FAANG companies. Or a \"growth stock\" strategy, which might include different indexes like VUG, IWO and RGAGX. I can report how well those strategies are doing, across brokers. Or geographically, \"developed APAC\", including EWY, EWT, EWS, EWA. By Broker. I can report returns by broker or broker account. In particular, this can be an easy way to separate realized profits by tax treatment (e.g., 401k is tax-deferred). Asset type. Comparing all index funds to all managed funds (e.g. mutual funds). Note that different reports can include the same investments. Groupings aren't exclusive. You define what makes most sense for your situation. For reference, I use more than 20 reporting groups.","title":"Grouping Accounts"},{"location":"calculating_portolio_returns.html#running-the-code","text":"Simply call ./experiments/returns/compute_returns.py to generate all reports and debugging files, where is in the format shown in \u201cConfiguration\u201d . It's a little slow \u2014 some performance improvements are possible \u2014 but if you supply a list of report names after the final argument, only those investments and reports will be processed, so you can iterate faster that way. See flags with --help for details, and config.proto for the configuration input documentation.","title":"Running the Code"},{"location":"calculating_portolio_returns.html#results-rendered","text":"For each reporting group, I currently produce: A plot of the cash flows over time , and a smaller plot of log (cash flows). Dividend payments are typically dwarfed by cash flows for the principal, so the log chart allows me to see timings. This is mainly used to get an overview of activity over time, and for debugging. A plot of cumulative value, where I render two curves: Cumulative cash flows , with a growth curve matching that returns I regressed over. The result should be a plot with gentle slope between cash flows (corresponding to the total returns growth), with a final drop to zero. Market value over time : A curve of the mark-to-market value of the portfolio over time. This allows me to make some sense of calendar returns, by witnessing how the asset value moves based on price. A table of total returns , returns without dividends, and returns from dividends only. A table of calendar returns for each year, also broken down by total, ex-dividends, dividends-only. (I'll probably render this as a plot in the future.) A table of trailing cumulative returns . This is going to get refined and augmented as I'm actively working on this code [September 2020].","title":"Results Rendered"},{"location":"calculating_portolio_returns.html#example","text":"Here's an example report, for a subset of accounts with a \"growth\" focus, held at different brokers. I produce one of these for each reporting group. (I greyed out parts for privacy.)","title":"Example"},{"location":"calculating_portolio_returns.html#interpretation-gotchas","text":"A few notes are in order: All rates are annualized . This makes it easy to compare numbers to each other, but it also means that positions held for a short amount of time will produce numbers that are unrealistic for long term extrapolation. In particular, new positions entered only a few months ago may be subject to high growth or a big drop, both of which when extrapolated to an entire year may show eye-popping percentages. Do keep this in mind, especially when looking at recent positions added to your portfolio. Taxes aren't factored in. Returns from taxable accounts and tax-deferred accounts should be evaluated differently and if the difference in tax is large, they can't be compared so readily. Do keep in mind that in most countries gains are only taxed on realization (sale) so in effect, long held investments behave more or less like tax-deferred ones. Just don't sell so much. This is a great advantage in holding broadly diversified ETFs (and usually unquantified, as people's attention is still overly focused on those benefits from registered accounts, e.g., 401k plan). Cost basis. Note that nowhere in our calculations was the cost basis used or factored in, so don't confuse it with market value. The cost basis is only useful for tax-related effects.","title":"Interpretation Gotchas"},{"location":"calculating_portolio_returns.html#other-instruments-types","text":"Note that computing returns in this manner isn't limited to only stocks and bonds. Using the same methodology, we can include other types of instruments:","title":"Other Instruments Types"},{"location":"calculating_portolio_returns.html#p2p-lending","text":"I've used LendingClub before all the hedge funds got involved to pick up the low-hanging fruit, and eventually let all the bonds I'd invested in expire. It was easy to apply the same methodology to compute returns from those investments. Originally, I had the discipline to record this investment from monthly PDF statements using transactions like this: 2016-10-31 * \"2016-10-31.Monthly_Statement.pdf\" Assets:US:LendingClub:FundsLent -451.52 LENCLUB {1 USD} Assets:US:LendingClub:Cash 451.52 USD Income:US:LendingClub:LoanInterest -21.68 USD Income:US:LendingClub:Recoveries -5.92 USD Expenses:Financial:Fees 1.08 USD ;; Recovery fees Expenses:Financial:Fees 4.71 USD ;; Service fees Expenses:Financial:Fees 0.45 USD ;; Collection fees Assets:US:LendingClub:Cash Assets:US:LendingClub:FundsLent -23.05 LENCLUB {1 USD} Income:US:LendingClub:ChargedOff 23.05 USD And later on, after the principal bonds expired, transactions like this: 2018-11-30 * \"2018-11-30.Monthly_Statement.pdf\" Income:US:LendingClub:Recoveries -2.73 USD Expenses:Financial:Fees 0.49 USD ;; Recovery fees Assets:US:LendingClub:Cash All it took to compute returns is this configuration for the investment: investment { currency: \"LENCLUB\" asset_account: \"Assets:US:LendingClub:FundsLent\" match_accounts: \"Income:US:LendingClub:Recoveries\" cash_accounts: \"Assets:US:LendingClub:Cash\" } Note that the match account configuration was necessary to pick up crumbs from later transactions with only recoveries (and no posting to the asset account). For what it's worth, I was able to generate a 6.75% return over this investment. Meh.","title":"P2P Lending"},{"location":"calculating_portolio_returns.html#real-estate","text":"Cash flows can be extracted from all transactions for a home, so you can compute the returns for all the money poured into your home, as if it was done purely for investment purposes. Usually buying a home is done for other reasons \u2014 stability for children, the ability to make your own improvements, the forced savings involved in regular principal payments, and oftentimes just a sense of \"having a home\" \u2014 but in the vast majority of the cases, home ownership is more of a cost center and returns would have been better invested elsewhere (see this book for a great expos\u00e9 of the pros & cons). Personally, I have better things to do than fix toilets and worry about leaky windows in the winter so I went back to renting, but I went through the experience once and it was quite worthwhile, as a learning experience but also to experience the \"joy\" of having my own place. Through the exercise is useful to calculate how much having your own home is actually costing you, and how much you might have made by putting the very same cash flows into the market instead. It's a little more involved, because, You will have to have discipline to segregate expenses you wouldn't have had if you'd have rented to accounts specific for your home. You will need to account for equivalent rent received that you didn't have to pay because you lived in the home. If you still own the home, you will have to simulate a substantial agent fee and other closing costs (by inserting a transaction). Most places have a large tax break when selling a property (sometimes the full capital appreciation), so that really needs to be factored in. It's not always simple to calculate, especially if you rented your property for some of its ownership lifetime, in which case you might only be able to deduct a part of the capital gains. For some, there is great value in the optionality of being able to move easily and at low cost (e..g, accepting a new job at the other side of the country) and that personal value will be difficult to estimate. This will require me to do more categorization work, and it will be best documented as a separate project, though using the same code. I did manage to create a simple configuration and extract a 5%-ish number out of it, but I think I'll need a bit more work to declare victory. More to come.","title":"Real Estate"},{"location":"calculating_portolio_returns.html#options","text":"I sometimes hedge some of my portfolio downrisk with long-dated OTM PUT positions. I've sold OTM PUTs and CALLs in the near term to finance it. The method I described for stocks in this document works equally well for options, that is, extracting cash flows from cash. The main differences are that: Currency names. Instrument names are specific to each contract\u2014they include the expiration date and strike price\u2014so I don't store the options to an account whose name includes the instrument name at the leaf. I just use a generic leaf account name, such as \"Options\" or \"Hedging\" which I select when I enter/exit positions. Prices for options. Prices for options aren't as easily fetchable programmatically. I'm having to use a private API for this. Perhaps more importantly, declining theta, varying vol and non-linearity near the strike means I do need to have to have pretty recent price estimates in my ledger in order to compute returns on unrealized gains/losses. I think the effect is strong enough that I'd want to eventually have some code to always update the prices just before generating the reports.","title":"Options"},{"location":"calculating_portolio_returns.html#future-work","text":"This section describes desired future improvements on the returns code, which I'm likely to implement, and the corresponding challenges. Note: As I'm writing this in September 2020, I'm actively working on this code and probably will continue over the next few months' weekends. It's possible this document falls slightly out-of-date and that some of the proposals below become implemented. Refer to the source code for the latest.","title":"Future Work"},{"location":"calculating_portolio_returns.html#relative-size-over-time","text":"Another useful bit of data I could easily add to reports is a stack plot of market values over time, as well as relative fractions of each investment in a report group. This would be useful for monitoring growth of particular instruments in a group to help with rebalancing.","title":"Relative Size over Time"},{"location":"calculating_portolio_returns.html#comparison-against-a-benchmark","text":"One of the important barometers of success in running your own portfolio is a comparison to an equivalent investment of capital in a simple portfolio of diversified stocks and bonds rebalanced regularly. After all, if my returns aren't beating that, in a normal environment, one could make the argument I shouldn't bother with a more complicated allocation. Assuming that my access to capital is the same (which it is, because in this project I'm looking at savings from income), I could simply replace my cash flows by cash flows to this simulated portfolio, in other words, use the same timings to simulate buying other assets. I would have to exclude the dividend payments because those are specific to the particular instruments I used, and at the same time generate simulated dividends from similarly sized positions in the benchmark portfolio. It should be possible to do this without modifying my ledger. One issue is that I will require a set of accurate prices for the benchmark, at the dates of my historical cash flows. Like my script already does for aging price points, this could easily be stored in a file for fetching. Perhaps more complicated is the fact that Beancount does not currently support a database of per-share dividend distributions. This could be added without introducing new syntax by attaching and honoring metadata on Price directives, such as 2020-09-01 price LQD 136.16 USD distribution: 0.295 USD Overall, it wouldn't be too difficult to implement this.","title":"Comparison Against a Benchmark"},{"location":"calculating_portolio_returns.html#including-uninvested-cash","text":"One of the problems I'm facing in investing my own portfolio is the lack of discipline around timely investing of available cash, especially when times are uncertain and difficult decisions have to be made. In order to account for the drag on performance from that, I should include an \"investment\" that's purely reflecting the total amount of uninvested cash over time. Because this varies a lot, a good approximation can be obtained by sampling total amount of investable cash every month and synthesizing cash flows from differences. If that cash flow series gets included as part of a portfolio, it'll suitably drag its returns by diluting them.","title":"Including Uninvested Cash"},{"location":"calculating_portolio_returns.html#after-tax-value","text":"At the moment I export all my holdings to a Google Sheets doc using the export script , and from that, break it down between long-term and short-term positions vs. pre-tax, after-tax, Roth, and taxable buckets. From those 8 aggregates, I remove estimated taxes and report a single \"estimated after-tax net worth\" and corresponding tax liability. This is a rough estimate. The returns report is however much more detailed, and I could simulate tax payments not only on liquidation, but also at the end of each year (from dividends and sales). I have all the lot dates on each position held so I can automatically figure out short-term vs. long-term lots.","title":"After-Tax Value"},{"location":"calculating_portolio_returns.html#inflation-adjustments","text":"The market value amounts reported on the returns charts and calendar returns do not account for inflation. Over long periods of time, those can make an important difference in returns. It would be useful to discount the returns over time using annual estimates for the CPI (or some other estimate of inflation; you could even make up your own, from the expenses in your ledger), so that we can look at a curve of real growth and not just nominal growth.","title":"Inflation Adjustments"},{"location":"calculating_portolio_returns.html#sales-commission","text":"The configuration could easily be improved to let the user specify expected commissions on sales of investments, either in absolute or relative (%) amounts. This would be used to mark positions with more realistic liquidation values. This could make a difference on investments with either small amounts or large commissions (i.e., working through a regular broker, or on real estate).","title":"Sales Commission"},{"location":"calculating_portolio_returns.html#risk-estimation-beta","text":"A perhaps more advanced topic would be to compute an estimate of the variance from the specific portfolio composition in order to calculate and report some measurement of risk, such as Sharpe Ratio . This would require a sufficient number of regularly spaced price points. Variation of the measures over time could be fun too, as well as calculating the current portfolio's specific sensitivity to market as a whole (beta).","title":"Risk Estimation & Beta"},{"location":"calculating_portolio_returns.html#conclusion","text":"I had expected it would be possible to produce a clear picture of returns from Beancount data, and having done it I'm more satisfied with the level of detail and clarity I was to produce from my ledger than I had expected. This goes well beyond plotting of net worth over time, this actually works really well, and I can use it to compare the performance of different investments fairly. I hope at least some Beancount users will be able to run it on their ledgers and I'm looking forward to hearing some feedback from those who set it up. Perhaps most importantly, I was very surprised to see results on my own portfolio returns. I'm one of those people who would normally shrug and guess an underwhelming ballpark number if asked what I thought my returns were, such as \"meh, 6% or something, not super happy.\" Doing this work was originally driven by not having the right answer to this question. It turns out, over the last 15 years, that I've generated an almost 12% average annual return, and 14% annual return in the last 5 years. I haven't yet made the benchmark comparison, and for sure these numbers should be put side-by-side with the market for a fair assessment. Nevertheless, going through the exercise has provided me with renewed confidence and hope about the future, and I hope the clarity it will bring to other Beancount users' own investments will be similarly enlightening.","title":"Conclusion"},{"location":"command_line_accounting_cookbook.html","text":"Command-line Accounting Cookbook \uf0c1 Martin Blais , July 2014 http://furius.ca/beancount/doc/cookbook Introduction A Note of Caution Account Naming Conventions Choosing an Account Type Choosing Opening Dates How to Deal with Cash Cash Withdrawals Tracking Cash Expenses Salary Income Employment Income Accounts Booking Salary Deposits Vacation Hours 401k Contributions Vesting Stock Grants Other Benefits Points Food Benefits Currency Transfers & Conversions Investing and Trading Accounts Setup Funds Transfers Making a Trade Receiving Dividends Conclusion Introduction \uf0c1 The best way to learn the double-entry method is to look at real-world examples. The method is elegant, but it can seem unintuitive to the newcomer how transactions have to be posted in order to perform the various operations that one needs to do in counting for different types financial events. This is why I wrote this cookbook. It is not meant to be a comprehensive description of all the features supported, but rather a set of practical guidelines to help you solve problems. I think this will likely be the most useful document in the Beancount documentation set! All the examples here apply to any double-entry accounting system: Ledger, GnuCash, or even commercial systems. Some of the details may differ only slightly. This cookbook is written using the syntax and calculation method of the Beancount software. This document also assumes that you are already familiar with the general balancing concepts of the double-entry method and with at least some of the syntax of Beancount which is available from its user\u2019s manual or its cheat sheet . If you haven\u2019t begun writing down your first file, you will want to read Getting Started with Beancount and do that first. Command-line accounting systems are agnostic about the types of things they can count and allow you to get creative with the kinds of units that you can invent to track various kinds of things. For instance, you can count \u201cIRA contribution dollars,\u201d which are not real dollars, but which correspond to \u201cpossible contributions in real dollars,\u201d and you obtain accounts of assets, income and expenses types for them - it works. Please do realize that some of those clever tricks may not be possible in more traditional accounting systems. In addition, some of the operations that would normally require a manual process in these systems can be automated away for us, e.g., \u201cclosing a year\u201d is entirely done by the software at any point in time, and balance assertions provide a safeguard that allow us to change the details of past transactions with little risk, so there is no need to \u201creconcile\u201d by baking the past into a frozen state. More flexibility is at hand. Finally, if you have a transaction entry problem that is not covered in this document, please do leave a comment in the margin, or write up your problem to the Ledger mailing-list . I would like for this document to cover as many realistic scenarios as possible. A Note of Caution \uf0c1 While reading this, please take note that the author is a dilettante: I am a computer scientist, not an accountant. In fact, apart from a general course I took in college and having completed the first year of a CFA program, I have no real training in accounting. Despite this, I do have some practical experience in maintaining three set of books using this software: my personal ledger (8 years worth of full financial data for all accounts), a joint ledger with my spouse, and the books of a contracting and consulting company I used to own. I also used my double-entry system to communicate with my accountant for many years and he made suggestions. Nevertheless\u2026 I may be making fundamental mistakes here and there, and I would appreciate you leaving a comment in the margin if you find anything dubious. Account Naming Conventions \uf0c1 You can define any account name you like, as long as it begins with one of the five categories: Assets, Liabilities, Income, Expenses, or Equity (note that you can customize those names with options - see the Language Syntax document for details). The accounts names are generally defined to have multiple name components, separated by a colon (:), which imply an accounts hierarchy, or \u201c chart of accounts \u201d: Assets:Component1:Component2:Component3:... Over time, I\u2019ve iterated over many ways of defining my account names and I have converged to the following convention for Assets, Liabilities, and Income accounts: Type : Country : Institution : Account : SubAccount What I like about this is that when you render a balance sheet, the tree that gets rendered nicely displays accounts by country first, then by institution. Some example account names: Assets:US:BofA:Savings ; Bank of America \u201cSavings\u201d account Assets:CA:RBC:Checking ; Royal Bank of Canada \u201cChecking\u201d account Liabilities:US:Amex:Platinum ; American Express Platinum credit card Liabilities:CA:RBC:Mortgage ; Mortgage loan account at RBC Income:US:ETrade:Interest ; Interest payments in E*Trade account Income:US:Acme:Salary ; Salary income from ACME corp. Sometimes I use a further sub-account or two, when it makes sense. For example, Vanguard internally keeps separate accounts depending on whether the contributions were from the employee or the employer\u2019s matching amount: Assets:US:Vanguard:Contrib401k:RGAGX ; My contributions to this fund Assets:US:Vanguard:Match401k:RGAGX ; Employer contributions For investment accounts, I tend organize all their contents by storing each particular type of stock in its own sub-account: Assets:US:ETrade:Cash ; The cash contents of the account Assets:US:ETrade:FB ; Shares of Facebook Assets:US:ETrade:AAPL ; Shares of Apple Assets:US:ETrade:MSFT ; Shares of Microsoft \u2026 This automatically organizes the balance sheet by types of shares, which I find really nice. Another convention that I like is to use the same institution component name when I have different related types of accounts. For instance, the E*Trade assets account above has associated income streams that would be booked under similarly named accounts: Income:US:ETrade:Interest ; Interest income from cash deposits Income:US:ETrade:Dividends ; Dividends received in this account Income:US:ETrade:PnL ; Capital gains or losses from trades \u2026 For \u201cExpenses\u201d accounts, I find that there are generally no relevant institutions. For those it makes more sense to treat them as categories and just have a simple hierarchy that corresponds to the kinds of expenses they count, some examples: Expenses:Sports:Scuba ; All matters of diving expenses Expenses:Transport:Train ; Train (mostly Amtrak, but not always) Expenses:Transport:Bus ; Various \u201cchinese bus\u201d companies Expenses:Transport:Flights ; Flights (various airlines) \u2026 I have a lot of these, like 250 or more. It is really up to you to decide how many to define and how finely to aggregate or \u201ccategorize\u201d your expenses this way. But of course, you should only define them as you need them; don\u2019t bother defining a huge list ahead of time. It\u2019s always easy to add new ones. It is worth noting that the institution does not have to be a \u201creal\u201d institution. For instance, I owned a condo unit in a building, and I used the Loft4530 \u201cinstitution\u201d for all its related accounts: Assets:CA:Loft4530:Property Assets:CA:Loft4530:Association Income:CA:Loft4530:Rental Expenses:Loft4530:Acquisition:Legal Expenses:Loft4530:Acquisition:SaleAgent Expenses:Loft4530:Loan-Interest Expenses:Loft4530:Electricity Expenses:Loft4530:Insurance Expenses:Loft4530:Taxes:Municipal Expenses:Loft4530:Taxes:School If you have all of your business in a single country and have no plans to move to another, you might want to skip the country component for brevity. Finally, for \u201cEquity\u201d accounts, well, \u2026. normally you don\u2019t end up defining many of them, because these are mostly created to report the net income and currency conversions from previous years or the current exercise period on the balance sheet. Typically you will need at least one, and it doesn\u2019t matter much what you name it: Equity:Opening-Balances ; Balances used to open accounts You can customize the name of the other Equity accounts that get automatically created for the balance sheet. Choosing an Account Type \uf0c1 Part of the art of learning what accounts to book transactions to is to come up with relevant account names and design a scheme for how money will flow between those accounts, by jotting down some example transactions. It can get a bit creative. As you\u2019re working out how to \u201ccount\u201d all the financial events in your life, you will often end up wondering what account type to select for some of the accounts. Should this be an \u201cAssets\u201d account? Or an \u201cIncome\u201d account? After all, other than for creating reports, Beancount doesn\u2019t treat any of these account types differently\u2026 But this does not mean that you can just use any type willy nilly. Whether an account appears in the balance sheet or income statement does matter\u2014there is usually a correct choice. When in doubt, here are some guidelines to choose an account type. First, if the amounts to be posted to the account are only relevant to be reported for a period of time , they should be one of the income statement accounts: Income or Expenses. On the other hand, if the amount always needs to be included in the total balance of an account, then it should be a balance sheet account: Assets or Liabilities. Second, if the amounts are generally 1 positive, or \u201cgood from your perspective,\u201d the account should be either an Assets or an Expenses account. If the amounts are generally negative, or \u201cbad from your perspective,\u201d the account should be either a Liabilities or an Income account. Based on these two indicators, you should be able to figure out any case. Let\u2019s work through some examples: A restaurant meal represents something that you obtained in exchange for some assets (cash) or a liability (paid by credit card). Nobody ever cares what the \u201csum total of all food since you were born\u201d amounts to. Only the transitional value matters: \u201cHow much did I spend in restaurants this month ?\u201d Or, since the beginning of the year ? Or, during this trip? This clearly points to an Expenses account. But you might wonder\u2026 this is a positive number, but it is money I spent? Yes, the account that you spent from was subtracted from (debited) in exchange for the expense you received . Think of the numbers in the expenses account as things you received that vanish into the ether right after you receive them. These meals are consumed.. and then they go somewhere. Okay, we\u2019ll stop the analogy here. You own some shares of a bond, and receive an interest payment. This interest is cash deposited in an Assets account, for example, a trading account. What is the other leg to be booked to? Choosing Opening Dates \uf0c1 Some of the accounts you need to define don\u2019t correspond to real world accounts. The Expenses:Groceries account represents the sum total of grocery expenses since you started counting. Personally, I like to use my birth date on those. There\u2019s a rationale to it: it sums all the groceries you\u2019ve ever spent money on, and this started only when you came to this world. You can use this rationale on other accounts. For example, all the income accounts associated with an employer should probably be opened at the date you began the job, and end on the date you left. Makes sense. How to Deal with Cash \uf0c1 Let\u2019s start with cash. I typically define two accounts at my birth date: 1973-04-27 open Assets:Cash 1973-04-27 open Assets:ForeignCash The first account is for active use, this represents my wallet, and usually contains only units of my operating currencies, that is, the commodities I usually think of as \u201ccash.\u201d For me, they are USD and CAD commodities. The second account is meant to hold all the paper bills that I keep stashed in a pocket from trips around the world, so they\u2019re out of the way in some other account and I don\u2019t see them in my cash balance. I transfer those to the main account when I do travel to such places, e.g., if I return to Japan, I\u2019ll move my JPY from Assets:ForeignCash to Assets:Cash right before the trip and use them during that time. Cash Withdrawals \uf0c1 An ATM withdrawal from a checking account to cash will typically look like this: 2014-06-28 * \"DDA WITHDRAW 0609C\" Assets:CA:BofA:Checking -700.00 USD Assets:Cash You would see this transaction be imported in your checking account transactions download. Tracking Cash Expenses \uf0c1 One mistake people make when you tell them you\u2019re tracking all of your financial accounts is to assume that you have to book every single little irrelevant cash transaction to a notebook. Not so! It is your choice to decide how many of these cash transactions to take down (or not). Personally, I try to minimize the amount of manual effort I put into updating my Ledger. My rule for dealing with cash is this: If it is for food or alcohol, I don\u2019t track it. If it is for something else, I keep the receipt and enter it later. This works for me, because the great majority of my cash expenses tend to be food (or maybe I just make it that way by paying for everything else with credit cards). Only a few receipts pile up somewhere on my desk for a couple of months before I bother to type them in. However, you will need to make occasional adjustments to your cash account to account for these expenses. I don\u2019t actually bother doing this very often\u2026 maybe once every three months, when I feel like it. The method I use is to take a snapshot of my wallet (manually, by counting the bills) and enter a corresponding balance assertion: 2014-05-12 balance Assets:Cash 234.13 USD Every time I do this I\u2019ll also add a cash distribution adjusted to balance the account: 2014-06-19 * \"Cash distribution\" Expenses:Food:Restaurant 402.30 USD Expenses:Food:Alcohol 100.00 USD Assets:Cash ; -502.30 USD 2014-06-20 balance Assets:Cash 194.34 USD If you wonder why the amounts in the cash account don\u2019t add up (234.13 -502.30 \u2260 194.34), it is because between the two assertions I added to the cash account by doing some ATM withdrawals against the checking account, and those appear somewhere else (in the checking account section). The withdrawal increased the balance of the cash account. It would appear if I rendered a journal for Assets:Cash . I could have made my life simpler and used a Pad directive if I had booked everything to food\u2014pad entries don\u2019t work solely at the beginning of an account\u2019s history, but also between any two balance assertions on the same account\u2014but I want to book 80% of it to food and 20% alcohol, to more accurately represent my real usage of cash 2 . Finally, if you end up with a long time period between the times that you do this, you may want to \u201cspread out\u201d your expenses by adding more than one cash distribution 3 manually, so that if you generate a monthly report, a large cash expense does not appear as a single lump in or outside that month. Salary Income \uf0c1 Accounting for your salary is rewarding: you will be able to obtain a summary of income earned during the year as well as the detail of where the various deductions are going, and you will enjoy the satisfaction of seeing matching numbers from your Beancount reports when you receive your W-2 form from your employer (or on your T4 if you\u2019re located in Canada). I put all entries related to an employer in their own dedicated section. I start it by setting an event to the date I began working there, for example, using the hypothetical company \u201cHooli\u201d (from the Silicon Valley show): 2012-12-13 event \"employer\" \"Hooli Inc.\" This allows me to automatically calculate the number of days I\u2019ve been working there. When I leave a job, I\u2019ll change it to the new one, or an empty string, if I don\u2019t leave for another job: 2019-03-02 event \"employer\" \"\" This section will make several assumptions. The goal is to expose you to the various ideas you can use to account for your income correctly. You will almost certainly end up having to adapt these ideas to your specific situation. Employment Income Accounts \uf0c1 Then you define accounts for your pay stubs. You need to make sure that you have an account corresponding to each line of your pay stub. For example, here are some of the income accounts I define for this employment income at Hooli Inc.: 2012-12-13 open Income:US:Hooli:Salary USD ; \"Regular Pay\" 2012-12-13 open Income:US:Hooli:ReferralBonus USD ; \"Referral bonuses\" 2012-12-13 open Income:US:Hooli:AnnualBonus USD ; \"Annual bonus\" 2012-12-13 open Income:US:Hooli:Match401k USD ; \"Employer 401k match\" 2012-12-13 open Income:US:Hooli:GroupTermLife USD ; \"Group Term Life\" These correspond to regular salary pay, bonuses received for employee referrals, annual bonus, receipts for 401k (Hooli in this example will match some percentage of your contributions to your retirement account), receipts for life insurance (it appears both as an income and an expense), and benefits paid for your gym subscription. There are more, but this is a good example. (In this example I wrote down the names used in the stub as a comment, but you could insert them as metadata instead if you prefer.) You will need to book taxes withheld at the source to accounts for that year (see the tax section for details on this): 2014-01-01 open Expenses:Taxes:TY2014:US:Medicare USD 2014-01-01 open Expenses:Taxes:TY2014:US:Federal USD 2014-01-01 open Expenses:Taxes:TY2014:US:StateNY USD 2014-01-01 open Expenses:Taxes:TY2014:US:CityNYC USD 2014-01-01 open Expenses:Taxes:TY2014:US:SDI USD 2014-01-01 open Expenses:Taxes:TY2014:US:SocSec USD These accounts are for Medicare taxes, Federal, New York State and NYC taxes (yes, New York City residents have an additional tax on top of state tax), state disability insurance (SDI) payments, and finally, taxes to pay for social security. You will also need to have some accounts defined elsewhere for the various expenses that are paid automatically from your pay: 2012-12-13 open Expenses:Health:Life:GroupTermLife USD ; \"Life Ins.\" 2012-12-13 open Expenses:Health:Dental:Insurance USD ; \"Dental\" 2012-12-13 open Expenses:Health:Medical:Insurance USD ; \"Medical\" 2012-12-13 open Expenses:Health:Vision:Insurance USD ; \"Vision\" 2012-12-13 open Expenses:Internet:Reimbursement USD ; \"Internet Reim\" 2012-12-13 open Expenses:Transportation:PreTax USD ; \"Transit PreTax\" These correspond to typical company group plan life insurance payments, premiums for dental, medical and vision insurances, reimbursements for home internet usage, and pre-tax payments for public transit (the city of New York allows you to pay for your MetroCard with pre-tax money through your employer). Booking Salary Deposits \uf0c1 Then, when I import details for a payment via direct deposit to my checking account, it will look like this: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD If I haven\u2019t received my pay stub yet, I might book it temporarily to the salary account until I do: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD ! Income:US:Hooli:Salary When I receive or fetch my pay stub, I remove this and complete the rest of the postings. A realistic entry for a gross salary of $140,000 would look something like this: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD Income:US:Hooli:GroupTermLife -25.38 USD Income:US:Hooli:Salary -5384.62 USD Expenses:Health:Dental:Insurance 2.88 USD Expenses:Health:Life:GroupTermLife 25.38 USD Expenses:Internet:Reimbursement -34.65 USD Expenses:Health:Medical:Insurance 36.33 USD Expenses:Transportation:PreTax 56.00 USD Expenses:Health:Vision:Insurance 0.69 USD Expenses:Taxes:TY2014:US:Medicare 78.08 USD Expenses:Taxes:TY2014:US:Federal 1135.91 USD Expenses:Taxes:TY2014:US:CityNYC 75.03 USD Expenses:Taxes:TY2014:US:SDI 1.20 USD Expenses:Taxes:TY2014:US:StateNY 340.06 USD Expenses:Taxes:TY2014:US:SocSec 328.42 USD It\u2019s quite unusual for a salary payment to have no variation at all from its previous one: rounding up or down from the payroll processor will often result in a difference of a penny, social security payments will cap to their maximum, and there are various deductions that will occur from time to time, e.g., deductions on taxable benefits received. Moreover, contributions to a 401k will affect that amounts of taxes withheld at the source. Therefore, you end up having to look at each pay stub individually to enter its information correctly. But this is not as time-consuming as it sounds! Here\u2019s a trick: it\u2019s a lot easier to update your transactions if you list your postings in the same order as they appear on your pay stub. You just copy-paste the previous entry, read the pay stub from top to bottom and adjust the numbers accordingly. It takes a minute for each. It\u2019s worth noting some unusual things about the previous entry. The \u201cgroup term life\u201d entry has both a $25.38 income leg and an expense one. This is because Hooli pays for the premium (it reads exactly like that on the stubs.) Hooli also reimburses some of home internet, because I use it to deal with production issues. This appears as a negative posting to reduce the amount of my expense Expenses:Internet account. Vacation Hours \uf0c1 Our pay stubs also include accrued vacation and the total vacation balance, in vacation hours. You can also track these amounts on the same transactions. You need to declare corresponding accounts: 2012-12-13 open Income:US:Hooli:Vacation VACHR 2012-12-13 open Assets:US:Hooli:Vacation VACHR 2012-12-13 open Expenses:Vacation:Hooli VACHR Vacation that accrues is something you receive and thus is treated as Income in units of \u201cVACHR\u201d, and accumulates in an Assets account, which holds how many of these hours you currently have available to \u201cspend\u201d as time off. Updating the previous salary income transaction entry: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD \u2026 Assets:US:Hooli:Vacation 4.62 VACHR Income:US:Hooli:Vacation -4.62 VACHR 4.62 VACHR on a bi-weekly paycheck 26 times per year is 26 x 4.62 ~= 120 hours. At 8 hours per day, that is 15 work days, or 3 weeks, which is a standard vacation package for new US Hooligans in this example. When you do take time off, you book an expense against your accumulated vacation time: 2014-06-17 * \"Going to the beach today\" Assets:US:Hooli:Vacation -8 VACHR Expenses:Vacation:Hooli The Expenses account tracks how much vacation you\u2019ve used. From time to time you can check that the balance reported on your pay stub\u2014the amount of vacation left that your employer thinks you have\u2014is the same as that which you have accounted for: 2014-02-29 balance Assets:US:Hooli:Vacation 112.3400 VACHR You can \u201cprice\u201d your vacation hour units to your hourly rate, so that your vacations Assets account shows how much the company would pay you if you decided to quit. Assuming that $140,000/year salary, 40 hour weeks and 50 weeks of work, which is 2000 hours per year, we obtain a rate of $70/hour, which you enter like this: 2012-12-13 price VACHR 70.00 USD Similarly, if your vacation hours expires or caps, you can calculate how much dollar-equivalent you\u2019re forfeiting by working too much and giving up your vacation time. You would write off some of the VACHR from your Assets account into an income account (representing losses). 401k Contributions \uf0c1 The 401k plan allows you to make contributions to a tax-deferred retirement account using pre-tax dollars. This is carried out via withholdings from your pay. To account for those, you simply include a posting with the corresponding contribution towards your retirement account: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD \u2026 Assets:US:Vanguard:Cash 1000.00 USD \u2026 If you\u2019re accounting for your available contributions (see the tax section of this document), you will want to reduce your \u201c401k contribution\u201d Assets account at the same time. You would add two more postings to the transaction: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD \u2026 Assets:US:Vanguard:Cash 1000.00 USD Assets:US:Federal:PreTax401k -1000.00 US401K Expenses:Taxes:TY2014:US:Federal:PreTax401k 1000.00 US401K \u2026 If your employer matches your contributions, this may not show on your pay stubs. Because these contributions are not taxable\u2014they are deposited directly to a tax-deferred account\u2014your employer does not have to include them in the withholding statement. You will see them appear directly in your investment account as deposits. You can book them like this to the retirement account\u2019s tax balance: 2013-03-16 * \"BUYMF - MATCH\" Income:US:Hooli:Match401k -1173.08 USD Assets:US:Vanguard:Cash 1173.08 USD And then insert a second transaction when you invest this case, or directly purchasing assets from the contribution if you have specified an asset allocation and this is automated by the broker: 2013-03-16 * \"BUYMF - MATCH\" Income:US:Hooli:Match401k -1173.08 USD Assets:US:Vanguard:VMBPX 106.741 VMBPX {10.99 USD} Note that the fund that manages your 401k accounts may be tracking your contributions and your employer\u2019s contributions in separate buckets. You would declare sub-accounts for this and make the corresponding changes: 2012-12-13 open Assets:US:Vanguard:PreTax401k:VMBPX VMBPX 2012-12-13 open Assets:US:Vanguard:Match401k:VMBPX VMBPX It is common for them to do this in order to track each source of contribution separately, because there are several constraints on rollovers to other accounts that depend on it. Vesting Stock Grants \uf0c1 See the dedicated document on this topic for more details. Other Benefits \uf0c1 You can go crazy with tracking benefits if you want. Here are a few wild ideas. Points \uf0c1 If your employer offers a sponsored massage therapy program on-site, you could presumably book a massage out of your paycheck or even from some internal website (if the company is modern), and you could pay for them using some sort of internal points system, say, \u201cHooli points\u201d. You could track those using a made-up currency, e.g., \u201cMASSA\u2019s\u201d and which could be priced at 0.50 USD, the price at which you could purchase them: 2012-12-13 open Assets:US:Hooli:Massage MASSA 2012-12-13 price MASSA 0.50 USD When I purchase new massage points, I 2013-03-15 * \"Buying points for future massages\" Liabilities:US:BofA:CreditCard -45.00 USD Assets:US:Hooli:Massage 90 MASSA {0.50 USD} If you\u2019re occasionally awarded some of these points, and you can track that in an Income account. Food Benefits \uf0c1 Like many of the larger technology companies, Hooli presumably provides free food for its employees. This saves time and encourages people to eat healthy. This is a bit of a trend in the tech world right now. This benefit does not show up anywhere, but if you want to price it as part of your compensation package, you can track it using an Income account: 2012-12-13 open Income:US:Hooli:Food Depending on how often you end up eating at work, you could guesstimate some monthly allowance per month: 2013-06-30 * \"Distribution for food eaten at Hooli\" Income:US:Hooli:Food -350 USD Expenses:Food:Restaurant Currency Transfers & Conversions \uf0c1 If you convert between currencies, such as when performing an international transfer between banks, you need to provide the exchange rate to Beancount. It looks like this: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9000.00 CHF Assets:US:BofA:Checking 10000.00 USD @ 0.90 CHF The balance amount of the second posting is calculated as 10,000.00 USD x 0.90 CHF/USD = 9,000 CHF, and the transaction balances. Depending on your preference, you could have placed the rate on the other posting, like this: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9000.00 CHF @ 1.11111 USD Assets:US:BofA:Checking 10000.00 USD The balance amount of the first posting is calculated as -9000.00 CHF x 1.11111 USD/CHF = 10000.00 USD 4 . Typically I will choose the rate that was reported to me and put it on the corresponding side. You may also want to use the direction that F/X markets use for trading the rate, for example, the Swiss franc trades as USD/CHF, so I would prefer the first transaction. The price database converts the rates in both directions, so it is not that important 5 . If you use wire transfers, which is typical for this type of money transfer, you might incur a fee: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9025.00 CHF Assets:US:BofA:Checking 10000.00 USD @ 0.90 CHF Expenses:Fees:Wires 25.00 CHF If you convert cash at one of these shady-looking currency exchange parlors found in tourist locations, it might look like this: 2014-03-03 * \"Changed some cash at the airport in Lausanne\" Assets:Cash -400.00 USD @ 0.90 CHF Assets:Cash 355.00 CHF Expenses:Fees:Services 5.00 CHF In any case, you should never convert currency units using the cost basis syntax, because the original conversion rate needs to be forgotten after depositing the units, and not kept around attached to simple currency. For example, this would be incorrect usage: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9000.00 CHF Assets:US:BofA:Checking 10000.00 USD {0.90 CHF} ; <-bad! If you did that by mistake, you would incur errors when you attempted to use the newly USD deposited: Beancount would require that you specify the cost of these \u201cUSD\u201d in CHF, e.g., \u201cdebit from my USD that I changed at 0.90 USD/CHF\u201d. Nobody does this in the real world, and neither should you when you represent your transactions: once the money has converted, it\u2019s just money in a different currency, with no associated cost. Finally, a rather subtle problem is that using these price conversions back and forth at different rates over time breaks the accounting equation to some extent: changes in exchange rate may create small amounts of money out of thin air and all the balances don\u2019t end up summing up to zero. However, this is not a problem, because Beancount implements an elegant solution to automatically correct for this problem, so you can use these conversions freely without having to worry about this: it inserts a special conversions entry on the balance sheet to invert the cumulative effect of conversions for the report and obtain a clean balance of zero. (A discussion of the conversions problem is beyond the scope of this cookbook; please refer to Solving the Conversions Problem if you\u2019d like to know more.) Investing and Trading \uf0c1 Tracking trades and associated gains is a fairly involved topic. You will find a more complete introduction to profit and loss and a detailed discussion of various scenarios in the Trading with Beancount document, which is dedicated to this topic. Here we will discuss how to setup your account and provide simple example transactions to get you started. Accounts Setup \uf0c1 You should create an account prefix to root various sub-accounts associated with your investment account. Say you have an account at ETrade, this could be \u201c Assets:US:ETrade \u201d. Choose an appropriate institution name. Your investment account will have a cash component. You should create a dedicated sub-account will represent uninvested cash deposits: 2013-02-01 open Assets:US:ETrade:Cash USD I recommend that you further define a sub-account for each of the commodity types that you will invest in. Although this is not strictly necessary\u2014Beancount accounts may contain any number of commodities\u2014it is a nice way to aggregate all the positions in that commodity together for reporting. Say you will buy shares of LQD and BND, two popular bond ETFs: 2013-02-01 open Assets:US:ETrade:LQD LQD 2013-02-01 open Assets:US:ETrade:BND BND This also helps produce nicer reports: balances are often shown at cost and it\u2019s nice to see the total cost aggregated by commodity for various reasons (i.e., each commodity provides exposure to different market characteristics). Using a dedicated sub-account for each commodity held within an institution is a good way to do that. Unless you have specific reasons not to do so, I highly suggest sticking with this by default (you can always change it later by renaming accounts). Specifying commodity constraints on your accounts will help you detect data entry mistakes. Stock trades tend to be a bit more involved than regular transactions, and this is certainly going to be helpful. Then, you will hopefully receive income in this account, in two forms: capital gains, or \u201cP&L\u201d, and dividends. I like to account for these by institution, because this is how they have to be declared for taxes. You may also receive interest income. Define these: 2013-02-01 open Income:US:ETrade:PnL USD 2013-02-01 open Income:US:ETrade:Dividends USD 2013-02-01 open Income:US:ETrade:Interest USD Finally, to account for transaction fees and commissions, you will need some general accounts to receive these: 1973-04-27 open Expenses:Financial:Fees 1973-04-27 open Expenses:Financial:Commissions Funds Transfers \uf0c1 You will normally add some initial money in this account by making a transfer from an external account, say, a checking account: 2014-02-04 * \"Transferring money for investing\" Assets:US:BofA:Checking -2000.00 USD Assets:US:ETrade:Cash 2000.00 USD Making a Trade \uf0c1 Buying stock should have a posting that deposits the new commodity in the commodity\u2019s sub-account, and debits the cash account to the corresponding amounts plus commissions: 2014-02-16 * \"Buying some LQD\" Assets:US:ETrade:LQD 10 LQD {119.24 USD} Assets:US:ETrade:Cash -1199.35 USD Expenses:Financial:Commissions 6.95 USD Note that when you\u2019re buying units of a commodity, you are establishing a new trade lot in the account\u2019s inventory and it is necessary that you provide the cost of each unit (in this example, 119.24 USD per share of LQD). This allows us to account for capital gains correctly. Selling some of the same stock work similarly, except that an extra posting is added to absorb the capital gain or loss: 2014-02-16 * \"Selling some LQD\" Assets:US:ETrade:LQD -5 LQD {119.24 USD} @ 123.40 USD Assets:US:ETrade:Cash 610.05 USD Expenses:Financial:Commissions 6.95 USD Income:US:Etrade:PnL Note that the postings of shares removed from the Assets:US:ETrade:LQD account is a lot reduction and you must provide information to identify which lot you\u2019re reducing, in this case, by providing the per-share cost basis of 119.24 USD. I normally let Beancount calculate the capital gain or loss for me, which is why I don\u2019t specify it in the last posting. Beancount will automatically balance the transaction by setting the amount of this posting to -20.80 USD, which is a gain of 20.80 USD (remember that the signs are inverted for income accounts). Specifying the sale price of 123.40 USD is optional, and it is ignored for the purpose of balancing the transaction, the cash deposit and commissions legs determine the profit. Receiving Dividends \uf0c1 Receiving dividends takes on two forms. First, you can receive dividends in cash, which will go into the cash account: 2014-02-16 * \"Dividends from LQD\" Income:US:ETrade:Dividends -87.45 USD Assets:US:ETrade:Cash 87.45 USD Note that the source of the dividends isn\u2019t specified here. You could use a sub-account of the income account to count it separately. Or you can receive dividends in shares reinvested: 2014-06-27 * \"Dividends reinvested\" Assets:US:Vanguard:VBMPX 1.77400 VBMPX {10.83 USD} Income:US:Vanguard:Dividends -19.21 USD This is booked similarly to a stock purchase, and you also have to provide the cost basis of the received units. This would typically happen in a non-taxable retirement account. Refer to the Trading with Beancount document for a more thorough discussion and numerous and more complex examples. Choosing a Date \uf0c1 Buying or selling a single lot of stock typically involves multiple events over time: the trade is placed, the trade is filled (usually on the same day), the trade is settled. Settlement usually occurs 2 or 3 business days after the trade is filled. For simplicity, I recommend using the trade date as the date of your transaction. In the US, this is the date that is recognized for tax purposes, and settlement has no impact on your account (brokers typically won\u2019t allow you to trade without the corresponding cash or margin anyhow). So normally I don\u2019t bother creating separate entries for settlement, it\u2019s not very useful. More complex schemes can be envisioned, e.g. you could store the settlement date as a metadata field and then use it in scripts later on, but that\u2019s beyond the scope of this document. Conclusion \uf0c1 This document is incomplete. I have many more example use cases that I\u2019m planning to add here as I complete them. I will be announcing those on the mailing-list as they materialize. In particular, the following topics will be discussed: Health Care Expenses, e.g., insurance premiums and rebates Taxes IRAs, 401k and other tax-deferred accounts Real Estate Options This is not strictly always true: in accounting for companies, some account types are held at their opposite value for reasons, usually to offset the value of another account of the same type. These are called \u201ccontra\u201d accounts. But as an individual, you\u2019re quite unlikely to have one of those. If you\u2019re setting up a chart of accounts for a company, Beancount doesn\u2019t actually care whether the balance is of one sign or other. You declare contra-accounts just like regular accounts. \u21a9 I am considering supporting an extended version of the Pad directive that can take a percentage value and make it possible to pad only a percentage of the full amount, to automate this. \u21a9 Yet another extension to Beancount involves support multiple Pad directives between two balance assertions and automatically support this spreading out of padding directives. \u21a9 If you\u2019re concerned about the issue of precision or rounding in balancing, see this document . \u21a9 Note that if the price database needs to invert the date its calculation may result in a price with a large number of digits. Beancount uses IEEE decimal objects and the default context of the Python implementation is 28 digits, so inverting 0.9 will result in 1.111111\u2026.111 with 28 digits. \u21a9","title":"Command Line Accounting Cookbook"},{"location":"command_line_accounting_cookbook.html#command-line-accounting-cookbook","text":"Martin Blais , July 2014 http://furius.ca/beancount/doc/cookbook Introduction A Note of Caution Account Naming Conventions Choosing an Account Type Choosing Opening Dates How to Deal with Cash Cash Withdrawals Tracking Cash Expenses Salary Income Employment Income Accounts Booking Salary Deposits Vacation Hours 401k Contributions Vesting Stock Grants Other Benefits Points Food Benefits Currency Transfers & Conversions Investing and Trading Accounts Setup Funds Transfers Making a Trade Receiving Dividends Conclusion","title":"Command-line Accounting Cookbook"},{"location":"command_line_accounting_cookbook.html#introduction","text":"The best way to learn the double-entry method is to look at real-world examples. The method is elegant, but it can seem unintuitive to the newcomer how transactions have to be posted in order to perform the various operations that one needs to do in counting for different types financial events. This is why I wrote this cookbook. It is not meant to be a comprehensive description of all the features supported, but rather a set of practical guidelines to help you solve problems. I think this will likely be the most useful document in the Beancount documentation set! All the examples here apply to any double-entry accounting system: Ledger, GnuCash, or even commercial systems. Some of the details may differ only slightly. This cookbook is written using the syntax and calculation method of the Beancount software. This document also assumes that you are already familiar with the general balancing concepts of the double-entry method and with at least some of the syntax of Beancount which is available from its user\u2019s manual or its cheat sheet . If you haven\u2019t begun writing down your first file, you will want to read Getting Started with Beancount and do that first. Command-line accounting systems are agnostic about the types of things they can count and allow you to get creative with the kinds of units that you can invent to track various kinds of things. For instance, you can count \u201cIRA contribution dollars,\u201d which are not real dollars, but which correspond to \u201cpossible contributions in real dollars,\u201d and you obtain accounts of assets, income and expenses types for them - it works. Please do realize that some of those clever tricks may not be possible in more traditional accounting systems. In addition, some of the operations that would normally require a manual process in these systems can be automated away for us, e.g., \u201cclosing a year\u201d is entirely done by the software at any point in time, and balance assertions provide a safeguard that allow us to change the details of past transactions with little risk, so there is no need to \u201creconcile\u201d by baking the past into a frozen state. More flexibility is at hand. Finally, if you have a transaction entry problem that is not covered in this document, please do leave a comment in the margin, or write up your problem to the Ledger mailing-list . I would like for this document to cover as many realistic scenarios as possible.","title":"Introduction"},{"location":"command_line_accounting_cookbook.html#a-note-of-caution","text":"While reading this, please take note that the author is a dilettante: I am a computer scientist, not an accountant. In fact, apart from a general course I took in college and having completed the first year of a CFA program, I have no real training in accounting. Despite this, I do have some practical experience in maintaining three set of books using this software: my personal ledger (8 years worth of full financial data for all accounts), a joint ledger with my spouse, and the books of a contracting and consulting company I used to own. I also used my double-entry system to communicate with my accountant for many years and he made suggestions. Nevertheless\u2026 I may be making fundamental mistakes here and there, and I would appreciate you leaving a comment in the margin if you find anything dubious.","title":"A Note of Caution"},{"location":"command_line_accounting_cookbook.html#account-naming-conventions","text":"You can define any account name you like, as long as it begins with one of the five categories: Assets, Liabilities, Income, Expenses, or Equity (note that you can customize those names with options - see the Language Syntax document for details). The accounts names are generally defined to have multiple name components, separated by a colon (:), which imply an accounts hierarchy, or \u201c chart of accounts \u201d: Assets:Component1:Component2:Component3:... Over time, I\u2019ve iterated over many ways of defining my account names and I have converged to the following convention for Assets, Liabilities, and Income accounts: Type : Country : Institution : Account : SubAccount What I like about this is that when you render a balance sheet, the tree that gets rendered nicely displays accounts by country first, then by institution. Some example account names: Assets:US:BofA:Savings ; Bank of America \u201cSavings\u201d account Assets:CA:RBC:Checking ; Royal Bank of Canada \u201cChecking\u201d account Liabilities:US:Amex:Platinum ; American Express Platinum credit card Liabilities:CA:RBC:Mortgage ; Mortgage loan account at RBC Income:US:ETrade:Interest ; Interest payments in E*Trade account Income:US:Acme:Salary ; Salary income from ACME corp. Sometimes I use a further sub-account or two, when it makes sense. For example, Vanguard internally keeps separate accounts depending on whether the contributions were from the employee or the employer\u2019s matching amount: Assets:US:Vanguard:Contrib401k:RGAGX ; My contributions to this fund Assets:US:Vanguard:Match401k:RGAGX ; Employer contributions For investment accounts, I tend organize all their contents by storing each particular type of stock in its own sub-account: Assets:US:ETrade:Cash ; The cash contents of the account Assets:US:ETrade:FB ; Shares of Facebook Assets:US:ETrade:AAPL ; Shares of Apple Assets:US:ETrade:MSFT ; Shares of Microsoft \u2026 This automatically organizes the balance sheet by types of shares, which I find really nice. Another convention that I like is to use the same institution component name when I have different related types of accounts. For instance, the E*Trade assets account above has associated income streams that would be booked under similarly named accounts: Income:US:ETrade:Interest ; Interest income from cash deposits Income:US:ETrade:Dividends ; Dividends received in this account Income:US:ETrade:PnL ; Capital gains or losses from trades \u2026 For \u201cExpenses\u201d accounts, I find that there are generally no relevant institutions. For those it makes more sense to treat them as categories and just have a simple hierarchy that corresponds to the kinds of expenses they count, some examples: Expenses:Sports:Scuba ; All matters of diving expenses Expenses:Transport:Train ; Train (mostly Amtrak, but not always) Expenses:Transport:Bus ; Various \u201cchinese bus\u201d companies Expenses:Transport:Flights ; Flights (various airlines) \u2026 I have a lot of these, like 250 or more. It is really up to you to decide how many to define and how finely to aggregate or \u201ccategorize\u201d your expenses this way. But of course, you should only define them as you need them; don\u2019t bother defining a huge list ahead of time. It\u2019s always easy to add new ones. It is worth noting that the institution does not have to be a \u201creal\u201d institution. For instance, I owned a condo unit in a building, and I used the Loft4530 \u201cinstitution\u201d for all its related accounts: Assets:CA:Loft4530:Property Assets:CA:Loft4530:Association Income:CA:Loft4530:Rental Expenses:Loft4530:Acquisition:Legal Expenses:Loft4530:Acquisition:SaleAgent Expenses:Loft4530:Loan-Interest Expenses:Loft4530:Electricity Expenses:Loft4530:Insurance Expenses:Loft4530:Taxes:Municipal Expenses:Loft4530:Taxes:School If you have all of your business in a single country and have no plans to move to another, you might want to skip the country component for brevity. Finally, for \u201cEquity\u201d accounts, well, \u2026. normally you don\u2019t end up defining many of them, because these are mostly created to report the net income and currency conversions from previous years or the current exercise period on the balance sheet. Typically you will need at least one, and it doesn\u2019t matter much what you name it: Equity:Opening-Balances ; Balances used to open accounts You can customize the name of the other Equity accounts that get automatically created for the balance sheet.","title":"Account Naming Conventions"},{"location":"command_line_accounting_cookbook.html#choosing-an-account-type","text":"Part of the art of learning what accounts to book transactions to is to come up with relevant account names and design a scheme for how money will flow between those accounts, by jotting down some example transactions. It can get a bit creative. As you\u2019re working out how to \u201ccount\u201d all the financial events in your life, you will often end up wondering what account type to select for some of the accounts. Should this be an \u201cAssets\u201d account? Or an \u201cIncome\u201d account? After all, other than for creating reports, Beancount doesn\u2019t treat any of these account types differently\u2026 But this does not mean that you can just use any type willy nilly. Whether an account appears in the balance sheet or income statement does matter\u2014there is usually a correct choice. When in doubt, here are some guidelines to choose an account type. First, if the amounts to be posted to the account are only relevant to be reported for a period of time , they should be one of the income statement accounts: Income or Expenses. On the other hand, if the amount always needs to be included in the total balance of an account, then it should be a balance sheet account: Assets or Liabilities. Second, if the amounts are generally 1 positive, or \u201cgood from your perspective,\u201d the account should be either an Assets or an Expenses account. If the amounts are generally negative, or \u201cbad from your perspective,\u201d the account should be either a Liabilities or an Income account. Based on these two indicators, you should be able to figure out any case. Let\u2019s work through some examples: A restaurant meal represents something that you obtained in exchange for some assets (cash) or a liability (paid by credit card). Nobody ever cares what the \u201csum total of all food since you were born\u201d amounts to. Only the transitional value matters: \u201cHow much did I spend in restaurants this month ?\u201d Or, since the beginning of the year ? Or, during this trip? This clearly points to an Expenses account. But you might wonder\u2026 this is a positive number, but it is money I spent? Yes, the account that you spent from was subtracted from (debited) in exchange for the expense you received . Think of the numbers in the expenses account as things you received that vanish into the ether right after you receive them. These meals are consumed.. and then they go somewhere. Okay, we\u2019ll stop the analogy here. You own some shares of a bond, and receive an interest payment. This interest is cash deposited in an Assets account, for example, a trading account. What is the other leg to be booked to?","title":"Choosing an Account Type"},{"location":"command_line_accounting_cookbook.html#choosing-opening-dates","text":"Some of the accounts you need to define don\u2019t correspond to real world accounts. The Expenses:Groceries account represents the sum total of grocery expenses since you started counting. Personally, I like to use my birth date on those. There\u2019s a rationale to it: it sums all the groceries you\u2019ve ever spent money on, and this started only when you came to this world. You can use this rationale on other accounts. For example, all the income accounts associated with an employer should probably be opened at the date you began the job, and end on the date you left. Makes sense.","title":"Choosing Opening Dates"},{"location":"command_line_accounting_cookbook.html#how-to-deal-with-cash","text":"Let\u2019s start with cash. I typically define two accounts at my birth date: 1973-04-27 open Assets:Cash 1973-04-27 open Assets:ForeignCash The first account is for active use, this represents my wallet, and usually contains only units of my operating currencies, that is, the commodities I usually think of as \u201ccash.\u201d For me, they are USD and CAD commodities. The second account is meant to hold all the paper bills that I keep stashed in a pocket from trips around the world, so they\u2019re out of the way in some other account and I don\u2019t see them in my cash balance. I transfer those to the main account when I do travel to such places, e.g., if I return to Japan, I\u2019ll move my JPY from Assets:ForeignCash to Assets:Cash right before the trip and use them during that time.","title":"How to Deal with Cash"},{"location":"command_line_accounting_cookbook.html#cash-withdrawals","text":"An ATM withdrawal from a checking account to cash will typically look like this: 2014-06-28 * \"DDA WITHDRAW 0609C\" Assets:CA:BofA:Checking -700.00 USD Assets:Cash You would see this transaction be imported in your checking account transactions download.","title":"Cash Withdrawals"},{"location":"command_line_accounting_cookbook.html#tracking-cash-expenses","text":"One mistake people make when you tell them you\u2019re tracking all of your financial accounts is to assume that you have to book every single little irrelevant cash transaction to a notebook. Not so! It is your choice to decide how many of these cash transactions to take down (or not). Personally, I try to minimize the amount of manual effort I put into updating my Ledger. My rule for dealing with cash is this: If it is for food or alcohol, I don\u2019t track it. If it is for something else, I keep the receipt and enter it later. This works for me, because the great majority of my cash expenses tend to be food (or maybe I just make it that way by paying for everything else with credit cards). Only a few receipts pile up somewhere on my desk for a couple of months before I bother to type them in. However, you will need to make occasional adjustments to your cash account to account for these expenses. I don\u2019t actually bother doing this very often\u2026 maybe once every three months, when I feel like it. The method I use is to take a snapshot of my wallet (manually, by counting the bills) and enter a corresponding balance assertion: 2014-05-12 balance Assets:Cash 234.13 USD Every time I do this I\u2019ll also add a cash distribution adjusted to balance the account: 2014-06-19 * \"Cash distribution\" Expenses:Food:Restaurant 402.30 USD Expenses:Food:Alcohol 100.00 USD Assets:Cash ; -502.30 USD 2014-06-20 balance Assets:Cash 194.34 USD If you wonder why the amounts in the cash account don\u2019t add up (234.13 -502.30 \u2260 194.34), it is because between the two assertions I added to the cash account by doing some ATM withdrawals against the checking account, and those appear somewhere else (in the checking account section). The withdrawal increased the balance of the cash account. It would appear if I rendered a journal for Assets:Cash . I could have made my life simpler and used a Pad directive if I had booked everything to food\u2014pad entries don\u2019t work solely at the beginning of an account\u2019s history, but also between any two balance assertions on the same account\u2014but I want to book 80% of it to food and 20% alcohol, to more accurately represent my real usage of cash 2 . Finally, if you end up with a long time period between the times that you do this, you may want to \u201cspread out\u201d your expenses by adding more than one cash distribution 3 manually, so that if you generate a monthly report, a large cash expense does not appear as a single lump in or outside that month.","title":"Tracking Cash Expenses"},{"location":"command_line_accounting_cookbook.html#salary-income","text":"Accounting for your salary is rewarding: you will be able to obtain a summary of income earned during the year as well as the detail of where the various deductions are going, and you will enjoy the satisfaction of seeing matching numbers from your Beancount reports when you receive your W-2 form from your employer (or on your T4 if you\u2019re located in Canada). I put all entries related to an employer in their own dedicated section. I start it by setting an event to the date I began working there, for example, using the hypothetical company \u201cHooli\u201d (from the Silicon Valley show): 2012-12-13 event \"employer\" \"Hooli Inc.\" This allows me to automatically calculate the number of days I\u2019ve been working there. When I leave a job, I\u2019ll change it to the new one, or an empty string, if I don\u2019t leave for another job: 2019-03-02 event \"employer\" \"\" This section will make several assumptions. The goal is to expose you to the various ideas you can use to account for your income correctly. You will almost certainly end up having to adapt these ideas to your specific situation.","title":"Salary Income"},{"location":"command_line_accounting_cookbook.html#employment-income-accounts","text":"Then you define accounts for your pay stubs. You need to make sure that you have an account corresponding to each line of your pay stub. For example, here are some of the income accounts I define for this employment income at Hooli Inc.: 2012-12-13 open Income:US:Hooli:Salary USD ; \"Regular Pay\" 2012-12-13 open Income:US:Hooli:ReferralBonus USD ; \"Referral bonuses\" 2012-12-13 open Income:US:Hooli:AnnualBonus USD ; \"Annual bonus\" 2012-12-13 open Income:US:Hooli:Match401k USD ; \"Employer 401k match\" 2012-12-13 open Income:US:Hooli:GroupTermLife USD ; \"Group Term Life\" These correspond to regular salary pay, bonuses received for employee referrals, annual bonus, receipts for 401k (Hooli in this example will match some percentage of your contributions to your retirement account), receipts for life insurance (it appears both as an income and an expense), and benefits paid for your gym subscription. There are more, but this is a good example. (In this example I wrote down the names used in the stub as a comment, but you could insert them as metadata instead if you prefer.) You will need to book taxes withheld at the source to accounts for that year (see the tax section for details on this): 2014-01-01 open Expenses:Taxes:TY2014:US:Medicare USD 2014-01-01 open Expenses:Taxes:TY2014:US:Federal USD 2014-01-01 open Expenses:Taxes:TY2014:US:StateNY USD 2014-01-01 open Expenses:Taxes:TY2014:US:CityNYC USD 2014-01-01 open Expenses:Taxes:TY2014:US:SDI USD 2014-01-01 open Expenses:Taxes:TY2014:US:SocSec USD These accounts are for Medicare taxes, Federal, New York State and NYC taxes (yes, New York City residents have an additional tax on top of state tax), state disability insurance (SDI) payments, and finally, taxes to pay for social security. You will also need to have some accounts defined elsewhere for the various expenses that are paid automatically from your pay: 2012-12-13 open Expenses:Health:Life:GroupTermLife USD ; \"Life Ins.\" 2012-12-13 open Expenses:Health:Dental:Insurance USD ; \"Dental\" 2012-12-13 open Expenses:Health:Medical:Insurance USD ; \"Medical\" 2012-12-13 open Expenses:Health:Vision:Insurance USD ; \"Vision\" 2012-12-13 open Expenses:Internet:Reimbursement USD ; \"Internet Reim\" 2012-12-13 open Expenses:Transportation:PreTax USD ; \"Transit PreTax\" These correspond to typical company group plan life insurance payments, premiums for dental, medical and vision insurances, reimbursements for home internet usage, and pre-tax payments for public transit (the city of New York allows you to pay for your MetroCard with pre-tax money through your employer).","title":"Employment Income Accounts"},{"location":"command_line_accounting_cookbook.html#booking-salary-deposits","text":"Then, when I import details for a payment via direct deposit to my checking account, it will look like this: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD If I haven\u2019t received my pay stub yet, I might book it temporarily to the salary account until I do: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD ! Income:US:Hooli:Salary When I receive or fetch my pay stub, I remove this and complete the rest of the postings. A realistic entry for a gross salary of $140,000 would look something like this: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD Income:US:Hooli:GroupTermLife -25.38 USD Income:US:Hooli:Salary -5384.62 USD Expenses:Health:Dental:Insurance 2.88 USD Expenses:Health:Life:GroupTermLife 25.38 USD Expenses:Internet:Reimbursement -34.65 USD Expenses:Health:Medical:Insurance 36.33 USD Expenses:Transportation:PreTax 56.00 USD Expenses:Health:Vision:Insurance 0.69 USD Expenses:Taxes:TY2014:US:Medicare 78.08 USD Expenses:Taxes:TY2014:US:Federal 1135.91 USD Expenses:Taxes:TY2014:US:CityNYC 75.03 USD Expenses:Taxes:TY2014:US:SDI 1.20 USD Expenses:Taxes:TY2014:US:StateNY 340.06 USD Expenses:Taxes:TY2014:US:SocSec 328.42 USD It\u2019s quite unusual for a salary payment to have no variation at all from its previous one: rounding up or down from the payroll processor will often result in a difference of a penny, social security payments will cap to their maximum, and there are various deductions that will occur from time to time, e.g., deductions on taxable benefits received. Moreover, contributions to a 401k will affect that amounts of taxes withheld at the source. Therefore, you end up having to look at each pay stub individually to enter its information correctly. But this is not as time-consuming as it sounds! Here\u2019s a trick: it\u2019s a lot easier to update your transactions if you list your postings in the same order as they appear on your pay stub. You just copy-paste the previous entry, read the pay stub from top to bottom and adjust the numbers accordingly. It takes a minute for each. It\u2019s worth noting some unusual things about the previous entry. The \u201cgroup term life\u201d entry has both a $25.38 income leg and an expense one. This is because Hooli pays for the premium (it reads exactly like that on the stubs.) Hooli also reimburses some of home internet, because I use it to deal with production issues. This appears as a negative posting to reduce the amount of my expense Expenses:Internet account.","title":"Booking Salary Deposits"},{"location":"command_line_accounting_cookbook.html#vacation-hours","text":"Our pay stubs also include accrued vacation and the total vacation balance, in vacation hours. You can also track these amounts on the same transactions. You need to declare corresponding accounts: 2012-12-13 open Income:US:Hooli:Vacation VACHR 2012-12-13 open Assets:US:Hooli:Vacation VACHR 2012-12-13 open Expenses:Vacation:Hooli VACHR Vacation that accrues is something you receive and thus is treated as Income in units of \u201cVACHR\u201d, and accumulates in an Assets account, which holds how many of these hours you currently have available to \u201cspend\u201d as time off. Updating the previous salary income transaction entry: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD \u2026 Assets:US:Hooli:Vacation 4.62 VACHR Income:US:Hooli:Vacation -4.62 VACHR 4.62 VACHR on a bi-weekly paycheck 26 times per year is 26 x 4.62 ~= 120 hours. At 8 hours per day, that is 15 work days, or 3 weeks, which is a standard vacation package for new US Hooligans in this example. When you do take time off, you book an expense against your accumulated vacation time: 2014-06-17 * \"Going to the beach today\" Assets:US:Hooli:Vacation -8 VACHR Expenses:Vacation:Hooli The Expenses account tracks how much vacation you\u2019ve used. From time to time you can check that the balance reported on your pay stub\u2014the amount of vacation left that your employer thinks you have\u2014is the same as that which you have accounted for: 2014-02-29 balance Assets:US:Hooli:Vacation 112.3400 VACHR You can \u201cprice\u201d your vacation hour units to your hourly rate, so that your vacations Assets account shows how much the company would pay you if you decided to quit. Assuming that $140,000/year salary, 40 hour weeks and 50 weeks of work, which is 2000 hours per year, we obtain a rate of $70/hour, which you enter like this: 2012-12-13 price VACHR 70.00 USD Similarly, if your vacation hours expires or caps, you can calculate how much dollar-equivalent you\u2019re forfeiting by working too much and giving up your vacation time. You would write off some of the VACHR from your Assets account into an income account (representing losses).","title":"Vacation Hours"},{"location":"command_line_accounting_cookbook.html#401k-contributions","text":"The 401k plan allows you to make contributions to a tax-deferred retirement account using pre-tax dollars. This is carried out via withholdings from your pay. To account for those, you simply include a posting with the corresponding contribution towards your retirement account: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD \u2026 Assets:US:Vanguard:Cash 1000.00 USD \u2026 If you\u2019re accounting for your available contributions (see the tax section of this document), you will want to reduce your \u201c401k contribution\u201d Assets account at the same time. You would add two more postings to the transaction: 2014-02-28 * \"HOOLI INC PAYROLL\" Assets:US:BofA:Checking 3364.67 USD \u2026 Assets:US:Vanguard:Cash 1000.00 USD Assets:US:Federal:PreTax401k -1000.00 US401K Expenses:Taxes:TY2014:US:Federal:PreTax401k 1000.00 US401K \u2026 If your employer matches your contributions, this may not show on your pay stubs. Because these contributions are not taxable\u2014they are deposited directly to a tax-deferred account\u2014your employer does not have to include them in the withholding statement. You will see them appear directly in your investment account as deposits. You can book them like this to the retirement account\u2019s tax balance: 2013-03-16 * \"BUYMF - MATCH\" Income:US:Hooli:Match401k -1173.08 USD Assets:US:Vanguard:Cash 1173.08 USD And then insert a second transaction when you invest this case, or directly purchasing assets from the contribution if you have specified an asset allocation and this is automated by the broker: 2013-03-16 * \"BUYMF - MATCH\" Income:US:Hooli:Match401k -1173.08 USD Assets:US:Vanguard:VMBPX 106.741 VMBPX {10.99 USD} Note that the fund that manages your 401k accounts may be tracking your contributions and your employer\u2019s contributions in separate buckets. You would declare sub-accounts for this and make the corresponding changes: 2012-12-13 open Assets:US:Vanguard:PreTax401k:VMBPX VMBPX 2012-12-13 open Assets:US:Vanguard:Match401k:VMBPX VMBPX It is common for them to do this in order to track each source of contribution separately, because there are several constraints on rollovers to other accounts that depend on it.","title":"401k Contributions"},{"location":"command_line_accounting_cookbook.html#vesting-stock-grants","text":"See the dedicated document on this topic for more details.","title":"Vesting Stock Grants"},{"location":"command_line_accounting_cookbook.html#other-benefits","text":"You can go crazy with tracking benefits if you want. Here are a few wild ideas.","title":"Other Benefits"},{"location":"command_line_accounting_cookbook.html#points","text":"If your employer offers a sponsored massage therapy program on-site, you could presumably book a massage out of your paycheck or even from some internal website (if the company is modern), and you could pay for them using some sort of internal points system, say, \u201cHooli points\u201d. You could track those using a made-up currency, e.g., \u201cMASSA\u2019s\u201d and which could be priced at 0.50 USD, the price at which you could purchase them: 2012-12-13 open Assets:US:Hooli:Massage MASSA 2012-12-13 price MASSA 0.50 USD When I purchase new massage points, I 2013-03-15 * \"Buying points for future massages\" Liabilities:US:BofA:CreditCard -45.00 USD Assets:US:Hooli:Massage 90 MASSA {0.50 USD} If you\u2019re occasionally awarded some of these points, and you can track that in an Income account.","title":"Points"},{"location":"command_line_accounting_cookbook.html#food-benefits","text":"Like many of the larger technology companies, Hooli presumably provides free food for its employees. This saves time and encourages people to eat healthy. This is a bit of a trend in the tech world right now. This benefit does not show up anywhere, but if you want to price it as part of your compensation package, you can track it using an Income account: 2012-12-13 open Income:US:Hooli:Food Depending on how often you end up eating at work, you could guesstimate some monthly allowance per month: 2013-06-30 * \"Distribution for food eaten at Hooli\" Income:US:Hooli:Food -350 USD Expenses:Food:Restaurant","title":"Food Benefits"},{"location":"command_line_accounting_cookbook.html#currency-transfers-conversions","text":"If you convert between currencies, such as when performing an international transfer between banks, you need to provide the exchange rate to Beancount. It looks like this: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9000.00 CHF Assets:US:BofA:Checking 10000.00 USD @ 0.90 CHF The balance amount of the second posting is calculated as 10,000.00 USD x 0.90 CHF/USD = 9,000 CHF, and the transaction balances. Depending on your preference, you could have placed the rate on the other posting, like this: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9000.00 CHF @ 1.11111 USD Assets:US:BofA:Checking 10000.00 USD The balance amount of the first posting is calculated as -9000.00 CHF x 1.11111 USD/CHF = 10000.00 USD 4 . Typically I will choose the rate that was reported to me and put it on the corresponding side. You may also want to use the direction that F/X markets use for trading the rate, for example, the Swiss franc trades as USD/CHF, so I would prefer the first transaction. The price database converts the rates in both directions, so it is not that important 5 . If you use wire transfers, which is typical for this type of money transfer, you might incur a fee: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9025.00 CHF Assets:US:BofA:Checking 10000.00 USD @ 0.90 CHF Expenses:Fees:Wires 25.00 CHF If you convert cash at one of these shady-looking currency exchange parlors found in tourist locations, it might look like this: 2014-03-03 * \"Changed some cash at the airport in Lausanne\" Assets:Cash -400.00 USD @ 0.90 CHF Assets:Cash 355.00 CHF Expenses:Fees:Services 5.00 CHF In any case, you should never convert currency units using the cost basis syntax, because the original conversion rate needs to be forgotten after depositing the units, and not kept around attached to simple currency. For example, this would be incorrect usage: 2014-03-03 * \"Transfer from Swiss account\" Assets:CH:UBS:Checking -9000.00 CHF Assets:US:BofA:Checking 10000.00 USD {0.90 CHF} ; <-bad! If you did that by mistake, you would incur errors when you attempted to use the newly USD deposited: Beancount would require that you specify the cost of these \u201cUSD\u201d in CHF, e.g., \u201cdebit from my USD that I changed at 0.90 USD/CHF\u201d. Nobody does this in the real world, and neither should you when you represent your transactions: once the money has converted, it\u2019s just money in a different currency, with no associated cost. Finally, a rather subtle problem is that using these price conversions back and forth at different rates over time breaks the accounting equation to some extent: changes in exchange rate may create small amounts of money out of thin air and all the balances don\u2019t end up summing up to zero. However, this is not a problem, because Beancount implements an elegant solution to automatically correct for this problem, so you can use these conversions freely without having to worry about this: it inserts a special conversions entry on the balance sheet to invert the cumulative effect of conversions for the report and obtain a clean balance of zero. (A discussion of the conversions problem is beyond the scope of this cookbook; please refer to Solving the Conversions Problem if you\u2019d like to know more.)","title":"Currency Transfers & Conversions"},{"location":"command_line_accounting_cookbook.html#investing-and-trading","text":"Tracking trades and associated gains is a fairly involved topic. You will find a more complete introduction to profit and loss and a detailed discussion of various scenarios in the Trading with Beancount document, which is dedicated to this topic. Here we will discuss how to setup your account and provide simple example transactions to get you started.","title":"Investing and Trading"},{"location":"command_line_accounting_cookbook.html#accounts-setup","text":"You should create an account prefix to root various sub-accounts associated with your investment account. Say you have an account at ETrade, this could be \u201c Assets:US:ETrade \u201d. Choose an appropriate institution name. Your investment account will have a cash component. You should create a dedicated sub-account will represent uninvested cash deposits: 2013-02-01 open Assets:US:ETrade:Cash USD I recommend that you further define a sub-account for each of the commodity types that you will invest in. Although this is not strictly necessary\u2014Beancount accounts may contain any number of commodities\u2014it is a nice way to aggregate all the positions in that commodity together for reporting. Say you will buy shares of LQD and BND, two popular bond ETFs: 2013-02-01 open Assets:US:ETrade:LQD LQD 2013-02-01 open Assets:US:ETrade:BND BND This also helps produce nicer reports: balances are often shown at cost and it\u2019s nice to see the total cost aggregated by commodity for various reasons (i.e., each commodity provides exposure to different market characteristics). Using a dedicated sub-account for each commodity held within an institution is a good way to do that. Unless you have specific reasons not to do so, I highly suggest sticking with this by default (you can always change it later by renaming accounts). Specifying commodity constraints on your accounts will help you detect data entry mistakes. Stock trades tend to be a bit more involved than regular transactions, and this is certainly going to be helpful. Then, you will hopefully receive income in this account, in two forms: capital gains, or \u201cP&L\u201d, and dividends. I like to account for these by institution, because this is how they have to be declared for taxes. You may also receive interest income. Define these: 2013-02-01 open Income:US:ETrade:PnL USD 2013-02-01 open Income:US:ETrade:Dividends USD 2013-02-01 open Income:US:ETrade:Interest USD Finally, to account for transaction fees and commissions, you will need some general accounts to receive these: 1973-04-27 open Expenses:Financial:Fees 1973-04-27 open Expenses:Financial:Commissions","title":"Accounts Setup"},{"location":"command_line_accounting_cookbook.html#funds-transfers","text":"You will normally add some initial money in this account by making a transfer from an external account, say, a checking account: 2014-02-04 * \"Transferring money for investing\" Assets:US:BofA:Checking -2000.00 USD Assets:US:ETrade:Cash 2000.00 USD","title":"Funds Transfers"},{"location":"command_line_accounting_cookbook.html#making-a-trade","text":"Buying stock should have a posting that deposits the new commodity in the commodity\u2019s sub-account, and debits the cash account to the corresponding amounts plus commissions: 2014-02-16 * \"Buying some LQD\" Assets:US:ETrade:LQD 10 LQD {119.24 USD} Assets:US:ETrade:Cash -1199.35 USD Expenses:Financial:Commissions 6.95 USD Note that when you\u2019re buying units of a commodity, you are establishing a new trade lot in the account\u2019s inventory and it is necessary that you provide the cost of each unit (in this example, 119.24 USD per share of LQD). This allows us to account for capital gains correctly. Selling some of the same stock work similarly, except that an extra posting is added to absorb the capital gain or loss: 2014-02-16 * \"Selling some LQD\" Assets:US:ETrade:LQD -5 LQD {119.24 USD} @ 123.40 USD Assets:US:ETrade:Cash 610.05 USD Expenses:Financial:Commissions 6.95 USD Income:US:Etrade:PnL Note that the postings of shares removed from the Assets:US:ETrade:LQD account is a lot reduction and you must provide information to identify which lot you\u2019re reducing, in this case, by providing the per-share cost basis of 119.24 USD. I normally let Beancount calculate the capital gain or loss for me, which is why I don\u2019t specify it in the last posting. Beancount will automatically balance the transaction by setting the amount of this posting to -20.80 USD, which is a gain of 20.80 USD (remember that the signs are inverted for income accounts). Specifying the sale price of 123.40 USD is optional, and it is ignored for the purpose of balancing the transaction, the cash deposit and commissions legs determine the profit.","title":"Making a Trade"},{"location":"command_line_accounting_cookbook.html#receiving-dividends","text":"Receiving dividends takes on two forms. First, you can receive dividends in cash, which will go into the cash account: 2014-02-16 * \"Dividends from LQD\" Income:US:ETrade:Dividends -87.45 USD Assets:US:ETrade:Cash 87.45 USD Note that the source of the dividends isn\u2019t specified here. You could use a sub-account of the income account to count it separately. Or you can receive dividends in shares reinvested: 2014-06-27 * \"Dividends reinvested\" Assets:US:Vanguard:VBMPX 1.77400 VBMPX {10.83 USD} Income:US:Vanguard:Dividends -19.21 USD This is booked similarly to a stock purchase, and you also have to provide the cost basis of the received units. This would typically happen in a non-taxable retirement account. Refer to the Trading with Beancount document for a more thorough discussion and numerous and more complex examples.","title":"Receiving Dividends"},{"location":"command_line_accounting_cookbook.html#choosing-a-date","text":"Buying or selling a single lot of stock typically involves multiple events over time: the trade is placed, the trade is filled (usually on the same day), the trade is settled. Settlement usually occurs 2 or 3 business days after the trade is filled. For simplicity, I recommend using the trade date as the date of your transaction. In the US, this is the date that is recognized for tax purposes, and settlement has no impact on your account (brokers typically won\u2019t allow you to trade without the corresponding cash or margin anyhow). So normally I don\u2019t bother creating separate entries for settlement, it\u2019s not very useful. More complex schemes can be envisioned, e.g. you could store the settlement date as a metadata field and then use it in scripts later on, but that\u2019s beyond the scope of this document.","title":"Choosing a Date"},{"location":"command_line_accounting_cookbook.html#conclusion","text":"This document is incomplete. I have many more example use cases that I\u2019m planning to add here as I complete them. I will be announcing those on the mailing-list as they materialize. In particular, the following topics will be discussed: Health Care Expenses, e.g., insurance premiums and rebates Taxes IRAs, 401k and other tax-deferred accounts Real Estate Options This is not strictly always true: in accounting for companies, some account types are held at their opposite value for reasons, usually to offset the value of another account of the same type. These are called \u201ccontra\u201d accounts. But as an individual, you\u2019re quite unlikely to have one of those. If you\u2019re setting up a chart of accounts for a company, Beancount doesn\u2019t actually care whether the balance is of one sign or other. You declare contra-accounts just like regular accounts. \u21a9 I am considering supporting an extended version of the Pad directive that can take a percentage value and make it possible to pad only a percentage of the full amount, to automate this. \u21a9 Yet another extension to Beancount involves support multiple Pad directives between two balance assertions and automatically support this spreading out of padding directives. \u21a9 If you\u2019re concerned about the issue of precision or rounding in balancing, see this document . \u21a9 Note that if the price database needs to invert the date its calculation may result in a price with a large number of digits. Beancount uses IEEE decimal objects and the default context of the Python implementation is 28 digits, so inverting 0.9 will result in 1.111111\u2026.111 with 28 digits. \u21a9","title":"Conclusion"},{"location":"command_line_accounting_in_context.html","text":"Command-line Accounting in Context \uf0c1 Martin Blais , July 2014 http://furius.ca/beancount/doc/motivation This text provides context and motivation for the usage of double-entry command-line accounting systems to manage personal finances. Motivation What exactly is \u201cAccounting\u201d? What can it do for me? Keeping Books Generating Reports Custom Scripting Filing Documents How the Pieces Fit Together Why not just use a spreadsheet? Why not just use a commercial app? How about Mint.com? How about Quicken? How about Quickbooks? How about GnuCash? Why build a computer language? Advantages of Command-Line Bookkeeping Why not just use an SQL database? But... I just want to do X? Why am I so Excited? Motivation \uf0c1 When I tell people about my command-line accounting hacking activities, I get a variety of reactions. Sometimes I hear echoes of desperation about their own finances: many people wish they had a better handle and understanding of their finances, they sigh and wish they were better organized. Other times, after I describe my process I get incredulous reactions: \u201cWhy do you even bother?\u201d Those are the people who feel that their financial life is so simple that it can be summarized in 5 minutes. These are most often blissfully unaware how many accounts they have and have only an imprecise idea of where they stand. They have at least 20 accounts to their name; it\u2019s only when we go through the details that they realize that their financial ecosystem is more complex than they thought. Finally, there are those who get really excited about the idea of using a powerful accounting system but who don\u2019t actually understand what it is that I\u2019m talking about. They might think it is a system to support investment portfolios, or something that I use to curate a budget like a stickler. Usually they end up thinking it is too complicated to actually get started with. This document attempts to explain what command-line bookkeeping is about in concrete terms, what you will get out of doing it, how the various software pieces fit together, and what kinds of results you can expect from this method. The fact is, the double-entry method is a basic technique that everyone should have been taught in high school. And using it for yourself is a simple and powerful process you can use to drive your entire financial life. What exactly is \u201cAccounting\u201d? \uf0c1 When one talks about \u201caccounting,\u201d they often implicitly refer to one or more of various distinct financial processes: Bookkeeping. Recording past transactions in a single place, a \u201cledger,\u201d also called \u201cthe books,\u201d as in, \u201cthe books of this company.\u201d Essentially, this means copying the amounts and category of financial transactions that occur in external account statements to a single system that includes all of the \u201caccounts\u201d relating to an entity and links them together. This is colloquially called \u201ckeeping books,\u201d or the activity of \u201cbookkeeping.\u201d Invoices. Preparing invoices and tracking payments. Contractors will often bring this up because it is a major concern of their activity: issuing requests to clients for payments for services rendered, and checking whether the corresponding payments have actually been received later on (and taking collection actions in case they haven\u2019t). If you\u2019re managing a company\u2019s finances, processing payroll is another aspect of this which is heavy in bookkeeping. Taxes. Finding or calculating taxable income, filling out tax forms and filing taxes to the various governmental authorities the entity is subject to. This process can be arduous, and for people who are beginning to see an increase in the complexity of their personal assets (many accounts of different types, new reporting requirements), it can be quite stressful. This is often the moment that they start thinking about organizing themselves. Expenses. Analyzing expenses, basically answering the question: \u201cWhere is my money going?\u201d A person with a small income and many credit cards may want to precisely track and calculate how much they\u2019re spending every month. This just develops awareness. Even in the presence of abundant resources, it is interesting to look at how much one is spending regularly, and where most of one\u2019s regular income is going, it often brings up surprises. Budgeting. Forecasting future expenses, allocating limited amounts to spend in various categories, and tracking how close one\u2019s actual spending is to those allocations. For individuals, this is usually in the context of trying to pay down debt or finding ways to save more. For companies, this occurs when planning for various projects. Investing. Summarizing and computing investment returns and capital gains. Many of us now manage our own savings via discount brokers, investing via ETFs and individually selected mutual funds. It is useful to be able to view asset distribution and risk exposure, as well as compute capital gains. Reporting. Public companies have regulatory requirements to provide transparency to their investors. As such, they usually report an annual income statement and a beginning and ending balance sheet . For an individual, those same reports are useful when applying for a personal loan or a mortgage at a bank, as they provide a window to someone\u2019s financial health situation. In this document, I will describe how command-line accounting can provide help and support for these activities. What can it do for me? \uf0c1 You might legitimately ask: sure, but why bother? We can look at the uses of accounting in terms of the questions it can answer for you: Where\u2019s my money, Lebowski? If you don\u2019t keep track of stuff, use cash a lot, have too many credit cards or if you are simply a disorganized and brilliant artist with his head in the clouds, you might wonder why there aren\u2019t as many beans left at the end of the month as you would like. An income statement will answer this question. I\u2019d like to be like Warren Buffet. How much do I have to save every month? I personally don\u2019t do a budget, but I know many people who do. If you set specific financial goals for a project or to save for later, the first thing you need to do is allocate a budget and set limits on your spending. Tracking your money is the first step towards doing that. You could even compute your returns in a way that can be compared against the market. I have some cash to invest. Given my portfolio, where should I invest it? Being able to report on the totality of your holdings, you can determine your asset class and currency exposures. This can help you decide where to place new savings in order to match a target portfolio allocation. How much am I worth? You have a 401k, an IRA and taxable accounts. A remaining student loan, or perhaps you\u2019re sitting on real-estate with two mortgages. Whatever. What\u2019s the total? It\u2019s really nice to obtain a single number that tells you how far you are from your retirement goals. Beancount can easily compute your net worth (to the cent). I still hurt from 2008. Are my investments safe? For example, I\u2019m managing my own money with various discount brokers, and many people are. I\u2019m basically running a miniature hedge fund using ETFs, where my goal is to be as diversified as possible between asset classes, sector exposure, currency exposure, etc. Plus, because of tax-deferred accounts I cannot do that in a single place. With Beancount you can generate a list of holdings and aggregate them by category. Taxes suck. Well, enough said. Why do they suck? Mostly because of uncertainty and doubt. It\u2019s not fun to not know what\u2019s going on. If you had all the data at your fingertips instantly it wouldn\u2019t be nearly as bad. What do you have to report? For some people there\u2019s a single stream of income, but for many others, it gets complicated (you might even be in that situation and you don\u2019t know it). Qualified vs. ordinary dividends, long-term vs. short-term capital gains, income from secondary sources, wash sales, etc. When it\u2019s time to do my taxes, I bring up the year\u2019s income statement, and I have a clear list of items to put in and deductions I can make. I want to buy a home. How much can I gather for a down payment? If you needed a lot of cash all of a sudden, how much could you afford? Well, if you can produce a list of your holdings, you can aggregate them \u201cby liquidity.\u201d This gives you an idea of available cash in case of an urgent need. Mr. Banker, please lend me some money. Impress the banker geek: just bring your balance sheet. Ideally the one generated fresh from that morning. You should have your complete balance sheet at any point in time. Instructions for your eventual passing. Making a will involves listing your assets. Make your children\u2019s lives easier should you pass by having a complete list of all the beans to be passed on or collected. The amounts are just part of the story: being able to list all the accounts and institutions will make it easier for someone to clear your assets. I can\u2019t remember if they paid me. If you send out invoices and have receivables, it\u2019s nice to have a method to track who has paid and who just says they\u2019ll pay. I\u2019m sure there is a lot more you can think of. Let me know. Keeping Books \uf0c1 Alright, so here\u2019s the part where I give you the insight of the method. The central and most basic activity that provides support for all the other ones is bookkeeping . A most important and fundamental realization is that this relatively simple act\u2014that of copying all of the financial transactions into a single integrated system\u2014allows you to produce reports that solve all of the other problems. You are building a corpus of data, a list of dated transaction objects that each represents movements of money between some of the accounts that you own. This data provides a full timeline of your financial activity. All you have to do is enter each transaction while respecting a simple constraint, that of the double-entry method , which is this: Each time an amount is posted to an account, it must have a corresponding inverse amount posted to some other account(s), and the sum of these amounts must be zero. That\u2019s it. This is the essence of the method, and an ensemble of transactions that respects this constraint acquires nice properties which are discussed in detail in some of my other documents. In order to generate sensible reports, all accounts are further labeled with one of four categories: Assets , Liabilities , Income and Expenses . A fifth category, Equity , exists only to summarize the history of all previous income and expense transactions. Command-line accounting systems are just simplistic computer languages for conveniently entering, reading and processing this transaction data, and ensure that the constraint of the double-entry method is respected. They\u2019re simple counting systems, that can add any kind of \u201cthing\u201d in dedicated counters (\u201caccounts\u201d). They\u2019re basically calculators with multiple buckets. Beancount and Ledger may differ slightly in their syntax and on some of their semantics, but the general principle is the same, and the simplifying assumptions are very similar. Here is a concrete example of what a transaction looks like, just so you can get a feeling for it. In a text file\u2014which is the input to a command-line accounting system\u2014something like the following is written and expresses a credit card transaction: 2014-05-23 * \"CAFE MOGADOR NEW YO\" \"Dinner with Caroline\" Liabilities:US:BofA:CreditCard -98.32 USD Expenses:Restaurant This is an example transaction with two \u201cpostings,\u201d or \u201clegs.\u201d The \u201c Expenses:Restaurant \u201d line is an account, not a category, though accounts often act like categories (there is no distinction between these two concepts). The amount on the expenses leg is left unspecified, and this is a convenience allowed by the input language. The software determines its amount automatically with the remainder of the balance which in this case will be 98.32 USD, so that -98.32 USD + 98.32 USD = 0 (remember that the sum of all postings must balance to zero\u2014this is enforced by the language). Most financial institutions provide some way for you to download a summary of account activity in some format or other. Much of the details of your transactions are automatically pulled in from such a downloaded file, using some custom script you write that converts it into the above syntax: A transaction date is always available from the downloadable files. The \u201c CAFE MOGADOR NEW YO \u201d bit is the \u201cmemo,\u201d also provided by the downloaded file, and those names are often good enough for you to figure out what the business you spent at was. Those memos are the same ugly names you would see appear on your credit card statements. This is attached to a transaction as a \u201cpayee\u201d attribute. I manually added \u201c Dinner with Caroline \u201d as a comment. I don\u2019t have to do this (it\u2019s optional), but I like to do it when I reconcile new transactions, it takes me only a minute and it helps me remember past events if I look for them. The importer brought in the Liabilities:US:BofA:CreditCard posting with its amount automatically, but I\u2019ve had to insert the Expenses:Restaurant account myself: I typed it in. I have shortcuts in my text editor that allow me to do that using account name completion, it takes a second and it\u2019s incredibly easy. Furthermore, pre-selecting this account could be automated as well, by running a simple learning algorithm on the previous history contained in the same input file (we tend to go to the same places all the time). The syntax gets a little bit more complicated, for example it allows you to represent stock purchases and sales, tracking the cost basis of your assets, and there are many other types of conveniences, like being able to define and count any kind of \u201cthing\u201d in an account (e.g., \u201cvacation hours accumulated\u201d), but this example captures the essence of what I do. I replicate all the transactions from all of the accounts that I own in this way, for the most part in an automated fashion. I spend about 1-2 hours every couple of weeks in order to update this input file, and only for the most used accounts (credit card and checking accounts). Other accounts I\u2019ll update every couple of months, or when I need to generate some reports. Because I have a solid understanding of my finances, this is not a burden anymore\u2026 it has become fun . What you obtain is a full history, a complete timeline of all the financial transactions from all the accounts over time, often connected together, your financial life, in a single text file . Generating Reports \uf0c1 So what\u2019s the point of manicuring this file? I have code that reads it, parses it, and that can serve various views and aggregations of it on a local web server running on my machine, or produce custom reports from this stream of data. The most useful views are undeniably the balance sheet and income statement , but there are others: Balance Sheet. A balance sheet lists the final balance of all of your assets and liabilities accounts on a single page. This is a snapshot of all your accounts at a single point in time, a well-understood overview of your financial situation. This shows your net worth (very precisely, if you update all your accounts) and is also what a banker would be interested in if you were to apply for a loan. Beancount can produce my balance sheet at any point in time, but most often I\u2019m interested in the \u201ccurrent\u201d or \u201clatest\u201d balance sheet. Income Statement. An income statement is a summary of all income and expenses that occur between two points in time. It renders the final balance for these accounts in format familiar to any accountant: income accounts on the left, expense accounts on the right. This provides insights on the changes that occurred during a time period, a delta of changes. This tells you how much you\u2019re earning and where your money is going, in detail. The difference between the two is how much you saved. Beancount can render such a statement for any arbitrary period of time, e.g., this year, or month by month. Journals. For each account (or category, if you like to think of them that way), I can render a list of all the transactions that posted changes to that account. I call this a journal (Ledger calls this a \u201cregister\u201d). If you\u2019re looking at your credit card account\u2019s journal, for instance, it should match that of your bank statement. On the other hand, if you look at your \u201crestaurants expense\u201d account, it should show all of your restaurant outings, across all methods of payment (including cash, if you choose to enter that). You can easily fetch any detail of your financial history, like \u201cWhere was that place I had dinner with Arthur in March three years ago?\u201d or \u201cI want to sell that couch\u2026 how much did I pay for it again?\u201d Payables and Receivables. If you have amounts known to be received, for example, you filed your taxes or sent an invoice and are expecting a payment, or you mailed a check and you are expecting it to be cashed at some point in the future, you can track this with dedicated accounts. Transactions have syntax that allows you to link many of them together and figure out what has been paid or received. Calculating taxes. If you enter your salary deposits with the detail from your pay stub, you can calculate exactly how much taxes you\u2019ve paid, and the amounts should match exactly those that will be reported on your employer\u2019s annual income reporting form (e.g., W2 form in the USA, T4 if in Canada, Form P60 in the UK, or otherwise if you live elsewhere). This is particularly convenient if you have to make tax installments, for instance. If you\u2019re a contractor, you will have to track many such things, including company expensable expenses. It is also useful to count dividends that you have to report as taxable income. Investing. I\u2019m not quite rich, but I manage my own assets using discount brokers and mainly ETFs. In addition, I take advantage of a variety of tax sheltered accounts, with limited choices in assets. This is pretty common now. You could say that I\u2019m managing a miniature hedge fund and you wouldn\u2019t be too far from reality. Using Beancount, I can produce a detailed list of all holdings, with reports on daily market value changes, capital gains, dividends, asset type distribution (e.g. stocks vs. fixed income), currency exposure, and I can figure out how much of my assets are liquid, e.g., how much I have available towards a downpayment on a house. Precisely, and at any point in time. This is nice. I can also produce various aggregations of the holdings, i.e., value by account, by type of instrument, by currency. Forecasting. The company I work for has an employee stock plan, with a vesting schedule. I can forecast my approximate expected income including the vesting shares under reasonable assumptions. I can answer the question: \u201cAt this rate, at which date do I reach a net worth of X?\u201d Say, if X is the amount which you require for retirement. Sharing Expenses. If you share expenses with others, like when you go on a trip with friends, or have a roommate, the double-entry method provides a natural and easy solution to reconciling expenses together. You can also produce reports of the various expenses incurred for clarity. No uncertainty. Budgeting. I make enough money that I don\u2019t particularly set specific goals myself, but I plan to support features to do this. You basically end up managing your personal finances like a company would\u2026 but it\u2019s very easy because you\u2019re using a simple and cleverly designed computer language that makes a lot of simplifications (doing away with the concepts of \u201ccredit and debits\u201d for example), reducing the process to its essential minimum. Custom Scripting \uf0c1 The applications are endless. I have all sorts of wild ideas for generating reports for custom projects. These are useful and fun experiments, \u201cchallenges\u201d as I call them. Some examples: I once owned a condo unit and I\u2019ve been doing double-entry bookkeeping throughout the period I owned it, through selling it. All of the corresponding accounts share the Loft4530 name in them. This means that I could potentially compute the precise internal rate of return on all of the cash flows related to it, including such petty things as replacement light bulbs expenses. To consider it as a pure investment. Just for fun. I can render a tree-map of my annual expenses and assets. This is a good visualization of these categories, that preserve their relative importance. I could look at average expenses with a correction for the time-value of money. This would be fun, tell me how my cost of living has changed over time. The beauty of it is that once you have the corpus of data, which is relatively easy to create if you maintain it incrementally, you can do all sorts of fun things by writing a little bit of Python code. I built Beancount to be able to do that in two ways: By providing a \u201cplugins\u201d system that allows you to filter a parsed set of transactions. This makes it possible for you to hack the syntax of Beancount and prototype new conveniences in data entry. Plugins provided by default provide extended features just by filtering and transforming the list of parsed transactions. And it\u2019s simple: all you have to do is implement a callback function with a particular signature and add a line to your input file. By writing scripts. You can parse and obtain the contents of a ledger with very little code. In Python, this looks no more complicated than this: import beancount.loader \u2026 entries, errors, options = beancount.loader.load_file('myfile.ledger') for entry in entries: \u2026 Voila. You\u2019re on your way to spitting out whatever output you want. You have access to all the libraries in the Python world, and my code is mostly functional, heavily documented and thoroughly unit-tested. You should be able to find your way easily. Moreover, if you\u2019re uncertain about using this system, you could just use it to begin entering your data and later write a script that converts it into something else. Filing Documents \uf0c1 If you have defined accounts for each of your real-world accounts, you have also created a natural method for organizing your statements and other paper documents. As we are communicating more and more by email with our accountants and institutions, it is becoming increasingly common to scan letters to PDFs and have those available as computer files. All the banks have now gone paperless, and you can download these statements if you care (I tend to do this once at the end of the year, for preservation, just in case). It\u2019s nice to be able to organize these nicely and retrieve those documents easily. What I do is keep a directory hierarchy mirroring the account names that I\u2019ve defined, something that looks like this: .../documents/ Assets/ US/ TDBank/ Checking/ 2014-04-08.statement.march.pdf 2014-05-07.statement.april.pdf \u2026 Liabilities/ US/ Amex/ Platinum/ 2014-04-17.March.pdf 2014-04-19.download.ofx \u2026 Expenses/ Health/ Medical/ 2014-04-02.anthem.eob-physiotherapy.pdf \u2026 These example files would correspond to accounts with names Assets:US:TDBank:Checking , Liabilities:US:Amex:Platinum , and Expenses:Health:Medical . I keep this directory under version control. As long as a file name begins with a date, such as \u201c 2014-04-02 \u201d, Beancount is able to find the files automatically and insert directives that are rendered in its web interface as part of an account\u2019s journal, which you can click on to view the document itself. This allows me to find all my statements in one place, and if I\u2019m searching for a document, it has a well-defined place where I know to find it. Moreover, the importing software I wrote is able to identify downloaded files and automatically move them into the corresponding account\u2019s directory. How the Pieces Fit Together \uf0c1 So I\u2019ve described what I do to organize my finances. Here I\u2019ll tell you about the various software pieces and how they fit together: Beancount is the core of the system. It reads the text file I update, parses the computer language I\u2019ve defined and produces reports from the resulting data structures. This software only reads the input file and does not communicate with other programs on purpose. It runs in isolation. Beancount\u2019s ingest package and tools help automate the updating of account data by extracting transactions from file downloads from banks and other institutions. These tools orchestrate the running of importers which you implement (this is where all the messy importing code lives, the code you need to make it easier to keep your text file up-to-date, which can parse OFX and CSV files, for instance). See bean-extract, bean-identify tools. The ingest package also helps with filing documents (see bean-file tool). Because it is able to identify which document belongs to which account, it can move the downloaded file to my documents archive automatically. This saves me time. Finally, in order to provide market values, a Beancount input file should have suitable price directives. Beancount also contains code to fetch latest or historical prices for the various commodities present in one\u2019s ledger file (see the bean-price tool). Like the extraction of transactions from OFX downloads, it also spits out Beancount input syntax used to define prices. See the diagram below for a pretty picture that illustrates how these work together. Why not just use a spreadsheet? \uf0c1 This is indeed a good question, and spreadsheets are incredibly useful for sure. I certainly would not be writing my own software if I could track my finances with a spreadsheet. The problem is that the intrinsic structure of the double-entry transactional data does not lend itself to a tabular representation. Each transaction has multiple attributes (date, narration, tags), and two or more legs, each of which has an associated amount and possibly a cost. If you put the dates on one axis and the accounts on the other, you would end up with a very sparse and very large table; that would not be very useful, and it would be incredibly difficult to edit. If on the other hand, you had a column dedicated to account names for each row, all of your computations would have to take the account cell into the calculation logic. It would be very difficult to deal with, if not impossible. All matters of data aggregations are performed relative to account names. Moreover, dealing with the accumulation of units with a cost basis would require difficult gymnastics. I don\u2019t even know how I would proceed forward to do this in a spreadsheet. A core part of command-line accounting systems is the inventory logic that allows you to track a cost basis for every unit held in an account and book reductions of positions against existing lots only. This allows the system to compute capital gains automatically. And this is related to the codes that enforce the constraint that all postings on a transaction balance out to zero. I believe that the \u201ctransaction <-> postings\u201d data representation combined with a sensible method for updating inventories is the essence of this system. The need for these is the justification to create a dedicated method to build this data, such as a computer language. Finally, having our own syntax offers the opportunity to provide other types of useful directives such as balance assertions, open/close dates, and sophisticated filtering of subsets of transactions. You just cannot do what we\u2019re doing in a spreadsheet. Why not just use a commercial app? \uf0c1 How about Mint.com? \uf0c1 Oftentimes people tell me they\u2019re using Mint.com to track their finances, usually with some amount of specific complaints following close behind. Online services such as Mint provide a subset of the functionality of double-entry accounting. The main focus of these services is the reporting of expenses by category, and the maintenance of a budget. As such, they do a great job at automating the download of your credit card and bank transactions. I think that if you want a low-maintenance option to tracking your finances and you don\u2019t have a problem with the obvious privacy risks, this is a fantastic option: it comes with a web interface, mobile apps, and updating your accounts probably requires clicking some buttons, providing some passwords, and a small amount of manual corrections every couple of weeks. I have several reservations about using it for myself, however: Passwords. I\u2019m just not comfortable enough with any commercial company to share the passwords to all my bank, credit card, and investment accounts. This sounds like an insane idea to me, a very scary one, and I\u2019m just not willing to put that much trust in any one company\u2019s hands. I don\u2019t care what they say: I worked for several software companies and I\u2019m aware of the disconnect between promises made by salespeople, website PR, and engineering reality. I also know the power of determined computer hackers. Insufficient reporting. Reporting is probably beautiful and colorful\u2014it\u2019s gorgeous on the website\u2014but certainly insufficient for all the reporting I want to do on my data. Reporting that doesn\u2019t do what they want it to is a common complaint I hear about these systems. With my system, I can always write a script and produce any kind of report I might possibly want in the future. For instance, spitting out a treemap visualization of my expenses instead of a useless pie chart. I can slice and dice my transactions in all kinds of unique ways. I can tag subsets of transactions for projects or trips and report on them. It\u2019s more powerful than generic reporting. Perennity. What if the company is not in business in 5 years? Where will my data be? If I spend any time at all curating my financial data, I want to ensure that it will be available to me forever, in an open format. In their favor, some of these sites probably have a downloadable option, but who uses it? Does it even work well? Would it include all of the transactions that they downloaded? I don\u2019t know. Not international. It probably does not work well with an international, multi-currency situation. These services target a \u201cmajority\u201d of users, most of which have all their affairs in a single country. I live in the USA, have a past history in Canada, which involves remaining tax-sheltered investment accounts, occasional expenses during visits which justify maintaining a credit card there, and a future history which might involve some years spent in another country such as Hong Kong or Australia. Will this work in Mint? Probably not. They might support Canada, but will they support accounts in both places? How about some other country I might want to move to? I want a single integrated view of all my accounts across all countries in all currencies forever, nothing less. My system supports that very well. (I\u2019m not aware of any double-entry system that deals with the international problem in a currency agnostic way as well as Beancount does.) Inability to deal with particular situations. What if I own real estate? Will I be able to price the value of my home at a reasonable amount, so it creates an accurate balance sheet? Regularly obtaining \u201ccomparables\u201d for my property from an eager real estate agent will tell me much more precisely how much my home is worth than services like Zillow ever could. I need to be able to input that for my balance sheet. What about those stock options from that privately held Australian company I used to work for? How do I price that? How about other intangible things, such as receivables from a personal loan I made to a friend? I doubt online services are able to provide you with the ability to enter those. If you want the whole picture with precision, you need to be able to make these adjustments. Custom tracking. Using \u201cimaginary currencies\u201d, I\u2019m able to track all kinds of other things than currencies and stocks. For example, by using an \u201c IRAUSD \u201d commodity in Beancount I\u2019m able to track how many 401k contributions I\u2019ve made at any point during the year. I can count the after-tax basis of a traditional IRA account similarly. I can even count my vacation hours using an imaginary \u201c VACHR \u201d currency and verify it against my pay stubs. Capital gains reporting. I haven\u2019t tried it myself, but I\u2019ve heard some discontent about Mint from others about its limited capabilities for capital gains reporting. Will it maintain trade lots? Will it handle average cost booking? How about PnL on FOREX gains? What about revenue received for that book I\u2019m selling via PayPal? I want to be able to use my accounting system as an input for my tax reporting. Cash transactions. How difficult is it to enter cash transactions? Do I have to log in, or start a slow, heavy program that will only work under Windows? With Beancount I bring up a file in a text editor, this is instant and easy. Is it even possible to enter custom cash entries in online services? In other words, I\u2019m a very sophisticated user and yes, a bit of a control freak . I\u2019m not lying about it. I don\u2019t have any ideological objections about using a commercial service, it is probably not for me. If it\u2019s good enough for you, suit yourself. Despite their beautiful and promising websites, I haven\u2019t heard of anyone being completely satisfied with these services. I hear a fair amount of complaining, some mild satisfaction, but I have yet to meet anyone who raves about it. How about Quicken? \uf0c1 Quicken is a single-entry system, that is, it replicates the transactions of a remote account locally, and allows you to add a label to each transaction to place it in a category. I believe it also has support for synchronizing investment accounts. This is not enough for me, I want to track all kinds of things, and I want to use the double-entry method, which provides an intrinsic check that I\u2019ve entered my data correctly. Single-entry accounting is just not good enough if you\u2019ve already crossed the bridge of understanding the double-entry method. How about Quickbooks? \uf0c1 So let\u2019s talk about sophisticated software that is good enough for a company. Why wouldn\u2019t I use that instead? If it\u2019s good enough for small businesses, it should be good enough for me, no? There\u2019s Quickbooks and other ones. Why don\u2019t I use them: It costs money. Commercial software comes at a price. Ok, I probably could afford to pay a few hundred dollars per year (Quickbooks 2014 looks like around 300$/year for the full set of features), but I don\u2019t really want to. Platform. These softwares usually run on Microsoft Windows and sometimes on Mac OS X . I\u2019m a software developer, I mostly use Linux , and a Macbook Air for a laptop, on which I get annoyed running anything other than tmux and a web browser . I\u2019m not going to reboot just to enter a quick cash transaction. Slow startup. I cannot speak specifically to Quickbooks\u2019 implementation, but virtually every software suite of commercial scope I\u2019ve had to use had a splash screen and a slow, slow startup that involved initializing tons of plugins. They assume you\u2019re going to spend hours in it, which is reasonable for commercial users, but not for me, if I want to do a quick update of my ledger. UIs are inadequate. I haven\u2019t seen their UI but given the nature of transactions and my desire to input precisely and search quickly and organize things, I want to be able to edit in as text. I imagine it would be inconvenient for me. With Emacs and org-mode , I can easily i-search my way to any transaction within seconds after opening my ledger file. Inflexible. How would I go about re-organizing all my account names? I think I\u2019m still learning about the double-entry bookkeeping method and I have made mistakes in the past, mistakes where I desire to revisit the way I organize my accounts in a hierarchy. With my text file, I was able to safely rename a large number of accounts several times, and evolve my chart-of-accounts to reflect my improving understanding of how to organize my financial tracking system. Text is powerful! How about GnuCash? \uf0c1 I don\u2019t like UIs; they\u2019re inconvenient. There\u2019s nothing quite like editing a text file if you are a programmer. Moreover, GnuCash does not deal with multiple currencies well. I find bugs in it within the first hour every time I kick the tires on it, which I do every couple of years, out of curiosity. Other programs, such as Skrooge, also take the heavy-handed big UI approach. Why build a computer language? \uf0c1 A bookkeeping system provides conditions for a solution that involves a simple computer language for many reasons. Single-entry bookkeeping is largely insufficient if you're trying to track everything holistically. Existing systems either limit themselves to expense categories with little checking beyond \"reconciling\" which sometimes involves freezing the past. If you're not doing the bookkeeping for a company, sometimes just changing the past and fixing the mistakes where they occurred makes more sense. More importantly, the single-entry method leaves us wanting for the natural error-checking mechanism involved in the double-entry system. The problem is also not solvable elegantly by using spreadsheets; the simple data structure that forms the basis of the double-entry system infers either a very sparse spreadsheet with accounts on one dimension and transactions on the other. For real-world usage, this is impractical. Another iteration on this theme would involve inserting the postings with two columns, one with the account and one with the amount, but the varying number of columns and the lookup code makes this inelegant as well. Plus, it's not obvious how you would deal with a large number of currencies. Programs that provide fancy graphical or web-based user interfaces are inevitably awkward, due to the nature of the problem: each transaction is organized by viewing it through the lens of one account's journal, but any of the accounts present in its postings provide equally valid views. Ideally, what you want, is just to look at the transaction. Organizing them for most convenient input has little to do with the order in which they are to be presented. Using text has a lot of advantages: You can easily used search-and-replace and/or sed to make global changes, for example, rename or reorganize your accounts; You can organize the transactions in the order that is most convenient for data entry; There are a number of existing tools to search the text; You can easily write various little tools to spit out the data syntax, i.e., for importing from other file types, or converting from other systems. Text is inherently open , that is the file format is one that you can read your data from and store anywhere else, not a blob of incomprehensible data that becomes unusable when the company that makes the software stops supporting it. Finally, systems that attempt to automate the process of importing all your data from automated sources (e.g., mint.com) have one major shortfall: most often it's not very easy or even possible to add information for accounts that aren't automated. It is my experience that in practice you will have some entries and accounts to track that will not have a nice downloadable file format, or that simply don't have a real-world counterpart. In order to produce a complete view of one's balance sheet, it is important to be able to enter all of an individual's account within a single system. In other words, custom accounts and manually entered transactions do matter a lot. For all the reasons mentioned above, I feel that a computer language is more appropriate to express this data structure than a heavy piece of software with a customized interface. Being able to easily bring up a text file and quickly type in a few lines of text to add a transaction is great\u2013it's fast and easy. The ledger file provides a naturally open way to express one's data, and can be source-controlled or shared between people as well. Multiple files can be merged together, and scripted manipulations on a source file can be used to reorganize one's data history. Furthermore, a read-only web interface that presents the various reports one expects and allows the user to explore different views on the dataset is sufficient for the purpose of viewing the data. Advantages of Command-Line Bookkeeping \uf0c1 In summary, there are many advantages to using a command-line accounting system over a commercial package that provides a user-interface: Fast. You don\u2019t have to fire up a slow program with a splash screen that will take a while to initialize in order to add something to your books. Bringing up a text file from a bookmark in your favorite editing program (I use Emacs) is easy and quick. And if you\u2019re normally using a text editor all day long, as many programmers do, you won\u2019t even blink before the file is in front of your eyes. It\u2019s quick and easy. Portable. It will work on all platforms. Beancount is written in Python 3 with some C extensions, and as such will work on Mac, Linux and Windows platforms. I am very careful and determined to keep external dependencies on third-party packages to an absolute minimum in order to avoid installation problems. It should be easy to install and update, and work everywhere the same. Openness. Your data is open, and will remain open forever . I plan on having my corpus of data until the day I die. With an open format you will never end up in a situation where your transactional data is sitting in a binary blob with an unknown format and the software goes unsupported. Furthermore, your data can be converted to other languages easily. You can easily invoke the parser I provide and write a script that spits it out in another program\u2019s input syntax. You can place it under version control. You can entirely reorganize the structure of your accounts by renaming strings with sed. Text is empowering. Customized. You can produce very customized reports, that address exactly the kinds of problems you are having. One complaint I often hear from people about other financial software is that it doesn\u2019t quite do what they want. With command-line accounting systems you can at least write it yourself, as a small extension that uses your corpus of data. Why not just use an SQL database? \uf0c1 I don\u2019t like to reinvent the wheel. If this problem could be solved by filling up an SQL database and then making queries on it, that\u2019s exactly what I would do. Creating a language is a large overhead, it needs to be maintained and evolved, it\u2019s not an easy task, but as it turns out, necessary and justified to solve this problem. The problem is due to a few reasons: Filtering occurs on a two-level data structure of transactions vs. their children postings, and it is inconvenient to represent the data in a single table upon which we could then make manipulations. You cannot simply work only with postings: in order to ensure that the reports balance, you need to be selecting complete transactions. When you select transactions, the semantics is \u201cselect all the postings for which the transaction has this or that property.\u201d One such property is \u201call transactions that have some posting with account X .\u201d These semantics are not obvious to implement with a database. The nature of the data structure makes it inconvenient. The wide variety of directives makes it difficult to design a single elegant table that can accommodate all of their data. Ideally we would want to define two tables for each type of directive: a table that holds all common data (date, source filename & lineno, directive type) and a table to hold the type-specific data. While this is possible, it steps away from the neat structure of a single table of data. Operations on inventories\u2014the data structure that holds the incrementally changing contents of accounts\u2014require special treatment that would be difficult to implement in a database. Lot reductions are constrained against a running inventory of lots, each of which has a specific cost basis. Some lot reductions trigger the merging of lots (for average cost booking). This requires some custom operations on these inventory objects. Aggregations (breakdowns) are hierarchical in nature. The tree-like structure of accounts allows us to perform operations on subtrees of postings. This would also not be easy to implement with tables. Finally, input would be difficult. By defining a language, we side-step the problem of having to build a custom user interface that would allow us to create the data. Nevertheless, I really like the idea of working with databases. A script (bean-sql) is provided to convert the contents of a Beancount file to an SQL database; it\u2019s not super elegant to carry out computations on those tables, but it\u2019s provided nonetheless. You should be able to play around with it, even if operations are difficult. I might even pre-compute some of the operation\u2019s outputs to make it easier to run SQL queries. But... I just want to do X ? \uf0c1 Some may infer that what we\u2019re doing must be terribly complicated given that they envision they might want to use such a system only for a single purpose. But the fact is, how many accounts you decide to track is a personal choice. You can choose to track as little as you want in as little detail as you deem sufficient. For example, if all that you\u2019re interested in is investing, then you can keep books on only your investment accounts. If you\u2019re interested in replacing your usage of Mint.com or Quicken , you can simply just replicate the statements for your credit cards and see your corresponding expenses. The simplicity of having to just replicate the transactions in all your accounts over doing a painful annual \u201cstarting from scratch\u201d evaluation of all your assets and expenses for the purpose of looking at your finances will convince you. Looking at your finances with a spreadsheet will require you to at least copy values from your portfolio. Every time you want to generate the report you\u2019ll have to update the values\u2026 with my method, you just update each account\u2019s full activity, and you can obtain the complete list holdings as a by-product. It\u2019s easy to bring everything up-to-date if you have a systematic method. To those I say: try it out, accounting just for the bits that you\u2019re interested in. Once you\u2019ll have a taste of how the double-entry method works and have learned a little bit about the language syntax, you will want to get a fuller picture. You will get sucked in. Why am I so Excited? \uf0c1 Okay, so I\u2019ve become an accounting nerd. I did not ask for it. Just kind-of happened while I wasn\u2019t looking (I still don\u2019t wear brown socks though). Why is it I can\u2019t stop talking about this stuff? I used to have a company. It was an umbrella for doing contract work (yes, I originally had bigger aspirations for it, but it ended up being just that. Bleh.) As a company owner in Canada, I had to periodically make five different kinds of tax installments to the Federal and Provincial governments, some every month, some every three months, and then it varied. An accountant was feeding me the \u201cmagic numbers\u201d to put in the forms, numbers which I was copying like a monkey, without fully understanding how they were going to get used later on. I had to count separately the various expenses I would incur in relation to my work (for deductions), and those which were only personal \u2014I did not have the enlightened view of getting separate credit cards so I was trying to track personal vs. company expenses from the same accounts. I also had to track transfers between company and personal accounts that would later on get reported as dividends or salary income. I often had multiple pending invoices that would take more than two months to get paid, from different clients. It was a demi-hell. You get the idea. I used to try to track all these things manually, using systems composed of little text files and spreadsheets and doing things very carefully and in a particular way. The thing is, although I am by nature quite methodical, I would sometimes, just sometimes forget to update one of the files. Things would fall out of sync. When this happened it was incredibly frustrating, I had to spend time digging around various statements and figure out where I had gone wrong. This was time-consuming and unpleasant. And of course, when something you have to do is unpleasant, you tend not to do it so well. I did not have a solid idea of the current state of affairs of my company during the year, I just worked and earned money for it. My accountant would draw a balance sheet once a year after doing taxes in July, and it was always a tiny bit of a surprise. I felt permanently somewhat confused, and frankly annoyed every time I had to deal with \u201cmoney things.\u201d It wasn\u2019t quite a nightmare, but it was definitely making me tired. I felt my life was so much simpler when I just had a job. But one day\u2026 one day\u2026 I saw the light: I discovered the double-entry method. I don\u2019t recall exactly how. I think it was on a website, yes\u2026 this site: dwmbeancounter.com (it\u2019s a wonderful site). I realized that using a single system , I could account for all of these problems at the same time, and in a way that would impose an inherent error-checking mechanism. I was blown away! I think I printed the whole thing on paper at the time and worked my way through every tutorial. This simple counting trick is exactly what I was in dire need for. But all the software I tried was either disappointing, broken, or too complicated. So I read up on Ledger. And I got in touch with its author . And then shortly after I started on Beancount 1 . I\u2019ll admit that going through the effort of designing my own system just to solve my accounting problems is a bit overkill, but I\u2019ve been known to be a little more than extreme about certain things , and I\u2019ve really enjoyed solving this problem. My life is fulfilled when I maintain a good balance of \u201clearning\u201d and \u201cdoing,\u201d and this falls squarely in the \u201cdoing\u201d domain. Ever since, I feel so excited about anything related to personal finance. Probably because it makes me so happy to have such a level of awareness about what\u2019s going on with mine. I even sometimes find myself loving spending money in a new way, just from knowing that I\u2019ll have to figure out how to account for it later. I feel so elated by the ability to solve these financial puzzles that I have had bouts of engaging complete weekends in building this software. They are small challenges with a truly practical application and a tangible consequence. Instant gratification. I get a feeling of empowerment . And I wish the same for you. For a full list of differences, refer to this document . \u21a9","title":"Command Line Accounting in Context"},{"location":"command_line_accounting_in_context.html#command-line-accounting-in-context","text":"Martin Blais , July 2014 http://furius.ca/beancount/doc/motivation This text provides context and motivation for the usage of double-entry command-line accounting systems to manage personal finances. Motivation What exactly is \u201cAccounting\u201d? What can it do for me? Keeping Books Generating Reports Custom Scripting Filing Documents How the Pieces Fit Together Why not just use a spreadsheet? Why not just use a commercial app? How about Mint.com? How about Quicken? How about Quickbooks? How about GnuCash? Why build a computer language? Advantages of Command-Line Bookkeeping Why not just use an SQL database? But... I just want to do X? Why am I so Excited?","title":"Command-line Accounting in Context"},{"location":"command_line_accounting_in_context.html#motivation","text":"When I tell people about my command-line accounting hacking activities, I get a variety of reactions. Sometimes I hear echoes of desperation about their own finances: many people wish they had a better handle and understanding of their finances, they sigh and wish they were better organized. Other times, after I describe my process I get incredulous reactions: \u201cWhy do you even bother?\u201d Those are the people who feel that their financial life is so simple that it can be summarized in 5 minutes. These are most often blissfully unaware how many accounts they have and have only an imprecise idea of where they stand. They have at least 20 accounts to their name; it\u2019s only when we go through the details that they realize that their financial ecosystem is more complex than they thought. Finally, there are those who get really excited about the idea of using a powerful accounting system but who don\u2019t actually understand what it is that I\u2019m talking about. They might think it is a system to support investment portfolios, or something that I use to curate a budget like a stickler. Usually they end up thinking it is too complicated to actually get started with. This document attempts to explain what command-line bookkeeping is about in concrete terms, what you will get out of doing it, how the various software pieces fit together, and what kinds of results you can expect from this method. The fact is, the double-entry method is a basic technique that everyone should have been taught in high school. And using it for yourself is a simple and powerful process you can use to drive your entire financial life.","title":"Motivation"},{"location":"command_line_accounting_in_context.html#what-exactly-is-accounting","text":"When one talks about \u201caccounting,\u201d they often implicitly refer to one or more of various distinct financial processes: Bookkeeping. Recording past transactions in a single place, a \u201cledger,\u201d also called \u201cthe books,\u201d as in, \u201cthe books of this company.\u201d Essentially, this means copying the amounts and category of financial transactions that occur in external account statements to a single system that includes all of the \u201caccounts\u201d relating to an entity and links them together. This is colloquially called \u201ckeeping books,\u201d or the activity of \u201cbookkeeping.\u201d Invoices. Preparing invoices and tracking payments. Contractors will often bring this up because it is a major concern of their activity: issuing requests to clients for payments for services rendered, and checking whether the corresponding payments have actually been received later on (and taking collection actions in case they haven\u2019t). If you\u2019re managing a company\u2019s finances, processing payroll is another aspect of this which is heavy in bookkeeping. Taxes. Finding or calculating taxable income, filling out tax forms and filing taxes to the various governmental authorities the entity is subject to. This process can be arduous, and for people who are beginning to see an increase in the complexity of their personal assets (many accounts of different types, new reporting requirements), it can be quite stressful. This is often the moment that they start thinking about organizing themselves. Expenses. Analyzing expenses, basically answering the question: \u201cWhere is my money going?\u201d A person with a small income and many credit cards may want to precisely track and calculate how much they\u2019re spending every month. This just develops awareness. Even in the presence of abundant resources, it is interesting to look at how much one is spending regularly, and where most of one\u2019s regular income is going, it often brings up surprises. Budgeting. Forecasting future expenses, allocating limited amounts to spend in various categories, and tracking how close one\u2019s actual spending is to those allocations. For individuals, this is usually in the context of trying to pay down debt or finding ways to save more. For companies, this occurs when planning for various projects. Investing. Summarizing and computing investment returns and capital gains. Many of us now manage our own savings via discount brokers, investing via ETFs and individually selected mutual funds. It is useful to be able to view asset distribution and risk exposure, as well as compute capital gains. Reporting. Public companies have regulatory requirements to provide transparency to their investors. As such, they usually report an annual income statement and a beginning and ending balance sheet . For an individual, those same reports are useful when applying for a personal loan or a mortgage at a bank, as they provide a window to someone\u2019s financial health situation. In this document, I will describe how command-line accounting can provide help and support for these activities.","title":"What exactly is \u201cAccounting\u201d?"},{"location":"command_line_accounting_in_context.html#what-can-it-do-for-me","text":"You might legitimately ask: sure, but why bother? We can look at the uses of accounting in terms of the questions it can answer for you: Where\u2019s my money, Lebowski? If you don\u2019t keep track of stuff, use cash a lot, have too many credit cards or if you are simply a disorganized and brilliant artist with his head in the clouds, you might wonder why there aren\u2019t as many beans left at the end of the month as you would like. An income statement will answer this question. I\u2019d like to be like Warren Buffet. How much do I have to save every month? I personally don\u2019t do a budget, but I know many people who do. If you set specific financial goals for a project or to save for later, the first thing you need to do is allocate a budget and set limits on your spending. Tracking your money is the first step towards doing that. You could even compute your returns in a way that can be compared against the market. I have some cash to invest. Given my portfolio, where should I invest it? Being able to report on the totality of your holdings, you can determine your asset class and currency exposures. This can help you decide where to place new savings in order to match a target portfolio allocation. How much am I worth? You have a 401k, an IRA and taxable accounts. A remaining student loan, or perhaps you\u2019re sitting on real-estate with two mortgages. Whatever. What\u2019s the total? It\u2019s really nice to obtain a single number that tells you how far you are from your retirement goals. Beancount can easily compute your net worth (to the cent). I still hurt from 2008. Are my investments safe? For example, I\u2019m managing my own money with various discount brokers, and many people are. I\u2019m basically running a miniature hedge fund using ETFs, where my goal is to be as diversified as possible between asset classes, sector exposure, currency exposure, etc. Plus, because of tax-deferred accounts I cannot do that in a single place. With Beancount you can generate a list of holdings and aggregate them by category. Taxes suck. Well, enough said. Why do they suck? Mostly because of uncertainty and doubt. It\u2019s not fun to not know what\u2019s going on. If you had all the data at your fingertips instantly it wouldn\u2019t be nearly as bad. What do you have to report? For some people there\u2019s a single stream of income, but for many others, it gets complicated (you might even be in that situation and you don\u2019t know it). Qualified vs. ordinary dividends, long-term vs. short-term capital gains, income from secondary sources, wash sales, etc. When it\u2019s time to do my taxes, I bring up the year\u2019s income statement, and I have a clear list of items to put in and deductions I can make. I want to buy a home. How much can I gather for a down payment? If you needed a lot of cash all of a sudden, how much could you afford? Well, if you can produce a list of your holdings, you can aggregate them \u201cby liquidity.\u201d This gives you an idea of available cash in case of an urgent need. Mr. Banker, please lend me some money. Impress the banker geek: just bring your balance sheet. Ideally the one generated fresh from that morning. You should have your complete balance sheet at any point in time. Instructions for your eventual passing. Making a will involves listing your assets. Make your children\u2019s lives easier should you pass by having a complete list of all the beans to be passed on or collected. The amounts are just part of the story: being able to list all the accounts and institutions will make it easier for someone to clear your assets. I can\u2019t remember if they paid me. If you send out invoices and have receivables, it\u2019s nice to have a method to track who has paid and who just says they\u2019ll pay. I\u2019m sure there is a lot more you can think of. Let me know.","title":"What can it do for me?"},{"location":"command_line_accounting_in_context.html#keeping-books","text":"Alright, so here\u2019s the part where I give you the insight of the method. The central and most basic activity that provides support for all the other ones is bookkeeping . A most important and fundamental realization is that this relatively simple act\u2014that of copying all of the financial transactions into a single integrated system\u2014allows you to produce reports that solve all of the other problems. You are building a corpus of data, a list of dated transaction objects that each represents movements of money between some of the accounts that you own. This data provides a full timeline of your financial activity. All you have to do is enter each transaction while respecting a simple constraint, that of the double-entry method , which is this: Each time an amount is posted to an account, it must have a corresponding inverse amount posted to some other account(s), and the sum of these amounts must be zero. That\u2019s it. This is the essence of the method, and an ensemble of transactions that respects this constraint acquires nice properties which are discussed in detail in some of my other documents. In order to generate sensible reports, all accounts are further labeled with one of four categories: Assets , Liabilities , Income and Expenses . A fifth category, Equity , exists only to summarize the history of all previous income and expense transactions. Command-line accounting systems are just simplistic computer languages for conveniently entering, reading and processing this transaction data, and ensure that the constraint of the double-entry method is respected. They\u2019re simple counting systems, that can add any kind of \u201cthing\u201d in dedicated counters (\u201caccounts\u201d). They\u2019re basically calculators with multiple buckets. Beancount and Ledger may differ slightly in their syntax and on some of their semantics, but the general principle is the same, and the simplifying assumptions are very similar. Here is a concrete example of what a transaction looks like, just so you can get a feeling for it. In a text file\u2014which is the input to a command-line accounting system\u2014something like the following is written and expresses a credit card transaction: 2014-05-23 * \"CAFE MOGADOR NEW YO\" \"Dinner with Caroline\" Liabilities:US:BofA:CreditCard -98.32 USD Expenses:Restaurant This is an example transaction with two \u201cpostings,\u201d or \u201clegs.\u201d The \u201c Expenses:Restaurant \u201d line is an account, not a category, though accounts often act like categories (there is no distinction between these two concepts). The amount on the expenses leg is left unspecified, and this is a convenience allowed by the input language. The software determines its amount automatically with the remainder of the balance which in this case will be 98.32 USD, so that -98.32 USD + 98.32 USD = 0 (remember that the sum of all postings must balance to zero\u2014this is enforced by the language). Most financial institutions provide some way for you to download a summary of account activity in some format or other. Much of the details of your transactions are automatically pulled in from such a downloaded file, using some custom script you write that converts it into the above syntax: A transaction date is always available from the downloadable files. The \u201c CAFE MOGADOR NEW YO \u201d bit is the \u201cmemo,\u201d also provided by the downloaded file, and those names are often good enough for you to figure out what the business you spent at was. Those memos are the same ugly names you would see appear on your credit card statements. This is attached to a transaction as a \u201cpayee\u201d attribute. I manually added \u201c Dinner with Caroline \u201d as a comment. I don\u2019t have to do this (it\u2019s optional), but I like to do it when I reconcile new transactions, it takes me only a minute and it helps me remember past events if I look for them. The importer brought in the Liabilities:US:BofA:CreditCard posting with its amount automatically, but I\u2019ve had to insert the Expenses:Restaurant account myself: I typed it in. I have shortcuts in my text editor that allow me to do that using account name completion, it takes a second and it\u2019s incredibly easy. Furthermore, pre-selecting this account could be automated as well, by running a simple learning algorithm on the previous history contained in the same input file (we tend to go to the same places all the time). The syntax gets a little bit more complicated, for example it allows you to represent stock purchases and sales, tracking the cost basis of your assets, and there are many other types of conveniences, like being able to define and count any kind of \u201cthing\u201d in an account (e.g., \u201cvacation hours accumulated\u201d), but this example captures the essence of what I do. I replicate all the transactions from all of the accounts that I own in this way, for the most part in an automated fashion. I spend about 1-2 hours every couple of weeks in order to update this input file, and only for the most used accounts (credit card and checking accounts). Other accounts I\u2019ll update every couple of months, or when I need to generate some reports. Because I have a solid understanding of my finances, this is not a burden anymore\u2026 it has become fun . What you obtain is a full history, a complete timeline of all the financial transactions from all the accounts over time, often connected together, your financial life, in a single text file .","title":"Keeping Books"},{"location":"command_line_accounting_in_context.html#generating-reports","text":"So what\u2019s the point of manicuring this file? I have code that reads it, parses it, and that can serve various views and aggregations of it on a local web server running on my machine, or produce custom reports from this stream of data. The most useful views are undeniably the balance sheet and income statement , but there are others: Balance Sheet. A balance sheet lists the final balance of all of your assets and liabilities accounts on a single page. This is a snapshot of all your accounts at a single point in time, a well-understood overview of your financial situation. This shows your net worth (very precisely, if you update all your accounts) and is also what a banker would be interested in if you were to apply for a loan. Beancount can produce my balance sheet at any point in time, but most often I\u2019m interested in the \u201ccurrent\u201d or \u201clatest\u201d balance sheet. Income Statement. An income statement is a summary of all income and expenses that occur between two points in time. It renders the final balance for these accounts in format familiar to any accountant: income accounts on the left, expense accounts on the right. This provides insights on the changes that occurred during a time period, a delta of changes. This tells you how much you\u2019re earning and where your money is going, in detail. The difference between the two is how much you saved. Beancount can render such a statement for any arbitrary period of time, e.g., this year, or month by month. Journals. For each account (or category, if you like to think of them that way), I can render a list of all the transactions that posted changes to that account. I call this a journal (Ledger calls this a \u201cregister\u201d). If you\u2019re looking at your credit card account\u2019s journal, for instance, it should match that of your bank statement. On the other hand, if you look at your \u201crestaurants expense\u201d account, it should show all of your restaurant outings, across all methods of payment (including cash, if you choose to enter that). You can easily fetch any detail of your financial history, like \u201cWhere was that place I had dinner with Arthur in March three years ago?\u201d or \u201cI want to sell that couch\u2026 how much did I pay for it again?\u201d Payables and Receivables. If you have amounts known to be received, for example, you filed your taxes or sent an invoice and are expecting a payment, or you mailed a check and you are expecting it to be cashed at some point in the future, you can track this with dedicated accounts. Transactions have syntax that allows you to link many of them together and figure out what has been paid or received. Calculating taxes. If you enter your salary deposits with the detail from your pay stub, you can calculate exactly how much taxes you\u2019ve paid, and the amounts should match exactly those that will be reported on your employer\u2019s annual income reporting form (e.g., W2 form in the USA, T4 if in Canada, Form P60 in the UK, or otherwise if you live elsewhere). This is particularly convenient if you have to make tax installments, for instance. If you\u2019re a contractor, you will have to track many such things, including company expensable expenses. It is also useful to count dividends that you have to report as taxable income. Investing. I\u2019m not quite rich, but I manage my own assets using discount brokers and mainly ETFs. In addition, I take advantage of a variety of tax sheltered accounts, with limited choices in assets. This is pretty common now. You could say that I\u2019m managing a miniature hedge fund and you wouldn\u2019t be too far from reality. Using Beancount, I can produce a detailed list of all holdings, with reports on daily market value changes, capital gains, dividends, asset type distribution (e.g. stocks vs. fixed income), currency exposure, and I can figure out how much of my assets are liquid, e.g., how much I have available towards a downpayment on a house. Precisely, and at any point in time. This is nice. I can also produce various aggregations of the holdings, i.e., value by account, by type of instrument, by currency. Forecasting. The company I work for has an employee stock plan, with a vesting schedule. I can forecast my approximate expected income including the vesting shares under reasonable assumptions. I can answer the question: \u201cAt this rate, at which date do I reach a net worth of X?\u201d Say, if X is the amount which you require for retirement. Sharing Expenses. If you share expenses with others, like when you go on a trip with friends, or have a roommate, the double-entry method provides a natural and easy solution to reconciling expenses together. You can also produce reports of the various expenses incurred for clarity. No uncertainty. Budgeting. I make enough money that I don\u2019t particularly set specific goals myself, but I plan to support features to do this. You basically end up managing your personal finances like a company would\u2026 but it\u2019s very easy because you\u2019re using a simple and cleverly designed computer language that makes a lot of simplifications (doing away with the concepts of \u201ccredit and debits\u201d for example), reducing the process to its essential minimum.","title":"Generating Reports"},{"location":"command_line_accounting_in_context.html#custom-scripting","text":"The applications are endless. I have all sorts of wild ideas for generating reports for custom projects. These are useful and fun experiments, \u201cchallenges\u201d as I call them. Some examples: I once owned a condo unit and I\u2019ve been doing double-entry bookkeeping throughout the period I owned it, through selling it. All of the corresponding accounts share the Loft4530 name in them. This means that I could potentially compute the precise internal rate of return on all of the cash flows related to it, including such petty things as replacement light bulbs expenses. To consider it as a pure investment. Just for fun. I can render a tree-map of my annual expenses and assets. This is a good visualization of these categories, that preserve their relative importance. I could look at average expenses with a correction for the time-value of money. This would be fun, tell me how my cost of living has changed over time. The beauty of it is that once you have the corpus of data, which is relatively easy to create if you maintain it incrementally, you can do all sorts of fun things by writing a little bit of Python code. I built Beancount to be able to do that in two ways: By providing a \u201cplugins\u201d system that allows you to filter a parsed set of transactions. This makes it possible for you to hack the syntax of Beancount and prototype new conveniences in data entry. Plugins provided by default provide extended features just by filtering and transforming the list of parsed transactions. And it\u2019s simple: all you have to do is implement a callback function with a particular signature and add a line to your input file. By writing scripts. You can parse and obtain the contents of a ledger with very little code. In Python, this looks no more complicated than this: import beancount.loader \u2026 entries, errors, options = beancount.loader.load_file('myfile.ledger') for entry in entries: \u2026 Voila. You\u2019re on your way to spitting out whatever output you want. You have access to all the libraries in the Python world, and my code is mostly functional, heavily documented and thoroughly unit-tested. You should be able to find your way easily. Moreover, if you\u2019re uncertain about using this system, you could just use it to begin entering your data and later write a script that converts it into something else.","title":"Custom Scripting"},{"location":"command_line_accounting_in_context.html#filing-documents","text":"If you have defined accounts for each of your real-world accounts, you have also created a natural method for organizing your statements and other paper documents. As we are communicating more and more by email with our accountants and institutions, it is becoming increasingly common to scan letters to PDFs and have those available as computer files. All the banks have now gone paperless, and you can download these statements if you care (I tend to do this once at the end of the year, for preservation, just in case). It\u2019s nice to be able to organize these nicely and retrieve those documents easily. What I do is keep a directory hierarchy mirroring the account names that I\u2019ve defined, something that looks like this: .../documents/ Assets/ US/ TDBank/ Checking/ 2014-04-08.statement.march.pdf 2014-05-07.statement.april.pdf \u2026 Liabilities/ US/ Amex/ Platinum/ 2014-04-17.March.pdf 2014-04-19.download.ofx \u2026 Expenses/ Health/ Medical/ 2014-04-02.anthem.eob-physiotherapy.pdf \u2026 These example files would correspond to accounts with names Assets:US:TDBank:Checking , Liabilities:US:Amex:Platinum , and Expenses:Health:Medical . I keep this directory under version control. As long as a file name begins with a date, such as \u201c 2014-04-02 \u201d, Beancount is able to find the files automatically and insert directives that are rendered in its web interface as part of an account\u2019s journal, which you can click on to view the document itself. This allows me to find all my statements in one place, and if I\u2019m searching for a document, it has a well-defined place where I know to find it. Moreover, the importing software I wrote is able to identify downloaded files and automatically move them into the corresponding account\u2019s directory.","title":"Filing Documents"},{"location":"command_line_accounting_in_context.html#how-the-pieces-fit-together","text":"So I\u2019ve described what I do to organize my finances. Here I\u2019ll tell you about the various software pieces and how they fit together: Beancount is the core of the system. It reads the text file I update, parses the computer language I\u2019ve defined and produces reports from the resulting data structures. This software only reads the input file and does not communicate with other programs on purpose. It runs in isolation. Beancount\u2019s ingest package and tools help automate the updating of account data by extracting transactions from file downloads from banks and other institutions. These tools orchestrate the running of importers which you implement (this is where all the messy importing code lives, the code you need to make it easier to keep your text file up-to-date, which can parse OFX and CSV files, for instance). See bean-extract, bean-identify tools. The ingest package also helps with filing documents (see bean-file tool). Because it is able to identify which document belongs to which account, it can move the downloaded file to my documents archive automatically. This saves me time. Finally, in order to provide market values, a Beancount input file should have suitable price directives. Beancount also contains code to fetch latest or historical prices for the various commodities present in one\u2019s ledger file (see the bean-price tool). Like the extraction of transactions from OFX downloads, it also spits out Beancount input syntax used to define prices. See the diagram below for a pretty picture that illustrates how these work together.","title":"How the Pieces Fit Together"},{"location":"command_line_accounting_in_context.html#why-not-just-use-a-spreadsheet","text":"This is indeed a good question, and spreadsheets are incredibly useful for sure. I certainly would not be writing my own software if I could track my finances with a spreadsheet. The problem is that the intrinsic structure of the double-entry transactional data does not lend itself to a tabular representation. Each transaction has multiple attributes (date, narration, tags), and two or more legs, each of which has an associated amount and possibly a cost. If you put the dates on one axis and the accounts on the other, you would end up with a very sparse and very large table; that would not be very useful, and it would be incredibly difficult to edit. If on the other hand, you had a column dedicated to account names for each row, all of your computations would have to take the account cell into the calculation logic. It would be very difficult to deal with, if not impossible. All matters of data aggregations are performed relative to account names. Moreover, dealing with the accumulation of units with a cost basis would require difficult gymnastics. I don\u2019t even know how I would proceed forward to do this in a spreadsheet. A core part of command-line accounting systems is the inventory logic that allows you to track a cost basis for every unit held in an account and book reductions of positions against existing lots only. This allows the system to compute capital gains automatically. And this is related to the codes that enforce the constraint that all postings on a transaction balance out to zero. I believe that the \u201ctransaction <-> postings\u201d data representation combined with a sensible method for updating inventories is the essence of this system. The need for these is the justification to create a dedicated method to build this data, such as a computer language. Finally, having our own syntax offers the opportunity to provide other types of useful directives such as balance assertions, open/close dates, and sophisticated filtering of subsets of transactions. You just cannot do what we\u2019re doing in a spreadsheet.","title":"Why not just use a spreadsheet?"},{"location":"command_line_accounting_in_context.html#why-not-just-use-a-commercial-app","text":"","title":"Why not just use a commercial app?"},{"location":"command_line_accounting_in_context.html#how-about-mintcom","text":"Oftentimes people tell me they\u2019re using Mint.com to track their finances, usually with some amount of specific complaints following close behind. Online services such as Mint provide a subset of the functionality of double-entry accounting. The main focus of these services is the reporting of expenses by category, and the maintenance of a budget. As such, they do a great job at automating the download of your credit card and bank transactions. I think that if you want a low-maintenance option to tracking your finances and you don\u2019t have a problem with the obvious privacy risks, this is a fantastic option: it comes with a web interface, mobile apps, and updating your accounts probably requires clicking some buttons, providing some passwords, and a small amount of manual corrections every couple of weeks. I have several reservations about using it for myself, however: Passwords. I\u2019m just not comfortable enough with any commercial company to share the passwords to all my bank, credit card, and investment accounts. This sounds like an insane idea to me, a very scary one, and I\u2019m just not willing to put that much trust in any one company\u2019s hands. I don\u2019t care what they say: I worked for several software companies and I\u2019m aware of the disconnect between promises made by salespeople, website PR, and engineering reality. I also know the power of determined computer hackers. Insufficient reporting. Reporting is probably beautiful and colorful\u2014it\u2019s gorgeous on the website\u2014but certainly insufficient for all the reporting I want to do on my data. Reporting that doesn\u2019t do what they want it to is a common complaint I hear about these systems. With my system, I can always write a script and produce any kind of report I might possibly want in the future. For instance, spitting out a treemap visualization of my expenses instead of a useless pie chart. I can slice and dice my transactions in all kinds of unique ways. I can tag subsets of transactions for projects or trips and report on them. It\u2019s more powerful than generic reporting. Perennity. What if the company is not in business in 5 years? Where will my data be? If I spend any time at all curating my financial data, I want to ensure that it will be available to me forever, in an open format. In their favor, some of these sites probably have a downloadable option, but who uses it? Does it even work well? Would it include all of the transactions that they downloaded? I don\u2019t know. Not international. It probably does not work well with an international, multi-currency situation. These services target a \u201cmajority\u201d of users, most of which have all their affairs in a single country. I live in the USA, have a past history in Canada, which involves remaining tax-sheltered investment accounts, occasional expenses during visits which justify maintaining a credit card there, and a future history which might involve some years spent in another country such as Hong Kong or Australia. Will this work in Mint? Probably not. They might support Canada, but will they support accounts in both places? How about some other country I might want to move to? I want a single integrated view of all my accounts across all countries in all currencies forever, nothing less. My system supports that very well. (I\u2019m not aware of any double-entry system that deals with the international problem in a currency agnostic way as well as Beancount does.) Inability to deal with particular situations. What if I own real estate? Will I be able to price the value of my home at a reasonable amount, so it creates an accurate balance sheet? Regularly obtaining \u201ccomparables\u201d for my property from an eager real estate agent will tell me much more precisely how much my home is worth than services like Zillow ever could. I need to be able to input that for my balance sheet. What about those stock options from that privately held Australian company I used to work for? How do I price that? How about other intangible things, such as receivables from a personal loan I made to a friend? I doubt online services are able to provide you with the ability to enter those. If you want the whole picture with precision, you need to be able to make these adjustments. Custom tracking. Using \u201cimaginary currencies\u201d, I\u2019m able to track all kinds of other things than currencies and stocks. For example, by using an \u201c IRAUSD \u201d commodity in Beancount I\u2019m able to track how many 401k contributions I\u2019ve made at any point during the year. I can count the after-tax basis of a traditional IRA account similarly. I can even count my vacation hours using an imaginary \u201c VACHR \u201d currency and verify it against my pay stubs. Capital gains reporting. I haven\u2019t tried it myself, but I\u2019ve heard some discontent about Mint from others about its limited capabilities for capital gains reporting. Will it maintain trade lots? Will it handle average cost booking? How about PnL on FOREX gains? What about revenue received for that book I\u2019m selling via PayPal? I want to be able to use my accounting system as an input for my tax reporting. Cash transactions. How difficult is it to enter cash transactions? Do I have to log in, or start a slow, heavy program that will only work under Windows? With Beancount I bring up a file in a text editor, this is instant and easy. Is it even possible to enter custom cash entries in online services? In other words, I\u2019m a very sophisticated user and yes, a bit of a control freak . I\u2019m not lying about it. I don\u2019t have any ideological objections about using a commercial service, it is probably not for me. If it\u2019s good enough for you, suit yourself. Despite their beautiful and promising websites, I haven\u2019t heard of anyone being completely satisfied with these services. I hear a fair amount of complaining, some mild satisfaction, but I have yet to meet anyone who raves about it.","title":"How about Mint.com?"},{"location":"command_line_accounting_in_context.html#how-about-quicken","text":"Quicken is a single-entry system, that is, it replicates the transactions of a remote account locally, and allows you to add a label to each transaction to place it in a category. I believe it also has support for synchronizing investment accounts. This is not enough for me, I want to track all kinds of things, and I want to use the double-entry method, which provides an intrinsic check that I\u2019ve entered my data correctly. Single-entry accounting is just not good enough if you\u2019ve already crossed the bridge of understanding the double-entry method.","title":"How about Quicken?"},{"location":"command_line_accounting_in_context.html#how-about-quickbooks","text":"So let\u2019s talk about sophisticated software that is good enough for a company. Why wouldn\u2019t I use that instead? If it\u2019s good enough for small businesses, it should be good enough for me, no? There\u2019s Quickbooks and other ones. Why don\u2019t I use them: It costs money. Commercial software comes at a price. Ok, I probably could afford to pay a few hundred dollars per year (Quickbooks 2014 looks like around 300$/year for the full set of features), but I don\u2019t really want to. Platform. These softwares usually run on Microsoft Windows and sometimes on Mac OS X . I\u2019m a software developer, I mostly use Linux , and a Macbook Air for a laptop, on which I get annoyed running anything other than tmux and a web browser . I\u2019m not going to reboot just to enter a quick cash transaction. Slow startup. I cannot speak specifically to Quickbooks\u2019 implementation, but virtually every software suite of commercial scope I\u2019ve had to use had a splash screen and a slow, slow startup that involved initializing tons of plugins. They assume you\u2019re going to spend hours in it, which is reasonable for commercial users, but not for me, if I want to do a quick update of my ledger. UIs are inadequate. I haven\u2019t seen their UI but given the nature of transactions and my desire to input precisely and search quickly and organize things, I want to be able to edit in as text. I imagine it would be inconvenient for me. With Emacs and org-mode , I can easily i-search my way to any transaction within seconds after opening my ledger file. Inflexible. How would I go about re-organizing all my account names? I think I\u2019m still learning about the double-entry bookkeeping method and I have made mistakes in the past, mistakes where I desire to revisit the way I organize my accounts in a hierarchy. With my text file, I was able to safely rename a large number of accounts several times, and evolve my chart-of-accounts to reflect my improving understanding of how to organize my financial tracking system. Text is powerful!","title":"How about Quickbooks?"},{"location":"command_line_accounting_in_context.html#how-about-gnucash","text":"I don\u2019t like UIs; they\u2019re inconvenient. There\u2019s nothing quite like editing a text file if you are a programmer. Moreover, GnuCash does not deal with multiple currencies well. I find bugs in it within the first hour every time I kick the tires on it, which I do every couple of years, out of curiosity. Other programs, such as Skrooge, also take the heavy-handed big UI approach.","title":"How about GnuCash?"},{"location":"command_line_accounting_in_context.html#why-build-a-computer-language","text":"A bookkeeping system provides conditions for a solution that involves a simple computer language for many reasons. Single-entry bookkeeping is largely insufficient if you're trying to track everything holistically. Existing systems either limit themselves to expense categories with little checking beyond \"reconciling\" which sometimes involves freezing the past. If you're not doing the bookkeeping for a company, sometimes just changing the past and fixing the mistakes where they occurred makes more sense. More importantly, the single-entry method leaves us wanting for the natural error-checking mechanism involved in the double-entry system. The problem is also not solvable elegantly by using spreadsheets; the simple data structure that forms the basis of the double-entry system infers either a very sparse spreadsheet with accounts on one dimension and transactions on the other. For real-world usage, this is impractical. Another iteration on this theme would involve inserting the postings with two columns, one with the account and one with the amount, but the varying number of columns and the lookup code makes this inelegant as well. Plus, it's not obvious how you would deal with a large number of currencies. Programs that provide fancy graphical or web-based user interfaces are inevitably awkward, due to the nature of the problem: each transaction is organized by viewing it through the lens of one account's journal, but any of the accounts present in its postings provide equally valid views. Ideally, what you want, is just to look at the transaction. Organizing them for most convenient input has little to do with the order in which they are to be presented. Using text has a lot of advantages: You can easily used search-and-replace and/or sed to make global changes, for example, rename or reorganize your accounts; You can organize the transactions in the order that is most convenient for data entry; There are a number of existing tools to search the text; You can easily write various little tools to spit out the data syntax, i.e., for importing from other file types, or converting from other systems. Text is inherently open , that is the file format is one that you can read your data from and store anywhere else, not a blob of incomprehensible data that becomes unusable when the company that makes the software stops supporting it. Finally, systems that attempt to automate the process of importing all your data from automated sources (e.g., mint.com) have one major shortfall: most often it's not very easy or even possible to add information for accounts that aren't automated. It is my experience that in practice you will have some entries and accounts to track that will not have a nice downloadable file format, or that simply don't have a real-world counterpart. In order to produce a complete view of one's balance sheet, it is important to be able to enter all of an individual's account within a single system. In other words, custom accounts and manually entered transactions do matter a lot. For all the reasons mentioned above, I feel that a computer language is more appropriate to express this data structure than a heavy piece of software with a customized interface. Being able to easily bring up a text file and quickly type in a few lines of text to add a transaction is great\u2013it's fast and easy. The ledger file provides a naturally open way to express one's data, and can be source-controlled or shared between people as well. Multiple files can be merged together, and scripted manipulations on a source file can be used to reorganize one's data history. Furthermore, a read-only web interface that presents the various reports one expects and allows the user to explore different views on the dataset is sufficient for the purpose of viewing the data.","title":"Why build a computer language?"},{"location":"command_line_accounting_in_context.html#advantages-of-command-line-bookkeeping","text":"In summary, there are many advantages to using a command-line accounting system over a commercial package that provides a user-interface: Fast. You don\u2019t have to fire up a slow program with a splash screen that will take a while to initialize in order to add something to your books. Bringing up a text file from a bookmark in your favorite editing program (I use Emacs) is easy and quick. And if you\u2019re normally using a text editor all day long, as many programmers do, you won\u2019t even blink before the file is in front of your eyes. It\u2019s quick and easy. Portable. It will work on all platforms. Beancount is written in Python 3 with some C extensions, and as such will work on Mac, Linux and Windows platforms. I am very careful and determined to keep external dependencies on third-party packages to an absolute minimum in order to avoid installation problems. It should be easy to install and update, and work everywhere the same. Openness. Your data is open, and will remain open forever . I plan on having my corpus of data until the day I die. With an open format you will never end up in a situation where your transactional data is sitting in a binary blob with an unknown format and the software goes unsupported. Furthermore, your data can be converted to other languages easily. You can easily invoke the parser I provide and write a script that spits it out in another program\u2019s input syntax. You can place it under version control. You can entirely reorganize the structure of your accounts by renaming strings with sed. Text is empowering. Customized. You can produce very customized reports, that address exactly the kinds of problems you are having. One complaint I often hear from people about other financial software is that it doesn\u2019t quite do what they want. With command-line accounting systems you can at least write it yourself, as a small extension that uses your corpus of data.","title":"Advantages of Command-Line Bookkeeping"},{"location":"command_line_accounting_in_context.html#why-not-just-use-an-sql-database","text":"I don\u2019t like to reinvent the wheel. If this problem could be solved by filling up an SQL database and then making queries on it, that\u2019s exactly what I would do. Creating a language is a large overhead, it needs to be maintained and evolved, it\u2019s not an easy task, but as it turns out, necessary and justified to solve this problem. The problem is due to a few reasons: Filtering occurs on a two-level data structure of transactions vs. their children postings, and it is inconvenient to represent the data in a single table upon which we could then make manipulations. You cannot simply work only with postings: in order to ensure that the reports balance, you need to be selecting complete transactions. When you select transactions, the semantics is \u201cselect all the postings for which the transaction has this or that property.\u201d One such property is \u201call transactions that have some posting with account X .\u201d These semantics are not obvious to implement with a database. The nature of the data structure makes it inconvenient. The wide variety of directives makes it difficult to design a single elegant table that can accommodate all of their data. Ideally we would want to define two tables for each type of directive: a table that holds all common data (date, source filename & lineno, directive type) and a table to hold the type-specific data. While this is possible, it steps away from the neat structure of a single table of data. Operations on inventories\u2014the data structure that holds the incrementally changing contents of accounts\u2014require special treatment that would be difficult to implement in a database. Lot reductions are constrained against a running inventory of lots, each of which has a specific cost basis. Some lot reductions trigger the merging of lots (for average cost booking). This requires some custom operations on these inventory objects. Aggregations (breakdowns) are hierarchical in nature. The tree-like structure of accounts allows us to perform operations on subtrees of postings. This would also not be easy to implement with tables. Finally, input would be difficult. By defining a language, we side-step the problem of having to build a custom user interface that would allow us to create the data. Nevertheless, I really like the idea of working with databases. A script (bean-sql) is provided to convert the contents of a Beancount file to an SQL database; it\u2019s not super elegant to carry out computations on those tables, but it\u2019s provided nonetheless. You should be able to play around with it, even if operations are difficult. I might even pre-compute some of the operation\u2019s outputs to make it easier to run SQL queries.","title":"Why not just use an SQL database?"},{"location":"command_line_accounting_in_context.html#but-i-just-want-to-do-x","text":"Some may infer that what we\u2019re doing must be terribly complicated given that they envision they might want to use such a system only for a single purpose. But the fact is, how many accounts you decide to track is a personal choice. You can choose to track as little as you want in as little detail as you deem sufficient. For example, if all that you\u2019re interested in is investing, then you can keep books on only your investment accounts. If you\u2019re interested in replacing your usage of Mint.com or Quicken , you can simply just replicate the statements for your credit cards and see your corresponding expenses. The simplicity of having to just replicate the transactions in all your accounts over doing a painful annual \u201cstarting from scratch\u201d evaluation of all your assets and expenses for the purpose of looking at your finances will convince you. Looking at your finances with a spreadsheet will require you to at least copy values from your portfolio. Every time you want to generate the report you\u2019ll have to update the values\u2026 with my method, you just update each account\u2019s full activity, and you can obtain the complete list holdings as a by-product. It\u2019s easy to bring everything up-to-date if you have a systematic method. To those I say: try it out, accounting just for the bits that you\u2019re interested in. Once you\u2019ll have a taste of how the double-entry method works and have learned a little bit about the language syntax, you will want to get a fuller picture. You will get sucked in.","title":"But... I just want to do X?"},{"location":"command_line_accounting_in_context.html#why-am-i-so-excited","text":"Okay, so I\u2019ve become an accounting nerd. I did not ask for it. Just kind-of happened while I wasn\u2019t looking (I still don\u2019t wear brown socks though). Why is it I can\u2019t stop talking about this stuff? I used to have a company. It was an umbrella for doing contract work (yes, I originally had bigger aspirations for it, but it ended up being just that. Bleh.) As a company owner in Canada, I had to periodically make five different kinds of tax installments to the Federal and Provincial governments, some every month, some every three months, and then it varied. An accountant was feeding me the \u201cmagic numbers\u201d to put in the forms, numbers which I was copying like a monkey, without fully understanding how they were going to get used later on. I had to count separately the various expenses I would incur in relation to my work (for deductions), and those which were only personal \u2014I did not have the enlightened view of getting separate credit cards so I was trying to track personal vs. company expenses from the same accounts. I also had to track transfers between company and personal accounts that would later on get reported as dividends or salary income. I often had multiple pending invoices that would take more than two months to get paid, from different clients. It was a demi-hell. You get the idea. I used to try to track all these things manually, using systems composed of little text files and spreadsheets and doing things very carefully and in a particular way. The thing is, although I am by nature quite methodical, I would sometimes, just sometimes forget to update one of the files. Things would fall out of sync. When this happened it was incredibly frustrating, I had to spend time digging around various statements and figure out where I had gone wrong. This was time-consuming and unpleasant. And of course, when something you have to do is unpleasant, you tend not to do it so well. I did not have a solid idea of the current state of affairs of my company during the year, I just worked and earned money for it. My accountant would draw a balance sheet once a year after doing taxes in July, and it was always a tiny bit of a surprise. I felt permanently somewhat confused, and frankly annoyed every time I had to deal with \u201cmoney things.\u201d It wasn\u2019t quite a nightmare, but it was definitely making me tired. I felt my life was so much simpler when I just had a job. But one day\u2026 one day\u2026 I saw the light: I discovered the double-entry method. I don\u2019t recall exactly how. I think it was on a website, yes\u2026 this site: dwmbeancounter.com (it\u2019s a wonderful site). I realized that using a single system , I could account for all of these problems at the same time, and in a way that would impose an inherent error-checking mechanism. I was blown away! I think I printed the whole thing on paper at the time and worked my way through every tutorial. This simple counting trick is exactly what I was in dire need for. But all the software I tried was either disappointing, broken, or too complicated. So I read up on Ledger. And I got in touch with its author . And then shortly after I started on Beancount 1 . I\u2019ll admit that going through the effort of designing my own system just to solve my accounting problems is a bit overkill, but I\u2019ve been known to be a little more than extreme about certain things , and I\u2019ve really enjoyed solving this problem. My life is fulfilled when I maintain a good balance of \u201clearning\u201d and \u201cdoing,\u201d and this falls squarely in the \u201cdoing\u201d domain. Ever since, I feel so excited about anything related to personal finance. Probably because it makes me so happy to have such a level of awareness about what\u2019s going on with mine. I even sometimes find myself loving spending money in a new way, just from knowing that I\u2019ll have to figure out how to account for it later. I feel so elated by the ability to solve these financial puzzles that I have had bouts of engaging complete weekends in building this software. They are small challenges with a truly practical application and a tangible consequence. Instant gratification. I get a feeling of empowerment . And I wish the same for you. For a full list of differences, refer to this document . \u21a9","title":"Why am I so Excited?"},{"location":"exporting_your_portfolio.html","text":"Exporting Your Portfolio \uf0c1 Martin Blais , December 2015 (v2) http://furius.ca/beancount/doc/export Overview \uf0c1 This document explains how to export your portfolio of holdings from Beancount to a Google Finance portfolio (and eventually to other portfolio tracking websites). Note: This is the second version of this document, rewritten in Dec 2015, after greatly simplifying the process of exporting portfolios and completely separating the specification of stock tickers for price downloads. This new, simplified version only uses a single metadata field name: \u201cexport\u201d. The previous document can be found here . Portfolio Tracking Tools \uf0c1 There are multiple websites on the internet that allow someone to create a portfolio of investments (or upload a list of transactions to create such a portfolio) and that reports on the changes in the portfolio due to price movements, shows you unrealized capital gains, etc. One such website is the Google Finance portal. Another example is the Yahoo Finance one. These are convenient because they allow you to monitor the impact of price changes on your entire portfolio of assets, across all accounts, during the day or otherwise. However, each of these sites expects their users to use their interfaces and workflows to painfully enter each of the positions one-by-one. A great advantage of using Beancount is that you should never have to enter this type of information manually; instead, you should be able to extract it and upload it to one of these sites. You can be independent of the particular portfolio tracking service you use and should be able to switch between them without losing any data; Beancount can serve as your pristine source for your list of holdings as your needs evolve. Google Finance supports an \u201cimport\u201d feature to create portfolio data which supports the Microsoft OFX financial interchange data format. In this document, we show how we built a Beancount report that exports the portfolio of holdings to OFX for creating a Google Finance portfolio. Exporting to Google Finance \uf0c1 Exporting your Holdings to OFX \uf0c1 First, create an OFX file corresponding to your Beancount holdings. You can use this command to do this: bean-report file.beancount export_portfolio > portfolio.ofx See the report\u2019s own help for options: bean-report file.beancount export_portfolio --help Importing the OFX File in Google Finance \uf0c1 Then we have to import that OFX file in a web-based portfolio. Visit http://finance.google.com and click on \u201cPortfolios\u201d on the left (or simply visit https://www.google.com/finance/portfolio , this works as of Jan 2015) If you have an existing, previously imported portfolio, click on \u201cDelete Portfolio\u201d to get rid of it. Click on \u201cImport Transactions\u201d, then \u201cChoose File\u201d and select the portfolio.ofx file you exported to, then click on \u201cPreview Import\u201d. You should see a list of imported lots, with familiar stock symbols and names, and Type \u201cBuy\u201d with realistic Shares and Price columns. If not, see the note below. Otherwise, scroll to the bottom of the page and click \u201cImport\u201d. Your portfolio should now appear. You are done. You should never bother updating this portfolio directly using the website\u2026 instead, update your Beancount ledger file, re-export to a new OFX file, delete the previous portfolio and re-import a brand new one over it. Your pristine source is always your Beancount file, ideally you should never have to be worried about corrupting or deleting the portfolio data in any external website. Controlling Exported Commodities \uf0c1 Declaring Your Commodities \uf0c1 Generally, we recommend that you explicitly declare each of the commodities used in your input file. It is a neat place to attach information about those commodities, metadata that you should be able to use later on from bean-query or in scripts that you make. For example, you could declare a human-readable description of the commodity, and some other attributes, like this: 2001-09-06 commodity XIN name: \"iShares MSCI EAFE Index ETF (CAD-Hedged)\" class: \"Stock\" type: \"ETF\" ... Beancount will work with or without these declarations (it automatically generates Commodity directives if you haven\u2019t provided them). If you like to be strict and have a bit of discipline, you can require that each commodity be declared by using a plugin that will issue an error when an undeclared commodity appears: plugin \"beancount.plugins.check_commodity\" You can use any date for that Commodity directive. I recommend using the date of the commodity\u2019s inception, or perhaps when it was first introduced by the issuing country, if it is a currency. You can find a suitable date on Wikipedia or on the issuer\u2019s websites. Google Finance may have the date itself. What Happens by Default \uf0c1 By default, all holdings are exported as positions with a ticker symbol named the same as the Beancount commodity that you used to define them. If you have a holding of \u201cAAPL\u201d units, it will create an export entry for \u201cAAPL\u201d. The export code attempts to export all holdings by default. However, in any but the simplest unambiguous cases, this is probably not good enough to produce a working Google Finance portfolio. The name for each commodity that you use in your Beancount input file may or may not correspond to a financial instrument in the Google Finance database; due to the very large number of symbols supported in its database, just specifying the ticker symbol is often ambiguous. Google Finance attempts to resolve an ambiguous symbol string to the most likely instrument in its database. It is possible that it resolves it to a different financial instrument from the one you intended. So even if you use the same basic symbol that is used by the exchange, you often still need to disambiguate the symbol by specifying which exchange or symbology it lives in. Google provides a list of these symbol spaces . Here is a real-life example. The symbol for the \u201c CAD-Hedged MSCI EAFE Index \u201d ETF product issued by iShares/Blackrock is \u201c XIN \u201d on the Toronto Stock Exchange ( TSE ). If you just looked up \u201cXIN\u201d on Google Finance , it would choose to resolve it by default to the more likely \u201c NYSE:XIN \u201d symbol ( Xinyuan Real Estate Co. on the New York Stock Exchange ). So you need to disambiguate it by specifying that the desired ETF ticker for this instrument is \u201c TSE:XIN \u201d. Explicitly Specifying Exported Symbols \uf0c1 You can specify which exchange-specific symbol is used to export a commodity by attaching an \u201c export \u201d metadata field to each of your Commodity directives, like this: 2001-09-06 commodity XIN ... export: \"TSE:XIN\" The \u201c export \u201d field is used to map your commodity name to the corresponding instrument in the Google Finance system. If a holding in that commodity needs to be exported, this code is used instead of the Beancount currency name. The symbology used by Google Finance appears to follow the following syntax: Exchange:Symbol \uf0c1 where Exchange is a code either for the exchange where the stock trades, or for another source of financial data, e.g. \u201c MUTF \u201d for \u201cmutual funds in the US\u201d, and more . Symbol is a name that is unique within that exchange. I recommend searching for each of your financial instruments in Google Finance, confirming that the instrument corresponds to your instrument (by inspecting the full name, description and price), and inserting the corresponding code like this. Exporting to a Cash Equivalent \uf0c1 To account for positions that aren\u2019t supported in Google Finance, the export report can convert a holding to its cash-equivalent value. This is also useful for cash positions (e.g., cash sitting idle in a savings or checking account). For example, I hold units of an insurance policy investment vehicle (this is common in Canada, for example, with London Life). This is a financial instrument, but each particular policy issuance has its own associated value\u2014there is no public source of data for each of those products, it\u2019s rather opaque, I can obtain its value with my annual statement, but definitely not in Google Finance. But I\u2019d still like for the asset\u2019s value to be reflected in my portfolio. The way you tell the export code to make this conversion is to specify a special value of \u201cCASH\u201d for the \u201cexport\u201d field, like this: 1878-01-01 commodity LDNLIFE export: \"CASH\" This would convert holdings in LDNLIFE commodities to their corresponding quoted value before exporting, using the price nearest to the date of exporting. Note that attempting to convert to cash a commodity that does not have a corresponding cost or price available for us to determine its value will generate an error. A price must be present to make the conversion. Simple currencies should also be marked as cash in order to be exported: 1999-01-01 commodity EUR name: \"European Union Euro currency\" export: \"CASH\" Finally, all converted holdings are agglomerated into a single cash position. There is no point in exporting these cash entries to separate OFX entries because the Google Finance code will agglomerate them to a single one anyhow. Declaring Money Instruments \uf0c1 There is a small hiccup in this cash conversion story: the Google Finance importer does not appear to correctly grok an OFX position in \u201ccash\u201d amounts in the importer; I think this is probably just a bug in Google Finance\u2019s import code (or perhaps I haven\u2019t found the correct OFX field values to make this work). Instead, in order to insert a cash position the exporter uses a cash-equivalent commodity which always prices at 1 unit of the currency, e.g. $1.00 for US dollars. For example, for US dollars I I use VMMXX which is a Vanguard Prime Money Market Fund, and for Canadian dollars I use IGI806 . A good type of commodity for this is some sort of Money Market fund. It doesn\u2019t matter so much which one you use, as long as it prices very close to 1. Find one. If you want to include cash commodities, you need to find such a commodity for each of the cash currencies you have on your books and tell Beancount about them. Typically that will be only one or two currencies. You declare them by append the special value \u201c MONEY \u201d for the \u201c export \u201d field, specifying which currency this commodity represents, like this: 1900-01-01 commodity VMMXX export: \"MUTF:VMMXX (MONEY:USD)\" 1900-01-01 commodity IGI806 export: \"MUTF_CA:IGI806 (MONEY:CAD)\" Ignoring Commodities \uf0c1 Finally, some commodities held in a ledger should be ignored. This is the case for the imaginary commodities used in mirror accounting, for example, to track unvested shares of an employment stock plan, or commodities used to track amounts contributed to a retirement account, like this: 1996-01-01 commodity RSPCAD name: \"Canada Registered Savings Plan Contributions\" You tell the export code to ignore a commodity specifying the special value \u201c IGNORE \u201d for the \u201c export \u201d field, like this: 1996-01-01 commodity RSPCAD name: \"Canada Registered Savings Plan Contributions\" export: \"IGNORE\" All holdings in units of RSPCAD will thus not be exported. The question of whether some commodities should be exported or not sometimes presents interesting choices. Here is an example: I track my accumulated vacation hours in an asset account. The units are \u201c VACHR \u201d. I associate with this commodity a price that is roughly equivalent to my net hourly salary. This gives me a rough idea how much vacation time money is accumulated on the books, e.g. if I quit my job, how much I\u2019d get paid. Do I want to them included in my total net worth? Should the value from those hours be reflected in the value of my exported portfolio? I think that largely depends on whether I plan to use up those vacations before I leave this job or not, whether I want to have this accumulated value show up on my balance sheet. Comparing with Net Worth \uf0c1 The end result is that the sum total of all your exported positions plus the cash position should approximate the value of all your assets, and the total value calculated by the Google Finance website should be very close to the one reported by this report: bean-report file.beancount networth As a point of comparison, the value of my own portfolio is usually close to within a few hundred US dollars. Details of the OFX Export \uf0c1 Import Failures \uf0c1 Exporting a portfolio with symbols that Google Finance does not recognize fatally trips up Google\u2019s import feature. Google Finance then proceeds to fail to recognize your entire file. I recommend that you use explicit exchange:symbol names on all commodities that get exported in order to avoid this problem, as is described further in this document. Google Finance can also be a little bit finicky about the format of the particular OFX file you give it to import. The export_portfolio command attempts to avoid OFX features that would break it but it\u2019s fragile, and it\u2019s possible that the particulars of your portfolio\u2019s contents triggers output that fails to import. If this is the case, at step (4) above, instead of a list of stock symbols you would see a long list of positions that look like XML tags (this is how failure manifests itself). If that is the case, send email to the mailing-list (best if you can isolate the positions that trigger breakage and have the capability to diff files and do some troubleshooting). Mutual Funds vs. Stocks \uf0c1 The OFX format distinguishes between stocks and mutual funds. In practice, the Google Finance importer does not appear to distinguish between these two (at least it appears to behave the same way), so this is likely an irrelevant implementation detail. Nevertheless, the export code is able to honor the OFX convention of distinguishing between \u201cBUYMF\u201d vs. \u201cBUYSTOCK\u201d XML elements. To this effect, the export code attempts to classify which commodities represent mutual funds by inspecting whether the ticker associated with the commodity begins with the letters \u201cMUTF\u201d and is followed by a colon. For example, \u201c MUTF:RGAGX \u201d and \u201c MUTF_CA:RBF1005 \" will both be detected as mutual funds, for example. Debugging the Export \uf0c1 In order to debug how each of your holdings gets exported, use the --debug flag, which will print a detailed account of how each holding is handled by the export script to stderr : bean-report file.beancount export_portfolio --debug 2>&1 >/dev/null | more The script should print the list of exported positions and their corresponding holdings, then the list of converted positions and their corresponding holdings (usually many cash positions are aggregated together) and finally, the list of ignored holdings. This should be enough to explain the entire contents of the exported portfolio. Purchase Dates \uf0c1 Beancount does not currently have all the lot purchase dates, so the purchase dates are exported as if purchased the day before the export. Eventually, when the purchase date is available in Beancount (pending the inventory booking changes ) the actual lot purchase date will probably be used in the export format. However, it\u2019s not yet clear that using the correct date is the right thing to do, because Google Finance might insist on inserting cash for dividends since the reported purchase date\u2026 but Beancount already takes care of inserting a special lot for cash that should already include this. We shall see when we get there. Disable Dividends \uf0c1 Under the \u201cEdit Portfolio\u201d option there is a checkbox that appears to disable the calculation of dividends offered. It would be nice to find a way to automatically disable this checkbox upon import. Automate Upload \uf0c1 It would be nice to automate the replacement of the portfolio with a Python script. Unfortunately, the Google Finance API has been deprecated. Maybe someone can write a screen-scraping routine to do this. Summary \uf0c1 Each holding\u2019s export can be controlled by how its commodity is treated, in one of the following ways: Exported to a portfolio position. This is the default, but you should specify the ticker symbol using the \u201c ticker \u201d or \u201c export \u201d metadata fields, in \u201c ExchangeCode:Symbol \u201d format. Converted to cash and exported to a money market cash-equivalent position, by setting the value of the \u201c export \u201d metadata field to the special value \u201c CASH \u201d. Ignored by specifying the \u201c export \u201d metadata field to the special value \u201c IGNORE \u201d. Provided as Money Instrument , to be used for cash-equivalent value of each holding intended to be converted to cash and included in the portfolio. These are identified by a special value \u201c( MONEY:) \u201d in the \u201c export \u201d metadata field.","title":"Exporting Your Portfolio"},{"location":"exporting_your_portfolio.html#exporting-your-portfolio","text":"Martin Blais , December 2015 (v2) http://furius.ca/beancount/doc/export","title":"Exporting Your Portfolio"},{"location":"exporting_your_portfolio.html#overview","text":"This document explains how to export your portfolio of holdings from Beancount to a Google Finance portfolio (and eventually to other portfolio tracking websites). Note: This is the second version of this document, rewritten in Dec 2015, after greatly simplifying the process of exporting portfolios and completely separating the specification of stock tickers for price downloads. This new, simplified version only uses a single metadata field name: \u201cexport\u201d. The previous document can be found here .","title":"Overview"},{"location":"exporting_your_portfolio.html#portfolio-tracking-tools","text":"There are multiple websites on the internet that allow someone to create a portfolio of investments (or upload a list of transactions to create such a portfolio) and that reports on the changes in the portfolio due to price movements, shows you unrealized capital gains, etc. One such website is the Google Finance portal. Another example is the Yahoo Finance one. These are convenient because they allow you to monitor the impact of price changes on your entire portfolio of assets, across all accounts, during the day or otherwise. However, each of these sites expects their users to use their interfaces and workflows to painfully enter each of the positions one-by-one. A great advantage of using Beancount is that you should never have to enter this type of information manually; instead, you should be able to extract it and upload it to one of these sites. You can be independent of the particular portfolio tracking service you use and should be able to switch between them without losing any data; Beancount can serve as your pristine source for your list of holdings as your needs evolve. Google Finance supports an \u201cimport\u201d feature to create portfolio data which supports the Microsoft OFX financial interchange data format. In this document, we show how we built a Beancount report that exports the portfolio of holdings to OFX for creating a Google Finance portfolio.","title":"Portfolio Tracking Tools"},{"location":"exporting_your_portfolio.html#exporting-to-google-finance","text":"","title":"Exporting to Google Finance"},{"location":"exporting_your_portfolio.html#exporting-your-holdings-to-ofx","text":"First, create an OFX file corresponding to your Beancount holdings. You can use this command to do this: bean-report file.beancount export_portfolio > portfolio.ofx See the report\u2019s own help for options: bean-report file.beancount export_portfolio --help","title":"Exporting your Holdings to OFX"},{"location":"exporting_your_portfolio.html#importing-the-ofx-file-in-google-finance","text":"Then we have to import that OFX file in a web-based portfolio. Visit http://finance.google.com and click on \u201cPortfolios\u201d on the left (or simply visit https://www.google.com/finance/portfolio , this works as of Jan 2015) If you have an existing, previously imported portfolio, click on \u201cDelete Portfolio\u201d to get rid of it. Click on \u201cImport Transactions\u201d, then \u201cChoose File\u201d and select the portfolio.ofx file you exported to, then click on \u201cPreview Import\u201d. You should see a list of imported lots, with familiar stock symbols and names, and Type \u201cBuy\u201d with realistic Shares and Price columns. If not, see the note below. Otherwise, scroll to the bottom of the page and click \u201cImport\u201d. Your portfolio should now appear. You are done. You should never bother updating this portfolio directly using the website\u2026 instead, update your Beancount ledger file, re-export to a new OFX file, delete the previous portfolio and re-import a brand new one over it. Your pristine source is always your Beancount file, ideally you should never have to be worried about corrupting or deleting the portfolio data in any external website.","title":"Importing the OFX File in Google Finance"},{"location":"exporting_your_portfolio.html#controlling-exported-commodities","text":"","title":"Controlling Exported Commodities"},{"location":"exporting_your_portfolio.html#declaring-your-commodities","text":"Generally, we recommend that you explicitly declare each of the commodities used in your input file. It is a neat place to attach information about those commodities, metadata that you should be able to use later on from bean-query or in scripts that you make. For example, you could declare a human-readable description of the commodity, and some other attributes, like this: 2001-09-06 commodity XIN name: \"iShares MSCI EAFE Index ETF (CAD-Hedged)\" class: \"Stock\" type: \"ETF\" ... Beancount will work with or without these declarations (it automatically generates Commodity directives if you haven\u2019t provided them). If you like to be strict and have a bit of discipline, you can require that each commodity be declared by using a plugin that will issue an error when an undeclared commodity appears: plugin \"beancount.plugins.check_commodity\" You can use any date for that Commodity directive. I recommend using the date of the commodity\u2019s inception, or perhaps when it was first introduced by the issuing country, if it is a currency. You can find a suitable date on Wikipedia or on the issuer\u2019s websites. Google Finance may have the date itself.","title":"Declaring Your Commodities"},{"location":"exporting_your_portfolio.html#what-happens-by-default","text":"By default, all holdings are exported as positions with a ticker symbol named the same as the Beancount commodity that you used to define them. If you have a holding of \u201cAAPL\u201d units, it will create an export entry for \u201cAAPL\u201d. The export code attempts to export all holdings by default. However, in any but the simplest unambiguous cases, this is probably not good enough to produce a working Google Finance portfolio. The name for each commodity that you use in your Beancount input file may or may not correspond to a financial instrument in the Google Finance database; due to the very large number of symbols supported in its database, just specifying the ticker symbol is often ambiguous. Google Finance attempts to resolve an ambiguous symbol string to the most likely instrument in its database. It is possible that it resolves it to a different financial instrument from the one you intended. So even if you use the same basic symbol that is used by the exchange, you often still need to disambiguate the symbol by specifying which exchange or symbology it lives in. Google provides a list of these symbol spaces . Here is a real-life example. The symbol for the \u201c CAD-Hedged MSCI EAFE Index \u201d ETF product issued by iShares/Blackrock is \u201c XIN \u201d on the Toronto Stock Exchange ( TSE ). If you just looked up \u201cXIN\u201d on Google Finance , it would choose to resolve it by default to the more likely \u201c NYSE:XIN \u201d symbol ( Xinyuan Real Estate Co. on the New York Stock Exchange ). So you need to disambiguate it by specifying that the desired ETF ticker for this instrument is \u201c TSE:XIN \u201d.","title":"What Happens by Default"},{"location":"exporting_your_portfolio.html#explicitly-specifying-exported-symbols","text":"You can specify which exchange-specific symbol is used to export a commodity by attaching an \u201c export \u201d metadata field to each of your Commodity directives, like this: 2001-09-06 commodity XIN ... export: \"TSE:XIN\" The \u201c export \u201d field is used to map your commodity name to the corresponding instrument in the Google Finance system. If a holding in that commodity needs to be exported, this code is used instead of the Beancount currency name. The symbology used by Google Finance appears to follow the following syntax:","title":"Explicitly Specifying Exported Symbols"},{"location":"exporting_your_portfolio.html#exchangesymbol","text":"where Exchange is a code either for the exchange where the stock trades, or for another source of financial data, e.g. \u201c MUTF \u201d for \u201cmutual funds in the US\u201d, and more . Symbol is a name that is unique within that exchange. I recommend searching for each of your financial instruments in Google Finance, confirming that the instrument corresponds to your instrument (by inspecting the full name, description and price), and inserting the corresponding code like this.","title":"Exchange:Symbol"},{"location":"exporting_your_portfolio.html#exporting-to-a-cash-equivalent","text":"To account for positions that aren\u2019t supported in Google Finance, the export report can convert a holding to its cash-equivalent value. This is also useful for cash positions (e.g., cash sitting idle in a savings or checking account). For example, I hold units of an insurance policy investment vehicle (this is common in Canada, for example, with London Life). This is a financial instrument, but each particular policy issuance has its own associated value\u2014there is no public source of data for each of those products, it\u2019s rather opaque, I can obtain its value with my annual statement, but definitely not in Google Finance. But I\u2019d still like for the asset\u2019s value to be reflected in my portfolio. The way you tell the export code to make this conversion is to specify a special value of \u201cCASH\u201d for the \u201cexport\u201d field, like this: 1878-01-01 commodity LDNLIFE export: \"CASH\" This would convert holdings in LDNLIFE commodities to their corresponding quoted value before exporting, using the price nearest to the date of exporting. Note that attempting to convert to cash a commodity that does not have a corresponding cost or price available for us to determine its value will generate an error. A price must be present to make the conversion. Simple currencies should also be marked as cash in order to be exported: 1999-01-01 commodity EUR name: \"European Union Euro currency\" export: \"CASH\" Finally, all converted holdings are agglomerated into a single cash position. There is no point in exporting these cash entries to separate OFX entries because the Google Finance code will agglomerate them to a single one anyhow.","title":"Exporting to a Cash Equivalent"},{"location":"exporting_your_portfolio.html#declaring-money-instruments","text":"There is a small hiccup in this cash conversion story: the Google Finance importer does not appear to correctly grok an OFX position in \u201ccash\u201d amounts in the importer; I think this is probably just a bug in Google Finance\u2019s import code (or perhaps I haven\u2019t found the correct OFX field values to make this work). Instead, in order to insert a cash position the exporter uses a cash-equivalent commodity which always prices at 1 unit of the currency, e.g. $1.00 for US dollars. For example, for US dollars I I use VMMXX which is a Vanguard Prime Money Market Fund, and for Canadian dollars I use IGI806 . A good type of commodity for this is some sort of Money Market fund. It doesn\u2019t matter so much which one you use, as long as it prices very close to 1. Find one. If you want to include cash commodities, you need to find such a commodity for each of the cash currencies you have on your books and tell Beancount about them. Typically that will be only one or two currencies. You declare them by append the special value \u201c MONEY \u201d for the \u201c export \u201d field, specifying which currency this commodity represents, like this: 1900-01-01 commodity VMMXX export: \"MUTF:VMMXX (MONEY:USD)\" 1900-01-01 commodity IGI806 export: \"MUTF_CA:IGI806 (MONEY:CAD)\"","title":"Declaring Money Instruments"},{"location":"exporting_your_portfolio.html#ignoring-commodities","text":"Finally, some commodities held in a ledger should be ignored. This is the case for the imaginary commodities used in mirror accounting, for example, to track unvested shares of an employment stock plan, or commodities used to track amounts contributed to a retirement account, like this: 1996-01-01 commodity RSPCAD name: \"Canada Registered Savings Plan Contributions\" You tell the export code to ignore a commodity specifying the special value \u201c IGNORE \u201d for the \u201c export \u201d field, like this: 1996-01-01 commodity RSPCAD name: \"Canada Registered Savings Plan Contributions\" export: \"IGNORE\" All holdings in units of RSPCAD will thus not be exported. The question of whether some commodities should be exported or not sometimes presents interesting choices. Here is an example: I track my accumulated vacation hours in an asset account. The units are \u201c VACHR \u201d. I associate with this commodity a price that is roughly equivalent to my net hourly salary. This gives me a rough idea how much vacation time money is accumulated on the books, e.g. if I quit my job, how much I\u2019d get paid. Do I want to them included in my total net worth? Should the value from those hours be reflected in the value of my exported portfolio? I think that largely depends on whether I plan to use up those vacations before I leave this job or not, whether I want to have this accumulated value show up on my balance sheet.","title":"Ignoring Commodities"},{"location":"exporting_your_portfolio.html#comparing-with-net-worth","text":"The end result is that the sum total of all your exported positions plus the cash position should approximate the value of all your assets, and the total value calculated by the Google Finance website should be very close to the one reported by this report: bean-report file.beancount networth As a point of comparison, the value of my own portfolio is usually close to within a few hundred US dollars.","title":"Comparing with Net Worth"},{"location":"exporting_your_portfolio.html#details-of-the-ofx-export","text":"","title":"Details of the OFX Export"},{"location":"exporting_your_portfolio.html#import-failures","text":"Exporting a portfolio with symbols that Google Finance does not recognize fatally trips up Google\u2019s import feature. Google Finance then proceeds to fail to recognize your entire file. I recommend that you use explicit exchange:symbol names on all commodities that get exported in order to avoid this problem, as is described further in this document. Google Finance can also be a little bit finicky about the format of the particular OFX file you give it to import. The export_portfolio command attempts to avoid OFX features that would break it but it\u2019s fragile, and it\u2019s possible that the particulars of your portfolio\u2019s contents triggers output that fails to import. If this is the case, at step (4) above, instead of a list of stock symbols you would see a long list of positions that look like XML tags (this is how failure manifests itself). If that is the case, send email to the mailing-list (best if you can isolate the positions that trigger breakage and have the capability to diff files and do some troubleshooting).","title":"Import Failures"},{"location":"exporting_your_portfolio.html#mutual-funds-vs-stocks","text":"The OFX format distinguishes between stocks and mutual funds. In practice, the Google Finance importer does not appear to distinguish between these two (at least it appears to behave the same way), so this is likely an irrelevant implementation detail. Nevertheless, the export code is able to honor the OFX convention of distinguishing between \u201cBUYMF\u201d vs. \u201cBUYSTOCK\u201d XML elements. To this effect, the export code attempts to classify which commodities represent mutual funds by inspecting whether the ticker associated with the commodity begins with the letters \u201cMUTF\u201d and is followed by a colon. For example, \u201c MUTF:RGAGX \u201d and \u201c MUTF_CA:RBF1005 \" will both be detected as mutual funds, for example.","title":"Mutual Funds vs. Stocks"},{"location":"exporting_your_portfolio.html#debugging-the-export","text":"In order to debug how each of your holdings gets exported, use the --debug flag, which will print a detailed account of how each holding is handled by the export script to stderr : bean-report file.beancount export_portfolio --debug 2>&1 >/dev/null | more The script should print the list of exported positions and their corresponding holdings, then the list of converted positions and their corresponding holdings (usually many cash positions are aggregated together) and finally, the list of ignored holdings. This should be enough to explain the entire contents of the exported portfolio.","title":"Debugging the Export"},{"location":"exporting_your_portfolio.html#purchase-dates","text":"Beancount does not currently have all the lot purchase dates, so the purchase dates are exported as if purchased the day before the export. Eventually, when the purchase date is available in Beancount (pending the inventory booking changes ) the actual lot purchase date will probably be used in the export format. However, it\u2019s not yet clear that using the correct date is the right thing to do, because Google Finance might insist on inserting cash for dividends since the reported purchase date\u2026 but Beancount already takes care of inserting a special lot for cash that should already include this. We shall see when we get there.","title":"Purchase Dates"},{"location":"exporting_your_portfolio.html#disable-dividends","text":"Under the \u201cEdit Portfolio\u201d option there is a checkbox that appears to disable the calculation of dividends offered. It would be nice to find a way to automatically disable this checkbox upon import.","title":"Disable Dividends"},{"location":"exporting_your_portfolio.html#automate-upload","text":"It would be nice to automate the replacement of the portfolio with a Python script. Unfortunately, the Google Finance API has been deprecated. Maybe someone can write a screen-scraping routine to do this.","title":"Automate Upload"},{"location":"exporting_your_portfolio.html#summary","text":"Each holding\u2019s export can be controlled by how its commodity is treated, in one of the following ways: Exported to a portfolio position. This is the default, but you should specify the ticker symbol using the \u201c ticker \u201d or \u201c export \u201d metadata fields, in \u201c ExchangeCode:Symbol \u201d format. Converted to cash and exported to a money market cash-equivalent position, by setting the value of the \u201c export \u201d metadata field to the special value \u201c CASH \u201d. Ignored by specifying the \u201c export \u201d metadata field to the special value \u201c IGNORE \u201d. Provided as Money Instrument , to be used for cash-equivalent value of each holding intended to be converted to cash and included in the portfolio. These are identified by a special value \u201c( MONEY:) \u201d in the \u201c export \u201d metadata field.","title":"Summary"},{"location":"external_contributions.html","text":"External Contributions to Beancount \uf0c1 Martin Blais - Updated: June 2024 http://furius.ca/beancount/doc/contrib Links to codes written by other people that build on top of or that are related to Beancount and/or Ledgerhub. Indexes \uf0c1 This document contains only packages that were discussed or have had an announcement sent to the mailing-list. You will be able to find other packages on public indices: PyPI: You can find a lot of other Beancount-related projects at PyPI . GitHub: A search for \"beancount\" as of September 2020 brings up 318 projects. Books and Articles \uf0c1 Managing Personal Finances using Python (Siddhant Goel): a 2020 book on plain-text accounting, and Beancount. The Five Minute Ledger Update (RedStreet) A series of articles showing how to automate downloading data from institutions (banks, credit cards, brokerages, etc.) so that ledger updates can be done in under five minutes. Mailing list thread . Tax Loss Harvesting with Beancount (RedStreet): An article about TLH from a US perspective, includes requirements, wash sale subtleties and safe to sell/buy dates, and comparisons to robo-advisors. (Related: fava_investor TLH module . for fava and plain-beancount command line version). Scaled Estimates of Mutual Fund NAVs (RedStreet) : Problem: Mutual fund NAVs (at least in the US) are updated exactly once per day, at the end of the day. When needing to make financial decisions when the trading window is still open (eg: when tax loss harvesting), and the end-of-day NAV is not yet available, it is sometimes useful to make a trivial estimate of that NAV, especially on days when there are huge changes in the market. A Shortcut to Scrape Trade History from Fidelity (David Avraamides) I wrote up a description of how I use a Shortcut to scrape trade history from Fidelity\u2019s website, run it through a Python script to convert to Beancount\u2019s ledger format, and then save it in the clipboard so I can paste it into a ledger file. Plugins \uf0c1 split_transactions : Johann Kl\u00e4hn wrote a plugin that can split a single transaction into many against a limbo account, as would be done for depreciation. zerosum : Red S wrote a plugin to match up transactions that when taken together should sum up to zero and move them to a separate account. effective_dates : Red S wrote a plugin to book different legs of a transaction to different dates beancount-plugins : Dave Stephens created a repository to share various of his plugins related to depreciation. beancount-plugins-zack : Stefano Zacchiroli created this repository to share his plugins. Contains sorting of directives and more. b eancount-oneliner : Akuukis created a plugin to write an entry in one line ( PyPi ). beancount-interpolate : Akuukis created plugins for Beancount to interpolate transactions (recur, split, depr, spread) ( PyPi ). metadata-spray : Add metadata across entries by regex expression rather than having explicit entries (by Vivek Gani). Akuukis/beancount_share : A beancount plugin to share expenses among multiple partners within one ledger. This plugin is very powerful and most probably can deal with all of your sharing needs. w1ndy/beancount_balexpr (Di Weng): A plugin that provides \"balance expressions\" to be run against the Beancount entries, as a Custom directive. See this thread . autobean.narration (Archimedes Smith): Allows to annotate each posting in a concise way by populating per-posting narration metadata from inline comments. autobean.sorted : Checks that transactions are in non-descending order in each file. Helps identifying misplaced or misdated directives, by warning on those directives not following a non-descending order by date in the file. hoostus/beancount-asset-transfer-plugin : A plugin to automatically generate in-kind transfers between two beancount accounts, while preserving the cost basis and acquisition date. PhracturedBlue/fava-portfolio-summary (Phractured Blue): Fava Plugin to show portfolio summaries with rate of return. rename_accounts : Plugin from Red S to rename accounts. E.g.: rename \u201cExpenses:Taxes\u201d to \u201cIncome:Taxes\u201d is helpful for expense analysis. More here . Long_short capital gains classifier : Plugin from Red S to classify capital gains into long and short based on duration the assets were held, and into gains and losses based on value. Autoclose_tree : Automatically closes all of an account's descendants when an account is closed. Tools \uf0c1 alfred-beancount (Yue Wu): An add-on to the \u201cAlfred\u201d macOS tool to quickly enter transactions in one\u2019s Beancount file. Supports full account names and payees match. bean-add (Simon Volpert): A Beancount transaction entry assistant. hoostus/fincen_114 (Justus Pendleton): An FBAR / FinCEN 114 report generator. ghislainbourgeois/beancount_portfolio_allocation ( Ghislain Bourgeois ): A quick way to figure out the asset allocations in different portfolios. hoostus/portfolio-returns (Justus Pendleton): portfolio returns calculator costflow/syntax (Leplay Li): A product that allows users to keep plain text accounting from their favorite messaging apps. A syntax for converting one-line message to beancount/*ledger format. process control chart (Justus Pendleton): Spending relative to portfolio size. Thread. Pinto (Sean Leavey): Supercharged command line interface for Beancount. Supports automatic insertions of transactions in ledger file. PhracturedBlue/fava-encrypt : A docker-base solution for keeping Fava online while keeping beancount data encrypted at rest. See this thread for context. kubauk/beancount-import-gmail : beancount-import-gmail uses the gmail API and OAuth to log into your mailbox and download order details which are then used to augment your transactions for easier classification. sulemankm/budget_report : A very simple command-line budget tracking tool for beancount ledger files. dyumnin/dyu_accounting : Accounting setup to automate generation of various financial statements for Compliance with Indian Govt. Gains Minimizer (RedStreet): Automatically determine lots to sell to minimize capital gains taxes. Live example. beanahead (Marcus Read): Adds the ability to include future transactions (automatically generates regular transactions, provides for ad hoc expected transactions, expected transactions are reconciled against imported transactions; all functionality accessible via cli). autobean-format (Archimedes Smith): Yet another formatter for beancount,, powered by earlier project autobean-refactor, a library for parsing and programmatically manipulating beancount files. based on a proper parser, allowing it to format every corner of your ledger, including arithmetic expressions. akirak/flymake-bean-check (Akira Komamura): flymake support for Emacs. bean-download (Red Street): A downloader that ships with beancount-reds-importers that you can configure to run arbitrary commands to download your account statements. It now has a new feature: the needs-update subcommand. gerdemb/beanpost (Ben Gerdemann): Beanpost consists of a PostgreSQL schema and import/export commands that let you transfer data between a beancount file and a PostgreSQL database. Much of Beancount's functionality is implemented using custom PostgreSQL functions, allowing for complex queries and data manipulation. This setup provides a flexible backend that can integrate with other tools like web apps or reporting systems LaunchPlatform/beanhub-cli (Fang-Pen Lin): Command line tools for BeanHub or Beancount users. zacchiro/beangrep : Beangrep is a grep-like filter for the Beancount plain text accounting system. Alternative Parsers \uf0c1 Bison \uf0c1 The Beancount v2 parser uses GNU flex + GNU bison (for maximum portability). The Beancount v3 parser uses RE/flex + GNU bison (for Unicode and C++). Using Antlr \uf0c1 jord1e/jbeancount (Jordie Biemold) / using Antlr: An alternative parser for Beancount input syntax in Java (using the Antlr4 parser generator). This provides access to parsed Beancount data - without the effect of plugins - from JVM languages. See this post for details. Using Tree-sitter \uf0c1 polarmutex/tree-sitter-beancount (Bryan Ryall): A tree-sitter parser for the Beancount syntax. https://github.com/dnicolodi/tree-sitter-beancount (Daniele Nicolodi): Another tree-sitter based parser for the Beancount syntax. In Rust \uf0c1 jcornaz/beancount-parser (Jonathan Cornaz): A beancount file parser library for Rust. Uses nom. beancount_parser_lima (Simon Guest): A zero-copy parser for Beancount in Rust. It is intended to be a complete implementation of the Beancount file format, except for those parts which are deprecated and other features as documented here (in a list which may not be comprehensive). Uses Logos , Chumsky , and Ariadne . Importers \uf0c1 reds importers : Simple importers and tools for several US based institutions, and various file types. Emphasizes ease of writing your own importers by providing well maintained common libraries for banks, credit cards, and investment houses, and for various file types, which minimizes the institution specific code you need to write. This is a reference implementation of the principles expressed in The Five Minute Ledger Update . Contributions welcome. By RedStreet plaid2text : An importer from Plaid which stores the transactions to a Mongo DB and is able to render it to Beancount syntax. By Micah Duke. jbms/beancount-import : A tool for semi-automatically importing transactions from external data sources, with support for merging and reconciling imported transactions with each other and with existing transactions in the beancount journal. The UI is web based. ( Announcement , link to previous version ). By Jeremy Maitin-Shepard. awesome-beancount : A collection of importers for Chinese banks + tips and tricks. By Zhuoyun Wei . beansoup : Filippo Tampieri is sharing some of his Beancount importers and auto-completer in this project. montaropdf/beancount-importers : An importer to extract overtime and vacation from a timesheet format for invoicing customers. siddhantgoel/beancount-dkb (Siddhant Goel): importer for DKB CSV files. prabusw/beancount-importer-zerodha : Importer for the Indian broker Zerodha. swapi/beancount-utils : Another importer for Zerodha. Dr-Nuke/drnuke-bean (Dr Nuke): An importer for IBKR, based on the flex query (API-like) and one for Swiss PostFinance. Beanborg (Luciano Fiandesio): Beanborg automatically imports financial transactions from external CSV files into the Beancount bookkeeping system. szabootibor/beancount-degiro ( PyPI ): Importer for the trading accounts of the Dutch broker Degiro. siddhantgoel/beancount-ing-diba ( PyPI ): ING account importer (NL). PaulsTek/csv2bean : Asimple application to preprocess csv files using google sheets in Go. ericaltendorf/magicbeans (Eric Altendorf): Beancount importers for crypto data. Detailed lot tracking and capital gains/losses reporting for crypto assets. \" I wrote it because I was not satisfied with the accuracy or transparency of existing commercial services for crypto tax reporting.\" OSadovy/uabean (Oleksii Sadovyi): A set of Beancount importers and scripts for popular Ukrainian banks and more. fdavies93/seneca (Frank Davies): Importer for Wise. Multi-currency transfers. LaunchPlatform/beanhub-import : New beancount importer with a UI. rlan/beancount-multitool (Rick Lan): Beancount Multitool is a command-line-interface (CLI) tool that converts financial data from financial institutions to Beancount files (supported: JA Bank \uff2a\uff21\u30cd\u30c3\u30c8\u30d0\u30f3\u30af, Rakuten Card \u697d\u5929\u30ab\u30fc\u30c9, Rakuten Bank \u697d\u5929\u9280\u884c, SBI Shinsei Bank \u65b0\u751f\u9280\u884c). Associated post: https://www.linkedin.com/feed/update/urn:li:activity:7198125470662500352/ LaunchPlatform/beanhub-import (Fang-Pen Lin): Beanhub-import is a simple, declarative, smart, and easy-to-use library for importing extracted transactions from beanhub-extract. It generates Beancount transactions based on predefined rules. Converters \uf0c1 plaid2text : Python Scripts to export Plaid transactions and transform them into Ledger or Beancount syntax formatted files. gnucash-to-beancount : A script from Henrique Bastos to convert a GNUcash SQLite database into an equivalent Beancount input file. debanjum/gnucash-to-beancount : A fork of the above. andrewStein/gnucash-to-beancount : A further fork from the above two, which fixes a lot of issues (see this thread ). hoostus/beancount-ynab : A converter from YNAB to Beancount. hoostus/beancount-ynab5 : Same convert for YNAB from the same author, but for the more recent version 5. ledger2beancount : A script to convert ledger files to beancount. It was developed by Stefano Zacchiroli and Martin Michlmayr. smart_importer : A smart importer for beancount and fava, with intelligent suggestions for account names. By Johannes Harms. beancount-export-patreon.js : JavaScript that will export your Patreon transactions so you can see details of exactly who you've been giving money to. By kanepyork@gmail. alensiljak/pta-converters (Alen \u0160iljak): GnuCash -> Beancount converter (2019). grostim/Beancount-myTools (Timothee Gros): Personal importer tools of the author for French banks. Downloaders \uf0c1 bean-download (RedStreet): bean-download is a tool to conveniently download your transactions from supporting institutions. You configure it with a list of your institutions and arbitrary commands to download them, typically via ofxget . It downloads all of them in parallel, names them appropriately and puts them in the directory of your choice, from which you can then import. The tool is installed as a part of beancount-reds-importers . See accompanying article . ofx-summarize (RedStreet): When building importers, it helps to be able to peek into a .ofx or a .qfx file that you are trying to import. The ofx-summarize command does just that. It ships with beancount-reds-importers , and should be available by simply invoking the command. Running the command on a file shows you a few transactions in the file. What is very useful is to be able to explore your .ofx file via the python debugger or interpreter. Price Sources \uf0c1 hoostus/beancount-price-sources : A Morningstar price fetcher which aggregates multiple exchanges, including non-US ones. andyjscott/beancount-financequote : Finance::Quote support for bean-price. aamerabbas/beancount-coinmarketcap : Price fetcher for coinmarketcap ( see post ). grostim/Beancount-myTools/.../iexcloud.py : Price fetcher for iexcloud by Timothee Gros. xuhcc/beancount-cryptoassets (Kirill Goncharov): Price sources for cryptocurrencies. xuhcc/beancount-ethereum-importer (Kirill Goncharov): Ethereum transaction importer for Beancount. Includes a script that downloads transactions from Etherscan and an importer for downloaded transactions. xuhcc/beancount-exchangerates (Kirill Goncharov): Price source for http://exchangeratesapi.io . tarioch/beancounttools (Patrick Ruckstuhl): Price sources and importers. https://gitlab.com/chrisberkhout/pricehist (Chris Berkhout): A command-line tool that can fetch daily historical prices from multiple sources and output them in several formats. Supports some sources for CoinDesk, European Central Bank, Alpha Vantage, CoinMarketCap. The user can request a specific price type such as high, low, open, close or adjusted close. It can also be used through bean-price. Development \uf0c1 Py3k type annotations : Yuchen Ying is implementing python3 type annotations for Beancount. bryall/tree-sitter-beancount (Bryan Ryall): A tree-sitter parser for the beancount syntax. jmgilman/beancount-stubs : Typing .pyi stubs for some of the Beancount source. Documentation \uf0c1 Beancount Documentation ( Kirill Goncharov ): Official conversion of the Beancount documentation from Google Docs source to Markdown and HTML. This includes most of the Google Docs documents and is maintained in a Beancount org repo here by Kirill Goncharov. Beancount Source Code Documentation ( Dominik Aumayr ): A Sphinx-generated source code documentation of the Beancount codebase. The code to produce this is located here . SQL queries for Beancount (Dominik Aumayr): Example SQL queries. Beancount \u2014\u2014 \u547d\u4ee4\u884c\u590d\u5f0f\u7c3f\u8bb0 (Zhuoyun Wei): A tutorial (blog post) in Chinese on how to use Beancount. Managing my personal finances with Beancount (Alex Johnstone) Counting beans\u2014and more\u2014with Beancount (LWN) Interfaces / Web \uf0c1 fava: A web interface for Beancount (Dominik Aumayr, Jakob Schnitzer): Beancount comes with its own simple web front-end (\u201cbean-web\u201d) intended merely as a thin shell to invoke and display HTML versions of its reports. \u201cFava\u201d is an alternative web application front-end with more & different features, intended initially as a playground and proof-of-concept to explore a newer, better design for presenting the contents of a Beancount file. Fava Classy Portfolio (Vivek Gani): Classy Portfolio is an Extension for Fava, a web interface for the Beancount plaintext accounting software. The extension displays a list of different portfolios (e.g. 'taxable' vs. 'retirement'), with breakdowns using 'asset-class' and 'asset-subclass' metadata labels on commodities. Fava Investor project (RedStreet): Fava_investor aims to be a comprehensive set of reports, analyses, and tools for investments, for Beancount and Fava. It is a collection of modules, with each module offering a Fava plugin, a Beancount library, and a Beancount based CLI (command line interface). Current modules include: Visual, tree structured asset allocation by class, asset allocation by account, tax loss harvester, cash drag analysis. Fava Miler (RedStreet): Airline miles and rewards points: expiration and value reporting. Fava Envelope (Brian Ryall): A beancount fava extension to add an envelope budgeting capability to fava and beancount. It is developed as a Fava plugin and CLI. scauligi/refried (Sunjay Cauligi): An envelope budgeting plugin for Fava, inspired by YNAB: all expense accounts become individual budgeting categories, budgeting is carried out using transactions to these accounts, and the plugin automaticallyapplies a tag to all rebudget transactions so they can easily be filtered out. Provides budget and account views like YNAB. BeanHub.io : A web front-end for Beancount content. \" Since I started using Beancount, I have dreamed of making it fully automatic. For a few years now, I've been building tools for that goal. Connecting to the bank and fetching data directly from there is one of the goals I want to achieve. I built this feature and have been testing it for a while for my accounting book. Now my Beancount books are 80% fully automatic. I can open my repository, and the transactions from the bank will automatically appear as a new commit like this without me lifting a finger. The whole import system is based on our open-source beanhub-import and beanhub-extract. The only proprietary part in the import flow is the Plaid integration. So, suppose you don't trust me and still want to import transactions automatically. As long as you connect to Plaid and write CSV files based on the transactions you fetched from Plaid, you should be able to have the same automatic transaction importing system without using the BeanHub service. \" Blog posts: https://beanhub.io/blog/2024/06/24/introduction-of-beanhub-connect/ https://beanhub.io/blog/2024/04/23/how-beanhub-works-part1-sandboxing/ https://beanhub.io/blog/2024/06/26/how-beanhub-works-part2-layer-based-git-repos/ jmgilman/bdantic : A package for extending beancount with pydantic . With this package you can convert your ledger to JSON, and more. autobean/refactor (Archimedes Smith): Tooling to programmatically edit one's ledger, including formatting, sorting, refactoring, rearranging accounts, optimizing via plugins, migration from v2, inserting transactions in a ledger on import, and more. seltzered/beancolage (Vivek Gani): An Eclipse Theia (vendor-agnostic vscode) app that tries to bundle existing beancount-based packages such as vscode-beancount and Fava. aaronstacy.com/personal-finances-dashboard : HTML + D3.js visualization dashboard for Beancount data. Mobile/Phone Data Entry \uf0c1 Beancount Mobile App (Kirill Goncharov): A mobile data entry app for Beancount. (Currently only Android is supported.) Repo: https://github.com/xuhcc/beancount-mobile ( Announcement ). http://costflow.io : Plain Text Accounting in WeChat. \" Send a message to our bot in Telegram, Facebook Messenger, Whatsapp, LINE, WeChat, etc. Costflow will transform your message into Beancount / Ledger / hledger format transaction magically. Append the transaction to the file in your Dropbox / Google Drive. With the help of their apps, the file will be synced to your computer. \"","title":"External Contributions"},{"location":"external_contributions.html#external-contributions-to-beancount","text":"Martin Blais - Updated: June 2024 http://furius.ca/beancount/doc/contrib Links to codes written by other people that build on top of or that are related to Beancount and/or Ledgerhub.","title":"External Contributions to Beancount"},{"location":"external_contributions.html#indexes","text":"This document contains only packages that were discussed or have had an announcement sent to the mailing-list. You will be able to find other packages on public indices: PyPI: You can find a lot of other Beancount-related projects at PyPI . GitHub: A search for \"beancount\" as of September 2020 brings up 318 projects.","title":"Indexes"},{"location":"external_contributions.html#books-and-articles","text":"Managing Personal Finances using Python (Siddhant Goel): a 2020 book on plain-text accounting, and Beancount. The Five Minute Ledger Update (RedStreet) A series of articles showing how to automate downloading data from institutions (banks, credit cards, brokerages, etc.) so that ledger updates can be done in under five minutes. Mailing list thread . Tax Loss Harvesting with Beancount (RedStreet): An article about TLH from a US perspective, includes requirements, wash sale subtleties and safe to sell/buy dates, and comparisons to robo-advisors. (Related: fava_investor TLH module . for fava and plain-beancount command line version). Scaled Estimates of Mutual Fund NAVs (RedStreet) : Problem: Mutual fund NAVs (at least in the US) are updated exactly once per day, at the end of the day. When needing to make financial decisions when the trading window is still open (eg: when tax loss harvesting), and the end-of-day NAV is not yet available, it is sometimes useful to make a trivial estimate of that NAV, especially on days when there are huge changes in the market. A Shortcut to Scrape Trade History from Fidelity (David Avraamides) I wrote up a description of how I use a Shortcut to scrape trade history from Fidelity\u2019s website, run it through a Python script to convert to Beancount\u2019s ledger format, and then save it in the clipboard so I can paste it into a ledger file.","title":"Books and Articles"},{"location":"external_contributions.html#plugins","text":"split_transactions : Johann Kl\u00e4hn wrote a plugin that can split a single transaction into many against a limbo account, as would be done for depreciation. zerosum : Red S wrote a plugin to match up transactions that when taken together should sum up to zero and move them to a separate account. effective_dates : Red S wrote a plugin to book different legs of a transaction to different dates beancount-plugins : Dave Stephens created a repository to share various of his plugins related to depreciation. beancount-plugins-zack : Stefano Zacchiroli created this repository to share his plugins. Contains sorting of directives and more. b eancount-oneliner : Akuukis created a plugin to write an entry in one line ( PyPi ). beancount-interpolate : Akuukis created plugins for Beancount to interpolate transactions (recur, split, depr, spread) ( PyPi ). metadata-spray : Add metadata across entries by regex expression rather than having explicit entries (by Vivek Gani). Akuukis/beancount_share : A beancount plugin to share expenses among multiple partners within one ledger. This plugin is very powerful and most probably can deal with all of your sharing needs. w1ndy/beancount_balexpr (Di Weng): A plugin that provides \"balance expressions\" to be run against the Beancount entries, as a Custom directive. See this thread . autobean.narration (Archimedes Smith): Allows to annotate each posting in a concise way by populating per-posting narration metadata from inline comments. autobean.sorted : Checks that transactions are in non-descending order in each file. Helps identifying misplaced or misdated directives, by warning on those directives not following a non-descending order by date in the file. hoostus/beancount-asset-transfer-plugin : A plugin to automatically generate in-kind transfers between two beancount accounts, while preserving the cost basis and acquisition date. PhracturedBlue/fava-portfolio-summary (Phractured Blue): Fava Plugin to show portfolio summaries with rate of return. rename_accounts : Plugin from Red S to rename accounts. E.g.: rename \u201cExpenses:Taxes\u201d to \u201cIncome:Taxes\u201d is helpful for expense analysis. More here . Long_short capital gains classifier : Plugin from Red S to classify capital gains into long and short based on duration the assets were held, and into gains and losses based on value. Autoclose_tree : Automatically closes all of an account's descendants when an account is closed.","title":"Plugins"},{"location":"external_contributions.html#tools","text":"alfred-beancount (Yue Wu): An add-on to the \u201cAlfred\u201d macOS tool to quickly enter transactions in one\u2019s Beancount file. Supports full account names and payees match. bean-add (Simon Volpert): A Beancount transaction entry assistant. hoostus/fincen_114 (Justus Pendleton): An FBAR / FinCEN 114 report generator. ghislainbourgeois/beancount_portfolio_allocation ( Ghislain Bourgeois ): A quick way to figure out the asset allocations in different portfolios. hoostus/portfolio-returns (Justus Pendleton): portfolio returns calculator costflow/syntax (Leplay Li): A product that allows users to keep plain text accounting from their favorite messaging apps. A syntax for converting one-line message to beancount/*ledger format. process control chart (Justus Pendleton): Spending relative to portfolio size. Thread. Pinto (Sean Leavey): Supercharged command line interface for Beancount. Supports automatic insertions of transactions in ledger file. PhracturedBlue/fava-encrypt : A docker-base solution for keeping Fava online while keeping beancount data encrypted at rest. See this thread for context. kubauk/beancount-import-gmail : beancount-import-gmail uses the gmail API and OAuth to log into your mailbox and download order details which are then used to augment your transactions for easier classification. sulemankm/budget_report : A very simple command-line budget tracking tool for beancount ledger files. dyumnin/dyu_accounting : Accounting setup to automate generation of various financial statements for Compliance with Indian Govt. Gains Minimizer (RedStreet): Automatically determine lots to sell to minimize capital gains taxes. Live example. beanahead (Marcus Read): Adds the ability to include future transactions (automatically generates regular transactions, provides for ad hoc expected transactions, expected transactions are reconciled against imported transactions; all functionality accessible via cli). autobean-format (Archimedes Smith): Yet another formatter for beancount,, powered by earlier project autobean-refactor, a library for parsing and programmatically manipulating beancount files. based on a proper parser, allowing it to format every corner of your ledger, including arithmetic expressions. akirak/flymake-bean-check (Akira Komamura): flymake support for Emacs. bean-download (Red Street): A downloader that ships with beancount-reds-importers that you can configure to run arbitrary commands to download your account statements. It now has a new feature: the needs-update subcommand. gerdemb/beanpost (Ben Gerdemann): Beanpost consists of a PostgreSQL schema and import/export commands that let you transfer data between a beancount file and a PostgreSQL database. Much of Beancount's functionality is implemented using custom PostgreSQL functions, allowing for complex queries and data manipulation. This setup provides a flexible backend that can integrate with other tools like web apps or reporting systems LaunchPlatform/beanhub-cli (Fang-Pen Lin): Command line tools for BeanHub or Beancount users. zacchiro/beangrep : Beangrep is a grep-like filter for the Beancount plain text accounting system.","title":"Tools"},{"location":"external_contributions.html#alternative-parsers","text":"","title":"Alternative Parsers"},{"location":"external_contributions.html#bison","text":"The Beancount v2 parser uses GNU flex + GNU bison (for maximum portability). The Beancount v3 parser uses RE/flex + GNU bison (for Unicode and C++).","title":"Bison"},{"location":"external_contributions.html#using-antlr","text":"jord1e/jbeancount (Jordie Biemold) / using Antlr: An alternative parser for Beancount input syntax in Java (using the Antlr4 parser generator). This provides access to parsed Beancount data - without the effect of plugins - from JVM languages. See this post for details.","title":"Using Antlr"},{"location":"external_contributions.html#using-tree-sitter","text":"polarmutex/tree-sitter-beancount (Bryan Ryall): A tree-sitter parser for the Beancount syntax. https://github.com/dnicolodi/tree-sitter-beancount (Daniele Nicolodi): Another tree-sitter based parser for the Beancount syntax.","title":"Using Tree-sitter"},{"location":"external_contributions.html#in-rust","text":"jcornaz/beancount-parser (Jonathan Cornaz): A beancount file parser library for Rust. Uses nom. beancount_parser_lima (Simon Guest): A zero-copy parser for Beancount in Rust. It is intended to be a complete implementation of the Beancount file format, except for those parts which are deprecated and other features as documented here (in a list which may not be comprehensive). Uses Logos , Chumsky , and Ariadne .","title":"In Rust"},{"location":"external_contributions.html#importers","text":"reds importers : Simple importers and tools for several US based institutions, and various file types. Emphasizes ease of writing your own importers by providing well maintained common libraries for banks, credit cards, and investment houses, and for various file types, which minimizes the institution specific code you need to write. This is a reference implementation of the principles expressed in The Five Minute Ledger Update . Contributions welcome. By RedStreet plaid2text : An importer from Plaid which stores the transactions to a Mongo DB and is able to render it to Beancount syntax. By Micah Duke. jbms/beancount-import : A tool for semi-automatically importing transactions from external data sources, with support for merging and reconciling imported transactions with each other and with existing transactions in the beancount journal. The UI is web based. ( Announcement , link to previous version ). By Jeremy Maitin-Shepard. awesome-beancount : A collection of importers for Chinese banks + tips and tricks. By Zhuoyun Wei . beansoup : Filippo Tampieri is sharing some of his Beancount importers and auto-completer in this project. montaropdf/beancount-importers : An importer to extract overtime and vacation from a timesheet format for invoicing customers. siddhantgoel/beancount-dkb (Siddhant Goel): importer for DKB CSV files. prabusw/beancount-importer-zerodha : Importer for the Indian broker Zerodha. swapi/beancount-utils : Another importer for Zerodha. Dr-Nuke/drnuke-bean (Dr Nuke): An importer for IBKR, based on the flex query (API-like) and one for Swiss PostFinance. Beanborg (Luciano Fiandesio): Beanborg automatically imports financial transactions from external CSV files into the Beancount bookkeeping system. szabootibor/beancount-degiro ( PyPI ): Importer for the trading accounts of the Dutch broker Degiro. siddhantgoel/beancount-ing-diba ( PyPI ): ING account importer (NL). PaulsTek/csv2bean : Asimple application to preprocess csv files using google sheets in Go. ericaltendorf/magicbeans (Eric Altendorf): Beancount importers for crypto data. Detailed lot tracking and capital gains/losses reporting for crypto assets. \" I wrote it because I was not satisfied with the accuracy or transparency of existing commercial services for crypto tax reporting.\" OSadovy/uabean (Oleksii Sadovyi): A set of Beancount importers and scripts for popular Ukrainian banks and more. fdavies93/seneca (Frank Davies): Importer for Wise. Multi-currency transfers. LaunchPlatform/beanhub-import : New beancount importer with a UI. rlan/beancount-multitool (Rick Lan): Beancount Multitool is a command-line-interface (CLI) tool that converts financial data from financial institutions to Beancount files (supported: JA Bank \uff2a\uff21\u30cd\u30c3\u30c8\u30d0\u30f3\u30af, Rakuten Card \u697d\u5929\u30ab\u30fc\u30c9, Rakuten Bank \u697d\u5929\u9280\u884c, SBI Shinsei Bank \u65b0\u751f\u9280\u884c). Associated post: https://www.linkedin.com/feed/update/urn:li:activity:7198125470662500352/ LaunchPlatform/beanhub-import (Fang-Pen Lin): Beanhub-import is a simple, declarative, smart, and easy-to-use library for importing extracted transactions from beanhub-extract. It generates Beancount transactions based on predefined rules.","title":"Importers"},{"location":"external_contributions.html#converters","text":"plaid2text : Python Scripts to export Plaid transactions and transform them into Ledger or Beancount syntax formatted files. gnucash-to-beancount : A script from Henrique Bastos to convert a GNUcash SQLite database into an equivalent Beancount input file. debanjum/gnucash-to-beancount : A fork of the above. andrewStein/gnucash-to-beancount : A further fork from the above two, which fixes a lot of issues (see this thread ). hoostus/beancount-ynab : A converter from YNAB to Beancount. hoostus/beancount-ynab5 : Same convert for YNAB from the same author, but for the more recent version 5. ledger2beancount : A script to convert ledger files to beancount. It was developed by Stefano Zacchiroli and Martin Michlmayr. smart_importer : A smart importer for beancount and fava, with intelligent suggestions for account names. By Johannes Harms. beancount-export-patreon.js : JavaScript that will export your Patreon transactions so you can see details of exactly who you've been giving money to. By kanepyork@gmail. alensiljak/pta-converters (Alen \u0160iljak): GnuCash -> Beancount converter (2019). grostim/Beancount-myTools (Timothee Gros): Personal importer tools of the author for French banks.","title":"Converters"},{"location":"external_contributions.html#downloaders","text":"bean-download (RedStreet): bean-download is a tool to conveniently download your transactions from supporting institutions. You configure it with a list of your institutions and arbitrary commands to download them, typically via ofxget . It downloads all of them in parallel, names them appropriately and puts them in the directory of your choice, from which you can then import. The tool is installed as a part of beancount-reds-importers . See accompanying article . ofx-summarize (RedStreet): When building importers, it helps to be able to peek into a .ofx or a .qfx file that you are trying to import. The ofx-summarize command does just that. It ships with beancount-reds-importers , and should be available by simply invoking the command. Running the command on a file shows you a few transactions in the file. What is very useful is to be able to explore your .ofx file via the python debugger or interpreter.","title":"Downloaders"},{"location":"external_contributions.html#price-sources","text":"hoostus/beancount-price-sources : A Morningstar price fetcher which aggregates multiple exchanges, including non-US ones. andyjscott/beancount-financequote : Finance::Quote support for bean-price. aamerabbas/beancount-coinmarketcap : Price fetcher for coinmarketcap ( see post ). grostim/Beancount-myTools/.../iexcloud.py : Price fetcher for iexcloud by Timothee Gros. xuhcc/beancount-cryptoassets (Kirill Goncharov): Price sources for cryptocurrencies. xuhcc/beancount-ethereum-importer (Kirill Goncharov): Ethereum transaction importer for Beancount. Includes a script that downloads transactions from Etherscan and an importer for downloaded transactions. xuhcc/beancount-exchangerates (Kirill Goncharov): Price source for http://exchangeratesapi.io . tarioch/beancounttools (Patrick Ruckstuhl): Price sources and importers. https://gitlab.com/chrisberkhout/pricehist (Chris Berkhout): A command-line tool that can fetch daily historical prices from multiple sources and output them in several formats. Supports some sources for CoinDesk, European Central Bank, Alpha Vantage, CoinMarketCap. The user can request a specific price type such as high, low, open, close or adjusted close. It can also be used through bean-price.","title":"Price Sources"},{"location":"external_contributions.html#development","text":"Py3k type annotations : Yuchen Ying is implementing python3 type annotations for Beancount. bryall/tree-sitter-beancount (Bryan Ryall): A tree-sitter parser for the beancount syntax. jmgilman/beancount-stubs : Typing .pyi stubs for some of the Beancount source.","title":"Development"},{"location":"external_contributions.html#documentation","text":"Beancount Documentation ( Kirill Goncharov ): Official conversion of the Beancount documentation from Google Docs source to Markdown and HTML. This includes most of the Google Docs documents and is maintained in a Beancount org repo here by Kirill Goncharov. Beancount Source Code Documentation ( Dominik Aumayr ): A Sphinx-generated source code documentation of the Beancount codebase. The code to produce this is located here . SQL queries for Beancount (Dominik Aumayr): Example SQL queries. Beancount \u2014\u2014 \u547d\u4ee4\u884c\u590d\u5f0f\u7c3f\u8bb0 (Zhuoyun Wei): A tutorial (blog post) in Chinese on how to use Beancount. Managing my personal finances with Beancount (Alex Johnstone) Counting beans\u2014and more\u2014with Beancount (LWN)","title":"Documentation"},{"location":"external_contributions.html#interfaces-web","text":"fava: A web interface for Beancount (Dominik Aumayr, Jakob Schnitzer): Beancount comes with its own simple web front-end (\u201cbean-web\u201d) intended merely as a thin shell to invoke and display HTML versions of its reports. \u201cFava\u201d is an alternative web application front-end with more & different features, intended initially as a playground and proof-of-concept to explore a newer, better design for presenting the contents of a Beancount file. Fava Classy Portfolio (Vivek Gani): Classy Portfolio is an Extension for Fava, a web interface for the Beancount plaintext accounting software. The extension displays a list of different portfolios (e.g. 'taxable' vs. 'retirement'), with breakdowns using 'asset-class' and 'asset-subclass' metadata labels on commodities. Fava Investor project (RedStreet): Fava_investor aims to be a comprehensive set of reports, analyses, and tools for investments, for Beancount and Fava. It is a collection of modules, with each module offering a Fava plugin, a Beancount library, and a Beancount based CLI (command line interface). Current modules include: Visual, tree structured asset allocation by class, asset allocation by account, tax loss harvester, cash drag analysis. Fava Miler (RedStreet): Airline miles and rewards points: expiration and value reporting. Fava Envelope (Brian Ryall): A beancount fava extension to add an envelope budgeting capability to fava and beancount. It is developed as a Fava plugin and CLI. scauligi/refried (Sunjay Cauligi): An envelope budgeting plugin for Fava, inspired by YNAB: all expense accounts become individual budgeting categories, budgeting is carried out using transactions to these accounts, and the plugin automaticallyapplies a tag to all rebudget transactions so they can easily be filtered out. Provides budget and account views like YNAB. BeanHub.io : A web front-end for Beancount content. \" Since I started using Beancount, I have dreamed of making it fully automatic. For a few years now, I've been building tools for that goal. Connecting to the bank and fetching data directly from there is one of the goals I want to achieve. I built this feature and have been testing it for a while for my accounting book. Now my Beancount books are 80% fully automatic. I can open my repository, and the transactions from the bank will automatically appear as a new commit like this without me lifting a finger. The whole import system is based on our open-source beanhub-import and beanhub-extract. The only proprietary part in the import flow is the Plaid integration. So, suppose you don't trust me and still want to import transactions automatically. As long as you connect to Plaid and write CSV files based on the transactions you fetched from Plaid, you should be able to have the same automatic transaction importing system without using the BeanHub service. \" Blog posts: https://beanhub.io/blog/2024/06/24/introduction-of-beanhub-connect/ https://beanhub.io/blog/2024/04/23/how-beanhub-works-part1-sandboxing/ https://beanhub.io/blog/2024/06/26/how-beanhub-works-part2-layer-based-git-repos/ jmgilman/bdantic : A package for extending beancount with pydantic . With this package you can convert your ledger to JSON, and more. autobean/refactor (Archimedes Smith): Tooling to programmatically edit one's ledger, including formatting, sorting, refactoring, rearranging accounts, optimizing via plugins, migration from v2, inserting transactions in a ledger on import, and more. seltzered/beancolage (Vivek Gani): An Eclipse Theia (vendor-agnostic vscode) app that tries to bundle existing beancount-based packages such as vscode-beancount and Fava. aaronstacy.com/personal-finances-dashboard : HTML + D3.js visualization dashboard for Beancount data.","title":"Interfaces / Web"},{"location":"external_contributions.html#mobilephone-data-entry","text":"Beancount Mobile App (Kirill Goncharov): A mobile data entry app for Beancount. (Currently only Android is supported.) Repo: https://github.com/xuhcc/beancount-mobile ( Announcement ). http://costflow.io : Plain Text Accounting in WeChat. \" Send a message to our bot in Telegram, Facebook Messenger, Whatsapp, LINE, WeChat, etc. Costflow will transform your message into Beancount / Ledger / hledger format transaction magically. Append the transaction to the file in your Dropbox / Google Drive. With the help of their apps, the file will be synced to your computer. \"","title":"Mobile/Phone Data Entry"},{"location":"fetching_prices_in_beancount.html","text":"Prices in Beancount \uf0c1 Martin Blais , December 2015 http://furius.ca/beancount/doc/prices Introduction \uf0c1 Processing a Beancount file is, by definition, constrained to the contents of the file itself. In particular, the latest prices of commodities are never fetched automatically from the internet. This is by design, so that any run of a report is deterministic and can also be run offline. No surprises. However, we do need access to price information in order to compute market values of assets. To this end, Beancount provides a Price directive which can be used to fill its in-memory price database by inserting these price points inline in the input file: 2015-11-20 price ITOT 95.46 USD 2015-11-20 price LQD 115.63 USD 2015-11-21 price USD 1.33495 CAD \u2026 Of course, you could do this manually, looking up the prices online and writing the directives yourself. But for assets which are traded publicly you can automate it, by invoking some code that will download prices and write out the directives for you. Beancount comes with some tools to help you do this. This document describes these tools. The Problem \uf0c1 In the context of maintaining a Beancount file, we have a few particular needs to address. First, we cannot expect the user to always update the input file in a timely manner. This means that we have to be able to fetch not only the latest prices, but also historical prices, from the past. Beancount provides an interface to provide these prices and a few implementations (fetching from Yahoo! Finance and from Google Finance). Second, we don't want to store too many prices for holdings which aren't relevant to us at a particular point in time. The list of assets held in a file varies over time. Ideally we would want to include the list of prices for just those assets, while they are held. The Beancount price maintenance tool is able to figure out which commodities it needs to fetch prices for at a particular date. Third, we don't want to fetch the same price if it already appears in the input file. The tools detect this and skip those prices. There's also a caching mechanism that avoids redundant network calls. Finally, while we provide some basic implementations of price sources, we cannot provide such codes for all possible online sources and websites. The problem is analogous to that of importing and extracting data from various institutions. In order to address that, we provide a mechanism for extensibility , a way that you can implement your own price source fetcher in a Python module and point to it from your input file by specifying the module's name as the source for that commodity. The \u201cbean-price\u201d Tool \uf0c1 Beancount comes with a \u201cbean-price\u201d command-line tool that integrates the ideas above. By default, this script accepts a list of Beancount input filenames, and fetches prices required to compute latest market values for current positions held in accounts: bean-price /home/joe/finances/joe.beancount It is also possible to provide a list of specific price fetching jobs to run, e.g., bean-price -e USD:google/TSE:XUS CAD:mysources.morningstar/RBF1005 These jobs are run concurrently so it should be fairly fast. Source Strings \uf0c1 The general format of each of these \"job source strings\" is :/[^] For example: USD:beancount.prices.sources.google/NASDAQ:AAPL The \u201cquote-currency\u201d is the currency the Commodity is quoted in. For example, shares of Apple are quoted in US dollars. The \"module\" is the name of a Python module that contains a Source class which can be instantiated and which connect to a data source to extract price data. These modules are automatically imported and a Source class therein instantiated in order to pull the price from the particular online source they support. This allows you to write your own fetcher codes without having to modify this script. Your code can be placed anywhere on your Python PYTHONPATH and you should not have to modify Beancount itself for this to work. The \u201csymbol\u201d is a string that is fed to the price fetcher to lookup the currency. For example, Apple shares trade on the Nasdaq, and the corresponding symbol in the Google Finance source is \u201cNASDAQ:AAPL\u201d. Other price sources may have a different symbology, e.g., some may require the asset's CUSIP. Default implementations of price sources are provided; we provide fetchers for Yahoo! Finance or Google Finance, which cover a large universe of common public investment types (e.g. stock and some mutual funds). As a convenience, the module name is always first searched under the \" beancount.prices.sources \" package, where those implementations live. This is how, for example, in order to use the provided Yahoo! Finance data fetcher you don't have to write all of \" beancount.prices.sources.yahoo/AAPL \" but you can simply use \" yahoo/AAPL \". Fallback Sources \uf0c1 In practice, fetching prices online often fails. Data sources typically only support some limited number of assets and even then, the support may vary between assets. As an example, Google Finance supports historical prices for stocks, but does not return historical prices for currency instruments (these restrictions may be more related to contractual arrangements between them and the data providers upstream than with technical limitations). To this extent, a source string may provide multiple sources for the data, separated with commas. For example: USD:google/CURRENCY:GBPUSD,yahoo/GBPUSD Each source is tried in turn, and if one fails to return a valid price, the next source is tried as a fallback. The hope is that at least one of the specified sources will work out. Inverted Prices \uf0c1 Sometimes, prices are only available for the inverse of an instrument. This is often the case for currencies. For example, the price of Canadian dollars quoted in US dollars is provided by the USD/CAD market, which gives the price of a US dollar in Canadian dollars (the inverse). In order use this, you can prepend \" ^ \" to the instrument name to instruct the tool to compute the inverse of the fetched price: USD:google/^CURRENCY:USDCAD If a source price is to be inverted, like this, the precision could be different than what is fetched. For instance, if the price of USD/CAD is 1.32759, for the above directive to price the \u201cCAD\u201d instrument it would output this: 2015-10-28 price CAD 0.753244601119 USD By default, inverted rates will be rounded similarly to how other Price directives were rounding those numbers. As you may now, Beancount's in-memory price database works in both directions (the reciprocals of all rates are stored automatically). So if you prefer to have the output Price entries with swapped currencies instead of inverting the rate number itself, you can use the --swap-inverted option. In the previous example for the price of CAD, it would output this: 2015-10-28 price USD 1.32759 CAD Date \uf0c1 By default, the latest prices for the assets are pulled in. You can use an option to fetch prices for a desired date in the past instead: bean-price --date=2015-02-03 \u2026 If you are using an input file to specify the list of prices to be fetched, the tool will figure out the list of assets held on the books at that time and fetch historical prices for those assets only. Caching \uf0c1 Prices are automatically cached (if current and latest, prices are cached for only a short period of time, about half an hour). This is convenient when you're having to run the script multiple times in a row for troubleshooting. You can disable the cache with an option: bean-price --no-cache You can also instruct the script to clear the cache before fetching its prices: bean-price --clear-cache Prices from a Beancount Input File \uf0c1 Generally, one uses a Beancount input file to specify the list of currencies to fetch. In order to do that, you should have Commodity directives in your input file for each of the currencies you mean to fetch prices for, like this: 2007-07-20 commodity VEA price: \"USD:google/NYSEARCA:VEA\" The \"price\" metadata should contain a list of price source strings. For example, a stock product might look like this: 2007-07-20 commodity CAD price: \"USD:google/CURRENCY:USDCAD,yahoo/USDCAD\" While a currency may have multiple target currencies it needs to get converted to: 1990-01-01 commodity GBP price: \"USD:yahoo/GBPUSD CAD:yahoo/GBPCAD CHF:yahoo/GBPCHF\" Which Assets are Fetched \uf0c1 There are many ways to compute a list of commodities with needed prices from a Beancount input file: Commodity directives. The list of all Commodity directives with \u201cprice\u201d metadata present in the file. For each of those holdings, the directive is consulted and its \" price \" metadata field is used to specify where to fetch prices from. Commodities of assets held at cost. Prices for all the holdings that were seen held at cost at a particular date. Because these holdings are held at cost, we can assume there is a corresponding time-varying price for their commodity. Converted commodities. Prices for holdings which were price-converted from some other commodity in the past (i.e., converting some cash in a currency from another). By default, the list of tickers to be fetched includes only the intersection of these three lists. This is because the most common usage of this script is to fetch missing prices for a particular date, and only the needed ones. Inactive commodities. You can use the \u201c --inactive \u201d option to fetch the entire set of prices from (1), regardless of asset holdings determined in (2) and (3). Undeclared commodities. Commodities without a corresponding \u201cCommodity\u201d directive will be ignored by default. To include the full list of commodities seen in an input file, use the \u201c --undeclared \u201d option. Clobber. Existing price directives for the same data are excluded by default, since the price is already in the file. You can use \u201c --clobber \u201d to ignore existing price directives and avoid filtering out what is fetched. Finally, you can use \u201c--all\u201d to include inactive and undeclared commodities and allow clobbering existing ones. You probably don't want to use that other than for testing. If you'd like to do some troubleshooting and print out the list of seen commodities, use the \u201c --verbose \u201d option twice, i.e., \u201c -vv \u201d. You can also just print the list of prices to be fetched with the \u201c --dry-run \u201d option, which stops short of actually fetching the missing prices. Conclusion \uf0c1 Writing Your Own Script \uf0c1 If the workflow defined by this tool does not fit your needs and you would like to cook up your own script, you should not have to start from scratch; you should be able to reuse the existing price fetching code to do that. I'm hoping to provide a few examples of such scripts in the experiments directory. For example, given an existing file it would be convenient to fetch all prices of assets every Friday in order to fill up a history of missing prices. Another example would be to fetch all price directives required in order to correctly compute investment returns in the presence of contributions and distributions. Contributions \uf0c1 If this workflow suits your needs well and you'd like to contribute some price source fetcher to Beancount, please contact the mailing-list. I'm open to including very general fetchers that have been very carefully unit-tested and used for a while.","title":"Fetching Prices in Beancount"},{"location":"fetching_prices_in_beancount.html#prices-in-beancount","text":"Martin Blais , December 2015 http://furius.ca/beancount/doc/prices","title":"Prices in Beancount"},{"location":"fetching_prices_in_beancount.html#introduction","text":"Processing a Beancount file is, by definition, constrained to the contents of the file itself. In particular, the latest prices of commodities are never fetched automatically from the internet. This is by design, so that any run of a report is deterministic and can also be run offline. No surprises. However, we do need access to price information in order to compute market values of assets. To this end, Beancount provides a Price directive which can be used to fill its in-memory price database by inserting these price points inline in the input file: 2015-11-20 price ITOT 95.46 USD 2015-11-20 price LQD 115.63 USD 2015-11-21 price USD 1.33495 CAD \u2026 Of course, you could do this manually, looking up the prices online and writing the directives yourself. But for assets which are traded publicly you can automate it, by invoking some code that will download prices and write out the directives for you. Beancount comes with some tools to help you do this. This document describes these tools.","title":"Introduction"},{"location":"fetching_prices_in_beancount.html#the-problem","text":"In the context of maintaining a Beancount file, we have a few particular needs to address. First, we cannot expect the user to always update the input file in a timely manner. This means that we have to be able to fetch not only the latest prices, but also historical prices, from the past. Beancount provides an interface to provide these prices and a few implementations (fetching from Yahoo! Finance and from Google Finance). Second, we don't want to store too many prices for holdings which aren't relevant to us at a particular point in time. The list of assets held in a file varies over time. Ideally we would want to include the list of prices for just those assets, while they are held. The Beancount price maintenance tool is able to figure out which commodities it needs to fetch prices for at a particular date. Third, we don't want to fetch the same price if it already appears in the input file. The tools detect this and skip those prices. There's also a caching mechanism that avoids redundant network calls. Finally, while we provide some basic implementations of price sources, we cannot provide such codes for all possible online sources and websites. The problem is analogous to that of importing and extracting data from various institutions. In order to address that, we provide a mechanism for extensibility , a way that you can implement your own price source fetcher in a Python module and point to it from your input file by specifying the module's name as the source for that commodity.","title":"The Problem"},{"location":"fetching_prices_in_beancount.html#the-bean-price-tool","text":"Beancount comes with a \u201cbean-price\u201d command-line tool that integrates the ideas above. By default, this script accepts a list of Beancount input filenames, and fetches prices required to compute latest market values for current positions held in accounts: bean-price /home/joe/finances/joe.beancount It is also possible to provide a list of specific price fetching jobs to run, e.g., bean-price -e USD:google/TSE:XUS CAD:mysources.morningstar/RBF1005 These jobs are run concurrently so it should be fairly fast.","title":"The \u201cbean-price\u201d Tool"},{"location":"fetching_prices_in_beancount.html#source-strings","text":"The general format of each of these \"job source strings\" is :/[^] For example: USD:beancount.prices.sources.google/NASDAQ:AAPL The \u201cquote-currency\u201d is the currency the Commodity is quoted in. For example, shares of Apple are quoted in US dollars. The \"module\" is the name of a Python module that contains a Source class which can be instantiated and which connect to a data source to extract price data. These modules are automatically imported and a Source class therein instantiated in order to pull the price from the particular online source they support. This allows you to write your own fetcher codes without having to modify this script. Your code can be placed anywhere on your Python PYTHONPATH and you should not have to modify Beancount itself for this to work. The \u201csymbol\u201d is a string that is fed to the price fetcher to lookup the currency. For example, Apple shares trade on the Nasdaq, and the corresponding symbol in the Google Finance source is \u201cNASDAQ:AAPL\u201d. Other price sources may have a different symbology, e.g., some may require the asset's CUSIP. Default implementations of price sources are provided; we provide fetchers for Yahoo! Finance or Google Finance, which cover a large universe of common public investment types (e.g. stock and some mutual funds). As a convenience, the module name is always first searched under the \" beancount.prices.sources \" package, where those implementations live. This is how, for example, in order to use the provided Yahoo! Finance data fetcher you don't have to write all of \" beancount.prices.sources.yahoo/AAPL \" but you can simply use \" yahoo/AAPL \".","title":"Source Strings"},{"location":"fetching_prices_in_beancount.html#fallback-sources","text":"In practice, fetching prices online often fails. Data sources typically only support some limited number of assets and even then, the support may vary between assets. As an example, Google Finance supports historical prices for stocks, but does not return historical prices for currency instruments (these restrictions may be more related to contractual arrangements between them and the data providers upstream than with technical limitations). To this extent, a source string may provide multiple sources for the data, separated with commas. For example: USD:google/CURRENCY:GBPUSD,yahoo/GBPUSD Each source is tried in turn, and if one fails to return a valid price, the next source is tried as a fallback. The hope is that at least one of the specified sources will work out.","title":"Fallback Sources"},{"location":"fetching_prices_in_beancount.html#inverted-prices","text":"Sometimes, prices are only available for the inverse of an instrument. This is often the case for currencies. For example, the price of Canadian dollars quoted in US dollars is provided by the USD/CAD market, which gives the price of a US dollar in Canadian dollars (the inverse). In order use this, you can prepend \" ^ \" to the instrument name to instruct the tool to compute the inverse of the fetched price: USD:google/^CURRENCY:USDCAD If a source price is to be inverted, like this, the precision could be different than what is fetched. For instance, if the price of USD/CAD is 1.32759, for the above directive to price the \u201cCAD\u201d instrument it would output this: 2015-10-28 price CAD 0.753244601119 USD By default, inverted rates will be rounded similarly to how other Price directives were rounding those numbers. As you may now, Beancount's in-memory price database works in both directions (the reciprocals of all rates are stored automatically). So if you prefer to have the output Price entries with swapped currencies instead of inverting the rate number itself, you can use the --swap-inverted option. In the previous example for the price of CAD, it would output this: 2015-10-28 price USD 1.32759 CAD","title":"Inverted Prices"},{"location":"fetching_prices_in_beancount.html#date","text":"By default, the latest prices for the assets are pulled in. You can use an option to fetch prices for a desired date in the past instead: bean-price --date=2015-02-03 \u2026 If you are using an input file to specify the list of prices to be fetched, the tool will figure out the list of assets held on the books at that time and fetch historical prices for those assets only.","title":"Date"},{"location":"fetching_prices_in_beancount.html#caching","text":"Prices are automatically cached (if current and latest, prices are cached for only a short period of time, about half an hour). This is convenient when you're having to run the script multiple times in a row for troubleshooting. You can disable the cache with an option: bean-price --no-cache You can also instruct the script to clear the cache before fetching its prices: bean-price --clear-cache","title":"Caching"},{"location":"fetching_prices_in_beancount.html#prices-from-a-beancount-input-file","text":"Generally, one uses a Beancount input file to specify the list of currencies to fetch. In order to do that, you should have Commodity directives in your input file for each of the currencies you mean to fetch prices for, like this: 2007-07-20 commodity VEA price: \"USD:google/NYSEARCA:VEA\" The \"price\" metadata should contain a list of price source strings. For example, a stock product might look like this: 2007-07-20 commodity CAD price: \"USD:google/CURRENCY:USDCAD,yahoo/USDCAD\" While a currency may have multiple target currencies it needs to get converted to: 1990-01-01 commodity GBP price: \"USD:yahoo/GBPUSD CAD:yahoo/GBPCAD CHF:yahoo/GBPCHF\"","title":"Prices from a Beancount Input File"},{"location":"fetching_prices_in_beancount.html#which-assets-are-fetched","text":"There are many ways to compute a list of commodities with needed prices from a Beancount input file: Commodity directives. The list of all Commodity directives with \u201cprice\u201d metadata present in the file. For each of those holdings, the directive is consulted and its \" price \" metadata field is used to specify where to fetch prices from. Commodities of assets held at cost. Prices for all the holdings that were seen held at cost at a particular date. Because these holdings are held at cost, we can assume there is a corresponding time-varying price for their commodity. Converted commodities. Prices for holdings which were price-converted from some other commodity in the past (i.e., converting some cash in a currency from another). By default, the list of tickers to be fetched includes only the intersection of these three lists. This is because the most common usage of this script is to fetch missing prices for a particular date, and only the needed ones. Inactive commodities. You can use the \u201c --inactive \u201d option to fetch the entire set of prices from (1), regardless of asset holdings determined in (2) and (3). Undeclared commodities. Commodities without a corresponding \u201cCommodity\u201d directive will be ignored by default. To include the full list of commodities seen in an input file, use the \u201c --undeclared \u201d option. Clobber. Existing price directives for the same data are excluded by default, since the price is already in the file. You can use \u201c --clobber \u201d to ignore existing price directives and avoid filtering out what is fetched. Finally, you can use \u201c--all\u201d to include inactive and undeclared commodities and allow clobbering existing ones. You probably don't want to use that other than for testing. If you'd like to do some troubleshooting and print out the list of seen commodities, use the \u201c --verbose \u201d option twice, i.e., \u201c -vv \u201d. You can also just print the list of prices to be fetched with the \u201c --dry-run \u201d option, which stops short of actually fetching the missing prices.","title":"Which Assets are Fetched"},{"location":"fetching_prices_in_beancount.html#conclusion","text":"","title":"Conclusion"},{"location":"fetching_prices_in_beancount.html#writing-your-own-script","text":"If the workflow defined by this tool does not fit your needs and you would like to cook up your own script, you should not have to start from scratch; you should be able to reuse the existing price fetching code to do that. I'm hoping to provide a few examples of such scripts in the experiments directory. For example, given an existing file it would be convenient to fetch all prices of assets every Friday in order to fill up a history of missing prices. Another example would be to fetch all price directives required in order to correctly compute investment returns in the presence of contributions and distributions.","title":"Writing Your Own Script"},{"location":"fetching_prices_in_beancount.html#contributions","text":"If this workflow suits your needs well and you'd like to contribute some price source fetcher to Beancount, please contact the mailing-list. I'm open to including very general fetchers that have been very carefully unit-tested and used for a while.","title":"Contributions"},{"location":"fund_accounting_with_beancount.html","text":"Fund Accounting with Beancount \uf0c1 Martin Blais , Carl Hauser, August 2014 http://furius.ca/beancount/doc/proposal-funds A discussion about how to carry out fund accounting within Beancount, various approaches, solutions and possible extensions. Motivation \uf0c1 Multiple users are attempting to solve the problem of fund accounting using command-line accounting systems, partially because this type of accounting occurs in the context of non-profit organizations that have small budgets and would prefer to use free software, and partially because the flexibility and customization required appear to be a good fit for command-line bookkeeping systems. What is Fund Accounting? \uf0c1 For example, see this thread : \u201cAnother religious duty I compute is effectively tithing (we call it Huq\u00faqu'll\u00e1h, and it's computed differently, but that's another story). In order to compute the tithe owed, I accrue 19% of every deposit to a virtual account, and then subtract from that account 19% of every needful expenditure. The total remaining at the end of the year is what I owe in tithe. This tithing account is not a real account, as it exists in no financial institution; but it is real enough as a personal duty. By using virtual account, I can track this \"side-band\" Liability, and then pay it off from an assets account when the time comes. If I report with --real I will simply see how much I've paid to this Liability; and if I report without --real I see how much Huq\u00faqu'll\u00e1h is presently owed.\u201d \u2014 John Wiegley Here\u2019s another description, as a comment from another user: [...] basically the idea that you split your financial life into separate pots called \"funds\". Each fund has its own chart of accounts (to a certain extent) and each fund obeys Assets+Liabilities+Equities == 0. This is often needed in non-profits where money given for specific purposes has to be accounted separately. The one area of non-separation is that actual asset accounts (e.g. bank accounts) and actual liability accounts (credit cards) may hold or owe money on behalf of multiple funds, so you can't use entirely separate ledger files. At our church we use a program called PowerChurchPlus for this and it works really well. My wife is now treasurer for a community music organization and facing the same problem but on such a small scale that the cost of a commercial program isn't warranted. I've seen what was posted in the ledger-cli list about non-profit accounting using C++ ledger and it just looks like it requires way too much discipline to use the tags correctly and consistently. The fund idea is much easier to explain and use (and the balance account invariant naturally enforced). So I was looking at the Beancount code to see whether I could adapt it to support fund accounting. I think the answer is \"yes and relatively easily so\". Furthermore, I think this idea has uses even for individuals: a couple of scenarios present themselves. First, I live in a community property state. My wife and I are each likely to inherit property which will be ours individually, but we also have the community property. These can each be treated as a separate fund and we will be able to report on them separately but also together for understanding our overall financial situation. Similarly, it potentially makes sense to account for retirement money with a separate fund for each person. \u2014 Carl Hauser From the PowerChurchPlus 11.5 Manual (PowerChurch, Inc. 2013): \u201cIn accounting, the term fund has a very specific meaning. An Accounting Fund is a self-balancing Chart of Accounts. [...] In accounting we need to think of a fund as a division, or sub-group of your church. Each Accounting Fund has its own assets, liabilities, equity, income, transfer and expense accounts. So when would you use an additional fund? If you have an area of your church that needs to produce its own reporting, you would need to use an additional fund. For example, if your church operates a preschool or play school, you might want to set up an additional fund to keep their finances separate from the church's. Depending on your needs, you might want to setup a separate fund for the men's or women's group. You might even setup a new fund to keep track of all fixed assets separately from the daily operating accounts.\u201d This is an interesting and apparently common problem. We will describe use cases in the rest of this section. Joint Account Management \uf0c1 I have personally used this \u201cfund accounting\u201d idea to manage a joint account that I had with my ex-wife, where we would both hold individual accounts\u2014we were both working professionals\u2014 and chip in to the joint account as needed. This section describes how I did this 1 . The accounting for the joint account was held in a separate file. Two sub-accounts were created to hold each of our \u201cportions\u201d: 2010-01-01 open Assets:BofA:Joint 2010-01-01 open Assets:BofA:Joint:Martin 2010-01-01 open Assets:BofA:Joint:Judie Transfers to the joint account were directly booked into one of the two sub-account: 2012-09-07 * \"Online Xfer Transfer from CK 345\" Assets:BofA:Joint:Judie 1000.00 USD Income:Contributions:Judie When we would incur expenses, we would reduce the asset account with two legs, one for each subaccount. We often booked them 2:1 to account for difference in income, or I just booked many of the transactions to myself (the fact that it was precisely accounted for does not imply that we weren\u2019t being generous to each other in that way): 2013-04-27 * \"Urban Vets for Grumpy\" Expenses:Medical:Cat 100.00 USD Assets:BofA:Joint:Martin -50 USD Assets:BofA:Joint:Judie -50 USD 2013-05-30 * \"Takahachi\" \"Dinner\" Expenses:Food:Restaurant 65.80 USD Assets:BofA:Joint:Judie -25.00 USD Assets:BofA:Joint:Martin It was convenient to elide one of the two amounts, as we weren\u2019t being very precise about this. Handling Multiple Funds \uf0c1 (Contributed from Carl Hauser) Here\u2019s the model used in the PowerChurchPlus system that is mentioned above (replacing the account numbers it uses with Beancount-style names). \u201cFund\u201d names are prefixed to the account names. Operations:Assets:Bank:... Endowment:Assets:Bank:... Operations:Liabilities:CreditCard:... Endowment:Liabilities:CreditCard:... Operations:Income:Pledges:2014 Operations:Expenses:Salaries:... Operations:Expenses:BuildingImprovement:... Endowment:Income:Investments:... Endowment:Expenses:BuildingImprovement:... \u2026 It is required that any transaction be balanced in every fund that it uses. For example, our Endowment fund often helps pay for things that are beyond the reach of current donations income. 2014-07-25 * \"Bill\u2019s Audio\" \"Sound system upgrade\" Endowment:Assets:Bank1:Checking 800.00 USD Operations:Assets:Bank1:Checking 200.00 USD Endowment:Expenses:BuildingImprovement:Sound -800.00 USD Operations:Expenses:BuildingImprovement:Sound -200.00 USD This represents a single check to Bill\u2019s Audio paid from assets of both the Endowment and Operations funds that are kept in the single external assets account Assets:Bank1:Checking. Note 1: An empty fund name could be allowed and the following \u201c:\u201d omitted, and in fact could be the default for people who don\u2019t want to use these features. (i.e., nothing changes if you don\u2019t use these features.) The Fund with the empty string for its name is, of course, distinct from all other Funds. Note 2: balance and pad directives are not very useful with accounts that participate in more than one Fund. Their use would require knowing the allocation of the account between the different funds and account statements from external holders (banks, e.g.) will not have this information. It might be useful to allow something like 2014-07-31 balance *:Assets:Bank1:Checking 579.39 USD as a check that things were right, but automatically correcting it with pad entries seems impossible. A balance sheet report can be run on any Fund or any combination of Funds and it will balance. You can keep track of what is owned for each different purpose easily. Transfers between funds are booked as expenses and decreases in the assets of one fund and income and increases in assets of the other. The income and expense accounts used for transfers may be generic ( Operations:Income:Transfer ) or you can use accounts set up for a particular kind of income or expense ( Endowment:Expense:BuildingImprovement:Sound ) would be fine as one leg of a transfer transaction. The account name syntax here is just one way it might work and relies on Beancount\u2019s use of five specifically-named top-level accounts. Anything to the left of the first of those could be treated as a fund name, or a different separator could be used between the fund part and the account name part. Similarly, I\u2019ve only shown single-level fund names but they might be hierarchical as well. I\u2019m not sure of the value of that, but observe that if transactions balance at the leaf-level funds they will also balance for funds higher in the hierarchy and there might be some mileage there. For John W.\u2019s Huq\u00faqu'll\u00e1h example one might set up a Fund whose liabilities were \u201cmoral obligations\u201d rather than legal ones (that seems to be the objection to simply tracking the tithes in an ordinary liability account). As income comes in (say, direct deposited in a real bank checking account), book 19% of it to the \u201cmoral obligation\u201d fund\u2019s checking account with a matching liability. When needful expenses are made, take back 19% from the \u201cmoral obligation\u201d fund\u2019s checking account and reduce the liability. No virtual postings or transactions -- everything must balance. This would work well if for example we were to have a HisRetirement fund and a HerRetirement fund -- useful to have separate for estate planning purposes -- but more commonly we want to know about our combined retirement which could be defined to be a virtual fund OurRetirement equal to the sum of HisRetirement and HerRetirement . Note that this only matters when creating reports: there is no need to do anything except normal, double-entry booking with balanced transactions in each real fund. When I say the \u201csum\u201d of two funds I mean a combination of taking the union of the contained account names, after stripping off the fund names, then summing the balances of the common accounts and keeping the balances of the others. (Balance Sheet) For reporting, one wants the capability for balance by fund and balance summed over a set of funds. I also use a report that shows a subset of funds, one per column, with corresponding account names lined up horizontally and a column at the right that is the \u201csum\u201d. When all funds are included in this latter report you get a complete picture of what you own and owe and what is set aside for different purposes, or restricted in different ways. Here\u2019s a small sample of a balance sheet for a subset of the church Funds. The terminology is a little different: what Beancount calls Equity is here Net Assets. And because using large numbers of Funds in PowerChurchPlus is not so easy, the NetAssets are actually categorized for different purposes -- this is where I think the ideas we\u2019re exploring here can really shine: if Funds are really easy to create and combine for reporting then some of the mechanisms that PCP has for divvying up assets within a fund become unnecessary. (Income Statement) For the Income and Expense report, usually I only care about expenses in one Fund at a time, but adding two funds together makes perfect sense if that\u2019s what you need to do. For me, this approach to fund accounting is appealing because it relies on and preserves the fundamental principles of double-entry bookkeeping: when transactions sum to 0 the balance sheet equation is always true. Out of this we automatically get the ability to combine any set of funds (we don\u2019t have to do anything special when entering transactions or organizing the deep structure of the accounts) and have it make at least arithmetical sense, and we don\u2019t rely on any \u201cmagic\u201d associated with renaming or tagging. I don\u2019t see how this can be so easily or neatly achieved by pushing the idea of the \u201cfunds\u201d down into the account hierarchy: funds belong above the five root accounts (Assets, Liabilities, Equity, Income and Expenses), not below them. Ideas for Implementation \uf0c1 Some random ideas for now. This needs a bit more work. If multiple redundant postings are required, the generation of these can be automated using a plugin . For instance, if a technique similar to mirror accounting is used in order to \u201csend the same dollars to multiple accounts\u201d, at least the user should not have to do this manually, which would be both tedious and prone to errors. A procedure to rename accounts upon parsing could be used, in order to merge multiple files into one. (Allowing the user to install such a mapping is an idea I\u2019ve had for a while but never implemented, though it could be implemented by a plugin filter.) We can rely on the fact that the transactions of subaccounts may be joined and summed in a parent account (despite the fact that reporting is lagging behind in that matter at the moment. It will be implemented eventually). Building off the earlier remark about doing something similar to the tag stack for Funds. What if the current tag architecture were extended to allow tags to have a value, #fund=Operations, or #fund=Endowment . Call them value-tags. You would also need to allow postings to have tags. Semantics of value-tags would be that they could only occur once for any posting, that a tag explicitly on a posting overrides the value-tag inherited from the transaction, and that an explicit tag on a transaction overrides a value from the tag-stack, and that only the last value-tag (with a particular key) in the tag-stack is applied to a transaction. This makes Funds a little less first-class than the earlier proposal to stick them in front of account names, but gets around the minor parsing difficulty previously mentioned. It suggests that opening of accounts within funds is not necessary where the previous syntax suggests that it is. The strict balancing rule for each fund in a transaction can still be implemented as a plugin. And reporting for a fund (or sum of funds) looks like: select transactions with any posting matching the desired fund (or funds) collect (and sum if necessary in the obvious way) postings associated with the fund (or funds) being reported on (A) collect (and sum in the obvious way) postings from the selected transactions not associated with the desired fund (B) Format a report with (A) as the information for the desired fund or funds and (B) as OTHER. OTHER is needed to make sure that the report balances, but could be omitted by choice. From an implementation perspective this seems more orthogonal to the current status quo, requiring even less change to existing code. It adds a new feature -- value tags and that can then be used by plugins and new reports to do what we want for fund accounting. Examples \uf0c1 (Carl) Here is an example of how I might try to handle things associated with my paycheck, which involves deferred compensation (403(b) -- extremely similar to a 401(k)) and a Flexible Spending Account (somewhat similar to an HSA which has been discussed previously on the ledger-cli group). Without Funds \uf0c1 (Carl) First, without Funds (this passes a bean-check): This is how I might set up my pay stub with health insurance, an FSA and 403b contributions in the absence of Funds. One problem is that it distributes Gross Income directly into the 403b and FSA accounts even though it recognizes that the health insurance contribution is a reduction in salary which the 403b and FSA ought to be as well. So tracking contributions to both of those is made more difficult as well as tracking taxable income. By thinking hard we could fix this -- we would come up with Income and Expense type accounts to represent the contributions, but they would end up looking kind of silly (in my opinion) because they are entirely internal to the accounts system. See the next example for how it would look using Funds. If you stick out your tongue, rub your tummy and stand on your head you will see that the Funds-based solution is equivalent to what we would have come up with in the paragraph above in terms of the complexity of its transactions -- just as many lines are required. The advantage is primarily a mental one -- it is much easier to see what to do to be both consistent and useful. option \u201ctitle\u201d \u201cPaystub - no funds\u201d 2014-07-15 open Assets:Bank:Checking 2014-07-15 open Assets:CreditUnion:Saving 2014-07-15 open Assets:FedIncTaxDeposits 2014-07-15 open Assets:Deferred:R-403b 2014-07-15 open Expenses:OASI 2014-07-15 open Expenses:Medicare 2014-07-15 open Expenses:MedicalAid 2014-07-15 open Expenses:SalReduction:HealthInsurance 2014-07-15 open Income:Gross:Emp1 2014-07-15 open Income:EmplContrib:Emp1:Retirement 2014-01-01 open Assets:FSA ; This way of setting up an FSA looks pretty good. It recognizes the ; rule that the designated amount for the year is immediately available ; (in the Asset account), and that we are obliged to make contributions ; to fund it over the course of the year (the Liability account). 2014-01-01 open Liabilities:FSA 2014-01-01 ! \"Set up FSA for 2014\" Assets:FSA 2000 USD Liabilities:FSA -2000 USD 2014-07-15 ! \"Emp1 Paystub\" Income:Gross:Emp1 -6000 USD Assets:Bank:Checking 3000 USD Assets:CreditUnion:Saving 1000 USD Assets:FedIncTaxDeposits 750 USD Expenses:OASI 375 USD Expenses:Medicare 100 USD Expenses:MedicalAid 10 USD Assets:Deferred:R-403b 600 USD Liabilities:FSA 75 USD Expenses:SalReduction:HealthInsurance 90 USD Income:EmplContrib:Emp1:Retirement -600 USD Assets:Deferred:R-403b 2014-01-01 open Expenses:Medical 2014-07-20 ! \"Direct expense from FSA\" Expenses:Medical 25 USD Assets:FSA 2014-07-20 ! \"Medical expense from checking\" Expenses:Medical 25 USD Assets:Bank:Checking 2014-07-20 ! \"Medical expense reimbursed from FSA\" Assets:Bank:Checking 25 USD Assets:FSA Using Funds \uf0c1 (Carl) And now using Funds (uses proposed features and hence can\u2019t be checked by bean-check): This is how I might set up my pay stub with health insurance, an FSA and 403b contributions using Funds. I can straightforwardly arrange things so that contributions to the FSA and 403b are recognized as salary reductions for income tax purposes. And I can easily see how much I have contributed to the 403b and how much my employer has contributed. See the previous example for how it would look without using Funds and which is not as accurate. What this does NOT do is track taxes ultimately owed on the 403b money. I think that is a matter of one's decision about whether to do cash-basis or accrual-basis accounting. If cash basis those taxes are not a current liability and cannot be reported as such. If accrual basis, they are a current liability and need to be recorded as such when the income is booked. For cash-basis books, we'd want the ability to report the state of affairs as if taxes were owed, but that is a matter for reporting rather than booking. We need to make sure we have enough identifiable information to automate creating those reports. I believe that taking a Fund-based accounting perspective easily does this. A problem not solved: what if your basis is different for Federal and State purposes, or even for Federal and multiple different states. Yikes! I've used the convention that the Fund name precedes the root account name. Note that with appropriate standing on one's head along with pivoting rules you can put the Fund name anywhere. Putting it first emphasizes that it identifies a set of accounts that must balance, and it makes it easy for the txn processor to guarantee this property. Accounts without a fund name in front belong to the Fund whose name is the empty string. option \"title\" \"Paystub - no Funds\" 2014-07-15 open Assets:Bank:Checking 2014-07-15 open Assets:CreditUnion:Saving 2014-07-15 open Assets:FedIncTaxDeposits 2014-07-15 open Expenses:OASI 2014-07-15 open Expenses:Medicare 2014-07-15 open Expenses:MedicalAid 2014-07-15 open Expenses:SalReduction:HealthInsurance 2014-07-15 open Expenses:SalReduction:FSA 2014-07-15 open Expenses:SalReduction:R-403b 2014-07-15 open Income:Gross:Emp1 2014-07-15 open Income:EmplContrib:Emp1:Retirement 2014-01-01 open FSA:Assets ; FSA fund accounts 2014-01-01 open FSA:Income:Contributions 2014-01-01 open FSA:Expenses:Medical 2014-01-01 open FSA:Expenses:ReimburseMedical 2014-01-01 open FSA:Liabilities 2014-07-15 open Retirement403b:Assets:CREF ; Retirement fund accounts 2014-07-15 open Retirement403b:Income:EmployeeContrib 2014-07-15 open Retirement403b:Income:EmployerContrib 2014-07-15 open Retirement403b:Income:EarningsGainsAndLosses ; This implements the same idea as above for the FSA, of balancing ; Assets and Liabilities at the opening, but now does it using a ; separate Fund. 2014-01-01 ! \"Set up FSA for 2014\" FSA:Assets 2000 USD FSA:Liabilities -2000 USD 2014-07-15 ! \"Emp1 Paystub\" Income:Gross:Emp1 -6000 USD Assets:Bank:Checking 3000 USD Assets:CreditUnion:Saving 1000 USD Assets:FedIncTaxDeposits 750 USD Expenses:OASI 375 USD Expenses:Medicare 100 USD Expenses:MedicalAid 10 USD Expenses:SalReduction:R-403b 600 USD Retirement403b:Income:EmployeeContrib -600 USD Retirement403b:Assets:CREF 600 USD Expenses:SalReduction:FSA 75 USD FSA:Income:Contributions -75 USD FSA:Liabilities Expenses:SalReduction:HealthInsurance Retirement403b:Income:EmployerContrib -600 USD Retirement403b:Assets:CREF 2014-01-01 open Expenses:Medical 2014-07-20 ! \"Direct expense from FSA\" FSA:Expenses:Medical 25 USD FSA:Assets 2014-07-20 ! \"Medical expense from checking\" Expenses:Medical 25 USD Assets:Bank:Checking 2014-07-20 ! \"Medical expense reimbursed from FSA\" Assets:Bank:Checking 25 USD Income:ReimburseMedical FSA:Assets -25 USD FSA:Expenses:ReimburseMedical Transfer Accounts Proposal \uf0c1 By Carl Hauser One problem that I\u2019ve experienced using the Fund approach is that it\u2019s a bit too easy to make mistakes when transferring money between funds, such as in the very last transaction above. Formalizing the idea of Transfer accounts can help with this. The most common mistake is to end up with something that moves assets in both accounts in the same direction -- both up or both down as in this mistaken version of the transaction in question: 2014-07-20 ! \"Medical expense reimbursed from FSA - with mistake\" Assets:Bank:Checking 25 USD Income:ReimburseMedical FSA:Assets 25 USD FSA:Expenses:ReimburseMedical This balances but isn\u2019t what we intended. Suppose we add the idea of Transfer accounts. They live at the same place in the hierarchy as Income and Expenses and like those are non-balance-sheet accounts. But they really come into play only for transactions that involve multiple funds. There is an additional rule for transactions containing Transfer accounts: the sum of the transfers must also be zero (additional in the sense that the rule about transactions balancing within each fund is still there). So to use this we set things up a little differently: 2014-07-15 open FSA:Transfer:Incoming:Contribution 2014-07-15 open FSA:Transfer:Outgoing:ReimburseMedical 2014-07-15 open Transfer:Outgoing:FSAContribution 2014-07-15 open Transfer:Incoming:ReimburseMedical The incorrect transaction is now flagged because sum of the transfers is -50 USD , not zero. 2014-07-20 ! \"Medical expense reimbursed from FSA - with mistake\" Assets:Bank:Checking 25 USD Transfer:Incoming:ReimburseMedical FSA:Assets 25 USD FSA:Transfer:Outgoing:ReimburseMedical The paycheck transaction using transfer accounts for the FSA and the retirement account amounts might look like this (after appropriate open s of course): 2014-07-15 ! \"Emp1 Paystub - using transfer accounts\" Income:Gross:Emp1 -6000 USD Assets:Bank:Checking 3000 USD Assets:CreditUnion:Saving 1000 USD Assets:FedIncTaxDeposits 750 USD Expenses:OASI 375 USD Expenses:Medicare 100 USD Expenses:MedicalAid 10 USD Transfer:Outgoing:Retirement403b:SalReduction 600 USD Retirement403b:Transfer:Incoming:EmployeeContrib Retirement403b:Assets:CREF 600 USD Transfer:Outgoing:FSA:SalReduction 75 USD FSA:Transfer:Incoming:Contributions FSA:Liabilities Expenses:SalReduction:HealthInsurance Retirement403b:Income:EmployerContrib -600 USD Retirement403b:Assets:CREF Some might think that this is too complicated. Without changing the Transfer accounts idea or rule, you can simplify booking to just a single account per fund, Fund:Transfer , losing some ability for precision in reporting but without losing the ability to check correctness of transfer transactions. Account Aliases \uf0c1 Simon Michael mentions that this is related to HLedger account aliases : \u201cI think this is related to the situation where you want to view entities' finances both separately and merged. Account aliases can be another way to approximate this, as in http://hledger.org/how-to-use-account-aliases .\u201d If you find yourself culturally challenged by our modern lifestyle, perhaps you can imagine the case of roommates, although I don\u2019t like the reductionist view this association brings to my mind. \u21a9","title":"Fund Accounting with Beancount"},{"location":"fund_accounting_with_beancount.html#fund-accounting-with-beancount","text":"Martin Blais , Carl Hauser, August 2014 http://furius.ca/beancount/doc/proposal-funds A discussion about how to carry out fund accounting within Beancount, various approaches, solutions and possible extensions.","title":"Fund Accounting with Beancount"},{"location":"fund_accounting_with_beancount.html#motivation","text":"Multiple users are attempting to solve the problem of fund accounting using command-line accounting systems, partially because this type of accounting occurs in the context of non-profit organizations that have small budgets and would prefer to use free software, and partially because the flexibility and customization required appear to be a good fit for command-line bookkeeping systems.","title":"Motivation"},{"location":"fund_accounting_with_beancount.html#what-is-fund-accounting","text":"For example, see this thread : \u201cAnother religious duty I compute is effectively tithing (we call it Huq\u00faqu'll\u00e1h, and it's computed differently, but that's another story). In order to compute the tithe owed, I accrue 19% of every deposit to a virtual account, and then subtract from that account 19% of every needful expenditure. The total remaining at the end of the year is what I owe in tithe. This tithing account is not a real account, as it exists in no financial institution; but it is real enough as a personal duty. By using virtual account, I can track this \"side-band\" Liability, and then pay it off from an assets account when the time comes. If I report with --real I will simply see how much I've paid to this Liability; and if I report without --real I see how much Huq\u00faqu'll\u00e1h is presently owed.\u201d \u2014 John Wiegley Here\u2019s another description, as a comment from another user: [...] basically the idea that you split your financial life into separate pots called \"funds\". Each fund has its own chart of accounts (to a certain extent) and each fund obeys Assets+Liabilities+Equities == 0. This is often needed in non-profits where money given for specific purposes has to be accounted separately. The one area of non-separation is that actual asset accounts (e.g. bank accounts) and actual liability accounts (credit cards) may hold or owe money on behalf of multiple funds, so you can't use entirely separate ledger files. At our church we use a program called PowerChurchPlus for this and it works really well. My wife is now treasurer for a community music organization and facing the same problem but on such a small scale that the cost of a commercial program isn't warranted. I've seen what was posted in the ledger-cli list about non-profit accounting using C++ ledger and it just looks like it requires way too much discipline to use the tags correctly and consistently. The fund idea is much easier to explain and use (and the balance account invariant naturally enforced). So I was looking at the Beancount code to see whether I could adapt it to support fund accounting. I think the answer is \"yes and relatively easily so\". Furthermore, I think this idea has uses even for individuals: a couple of scenarios present themselves. First, I live in a community property state. My wife and I are each likely to inherit property which will be ours individually, but we also have the community property. These can each be treated as a separate fund and we will be able to report on them separately but also together for understanding our overall financial situation. Similarly, it potentially makes sense to account for retirement money with a separate fund for each person. \u2014 Carl Hauser From the PowerChurchPlus 11.5 Manual (PowerChurch, Inc. 2013): \u201cIn accounting, the term fund has a very specific meaning. An Accounting Fund is a self-balancing Chart of Accounts. [...] In accounting we need to think of a fund as a division, or sub-group of your church. Each Accounting Fund has its own assets, liabilities, equity, income, transfer and expense accounts. So when would you use an additional fund? If you have an area of your church that needs to produce its own reporting, you would need to use an additional fund. For example, if your church operates a preschool or play school, you might want to set up an additional fund to keep their finances separate from the church's. Depending on your needs, you might want to setup a separate fund for the men's or women's group. You might even setup a new fund to keep track of all fixed assets separately from the daily operating accounts.\u201d This is an interesting and apparently common problem. We will describe use cases in the rest of this section.","title":"What is Fund Accounting?"},{"location":"fund_accounting_with_beancount.html#joint-account-management","text":"I have personally used this \u201cfund accounting\u201d idea to manage a joint account that I had with my ex-wife, where we would both hold individual accounts\u2014we were both working professionals\u2014 and chip in to the joint account as needed. This section describes how I did this 1 . The accounting for the joint account was held in a separate file. Two sub-accounts were created to hold each of our \u201cportions\u201d: 2010-01-01 open Assets:BofA:Joint 2010-01-01 open Assets:BofA:Joint:Martin 2010-01-01 open Assets:BofA:Joint:Judie Transfers to the joint account were directly booked into one of the two sub-account: 2012-09-07 * \"Online Xfer Transfer from CK 345\" Assets:BofA:Joint:Judie 1000.00 USD Income:Contributions:Judie When we would incur expenses, we would reduce the asset account with two legs, one for each subaccount. We often booked them 2:1 to account for difference in income, or I just booked many of the transactions to myself (the fact that it was precisely accounted for does not imply that we weren\u2019t being generous to each other in that way): 2013-04-27 * \"Urban Vets for Grumpy\" Expenses:Medical:Cat 100.00 USD Assets:BofA:Joint:Martin -50 USD Assets:BofA:Joint:Judie -50 USD 2013-05-30 * \"Takahachi\" \"Dinner\" Expenses:Food:Restaurant 65.80 USD Assets:BofA:Joint:Judie -25.00 USD Assets:BofA:Joint:Martin It was convenient to elide one of the two amounts, as we weren\u2019t being very precise about this.","title":"Joint Account Management"},{"location":"fund_accounting_with_beancount.html#handling-multiple-funds","text":"(Contributed from Carl Hauser) Here\u2019s the model used in the PowerChurchPlus system that is mentioned above (replacing the account numbers it uses with Beancount-style names). \u201cFund\u201d names are prefixed to the account names. Operations:Assets:Bank:... Endowment:Assets:Bank:... Operations:Liabilities:CreditCard:... Endowment:Liabilities:CreditCard:... Operations:Income:Pledges:2014 Operations:Expenses:Salaries:... Operations:Expenses:BuildingImprovement:... Endowment:Income:Investments:... Endowment:Expenses:BuildingImprovement:... \u2026 It is required that any transaction be balanced in every fund that it uses. For example, our Endowment fund often helps pay for things that are beyond the reach of current donations income. 2014-07-25 * \"Bill\u2019s Audio\" \"Sound system upgrade\" Endowment:Assets:Bank1:Checking 800.00 USD Operations:Assets:Bank1:Checking 200.00 USD Endowment:Expenses:BuildingImprovement:Sound -800.00 USD Operations:Expenses:BuildingImprovement:Sound -200.00 USD This represents a single check to Bill\u2019s Audio paid from assets of both the Endowment and Operations funds that are kept in the single external assets account Assets:Bank1:Checking. Note 1: An empty fund name could be allowed and the following \u201c:\u201d omitted, and in fact could be the default for people who don\u2019t want to use these features. (i.e., nothing changes if you don\u2019t use these features.) The Fund with the empty string for its name is, of course, distinct from all other Funds. Note 2: balance and pad directives are not very useful with accounts that participate in more than one Fund. Their use would require knowing the allocation of the account between the different funds and account statements from external holders (banks, e.g.) will not have this information. It might be useful to allow something like 2014-07-31 balance *:Assets:Bank1:Checking 579.39 USD as a check that things were right, but automatically correcting it with pad entries seems impossible. A balance sheet report can be run on any Fund or any combination of Funds and it will balance. You can keep track of what is owned for each different purpose easily. Transfers between funds are booked as expenses and decreases in the assets of one fund and income and increases in assets of the other. The income and expense accounts used for transfers may be generic ( Operations:Income:Transfer ) or you can use accounts set up for a particular kind of income or expense ( Endowment:Expense:BuildingImprovement:Sound ) would be fine as one leg of a transfer transaction. The account name syntax here is just one way it might work and relies on Beancount\u2019s use of five specifically-named top-level accounts. Anything to the left of the first of those could be treated as a fund name, or a different separator could be used between the fund part and the account name part. Similarly, I\u2019ve only shown single-level fund names but they might be hierarchical as well. I\u2019m not sure of the value of that, but observe that if transactions balance at the leaf-level funds they will also balance for funds higher in the hierarchy and there might be some mileage there. For John W.\u2019s Huq\u00faqu'll\u00e1h example one might set up a Fund whose liabilities were \u201cmoral obligations\u201d rather than legal ones (that seems to be the objection to simply tracking the tithes in an ordinary liability account). As income comes in (say, direct deposited in a real bank checking account), book 19% of it to the \u201cmoral obligation\u201d fund\u2019s checking account with a matching liability. When needful expenses are made, take back 19% from the \u201cmoral obligation\u201d fund\u2019s checking account and reduce the liability. No virtual postings or transactions -- everything must balance. This would work well if for example we were to have a HisRetirement fund and a HerRetirement fund -- useful to have separate for estate planning purposes -- but more commonly we want to know about our combined retirement which could be defined to be a virtual fund OurRetirement equal to the sum of HisRetirement and HerRetirement . Note that this only matters when creating reports: there is no need to do anything except normal, double-entry booking with balanced transactions in each real fund. When I say the \u201csum\u201d of two funds I mean a combination of taking the union of the contained account names, after stripping off the fund names, then summing the balances of the common accounts and keeping the balances of the others. (Balance Sheet) For reporting, one wants the capability for balance by fund and balance summed over a set of funds. I also use a report that shows a subset of funds, one per column, with corresponding account names lined up horizontally and a column at the right that is the \u201csum\u201d. When all funds are included in this latter report you get a complete picture of what you own and owe and what is set aside for different purposes, or restricted in different ways. Here\u2019s a small sample of a balance sheet for a subset of the church Funds. The terminology is a little different: what Beancount calls Equity is here Net Assets. And because using large numbers of Funds in PowerChurchPlus is not so easy, the NetAssets are actually categorized for different purposes -- this is where I think the ideas we\u2019re exploring here can really shine: if Funds are really easy to create and combine for reporting then some of the mechanisms that PCP has for divvying up assets within a fund become unnecessary. (Income Statement) For the Income and Expense report, usually I only care about expenses in one Fund at a time, but adding two funds together makes perfect sense if that\u2019s what you need to do. For me, this approach to fund accounting is appealing because it relies on and preserves the fundamental principles of double-entry bookkeeping: when transactions sum to 0 the balance sheet equation is always true. Out of this we automatically get the ability to combine any set of funds (we don\u2019t have to do anything special when entering transactions or organizing the deep structure of the accounts) and have it make at least arithmetical sense, and we don\u2019t rely on any \u201cmagic\u201d associated with renaming or tagging. I don\u2019t see how this can be so easily or neatly achieved by pushing the idea of the \u201cfunds\u201d down into the account hierarchy: funds belong above the five root accounts (Assets, Liabilities, Equity, Income and Expenses), not below them.","title":"Handling Multiple Funds"},{"location":"fund_accounting_with_beancount.html#ideas-for-implementation","text":"Some random ideas for now. This needs a bit more work. If multiple redundant postings are required, the generation of these can be automated using a plugin . For instance, if a technique similar to mirror accounting is used in order to \u201csend the same dollars to multiple accounts\u201d, at least the user should not have to do this manually, which would be both tedious and prone to errors. A procedure to rename accounts upon parsing could be used, in order to merge multiple files into one. (Allowing the user to install such a mapping is an idea I\u2019ve had for a while but never implemented, though it could be implemented by a plugin filter.) We can rely on the fact that the transactions of subaccounts may be joined and summed in a parent account (despite the fact that reporting is lagging behind in that matter at the moment. It will be implemented eventually). Building off the earlier remark about doing something similar to the tag stack for Funds. What if the current tag architecture were extended to allow tags to have a value, #fund=Operations, or #fund=Endowment . Call them value-tags. You would also need to allow postings to have tags. Semantics of value-tags would be that they could only occur once for any posting, that a tag explicitly on a posting overrides the value-tag inherited from the transaction, and that an explicit tag on a transaction overrides a value from the tag-stack, and that only the last value-tag (with a particular key) in the tag-stack is applied to a transaction. This makes Funds a little less first-class than the earlier proposal to stick them in front of account names, but gets around the minor parsing difficulty previously mentioned. It suggests that opening of accounts within funds is not necessary where the previous syntax suggests that it is. The strict balancing rule for each fund in a transaction can still be implemented as a plugin. And reporting for a fund (or sum of funds) looks like: select transactions with any posting matching the desired fund (or funds) collect (and sum if necessary in the obvious way) postings associated with the fund (or funds) being reported on (A) collect (and sum in the obvious way) postings from the selected transactions not associated with the desired fund (B) Format a report with (A) as the information for the desired fund or funds and (B) as OTHER. OTHER is needed to make sure that the report balances, but could be omitted by choice. From an implementation perspective this seems more orthogonal to the current status quo, requiring even less change to existing code. It adds a new feature -- value tags and that can then be used by plugins and new reports to do what we want for fund accounting.","title":"Ideas for Implementation"},{"location":"fund_accounting_with_beancount.html#examples","text":"(Carl) Here is an example of how I might try to handle things associated with my paycheck, which involves deferred compensation (403(b) -- extremely similar to a 401(k)) and a Flexible Spending Account (somewhat similar to an HSA which has been discussed previously on the ledger-cli group).","title":"Examples"},{"location":"fund_accounting_with_beancount.html#without-funds","text":"(Carl) First, without Funds (this passes a bean-check): This is how I might set up my pay stub with health insurance, an FSA and 403b contributions in the absence of Funds. One problem is that it distributes Gross Income directly into the 403b and FSA accounts even though it recognizes that the health insurance contribution is a reduction in salary which the 403b and FSA ought to be as well. So tracking contributions to both of those is made more difficult as well as tracking taxable income. By thinking hard we could fix this -- we would come up with Income and Expense type accounts to represent the contributions, but they would end up looking kind of silly (in my opinion) because they are entirely internal to the accounts system. See the next example for how it would look using Funds. If you stick out your tongue, rub your tummy and stand on your head you will see that the Funds-based solution is equivalent to what we would have come up with in the paragraph above in terms of the complexity of its transactions -- just as many lines are required. The advantage is primarily a mental one -- it is much easier to see what to do to be both consistent and useful. option \u201ctitle\u201d \u201cPaystub - no funds\u201d 2014-07-15 open Assets:Bank:Checking 2014-07-15 open Assets:CreditUnion:Saving 2014-07-15 open Assets:FedIncTaxDeposits 2014-07-15 open Assets:Deferred:R-403b 2014-07-15 open Expenses:OASI 2014-07-15 open Expenses:Medicare 2014-07-15 open Expenses:MedicalAid 2014-07-15 open Expenses:SalReduction:HealthInsurance 2014-07-15 open Income:Gross:Emp1 2014-07-15 open Income:EmplContrib:Emp1:Retirement 2014-01-01 open Assets:FSA ; This way of setting up an FSA looks pretty good. It recognizes the ; rule that the designated amount for the year is immediately available ; (in the Asset account), and that we are obliged to make contributions ; to fund it over the course of the year (the Liability account). 2014-01-01 open Liabilities:FSA 2014-01-01 ! \"Set up FSA for 2014\" Assets:FSA 2000 USD Liabilities:FSA -2000 USD 2014-07-15 ! \"Emp1 Paystub\" Income:Gross:Emp1 -6000 USD Assets:Bank:Checking 3000 USD Assets:CreditUnion:Saving 1000 USD Assets:FedIncTaxDeposits 750 USD Expenses:OASI 375 USD Expenses:Medicare 100 USD Expenses:MedicalAid 10 USD Assets:Deferred:R-403b 600 USD Liabilities:FSA 75 USD Expenses:SalReduction:HealthInsurance 90 USD Income:EmplContrib:Emp1:Retirement -600 USD Assets:Deferred:R-403b 2014-01-01 open Expenses:Medical 2014-07-20 ! \"Direct expense from FSA\" Expenses:Medical 25 USD Assets:FSA 2014-07-20 ! \"Medical expense from checking\" Expenses:Medical 25 USD Assets:Bank:Checking 2014-07-20 ! \"Medical expense reimbursed from FSA\" Assets:Bank:Checking 25 USD Assets:FSA","title":"Without Funds"},{"location":"fund_accounting_with_beancount.html#using-funds","text":"(Carl) And now using Funds (uses proposed features and hence can\u2019t be checked by bean-check): This is how I might set up my pay stub with health insurance, an FSA and 403b contributions using Funds. I can straightforwardly arrange things so that contributions to the FSA and 403b are recognized as salary reductions for income tax purposes. And I can easily see how much I have contributed to the 403b and how much my employer has contributed. See the previous example for how it would look without using Funds and which is not as accurate. What this does NOT do is track taxes ultimately owed on the 403b money. I think that is a matter of one's decision about whether to do cash-basis or accrual-basis accounting. If cash basis those taxes are not a current liability and cannot be reported as such. If accrual basis, they are a current liability and need to be recorded as such when the income is booked. For cash-basis books, we'd want the ability to report the state of affairs as if taxes were owed, but that is a matter for reporting rather than booking. We need to make sure we have enough identifiable information to automate creating those reports. I believe that taking a Fund-based accounting perspective easily does this. A problem not solved: what if your basis is different for Federal and State purposes, or even for Federal and multiple different states. Yikes! I've used the convention that the Fund name precedes the root account name. Note that with appropriate standing on one's head along with pivoting rules you can put the Fund name anywhere. Putting it first emphasizes that it identifies a set of accounts that must balance, and it makes it easy for the txn processor to guarantee this property. Accounts without a fund name in front belong to the Fund whose name is the empty string. option \"title\" \"Paystub - no Funds\" 2014-07-15 open Assets:Bank:Checking 2014-07-15 open Assets:CreditUnion:Saving 2014-07-15 open Assets:FedIncTaxDeposits 2014-07-15 open Expenses:OASI 2014-07-15 open Expenses:Medicare 2014-07-15 open Expenses:MedicalAid 2014-07-15 open Expenses:SalReduction:HealthInsurance 2014-07-15 open Expenses:SalReduction:FSA 2014-07-15 open Expenses:SalReduction:R-403b 2014-07-15 open Income:Gross:Emp1 2014-07-15 open Income:EmplContrib:Emp1:Retirement 2014-01-01 open FSA:Assets ; FSA fund accounts 2014-01-01 open FSA:Income:Contributions 2014-01-01 open FSA:Expenses:Medical 2014-01-01 open FSA:Expenses:ReimburseMedical 2014-01-01 open FSA:Liabilities 2014-07-15 open Retirement403b:Assets:CREF ; Retirement fund accounts 2014-07-15 open Retirement403b:Income:EmployeeContrib 2014-07-15 open Retirement403b:Income:EmployerContrib 2014-07-15 open Retirement403b:Income:EarningsGainsAndLosses ; This implements the same idea as above for the FSA, of balancing ; Assets and Liabilities at the opening, but now does it using a ; separate Fund. 2014-01-01 ! \"Set up FSA for 2014\" FSA:Assets 2000 USD FSA:Liabilities -2000 USD 2014-07-15 ! \"Emp1 Paystub\" Income:Gross:Emp1 -6000 USD Assets:Bank:Checking 3000 USD Assets:CreditUnion:Saving 1000 USD Assets:FedIncTaxDeposits 750 USD Expenses:OASI 375 USD Expenses:Medicare 100 USD Expenses:MedicalAid 10 USD Expenses:SalReduction:R-403b 600 USD Retirement403b:Income:EmployeeContrib -600 USD Retirement403b:Assets:CREF 600 USD Expenses:SalReduction:FSA 75 USD FSA:Income:Contributions -75 USD FSA:Liabilities Expenses:SalReduction:HealthInsurance Retirement403b:Income:EmployerContrib -600 USD Retirement403b:Assets:CREF 2014-01-01 open Expenses:Medical 2014-07-20 ! \"Direct expense from FSA\" FSA:Expenses:Medical 25 USD FSA:Assets 2014-07-20 ! \"Medical expense from checking\" Expenses:Medical 25 USD Assets:Bank:Checking 2014-07-20 ! \"Medical expense reimbursed from FSA\" Assets:Bank:Checking 25 USD Income:ReimburseMedical FSA:Assets -25 USD FSA:Expenses:ReimburseMedical","title":"Using Funds"},{"location":"fund_accounting_with_beancount.html#transfer-accounts-proposal","text":"By Carl Hauser One problem that I\u2019ve experienced using the Fund approach is that it\u2019s a bit too easy to make mistakes when transferring money between funds, such as in the very last transaction above. Formalizing the idea of Transfer accounts can help with this. The most common mistake is to end up with something that moves assets in both accounts in the same direction -- both up or both down as in this mistaken version of the transaction in question: 2014-07-20 ! \"Medical expense reimbursed from FSA - with mistake\" Assets:Bank:Checking 25 USD Income:ReimburseMedical FSA:Assets 25 USD FSA:Expenses:ReimburseMedical This balances but isn\u2019t what we intended. Suppose we add the idea of Transfer accounts. They live at the same place in the hierarchy as Income and Expenses and like those are non-balance-sheet accounts. But they really come into play only for transactions that involve multiple funds. There is an additional rule for transactions containing Transfer accounts: the sum of the transfers must also be zero (additional in the sense that the rule about transactions balancing within each fund is still there). So to use this we set things up a little differently: 2014-07-15 open FSA:Transfer:Incoming:Contribution 2014-07-15 open FSA:Transfer:Outgoing:ReimburseMedical 2014-07-15 open Transfer:Outgoing:FSAContribution 2014-07-15 open Transfer:Incoming:ReimburseMedical The incorrect transaction is now flagged because sum of the transfers is -50 USD , not zero. 2014-07-20 ! \"Medical expense reimbursed from FSA - with mistake\" Assets:Bank:Checking 25 USD Transfer:Incoming:ReimburseMedical FSA:Assets 25 USD FSA:Transfer:Outgoing:ReimburseMedical The paycheck transaction using transfer accounts for the FSA and the retirement account amounts might look like this (after appropriate open s of course): 2014-07-15 ! \"Emp1 Paystub - using transfer accounts\" Income:Gross:Emp1 -6000 USD Assets:Bank:Checking 3000 USD Assets:CreditUnion:Saving 1000 USD Assets:FedIncTaxDeposits 750 USD Expenses:OASI 375 USD Expenses:Medicare 100 USD Expenses:MedicalAid 10 USD Transfer:Outgoing:Retirement403b:SalReduction 600 USD Retirement403b:Transfer:Incoming:EmployeeContrib Retirement403b:Assets:CREF 600 USD Transfer:Outgoing:FSA:SalReduction 75 USD FSA:Transfer:Incoming:Contributions FSA:Liabilities Expenses:SalReduction:HealthInsurance Retirement403b:Income:EmployerContrib -600 USD Retirement403b:Assets:CREF Some might think that this is too complicated. Without changing the Transfer accounts idea or rule, you can simplify booking to just a single account per fund, Fund:Transfer , losing some ability for precision in reporting but without losing the ability to check correctness of transfer transactions.","title":"Transfer Accounts Proposal"},{"location":"fund_accounting_with_beancount.html#account-aliases","text":"Simon Michael mentions that this is related to HLedger account aliases : \u201cI think this is related to the situation where you want to view entities' finances both separately and merged. Account aliases can be another way to approximate this, as in http://hledger.org/how-to-use-account-aliases .\u201d If you find yourself culturally challenged by our modern lifestyle, perhaps you can imagine the case of roommates, although I don\u2019t like the reductionist view this association brings to my mind. \u21a9","title":"Account Aliases"},{"location":"getting_started_with_beancount.html","text":"Getting Started with Beancount \uf0c1 Martin Blais , July 2014 http://furius.ca/beancount/doc/getting-started Introduction \uf0c1 This document is a gentle guide to creating your first Beancount file, initializing it with some options, some guidelines for how to organize your file, and instructions for declaring accounts and making sure their initial balance does not raise errors. It also contains some material on configuring the Emacs text editor, if you use that. You will probably want to have read some of the User\u2019s Manual first in order to familiarize yourself with the syntax and kinds of available directives, or move on to the Cookbook if you\u2019ve already setup a file or know how to do that. If you\u2019re familiar with Ledger, you may want to read up on the differences between Ledger and Beancount first. Editor Support \uf0c1 Beancount ledgers are simple text files. You can use any text editor to compose your input file. However, a good text editor which understands enough of the Beancount syntax to offer focused facilities like syntax highlighting, autocompletion, and automatic indentation highly has the potential to greatly increase productivity in compiling and maintaining your ledger. Emacs \uf0c1 Support for editing Beancount ledger files in Emacs was traditionally distributed with Beancount. It now lives as its own project in this Github repository . Vim \uf0c1 Support for editing Beancount ledger files in Vim has been implemented by Nathan Grigg and is available in this Github repository . Sublime \uf0c1 Support for editing with Sublime has been contributed by Martin Andreas Andersen and is available in this github repository or as a Sublime package here . VSCode \uf0c1 There are a number of plugins for working with Beancount text files including VSCode-Beancount by Lencerf. Creating your First Input File \uf0c1 To get started, let\u2019s create a minimal input file with two accounts and a single transaction. Enter or copy the following input to a text file: 2014-01-01 open Assets:Checking 2014-01-01 open Equity:Opening-Balances 2014-01-02 * \"Deposit\" Assets:Checking 100.00 USD Equity:Opening-Balances Brief Syntax Overview \uf0c1 A few notes and an ultra brief overview of the Beancount syntax: Currencies must be entirely in capital letters (allowing numbers and some special characters, like \u201c_\u201d or \u201c-\u201d). Currency symbols (such as $ or \u20ac) are not supported. Account names do not admit spaces (though you can use dashes), and must have at least two components, separated by colons. Each component of an account name must begin with a capital letter or number. Description strings must be quoted, like this: \"AMEX PMNT\" . Dates are only parsed in ISO8601 format, that is, YYYY-MM-DD. Tags must begin with \u201c#\u201d, and links with \u201c^\u201d. For a complete description of the syntax, visit the User\u2019s Manual . Validating your File \uf0c1 The purpose of Beancount is to produce reports from your input file (either on the console or serve via its web interface). However, there is a tool that you can use to simply load its contents and make some validation checks on it, to ensure that your input does not contain errors. Beancount can be quite strict; this is a tool that you use while you\u2019re entering your data to ensure that your input file is legal. The tool is called \u201cbean-check\u201d and you invoke it like this: bean-check /path/to/your/file.beancount Try it now on the file you just created from the previous section. It should exit with no output. If there are errors, they will be printed on the console. The errors are printed out in a format that Emacs recognizes by default, so you can use Emacs\u2019 next-error and previous-error built-in functions to move the cursor to the location of the problem. Viewing the Web Interface \uf0c1 A convenient way to view reports is to bring up the \u201cbean-web\u201d tool on your input file. Try it: bean-web /path/to/your/file.beancount You can then point a web browser to http://localhost:8080 and click your way around the various reports generated by Beancount. You can then modify the input file and reload the web page your browser is pointing to\u2014bean-web will automatically reload the file contents. At this point, you should probably read some of the Language Syntax document. How to Organize your File \uf0c1 In this section we provide general guidelines for how to organize your file. This assumes you\u2019ve read the Language Syntax document. Preamble to your Input File \uf0c1 I recommend that you begin with just a single file 1 . My file has a header that tells Emacs what mode to open the file with, followed by some common options: ;; -*- mode: beancount; coding: utf-8; fill-column: 400; -*- option \"title\" \"My Personal Ledger\" option \"operating_currency\" \"USD\" option \"operating_currency\" \"CAD\" The title option is used in reports. The list of \u201coperating currencies\u201d identify those commodities which you use most commonly as \u201ccurrencies\u201d and which warrant rendering in their own dedicated columns in reports (this declaration has no other effect on the behavior of any of the calculations). Sections & Declaring Accounts \uf0c1 I like to organize my input file in sections that correspond to each real-world account. Each section defines all the accounts related to this real-world account by using an Open directive. For example, this is a checking account: 2007-02-01 open Assets:US:BofA:Savings USD 2007-02-01 open Income:US:BofA:Savings:Interest USD I like to declare the currency constraints as much as possible, to avoid mistakes. Also, note how I declare an income account specific to this account. This helps break down income in reporting for taxes, as you will likely receive a tax document in relation to that specific account\u2019s income (in the US this would be a 1099-INT form produced by your bank). Here\u2019s what the opening accounts might look like for an investment account: 2012-03-01 open Assets:US:Etrade:Main:Cash USD 2012-03-01 open Assets:US:Etrade:Main:ITOT ITOT 2012-03-01 open Assets:US:Etrade:Main:IXUS IXUS 2012-03-01 open Assets:US:Etrade:Main:IEFA IEFA 2012-03-01 open Income:US:Etrade:Main:Interest USD 2012-03-01 open Income:US:Etrade:Main:PnL USD 2012-03-01 open Income:US:Etrade:Main:Dividend USD 2012-03-01 open Income:US:Etrade:Main:DividendNoTax USD The point is that all these accounts are related somehow. The various sections of the cookbook will describe the set of accounts suggested to create for each section. Not all sections have to be that way. For example, I have the following sections: Eternal accounts. I have a section at the top dedicated to contain special and \u201ceternal\u201d accounts, such as payables and receivables. Daybook. I have a \u201cdaybook\u201d section at the bottom that contains all cash expenses, in chronological order. Expense accounts. All my expenses accounts (categories) are defined in their own section. Employers. For each employer I\u2019ve defined a section where I put the entries for their direct deposits, and track vacations, stock vesting and other job-related transactions. Taxes. I have a section for taxes, organized by taxation year. You can organize it any way you like, because Beancount doesn\u2019t care about the ordering of declarations. Closing Accounts \uf0c1 If a real-world account has closed, or is never going to have any more transactions posted to it, you can declare it \u201cclosed\u201d at a particular date by using a Close directive: ; Moving to another bank. 2013-06-13 close Assets:US:BofA:Savings This tells Beancount not to show the account in reports that don\u2019t include any date where it was active. It also avoids errors by triggering an error if you do try to post to it at a later date. De-duping \uf0c1 One problem that will occur frequently is that once you have some sort of code or process set up to automatically extract postings from downloaded files, you will end up importing postings which provide two separate sides of the same transaction. An example is the payment of a credit card balance via a transfer from a checking account. If you download the transactions for your checking account, you will extract something like this: 2014-06-08 * \"ONLINE PAYMENT - THANK YOU\" Assets:CA:BofA:Checking -923.24 USD The credit card download will yield you this: 2014-06-10 * \"AMEX EPAYMENT ACH PMT\" Liabilities:US:Amex:Platinum 923.24 USD Many times, transactions from these accounts need to be booked to an expense account, but in this case, these are two separate legs of the same transaction: a transfer. When you import one of these, you normally look for the other side and merge them together: ;2014-06-08 * \"ONLINE PAYMENT - THANK YOU\" 2014-06-10 * \"AMEX EPAYMENT ACH PMT\" Liabilities:US:Amex:Platinum 923.24 USD Assets:CA:BofA:Checking -923.24 USD I often leave one of the description lines in comments\u2014just my choice, Beancount ignores it. Also note that I had to choose one of the two dates. I just choose the one I prefer, as long as it does not break any balance assertion. In the case that you would forget to merge those two imported transactions, worry not! That\u2019s what balance assertions are for. Regularly place a balance assertion in either of these accounts, e.g., every time you import, and you will get a nice error if you end up entering the transaction twice. This is pretty common and after a while it becomes second nature to interpret that compiler error and fix it in seconds. Finally, when I know I import just one side of these, I select the other account manually and I mark the posting I know will be imported later with a flag, which tells me I haven\u2019t de-duped this transaction yet: 2014-06-10 * \"AMEX EPAYMENT ACH PMT\" Liabilities:US:Amex:Platinum 923.24 USD ! Assets:CA:BofA:Checking Later on, when I import the checking account\u2019s transactions and go fishing for the other side of this payment, I will find this and get a good feeling that the world is operating as it should. (If you\u2019re interested in more of a discussion around de-duplicating and merging transactions, see this feature proposal . Also, you might be interested in the \u201ceffective_date\u201d plugin external contribution, which splits transactions in two.) Which Side? \uf0c1 So if you organize your account in sections the way I suggest above, which section of the file should you leave such \u201cmerged\u201d transactions in, that is, transactions that involve two separate accounts? Well, it\u2019s your call. For example, in the case of a transfer between two accounts organized such that they have their own dedicated sections, it would be nice to be able to leave both transactions there so that when you edit your input file you see them in either section, but unfortunately, the transaction must occur in only one place in your document. You have to choose one. Personally I\u2019m a little careless about being consistent which of the section I choose to leave the transaction in; sometimes I choose one section of my input file, or that of the other account, for the same pair of accounts. It hasn\u2019t been a problem, as I use Emacs and i-search liberally which makes it easy to dig around my gigantic input file. If you want to keep your input more tidy and organized, you could come up with a rule for yourself, e.g. \u201ccredit card payments are always left in the paying account\u2019s section, not in the credit card account\u2019s section\u201d, or perhaps you could leave the transaction in both sections and comment one out 2 . Padding \uf0c1 If you\u2019re just starting out\u2014and you probably are if you\u2019re reading this\u2014you will have no historical data. This means that the balances of your Assets and Liabilities accounts in Beancount will all be zero. But the first thing you should want to do after defining some accounts is establish a balance sheet and bring those amounts to their actual current value. Let\u2019s take your checking account as an example, say you opened it a while back. You don\u2019t remember exactly when, so let\u2019s use an approximate date: 2000-05-28 open Assets:CA:BofA:Checking USD The next thing you do is look up your current balance and put a balance assertion for the corresponding amount: 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Running Beancount on just this will correctly produce an error because Beancount assumes an implicit balance assertion of \u201cempty\u201d at the time you open an account. You will have to \u201cpad\u201d your account to today\u2019s balance by inserting a balance adjustment at some point in time between the opening and the balance, against some equity account, which is an arbitrary place to book \u201cwhere you received the initial balance from.\u201d For this purpose, this is usually the \u201c Equity:Opening-Balances \u201d account. So let\u2019s include this padding transaction and recap what we have so far: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 * \"Initialize account\" Equity:Opening-Balances -1256.35 USD Assets:CA:BofA:Checking 1256.35 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD From here onwards, you would start adding entries reflecting everything that happened after 7/1. However, what if you wanted to go back in time? It is perfectly reasonable that once you\u2019ve got your chart-of-accounts set up you might want to fill in the missing history until at least the beginning of this year. Let\u2019s assume you had a single transaction in June 2014, and let\u2019s add it: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 * \"Initialize account\" Equity:Opening-Balances -1256.35 USD Assets:CA:BofA:Checking 1256.35 USD 2014-06-28 * \"Paid credit card bill\" Assets:CA:BofA:Checking -700.00 USD Liabilities:US:Amex:Platinum 700.00 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Now the balance assertion fails! You would need to adjust the initialization entry to fix this: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 * \"Initialize account\" Equity:Opening-Balances -1956.35 USD Assets:CA:BofA:Checking 1956.35 USD 2014-06-28 * \"Paid credit card bill\" Assets:CA:BofA:Checking -700.00 USD Liabilities:US:Amex:Platinum 700.00 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Now this works. So basically, every single time you insert an entry in the past, you would have to adjust the balance. Isn\u2019t this annoying? Well, yes. Fortunately, we can provide some help: you can use a Pad directive to replace and automatically synthesize the balance adjustment to match the next balance check, like this: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 pad Assets:CA:BofA:Checking Equity:Opening-Balances 2014-06-28 * \"Paid credit card bill\" Assets:CA:BofA:Checking -700.00 USD Liabilities:US:Amex:Platinum 700.00 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Note that this is only needed for balance sheet accounts (Assets and Liabilities) because we don\u2019t care about the initial balances of the Income and Expenses accounts, we only care about their transitional value (the changes they post during a period). For example, it makes no sense to bring up the Expenses:Restaurant account to the sum total value of all the costs of the meals you consumed since you were born. So you will probably want to get started with Open & Pad directives for each Assets and Liabilities accounts. What\u2019s Next? \uf0c1 At this point you will probably move onwards to the Cookbook , or read the User\u2019s Manual if you haven\u2019t already done that. It is tempting to want to break down a large file into many smaller ones, but especially at first, the convenience of having everything in a single place is great. \u21a9 Some people have suggested that Beancount automatically detect duplicated transactions based on a heuristic and automatically ignore (remove) one of the two, but this has not been tried out yet. In particular, this would lend itself well to organizing transactions not just per section, but in separate files, i.e., all files would contain all the transactions for the accounts they represent. If you\u2019re interested in adding this feature, you could easily implement this as a plugin, without disrupting the rest of the system. \u21a9","title":"Getting Started with Beancount"},{"location":"getting_started_with_beancount.html#getting-started-with-beancount","text":"Martin Blais , July 2014 http://furius.ca/beancount/doc/getting-started","title":"Getting Started with Beancount"},{"location":"getting_started_with_beancount.html#introduction","text":"This document is a gentle guide to creating your first Beancount file, initializing it with some options, some guidelines for how to organize your file, and instructions for declaring accounts and making sure their initial balance does not raise errors. It also contains some material on configuring the Emacs text editor, if you use that. You will probably want to have read some of the User\u2019s Manual first in order to familiarize yourself with the syntax and kinds of available directives, or move on to the Cookbook if you\u2019ve already setup a file or know how to do that. If you\u2019re familiar with Ledger, you may want to read up on the differences between Ledger and Beancount first.","title":"Introduction"},{"location":"getting_started_with_beancount.html#editor-support","text":"Beancount ledgers are simple text files. You can use any text editor to compose your input file. However, a good text editor which understands enough of the Beancount syntax to offer focused facilities like syntax highlighting, autocompletion, and automatic indentation highly has the potential to greatly increase productivity in compiling and maintaining your ledger.","title":"Editor Support"},{"location":"getting_started_with_beancount.html#emacs","text":"Support for editing Beancount ledger files in Emacs was traditionally distributed with Beancount. It now lives as its own project in this Github repository .","title":"Emacs"},{"location":"getting_started_with_beancount.html#vim","text":"Support for editing Beancount ledger files in Vim has been implemented by Nathan Grigg and is available in this Github repository .","title":"Vim"},{"location":"getting_started_with_beancount.html#sublime","text":"Support for editing with Sublime has been contributed by Martin Andreas Andersen and is available in this github repository or as a Sublime package here .","title":"Sublime"},{"location":"getting_started_with_beancount.html#vscode","text":"There are a number of plugins for working with Beancount text files including VSCode-Beancount by Lencerf.","title":"VSCode"},{"location":"getting_started_with_beancount.html#creating-your-first-input-file","text":"To get started, let\u2019s create a minimal input file with two accounts and a single transaction. Enter or copy the following input to a text file: 2014-01-01 open Assets:Checking 2014-01-01 open Equity:Opening-Balances 2014-01-02 * \"Deposit\" Assets:Checking 100.00 USD Equity:Opening-Balances","title":"Creating your First Input File"},{"location":"getting_started_with_beancount.html#brief-syntax-overview","text":"A few notes and an ultra brief overview of the Beancount syntax: Currencies must be entirely in capital letters (allowing numbers and some special characters, like \u201c_\u201d or \u201c-\u201d). Currency symbols (such as $ or \u20ac) are not supported. Account names do not admit spaces (though you can use dashes), and must have at least two components, separated by colons. Each component of an account name must begin with a capital letter or number. Description strings must be quoted, like this: \"AMEX PMNT\" . Dates are only parsed in ISO8601 format, that is, YYYY-MM-DD. Tags must begin with \u201c#\u201d, and links with \u201c^\u201d. For a complete description of the syntax, visit the User\u2019s Manual .","title":"Brief Syntax Overview"},{"location":"getting_started_with_beancount.html#validating-your-file","text":"The purpose of Beancount is to produce reports from your input file (either on the console or serve via its web interface). However, there is a tool that you can use to simply load its contents and make some validation checks on it, to ensure that your input does not contain errors. Beancount can be quite strict; this is a tool that you use while you\u2019re entering your data to ensure that your input file is legal. The tool is called \u201cbean-check\u201d and you invoke it like this: bean-check /path/to/your/file.beancount Try it now on the file you just created from the previous section. It should exit with no output. If there are errors, they will be printed on the console. The errors are printed out in a format that Emacs recognizes by default, so you can use Emacs\u2019 next-error and previous-error built-in functions to move the cursor to the location of the problem.","title":"Validating your File"},{"location":"getting_started_with_beancount.html#viewing-the-web-interface","text":"A convenient way to view reports is to bring up the \u201cbean-web\u201d tool on your input file. Try it: bean-web /path/to/your/file.beancount You can then point a web browser to http://localhost:8080 and click your way around the various reports generated by Beancount. You can then modify the input file and reload the web page your browser is pointing to\u2014bean-web will automatically reload the file contents. At this point, you should probably read some of the Language Syntax document.","title":"Viewing the Web Interface"},{"location":"getting_started_with_beancount.html#how-to-organize-your-file","text":"In this section we provide general guidelines for how to organize your file. This assumes you\u2019ve read the Language Syntax document.","title":"How to Organize your File"},{"location":"getting_started_with_beancount.html#preamble-to-your-input-file","text":"I recommend that you begin with just a single file 1 . My file has a header that tells Emacs what mode to open the file with, followed by some common options: ;; -*- mode: beancount; coding: utf-8; fill-column: 400; -*- option \"title\" \"My Personal Ledger\" option \"operating_currency\" \"USD\" option \"operating_currency\" \"CAD\" The title option is used in reports. The list of \u201coperating currencies\u201d identify those commodities which you use most commonly as \u201ccurrencies\u201d and which warrant rendering in their own dedicated columns in reports (this declaration has no other effect on the behavior of any of the calculations).","title":"Preamble to your Input File"},{"location":"getting_started_with_beancount.html#sections-declaring-accounts","text":"I like to organize my input file in sections that correspond to each real-world account. Each section defines all the accounts related to this real-world account by using an Open directive. For example, this is a checking account: 2007-02-01 open Assets:US:BofA:Savings USD 2007-02-01 open Income:US:BofA:Savings:Interest USD I like to declare the currency constraints as much as possible, to avoid mistakes. Also, note how I declare an income account specific to this account. This helps break down income in reporting for taxes, as you will likely receive a tax document in relation to that specific account\u2019s income (in the US this would be a 1099-INT form produced by your bank). Here\u2019s what the opening accounts might look like for an investment account: 2012-03-01 open Assets:US:Etrade:Main:Cash USD 2012-03-01 open Assets:US:Etrade:Main:ITOT ITOT 2012-03-01 open Assets:US:Etrade:Main:IXUS IXUS 2012-03-01 open Assets:US:Etrade:Main:IEFA IEFA 2012-03-01 open Income:US:Etrade:Main:Interest USD 2012-03-01 open Income:US:Etrade:Main:PnL USD 2012-03-01 open Income:US:Etrade:Main:Dividend USD 2012-03-01 open Income:US:Etrade:Main:DividendNoTax USD The point is that all these accounts are related somehow. The various sections of the cookbook will describe the set of accounts suggested to create for each section. Not all sections have to be that way. For example, I have the following sections: Eternal accounts. I have a section at the top dedicated to contain special and \u201ceternal\u201d accounts, such as payables and receivables. Daybook. I have a \u201cdaybook\u201d section at the bottom that contains all cash expenses, in chronological order. Expense accounts. All my expenses accounts (categories) are defined in their own section. Employers. For each employer I\u2019ve defined a section where I put the entries for their direct deposits, and track vacations, stock vesting and other job-related transactions. Taxes. I have a section for taxes, organized by taxation year. You can organize it any way you like, because Beancount doesn\u2019t care about the ordering of declarations.","title":"Sections & Declaring Accounts"},{"location":"getting_started_with_beancount.html#closing-accounts","text":"If a real-world account has closed, or is never going to have any more transactions posted to it, you can declare it \u201cclosed\u201d at a particular date by using a Close directive: ; Moving to another bank. 2013-06-13 close Assets:US:BofA:Savings This tells Beancount not to show the account in reports that don\u2019t include any date where it was active. It also avoids errors by triggering an error if you do try to post to it at a later date.","title":"Closing Accounts"},{"location":"getting_started_with_beancount.html#de-duping","text":"One problem that will occur frequently is that once you have some sort of code or process set up to automatically extract postings from downloaded files, you will end up importing postings which provide two separate sides of the same transaction. An example is the payment of a credit card balance via a transfer from a checking account. If you download the transactions for your checking account, you will extract something like this: 2014-06-08 * \"ONLINE PAYMENT - THANK YOU\" Assets:CA:BofA:Checking -923.24 USD The credit card download will yield you this: 2014-06-10 * \"AMEX EPAYMENT ACH PMT\" Liabilities:US:Amex:Platinum 923.24 USD Many times, transactions from these accounts need to be booked to an expense account, but in this case, these are two separate legs of the same transaction: a transfer. When you import one of these, you normally look for the other side and merge them together: ;2014-06-08 * \"ONLINE PAYMENT - THANK YOU\" 2014-06-10 * \"AMEX EPAYMENT ACH PMT\" Liabilities:US:Amex:Platinum 923.24 USD Assets:CA:BofA:Checking -923.24 USD I often leave one of the description lines in comments\u2014just my choice, Beancount ignores it. Also note that I had to choose one of the two dates. I just choose the one I prefer, as long as it does not break any balance assertion. In the case that you would forget to merge those two imported transactions, worry not! That\u2019s what balance assertions are for. Regularly place a balance assertion in either of these accounts, e.g., every time you import, and you will get a nice error if you end up entering the transaction twice. This is pretty common and after a while it becomes second nature to interpret that compiler error and fix it in seconds. Finally, when I know I import just one side of these, I select the other account manually and I mark the posting I know will be imported later with a flag, which tells me I haven\u2019t de-duped this transaction yet: 2014-06-10 * \"AMEX EPAYMENT ACH PMT\" Liabilities:US:Amex:Platinum 923.24 USD ! Assets:CA:BofA:Checking Later on, when I import the checking account\u2019s transactions and go fishing for the other side of this payment, I will find this and get a good feeling that the world is operating as it should. (If you\u2019re interested in more of a discussion around de-duplicating and merging transactions, see this feature proposal . Also, you might be interested in the \u201ceffective_date\u201d plugin external contribution, which splits transactions in two.)","title":"De-duping"},{"location":"getting_started_with_beancount.html#which-side","text":"So if you organize your account in sections the way I suggest above, which section of the file should you leave such \u201cmerged\u201d transactions in, that is, transactions that involve two separate accounts? Well, it\u2019s your call. For example, in the case of a transfer between two accounts organized such that they have their own dedicated sections, it would be nice to be able to leave both transactions there so that when you edit your input file you see them in either section, but unfortunately, the transaction must occur in only one place in your document. You have to choose one. Personally I\u2019m a little careless about being consistent which of the section I choose to leave the transaction in; sometimes I choose one section of my input file, or that of the other account, for the same pair of accounts. It hasn\u2019t been a problem, as I use Emacs and i-search liberally which makes it easy to dig around my gigantic input file. If you want to keep your input more tidy and organized, you could come up with a rule for yourself, e.g. \u201ccredit card payments are always left in the paying account\u2019s section, not in the credit card account\u2019s section\u201d, or perhaps you could leave the transaction in both sections and comment one out 2 .","title":"Which Side?"},{"location":"getting_started_with_beancount.html#padding","text":"If you\u2019re just starting out\u2014and you probably are if you\u2019re reading this\u2014you will have no historical data. This means that the balances of your Assets and Liabilities accounts in Beancount will all be zero. But the first thing you should want to do after defining some accounts is establish a balance sheet and bring those amounts to their actual current value. Let\u2019s take your checking account as an example, say you opened it a while back. You don\u2019t remember exactly when, so let\u2019s use an approximate date: 2000-05-28 open Assets:CA:BofA:Checking USD The next thing you do is look up your current balance and put a balance assertion for the corresponding amount: 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Running Beancount on just this will correctly produce an error because Beancount assumes an implicit balance assertion of \u201cempty\u201d at the time you open an account. You will have to \u201cpad\u201d your account to today\u2019s balance by inserting a balance adjustment at some point in time between the opening and the balance, against some equity account, which is an arbitrary place to book \u201cwhere you received the initial balance from.\u201d For this purpose, this is usually the \u201c Equity:Opening-Balances \u201d account. So let\u2019s include this padding transaction and recap what we have so far: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 * \"Initialize account\" Equity:Opening-Balances -1256.35 USD Assets:CA:BofA:Checking 1256.35 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD From here onwards, you would start adding entries reflecting everything that happened after 7/1. However, what if you wanted to go back in time? It is perfectly reasonable that once you\u2019ve got your chart-of-accounts set up you might want to fill in the missing history until at least the beginning of this year. Let\u2019s assume you had a single transaction in June 2014, and let\u2019s add it: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 * \"Initialize account\" Equity:Opening-Balances -1256.35 USD Assets:CA:BofA:Checking 1256.35 USD 2014-06-28 * \"Paid credit card bill\" Assets:CA:BofA:Checking -700.00 USD Liabilities:US:Amex:Platinum 700.00 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Now the balance assertion fails! You would need to adjust the initialization entry to fix this: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 * \"Initialize account\" Equity:Opening-Balances -1956.35 USD Assets:CA:BofA:Checking 1956.35 USD 2014-06-28 * \"Paid credit card bill\" Assets:CA:BofA:Checking -700.00 USD Liabilities:US:Amex:Platinum 700.00 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Now this works. So basically, every single time you insert an entry in the past, you would have to adjust the balance. Isn\u2019t this annoying? Well, yes. Fortunately, we can provide some help: you can use a Pad directive to replace and automatically synthesize the balance adjustment to match the next balance check, like this: 2000-05-28 open Assets:CA:BofA:Checking USD 2000-05-28 pad Assets:CA:BofA:Checking Equity:Opening-Balances 2014-06-28 * \"Paid credit card bill\" Assets:CA:BofA:Checking -700.00 USD Liabilities:US:Amex:Platinum 700.00 USD 2014-07-01 balance Assets:CA:BofA:Checking 1256.35 USD Note that this is only needed for balance sheet accounts (Assets and Liabilities) because we don\u2019t care about the initial balances of the Income and Expenses accounts, we only care about their transitional value (the changes they post during a period). For example, it makes no sense to bring up the Expenses:Restaurant account to the sum total value of all the costs of the meals you consumed since you were born. So you will probably want to get started with Open & Pad directives for each Assets and Liabilities accounts.","title":"Padding"},{"location":"getting_started_with_beancount.html#whats-next","text":"At this point you will probably move onwards to the Cookbook , or read the User\u2019s Manual if you haven\u2019t already done that. It is tempting to want to break down a large file into many smaller ones, but especially at first, the convenience of having everything in a single place is great. \u21a9 Some people have suggested that Beancount automatically detect duplicated transactions based on a heuristic and automatically ignore (remove) one of the two, but this has not been tried out yet. In particular, this would lend itself well to organizing transactions not just per section, but in separate files, i.e., all files would contain all the transactions for the accounts they represent. If you\u2019re interested in adding this feature, you could easily implement this as a plugin, without disrupting the rest of the system. \u21a9","title":"What\u2019s Next?"},{"location":"health_care_expenses.html","text":"Health Care Expenses \uf0c1 Martin Blais , July 2014 This is incomplete, work-in-progress, not released yet. Accounting for your health care expenses is a little different than regular expenses because of the various maximums imposed by your insurance plan. For the purpose of this section, we will assume a context of privatized health care system as is present in the USA, but the same principles may apply to other countries (if anything, it will be simpler). Accounting With No Insurance Plan - The Naive Way \uf0c1 So what\u2019s so different about health care expenses? You might argue that they should be treated the same as other expenses. Say, we could define a few categories like these: 1973-04-27 open Expenses:Health:Medical 1973-04-27 open Expenses:Health:Dental 1973-04-27 open Expenses:Health:Vision 1973-04-27 open Expenses:Health:Drugs 1973-04-27 open Expenses:Health:Acupuncture 1973-04-27 open Expenses:Health:Massage-Therapy 1973-04-27 open Expenses:Health:Physical-Therapy And simply book outflows of moneys to them when you spend on one of these categories: 2014-01-08 * \"CityPT\" \"Copay for PT session w/ Rob on 12/28\" Liabilities:US:BofA:Credit-Card -25 USD Expenses:Health:Physical-Therapy 25 USD In fact, this would be work just fine if you paid for all of your health care costs out-of-pocket. If that is your situation, this is what you should do. It\u2019s straightforward. The problem is that in practice, the great majority of people don\u2019t pay for health care costs themselves. The great majority of your health-related costs are paid for by your insurance. This does not mean that there are no costs: your insurance usually only pays for a portion of the actual expenses, depending on the service. In particular, depending on your insurance plan, in any calendar year you pay for 100% of your health care costs up to a fixed amount (usually a few hundred dollars). This is called the deductible amount. Heretofore, you pay a percentage of the service (the copayment amounts ) , and after a higher limit amount (the out-of-pocket maximum amount), you don\u2019t pay anything anymore; plans with such limits guarantee that you will never pay more than this amount. If you booked the amounts that you pay for your deductible, the scale of the expenses that would get reflected on your income statement would depend mostly on which type of service you happened to use first after January 1st. It would not accurately reflect the cost of each service that you use. It would not be that useful. Counting the Cash Payments - The Incorrect Way \uf0c1 So you might argue that you should instead book the same outflows of money to categories that reflect their true nature: 1973-04-27 open Expenses:Health:Medical:Deductible 1973-04-27 open Expenses:Health:Medical:Copayments 1973-04-27 open Expenses:Health:Vision:Deductible 1973-04-27 open Expenses:Health:Vision:Copayments 1973-04-27 open Expenses:Health:Dental:Deductible 1973-04-27 open Expenses:Health:Dental:Copayments Then booking the payments to those categories: 2014-01-08 * \"CityPT\" \"Copay for PT session w/ Rob on 12/28\" Liabilities:US:BofA:Credit-Card -25 USD Expenses:Health:Medical:Copayments 25 USD But this is largely unsatisfying, for a few reasons: while this tracks how much of was paid in total in deductible and copayments, it does not reflect anything about how much of a particular health provider services was used. More importantly, it is incorrect, because deductible and copayment limits apply to each calendar year for dates when the service was provided, not when it was paid for. In other words, if you received the service on December 28th, 2013 but paid for it the year following on January 8th, 2014, the copayment accrual is going towards the wrong year. In order to do this correctly, you must accrue the deductible and copayment amounts on the date of service , which is always provided on the documents your insurance company provides (the Explanation of Benefits , more later about this). The date of service is the date you actually were seen by the provider. Another solution would be to define accounts for each year, similarly to tax accounts (see chapter on Taxes): 2012-01-01 open Expenses:Health:Medical:Y2012:Deductible 2012-01-01 open Expenses:Health:Medical:Y2012:Copayments 2013-01-01 open Expenses:Health:Medical:Y2013:Deductible 2013-01-01 open Expenses:Health:Medical:Y2013:Copayments 2014-01-01 open Expenses:Health:Medical:Y2014:Deductible 2014-01-91 open Expenses:Health:Medical:Y2014:Copayments 2014-01-01 open Expenses:Health:Dental:Y2012:Deductible 2014-01-91 open Expenses:Health:Dental:Y2012:Copayments \u2026 Then when you book your expense, you could use the account corresponding to the year the service was received: 2014-01-08 * \"CityPT\" \"Copay for PT session w/ Rob on 12/28\" Liabilities:US:BofA:Credit-Card -25 USD Expenses:Health:Medical:Y2013:Copayments 25 USD This is not a nice solution however, due to the proliferation of accounts that need to get created for each year, and it still does not tell us how much of each service we\u2019ve consumed. We clearly need a better solution. How Health Care Insurance Payments Work \uf0c1 Let\u2019s first review how insurance payments work, as it will be needed to have a basic understanding of claims payments to correctly account for these costs. For each service, your insurance covers a portion of it and you normally pay the difference. The way that this works is that your doctor sends a bill to the insurance company and the insurance company either (a) responds to them directly with payment for the covered part of the service, or (b) the insurance company sends you a check and you are meant to sign it over to your doctor (usually after some annoying reminders for you to do so). Sometimes, but not always, your insurance company sends you a report of the doctor\u2019s claim. This document is called an explanation of benefits or \u201cEOB\u201d, and it details the portion of the service that the insurance paid for, the portion that you\u2019re responsible to pay for, and other amounts. Service providers that have no direct relationship with your insurance company will need this detail. This is a useful document for the purpose of accounting: make sure to keep a copy for yourself 1 . Here is a sample from an EOB, with the important areas highlighted: The example above exhibits the following amounts: Claims/Billed. How much the doctor asked the insurance for the service. Patient Savings. How much the service was reduced as a result of pre-established negotiations with the provider (for in-network providers). Applied to Deductible. How much of this service you need to cover yourself as part of this calendar year\u2019s deductible amount. You need to pay this. Copayments. The portion of the service not covered by the insurance. You have to pay this. Claims Payments. How much the insurance actually sent a check to the provider for. The terminology used on the EOB provided by your insurance company may vary slightly, but you should be able to find corresponding amounts easily. The doctor\u2019s billing dept. then sends you a bill for the uncovered portion 2 and you issue them a payment, by credit card, check or otherwise. Consider this a payable when the bill or EOB arrives at your door. All in all, there are four dates relevant to a claim: Date of Service. That is the date you visited the doctor\u2019s office, the date you received the service. This is the date relevant for accounting for deductibles and copayments. Date of Issue. The date that the claim was processed by the insurance. You may ignore this. Billing Date. The date the provider receive payments for its claim and issue you a bill for the remaining portion. We don\u2019t care much about this either. Date of Payment. The date you made the payment. This will automatically appear on your credit card or checking account statement, or if you paid cash, you need to enter this manually (as you do all cash transactions you care about). The most important date when you refer to any claim is the date of service . If you scan and file your EOBs to documents, it is wise to rename them to include this date in the filename. Provider Networks \uf0c1 In the USA, each medical professional (or \u201cservice provider\u201d) decides whether to maintain an established arrangement with each insurance company, depending on their rates of payment and on how much of a pain they are about paying up bills. When they do, they are considered an \u201cin-network provider\u201d by the insurance company. Otherwise they are considered an \u201cout-of-network provider\u201d and the proportion of the services that the insurance covers is much smaller. That\u2019s the only difference. The list of providers that an insurance has in their network\u2014and how difficult they are about refusing to pay bills\u2014is usually a major consideration in the selection of an insurance plan, for someone who has a choice. But because most employers compete on compensation benefits by paying for their employees\u2019 health care insurance costs, as well as the fact that they are usually able to negotiate better rates from insurance companies than an individual can because they represent a large pool of customers (the employees), the great majority of people with jobs end up choosing their employer\u2019s plan and then try to go to in-network providers. This is not always possible, however: your family doctor may decide to stop accepting your insurance as in-network during the course of the year, and you might prefer to maintain an established relationship with your doctor rather than switch, so you end up having to pay a bit more. The bottom line is that in a typical year, you usually use some services of both in-network and out-of-network professionals and you have to account for both of these costs. Accruing on Service Date - The Correct Way \uf0c1 Ideally, we would like to obtain balances for the following amounts: The amount of money that each service cost, regardless of who paid The amount of deductible and copayments that are used in each calendar year What we will do, is enter two entries: An entry for each EOB, at the date of service, for which we obtain a payable. An entry for each payments, that comes out of an Asset or Liability account. In-Network Providers \uf0c1 For in-network providers a typical entry looks like this: 2013-04-01 * \"DR RAPPOPORT\" \"Office visit\" ^anthem-claim-8765937424 Expenses:Health:Medical:Claims 225.00 USD Expenses:Health:Medical:PatientSavings -151.53 USD Liabilities:US:Accounts-Payable -10.00 USD ; Copay Expenses:Health:Medical:ClaimsPayment -63.47 USD Once you\u2019ve figured out which numbers to pull out of of these EOBs, it becomes simpler to enter others, because they are very regular and all look the same. Note that an in-network EOB will include a \u201cPatient Savings\u201d number. This is meant to reflect how much lower you\u2019re paying due to your insurance plan selection. (In practice, this is a bit of smoke and mirrors, because all the claims made by doctors are inflated to reflect the amount they actually get paid for, but I think the insurance likes to show these to you anyway.) I like to immediately reflect the missing portion to an \u201caccounts payable\u201d liability, which tells me I have this registered as an upcoming payment: 1973-04-27 open Liabilities:US:Accounts-Payable You can then eliminate this payable as the payment entry comes in through your credit card import: 2014-01-08 * \"Rappoport Medical Office\" ^anthem-claim-8765937424 Liabilities:US:BofA:Credit-Card -10.00 USD Liabilities:US:Accounts-Payable 10.00 USD Note how I \u201clink\u201d both of the transactions to each other with \u201c ^anthem-claim-8765937424 \u201d so they can easily be found and verified later on. For in-network doctors, because the payments from the insurance are generally predictable, the doctor\u2019s office will sometimes require the copayment on the same day you visit. Just book this as a cash entry against the same liability account. If you have a doctor that you visit regularly, you may create dedicated Liabilities accounts for them: 2009-09-17 open Liabilities:US:Accounts-Payable:DrRappoport Out-of-Network Providers \uf0c1 For out-of-network providers, the EOBs are slightly different: What we noticed is that there is an \u201cother amount\u201d section, which is essentially what will be written off by the provider, and the Patient Savings section is empty. A typical entry corresponding to this EOB would look like this: 2014-02-13 * \"ROB BOONAN\" | \"Physical Therapy\" ^anthem-claim-17646398 Expenses:Health:PhysicalTherapy:Claims 320.00 USD Expenses:Health:PhysicalTherapy:ClaimsPayment -115.14 USD Expenses:Health:PhysicalTherapy:Uncovered -155.52 USD Liabilities:US:Accounts-Payable:CityPT -49.34 USD You will typically receive the check for your service provider\u2019s payment (in the example, that is a check for $115.14) and have to send it over to them yourself. You can either deposit the check right away and make the full payment with your credit card, or send the check and just pay the difference (this is what most people do): 2014-01-08 * \"City PhysioTherapy\" ^anthem-claim-17646398 Liabilities:US:BofA:Credit-Card -49.34 USD Liabilities:US:Accounts-Payable:CityPT 49.34 USD One interesting twist is that many out-of-network providers will accept the lower amount that insurance companies pay for out-of-network services and write off the excess amount, or charge you nominal amount only. For example, the \u201cdeal\u201d with my favorite physical therapy place is that they charge me 25$ per session. Once again, the claim is inflated, of course, and the apparent amount the insurance says you have to pay has to be largely written off. This is how you\u2019d book such an entry\u2019s EOB: 2014-02-13 * \"ROB BOONAN\" | \"Physical Therapy\" ^anthem-claim-17646398 Expenses:Health:PhysicalTherapy:Claims 320.00 USD Expenses:Health:PhysicalTherapy:ClaimsPayment -115.14 USD Liabilities:US:Accounts-Payable:CityPT -25.00 USD Expenses:Health:PhysicalTherapy:WriteOffs The provider\u2019s administrator does not charge me on every visit. She accrues the amounts and whenever decides to process her accounting, she makes a charge for the total, which clears the account to zero. This might look like this: 2014-02-26 * \"Payment for 3 Physical Therapy treatments\" Liabilities:US:BofA:CreditCard -75.00 USD Liabilities:US:Accounts-Payable:CityPT Tracking Deductible and Copayment Limits \uf0c1 As we\u2019ve seen previously, there are limits on how much you end up paying for health care services. These limits are applied by calendar year, so they need to be applied at the date of service. For this reasons, we can attach corresponding entries to the EOB entries, and then filter transactions by year to calculate the balances for that year. I like to keep tabs on these limits and ensure that they are applied properly. According to my insurance plan, the limits are defined like this: Deductible. \u201cThe amount you pay out-of-pocket within a calendar year before your health insurance begins to pay for covered service.\u201d Out-of-pocket maximum. \u201cThe maximum amount of money you could pay out-of-pocket in a calendar year for covered health care services. There are exclusions, e.g. for prescription drugs and some services.\u201d Co-payment. \u201cA fixed fee that you pay out-of-pocket for a service. This fee does not vary according to the actual cost of the service.\u201d It is not straightforward, however, because there are distinct limits for in-network and out-of-network providers, both for deductible amounts and copayment amounts: Amounts paid for in-network deductibles count towards your limit for out-of-network deductibles. Amounts paid for in-network copayments count towards your limit for out-of-network copayments. Amounts paid for deductibles count towards your limit for copayments. I carry this out using the \u201cmirror accounting\u201d technique I describe in another document. The idea is to use an alternative currency to count for these amounts. TODO: complete this Insurance Premiums \uf0c1 Then you have to track how much you spend on your insurance premiums. These are the fixed semi-monthly payments you make\u2014usually directly processed out of your paycheck\u2014for getting the insurance policy itself. As for most people with jobs in the US, my employer offers a few health coverage plans and pays for most of it, but there is always a portion I have to pay myself: 1973-04-27 open Expenses:Health:Insurance A fixed premium is automatically deducted from my paycheck entries, as it appears on my pay stub: 2014-02-08 * \"Acme Corp - Salary\" \u2026 Expenses:Health:Insurance 42.45 USD \u2026 On your pay stub, be careful not to confuse this with the \u201cMedicare tax,\u201d which is a tax used to pay for the bit of socialized medicine costs the US has for older people. This is just a tax and has little to do with your own health care expenses. Drugs \uf0c1 Drugs are accounted for separately. TODO: complete this You do not always receive these through the mail, but insurance companies are now finally coming up with websites where you can download all the claims that were made on your behalf (and account for them), even those which weren\u2019t mailed to you. \u21a9 This is a tremendous annoyance because each doctor has a different billing department, they are typically antiquated and their online payment options are almost always broken (you end up having to call a phone number and convince a grumpy administrator to pay by credit card because they are still used to receiving checks ), and all the different service providers that collaborate together to offer you a health service will bill you separately (with different billing departments as well). For example, if you undergo surgery, over the following 6 months you can expect to get different bills from your surgeon, his assistant, the anesthesiologist, the person who monitored your brain activity while asleep, the lab who did the blood work, the person who read an X-ray, and so on. And the bills may be sent more than 6 months after the operation took place, well into the following year. It\u2019s completely Kafkaesque. You have to really wonder why the insurance company does not always pay the full service to the providers and then bill you , just once, for the deductibles. That would make the process a lot simpler and reduce the exhorbitant cost of administering health care. \u21a9","title":"Health Care Expenses"},{"location":"health_care_expenses.html#health-care-expenses","text":"Martin Blais , July 2014 This is incomplete, work-in-progress, not released yet. Accounting for your health care expenses is a little different than regular expenses because of the various maximums imposed by your insurance plan. For the purpose of this section, we will assume a context of privatized health care system as is present in the USA, but the same principles may apply to other countries (if anything, it will be simpler).","title":"Health Care Expenses"},{"location":"health_care_expenses.html#accounting-with-no-insurance-plan-the-naive-way","text":"So what\u2019s so different about health care expenses? You might argue that they should be treated the same as other expenses. Say, we could define a few categories like these: 1973-04-27 open Expenses:Health:Medical 1973-04-27 open Expenses:Health:Dental 1973-04-27 open Expenses:Health:Vision 1973-04-27 open Expenses:Health:Drugs 1973-04-27 open Expenses:Health:Acupuncture 1973-04-27 open Expenses:Health:Massage-Therapy 1973-04-27 open Expenses:Health:Physical-Therapy And simply book outflows of moneys to them when you spend on one of these categories: 2014-01-08 * \"CityPT\" \"Copay for PT session w/ Rob on 12/28\" Liabilities:US:BofA:Credit-Card -25 USD Expenses:Health:Physical-Therapy 25 USD In fact, this would be work just fine if you paid for all of your health care costs out-of-pocket. If that is your situation, this is what you should do. It\u2019s straightforward. The problem is that in practice, the great majority of people don\u2019t pay for health care costs themselves. The great majority of your health-related costs are paid for by your insurance. This does not mean that there are no costs: your insurance usually only pays for a portion of the actual expenses, depending on the service. In particular, depending on your insurance plan, in any calendar year you pay for 100% of your health care costs up to a fixed amount (usually a few hundred dollars). This is called the deductible amount. Heretofore, you pay a percentage of the service (the copayment amounts ) , and after a higher limit amount (the out-of-pocket maximum amount), you don\u2019t pay anything anymore; plans with such limits guarantee that you will never pay more than this amount. If you booked the amounts that you pay for your deductible, the scale of the expenses that would get reflected on your income statement would depend mostly on which type of service you happened to use first after January 1st. It would not accurately reflect the cost of each service that you use. It would not be that useful.","title":"Accounting With No Insurance Plan - The Naive Way"},{"location":"health_care_expenses.html#counting-the-cash-payments-the-incorrect-way","text":"So you might argue that you should instead book the same outflows of money to categories that reflect their true nature: 1973-04-27 open Expenses:Health:Medical:Deductible 1973-04-27 open Expenses:Health:Medical:Copayments 1973-04-27 open Expenses:Health:Vision:Deductible 1973-04-27 open Expenses:Health:Vision:Copayments 1973-04-27 open Expenses:Health:Dental:Deductible 1973-04-27 open Expenses:Health:Dental:Copayments Then booking the payments to those categories: 2014-01-08 * \"CityPT\" \"Copay for PT session w/ Rob on 12/28\" Liabilities:US:BofA:Credit-Card -25 USD Expenses:Health:Medical:Copayments 25 USD But this is largely unsatisfying, for a few reasons: while this tracks how much of was paid in total in deductible and copayments, it does not reflect anything about how much of a particular health provider services was used. More importantly, it is incorrect, because deductible and copayment limits apply to each calendar year for dates when the service was provided, not when it was paid for. In other words, if you received the service on December 28th, 2013 but paid for it the year following on January 8th, 2014, the copayment accrual is going towards the wrong year. In order to do this correctly, you must accrue the deductible and copayment amounts on the date of service , which is always provided on the documents your insurance company provides (the Explanation of Benefits , more later about this). The date of service is the date you actually were seen by the provider. Another solution would be to define accounts for each year, similarly to tax accounts (see chapter on Taxes): 2012-01-01 open Expenses:Health:Medical:Y2012:Deductible 2012-01-01 open Expenses:Health:Medical:Y2012:Copayments 2013-01-01 open Expenses:Health:Medical:Y2013:Deductible 2013-01-01 open Expenses:Health:Medical:Y2013:Copayments 2014-01-01 open Expenses:Health:Medical:Y2014:Deductible 2014-01-91 open Expenses:Health:Medical:Y2014:Copayments 2014-01-01 open Expenses:Health:Dental:Y2012:Deductible 2014-01-91 open Expenses:Health:Dental:Y2012:Copayments \u2026 Then when you book your expense, you could use the account corresponding to the year the service was received: 2014-01-08 * \"CityPT\" \"Copay for PT session w/ Rob on 12/28\" Liabilities:US:BofA:Credit-Card -25 USD Expenses:Health:Medical:Y2013:Copayments 25 USD This is not a nice solution however, due to the proliferation of accounts that need to get created for each year, and it still does not tell us how much of each service we\u2019ve consumed. We clearly need a better solution.","title":"Counting the Cash Payments - The Incorrect Way"},{"location":"health_care_expenses.html#how-health-care-insurance-payments-work","text":"Let\u2019s first review how insurance payments work, as it will be needed to have a basic understanding of claims payments to correctly account for these costs. For each service, your insurance covers a portion of it and you normally pay the difference. The way that this works is that your doctor sends a bill to the insurance company and the insurance company either (a) responds to them directly with payment for the covered part of the service, or (b) the insurance company sends you a check and you are meant to sign it over to your doctor (usually after some annoying reminders for you to do so). Sometimes, but not always, your insurance company sends you a report of the doctor\u2019s claim. This document is called an explanation of benefits or \u201cEOB\u201d, and it details the portion of the service that the insurance paid for, the portion that you\u2019re responsible to pay for, and other amounts. Service providers that have no direct relationship with your insurance company will need this detail. This is a useful document for the purpose of accounting: make sure to keep a copy for yourself 1 . Here is a sample from an EOB, with the important areas highlighted: The example above exhibits the following amounts: Claims/Billed. How much the doctor asked the insurance for the service. Patient Savings. How much the service was reduced as a result of pre-established negotiations with the provider (for in-network providers). Applied to Deductible. How much of this service you need to cover yourself as part of this calendar year\u2019s deductible amount. You need to pay this. Copayments. The portion of the service not covered by the insurance. You have to pay this. Claims Payments. How much the insurance actually sent a check to the provider for. The terminology used on the EOB provided by your insurance company may vary slightly, but you should be able to find corresponding amounts easily. The doctor\u2019s billing dept. then sends you a bill for the uncovered portion 2 and you issue them a payment, by credit card, check or otherwise. Consider this a payable when the bill or EOB arrives at your door. All in all, there are four dates relevant to a claim: Date of Service. That is the date you visited the doctor\u2019s office, the date you received the service. This is the date relevant for accounting for deductibles and copayments. Date of Issue. The date that the claim was processed by the insurance. You may ignore this. Billing Date. The date the provider receive payments for its claim and issue you a bill for the remaining portion. We don\u2019t care much about this either. Date of Payment. The date you made the payment. This will automatically appear on your credit card or checking account statement, or if you paid cash, you need to enter this manually (as you do all cash transactions you care about). The most important date when you refer to any claim is the date of service . If you scan and file your EOBs to documents, it is wise to rename them to include this date in the filename.","title":"How Health Care Insurance Payments Work"},{"location":"health_care_expenses.html#provider-networks","text":"In the USA, each medical professional (or \u201cservice provider\u201d) decides whether to maintain an established arrangement with each insurance company, depending on their rates of payment and on how much of a pain they are about paying up bills. When they do, they are considered an \u201cin-network provider\u201d by the insurance company. Otherwise they are considered an \u201cout-of-network provider\u201d and the proportion of the services that the insurance covers is much smaller. That\u2019s the only difference. The list of providers that an insurance has in their network\u2014and how difficult they are about refusing to pay bills\u2014is usually a major consideration in the selection of an insurance plan, for someone who has a choice. But because most employers compete on compensation benefits by paying for their employees\u2019 health care insurance costs, as well as the fact that they are usually able to negotiate better rates from insurance companies than an individual can because they represent a large pool of customers (the employees), the great majority of people with jobs end up choosing their employer\u2019s plan and then try to go to in-network providers. This is not always possible, however: your family doctor may decide to stop accepting your insurance as in-network during the course of the year, and you might prefer to maintain an established relationship with your doctor rather than switch, so you end up having to pay a bit more. The bottom line is that in a typical year, you usually use some services of both in-network and out-of-network professionals and you have to account for both of these costs.","title":"Provider Networks"},{"location":"health_care_expenses.html#accruing-on-service-date-the-correct-way","text":"Ideally, we would like to obtain balances for the following amounts: The amount of money that each service cost, regardless of who paid The amount of deductible and copayments that are used in each calendar year What we will do, is enter two entries: An entry for each EOB, at the date of service, for which we obtain a payable. An entry for each payments, that comes out of an Asset or Liability account.","title":"Accruing on Service Date - The Correct Way"},{"location":"health_care_expenses.html#in-network-providers","text":"For in-network providers a typical entry looks like this: 2013-04-01 * \"DR RAPPOPORT\" \"Office visit\" ^anthem-claim-8765937424 Expenses:Health:Medical:Claims 225.00 USD Expenses:Health:Medical:PatientSavings -151.53 USD Liabilities:US:Accounts-Payable -10.00 USD ; Copay Expenses:Health:Medical:ClaimsPayment -63.47 USD Once you\u2019ve figured out which numbers to pull out of of these EOBs, it becomes simpler to enter others, because they are very regular and all look the same. Note that an in-network EOB will include a \u201cPatient Savings\u201d number. This is meant to reflect how much lower you\u2019re paying due to your insurance plan selection. (In practice, this is a bit of smoke and mirrors, because all the claims made by doctors are inflated to reflect the amount they actually get paid for, but I think the insurance likes to show these to you anyway.) I like to immediately reflect the missing portion to an \u201caccounts payable\u201d liability, which tells me I have this registered as an upcoming payment: 1973-04-27 open Liabilities:US:Accounts-Payable You can then eliminate this payable as the payment entry comes in through your credit card import: 2014-01-08 * \"Rappoport Medical Office\" ^anthem-claim-8765937424 Liabilities:US:BofA:Credit-Card -10.00 USD Liabilities:US:Accounts-Payable 10.00 USD Note how I \u201clink\u201d both of the transactions to each other with \u201c ^anthem-claim-8765937424 \u201d so they can easily be found and verified later on. For in-network doctors, because the payments from the insurance are generally predictable, the doctor\u2019s office will sometimes require the copayment on the same day you visit. Just book this as a cash entry against the same liability account. If you have a doctor that you visit regularly, you may create dedicated Liabilities accounts for them: 2009-09-17 open Liabilities:US:Accounts-Payable:DrRappoport","title":"In-Network Providers"},{"location":"health_care_expenses.html#out-of-network-providers","text":"For out-of-network providers, the EOBs are slightly different: What we noticed is that there is an \u201cother amount\u201d section, which is essentially what will be written off by the provider, and the Patient Savings section is empty. A typical entry corresponding to this EOB would look like this: 2014-02-13 * \"ROB BOONAN\" | \"Physical Therapy\" ^anthem-claim-17646398 Expenses:Health:PhysicalTherapy:Claims 320.00 USD Expenses:Health:PhysicalTherapy:ClaimsPayment -115.14 USD Expenses:Health:PhysicalTherapy:Uncovered -155.52 USD Liabilities:US:Accounts-Payable:CityPT -49.34 USD You will typically receive the check for your service provider\u2019s payment (in the example, that is a check for $115.14) and have to send it over to them yourself. You can either deposit the check right away and make the full payment with your credit card, or send the check and just pay the difference (this is what most people do): 2014-01-08 * \"City PhysioTherapy\" ^anthem-claim-17646398 Liabilities:US:BofA:Credit-Card -49.34 USD Liabilities:US:Accounts-Payable:CityPT 49.34 USD One interesting twist is that many out-of-network providers will accept the lower amount that insurance companies pay for out-of-network services and write off the excess amount, or charge you nominal amount only. For example, the \u201cdeal\u201d with my favorite physical therapy place is that they charge me 25$ per session. Once again, the claim is inflated, of course, and the apparent amount the insurance says you have to pay has to be largely written off. This is how you\u2019d book such an entry\u2019s EOB: 2014-02-13 * \"ROB BOONAN\" | \"Physical Therapy\" ^anthem-claim-17646398 Expenses:Health:PhysicalTherapy:Claims 320.00 USD Expenses:Health:PhysicalTherapy:ClaimsPayment -115.14 USD Liabilities:US:Accounts-Payable:CityPT -25.00 USD Expenses:Health:PhysicalTherapy:WriteOffs The provider\u2019s administrator does not charge me on every visit. She accrues the amounts and whenever decides to process her accounting, she makes a charge for the total, which clears the account to zero. This might look like this: 2014-02-26 * \"Payment for 3 Physical Therapy treatments\" Liabilities:US:BofA:CreditCard -75.00 USD Liabilities:US:Accounts-Payable:CityPT","title":"Out-of-Network Providers"},{"location":"health_care_expenses.html#tracking-deductible-and-copayment-limits","text":"As we\u2019ve seen previously, there are limits on how much you end up paying for health care services. These limits are applied by calendar year, so they need to be applied at the date of service. For this reasons, we can attach corresponding entries to the EOB entries, and then filter transactions by year to calculate the balances for that year. I like to keep tabs on these limits and ensure that they are applied properly. According to my insurance plan, the limits are defined like this: Deductible. \u201cThe amount you pay out-of-pocket within a calendar year before your health insurance begins to pay for covered service.\u201d Out-of-pocket maximum. \u201cThe maximum amount of money you could pay out-of-pocket in a calendar year for covered health care services. There are exclusions, e.g. for prescription drugs and some services.\u201d Co-payment. \u201cA fixed fee that you pay out-of-pocket for a service. This fee does not vary according to the actual cost of the service.\u201d It is not straightforward, however, because there are distinct limits for in-network and out-of-network providers, both for deductible amounts and copayment amounts: Amounts paid for in-network deductibles count towards your limit for out-of-network deductibles. Amounts paid for in-network copayments count towards your limit for out-of-network copayments. Amounts paid for deductibles count towards your limit for copayments. I carry this out using the \u201cmirror accounting\u201d technique I describe in another document. The idea is to use an alternative currency to count for these amounts. TODO: complete this","title":"Tracking Deductible and Copayment Limits"},{"location":"health_care_expenses.html#insurance-premiums","text":"Then you have to track how much you spend on your insurance premiums. These are the fixed semi-monthly payments you make\u2014usually directly processed out of your paycheck\u2014for getting the insurance policy itself. As for most people with jobs in the US, my employer offers a few health coverage plans and pays for most of it, but there is always a portion I have to pay myself: 1973-04-27 open Expenses:Health:Insurance A fixed premium is automatically deducted from my paycheck entries, as it appears on my pay stub: 2014-02-08 * \"Acme Corp - Salary\" \u2026 Expenses:Health:Insurance 42.45 USD \u2026 On your pay stub, be careful not to confuse this with the \u201cMedicare tax,\u201d which is a tax used to pay for the bit of socialized medicine costs the US has for older people. This is just a tax and has little to do with your own health care expenses.","title":"Insurance Premiums"},{"location":"health_care_expenses.html#drugs","text":"Drugs are accounted for separately. TODO: complete this You do not always receive these through the mail, but insurance companies are now finally coming up with websites where you can download all the claims that were made on your behalf (and account for them), even those which weren\u2019t mailed to you. \u21a9 This is a tremendous annoyance because each doctor has a different billing department, they are typically antiquated and their online payment options are almost always broken (you end up having to call a phone number and convince a grumpy administrator to pay by credit card because they are still used to receiving checks ), and all the different service providers that collaborate together to offer you a health service will bill you separately (with different billing departments as well). For example, if you undergo surgery, over the following 6 months you can expect to get different bills from your surgeon, his assistant, the anesthesiologist, the person who monitored your brain activity while asleep, the lab who did the blood work, the person who read an X-ray, and so on. And the bills may be sent more than 6 months after the operation took place, well into the following year. It\u2019s completely Kafkaesque. You have to really wonder why the insurance company does not always pay the full service to the providers and then bill you , just once, for the deductibles. That would make the process a lot simpler and reduce the exhorbitant cost of administering health care. \u21a9","title":"Drugs"},{"location":"how_inventories_work.html","text":"How Inventories Work \uf0c1 Martin Blais , December 2016 http://furius.ca/beancount/doc/booking This document explains how we accumulate commodities and match sales (reductions) against accumulated inventory contents. Introduction \uf0c1 Beyond the ability to track and list the postings made to each of the accounts (an operation that produces a journal of entries), one of the most common and useful operations of Beancount is to sum up the positions of arbitrary sets of postings. These aggregations are at the heart of how Beancount works, and are implemented in an object called \u201cinventory.\u201d This document explains how this aggregation process works. If you\u2019re going to track investments, it\u2019s necessary to understand what follows. Matches & Booking Methods \uf0c1 In order to get the big picture, let\u2019s walk through the various booking features by way of a simple examples. This should expose you to all the main ideas in one go. Simple Postings \u2014 No Cost \uf0c1 Consider a set of simple postings to an account, e.g., 2016-04-24 * \"Deposit check\" Assets:Bank:Checking 221.23 USD \u2026 2016-04-29 * \"ATM Withdrawal\" Assets:Bank:Checking -100.00 USD \u2026 2016-04-29 * \"Debit card payment\" Assets:Bank:Checking -45.67 USD \u2026 The inventory of the Checking account begins empty. After the first transaction, its contents are 221.23 USD. After the second, 121.23 USD. Finally, the third transaction brings this balance to 75.56 USD. This seems very natural; the numbers simply add to. It might be obvious, but note also that the numbers are allowed to change the sign (go negative). Multiple Commodities \uf0c1 An inventory may contain more than one type of commodity. It is equivalent to a mapping from commodity to some number of units. For example, 2016-07-24 * \"Dinner before leaving NYC\" Expenses:Restaurants 34.58 USD \u2026 2016-07-26 * \"Food with friends after landing\" Expenses:Restaurants 62.11 CAD \u2026 After those two transactions, the Restaurants account contains 34.58 USD and 62.11 CAD. Its contents are said to be of mixed commodities. And naturally, postings are applied to just the currencies they affect. For instance, the following transaction 2016-07-27 * \"Brunch\" Expenses:Restaurants 23.91 CAD \u2026 brings the balance of that account to 34.58 USD and 86.02 CAD. The number of units USD hasn\u2019t changed. Note that accounts may contain any number of commodities, and this is also true for commodities held at cost, which we\u2019ll see shortly. While this is made possible, I recommend that you define enough accounts to keep a single commodity in each; this can be enforced with the \u201c onecommodity \u201d plugin. Cost Basis \uf0c1 Things get a little more hairy when we consider the tracking of investments with a cost basis. Beancount allows you to associate a cost basis and an optional label with a particular lot acquired. Consider these two purchases to an investment account: 2015-04-01 * \"Buy some shares of Hooli in April\" Assets:Invest 25 HOOL {23.00 USD, \"first-lot\"} \u2026 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest 35 HOOL {27.00 USD} \u2026 So now, the investment account\u2019s inventory contains units ccy cost cost-ccy lot-date label 25 HOOL {23.00 USD, 2015-04-01, \"first-lot\"} 35 HOOL {27.00 USD, 2015-05-01, None} Those two lots were not merged, they are still two distinct positions in the inventory. Inventories merge lots together and adjust the units only if the commodity and all of its cost attributes exactly match. (In practice, it\u2019s pretty rare that two augmentations will have the same cost and date attributes.) Note how Beancount automatically associated the acquisition date to each lot; you can override it if desired, by adding the date similar to the optional label. this is useful for making cost basis adjustments). Postings that add to the content of an inventory are called augmentations . Reductions \uf0c1 But how do we remove commodities from an inventory? You could eat away at an existing lot by selling some of it. You do this by posting a reduction to the account, like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {23.00 USD} \u2026 Just to be clear, what makes this posting a reduction is the mere fact that the sign (-) is opposite that of the balance of the account (+25) for that commodity. This posting tells Beancount to find all lots with a cost basis of 23.00 USD and remove 12 units from it. The resulting inventory will be units ccy cost cost-ccy lot-date label 13 HOOL {23.00 USD, 2015-04-01, \"first-lot\"} 35 HOOL {27.00 USD, 2015-05-01, None} Note how the first posting was reduced by 12 units. We didn\u2019t have to specify all of the lot\u2019s attributes, just the cost. We could have equivalently used the date to specify which lot to reduce: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {2015-04-01} \u2026 Or the label: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {\"first-lot\"} \u2026 Or a combination of these. Any combination of attributes will be matched against the inventory contents to find which lot to reduce. In fact, if the inventory happened to have just a single lot in it, you could reduce it like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {} \u2026 Ambiguous Matches \uf0c1 But what happens if multiple lots match the reduction? For example, with the previous inventory containing two lots, if you wrote your sale like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {} \u2026 Beancount wouldn\u2019t be able to figure out which lot needs to get reduced. We have an ambiguous match. Partially ambiguous matches are also possible. For example, if you have the following inventory to reduce from: units ccy cost cost-ccy lot-date label 25 HOOL {23.00 USD, 2015-04-01, None} 30 HOOL {25.00 USD, 2015-04-01, None} 35 HOOL {27.00 USD, 2015-05-01, None} And you attempted to reduce like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {2015-04-01} \u2026 The first two lots are selected as matches. Strict Booking \uf0c1 What does Beancount do with ambiguous matches? By default, it issues an error. More precisely, what happens is that Beancount invokes the booking method and it handles the ambiguous match depending on what it is set. The default booking method is \u201c STRICT \u201d and it just gives up and spits out an error, telling you you need to refine your input in order to disambiguate your inventory reduction. FIFO and LIFO Booking \uf0c1 Other booking methods are available. They can be configured using options, like this: option \"booking_method\" \"FIFO\" The \u201c FIFO \u201d method automatically selects the oldest of the matching lots up to the requested size of the reduction. For example, given our previous inventory: units ccy cost cost-ccy lot-date label 25 HOOL {23.00 USD, 2015-04-01, \"first-lot\"} 35 HOOL {27.00 USD, 2015-05-01, None} Attempting to reduce 28 shares like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -28 HOOL {} \u2026 would match both lots, completely reduce the first one to zero units and remove the remaining 3 units from the second lot, to result in the following inventory: units ccy cost cost-ccy lot-date label 32 HOOL {27.00 USD, 2015-05-01, None} The \u201cLIFO\u201d method works similarly, but consumes the youngest (latest) lots first, working its way backward in time to remove the volume. Per-account Booking Method \uf0c1 You don\u2019t have to make all accounts follow the same booking method; the option in the previous section sets the default method for all accounts. In order to override the booking method for a particular account, you can use an optional string option on the account\u2019s Open directive, like this: 2014-01-01 open Assets:Invest \"FIFO\" This allows you to treat different accounts with a different booking resolution. Total Matches \uf0c1 There is an exception to strict booking: if the entire inventory is being reduced by exactly the total number of units, it\u2019s clear that all the matching lots are to be selected and this is considered unambiguous, even under \u201cSTRICT\u201d booking. For example, under \u201cSTRICT\u201d booking, this reduction would empty up the previous inventory without raising an error, because there are 25 + 35 shares matching: 2015-05-15 * \"Sell all my shares\" Assets:Invest -60 HOOL {} \u2026 Average Booking \uf0c1 Retirement accounts created by government incentive programs (such as the 401k plan in the US or the RRSP in Canada) typically consist in pre-tax money. For these types of accounts, brokers usually disregard the calculation of cost basis because the taxation is to be made upon distributing money outside the account. These accounts are often managed to the extent that they are fully invested; therefore, fees are often taken as shares of the investment portfolio, priced on the day the fee is paid out. This makes it awkward to track the cost basis of individual lots. The correct way to deal with this is to treat the number of units and the cost basis of each commodity\u2019s entire set of lots separately. For example, the following two transactions: 2016-07-28 * \"Buy some shares of retirement fund\" Assets:Invest 45.0045 VBMPX {11.11 USD} \u2026 2016-10-12 * \"Buy some shares of retirement fund\" Assets:Invest 54.5951 VBMPX {10.99 USD} \u2026 Should result in a single lot with the total number of units and the averaged cost: units ccy cost cost-ccy lot-date label 99.5996 VBMPX {11.0442 USD, 2016-07-28, None} A fee taken in this account might look like this: 2016-12-30 * \"Quarterly management fee\" Assets:Invest -1.4154 VBMPX {10.59 USD} Expenses:Fees Even with negative units the number and cost get aggregated separately: units ccy cost cost-ccy lot-date label 98.1842 VBMPX {11.0508 USD, 2016-07-28, None} This feature isn\u2019t yet supported in Beancount; it\u2019s fairly tricky to implement, and will be the subject in a minor release in the future. No Booking \uf0c1 However, there is another way to deal with non-taxable accounts in the meantime: you can simply disable the booking. There is a booking method called \u201c NONE \u201d which implements a very liberal strategy which accepts any new lot.. New lots are always appended unconditionally to the inventory. Using this strategy on the transactions from the previous section would result in this inventory: units ccy cost cost-ccy lot-date label 45.0045 VBMPX {11.11 USD, 2016-07-28, None} 54.5951 VBMPX {10.99 USD, 2016-10-12, None} -1.4154 VBMPX {10.59 USD, 2016-12-30, None} Observe how the resulting inventory has a mix of signs; normally this is not allowed, but it is tolerated under this degenerate booking method. Note that under this method, the only meaningful values are the total number of units and the total or average cost amounts. The individual lots aren\u2019t really lots, they only represent the list of all postings made to that account. Note: If you are familiar with Ledger, this is the default and only booking method that it supports. Summary \uf0c1 In summary, here\u2019s what we presented in the walkthrough. Augmentations are never problematic; they always add a new position to an existing inventory. On the other hand, reductions may result in a few outcomes: Single match. Only one position matches the reduction; it is reduced. Total match. The total number of units requested matches the total number of units of the positions matched. These positions are reduced away. No match. None of the positions matches the reducing posting. An error is raised. Ambiguous matches. More than one position in the inventory matches the reducing posting; the booking method is invoked to handle this . There are a few booking methods available to handle the last case: STRICT. An error is raised. FIFO. Units from oldest (earliest) lots are selected until the reduction is complete. LIFO. Units from youngest (latest) lots are selected until the reduction is complete. AVERAGE. After every reduction, all the units of the affected commodity are merged and their new average cost is recalculated. NONE. Booking is disabled; the reducing lots is simply added to the inventory. This results in an inventory with mixed signs and only the total number of units and total cost basis are sensible numbers. Beancount has a default booking method for all accounts, which can be overridden with an option: option \"booking_method\" \"FIFO\" The default value for the booking method is \u201cSTRICT\u201d. I recommend that you leave it that way and override the booking method for specific accounts. The method can be specified for each account by adding a string to its Open directive: 2016-05-01 open Assets:Vanguard:RGAGX \"AVERAGE\" How Prices are Used \uf0c1 The short answer is that prices aren\u2019t used nor affect the booking algorithm at all. However, it is relevant to discuss what they do in this context because users invariably get confused about their interpretation. There are two use cases for prices: making conversions between commodities and tagging a reducing lot with its sale price in order to record it and optionally balance the proceeds. Commodity Conversions \uf0c1 Conversions are used to exchange one currency for another. They look like this: 2016-04-24 * \"Deposit check\" Assets:Bank:Checking 220.00 USD @ 1.3 CAD Income:Payment -286.00 CAD For the purpose of booking it against the Checking account\u2019s inventory, the posting with the price attached to it is treated just the same as if there was no price: the Checking account simply receives a deposit of 220.00 units of USD and will match against positions of commodity \u201cUSD\u201d. The price is used only to verify that the transaction balances and ensure the double-entry accounting rule is respected (220.00 x 1.3 CAD + -286.00 CAD = 0.00). It is otherwise ignored for the purpose of modifying the inventory contents. In a sense, after the postings have been applied to the account inventories, the price is forgotten and the inventory balance retains no memory of the deposit having occurred from a conversion. Price vs. Cost Basis \uf0c1 One might wonder how the price is used if there is a cost basis specification, like this: 2015-05-15 * \"Sell some shares\" Assets:Invest:HOOL -12 HOOL {23.00 USD} @ 24.70 USD Assets:Invest:Cash 296.40 USD Income:Invest:Gains The answer is often surprising to many users: the price is not used by the balancing algorithm if there is a cost basis; the cost basis is the number used to balance the postings. This is a useful property that allows us to compute capital gains automatically. In the previous example, the balance algorithm would sum up -12 x 23.00 + 296.40 = -20.40 USD, which is the capital gain, (24.70 - 23.00) * 12. It would complete the last posting and assign it this value. In general, the way that profits on sales are calculated is by weighing the proceedings, i.e., the cash deposits, against the cost basis of the sold lots, and this is sufficient to establish the gain difference. Also, if an augmenting posting happens to have a price annotation on it, it is also unused. The price is an annotation for your records. It remains attached to the Posting objects and if you want to make use of it somehow, you can always do that by writing some Python code. There are already two plugins which make use of this annotation: beancount.plugins.implicit_prices : This plugin takes the prices attached to the postings and automatically creates and inserts Price directives for each of them, in order to feed the global price database. beancount.plugins.sellgains : This plugin implements an additional balancing check: it uses the prices to compute the expected proceeds and weighs them against all the other postings of the transaction excluding any postings to Income accounts. In our example, it would check that (-12 x 24.70 + 296.40) = 0. This provides yet another means of verifying the correctness of your input. See the Trading with Beancount document for more details on this topic. Trades \uf0c1 The combination of acquiring some asset and selling it back is what we call a \u201ctrade.\u201d In Beancount we consider only assets with a cost basis to be the subject of trades. Since booking reductions against accumulated inventory contents happens during the booking process, this is where trades should be identified and recorded. As of now [Dec 2016], trade recording has not been implemented. Some prototypes for it have been tested previously and I believe it will be very easy to add in the near future. This will be documented here. Watch this space. The way trades will be implemented is by allowing the booking process to insert matching metadata with unique UUIDs on both the augmenting and reducing postings, in the stream of transactions. Functions and reports will be provided that are able to easily extract the pairs of postings for each reducing postings and filter those out in different ways. Ultimately, one should be able to extract a list of all trades to a table, with the acquisition and sale price, as well as other fees. Debugging Booking Issues \uf0c1 If you\u2019re experiencing difficulties in recording your sales due to the matching process, there are tools you can use to view an account\u2019s detailed inventory contents before and after applying a Transaction to it. To do this, you can use the bean-doctor command. You invoke the program providing it with the file and line number close to the Transaction you want to select, like this: bean-doctor context The resulting output will show the list of inventory contents of all affected accounts prior to the transaction being applied, including cost basis, acquisition date, and optional label fully rendered. Note that some failures are typically handled by throwing away an invalid Transaction\u2019s effects (but never quietly). From Emacs or VI, placing the cursor near a transaction and invoking the corresponding command is the easiest way to invoke the command, as it inserts the line number automatically. Appendix \uf0c1 The rest of this document delves into more technical details. You should feel free to ignore this entirely, it\u2019s not necessary reading to understand how Beancount works. Only bother if you\u2019re interested in the details. Data Representation \uf0c1 It is useful to know how positions are represented in an inventory object. A Position is essentially some number of units of a commodity with some optional information about its acquisition: Cost. Its per-unit acquisition cost (the \u201ccost basis\u201d). Date. The date at which the units were acquired. Label. Some user-specified label which can be used to refer to the lot). We often refer to these position objects as \u201clots\u201d or \u201clegs.\u201d Schematically, a position object looks like this: The structure of a Position object. There are two different types of positions, discussed in detail in the sections that follow: Simple positions. These are positions with no cost basis. The \u201ccost\u201d attribute is set to a null value. (\u201c None \u201d in Python.) Positions held at cost. These are positions with an associated cost basis and acquisition details. An Inventory is simply an accumulation of such positions, represented as a list. We sometimes talk of the ante-inventory to refer to the contents of the inventory before a transaction\u2019s postings have been applied to it, and the ex-inventory to the resulting inventory after they have been applied. A Posting is an object which is a superset of a position: in addition to units and cost, it has an associated account and an optional price attributes. If present, the price has the same type as units. It represents one of the legs of a transaction in the input. Postings imply positions, and these positions are added to inventories. We can say that a position is posted to an account. For more details on the internal data structures used in Beancount, please refer to the Design Doc which expands on this topic further. Why Booking is Not Simple \uf0c1 The complexity of the reduction process shows itself when we consider how to keep track of the cost basis of various lots. To demonstrate how this works, let us consider a simple example that we shall reuse in the different sections below: 25 shares are bought on 4/1 at $23/share. 35 shares are bought on 5/1 at $27/share. 30 shares are sold on 5/15; at that time, the price is $26/share. We\u2019ll ignore commissions for now, they don\u2019t introduce any additional complexity. In Beancount, this scenario would be recorded like this: 2015-04-01 * \"Buy some shares of Hooli in April\" Assets:Invest:HOOL 25 HOOL {23.00 USD} Assets:Invest:Cash -575.00 USD 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest:HOOL 35 HOOL {27.00 USD} Assets:Invest:Cash -945.00 USD 2015-05-15 * \"Sell 30 shares\" Assets:Invest:HOOL -30 HOOL {...} @ 26.00 USD Assets:Invest:Cash 780.00 USD Income:Invest:HOOL:Gains Now, the entire question revolves around which of the shares are selected to be sold. I\u2019ve rendered this input as a red ellipsis (\u201c\u2026\u201d). Whatever the user puts in that spot will be used to determine which lot we want to use. Whichever lot(s) we elect to be the ones sold will determine the amount of gains, because that is a function of the cost basis of those shares. This is why this matters. Augmentations vs. Reductions \uf0c1 The most important observation is that there are two distinct kinds of lot specifications which look very similar in the input but which are processed very differently. When we buy, as in the first transaction above, the {...} cost basis syntax provides Beancount with information about a new lot: 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest:HOOL 35 HOOL {27.00 USD} Assets:Invest:Cash -945.00 USD We call this an \u201caugmentation\u201d to the inventory, because it will simply add a new position to it. The cost basis that you provide is attached to this position and preserved through time, in the inventory. In addition, there are a few other pieces of data you can provide for an augmenting lot. Let\u2019s have a look at all the data that can be provided in the cost spec: Cost basis. This consists in per-unit and total cost numbers\u2014which are combined into a single per-unit number\u2014and a currency. Acquisition date. A lot has an acquisition date. By default, the date attached of its parent transaction will be set as its acquisition date automatically. You may override this date by providing one. This comes in handy to handle stock splits or wash sales and preserve the original acquisition date of the replacement shares, as we\u2019ll see later. Label. You can provide a unique label for it, so that you can more easily refer to it later on, when you sell some or all of it. Merge. An indicator (a flag) that the lot should be merged (this will be useful for average cost booking which will be implemented later). For an augmenting postings , these informations must be either provided or inferred automatically. They can be provided in any order: 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest:HOOL 35 HOOL {2015-04-25, 27.00 USD, \"hooli-123\"} Assets:Invest:Cash -945.00 USD If you omit the date, the date of the Transaction is attached to it. If you omit the cost, the rest of the postings must be filled in such that the cost amount can be inferred from them. Since the label is optional anyway, an unspecified label field is left as a null value. You might wonder why it is allowed to override the date of an augmentation; it is useful when making cost basis adjustments to preserve the original acquisition date of a posting: You remove the posting, and then replace it with its original date and a new cost basis. Now, when we sell those shares, we will refer to the posting as a \u201creducing\u201d posting , a \u201c reduction \u201d. Note that the terms \u201caugmenting\u201d and \u201creducing\u201d are just terminology I\u2019m came up with in the context of designing how Beancount processes inventories; they\u2019re not general accounting terms. It\u2019s a \u201creduction\u201d because we\u2019re removing shares from the inventory that has been accumulated up to the date of its transaction. For example, if we were to sell 30 shares from that lot of 35 shares, the input might look like this: 2015-05-15 * \"Sell 30 shares\" Assets:Invest:HOOL -30 HOOL {27.00 USD} @ 26.00 USD Assets:Invest:Cash 780.00 USD Income:Invest:HOOL:Gains While the input looks the same as on the augmenting posting, Beancount handles this quite differently: it looks at the state of the account\u2019s inventory before applying the transaction and finds all the positions that match the lot data you provided. It then uses the details of the matched lots as the cost basis information for the reducing posting. In this example, it would simply match all the positions which have a cost basis of $27.00. This example\u2019s inventory before the sale contains a single lot of 35 shares at $27.00, so there is a single position matching it and that lot is reduced by 30 shares and 5 shares remain. We\u2019ll see later what happens in the case of multiple lots matching the specification. Note that you could have provide other subsets of lot information to match against, like just providing the label, for example: 2015-05-15 * \"Sell 30 shares\" Assets:Invest:HOOL -30 HOOL {\"hooli-123\"} @ 26.00 USD Assets:Invest:Cash 780.00 USD Income:Invest:HOOL:Gains This is also a valid way to identify the particular lot you wish to reduce. If you had provided a date here, it would also only be used to match against the inventory contents, to disambiguate between lots acquired at different dates, not to attach the date anywhere. And furthermore, if there was a single lot in the inventory you could have also just provided just an empty cost basis spec like this: \u201c {} \u201d. The Booking Methods section below will delve into the detail of what happens when the matches are ambiguous. In summary: When you\u2019re adding something to an account\u2019s inventory (augmenting), the information you provide is used to create a new lot and is attached to it. When you\u2019re removing from an account\u2019s inventory (reducing), the information you provide is used to filter the inventory contents to select which of the lot(s) to reduce, and information from the selected lots is filled in. Homogeneous and Mixed Inventories \uf0c1 So far in the example and in the vast majority of the examples in the documentation, \u201caugmenting\u201d means adding a positive number of shares. But in Beancount many of the accounts normally have a negative balance, e.g., liabilities accounts. It\u2019s fair to ask if it makes sense to hold a negative balance of commodities held at cost. The answer is yes. These would correspond to \u201cshort\u201d positions. Most people are unlikely to be selling short, but Beancount inventories support it. How we define \u201caugmenting\u201d is in relation to the existing balance of lots of a particular commodity. For example, if an account\u2019s inventory contains the following positions: 25 HOOL {23.00 USD, 2016-04-01} 35 HOOL {27.00 USD, 2016-05-01} Then \u201cadding\u201d means a positive number of shares. On the other hand, if the account contains only short positions, like this: -20 HOOL {23.00 USD, 2016-04-15} -10 HOOL {27.00 USD, 2016-05-15} Then \u201cadding\u201d means a negative number of shares, and \u201creducing\u201d would be carried out by matching a positive number of shares against it. The two inventories portrayed above are homogeneous in units of HOOL, that is, all of the positions have the same sign. With of the most booking methods we will see further, Beancount makes it impossible to create a non-homogeneous, or \u201cmixed,\u201d inventory. But the \u201cNONE\u201d method allows it. A mixed inventory might have the following contents, for example: 25 HOOL {23.00 USD, 2016-04-01} -20 HOOL {23.00 USD, 2016-04-15} As you may intuit, the notion of \u201caugmenting\u201d or \u201creducing\u201d only makes sense for homogeneous inventories. Original Proposal \uf0c1 If you\u2019re interested in the design doc that led to this implementation, you can find the document here . I hope the resulting implementation is simple enough yet general.","title":"How Inventories Work"},{"location":"how_inventories_work.html#how-inventories-work","text":"Martin Blais , December 2016 http://furius.ca/beancount/doc/booking This document explains how we accumulate commodities and match sales (reductions) against accumulated inventory contents.","title":"How Inventories Work"},{"location":"how_inventories_work.html#introduction","text":"Beyond the ability to track and list the postings made to each of the accounts (an operation that produces a journal of entries), one of the most common and useful operations of Beancount is to sum up the positions of arbitrary sets of postings. These aggregations are at the heart of how Beancount works, and are implemented in an object called \u201cinventory.\u201d This document explains how this aggregation process works. If you\u2019re going to track investments, it\u2019s necessary to understand what follows.","title":"Introduction"},{"location":"how_inventories_work.html#matches-booking-methods","text":"In order to get the big picture, let\u2019s walk through the various booking features by way of a simple examples. This should expose you to all the main ideas in one go.","title":"Matches & Booking Methods"},{"location":"how_inventories_work.html#simple-postings-no-cost","text":"Consider a set of simple postings to an account, e.g., 2016-04-24 * \"Deposit check\" Assets:Bank:Checking 221.23 USD \u2026 2016-04-29 * \"ATM Withdrawal\" Assets:Bank:Checking -100.00 USD \u2026 2016-04-29 * \"Debit card payment\" Assets:Bank:Checking -45.67 USD \u2026 The inventory of the Checking account begins empty. After the first transaction, its contents are 221.23 USD. After the second, 121.23 USD. Finally, the third transaction brings this balance to 75.56 USD. This seems very natural; the numbers simply add to. It might be obvious, but note also that the numbers are allowed to change the sign (go negative).","title":"Simple Postings \u2014 No Cost"},{"location":"how_inventories_work.html#multiple-commodities","text":"An inventory may contain more than one type of commodity. It is equivalent to a mapping from commodity to some number of units. For example, 2016-07-24 * \"Dinner before leaving NYC\" Expenses:Restaurants 34.58 USD \u2026 2016-07-26 * \"Food with friends after landing\" Expenses:Restaurants 62.11 CAD \u2026 After those two transactions, the Restaurants account contains 34.58 USD and 62.11 CAD. Its contents are said to be of mixed commodities. And naturally, postings are applied to just the currencies they affect. For instance, the following transaction 2016-07-27 * \"Brunch\" Expenses:Restaurants 23.91 CAD \u2026 brings the balance of that account to 34.58 USD and 86.02 CAD. The number of units USD hasn\u2019t changed. Note that accounts may contain any number of commodities, and this is also true for commodities held at cost, which we\u2019ll see shortly. While this is made possible, I recommend that you define enough accounts to keep a single commodity in each; this can be enforced with the \u201c onecommodity \u201d plugin.","title":"Multiple Commodities"},{"location":"how_inventories_work.html#cost-basis","text":"Things get a little more hairy when we consider the tracking of investments with a cost basis. Beancount allows you to associate a cost basis and an optional label with a particular lot acquired. Consider these two purchases to an investment account: 2015-04-01 * \"Buy some shares of Hooli in April\" Assets:Invest 25 HOOL {23.00 USD, \"first-lot\"} \u2026 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest 35 HOOL {27.00 USD} \u2026 So now, the investment account\u2019s inventory contains units ccy cost cost-ccy lot-date label 25 HOOL {23.00 USD, 2015-04-01, \"first-lot\"} 35 HOOL {27.00 USD, 2015-05-01, None} Those two lots were not merged, they are still two distinct positions in the inventory. Inventories merge lots together and adjust the units only if the commodity and all of its cost attributes exactly match. (In practice, it\u2019s pretty rare that two augmentations will have the same cost and date attributes.) Note how Beancount automatically associated the acquisition date to each lot; you can override it if desired, by adding the date similar to the optional label. this is useful for making cost basis adjustments). Postings that add to the content of an inventory are called augmentations .","title":"Cost Basis"},{"location":"how_inventories_work.html#reductions","text":"But how do we remove commodities from an inventory? You could eat away at an existing lot by selling some of it. You do this by posting a reduction to the account, like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {23.00 USD} \u2026 Just to be clear, what makes this posting a reduction is the mere fact that the sign (-) is opposite that of the balance of the account (+25) for that commodity. This posting tells Beancount to find all lots with a cost basis of 23.00 USD and remove 12 units from it. The resulting inventory will be units ccy cost cost-ccy lot-date label 13 HOOL {23.00 USD, 2015-04-01, \"first-lot\"} 35 HOOL {27.00 USD, 2015-05-01, None} Note how the first posting was reduced by 12 units. We didn\u2019t have to specify all of the lot\u2019s attributes, just the cost. We could have equivalently used the date to specify which lot to reduce: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {2015-04-01} \u2026 Or the label: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {\"first-lot\"} \u2026 Or a combination of these. Any combination of attributes will be matched against the inventory contents to find which lot to reduce. In fact, if the inventory happened to have just a single lot in it, you could reduce it like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {} \u2026","title":"Reductions"},{"location":"how_inventories_work.html#ambiguous-matches","text":"But what happens if multiple lots match the reduction? For example, with the previous inventory containing two lots, if you wrote your sale like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {} \u2026 Beancount wouldn\u2019t be able to figure out which lot needs to get reduced. We have an ambiguous match. Partially ambiguous matches are also possible. For example, if you have the following inventory to reduce from: units ccy cost cost-ccy lot-date label 25 HOOL {23.00 USD, 2015-04-01, None} 30 HOOL {25.00 USD, 2015-04-01, None} 35 HOOL {27.00 USD, 2015-05-01, None} And you attempted to reduce like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -12 HOOL {2015-04-01} \u2026 The first two lots are selected as matches.","title":"Ambiguous Matches"},{"location":"how_inventories_work.html#strict-booking","text":"What does Beancount do with ambiguous matches? By default, it issues an error. More precisely, what happens is that Beancount invokes the booking method and it handles the ambiguous match depending on what it is set. The default booking method is \u201c STRICT \u201d and it just gives up and spits out an error, telling you you need to refine your input in order to disambiguate your inventory reduction.","title":"Strict Booking"},{"location":"how_inventories_work.html#fifo-and-lifo-booking","text":"Other booking methods are available. They can be configured using options, like this: option \"booking_method\" \"FIFO\" The \u201c FIFO \u201d method automatically selects the oldest of the matching lots up to the requested size of the reduction. For example, given our previous inventory: units ccy cost cost-ccy lot-date label 25 HOOL {23.00 USD, 2015-04-01, \"first-lot\"} 35 HOOL {27.00 USD, 2015-05-01, None} Attempting to reduce 28 shares like this: 2015-05-15 * \"Sell some shares\" Assets:Invest -28 HOOL {} \u2026 would match both lots, completely reduce the first one to zero units and remove the remaining 3 units from the second lot, to result in the following inventory: units ccy cost cost-ccy lot-date label 32 HOOL {27.00 USD, 2015-05-01, None} The \u201cLIFO\u201d method works similarly, but consumes the youngest (latest) lots first, working its way backward in time to remove the volume.","title":"FIFO and LIFO Booking"},{"location":"how_inventories_work.html#per-account-booking-method","text":"You don\u2019t have to make all accounts follow the same booking method; the option in the previous section sets the default method for all accounts. In order to override the booking method for a particular account, you can use an optional string option on the account\u2019s Open directive, like this: 2014-01-01 open Assets:Invest \"FIFO\" This allows you to treat different accounts with a different booking resolution.","title":"Per-account Booking Method"},{"location":"how_inventories_work.html#total-matches","text":"There is an exception to strict booking: if the entire inventory is being reduced by exactly the total number of units, it\u2019s clear that all the matching lots are to be selected and this is considered unambiguous, even under \u201cSTRICT\u201d booking. For example, under \u201cSTRICT\u201d booking, this reduction would empty up the previous inventory without raising an error, because there are 25 + 35 shares matching: 2015-05-15 * \"Sell all my shares\" Assets:Invest -60 HOOL {} \u2026","title":"Total Matches"},{"location":"how_inventories_work.html#average-booking","text":"Retirement accounts created by government incentive programs (such as the 401k plan in the US or the RRSP in Canada) typically consist in pre-tax money. For these types of accounts, brokers usually disregard the calculation of cost basis because the taxation is to be made upon distributing money outside the account. These accounts are often managed to the extent that they are fully invested; therefore, fees are often taken as shares of the investment portfolio, priced on the day the fee is paid out. This makes it awkward to track the cost basis of individual lots. The correct way to deal with this is to treat the number of units and the cost basis of each commodity\u2019s entire set of lots separately. For example, the following two transactions: 2016-07-28 * \"Buy some shares of retirement fund\" Assets:Invest 45.0045 VBMPX {11.11 USD} \u2026 2016-10-12 * \"Buy some shares of retirement fund\" Assets:Invest 54.5951 VBMPX {10.99 USD} \u2026 Should result in a single lot with the total number of units and the averaged cost: units ccy cost cost-ccy lot-date label 99.5996 VBMPX {11.0442 USD, 2016-07-28, None} A fee taken in this account might look like this: 2016-12-30 * \"Quarterly management fee\" Assets:Invest -1.4154 VBMPX {10.59 USD} Expenses:Fees Even with negative units the number and cost get aggregated separately: units ccy cost cost-ccy lot-date label 98.1842 VBMPX {11.0508 USD, 2016-07-28, None} This feature isn\u2019t yet supported in Beancount; it\u2019s fairly tricky to implement, and will be the subject in a minor release in the future.","title":"Average Booking"},{"location":"how_inventories_work.html#no-booking","text":"However, there is another way to deal with non-taxable accounts in the meantime: you can simply disable the booking. There is a booking method called \u201c NONE \u201d which implements a very liberal strategy which accepts any new lot.. New lots are always appended unconditionally to the inventory. Using this strategy on the transactions from the previous section would result in this inventory: units ccy cost cost-ccy lot-date label 45.0045 VBMPX {11.11 USD, 2016-07-28, None} 54.5951 VBMPX {10.99 USD, 2016-10-12, None} -1.4154 VBMPX {10.59 USD, 2016-12-30, None} Observe how the resulting inventory has a mix of signs; normally this is not allowed, but it is tolerated under this degenerate booking method. Note that under this method, the only meaningful values are the total number of units and the total or average cost amounts. The individual lots aren\u2019t really lots, they only represent the list of all postings made to that account. Note: If you are familiar with Ledger, this is the default and only booking method that it supports.","title":"No Booking"},{"location":"how_inventories_work.html#summary","text":"In summary, here\u2019s what we presented in the walkthrough. Augmentations are never problematic; they always add a new position to an existing inventory. On the other hand, reductions may result in a few outcomes: Single match. Only one position matches the reduction; it is reduced. Total match. The total number of units requested matches the total number of units of the positions matched. These positions are reduced away. No match. None of the positions matches the reducing posting. An error is raised. Ambiguous matches. More than one position in the inventory matches the reducing posting; the booking method is invoked to handle this . There are a few booking methods available to handle the last case: STRICT. An error is raised. FIFO. Units from oldest (earliest) lots are selected until the reduction is complete. LIFO. Units from youngest (latest) lots are selected until the reduction is complete. AVERAGE. After every reduction, all the units of the affected commodity are merged and their new average cost is recalculated. NONE. Booking is disabled; the reducing lots is simply added to the inventory. This results in an inventory with mixed signs and only the total number of units and total cost basis are sensible numbers. Beancount has a default booking method for all accounts, which can be overridden with an option: option \"booking_method\" \"FIFO\" The default value for the booking method is \u201cSTRICT\u201d. I recommend that you leave it that way and override the booking method for specific accounts. The method can be specified for each account by adding a string to its Open directive: 2016-05-01 open Assets:Vanguard:RGAGX \"AVERAGE\"","title":"Summary"},{"location":"how_inventories_work.html#how-prices-are-used","text":"The short answer is that prices aren\u2019t used nor affect the booking algorithm at all. However, it is relevant to discuss what they do in this context because users invariably get confused about their interpretation. There are two use cases for prices: making conversions between commodities and tagging a reducing lot with its sale price in order to record it and optionally balance the proceeds.","title":"How Prices are Used"},{"location":"how_inventories_work.html#commodity-conversions","text":"Conversions are used to exchange one currency for another. They look like this: 2016-04-24 * \"Deposit check\" Assets:Bank:Checking 220.00 USD @ 1.3 CAD Income:Payment -286.00 CAD For the purpose of booking it against the Checking account\u2019s inventory, the posting with the price attached to it is treated just the same as if there was no price: the Checking account simply receives a deposit of 220.00 units of USD and will match against positions of commodity \u201cUSD\u201d. The price is used only to verify that the transaction balances and ensure the double-entry accounting rule is respected (220.00 x 1.3 CAD + -286.00 CAD = 0.00). It is otherwise ignored for the purpose of modifying the inventory contents. In a sense, after the postings have been applied to the account inventories, the price is forgotten and the inventory balance retains no memory of the deposit having occurred from a conversion.","title":"Commodity Conversions"},{"location":"how_inventories_work.html#price-vs-cost-basis","text":"One might wonder how the price is used if there is a cost basis specification, like this: 2015-05-15 * \"Sell some shares\" Assets:Invest:HOOL -12 HOOL {23.00 USD} @ 24.70 USD Assets:Invest:Cash 296.40 USD Income:Invest:Gains The answer is often surprising to many users: the price is not used by the balancing algorithm if there is a cost basis; the cost basis is the number used to balance the postings. This is a useful property that allows us to compute capital gains automatically. In the previous example, the balance algorithm would sum up -12 x 23.00 + 296.40 = -20.40 USD, which is the capital gain, (24.70 - 23.00) * 12. It would complete the last posting and assign it this value. In general, the way that profits on sales are calculated is by weighing the proceedings, i.e., the cash deposits, against the cost basis of the sold lots, and this is sufficient to establish the gain difference. Also, if an augmenting posting happens to have a price annotation on it, it is also unused. The price is an annotation for your records. It remains attached to the Posting objects and if you want to make use of it somehow, you can always do that by writing some Python code. There are already two plugins which make use of this annotation: beancount.plugins.implicit_prices : This plugin takes the prices attached to the postings and automatically creates and inserts Price directives for each of them, in order to feed the global price database. beancount.plugins.sellgains : This plugin implements an additional balancing check: it uses the prices to compute the expected proceeds and weighs them against all the other postings of the transaction excluding any postings to Income accounts. In our example, it would check that (-12 x 24.70 + 296.40) = 0. This provides yet another means of verifying the correctness of your input. See the Trading with Beancount document for more details on this topic.","title":"Price vs. Cost Basis"},{"location":"how_inventories_work.html#trades","text":"The combination of acquiring some asset and selling it back is what we call a \u201ctrade.\u201d In Beancount we consider only assets with a cost basis to be the subject of trades. Since booking reductions against accumulated inventory contents happens during the booking process, this is where trades should be identified and recorded. As of now [Dec 2016], trade recording has not been implemented. Some prototypes for it have been tested previously and I believe it will be very easy to add in the near future. This will be documented here. Watch this space. The way trades will be implemented is by allowing the booking process to insert matching metadata with unique UUIDs on both the augmenting and reducing postings, in the stream of transactions. Functions and reports will be provided that are able to easily extract the pairs of postings for each reducing postings and filter those out in different ways. Ultimately, one should be able to extract a list of all trades to a table, with the acquisition and sale price, as well as other fees.","title":"Trades"},{"location":"how_inventories_work.html#debugging-booking-issues","text":"If you\u2019re experiencing difficulties in recording your sales due to the matching process, there are tools you can use to view an account\u2019s detailed inventory contents before and after applying a Transaction to it. To do this, you can use the bean-doctor command. You invoke the program providing it with the file and line number close to the Transaction you want to select, like this: bean-doctor context The resulting output will show the list of inventory contents of all affected accounts prior to the transaction being applied, including cost basis, acquisition date, and optional label fully rendered. Note that some failures are typically handled by throwing away an invalid Transaction\u2019s effects (but never quietly). From Emacs or VI, placing the cursor near a transaction and invoking the corresponding command is the easiest way to invoke the command, as it inserts the line number automatically.","title":"Debugging Booking Issues"},{"location":"how_inventories_work.html#appendix","text":"The rest of this document delves into more technical details. You should feel free to ignore this entirely, it\u2019s not necessary reading to understand how Beancount works. Only bother if you\u2019re interested in the details.","title":"Appendix"},{"location":"how_inventories_work.html#data-representation","text":"It is useful to know how positions are represented in an inventory object. A Position is essentially some number of units of a commodity with some optional information about its acquisition: Cost. Its per-unit acquisition cost (the \u201ccost basis\u201d). Date. The date at which the units were acquired. Label. Some user-specified label which can be used to refer to the lot). We often refer to these position objects as \u201clots\u201d or \u201clegs.\u201d Schematically, a position object looks like this: The structure of a Position object. There are two different types of positions, discussed in detail in the sections that follow: Simple positions. These are positions with no cost basis. The \u201ccost\u201d attribute is set to a null value. (\u201c None \u201d in Python.) Positions held at cost. These are positions with an associated cost basis and acquisition details. An Inventory is simply an accumulation of such positions, represented as a list. We sometimes talk of the ante-inventory to refer to the contents of the inventory before a transaction\u2019s postings have been applied to it, and the ex-inventory to the resulting inventory after they have been applied. A Posting is an object which is a superset of a position: in addition to units and cost, it has an associated account and an optional price attributes. If present, the price has the same type as units. It represents one of the legs of a transaction in the input. Postings imply positions, and these positions are added to inventories. We can say that a position is posted to an account. For more details on the internal data structures used in Beancount, please refer to the Design Doc which expands on this topic further.","title":"Data Representation"},{"location":"how_inventories_work.html#why-booking-is-not-simple","text":"The complexity of the reduction process shows itself when we consider how to keep track of the cost basis of various lots. To demonstrate how this works, let us consider a simple example that we shall reuse in the different sections below: 25 shares are bought on 4/1 at $23/share. 35 shares are bought on 5/1 at $27/share. 30 shares are sold on 5/15; at that time, the price is $26/share. We\u2019ll ignore commissions for now, they don\u2019t introduce any additional complexity. In Beancount, this scenario would be recorded like this: 2015-04-01 * \"Buy some shares of Hooli in April\" Assets:Invest:HOOL 25 HOOL {23.00 USD} Assets:Invest:Cash -575.00 USD 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest:HOOL 35 HOOL {27.00 USD} Assets:Invest:Cash -945.00 USD 2015-05-15 * \"Sell 30 shares\" Assets:Invest:HOOL -30 HOOL {...} @ 26.00 USD Assets:Invest:Cash 780.00 USD Income:Invest:HOOL:Gains Now, the entire question revolves around which of the shares are selected to be sold. I\u2019ve rendered this input as a red ellipsis (\u201c\u2026\u201d). Whatever the user puts in that spot will be used to determine which lot we want to use. Whichever lot(s) we elect to be the ones sold will determine the amount of gains, because that is a function of the cost basis of those shares. This is why this matters.","title":"Why Booking is Not Simple"},{"location":"how_inventories_work.html#augmentations-vs-reductions","text":"The most important observation is that there are two distinct kinds of lot specifications which look very similar in the input but which are processed very differently. When we buy, as in the first transaction above, the {...} cost basis syntax provides Beancount with information about a new lot: 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest:HOOL 35 HOOL {27.00 USD} Assets:Invest:Cash -945.00 USD We call this an \u201caugmentation\u201d to the inventory, because it will simply add a new position to it. The cost basis that you provide is attached to this position and preserved through time, in the inventory. In addition, there are a few other pieces of data you can provide for an augmenting lot. Let\u2019s have a look at all the data that can be provided in the cost spec: Cost basis. This consists in per-unit and total cost numbers\u2014which are combined into a single per-unit number\u2014and a currency. Acquisition date. A lot has an acquisition date. By default, the date attached of its parent transaction will be set as its acquisition date automatically. You may override this date by providing one. This comes in handy to handle stock splits or wash sales and preserve the original acquisition date of the replacement shares, as we\u2019ll see later. Label. You can provide a unique label for it, so that you can more easily refer to it later on, when you sell some or all of it. Merge. An indicator (a flag) that the lot should be merged (this will be useful for average cost booking which will be implemented later). For an augmenting postings , these informations must be either provided or inferred automatically. They can be provided in any order: 2015-05-01 * \"Buy some more shares of Hooli in May\" Assets:Invest:HOOL 35 HOOL {2015-04-25, 27.00 USD, \"hooli-123\"} Assets:Invest:Cash -945.00 USD If you omit the date, the date of the Transaction is attached to it. If you omit the cost, the rest of the postings must be filled in such that the cost amount can be inferred from them. Since the label is optional anyway, an unspecified label field is left as a null value. You might wonder why it is allowed to override the date of an augmentation; it is useful when making cost basis adjustments to preserve the original acquisition date of a posting: You remove the posting, and then replace it with its original date and a new cost basis. Now, when we sell those shares, we will refer to the posting as a \u201creducing\u201d posting , a \u201c reduction \u201d. Note that the terms \u201caugmenting\u201d and \u201creducing\u201d are just terminology I\u2019m came up with in the context of designing how Beancount processes inventories; they\u2019re not general accounting terms. It\u2019s a \u201creduction\u201d because we\u2019re removing shares from the inventory that has been accumulated up to the date of its transaction. For example, if we were to sell 30 shares from that lot of 35 shares, the input might look like this: 2015-05-15 * \"Sell 30 shares\" Assets:Invest:HOOL -30 HOOL {27.00 USD} @ 26.00 USD Assets:Invest:Cash 780.00 USD Income:Invest:HOOL:Gains While the input looks the same as on the augmenting posting, Beancount handles this quite differently: it looks at the state of the account\u2019s inventory before applying the transaction and finds all the positions that match the lot data you provided. It then uses the details of the matched lots as the cost basis information for the reducing posting. In this example, it would simply match all the positions which have a cost basis of $27.00. This example\u2019s inventory before the sale contains a single lot of 35 shares at $27.00, so there is a single position matching it and that lot is reduced by 30 shares and 5 shares remain. We\u2019ll see later what happens in the case of multiple lots matching the specification. Note that you could have provide other subsets of lot information to match against, like just providing the label, for example: 2015-05-15 * \"Sell 30 shares\" Assets:Invest:HOOL -30 HOOL {\"hooli-123\"} @ 26.00 USD Assets:Invest:Cash 780.00 USD Income:Invest:HOOL:Gains This is also a valid way to identify the particular lot you wish to reduce. If you had provided a date here, it would also only be used to match against the inventory contents, to disambiguate between lots acquired at different dates, not to attach the date anywhere. And furthermore, if there was a single lot in the inventory you could have also just provided just an empty cost basis spec like this: \u201c {} \u201d. The Booking Methods section below will delve into the detail of what happens when the matches are ambiguous. In summary: When you\u2019re adding something to an account\u2019s inventory (augmenting), the information you provide is used to create a new lot and is attached to it. When you\u2019re removing from an account\u2019s inventory (reducing), the information you provide is used to filter the inventory contents to select which of the lot(s) to reduce, and information from the selected lots is filled in.","title":"Augmentations vs. Reductions"},{"location":"how_inventories_work.html#homogeneous-and-mixed-inventories","text":"So far in the example and in the vast majority of the examples in the documentation, \u201caugmenting\u201d means adding a positive number of shares. But in Beancount many of the accounts normally have a negative balance, e.g., liabilities accounts. It\u2019s fair to ask if it makes sense to hold a negative balance of commodities held at cost. The answer is yes. These would correspond to \u201cshort\u201d positions. Most people are unlikely to be selling short, but Beancount inventories support it. How we define \u201caugmenting\u201d is in relation to the existing balance of lots of a particular commodity. For example, if an account\u2019s inventory contains the following positions: 25 HOOL {23.00 USD, 2016-04-01} 35 HOOL {27.00 USD, 2016-05-01} Then \u201cadding\u201d means a positive number of shares. On the other hand, if the account contains only short positions, like this: -20 HOOL {23.00 USD, 2016-04-15} -10 HOOL {27.00 USD, 2016-05-15} Then \u201cadding\u201d means a negative number of shares, and \u201creducing\u201d would be carried out by matching a positive number of shares against it. The two inventories portrayed above are homogeneous in units of HOOL, that is, all of the positions have the same sign. With of the most booking methods we will see further, Beancount makes it impossible to create a non-homogeneous, or \u201cmixed,\u201d inventory. But the \u201cNONE\u201d method allows it. A mixed inventory might have the following contents, for example: 25 HOOL {23.00 USD, 2016-04-01} -20 HOOL {23.00 USD, 2016-04-15} As you may intuit, the notion of \u201caugmenting\u201d or \u201creducing\u201d only makes sense for homogeneous inventories.","title":"Homogeneous and Mixed Inventories"},{"location":"how_inventories_work.html#original-proposal","text":"If you\u2019re interested in the design doc that led to this implementation, you can find the document here . I hope the resulting implementation is simple enough yet general.","title":"Original Proposal"},{"location":"how_we_share_expenses.html","text":"How We Share Expenses \uf0c1 This document explains how I share expenses with my wife. This is a bit involved and I\u2019ve developed a good working system, but it\u2019s not so simple for most people to do that, so I figured I would take the time to describe it to help others with designing similar processes for themselves. Context \uf0c1 We\u2019re both working professionals and have decided to share all expenses roughly 70%/30%. This is what we shoot for, this is not a hard rule, but we track it as if it were rigid, and accept it as a good approximation. We have two types of shared expenses: Shared. Common expenses between us, e.g., rent, a dinner out with friends, etc. Kyle. Expenses for our child, e.g., daycare, diapers, nanny, baby food. These get handled very differently, because we book our child's expenses as if it were a separate project on its own. (If you prefer pictures, there\u2019s a diagram at the end of this document that provides an overview of the system.) Shared Expenses \uf0c1 For our shared expenses, I maintain an account for her on my personal ledger file. This is simple and I\u2019m not too interested in maintaining a separate ledger for the totality of our common shared expenses. It\u2019s sufficient for me to just keep track of her balance on that one account. You can imagine that account like a credit card (I\u2019m the credit provider) that she pays off with transfers and also by making her own shared expenses. I just declare an Assets:US:Share:Carolyn account on my personal ledger ( blais.beancount ). My Shared Expenses \uf0c1 Whenever I incur an expense that is for both of us, I book it normally but I will tag it with #carolyn: 2018-12-23 * \"WHISK\" \"Water refill\" #carolyn Liabilities:US:Amex:BlueCash -32.66 USD Expenses:Food:Grocery This gets automatically converted to: 2018-12-23 * \"WHISK\" \"Water refill\" #carolyn Liabilities:US:Amex:BlueCash -32.66 USD Expenses:Food:Grocery 19.60 USD Assets:US:Share:Carolyn 13.06 USD share: TRUE This is done by a custom plugin I built that splits the expenses according to some rules that we have between us (see also this plugin by Akkukis ). In this example, 40% of 32.66 (13.06) gets rerouted to her account. Note that this is an asset account for me, because she owes this. Her Shared Expenses \uf0c1 We also have to keep track of the money she spends on her own for shared expenses. Since she\u2019s not a Beancount user, I\u2019ve set up a Google Sheets doc in which she can add rows to a particular sheet. This sheet has fields: Date, Description, Account, Amount. I try to keep it simple. Then, I built an extract_sheets.py script that can pull down this data automatically and it writes it to a dedicated file for this, overwriting the entire contents each time. The contents of this ledger ( carolyn.beancount ) look like this: pushtag #carolyn ... 2017-05-21 * \"Amazon\" \"Cotton Mattress Protector in White\" Expenses:Home:Furniture 199.99 USD Assets:US:Share:Carolyn -199.99 USD ... poptag #carolyn All of those transactions are tagged as #carolyn from the pushtag/poptag directive. They get translated by the plugin to reduce the amount by the portion I\u2019m supposed to pay: 2017-05-21 * \"Amazon\" \"Cotton Mattress Protector in White\" #carolyn Expenses:Home:Furniture 119.99 USD Assets:US:Share:Carolyn -119.99 USD share: TRUE These postings typically reduce her asset account by that much, and thus remove the portion she paid for me on these shared expenses. I generate the file with a single command like this: extract_sheets.py --tag='carolyn' --sheet='Shared Expenses' '' 'Assets:US:Share:Carolyn' > carolyn.beancount In my personal ledger, I include this file to merge those expenses with my own directives flow. I also define a query to generate a statement for her account. In blais.beancount : include \"carolyn.beancount\" 2020-01-01 query \"carolyn\" \" select date, description, position, balance from open on 2017-01-01 where account ~ 'Assets:US:Share:Carolyn' \" Reviewing & Statement \uf0c1 In order to produce a statement for her to review (and spot the occasional mistake in data entry), I simply produce a journal of that account to a CSV file and upload that to another sheet in the same Google Sheets doc she inputs her expenses: bean-query -f csv -o carolyn.csv --numberify $L run carolyn upload-to-sheets -v --docid=\"\" carolyn.csv:\"Shared Account (Read-Only)\" This makes it easy for her to eyeball all the amounts posted to her balance with me and point out errors if they occur (some always appear). Moreover, all the information is in one place\u2014it\u2019s important to keep it simple. Finally, we can use the collaborative features of Sheets to communicate, e.g. comments, highlighting text, etc. Note that this system has the benefit of correctly accruing my expenses, by reducing my portion on categories for the stuff I pay and by including her portion on categories for the stuff she pays for. Reconciling our Shared Expenses \uf0c1 Finally, in order to reconcile this account, my wife (or I, but usually she\u2019s behind) just makes a bank transfer to my account, which I book to reduce her running account balance: 2019-01-30 * \"HERBANK EXT TRNSFR; DIRECTDEP\" Assets:US:MyBank:Checking 3000 USD Assets:US:Share:Carolyn Typically she'll do this every month or two. She'll be fiddling on her laptop and ask casually \"Hey, what's my balance I can do a transfer now?\" It\u2019s all fine to relax about the particulars since the system is keeping track of everything precisely, so she can send some approximate amount, it doesn't matter, it'll post to her account. Child Expenses \uf0c1 I designed a very different system to track our child\u2019s expenses. For Kyle, I\u2019m definitely interested in tracking the total cash flows and expenses related to him, regardless of who paid for them. It\u2019s interesting to be able to ask (our ledger) a question like: \u201cHow much did his schooling cost?\u201d, for example, or \u201cHow much did we pay in diapers, in total?\u201d. Furthermore, we tend to pay for different things for Kyle, e.g. I deal with the daycare expenses (I\u2019m the accounting nerd after all, so this shouldn\u2019t be surprising), and his mother tends to buy all the clothing and prepare his food. To have a global picture of all costs related to him, we need to account for these things correctly. One interesting detail is that it would be difficult to do this with the previously described method, because I\u2019d have to have a mirror of all expense accounts I\u2019d use for him. This would make my personal ledger really ugly. For example, I want to book diapers to Expenses:Pharmacy , but I also have my own personal pharmacy expenses. So in theory, to do that I\u2019d like to have separate accounts for him and me, e.g., Expenses:Kyle:Pharmacy and Expenses:Pharmacy . This would have to be done for all the accounts we use for him. I don't do that. My Child Expenses on my Personal Ledger \uf0c1 Instead of doing that, what I want is for my personal ledger to book all the expenses I make for him to a single category: Expenses:Kyle , and to track all the detail in a shared ledger. But I still have to book all the expenses to some category, and these appear on my personal ledger, there\u2019s no option (I won\u2019t maintain a separate credit card to pay for his expenses, that would be overkill, so I have to find a way). I accomplish this by booking the expenses to my own expenses account, as usual, but tagging the transaction with #kyle: 2019-02-01 * \"AMAZON.COM\" \"MERCHANDISE - Diapers size 4 for Kyle\" #kyle Liabilities:US:Amex:BlueCash -49.99 USD Expenses:Pharmacy And I have a different plugin that automatically makes the conversion of those transactions to: 2019-02-01 * \"AMAZON.COM\" \"MERCHANDISE - Diapers size 4 for Kyle\" #kyle Liabilities:US:Amex:BlueCash -49.99 USD Expenses:Kyle 49.99 USD diverted_account: \"Expenses:Pharmacy\" So from my personal side, all those expenses get booked to my \u201cKyle project\u201d account. This is accomplished by the divert_expenses plugin, with this configuration: plugin \"beancount.plugins.divert_expenses\" \"{ 'tag': 'kyle', 'account': 'Expenses:Kyle' }\" The \u201cdiverted_account\u201d metadata is used to keep track of the original account, and this is used later by another script that generates a ledger file decided to my expenses for him (more below). My Child Expenses in Kyle\u2019s own Ledger \uf0c1 Now, because we\u2019re considering Kyle\u2019s expenses a project of his own, I have to maintain a set of ledgers for him. I automatically pull the transactions I described in the previous section from my personal ledger and automatically convert them to a file dedicated to his dad (me). This is done by calling the extract_tagged script: extract_tagged.py blais.beancount '#kyle' 'Income:Dad' --translate \"Expenses:Kyle:Mom=Income:Mom\" > dad.beancount The matching transaction from the previous section would look like this in it: 2019-02-01 * \"AMAZON.COM\" \"MERCHANDISE - Diapers size 4 for Kyle\" #kyle Income:Dad -49.99 USD Expenses:Pharmacy 49.99 USD As you can see, the script was able to reconstruct the original account name in Kyle\u2019s ledger by using the metadata saved by the divert_expenses plugin in the previous section. It also books the source of the payment to a single account showing it came from his father ( Income:Dad ). There\u2019s no need for me to keep track of which payment method I used on Kyle\u2019s side (e.g. by credit card), that\u2019s not important to him. This ledger contains the sum total of all expenses I\u2019ve made for him to date. The file gets entirely overwritten each time I run this (this is a purely generated output, no hand editing is ever done here, if I change anything I change it in my personal ledger file). Her Child Expenses \uf0c1 In order to keep track of her expenses for Kyle, we use the same method (and programs) as we use for our shared accounts in order to pull a set of \u201cCarolyn\u2019s expenses for Kyle\u201d from another sheet in the same Google Sheets doc: extract_sheets.py --sheet='Kyle Expenses (Regular)' '' 'Income:Mom' > mom.beancount This pulls in transactions that look like this: 2018-09-23 * \"SPROUT SAN FRANCISCO\" \"Clothing for Kyle 9-12 months\" Expenses:Clothing 118.30 USD Income:Mom -118.30 USD The expenses accounts are pulled from the sheet\u2014sometimes I have to go fix that by hand a bit, as they may not track precisely those from our ledger\u2014and the income shows that the contribution was made by his mother. Putting it All Together \uf0c1 Finally, we need to put together all these files. I created a top-level kyle.beancount file that simple declares all of his account categories and includes his mom and dad files. We have three files: dad.beancount mom.beancount kyle.beancount -> includes transactions from dad.beancount and mom.beancount I can then run bean-web or bean-query on kyle.beancount. There are two things which are interesting to see on that ledger: The distribution of Kyle\u2019s expenses, in other words, what\u2019s it costing us to raise a child (regardless of who pays). The difference between our contributions. In order to reconcile (2), we basically compare the balances of the Income:Mom and Income:Dad accounts. This can be done \u201cby hand\u201d, visually (using a calculator), but since Beancount\u2019s API make it so easy to pull any number from a ledger, I wrote a simple script which does that, computes the total amount of Income contributions, breaks it down to the expected numbers based on our chosen breakdown, compares it to each parent\u2019s actual contribution, and simply prints out the difference: $ reconcile_shared.py kyle.beancount Total contributions: 71009.30 Dad expected contribution: 42605.58 Dad actual contribution: 42906.58 Mom expected contribution: 28403.72 Mom actual contribution: 28102.72 Mom OWES Dad: 301.00 After reconciling, the final number should be zero. Reconciling the Child Ledger \uf0c1 In order to account for the difference and make the contributions to Kyle\u2019s upbringing in line with our mutual arrangement of 60%/40%, in the previous section, my wife would need to transfer $301 to me. Of course, we don\u2019t actually transfer anything in practice, I just add a virtual transfer to book the difference to her shared account on my ledger. To do this, all I have to do is insert a transaction in blais.beancount that looks like this: 2019-02-09 * \"Transfer to reconcile Kyle's expenses\" #kyle Assets:US:Share:Carolyn 301.00 USD Expenses:Kyle:Mom When this gets pulled into dad.beancount (as explained previously), it looks like this: 2019-02-09 * \"Transfer to reconcile Kyle's expenses\" #kyle Income:Mom -301.00 USD Income:Dad 301.00 USD After that, going back to the Kyle ledger, pulling in all the transactions again (this is done by using a Makefile) would show an updated balance of 0: $ reconcile_shared.py kyle.beancount Total contributions: 71009.30 Dad expected contribution: 42605.58 Dad actual contribution: 42605.58 Mom expected contribution: 28403.72 Mom actual contribution: 28403.72 Mom OWES Dad: 0.00 Summary of the System \uf0c1 Here\u2019s a diagram that puts in perspective the entire system together: I (\u201cDad\u201d) use Beancount via Emacs, exclusively. Carolyn (\u201cMom\u201d) only interacts with a single Google Sheets doc with three sheets in it. I pull in Carolyn\u2019s shared expenses from a sheet that she fills in to a ledger which gets included in my personal ledger. I also pull in her expenses for Kyle in a similar document, and from my personal ledger I generate my own expenses for Kyle. Both of these documents are merged in a top-level ledger dedicated to Kyle\u2019s expenses. I\u2019m not going to claim it\u2019s simple and that it\u2019s always up-to-date. I tend to update all of these things once/quarter and fix a few input errors each time we review it. I automate much of it via Makefiles, but frankly I don\u2019t update it frequently enough to just remember where all the moving pieces are, so every time, there\u2019s a bit of scrambling and reading my makefiles and figuring out what\u2019s what (in fact, I was motivated to write this to cement my understanding solidly for the future). Amazingly enough, it hasn\u2019t broken at all and I find all the pieces every time and make it work. And we have enough loose change between the two of us that it\u2019s never that urgent to reconcile every week. My wife tends to pay more shared expenses and I tend to pay more Kyle\u2019s expenses, so I often end up doing a transfer from one to the other to even things out and it\u2019s relatively close\u2014the shortfall in Kyle expenses makes up for my shortfall on the shared expenses. Conclusion \uf0c1 I hope this was useful to some of you trying to solve problems using the double-entry bookkeeping method. Please write questions and comments on the Beancount mailing-list, or as comments on this doc.","title":"How We Share Expenses"},{"location":"how_we_share_expenses.html#how-we-share-expenses","text":"This document explains how I share expenses with my wife. This is a bit involved and I\u2019ve developed a good working system, but it\u2019s not so simple for most people to do that, so I figured I would take the time to describe it to help others with designing similar processes for themselves.","title":"How We Share Expenses"},{"location":"how_we_share_expenses.html#context","text":"We\u2019re both working professionals and have decided to share all expenses roughly 70%/30%. This is what we shoot for, this is not a hard rule, but we track it as if it were rigid, and accept it as a good approximation. We have two types of shared expenses: Shared. Common expenses between us, e.g., rent, a dinner out with friends, etc. Kyle. Expenses for our child, e.g., daycare, diapers, nanny, baby food. These get handled very differently, because we book our child's expenses as if it were a separate project on its own. (If you prefer pictures, there\u2019s a diagram at the end of this document that provides an overview of the system.)","title":"Context"},{"location":"how_we_share_expenses.html#shared-expenses","text":"For our shared expenses, I maintain an account for her on my personal ledger file. This is simple and I\u2019m not too interested in maintaining a separate ledger for the totality of our common shared expenses. It\u2019s sufficient for me to just keep track of her balance on that one account. You can imagine that account like a credit card (I\u2019m the credit provider) that she pays off with transfers and also by making her own shared expenses. I just declare an Assets:US:Share:Carolyn account on my personal ledger ( blais.beancount ).","title":"Shared Expenses"},{"location":"how_we_share_expenses.html#my-shared-expenses","text":"Whenever I incur an expense that is for both of us, I book it normally but I will tag it with #carolyn: 2018-12-23 * \"WHISK\" \"Water refill\" #carolyn Liabilities:US:Amex:BlueCash -32.66 USD Expenses:Food:Grocery This gets automatically converted to: 2018-12-23 * \"WHISK\" \"Water refill\" #carolyn Liabilities:US:Amex:BlueCash -32.66 USD Expenses:Food:Grocery 19.60 USD Assets:US:Share:Carolyn 13.06 USD share: TRUE This is done by a custom plugin I built that splits the expenses according to some rules that we have between us (see also this plugin by Akkukis ). In this example, 40% of 32.66 (13.06) gets rerouted to her account. Note that this is an asset account for me, because she owes this.","title":"My Shared Expenses"},{"location":"how_we_share_expenses.html#her-shared-expenses","text":"We also have to keep track of the money she spends on her own for shared expenses. Since she\u2019s not a Beancount user, I\u2019ve set up a Google Sheets doc in which she can add rows to a particular sheet. This sheet has fields: Date, Description, Account, Amount. I try to keep it simple. Then, I built an extract_sheets.py script that can pull down this data automatically and it writes it to a dedicated file for this, overwriting the entire contents each time. The contents of this ledger ( carolyn.beancount ) look like this: pushtag #carolyn ... 2017-05-21 * \"Amazon\" \"Cotton Mattress Protector in White\" Expenses:Home:Furniture 199.99 USD Assets:US:Share:Carolyn -199.99 USD ... poptag #carolyn All of those transactions are tagged as #carolyn from the pushtag/poptag directive. They get translated by the plugin to reduce the amount by the portion I\u2019m supposed to pay: 2017-05-21 * \"Amazon\" \"Cotton Mattress Protector in White\" #carolyn Expenses:Home:Furniture 119.99 USD Assets:US:Share:Carolyn -119.99 USD share: TRUE These postings typically reduce her asset account by that much, and thus remove the portion she paid for me on these shared expenses. I generate the file with a single command like this: extract_sheets.py --tag='carolyn' --sheet='Shared Expenses' '' 'Assets:US:Share:Carolyn' > carolyn.beancount In my personal ledger, I include this file to merge those expenses with my own directives flow. I also define a query to generate a statement for her account. In blais.beancount : include \"carolyn.beancount\" 2020-01-01 query \"carolyn\" \" select date, description, position, balance from open on 2017-01-01 where account ~ 'Assets:US:Share:Carolyn' \"","title":"Her Shared Expenses"},{"location":"how_we_share_expenses.html#reviewing-statement","text":"In order to produce a statement for her to review (and spot the occasional mistake in data entry), I simply produce a journal of that account to a CSV file and upload that to another sheet in the same Google Sheets doc she inputs her expenses: bean-query -f csv -o carolyn.csv --numberify $L run carolyn upload-to-sheets -v --docid=\"\" carolyn.csv:\"Shared Account (Read-Only)\" This makes it easy for her to eyeball all the amounts posted to her balance with me and point out errors if they occur (some always appear). Moreover, all the information is in one place\u2014it\u2019s important to keep it simple. Finally, we can use the collaborative features of Sheets to communicate, e.g. comments, highlighting text, etc. Note that this system has the benefit of correctly accruing my expenses, by reducing my portion on categories for the stuff I pay and by including her portion on categories for the stuff she pays for.","title":"Reviewing & Statement"},{"location":"how_we_share_expenses.html#reconciling-our-shared-expenses","text":"Finally, in order to reconcile this account, my wife (or I, but usually she\u2019s behind) just makes a bank transfer to my account, which I book to reduce her running account balance: 2019-01-30 * \"HERBANK EXT TRNSFR; DIRECTDEP\" Assets:US:MyBank:Checking 3000 USD Assets:US:Share:Carolyn Typically she'll do this every month or two. She'll be fiddling on her laptop and ask casually \"Hey, what's my balance I can do a transfer now?\" It\u2019s all fine to relax about the particulars since the system is keeping track of everything precisely, so she can send some approximate amount, it doesn't matter, it'll post to her account.","title":"Reconciling our Shared Expenses"},{"location":"how_we_share_expenses.html#child-expenses","text":"I designed a very different system to track our child\u2019s expenses. For Kyle, I\u2019m definitely interested in tracking the total cash flows and expenses related to him, regardless of who paid for them. It\u2019s interesting to be able to ask (our ledger) a question like: \u201cHow much did his schooling cost?\u201d, for example, or \u201cHow much did we pay in diapers, in total?\u201d. Furthermore, we tend to pay for different things for Kyle, e.g. I deal with the daycare expenses (I\u2019m the accounting nerd after all, so this shouldn\u2019t be surprising), and his mother tends to buy all the clothing and prepare his food. To have a global picture of all costs related to him, we need to account for these things correctly. One interesting detail is that it would be difficult to do this with the previously described method, because I\u2019d have to have a mirror of all expense accounts I\u2019d use for him. This would make my personal ledger really ugly. For example, I want to book diapers to Expenses:Pharmacy , but I also have my own personal pharmacy expenses. So in theory, to do that I\u2019d like to have separate accounts for him and me, e.g., Expenses:Kyle:Pharmacy and Expenses:Pharmacy . This would have to be done for all the accounts we use for him. I don't do that.","title":"Child Expenses"},{"location":"how_we_share_expenses.html#my-child-expenses-on-my-personal-ledger","text":"Instead of doing that, what I want is for my personal ledger to book all the expenses I make for him to a single category: Expenses:Kyle , and to track all the detail in a shared ledger. But I still have to book all the expenses to some category, and these appear on my personal ledger, there\u2019s no option (I won\u2019t maintain a separate credit card to pay for his expenses, that would be overkill, so I have to find a way). I accomplish this by booking the expenses to my own expenses account, as usual, but tagging the transaction with #kyle: 2019-02-01 * \"AMAZON.COM\" \"MERCHANDISE - Diapers size 4 for Kyle\" #kyle Liabilities:US:Amex:BlueCash -49.99 USD Expenses:Pharmacy And I have a different plugin that automatically makes the conversion of those transactions to: 2019-02-01 * \"AMAZON.COM\" \"MERCHANDISE - Diapers size 4 for Kyle\" #kyle Liabilities:US:Amex:BlueCash -49.99 USD Expenses:Kyle 49.99 USD diverted_account: \"Expenses:Pharmacy\" So from my personal side, all those expenses get booked to my \u201cKyle project\u201d account. This is accomplished by the divert_expenses plugin, with this configuration: plugin \"beancount.plugins.divert_expenses\" \"{ 'tag': 'kyle', 'account': 'Expenses:Kyle' }\" The \u201cdiverted_account\u201d metadata is used to keep track of the original account, and this is used later by another script that generates a ledger file decided to my expenses for him (more below).","title":"My Child Expenses on my Personal Ledger"},{"location":"how_we_share_expenses.html#my-child-expenses-in-kyles-own-ledger","text":"Now, because we\u2019re considering Kyle\u2019s expenses a project of his own, I have to maintain a set of ledgers for him. I automatically pull the transactions I described in the previous section from my personal ledger and automatically convert them to a file dedicated to his dad (me). This is done by calling the extract_tagged script: extract_tagged.py blais.beancount '#kyle' 'Income:Dad' --translate \"Expenses:Kyle:Mom=Income:Mom\" > dad.beancount The matching transaction from the previous section would look like this in it: 2019-02-01 * \"AMAZON.COM\" \"MERCHANDISE - Diapers size 4 for Kyle\" #kyle Income:Dad -49.99 USD Expenses:Pharmacy 49.99 USD As you can see, the script was able to reconstruct the original account name in Kyle\u2019s ledger by using the metadata saved by the divert_expenses plugin in the previous section. It also books the source of the payment to a single account showing it came from his father ( Income:Dad ). There\u2019s no need for me to keep track of which payment method I used on Kyle\u2019s side (e.g. by credit card), that\u2019s not important to him. This ledger contains the sum total of all expenses I\u2019ve made for him to date. The file gets entirely overwritten each time I run this (this is a purely generated output, no hand editing is ever done here, if I change anything I change it in my personal ledger file).","title":"My Child Expenses in Kyle\u2019s own Ledger"},{"location":"how_we_share_expenses.html#her-child-expenses","text":"In order to keep track of her expenses for Kyle, we use the same method (and programs) as we use for our shared accounts in order to pull a set of \u201cCarolyn\u2019s expenses for Kyle\u201d from another sheet in the same Google Sheets doc: extract_sheets.py --sheet='Kyle Expenses (Regular)' '' 'Income:Mom' > mom.beancount This pulls in transactions that look like this: 2018-09-23 * \"SPROUT SAN FRANCISCO\" \"Clothing for Kyle 9-12 months\" Expenses:Clothing 118.30 USD Income:Mom -118.30 USD The expenses accounts are pulled from the sheet\u2014sometimes I have to go fix that by hand a bit, as they may not track precisely those from our ledger\u2014and the income shows that the contribution was made by his mother.","title":"Her Child Expenses"},{"location":"how_we_share_expenses.html#putting-it-all-together","text":"Finally, we need to put together all these files. I created a top-level kyle.beancount file that simple declares all of his account categories and includes his mom and dad files. We have three files: dad.beancount mom.beancount kyle.beancount -> includes transactions from dad.beancount and mom.beancount I can then run bean-web or bean-query on kyle.beancount. There are two things which are interesting to see on that ledger: The distribution of Kyle\u2019s expenses, in other words, what\u2019s it costing us to raise a child (regardless of who pays). The difference between our contributions. In order to reconcile (2), we basically compare the balances of the Income:Mom and Income:Dad accounts. This can be done \u201cby hand\u201d, visually (using a calculator), but since Beancount\u2019s API make it so easy to pull any number from a ledger, I wrote a simple script which does that, computes the total amount of Income contributions, breaks it down to the expected numbers based on our chosen breakdown, compares it to each parent\u2019s actual contribution, and simply prints out the difference: $ reconcile_shared.py kyle.beancount Total contributions: 71009.30 Dad expected contribution: 42605.58 Dad actual contribution: 42906.58 Mom expected contribution: 28403.72 Mom actual contribution: 28102.72 Mom OWES Dad: 301.00 After reconciling, the final number should be zero.","title":"Putting it All Together"},{"location":"how_we_share_expenses.html#reconciling-the-child-ledger","text":"In order to account for the difference and make the contributions to Kyle\u2019s upbringing in line with our mutual arrangement of 60%/40%, in the previous section, my wife would need to transfer $301 to me. Of course, we don\u2019t actually transfer anything in practice, I just add a virtual transfer to book the difference to her shared account on my ledger. To do this, all I have to do is insert a transaction in blais.beancount that looks like this: 2019-02-09 * \"Transfer to reconcile Kyle's expenses\" #kyle Assets:US:Share:Carolyn 301.00 USD Expenses:Kyle:Mom When this gets pulled into dad.beancount (as explained previously), it looks like this: 2019-02-09 * \"Transfer to reconcile Kyle's expenses\" #kyle Income:Mom -301.00 USD Income:Dad 301.00 USD After that, going back to the Kyle ledger, pulling in all the transactions again (this is done by using a Makefile) would show an updated balance of 0: $ reconcile_shared.py kyle.beancount Total contributions: 71009.30 Dad expected contribution: 42605.58 Dad actual contribution: 42605.58 Mom expected contribution: 28403.72 Mom actual contribution: 28403.72 Mom OWES Dad: 0.00","title":"Reconciling the Child Ledger"},{"location":"how_we_share_expenses.html#summary-of-the-system","text":"Here\u2019s a diagram that puts in perspective the entire system together: I (\u201cDad\u201d) use Beancount via Emacs, exclusively. Carolyn (\u201cMom\u201d) only interacts with a single Google Sheets doc with three sheets in it. I pull in Carolyn\u2019s shared expenses from a sheet that she fills in to a ledger which gets included in my personal ledger. I also pull in her expenses for Kyle in a similar document, and from my personal ledger I generate my own expenses for Kyle. Both of these documents are merged in a top-level ledger dedicated to Kyle\u2019s expenses. I\u2019m not going to claim it\u2019s simple and that it\u2019s always up-to-date. I tend to update all of these things once/quarter and fix a few input errors each time we review it. I automate much of it via Makefiles, but frankly I don\u2019t update it frequently enough to just remember where all the moving pieces are, so every time, there\u2019s a bit of scrambling and reading my makefiles and figuring out what\u2019s what (in fact, I was motivated to write this to cement my understanding solidly for the future). Amazingly enough, it hasn\u2019t broken at all and I find all the pieces every time and make it work. And we have enough loose change between the two of us that it\u2019s never that urgent to reconcile every week. My wife tends to pay more shared expenses and I tend to pay more Kyle\u2019s expenses, so I often end up doing a transfer from one to the other to even things out and it\u2019s relatively close\u2014the shortfall in Kyle expenses makes up for my shortfall on the shared expenses.","title":"Summary of the System"},{"location":"how_we_share_expenses.html#conclusion","text":"I hope this was useful to some of you trying to solve problems using the double-entry bookkeeping method. Please write questions and comments on the Beancount mailing-list, or as comments on this doc.","title":"Conclusion"},{"location":"importing_external_data.html","text":"Importing External Data in Beancount \uf0c1 Martin Blais , March 2016 http://furius.ca/beancount/doc/ingest This document is about Beancount v2; Beancount v3 is in development and uses a completely different build and installation system. For instructions on importing v3, see this document (Beangulp). Introduction The Importing Process Automating Network Downloads Typical Downloads Extracting Data from PDF Files Tools Invocation Configuration Configuring from an Input File Writing an Importer Regression Testing your Importers Generating Test Input Making Incremental Improvements Running the Tests Caching Data In-Memory Caching On-Disk Caching Organizing your Files Example Importers Cleaning Up Automatic Categorization Cleaning up Payees Future Work Related Discussion Threads Historical Note Introduction \uf0c1 This is the user\u2019s manual for the library and tools in Beancount which can help you automate the importing of external transaction data into your Beancount input file and manage the documents you download from your financial institutions\u2019 websites. The Importing Process \uf0c1 People often wonder how we do this, so let me describe candidly and in more detail what we\u2019re talking about doing here. The essence of the task at hand is to transcribe the transactions that occur in a person\u2019s entire set of accounts to a single text file: the Beancount input file. Having the entire set of transactions ingested in a single system is what we need to do in order to generate comprehensive reports about one\u2019s wealth and expenses. Some people call this \u201creconciling\u201d. We could transcribe all the transactions manually from paper statements by typing them in. However nowadays most financial institutions have a website where you can download a statement of historical transactions in a number of data formats which you can parse to output Beancount syntax for them. Importing transactions from these documents involves: Manually reviewing the transactions for correctness or even fraud; Merging new transactions with previous transactions imported from another account. For example, a payment from a bank account to pay off one\u2019s credit card will typically be imported from both the bank AND the credit card account. You must manually merge the corresponding transactions together 1 . Assigning the right category to an expense transaction Organizing your file by moving the resulting directives to the right place in your file. Verifying balances either visually or inserting a Balance directive which asserts what the final account balance should be after the new transactions are inserted. If my importers work without bugs, this is a process that takes me 30-60 minutes to update the majority of my active accounts. Less active accounts are updated every quarter or when I feel like it. I tend to do this on Saturday morning maybe twice per month, or sometimes weekly. If you maintain a well-organized input file with lots of assertions, mismatches are easily found, it\u2019s a pleasant and easy process, and after you\u2019re done generating an updated balance sheet is rewarding (I typically re-export to a Google Finance portfolio). Automating Network Downloads \uf0c1 The downloading of files is not something I automate, and Beancount provides no tools to connect to the network and fetch your files. There is simply too great a variety of protocols out there to make a meaningful contribution to this problem 2 . Given the nature of today's secure websites and the castles of JavaScript used to implement them, it would be a nightmare to implement. Web scraping is probably too much to be a worthwhile, viable solution. I manually log into the various websites with my usernames & passwords and click the right buttons to generate the downloaded files I need. These files are recognized automatically by the importers and extracting transactions and filing the documents in a well-organized directory hierarchy is automated using the tools described in this document. While I\u2019m not scripting the fetching, I think it\u2019s possible to do so on some sites. That work is left for you to implement where you think it\u2019s worth the time. Typical Downloads \uf0c1 Here\u2019s a description of the typical kinds of files involved; this describes my use case and what I\u2019ve managed to do. This should give you a qualitative sense of what\u2019s involved. Credit cards and banks provide fairly good quality historical statement downloads in OFX or CSV file formats but I need to categorize the other side of those transactions manually and merge some of the transactions together. Investment accounts provide me with great quality of processable statements and the extraction of purchase transactions is fully automated, but I need to manually edit sales transactions in order to associate the correct cost basis. Some institutions for specialized products (e.g., P2P lending) provide only PDF files and those are translated manually. Payroll stubs and vesting events are usually provided only as PDFs and I don't bother trying to extract data automatically; I transcribe those manually, keeping the input very regular and with postings in the same order as they appear on the statements. This makes it easier. Cash transactions : I have to enter those by hand. I only book non-food expenses as individual transactions directly, and for food maybe once every six months I'll count my wallet balance and insert a summarizing transaction for each month to debit away the cash account towards food to make it balance. If you do this, you end up with surprisingly little transactions to type manually, maybe just a few each week (it depends on lifestyle choices, for me this works). When I\u2019m on the go, I just note those on my phone in Google Keep and eventually transcribe them after they accumulate. Extracting Data from PDF Files \uf0c1 I've made some headway toward converting data from PDF files, which is a common need, but it's incomplete; it turns out that fully automating table extraction from PDF isn't easy in the general case. I have some code that is close to working and will release it when the time is right. Otherwise, the best FOSS solution I\u2019ve found for this is a tool called TabulaPDF but you still need to manually identify where the tables of data are located on the page; you may be able to automate some fetching using its sister project tabula-java . Nevertheless, I usually have good success with my importers grepping around PDF statements converted to ugly text in order to identify what institution they are for and extracting the date of issuance of the document. Finally, there are a number of different tools used to extract text from PDF documents, such as PDFMiner , LibreOffice , the xpdf library, the poppler library 3 and more... but none of them works consistently on all input documents; you will likely end up installing many and relying on different ones for different input files. For this reason, I\u2019m not requiring a dependency on PDF conversion tools from within Beancount. You should test what works on your specific documents and invoke those tools from your importer implementations. Tools \uf0c1 There are three Beancount tools provided to orchestrate the three stages of importing: bean-identify : Given a messy list of downloaded files (e.g. in ~/Downloads), automatically identify which of your configured importers is able to handle them and print them out. This is to be used for debugging and figuring out if your configuration is properly associating a suitable importer for each of the files you downloaded; bean-extract : Extracting transactions and statement date from each file, if at all possible. This produces some Beancount input text to be moved to your input file; bean-file : Filing away the downloaded files to a directory hierarchy which mirrors the chart of accounts, for preservation, e.g. in a personal git repo. The filenames are cleaned, the files are moved and an appropriate statement date is prepended to each of them so that Beancount may produce corresponding Document directives. Invocation \uf0c1 All tools accept the same input parameters: bean- For example, bean-extract blais.config ~/Downloads The filing tool accepts an extra option that lets the user decide where to move the files, e.g., bean-file -o ~/accounting/documents blais.config ~/Downloads Its default behavior is to move the files to the same directory as that of the configuration file. Configuration \uf0c1 The tools introduced previously orchestrate the processes, but they don\u2019t do all that much of the concrete work of groking the individual downloads themselves. They call methods on importer objects. You must provide a list of such importers; this list is the configuration for the importing process (without it, those tools don\u2019t do anything useful). For each file found, each of the importers is called to assert whether it can or cannot handle that file. If it deems that it can, methods can be called to produce a list of transactions, extract a date, or produce a cleaned up filename for the downloaded file. The configuration should be a Python3 module in which you instantiate the importers and assign the list to the module-level \u201c CONFIG \u201d variable, like this: #!/usr/bin/env python3 from myimporters.bank import acmebank from myimporters.bank import chase \u2026 CONFIG = [ acmebank.Importer(), chase.Importer(), \u2026 ] Of course, since you\u2019re crafting a Python script, you can insert whatever other code in there you like. All that matters is that this \u201c CONFIG \u201d variable refers to a list of objects which comply with the importer protocol (described in the next section). Their order does not matter. In particular, it\u2019s a good idea to write your importers as generically as possible and to parameterize them with the particular account names you use in your input file. This helps keep your code independent of the particular accounts and forces you to define logical accounts, and I\u2019ve found that this helps with clarity. Or not\u2026 At the end of the day, these importer codes live in some of your own personal place, not with Beancount. If you so desire, you can keep them as messy and unshareable as you like. Configuring from an Input File \uf0c1 An interesting idea that I haven\u2019t tested yet is to use one\u2019s Beancount input file to infer the configuration of importers. If you want to try this out and hack something, you can load your input file from the import configuration Python config, by using the API\u2019s beancount.loader.load_file() function. Writing an Importer \uf0c1 Each of the importers must comply with a particular protocol and implement at least some of its methods. The full detail of this protocol is best found in the source code itself: importer.py . The tools above will take care of finding the downloads and invoking the appropriate methods on your importer objects. Here\u2019s a brief summary of the methods you need to, or may want to, implement: name(): This method provides a unique id for each importer instance. It\u2019s convenient to be able to refer to your importers with a unique name; it gets printed out by the identification process, for instance. identify(): This method just returns true if this importer can handle the given file. You must implement this method, and all the tools invoke it to figure out the list of (file, importer) pairs. extract(): This is called to attempt to extract some Beancount directives from the file contents. It must create the directives by instantiating the objects defined in beancount.core.data and return them. file_account(): This method returns the root account associated with this importer. This is where the downloaded file will be moved by the filing script file_date(): If a date can be extracted from the statement\u2019s contents, return it here. This is useful for dated PDF statements\u2026 it\u2019s often possible using regular expressions to grep out the date from a PDF converted to text. This allows the filing script to prepend a relevant date instead of using the date when the file was downloaded (the default). file_name(): It\u2019s most convenient not to bother renaming downloaded files. Oftentimes, the files generated from your bank all have a unique name and they end up getting renamed by your browser when you download multiple ones and the names collide. This function is used for the importer to provide a \u201cnice\u201d name to file the download under. So basically, you create some module somewhere on your PYTHONPATH\u2014anywhere you like, somewhere private\u2014and you implement a class, something like this: from beancount.ingest import importer class Importer(importer.ImporterProtocol): def identify(self, file): \u2026 # Override other methods\u2026 Typically I create my importer module files in directories dedicated to each importer, so that I can place example input files all in that directory for regression testing. Regression Testing your Importers \uf0c1 I've found over time that regression testing is key to maintaining your importer code working. Importers are often written against file formats with no official spec and unexpected surprises routinely occur. For example, I have XML files with some unescaped \"&\" characters, which require a custom fix just for that bank 4 . I\u2019ve also witnessed a discount brokerage switching its dates format between MM/DD/YY and DD/MM/YY; that importer now needs to be able to handle both types. So you make the necessary adjustment, and eventually you find out that something else breaks; this isn\u2019t great. And the timing is particularly annoying: usually things break when you\u2019re trying to update your ledger: you have other things to do. The easiest, laziest and most relevant way to test those importers is to use some real data files and compare what your importer extracts from them to expected outputs. For the importers to be at least somewhat reliable, you really need to be able to reproduce the extractions on a number of real inputs. And since the inputs are so unpredictable and poorly defined, it\u2019s not practical to write exhaustive tests on what they could be. In practice, I have to make at least some fix to some of my importers every couple of months, and with this process, it only sinks about a half-hour of my time: I add the new downloaded file which causes breakage to the importer directory, I fix the code by running it there locally as a test. And I also run the tests over all the previously downloaded test inputs in that directory (old and new) to ensure my importer is still working as intended on the older files. There is some support for automating this process in beancount.ingest.regression . What we want is some routine that will list the importer\u2019s package directory, identify the input files which are to be used for testing, and generate a suite of unit tests which compares the output produced by importer methods to the contents of \u201cexpected files\u201d placed next to the test file. For example, given a package with an implementation of an importer and two sample input files: /home/joe/importers/acmebank/__init__.py <- code goes here /home/joe/importers/acmebank/sample1.csv /home/joe/importers/acmebank/sample2.csv You can place this code in the Python module (the __init__.py file): from beancount.ingest import regression \u2026 def test(): importer = Importer(...) yield from regression.compare_sample_files(importer) If your importer overrides the extract() and file_date() methods, this will generate four unit tests which get run automatically by pytest : A test which calls extract() on sample1.csv , prints the extracted entries to a string, and compares this string with the contents of sample1.csv.extract A test which calls file_date() on sample1.csv and compares the date with the one found in the sample1.csv.file_date file. A test like (1) but on sample2.csv A test like (2) but on sample2.csv Generating Test Input \uf0c1 At first, the files containing the expected outputs do not exist. When an expected output file is absent like this, the regression tests automatically generate those files from the extracted output. This would result in the following list of files: /home/joe/importers/acmebank/__init__.py <- code goes here /home/joe/importers/acmebank/sample1.csv /home/joe/importers/acmebank/sample1.csv.extract /home/joe/importers/acmebank/sample1.csv.file_date /home/joe/importers/acmebank/sample2.csv /home/joe/importers/acmebank/sample2.csv.extract /home/joe/importers/acmebank/sample2.csv.file_date You should inspect the contents of the expected output files to visually assert that they represent the contents of the downloaded files. If you run the tests again with those files present, the expected output files will be used as inputs to the tests. If the contents differ in the future, the test will fail and an error will be generated. (You can test this out now if you want, by manually editing and inserting some unexpected data in one of those files.) When you edit your source code, you can always re-run the tests to make sure it still works on those older files. When a newly downloaded file fails, you repeat the process above: You make a copy of it in that directory, fix the importer, run it, check the expected files. That\u2019s it 5 . Making Incremental Improvements \uf0c1 Sometimes I make improvements to the importers that result in more or better output being generated even in the older files, so that all the old tests will now fail. A good way to deal with this is to keep all of these files under source control, locally delete all the expected files, run the tests to regenerate new ones, and then diff against the most recent commit to check that the changes are as expected. Caching Data \uf0c1 Some of the data conversions for binary files can be costly and slow. This is usually the case for converting PDF files to text 6 . This is particularly painful, since in the process of ingesting our downloaded data we\u2019re typically going to run the tools multiple times\u2014at least twice if everything works without flaw: once to extract, twice to file\u2014and usually many more times if there are problems. For this reason, we want to cache these conversions, so that a painful 40 second PDF-to-text conversion doesn\u2019t have to be run twice, for example. Beancount aims to provide two levels of caching for conversions on downloaded files: An in-memory caching of conversions so that multiple importers requesting the same conversion runs them only once, and An on-disk caching of conversions so that multiple invocations of the tools get reused. In-Memory Caching \uf0c1 In-memory caching works like this: Your methods receive a wrapper object for a given file and invoke the wrapper\u2019s convert() method, providing a converter callable/function. class MyImporter(ImporterProtocol): ... def extract(self, file): text = file.convert(slow_convert_pdf_to_text) match = re.search(..., text) This conversion is automatically memoized: if two importers or two different methods use the same converter on the file, the conversion is only run once. This is a simple way of handling redundant conversions in-memory. Make sure to always call those through the .convert() method and share the converter functions to take advantage of this. On-Disk Caching \uf0c1 At the moment. Beancount only implements (1). On-disk caching will be implemented later. Track this ticket for status updates. Organizing your Files \uf0c1 The tools described in this document are pretty flexible in terms of letting you specify Import configuration : The Python file which provides the list of importer objects as a configuration; Importers implementation : The Python modules which implement the individual importers and their regression testing files; Downloads directory : Which directory the downloaded files are to be found in; Filing directory : Which directory the downloaded files are intended to be filed to. You can specify these from any location you want. Despite this, some people are often asking how to organize their files, so I provide a template example under beancount/examples/ingest/office , and I describe this here. I recommend that you create a Git or Mercurial 7 source-controlled repository following this structure: office \u251c\u2500\u2500 documents \u2502 \u251c\u2500\u2500 Assets \u2502 \u251c\u2500\u2500 Liabilities \u2502 \u251c\u2500\u2500 Income \u2502 \u2514\u2500\u2500 Expenses \u251c\u2500\u2500 importers \u2502 \u251c\u2500\u2500 __init__.py \u2502 \u2514\u2500\u2500 \u2026 \u2502 \u251c\u2500\u2500 __init__.py \u2502 \u251c\u2500\u2500 sample-download-1.csv \u2502 \u251c\u2500\u2500 sample-download-1.extract \u2502 \u251c\u2500\u2500 sample-download-1.file_date \u2502 \u2514\u2500\u2500 sample-download-1.file_name \u251c\u2500\u2500 personal.beancount \u2514\u2500\u2500 personal.import The root \u201coffice\u201d directory is your repository. It contains your ledger file (\u201c personal.beancount \u201d), your importer configuration (\u201c personal.import \u201d), your custom importers source code (\u201c importers/ \u201d) and your history of documents (\u201c documents/ \u201d), which should be well-organized by bean-file. You always run the commands from this root directory. An advantage of storing your documents in the same repository as your importers source code is that you can just symlink your regression tests to some files under the documents/ directory. You can check your configuration by running identify: bean-identify example.import ~/Downloads If it works, you can extract transactions from your downloaded files at once: bean-extract -e example.beancount example.import ~/Downloads > tmp.beancount You then open tmp.beancount and move its contents to your personal.beancount file. Once you\u2019re finished, you can stash away the downloaded files for posterity like this: bean-file example.import ~/Downloads -o documents If my importers work, I usually don\u2019t even bother opening those files. You can use the --dry-run option to test moving destinations before doing so. To run the regression tests of the custom importers, use the following command: pytest -v importers Personally, I have a Makefile in my root directory with these targets to make my life easier. Note that you will have to install \u201cpytest\u201d, which is a test runner; it is often packaged as \u201cpython3-pytest\u201d or \u201cpytest\u201d. Example Importers \uf0c1 Beyond the documentation above, I cooked up an example importer for a made-up CSV file format for a made-up investment account. See this directory . There\u2019s also an example of an importer which uses an external tool (PDFMiner2) to convert a PDF file to text to identify it and to extract the statement date from it. See this directory . Beancount also comes with some very basic generic importers. See this directory . There is a simple OFX importer that has worked for me for a long time. Though it\u2019s pretty simple, I\u2019ve used it for years, it\u2019s good enough to pull info out of most credit card accounts. There are also a couple of mixin classes you can mix into your importer implementation to make it more convenient; these are relics from the LedgerHub project\u2014you don\u2019t really need to use them\u2014which can help in the transition to it. Eventually I plan to build and provide a generic CSV file parser in this framework, as well as a parser for QIF files which should allow one to transition from Quicken to Beancount. (I need example inputs to do this; if you\u2019re comfortable sharing your file I could use it to build this, as I don\u2019t have any real input, I don\u2019t use Quicken.) It would also be nice to build a converter from GnuCash at some point; this would go here as well. Cleaning Up \uf0c1 Automatic Categorization \uf0c1 A frequently asked question and common idea from first-time users is \u201cHow do I automatically assign a category to transactions I\u2019ve imported which have only one side?\u201d For example, importing transactions from a credit card account usually provides only one posting, like this: 2016-03-18 * \"UNION MARKET\" Liabilities:US:CreditCard -12.99 USD For which you must manually insert an Expenses posting, like this: 2016-03-18 * \"UNION MARKET\" Liabilities:US:CreditCard -12.99 USD Expenses:Food:Grocery People often have the impression that it is time-consuming to do this. My standard answer is that while it would be fun to have, if you have a text editor with account name completion configured properly, it\u2019s a breeze to do this manually and you don\u2019t really need it. You wouldn\u2019t save much time by automating this away. And personally I like to go over each of the transactions to check what they are and sometimes add comments (e.g., who I had dinner with, what that Amazon charge was for, etc.) and that\u2019s when I categorize. It\u2019s something that could eventually be solved by letting the user provide some simple rules, or by using the history of past transactions to feed into a simple learning classifier. Beancount does not currently provide a mechanism to automatically categorize transactions. You can build this into your importer code. I want to provide a hook for the user to register a completion function that could run across all the importers where you could hook that code in. Cleaning up Payees \uf0c1 The payees that one can find in the downloads are usually ugly names: They are sometimes the legal names of the business, which often does not reflect the street name of the place you went, for various reasons. For example, I recently ate at a restaurant called the \u201cLucky Bee\u201d in New York, and the memo from the OFX file was \u201cKING BEE\u201d. The names are sometimes abbreviated or contain some crud. In the previous example, the actual memo was \u201cKING BEE NEW YO\u201d, where \u201cNEW YO\u201d is a truncated location string. The amount of ugliness is inconsistent between data sources. It would be nice to be able to normalize the payee names by translating them at import time. I think you can do most of it using some simple rules mapping regular expressions to names provided by the user. There\u2019s really no good automated way to obtain the \u201cclean name\u201d of the payee. Beancount does not provide a hook for letting you do this this yet. It will eventually. You could also build a plugin to rename those accounts when loading your ledger. I\u2019ll build that too\u2014it\u2019s easy and would result in much nicer output. Future Work \uf0c1 A list of things I\u2019d really want to add, beyond fortifying what\u2019s already there: A generic, configurable CSV importer which you can instantiate. I plan to play with this a bit and build a sniffer that could automatically figure out the role of each column. A hook to allow you to register a callback for post-processing transactions that works across all importers. Related Discussion Threads \uf0c1 Getting started; assigning accounts to bank .csv data Status of LedgerHub\u2026 how can I get started? Rekon wants your CSV files Historical Note \uf0c1 There once was a first implementation of the process described in this document. The project was called LedgerHub and has been decommissioned in February 2016, rewritten and the resulting code integrated in Beancount itself, into this beancount.ingest library. The original project was intended to include the implementation of various importers to share them with other people, but this sharing was not very successful, and so the rewrite includes only the scaffolding for building your own importers and invoking them, and only a very limited number of example importer implementations. Documents about LedgerHub are preserved, and can help you understand the origins and design choices for Beancount\u2019s importer support. They can be found here: Original design Original instructions & final status (the old version of this doc) An analysis of the reasons why it the project was terminated (post-mortem) There are essentially three conceptual modes of entering such transactions: (1) a user crafts a single transaction manually, (2) another where a user inputs the two sides as a single transaction to transfer accounts, and (3) the two separate transactions get merged into a single one automatically. These are dual modes of each other. The twist in this story is that the same transaction often posts at different dates in each of its accounts. Beancount currently [March 2016] does not support multiple dates for a single transaction\u2019s postings, but a discussion is ongoing to implement support for these input modes. See this document for more details. \u21a9 The closest to universal downloader you will find in the free software world is ofxclient for OFX files, and in the commercial world, Yodlee provides a service that connects to many financial institutions. \u21a9 The \u2018pdftotext\u2019 utility in poppler provides the useful \u2018-layout\u2019 flag which outputs a text file without mangling tables, which can be helpful in the normal case of \u2018transaction-per-row\u2019 \u21a9 After sending them a few detailed emails about this and getting no response nor seeing any change in the downloaded files, I have given up on them fixing the issue. \u21a9 As you can see, this process is partly why I don\u2019t share my importers code. It requires the storage of way too much personal data in order to keep them working. \u21a9 I don\u2019t really understand why, since opening them up for viewing is almost instant, but nearly all the tools to convert them to other formats are vastly slower. \u21a9 I personally much prefer Mercurial for the clarity of its commands and output and its extensibility, but an advantage of Git\u2019s storage model is that moving files within it comes for free (no extra copy is stored). Moving files in a Mercurial repository costs you a bit in storage space. And if you rename accounts or change how you organize your files you will end up potentially copying many large files. \u21a9","title":"Importing External Data"},{"location":"importing_external_data.html#importing-external-data-in-beancount","text":"Martin Blais , March 2016 http://furius.ca/beancount/doc/ingest This document is about Beancount v2; Beancount v3 is in development and uses a completely different build and installation system. For instructions on importing v3, see this document (Beangulp). Introduction The Importing Process Automating Network Downloads Typical Downloads Extracting Data from PDF Files Tools Invocation Configuration Configuring from an Input File Writing an Importer Regression Testing your Importers Generating Test Input Making Incremental Improvements Running the Tests Caching Data In-Memory Caching On-Disk Caching Organizing your Files Example Importers Cleaning Up Automatic Categorization Cleaning up Payees Future Work Related Discussion Threads Historical Note","title":"Importing External Data in Beancount"},{"location":"importing_external_data.html#introduction","text":"This is the user\u2019s manual for the library and tools in Beancount which can help you automate the importing of external transaction data into your Beancount input file and manage the documents you download from your financial institutions\u2019 websites.","title":"Introduction"},{"location":"importing_external_data.html#the-importing-process","text":"People often wonder how we do this, so let me describe candidly and in more detail what we\u2019re talking about doing here. The essence of the task at hand is to transcribe the transactions that occur in a person\u2019s entire set of accounts to a single text file: the Beancount input file. Having the entire set of transactions ingested in a single system is what we need to do in order to generate comprehensive reports about one\u2019s wealth and expenses. Some people call this \u201creconciling\u201d. We could transcribe all the transactions manually from paper statements by typing them in. However nowadays most financial institutions have a website where you can download a statement of historical transactions in a number of data formats which you can parse to output Beancount syntax for them. Importing transactions from these documents involves: Manually reviewing the transactions for correctness or even fraud; Merging new transactions with previous transactions imported from another account. For example, a payment from a bank account to pay off one\u2019s credit card will typically be imported from both the bank AND the credit card account. You must manually merge the corresponding transactions together 1 . Assigning the right category to an expense transaction Organizing your file by moving the resulting directives to the right place in your file. Verifying balances either visually or inserting a Balance directive which asserts what the final account balance should be after the new transactions are inserted. If my importers work without bugs, this is a process that takes me 30-60 minutes to update the majority of my active accounts. Less active accounts are updated every quarter or when I feel like it. I tend to do this on Saturday morning maybe twice per month, or sometimes weekly. If you maintain a well-organized input file with lots of assertions, mismatches are easily found, it\u2019s a pleasant and easy process, and after you\u2019re done generating an updated balance sheet is rewarding (I typically re-export to a Google Finance portfolio).","title":"The Importing Process"},{"location":"importing_external_data.html#automating-network-downloads","text":"The downloading of files is not something I automate, and Beancount provides no tools to connect to the network and fetch your files. There is simply too great a variety of protocols out there to make a meaningful contribution to this problem 2 . Given the nature of today's secure websites and the castles of JavaScript used to implement them, it would be a nightmare to implement. Web scraping is probably too much to be a worthwhile, viable solution. I manually log into the various websites with my usernames & passwords and click the right buttons to generate the downloaded files I need. These files are recognized automatically by the importers and extracting transactions and filing the documents in a well-organized directory hierarchy is automated using the tools described in this document. While I\u2019m not scripting the fetching, I think it\u2019s possible to do so on some sites. That work is left for you to implement where you think it\u2019s worth the time.","title":"Automating Network Downloads"},{"location":"importing_external_data.html#typical-downloads","text":"Here\u2019s a description of the typical kinds of files involved; this describes my use case and what I\u2019ve managed to do. This should give you a qualitative sense of what\u2019s involved. Credit cards and banks provide fairly good quality historical statement downloads in OFX or CSV file formats but I need to categorize the other side of those transactions manually and merge some of the transactions together. Investment accounts provide me with great quality of processable statements and the extraction of purchase transactions is fully automated, but I need to manually edit sales transactions in order to associate the correct cost basis. Some institutions for specialized products (e.g., P2P lending) provide only PDF files and those are translated manually. Payroll stubs and vesting events are usually provided only as PDFs and I don't bother trying to extract data automatically; I transcribe those manually, keeping the input very regular and with postings in the same order as they appear on the statements. This makes it easier. Cash transactions : I have to enter those by hand. I only book non-food expenses as individual transactions directly, and for food maybe once every six months I'll count my wallet balance and insert a summarizing transaction for each month to debit away the cash account towards food to make it balance. If you do this, you end up with surprisingly little transactions to type manually, maybe just a few each week (it depends on lifestyle choices, for me this works). When I\u2019m on the go, I just note those on my phone in Google Keep and eventually transcribe them after they accumulate.","title":"Typical Downloads"},{"location":"importing_external_data.html#extracting-data-from-pdf-files","text":"I've made some headway toward converting data from PDF files, which is a common need, but it's incomplete; it turns out that fully automating table extraction from PDF isn't easy in the general case. I have some code that is close to working and will release it when the time is right. Otherwise, the best FOSS solution I\u2019ve found for this is a tool called TabulaPDF but you still need to manually identify where the tables of data are located on the page; you may be able to automate some fetching using its sister project tabula-java . Nevertheless, I usually have good success with my importers grepping around PDF statements converted to ugly text in order to identify what institution they are for and extracting the date of issuance of the document. Finally, there are a number of different tools used to extract text from PDF documents, such as PDFMiner , LibreOffice , the xpdf library, the poppler library 3 and more... but none of them works consistently on all input documents; you will likely end up installing many and relying on different ones for different input files. For this reason, I\u2019m not requiring a dependency on PDF conversion tools from within Beancount. You should test what works on your specific documents and invoke those tools from your importer implementations.","title":"Extracting Data from PDF Files"},{"location":"importing_external_data.html#tools","text":"There are three Beancount tools provided to orchestrate the three stages of importing: bean-identify : Given a messy list of downloaded files (e.g. in ~/Downloads), automatically identify which of your configured importers is able to handle them and print them out. This is to be used for debugging and figuring out if your configuration is properly associating a suitable importer for each of the files you downloaded; bean-extract : Extracting transactions and statement date from each file, if at all possible. This produces some Beancount input text to be moved to your input file; bean-file : Filing away the downloaded files to a directory hierarchy which mirrors the chart of accounts, for preservation, e.g. in a personal git repo. The filenames are cleaned, the files are moved and an appropriate statement date is prepended to each of them so that Beancount may produce corresponding Document directives.","title":"Tools"},{"location":"importing_external_data.html#invocation","text":"All tools accept the same input parameters: bean- For example, bean-extract blais.config ~/Downloads The filing tool accepts an extra option that lets the user decide where to move the files, e.g., bean-file -o ~/accounting/documents blais.config ~/Downloads Its default behavior is to move the files to the same directory as that of the configuration file.","title":"Invocation"},{"location":"importing_external_data.html#configuration","text":"The tools introduced previously orchestrate the processes, but they don\u2019t do all that much of the concrete work of groking the individual downloads themselves. They call methods on importer objects. You must provide a list of such importers; this list is the configuration for the importing process (without it, those tools don\u2019t do anything useful). For each file found, each of the importers is called to assert whether it can or cannot handle that file. If it deems that it can, methods can be called to produce a list of transactions, extract a date, or produce a cleaned up filename for the downloaded file. The configuration should be a Python3 module in which you instantiate the importers and assign the list to the module-level \u201c CONFIG \u201d variable, like this: #!/usr/bin/env python3 from myimporters.bank import acmebank from myimporters.bank import chase \u2026 CONFIG = [ acmebank.Importer(), chase.Importer(), \u2026 ] Of course, since you\u2019re crafting a Python script, you can insert whatever other code in there you like. All that matters is that this \u201c CONFIG \u201d variable refers to a list of objects which comply with the importer protocol (described in the next section). Their order does not matter. In particular, it\u2019s a good idea to write your importers as generically as possible and to parameterize them with the particular account names you use in your input file. This helps keep your code independent of the particular accounts and forces you to define logical accounts, and I\u2019ve found that this helps with clarity. Or not\u2026 At the end of the day, these importer codes live in some of your own personal place, not with Beancount. If you so desire, you can keep them as messy and unshareable as you like.","title":"Configuration"},{"location":"importing_external_data.html#configuring-from-an-input-file","text":"An interesting idea that I haven\u2019t tested yet is to use one\u2019s Beancount input file to infer the configuration of importers. If you want to try this out and hack something, you can load your input file from the import configuration Python config, by using the API\u2019s beancount.loader.load_file() function.","title":"Configuring from an Input File"},{"location":"importing_external_data.html#writing-an-importer","text":"Each of the importers must comply with a particular protocol and implement at least some of its methods. The full detail of this protocol is best found in the source code itself: importer.py . The tools above will take care of finding the downloads and invoking the appropriate methods on your importer objects. Here\u2019s a brief summary of the methods you need to, or may want to, implement: name(): This method provides a unique id for each importer instance. It\u2019s convenient to be able to refer to your importers with a unique name; it gets printed out by the identification process, for instance. identify(): This method just returns true if this importer can handle the given file. You must implement this method, and all the tools invoke it to figure out the list of (file, importer) pairs. extract(): This is called to attempt to extract some Beancount directives from the file contents. It must create the directives by instantiating the objects defined in beancount.core.data and return them. file_account(): This method returns the root account associated with this importer. This is where the downloaded file will be moved by the filing script file_date(): If a date can be extracted from the statement\u2019s contents, return it here. This is useful for dated PDF statements\u2026 it\u2019s often possible using regular expressions to grep out the date from a PDF converted to text. This allows the filing script to prepend a relevant date instead of using the date when the file was downloaded (the default). file_name(): It\u2019s most convenient not to bother renaming downloaded files. Oftentimes, the files generated from your bank all have a unique name and they end up getting renamed by your browser when you download multiple ones and the names collide. This function is used for the importer to provide a \u201cnice\u201d name to file the download under. So basically, you create some module somewhere on your PYTHONPATH\u2014anywhere you like, somewhere private\u2014and you implement a class, something like this: from beancount.ingest import importer class Importer(importer.ImporterProtocol): def identify(self, file): \u2026 # Override other methods\u2026 Typically I create my importer module files in directories dedicated to each importer, so that I can place example input files all in that directory for regression testing.","title":"Writing an Importer"},{"location":"importing_external_data.html#regression-testing-your-importers","text":"I've found over time that regression testing is key to maintaining your importer code working. Importers are often written against file formats with no official spec and unexpected surprises routinely occur. For example, I have XML files with some unescaped \"&\" characters, which require a custom fix just for that bank 4 . I\u2019ve also witnessed a discount brokerage switching its dates format between MM/DD/YY and DD/MM/YY; that importer now needs to be able to handle both types. So you make the necessary adjustment, and eventually you find out that something else breaks; this isn\u2019t great. And the timing is particularly annoying: usually things break when you\u2019re trying to update your ledger: you have other things to do. The easiest, laziest and most relevant way to test those importers is to use some real data files and compare what your importer extracts from them to expected outputs. For the importers to be at least somewhat reliable, you really need to be able to reproduce the extractions on a number of real inputs. And since the inputs are so unpredictable and poorly defined, it\u2019s not practical to write exhaustive tests on what they could be. In practice, I have to make at least some fix to some of my importers every couple of months, and with this process, it only sinks about a half-hour of my time: I add the new downloaded file which causes breakage to the importer directory, I fix the code by running it there locally as a test. And I also run the tests over all the previously downloaded test inputs in that directory (old and new) to ensure my importer is still working as intended on the older files. There is some support for automating this process in beancount.ingest.regression . What we want is some routine that will list the importer\u2019s package directory, identify the input files which are to be used for testing, and generate a suite of unit tests which compares the output produced by importer methods to the contents of \u201cexpected files\u201d placed next to the test file. For example, given a package with an implementation of an importer and two sample input files: /home/joe/importers/acmebank/__init__.py <- code goes here /home/joe/importers/acmebank/sample1.csv /home/joe/importers/acmebank/sample2.csv You can place this code in the Python module (the __init__.py file): from beancount.ingest import regression \u2026 def test(): importer = Importer(...) yield from regression.compare_sample_files(importer) If your importer overrides the extract() and file_date() methods, this will generate four unit tests which get run automatically by pytest : A test which calls extract() on sample1.csv , prints the extracted entries to a string, and compares this string with the contents of sample1.csv.extract A test which calls file_date() on sample1.csv and compares the date with the one found in the sample1.csv.file_date file. A test like (1) but on sample2.csv A test like (2) but on sample2.csv","title":"Regression Testing your Importers"},{"location":"importing_external_data.html#generating-test-input","text":"At first, the files containing the expected outputs do not exist. When an expected output file is absent like this, the regression tests automatically generate those files from the extracted output. This would result in the following list of files: /home/joe/importers/acmebank/__init__.py <- code goes here /home/joe/importers/acmebank/sample1.csv /home/joe/importers/acmebank/sample1.csv.extract /home/joe/importers/acmebank/sample1.csv.file_date /home/joe/importers/acmebank/sample2.csv /home/joe/importers/acmebank/sample2.csv.extract /home/joe/importers/acmebank/sample2.csv.file_date You should inspect the contents of the expected output files to visually assert that they represent the contents of the downloaded files. If you run the tests again with those files present, the expected output files will be used as inputs to the tests. If the contents differ in the future, the test will fail and an error will be generated. (You can test this out now if you want, by manually editing and inserting some unexpected data in one of those files.) When you edit your source code, you can always re-run the tests to make sure it still works on those older files. When a newly downloaded file fails, you repeat the process above: You make a copy of it in that directory, fix the importer, run it, check the expected files. That\u2019s it 5 .","title":"Generating Test Input"},{"location":"importing_external_data.html#making-incremental-improvements","text":"Sometimes I make improvements to the importers that result in more or better output being generated even in the older files, so that all the old tests will now fail. A good way to deal with this is to keep all of these files under source control, locally delete all the expected files, run the tests to regenerate new ones, and then diff against the most recent commit to check that the changes are as expected.","title":"Making Incremental Improvements"},{"location":"importing_external_data.html#caching-data","text":"Some of the data conversions for binary files can be costly and slow. This is usually the case for converting PDF files to text 6 . This is particularly painful, since in the process of ingesting our downloaded data we\u2019re typically going to run the tools multiple times\u2014at least twice if everything works without flaw: once to extract, twice to file\u2014and usually many more times if there are problems. For this reason, we want to cache these conversions, so that a painful 40 second PDF-to-text conversion doesn\u2019t have to be run twice, for example. Beancount aims to provide two levels of caching for conversions on downloaded files: An in-memory caching of conversions so that multiple importers requesting the same conversion runs them only once, and An on-disk caching of conversions so that multiple invocations of the tools get reused.","title":"Caching Data"},{"location":"importing_external_data.html#in-memory-caching","text":"In-memory caching works like this: Your methods receive a wrapper object for a given file and invoke the wrapper\u2019s convert() method, providing a converter callable/function. class MyImporter(ImporterProtocol): ... def extract(self, file): text = file.convert(slow_convert_pdf_to_text) match = re.search(..., text) This conversion is automatically memoized: if two importers or two different methods use the same converter on the file, the conversion is only run once. This is a simple way of handling redundant conversions in-memory. Make sure to always call those through the .convert() method and share the converter functions to take advantage of this.","title":"In-Memory Caching"},{"location":"importing_external_data.html#on-disk-caching","text":"At the moment. Beancount only implements (1). On-disk caching will be implemented later. Track this ticket for status updates.","title":"On-Disk Caching"},{"location":"importing_external_data.html#organizing-your-files","text":"The tools described in this document are pretty flexible in terms of letting you specify Import configuration : The Python file which provides the list of importer objects as a configuration; Importers implementation : The Python modules which implement the individual importers and their regression testing files; Downloads directory : Which directory the downloaded files are to be found in; Filing directory : Which directory the downloaded files are intended to be filed to. You can specify these from any location you want. Despite this, some people are often asking how to organize their files, so I provide a template example under beancount/examples/ingest/office , and I describe this here. I recommend that you create a Git or Mercurial 7 source-controlled repository following this structure: office \u251c\u2500\u2500 documents \u2502 \u251c\u2500\u2500 Assets \u2502 \u251c\u2500\u2500 Liabilities \u2502 \u251c\u2500\u2500 Income \u2502 \u2514\u2500\u2500 Expenses \u251c\u2500\u2500 importers \u2502 \u251c\u2500\u2500 __init__.py \u2502 \u2514\u2500\u2500 \u2026 \u2502 \u251c\u2500\u2500 __init__.py \u2502 \u251c\u2500\u2500 sample-download-1.csv \u2502 \u251c\u2500\u2500 sample-download-1.extract \u2502 \u251c\u2500\u2500 sample-download-1.file_date \u2502 \u2514\u2500\u2500 sample-download-1.file_name \u251c\u2500\u2500 personal.beancount \u2514\u2500\u2500 personal.import The root \u201coffice\u201d directory is your repository. It contains your ledger file (\u201c personal.beancount \u201d), your importer configuration (\u201c personal.import \u201d), your custom importers source code (\u201c importers/ \u201d) and your history of documents (\u201c documents/ \u201d), which should be well-organized by bean-file. You always run the commands from this root directory. An advantage of storing your documents in the same repository as your importers source code is that you can just symlink your regression tests to some files under the documents/ directory. You can check your configuration by running identify: bean-identify example.import ~/Downloads If it works, you can extract transactions from your downloaded files at once: bean-extract -e example.beancount example.import ~/Downloads > tmp.beancount You then open tmp.beancount and move its contents to your personal.beancount file. Once you\u2019re finished, you can stash away the downloaded files for posterity like this: bean-file example.import ~/Downloads -o documents If my importers work, I usually don\u2019t even bother opening those files. You can use the --dry-run option to test moving destinations before doing so. To run the regression tests of the custom importers, use the following command: pytest -v importers Personally, I have a Makefile in my root directory with these targets to make my life easier. Note that you will have to install \u201cpytest\u201d, which is a test runner; it is often packaged as \u201cpython3-pytest\u201d or \u201cpytest\u201d.","title":"Organizing your Files"},{"location":"importing_external_data.html#example-importers","text":"Beyond the documentation above, I cooked up an example importer for a made-up CSV file format for a made-up investment account. See this directory . There\u2019s also an example of an importer which uses an external tool (PDFMiner2) to convert a PDF file to text to identify it and to extract the statement date from it. See this directory . Beancount also comes with some very basic generic importers. See this directory . There is a simple OFX importer that has worked for me for a long time. Though it\u2019s pretty simple, I\u2019ve used it for years, it\u2019s good enough to pull info out of most credit card accounts. There are also a couple of mixin classes you can mix into your importer implementation to make it more convenient; these are relics from the LedgerHub project\u2014you don\u2019t really need to use them\u2014which can help in the transition to it. Eventually I plan to build and provide a generic CSV file parser in this framework, as well as a parser for QIF files which should allow one to transition from Quicken to Beancount. (I need example inputs to do this; if you\u2019re comfortable sharing your file I could use it to build this, as I don\u2019t have any real input, I don\u2019t use Quicken.) It would also be nice to build a converter from GnuCash at some point; this would go here as well.","title":"Example Importers"},{"location":"importing_external_data.html#cleaning-up","text":"","title":"Cleaning Up"},{"location":"importing_external_data.html#automatic-categorization","text":"A frequently asked question and common idea from first-time users is \u201cHow do I automatically assign a category to transactions I\u2019ve imported which have only one side?\u201d For example, importing transactions from a credit card account usually provides only one posting, like this: 2016-03-18 * \"UNION MARKET\" Liabilities:US:CreditCard -12.99 USD For which you must manually insert an Expenses posting, like this: 2016-03-18 * \"UNION MARKET\" Liabilities:US:CreditCard -12.99 USD Expenses:Food:Grocery People often have the impression that it is time-consuming to do this. My standard answer is that while it would be fun to have, if you have a text editor with account name completion configured properly, it\u2019s a breeze to do this manually and you don\u2019t really need it. You wouldn\u2019t save much time by automating this away. And personally I like to go over each of the transactions to check what they are and sometimes add comments (e.g., who I had dinner with, what that Amazon charge was for, etc.) and that\u2019s when I categorize. It\u2019s something that could eventually be solved by letting the user provide some simple rules, or by using the history of past transactions to feed into a simple learning classifier. Beancount does not currently provide a mechanism to automatically categorize transactions. You can build this into your importer code. I want to provide a hook for the user to register a completion function that could run across all the importers where you could hook that code in.","title":"Automatic Categorization"},{"location":"importing_external_data.html#cleaning-up-payees","text":"The payees that one can find in the downloads are usually ugly names: They are sometimes the legal names of the business, which often does not reflect the street name of the place you went, for various reasons. For example, I recently ate at a restaurant called the \u201cLucky Bee\u201d in New York, and the memo from the OFX file was \u201cKING BEE\u201d. The names are sometimes abbreviated or contain some crud. In the previous example, the actual memo was \u201cKING BEE NEW YO\u201d, where \u201cNEW YO\u201d is a truncated location string. The amount of ugliness is inconsistent between data sources. It would be nice to be able to normalize the payee names by translating them at import time. I think you can do most of it using some simple rules mapping regular expressions to names provided by the user. There\u2019s really no good automated way to obtain the \u201cclean name\u201d of the payee. Beancount does not provide a hook for letting you do this this yet. It will eventually. You could also build a plugin to rename those accounts when loading your ledger. I\u2019ll build that too\u2014it\u2019s easy and would result in much nicer output.","title":"Cleaning up Payees"},{"location":"importing_external_data.html#future-work","text":"A list of things I\u2019d really want to add, beyond fortifying what\u2019s already there: A generic, configurable CSV importer which you can instantiate. I plan to play with this a bit and build a sniffer that could automatically figure out the role of each column. A hook to allow you to register a callback for post-processing transactions that works across all importers.","title":"Future Work"},{"location":"importing_external_data.html#related-discussion-threads","text":"Getting started; assigning accounts to bank .csv data Status of LedgerHub\u2026 how can I get started? Rekon wants your CSV files","title":"Related Discussion Threads"},{"location":"importing_external_data.html#historical-note","text":"There once was a first implementation of the process described in this document. The project was called LedgerHub and has been decommissioned in February 2016, rewritten and the resulting code integrated in Beancount itself, into this beancount.ingest library. The original project was intended to include the implementation of various importers to share them with other people, but this sharing was not very successful, and so the rewrite includes only the scaffolding for building your own importers and invoking them, and only a very limited number of example importer implementations. Documents about LedgerHub are preserved, and can help you understand the origins and design choices for Beancount\u2019s importer support. They can be found here: Original design Original instructions & final status (the old version of this doc) An analysis of the reasons why it the project was terminated (post-mortem) There are essentially three conceptual modes of entering such transactions: (1) a user crafts a single transaction manually, (2) another where a user inputs the two sides as a single transaction to transfer accounts, and (3) the two separate transactions get merged into a single one automatically. These are dual modes of each other. The twist in this story is that the same transaction often posts at different dates in each of its accounts. Beancount currently [March 2016] does not support multiple dates for a single transaction\u2019s postings, but a discussion is ongoing to implement support for these input modes. See this document for more details. \u21a9 The closest to universal downloader you will find in the free software world is ofxclient for OFX files, and in the commercial world, Yodlee provides a service that connects to many financial institutions. \u21a9 The \u2018pdftotext\u2019 utility in poppler provides the useful \u2018-layout\u2019 flag which outputs a text file without mangling tables, which can be helpful in the normal case of \u2018transaction-per-row\u2019 \u21a9 After sending them a few detailed emails about this and getting no response nor seeing any change in the downloaded files, I have given up on them fixing the issue. \u21a9 As you can see, this process is partly why I don\u2019t share my importers code. It requires the storage of way too much personal data in order to keep them working. \u21a9 I don\u2019t really understand why, since opening them up for viewing is almost instant, but nearly all the tools to convert them to other formats are vastly slower. \u21a9 I personally much prefer Mercurial for the clarity of its commands and output and its extensibility, but an advantage of Git\u2019s storage model is that moving files within it comes for free (no extra copy is stored). Moving files in a Mercurial repository costs you a bit in storage space. And if you rename accounts or change how you organize your files you will end up potentially copying many large files. \u21a9","title":"Historical Note"},{"location":"installing_beancount.html","text":"Installing Beancount \uf0c1 Martin Blais - Updated: June 2024 http://furius.ca/beancount/doc/install Instructions for downloading and installing Beancount on your computer. Releases \uf0c1 Beancount is a mature project: the first version was written in 2008. The current version (v3) of Beancount is stable and under continued maintenance and development. There is a mailing-list and a PyPI page. Where to Get It \uf0c1 This is the official location for the source code: https://github.com/beancount/beancount Download it like this, by using Git to make a clone on your machine: git clone https://github.com/beancount/beancount How to Install \uf0c1 Installing Python \uf0c1 Beancount uses Python 3.8 or above, which is a pretty recent version of Python (as of this writing), and a few common library dependencies. I try to minimize dependencies, but you do have to install a few. This is very easy. First, you should have a working Python install. Install the latest stable version >=3.8 using the download from python.org . Make sure you have the development headers and libraries installed as well (e.g., the \u201cPython.h\u201d header file). For example, on a Debian/Ubuntu system you would install the python3-dev package. Beancount supports setuptools since Feb 2016, and you will need to install dependencies. You will want to have the \u201cpip3\u201d tool installed. It\u2019s installed by default along with Python3 by now\u2014test this out by invoking \u201cpython3 -m pip --help\u201d command. Installing Beancount \uf0c1 Installing Beancount using pip \uf0c1 This is the easiest way to install Beancount. You just install Beancount using sudo -H python3 -m pip install beancount This should automatically download and install all the dependencies. Note, however, that this will install the latest version that was pushed to the PyPI repository and not the very latest version available from source. Releases to PyPI are made sporadically but frequently enough not to be too far behind. Consult the CHANGES file if you\u2019d like to find out what is not included since the release date. Installing Beancount using pip from the Repository \uf0c1 You can also use pip to install Beancount from its source code repository directly: sudo -H python3 -m pip install git+https://github.com/beancount/beancount#egg=beancount Installing Beancount from Source \uf0c1 Installing from source offers the advantage of providing you with the very latest version of the stable branch (\u201cmaster\u201d). The master branch should be as stable as the released version most of the time. Get the source code from the official repository: git clone https://github.com/beancount/beancount You might need to install some non-Python library dependencies, such as bison and flex and perhaps a few more (you'll find out when you try to build). It should be obvious what\u2019s missing. If on Ubuntu, use apt get to install those. If installing on Windows, see the Windows section below. Build and Install Beancount from source using pip3 \uf0c1 You can then install all the dependencies and Beancount itself using pip: cd beancount sudo -H python3 -m pip install . Installing for Development \uf0c1 If you want to execute the source in-place for making changes to it, you can use the setuptools \u201cdevelop\u201d command to point to it: cd beancount sudo python3 setup.py develop Warning: This modifies a .pth file in your Python installation to point to the path to your clone. You may or may not want this. I don't do this myself; the way I work with it is the \"old school\" way; I just build it locally and modify my shell's environment to find its libraries. You build it like this: cd beancount python3 setup.py build_ext -i # or \"make build\" and then both the PATH and PYTHONPATH environment variables need to be updated for it like this: export PATH=$PATH:/path/to/beancount/bin export PYTHONPATH=$PYTHONPATH:/path/to/beancount Dependencies for Development \uf0c1 Beancount needs a few more tools for development. If you\u2019re reading this, you\u2019re a developer, so I\u2019ll assume you can figure out how to install packages, skip the detail, and just list what you might need: pytest: for unit tests ruff: for linting GNU flex : This lexer generator is needed if you intend to modify the lexer. It generated C code that chops up the input files into tokens for the parser to consume. GNU bison : This old-school parser generator is needed if you intend to modify the grammar. It generates C code that parses the tokens generated by flex. (I like using old tools like this because they are pretty low on dependencies, just C code. It should work everywhere.)` python-dateutil : to run the beancount.scripts.example example generator script. Installing from Distribution Packages \uf0c1 Various distributions may package Beancount. Here are links to those known to exist: Arch: https://aur.archlinux.org/packages/beancount/ Windows Installation \uf0c1 Native \uf0c1 Installing this package by pip requires compiling some C++ code during the installation procedure which is only possible if an appropriate compiler is available on the computer, otherwise you will receive an error message about missing vsvarsall.bat or cl.exe . To be able to compile C++ code for Python you will need to install the same major version of the C++ compiler as your Python installation was compiled with. By running python in a console and looking for a text similar to [MSC v.1900 64 bit (AMD64)] you can determine which compiler was used for your particular Python distribution. In this example it is v.1900 . Using this number find the required Visual C++ version here . Since different versions seem to be compatible as long as the first two digits are the same you can in theory use any Visual C++ compiler between 1900 and 1999. According to my experience both Python 3.8 and 3.6 was compiled with MSC v.1900 so you can do either of the following to satisfy this requirement: Install the standalone Build Tools for Visual Studio 2017 or Install the standalone Visual C++ Build Tools 2015 or Modify an existing Visual Studio 2017 installation Start the Visual Studio 2017 installer from Add or remove programs Select Individual components Check VC++ 2017 version 15.9 v14.16 latest v141 tools or newer under Compilers, build tools, and runtimes Install Visual Studio 2019 add C++ build tools: C++ core features, MSVC v142 build tools If cl.exe is not in your path after installation, run Developer Command Prompt for Visual Studio and run the commands there. With Cygwin \uf0c1 Windows installation is, of course, a bit different. It\u2019s a breeze if you use Cygwin. You just have to prep your machine first. Here\u2019s how. Install the latest Cygwin . This may take a while (it downloads a lot of stuff), but it is well worth it in any case. But before you kick off the install, make sure the following packages are all manually enabled in the interface provided by setup.exe (they\u2019re not selected by default): python3 python3-devel python3-setuptools git make gcc-core flex bison Start a new Cygwin bash shell (there should be a new icon on your desktop) and install the pip3 installer tool by running this command: easy_install-3.4 pip Make sure you invoke the version of easy_install which matches your Python version, e.g. easy_install-3.8 if you have Python 3.8 installed, or more. At this point, you should be able to follow the instructions from the previous sections as is, starting from \u201cInstall Beancount using pip\u201d. With WSL \uf0c1 The newly released Windows 10 Anniversary Update brings WSL 'Windows Subsystem for Linux' with bash on Ubuntu on Windows (installation instructions and more at https://msdn.microsoft.com/commandline/wsl/about ). This makes beancount installation easy, from bash: sudo apt-get install python3-pip sudo pip3 install m3-cdecimal sudo pip3 install beancount --pre This is not totally \u201cWindows compatible\u201d, as it is running in a pico-process, but provides a convenient way to get the Linux command-line experience on Windows. (Contrib: willwray) Checking your Install \uf0c1 You should be able to run the binaries from this document . For example, running bean-check should produce something like this: $ bean-check usage: bean-check [-h] [-v] filename bean-check: error: the following arguments are required: filename If this works, you can now go to the tutorial and begin learning how Beancount works. Reporting Problems \uf0c1 If you need to report a problem, either send email on the mailing-list or file a ticket on Github. Running the following command lists the presence and versions of dependencies installed on your computer and it might be useful to include the output of this command in your bug report: python3 -m beancount.scripts.deps Editor Support \uf0c1 There is support for some editors available: Emacs support is provided in a separate repo . See the Getting Started text for installation instruction. Support for editing with Sublime has been contributed by Martin Andreas Andersen . See his github repo . Support for editing with Vim has been contributed by Nathan Grigg . See his GitHub repo . Visual Studio Code currently has two extensions available. Both have been tested on Linux. Beancount , with syntax checking (bean-check) and support for accounts, currencies, etc. It not only allows selecting existing open accounts but also displays their balance and other metadata. Quite helpful! Beancount Formatter , which can format the whole document, aligning the numbers, etc. using bean-format. If You Have Problems \uf0c1 If you run into any installation problems, file a ticket or email the mailing-list . Post-Installation Usage \uf0c1 Normally the scripts located under beancount/bin should be automatically installed somewhere in your executable path. For example, you should be able to just run \u201cbean-check\u201d on the command-line.","title":"Installing Beancount"},{"location":"installing_beancount.html#installing-beancount","text":"Martin Blais - Updated: June 2024 http://furius.ca/beancount/doc/install Instructions for downloading and installing Beancount on your computer.","title":"Installing Beancount"},{"location":"installing_beancount.html#releases","text":"Beancount is a mature project: the first version was written in 2008. The current version (v3) of Beancount is stable and under continued maintenance and development. There is a mailing-list and a PyPI page.","title":"Releases"},{"location":"installing_beancount.html#where-to-get-it","text":"This is the official location for the source code: https://github.com/beancount/beancount Download it like this, by using Git to make a clone on your machine: git clone https://github.com/beancount/beancount","title":"Where to Get It"},{"location":"installing_beancount.html#how-to-install","text":"","title":"How to Install"},{"location":"installing_beancount.html#installing-python","text":"Beancount uses Python 3.8 or above, which is a pretty recent version of Python (as of this writing), and a few common library dependencies. I try to minimize dependencies, but you do have to install a few. This is very easy. First, you should have a working Python install. Install the latest stable version >=3.8 using the download from python.org . Make sure you have the development headers and libraries installed as well (e.g., the \u201cPython.h\u201d header file). For example, on a Debian/Ubuntu system you would install the python3-dev package. Beancount supports setuptools since Feb 2016, and you will need to install dependencies. You will want to have the \u201cpip3\u201d tool installed. It\u2019s installed by default along with Python3 by now\u2014test this out by invoking \u201cpython3 -m pip --help\u201d command.","title":"Installing Python"},{"location":"installing_beancount.html#installing-beancount_1","text":"","title":"Installing Beancount"},{"location":"installing_beancount.html#installing-beancount-using-pip","text":"This is the easiest way to install Beancount. You just install Beancount using sudo -H python3 -m pip install beancount This should automatically download and install all the dependencies. Note, however, that this will install the latest version that was pushed to the PyPI repository and not the very latest version available from source. Releases to PyPI are made sporadically but frequently enough not to be too far behind. Consult the CHANGES file if you\u2019d like to find out what is not included since the release date.","title":"Installing Beancount using pip"},{"location":"installing_beancount.html#installing-beancount-using-pip-from-the-repository","text":"You can also use pip to install Beancount from its source code repository directly: sudo -H python3 -m pip install git+https://github.com/beancount/beancount#egg=beancount","title":"Installing Beancount using pip from the Repository"},{"location":"installing_beancount.html#installing-beancount-from-source","text":"Installing from source offers the advantage of providing you with the very latest version of the stable branch (\u201cmaster\u201d). The master branch should be as stable as the released version most of the time. Get the source code from the official repository: git clone https://github.com/beancount/beancount You might need to install some non-Python library dependencies, such as bison and flex and perhaps a few more (you'll find out when you try to build). It should be obvious what\u2019s missing. If on Ubuntu, use apt get to install those. If installing on Windows, see the Windows section below.","title":"Installing Beancount from Source"},{"location":"installing_beancount.html#build-and-install-beancount-from-source-using-pip3","text":"You can then install all the dependencies and Beancount itself using pip: cd beancount sudo -H python3 -m pip install .","title":"Build and Install Beancount from source using pip3"},{"location":"installing_beancount.html#installing-for-development","text":"If you want to execute the source in-place for making changes to it, you can use the setuptools \u201cdevelop\u201d command to point to it: cd beancount sudo python3 setup.py develop Warning: This modifies a .pth file in your Python installation to point to the path to your clone. You may or may not want this. I don't do this myself; the way I work with it is the \"old school\" way; I just build it locally and modify my shell's environment to find its libraries. You build it like this: cd beancount python3 setup.py build_ext -i # or \"make build\" and then both the PATH and PYTHONPATH environment variables need to be updated for it like this: export PATH=$PATH:/path/to/beancount/bin export PYTHONPATH=$PYTHONPATH:/path/to/beancount","title":"Installing for Development"},{"location":"installing_beancount.html#dependencies-for-development","text":"Beancount needs a few more tools for development. If you\u2019re reading this, you\u2019re a developer, so I\u2019ll assume you can figure out how to install packages, skip the detail, and just list what you might need: pytest: for unit tests ruff: for linting GNU flex : This lexer generator is needed if you intend to modify the lexer. It generated C code that chops up the input files into tokens for the parser to consume. GNU bison : This old-school parser generator is needed if you intend to modify the grammar. It generates C code that parses the tokens generated by flex. (I like using old tools like this because they are pretty low on dependencies, just C code. It should work everywhere.)` python-dateutil : to run the beancount.scripts.example example generator script.","title":"Dependencies for Development"},{"location":"installing_beancount.html#installing-from-distribution-packages","text":"Various distributions may package Beancount. Here are links to those known to exist: Arch: https://aur.archlinux.org/packages/beancount/","title":"Installing from Distribution Packages"},{"location":"installing_beancount.html#windows-installation","text":"","title":"Windows Installation"},{"location":"installing_beancount.html#native","text":"Installing this package by pip requires compiling some C++ code during the installation procedure which is only possible if an appropriate compiler is available on the computer, otherwise you will receive an error message about missing vsvarsall.bat or cl.exe . To be able to compile C++ code for Python you will need to install the same major version of the C++ compiler as your Python installation was compiled with. By running python in a console and looking for a text similar to [MSC v.1900 64 bit (AMD64)] you can determine which compiler was used for your particular Python distribution. In this example it is v.1900 . Using this number find the required Visual C++ version here . Since different versions seem to be compatible as long as the first two digits are the same you can in theory use any Visual C++ compiler between 1900 and 1999. According to my experience both Python 3.8 and 3.6 was compiled with MSC v.1900 so you can do either of the following to satisfy this requirement: Install the standalone Build Tools for Visual Studio 2017 or Install the standalone Visual C++ Build Tools 2015 or Modify an existing Visual Studio 2017 installation Start the Visual Studio 2017 installer from Add or remove programs Select Individual components Check VC++ 2017 version 15.9 v14.16 latest v141 tools or newer under Compilers, build tools, and runtimes Install Visual Studio 2019 add C++ build tools: C++ core features, MSVC v142 build tools If cl.exe is not in your path after installation, run Developer Command Prompt for Visual Studio and run the commands there.","title":"Native"},{"location":"installing_beancount.html#with-cygwin","text":"Windows installation is, of course, a bit different. It\u2019s a breeze if you use Cygwin. You just have to prep your machine first. Here\u2019s how. Install the latest Cygwin . This may take a while (it downloads a lot of stuff), but it is well worth it in any case. But before you kick off the install, make sure the following packages are all manually enabled in the interface provided by setup.exe (they\u2019re not selected by default): python3 python3-devel python3-setuptools git make gcc-core flex bison Start a new Cygwin bash shell (there should be a new icon on your desktop) and install the pip3 installer tool by running this command: easy_install-3.4 pip Make sure you invoke the version of easy_install which matches your Python version, e.g. easy_install-3.8 if you have Python 3.8 installed, or more. At this point, you should be able to follow the instructions from the previous sections as is, starting from \u201cInstall Beancount using pip\u201d.","title":"With Cygwin"},{"location":"installing_beancount.html#with-wsl","text":"The newly released Windows 10 Anniversary Update brings WSL 'Windows Subsystem for Linux' with bash on Ubuntu on Windows (installation instructions and more at https://msdn.microsoft.com/commandline/wsl/about ). This makes beancount installation easy, from bash: sudo apt-get install python3-pip sudo pip3 install m3-cdecimal sudo pip3 install beancount --pre This is not totally \u201cWindows compatible\u201d, as it is running in a pico-process, but provides a convenient way to get the Linux command-line experience on Windows. (Contrib: willwray)","title":"With WSL"},{"location":"installing_beancount.html#checking-your-install","text":"You should be able to run the binaries from this document . For example, running bean-check should produce something like this: $ bean-check usage: bean-check [-h] [-v] filename bean-check: error: the following arguments are required: filename If this works, you can now go to the tutorial and begin learning how Beancount works.","title":"Checking your Install"},{"location":"installing_beancount.html#reporting-problems","text":"If you need to report a problem, either send email on the mailing-list or file a ticket on Github. Running the following command lists the presence and versions of dependencies installed on your computer and it might be useful to include the output of this command in your bug report: python3 -m beancount.scripts.deps","title":"Reporting Problems"},{"location":"installing_beancount.html#editor-support","text":"There is support for some editors available: Emacs support is provided in a separate repo . See the Getting Started text for installation instruction. Support for editing with Sublime has been contributed by Martin Andreas Andersen . See his github repo . Support for editing with Vim has been contributed by Nathan Grigg . See his GitHub repo . Visual Studio Code currently has two extensions available. Both have been tested on Linux. Beancount , with syntax checking (bean-check) and support for accounts, currencies, etc. It not only allows selecting existing open accounts but also displays their balance and other metadata. Quite helpful! Beancount Formatter , which can format the whole document, aligning the numbers, etc. using bean-format.","title":"Editor Support"},{"location":"installing_beancount.html#if-you-have-problems","text":"If you run into any installation problems, file a ticket or email the mailing-list .","title":"If You Have Problems"},{"location":"installing_beancount.html#post-installation-usage","text":"Normally the scripts located under beancount/bin should be automatically installed somewhere in your executable path. For example, you should be able to just run \u201cbean-check\u201d on the command-line.","title":"Post-Installation Usage"},{"location":"installing_beancount_v3.html","text":"Installing Beancount (C++ version) \uf0c1 Martin Blais - July 2020 http://furius.ca/beancount/doc/v3-install Instructions for downloading and running Beancount v3 (in development) on your computer. For v2, see this document instead: Beancount - Install (v2) This document is about Beancount v3, an experimental in-development version (as of July 2020); Instructions for building the stable version (Beancount v2) can be found in this other document . Setup Python \uf0c1 Python dependencies are still required to run programs. pip install \u2013r requirements/dev.txt Building with Bazel \uf0c1 Warning: This is an experimental development branch. Do not expect everything to be polished perfectly. Bazel Dependencies \uf0c1 Beancount v3 uses the Bazel build system, which for the most part insulates you from local installs and dependencies from your computer. The dependencies to install are: Bazel itself. Follow instructions on https://bazel.build/ A C++ compiler. Either g++ or clang works. I'm using clang-11. A Python runtime (version 3.8 or above). Install from your distribution. Bazel will download and compile all the libraries it requires itself (even the code generators, e.g., Bison), building them at their precise versions as specified in the build, so you will not have to worry about them. Building & Testing \uf0c1 Simply run the following command: bazel build //bin:all There is currently no installation script, you have to run from source. You can run individual programs (e.g. bean-check) with this command: bazel run //bin:bean_check -- /path/to/ledger.beancount Or if you don't care to automatically rebuild modified code, like this: ./bazel-bin/bin/bean_check /path/to/ledger.beancount Development \uf0c1 You can run all the unit tests like this: bazel test //... Because Bazel has a detailed account of all dependencies, re-running this command after modifying code will result in only the touched targets being re-tested; this makes iterative development with testing a bit more fun. Another advantage is that since all the libraries the build depends on are downloaded and built, while this can be slow on the first build, it allows us to use very recently released versions of the code we depend on. Targets are defined in BUILD files local to their directories. All the build rules can be found under //third_party. Ingestion \uf0c1 The ingestion code involves importing code that lives outside the repository. Bazel binaries are self-contained and will fail to import modules that haven't been declared as dependencies, so running the //bin:bean_extract target, for example, probably won't work. This does not work yet (short of building your import configuration as a py_binary() target that would explicitly link to Beancount). This is doable without writing much Bazel code by defining a suitable WORKSPACE file that fetches the rules from it. I haven't produced an example of this yet (TBD). As a workaround, you can set your PYTHONPATH to import from the source location and create a symlink to the parser .so file beforehand. You can do it like this: make bazel-link TBD \uf0c1 A few build integration tasks remain to be done: pytype is not supported yet. pylint is not integrated in the build either. Installation for development with meson \uf0c1 Note : this section is updated base on the following discussion: https://groups.google.com/g/beancount/c/7ppbyz_5B5w The Bazel build for Beancount v3 builds some experimental C++ code that is at the time of writing (9 March 2024) not yet used for anything else than a technology demonstration. In \u201cproduction\u201d v3 uses meson-python to build the extension modules and pack them up in a Python wheel. This is what is used by pip during installation. This section describes installation processes for the purposes of development beancount v3 python code, rather than experimenting with C++ code. On Linux \uf0c1 Tested with python3.12 on Ubuntu Related links https://mesonbuild.com/meson-python/how-to-guides/editable-installs.html git clone https://github.com/beancount/beancount.git Create and activate virtual environment python3.12 -m venv beancount/venv . beancount/venv/bin/activate cd beancount Install required packages. python -m pip install meson-python meson ninja Install beancount in editable mode with no build isolation python -m pip install --no-build-isolation --editable . Note: There is an opinion that --no-build-isolation option is not needed, but it was also mentioned that the installation wasn\u2019t working without this option. Also the documentation suggests that this option is needed. This may depend on the type of Linux environment Install pytest python -m pip install pytest Run the tests and make sure everything is ok: pytest --import-mode=importlib beancount/ On Windows \uf0c1 Tested on 64 bit Windows 10 Pro prerequisites Install Microsoft Visual Studio (tested on v 2022) Procedure git clone https://github.com/beancount/beancount.git cd beancount If running x64 bit Windows, start the \"x64 Native Tools Command Prompt for VS 20XX\". To do this press and release the Windows keys and type x64 Open this prompt Go to the directory, where beancount is installed C:\\Program Files\\Microsoft Visual Studio\\2022\\Community>cd C:\\_code\\t\\beancount C:\\_code\\t\\beancount> Activate the virtual environment C:\\_code\\t\\beancount>venv\\Scripts\\Activate (venv) C:\\_code\\t\\beancount> Instal required packages (venv) C:\\_code\\t\\beancount>py -m pip install meson-python meson ninja Install beancount in editable mode. Unlike on Linux the --no-build-isolation on Windows from one side throws some errors from the other side does not seem to be needed (venv) C:\\_code\\t\\beancount> py -m pip install --editable . Install pytest (venv) C:\\_code\\t\\beancount>py -m pip install pytest Run tests (venv) C:\\_code\\t\\beancount>pytest --import-mode=importlib beancount Note: some of the tests on Windows fail, but this is due to general portability issue","title":"Installing Beancount"},{"location":"installing_beancount_v3.html#installing-beancount-c-version","text":"Martin Blais - July 2020 http://furius.ca/beancount/doc/v3-install Instructions for downloading and running Beancount v3 (in development) on your computer. For v2, see this document instead: Beancount - Install (v2) This document is about Beancount v3, an experimental in-development version (as of July 2020); Instructions for building the stable version (Beancount v2) can be found in this other document .","title":"Installing Beancount (C++ version)"},{"location":"installing_beancount_v3.html#setup-python","text":"Python dependencies are still required to run programs. pip install \u2013r requirements/dev.txt","title":"Setup Python"},{"location":"installing_beancount_v3.html#building-with-bazel","text":"Warning: This is an experimental development branch. Do not expect everything to be polished perfectly.","title":"Building with Bazel"},{"location":"installing_beancount_v3.html#bazel-dependencies","text":"Beancount v3 uses the Bazel build system, which for the most part insulates you from local installs and dependencies from your computer. The dependencies to install are: Bazel itself. Follow instructions on https://bazel.build/ A C++ compiler. Either g++ or clang works. I'm using clang-11. A Python runtime (version 3.8 or above). Install from your distribution. Bazel will download and compile all the libraries it requires itself (even the code generators, e.g., Bison), building them at their precise versions as specified in the build, so you will not have to worry about them.","title":"Bazel Dependencies"},{"location":"installing_beancount_v3.html#building-testing","text":"Simply run the following command: bazel build //bin:all There is currently no installation script, you have to run from source. You can run individual programs (e.g. bean-check) with this command: bazel run //bin:bean_check -- /path/to/ledger.beancount Or if you don't care to automatically rebuild modified code, like this: ./bazel-bin/bin/bean_check /path/to/ledger.beancount","title":"Building & Testing"},{"location":"installing_beancount_v3.html#development","text":"You can run all the unit tests like this: bazel test //... Because Bazel has a detailed account of all dependencies, re-running this command after modifying code will result in only the touched targets being re-tested; this makes iterative development with testing a bit more fun. Another advantage is that since all the libraries the build depends on are downloaded and built, while this can be slow on the first build, it allows us to use very recently released versions of the code we depend on. Targets are defined in BUILD files local to their directories. All the build rules can be found under //third_party.","title":"Development"},{"location":"installing_beancount_v3.html#ingestion","text":"The ingestion code involves importing code that lives outside the repository. Bazel binaries are self-contained and will fail to import modules that haven't been declared as dependencies, so running the //bin:bean_extract target, for example, probably won't work. This does not work yet (short of building your import configuration as a py_binary() target that would explicitly link to Beancount). This is doable without writing much Bazel code by defining a suitable WORKSPACE file that fetches the rules from it. I haven't produced an example of this yet (TBD). As a workaround, you can set your PYTHONPATH to import from the source location and create a symlink to the parser .so file beforehand. You can do it like this: make bazel-link","title":"Ingestion"},{"location":"installing_beancount_v3.html#tbd","text":"A few build integration tasks remain to be done: pytype is not supported yet. pylint is not integrated in the build either.","title":"TBD"},{"location":"installing_beancount_v3.html#installation-for-development-with-meson","text":"Note : this section is updated base on the following discussion: https://groups.google.com/g/beancount/c/7ppbyz_5B5w The Bazel build for Beancount v3 builds some experimental C++ code that is at the time of writing (9 March 2024) not yet used for anything else than a technology demonstration. In \u201cproduction\u201d v3 uses meson-python to build the extension modules and pack them up in a Python wheel. This is what is used by pip during installation. This section describes installation processes for the purposes of development beancount v3 python code, rather than experimenting with C++ code.","title":"Installation for development with meson"},{"location":"installing_beancount_v3.html#on-linux","text":"Tested with python3.12 on Ubuntu Related links https://mesonbuild.com/meson-python/how-to-guides/editable-installs.html git clone https://github.com/beancount/beancount.git Create and activate virtual environment python3.12 -m venv beancount/venv . beancount/venv/bin/activate cd beancount Install required packages. python -m pip install meson-python meson ninja Install beancount in editable mode with no build isolation python -m pip install --no-build-isolation --editable . Note: There is an opinion that --no-build-isolation option is not needed, but it was also mentioned that the installation wasn\u2019t working without this option. Also the documentation suggests that this option is needed. This may depend on the type of Linux environment Install pytest python -m pip install pytest Run the tests and make sure everything is ok: pytest --import-mode=importlib beancount/","title":"On Linux"},{"location":"installing_beancount_v3.html#on-windows","text":"Tested on 64 bit Windows 10 Pro prerequisites Install Microsoft Visual Studio (tested on v 2022) Procedure git clone https://github.com/beancount/beancount.git cd beancount If running x64 bit Windows, start the \"x64 Native Tools Command Prompt for VS 20XX\". To do this press and release the Windows keys and type x64 Open this prompt Go to the directory, where beancount is installed C:\\Program Files\\Microsoft Visual Studio\\2022\\Community>cd C:\\_code\\t\\beancount C:\\_code\\t\\beancount> Activate the virtual environment C:\\_code\\t\\beancount>venv\\Scripts\\Activate (venv) C:\\_code\\t\\beancount> Instal required packages (venv) C:\\_code\\t\\beancount>py -m pip install meson-python meson ninja Install beancount in editable mode. Unlike on Linux the --no-build-isolation on Windows from one side throws some errors from the other side does not seem to be needed (venv) C:\\_code\\t\\beancount> py -m pip install --editable . Install pytest (venv) C:\\_code\\t\\beancount>py -m pip install pytest Run tests (venv) C:\\_code\\t\\beancount>pytest --import-mode=importlib beancount Note: some of the tests on Windows fail, but this is due to general portability issue","title":"On Windows"},{"location":"ledgerhub_design_doc.html","text":"Design Doc for Ledgerhub \uf0c1 Martin Blais , February 2014 http://furius.ca/beancount/doc/ledgerhub-design-doc Motivation Goals & Stages Details of Stages Fetching Identification Extraction Transform Rendering Filing Implementation Details Importers Interface References Please note that this document is the original design doc for LedgerHub. LedgerHub is being transitioned back to Beancount. See this postmortem document for details [blais, 2015-12]. Motivation \uf0c1 Several open source projects currently exist that provide the capability to create double-entry transactions for bookkeeping from a text file input. These various double-entry bookkeeping projects include Beancount , Ledger , HLedger , Abandon , and they are independent implementations of a similar goal: the creation of an in-memory representation for double-entry accounting transactions from a text file, and the production of various reports from it, such as balance sheets, income statements, journals, and others. Each implementation explores slightly different feature sets, but essentially all work by reading their input from a file whose format is custom declarative language that describe the transactions, a language which is meant to be written by humans and whose syntax is designed with that goal in mind. While the languages do vary somewhat, the underlying data structures that they define are fairly similar. An essential part of the process of regularly updating one\u2019s journal files is the replication of a real-world account\u2019s transaction detail to a single input file in a consistent data format. This is essentially a translation step, meant to bring the transaction details of many institutions\u2019 accounts into a single system. Various banks and credit card companies provide downloadable transaction data in either Quicken or Microsoft Money (OFX) formats, and many institutions provide custom CSV files with transaction detail. Moreover, many of these institutions also make regular statements available for download as PDF files, and these can be associated with one\u2019s ledger accounts. The process of translating these external data formats can be automated to some extent. These various files can be translated to output text that can then be massaged by the user to be integrated into input file formats accepted by a double-entry bookkeeping package. Several projects have begun to make inroads in that domain: Ledger-autosync aims at fetching transactions automatically from OFX servers for and translating them for Ledger and HLedger, and Reckon converts CSV files for Ledger. Beancount includes code that can automate the identification of downloaded files to the accounts from a ledger, extract their transaction detail, and automatically file them to a directory hierarchy that mirrors the ledger\u2019s chart of accounts. This code should probably live outside of Beancount. Ledger also sports a \u201cconvert\u201d command that attempts to do similar things and a CSV2Ledger Perl script is available that can convert CSV files. HLedger also had a convert command which translated CSV files with optional conversion hints defined in a separate file; HLedger now does the same conversion on-the-fly when the input file is CSV (i.e., CSV is considered a first-class input format). The programs that fetch and convert external data files do not have to be tied to a single system. Moreover, this is often cumbersome code that would benefit greatly from having a large number of contributors, which could each benefit each other from having common parsers ready and working for the various institutions that they\u2019re using or likely to use in the future. I - the author of Beancount - have decided to move Beancount\u2019s importing and filing source code outside of its home project and to decouple it from the Beancount source code, so that others can contribute to it, with the intent of providing project-agnostic functionality. This document describes the goals and design of this project. Goals & Stages \uf0c1 This new project should address the following aspects in a project-agnostic manner: Fetching : Automate obtaining the external data files by connecting to the data sources directly. External tools and libraries such as ofxclient for OFX sources can be leveraged for this purpose. Web scraping could be used to fetch downloadable files where possible. The output of this stage is a list of institution-specific files downloaded to a directory. Note that fetching does not just apply to transaction data here; we will also support fetching prices . A list of (date, price) entries may be created from this data. We will likely want to support an intermediate format for expressing a list of positions (and appropriate support in the ledgerhub-Ledger/Beancount/HLedger interface to obtain it). Identification : Given a filename and its contents, automatically guess which institution and account configuration the file is for, and ideally be able to extract the date from the file or statement. This should also work with PDF files. The output of this stage is an association of each input file to a particular extractor and configuration (e.g. a particular account name). Extraction : Parse each file (if possible) and extract a list of information required to generate double-entry transactions data structures from it, in some sort of generic data structure, such as dicts of strings and numbers, independent of the underlying project\u2019s desired output. If possible, a verbatim snippet of the original text that generated the transaction should be attached to the output data structure. The output of this stage is a data structure, e.g., a list of Python dictionaries in some defined format. Transform : Given some information from the past transaction history contained in a journal, using simple learning algorithms, a program should be able to apply transformations on the transactional information extracted from the previous step. The most common use case for this is to automatically add a categorization posting to transactions that have a single posting only. For example, transactions from credit card statements typically include the changes in balance of the credit card account but all transactions are left to be manually associated with a particular expense account. Some of this process can be automated at this stage. Rendering : Convert the internal transactions data structures to the particular syntax of a double-entry bookkeeping project implementation and to the particular desired syntax variants (e.g. currency formatting, comma vs. dot decimal separator, localized input date format). This steps spits out text to be inserted into an input file compatible with the ledger software of choice. Filing : Sanitize the downloaded files\u2019 filenames and move them into a well organized and structured directory hierarchy corresponding to the identified account. This can run from the same associations derived in the identification step. Apart from the Render stage, all the other stages should be implemented without regard for a particular project, this should work across all ledger implementations. The Rendering code, however, should specialize, import source code, and attempt to add as many of the particular features provided by each project to its output text. Where necessary, interfaces to obtain particular data sets from each ledger implementation\u2019s input files should be provided to shield the common code from the particular implementation details of that project. For instance, a categorization Transform step would need to train its algorithm on some of the transaction data (i.e., the narration fields and perhaps some of the amounts, account names, and dates). Each project should provide a way to obtain the necessary data from its input data file, in the same format. Details of Stages \uf0c1 Fetching \uf0c1 By default, a user should be able to click their way to their institution\u2019s website and download documents to their ~/Downloads directory. A directory with some files in it should be the reasonable default input to the identification stage. This directory should be allowed to have other/garbage files in it, the identification step should be able to skip those automatically. A module that can automatically fetch the data needs to be implemented. Ideally this would not require an external tool. The data extracted should also have a copy saved in some Downloads directory. This is the domain of the ledger-autosync project. Perhaps we should coordinate input/outputs or even integrate call some of its library code at this stage. The author notes that fetching data from OFX servers is pretty easy, though the begin/end dates will have to get processed and filtered. Automatic fetching support will vary widely depending on where the institutions are located. Some places have solid support, some less. Use the data from ofxhome.com to configure. Fetching Prices \uf0c1 For fetching prices, there are many libraries out there. Initially we will port Beancount\u2019s bean-prices to ledgerhub. Identification \uf0c1 The identification stage consists in running a driver program that Searches for files in a directory hierarchy (typically your ~/Downloads folder) If necessary, converts the files into some text/ascii format, so that regular expressions can be matched against it (even if the output is messy, e.g., with PDF files converted to ASCII). This works well for PDF files: despite the fact that we cannot typically extract transactional data from them, we can generally pretty reliably identify which account they\u2019re for and almost always extract the statement date as well. Check a list of regular expressions against the ASCII\u2019fied contents. If the regular expressions all match, the configuration is associated to the filename. Note that more than one configuration may be associated to the same file because some files contain many sections, sections for which different importers may be called on to extract their data (e.g., OFX banking + OFX credit card can be mixed in the same file, and some institutions do). The net result of this process is an association of each filename with the a specific importer object instantiated in the configuration file. These importer objects are created with a set of required account names which they use to produce the Ledger-like syntax from the downloaded file that was associated with it. Here is an example configuration for two importers: from ledgerhub.sources.rbc import rbcinvesting, rbcpdf CONFIG = [ ... (('FileType: application/vnd.ms-excel', r'Filename: .*Activity-123456789-', ), rbcinvesting.Importer({ 'FILE' : 'Assets:CA:RBC-Investing:Taxable', 'cash' : 'Assets:CA:RBC-Investing:Taxable:Cash', 'positions' : 'Assets:CA:RBC-Investing:Taxable', 'interest' : 'Income:CA:RBC-Investing:Taxable:Interest', 'dividend' : 'Income:CA:RBC-Investing:Taxable:Dividends', 'fees' : 'Expenses:Financial:Fees', 'commission' : 'Expenses:Financial:Commissions', 'transfer' : 'Assets:CA:RBC:Checking', })), (('FileType: application/pdf', 'Filename:.*/123456789-\\d\\d\\d\\d[A-Z][a-z][a-z]\\d\\d-\\d\\d\\d\\d[A-Z][a-z][a-z]\\d\\d.pdf'), rbcpdf.Importer({ 'FILE': 'Assets:CA:RBC-Investing:RRSP', })), The configuration consists in a list, for each possible importer, of a pair of 1) a list of regular expressions which all should match against a \u201cmatch text\u201d, which is a \u201ctextified\u201d version of the contents of a file to be imported, and 2) an importer object, configured with a specific set of accounts to use for producing transactions. Each importer requires a particular set of output accounts which it uses to create its transactions and postings. The ledger\u2019s filename, and a list of these (regexps, importer) pairs is all that is necessary for the driver to carry out all of its work. The textification consists in a simple and imperfect conversion of downloaded file that are in binary format to something that we can run regular expressions against. For an OFX file or CSV file there is no conversion required for textification, we can just match against the text contents of those files; for an Excel/XLS file, we need to convert that to a CSV file, which can then be searched; for a PDF file, a number of different pdf-to-text converters are attempted until one succeeds (the tools for this are notoriously unreliable, so we have to try various ones). Note that this converted \"match text\" is only created temporarily and only for the purpose of identification; the importer will get the original binary file to do its work. It is not entirely clear whether the regular expressions can be standardized to avoid having the user configure them manually. In practice, I have found it often necessary, or at least very convenient, to place an account id in my import configuration. It is true that configuring each of the possible downloads can be a hassle that requires the user to do a bit of guesswork while looking at the contents of each file, but this has been much more reliable in practice than attempts at normalizing this process, likely because it is a much easier problem to uniquely distinguish between all the files of a particular user than to distinguish between all the types of files. Using an account id in one of the regular expressions is the easy way to do that, and it works well. This also provides a clear place to attach the list of accounts to a particular importer, something that necessarily requires user input anyway. Extraction \uf0c1 Once the association is made, we run the importers on each of the files. Some data structure is produced. The importers each do what they do - this is where the ugly tricks go. Ideally, we should build a library of common utilities to help parsing similar file types. Though each of the importer modules should be pretty much independent, some common functionality can be imagined, for example, how one deals with different stocks held in a single investment account, could be configured outside of each importer (e.g., preferred method could be to create a subaccount of that account, with the symbol of the stock, or otherwise). Note [AMaffei]: This could output a generic and well-defined CSV file format if you want to have the option of running the various steps as separate UNIX-style tools and/or process the intermediate files with regular text processing tools. Transform \uf0c1 Some transformations should be independent of importers. In particular, automatically categorizing incomplete transactions is not dependent on which importer created the transaction. I\u2019d like to keep this step as general as possible so that other embellishment steps can be inserted here in the future. Right now, I can only think of the following uses: Auto-categorization of transactions with only a single leg Detection of duplicate transactions: imported files often contain transactions which are already in the ledger; those should be either ignored or marked as such. In practice, this is not as easy as it sounds, because a straightforward date + narration comparison will fail: if the same transaction comes from two input data files, one side always ends up getting merged to the other, and sometimes even the date differs a bit. Some amount of fuzzy matching is required. Normalization of payee names: the imported names of payees are often cut short or include some irrelevant words, such as \u201cLLC\u201d, city names, and/or number codes. It may be desirable to somehow clean those up automatically. This step involves a bootstrapping phase, where we will extract some data from the actual ledger that the transactions are meant to be imported into. We will implement a generic interface that should allow each ledger language implementation to provide relevant data for training. The output data here should be in the same format as its input, so that we can optionally skip this phase. Rendering \uf0c1 An output renderer should be selected by the driver. This is where we convert the extracted data structures to the particular flavor of ledger implementation you\u2019re using. Each of the renderer implementations should be free to import modules from its particular implementation, and we should be careful to constraint these import dependencies to only these modules, to make sure that only a single ledger implementation is required in order for the code to run. Options for rendering style could be defined here, for each renderer, because each of the languages have particularies. [AMaffei] Also, it should be possible to provide a generic renderer that takes printf-style format strings to output in any desired format. Filing \uf0c1 Importers should be able to look at the textified contents of the files and find the file/statement date. This is useful, because we can rename the file by prepending the date of the statement, and the date at which we download the statement or transaction files is rarely the same date at which it was generated. In the case where we are not able to extract a date from the file, we fall back on the filename\u2019s last modified time. A target directory should be provided and we should move each file to the account with which it is associated. For example, a file like this: ~/Downloads/ofx32755.qbo should be moved to a directory .../Assets/US/RBC/Checking/2013-11-27.ofx32755.qbo if it is associated by the identification step with an importer for the Assets:US:RBC:Checking account. For this purpose, all the importers should have a required \u201cfiling\u201d account associated with them. As far as I know only Beancount implements this at the moment, but I suspect this convenient mechanism of organizing and preserving your imported files will be found useful by others. Given a list of directories, Beancount automatically finds those files and using the date in the filename, is able to render links to the files as line items in the journal web pages, and serve their contents when the user clicks on the links. Even without this capability, it can be used to maintain a cache of your documents (I maintain mine in a repository which I sync to an external drive for backup). Implementation Details \uf0c1 Notes about the initial implementation: The implementation of this project will be carried out in Python3. Why Python? The performance of importing and extracting is largely irrelevant, a dynamic language works well for this type of task Parsing in a dynamic language works great, there are many available libraries Python3 is now widely distributed and all desired parsing libraries are commonly available for it at this point All modules should be tested, including testing with sample input. If you want to add a new module, you should need to provide an anonymized sample file for it. We will have to have an automated test suite, because past experience has shown this type of code to be quite brittle and fragile to new and unexpected inputs. It\u2019s easy to write, but it\u2019s also easy to break. In order to test binary files that cannot be anonymized, we will provide the ability to test from match-text instead of from original binary statement PDF. Those files are generally not extractable anyhow and are only there for identification and filing (e.g. a PDF statement, we can\u2019t extract any meaningful data out of those except perhaps for the statement date). There should be a quick way to test a particular importer with a particular downloaded file with zero configuration, even if the output account names are a little wonky. There needs to be clean and readable tracing for what the importers are doing, including a debugging/verbose option. We provide a single function to call as the driver for your own import script. Your configuration is a script / your script is the configuration. You call a function at the end. We will also provide a script that imports a filename and fetches an attribute from it, for those who want a more traditional invocation. We should keep types simples, but use the standard datetime types for dates, decimal.Decimal for numbers, and strings for currencies/commodities. This is obviously based on my current importers code in Beancount. I\u2019m very open to new ideas and suggestions for this project. Collaborations will be most welcome. The more importers we can support, the better. Importers Interface \uf0c1 Each importer should be implemented as a class that derives from this one: class ImporterBase: \"Base class/interface for all source importers.\" # A dict of required configuration variables to their docstring. # This declares the list of options required for the importer # to be provided with, and their meaning. REQUIRED_CONFIG = {} def __init__(self, config): \"\"\"Create an importer. Most concrete implementations can just use this without overriding. Args: config: A dict of configuration accounts, that must match the REQUIRED_CONFIG values. \"\"\" # a dict of Configuration values. This can be accessed publicly. assert isinstance(config, dict) self.config = config # Check that the config has just the required configuration values. if not verify_config(self, config, self.REQUIRED_CONFIG): raise ValueError(\"Invalid config {}, requires {}\".format( config, self.REQUIRED_CONFIG)) def get_filing_account(self): \"\"\"Return the account for moving the input file to. Returns: The name of the account that corresponds to this importer. \"\"\" return self.config['FILE'] def import_file (self, filename): \"\"\"Attempt to import a file. Args: filename: the name of the file to be imported. Returns: A list of new, imported entries extracted from the file. \"\"\" raise NotImplementedError def import_date (self, filename, text_contents): \"\"\"Attempt to obtain a date that corresponds to the given file. Args: filename: the name of the file to extract the date from text_contents: an ASCII text version of the file contents, whatever format it is originally in. Returns: A date object, if successful, or None. \"\"\" raise NotImplementedError For each importer, a detailed explanation of how the original input file on the institution\u2019s website is to be found and downloaded should be provided, to help those find the correct download when adding this importer (some institutions provide a variety of download formats). In addition, a one-line description of the input file support should be provided, so that we can render at runtime a list of the supported file types. References \uf0c1 Other projects with the same goal as importing account data into Ledger are listed here. Ledger\u2019s \u201cconvert\u201d command HLedger with its built-in readers Reckon OFXmate (GUI for ledger-autosync) CSV2Ledger icsv2ledger csv2ledger (seems to lack active maintainers) Update (Nov 2015): This design doc has been implemented and the project is being transitioned back to Beancount. Read the details here .","title":"Ledgerhub Design Doc"},{"location":"ledgerhub_design_doc.html#design-doc-for-ledgerhub","text":"Martin Blais , February 2014 http://furius.ca/beancount/doc/ledgerhub-design-doc Motivation Goals & Stages Details of Stages Fetching Identification Extraction Transform Rendering Filing Implementation Details Importers Interface References Please note that this document is the original design doc for LedgerHub. LedgerHub is being transitioned back to Beancount. See this postmortem document for details [blais, 2015-12].","title":"Design Doc for Ledgerhub"},{"location":"ledgerhub_design_doc.html#motivation","text":"Several open source projects currently exist that provide the capability to create double-entry transactions for bookkeeping from a text file input. These various double-entry bookkeeping projects include Beancount , Ledger , HLedger , Abandon , and they are independent implementations of a similar goal: the creation of an in-memory representation for double-entry accounting transactions from a text file, and the production of various reports from it, such as balance sheets, income statements, journals, and others. Each implementation explores slightly different feature sets, but essentially all work by reading their input from a file whose format is custom declarative language that describe the transactions, a language which is meant to be written by humans and whose syntax is designed with that goal in mind. While the languages do vary somewhat, the underlying data structures that they define are fairly similar. An essential part of the process of regularly updating one\u2019s journal files is the replication of a real-world account\u2019s transaction detail to a single input file in a consistent data format. This is essentially a translation step, meant to bring the transaction details of many institutions\u2019 accounts into a single system. Various banks and credit card companies provide downloadable transaction data in either Quicken or Microsoft Money (OFX) formats, and many institutions provide custom CSV files with transaction detail. Moreover, many of these institutions also make regular statements available for download as PDF files, and these can be associated with one\u2019s ledger accounts. The process of translating these external data formats can be automated to some extent. These various files can be translated to output text that can then be massaged by the user to be integrated into input file formats accepted by a double-entry bookkeeping package. Several projects have begun to make inroads in that domain: Ledger-autosync aims at fetching transactions automatically from OFX servers for and translating them for Ledger and HLedger, and Reckon converts CSV files for Ledger. Beancount includes code that can automate the identification of downloaded files to the accounts from a ledger, extract their transaction detail, and automatically file them to a directory hierarchy that mirrors the ledger\u2019s chart of accounts. This code should probably live outside of Beancount. Ledger also sports a \u201cconvert\u201d command that attempts to do similar things and a CSV2Ledger Perl script is available that can convert CSV files. HLedger also had a convert command which translated CSV files with optional conversion hints defined in a separate file; HLedger now does the same conversion on-the-fly when the input file is CSV (i.e., CSV is considered a first-class input format). The programs that fetch and convert external data files do not have to be tied to a single system. Moreover, this is often cumbersome code that would benefit greatly from having a large number of contributors, which could each benefit each other from having common parsers ready and working for the various institutions that they\u2019re using or likely to use in the future. I - the author of Beancount - have decided to move Beancount\u2019s importing and filing source code outside of its home project and to decouple it from the Beancount source code, so that others can contribute to it, with the intent of providing project-agnostic functionality. This document describes the goals and design of this project.","title":"Motivation"},{"location":"ledgerhub_design_doc.html#goals-stages","text":"This new project should address the following aspects in a project-agnostic manner: Fetching : Automate obtaining the external data files by connecting to the data sources directly. External tools and libraries such as ofxclient for OFX sources can be leveraged for this purpose. Web scraping could be used to fetch downloadable files where possible. The output of this stage is a list of institution-specific files downloaded to a directory. Note that fetching does not just apply to transaction data here; we will also support fetching prices . A list of (date, price) entries may be created from this data. We will likely want to support an intermediate format for expressing a list of positions (and appropriate support in the ledgerhub-Ledger/Beancount/HLedger interface to obtain it). Identification : Given a filename and its contents, automatically guess which institution and account configuration the file is for, and ideally be able to extract the date from the file or statement. This should also work with PDF files. The output of this stage is an association of each input file to a particular extractor and configuration (e.g. a particular account name). Extraction : Parse each file (if possible) and extract a list of information required to generate double-entry transactions data structures from it, in some sort of generic data structure, such as dicts of strings and numbers, independent of the underlying project\u2019s desired output. If possible, a verbatim snippet of the original text that generated the transaction should be attached to the output data structure. The output of this stage is a data structure, e.g., a list of Python dictionaries in some defined format. Transform : Given some information from the past transaction history contained in a journal, using simple learning algorithms, a program should be able to apply transformations on the transactional information extracted from the previous step. The most common use case for this is to automatically add a categorization posting to transactions that have a single posting only. For example, transactions from credit card statements typically include the changes in balance of the credit card account but all transactions are left to be manually associated with a particular expense account. Some of this process can be automated at this stage. Rendering : Convert the internal transactions data structures to the particular syntax of a double-entry bookkeeping project implementation and to the particular desired syntax variants (e.g. currency formatting, comma vs. dot decimal separator, localized input date format). This steps spits out text to be inserted into an input file compatible with the ledger software of choice. Filing : Sanitize the downloaded files\u2019 filenames and move them into a well organized and structured directory hierarchy corresponding to the identified account. This can run from the same associations derived in the identification step. Apart from the Render stage, all the other stages should be implemented without regard for a particular project, this should work across all ledger implementations. The Rendering code, however, should specialize, import source code, and attempt to add as many of the particular features provided by each project to its output text. Where necessary, interfaces to obtain particular data sets from each ledger implementation\u2019s input files should be provided to shield the common code from the particular implementation details of that project. For instance, a categorization Transform step would need to train its algorithm on some of the transaction data (i.e., the narration fields and perhaps some of the amounts, account names, and dates). Each project should provide a way to obtain the necessary data from its input data file, in the same format.","title":"Goals & Stages"},{"location":"ledgerhub_design_doc.html#details-of-stages","text":"","title":"Details of Stages"},{"location":"ledgerhub_design_doc.html#fetching","text":"By default, a user should be able to click their way to their institution\u2019s website and download documents to their ~/Downloads directory. A directory with some files in it should be the reasonable default input to the identification stage. This directory should be allowed to have other/garbage files in it, the identification step should be able to skip those automatically. A module that can automatically fetch the data needs to be implemented. Ideally this would not require an external tool. The data extracted should also have a copy saved in some Downloads directory. This is the domain of the ledger-autosync project. Perhaps we should coordinate input/outputs or even integrate call some of its library code at this stage. The author notes that fetching data from OFX servers is pretty easy, though the begin/end dates will have to get processed and filtered. Automatic fetching support will vary widely depending on where the institutions are located. Some places have solid support, some less. Use the data from ofxhome.com to configure.","title":"Fetching"},{"location":"ledgerhub_design_doc.html#fetching-prices","text":"For fetching prices, there are many libraries out there. Initially we will port Beancount\u2019s bean-prices to ledgerhub.","title":"Fetching Prices"},{"location":"ledgerhub_design_doc.html#identification","text":"The identification stage consists in running a driver program that Searches for files in a directory hierarchy (typically your ~/Downloads folder) If necessary, converts the files into some text/ascii format, so that regular expressions can be matched against it (even if the output is messy, e.g., with PDF files converted to ASCII). This works well for PDF files: despite the fact that we cannot typically extract transactional data from them, we can generally pretty reliably identify which account they\u2019re for and almost always extract the statement date as well. Check a list of regular expressions against the ASCII\u2019fied contents. If the regular expressions all match, the configuration is associated to the filename. Note that more than one configuration may be associated to the same file because some files contain many sections, sections for which different importers may be called on to extract their data (e.g., OFX banking + OFX credit card can be mixed in the same file, and some institutions do). The net result of this process is an association of each filename with the a specific importer object instantiated in the configuration file. These importer objects are created with a set of required account names which they use to produce the Ledger-like syntax from the downloaded file that was associated with it. Here is an example configuration for two importers: from ledgerhub.sources.rbc import rbcinvesting, rbcpdf CONFIG = [ ... (('FileType: application/vnd.ms-excel', r'Filename: .*Activity-123456789-', ), rbcinvesting.Importer({ 'FILE' : 'Assets:CA:RBC-Investing:Taxable', 'cash' : 'Assets:CA:RBC-Investing:Taxable:Cash', 'positions' : 'Assets:CA:RBC-Investing:Taxable', 'interest' : 'Income:CA:RBC-Investing:Taxable:Interest', 'dividend' : 'Income:CA:RBC-Investing:Taxable:Dividends', 'fees' : 'Expenses:Financial:Fees', 'commission' : 'Expenses:Financial:Commissions', 'transfer' : 'Assets:CA:RBC:Checking', })), (('FileType: application/pdf', 'Filename:.*/123456789-\\d\\d\\d\\d[A-Z][a-z][a-z]\\d\\d-\\d\\d\\d\\d[A-Z][a-z][a-z]\\d\\d.pdf'), rbcpdf.Importer({ 'FILE': 'Assets:CA:RBC-Investing:RRSP', })), The configuration consists in a list, for each possible importer, of a pair of 1) a list of regular expressions which all should match against a \u201cmatch text\u201d, which is a \u201ctextified\u201d version of the contents of a file to be imported, and 2) an importer object, configured with a specific set of accounts to use for producing transactions. Each importer requires a particular set of output accounts which it uses to create its transactions and postings. The ledger\u2019s filename, and a list of these (regexps, importer) pairs is all that is necessary for the driver to carry out all of its work. The textification consists in a simple and imperfect conversion of downloaded file that are in binary format to something that we can run regular expressions against. For an OFX file or CSV file there is no conversion required for textification, we can just match against the text contents of those files; for an Excel/XLS file, we need to convert that to a CSV file, which can then be searched; for a PDF file, a number of different pdf-to-text converters are attempted until one succeeds (the tools for this are notoriously unreliable, so we have to try various ones). Note that this converted \"match text\" is only created temporarily and only for the purpose of identification; the importer will get the original binary file to do its work. It is not entirely clear whether the regular expressions can be standardized to avoid having the user configure them manually. In practice, I have found it often necessary, or at least very convenient, to place an account id in my import configuration. It is true that configuring each of the possible downloads can be a hassle that requires the user to do a bit of guesswork while looking at the contents of each file, but this has been much more reliable in practice than attempts at normalizing this process, likely because it is a much easier problem to uniquely distinguish between all the files of a particular user than to distinguish between all the types of files. Using an account id in one of the regular expressions is the easy way to do that, and it works well. This also provides a clear place to attach the list of accounts to a particular importer, something that necessarily requires user input anyway.","title":"Identification"},{"location":"ledgerhub_design_doc.html#extraction","text":"Once the association is made, we run the importers on each of the files. Some data structure is produced. The importers each do what they do - this is where the ugly tricks go. Ideally, we should build a library of common utilities to help parsing similar file types. Though each of the importer modules should be pretty much independent, some common functionality can be imagined, for example, how one deals with different stocks held in a single investment account, could be configured outside of each importer (e.g., preferred method could be to create a subaccount of that account, with the symbol of the stock, or otherwise). Note [AMaffei]: This could output a generic and well-defined CSV file format if you want to have the option of running the various steps as separate UNIX-style tools and/or process the intermediate files with regular text processing tools.","title":"Extraction"},{"location":"ledgerhub_design_doc.html#transform","text":"Some transformations should be independent of importers. In particular, automatically categorizing incomplete transactions is not dependent on which importer created the transaction. I\u2019d like to keep this step as general as possible so that other embellishment steps can be inserted here in the future. Right now, I can only think of the following uses: Auto-categorization of transactions with only a single leg Detection of duplicate transactions: imported files often contain transactions which are already in the ledger; those should be either ignored or marked as such. In practice, this is not as easy as it sounds, because a straightforward date + narration comparison will fail: if the same transaction comes from two input data files, one side always ends up getting merged to the other, and sometimes even the date differs a bit. Some amount of fuzzy matching is required. Normalization of payee names: the imported names of payees are often cut short or include some irrelevant words, such as \u201cLLC\u201d, city names, and/or number codes. It may be desirable to somehow clean those up automatically. This step involves a bootstrapping phase, where we will extract some data from the actual ledger that the transactions are meant to be imported into. We will implement a generic interface that should allow each ledger language implementation to provide relevant data for training. The output data here should be in the same format as its input, so that we can optionally skip this phase.","title":"Transform"},{"location":"ledgerhub_design_doc.html#rendering","text":"An output renderer should be selected by the driver. This is where we convert the extracted data structures to the particular flavor of ledger implementation you\u2019re using. Each of the renderer implementations should be free to import modules from its particular implementation, and we should be careful to constraint these import dependencies to only these modules, to make sure that only a single ledger implementation is required in order for the code to run. Options for rendering style could be defined here, for each renderer, because each of the languages have particularies. [AMaffei] Also, it should be possible to provide a generic renderer that takes printf-style format strings to output in any desired format.","title":"Rendering"},{"location":"ledgerhub_design_doc.html#filing","text":"Importers should be able to look at the textified contents of the files and find the file/statement date. This is useful, because we can rename the file by prepending the date of the statement, and the date at which we download the statement or transaction files is rarely the same date at which it was generated. In the case where we are not able to extract a date from the file, we fall back on the filename\u2019s last modified time. A target directory should be provided and we should move each file to the account with which it is associated. For example, a file like this: ~/Downloads/ofx32755.qbo should be moved to a directory .../Assets/US/RBC/Checking/2013-11-27.ofx32755.qbo if it is associated by the identification step with an importer for the Assets:US:RBC:Checking account. For this purpose, all the importers should have a required \u201cfiling\u201d account associated with them. As far as I know only Beancount implements this at the moment, but I suspect this convenient mechanism of organizing and preserving your imported files will be found useful by others. Given a list of directories, Beancount automatically finds those files and using the date in the filename, is able to render links to the files as line items in the journal web pages, and serve their contents when the user clicks on the links. Even without this capability, it can be used to maintain a cache of your documents (I maintain mine in a repository which I sync to an external drive for backup).","title":"Filing"},{"location":"ledgerhub_design_doc.html#implementation-details","text":"Notes about the initial implementation: The implementation of this project will be carried out in Python3. Why Python? The performance of importing and extracting is largely irrelevant, a dynamic language works well for this type of task Parsing in a dynamic language works great, there are many available libraries Python3 is now widely distributed and all desired parsing libraries are commonly available for it at this point All modules should be tested, including testing with sample input. If you want to add a new module, you should need to provide an anonymized sample file for it. We will have to have an automated test suite, because past experience has shown this type of code to be quite brittle and fragile to new and unexpected inputs. It\u2019s easy to write, but it\u2019s also easy to break. In order to test binary files that cannot be anonymized, we will provide the ability to test from match-text instead of from original binary statement PDF. Those files are generally not extractable anyhow and are only there for identification and filing (e.g. a PDF statement, we can\u2019t extract any meaningful data out of those except perhaps for the statement date). There should be a quick way to test a particular importer with a particular downloaded file with zero configuration, even if the output account names are a little wonky. There needs to be clean and readable tracing for what the importers are doing, including a debugging/verbose option. We provide a single function to call as the driver for your own import script. Your configuration is a script / your script is the configuration. You call a function at the end. We will also provide a script that imports a filename and fetches an attribute from it, for those who want a more traditional invocation. We should keep types simples, but use the standard datetime types for dates, decimal.Decimal for numbers, and strings for currencies/commodities. This is obviously based on my current importers code in Beancount. I\u2019m very open to new ideas and suggestions for this project. Collaborations will be most welcome. The more importers we can support, the better.","title":"Implementation Details"},{"location":"ledgerhub_design_doc.html#importers-interface","text":"Each importer should be implemented as a class that derives from this one: class ImporterBase: \"Base class/interface for all source importers.\" # A dict of required configuration variables to their docstring. # This declares the list of options required for the importer # to be provided with, and their meaning. REQUIRED_CONFIG = {} def __init__(self, config): \"\"\"Create an importer. Most concrete implementations can just use this without overriding. Args: config: A dict of configuration accounts, that must match the REQUIRED_CONFIG values. \"\"\" # a dict of Configuration values. This can be accessed publicly. assert isinstance(config, dict) self.config = config # Check that the config has just the required configuration values. if not verify_config(self, config, self.REQUIRED_CONFIG): raise ValueError(\"Invalid config {}, requires {}\".format( config, self.REQUIRED_CONFIG)) def get_filing_account(self): \"\"\"Return the account for moving the input file to. Returns: The name of the account that corresponds to this importer. \"\"\" return self.config['FILE'] def import_file (self, filename): \"\"\"Attempt to import a file. Args: filename: the name of the file to be imported. Returns: A list of new, imported entries extracted from the file. \"\"\" raise NotImplementedError def import_date (self, filename, text_contents): \"\"\"Attempt to obtain a date that corresponds to the given file. Args: filename: the name of the file to extract the date from text_contents: an ASCII text version of the file contents, whatever format it is originally in. Returns: A date object, if successful, or None. \"\"\" raise NotImplementedError For each importer, a detailed explanation of how the original input file on the institution\u2019s website is to be found and downloaded should be provided, to help those find the correct download when adding this importer (some institutions provide a variety of download formats). In addition, a one-line description of the input file support should be provided, so that we can render at runtime a list of the supported file types.","title":"Importers Interface"},{"location":"ledgerhub_design_doc.html#references","text":"Other projects with the same goal as importing account data into Ledger are listed here. Ledger\u2019s \u201cconvert\u201d command HLedger with its built-in readers Reckon OFXmate (GUI for ledger-autosync) CSV2Ledger icsv2ledger csv2ledger (seems to lack active maintainers) Update (Nov 2015): This design doc has been implemented and the project is being transitioned back to Beancount. Read the details here .","title":"References"},{"location":"precision_tolerances.html","text":"Beancount Precision & Tolerances \uf0c1 Martin Blais , May 2015 http://furius.ca/beancount/doc/tolerances This document describes how Beancount handles the limited precision of numbers in transaction balance checks and balance assertions. It also documents rounding that may occur in inferring numbers automatically. Motivation \uf0c1 Beancount automatically enforces that the amounts on the Postings of Transactions entered in an input file sum up to zero. In order for Beancount to verify this in a realistic way, it must tolerate a small amount of imprecision. This is because Beancount lets you replicate what happens in real world account transactions , and in the real world, institutions round amounts up or down for practical reasons. Here\u2019s an example: Consider the following transaction which consists in a transfer between two accounts denominated in different currencies (US dollars and Euros): 2015-05-01 * \"Transfer from secret Swiss bank account\" Assets:CH:SBS:Checking -9000.00 CHF Assets:US:BofA:Checking 9643.82 USD @ 0.93324 CHF In this example, the exchange rate used was 0.93324 USD/CHF, that is, 0.93324 Swiss Francs per US dollar. This rate was quoted to 5 digits of precision by the bank. A full-precision conversion of 9000.00 CHF / 0.93324 CHF yields 9643.82152501... USD. Similarly, converting the US dollars to Francs using the given rate yields an imprecise result as well: 9643.82 x 0.93324 = 8999.9985768\u2026 . Here is another example where this type of rounding may occur: A transaction for a fractional number of shares of a mutual fund: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.22626 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384.61 USD Once again, rounding occurs in this transaction: not only the Net Asset Value of the fund is rounded to its nearest penny value ($37.61), but the number of units is also rounded and accounted for by Vanguard with a fixed number of digits (10.22626 units of VPMBX). And the balance of the entire transaction needs to tolerate some imprecision, whether you compute the value of the shares (10.22626 x $37.61 = $384.6096386 ) or whether you compute the number of shares from the desired dollar amount of the contribution ($384.61 / $37.61 = 10.2262696091 ). From Beancount\u2019s point-of-view, both of the examples above are balancing transactions. Clearly, if we are to try to represent and reproduce the transactions of external accounts to our input file, there needs to be some tolerance in the balance verification algorithm. How Precision is Determined \uf0c1 Beancount attempts to derive the precision from each transaction automatically , from the input, for each Transaction in isolation 1 . Let us inspect our last example again: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.22626 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384.61 USD In this transaction, Beancount will infer the tolerance of RGAGX at 5 fractional digits, that is, 0.000005 RGAGX , and USD at 2 fractional digits, that is, 0.005 USD . Note that the tolerance used is half of the last digit of precision provided by the user. This is entirely inferred from the input, without having to fetch any global tolerance declaration. Also note how the precision is calculated separately for each currency . Observe that although we are inferring a tolerance for units of RGAGX, it is actually not used in the balancing of this transaction, because the \u201cweight\u201d of the first posting is in USD (10.22626 x 37.61 = 384.6096386 USD). So what happens here? The weights of each postings are calculated: 384.6096386 USD for the first posting -384.61 USD for the second These are summed together, by currency (there is only USD in the weights of this transaction) which results in a residual value of -0.0003614 USD. This value is compared to the tolerance for units of USD: |-0.0003614| < 0.005, and this transaction balances. Prices and Costs \uf0c1 For the purpose of inferring the tolerance to be used, the price and cost amounts declared on a transaction\u2019s Postings are ignored . This makes sense if you consider that these are usually specified at a higher precision than the base amounts of the postings\u2014and sometimes this extra precision is necessary to make the transaction balance. These should not be used in setting the precision of the whole transaction. For example, in the following transaction: 1999-09-30 * \"Vest ESPP - Bought at discount: 18.5980 USD\" Assets:US:Schwab:ESPP 54 HOOL {21.8800 USD} Income:CA:ESPP:PayContrib -1467.84 CAD @ 0.6842 USD Income:CA:ESPP:Discount -259.03 CAD @ 0.6842 USD The only tolerance inferred here is 0.005 for CAD. (54 HOOL does not yield anything in this case because it is integral; the next section explains this). There is no tolerance inferred for USD, neither from the cost from the first posting (21.8800 USD), nor from the prices of the remaining postings (0.6842 USD). Integer Amounts \uf0c1 For integer amounts in the input, the precision is not inferred to 0.5, that is, this should fail to balance: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.21005 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384 USD In other words, integer amounts do not contribute a number of digits to the determination of the tolerance for their currency. By default, the tolerance used on amounts without an inferred precision is zero . So in this example, because we cannot infer the precision of USD (recall that the cost is ignored), this transaction will fail to balance, because its residual is non-zero (|-0.0003614| > 0). You can customize what the default tolerance should be for each currency separately and for any currency as well (see section below on how to do this). This treatment of integer amounts implies that the maximum amount of precision that one can specify just by inputting numbers is 0.05 units of the currency, for example, by providing a number such as 10.7 as input 2 . On the other hand, the settings for the default tolerance to use allows specifying arbitrary numbers. Resolving Ambiguities \uf0c1 A case that presents itself rarely is one where multiple different precisions are being input for the same currency. In this case, the largest (coarsest) of the inferred input tolerances is used. For example, if we wanted to track income to more than pennies, we might write this: 1999-08-20 * \"Sell\" Assets:US:BRS:ESPP -81 HOOL {26.3125 USD} Assets:US:BRS:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:CA:ESPP:PnL -10.125 USD The amounts we have for USD in this case are 2141.36, 0.08 and -10.125, which infer tolerances of either 0.005 or 0.0005. We select the coarsest amount: this transaction tolerates an imprecision of 0.005 USD. Default Tolerances \uf0c1 When a transaction\u2019s numbers do not provide enough information to infer a tolerance locally , we fall back to some default tolerance value. As seen in previous examples, this may occur either because (a) the numbers associated with the currency we need it for are integral, or (b) sufficient numbers are simply absent from the input. By default, this default tolerance is zero for all currencies. This can be specified with an option, like this: option \"inferred_tolerance_default\" \"*:0.001\" The default tolerance can be further refined for each currency involved, by providing the currency to the option, like this: option \"inferred_tolerance_default\" \"USD:0.003\" If provided, the currency-specific tolerance will be used over the global value. The general form for this option is: option \"inferred_tolerance_default\" \":\" Just to be clear: this option is only used when the tolerance cannot be inferred. If you have overly large rounding errors and the numbers in your transactions do infer some tolerance value, this value will be ignored (e.g., setting it to a larger number to try to address that fix will not work). If you need to loosen up the tolerance, see the \u201c inferred_tolerance_multiplier \u201d in the next section. (Note: I\u2019ve been considering dedicating a special meta-data field to the Commodity directive for this, but this would break from the invariant that meta-data is only there to be used by users and plugins, so I\u2019ve refrained so far.) Tolerance Multiplier \uf0c1 We\u2019re shown previously that when the tolerance value isn\u2019t provided explicitly, that it is inferred from the numbers on the postings. By default, the smallest digit found on those numbers is divided by half to obtain the tolerance because we assume that the institutions which we\u2019re reproducing the transactions apply rounding and so the error should never be more than half. But in reality, you may find that the rounding errors sometime exceed this value. For this reason, we provide an option to set the multiplier for the inferred tolerance: option \"inferred_tolerance_multiplier\" \"1.2\" This value overrides the default multiplier. In this example, for a transaction with postings only with values such as 24.45 CHF, the inferred tolerance for CHF would be +/- 0.012 CHF. Inferring Tolerances from Cost \uf0c1 There is also a feature that expands the maximum tolerance inferred on transactions to include values on cost currencies inferred by postings held at-cost or converted at price. Those postings can imply a tolerance value by multiplying the smallest digit of the unit by the cost or price value and taking half of that value. For example, if a posting has an amount of \"2.345 RGAGX {45.00 USD}\" attached to it, it implies a tolerance of 0.001 x 45.00 / 2 = 0.045 USD and the sum of all such possible rounding errors is calculate for all postings held at cost or converted from a price, and the resulting tolerance is added to the list of candidates used to figure out the tolerance we should use for the given commodity (we use the maximum value of all the inferred tolerances). You turn on the feature like this: option \"infer_tolerance_from_cost\" \"TRUE\" Enabling this flag only makes the tolerances potentially wider, never smaller. Balance Assertions & Padding \uf0c1 There are a few other places where approximate comparisons are needed. Balance assertions also compare two numbers: 2015-05-08 balance Assets:Investments:RGAGX 4.271 RGAGX This asserts that the accumulated balance for this account has 4.271 units of RGAGX, plus or minus 0.001 RGAGX. So accumulated values of 4.270 RGAGX up to 4.272 RGAGX will check as asserted. The tolerance is inferred automatically to be 1 unit of the least significant digit of the number on the balance assertion. If you wanted a looser assertion, you could have declared: 2015-05-08 balance Assets:Investments:RGAGX 4.27 RGAGX This assertion would accept values from 4.26 RGAGX to 4.28 RGAGX. Note that the inferred tolerances are also expanded by the inferred tolerance multiplier discussed above. Tolerances that Trigger Padding \uf0c1 Pad directives automatically insert transactions to bring account balances in-line with a subsequent balance assertion. The insertion only triggers if the balance differs from the expected value, and the tolerance for this to occur behaves exactly the same as for balance assertions. Explicit Tolerances on Balance Assertions \uf0c1 Beancount supports the specification of an explicit tolerance amount, like this: 2015-05-08 balance Assets:Investments:RGAGX 4.271 ~ 0.01 RGAGX This feature was added because of some observed peculiarities in Vanguard investment accounts whereby rounding appears to follow odd rules and balances don\u2019t match. Saving Rounding Error \uf0c1 As we saw previously, transactions don\u2019t have to balance exactly, they allow for a small amount of imprecision. This bothers some people. If you would like to track and measure the residual amounts allowed by the tolerances, Beancount offers an option to automatically insert postings that will make each transaction balance exactly. You enable the feature like this: option \"account_rounding\" \"Equity:RoundingError\" This tells Beancount to insert postings to compensate for the rounding error to an \u201c Equity:RoundingError \u201d account. For example, with the feature enabled, the following transaction: 2013-02-23 * \"Buying something\" Assets:Invest 1.245 RGAGX {43.23 USD} Assets:Cash -53.82 USD will be automatically transformed into this: 2013-02-23 * \"Buying something\" Assets:Invest 1.245 RGAGX {43.23 USD} Assets:Cash -53.82 USD Equity:RoundingError -0.00135 USD You can verify that this transaction balances exactly. If the transaction already balances exactly (this is the case for most transactions) no posting is inserted. Finally, if you require that all accounts be opened explicitly, you should remember to declare the rounding account in your file at an appropriate date, like this: 2000-01-01 open Equity:RoundingError Precision of Inferred Numbers \uf0c1 Beancount is able to infer some missing numbers in the input. For example, the second posting in this transaction is \u201cinterpolated\u201d automatically by Beancount: 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash The calculated amount to be inserted from the first posting is -227.2067 USD. Now, you might ask, to which precision is it inserted at? Does it insert 227.2067 USD at the full precision or does the number get rounded to a penny, e.g. 227.21 USD? It depends on the tolerance inferred for that currency. In this example, no tolerance is able to get inferred (there is no USD amount provided other than the cost amount, which is ignored for the purpose of inferring the tolerance), so we have to defer to the default tolerance. If the default tolerance is not overridden in the input file\u2014and therefore is zero\u2014the full precision will be used; no rounding occurs. This will result in the following transaction: 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.2067 USD Note that if a tolerance could be inferred from other numbers on that transaction, it would be used for rounding, such as in this example where the Cash posting is rounded to two digits because of the 9.95 USD number on the Commissions posting: 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Expenses:Commissions 9.95 USD Assets:Investments:Cash -237.16 USD However, if no inference is possible, and the default tolerance for USD is set to 0.001, the number will be quantized to 0.001 before insertion, that is, 227.207 USD will be stored: option \"default_tolerance\" \"USD:0.001\" 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.207 USD Finally, if you enabled the accumulation of rounding error, the posting\u2019s amount will reflect the correct residual, taking into account the rounded amount that was automatically inserted: option \"default_tolerance\" \"USD:0.01\" option \"account_rounding\" \"Equity:RoundingError\" 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.207 USD Equity:RoundingError 0.0003 USD Porting Existing Input \uf0c1 The inference of tolerance values from the transaction\u2019s numbers is generally good enough to keep existing files working without changes. There may be new errors appearing in older files once we process them with the method described in this document, but they should either point to previously undetected errors in the input, or be fixable with simple addition of a suitable number of digits. As a testimony, porting the author\u2019s very large input file has been a relatively painless process that took less than 1 hour. In order to ease the transition, you will probably want to change the default tolerance for all currencies to match the previous value that Beancount had been using, like this: option \"inferred_tolerance_default\" \"*:0.005\" I would recommend you start with this and fix all errors in your file, then proceed to removing this and fix the rest of errors. This should make it easier to adapt your file to this new behavior. As an example of how to fix a new error\u2026 converting this newly failing transaction from the Integer Amounts section: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.21005 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384 USD by inserting zero\u2019s to provide a locally inferred value like this: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.21005 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384.00 USD is sufficient to silence the balance check. Representational Issues \uf0c1 Internally, Beancount uses a decimal number representation (not a binary/float representation, neither rational numbers). Calculations that result in a large number of fractional digits are carried out to 28 decimal places (the default precision from the context of Python\u2019s IEEE decimal implementation). This is plenty sufficient, because the method we propose above rarely trickles these types of numbers throughout the system: the tolerances allows us to post the precise amounts declared by users, and only automatically derived prices and costs will possibly result in precisions calculated to an unrealistic number of digits that could creep into aggregations in the rest of the system. References \uf0c1 The original proposal that led to this implementation can be found here . In particular, the proposal highlights on the other systems have attempted to deal with this issue. There are also some discussions on the mailing-list dedicated to this topic. Note that for the longest time, Beancount used a fixed precision of 0.005 across all currencies. This was eliminated once the method described in this document was implemented. Also, for Balance and Pad directives, there used to be a \u201ctolerance\u201d option that was set by default to 0.015 of any units. This option has been deprecated with the merging of the changes described in this document. Historical Notes \uf0c1 Here\u2019s an overview of the status of numbers rendering in Beancount as of March 2016, from the mailing-list : First, it's important to realize how these numbers are represented in memory. They are using the Decimal representation which beyond being able to accurately representing decimal numbers (as opposed to the approximation that binary floats provides) also contains a specific precision. That is, the number 2.00 is represented differently than the numbers 2.0 and 2.000. The numbers \"remember\" which precision they are represented up to. This is important. When I say rendering the numbers to their \"natural precision\" I mean the precision with which they are represented, i.e., 2.0 renders as \"2.0\", 2.000 renders as \"2.000\". Then, there are two DISTINCT topics: (1) tolerances, and (2) precision. \"Tolerances\" are values used to determine how much imprecision is acceptable in balancing transactions. This is used in the verification stage, to determine how much looseness to allow. It should not affect how numbers are rendered. \"Precision\" is perhaps a bit of a misnomer: By that I'm referring to is how many digits the numbers are to be rendered with. Once upon a time - after the shell was already written - these concepts weren't well defined in Beancount and I wasn't dealing with these things consistently. At some point it became clear what I needed to do and I created a class called \"DisplayContext\" which could contain appropriate settings for rendering the precision of numbers for each currency (each currency tends to have its own most common rendering precision, e.g. two digits for USD, one digit for MXN, no digits for JPY and in reports we're typically fine rounding the actual numbers to that precision). So an instance of this DisplayContext is automatically instantiated in the parser and in order to avoid the user having to set these values manually - for Beancount to \"do the right thing\" by default - it is able to accumulate the numbers seen and to deduce the most common and maximum number of digits used from the input, and to use that as the default number of digits for rendering numbers. The most common format/number of digits is used to render the number of units, and the maximum number of digits seen is used to render costs and prices. In addition, this class also has capabilities for aligning to the decimal dot and to insert commas on thousands as well. It separates the control of the formatting from the numbers themselves. MOST of the code that renders numbers uses the DisplayContext (via the to_string() methods) to convert the numbers into strings, such as the web interface and explicit text reports. But NOT ALL... there's a bit of HISTORY here... the SQL shell uses some old special-purpose code to render numbers that I never bothered to convert to the DisplayContext class. There's a TODO item for it. It needs to get converted at some point, but I've neglected doing this so far because I have much bigger plans for the SQL query engine that involve a full rewrite of it with many improvements and I figured I'd do that then. If you recall, the SQL query engine was a prototype, and actually it works, but it is not well covered by unit tests. My purpose with it was to discover through usage what would be useful and to then write a v2 of it that would be much better. Now, about that PRINT command... this is not intended as a reporting tool. The printer's purpose is to print input that accurately represents the content of the transactions. In order to do this, it needs to render the numbers at their \"natural\" precision, so that when they get read back in, they parse into the very same number, that is, with the same number of digits (even if zeros). For this reason, the PRINT command does not attempt to render using the DisplayContext instance derived from the input file - this is on purpose. I could change that, but then round-trip would break: the rounding resulting from formatting using the display context may output transactions which don't balance anymore. As you can see, it's not an obvious topic... Hopefully this should allow you to understand what comes out of Beancount in terms of the precision of the numbers it renders. Note: \"default_tolerances\" has been renamed to \"inferred_tolerance_default\" recently because the name was too general and confusing. Old name will work but generate a warning. I just noticed from your comments and some grepping around that the \"render_commas\" option is not used anymore. I'm not sure how that happened, but I'll go ad fix that right away and set the default value of the DisplayContext derived from the input. I should probably also convert the SQL shell rendering to use the display context regardless of future plans, so that it renders consistently with all the rest. Not sure I can do that this weekend, but I'll log a ticket, here . I hope this helps. You're welcome to ask questions if the above isn't clear. I'm sorry if this isn't entirely obvious... there's been a fair bit of history there and there's a lot of code. I should review the naming of options, I think the tolerance options all have \"tolerance\" in their name, but there aren't options to override the rendering and when I add them they should all have a common name as well. Further Reading \uf0c1 What Every Computer Scientist Should Know About Floating-Point Arithmetic This stands in contrast to Ledger which attempts to infer the precision based on other transactions recently parsed in the file, in file order. This has the unfortunate effect of creating \u201ccross-talk\u201d between the transactions in terms of what precision can be used. \u21a9 Note that due to the way Beancount represents numbers internally, it is also not able to distinguish between \u201c230\u201d and \u201c230.\u201d; these parse into the same representation for Beancount. Therefore, we are not able to use that distinction in the input to support a precision of 0.5. \u21a9","title":"Precision Tolerances"},{"location":"precision_tolerances.html#beancount-precision-tolerances","text":"Martin Blais , May 2015 http://furius.ca/beancount/doc/tolerances This document describes how Beancount handles the limited precision of numbers in transaction balance checks and balance assertions. It also documents rounding that may occur in inferring numbers automatically.","title":"Beancount Precision & Tolerances"},{"location":"precision_tolerances.html#motivation","text":"Beancount automatically enforces that the amounts on the Postings of Transactions entered in an input file sum up to zero. In order for Beancount to verify this in a realistic way, it must tolerate a small amount of imprecision. This is because Beancount lets you replicate what happens in real world account transactions , and in the real world, institutions round amounts up or down for practical reasons. Here\u2019s an example: Consider the following transaction which consists in a transfer between two accounts denominated in different currencies (US dollars and Euros): 2015-05-01 * \"Transfer from secret Swiss bank account\" Assets:CH:SBS:Checking -9000.00 CHF Assets:US:BofA:Checking 9643.82 USD @ 0.93324 CHF In this example, the exchange rate used was 0.93324 USD/CHF, that is, 0.93324 Swiss Francs per US dollar. This rate was quoted to 5 digits of precision by the bank. A full-precision conversion of 9000.00 CHF / 0.93324 CHF yields 9643.82152501... USD. Similarly, converting the US dollars to Francs using the given rate yields an imprecise result as well: 9643.82 x 0.93324 = 8999.9985768\u2026 . Here is another example where this type of rounding may occur: A transaction for a fractional number of shares of a mutual fund: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.22626 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384.61 USD Once again, rounding occurs in this transaction: not only the Net Asset Value of the fund is rounded to its nearest penny value ($37.61), but the number of units is also rounded and accounted for by Vanguard with a fixed number of digits (10.22626 units of VPMBX). And the balance of the entire transaction needs to tolerate some imprecision, whether you compute the value of the shares (10.22626 x $37.61 = $384.6096386 ) or whether you compute the number of shares from the desired dollar amount of the contribution ($384.61 / $37.61 = 10.2262696091 ). From Beancount\u2019s point-of-view, both of the examples above are balancing transactions. Clearly, if we are to try to represent and reproduce the transactions of external accounts to our input file, there needs to be some tolerance in the balance verification algorithm.","title":"Motivation"},{"location":"precision_tolerances.html#how-precision-is-determined","text":"Beancount attempts to derive the precision from each transaction automatically , from the input, for each Transaction in isolation 1 . Let us inspect our last example again: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.22626 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384.61 USD In this transaction, Beancount will infer the tolerance of RGAGX at 5 fractional digits, that is, 0.000005 RGAGX , and USD at 2 fractional digits, that is, 0.005 USD . Note that the tolerance used is half of the last digit of precision provided by the user. This is entirely inferred from the input, without having to fetch any global tolerance declaration. Also note how the precision is calculated separately for each currency . Observe that although we are inferring a tolerance for units of RGAGX, it is actually not used in the balancing of this transaction, because the \u201cweight\u201d of the first posting is in USD (10.22626 x 37.61 = 384.6096386 USD). So what happens here? The weights of each postings are calculated: 384.6096386 USD for the first posting -384.61 USD for the second These are summed together, by currency (there is only USD in the weights of this transaction) which results in a residual value of -0.0003614 USD. This value is compared to the tolerance for units of USD: |-0.0003614| < 0.005, and this transaction balances.","title":"How Precision is Determined"},{"location":"precision_tolerances.html#prices-and-costs","text":"For the purpose of inferring the tolerance to be used, the price and cost amounts declared on a transaction\u2019s Postings are ignored . This makes sense if you consider that these are usually specified at a higher precision than the base amounts of the postings\u2014and sometimes this extra precision is necessary to make the transaction balance. These should not be used in setting the precision of the whole transaction. For example, in the following transaction: 1999-09-30 * \"Vest ESPP - Bought at discount: 18.5980 USD\" Assets:US:Schwab:ESPP 54 HOOL {21.8800 USD} Income:CA:ESPP:PayContrib -1467.84 CAD @ 0.6842 USD Income:CA:ESPP:Discount -259.03 CAD @ 0.6842 USD The only tolerance inferred here is 0.005 for CAD. (54 HOOL does not yield anything in this case because it is integral; the next section explains this). There is no tolerance inferred for USD, neither from the cost from the first posting (21.8800 USD), nor from the prices of the remaining postings (0.6842 USD).","title":"Prices and Costs"},{"location":"precision_tolerances.html#integer-amounts","text":"For integer amounts in the input, the precision is not inferred to 0.5, that is, this should fail to balance: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.21005 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384 USD In other words, integer amounts do not contribute a number of digits to the determination of the tolerance for their currency. By default, the tolerance used on amounts without an inferred precision is zero . So in this example, because we cannot infer the precision of USD (recall that the cost is ignored), this transaction will fail to balance, because its residual is non-zero (|-0.0003614| > 0). You can customize what the default tolerance should be for each currency separately and for any currency as well (see section below on how to do this). This treatment of integer amounts implies that the maximum amount of precision that one can specify just by inputting numbers is 0.05 units of the currency, for example, by providing a number such as 10.7 as input 2 . On the other hand, the settings for the default tolerance to use allows specifying arbitrary numbers.","title":"Integer Amounts"},{"location":"precision_tolerances.html#resolving-ambiguities","text":"A case that presents itself rarely is one where multiple different precisions are being input for the same currency. In this case, the largest (coarsest) of the inferred input tolerances is used. For example, if we wanted to track income to more than pennies, we might write this: 1999-08-20 * \"Sell\" Assets:US:BRS:ESPP -81 HOOL {26.3125 USD} Assets:US:BRS:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:CA:ESPP:PnL -10.125 USD The amounts we have for USD in this case are 2141.36, 0.08 and -10.125, which infer tolerances of either 0.005 or 0.0005. We select the coarsest amount: this transaction tolerates an imprecision of 0.005 USD.","title":"Resolving Ambiguities"},{"location":"precision_tolerances.html#default-tolerances","text":"When a transaction\u2019s numbers do not provide enough information to infer a tolerance locally , we fall back to some default tolerance value. As seen in previous examples, this may occur either because (a) the numbers associated with the currency we need it for are integral, or (b) sufficient numbers are simply absent from the input. By default, this default tolerance is zero for all currencies. This can be specified with an option, like this: option \"inferred_tolerance_default\" \"*:0.001\" The default tolerance can be further refined for each currency involved, by providing the currency to the option, like this: option \"inferred_tolerance_default\" \"USD:0.003\" If provided, the currency-specific tolerance will be used over the global value. The general form for this option is: option \"inferred_tolerance_default\" \":\" Just to be clear: this option is only used when the tolerance cannot be inferred. If you have overly large rounding errors and the numbers in your transactions do infer some tolerance value, this value will be ignored (e.g., setting it to a larger number to try to address that fix will not work). If you need to loosen up the tolerance, see the \u201c inferred_tolerance_multiplier \u201d in the next section. (Note: I\u2019ve been considering dedicating a special meta-data field to the Commodity directive for this, but this would break from the invariant that meta-data is only there to be used by users and plugins, so I\u2019ve refrained so far.)","title":"Default Tolerances"},{"location":"precision_tolerances.html#tolerance-multiplier","text":"We\u2019re shown previously that when the tolerance value isn\u2019t provided explicitly, that it is inferred from the numbers on the postings. By default, the smallest digit found on those numbers is divided by half to obtain the tolerance because we assume that the institutions which we\u2019re reproducing the transactions apply rounding and so the error should never be more than half. But in reality, you may find that the rounding errors sometime exceed this value. For this reason, we provide an option to set the multiplier for the inferred tolerance: option \"inferred_tolerance_multiplier\" \"1.2\" This value overrides the default multiplier. In this example, for a transaction with postings only with values such as 24.45 CHF, the inferred tolerance for CHF would be +/- 0.012 CHF.","title":"Tolerance Multiplier"},{"location":"precision_tolerances.html#inferring-tolerances-from-cost","text":"There is also a feature that expands the maximum tolerance inferred on transactions to include values on cost currencies inferred by postings held at-cost or converted at price. Those postings can imply a tolerance value by multiplying the smallest digit of the unit by the cost or price value and taking half of that value. For example, if a posting has an amount of \"2.345 RGAGX {45.00 USD}\" attached to it, it implies a tolerance of 0.001 x 45.00 / 2 = 0.045 USD and the sum of all such possible rounding errors is calculate for all postings held at cost or converted from a price, and the resulting tolerance is added to the list of candidates used to figure out the tolerance we should use for the given commodity (we use the maximum value of all the inferred tolerances). You turn on the feature like this: option \"infer_tolerance_from_cost\" \"TRUE\" Enabling this flag only makes the tolerances potentially wider, never smaller.","title":"Inferring Tolerances from Cost"},{"location":"precision_tolerances.html#balance-assertions-padding","text":"There are a few other places where approximate comparisons are needed. Balance assertions also compare two numbers: 2015-05-08 balance Assets:Investments:RGAGX 4.271 RGAGX This asserts that the accumulated balance for this account has 4.271 units of RGAGX, plus or minus 0.001 RGAGX. So accumulated values of 4.270 RGAGX up to 4.272 RGAGX will check as asserted. The tolerance is inferred automatically to be 1 unit of the least significant digit of the number on the balance assertion. If you wanted a looser assertion, you could have declared: 2015-05-08 balance Assets:Investments:RGAGX 4.27 RGAGX This assertion would accept values from 4.26 RGAGX to 4.28 RGAGX. Note that the inferred tolerances are also expanded by the inferred tolerance multiplier discussed above.","title":"Balance Assertions & Padding"},{"location":"precision_tolerances.html#tolerances-that-trigger-padding","text":"Pad directives automatically insert transactions to bring account balances in-line with a subsequent balance assertion. The insertion only triggers if the balance differs from the expected value, and the tolerance for this to occur behaves exactly the same as for balance assertions.","title":"Tolerances that Trigger Padding"},{"location":"precision_tolerances.html#explicit-tolerances-on-balance-assertions","text":"Beancount supports the specification of an explicit tolerance amount, like this: 2015-05-08 balance Assets:Investments:RGAGX 4.271 ~ 0.01 RGAGX This feature was added because of some observed peculiarities in Vanguard investment accounts whereby rounding appears to follow odd rules and balances don\u2019t match.","title":"Explicit Tolerances on Balance Assertions"},{"location":"precision_tolerances.html#saving-rounding-error","text":"As we saw previously, transactions don\u2019t have to balance exactly, they allow for a small amount of imprecision. This bothers some people. If you would like to track and measure the residual amounts allowed by the tolerances, Beancount offers an option to automatically insert postings that will make each transaction balance exactly. You enable the feature like this: option \"account_rounding\" \"Equity:RoundingError\" This tells Beancount to insert postings to compensate for the rounding error to an \u201c Equity:RoundingError \u201d account. For example, with the feature enabled, the following transaction: 2013-02-23 * \"Buying something\" Assets:Invest 1.245 RGAGX {43.23 USD} Assets:Cash -53.82 USD will be automatically transformed into this: 2013-02-23 * \"Buying something\" Assets:Invest 1.245 RGAGX {43.23 USD} Assets:Cash -53.82 USD Equity:RoundingError -0.00135 USD You can verify that this transaction balances exactly. If the transaction already balances exactly (this is the case for most transactions) no posting is inserted. Finally, if you require that all accounts be opened explicitly, you should remember to declare the rounding account in your file at an appropriate date, like this: 2000-01-01 open Equity:RoundingError","title":"Saving Rounding Error"},{"location":"precision_tolerances.html#precision-of-inferred-numbers","text":"Beancount is able to infer some missing numbers in the input. For example, the second posting in this transaction is \u201cinterpolated\u201d automatically by Beancount: 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash The calculated amount to be inserted from the first posting is -227.2067 USD. Now, you might ask, to which precision is it inserted at? Does it insert 227.2067 USD at the full precision or does the number get rounded to a penny, e.g. 227.21 USD? It depends on the tolerance inferred for that currency. In this example, no tolerance is able to get inferred (there is no USD amount provided other than the cost amount, which is ignored for the purpose of inferring the tolerance), so we have to defer to the default tolerance. If the default tolerance is not overridden in the input file\u2014and therefore is zero\u2014the full precision will be used; no rounding occurs. This will result in the following transaction: 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.2067 USD Note that if a tolerance could be inferred from other numbers on that transaction, it would be used for rounding, such as in this example where the Cash posting is rounded to two digits because of the 9.95 USD number on the Commissions posting: 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Expenses:Commissions 9.95 USD Assets:Investments:Cash -237.16 USD However, if no inference is possible, and the default tolerance for USD is set to 0.001, the number will be quantized to 0.001 before insertion, that is, 227.207 USD will be stored: option \"default_tolerance\" \"USD:0.001\" 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.207 USD Finally, if you enabled the accumulation of rounding error, the posting\u2019s amount will reflect the correct residual, taking into account the rounded amount that was automatically inserted: option \"default_tolerance\" \"USD:0.01\" option \"account_rounding\" \"Equity:RoundingError\" 2014-05-06 * \"Buy mutual fund\" Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.207 USD Equity:RoundingError 0.0003 USD","title":"Precision of Inferred Numbers"},{"location":"precision_tolerances.html#porting-existing-input","text":"The inference of tolerance values from the transaction\u2019s numbers is generally good enough to keep existing files working without changes. There may be new errors appearing in older files once we process them with the method described in this document, but they should either point to previously undetected errors in the input, or be fixable with simple addition of a suitable number of digits. As a testimony, porting the author\u2019s very large input file has been a relatively painless process that took less than 1 hour. In order to ease the transition, you will probably want to change the default tolerance for all currencies to match the previous value that Beancount had been using, like this: option \"inferred_tolerance_default\" \"*:0.005\" I would recommend you start with this and fix all errors in your file, then proceed to removing this and fix the rest of errors. This should make it easier to adapt your file to this new behavior. As an example of how to fix a new error\u2026 converting this newly failing transaction from the Integer Amounts section: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.21005 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384 USD by inserting zero\u2019s to provide a locally inferred value like this: 2013-04-03 * \"Buy Mutual Fund - Price as of date based on closing price\" Assets:US:Vanguard:RGAGX 10.21005 RGAGX {37.61 USD} Assets:US:Vanguard:Cash -384.00 USD is sufficient to silence the balance check.","title":"Porting Existing Input"},{"location":"precision_tolerances.html#representational-issues","text":"Internally, Beancount uses a decimal number representation (not a binary/float representation, neither rational numbers). Calculations that result in a large number of fractional digits are carried out to 28 decimal places (the default precision from the context of Python\u2019s IEEE decimal implementation). This is plenty sufficient, because the method we propose above rarely trickles these types of numbers throughout the system: the tolerances allows us to post the precise amounts declared by users, and only automatically derived prices and costs will possibly result in precisions calculated to an unrealistic number of digits that could creep into aggregations in the rest of the system.","title":"Representational Issues"},{"location":"precision_tolerances.html#references","text":"The original proposal that led to this implementation can be found here . In particular, the proposal highlights on the other systems have attempted to deal with this issue. There are also some discussions on the mailing-list dedicated to this topic. Note that for the longest time, Beancount used a fixed precision of 0.005 across all currencies. This was eliminated once the method described in this document was implemented. Also, for Balance and Pad directives, there used to be a \u201ctolerance\u201d option that was set by default to 0.015 of any units. This option has been deprecated with the merging of the changes described in this document.","title":"References"},{"location":"precision_tolerances.html#historical-notes","text":"Here\u2019s an overview of the status of numbers rendering in Beancount as of March 2016, from the mailing-list : First, it's important to realize how these numbers are represented in memory. They are using the Decimal representation which beyond being able to accurately representing decimal numbers (as opposed to the approximation that binary floats provides) also contains a specific precision. That is, the number 2.00 is represented differently than the numbers 2.0 and 2.000. The numbers \"remember\" which precision they are represented up to. This is important. When I say rendering the numbers to their \"natural precision\" I mean the precision with which they are represented, i.e., 2.0 renders as \"2.0\", 2.000 renders as \"2.000\". Then, there are two DISTINCT topics: (1) tolerances, and (2) precision. \"Tolerances\" are values used to determine how much imprecision is acceptable in balancing transactions. This is used in the verification stage, to determine how much looseness to allow. It should not affect how numbers are rendered. \"Precision\" is perhaps a bit of a misnomer: By that I'm referring to is how many digits the numbers are to be rendered with. Once upon a time - after the shell was already written - these concepts weren't well defined in Beancount and I wasn't dealing with these things consistently. At some point it became clear what I needed to do and I created a class called \"DisplayContext\" which could contain appropriate settings for rendering the precision of numbers for each currency (each currency tends to have its own most common rendering precision, e.g. two digits for USD, one digit for MXN, no digits for JPY and in reports we're typically fine rounding the actual numbers to that precision). So an instance of this DisplayContext is automatically instantiated in the parser and in order to avoid the user having to set these values manually - for Beancount to \"do the right thing\" by default - it is able to accumulate the numbers seen and to deduce the most common and maximum number of digits used from the input, and to use that as the default number of digits for rendering numbers. The most common format/number of digits is used to render the number of units, and the maximum number of digits seen is used to render costs and prices. In addition, this class also has capabilities for aligning to the decimal dot and to insert commas on thousands as well. It separates the control of the formatting from the numbers themselves. MOST of the code that renders numbers uses the DisplayContext (via the to_string() methods) to convert the numbers into strings, such as the web interface and explicit text reports. But NOT ALL... there's a bit of HISTORY here... the SQL shell uses some old special-purpose code to render numbers that I never bothered to convert to the DisplayContext class. There's a TODO item for it. It needs to get converted at some point, but I've neglected doing this so far because I have much bigger plans for the SQL query engine that involve a full rewrite of it with many improvements and I figured I'd do that then. If you recall, the SQL query engine was a prototype, and actually it works, but it is not well covered by unit tests. My purpose with it was to discover through usage what would be useful and to then write a v2 of it that would be much better. Now, about that PRINT command... this is not intended as a reporting tool. The printer's purpose is to print input that accurately represents the content of the transactions. In order to do this, it needs to render the numbers at their \"natural\" precision, so that when they get read back in, they parse into the very same number, that is, with the same number of digits (even if zeros). For this reason, the PRINT command does not attempt to render using the DisplayContext instance derived from the input file - this is on purpose. I could change that, but then round-trip would break: the rounding resulting from formatting using the display context may output transactions which don't balance anymore. As you can see, it's not an obvious topic... Hopefully this should allow you to understand what comes out of Beancount in terms of the precision of the numbers it renders. Note: \"default_tolerances\" has been renamed to \"inferred_tolerance_default\" recently because the name was too general and confusing. Old name will work but generate a warning. I just noticed from your comments and some grepping around that the \"render_commas\" option is not used anymore. I'm not sure how that happened, but I'll go ad fix that right away and set the default value of the DisplayContext derived from the input. I should probably also convert the SQL shell rendering to use the display context regardless of future plans, so that it renders consistently with all the rest. Not sure I can do that this weekend, but I'll log a ticket, here . I hope this helps. You're welcome to ask questions if the above isn't clear. I'm sorry if this isn't entirely obvious... there's been a fair bit of history there and there's a lot of code. I should review the naming of options, I think the tolerance options all have \"tolerance\" in their name, but there aren't options to override the rendering and when I add them they should all have a common name as well.","title":"Historical Notes"},{"location":"precision_tolerances.html#further-reading","text":"What Every Computer Scientist Should Know About Floating-Point Arithmetic This stands in contrast to Ledger which attempts to infer the precision based on other transactions recently parsed in the file, in file order. This has the unfortunate effect of creating \u201ccross-talk\u201d between the transactions in terms of what precision can be used. \u21a9 Note that due to the way Beancount represents numbers internally, it is also not able to distinguish between \u201c230\u201d and \u201c230.\u201d; these parse into the same representation for Beancount. Therefore, we are not able to use that distinction in the input to support a precision of 0.5. \u21a9","title":"Further Reading"},{"location":"rounding_precision_in_beancount.html","text":"Proposal: Rounding & Precision in Beancount \uf0c1 Martin Blais , October 2014 This document describes the problem of rounding errors on Beancount transactions and how they are handled. It also includes a proposal for better handling precision issues in Beancount. Motivation \uf0c1 Balancing Precision \uf0c1 Balancing transactions cannot be done precisely. This has been discussed on the Ledger mailing-list before . It is necessary to allow for some tolerance on the amounts used to balance a transaction. This need is clear when you consider that inputting numbers in a text file implies a limited decimal representation. For example, if you\u2019re going to multiply a number of units and a cost, say both written down with 2 fractional digits, you might end up with a number that has 4 fractional digits, and then you need to compare that result with a cash amount that would typically be entered with only 2 fractional digits, something like this example: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.21 USD If you calculate it, the first posting\u2019s precise balance amount is 227.2067 USD, not 227.21 USD. However, the broker company managing the investment account will apply rounding to the closest cent for the cash withdrawal, and the rounded amount is the correct one to be used. This transaction has to balance; we need to allow for some looseness somehow. The great majority of the cases where mathematical operations occur involve the conversion from a number of units and a price or a cost to a corresponding cash value (e.g., units x cost = total cost). Our task in representing transactional information is the replication of operations that take place mostly in institutions. These operations always involve the rounding of numbers for units and currencies (banks do apply stochastic rounding), and the correct numbers to be used from the perspective of these institutions, and from the perspective of the government, are indeed the rounded numbers themselves. It is a not a question of mathematical purity, but one of practicality, and our system should do the same that banks do. Therefore, I think that we should always post the rounded numbers to accounts. Using rational numbers is not a limitation in that sense, but we must be careful to store rounded numbers where it matters. Automatic Rounding \uf0c1 Another related issue is that of automatically rounding amounts for interpolated numbers. Let\u2019s take our original problematic example again: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash ;; Interpolated posting = -227.2067 USD Here the amount from the second posting is interpolated from the balance amount for the first posting. Ideally, we should find a way to specify how it should round to 2 fractional digits of precision. Note that this affects interpolated prices and costs too: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.27 RGAGX {USD} Assets:Investments:Cash -227.21 USD Here, the cost is intended to be automatically calculated from the cash legs: 227.21 / 4.27 = 53.2107728337\u2026 USD. The correct cost to be inferred is also the rounded amount of 53.21 USD. We would like a mechanism to allow us to infer the desired precision. This mechanism cannot unfortunately be solely based on commodity: different accounts may track currencies with different precisions. As a real-world example, I have a retail FOREX trading account that really uses 4 digits of precision for its prices and deposits. Precision of Balance Assertions \uf0c1 The precision of a balance assertions is also subject to this problem, assertions like this one: 2014-04-01 balance Assets:Investments:Cash 4526.77 USD The user does not intend for this balance check to precisely sum up to 4526.77000000\u2026 USD. However, it this cash account previously received a deposit with a greater precision as in the previous section\u2019s example, then we have a problem. Now the cash amount contains some of the crumbs deposited from the interpolation (0.0067 USD). If we were able to find a good solution for the automatic rounding of postings in the previous section, this would not be a problem. But in the meantime, we must find a solution. Beancount\u2019s current approach is a kludge: it uses a user-configurable tolerance of 0.0150 (in any unit). We\u2019d like to change this so that the tolerance used is able to depend on the commodity, the account, or even the particular directive in use. Other Systems \uf0c1 Other command-line accounting systems differ in how they choose that tolerance: Ledger attempts to automatically derive the precision to use for its balance checks by using recently parsed context (in file order). The precision to be used is that of the last value parsed for the particular commodity under consideration. This can be problematic: it can lead to unnecessary side-effects between transactions which can be difficult to debug . HLedger, on the other hand, uses global precision settings. The whole file is processed first, then the precisions are derived from the most precise numbers seen in the entire input file. At the moment, Beancount uses a constant value for the tolerance used in its balance checking algorithm (0.005 of any unit). This is weak and should, at the very least, be commodity-dependent, if not also dependent on the particular account in which the commodity is used. Proposal \uf0c1 Automatically Inferring Tolerance \uf0c1 Beancount should derive its precision using a method entirely local to each transaction, perhaps with a global value for defaults. That is, for each transaction, it will inspect postings with simple amounts (no cost, no price) and infer the precision to be used for tolerance as half of that of the most precise amount entered by the user on this transaction. For example: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.278 RGAGX {53.21 USD} Assets:Investments:Cash -227.6324 USD Expenses:Commissions 9.95 USD The number of digits for the precision to be used here is the maximum of the 2nd and 3rd postings, that is, max(4, 2) = 4. The first postings is ignored because its amount is the result of a mathematical operation. The tolerance value should be half of the most precise digit, that is 0.00005 USD. This should allow the user to use an arbitrary precision, simply by inserting more digits in the input. Only fractional digits will be used to derive precision\u2026 an integer should imply exact matching. The user could specify a single trailing period to imply a sub-dollar precision. For example, the following transaction should fail to balance because the calculated amount is 999.999455 USD: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 23.45 RGAGX {42.6439 USD} Assets:Investments:Cash -1000 USD Instead, the user should explicitly allow for some tolerance to be used: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 23.45 RGAGX {42.6439 USD} Assets:Investments:Cash -1000. USD Or better, use 1000.00 USD. This has the disadvantage that is prevents the user from specifying the simpler integer amount. I\u2019m not sure if this is a big deal. Finally, no global effect implied by transactions will be applied. No transaction should ever affect any other transaction\u2019s balancing context. Inference on Amounts Held at Cost \uf0c1 An idea from by Matthew Harris ( here ) is that we could also use the value of the to the smallest decimal of the number of units times the cost as a number to use in establishing the tolerance for balancing transactions. For example, in the following transaction: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 23.45 RGAGX {42.6439 USD} ... The tolerance value that could be derived is 0.01 RGAGX x 42.6439 USD = 0.426439 USD The original use case presented by Matthew was of a transaction that did not contain a simple amount, just a conversion with both legs held at cost: 2011-01-25 * \"Transfer of Assets, 3467.90 USD\" * Assets:RothIRA:Vanguard:VTIVX 250.752 VTIVX {18.35 USD} @ 13.83 USD * Assets:RothIRA:DodgeCox:DODGX -30.892 DODGX {148.93 USD} @ 112.26 USD I\u2019ve actually tried to implement this and the resulting tolerances are either unacceptably wide or unacceptably small. It does not work well in practice so I\u2019ve abandoned the idea. Automated Rounding \uf0c1 For values that are automatically calculated, for example, on auto-postings where the remaining value is derived automatically, we should consider rounding the values. No work has been done on this yet; these values are currently not rounded. Fixing Balance Assertions \uf0c1 To fix balance assertions, we will derive the required precision by the number of digits used in the balance amount itself, by looking at the most precision fractional digit and using half of that digit\u2019s value to compute the tolerance: 2014-04-01 balance Assets:Investments:Cash 4526.7702 USD This balance check implies a precision of 0.00005 USD. If you use an integer number of units, no tolerance is allowed. The precise number should match: 2014-04-01 balance Assets:Investments:Cash 4526 USD If you want to allow for sub-dollar variance, use a single comma: 2014-04-01 balance Assets:Investments:Cash 4526. USD This balance check implies a precision of 0.50 USD. Approximate Assertions \uf0c1 Another idea, proposed in this ticket on Ledger , proposes an explicitly approximate assertion. We could implement it this way (just an idea): 2014-04-01 balance Assets:Investments:Cash 4526.00 +/- 0.05 USD Accumulating & Reporting Residuals \uf0c1 In order to explicitly render and monitor the amount of rounding errors that occur in a Ledger, we should accumulate it to an Equity account , such as \u201cEquity:Rounding\u201d. This should be turned on optionally. It should be possible for the user to specify an account to be used to accumulate the error. Whenever a transaction does not balance exactly, the residual, or rounding error, will be inserted as a posting of the transaction to the equity account. By default, this accumulation should be turned off. It\u2019s not clear whether the extra postings will be disruptive yet (if they\u2019re not, maybe this should be turned on by default; practice will inform us). Implementation \uf0c1 The implementation of this proposal is documented here .","title":"Rounding Precision in Beancount"},{"location":"rounding_precision_in_beancount.html#proposal-rounding-precision-in-beancount","text":"Martin Blais , October 2014 This document describes the problem of rounding errors on Beancount transactions and how they are handled. It also includes a proposal for better handling precision issues in Beancount.","title":"Proposal: Rounding & Precision in Beancount"},{"location":"rounding_precision_in_beancount.html#motivation","text":"","title":"Motivation"},{"location":"rounding_precision_in_beancount.html#balancing-precision","text":"Balancing transactions cannot be done precisely. This has been discussed on the Ledger mailing-list before . It is necessary to allow for some tolerance on the amounts used to balance a transaction. This need is clear when you consider that inputting numbers in a text file implies a limited decimal representation. For example, if you\u2019re going to multiply a number of units and a cost, say both written down with 2 fractional digits, you might end up with a number that has 4 fractional digits, and then you need to compare that result with a cash amount that would typically be entered with only 2 fractional digits, something like this example: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash -227.21 USD If you calculate it, the first posting\u2019s precise balance amount is 227.2067 USD, not 227.21 USD. However, the broker company managing the investment account will apply rounding to the closest cent for the cash withdrawal, and the rounded amount is the correct one to be used. This transaction has to balance; we need to allow for some looseness somehow. The great majority of the cases where mathematical operations occur involve the conversion from a number of units and a price or a cost to a corresponding cash value (e.g., units x cost = total cost). Our task in representing transactional information is the replication of operations that take place mostly in institutions. These operations always involve the rounding of numbers for units and currencies (banks do apply stochastic rounding), and the correct numbers to be used from the perspective of these institutions, and from the perspective of the government, are indeed the rounded numbers themselves. It is a not a question of mathematical purity, but one of practicality, and our system should do the same that banks do. Therefore, I think that we should always post the rounded numbers to accounts. Using rational numbers is not a limitation in that sense, but we must be careful to store rounded numbers where it matters.","title":"Balancing Precision"},{"location":"rounding_precision_in_beancount.html#automatic-rounding","text":"Another related issue is that of automatically rounding amounts for interpolated numbers. Let\u2019s take our original problematic example again: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.27 RGAGX {53.21 USD} Assets:Investments:Cash ;; Interpolated posting = -227.2067 USD Here the amount from the second posting is interpolated from the balance amount for the first posting. Ideally, we should find a way to specify how it should round to 2 fractional digits of precision. Note that this affects interpolated prices and costs too: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.27 RGAGX {USD} Assets:Investments:Cash -227.21 USD Here, the cost is intended to be automatically calculated from the cash legs: 227.21 / 4.27 = 53.2107728337\u2026 USD. The correct cost to be inferred is also the rounded amount of 53.21 USD. We would like a mechanism to allow us to infer the desired precision. This mechanism cannot unfortunately be solely based on commodity: different accounts may track currencies with different precisions. As a real-world example, I have a retail FOREX trading account that really uses 4 digits of precision for its prices and deposits.","title":"Automatic Rounding"},{"location":"rounding_precision_in_beancount.html#precision-of-balance-assertions","text":"The precision of a balance assertions is also subject to this problem, assertions like this one: 2014-04-01 balance Assets:Investments:Cash 4526.77 USD The user does not intend for this balance check to precisely sum up to 4526.77000000\u2026 USD. However, it this cash account previously received a deposit with a greater precision as in the previous section\u2019s example, then we have a problem. Now the cash amount contains some of the crumbs deposited from the interpolation (0.0067 USD). If we were able to find a good solution for the automatic rounding of postings in the previous section, this would not be a problem. But in the meantime, we must find a solution. Beancount\u2019s current approach is a kludge: it uses a user-configurable tolerance of 0.0150 (in any unit). We\u2019d like to change this so that the tolerance used is able to depend on the commodity, the account, or even the particular directive in use.","title":"Precision of Balance Assertions"},{"location":"rounding_precision_in_beancount.html#other-systems","text":"Other command-line accounting systems differ in how they choose that tolerance: Ledger attempts to automatically derive the precision to use for its balance checks by using recently parsed context (in file order). The precision to be used is that of the last value parsed for the particular commodity under consideration. This can be problematic: it can lead to unnecessary side-effects between transactions which can be difficult to debug . HLedger, on the other hand, uses global precision settings. The whole file is processed first, then the precisions are derived from the most precise numbers seen in the entire input file. At the moment, Beancount uses a constant value for the tolerance used in its balance checking algorithm (0.005 of any unit). This is weak and should, at the very least, be commodity-dependent, if not also dependent on the particular account in which the commodity is used.","title":"Other Systems"},{"location":"rounding_precision_in_beancount.html#proposal","text":"","title":"Proposal"},{"location":"rounding_precision_in_beancount.html#automatically-inferring-tolerance","text":"Beancount should derive its precision using a method entirely local to each transaction, perhaps with a global value for defaults. That is, for each transaction, it will inspect postings with simple amounts (no cost, no price) and infer the precision to be used for tolerance as half of that of the most precise amount entered by the user on this transaction. For example: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 4.278 RGAGX {53.21 USD} Assets:Investments:Cash -227.6324 USD Expenses:Commissions 9.95 USD The number of digits for the precision to be used here is the maximum of the 2nd and 3rd postings, that is, max(4, 2) = 4. The first postings is ignored because its amount is the result of a mathematical operation. The tolerance value should be half of the most precise digit, that is 0.00005 USD. This should allow the user to use an arbitrary precision, simply by inserting more digits in the input. Only fractional digits will be used to derive precision\u2026 an integer should imply exact matching. The user could specify a single trailing period to imply a sub-dollar precision. For example, the following transaction should fail to balance because the calculated amount is 999.999455 USD: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 23.45 RGAGX {42.6439 USD} Assets:Investments:Cash -1000 USD Instead, the user should explicitly allow for some tolerance to be used: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 23.45 RGAGX {42.6439 USD} Assets:Investments:Cash -1000. USD Or better, use 1000.00 USD. This has the disadvantage that is prevents the user from specifying the simpler integer amount. I\u2019m not sure if this is a big deal. Finally, no global effect implied by transactions will be applied. No transaction should ever affect any other transaction\u2019s balancing context.","title":"Automatically Inferring Tolerance"},{"location":"rounding_precision_in_beancount.html#inference-on-amounts-held-at-cost","text":"An idea from by Matthew Harris ( here ) is that we could also use the value of the to the smallest decimal of the number of units times the cost as a number to use in establishing the tolerance for balancing transactions. For example, in the following transaction: 2014-05-06 * \u201cBuy mutual fund\u201d Assets:Investments:RGXGX 23.45 RGAGX {42.6439 USD} ... The tolerance value that could be derived is 0.01 RGAGX x 42.6439 USD = 0.426439 USD The original use case presented by Matthew was of a transaction that did not contain a simple amount, just a conversion with both legs held at cost: 2011-01-25 * \"Transfer of Assets, 3467.90 USD\" * Assets:RothIRA:Vanguard:VTIVX 250.752 VTIVX {18.35 USD} @ 13.83 USD * Assets:RothIRA:DodgeCox:DODGX -30.892 DODGX {148.93 USD} @ 112.26 USD I\u2019ve actually tried to implement this and the resulting tolerances are either unacceptably wide or unacceptably small. It does not work well in practice so I\u2019ve abandoned the idea.","title":"Inference on Amounts Held at Cost"},{"location":"rounding_precision_in_beancount.html#automated-rounding","text":"For values that are automatically calculated, for example, on auto-postings where the remaining value is derived automatically, we should consider rounding the values. No work has been done on this yet; these values are currently not rounded.","title":"Automated Rounding"},{"location":"rounding_precision_in_beancount.html#fixing-balance-assertions","text":"To fix balance assertions, we will derive the required precision by the number of digits used in the balance amount itself, by looking at the most precision fractional digit and using half of that digit\u2019s value to compute the tolerance: 2014-04-01 balance Assets:Investments:Cash 4526.7702 USD This balance check implies a precision of 0.00005 USD. If you use an integer number of units, no tolerance is allowed. The precise number should match: 2014-04-01 balance Assets:Investments:Cash 4526 USD If you want to allow for sub-dollar variance, use a single comma: 2014-04-01 balance Assets:Investments:Cash 4526. USD This balance check implies a precision of 0.50 USD.","title":"Fixing Balance Assertions"},{"location":"rounding_precision_in_beancount.html#approximate-assertions","text":"Another idea, proposed in this ticket on Ledger , proposes an explicitly approximate assertion. We could implement it this way (just an idea): 2014-04-01 balance Assets:Investments:Cash 4526.00 +/- 0.05 USD","title":"Approximate Assertions"},{"location":"rounding_precision_in_beancount.html#accumulating-reporting-residuals","text":"In order to explicitly render and monitor the amount of rounding errors that occur in a Ledger, we should accumulate it to an Equity account , such as \u201cEquity:Rounding\u201d. This should be turned on optionally. It should be possible for the user to specify an account to be used to accumulate the error. Whenever a transaction does not balance exactly, the residual, or rounding error, will be inserted as a posting of the transaction to the equity account. By default, this accumulation should be turned off. It\u2019s not clear whether the extra postings will be disruptive yet (if they\u2019re not, maybe this should be turned on by default; practice will inform us).","title":"Accumulating & Reporting Residuals"},{"location":"rounding_precision_in_beancount.html#implementation","text":"The implementation of this proposal is documented here .","title":"Implementation"},{"location":"running_beancount_and_generating_reports.html","text":"Running Beancount & Generating Reports \uf0c1 Martin Blais , July-Sep 2014 http://furius.ca/beancount/doc/tools Introduction Tools bean-check bean-report bean-query bean-web Global Pages View Reports Pages bean-bake bean-doctor Context bean-format bean-example Filtering Transactions Reports Balance Reports Trial Balance (balances) Balance Sheet (balsheet) Opening Balances (openbal) Income Statement (income) Journal Reports Journal (journal) Rendering at Cost Adding a Balance Column Character Width Precision Compact, Normal or Verbose Summary Equivalent SQL Query Conversions (conversions) Documents (documents) Holdings Reports Holdings & Aggregations (holdings*) Net Worth (networth) Other Report Types Cash Prices (prices) Statistics (stats) Update Activity (activity) Introduction \uf0c1 This document describes the tools you use to process Beancount input files, and many of the reports available from it. The syntax of the language is described in the Beancount Language Syntax document. This manual only covers the technical details for using Beancount from the command-line. Tools \uf0c1 bean-check \uf0c1 bean-check is the program you use to verify that your input syntax and transactions work correctly. All it does is load your input file and run the various plugins you configured in it, plus some extra validation checks. It report errors (if any), and then exits. You run it on your input file, like this: bean-check /path/to/my/file.beancount If there are no errors, there should be no output, it should exit quietly. If there were errors, they will be printed to stderr with the filename, line number and error description (in a format that is understood by Emacs, so you can just use next-error and previous-error to navigate to the file if you want): /home/user/myledger.beancount:44381: Transaction does not balance: 34.46 USD 2014-07-12 * \"Black Iron Burger\" \"\" Expenses:Food:Restaurant 17.23 USD Assets:Cash 17.23 USD You should always fix all the errors before producing reports. bean-report \uf0c1 This is the main tool used to extract specialized reports to the console in text or one of the various other formats. You invoke it like this: bean-report /path/to/my/file.beancount For example: bean-report /path/to/my/file.beancount balances There are many reports available. See the section on reports for a description of the main ones. If you want to produce the full list of reports, ask it for help: bean-report --help-reports Report names can sometimes accept arguments. At the moment the arguments are specified as part of the report name itself, often separated by a colon (:), like this: bean-report /path/to/my/file.beancount balances:Vanguard There are a few special reports you should know about: check, or validate: This is the same as running the bean-check command. print: This simply prints out the entries that Beancount has parsed, in Beancount syntax. This can be used to confirm that Beancount has read and interpreted your input data correctly (if you\u2019re debugging something difficult). The other reports are what you\u2019d expect: they print out various tables of aggregations of amounts. The reports you can generate are described in a dedicated section below. PLEASE NOTE! At the moment of release, the list of reports available from the web page will differ from the list available from the console. In a future release, I will consolidate those two lists and all the reports that are available from the web pages will also be available from the console, and in many different formats. Stay tuned. bean-query \uf0c1 Beancount\u2019s parsed list of transactions and postings is like an in-memory database. bean-query is a command-line tool that acts like a client to that in-memory database in which you can type queries in a variant of SQL. You invoke it like this: bean-query /path/to/my/file.beancount Input file: \"/path/to/my/file.beancount\" Ready with 14212 directives (21284 postings in 8879 transactions). beancount> _ More details are available in its own document . bean-web \uf0c1 bean-web serves all the reports on a web server that runs on your computer. You run it like this: bean-web /path/to/my/file.beancount It will serve all pages on port 8080 on your machine. Navigate to http://localhost:8080 with a web browser. You should be able to click your way through all the reports easily. The web interface provides a set of global pages and a set of report pages for each \u201cview.\u201d Global Pages \uf0c1 The top-level table of contents page provides links to all the global pages at the top: The table of contents (the page you\u2019re looking at) A list of the errors that occurred in your ledger file A view of the source code of your Ledger file (this is used by various other links when referring to a location in your input file.) The table of contents provides a convenient list of links to all the common views, such as \u201cview by year\u201d, \u201cview by tag\u201d, and of course, \u201cview all transactions.\u201d There are a few more. View Reports Pages \uf0c1 When you click on a view report page, you enter a set of pages for the subset of transactions for that view. Various reports about those transactions are available from here: Opening balances (a balance sheet at the beginning of the view) Balance sheet (a balance sheet at the end of the view) Income statement (for the period of the view) Various journals for each account (just click on an account name) Various reports of holdings at the end of the view Lists of documents and prices included in the view\u2019s entries Some statistics about the view data \u2026 and much more. There should be an index of all the available view reports. bean-bake \uf0c1 bean-bake runs a bean-web instance and bakes all the pages to a directory: bean-bake /path/to/my/file.beancount myfinances It also support baking directly to an archive file: bean-bake /path/to/my/file.beancount myfinances.zip Various compression methods are supported, e.g. .tar.gz. This is useful to share the web interface with your accountant or other people, who usually don\u2019t have the ability to run Beancount. The page links have all been converted to relative links, and they should be able to extract the archive to a directory and browse all the reports the same way you do with bean-web . bean-doctor \uf0c1 This is a debugging tool used to perform various diagnostics and run debugging commands, and to help provide information for reporting bugs. For example, it can do the following: List the Beancount dependencies that are installed on your machine, and the ones that are missing. It can tell you what you\u2019re missing that you should be installing. Print a dump of the parser\u2019s lexer tokens for your input file. If you report a parsing bug, it can be useful to look at the lexer output (if you know what you\u2019re doing). It can run your input file through a parsing round-trip, that is, print out the file and re-read it again and compare it. This is a useful parser and printer test. It can check that a directory hierarchy corresponds to a Beancount input file\u2019s chart-of-accounts, and reporting directories that do not comply. This is useful in case you decide to change some account names and are maintaining a corresponding archive of documents which needs to be adjusted accordingly. Context \uf0c1 It can list the context upon which a transaction is applied, i.e., the inventory balances of each account the transaction postings modify, before and after it is applied. Use it like this: $ bean-doctor context /home/blais/accounting/blais.beancount 28514 /home/blais/accounting/blais.beancount:28513: ; Assets:US:ETrade:BND 100.00 BND {81.968730000000 USD} ; Assets:US:ETrade:Cash 8143.97 USD 2014-07-25 * \"(TRD) BOT +50 BND @82.10\" ^273755872 Assets:US:ETrade:BND 50.00 BND {82.10 USD} Assets:US:ETrade:Cash -4105.00 USD ; Assets:US:ETrade:BND 100.00 BND {81.968730000000 USD} ; ! Assets:US:ETrade:BND 50.00 BND {82.10 USD} ; ! Assets:US:ETrade:Cash 4038.97 USD There is a corresponding Emacs binding (C-c x) to invoke this around the cursor. bean-format \uf0c1 This pure text processing tool will reformat Beancount input to right-align all the numbers at the same, minimal column. It left-aligns all the currencies. It only modifies whitespace. This tool accepts a filename as arguments and outputs the aligned file on stdout (similar to UNIX cat ). bean-example \uf0c1 This program generates an example Beancount input file. See the Tutorial for more details about the contents of this example file. Filtering Transactions \uf0c1 In order to produce different views of your financial transactions, we select a subset of full list of parsed transactions, for example, \u201call the transactions that occurred in year 2013\u201d, and then use that to produce the various available reports that Beancount provides. At the moment, only a preset list of filters are available as \u201cviews\u201d from the web interface. These views include: All transactions Transactions that occur in a particular year Transactions with a particular tag Transactions with a particular payee Transactions that involve at least one account with a particular name component At the moment, in order to access reports from these subsets of transactions, you need to use the bean-web web interface, and click on the related keyword in the root global page, which enters you into a set of reports for that view. Reports \uf0c1 The whole point of entering your transactions in a single input file in the first place is that it allows you to sum, filter, aggregate and arrange various subsets of your data into well-known reports. There are three distinct ways to produce reports from Beancount: by using bean-web and browsing to a view and then to a specific report (this is the easy way), by using bean-report and providing the name of a desired report (and possibly some report-specific arguments), and by using bean-query and requesting data by specifying an SQL statement. Reports can sometimes be rendered in different file formats. Each report type will support being rendered in a list of common ones, such as console text, HTML and CSV. Some reports render in Beancount syntax itself, and we simply call this format name \u201cbeancount.\u201d There are many types of reports available, and there will be many more in the future, as many of the features on the roadmap involve new types of output. This section provides an overview of the most common ones. Use bean-report to inquire about the full list supported by your installed version: bean-report --help-reports PLEASE NOTE! At the moment, the sets of reports that are available from the web interface and from the console are different, though there is some overlap. In a subsequent version, the list of reports will be reconciled and all reports will be made available via both the web interface and the console, in a variety of data formats (text, CSV, HTML and maybe others.) For now, we will document these in the sections below. Balance Reports \uf0c1 All the balance reports are similar in that they produce tables of some set of accounts and their associated balances: [output] |-- Assets | `-- US | |-- BofA | | `-- Checking 596.05 USD | |-- ETrade | | |-- Cash 5,120.50 USD | | |-- GLD 70.00 GLD | | |-- ITOT 17.00 ITOT | | |-- VEA 36.00 VEA | | `-- VHT 294.00 VHT | |-- Federal | | `-- PreTax401k | |-- Hoogle | | `-- Vacation 337.26 VACHR | `-- Vanguard ... Balances for commodities held \u201cat cost\u201d are rendered at their book value. (Unrealized gains, if any, are inserted as separate transactions by an optional plugin and the result amounts get mixed in with the cost basis if rendered in the same account.) If an account\u2019s balance contains many different types of currencies (commodities not held \u201cat cost\u201d, such as dollars, euros, yen), each gets printed on its own line. This can render the balance column a bit too busy and messes with the vertical regularity of account names. This is to some extent an unavoidable compromise, but in practice, there are only a small number of commodities that form the large majority of a user\u2019s ledger: the home country\u2019s currency units. To this extent, amounts in common currencies can be broken out into their own column using the \u201c operating_currency \u201d option: option \"operating_currency\" \"USD\" option \"operating_currency\" \"CAD\" You may use this option multiple times if you have many of them (this is my case, for instance, because I am an expat and hold assets in both my host and home countries). Declaring operating currencies also hold the advantage that the name of the currency need not be rendered, and it can thus more easily be imported into a spreadsheet. Finally, some accounts are deemed \u201cactive\u201d if they have not been closed. A closed account with no transactions in the filtered set of transactions will not be rendered. Trial Balance ( balances ) \uf0c1 A trial balance report simply produces a table of final balances of all the active accounts, with all the accounts rendered vertically. The sum total of all balances is reported at the bottom. Unlike a balance sheet, it may not always balance to zero because of currency conversions. (This is the equivalent of Ledger\u2019s bal report.) The equivalent bean-query command is: SELECT account, sum(position) GROUP BY account ORDER BY account; Balance Sheet ( balsheet ) \uf0c1 A balance sheet is a snapshot of the balances of the Assets, Liabilities and Equity accounts at a particular point in time. In order to build such a report, we have to move balances from the other accounts to it: compute the balances of the Income and Expenses account at that point in time, insert transactions that will zero out balances from these accounts by transferring them to an equity account ( Equity:Earnings:Current ), render a tree of the balances of the Assets accounts on the left side, render a tree of the Liabilities accounts on the right side, render a tree of the Equity accounts below the Liabilities accounts. See the introduction document for an example. Note that the Equity accounts include the amounts reported from the Income and Expenses accounts, also often called \u201cNet Income.\u201d Also, in practice, we make two transfers because we\u2019re typically looking at a reporting period, and we want to differentiate between Net Income amounts transferred before the beginning of the period ( Equity:Earnings:Previous ) and during the period itself ( Equity:Earnings:Current ). And similar pair of transfers is carried out in order to handle currency conversions (this is a bit of a hairy topic, but one with a great solution; refer to the dedicated document if you want all the details). The equivalent bean-query command is: SELECT account, sum(position) FROM CLOSE ON 2016-01-01 GROUP BY account ORDER BY account; Opening Balances ( openbal ) \uf0c1 The opening balances report is simply a balance sheet drawn at the beginning of the reporting period. This report only makes sense for a list of filtered entries that represents a period of time, such as \u201cyear 2014.\u201d The balance sheet is generated using only the summarization entries that were synthesized when the transactions were filtered (see the double-entry method document ). Income Statement ( income ) \uf0c1 An income statement lists the final balances of the Income and Expenses accounts. It represents a summary of the transient activity within these accounts. If the balance sheet is the snapshot at a particular point in time, this is the difference between the beginning and the end of a period (in our case: of a filtered set of transactions). The balances of the active Income accounts are rendered on the left, and those of the active Expenses accounts on the right. See the introduction document for an example. The difference between the total of Income and Expenses balances is the Net Income. Note that the initial balances of the Income and Expenses accounts should have been zero\u2019ed out by summarization transactions that occur at the beginning of the period, because we\u2019re only interested in the changes in these accounts. Journal Reports \uf0c1 The reports in this section render lists of transactions and other directives in a linear fashion. Journal ( journal ) \uf0c1 This report is the equivalent of an \u201caccount statement\u201d from an institution, a list of transactions with at least one posting in that account. This is the equivalent of Ledger\u2019s register report ( reg ). You generate a journal report like this: bean-report myfile.beancount journal \u2026 By default, this renders a journal of all the transactions, which is unlikely to be what you want. Select a particular account to render like this: bean-report myfile.beancount journal -a Expenses:Restaurant At the moment, the \u201c -a \u201d option accepts only a complete account name, or the name of one of the parent accounts. Eventually we will extend it to handle expressions. Rendering at Cost \uf0c1 The numbers column on the right displays the changes from the postings of the selected account. Notice that only the balances for the postings affecting the given account are rendered. The change column renders the changes in the units affected. For example, if this posting is selected: Assets:Investments:Apple 2 AAPL {402.00 USD} The value reported for the change will be \u201c 2 AAPL \u201d. If you would like to render the values at cost, use the \u201c --at-cost \u201d or \u201c -c \u201d option, which will in this case render \u201c 804.00 USD \u201d instead. There is no \u201cmarket value\u201d option. Unrealized gains are automatically inserted at the end of the history by the \u201c beancount.plugins.unrealized \u201d plugin. See options for that plugin to insert its unrealized gains. Note that if the sum of the selected postings is zero, no amount is rendered in the change column. Adding a Balance Column \uf0c1 If you want to add a column that sums up the running balance for the reported changes, use the \u201c --render-balance \u201d or \u201c -b \u201d option. This does not always make sense to report, so it is up to you to decide whether you want a running balance. Character Width \uf0c1 By default, the report will be as wide as your terminal allows. Restrict the width to a set number of characters with the \u201c-w\u201d option. Precision \uf0c1 The number of fractional digits for the number rendering can be specified via \u201c --precision \u201d or \u201c -k \u201d. Compact, Normal or Verbose \uf0c1 In its normal operation, Beancount renders an empty line between transactions. This helps delineate transactions where there are multiple currencies affected, as they render on separate lines. If you want a more compact rendering, use the \u201c --compact \u201d or \u201c -x \u201d option. On the other hand, if you want to render the affected postings under the transaction line, use the \u201c --verbose \u201d or \u201c -X \u201d option. Summary \uf0c1 Here is a summary of its arguments: optional arguments: -h, --help show this help message and exit -a ACCOUNT, --account ACCOUNT Account to render -w WIDTH, --width WIDTH The number of characters wide to render the report to -k PRECISION, --precision PRECISION The number of digits to render after the period -b, --render-balance, --balance If true, render a running balance -c, --at-cost, --cost If true, render values at cost -x, --compact Rendering compactly -X, --verbose Rendering verbosely Equivalent SQL Query \uf0c1 The equivalent bean-query command is: SELECT date, flag, description, account, cost(position), cost(balance); Conversions ( conversions ) \uf0c1 This report lists the total of currency conversions that result from the selected transactions. (Most people won\u2019t need this.) Documents ( documents ) \uf0c1 This report produces an HTML list of all the external documents found in the ledger, either from explicit directives or from plugins that automatically find the documents and add them to the stream of transactions. Holdings Reports \uf0c1 These reports produces aggregations for assets held at cost. Holdings & Aggregations ( holdings* ) \uf0c1 This report produces a detailed list of all holdings found in the ledger. You can produce aggregates by commodity and accounts using the \u201c-g\u201d option. Net Worth ( networth ) \uf0c1 This report produces a short summary of the net worth (equity) of the ledger, in each of the operating currencies. Other Report Types \uf0c1 Cash \uf0c1 This report renders balances in commodities not held at cost, in other words, cash: bean-report example.beancount cash -c USD Account Units Currency Cost Currency Average Cost Price Book Value Market Value -------------------------- --------- -------- ------------- ------------ ----- ---------- ------------ Assets:US:BofA:Checking 596.05 USD USD 596.05 596.05 Assets:US:ETrade:Cash 5,120.50 USD USD 5,120.50 5,120.50 Assets:US:Hoogle:Vacation 337.26 VACHR Assets:US:Vanguard:Cash -0.02 USD USD -0.02 -0.02 Liabilities:US:Chase:Slate -2,891.85 USD USD -2,891.85 -2,891.85 -------------------------- --------- -------- ------------- ------------ ----- ---------- ------------ The report allows you to convert all currencies to a common currency (in the example above, \"convert everything to USD\"). There's also an option to report only on the operating currencies. I use this to get an overview of all uninvested cash. Prices ( prices ) \uf0c1 This report renders a list of price points for a base currency in terms of a quote currency. The list is sorted by date. You can output this table in beancount format as well. This is convenient to save a price database to a file, that can then be combined and loaded into another input file. Statistics ( stats ) \uf0c1 This report simply provides various statistics on the parsed entries. Update Activity ( activity ) \uf0c1 This table renders for each account the date of the last entry.","title":"Running Beancount and Generating Reports"},{"location":"running_beancount_and_generating_reports.html#running-beancount-generating-reports","text":"Martin Blais , July-Sep 2014 http://furius.ca/beancount/doc/tools Introduction Tools bean-check bean-report bean-query bean-web Global Pages View Reports Pages bean-bake bean-doctor Context bean-format bean-example Filtering Transactions Reports Balance Reports Trial Balance (balances) Balance Sheet (balsheet) Opening Balances (openbal) Income Statement (income) Journal Reports Journal (journal) Rendering at Cost Adding a Balance Column Character Width Precision Compact, Normal or Verbose Summary Equivalent SQL Query Conversions (conversions) Documents (documents) Holdings Reports Holdings & Aggregations (holdings*) Net Worth (networth) Other Report Types Cash Prices (prices) Statistics (stats) Update Activity (activity)","title":"Running Beancount & Generating Reports"},{"location":"running_beancount_and_generating_reports.html#introduction","text":"This document describes the tools you use to process Beancount input files, and many of the reports available from it. The syntax of the language is described in the Beancount Language Syntax document. This manual only covers the technical details for using Beancount from the command-line.","title":"Introduction"},{"location":"running_beancount_and_generating_reports.html#tools","text":"","title":"Tools"},{"location":"running_beancount_and_generating_reports.html#bean-check","text":"bean-check is the program you use to verify that your input syntax and transactions work correctly. All it does is load your input file and run the various plugins you configured in it, plus some extra validation checks. It report errors (if any), and then exits. You run it on your input file, like this: bean-check /path/to/my/file.beancount If there are no errors, there should be no output, it should exit quietly. If there were errors, they will be printed to stderr with the filename, line number and error description (in a format that is understood by Emacs, so you can just use next-error and previous-error to navigate to the file if you want): /home/user/myledger.beancount:44381: Transaction does not balance: 34.46 USD 2014-07-12 * \"Black Iron Burger\" \"\" Expenses:Food:Restaurant 17.23 USD Assets:Cash 17.23 USD You should always fix all the errors before producing reports.","title":"bean-check"},{"location":"running_beancount_and_generating_reports.html#bean-report","text":"This is the main tool used to extract specialized reports to the console in text or one of the various other formats. You invoke it like this: bean-report /path/to/my/file.beancount For example: bean-report /path/to/my/file.beancount balances There are many reports available. See the section on reports for a description of the main ones. If you want to produce the full list of reports, ask it for help: bean-report --help-reports Report names can sometimes accept arguments. At the moment the arguments are specified as part of the report name itself, often separated by a colon (:), like this: bean-report /path/to/my/file.beancount balances:Vanguard There are a few special reports you should know about: check, or validate: This is the same as running the bean-check command. print: This simply prints out the entries that Beancount has parsed, in Beancount syntax. This can be used to confirm that Beancount has read and interpreted your input data correctly (if you\u2019re debugging something difficult). The other reports are what you\u2019d expect: they print out various tables of aggregations of amounts. The reports you can generate are described in a dedicated section below. PLEASE NOTE! At the moment of release, the list of reports available from the web page will differ from the list available from the console. In a future release, I will consolidate those two lists and all the reports that are available from the web pages will also be available from the console, and in many different formats. Stay tuned.","title":"bean-report"},{"location":"running_beancount_and_generating_reports.html#bean-query","text":"Beancount\u2019s parsed list of transactions and postings is like an in-memory database. bean-query is a command-line tool that acts like a client to that in-memory database in which you can type queries in a variant of SQL. You invoke it like this: bean-query /path/to/my/file.beancount Input file: \"/path/to/my/file.beancount\" Ready with 14212 directives (21284 postings in 8879 transactions). beancount> _ More details are available in its own document .","title":"bean-query"},{"location":"running_beancount_and_generating_reports.html#bean-web","text":"bean-web serves all the reports on a web server that runs on your computer. You run it like this: bean-web /path/to/my/file.beancount It will serve all pages on port 8080 on your machine. Navigate to http://localhost:8080 with a web browser. You should be able to click your way through all the reports easily. The web interface provides a set of global pages and a set of report pages for each \u201cview.\u201d","title":"bean-web"},{"location":"running_beancount_and_generating_reports.html#global-pages","text":"The top-level table of contents page provides links to all the global pages at the top: The table of contents (the page you\u2019re looking at) A list of the errors that occurred in your ledger file A view of the source code of your Ledger file (this is used by various other links when referring to a location in your input file.) The table of contents provides a convenient list of links to all the common views, such as \u201cview by year\u201d, \u201cview by tag\u201d, and of course, \u201cview all transactions.\u201d There are a few more.","title":"Global Pages"},{"location":"running_beancount_and_generating_reports.html#view-reports-pages","text":"When you click on a view report page, you enter a set of pages for the subset of transactions for that view. Various reports about those transactions are available from here: Opening balances (a balance sheet at the beginning of the view) Balance sheet (a balance sheet at the end of the view) Income statement (for the period of the view) Various journals for each account (just click on an account name) Various reports of holdings at the end of the view Lists of documents and prices included in the view\u2019s entries Some statistics about the view data \u2026 and much more. There should be an index of all the available view reports.","title":"View Reports Pages"},{"location":"running_beancount_and_generating_reports.html#bean-bake","text":"bean-bake runs a bean-web instance and bakes all the pages to a directory: bean-bake /path/to/my/file.beancount myfinances It also support baking directly to an archive file: bean-bake /path/to/my/file.beancount myfinances.zip Various compression methods are supported, e.g. .tar.gz. This is useful to share the web interface with your accountant or other people, who usually don\u2019t have the ability to run Beancount. The page links have all been converted to relative links, and they should be able to extract the archive to a directory and browse all the reports the same way you do with bean-web .","title":"bean-bake"},{"location":"running_beancount_and_generating_reports.html#bean-doctor","text":"This is a debugging tool used to perform various diagnostics and run debugging commands, and to help provide information for reporting bugs. For example, it can do the following: List the Beancount dependencies that are installed on your machine, and the ones that are missing. It can tell you what you\u2019re missing that you should be installing. Print a dump of the parser\u2019s lexer tokens for your input file. If you report a parsing bug, it can be useful to look at the lexer output (if you know what you\u2019re doing). It can run your input file through a parsing round-trip, that is, print out the file and re-read it again and compare it. This is a useful parser and printer test. It can check that a directory hierarchy corresponds to a Beancount input file\u2019s chart-of-accounts, and reporting directories that do not comply. This is useful in case you decide to change some account names and are maintaining a corresponding archive of documents which needs to be adjusted accordingly.","title":"bean-doctor"},{"location":"running_beancount_and_generating_reports.html#context","text":"It can list the context upon which a transaction is applied, i.e., the inventory balances of each account the transaction postings modify, before and after it is applied. Use it like this: $ bean-doctor context /home/blais/accounting/blais.beancount 28514 /home/blais/accounting/blais.beancount:28513: ; Assets:US:ETrade:BND 100.00 BND {81.968730000000 USD} ; Assets:US:ETrade:Cash 8143.97 USD 2014-07-25 * \"(TRD) BOT +50 BND @82.10\" ^273755872 Assets:US:ETrade:BND 50.00 BND {82.10 USD} Assets:US:ETrade:Cash -4105.00 USD ; Assets:US:ETrade:BND 100.00 BND {81.968730000000 USD} ; ! Assets:US:ETrade:BND 50.00 BND {82.10 USD} ; ! Assets:US:ETrade:Cash 4038.97 USD There is a corresponding Emacs binding (C-c x) to invoke this around the cursor.","title":"Context"},{"location":"running_beancount_and_generating_reports.html#bean-format","text":"This pure text processing tool will reformat Beancount input to right-align all the numbers at the same, minimal column. It left-aligns all the currencies. It only modifies whitespace. This tool accepts a filename as arguments and outputs the aligned file on stdout (similar to UNIX cat ).","title":"bean-format"},{"location":"running_beancount_and_generating_reports.html#bean-example","text":"This program generates an example Beancount input file. See the Tutorial for more details about the contents of this example file.","title":"bean-example"},{"location":"running_beancount_and_generating_reports.html#filtering-transactions","text":"In order to produce different views of your financial transactions, we select a subset of full list of parsed transactions, for example, \u201call the transactions that occurred in year 2013\u201d, and then use that to produce the various available reports that Beancount provides. At the moment, only a preset list of filters are available as \u201cviews\u201d from the web interface. These views include: All transactions Transactions that occur in a particular year Transactions with a particular tag Transactions with a particular payee Transactions that involve at least one account with a particular name component At the moment, in order to access reports from these subsets of transactions, you need to use the bean-web web interface, and click on the related keyword in the root global page, which enters you into a set of reports for that view.","title":"Filtering Transactions"},{"location":"running_beancount_and_generating_reports.html#reports","text":"The whole point of entering your transactions in a single input file in the first place is that it allows you to sum, filter, aggregate and arrange various subsets of your data into well-known reports. There are three distinct ways to produce reports from Beancount: by using bean-web and browsing to a view and then to a specific report (this is the easy way), by using bean-report and providing the name of a desired report (and possibly some report-specific arguments), and by using bean-query and requesting data by specifying an SQL statement. Reports can sometimes be rendered in different file formats. Each report type will support being rendered in a list of common ones, such as console text, HTML and CSV. Some reports render in Beancount syntax itself, and we simply call this format name \u201cbeancount.\u201d There are many types of reports available, and there will be many more in the future, as many of the features on the roadmap involve new types of output. This section provides an overview of the most common ones. Use bean-report to inquire about the full list supported by your installed version: bean-report --help-reports PLEASE NOTE! At the moment, the sets of reports that are available from the web interface and from the console are different, though there is some overlap. In a subsequent version, the list of reports will be reconciled and all reports will be made available via both the web interface and the console, in a variety of data formats (text, CSV, HTML and maybe others.) For now, we will document these in the sections below.","title":"Reports"},{"location":"running_beancount_and_generating_reports.html#balance-reports","text":"All the balance reports are similar in that they produce tables of some set of accounts and their associated balances: [output] |-- Assets | `-- US | |-- BofA | | `-- Checking 596.05 USD | |-- ETrade | | |-- Cash 5,120.50 USD | | |-- GLD 70.00 GLD | | |-- ITOT 17.00 ITOT | | |-- VEA 36.00 VEA | | `-- VHT 294.00 VHT | |-- Federal | | `-- PreTax401k | |-- Hoogle | | `-- Vacation 337.26 VACHR | `-- Vanguard ... Balances for commodities held \u201cat cost\u201d are rendered at their book value. (Unrealized gains, if any, are inserted as separate transactions by an optional plugin and the result amounts get mixed in with the cost basis if rendered in the same account.) If an account\u2019s balance contains many different types of currencies (commodities not held \u201cat cost\u201d, such as dollars, euros, yen), each gets printed on its own line. This can render the balance column a bit too busy and messes with the vertical regularity of account names. This is to some extent an unavoidable compromise, but in practice, there are only a small number of commodities that form the large majority of a user\u2019s ledger: the home country\u2019s currency units. To this extent, amounts in common currencies can be broken out into their own column using the \u201c operating_currency \u201d option: option \"operating_currency\" \"USD\" option \"operating_currency\" \"CAD\" You may use this option multiple times if you have many of them (this is my case, for instance, because I am an expat and hold assets in both my host and home countries). Declaring operating currencies also hold the advantage that the name of the currency need not be rendered, and it can thus more easily be imported into a spreadsheet. Finally, some accounts are deemed \u201cactive\u201d if they have not been closed. A closed account with no transactions in the filtered set of transactions will not be rendered.","title":"Balance Reports"},{"location":"running_beancount_and_generating_reports.html#trial-balance-balances","text":"A trial balance report simply produces a table of final balances of all the active accounts, with all the accounts rendered vertically. The sum total of all balances is reported at the bottom. Unlike a balance sheet, it may not always balance to zero because of currency conversions. (This is the equivalent of Ledger\u2019s bal report.) The equivalent bean-query command is: SELECT account, sum(position) GROUP BY account ORDER BY account;","title":"Trial Balance (balances)"},{"location":"running_beancount_and_generating_reports.html#balance-sheet-balsheet","text":"A balance sheet is a snapshot of the balances of the Assets, Liabilities and Equity accounts at a particular point in time. In order to build such a report, we have to move balances from the other accounts to it: compute the balances of the Income and Expenses account at that point in time, insert transactions that will zero out balances from these accounts by transferring them to an equity account ( Equity:Earnings:Current ), render a tree of the balances of the Assets accounts on the left side, render a tree of the Liabilities accounts on the right side, render a tree of the Equity accounts below the Liabilities accounts. See the introduction document for an example. Note that the Equity accounts include the amounts reported from the Income and Expenses accounts, also often called \u201cNet Income.\u201d Also, in practice, we make two transfers because we\u2019re typically looking at a reporting period, and we want to differentiate between Net Income amounts transferred before the beginning of the period ( Equity:Earnings:Previous ) and during the period itself ( Equity:Earnings:Current ). And similar pair of transfers is carried out in order to handle currency conversions (this is a bit of a hairy topic, but one with a great solution; refer to the dedicated document if you want all the details). The equivalent bean-query command is: SELECT account, sum(position) FROM CLOSE ON 2016-01-01 GROUP BY account ORDER BY account;","title":"Balance Sheet (balsheet)"},{"location":"running_beancount_and_generating_reports.html#opening-balances-openbal","text":"The opening balances report is simply a balance sheet drawn at the beginning of the reporting period. This report only makes sense for a list of filtered entries that represents a period of time, such as \u201cyear 2014.\u201d The balance sheet is generated using only the summarization entries that were synthesized when the transactions were filtered (see the double-entry method document ).","title":"Opening Balances (openbal)"},{"location":"running_beancount_and_generating_reports.html#income-statement-income","text":"An income statement lists the final balances of the Income and Expenses accounts. It represents a summary of the transient activity within these accounts. If the balance sheet is the snapshot at a particular point in time, this is the difference between the beginning and the end of a period (in our case: of a filtered set of transactions). The balances of the active Income accounts are rendered on the left, and those of the active Expenses accounts on the right. See the introduction document for an example. The difference between the total of Income and Expenses balances is the Net Income. Note that the initial balances of the Income and Expenses accounts should have been zero\u2019ed out by summarization transactions that occur at the beginning of the period, because we\u2019re only interested in the changes in these accounts.","title":"Income Statement (income)"},{"location":"running_beancount_and_generating_reports.html#journal-reports","text":"The reports in this section render lists of transactions and other directives in a linear fashion.","title":"Journal Reports"},{"location":"running_beancount_and_generating_reports.html#journal-journal","text":"This report is the equivalent of an \u201caccount statement\u201d from an institution, a list of transactions with at least one posting in that account. This is the equivalent of Ledger\u2019s register report ( reg ). You generate a journal report like this: bean-report myfile.beancount journal \u2026 By default, this renders a journal of all the transactions, which is unlikely to be what you want. Select a particular account to render like this: bean-report myfile.beancount journal -a Expenses:Restaurant At the moment, the \u201c -a \u201d option accepts only a complete account name, or the name of one of the parent accounts. Eventually we will extend it to handle expressions.","title":"Journal (journal)"},{"location":"running_beancount_and_generating_reports.html#rendering-at-cost","text":"The numbers column on the right displays the changes from the postings of the selected account. Notice that only the balances for the postings affecting the given account are rendered. The change column renders the changes in the units affected. For example, if this posting is selected: Assets:Investments:Apple 2 AAPL {402.00 USD} The value reported for the change will be \u201c 2 AAPL \u201d. If you would like to render the values at cost, use the \u201c --at-cost \u201d or \u201c -c \u201d option, which will in this case render \u201c 804.00 USD \u201d instead. There is no \u201cmarket value\u201d option. Unrealized gains are automatically inserted at the end of the history by the \u201c beancount.plugins.unrealized \u201d plugin. See options for that plugin to insert its unrealized gains. Note that if the sum of the selected postings is zero, no amount is rendered in the change column.","title":"Rendering at Cost"},{"location":"running_beancount_and_generating_reports.html#adding-a-balance-column","text":"If you want to add a column that sums up the running balance for the reported changes, use the \u201c --render-balance \u201d or \u201c -b \u201d option. This does not always make sense to report, so it is up to you to decide whether you want a running balance.","title":"Adding a Balance Column"},{"location":"running_beancount_and_generating_reports.html#character-width","text":"By default, the report will be as wide as your terminal allows. Restrict the width to a set number of characters with the \u201c-w\u201d option.","title":"Character Width"},{"location":"running_beancount_and_generating_reports.html#precision","text":"The number of fractional digits for the number rendering can be specified via \u201c --precision \u201d or \u201c -k \u201d.","title":"Precision"},{"location":"running_beancount_and_generating_reports.html#compact-normal-or-verbose","text":"In its normal operation, Beancount renders an empty line between transactions. This helps delineate transactions where there are multiple currencies affected, as they render on separate lines. If you want a more compact rendering, use the \u201c --compact \u201d or \u201c -x \u201d option. On the other hand, if you want to render the affected postings under the transaction line, use the \u201c --verbose \u201d or \u201c -X \u201d option.","title":"Compact, Normal or Verbose"},{"location":"running_beancount_and_generating_reports.html#summary","text":"Here is a summary of its arguments: optional arguments: -h, --help show this help message and exit -a ACCOUNT, --account ACCOUNT Account to render -w WIDTH, --width WIDTH The number of characters wide to render the report to -k PRECISION, --precision PRECISION The number of digits to render after the period -b, --render-balance, --balance If true, render a running balance -c, --at-cost, --cost If true, render values at cost -x, --compact Rendering compactly -X, --verbose Rendering verbosely","title":"Summary"},{"location":"running_beancount_and_generating_reports.html#equivalent-sql-query","text":"The equivalent bean-query command is: SELECT date, flag, description, account, cost(position), cost(balance);","title":"Equivalent SQL Query"},{"location":"running_beancount_and_generating_reports.html#conversions-conversions","text":"This report lists the total of currency conversions that result from the selected transactions. (Most people won\u2019t need this.)","title":"Conversions (conversions)"},{"location":"running_beancount_and_generating_reports.html#documents-documents","text":"This report produces an HTML list of all the external documents found in the ledger, either from explicit directives or from plugins that automatically find the documents and add them to the stream of transactions.","title":"Documents (documents)"},{"location":"running_beancount_and_generating_reports.html#holdings-reports","text":"These reports produces aggregations for assets held at cost.","title":"Holdings Reports"},{"location":"running_beancount_and_generating_reports.html#holdings-aggregations-holdings","text":"This report produces a detailed list of all holdings found in the ledger. You can produce aggregates by commodity and accounts using the \u201c-g\u201d option.","title":"Holdings & Aggregations (holdings*)"},{"location":"running_beancount_and_generating_reports.html#net-worth-networth","text":"This report produces a short summary of the net worth (equity) of the ledger, in each of the operating currencies.","title":"Net Worth (networth)"},{"location":"running_beancount_and_generating_reports.html#other-report-types","text":"","title":"Other Report Types"},{"location":"running_beancount_and_generating_reports.html#cash","text":"This report renders balances in commodities not held at cost, in other words, cash: bean-report example.beancount cash -c USD Account Units Currency Cost Currency Average Cost Price Book Value Market Value -------------------------- --------- -------- ------------- ------------ ----- ---------- ------------ Assets:US:BofA:Checking 596.05 USD USD 596.05 596.05 Assets:US:ETrade:Cash 5,120.50 USD USD 5,120.50 5,120.50 Assets:US:Hoogle:Vacation 337.26 VACHR Assets:US:Vanguard:Cash -0.02 USD USD -0.02 -0.02 Liabilities:US:Chase:Slate -2,891.85 USD USD -2,891.85 -2,891.85 -------------------------- --------- -------- ------------- ------------ ----- ---------- ------------ The report allows you to convert all currencies to a common currency (in the example above, \"convert everything to USD\"). There's also an option to report only on the operating currencies. I use this to get an overview of all uninvested cash.","title":"Cash"},{"location":"running_beancount_and_generating_reports.html#prices-prices","text":"This report renders a list of price points for a base currency in terms of a quote currency. The list is sorted by date. You can output this table in beancount format as well. This is convenient to save a price database to a file, that can then be combined and loaded into another input file.","title":"Prices (prices)"},{"location":"running_beancount_and_generating_reports.html#statistics-stats","text":"This report simply provides various statistics on the parsed entries.","title":"Statistics (stats)"},{"location":"running_beancount_and_generating_reports.html#update-activity-activity","text":"This table renders for each account the date of the last entry.","title":"Update Activity (activity)"},{"location":"settlement_dates_in_beancount.html","text":"Settlement Dates & Transfer Accounts in Beancount \uf0c1 Martin Blais, July 2014 http://furius.ca/beancount/doc/proposal-dates Motivation Proposal Description Remaining Questions Previous Work Ledger Effective and Auxiliary Dates References Motivation \uf0c1 When a trade executes in an investment account, there is most often a delay between the date that the transaction is carried out (the \u201ctransaction date\u201d) and the date that the funds are deposited in an associated cash account (the \u201csettlement date\u201d). This makes imported balance assertions sometimes requiring the fudging of their dates, and sometimes they can even be impossible. This document proposes the addition of an optional \u201csettlement date\u201d to be attached to a transaction or a posting, and associated semantics for how to deal with the problem. Proposal Description \uf0c1 Settlement Dates \uf0c1 In the first implementation of Beancount I used to have two dates attached to a transaction, but I never did anything with them. The alternate date would get attached but was ignored thereafter. The meaning of it is that it should have split the transaction into two, with some sort of transfer account, that might have been useful semantics, I never developed it. Something like this as input: 2014-06-23=2014-06-28 * \"Sale\" Assets:ScottTrade:GOOG -10 GOOG {589.00 USD} S Assets:ScottTrade:Cash where the \u201cS\u201d posting flag marks the leg \u201cto be postponed to the settlement date.\u201d Alternatively, you could attach the date to a posting: 2014-06-23 * \"Sale\" Assets:ScottTrade:GOOG -10 GOOG {589.00 USD} 2014-06-28 Assets:ScottTrade:Cash Both of the above syntax proposals allow you to specify which postings are meant to be postponed to settlement. The second one is more flexible, as each posting could potentially have a different date, but the more constrained syntax of the first would create less complications. Either of these could get translated to multiple transactions with a transfer account to absorb the pending amount: 2014-06-23 * \"Sale\" ^settlement-3463873948 Assets:ScottTrade:GOOG -10 GOOG {589.00 USD} Assets:ScottTrade:Transfer 2014-06-28 * \"Sale\" ^settlement-3463873948 Assets:ScottTrade:Transfer Assets:ScottTrade:Cash 5890.00 USD So far, I\u2019ve been getting away with fudging the dates on balance assertions where necessary. I have relatively few sales, so this hasn\u2019t been a big problem so far. I\u2019m not convinced it needs a solution yet, maybe the best thing to do is just to document how to deal with the issue when it occurs. Maybe someone can convince me otherwise. Transfer Accounts \uf0c1 In the previous section, we discuss a style whereby a single entry moving money between two accounts contains two dates and results in two separate entries. An auxiliary problem, which is related in its solution, is how to carry out the reverse operation, that is, how to merge two separate entries posting to a common transfer account (sometimes called a \u201c suspense account \u201d). For example, a user may want to input the two sides of a transaction separately, e.g. by running import scripts on separate input files, and instead of having to reconcile and merge those by hand, we would want to explicitly support this by identifying matching transactions to these transfer accounts and creating a common link between them. Most importantly, we want to be able to easily identify which of the transactions is not matched on the other side, which indicates missing data. There is a prototype of this under beancount.plugins.tag_pending . Also see redstreet0\u2019s \u201c zerosum \u201d plugin from this thread . Remaining Questions \uf0c1 How do we determine a proper transfer account name? Is a subaccount a reasonable approach? What if a user would like to have a single global limbo account? TODO Does this property solve the problem of making balance assertions between trade and settlement? TODO [Write out a detailed example] Any drawbacks? TODO How does this affect the balance sheet and income statement, if any? Is it going to be obvious to users what the amounts in these limbo/transfer accounts are? TODO Unrooting Transactions \uf0c1 A wilder idea would be to add an extra level in the transaction-posting hierarchy, adding the capability to group multiple partial transactions, and move the balancing rule to that level. Basically, two transactions input separately and then grouped - by some rule, or trivially by themselves - could form a new unit of balance rule. That would be a much more demanding change on the schema and on the Beancount design but would allow to natively support partial transactions, keeping their individual dates, descriptions, etc. Maybe that's a better model? Consider the advantages. Previous Work \uf0c1 Ledger Effective and Auxiliary Dates \uf0c1 Ledger has the concept of \u201c auxiliary dates \u201d. The way these work is straightforward: any transaction may have a second date, and the user can select at runtime (with --aux-date ) whether the main date or the auxiliary dates are meant to be used. It is unclear to me how this is meant to be used in practice, in the presence of balance assertions. Without balance assertions, I can see how it would just work: you\u2019d render everything with settlement dates only. This would probably only make sense for specific reports. I would much rather keep a single semantic for the set of transactions that gets parsed in; the idea that the meaning of the transactions varies depending on the invocation conditions would set a precedent in Beancount, I\u2019d prefer not to break this nice property, so by default I\u2019d prefer to avoid implementing this solution. Auxiliary dates are also known as \u201c effective dates \u201d and can be associated with each individual posting. Auxiliary dates are secondary to the the \u201cprimary date\u201d or the \u201cactual date\u201d, being the posting date of the record): 2008/10/16 * (2090) Bountiful Blessings Farm Expenses:Food:Groceries $ 37.50 ; [=2008/10/01] Expenses:Food:Groceries $ 37.50 ; [=2008/11/01] Expenses:Food:Groceries $ 37.50 ; [=2008/12/01] Expenses:Food:Groceries $ 37.50 ; [=2009/01/01] Expenses:Food:Groceries $ 37.50 ; [=2009/02/01] Expenses:Food:Groceries $ 37.50 ; [=2009/03/01] Assets:Checking The original motivation for this was for budgeting, allow one to move accounting of expenses to neighboring budget periods in order to carry over actual paid amounts to those periods. Bank amounts in one month could be set against a budget from the immediately preceding or following month, as needed. (Note: John Wiegley) This is similar to one of the syntaxes I\u2019m suggesting above\u2014letting the user specify a date for each posting\u2014but the other postings are not split as independent transactions. The usage of those dates is similarly triggered by a command-line option ( --effective ). I\u2019m assuming that the posting on the checking account above occurs at once at 2008/10/16, regardless of reporting date. Let\u2019s verify this: $ ledger -f settlement1.lgr reg checking --effective 08-Oct-16 Bountiful Blessings.. Assets:Checking $ -225.00 $ -225.00 That\u2019s what I thought. This works, but a problem with this approach is that any balance sheet drawn between 2008/10/01 (the earliest effective date) and 2009/03/01 (the latest effective date) would not balance. Between those dates, some amounts are \u201cin limbo\u201d and drawing up a balance sheet at one of those dates would not balance. This would break an invariant in Beancount: we require that you should always be able to draw a balance sheet at any point in time, and any subset of transactions should balance. I would rather implement this by splitting this example transaction into many other ones, as in the proposal above, moving those temporary amounts living in limbo in an explicit \u201climbo\u201d or \u201ctransfer\u201d account, where each transaction balances. Moreover, this step can be implemented as a transformation stage, replacing the transaction with effective dates by one transaction for each posting where the effective date differs from the transaction\u2019s date (this could be enabled on demand via a plugin). GnuCash \uf0c1 TODO(blais) - How is this handled in GnuCash and other GUI systems? Is there a standard account method? References \uf0c1 The IRS requires you to use the trade date and NOT the settlement date for tax reporting; from the IRS Publication 17: Securities traded on established market. For securities traded on an established securities market, your holding period begins the day after the trade date you bought the securities, and ends on the trade date you sold them. Do not confuse the trade date with the settlement date, which is the date by which the stock must be delivered and payment must be made. Example. You are a cash method, calendar year taxpayer. You sold stock at a gain on December 30, 2013. According to the rules of the stock exchange, the sale was closed by delivery of the stock 4 trading days after the sale, on January 6, 2014. You received payment of the sales price on that same day. Report your gain on your 2013 return, even though you received the payment in 2014. The gain is long term or short term depending on whether you held the stock more than 1 year. Your holding period ended on December 30. If you had sold the stock at a loss, you would also report it on your 2013 return. Threads \uf0c1 An interesting \"feature by coincidence\" First Opinions, Coming from Ledger","title":"Settlement Dates in Beancount"},{"location":"settlement_dates_in_beancount.html#settlement-dates-transfer-accounts-in-beancount","text":"Martin Blais, July 2014 http://furius.ca/beancount/doc/proposal-dates Motivation Proposal Description Remaining Questions Previous Work Ledger Effective and Auxiliary Dates References","title":"Settlement Dates & Transfer Accounts in Beancount"},{"location":"settlement_dates_in_beancount.html#motivation","text":"When a trade executes in an investment account, there is most often a delay between the date that the transaction is carried out (the \u201ctransaction date\u201d) and the date that the funds are deposited in an associated cash account (the \u201csettlement date\u201d). This makes imported balance assertions sometimes requiring the fudging of their dates, and sometimes they can even be impossible. This document proposes the addition of an optional \u201csettlement date\u201d to be attached to a transaction or a posting, and associated semantics for how to deal with the problem.","title":"Motivation"},{"location":"settlement_dates_in_beancount.html#proposal-description","text":"","title":"Proposal Description"},{"location":"settlement_dates_in_beancount.html#settlement-dates","text":"In the first implementation of Beancount I used to have two dates attached to a transaction, but I never did anything with them. The alternate date would get attached but was ignored thereafter. The meaning of it is that it should have split the transaction into two, with some sort of transfer account, that might have been useful semantics, I never developed it. Something like this as input: 2014-06-23=2014-06-28 * \"Sale\" Assets:ScottTrade:GOOG -10 GOOG {589.00 USD} S Assets:ScottTrade:Cash where the \u201cS\u201d posting flag marks the leg \u201cto be postponed to the settlement date.\u201d Alternatively, you could attach the date to a posting: 2014-06-23 * \"Sale\" Assets:ScottTrade:GOOG -10 GOOG {589.00 USD} 2014-06-28 Assets:ScottTrade:Cash Both of the above syntax proposals allow you to specify which postings are meant to be postponed to settlement. The second one is more flexible, as each posting could potentially have a different date, but the more constrained syntax of the first would create less complications. Either of these could get translated to multiple transactions with a transfer account to absorb the pending amount: 2014-06-23 * \"Sale\" ^settlement-3463873948 Assets:ScottTrade:GOOG -10 GOOG {589.00 USD} Assets:ScottTrade:Transfer 2014-06-28 * \"Sale\" ^settlement-3463873948 Assets:ScottTrade:Transfer Assets:ScottTrade:Cash 5890.00 USD So far, I\u2019ve been getting away with fudging the dates on balance assertions where necessary. I have relatively few sales, so this hasn\u2019t been a big problem so far. I\u2019m not convinced it needs a solution yet, maybe the best thing to do is just to document how to deal with the issue when it occurs. Maybe someone can convince me otherwise.","title":"Settlement Dates"},{"location":"settlement_dates_in_beancount.html#transfer-accounts","text":"In the previous section, we discuss a style whereby a single entry moving money between two accounts contains two dates and results in two separate entries. An auxiliary problem, which is related in its solution, is how to carry out the reverse operation, that is, how to merge two separate entries posting to a common transfer account (sometimes called a \u201c suspense account \u201d). For example, a user may want to input the two sides of a transaction separately, e.g. by running import scripts on separate input files, and instead of having to reconcile and merge those by hand, we would want to explicitly support this by identifying matching transactions to these transfer accounts and creating a common link between them. Most importantly, we want to be able to easily identify which of the transactions is not matched on the other side, which indicates missing data. There is a prototype of this under beancount.plugins.tag_pending . Also see redstreet0\u2019s \u201c zerosum \u201d plugin from this thread .","title":"Transfer Accounts"},{"location":"settlement_dates_in_beancount.html#remaining-questions","text":"How do we determine a proper transfer account name? Is a subaccount a reasonable approach? What if a user would like to have a single global limbo account? TODO Does this property solve the problem of making balance assertions between trade and settlement? TODO [Write out a detailed example] Any drawbacks? TODO How does this affect the balance sheet and income statement, if any? Is it going to be obvious to users what the amounts in these limbo/transfer accounts are? TODO","title":"Remaining Questions"},{"location":"settlement_dates_in_beancount.html#unrooting-transactions","text":"A wilder idea would be to add an extra level in the transaction-posting hierarchy, adding the capability to group multiple partial transactions, and move the balancing rule to that level. Basically, two transactions input separately and then grouped - by some rule, or trivially by themselves - could form a new unit of balance rule. That would be a much more demanding change on the schema and on the Beancount design but would allow to natively support partial transactions, keeping their individual dates, descriptions, etc. Maybe that's a better model? Consider the advantages.","title":"Unrooting Transactions"},{"location":"settlement_dates_in_beancount.html#previous-work","text":"","title":"Previous Work"},{"location":"settlement_dates_in_beancount.html#ledger-effective-and-auxiliary-dates","text":"Ledger has the concept of \u201c auxiliary dates \u201d. The way these work is straightforward: any transaction may have a second date, and the user can select at runtime (with --aux-date ) whether the main date or the auxiliary dates are meant to be used. It is unclear to me how this is meant to be used in practice, in the presence of balance assertions. Without balance assertions, I can see how it would just work: you\u2019d render everything with settlement dates only. This would probably only make sense for specific reports. I would much rather keep a single semantic for the set of transactions that gets parsed in; the idea that the meaning of the transactions varies depending on the invocation conditions would set a precedent in Beancount, I\u2019d prefer not to break this nice property, so by default I\u2019d prefer to avoid implementing this solution. Auxiliary dates are also known as \u201c effective dates \u201d and can be associated with each individual posting. Auxiliary dates are secondary to the the \u201cprimary date\u201d or the \u201cactual date\u201d, being the posting date of the record): 2008/10/16 * (2090) Bountiful Blessings Farm Expenses:Food:Groceries $ 37.50 ; [=2008/10/01] Expenses:Food:Groceries $ 37.50 ; [=2008/11/01] Expenses:Food:Groceries $ 37.50 ; [=2008/12/01] Expenses:Food:Groceries $ 37.50 ; [=2009/01/01] Expenses:Food:Groceries $ 37.50 ; [=2009/02/01] Expenses:Food:Groceries $ 37.50 ; [=2009/03/01] Assets:Checking The original motivation for this was for budgeting, allow one to move accounting of expenses to neighboring budget periods in order to carry over actual paid amounts to those periods. Bank amounts in one month could be set against a budget from the immediately preceding or following month, as needed. (Note: John Wiegley) This is similar to one of the syntaxes I\u2019m suggesting above\u2014letting the user specify a date for each posting\u2014but the other postings are not split as independent transactions. The usage of those dates is similarly triggered by a command-line option ( --effective ). I\u2019m assuming that the posting on the checking account above occurs at once at 2008/10/16, regardless of reporting date. Let\u2019s verify this: $ ledger -f settlement1.lgr reg checking --effective 08-Oct-16 Bountiful Blessings.. Assets:Checking $ -225.00 $ -225.00 That\u2019s what I thought. This works, but a problem with this approach is that any balance sheet drawn between 2008/10/01 (the earliest effective date) and 2009/03/01 (the latest effective date) would not balance. Between those dates, some amounts are \u201cin limbo\u201d and drawing up a balance sheet at one of those dates would not balance. This would break an invariant in Beancount: we require that you should always be able to draw a balance sheet at any point in time, and any subset of transactions should balance. I would rather implement this by splitting this example transaction into many other ones, as in the proposal above, moving those temporary amounts living in limbo in an explicit \u201climbo\u201d or \u201ctransfer\u201d account, where each transaction balances. Moreover, this step can be implemented as a transformation stage, replacing the transaction with effective dates by one transaction for each posting where the effective date differs from the transaction\u2019s date (this could be enabled on demand via a plugin).","title":"Ledger Effective and Auxiliary Dates"},{"location":"settlement_dates_in_beancount.html#gnucash","text":"TODO(blais) - How is this handled in GnuCash and other GUI systems? Is there a standard account method?","title":"GnuCash"},{"location":"settlement_dates_in_beancount.html#references","text":"The IRS requires you to use the trade date and NOT the settlement date for tax reporting; from the IRS Publication 17: Securities traded on established market. For securities traded on an established securities market, your holding period begins the day after the trade date you bought the securities, and ends on the trade date you sold them. Do not confuse the trade date with the settlement date, which is the date by which the stock must be delivered and payment must be made. Example. You are a cash method, calendar year taxpayer. You sold stock at a gain on December 30, 2013. According to the rules of the stock exchange, the sale was closed by delivery of the stock 4 trading days after the sale, on January 6, 2014. You received payment of the sales price on that same day. Report your gain on your 2013 return, even though you received the payment in 2014. The gain is long term or short term depending on whether you held the stock more than 1 year. Your holding period ended on December 30. If you had sold the stock at a loss, you would also report it on your 2013 return.","title":"References"},{"location":"settlement_dates_in_beancount.html#threads","text":"An interesting \"feature by coincidence\" First Opinions, Coming from Ledger","title":"Threads"},{"location":"sharing_expenses_with_beancount.html","text":"Sharing Expenses in Beancount \uf0c1 Martin Blais , May 2015 http://furius.ca/beancount/doc/shared Introduction \uf0c1 This document presents a method for precisely and easily accounting for shared expenses in complicated group situations. For example, traveling with a group of friends where people pay for different things. We show it is possible to deal with expenses to be split evenly among the group as well as group expenses that should be allocated to specific persons. The method uses the double-entry accounting system. The essence of the method consists in separating the accounting of expenses and their associated payments. This makes it much easier to deal with shared costs, because using this method it doesn\u2019t matter who pays for what: we reconcile the precise amounts owed for each individual at the end and automatically compute final correcting transfers for each. This article is structured around a simple trip with expenses shared equally by two people. However, the same method generalizes to any type of project with incomes and expenses to be shared among multiple participants, and splitting expenses evenly or not. A Travel Example \uf0c1 Martin (the author) and Caroline (his girlfriend) visited Mexico in March 2015. We visited the island of Cozumel for a SCUBA diving trip for three days and then headed to Tulum for two days to relax and dive in the cenotes . Our assumptions are: Chaotic payments . We lead very busy lives\u2026 both of us are going to make payments ahead of time and during the trip, without any forethought regarding who will pay for what, though we will carefully record every payment. Each of us just pays for whatever preparation costs as needed to arrange this trip. For example, Caroline selected and booked flights early while I paid for the resort and booked the rental and activities at the local dive shop one week before departure. Use of shared and individual assets. Both of us are bringing cash, and are going to make payments during the trip from our respective wallets as well as from a shared pool of cash converted to local currency (Mexican pesos, for which I will use the \u201cMXN\u201d symbol), and we will use credit cards as necessary before and during the trip. Multiple currencies. Some of our expenses will be denominated in US dollars and some in Mexican pesos. For example, flights were paid in US dollars, local meals and the accommodation were paid in pesos, but the local dive shop charged us dollars. Converted amounts will come from both cash and credit cards sources. Individual expenses in a shared pool. While most of the expenses are to be shared equally, some of the expenses will apply to only one of us, and we want to account for those explicitly. For example, Caroline took a SCUBA certification course ( PADI Open Water ) and will pay for that on her own; similarly, she should not be paying for Martin\u2019s expensive boat diving costs. To complicate matters, the dive shop issued us a single joint bill for everything at the end of our stay. A Note About Sharing \uf0c1 I feel that something should be said about the \u201csharing\u201d aspect of our expenses, as this topic has come up on previous discussions on the mailing-lists involving sharing examples. We are nitpicking on purpose. For the purpose of this exercise, we are accounting for every little penny spent in an incredibly detailed manner. The point of this document is specifically to show how a complex set of transactions well accounted for can be efficiently and simply disentangled to a precise accounting of expenses for each participant, regardless of who actually makes the payments. We are not cheapskates. We will assume that we\u2019ve decided to split expenses evenly. Our \u201cgenerosity\u201d to each other is not a topic relevant to this document. We\u2019re both well compensated working professionals and you can assume that we\u2019ve agreed to split the common costs for this trip evenly (50/50). One of the attributes of the method we show here is that the decision of how we choose to split expenses can be made separately from the actual payment of costs. For example, we may decide in the end that I pay for \u2154rd of the trip, but that can be decided precisely rather than in an ad-hoc \u201coh, I think I remember I paid for this\u201d manner. This is especially useful in a larger group of people because when expenses aren\u2019t tracked accurately, usually everyone is left with a feeling that they paid more than the others. The sum of the perceived fractions of expenses paid in a group is always greater than 100%... Overview of the Method \uf0c1 In this section we present a brief illustrated overview of the method. A set of common Assets accounts that belong to the project, and book all our individual expenses and transfer for the trip as coming from external Income accounts: During the trip, we use the common Assets to make expenses. Most of the expenses are attributed to both of us (and to be shared eventually), but some of the expenses are intended to be attributed to each of us individually: After the trip, remaining Assets (like cash we walked home with) gets distributed back to ourselves to zero out the balances of the Assets accounts and we record this through contra postings to Income accounts: Finally, the list of shared Expenses are split between each other\u2014using a plugin that forks every posting that is intended to be a shared expense\u2014and the final amount is used to make a final transfer between each other so that we\u2019ve each paid for our respective expenses and we\u2019re square: Note that the final balance of expenses for each participant may differ, and these are due to particular expenses that were attributed separately, or if we decide to split the total unevenly. How to Track Expenses \uf0c1 In order to write down all expenses for this trip we will open a brand new Beancount input file. Despite the fact that the expenses will come from each person\u2019s personal accounts, it is useful to think of the trip as a special project, as if it were a separate entity, e.g., a company that exists only for the duration of the trip. The example file we wrote for our trip can be found here and should help you follow along this text. Accounts \uf0c1 The set of accounts in the input file should not have to match your personal Beancount file\u2019s account names. The accounts we will use include accounts that correspond to Martin and Caroline\u2019s personal accounts but with generic names (e.g., Income:Martin:CreditCard instead of Liabilities:US:Chase ), and expense accounts may not match any regular expenses in my personal Beancount file\u2014it\u2019s not important. As a convention, any account that pertains specifically to one of the travelers will include that person\u2019s name in the account name. For example, Caroline\u2019s credit card will be named \u201c Income:Caroline:CreditCard \u201d. This is important, as we will use this later on to split contributions and expenses. Let\u2019s examine the different types of accounts we will need to carry this out. External Income Accounts \uf0c1 The \u201cproject\u201d will receive income in the form of transfers from personal accounts of either traveler. These are accounts we will consider external to the project and so will define them as Income accounts: ;; External accounts for Martin. 2015-02-01 open Income:Martin:Cash 2015-02-01 open Income:Martin:Cash:Foreign 2015-02-01 open Income:Martin:Wallet 2015-02-01 open Income:Martin:CreditCard ;; External accounts for Caroline. 2015-02-01 open Income:Caroline:Cash 2015-02-01 open Income:Caroline:Wallet 2015-02-01 open Income:Caroline:MetroCard 2015-02-01 open Income:Caroline:CreditCard Transactions carried out from these accounts must be copied from your personal Beancount file. Obviously, you must be careful to include all the transactions pertaining to the trip. I used a tag to do this in my personal file. Assets & Liabilities Accounts \uf0c1 There will be a few Asset accounts that will be active and exist for the duration of the trip. These temporary accounts will be zero\u2019ed out at the end of it. One example is a pool of petty cash in local currency: 2015-02-01 open Assets:Cash:Pesos description: \"A shared account to contain our pocket of pesos\" We also carried cash in each of our pockets while traveling, so I created two separate accounts for that: 2015-02-01 open Assets:Cash:Martin description: \"Cash for the trip held by Martin\" 2015-02-01 open Assets:Cash:Caroline description: \"Cash for the trip held by Caroline\" Note however that despite their individual names, those accounts are considered as part of the project. It was just convenient to separately track balances for the cash we each held during the trip. Expenses Accounts \uf0c1 We will define various accounts to book our Expenses to. For example, \u201c Expenses:Flights \u201d will contain our costs associated with flight travel. For convenience, and because there are many types of expenses in this file, we chose to leverage the \u201cauto-accounts\u201d plugin and let Beancount automatically open these accounts: plugin \"beancount.ops.auto_accounts\" The great majority of these accounts are for shared expenses to be split between us. For example, shared SCUBA diving expenses will be booked to \u201c Expenses:Scuba \u201d. However, for expenses that are intended to be covered by one of us only, we simply include the name of the traveler in the account name. For example, Martin\u2019s extra costs for boat diving will be booked to the \u201c Expenses:Scuba:Martin \u201d account. Example Transactions \uf0c1 Let\u2019s turn our attention to the different types of transactions present in the file. In this section I will walk you through some representative transactions (the names I give to these are arbitrary). Contributing Transactions \uf0c1 Contributions to the project\u2019s costs are usually done in the form of expenses paid from external accounts. For example, Caroline paying for flights with her credit card looks like this: 2015-02-01 * \"Flights to Cancun\" Income:Caroline:CreditCard -976.00 USD Expenses:Flights Martin paying for the taxi to the airport looks like this: 2015-02-25 * \"Taxi to airport\" ^433f66ea0e4e Expenses:Transport:Taxi 62.80 USD Income:Martin:CreditCard Bringing Cash \uf0c1 We both brought some cash on us, as we were warned it might be difficult to use credit cards in Mexico. In my personal Beancount file, the cash account is \u201c Assets:Cash \u201d but here it must get booked as an external contribution with my name on it: ;; Initial cash on us. 2015-02-24 * \"Getting cash for travel\" Income:Martin:Cash -1200 USD Assets:Cash:Martin 1200 USD Caroline\u2019s cash is similar: 2015-02-24 * \"Getting cash for travel\" Income:Caroline:Cash -300 USD Assets:Cash:Caroline 300 USD Once again, note that the Assets:Cash:Martin and Assets:Cash:Caroline accounts are considered a part of the project, in this case it just refers to who is carrying it. (These accounts are cleared at the end, so it does not matter that our names are in them.) Transfers \uf0c1 Before the trip, I was really busy. It looked like Caroline was going to make most of arrangements and upfront payments, so I made a transfer to her Google Wallet to help her cover for some expenses ahead of time: 2015-02-01 * \"Transfer Martin -> Caroline on Google Wallet\" Income:Martin:Wallet -1000 USD Income:Caroline:Wallet 1000 USD Cash Conversions \uf0c1 Obtaining local currency was done by changing a small amount of cash at the airport (at a very bad rate): 2015-02-25 * \"Exchanged cash at XIC at CUN airport\" Assets:Cash:Caroline -100.00 USD @ 12.00 MXN Assets:Cash:Pesos 1200 MXN The \u201c Assets:Cash:Pesos \u201d account tracks our common pool of local currency that we use for various small expenses. Cash Expenses in US Dollars \uf0c1 Some local expenses will call for US money, which in this example I paid from my cash pocket: 2015-03-01 * \"Motmot Diving\" | \"Deposit for cenote diving\" Expenses:Scuba 50.00 USD Assets:Cash:Martin Cash Expenses in Local Currency \uf0c1 Paying cash using pesos from our shared stash of pesos looks like this: 2015-02-25 * \"UltraMar Ferry across to Cozumel\" Expenses:Transport:Bus 326 MXN Assets:Cash:Pesos Sometimes we even had to pay with a mix of US dollars and pesos. In this example, we ran out of pesos, so we have to give them dollars and pesos (all the restaurants and hotels in the beach part of Tulum accept US currency): 2015-03-01 * \"Hartwood\" | \"Dinner - ran out of pesos\" Expenses:Restaurant 1880 MXN Assets:Cash:Pesos -1400 MXN Assets:Cash:Martin -40.00 USD @ 12.00 MXN I used the unfavorable rate the restaurant was offering to accept dollars at (the market rate was 14.5 at the time). Individual Expenses \uf0c1 Here is an example of booking individual expenses using shared money. In order for us to have access to the reef for diving, we had to pay a \u201cmarine park\u201d fee of $2.50 per day to the island. This was a short trip where I dove only three days there and Caroline\u2019s fee was included in her course except for one day: 2015-02-25 * \"Marine Park (3 days Martin, 1 day Caroline)\" Expenses:Scuba:ParkFees:Martin 7.50 USD Expenses:Scuba:ParkFees:Caroline 2.50 USD Assets:Cash:Martin All that needs to be done is to book these to expense accounts with our names in them, which will get separated out at the end. Here is a more complicated example: the dive shop at Scuba Club Cozumel charged us a single bill at the end of our stay for all our rental gear and extra dives. All I did here was translate the itemized bill into a single transaction, booking to each other: 2015-03-01 * \"Scuba Club Cozumel\" | \"Dive shop bill\" ^69b409189b37 Income:Martin:CreditCard -381.64 USD Expenses:Scuba:Martin 27 USD ;; Regulator w/ Gauge Expenses:Scuba:Caroline 9 USD ;; Regulator w/ Gauge Expenses:Scuba:Martin 27 USD ;; BCD Expenses:Scuba:Caroline 9 USD ;; BCD Expenses:Scuba:Martin 6 USD ;; Fins Expenses:Scuba:Martin 24 USD ;; Wetsuit Expenses:Scuba:Caroline 8 USD ;; Wetsuit Expenses:Scuba:Caroline 9 USD ;; Dive computer Expenses:Scuba:Martin 5 USD ;; U/W Light Expenses:Scuba:Caroline 70 USD ;; Dive trip (2 tank) Expenses:Scuba:Martin 45 USD ;; Wreck Dive w/ Lite Expenses:Scuba:Martin 45 USD ;; Afternoon dive Expenses:Scuba:Caroline 45 USD ;; Afternoon dive Expenses:Scuba:Martin 28.64 USD ;; Taxes Expenses:Scuba:Caroline 24.00 USD ;; Taxes Final Balances \uf0c1 Of course you can use balance assertions at any time during the trip. For example, just before leaving at the Cancun airport we knew we wouldn\u2019t spend any more Mexican pesos for a while, so I counted what we had left after Caroline decided to splurge the remainder of them into overpriced chocolate sold at the airport shop: 2015-03-04 balance Assets:Cash:Pesos 65 MXN Ideally the bookkeeper should want to do this at a quiet moment at the end of every day or couple of days, which makes it easier to triangulate an expense you might have forgotten to enter (we are on vacation after all, in our relaxed state of mind we forget to write down stuff here and there). Clearing Asset Accounts \uf0c1 At the end of the trip, you should clear the final balances of all Assets and Liabilities accounts by transferring the remaining funds out to the participants, i.e., to the Income accounts. This should leave all the balances of the trip at zero and ensures all the cash put forward for trip expenses has been moved out to travelers. By the way, it doesn\u2019t matter much who keeps this money, because at the end we will end up making a single correcting transfer that should account for the balance required to create an even split. You can transfer it to anyone; the end result will be the same. Our clearing transaction looked like this: 2015-03-06 * \"Final transfer to clear internal balances to external ones\" Assets:Cash:Pesos -65 MXN Income:Martin:Cash:Foreign 60 MXN Income:Caroline:Cash 5 MXN Assets:Cash:Martin -330 USD Income:Martin:Cash 330 USD Assets:Cash:Caroline -140 USD Income:Caroline:Cash 140 USD 2015-03-07 balance Assets:Cash:Pesos 0 MXN 2015-03-07 balance Assets:Cash:Pesos 0 USD 2015-03-07 balance Assets:Cash:Martin 0 USD 2015-03-07 balance Assets:Cash:Caroline 0 USD We had three 20 peso bills, and I kept the bills for future travels. Caroline kept the 5 peso coin (forgot to hand it over as a tip). We transferred out the respective cash amounts we had been carrying together during the trip. How to Take Notes \uf0c1 During this trip I did not carry a laptop\u2014this was vacation after all. I like to disconnect. I did not carry a notebook either. Instead, I just took notes at the end of the day on a few sheets of paper at the hotel. This process took about 5-10 minutes each night, just recalling from memory and writing things down. These notes look like this: I made a paper spreadsheet where each line had The narration for the transaction (a description that would allow me to select an Expenses account later on) Who paid (which Assets or Income account the money came from) The amount (either in USD or MXN) After our trip, I sat down at the computer and typed the corresponding Beancount file . If I had a computer during my vacation I probably would have typed it as we went along. Of course, I had to do a few adjustments here and there because of mistakes. The bottom line is: if you\u2019re organized well, the overhead of doing this is minimal. Reconciling Expenses \uf0c1 Running bean-web on the trip\u2019s file: bean-web beancount/examples/sharing/cozumel2015.beancount You can view the balances in the \u201cAll Transactions\u201d view (click on \u201cAll Transactions\u201d). The Balance Sheet should show empty balances for Assets accounts: The balances of the equity accounts should reflect the total amount of currency conversions made during the trip. You can verify this by calculating the amount-weight average rate like this: 7539.00 / 559.88 ~= 13.465 USD/MXN (which is about right). Reviewing Contributions \uf0c1 The Income Statement should show a summary of all expenses and contributions to the project: The Income account balances show the total amounts of contributions for each person. Note that in creating the Income accounts, I went through the extra step of creating some specific accounts for each source of payment, like \u201cCaroline\u2019s Credit Card\u201d, etc. From this view, we can see that we contributed a total of 4254.28 USD (and were left with 65 MXN in hand) for this trip. The expenses side should match, considering the currency exchanges: 3694.40 + 7474 / 13.465 ~= 4249 USD which is approximately right (the small difference can be explained by the varying currency conversions). If you want to view the list of contribution payments and the final balance, click on a particular traveler\u2019s root account, e.g., \u201cIncome:Caroline\u201d (click on \u201cCaroline\u201d) which should take you to the Journal for that root account: This journal includes all the transactions in its sub-accounts. The final value at the bottom should show the total balance of those accounts, and thus, the amount of money Caroline contributed to this trip: 415 USD, and kept 5 MXN (in coin). We can do the same for Martin and find the final balance of 3839.28 USD and kept 60 MXN (in bills). You can also get the same amounts by using bean-query to achieve the same thing: ~/p/.../examples/sharing$ bean-query cozumel2015.beancount Input file: \"Mexico Trip: Cozumel & Tulum SCUBA Diving\" Ready with 105 directives (160 postings in 45 transactions). beancount> SELECT sum(position) WHERE account ~ '^Income:.*Caroline' sum_positio ----------- -415.00 USD 5 MXN Splitting Expenses \uf0c1 The expenses side of the Income Statement shows the breakdown of expenses. Note how some of the expense accounts are explicitly booked to each member separately by their account name, e.g., \u201c Expenses:Scuba:Martin \u201d. The other accounts, e.g. \u201c Expenses:Groceries \u201d are intended to be split. How we\u2019re going to carry this out is by adding a plugin that will transform all the transactions to actually split the postings which are intended to be shared, that is, postings without any member\u2019s name as a component. For example, the following input transaction: 2015-02-28 * \"Scuba Club Cozumel\" | \"Room bill\" Income:Martin:CreditCard -1380.40 USD Expenses:Scuba:Martin 178.50 USD Expenses:Accommodation 1201.90 USD Will be transformed by the plugin into one like this: 2015-02-28 * \"Scuba Club Cozumel\" | \"Room bill\" Income:Martin:CreditCard -1380.40 USD Expenses:Scuba:Martin 178.50 USD Expenses:Accommodation:Martin 600.95 USD Expenses:Accommodation:Caroline 600.95 USD Note that: Only Expenses accounts get split into multiple postings. Accounts with the name of a member are assumed by convention to be already attributed to that person. In order to achieve this, the plugin has to be provided with the names of the members. All the resulting Income and Expenses accounts include the name of a member. The plugin is enabled like this: plugin \"beancount.plugins.split_expenses\" \"Martin Caroline\" Reloading the web page after uncommenting this line from the input file and visiting the Income Statement page should show a long list of Expenses accounts split by person. Now, in order to generate the total amount of expenses incurred for each person, we have to produce the balance of all Expenses accounts with a member\u2019s name in it, accounts like \u201c Expenses:.*Martin \u201d. The web tool does not provide such a filtering capability at the moment 1 , but we can use the bean-query tool to produce the total of expenses for each person: beancount> SELECT sum(position) WHERE account ~ '^Expenses:.*Martin' sum_positio ----------- 2007.43 USD 3837.0 MXN beancount> SELECT sum(position) WHERE account '^Expenses:.*Caroline' sum_positio ----------- 1686.97 USD 3637.0 MXN This says \u201cMartin accrued expenses of 2007.43 USD and 3837.0 MXN.\u201d You can manually convert this to a dollar amount: yuzu:~$ dc -e '2007.43 3837.0 13.465 / +p' 2291.43 Or you can use the recently introduced \u201c CONVERT \u201d function of bean-query to do this: beancount> SELECT convert(sum(position), 'USD') WHERE account ~ '^Expenses:.*Martin' convert_sum_position_c_ --------------------------------- 2288.528901098901098901098901 USD beancount> SELECT convert(sum(position), 'USD') WHERE account ~ '^Expenses:.*Caroline' convert_sum_position_c_ --------------------------------- 1953.416886446886446886446886 USD (The difference between the 2291.43 and 2288.53 amounts can be attributed to the usage of slightly different exchange rates used in the converting transactions.) Similarly, you can generate the list of payments made by each person, e.g.: beancount> SELECT sum(position) WHERE account ~ '^Income:.*Caroline' Final Transfer \uf0c1 In order to figure out the total amount owed by each member, the process is similar: Simply sum up the balances of all the accounts attributed to that particular member: beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Caroline' convert_sum_position_c_ --------------------------------- 1538.783186813186813186813187 USD beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Martin' convert_sum_position_c_ ---------------------------------- -1546.355494505494505494505494 USD Notice that this includes the Income and Expenses accounts for that person. It\u2019s as if we had two separate ledgers merged into one. (Once again, the small differences can be attributed to differences in exchange rate over time.) We can now make a final transfer amount in order to account for each of our expenses; we\u2019ve agreed to round this to 1500 USD: 2015-03-06 * \"Final transfer from Caroline to Martin to pay for difference\" Income:Caroline:Wallet -1500 USD Income:Martin:Wallet 1500 USD If you uncomment this transaction from the input file (near the end), you will find corrected balances: beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Martin' convert_sum_position_c_ --------------------------------- -46.3554945054945054945054945 USD beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Caroline' convert_sum_position_c_ -------------------------------- 38.7831868131868131868131868 USD Updating your Personal Ledger \uf0c1 So great! Now we\u2019ve reconciled each other for the trip. But you still need to reflect these expenses in your personal ledger (if you have one). In this section, I will explain how you should reflect these transactions in that file. Account Names \uf0c1 First, let us take note that the accounts in your personal ledger do not have to match the account names used in the trip\u2019s ledger file. I never process those files together. (A good argument for not attempting this is that each trip will include account names from other people and I prefer not to have those leak into my main ledger file.) Using a Tag \uf0c1 I like to use a tag to report on the entire set of transactions related to the trip. In this example, I used the tag #trip-cozumel2015 . Booking Contributions \uf0c1 Your contributions into the project should be booked to a temporary holding account. I call mine \u201c Assets:Travel:Pending \u201d. For example, this transaction from the trip\u2019s file: 2015-02-25 * \"Yellow Transfers\" | \"SuperShuttle to Playa del Carmen\" Expenses:Transport:Bus 656 MXN Income:Martin:CreditCard -44.12 USD @ 14.86854 MXN will eventually be imported in my personal file and categorized like this: 2015-02-25 * \"YELLOW TRANSFER MX CO\" #trip-cozumel2015 Liabilities:US:BofA:CreditCard -44.12 USD Assets:Travel:Pending 44.12 USD You can conceptualize this as contributing money to a pending pool of cash which you will eventually receive expenses from. The same goes for cash that I brought for the trip: 2015-02-24 * \"Taking cash with me on the trip\" Assets:Cash -1200.00 USD Assets:Travel:Pending 1200.00 USD Note that absolutely all of the transactions related to the trip should be booked to that one account, including the inter-account transfers: 2015-03-06 * \"Final transfer from Mexico trip\" #trip-cozumel2015 Assets:Google:Wallet 1500 USD Assets:Travel:Pending -1500 USD Booking Expenses \uf0c1 After the trip is concluded, we want to convert the balance in the pending account to a list of Expenses. To find the list of expenses for yourself, you can issue a query like this: beancount> SELECT account, sum(position) WHERE account ~ '^Expenses:.*:Martin' GROUP BY 1 ORDER BY 1 account sum_positio ------------------------------- ----------- Expenses:Accommodation:Martin 735.45 USD Expenses:Alcohol:Martin 483 MXN Expenses:Bicycles:Martin 69.5 MXN Expenses:Flights:Martin 488.00 USD Expenses:Groceries:Martin 197.0 MXN Expenses:Museum:Martin 64 MXN Expenses:Restaurant:Martin 22.28 USD 1795.5 MXN Expenses:Scuba:Martin 506.14 USD Expenses:Scuba:ParkFees:Martin 7.50 USD Expenses:Tips:Martin 225 MXN 189.16 USD Expenses:Transport:Bus:Martin 709 MXN Expenses:Transport:Taxi:Martin 53.90 USD 294 MXN Expenses:Transport:Train:Martin 5.00 USD I suppose you could script away this part to remove the member\u2019s name from the account names and have the script spit output already formatted as a nicely formatted Transaction, but because the account names will not match the personal ledger file\u2019s 1-to-1, you will have to perform a manual conversion anyway, so I did not bother automating this. Furthermore, you only have a single one of these to make after your trip, so it\u2019s not worth spending too much time making this part easier 2 . Here\u2019s what the final transaction looks like; I have a section in my input file with this: 2015-02-25 event \"location\" \"Cozumel, Mexico\" ;; (1) pushtag #trip-mexico-cozumel-2015 \u2026 other transactions related to the trip\u2026 2015-03-01 event \"location\" \"Tulum, Mexico\" ;; (1) 2015-03-07 * \"Final reconciliation - Booking pending expenses for trip\" Expenses:Travel:Accommodation 735.45 USD Expenses:Food:Alcohol 483.0 MXN Expenses:Sports:Velo 69.5 MXN Expenses:Transportation:Flights 488.00 USD Expenses:Food:Grocery 197.0 MXN Expenses:Fun:Museum 64.0 MXN Expenses:Food:Restaurant 22.28 USD Expenses:Food:Restaurant 1795.5 MXN Expenses:Scuba:Dives 506.14 USD Expenses:Scuba:Fees 7.50 USD Expenses:Scuba:Tips 225.0 MXN Expenses:Scuba:Tips 189.16 USD Expenses:Transportation:Bus 709.0 MXN Expenses:Transportation:Taxi 53.90 USD Expenses:Transportation:Taxi 294.0 MXN Expenses:Transportation:Train 5.00 USD Assets:Cash:Foreign 60.0 MXN ;; (2) Assets:Cash 330.00 USD ;; (3) Assets:Travel:Pending -2337.43 USD ;; (4) Assets:Travel:Pending -288.67 USD @@ 3897.0 MXN ;; (5) 2015-03-07 * \"Writing off difference as gift to Caroline\" ;; (6) Assets:Travel:Pending -43.18 USD Expenses:Gifts 2015-03-14 balance Assets:Travel:Pending 0 USD ;; (7) 2015-03-14 balance Assets:Travel:Pending 0 MXN poptag #trip-mexico-cozumel-2015 Observations: I used \u201cevent\u201d directives in order to track my location. I\u2019m doing this because I will eventually need it for immigration purposes (and just for fun, to track the days I spend in places). I moved the extra cash I kept to my \u201cforeign cash pocket\u201d account: Assets:Cash:Foreign I \u201cmoved\u201d back the cash I had with me after I returned from the trip. This leg removes the USD expenses from the Pending account. The leg removes the MXN expenses from the Pending account. I calculated its amount manually (3897 / 13.5 ~= 288.67 USD) and use the full amount as the exchange rate. I want to completely zero out the Pending account after the trip, and so I write off the excess amount we agreed not to pay (the difference between the imbalance and 1500.00 USD). Finally, I assert that the Pending account is empty of USD and of MXN. I found the missing amounts by running bean-check or using bean-doctor context on an incomplete transaction from Emacs. Generating Reports \uf0c1 If you want to automate the generation of reports for each of the participants in a trip, there is a script that generates text (and eventually CSV) reports for the queries mentioned in the previous sections. You can use this script to provide expenses status after or even during the trip or project, for each of the participants. The script lives in the split_expenses plugin itself, and you invoke it like this: python3 -m beancount.plugins.split_expenses --text= For each person, it will generate the following reports: A detailed journal of expenses A detailed journal of contributions A breakdown of expenses for each category (account type) Finally, it will also generate a final balance for each person, which you can use to send final reconciling transfers to each other. Try it now on one of the example files provided with Beancount. Other Examples \uf0c1 There is another example file that shows how to share expenses between three participants in duxbury2015.beancount . Look to more example files to be introduced over time. Conclusion \uf0c1 There is more than just one way to carry out the task we describe here. However, the method we present extends nicely to a larger group of participants, allows us to account for situations where particular expenses are incurred for one individual as part of a group, and finally, allows for a non-even split between the participants. This is pretty general. It\u2019s also a short step to accounting for an ongoing project with a longer term. The ideas presented in this document provide a nice use case for the usage of the double-entry method in a simple setting. I hope that working through this use case will help people develop the intuitions necessary in using the double-entry method. As our SQL-like query language matures, bean-web will eventually allow the user to create views from a set of transactions filtered from an expression. This will be implemented eventually. \u21a9 Note that we could implement a special \u201cbeancount\u201d supported format to the bean-query tool in order to write out balances in Transaction form. I\u2019m not sure how useful that would be but it\u2019s an idea worth considering for the future. \u21a9","title":"Sharing Expenses with Beancount"},{"location":"sharing_expenses_with_beancount.html#sharing-expenses-in-beancount","text":"Martin Blais , May 2015 http://furius.ca/beancount/doc/shared","title":"Sharing Expenses in Beancount"},{"location":"sharing_expenses_with_beancount.html#introduction","text":"This document presents a method for precisely and easily accounting for shared expenses in complicated group situations. For example, traveling with a group of friends where people pay for different things. We show it is possible to deal with expenses to be split evenly among the group as well as group expenses that should be allocated to specific persons. The method uses the double-entry accounting system. The essence of the method consists in separating the accounting of expenses and their associated payments. This makes it much easier to deal with shared costs, because using this method it doesn\u2019t matter who pays for what: we reconcile the precise amounts owed for each individual at the end and automatically compute final correcting transfers for each. This article is structured around a simple trip with expenses shared equally by two people. However, the same method generalizes to any type of project with incomes and expenses to be shared among multiple participants, and splitting expenses evenly or not.","title":"Introduction"},{"location":"sharing_expenses_with_beancount.html#a-travel-example","text":"Martin (the author) and Caroline (his girlfriend) visited Mexico in March 2015. We visited the island of Cozumel for a SCUBA diving trip for three days and then headed to Tulum for two days to relax and dive in the cenotes . Our assumptions are: Chaotic payments . We lead very busy lives\u2026 both of us are going to make payments ahead of time and during the trip, without any forethought regarding who will pay for what, though we will carefully record every payment. Each of us just pays for whatever preparation costs as needed to arrange this trip. For example, Caroline selected and booked flights early while I paid for the resort and booked the rental and activities at the local dive shop one week before departure. Use of shared and individual assets. Both of us are bringing cash, and are going to make payments during the trip from our respective wallets as well as from a shared pool of cash converted to local currency (Mexican pesos, for which I will use the \u201cMXN\u201d symbol), and we will use credit cards as necessary before and during the trip. Multiple currencies. Some of our expenses will be denominated in US dollars and some in Mexican pesos. For example, flights were paid in US dollars, local meals and the accommodation were paid in pesos, but the local dive shop charged us dollars. Converted amounts will come from both cash and credit cards sources. Individual expenses in a shared pool. While most of the expenses are to be shared equally, some of the expenses will apply to only one of us, and we want to account for those explicitly. For example, Caroline took a SCUBA certification course ( PADI Open Water ) and will pay for that on her own; similarly, she should not be paying for Martin\u2019s expensive boat diving costs. To complicate matters, the dive shop issued us a single joint bill for everything at the end of our stay.","title":"A Travel Example"},{"location":"sharing_expenses_with_beancount.html#a-note-about-sharing","text":"I feel that something should be said about the \u201csharing\u201d aspect of our expenses, as this topic has come up on previous discussions on the mailing-lists involving sharing examples. We are nitpicking on purpose. For the purpose of this exercise, we are accounting for every little penny spent in an incredibly detailed manner. The point of this document is specifically to show how a complex set of transactions well accounted for can be efficiently and simply disentangled to a precise accounting of expenses for each participant, regardless of who actually makes the payments. We are not cheapskates. We will assume that we\u2019ve decided to split expenses evenly. Our \u201cgenerosity\u201d to each other is not a topic relevant to this document. We\u2019re both well compensated working professionals and you can assume that we\u2019ve agreed to split the common costs for this trip evenly (50/50). One of the attributes of the method we show here is that the decision of how we choose to split expenses can be made separately from the actual payment of costs. For example, we may decide in the end that I pay for \u2154rd of the trip, but that can be decided precisely rather than in an ad-hoc \u201coh, I think I remember I paid for this\u201d manner. This is especially useful in a larger group of people because when expenses aren\u2019t tracked accurately, usually everyone is left with a feeling that they paid more than the others. The sum of the perceived fractions of expenses paid in a group is always greater than 100%...","title":"A Note About Sharing"},{"location":"sharing_expenses_with_beancount.html#overview-of-the-method","text":"In this section we present a brief illustrated overview of the method. A set of common Assets accounts that belong to the project, and book all our individual expenses and transfer for the trip as coming from external Income accounts: During the trip, we use the common Assets to make expenses. Most of the expenses are attributed to both of us (and to be shared eventually), but some of the expenses are intended to be attributed to each of us individually: After the trip, remaining Assets (like cash we walked home with) gets distributed back to ourselves to zero out the balances of the Assets accounts and we record this through contra postings to Income accounts: Finally, the list of shared Expenses are split between each other\u2014using a plugin that forks every posting that is intended to be a shared expense\u2014and the final amount is used to make a final transfer between each other so that we\u2019ve each paid for our respective expenses and we\u2019re square: Note that the final balance of expenses for each participant may differ, and these are due to particular expenses that were attributed separately, or if we decide to split the total unevenly.","title":"Overview of the Method"},{"location":"sharing_expenses_with_beancount.html#how-to-track-expenses","text":"In order to write down all expenses for this trip we will open a brand new Beancount input file. Despite the fact that the expenses will come from each person\u2019s personal accounts, it is useful to think of the trip as a special project, as if it were a separate entity, e.g., a company that exists only for the duration of the trip. The example file we wrote for our trip can be found here and should help you follow along this text.","title":"How to Track Expenses"},{"location":"sharing_expenses_with_beancount.html#accounts","text":"The set of accounts in the input file should not have to match your personal Beancount file\u2019s account names. The accounts we will use include accounts that correspond to Martin and Caroline\u2019s personal accounts but with generic names (e.g., Income:Martin:CreditCard instead of Liabilities:US:Chase ), and expense accounts may not match any regular expenses in my personal Beancount file\u2014it\u2019s not important. As a convention, any account that pertains specifically to one of the travelers will include that person\u2019s name in the account name. For example, Caroline\u2019s credit card will be named \u201c Income:Caroline:CreditCard \u201d. This is important, as we will use this later on to split contributions and expenses. Let\u2019s examine the different types of accounts we will need to carry this out.","title":"Accounts"},{"location":"sharing_expenses_with_beancount.html#external-income-accounts","text":"The \u201cproject\u201d will receive income in the form of transfers from personal accounts of either traveler. These are accounts we will consider external to the project and so will define them as Income accounts: ;; External accounts for Martin. 2015-02-01 open Income:Martin:Cash 2015-02-01 open Income:Martin:Cash:Foreign 2015-02-01 open Income:Martin:Wallet 2015-02-01 open Income:Martin:CreditCard ;; External accounts for Caroline. 2015-02-01 open Income:Caroline:Cash 2015-02-01 open Income:Caroline:Wallet 2015-02-01 open Income:Caroline:MetroCard 2015-02-01 open Income:Caroline:CreditCard Transactions carried out from these accounts must be copied from your personal Beancount file. Obviously, you must be careful to include all the transactions pertaining to the trip. I used a tag to do this in my personal file.","title":"External Income Accounts"},{"location":"sharing_expenses_with_beancount.html#assets-liabilities-accounts","text":"There will be a few Asset accounts that will be active and exist for the duration of the trip. These temporary accounts will be zero\u2019ed out at the end of it. One example is a pool of petty cash in local currency: 2015-02-01 open Assets:Cash:Pesos description: \"A shared account to contain our pocket of pesos\" We also carried cash in each of our pockets while traveling, so I created two separate accounts for that: 2015-02-01 open Assets:Cash:Martin description: \"Cash for the trip held by Martin\" 2015-02-01 open Assets:Cash:Caroline description: \"Cash for the trip held by Caroline\" Note however that despite their individual names, those accounts are considered as part of the project. It was just convenient to separately track balances for the cash we each held during the trip.","title":"Assets & Liabilities Accounts"},{"location":"sharing_expenses_with_beancount.html#expenses-accounts","text":"We will define various accounts to book our Expenses to. For example, \u201c Expenses:Flights \u201d will contain our costs associated with flight travel. For convenience, and because there are many types of expenses in this file, we chose to leverage the \u201cauto-accounts\u201d plugin and let Beancount automatically open these accounts: plugin \"beancount.ops.auto_accounts\" The great majority of these accounts are for shared expenses to be split between us. For example, shared SCUBA diving expenses will be booked to \u201c Expenses:Scuba \u201d. However, for expenses that are intended to be covered by one of us only, we simply include the name of the traveler in the account name. For example, Martin\u2019s extra costs for boat diving will be booked to the \u201c Expenses:Scuba:Martin \u201d account.","title":"Expenses Accounts"},{"location":"sharing_expenses_with_beancount.html#example-transactions","text":"Let\u2019s turn our attention to the different types of transactions present in the file. In this section I will walk you through some representative transactions (the names I give to these are arbitrary).","title":"Example Transactions"},{"location":"sharing_expenses_with_beancount.html#contributing-transactions","text":"Contributions to the project\u2019s costs are usually done in the form of expenses paid from external accounts. For example, Caroline paying for flights with her credit card looks like this: 2015-02-01 * \"Flights to Cancun\" Income:Caroline:CreditCard -976.00 USD Expenses:Flights Martin paying for the taxi to the airport looks like this: 2015-02-25 * \"Taxi to airport\" ^433f66ea0e4e Expenses:Transport:Taxi 62.80 USD Income:Martin:CreditCard","title":"Contributing Transactions"},{"location":"sharing_expenses_with_beancount.html#bringing-cash","text":"We both brought some cash on us, as we were warned it might be difficult to use credit cards in Mexico. In my personal Beancount file, the cash account is \u201c Assets:Cash \u201d but here it must get booked as an external contribution with my name on it: ;; Initial cash on us. 2015-02-24 * \"Getting cash for travel\" Income:Martin:Cash -1200 USD Assets:Cash:Martin 1200 USD Caroline\u2019s cash is similar: 2015-02-24 * \"Getting cash for travel\" Income:Caroline:Cash -300 USD Assets:Cash:Caroline 300 USD Once again, note that the Assets:Cash:Martin and Assets:Cash:Caroline accounts are considered a part of the project, in this case it just refers to who is carrying it. (These accounts are cleared at the end, so it does not matter that our names are in them.)","title":"Bringing Cash"},{"location":"sharing_expenses_with_beancount.html#transfers","text":"Before the trip, I was really busy. It looked like Caroline was going to make most of arrangements and upfront payments, so I made a transfer to her Google Wallet to help her cover for some expenses ahead of time: 2015-02-01 * \"Transfer Martin -> Caroline on Google Wallet\" Income:Martin:Wallet -1000 USD Income:Caroline:Wallet 1000 USD","title":"Transfers"},{"location":"sharing_expenses_with_beancount.html#cash-conversions","text":"Obtaining local currency was done by changing a small amount of cash at the airport (at a very bad rate): 2015-02-25 * \"Exchanged cash at XIC at CUN airport\" Assets:Cash:Caroline -100.00 USD @ 12.00 MXN Assets:Cash:Pesos 1200 MXN The \u201c Assets:Cash:Pesos \u201d account tracks our common pool of local currency that we use for various small expenses.","title":"Cash Conversions"},{"location":"sharing_expenses_with_beancount.html#cash-expenses-in-us-dollars","text":"Some local expenses will call for US money, which in this example I paid from my cash pocket: 2015-03-01 * \"Motmot Diving\" | \"Deposit for cenote diving\" Expenses:Scuba 50.00 USD Assets:Cash:Martin","title":"Cash Expenses in US Dollars"},{"location":"sharing_expenses_with_beancount.html#cash-expenses-in-local-currency","text":"Paying cash using pesos from our shared stash of pesos looks like this: 2015-02-25 * \"UltraMar Ferry across to Cozumel\" Expenses:Transport:Bus 326 MXN Assets:Cash:Pesos Sometimes we even had to pay with a mix of US dollars and pesos. In this example, we ran out of pesos, so we have to give them dollars and pesos (all the restaurants and hotels in the beach part of Tulum accept US currency): 2015-03-01 * \"Hartwood\" | \"Dinner - ran out of pesos\" Expenses:Restaurant 1880 MXN Assets:Cash:Pesos -1400 MXN Assets:Cash:Martin -40.00 USD @ 12.00 MXN I used the unfavorable rate the restaurant was offering to accept dollars at (the market rate was 14.5 at the time).","title":"Cash Expenses in Local Currency"},{"location":"sharing_expenses_with_beancount.html#individual-expenses","text":"Here is an example of booking individual expenses using shared money. In order for us to have access to the reef for diving, we had to pay a \u201cmarine park\u201d fee of $2.50 per day to the island. This was a short trip where I dove only three days there and Caroline\u2019s fee was included in her course except for one day: 2015-02-25 * \"Marine Park (3 days Martin, 1 day Caroline)\" Expenses:Scuba:ParkFees:Martin 7.50 USD Expenses:Scuba:ParkFees:Caroline 2.50 USD Assets:Cash:Martin All that needs to be done is to book these to expense accounts with our names in them, which will get separated out at the end. Here is a more complicated example: the dive shop at Scuba Club Cozumel charged us a single bill at the end of our stay for all our rental gear and extra dives. All I did here was translate the itemized bill into a single transaction, booking to each other: 2015-03-01 * \"Scuba Club Cozumel\" | \"Dive shop bill\" ^69b409189b37 Income:Martin:CreditCard -381.64 USD Expenses:Scuba:Martin 27 USD ;; Regulator w/ Gauge Expenses:Scuba:Caroline 9 USD ;; Regulator w/ Gauge Expenses:Scuba:Martin 27 USD ;; BCD Expenses:Scuba:Caroline 9 USD ;; BCD Expenses:Scuba:Martin 6 USD ;; Fins Expenses:Scuba:Martin 24 USD ;; Wetsuit Expenses:Scuba:Caroline 8 USD ;; Wetsuit Expenses:Scuba:Caroline 9 USD ;; Dive computer Expenses:Scuba:Martin 5 USD ;; U/W Light Expenses:Scuba:Caroline 70 USD ;; Dive trip (2 tank) Expenses:Scuba:Martin 45 USD ;; Wreck Dive w/ Lite Expenses:Scuba:Martin 45 USD ;; Afternoon dive Expenses:Scuba:Caroline 45 USD ;; Afternoon dive Expenses:Scuba:Martin 28.64 USD ;; Taxes Expenses:Scuba:Caroline 24.00 USD ;; Taxes","title":"Individual Expenses"},{"location":"sharing_expenses_with_beancount.html#final-balances","text":"Of course you can use balance assertions at any time during the trip. For example, just before leaving at the Cancun airport we knew we wouldn\u2019t spend any more Mexican pesos for a while, so I counted what we had left after Caroline decided to splurge the remainder of them into overpriced chocolate sold at the airport shop: 2015-03-04 balance Assets:Cash:Pesos 65 MXN Ideally the bookkeeper should want to do this at a quiet moment at the end of every day or couple of days, which makes it easier to triangulate an expense you might have forgotten to enter (we are on vacation after all, in our relaxed state of mind we forget to write down stuff here and there).","title":"Final Balances"},{"location":"sharing_expenses_with_beancount.html#clearing-asset-accounts","text":"At the end of the trip, you should clear the final balances of all Assets and Liabilities accounts by transferring the remaining funds out to the participants, i.e., to the Income accounts. This should leave all the balances of the trip at zero and ensures all the cash put forward for trip expenses has been moved out to travelers. By the way, it doesn\u2019t matter much who keeps this money, because at the end we will end up making a single correcting transfer that should account for the balance required to create an even split. You can transfer it to anyone; the end result will be the same. Our clearing transaction looked like this: 2015-03-06 * \"Final transfer to clear internal balances to external ones\" Assets:Cash:Pesos -65 MXN Income:Martin:Cash:Foreign 60 MXN Income:Caroline:Cash 5 MXN Assets:Cash:Martin -330 USD Income:Martin:Cash 330 USD Assets:Cash:Caroline -140 USD Income:Caroline:Cash 140 USD 2015-03-07 balance Assets:Cash:Pesos 0 MXN 2015-03-07 balance Assets:Cash:Pesos 0 USD 2015-03-07 balance Assets:Cash:Martin 0 USD 2015-03-07 balance Assets:Cash:Caroline 0 USD We had three 20 peso bills, and I kept the bills for future travels. Caroline kept the 5 peso coin (forgot to hand it over as a tip). We transferred out the respective cash amounts we had been carrying together during the trip.","title":"Clearing Asset Accounts"},{"location":"sharing_expenses_with_beancount.html#how-to-take-notes","text":"During this trip I did not carry a laptop\u2014this was vacation after all. I like to disconnect. I did not carry a notebook either. Instead, I just took notes at the end of the day on a few sheets of paper at the hotel. This process took about 5-10 minutes each night, just recalling from memory and writing things down. These notes look like this: I made a paper spreadsheet where each line had The narration for the transaction (a description that would allow me to select an Expenses account later on) Who paid (which Assets or Income account the money came from) The amount (either in USD or MXN) After our trip, I sat down at the computer and typed the corresponding Beancount file . If I had a computer during my vacation I probably would have typed it as we went along. Of course, I had to do a few adjustments here and there because of mistakes. The bottom line is: if you\u2019re organized well, the overhead of doing this is minimal.","title":"How to Take Notes"},{"location":"sharing_expenses_with_beancount.html#reconciling-expenses","text":"Running bean-web on the trip\u2019s file: bean-web beancount/examples/sharing/cozumel2015.beancount You can view the balances in the \u201cAll Transactions\u201d view (click on \u201cAll Transactions\u201d). The Balance Sheet should show empty balances for Assets accounts: The balances of the equity accounts should reflect the total amount of currency conversions made during the trip. You can verify this by calculating the amount-weight average rate like this: 7539.00 / 559.88 ~= 13.465 USD/MXN (which is about right).","title":"Reconciling Expenses"},{"location":"sharing_expenses_with_beancount.html#reviewing-contributions","text":"The Income Statement should show a summary of all expenses and contributions to the project: The Income account balances show the total amounts of contributions for each person. Note that in creating the Income accounts, I went through the extra step of creating some specific accounts for each source of payment, like \u201cCaroline\u2019s Credit Card\u201d, etc. From this view, we can see that we contributed a total of 4254.28 USD (and were left with 65 MXN in hand) for this trip. The expenses side should match, considering the currency exchanges: 3694.40 + 7474 / 13.465 ~= 4249 USD which is approximately right (the small difference can be explained by the varying currency conversions). If you want to view the list of contribution payments and the final balance, click on a particular traveler\u2019s root account, e.g., \u201cIncome:Caroline\u201d (click on \u201cCaroline\u201d) which should take you to the Journal for that root account: This journal includes all the transactions in its sub-accounts. The final value at the bottom should show the total balance of those accounts, and thus, the amount of money Caroline contributed to this trip: 415 USD, and kept 5 MXN (in coin). We can do the same for Martin and find the final balance of 3839.28 USD and kept 60 MXN (in bills). You can also get the same amounts by using bean-query to achieve the same thing: ~/p/.../examples/sharing$ bean-query cozumel2015.beancount Input file: \"Mexico Trip: Cozumel & Tulum SCUBA Diving\" Ready with 105 directives (160 postings in 45 transactions). beancount> SELECT sum(position) WHERE account ~ '^Income:.*Caroline' sum_positio ----------- -415.00 USD 5 MXN","title":"Reviewing Contributions"},{"location":"sharing_expenses_with_beancount.html#splitting-expenses","text":"The expenses side of the Income Statement shows the breakdown of expenses. Note how some of the expense accounts are explicitly booked to each member separately by their account name, e.g., \u201c Expenses:Scuba:Martin \u201d. The other accounts, e.g. \u201c Expenses:Groceries \u201d are intended to be split. How we\u2019re going to carry this out is by adding a plugin that will transform all the transactions to actually split the postings which are intended to be shared, that is, postings without any member\u2019s name as a component. For example, the following input transaction: 2015-02-28 * \"Scuba Club Cozumel\" | \"Room bill\" Income:Martin:CreditCard -1380.40 USD Expenses:Scuba:Martin 178.50 USD Expenses:Accommodation 1201.90 USD Will be transformed by the plugin into one like this: 2015-02-28 * \"Scuba Club Cozumel\" | \"Room bill\" Income:Martin:CreditCard -1380.40 USD Expenses:Scuba:Martin 178.50 USD Expenses:Accommodation:Martin 600.95 USD Expenses:Accommodation:Caroline 600.95 USD Note that: Only Expenses accounts get split into multiple postings. Accounts with the name of a member are assumed by convention to be already attributed to that person. In order to achieve this, the plugin has to be provided with the names of the members. All the resulting Income and Expenses accounts include the name of a member. The plugin is enabled like this: plugin \"beancount.plugins.split_expenses\" \"Martin Caroline\" Reloading the web page after uncommenting this line from the input file and visiting the Income Statement page should show a long list of Expenses accounts split by person. Now, in order to generate the total amount of expenses incurred for each person, we have to produce the balance of all Expenses accounts with a member\u2019s name in it, accounts like \u201c Expenses:.*Martin \u201d. The web tool does not provide such a filtering capability at the moment 1 , but we can use the bean-query tool to produce the total of expenses for each person: beancount> SELECT sum(position) WHERE account ~ '^Expenses:.*Martin' sum_positio ----------- 2007.43 USD 3837.0 MXN beancount> SELECT sum(position) WHERE account '^Expenses:.*Caroline' sum_positio ----------- 1686.97 USD 3637.0 MXN This says \u201cMartin accrued expenses of 2007.43 USD and 3837.0 MXN.\u201d You can manually convert this to a dollar amount: yuzu:~$ dc -e '2007.43 3837.0 13.465 / +p' 2291.43 Or you can use the recently introduced \u201c CONVERT \u201d function of bean-query to do this: beancount> SELECT convert(sum(position), 'USD') WHERE account ~ '^Expenses:.*Martin' convert_sum_position_c_ --------------------------------- 2288.528901098901098901098901 USD beancount> SELECT convert(sum(position), 'USD') WHERE account ~ '^Expenses:.*Caroline' convert_sum_position_c_ --------------------------------- 1953.416886446886446886446886 USD (The difference between the 2291.43 and 2288.53 amounts can be attributed to the usage of slightly different exchange rates used in the converting transactions.) Similarly, you can generate the list of payments made by each person, e.g.: beancount> SELECT sum(position) WHERE account ~ '^Income:.*Caroline'","title":"Splitting Expenses"},{"location":"sharing_expenses_with_beancount.html#final-transfer","text":"In order to figure out the total amount owed by each member, the process is similar: Simply sum up the balances of all the accounts attributed to that particular member: beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Caroline' convert_sum_position_c_ --------------------------------- 1538.783186813186813186813187 USD beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Martin' convert_sum_position_c_ ---------------------------------- -1546.355494505494505494505494 USD Notice that this includes the Income and Expenses accounts for that person. It\u2019s as if we had two separate ledgers merged into one. (Once again, the small differences can be attributed to differences in exchange rate over time.) We can now make a final transfer amount in order to account for each of our expenses; we\u2019ve agreed to round this to 1500 USD: 2015-03-06 * \"Final transfer from Caroline to Martin to pay for difference\" Income:Caroline:Wallet -1500 USD Income:Martin:Wallet 1500 USD If you uncomment this transaction from the input file (near the end), you will find corrected balances: beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Martin' convert_sum_position_c_ --------------------------------- -46.3554945054945054945054945 USD beancount> SELECT convert(sum(position), 'USD') WHERE account ~ 'Caroline' convert_sum_position_c_ -------------------------------- 38.7831868131868131868131868 USD","title":"Final Transfer"},{"location":"sharing_expenses_with_beancount.html#updating-your-personal-ledger","text":"So great! Now we\u2019ve reconciled each other for the trip. But you still need to reflect these expenses in your personal ledger (if you have one). In this section, I will explain how you should reflect these transactions in that file.","title":"Updating your Personal Ledger"},{"location":"sharing_expenses_with_beancount.html#account-names","text":"First, let us take note that the accounts in your personal ledger do not have to match the account names used in the trip\u2019s ledger file. I never process those files together. (A good argument for not attempting this is that each trip will include account names from other people and I prefer not to have those leak into my main ledger file.)","title":"Account Names"},{"location":"sharing_expenses_with_beancount.html#using-a-tag","text":"I like to use a tag to report on the entire set of transactions related to the trip. In this example, I used the tag #trip-cozumel2015 .","title":"Using a Tag"},{"location":"sharing_expenses_with_beancount.html#booking-contributions","text":"Your contributions into the project should be booked to a temporary holding account. I call mine \u201c Assets:Travel:Pending \u201d. For example, this transaction from the trip\u2019s file: 2015-02-25 * \"Yellow Transfers\" | \"SuperShuttle to Playa del Carmen\" Expenses:Transport:Bus 656 MXN Income:Martin:CreditCard -44.12 USD @ 14.86854 MXN will eventually be imported in my personal file and categorized like this: 2015-02-25 * \"YELLOW TRANSFER MX CO\" #trip-cozumel2015 Liabilities:US:BofA:CreditCard -44.12 USD Assets:Travel:Pending 44.12 USD You can conceptualize this as contributing money to a pending pool of cash which you will eventually receive expenses from. The same goes for cash that I brought for the trip: 2015-02-24 * \"Taking cash with me on the trip\" Assets:Cash -1200.00 USD Assets:Travel:Pending 1200.00 USD Note that absolutely all of the transactions related to the trip should be booked to that one account, including the inter-account transfers: 2015-03-06 * \"Final transfer from Mexico trip\" #trip-cozumel2015 Assets:Google:Wallet 1500 USD Assets:Travel:Pending -1500 USD","title":"Booking Contributions"},{"location":"sharing_expenses_with_beancount.html#booking-expenses","text":"After the trip is concluded, we want to convert the balance in the pending account to a list of Expenses. To find the list of expenses for yourself, you can issue a query like this: beancount> SELECT account, sum(position) WHERE account ~ '^Expenses:.*:Martin' GROUP BY 1 ORDER BY 1 account sum_positio ------------------------------- ----------- Expenses:Accommodation:Martin 735.45 USD Expenses:Alcohol:Martin 483 MXN Expenses:Bicycles:Martin 69.5 MXN Expenses:Flights:Martin 488.00 USD Expenses:Groceries:Martin 197.0 MXN Expenses:Museum:Martin 64 MXN Expenses:Restaurant:Martin 22.28 USD 1795.5 MXN Expenses:Scuba:Martin 506.14 USD Expenses:Scuba:ParkFees:Martin 7.50 USD Expenses:Tips:Martin 225 MXN 189.16 USD Expenses:Transport:Bus:Martin 709 MXN Expenses:Transport:Taxi:Martin 53.90 USD 294 MXN Expenses:Transport:Train:Martin 5.00 USD I suppose you could script away this part to remove the member\u2019s name from the account names and have the script spit output already formatted as a nicely formatted Transaction, but because the account names will not match the personal ledger file\u2019s 1-to-1, you will have to perform a manual conversion anyway, so I did not bother automating this. Furthermore, you only have a single one of these to make after your trip, so it\u2019s not worth spending too much time making this part easier 2 . Here\u2019s what the final transaction looks like; I have a section in my input file with this: 2015-02-25 event \"location\" \"Cozumel, Mexico\" ;; (1) pushtag #trip-mexico-cozumel-2015 \u2026 other transactions related to the trip\u2026 2015-03-01 event \"location\" \"Tulum, Mexico\" ;; (1) 2015-03-07 * \"Final reconciliation - Booking pending expenses for trip\" Expenses:Travel:Accommodation 735.45 USD Expenses:Food:Alcohol 483.0 MXN Expenses:Sports:Velo 69.5 MXN Expenses:Transportation:Flights 488.00 USD Expenses:Food:Grocery 197.0 MXN Expenses:Fun:Museum 64.0 MXN Expenses:Food:Restaurant 22.28 USD Expenses:Food:Restaurant 1795.5 MXN Expenses:Scuba:Dives 506.14 USD Expenses:Scuba:Fees 7.50 USD Expenses:Scuba:Tips 225.0 MXN Expenses:Scuba:Tips 189.16 USD Expenses:Transportation:Bus 709.0 MXN Expenses:Transportation:Taxi 53.90 USD Expenses:Transportation:Taxi 294.0 MXN Expenses:Transportation:Train 5.00 USD Assets:Cash:Foreign 60.0 MXN ;; (2) Assets:Cash 330.00 USD ;; (3) Assets:Travel:Pending -2337.43 USD ;; (4) Assets:Travel:Pending -288.67 USD @@ 3897.0 MXN ;; (5) 2015-03-07 * \"Writing off difference as gift to Caroline\" ;; (6) Assets:Travel:Pending -43.18 USD Expenses:Gifts 2015-03-14 balance Assets:Travel:Pending 0 USD ;; (7) 2015-03-14 balance Assets:Travel:Pending 0 MXN poptag #trip-mexico-cozumel-2015 Observations: I used \u201cevent\u201d directives in order to track my location. I\u2019m doing this because I will eventually need it for immigration purposes (and just for fun, to track the days I spend in places). I moved the extra cash I kept to my \u201cforeign cash pocket\u201d account: Assets:Cash:Foreign I \u201cmoved\u201d back the cash I had with me after I returned from the trip. This leg removes the USD expenses from the Pending account. The leg removes the MXN expenses from the Pending account. I calculated its amount manually (3897 / 13.5 ~= 288.67 USD) and use the full amount as the exchange rate. I want to completely zero out the Pending account after the trip, and so I write off the excess amount we agreed not to pay (the difference between the imbalance and 1500.00 USD). Finally, I assert that the Pending account is empty of USD and of MXN. I found the missing amounts by running bean-check or using bean-doctor context on an incomplete transaction from Emacs.","title":"Booking Expenses"},{"location":"sharing_expenses_with_beancount.html#generating-reports","text":"If you want to automate the generation of reports for each of the participants in a trip, there is a script that generates text (and eventually CSV) reports for the queries mentioned in the previous sections. You can use this script to provide expenses status after or even during the trip or project, for each of the participants. The script lives in the split_expenses plugin itself, and you invoke it like this: python3 -m beancount.plugins.split_expenses --text= For each person, it will generate the following reports: A detailed journal of expenses A detailed journal of contributions A breakdown of expenses for each category (account type) Finally, it will also generate a final balance for each person, which you can use to send final reconciling transfers to each other. Try it now on one of the example files provided with Beancount.","title":"Generating Reports"},{"location":"sharing_expenses_with_beancount.html#other-examples","text":"There is another example file that shows how to share expenses between three participants in duxbury2015.beancount . Look to more example files to be introduced over time.","title":"Other Examples"},{"location":"sharing_expenses_with_beancount.html#conclusion","text":"There is more than just one way to carry out the task we describe here. However, the method we present extends nicely to a larger group of participants, allows us to account for situations where particular expenses are incurred for one individual as part of a group, and finally, allows for a non-even split between the participants. This is pretty general. It\u2019s also a short step to accounting for an ongoing project with a longer term. The ideas presented in this document provide a nice use case for the usage of the double-entry method in a simple setting. I hope that working through this use case will help people develop the intuitions necessary in using the double-entry method. As our SQL-like query language matures, bean-web will eventually allow the user to create views from a set of transactions filtered from an expression. This will be implemented eventually. \u21a9 Note that we could implement a special \u201cbeancount\u201d supported format to the bean-query tool in order to write out balances in Transaction form. I\u2019m not sure how useful that would be but it\u2019s an idea worth considering for the future. \u21a9","title":"Conclusion"},{"location":"stock_vesting_in_beancount.html","text":"Stock Vesting in Beancount \uf0c1 Martin Blais , June 2015 http://furius.ca/beancount/doc/vesting Introduction \uf0c1 This document explains the vesting of restricted stock units in Beancount, by way of an example. This example may not exactly match your situation, but enough detail is provided that you should be able to adapt it for your own particular differences. A working example file can be found here to follow along with this text. Restricted Stock Compensation \uf0c1 Many technology companies offer their employees incentive compensation in the form of \u201cgrants\u201d (or \u201cawards\u201d) of \u201crestricted stock units\u201d (RSU), which is essentially a promise for the \u201crelease\u201d to you of actual shares in the future. The stock is \u201crestricted\u201d in the sense that you cannot access it\u2014you only receive it when it \u201cvests\u201d, and this happens based on a schedule. Typically, you are promised a fixed number of shares that vest every quarter or every month over a period of 3 or 4 years. If you leave the company, your remaining unvested shares are lost. One way you can view these RSUs is as an asset, a receivable that arrives regularly over time. These RSUs are essentially compensation denominated in the currency of the company\u2019s shares itself. We want to track the unraveling of these unvested units, and correctly account for their conversion to real stock with a cost basis and including whatever taxes were paid upon vesting. Tracking Awards \uf0c1 Commodities \uf0c1 First we want to define some commodities. In this example, I work for \u201cHooli Inc.\u201d and will eventually receive shares of that company (valued in US dollars): 1990-12-02 commodity HOOL name: \"Common shares of Hooli Inc.\" quote: USD We will also want to track the amount of unvested shares: 2013-01-28 commodity HOOL.UNVEST name: \"Unvested shares of Hooli from awards.\" Accounts for Awards \uf0c1 Grants received is income. I use \u201cIncome:US:Hooli\u201d as the root for all income accounts from Hooli, but in particular, I define an account for the awards, which contains units of unvested stock: 2013-01-28 open Income:US:Hooli:Awards HOOL.UNVEST When the stock vests, we will need to book the other side of this income somewhere, so we define an expenses account to count how much stock has been vested over a period of time: 2014-01-28 open Expenses:Hooli:Vested HOOL.UNVEST Receiving Awards \uf0c1 When you receive a new award (this may occur every year, for example, some people call this a \u201cstock refresh\u201d), you receive it as income and deposit it into a fresh new account, used to track this particular award: 2014-04-02 * \"Award S0012345\" Income:US:Hooli:Awards -1680 HOOL.UNVEST Assets:US:Hooli:Unvested:S0012345 1680 HOOL.UNVEST 2014-04-02 open Assets:US:Hooli:Unvested:S0012345 You may have multiple active awards at the same time. It\u2019s nice to have a separate account per award, as it offers a natural way to list their contents and when the award expires, you can close the account\u2014the list of open award accounts gives you the list of outstanding & actively vesting awards. In this example I used the number of the award (#S0012345) as the sub-account name. It\u2019s useful to use the number as the statements typically include it. I like to keep all the awards in a small dedicated section. Vesting Events \uf0c1 Then I have a different section that contains all the transactions that follow a vesting event. Accounts \uf0c1 First, when we vest stock, it\u2019s a taxable income event. The cash value for the stock needs an Income account: 2013-04-04 open Income:US:Hooli:RSU Taxes paid should be on the annual expenses accounts you should have defined somewhere else to account for that year\u2019s taxes (this is covered elsewhere in the cookbook): 2015-01-01 open Expenses:Taxes:TY2015:US:StateNY 2015-01-01 open Expenses:Taxes:TY2015:US:Federal 2015-01-01 open Expenses:Taxes:TY2015:US:SocSec 2015-01-01 open Expenses:Taxes:TY2015:US:SDI 2015-01-01 open Expenses:Taxes:TY2015:US:Medicare 2015-01-01 open Expenses:Taxes:TY2015:US:CityNYC After paying taxes on the received income, the remaining cash is deposited in a limbo account before getting converted: 2013-01-28 open Assets:US:Hooli:RSURefund Also, in another section we should have an account for the brokerage which holds and manages the shares for you: 2013-04-04 open Assets:US:Schwab:HOOL Generally you don\u2019t have a choice for this broker because the company you work normally makes an arrangement with an external firm in order to administer the restricted stock program. Typical firms doing this type of administration are Morgan Stanley, Salomon Smith Barney, Schwab, even E*Trade. In the example we\u2019ll use a Schwab account. And we also need some sort of checking account to receive cash in lieu of fractional shares. I\u2019ll assume a Bank of America account in the example: 2001-01-01 open Assets:US:BofA:Checking Vesting \uf0c1 First, the vesting events themselves: 2015-05-27 * \"Vesting Event - S0012345 - HOOL\" #award-S0012345 ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Income:US:Hooli:RSU -4597.95 USD Expenses:Taxes:TY2015:US:Medicare 66.68 USD Expenses:Taxes:TY2015:US:Federal 1149.48 USD Expenses:Taxes:TY2015:US:CityNYC 195.42 USD Expenses:Taxes:TY2015:US:SDI 0.00 USD Expenses:Taxes:TY2015:US:StateNY 442.32 USD Expenses:Taxes:TY2015:US:SocSec 285.08 USD Assets:US:Hooli:RSURefund 2458.97 USD This corresponds line-by-line to a payroll stub that I receive each vesting event for each award. Since all the RSU awards at Hooli vest on the same day of the month, this means that I have clusters of these, one for each award (in the example file I show two awards). Some observations are in order: The value of the Income posting consists of the number of shares vested times the FMV of the stock. In this case, that is 35 shares \u2a09 $131.37/share = $4597.95. I just write the dollar amount because it is provided to me on the pay stub. Receiving vested stock is a taxable income event, and Hooli automatically withholds taxes and pays them to the government on its employees\u2019 behalf. These are the Expenses:Taxes accounts corresponding to your W-2. Finally, I don\u2019t directly deposit the converted shares to the brokerage account; this is because the remainder of cash after paying tax expenses is not necessarily a round number of shares. Therefore, I used a limbo account ( Assets:US:Hooli:RSURefund ) and decouple the receipt from the conversion. This way, each transaction corresponds exactly to one pay stub. It makes it easier to enter the data. Also note that I used a unique link ( ^392f97dd62d0 ) to group all the transactions for a particular vesting event. You could also use tags if you prefer. Conversion to Actual Stock \uf0c1 Now we\u2019re ready to convert the remaining cash to stock units. This happens in the brokerage firm and you should see the new shares in your brokerage account. The brokerage will typically issue a \u201cstock release report\u201d statement for each vesting event for each award, with the details necessary to make the conversion, namely, the actual number of shares converted from cash and the cost basis (the FMV on the vesting day): 2015-05-25 * \"Conversion into shares\" ^392f97dd62d0 Assets:US:Schwab:HOOL 18 HOOL {131.3700 USD} Assets:US:Hooli:RSURefund Assets:US:Hooli:Unvested:S0012345 -35 HOOL.UNVEST Expenses:Hooli:Vested 35 HOOL.UNVEST The first two postings deposit the shares and subtract the dollar value from the limbo account where the vesting transaction left the remaining cash. You should make sure to use the specific FMV price provided by the statement and not an approximate price, because this has tax consequences later on. Note that you cannot buy fractional shares, so the cost of the rounded amount of shares (18) will leave some remaining cash in the limbo account. The last two postings deduct from the balance of unvested shares and I \u201creceive\u201d an expenses; that expenses account basically counts how many shares were vested over a particular time period. Here again you will probably have one of these conversions for each stock grant you have. I enter them separately, so that one statement matches one transaction. This is a good rule to follow. Refund for Fractions \uf0c1 After all the conversion events have moved cash out of the limbo account, it is left the fractional remainders from all the conversions. In my case, this remainder is refunded by Hooli 3-4 weeks after vesting as a single separate pay stub that includes all the remainders (it even lists each of them separately). I enter this as a transaction as well: 2015-06-13 * \"HOOLI INC PAYROLL\" ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Assets:US:Hooli:RSURefund -94.31 USD Assets:US:Hooli:RSURefund -2.88 USD ; (For second award in example) Assets:US:BofA:Checking 97.19 USD After the fractions have been paid the limbo account should be empty. I verify this claim using a balance assertion: 2015-06-14 balance Assets:US:Hooli:RSURefund 0 USD This provides me with some sense that the numbers are right. Organizing your Input \uf0c1 I like to put all the vesting events together in my input file; this makes them much easier to update and reconcile, especially with multiple awards. For example, with two awards I would have multiple chunks of transactions like this, separated with 4-5 empty lines to delineate them: 2015-05-27 * \"Vesting Event - S0012345 - HOOL\" #award-S0012345 ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Income:US:Hooli:RSU -4597.95 USD Assets:US:Hooli:RSURefund 2458.97 USD Expenses:Taxes:TY2015:US:Medicare 66.68 USD Expenses:Taxes:TY2015:US:Federal 1149.48 USD Expenses:Taxes:TY2015:US:CityNYC 195.42 USD Expenses:Taxes:TY2015:US:SDI 0.00 USD Expenses:Taxes:TY2015:US:StateNY 442.32 USD Expenses:Taxes:TY2015:US:SocSec 285.08 USD 2015-05-27 * \"Vesting Event - C123456 - HOOL\" #award-C123456 ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Income:US:Hooli:RSU -1970.55 USD Assets:US:Hooli:RSURefund 1053.84 USD Expenses:Taxes:TY2015:US:Medicare 28.58 USD Expenses:Taxes:TY2015:US:Federal 492.63 USD Expenses:Taxes:TY2015:US:CityNYC 83.75 USD Expenses:Taxes:TY2015:US:SDI 0.00 USD Expenses:Taxes:TY2015:US:StateNY 189.57 USD Expenses:Taxes:TY2015:US:SocSec 122.18 USD 2015-05-25 * \"Conversion into shares\" ^392f97dd62d0 Assets:US:Schwab:HOOL 18 HOOL {131.3700 USD} Assets:US:Hooli:RSURefund Assets:US:Hooli:Unvested:S0012345 -35 HOOL.UNVEST Expenses:Hooli:Vested 35 HOOL.UNVEST 2015-05-25 * \"Conversion into shares\" ^392f97dd62d0 Assets:US:Schwab:HOOL 9 HOOL {131.3700 USD} Assets:US:Hooli:RSURefund Assets:US:Hooli:Unvested:C123456 -15 HOOL.UNVEST Expenses:Hooli:Vested 15 HOOL.UNVEST 2015-06-13 * \"HOOLI INC PAYROLL\" ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Assets:US:Hooli:RSURefund -94.30 USD Assets:US:Hooli:RSURefund -2.88 USD Assets:US:BofA:Checking 97.18 USD 2015-02-14 balance Assets:US:Hooli:RSURefund 0 USD Unvested Shares \uf0c1 Asserting Unvested Balances \uf0c1 Finally, you may occasionally want to assert the number of unvested shares. I like to do this semi-annually, for example. The brokerage company that handles the RSUs for Hooli should be able to list how many unvested shares of each award remain, so it\u2019s as simple as looking it up on a website: 2015-06-04 balance Assets:US:Hooli:Unvested:S0012345 1645 HOOL.UNVEST 2015-06-04 balance Assets:US:Hooli:Unvested:C123456 705 HOOL.UNVEST Pricing Unvested Shares \uf0c1 You can also put a price on the unvested shares in order to estimate the unvested dollar amount. You should use a fictional currency for this, because we want to avoid a situation where a balance sheet is produced that includes these unvested assets as regular dollars: 2015-06-02 price HOOL.UNVEST 132.4300 USD.UNVEST At the time of this writing, the bean-web interface does not convert the units if they are not held at cost, but using the SQL query interface or writing a custom script you should be able to produce those numbers: $ bean-query examples/vesting/vesting.beancount beancount> select account, sum(convert(position, 'USD.UNVEST')) as unvested where account ~ 'Unvested' group by account; account unvested --------------------------------- ---------------------- Assets:US:Hooli:Unvested:S0012345 217847.3500 USD.UNVEST Assets:US:Hooli:Unvested:C123456 93363.1500 USD.UNVEST Selling Vested Stock \uf0c1 After each vesting event, the stock is left in your brokerage account. Selling this stock proceeds just as in any other trading transaction (see Trading with Beancount for full details). For example, selling the shares from the example would look something like this: 2015-09-10 * \"Selling shares\" Assets:US:Schwab:HOOL -26 HOOL {131.3700 USD} @ 138.23 USD Assets:US:Schwab:Cash 3593.98 USD Income:US:Schwab:Gains Here you can see why it matters that the cost basis you used on the conversion event is the correct one: You will have to pay taxes on the difference (in Income:US:Schwab:Gains ). In this example the taxable difference is (138.23 - 131.37) dollars per share. I like to keep all the brokerage transactions in a separate section of my document, where other transactions related to the brokerage occur, such as fees, dividends and transfers. Conclusion \uf0c1 This is a simple example that is modeled after how technology companies deal with this type of compensation. It is by no means comprehensive, and some of the details will necessarily vary in your situation. In particular, it does not explain how to deal with options (ISOs). My hope is that there is enough meat in this document to allow you to extrapolate and adapt to your particular situation. If you get stuck, please reach out on the mailing-list .","title":"Stock Vesting in Beancount"},{"location":"stock_vesting_in_beancount.html#stock-vesting-in-beancount","text":"Martin Blais , June 2015 http://furius.ca/beancount/doc/vesting","title":"Stock Vesting in Beancount"},{"location":"stock_vesting_in_beancount.html#introduction","text":"This document explains the vesting of restricted stock units in Beancount, by way of an example. This example may not exactly match your situation, but enough detail is provided that you should be able to adapt it for your own particular differences. A working example file can be found here to follow along with this text.","title":"Introduction"},{"location":"stock_vesting_in_beancount.html#restricted-stock-compensation","text":"Many technology companies offer their employees incentive compensation in the form of \u201cgrants\u201d (or \u201cawards\u201d) of \u201crestricted stock units\u201d (RSU), which is essentially a promise for the \u201crelease\u201d to you of actual shares in the future. The stock is \u201crestricted\u201d in the sense that you cannot access it\u2014you only receive it when it \u201cvests\u201d, and this happens based on a schedule. Typically, you are promised a fixed number of shares that vest every quarter or every month over a period of 3 or 4 years. If you leave the company, your remaining unvested shares are lost. One way you can view these RSUs is as an asset, a receivable that arrives regularly over time. These RSUs are essentially compensation denominated in the currency of the company\u2019s shares itself. We want to track the unraveling of these unvested units, and correctly account for their conversion to real stock with a cost basis and including whatever taxes were paid upon vesting.","title":"Restricted Stock Compensation"},{"location":"stock_vesting_in_beancount.html#tracking-awards","text":"","title":"Tracking Awards"},{"location":"stock_vesting_in_beancount.html#commodities","text":"First we want to define some commodities. In this example, I work for \u201cHooli Inc.\u201d and will eventually receive shares of that company (valued in US dollars): 1990-12-02 commodity HOOL name: \"Common shares of Hooli Inc.\" quote: USD We will also want to track the amount of unvested shares: 2013-01-28 commodity HOOL.UNVEST name: \"Unvested shares of Hooli from awards.\"","title":"Commodities"},{"location":"stock_vesting_in_beancount.html#accounts-for-awards","text":"Grants received is income. I use \u201cIncome:US:Hooli\u201d as the root for all income accounts from Hooli, but in particular, I define an account for the awards, which contains units of unvested stock: 2013-01-28 open Income:US:Hooli:Awards HOOL.UNVEST When the stock vests, we will need to book the other side of this income somewhere, so we define an expenses account to count how much stock has been vested over a period of time: 2014-01-28 open Expenses:Hooli:Vested HOOL.UNVEST","title":"Accounts for Awards"},{"location":"stock_vesting_in_beancount.html#receiving-awards","text":"When you receive a new award (this may occur every year, for example, some people call this a \u201cstock refresh\u201d), you receive it as income and deposit it into a fresh new account, used to track this particular award: 2014-04-02 * \"Award S0012345\" Income:US:Hooli:Awards -1680 HOOL.UNVEST Assets:US:Hooli:Unvested:S0012345 1680 HOOL.UNVEST 2014-04-02 open Assets:US:Hooli:Unvested:S0012345 You may have multiple active awards at the same time. It\u2019s nice to have a separate account per award, as it offers a natural way to list their contents and when the award expires, you can close the account\u2014the list of open award accounts gives you the list of outstanding & actively vesting awards. In this example I used the number of the award (#S0012345) as the sub-account name. It\u2019s useful to use the number as the statements typically include it. I like to keep all the awards in a small dedicated section.","title":"Receiving Awards"},{"location":"stock_vesting_in_beancount.html#vesting-events","text":"Then I have a different section that contains all the transactions that follow a vesting event.","title":"Vesting Events"},{"location":"stock_vesting_in_beancount.html#accounts","text":"First, when we vest stock, it\u2019s a taxable income event. The cash value for the stock needs an Income account: 2013-04-04 open Income:US:Hooli:RSU Taxes paid should be on the annual expenses accounts you should have defined somewhere else to account for that year\u2019s taxes (this is covered elsewhere in the cookbook): 2015-01-01 open Expenses:Taxes:TY2015:US:StateNY 2015-01-01 open Expenses:Taxes:TY2015:US:Federal 2015-01-01 open Expenses:Taxes:TY2015:US:SocSec 2015-01-01 open Expenses:Taxes:TY2015:US:SDI 2015-01-01 open Expenses:Taxes:TY2015:US:Medicare 2015-01-01 open Expenses:Taxes:TY2015:US:CityNYC After paying taxes on the received income, the remaining cash is deposited in a limbo account before getting converted: 2013-01-28 open Assets:US:Hooli:RSURefund Also, in another section we should have an account for the brokerage which holds and manages the shares for you: 2013-04-04 open Assets:US:Schwab:HOOL Generally you don\u2019t have a choice for this broker because the company you work normally makes an arrangement with an external firm in order to administer the restricted stock program. Typical firms doing this type of administration are Morgan Stanley, Salomon Smith Barney, Schwab, even E*Trade. In the example we\u2019ll use a Schwab account. And we also need some sort of checking account to receive cash in lieu of fractional shares. I\u2019ll assume a Bank of America account in the example: 2001-01-01 open Assets:US:BofA:Checking","title":"Accounts"},{"location":"stock_vesting_in_beancount.html#vesting","text":"First, the vesting events themselves: 2015-05-27 * \"Vesting Event - S0012345 - HOOL\" #award-S0012345 ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Income:US:Hooli:RSU -4597.95 USD Expenses:Taxes:TY2015:US:Medicare 66.68 USD Expenses:Taxes:TY2015:US:Federal 1149.48 USD Expenses:Taxes:TY2015:US:CityNYC 195.42 USD Expenses:Taxes:TY2015:US:SDI 0.00 USD Expenses:Taxes:TY2015:US:StateNY 442.32 USD Expenses:Taxes:TY2015:US:SocSec 285.08 USD Assets:US:Hooli:RSURefund 2458.97 USD This corresponds line-by-line to a payroll stub that I receive each vesting event for each award. Since all the RSU awards at Hooli vest on the same day of the month, this means that I have clusters of these, one for each award (in the example file I show two awards). Some observations are in order: The value of the Income posting consists of the number of shares vested times the FMV of the stock. In this case, that is 35 shares \u2a09 $131.37/share = $4597.95. I just write the dollar amount because it is provided to me on the pay stub. Receiving vested stock is a taxable income event, and Hooli automatically withholds taxes and pays them to the government on its employees\u2019 behalf. These are the Expenses:Taxes accounts corresponding to your W-2. Finally, I don\u2019t directly deposit the converted shares to the brokerage account; this is because the remainder of cash after paying tax expenses is not necessarily a round number of shares. Therefore, I used a limbo account ( Assets:US:Hooli:RSURefund ) and decouple the receipt from the conversion. This way, each transaction corresponds exactly to one pay stub. It makes it easier to enter the data. Also note that I used a unique link ( ^392f97dd62d0 ) to group all the transactions for a particular vesting event. You could also use tags if you prefer.","title":"Vesting"},{"location":"stock_vesting_in_beancount.html#conversion-to-actual-stock","text":"Now we\u2019re ready to convert the remaining cash to stock units. This happens in the brokerage firm and you should see the new shares in your brokerage account. The brokerage will typically issue a \u201cstock release report\u201d statement for each vesting event for each award, with the details necessary to make the conversion, namely, the actual number of shares converted from cash and the cost basis (the FMV on the vesting day): 2015-05-25 * \"Conversion into shares\" ^392f97dd62d0 Assets:US:Schwab:HOOL 18 HOOL {131.3700 USD} Assets:US:Hooli:RSURefund Assets:US:Hooli:Unvested:S0012345 -35 HOOL.UNVEST Expenses:Hooli:Vested 35 HOOL.UNVEST The first two postings deposit the shares and subtract the dollar value from the limbo account where the vesting transaction left the remaining cash. You should make sure to use the specific FMV price provided by the statement and not an approximate price, because this has tax consequences later on. Note that you cannot buy fractional shares, so the cost of the rounded amount of shares (18) will leave some remaining cash in the limbo account. The last two postings deduct from the balance of unvested shares and I \u201creceive\u201d an expenses; that expenses account basically counts how many shares were vested over a particular time period. Here again you will probably have one of these conversions for each stock grant you have. I enter them separately, so that one statement matches one transaction. This is a good rule to follow.","title":"Conversion to Actual Stock"},{"location":"stock_vesting_in_beancount.html#refund-for-fractions","text":"After all the conversion events have moved cash out of the limbo account, it is left the fractional remainders from all the conversions. In my case, this remainder is refunded by Hooli 3-4 weeks after vesting as a single separate pay stub that includes all the remainders (it even lists each of them separately). I enter this as a transaction as well: 2015-06-13 * \"HOOLI INC PAYROLL\" ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Assets:US:Hooli:RSURefund -94.31 USD Assets:US:Hooli:RSURefund -2.88 USD ; (For second award in example) Assets:US:BofA:Checking 97.19 USD After the fractions have been paid the limbo account should be empty. I verify this claim using a balance assertion: 2015-06-14 balance Assets:US:Hooli:RSURefund 0 USD This provides me with some sense that the numbers are right.","title":"Refund for Fractions"},{"location":"stock_vesting_in_beancount.html#organizing-your-input","text":"I like to put all the vesting events together in my input file; this makes them much easier to update and reconcile, especially with multiple awards. For example, with two awards I would have multiple chunks of transactions like this, separated with 4-5 empty lines to delineate them: 2015-05-27 * \"Vesting Event - S0012345 - HOOL\" #award-S0012345 ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Income:US:Hooli:RSU -4597.95 USD Assets:US:Hooli:RSURefund 2458.97 USD Expenses:Taxes:TY2015:US:Medicare 66.68 USD Expenses:Taxes:TY2015:US:Federal 1149.48 USD Expenses:Taxes:TY2015:US:CityNYC 195.42 USD Expenses:Taxes:TY2015:US:SDI 0.00 USD Expenses:Taxes:TY2015:US:StateNY 442.32 USD Expenses:Taxes:TY2015:US:SocSec 285.08 USD 2015-05-27 * \"Vesting Event - C123456 - HOOL\" #award-C123456 ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Income:US:Hooli:RSU -1970.55 USD Assets:US:Hooli:RSURefund 1053.84 USD Expenses:Taxes:TY2015:US:Medicare 28.58 USD Expenses:Taxes:TY2015:US:Federal 492.63 USD Expenses:Taxes:TY2015:US:CityNYC 83.75 USD Expenses:Taxes:TY2015:US:SDI 0.00 USD Expenses:Taxes:TY2015:US:StateNY 189.57 USD Expenses:Taxes:TY2015:US:SocSec 122.18 USD 2015-05-25 * \"Conversion into shares\" ^392f97dd62d0 Assets:US:Schwab:HOOL 18 HOOL {131.3700 USD} Assets:US:Hooli:RSURefund Assets:US:Hooli:Unvested:S0012345 -35 HOOL.UNVEST Expenses:Hooli:Vested 35 HOOL.UNVEST 2015-05-25 * \"Conversion into shares\" ^392f97dd62d0 Assets:US:Schwab:HOOL 9 HOOL {131.3700 USD} Assets:US:Hooli:RSURefund Assets:US:Hooli:Unvested:C123456 -15 HOOL.UNVEST Expenses:Hooli:Vested 15 HOOL.UNVEST 2015-06-13 * \"HOOLI INC PAYROLL\" ^392f97dd62d0 doc: \"2015-02-13.hooli.38745783.pdf\" Assets:US:Hooli:RSURefund -94.30 USD Assets:US:Hooli:RSURefund -2.88 USD Assets:US:BofA:Checking 97.18 USD 2015-02-14 balance Assets:US:Hooli:RSURefund 0 USD","title":"Organizing your Input"},{"location":"stock_vesting_in_beancount.html#unvested-shares","text":"","title":"Unvested Shares"},{"location":"stock_vesting_in_beancount.html#asserting-unvested-balances","text":"Finally, you may occasionally want to assert the number of unvested shares. I like to do this semi-annually, for example. The brokerage company that handles the RSUs for Hooli should be able to list how many unvested shares of each award remain, so it\u2019s as simple as looking it up on a website: 2015-06-04 balance Assets:US:Hooli:Unvested:S0012345 1645 HOOL.UNVEST 2015-06-04 balance Assets:US:Hooli:Unvested:C123456 705 HOOL.UNVEST","title":"Asserting Unvested Balances"},{"location":"stock_vesting_in_beancount.html#pricing-unvested-shares","text":"You can also put a price on the unvested shares in order to estimate the unvested dollar amount. You should use a fictional currency for this, because we want to avoid a situation where a balance sheet is produced that includes these unvested assets as regular dollars: 2015-06-02 price HOOL.UNVEST 132.4300 USD.UNVEST At the time of this writing, the bean-web interface does not convert the units if they are not held at cost, but using the SQL query interface or writing a custom script you should be able to produce those numbers: $ bean-query examples/vesting/vesting.beancount beancount> select account, sum(convert(position, 'USD.UNVEST')) as unvested where account ~ 'Unvested' group by account; account unvested --------------------------------- ---------------------- Assets:US:Hooli:Unvested:S0012345 217847.3500 USD.UNVEST Assets:US:Hooli:Unvested:C123456 93363.1500 USD.UNVEST","title":"Pricing Unvested Shares"},{"location":"stock_vesting_in_beancount.html#selling-vested-stock","text":"After each vesting event, the stock is left in your brokerage account. Selling this stock proceeds just as in any other trading transaction (see Trading with Beancount for full details). For example, selling the shares from the example would look something like this: 2015-09-10 * \"Selling shares\" Assets:US:Schwab:HOOL -26 HOOL {131.3700 USD} @ 138.23 USD Assets:US:Schwab:Cash 3593.98 USD Income:US:Schwab:Gains Here you can see why it matters that the cost basis you used on the conversion event is the correct one: You will have to pay taxes on the difference (in Income:US:Schwab:Gains ). In this example the taxable difference is (138.23 - 131.37) dollars per share. I like to keep all the brokerage transactions in a separate section of my document, where other transactions related to the brokerage occur, such as fees, dividends and transfers.","title":"Selling Vested Stock"},{"location":"stock_vesting_in_beancount.html#conclusion","text":"This is a simple example that is modeled after how technology companies deal with this type of compensation. It is by no means comprehensive, and some of the details will necessarily vary in your situation. In particular, it does not explain how to deal with options (ISOs). My hope is that there is enough meat in this document to allow you to extrapolate and adapt to your particular situation. If you get stuck, please reach out on the mailing-list .","title":"Conclusion"},{"location":"the_double_entry_counting_method.html","text":"The Double-Entry Counting Method \uf0c1 Martin Blais, December 2016 http://furius.ca/beancount/doc/double-entry Introduction Basics of Double-Entry Bookkeeping Statements Single-Entry Bookkeeping Double-Entry Bookkeeping Many Accounts Multiple Postings Types of Accounts Trial Balance Income Statement Clearing Income Equity Balance Sheet Summarizing Period Reporting Chart of Accounts Country-Institution Convention Credits & Debits Accounting Equations Plain-Text Accounting The Table Perspective Introduction \uf0c1 This document is a gentle introduction to the double-entry counting method, as written from the perspective of a computer scientist. It is an attempt to explain basic bookkeeping using as simple an approach as possible, doing away with some of the idiosyncrasies normally involved in accounting. It is also representative of how Beancount works, and it should be useful to all users of plain-text accounting . Note that I am not an accountant, and in the process of writing this document I may have used terminology that is slightly different or unusual to that which is taught in perhaps more traditional training in accounting. I granted myself license to create something new and perhaps even unusual in order to explain those ideas as simply and clearly as possible to someone unfamiliar with them. I believe that the method of double-entry counting should be taught to everyone at the high school level everywhere as it is a tremendously useful organizational skill, and I hope that this text can help spread its knowledge beyond professional circles. Basics of Double-Entry Bookkeeping \uf0c1 The double-entry system is just a simple method of counting , with some simple rules. Let\u2019s begin by defining the notion of an account . An account is something that can contain things, like a bag. It is used to count things, to accumulate things. Let\u2019s draw a horizontal arrow to visually represent the evolving contents of an account over time: On the left, we have the past, and to the right, increasing time: the present, the future, etc. For now, let\u2019s assume that accounts can contain only one kind of thing, for example, dollars . All accounts begin with an empty content of zero dollars. We will call the number of units in the account the balance of an account. Note that it represents its contents at a particular point in time. I will draw the balance using a number above the account\u2019s timeline: The contents of accounts can change over time. In order to change the content of an account, we have to add something to it. We will call this addition a posting to an account, and I will draw this change as a circled number on the account\u2019s timeline, for example, adding $100 to the account: Now, we can draw the updated balance of the account after the posting with another little number right after it: The account\u2019s balance, after adding $100, is now $100. We can also remove from the contents of an account. For example, we could remove $25, and the resulting account balance is now $75: Account balances can also become negative , if we remove more dollars than there are in the account. For example, if we remove $200 from this account, the balance now becomes $-125: It\u2019s perfectly fine for accounts to contain a negative balance number. Remember that all we\u2019re doing is counting things. As we will see shortly, some accounts will remain with a negative balance for most of their timeline. Statements \uf0c1 Something worthy of notice is how the timeline notation I\u2019ve written in the previous section is analogous to paper account statements institutions maintain for each client and which you typically receive through the mail: Date Description Amount Balance 2016-10-02 ... 100.00 1100.00 2016-10-05 ... -25.00 1075.00 2016-10-06 ... -200.00 875.00 Final Balance 875.00 Sometimes the amount column is split into two, one showing the positive amounts and the other the negative ones: Date Description Debit Credit Balance 2016-10-02 ... 100.00 1100.00 2016-10-05 ... 25.00 1075.00 2016-10-06 ... 200.00 875.00 Final Balance 875.00 Here, \u201cdebit\u201d means \u201cremoved from your account\u201d and \u201ccredit\u201d means \u201cdeposited in your account.\u201d Sometimes the words \u201cwithdrawals\u201d and \u201cdeposits\u201d will be used. It all depends on context: for checking and savings accounts it is usual to have both types of postings, but for a credit card account typically it shows only positive numbers and then the occasional monthly payment so the single column format is used. In any case, the \u201cbalance\u201d column always shows the resulting balance after the amount has been posted to the account. And sometimes the statements are rendered in decreasing order of time. Single-Entry Bookkeeping \uf0c1 In this story, this account belongs to someone. We\u2019ll call this person the owner of the account. The account can be used to represent a real world account, for example, imagine that we use it to represent the content of the owner\u2019s checking account at a bank. So we\u2019re going to label the account by giving it a name, in this case \u201cChecking\u201d: Imagine that at some point, this account has a balance of $1000, like I\u2019ve drawn on the picture. Now, if the owner spends $79 of this account, we would represent it like this: Furthermore, if the expense was for a meal at a restaurant, we could flag the posting with a category to indicate what the change was used for. Let\u2019s say, \u201cRestaurant\u201d, like this: Now, if we have a lot of these, we could write a computer program to accumulate all the changes for each category and calculate the sums for each of them. That would tell us how much we spent in restaurants in total, for example. This is called the single-entry method of accounting. But we\u2019re not going to do it this way; we have a better way. Bear with me for a few more sections. Double-Entry Bookkeeping \uf0c1 An owner may have multiple accounts. I will represent this by drawing many similar account timelines on the same graphic. As before, these are labeled with unique names. Let\u2019s assume that the owner has the same \u201cChecking\u201d account as previously, but now also a \u201c Restaurant \u201d account as well, which can be used to accumulate all food expenses at restaurants. It looks like this: Now, instead of categorizing the posting to a \u201crestaurant category\u201d as we did previously, we could create a matching posting on the \u201cRestaurant\u201d account to record how much we spent for food, with the amount spent ($79): The \u201cRestaurant\u201d account, like all other accounts, also has an accumulated balance, so we can find out how much we spent in \u201cRestaurant\u201d in total. This is entirely symmetrical to counting changes in a checking account. Now, we can associate the two postings together, by creating a kind of \u201cparent\u201d box that refers to both of them. We will call this object a transaction : Notice here that we\u2019ve also associated a description to this transaction: \u201cDinner at Uncle Boons\u201d. A transaction also has a date , and all of its postings are recorded to occur on that date. We call this the transaction date. We can now introduce the fundamental rule of double-entry bookkeeping system: The sum of all the postings of a transaction must equal zero. Remember this, as this is the foundation of the double-entry method, and its most important characteristic. It has important consequences which I will discuss later in this document. In our example, we remove $79 from the \u201cChecking\u201d account and \u201cgive it\u201d to the \u201cRestaurant\u201d account. ($79) + ($-79) = $0. To emphasize this, I could draw a little summation line under the postings of the transaction, like this: Many Accounts \uf0c1 There may be many such transactions, over many different accounts. For example, if the owner of the accounts had a lunch the next day which she paid using a credit card, it could be represented by creating a \u201cCredit Card\u201d account dedicated to tracking the real world credit card balance, and with a corresponding transaction: In this example, the owner spent $35 at a restaurant called \u201cEataly.\u201d The previous balance of the owner\u2019s credit card was $-450; after the expense, the new balance is $-485. For each real world account, the owner can create a bookkeeping account like we did. Also, for each category of expenditure, the owner also creates a bookkeeping account. In this system, there are no limits to how many accounts can be created. Note that the balance in the example is a negative number; this is not an error. Balances for credit card accounts are normally negative: they represent an amount you owe , that the bank is lending you on credit . When your credit card company keeps track of your expenses, they write out your statement from their perspective, as positive numbers. For you, those are amounts you need to eventually pay. But here, in our accounting system, we\u2019re representing numbers from the owner\u2019s point-of-view, and from her perspective, this is money she owes, not something she has. What we have is a meal sitting in our stomach (a positive number of $ of \u201cRestaurant\u201d). Multiple Postings \uf0c1 Finally, transactions may have more than two postings; in fact, they may have any number of postings. The only thing that matters is that the sum of their amounts is zero (from the rule of double-entry bookkeeping above). For example, let\u2019s look at what would happen if the owner gets her salary paid for December: Her gross salary received in this example is recorded as $-2,905 (I\u2019ll explain the sign in a moment). $905 is set aside for taxes. Her \u201cnet\u201d salary of $2,000, the remainder, is deposited in her \u201cChecking\u201d account and the resulting balance of that account is $2,921 (the previous balance of $921 + $2,000 = $2,921). This transaction has three postings: (+2,000) + (-2,905) + (+905) = 0. The double-entry rule is respected. Now, you may ask: Why is her salary recorded as a negative number? The reasoning here is similar to that of the credit card above, though perhaps a bit more subtle. These accounts exist to track all the amounts from the owner\u2019s point-of-view. The owner gives out work, and receives money and taxes in exchange for it (positive amounts). The work given away is denominated in dollar units. It \u201cleaves\u201d the owner (imagine that the owner has potential work stored in her pocket and as she goes into work every day sprinkles that work potential giving it to the company). The owner gave $2,905\u2019s worth of work away. We want to track how much work was given, and it\u2019s done with the \u201cSalary\u201d account. That\u2019s her gross salary. Note also that we\u2019ve simplified this paycheck transaction a bit, for the sake of keeping things simple. A more realistic recording of one\u2019s pay stub would have many more accounts; we would separately account for state and federal tax amounts, as well as social security and medicare payments, deductions, insurance paid through work, and vacation time accrued during the period. But it wouldn\u2019t be much more complicated: the owner would simply translate all the amounts available from her pay stub into a single transaction with more postings. The structure remains similar. Types of Accounts \uf0c1 Let\u2019s now turn our attention to the different types of accounts an owner can have. Balance or Delta. First, the most important distinction between accounts is about whether we care about the balance at a particular point in time, or whether it only makes sense to care about differences over a period of time. For example, the balance of someone\u2019s Checking or Savings accounts is a meaningful number that both the owner and its corresponding bank will care about. Similarly, the total amount owed on someone\u2019s Credit Card account is also meaningful. The same goes with someone\u2019s remaining Mortgage amount to pay on a house. On the other hand, the total amount of Restaurant expenses since the beginning of somebody\u2019s life on earth is not particularly interesting. What we might care about for this account is the amount of Restaurant expenses incurred over a particular period of time . For example, \u201chow much did you spend in restaurants last month?\u201d Or last quarter. Or last year. Similarly, the total amount of gross salary since the beginning of someone\u2019s employment at a company a few years ago is not very important. But we would care about the total amount earned during a tax year, that is, for that time period, because it is used for reporting one\u2019s income to the tax man. Accounts whose balance at a point in time is meaningful are called balance sheet accounts . There are two types of such accounts: \u201c Assets \u201d and \u201c Liabilities .\u201d The other accounts, that is, those whose balance is not particularly meaningful but for which we are interested in calculating changes over a period of time are called income statement accounts . Again, there are two kinds: \u201c Income \u201d and \u201c Expenses .\u201d Normal sign. Secondly, we consider the usual sign of an account\u2019s balance . The great majority of accounts in the double-entry system tend to have a balance with always a positive sign, or always a negative sign (though as we\u2019ve seen previously, it is not impossible that an account\u2019s balance could change signs). This is how we will distinguish between the pairs of accounts mentioned before: For a balance sheet account, Assets normally have positive balances, and Liabilities normally have negative balances. For income statement accounts, Expenses normally have a positive balance, and Income accounts normally have a negative balance. This situation is summarized in the following table: Balance: Positive (+) Balance: Negative (-) Balance matters at a point in time (Balance Sheet) Assets Liabilities Change in balance matters over a period of time (Income Statement) Expenses Income Let\u2019s discuss each type of account and provide some examples, so that it doesn\u2019t remain too abstract. Assets. (+) Asset accounts represent something the owner has . A canonical example is banking accounts. Another one is a \u201ccash\u201d account, which counts how much money is in your wallet. Investments are also assets (their units aren\u2019t dollars in this case, but rather some number of shares of some mutual fund or stock). Finally, if you own a home, the home itself is considered an asset (and its market value fluctuates over time). Liabilities. (-) A liability account represents something the owner owes . The most common example is a credit card. Again, the statement provided by your bank will show positive numbers, but from your own perspective, they are negative numbers. A loan is also a liability account. For example, if you take out a mortgage on a home, this is money you owe, and will be tracked by an account with a negative amount. As you pay off the mortgage every month the negative number goes up, that is, its absolute value gets smaller and smaller over time (e.g., -120,000 -> -117,345). Expenses. (+) An expense account represents something you\u2019ve received , perhaps by exchanging something else to purchase it. This type of account will seem pretty natural: food, drinks, clothing, rent, flights, hotels and most other categories of things you typically spend your disposable income on. However, taxes are also typically tracked by an expense account: when you receive some salary income, the amount of taxes withheld at the source is recorded immediately as an expense. Think of it as paying for government services you receive throughout the year. Income. (-) An income account is used to count something you\u2019ve given away in order to receive something else (typically assets or expenses). For most people with jobs, that is the value of their time (a salary income). Specifically, here we\u2019re talking about the gross income. For example, if you\u2019re earning a salary of $120,000/year, that number is $120,000, not whatever amount remains after paying for taxes. Other types of income includes dividends received from investments, or interest paid from bonds held. There are also a number of oddball things received you might record as income, such the value of rewards received, e.g., cash back from a credit card, or monetary gifts from someone. In Beancount, all account names, without exception, must be associated to one of the types of accounts described previously. Since the type of an account never changes during its lifetime, we will make its type a part of an account\u2019s name, as a prefix , by convention. For example, the qualified account name for restaurant will be \u201cExpenses:Restaurant\u201d. For the bank checking account, the qualified account name will be \u201cAssets:Checking\u201d. Other than that, you can select any name you like for your accounts. You can create as many accounts as you like, and as we will see later, you can organize them in a hierarchy. As of the writing of this document, I\u2019m using more than 700 accounts to track my personal affairs. Let us now revisit our example and add some more accounts: And let\u2019s imagine there are more transactions: \u2026 and even more of them: Finally, we can label each of those accounts with one of the four types of accounts by prepending the type to their account names: A realistic book from someone tracking all of their personal affairs might easily contain thousands of transactions per year. But the principles remain simple and they remain the same: postings are applied to accounts over time, and must be parented to a transaction, and within this transaction the sum of all the postings is zero. When you do bookkeeping for a set of accounts, you are essentially describing all the postings that happen on all the accounts over time, subject to the constraint of the rule. You are creating a database of those postings in a book . You are \u201ckeeping the book,\u201d that is, traditionally, the book which contains all those transactions. Some people call this \u201cmaintaining a journal.\u201d We will now turn our attention to obtaining useful information from this data, summarizing information from the book. Trial Balance \uf0c1 Take our last example: we can easily reorder all the accounts such that all the Asset accounts appear together at the top, then all the Liabilities accounts, then Income, and finally Expenses accounts. We are simply changing the order without modifying the structure of transactions, in order to group each type of accounts together: We\u2019ve reordered the accounts with Assets accounts grouped at the top, then Liabilities, then some Equity accounts (which we have just introduced, more about them is discussed later), then Income and finally Expenses at the bottom. If we sum up the postings on all of the accounts and render just the account name and its final balance on the right, we obtain a report we call the \u201ctrial balance.\u201d This simply reflects the balance of each account at a particular point in time. And because each of the accounts began with a zero balance, and each transaction has itself a zero balance, we know that the sum of all those balances must equal zero. 1 This is a consequence of our constraining that each of the postings be part of a transaction, and that each transaction have postings that balance each other out. Income Statement \uf0c1 One kind of common information that is useful to extract from the list of transactions is a summary of changes in income statement accounts during a particular period of time. This tells us how much money was earned and spent during this period, and the difference tells us how much profit (or loss) was incurred. We call this the \u201cnet income.\u201d In order to generate this summary, we simply turn our attention to the balances of the accounts of types Income and Expenses, summing up just the transactions for a particular period, and we draw the Income balances on the left, and Expenses balances on the right: It is important to take note of the signs here: Income numbers are negative, and Expenses numbers are positive. So if you earned more than you spent (a good outcome), the final sum of Income + Expenses balances will be a negative number. Like any other income, a net income that has a negative number means that there is a corresponding amount of Assets and/or Liabilities with positive numbers (this is good for you). An Income Statement tells us what changed during a particular period of time. Companies typically report this information quarterly to investors and perhaps the public (if they are a publicly traded company) in order to share how much profit they were able to make. Individuals typically report this information on their annual tax returns. Clearing Income \uf0c1 Notice how in the income statement only the transactions within a particular interval of time are summed up. This allows one, for instance, to compute the sum of all income earned during a year. If we were to sum up all of the transactions of this account since its inception we would obtain the total amount of income earned since the account was created. A better way to achieve the same thing is to zero out the balances of the Income and Expenses accounts. Beancount calls this basic transformation \u201cclearing 2 .\u201d It is carried out by: Computing the balances of those accounts from the beginning of time to the start of the reporting period. For example, if you created your accounts in year 2000 and you wanted to generate an income statement for year 2016, you would sum up the balances from 2000 to Jan 1, 2016. Inserting transactions to empty those balances and transfer them to some other account that isn\u2019t Income nor Expenses. For instance, if the restaurant expense account for those 16 years amounts to $85,321 on Jan 1, 2016, it would insert a transaction of $-85,321 to restaurants and $+85,321 to \u201cprevious earnings\u201d. The transactions would be dated Jan 1, 2016. Including this transaction, the sum of that account would zero on that date. This is what we want. Those transactions inserted for all income statement accounts are pictured in green below. Now summing the entire set of transactions through the end of the ledger would yield only the changes during year 2016 because the balances were zero on that date: This is the semantics of the \u201cCLEAR\u201d operation of the bean-query shell. (Note that another way to achieve the same thing for income statement accounts would be to segregate and count amounts only for the transactions after the clearing date; however, jointly reporting on income statement accounts and balance sheet accounts would have incorrect balances for the balance sheet accounts.) Equity \uf0c1 The account that receives those previously accumulated incomes is called \u201cPrevious Earnings\u201d. It lives in a fifth and final type of accounts: Equity . We did not talk about this type of accounts earlier because they are most often only used to transfer amounts to build up reports, and the owner usually doesn\u2019t post changes to those types of accounts; the software does that automatically, e.g., when clearing net income. The account type \u201cequity\u201d is used for accounts that hold a summary of the net income implied by all the past activity. The point is that if we now list together the Assets, Liabilities and Equity accounts, because the Income and Expenses accounts have been zero\u2019ed out, the sum total of all these balances should equal exactly zero. And summing up all the Equity accounts clearly tells us what\u2019s our stake in the entity, in other words, if you used the assets to pay off all the liabilities, how much is left in the business\u2026 how much it\u2019s worth. Note that the normal sign of the Equity accounts is negative . There is no particular meaning to that, just that they are used to counterbalance Assets and Liabilities and if the owner has any value, that number should be negative. (A negative Equity means some positive net worth.) There are a few different Equity accounts in use in Beancount: Previous Earnings or Retained Earnings. An account used to hold the sum total of Income & Expenses balances from the beginning of time until the beginning of a reporting period. This is the account we were referring to in the previous section. Current Earnings , also called Net Income. An account used to contain the sum of Income & Expenses balances incurred during the reporting period. They are filled in by \u201cclearing\u201d the Income & Expenses accounts at the end of the reporting period. Opening Balances. An equity account used to counterbalance deposits used to initialize accounts. This type of account is used when we truncate the past history of transactions, but we also need to ensure that an account\u2019s balance begins its history with a particular amount. Once again: you don\u2019t need to define nor use these accounts yourself, as these are created for the purpose of summarizing transactions. Generally, the accounts are filled in by the clearing process described above, or filled in by Pad directives to \u201copening balances\u201d equity accounts, to account for summarized balances from the past. They are created and filled in automatically by the software. We\u2019ll see how these get used in the following sections. Balance Sheet \uf0c1 Another kind of summary is a listing of the owner\u2019s assets and debts, for each of the accounts. This answers the question: \u201c Where\u2019s the money? \u201d In theory, we could just restrict our focus to the Assets and Liabilities accounts and draw those up in a report: However, in practice, there is another closely related question that comes up and which is usually answered at the same time: \u201c Once all debts are paid off, how much are we left with? \u201d This is called the net worth . If the Income & Expenses accounts have been cleared to zero and all their balances have been transferred to Equity accounts, the net worth should be equal to the sum of all the Equity accounts. So in building up the Balance Sheet, it it customary to clear the net income and then display the balances of the Equity accounts. The report looks like this: Note that the balance sheet can be drawn for any point in time , simply by truncating the list of transactions following a particular date. A balance sheet displays a snapshot of balances at one date; an income statement displays the difference of those balances between two dates. Summarizing \uf0c1 It is useful to summarize a history of past transactions into a single equivalent deposit. For example, if we\u2019re interested in transactions for year 2016 for an account which has a balance of $450 on Jan 1, 2016, we can delete all the previous transactions and replace them with a single one that deposits $450 on Dec 31, 2015 and that takes it from somewhere else. That somewhere else will be the Equity account Opening Balances . First, we can do this for all Assets and Liabilities accounts (see transactions in blue): Then we delete all the transactions that precede the opening date, to obtain a truncated list of transactions: This is a useful operation when we\u2019re focused on the transactions for a particular interval of time. (This is a bit of an implementation detail: these operations are related to how Beancount is designed. Instead of making all the reporting operations with parameters, all of its reporting routines are simplified and instead operate on the entire stream of transactions; in this way, we convert the list of transactions to include only the data we want to report on. In this case, summarization is just a transformation which accepts the full set of transactions and returns an equivalent truncated stream. Then, from this stream, a journal can be produced that excludes the transactions from the past. From a program design perspective, this is appealing because the only state of the program is a stream of transactions, and it is never modified directly. It\u2019s simple and robust.) Period Reporting \uf0c1 Now we know we can produce a statement of changes over a period of time, by \u201cclearing\u201d and looking at just the Income & Expenses accounts (the Income Statement). We also know we can clear to produce a snapshot of Assets, Liabilities & Equity at any point in time (the Balance Sheet). More generally, we\u2019re interested in inspecting a particular period of time. That implies an income statement, but also two balance sheet statements: the balance sheet at the beginning of the period, and the balance sheet at the end of the period. In order to do this, we apply the following transformations: Open. We first clear net income at the beginning of the period, to move all previous income balances to the Equity Previous Earnings account. We then summarize up to the beginning of the period. We call the combination of clearing + summarizing: \u201cOpening.\u201d Close. We also truncate all the transactions following the end of the reporting period. We call this operation \u201cClosing.\u201d These are the meaning of the \u201cOPEN\u201d and \u201cCLOSE\u201d operations of the bean-query shell 3 . The resulting set of transactions should look like this. \u201cClosing\u201d involves two steps. First, we remove all transactions following the closing date: We can process this stream of transactions to produce an Income Statement for the period. Then we clear again at the end date of the desired report, but this time we clear the net income to \u201cEquity:Earnings:Current\u201d: From these transactions, we produce the Balance Sheet at the end of the period. This sums up the operations involved in preparing the streams of transactions to produce reports with Beancount, as well as a basic introduction to those types of reports. Chart of Accounts \uf0c1 New users are often wondering how much detail they should use in their account names. For example, should one include the payee in the account name itself, such as in these examples? Expenses:Phone:Mobile:VerizonWireless Assets:AccountsReceivable:Clients:AcmeInc Or should one use simpler names like the following, relying instead on the \u201cpayee\u201d, \u201ctags\u201d, or perhaps some other metadata in order to group the postings? Expenses:Phone Assets:AccountsReceivable The answer is that it depends on you . This is an arbitrary choice to make. It\u2019s a matter of taste. Personally I like to abuse the account names a bit and create long descriptive ones, other people prefer to keep them simple and use tags to group their postings. Sometimes one doesn\u2019t even need to filter subgroups of postings. There\u2019s no right answer, it depends on what you\u2019d like to do. One consideration to keep in mind is that account names implicitly define a hierarchy. The \u201c:\u201d separator is interpreted by some reporting code to create an in-memory tree and can allow you to collapse a node\u2019s children subaccounts and compute aggregates on the parent. Think of this as an additional way to group postings. Country-Institution Convention \uf0c1 One convention I\u2019ve come up with that works well for my assets, liabilities and income accounts is to root the tree with a code for the country the account lives in, followed by a short string for the institution it corresponds to. Underneath that, a unique name for the particular account in that institution. Like this: : : : For example, a checking account could be chosen to be \u201c Assets:US:BofA:Checking \u201d, where \u201cBofA\u201d stands for \u201cBank of America.\u201d A credit card account could include the name of the particular type of card as the account name, like \u201c Liabilities:US:Amex:Platinum \u201d, which can be useful if you have multiple cards. I\u2019ve found it doesn\u2019t make sense for me to use this scheme for expense accounts, since those tend to represent generic categories. For those, it seems to make more sense to group them by category, as in using \u201c Expenses:Food:Restaurant \u201d instead of just \u201c Expenses:Restaurant \u201d. In any case, Beancount doesn\u2019t enforce anything other than the root accounts; this is just a suggestion and this convention is not coded anywhere in the software. You have great freedom to experiment, and you can easily change all the names later by processing the text file. See the Cookbook for more practical guidance. Credits & Debits \uf0c1 At this point, we haven\u2019t discussed the concepts of \u201ccredits\u201d and \u201cdebits.\u201d This is on purpose: Beancount largely does away with these concepts because it makes everything else simpler. I believe that it is simpler to just learn that the signs of Income, Liabilities and Equity accounts are normally negative and to treat all accounts the same way than to deal with the debits and credits terminology and to treat different account categories differently. In any case, this section explains what these are. As I have pointed out in previous sections, we consider \u201cIncome\u201d, \u201cLiabilities\u201d and \u201cEquity\u201d accounts to normally have a negative balance. This may sound odd; after all, nobody thinks of their gross salary as a negative amount, and certainly your credit-card bill or mortgage loan statements report positive numbers. This is because in our double-entry accounting system we consider all accounts to be held from the perspective of the owner of the account . We use signs consistent from this perspective, because it makes all operations on account contents straightforward: they\u2019re all just simple additions and all the accounts are treated the same. In contrast, accountants traditionally keep all the balances of their accounts as positive numbers and then handle postings to those accounts differently depending on the account type upon which they are applied. The sign to apply to each account is entirely dictated by its type: Assets and Expenses accounts are debit accounts and Liabilities, Equity and Income accounts are credit accounts and require a sign adjustment. Moreover, posting a positive amount on an account is called \u201cdebiting\u201d and removing from an account is called \u201ccrediting.\u201d See this external document , for example, which nearly makes my head explode, and this recent thread has more detail. This way of handling postings makes everything much more complicated than it needs to be. The problem with this approach is that summing of amounts over the postings of a transaction is not a straightforward sum anymore. For example, let\u2019s say you\u2019re creating a new transaction with postings to two Asset accounts, an Expenses account and an Income account and the system tells you there is a $9.95 imbalance error somewhere. You\u2019re staring at the entry intently; which of the postings is too small? Or is one of the postings too large? Also, maybe a new posting needs to be added, but is it to a debit account or to a credit account? The mental gymnastics required to do this are taxing. Some double-entry accounting software tries to deal with this by creating separate columns for debits and credits and allowing the user enter an amount only in the column that corresponds to each posting account\u2019s type. This helps visually, but why not just use signs instead? Moreover, when you look at the accounting equations, you have to consider their signs as well. This makes it awkward to do transformations on them and make what is essentially a simple summation over postings into a convoluted mess that is difficult to understand. In plain-text accounting, we would rather just do away with this inconvenient baggage. We just use additions everywhere and learn to keep in mind that Liabilities, Equity and Income accounts normally have a negative balance. While this is unconventional, it\u2019s much easier to grok. And If there is a need to view a conventional report with positive numbers only, we will be able to trigger that in reporting code 4 , inverting the signs just to render them in the output. Save yourself some pain: Flush your brain from the \"debit\" and \"credit\" terminology. Accounting Equations \uf0c1 In light of the previous sections, we can easily express the accounting equations in signed terms. If, A = the sum of all Assets postings L = the sum of all Liabilities postings X = the sum of all Expenses postings I = the sum of all Income postings E = the sum of all Equity postings We can say that: A + L + E + X + I = 0 This follows from the fact that sum(all postings) = 0 Which follows from the fact that each transaction is guaranteed to sum up to zero (which is enforced by Beancount): for all transactions t, sum(postings of t) = 0 Moreover, the sum of postings from Income and Expenses is the Net Income (NI): NI = X + I If we adjust the equity to reflect the total Net Income effect by clearing the income to the Equity retained earnings account, we get an updated Equity value (E\u2019): E\u2019 = E + NI = E + X + I And we have a simplified accounting equation: A + L + E\u2019 = 0 If we were to adjust the signs for credits and debits (see previous section) and have sums that are all positive number, this becomes the familiar accounting equation: Assets - Liabilities = Equity As you can see, it\u2019s much easier to just always add up the numbers. Plain-Text Accounting \uf0c1 Ok, so now we understand the method and what it can do for us, at least in theory. The purpose of a double-entry bookkeeping system is to allow you to replicate the transactions that occur in various real world accounts into a single, unified system, in a common representation, and to extract various views and reports from this data. Let us now turn our attention to how we record this data in practice. This document talks about Beancount, whose purpose is \u201cdouble-entry bookkeeping using text files.\u201d Beancount implements a parser for a simple syntax that allows you to record transactions and postings. The syntax for an example transaction looks something like this: 2016-12-06 * \"Biang!\" \"Dinner\" Liabilities:CreditCard -47.23 USD Expenses:Restaurants You write many of declarations like these in a file, and Beancount will read it and create the corresponding data structures in memory. Verification. After parsing the transactions, Beancount also verifies the rule of the double-entry method: it checks that the sum of the postings on all your transactions is zero. If you make a mistake and record a transaction with a non-zero balance, an error will be displayed. Balance Assertions. Beancount allows you to replicate balances declared from external accounts, for example, a balance written on a monthly statement. It processes those and checks that the balances resulting from your input transactions match those declared balances. This helps you detect and find mistakes easily. Plugins. Beancount allows you to build programs which can automate and/or process the streams of transactions in your input files. You can build custom functionality by writing code which directly processes the transaction stream. Querying & Reporting. It provides tools to then process this stream of transactions to produce the kinds of reports we discussed earlier in this document. There are a few more details, for example, Beancount allows you to track cost basis and make currency conversions, but that\u2019s the essence of it. The Table Perspective \uf0c1 Almost always, questions asked by users on the mailing-list about how to calculate or track some value or other can be resolved easily simply by thinking of the data as a long list of rows, some of which need to be filtered and aggregated. If you consider that all that we\u2019re doing in the end is deriving \u201csums\u201d of these postings, and that the attributes of transactions and postings are what allows us to filter subsets of postings, it always becomes very simple. In almost all the cases, the answer is to find some way to disambiguate postings to select them, e.g. by account name, by attaching some tag, by using some metadata, etc. It can be illuminating to consider how this data can be represented as a table. Imagine that you have two tables: a table containing the fields of each Transaction such as date and description, and a table for the fields of each Posting, such as account, amount and currency, as well as a reference to its parent transaction. The simplest way to represent the data is to join those two tables, replicating values of the parent transaction across each of the postings. For example, this Beancount input: 2016-12-04 * \"Christmas gift\" Liabilities:CreditCard -153.45 USD Expenses:Gifts 2016-12-06 * \"Biang!\" \"Dinner\" Liabilities:CreditCard -47.23 USD Expenses:Restaurants 2016-12-07 * \"Pouring Ribbons\" \"Drinks with friends\" Assets:Cash -25.00 USD Expenses:Tips 4.00 USD Expenses:Alcohol could be rendered as a table like this: Date Fl Payee Narration Account Number Ccy 2016-12-04 * Christmas gift Liabilities:CreditCard -153.45 USD 2016-12-04 * Christmas gift Expenses:Gifts 153.45 USD 2016-12-06 * Biang! Dinner Liabilities:CreditCard -47.23 USD 2016-12-06 * Biang! Dinner Expenses:Restaurants 47.23 USD 2016-12-07 * Pouring Ribbons Drinks with friends Assets:Cash -25.00 USD 2016-12-07 * Pouring Ribbons Drinks with friends Expenses:Tips 4.00 USD 2016-12-07 * Pouring Ribbons Drinks with friends Expenses:Alcohol 21.00 USD Notice how the values of Transaction fields are replicated for each posting. This is exactly like a regular database join operation. The posting fields begin at column \u201cAccount.\u201d (Also note that this example table is simplified; in practice there are many more fields.) If you had a joined table just like this you could filter it and sum up amounts for arbitrary groups of postings. This is exactly what the bean-query tool allows you to do: You can run an SQL query on the data equivalent to this in-memory table and list values like this: SELECT date, payee, number WHERE account = \"Liabilities:CreditCard\"; Or sum up positions like this: SELECT account, sum(position) GROUP BY account; This simple last command generates the trial balance report. Note that the table representation does not inherently constrain the postings to sum to zero. If your selection criteria for the rows (in the WHERE clause) always selects all the postings for each of the matching transactions, you are ensured that the final sum of all the postings is zero. If not, the sum may be anything else. Just something to keep in mind. If you\u2019re familiar with SQL databases, you might ask why Beancount doesn\u2019t simply process its data in order to fill up an existing database system, so that the user could then use those database\u2019s tools. There are two main reasons for this: Reporting Operations. In order to generate income statements and balance sheets, the list of transactions needs to be preprocessed using the clear, open and close operations described previously. These operations are not trivial to implement in database queries and are dependent on just the report and ideally don\u2019t need to modify the input data. We\u2019d have to load up the posting data into memory and then run some code. We\u2019re already doing that by parsing the input file; the database step would be superfluous. Aggregating Positions. Though we haven\u2019t discussed it in this document so far, the contents of accounts may contain different types of commodities, as well as positions with an attached cost basis. The way that these positions are aggregated together requires the implementation of a custom data type because it obeys some rules about how positions are able to cancel each other out (see How Inventories Work for details). It would be very difficult to build these operations with an SQL database beyond the context of using just a single currency and ignoring cost basis. This is why Beancount provides a custom tool to directly process and query its data: It provides its own implementation of an SQL client that lets you specify open and close dates and leverages a custom \u201cInventory\u201d data structure to create sums of the positions of postings. This tools supports columns of Beancount\u2019s core types: Amount, Position and Inventory objects. (In any case, if you\u2019re not convinced, Beancount provides a tool to export its contents to a regular SQL database system. Feel free to experiment with it if you like, knock yourself out.) Please don\u2019t pay attention to the numbers in these large figures, they were randomly generated and don\u2019t reflect this. We\u2019re just interested in showing the structure, in these figures. \u21a9 Note that this is unrelated to the term \u201cclearing transactions\u201d which means acknowledging or marking that some transactions have been eyeballed by the bookkeeper and checked for correctness. \u21a9 Note that operations have nothing to do with the Open and Close directives Beancount provides. \u21a9 This is not provided yet in Beancount, but would be trivial to implement. All we'd need to do is invert the signs of balances from Liabilities, Income and Equity accounts. It's on the roadmap to provide this eventually. \u21a9","title":"The Double Entry Counting Method"},{"location":"the_double_entry_counting_method.html#the-double-entry-counting-method","text":"Martin Blais, December 2016 http://furius.ca/beancount/doc/double-entry Introduction Basics of Double-Entry Bookkeeping Statements Single-Entry Bookkeeping Double-Entry Bookkeeping Many Accounts Multiple Postings Types of Accounts Trial Balance Income Statement Clearing Income Equity Balance Sheet Summarizing Period Reporting Chart of Accounts Country-Institution Convention Credits & Debits Accounting Equations Plain-Text Accounting The Table Perspective","title":"The Double-Entry Counting Method"},{"location":"the_double_entry_counting_method.html#introduction","text":"This document is a gentle introduction to the double-entry counting method, as written from the perspective of a computer scientist. It is an attempt to explain basic bookkeeping using as simple an approach as possible, doing away with some of the idiosyncrasies normally involved in accounting. It is also representative of how Beancount works, and it should be useful to all users of plain-text accounting . Note that I am not an accountant, and in the process of writing this document I may have used terminology that is slightly different or unusual to that which is taught in perhaps more traditional training in accounting. I granted myself license to create something new and perhaps even unusual in order to explain those ideas as simply and clearly as possible to someone unfamiliar with them. I believe that the method of double-entry counting should be taught to everyone at the high school level everywhere as it is a tremendously useful organizational skill, and I hope that this text can help spread its knowledge beyond professional circles.","title":"Introduction"},{"location":"the_double_entry_counting_method.html#basics-of-double-entry-bookkeeping","text":"The double-entry system is just a simple method of counting , with some simple rules. Let\u2019s begin by defining the notion of an account . An account is something that can contain things, like a bag. It is used to count things, to accumulate things. Let\u2019s draw a horizontal arrow to visually represent the evolving contents of an account over time: On the left, we have the past, and to the right, increasing time: the present, the future, etc. For now, let\u2019s assume that accounts can contain only one kind of thing, for example, dollars . All accounts begin with an empty content of zero dollars. We will call the number of units in the account the balance of an account. Note that it represents its contents at a particular point in time. I will draw the balance using a number above the account\u2019s timeline: The contents of accounts can change over time. In order to change the content of an account, we have to add something to it. We will call this addition a posting to an account, and I will draw this change as a circled number on the account\u2019s timeline, for example, adding $100 to the account: Now, we can draw the updated balance of the account after the posting with another little number right after it: The account\u2019s balance, after adding $100, is now $100. We can also remove from the contents of an account. For example, we could remove $25, and the resulting account balance is now $75: Account balances can also become negative , if we remove more dollars than there are in the account. For example, if we remove $200 from this account, the balance now becomes $-125: It\u2019s perfectly fine for accounts to contain a negative balance number. Remember that all we\u2019re doing is counting things. As we will see shortly, some accounts will remain with a negative balance for most of their timeline.","title":"Basics of Double-Entry Bookkeeping"},{"location":"the_double_entry_counting_method.html#statements","text":"Something worthy of notice is how the timeline notation I\u2019ve written in the previous section is analogous to paper account statements institutions maintain for each client and which you typically receive through the mail: Date Description Amount Balance 2016-10-02 ... 100.00 1100.00 2016-10-05 ... -25.00 1075.00 2016-10-06 ... -200.00 875.00 Final Balance 875.00 Sometimes the amount column is split into two, one showing the positive amounts and the other the negative ones: Date Description Debit Credit Balance 2016-10-02 ... 100.00 1100.00 2016-10-05 ... 25.00 1075.00 2016-10-06 ... 200.00 875.00 Final Balance 875.00 Here, \u201cdebit\u201d means \u201cremoved from your account\u201d and \u201ccredit\u201d means \u201cdeposited in your account.\u201d Sometimes the words \u201cwithdrawals\u201d and \u201cdeposits\u201d will be used. It all depends on context: for checking and savings accounts it is usual to have both types of postings, but for a credit card account typically it shows only positive numbers and then the occasional monthly payment so the single column format is used. In any case, the \u201cbalance\u201d column always shows the resulting balance after the amount has been posted to the account. And sometimes the statements are rendered in decreasing order of time.","title":"Statements"},{"location":"the_double_entry_counting_method.html#single-entry-bookkeeping","text":"In this story, this account belongs to someone. We\u2019ll call this person the owner of the account. The account can be used to represent a real world account, for example, imagine that we use it to represent the content of the owner\u2019s checking account at a bank. So we\u2019re going to label the account by giving it a name, in this case \u201cChecking\u201d: Imagine that at some point, this account has a balance of $1000, like I\u2019ve drawn on the picture. Now, if the owner spends $79 of this account, we would represent it like this: Furthermore, if the expense was for a meal at a restaurant, we could flag the posting with a category to indicate what the change was used for. Let\u2019s say, \u201cRestaurant\u201d, like this: Now, if we have a lot of these, we could write a computer program to accumulate all the changes for each category and calculate the sums for each of them. That would tell us how much we spent in restaurants in total, for example. This is called the single-entry method of accounting. But we\u2019re not going to do it this way; we have a better way. Bear with me for a few more sections.","title":"Single-Entry Bookkeeping"},{"location":"the_double_entry_counting_method.html#double-entry-bookkeeping","text":"An owner may have multiple accounts. I will represent this by drawing many similar account timelines on the same graphic. As before, these are labeled with unique names. Let\u2019s assume that the owner has the same \u201cChecking\u201d account as previously, but now also a \u201c Restaurant \u201d account as well, which can be used to accumulate all food expenses at restaurants. It looks like this: Now, instead of categorizing the posting to a \u201crestaurant category\u201d as we did previously, we could create a matching posting on the \u201cRestaurant\u201d account to record how much we spent for food, with the amount spent ($79): The \u201cRestaurant\u201d account, like all other accounts, also has an accumulated balance, so we can find out how much we spent in \u201cRestaurant\u201d in total. This is entirely symmetrical to counting changes in a checking account. Now, we can associate the two postings together, by creating a kind of \u201cparent\u201d box that refers to both of them. We will call this object a transaction : Notice here that we\u2019ve also associated a description to this transaction: \u201cDinner at Uncle Boons\u201d. A transaction also has a date , and all of its postings are recorded to occur on that date. We call this the transaction date. We can now introduce the fundamental rule of double-entry bookkeeping system: The sum of all the postings of a transaction must equal zero. Remember this, as this is the foundation of the double-entry method, and its most important characteristic. It has important consequences which I will discuss later in this document. In our example, we remove $79 from the \u201cChecking\u201d account and \u201cgive it\u201d to the \u201cRestaurant\u201d account. ($79) + ($-79) = $0. To emphasize this, I could draw a little summation line under the postings of the transaction, like this:","title":"Double-Entry Bookkeeping"},{"location":"the_double_entry_counting_method.html#many-accounts","text":"There may be many such transactions, over many different accounts. For example, if the owner of the accounts had a lunch the next day which she paid using a credit card, it could be represented by creating a \u201cCredit Card\u201d account dedicated to tracking the real world credit card balance, and with a corresponding transaction: In this example, the owner spent $35 at a restaurant called \u201cEataly.\u201d The previous balance of the owner\u2019s credit card was $-450; after the expense, the new balance is $-485. For each real world account, the owner can create a bookkeeping account like we did. Also, for each category of expenditure, the owner also creates a bookkeeping account. In this system, there are no limits to how many accounts can be created. Note that the balance in the example is a negative number; this is not an error. Balances for credit card accounts are normally negative: they represent an amount you owe , that the bank is lending you on credit . When your credit card company keeps track of your expenses, they write out your statement from their perspective, as positive numbers. For you, those are amounts you need to eventually pay. But here, in our accounting system, we\u2019re representing numbers from the owner\u2019s point-of-view, and from her perspective, this is money she owes, not something she has. What we have is a meal sitting in our stomach (a positive number of $ of \u201cRestaurant\u201d).","title":"Many Accounts"},{"location":"the_double_entry_counting_method.html#multiple-postings","text":"Finally, transactions may have more than two postings; in fact, they may have any number of postings. The only thing that matters is that the sum of their amounts is zero (from the rule of double-entry bookkeeping above). For example, let\u2019s look at what would happen if the owner gets her salary paid for December: Her gross salary received in this example is recorded as $-2,905 (I\u2019ll explain the sign in a moment). $905 is set aside for taxes. Her \u201cnet\u201d salary of $2,000, the remainder, is deposited in her \u201cChecking\u201d account and the resulting balance of that account is $2,921 (the previous balance of $921 + $2,000 = $2,921). This transaction has three postings: (+2,000) + (-2,905) + (+905) = 0. The double-entry rule is respected. Now, you may ask: Why is her salary recorded as a negative number? The reasoning here is similar to that of the credit card above, though perhaps a bit more subtle. These accounts exist to track all the amounts from the owner\u2019s point-of-view. The owner gives out work, and receives money and taxes in exchange for it (positive amounts). The work given away is denominated in dollar units. It \u201cleaves\u201d the owner (imagine that the owner has potential work stored in her pocket and as she goes into work every day sprinkles that work potential giving it to the company). The owner gave $2,905\u2019s worth of work away. We want to track how much work was given, and it\u2019s done with the \u201cSalary\u201d account. That\u2019s her gross salary. Note also that we\u2019ve simplified this paycheck transaction a bit, for the sake of keeping things simple. A more realistic recording of one\u2019s pay stub would have many more accounts; we would separately account for state and federal tax amounts, as well as social security and medicare payments, deductions, insurance paid through work, and vacation time accrued during the period. But it wouldn\u2019t be much more complicated: the owner would simply translate all the amounts available from her pay stub into a single transaction with more postings. The structure remains similar.","title":"Multiple Postings"},{"location":"the_double_entry_counting_method.html#types-of-accounts","text":"Let\u2019s now turn our attention to the different types of accounts an owner can have. Balance or Delta. First, the most important distinction between accounts is about whether we care about the balance at a particular point in time, or whether it only makes sense to care about differences over a period of time. For example, the balance of someone\u2019s Checking or Savings accounts is a meaningful number that both the owner and its corresponding bank will care about. Similarly, the total amount owed on someone\u2019s Credit Card account is also meaningful. The same goes with someone\u2019s remaining Mortgage amount to pay on a house. On the other hand, the total amount of Restaurant expenses since the beginning of somebody\u2019s life on earth is not particularly interesting. What we might care about for this account is the amount of Restaurant expenses incurred over a particular period of time . For example, \u201chow much did you spend in restaurants last month?\u201d Or last quarter. Or last year. Similarly, the total amount of gross salary since the beginning of someone\u2019s employment at a company a few years ago is not very important. But we would care about the total amount earned during a tax year, that is, for that time period, because it is used for reporting one\u2019s income to the tax man. Accounts whose balance at a point in time is meaningful are called balance sheet accounts . There are two types of such accounts: \u201c Assets \u201d and \u201c Liabilities .\u201d The other accounts, that is, those whose balance is not particularly meaningful but for which we are interested in calculating changes over a period of time are called income statement accounts . Again, there are two kinds: \u201c Income \u201d and \u201c Expenses .\u201d Normal sign. Secondly, we consider the usual sign of an account\u2019s balance . The great majority of accounts in the double-entry system tend to have a balance with always a positive sign, or always a negative sign (though as we\u2019ve seen previously, it is not impossible that an account\u2019s balance could change signs). This is how we will distinguish between the pairs of accounts mentioned before: For a balance sheet account, Assets normally have positive balances, and Liabilities normally have negative balances. For income statement accounts, Expenses normally have a positive balance, and Income accounts normally have a negative balance. This situation is summarized in the following table: Balance: Positive (+) Balance: Negative (-) Balance matters at a point in time (Balance Sheet) Assets Liabilities Change in balance matters over a period of time (Income Statement) Expenses Income Let\u2019s discuss each type of account and provide some examples, so that it doesn\u2019t remain too abstract. Assets. (+) Asset accounts represent something the owner has . A canonical example is banking accounts. Another one is a \u201ccash\u201d account, which counts how much money is in your wallet. Investments are also assets (their units aren\u2019t dollars in this case, but rather some number of shares of some mutual fund or stock). Finally, if you own a home, the home itself is considered an asset (and its market value fluctuates over time). Liabilities. (-) A liability account represents something the owner owes . The most common example is a credit card. Again, the statement provided by your bank will show positive numbers, but from your own perspective, they are negative numbers. A loan is also a liability account. For example, if you take out a mortgage on a home, this is money you owe, and will be tracked by an account with a negative amount. As you pay off the mortgage every month the negative number goes up, that is, its absolute value gets smaller and smaller over time (e.g., -120,000 -> -117,345). Expenses. (+) An expense account represents something you\u2019ve received , perhaps by exchanging something else to purchase it. This type of account will seem pretty natural: food, drinks, clothing, rent, flights, hotels and most other categories of things you typically spend your disposable income on. However, taxes are also typically tracked by an expense account: when you receive some salary income, the amount of taxes withheld at the source is recorded immediately as an expense. Think of it as paying for government services you receive throughout the year. Income. (-) An income account is used to count something you\u2019ve given away in order to receive something else (typically assets or expenses). For most people with jobs, that is the value of their time (a salary income). Specifically, here we\u2019re talking about the gross income. For example, if you\u2019re earning a salary of $120,000/year, that number is $120,000, not whatever amount remains after paying for taxes. Other types of income includes dividends received from investments, or interest paid from bonds held. There are also a number of oddball things received you might record as income, such the value of rewards received, e.g., cash back from a credit card, or monetary gifts from someone. In Beancount, all account names, without exception, must be associated to one of the types of accounts described previously. Since the type of an account never changes during its lifetime, we will make its type a part of an account\u2019s name, as a prefix , by convention. For example, the qualified account name for restaurant will be \u201cExpenses:Restaurant\u201d. For the bank checking account, the qualified account name will be \u201cAssets:Checking\u201d. Other than that, you can select any name you like for your accounts. You can create as many accounts as you like, and as we will see later, you can organize them in a hierarchy. As of the writing of this document, I\u2019m using more than 700 accounts to track my personal affairs. Let us now revisit our example and add some more accounts: And let\u2019s imagine there are more transactions: \u2026 and even more of them: Finally, we can label each of those accounts with one of the four types of accounts by prepending the type to their account names: A realistic book from someone tracking all of their personal affairs might easily contain thousands of transactions per year. But the principles remain simple and they remain the same: postings are applied to accounts over time, and must be parented to a transaction, and within this transaction the sum of all the postings is zero. When you do bookkeeping for a set of accounts, you are essentially describing all the postings that happen on all the accounts over time, subject to the constraint of the rule. You are creating a database of those postings in a book . You are \u201ckeeping the book,\u201d that is, traditionally, the book which contains all those transactions. Some people call this \u201cmaintaining a journal.\u201d We will now turn our attention to obtaining useful information from this data, summarizing information from the book.","title":"Types of Accounts"},{"location":"the_double_entry_counting_method.html#trial-balance","text":"Take our last example: we can easily reorder all the accounts such that all the Asset accounts appear together at the top, then all the Liabilities accounts, then Income, and finally Expenses accounts. We are simply changing the order without modifying the structure of transactions, in order to group each type of accounts together: We\u2019ve reordered the accounts with Assets accounts grouped at the top, then Liabilities, then some Equity accounts (which we have just introduced, more about them is discussed later), then Income and finally Expenses at the bottom. If we sum up the postings on all of the accounts and render just the account name and its final balance on the right, we obtain a report we call the \u201ctrial balance.\u201d This simply reflects the balance of each account at a particular point in time. And because each of the accounts began with a zero balance, and each transaction has itself a zero balance, we know that the sum of all those balances must equal zero. 1 This is a consequence of our constraining that each of the postings be part of a transaction, and that each transaction have postings that balance each other out.","title":"Trial Balance"},{"location":"the_double_entry_counting_method.html#income-statement","text":"One kind of common information that is useful to extract from the list of transactions is a summary of changes in income statement accounts during a particular period of time. This tells us how much money was earned and spent during this period, and the difference tells us how much profit (or loss) was incurred. We call this the \u201cnet income.\u201d In order to generate this summary, we simply turn our attention to the balances of the accounts of types Income and Expenses, summing up just the transactions for a particular period, and we draw the Income balances on the left, and Expenses balances on the right: It is important to take note of the signs here: Income numbers are negative, and Expenses numbers are positive. So if you earned more than you spent (a good outcome), the final sum of Income + Expenses balances will be a negative number. Like any other income, a net income that has a negative number means that there is a corresponding amount of Assets and/or Liabilities with positive numbers (this is good for you). An Income Statement tells us what changed during a particular period of time. Companies typically report this information quarterly to investors and perhaps the public (if they are a publicly traded company) in order to share how much profit they were able to make. Individuals typically report this information on their annual tax returns.","title":"Income Statement"},{"location":"the_double_entry_counting_method.html#clearing-income","text":"Notice how in the income statement only the transactions within a particular interval of time are summed up. This allows one, for instance, to compute the sum of all income earned during a year. If we were to sum up all of the transactions of this account since its inception we would obtain the total amount of income earned since the account was created. A better way to achieve the same thing is to zero out the balances of the Income and Expenses accounts. Beancount calls this basic transformation \u201cclearing 2 .\u201d It is carried out by: Computing the balances of those accounts from the beginning of time to the start of the reporting period. For example, if you created your accounts in year 2000 and you wanted to generate an income statement for year 2016, you would sum up the balances from 2000 to Jan 1, 2016. Inserting transactions to empty those balances and transfer them to some other account that isn\u2019t Income nor Expenses. For instance, if the restaurant expense account for those 16 years amounts to $85,321 on Jan 1, 2016, it would insert a transaction of $-85,321 to restaurants and $+85,321 to \u201cprevious earnings\u201d. The transactions would be dated Jan 1, 2016. Including this transaction, the sum of that account would zero on that date. This is what we want. Those transactions inserted for all income statement accounts are pictured in green below. Now summing the entire set of transactions through the end of the ledger would yield only the changes during year 2016 because the balances were zero on that date: This is the semantics of the \u201cCLEAR\u201d operation of the bean-query shell. (Note that another way to achieve the same thing for income statement accounts would be to segregate and count amounts only for the transactions after the clearing date; however, jointly reporting on income statement accounts and balance sheet accounts would have incorrect balances for the balance sheet accounts.)","title":"Clearing Income"},{"location":"the_double_entry_counting_method.html#equity","text":"The account that receives those previously accumulated incomes is called \u201cPrevious Earnings\u201d. It lives in a fifth and final type of accounts: Equity . We did not talk about this type of accounts earlier because they are most often only used to transfer amounts to build up reports, and the owner usually doesn\u2019t post changes to those types of accounts; the software does that automatically, e.g., when clearing net income. The account type \u201cequity\u201d is used for accounts that hold a summary of the net income implied by all the past activity. The point is that if we now list together the Assets, Liabilities and Equity accounts, because the Income and Expenses accounts have been zero\u2019ed out, the sum total of all these balances should equal exactly zero. And summing up all the Equity accounts clearly tells us what\u2019s our stake in the entity, in other words, if you used the assets to pay off all the liabilities, how much is left in the business\u2026 how much it\u2019s worth. Note that the normal sign of the Equity accounts is negative . There is no particular meaning to that, just that they are used to counterbalance Assets and Liabilities and if the owner has any value, that number should be negative. (A negative Equity means some positive net worth.) There are a few different Equity accounts in use in Beancount: Previous Earnings or Retained Earnings. An account used to hold the sum total of Income & Expenses balances from the beginning of time until the beginning of a reporting period. This is the account we were referring to in the previous section. Current Earnings , also called Net Income. An account used to contain the sum of Income & Expenses balances incurred during the reporting period. They are filled in by \u201cclearing\u201d the Income & Expenses accounts at the end of the reporting period. Opening Balances. An equity account used to counterbalance deposits used to initialize accounts. This type of account is used when we truncate the past history of transactions, but we also need to ensure that an account\u2019s balance begins its history with a particular amount. Once again: you don\u2019t need to define nor use these accounts yourself, as these are created for the purpose of summarizing transactions. Generally, the accounts are filled in by the clearing process described above, or filled in by Pad directives to \u201copening balances\u201d equity accounts, to account for summarized balances from the past. They are created and filled in automatically by the software. We\u2019ll see how these get used in the following sections.","title":"Equity"},{"location":"the_double_entry_counting_method.html#balance-sheet","text":"Another kind of summary is a listing of the owner\u2019s assets and debts, for each of the accounts. This answers the question: \u201c Where\u2019s the money? \u201d In theory, we could just restrict our focus to the Assets and Liabilities accounts and draw those up in a report: However, in practice, there is another closely related question that comes up and which is usually answered at the same time: \u201c Once all debts are paid off, how much are we left with? \u201d This is called the net worth . If the Income & Expenses accounts have been cleared to zero and all their balances have been transferred to Equity accounts, the net worth should be equal to the sum of all the Equity accounts. So in building up the Balance Sheet, it it customary to clear the net income and then display the balances of the Equity accounts. The report looks like this: Note that the balance sheet can be drawn for any point in time , simply by truncating the list of transactions following a particular date. A balance sheet displays a snapshot of balances at one date; an income statement displays the difference of those balances between two dates.","title":"Balance Sheet"},{"location":"the_double_entry_counting_method.html#summarizing","text":"It is useful to summarize a history of past transactions into a single equivalent deposit. For example, if we\u2019re interested in transactions for year 2016 for an account which has a balance of $450 on Jan 1, 2016, we can delete all the previous transactions and replace them with a single one that deposits $450 on Dec 31, 2015 and that takes it from somewhere else. That somewhere else will be the Equity account Opening Balances . First, we can do this for all Assets and Liabilities accounts (see transactions in blue): Then we delete all the transactions that precede the opening date, to obtain a truncated list of transactions: This is a useful operation when we\u2019re focused on the transactions for a particular interval of time. (This is a bit of an implementation detail: these operations are related to how Beancount is designed. Instead of making all the reporting operations with parameters, all of its reporting routines are simplified and instead operate on the entire stream of transactions; in this way, we convert the list of transactions to include only the data we want to report on. In this case, summarization is just a transformation which accepts the full set of transactions and returns an equivalent truncated stream. Then, from this stream, a journal can be produced that excludes the transactions from the past. From a program design perspective, this is appealing because the only state of the program is a stream of transactions, and it is never modified directly. It\u2019s simple and robust.)","title":"Summarizing"},{"location":"the_double_entry_counting_method.html#period-reporting","text":"Now we know we can produce a statement of changes over a period of time, by \u201cclearing\u201d and looking at just the Income & Expenses accounts (the Income Statement). We also know we can clear to produce a snapshot of Assets, Liabilities & Equity at any point in time (the Balance Sheet). More generally, we\u2019re interested in inspecting a particular period of time. That implies an income statement, but also two balance sheet statements: the balance sheet at the beginning of the period, and the balance sheet at the end of the period. In order to do this, we apply the following transformations: Open. We first clear net income at the beginning of the period, to move all previous income balances to the Equity Previous Earnings account. We then summarize up to the beginning of the period. We call the combination of clearing + summarizing: \u201cOpening.\u201d Close. We also truncate all the transactions following the end of the reporting period. We call this operation \u201cClosing.\u201d These are the meaning of the \u201cOPEN\u201d and \u201cCLOSE\u201d operations of the bean-query shell 3 . The resulting set of transactions should look like this. \u201cClosing\u201d involves two steps. First, we remove all transactions following the closing date: We can process this stream of transactions to produce an Income Statement for the period. Then we clear again at the end date of the desired report, but this time we clear the net income to \u201cEquity:Earnings:Current\u201d: From these transactions, we produce the Balance Sheet at the end of the period. This sums up the operations involved in preparing the streams of transactions to produce reports with Beancount, as well as a basic introduction to those types of reports.","title":"Period Reporting"},{"location":"the_double_entry_counting_method.html#chart-of-accounts","text":"New users are often wondering how much detail they should use in their account names. For example, should one include the payee in the account name itself, such as in these examples? Expenses:Phone:Mobile:VerizonWireless Assets:AccountsReceivable:Clients:AcmeInc Or should one use simpler names like the following, relying instead on the \u201cpayee\u201d, \u201ctags\u201d, or perhaps some other metadata in order to group the postings? Expenses:Phone Assets:AccountsReceivable The answer is that it depends on you . This is an arbitrary choice to make. It\u2019s a matter of taste. Personally I like to abuse the account names a bit and create long descriptive ones, other people prefer to keep them simple and use tags to group their postings. Sometimes one doesn\u2019t even need to filter subgroups of postings. There\u2019s no right answer, it depends on what you\u2019d like to do. One consideration to keep in mind is that account names implicitly define a hierarchy. The \u201c:\u201d separator is interpreted by some reporting code to create an in-memory tree and can allow you to collapse a node\u2019s children subaccounts and compute aggregates on the parent. Think of this as an additional way to group postings.","title":"Chart of Accounts"},{"location":"the_double_entry_counting_method.html#country-institution-convention","text":"One convention I\u2019ve come up with that works well for my assets, liabilities and income accounts is to root the tree with a code for the country the account lives in, followed by a short string for the institution it corresponds to. Underneath that, a unique name for the particular account in that institution. Like this: : : : For example, a checking account could be chosen to be \u201c Assets:US:BofA:Checking \u201d, where \u201cBofA\u201d stands for \u201cBank of America.\u201d A credit card account could include the name of the particular type of card as the account name, like \u201c Liabilities:US:Amex:Platinum \u201d, which can be useful if you have multiple cards. I\u2019ve found it doesn\u2019t make sense for me to use this scheme for expense accounts, since those tend to represent generic categories. For those, it seems to make more sense to group them by category, as in using \u201c Expenses:Food:Restaurant \u201d instead of just \u201c Expenses:Restaurant \u201d. In any case, Beancount doesn\u2019t enforce anything other than the root accounts; this is just a suggestion and this convention is not coded anywhere in the software. You have great freedom to experiment, and you can easily change all the names later by processing the text file. See the Cookbook for more practical guidance.","title":"Country-Institution Convention"},{"location":"the_double_entry_counting_method.html#credits-debits","text":"At this point, we haven\u2019t discussed the concepts of \u201ccredits\u201d and \u201cdebits.\u201d This is on purpose: Beancount largely does away with these concepts because it makes everything else simpler. I believe that it is simpler to just learn that the signs of Income, Liabilities and Equity accounts are normally negative and to treat all accounts the same way than to deal with the debits and credits terminology and to treat different account categories differently. In any case, this section explains what these are. As I have pointed out in previous sections, we consider \u201cIncome\u201d, \u201cLiabilities\u201d and \u201cEquity\u201d accounts to normally have a negative balance. This may sound odd; after all, nobody thinks of their gross salary as a negative amount, and certainly your credit-card bill or mortgage loan statements report positive numbers. This is because in our double-entry accounting system we consider all accounts to be held from the perspective of the owner of the account . We use signs consistent from this perspective, because it makes all operations on account contents straightforward: they\u2019re all just simple additions and all the accounts are treated the same. In contrast, accountants traditionally keep all the balances of their accounts as positive numbers and then handle postings to those accounts differently depending on the account type upon which they are applied. The sign to apply to each account is entirely dictated by its type: Assets and Expenses accounts are debit accounts and Liabilities, Equity and Income accounts are credit accounts and require a sign adjustment. Moreover, posting a positive amount on an account is called \u201cdebiting\u201d and removing from an account is called \u201ccrediting.\u201d See this external document , for example, which nearly makes my head explode, and this recent thread has more detail. This way of handling postings makes everything much more complicated than it needs to be. The problem with this approach is that summing of amounts over the postings of a transaction is not a straightforward sum anymore. For example, let\u2019s say you\u2019re creating a new transaction with postings to two Asset accounts, an Expenses account and an Income account and the system tells you there is a $9.95 imbalance error somewhere. You\u2019re staring at the entry intently; which of the postings is too small? Or is one of the postings too large? Also, maybe a new posting needs to be added, but is it to a debit account or to a credit account? The mental gymnastics required to do this are taxing. Some double-entry accounting software tries to deal with this by creating separate columns for debits and credits and allowing the user enter an amount only in the column that corresponds to each posting account\u2019s type. This helps visually, but why not just use signs instead? Moreover, when you look at the accounting equations, you have to consider their signs as well. This makes it awkward to do transformations on them and make what is essentially a simple summation over postings into a convoluted mess that is difficult to understand. In plain-text accounting, we would rather just do away with this inconvenient baggage. We just use additions everywhere and learn to keep in mind that Liabilities, Equity and Income accounts normally have a negative balance. While this is unconventional, it\u2019s much easier to grok. And If there is a need to view a conventional report with positive numbers only, we will be able to trigger that in reporting code 4 , inverting the signs just to render them in the output. Save yourself some pain: Flush your brain from the \"debit\" and \"credit\" terminology.","title":"Credits & Debits"},{"location":"the_double_entry_counting_method.html#accounting-equations","text":"In light of the previous sections, we can easily express the accounting equations in signed terms. If, A = the sum of all Assets postings L = the sum of all Liabilities postings X = the sum of all Expenses postings I = the sum of all Income postings E = the sum of all Equity postings We can say that: A + L + E + X + I = 0 This follows from the fact that sum(all postings) = 0 Which follows from the fact that each transaction is guaranteed to sum up to zero (which is enforced by Beancount): for all transactions t, sum(postings of t) = 0 Moreover, the sum of postings from Income and Expenses is the Net Income (NI): NI = X + I If we adjust the equity to reflect the total Net Income effect by clearing the income to the Equity retained earnings account, we get an updated Equity value (E\u2019): E\u2019 = E + NI = E + X + I And we have a simplified accounting equation: A + L + E\u2019 = 0 If we were to adjust the signs for credits and debits (see previous section) and have sums that are all positive number, this becomes the familiar accounting equation: Assets - Liabilities = Equity As you can see, it\u2019s much easier to just always add up the numbers.","title":"Accounting Equations"},{"location":"the_double_entry_counting_method.html#plain-text-accounting","text":"Ok, so now we understand the method and what it can do for us, at least in theory. The purpose of a double-entry bookkeeping system is to allow you to replicate the transactions that occur in various real world accounts into a single, unified system, in a common representation, and to extract various views and reports from this data. Let us now turn our attention to how we record this data in practice. This document talks about Beancount, whose purpose is \u201cdouble-entry bookkeeping using text files.\u201d Beancount implements a parser for a simple syntax that allows you to record transactions and postings. The syntax for an example transaction looks something like this: 2016-12-06 * \"Biang!\" \"Dinner\" Liabilities:CreditCard -47.23 USD Expenses:Restaurants You write many of declarations like these in a file, and Beancount will read it and create the corresponding data structures in memory. Verification. After parsing the transactions, Beancount also verifies the rule of the double-entry method: it checks that the sum of the postings on all your transactions is zero. If you make a mistake and record a transaction with a non-zero balance, an error will be displayed. Balance Assertions. Beancount allows you to replicate balances declared from external accounts, for example, a balance written on a monthly statement. It processes those and checks that the balances resulting from your input transactions match those declared balances. This helps you detect and find mistakes easily. Plugins. Beancount allows you to build programs which can automate and/or process the streams of transactions in your input files. You can build custom functionality by writing code which directly processes the transaction stream. Querying & Reporting. It provides tools to then process this stream of transactions to produce the kinds of reports we discussed earlier in this document. There are a few more details, for example, Beancount allows you to track cost basis and make currency conversions, but that\u2019s the essence of it.","title":"Plain-Text Accounting"},{"location":"the_double_entry_counting_method.html#the-table-perspective","text":"Almost always, questions asked by users on the mailing-list about how to calculate or track some value or other can be resolved easily simply by thinking of the data as a long list of rows, some of which need to be filtered and aggregated. If you consider that all that we\u2019re doing in the end is deriving \u201csums\u201d of these postings, and that the attributes of transactions and postings are what allows us to filter subsets of postings, it always becomes very simple. In almost all the cases, the answer is to find some way to disambiguate postings to select them, e.g. by account name, by attaching some tag, by using some metadata, etc. It can be illuminating to consider how this data can be represented as a table. Imagine that you have two tables: a table containing the fields of each Transaction such as date and description, and a table for the fields of each Posting, such as account, amount and currency, as well as a reference to its parent transaction. The simplest way to represent the data is to join those two tables, replicating values of the parent transaction across each of the postings. For example, this Beancount input: 2016-12-04 * \"Christmas gift\" Liabilities:CreditCard -153.45 USD Expenses:Gifts 2016-12-06 * \"Biang!\" \"Dinner\" Liabilities:CreditCard -47.23 USD Expenses:Restaurants 2016-12-07 * \"Pouring Ribbons\" \"Drinks with friends\" Assets:Cash -25.00 USD Expenses:Tips 4.00 USD Expenses:Alcohol could be rendered as a table like this: Date Fl Payee Narration Account Number Ccy 2016-12-04 * Christmas gift Liabilities:CreditCard -153.45 USD 2016-12-04 * Christmas gift Expenses:Gifts 153.45 USD 2016-12-06 * Biang! Dinner Liabilities:CreditCard -47.23 USD 2016-12-06 * Biang! Dinner Expenses:Restaurants 47.23 USD 2016-12-07 * Pouring Ribbons Drinks with friends Assets:Cash -25.00 USD 2016-12-07 * Pouring Ribbons Drinks with friends Expenses:Tips 4.00 USD 2016-12-07 * Pouring Ribbons Drinks with friends Expenses:Alcohol 21.00 USD Notice how the values of Transaction fields are replicated for each posting. This is exactly like a regular database join operation. The posting fields begin at column \u201cAccount.\u201d (Also note that this example table is simplified; in practice there are many more fields.) If you had a joined table just like this you could filter it and sum up amounts for arbitrary groups of postings. This is exactly what the bean-query tool allows you to do: You can run an SQL query on the data equivalent to this in-memory table and list values like this: SELECT date, payee, number WHERE account = \"Liabilities:CreditCard\"; Or sum up positions like this: SELECT account, sum(position) GROUP BY account; This simple last command generates the trial balance report. Note that the table representation does not inherently constrain the postings to sum to zero. If your selection criteria for the rows (in the WHERE clause) always selects all the postings for each of the matching transactions, you are ensured that the final sum of all the postings is zero. If not, the sum may be anything else. Just something to keep in mind. If you\u2019re familiar with SQL databases, you might ask why Beancount doesn\u2019t simply process its data in order to fill up an existing database system, so that the user could then use those database\u2019s tools. There are two main reasons for this: Reporting Operations. In order to generate income statements and balance sheets, the list of transactions needs to be preprocessed using the clear, open and close operations described previously. These operations are not trivial to implement in database queries and are dependent on just the report and ideally don\u2019t need to modify the input data. We\u2019d have to load up the posting data into memory and then run some code. We\u2019re already doing that by parsing the input file; the database step would be superfluous. Aggregating Positions. Though we haven\u2019t discussed it in this document so far, the contents of accounts may contain different types of commodities, as well as positions with an attached cost basis. The way that these positions are aggregated together requires the implementation of a custom data type because it obeys some rules about how positions are able to cancel each other out (see How Inventories Work for details). It would be very difficult to build these operations with an SQL database beyond the context of using just a single currency and ignoring cost basis. This is why Beancount provides a custom tool to directly process and query its data: It provides its own implementation of an SQL client that lets you specify open and close dates and leverages a custom \u201cInventory\u201d data structure to create sums of the positions of postings. This tools supports columns of Beancount\u2019s core types: Amount, Position and Inventory objects. (In any case, if you\u2019re not convinced, Beancount provides a tool to export its contents to a regular SQL database system. Feel free to experiment with it if you like, knock yourself out.) Please don\u2019t pay attention to the numbers in these large figures, they were randomly generated and don\u2019t reflect this. We\u2019re just interested in showing the structure, in these figures. \u21a9 Note that this is unrelated to the term \u201cclearing transactions\u201d which means acknowledging or marking that some transactions have been eyeballed by the bookkeeper and checked for correctness. \u21a9 Note that operations have nothing to do with the Open and Close directives Beancount provides. \u21a9 This is not provided yet in Beancount, but would be trivial to implement. All we'd need to do is invert the signs of balances from Liabilities, Income and Equity accounts. It's on the roadmap to provide this eventually. \u21a9","title":"The Table Perspective"},{"location":"tracking_medical_claims.html","text":"Tracking Out-of-Network Medical Claims in Beancount \uf0c1 Martin Blais - Updated: November 2023 Let's illustrate how one might handle dealing with medical treatment with insurance and HSA claims. Let's use an example of psychotherapy sessions, received out-of-network and paid upfront out of pocket and reimbursed later. When a session is received, it is booked to receivables and payables: 2023-09-06 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD 2023-09-11 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD Later on, some payments are made for it, clearing the payables: 2023-09-12 * \"ZELLE SENT\" #freud-2023-09 Assets:US:BofA:Checking -520.00 USD Liabilities:AccountsPayable:Psychotherapy 520.00 USD And so on, for the entire month: 2023-09-20 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD 2023-09-23 * \"ZELLE SENT\" #freud-2023-09 Assets:US:BofA:Checking -260.00 USD Liabilities:AccountsPayable:Psychotherapy 260.00 USD 2023-09-27 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD 2023-09-28 * \"ZELLE SENT\" #freud-2023-09 Assets:US:BofA:Checking -260.00 USD Liabilities:AccountsPayable:Psychotherapy 260.00 USD At the end of the month, a claim form is produced by the therapist. We file the claim with the insurance company, clearing the receivable and shifting the remaining portion to a insurance company check to be issued: 2023-09-28 * \"Claim for September filed with insurance\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy -1040.00 USD Expenses:Mental:Psychotherapy 312.00 USD Assets:AccountsReceivable:Anthem 728.00 USD Eventually, an EOB is produced to confirm how much is covered (in this example, 70%, which was known from the terms) and a check is received and deposited at the bank: 2023-10-24 * \"ATM CHECK DEPOSIT\" #freud-2023-09 Assets:US:BofA:Checking 728.00 USD Assets:AccountsReceivable:Anthem -728.00 USD At this stage we know the amount of the remaining portion eligible to be paid from the HSA, so we file a claim to the HSA company, once again booking them to a receivable and a payable: 2023-10-25 * \"HealthEquity\" \"Filed for reimbursement\" #freud-2023-09 Liabilities:AccountsPayable:HealthEquity -312.00 USD Assets:AccountsReceivable:HealthEquity 312.00 USD The HSA company makes a direct deposit to our checking account: 2023-11-01 * \"BofA bank (Claim ID:1234567-890); EFT to bank\" #freud-2023-09 Assets:US:HealthEquity:Cash -312.00 USD Liabilities:AccountsPayable:HealthEquity 312.00 USD And on the bank side when we import this transaction we book it against the receivable: 2023-11-01 * \"HEALTHEQUITY INC\" #freud-2023-09 Assets:US:BofA:Checking 312.00 USD Assets:AccountsReceivable:HealthEquity -312.00 USD The final result is that the HSA was used to cover the uninsured portion of the cost. |-- Assets | |-- AccountsReceivable | | |-- Anthem | | |-- HealthEquity | | `-- Psychotherapy | `-- US | |-- HealthEquity | | `-- Cash -312.00 USD | `-- BofA | `-- Checking |-- Expenses | `-- Mental | `-- Psychotherapy 312.00 USD `-- Liabilities `-- AccountsPayable |-- HealthEquity `-- Psychotherapy Net Income: (-312.00 USD) There are some flaws with the approach above: The amount covered by insurance is known ahead of time; in many cases the amount is not known before the EOB is issued by the insurance (after all that's what it's for, it's the \"Explanation of Benefits\"). This would require modifying the above.","title":"Tracking Out-of-Network Medical Claims in Beancount"},{"location":"tracking_medical_claims.html#tracking-out-of-network-medical-claims-in-beancount","text":"Martin Blais - Updated: November 2023 Let's illustrate how one might handle dealing with medical treatment with insurance and HSA claims. Let's use an example of psychotherapy sessions, received out-of-network and paid upfront out of pocket and reimbursed later. When a session is received, it is booked to receivables and payables: 2023-09-06 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD 2023-09-11 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD Later on, some payments are made for it, clearing the payables: 2023-09-12 * \"ZELLE SENT\" #freud-2023-09 Assets:US:BofA:Checking -520.00 USD Liabilities:AccountsPayable:Psychotherapy 520.00 USD And so on, for the entire month: 2023-09-20 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD 2023-09-23 * \"ZELLE SENT\" #freud-2023-09 Assets:US:BofA:Checking -260.00 USD Liabilities:AccountsPayable:Psychotherapy 260.00 USD 2023-09-27 * \"Dr. Freud\" \"Session\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy 260.00 USD Liabilities:AccountsPayable:Psychotherapy -260.00 USD 2023-09-28 * \"ZELLE SENT\" #freud-2023-09 Assets:US:BofA:Checking -260.00 USD Liabilities:AccountsPayable:Psychotherapy 260.00 USD At the end of the month, a claim form is produced by the therapist. We file the claim with the insurance company, clearing the receivable and shifting the remaining portion to a insurance company check to be issued: 2023-09-28 * \"Claim for September filed with insurance\" #freud-2023-09 Assets:AccountsReceivable:Psychotherapy -1040.00 USD Expenses:Mental:Psychotherapy 312.00 USD Assets:AccountsReceivable:Anthem 728.00 USD Eventually, an EOB is produced to confirm how much is covered (in this example, 70%, which was known from the terms) and a check is received and deposited at the bank: 2023-10-24 * \"ATM CHECK DEPOSIT\" #freud-2023-09 Assets:US:BofA:Checking 728.00 USD Assets:AccountsReceivable:Anthem -728.00 USD At this stage we know the amount of the remaining portion eligible to be paid from the HSA, so we file a claim to the HSA company, once again booking them to a receivable and a payable: 2023-10-25 * \"HealthEquity\" \"Filed for reimbursement\" #freud-2023-09 Liabilities:AccountsPayable:HealthEquity -312.00 USD Assets:AccountsReceivable:HealthEquity 312.00 USD The HSA company makes a direct deposit to our checking account: 2023-11-01 * \"BofA bank (Claim ID:1234567-890); EFT to bank\" #freud-2023-09 Assets:US:HealthEquity:Cash -312.00 USD Liabilities:AccountsPayable:HealthEquity 312.00 USD And on the bank side when we import this transaction we book it against the receivable: 2023-11-01 * \"HEALTHEQUITY INC\" #freud-2023-09 Assets:US:BofA:Checking 312.00 USD Assets:AccountsReceivable:HealthEquity -312.00 USD The final result is that the HSA was used to cover the uninsured portion of the cost. |-- Assets | |-- AccountsReceivable | | |-- Anthem | | |-- HealthEquity | | `-- Psychotherapy | `-- US | |-- HealthEquity | | `-- Cash -312.00 USD | `-- BofA | `-- Checking |-- Expenses | `-- Mental | `-- Psychotherapy 312.00 USD `-- Liabilities `-- AccountsPayable |-- HealthEquity `-- Psychotherapy Net Income: (-312.00 USD) There are some flaws with the approach above: The amount covered by insurance is known ahead of time; in many cases the amount is not known before the EOB is issued by the insurance (after all that's what it's for, it's the \"Explanation of Benefits\"). This would require modifying the above.","title":"Tracking Out-of-Network Medical Claims in Beancount"},{"location":"trading_with_beancount.html","text":"Trading with Beancount \uf0c1 Martin Blais , July 2014 http://furius.ca/beancount/doc/trading Introduction What is Profit and Loss? Realized and Unrealized P/L Trade Lots Booking Methods Dated lots Reporting Unrealized P/L Commissions Stock Splits Cost Basis Adjustments Dividends Average Cost Booking Future Topics Introduction \uf0c1 This is a companion document for the Command-Line Accounting Cookbook that deals exclusively with the subject of trading and investments in Beancount. You probably should have read an introduction to the double-entry method before reading this document. The subject of stock trading needs to be preceded by a discussion of \u201cprofit and loss,\u201d or P/L, for short (pronounce: \u201cP and L\u201d), also called capital gains or losses. The notion of P/L against multiple trades can be difficult for a novice to understand, and I\u2019ve even seen professional traders lack sophistication in their understanding of P/L over varying time periods. It is worth spending a bit of time to explain this, and necessary to understand how to book your trades in a double-entry system. This discussion will be weaved with detailed examples of how to book these trades in Beancount, wherever possible. There is a related, active proposal for improving the booking methods in Beancount that you might also be interested in. Discussions of basis for tax-deferred accounts will not be treated here, but in the more general cookbook. What is Profit and Loss? \uf0c1 Let\u2019s imagine you have an account at the E*Trade discount broker and you buy some shares of a company, say IBM. If you buy 10 shares of IBM when its price is 160$/share, it will cost you 1600$. That value is what we will call the \u201cbook value\u201d, or equivalently, \u201cthe cost.\u201d This is how much money you had to spend in order to acquire the shares, also called \u201cthe position.\u201d This is how you would enter this transaction in Beancount: 2014-02-16 * \"Buying some IBM\" Assets:US:ETrade:IBM 10 IBM {160.00 USD} Assets:US:ETrade:Cash -1600.00 USD In practice you will probably pay some commission to E*Trade for this service, so let\u2019s put that in for completeness: 2014-02-16 * \"Buying some IBM\" Assets:US:ETrade:IBM 10 IBM {160.00 USD} Assets:US:ETrade:Cash -1609.95 USD Expenses:Financial:Commissions 9.95 USD This is how you tell Beancount to deposit some units \u201cat cost\u201d, in this case, units of \u201cIBM at 160 USD/share\u201d cost. This transaction balances because the sum of its legs is zero: 10 x 160 + -1609.95 + 9.95 = 0. Also note that we\u2019re choosing to use a subaccount dedicated to the shares of IBM; this is not strictly necessary but it is convenient for reporting in, for example, a balance sheet, because it will naturally aggregate all of your shares of each of your positions on their own line. Having a \u201ccash\u201d subaccount also emphasizes that uninvested funds you have there are not providing any return. The next day, the market opens and IBM shares are going for 170$/share. In this context, we will call this \u201cthe price.\u201d 1 The \u201cmarket value\u201d of your position, your shares, is the number of them x the market price, that is, 10 shares x 170$/share = 1700$. The difference between these two amounts is what we will call the P/L: market value - book value = P/L 10 x 170$ - 10 x 160$ = 1700$ - 1600$ = 100$ (profit) We will call a positive amount \u201ca profit\u201d and if the amount is negative, \u201ca loss.\u201d Realized and Unrealized P/L \uf0c1 The profit from the previous section is called an \u201cunrealized profit.\u201d That is because the shares have not actually been sold yet - this is a hypothetical profit: if I can sell those shares at the market value, this is how much I would pocket. The 100$ I mentioned in the previous section is actually an \u201cunrealized P/L.\u201d So let\u2019s say you like this unrealized profit and you feel that it\u2019s temporary luck that IBM went up. You decide to sell 3 of these 10 shares to the market at 170$/share. The profit on these share will now be \u201crealized\u201d: market value - book value = P/L 3 x 170$ - 3 x 160$ = 3 x (170 - 160) = 30$ (profit) This 30$ is a \u201crealized P/L.\u201d The remaining portion of your position is still showing an unrealized profit, that is, the price could fluctuate some more until you sell it: market value - book value = P/L 7 x 170$ - 7 x 160$ = 70$ This is how you would book this partial sale of your position in Beancount (again including a commission): 2014-02-17 * \"Selling some IBM\" Assets:US:ETrade:IBM -3 IBM {160.00 USD} Assets:US:ETrade:Cash 500.05 USD Expenses:Financial:Commissions 9.95 USD Do you notice something funny going on here? -3 x 160 = -480, -480 + 500.05 + 9.95 = 30\u2026 This transaction does not balance to zero! The problem is that we received 510$ in cash in exchange for the 3 shares we sold. This is because the actual price we sold them at was 170$: 3 x 170 = 510$. This is where we need to account for the profit, by adding another leg which will absorb this profit, and conveniently enough, automatically calculate and track our profits for us: 2014-02-17 * \"Selling some IBM\" Assets:US:ETrade:IBM -3 IBM {160.00 USD} Assets:US:ETrade:Cash 500.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL The last leg will be automatically filled in by Beancount to -30 USD , as we\u2019re allowed one posting without an amount (and remember that in the double-entry system without credits and debits, a profit is a negative number for \u201cIncome\u201d accounts). This is the number the government is interested in for your taxes. In summary, you now have: A position of 7 \u201cshares at book value of 160$\u201d = 1120$ (its book value) A realized P/L of 30$ An unrealized P/L of 70$ Now at this point, some of you will jump up and down and say: \u201cBut wait, waiiit! I sold at 170$/share, not 160$/share, why do you put 160$ here?\u201d The answer is that you did not have shares held at 170$ to sell. In order to explain this, I need to make a little detour to explain how we keep track of things in accounts... So how do we keep track of these shares? It\u2019s actually easy: when Beancount stores things in accounts, we use something called \u201can inventory.\u201d Imagine that an \u201cinventory\u201d is a bag with the name of that account on it. Each account has one such bag to hold the things in the account at a particular point in time, the \u201cbalance\u201d of this account at that time. Imagine that the things it contains have a little label attached to each of them, with their cost, that is, the price that was paid to acquire them. Whenever you put a thing in the bag, you attach a new label to the thing. For things to work right, all things need to be labeled 2 . In our example, the bag contained 10 items of \u201cshares of IBM bought at 160$/share\u201d. The syntax we used to put the IBM in the account can seem a little misleading; we wrote: Assets:US:ETrade:IBM 10 IBM {160.00 USD} but really, this is understood by Beancount closer to the following syntax: Assets:US:ETrade:IBM 10 {IBM 160.00 USD} But \u2026 it would be annoying to write this, so we use a syntax more intuitive to humans. So the thing is, you can\u2019t subtract units of {IBM at 170.00 USD} ... because there just aren\u2019t any in that bag. What you have in the bag are units of {IBM at 160.00 USD} . You can only take out these ones. Now that being said, do you see how it\u2019s the amount that was exchanged to us for the shares that really helps us track the P/L? Nowhere did we actually need to indicate the price at which we sold the shares. It\u2019s the fact that we received a certain amount of cash that is different than the cost of the position we\u2019re selling that triggers the imbalance, which we book to a capital gain. Hmmm\u2026 Beancount maintains a price database, wouldn\u2019t it be nice to at least record and attach that price to the transaction for documentation purposes? Indeed. Beancount allows you to also attach a price to that posting, but for the purpose of balancing the transaction, it ignores it completely. It is mainly there for documentation, and you can use it if you write scripts. And if you use the beancount.plugins.implicit_prices plugin, it will be used to automatically synthesize a price entry that will enrich our historical price database, which may be used in reporting the market value of the account contents (more details on this follow). So the complete and final transaction for selling those shares should be: 2014-02-17 * \"Selling some IBM\" Assets:US:ETrade:IBM -3 IBM {160.00 USD} @ 170.00 USD Assets:US:ETrade:Cash 500.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Trade Lots \uf0c1 In practice, the reality of trading gets a tiny bit more complicated than this. You might decide to buy some IBM multiple times, and each time, it is likely that you would buy them at a different price. Let\u2019s see how this works with another example trade. Given your previous position of 7 shares held at 160$ cost, the following day you see that the price went up some more, you change your mind on IBM and decide to \u201cgo long\u201d and buy 5 more shares. The price you get is 180$/share this time: 2014-02-18 * \"I put my chips on big blue!\" Assets:US:ETrade:IBM 5 IBM {180.00 USD} Assets:US:ETrade:Cash -909.95 USD Expenses:Financial:Commissions 9.95 USD Now, what do we have in the bag for Assets:US:ETrade:IBM ? We have two kinds of things: 7 shares of \u201cIBM held at 160 USD/share\u201d, from the first trade 5 shares of \u201cIBM held at 180 USD/share\u201d, from this last trade We will call these \u201clots,\u201d or \u201ctrade lots.\u201d In fact, if you were to sell this entire position, say, a month later, the way to legally sell it in Beancount (that is, without issuing an error), is by specifying both legs. Say the price is 172$/share at that moment: 2014-03-18 * \"Selling all my blue chips.\" Assets:US:ETrade:IBM -7 IBM {160.00 USD} @ 172.00 USD Assets:US:ETrade:IBM -5 IBM {180.00 USD} Assets:US:ETrade:Cash 2054.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Now your final position of IBM would be 0 shares. Alternatively, since you\u2019re selling the entire position, Beancount should be able to unambiguously match all the lots against an unspecified cost. This is equivalent: 2014-03-18 * \"Selling all my blue chips.\" Assets:US:ETrade:IBM -12 IBM {} @ 172.00 USD Assets:US:ETrade:Cash 2054.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Note that this won\u2019t work if the total amount of shares doesn\u2019t match all the lots (this would be ambiguous\u2026 which subset of the lots should be chosen isn\u2019t obvious). Booking Methods \uf0c1 But what if you decided to sell only some of those shares? Say you need some cash to buy a gift to your loved one and you want to sell 4 shares this time. Say the price is now 175$/share. Now you have a choice to make. You can choose to sell the older shares and realize a larger profit: 2014-03-18 * \"Selling my older blue chips.\" Assets:US:ETrade:IBM -4 IBM {160.00 USD} @ 175.00 USD Assets:US:ETrade:Cash 690.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL ;; -60.00 USD (profit) Or you may choose to sell the most recently acquired ones and realize a loss: 2014-03-18 * \"Selling my most recent blue chips.\" Assets:US:ETrade:IBM -4 IBM {180.00 USD} @ 175.00 USD Assets:US:ETrade:Cash 690.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL ;; 20.00 USD (loss) Or you can choose to sell a mix of both: just use two legs. Note that in practice this choice will depend on a number of factors: The tax law of the jurisdiction where you trade the shares may have a defined method for how to book the shares and you may not actually have a choice. For example, they may state that you must trade the oldest lot you bought, a method called \u201cfirst-in-first out.\u201d If you have a choice, the various lots you\u2019re holding may have different taxation characteristics because you\u2019ve held them for a different period of time. In the USA, for example, positions held for more than one year benefit from a lower taxation rate (the \u201clong-term\u201d capital gains rate). You may have other gains or losses that you want to offset in order to minimize your cash flow requirements on your tax liability. This is sometimes called \u201c tax loss harvesting .\u201d There are more\u2026 but I\u2019m not going to elaborate on them here. My goal is to show you how to book these things with the double-entry method. Dated lots \uf0c1 We\u2019ve almost completed the whole picture of how this works. There is one more rather technical detail to add and it begins with a question: What if I bought multiple lots of share at the same price? As we alluded to in the previous section, the duration for which you held a position may have an impact on your taxation, even if the P/L ends up being the same. How do we differentiate between these lots? Well\u2026 I had simplified things a tiny bit earlier, just to make it simpler to understand. When we put positions in an inventory, on the label that we attach to the things we put in it, we also mark down the date that lot was acquired if you supply it. This is how you would book entering the position this way: 2014-05-20 * \"First trade\" Assets:US:ETrade:IBM 5 IBM {180.00 USD, 2014-05-20} Assets:US:ETrade:Cash -909.95 USD Expenses:Financial:Commissions 9.95 USD 2014-05-21 * \"Second trade\" Assets:US:ETrade:IBM 3 IBM {180.00 USD, 2014-05-21} Assets:US:ETrade:Cash -549.95 USD Expenses:Financial:Commissions 9.95 USD Now when you sell, you can do the same thing to disambiguate which lot\u2019s position you want to reduce: 2014-08-04 * \"Selling off first trade\" Assets:US:ETrade:IBM -5 IBM {180.00 USD, 2014-05-20} Assets:US:ETrade:Cash 815.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Note that it\u2019s really unlikely that your broker will provide the information in the downloadable CSV or OFX files from their website\u2026 you probably won\u2019t be able to automate the lot detail of this transaction, you might have to pick up the PDF trade confirmations your broker provides to enter this manually, if it ever happens. But how often does it happen that you buy two lots at the same price? I trade relatively frequently - about every two weeks - and in 8 years worth of data I don\u2019t have a single occurrence of it. In practice, unless you do thousands of trades per day- and Beancount isn\u2019t really designed to handle that kind of activity, at least not in the most efficient way - it just won\u2019t happen very much. ( Technical Detail : that we\u2019re working on bettering the mechanism for lot selection so that you never have to insert the lot-date yourself, and so that you could disambiguate lot selection by supplying a name instead. See upcoming changes.) Reporting Unrealized P/L \uf0c1 Okay, so our account balances are holding the cost of each unit, and that provides us with the book value of these positions. Nice. But what about viewing the market value? The market value of the positions is simply the number of units of these instruments x the market price at the time we\u2019re interested in. This price fluctuates. So we need the price. Beancount supports a type of entry called a price entry that allows you to tell it what the price of an instrument was at a particular point in time, e.g. 2014-05-25 price IBM 182.27 USD In order to keep Beancount simple and with few dependencies, the software does not automatically fetch these prices (you can check out LedgerHub for this purpose, or write your own script that will insert the latest prices in your input file if so desired\u2026 there are many libraries to fetch prices from the internet online). It only knows about market prices from all these price entries. Using these, it builds an in-memory historical database of prices over time and can query it to obtain the most current values. Instead of supporting different reporting modes with options, you can trigger the insertion of unrealized gains by enabling a plugin: plugin \"beancount.plugins.unrealized\" \"Unrealized\" This will create a synthetic transaction at the date of the last of directives, that reflects the unrealized P/L. It books one side as Income and the other side as a change in Asset: 2014-05-25 U \"Unrealized gain for 7 units of IBM (price: 182.2700 USD as of 2014-05-25, average cost: 160.0000 USD)\" Assets:US:ETrade:IBM:Unrealized 155.89 USD Income:US:ETrade:IBM:Unrealized -155.89 USD Note that I used an option in this example to specify a sub-account to book the unrealized gains to. The unrealized P/L shows up on a separate line in the balance sheet and the parent account should show the market value on its balance (which includes that of its sub-accounts). Commissions \uf0c1 So far we have not discussed trading commissions. Depending on the tax law that applies to you, the costs associated with trading may be deductible from the raw capital gain as we\u2019ve calculated it in the previous examples. These are considered expenses by the government, and it is often the case that you can deduct those trading commissions (it\u2019s entirely reasonable from their part, you did not pocket that money after all). In the examples above, the capital gains and commission expenses get tracked into two separate accounts. For example, you could end up with reported balances that look like this: Income:US:ETrade:PnL -645.02 USD Expenses:Financial:Commissions 39.80 USD (Just to be clear, this is to be interpreted as a profit of $645.02 and an expense of $39.80.) You could subtract these numbers to obtain an approximation of the P/L without costs: 645.02 - 39.80 = $605.22. However, this is only an approximation of the correct P/L value. To understand why, we need to look at an example where a partial number of shares are sold across a reporting period. Imagine that we have an account with a commission rate of $10 per trade, 100 shares of ITOT were bought in 2013, 40 of those shares were later sold in that same year, and the remaining 60 were sold the year after, a scenario that looks like this: 2013-09-01 Buy 100 ITOT at $80, commission = 10$ 2013-11-01 Sell 40 ITOT at $82, commission = 10$ 2014-02-01 Sell 60 ITOT at $84, commission = 10$ If you computed the sum of commissions paid at the end of 2013, you would have $20, and using the approximate method outlined previously, for so 2013 and 2014 you would declare 2013: P/L of 40 x ($82 - $80) - ($10 + $10) = $60 2014: P/L of 60 x ($84 - $80) - $10 = $230 However, strictly speaking, this is incorrect. The $10 commission paid on acquiring the 100 shares has to be pro-rated with respect to the number of shares sold. This means that on that first sale of 40 shares only 4$ of the commission is deductible: $10 x (40 shares / 100 shares), and so we obtain: 2013: P/L of 40 x ($82 - $80) - $(4 + 10) = $66 2014: P/L of 60 x ($84 - $80) - $(6 + 10) = $224 As you can see, the P/L declared for each year differs, even if the sum of the P/L for both years is the same ($290). A convenient method to automatically allocate the acquisition costs to the pro-rata value of the number of shares sold is to add the acquisition trading cost to the total book value of the position. In this example, you would say that the position of 100 shares has a book value $8010 instead of $8000: 100 share x $80/share + $10, or equivalently, that the individual shares have a book value of $80.10 each. This would result in the following calculation: 2013: P/L of 40 x ($82 - $80.10) - $10 = $66 2014: P/L of 60 x ($84 - $80.10) - $10 = $224 You could even go one step further and fold the commission on sale into the price of each share sold as well: 2013: P/L of 40 x ($81.75 - $80.10) = $66 2014: P/L of 60 x ($83.8333 - $80.10) = $224 This may seem overkill, but imagine that those costs were much higher, as is the case on large commercial transactions; the details do begin to matter to the tax man. Accurate accounting is important, and we need to develop a method to do this more precisely. We don\u2019t currently have a good method of doing this with our input syntax. A suitable method is currently being developed and a proposal is on the table. Also see mailing-list for details. [June 2014] Stock Splits \uf0c1 Stock splits are currently dealt with by emptying an account\u2019s positions and recreating the positions at a different price: 2004-12-21 * \"Autodesk stock splits\" Assets:US:MSSB:ADSK -100 ADSK {66.30 USD} Assets:US:MSSB:ADSK 200 ADSK {33.15 USD} The postings balance each other, so the rule is respected. As you can see, this requires no special syntax feature. It also handles more general scenarios, such as the odd split of the Google company that occurred on the NASDAQ exchange in April 2014, into two different classes of stock (voting and non-voting shares, at 50.08% and 49.92%, respectively): 2014-04-07 * \"Stock splits into voting and non-voting shares\" Assets:US:MSSB:GOOG -25 GOOG {1212.51 USD} ; Old GOOG Assets:US:MSSB:GOOG 25 GOOG { 605.2850 USD} ; New GOOG Assets:US:MSSB:GOOGL 25 GOOG { 607.2250 USD} Ultimately, maybe a plug-in module should be provided to more easily create such stock split transactions, as there is some amount of redundancy involved. We need to figure out the most general way to do this. But the above will work for now. One problem with this approach is that the continuity of the trade lots is lost, that is, the purchase date of each lot has now been reset as a result of the transaction above, and it becomes impossible to automatically figure out the duration of the trade and its associated impact on taxation, i.e. long-term vs. short-term trade. Even without this the profit is still calculated correctly, but it is an annoying detail nonetheless. One way to handle this is by using the Dated Lots (see the appropriate section of this doc). That way, the original trade date can be preserved on the new lots. This provides accurate timing information in addition to the capital gain/loss based on the price. Another method for solving this and for easily propagating the lot trade date has been proposed and will be implemented in Beancount later on. A more important problem with the current implementation is that the meaning of a unit of ADSK before and after the stock split is different. The price graph for this commodity unit will show a radical discontinuity! This is a more general problem that has yet to be addressed in both Beancount and Ledger. The Commodity Definition Changes document has a discussion to address this topic. Cost Basis Adjustment and Return of Capital \uf0c1 Readjustment in cost basis may occur in managed funds, due to the fund\u2019s internal trading activities. This will typically occur in tax-sheltered accounts where the gain that occurs from such an adjustment has no impact on taxes, and where the cost basis is held at the average cost of all shares in each position. If we have the specific lot prices being adjusted, it is doable to book these in the same manner as we dealt with stock splits: 2014-04-07 * \"Cost basis adjustment for XSP\" Assets:CA:RRSP:XSP -100 ADSK {21.10 CAD} Assets:CA:RRSP:XSP 100 ADSK {23.40 CAD} Income:CA:RRSP:Gains -230.00 CAD However, this is really uncommon. The more common case of this is of an account using the average cost booking method, we don\u2019t currently have a way to deal with this. There is an active proposal in place to make this possible. The cost basis adjustment is commonly found in Return of Capital events. These happen, for example, when funds are returning capital to the shareholders. This can be caused by winding down the operation. From the taxation point of view, these are non-taxable events and affect the cost basis of the equity in the fund. The number of shares might stay the same, but their cost basis needs to be adjusted for potential Gain/Loss calculation at the point of sale in the future. Dividends \uf0c1 Dividends don\u2019t pose a particular problem. They are just income. They can be received as cash: 2014-02-01 * \"Cash dividends received from mutual fund RBF1005\" Assets:Investments:Cash 171.02 CAD Income:Investments:Dividends Or they can be received as stock itself: 2014-02-01 * \"Stock dividends received in shares\" Assets:Investments:RBF1005 7.234 RBF1005 {23.64 CAD} Income:Investments:Dividends In the case of dividends received as stock, as for stock purchases, you provide the cost basis at which the dividend was received (this should be available in your statements). If the account is held at average cost, this posting will simply merge with the other legs at the time an average cost booking is needed to be performed. Average Cost Booking \uf0c1 At the moment, the only way to perform booking at average cost is painful: you would have to use the method outlined in the Stock Split section in order to revalue your inventory. This is impractical, however. There is an active proposal with an associated syntax to fully solve this problem. Once the proposal is implemented, it will look like this: 2014-02-01 * \"Selling 5 shares at market price 550 USD\" Assets:Investments:Stock -5 GOOG {*} Assets:Investments:Cash 2740.05 USD Expenses:Commissions 9.95 USD Income:Investments:CapitalGains Any posting with a cost of \u201c*\u201d acting on an inventory will select all the shares of that currency (GOOG), merge them into a single one at the average cost, and then reduce that position at this new average cost. Future Topics \uf0c1 I\u2019ll be handling the following topics later on: Mark-to-Market : Handling end-of-year mark-to-market for Section 1256 instruments (i.e., futures and options), by re-evaluating the cost basis. This is similar to a cost basis readjustment applied at the end of each year for all of these types of instruments. Short Sales : these require little changes. We just have to allow negative numbers of units held at cost. At the moment we spit a warning when units held at cost go negative in order to detect data entry errors, but it would be easy to extend the Open directive syntax to allow this to occur on specific accounts which can hold short sales, which should just show as negative shares. All the arithmetic should otherwise just work naturally. Interest payments on margins would show up as distinct transactions. Also, when you short the stock, you don\u2019t receive dividends for those positions, but rather you have to pay them out. You would have expense account for this, e.g., Expenses:StockLoans:Dividends . Trading Options : I have no idea how to do this at the moment, but I imagine these could be held like shares of stock, with no distinctions. I don\u2019t foresee any difficulty. Currency Trading : At the moment, I\u2019m not accounting for the positions in my FOREX accounts, just their P/L and interest payments. This poses interesting problems: Positions held in a FOREX account aren\u2019t just long or short the way that stocks are: they are actually offsetting two commodities at the same time. For example, a long position in USD/CAD should increase the exposure of USD and decrease the exposure in CAD, it can be seen as holding a long asset of USD and a short asset in CAD, at the same time. While it is possible to hold these positions as if they were distinct instruments (e.g., units of \u201cUSDCAD\u201d with disregard for its components) but for large positions, especially if held over long periods of time for hedging purposes, it is important to deal with this and somehow allow the user to reflect the net currency exposures of multiple currency positions against the rest of their assets and liabilities. We also need to deal with the gains generated by the closing of these positions: those generate a gain in the currency of the account, after conversion to this currency. For example, if you hold a currency account denominated in USD, and you go long EUR/JPY, when you close the position you will obtain a gain in EUR, and after conversion of the P/L from the EUR into the equivalent number of USD (via EUR/USD) the USD gain will be deposited in your account. This means that two rates are being used to estimate the current market value of any position: the differential between the current rate and the rate at the time of buying, and the rate of the base currency (e.g., EUR) in the account currency (e.g., USD). Some of these involve new features in Beancount, but some not. Ideas welcome. This is a misleading notion, however. In reality, there is no price , there exist only markets where you get a \u201chint\u201d of how much someone else might be willing to exchange your shares for (for different amounts to buy or to sell them, and for some limited number of them, we call this \u201ca market\u201d), but until you\u2019ve actually completed selling your shares, you don\u2019t really know precisely how much you will be able to execute that trade at, only an estimate. If you\u2019re not intimately familiar with trading, this should give you pause, and hopefully a big \u201cah-ha! \u201cmoment about how the world works - there really does not exist a price for anything in the world - but in the context of this discussion, let\u2019s make abstraction of this and assume that you can buy or sell as many shares as you have instantly on the markets at the middle price, such as Google Finance or Yahoo Finance or your broker would report it. The process of deciding that we would be able to sell the shares at 170$ each is called \u201cmarking\u201d, that is, under reasonable assumptions, we believe that we would be able to actually sell those shares at that price (the term of art \u201cmarking to market\u201d refers to the fact that we use the market price as the best indicator to our knowledge of our capability to realize the trade). For an individual buying and selling small amounts of shares that don\u2019t move the market and with an honest broker, this is mostly true in practice. \u21a9 As an aside, putting regular currencies in an account is just the degenerate case of a thing with an empty label. This is an implementation detail that works great in practice. \u21a9","title":"Trading with Beancount"},{"location":"trading_with_beancount.html#trading-with-beancount","text":"Martin Blais , July 2014 http://furius.ca/beancount/doc/trading Introduction What is Profit and Loss? Realized and Unrealized P/L Trade Lots Booking Methods Dated lots Reporting Unrealized P/L Commissions Stock Splits Cost Basis Adjustments Dividends Average Cost Booking Future Topics","title":"Trading with Beancount"},{"location":"trading_with_beancount.html#introduction","text":"This is a companion document for the Command-Line Accounting Cookbook that deals exclusively with the subject of trading and investments in Beancount. You probably should have read an introduction to the double-entry method before reading this document. The subject of stock trading needs to be preceded by a discussion of \u201cprofit and loss,\u201d or P/L, for short (pronounce: \u201cP and L\u201d), also called capital gains or losses. The notion of P/L against multiple trades can be difficult for a novice to understand, and I\u2019ve even seen professional traders lack sophistication in their understanding of P/L over varying time periods. It is worth spending a bit of time to explain this, and necessary to understand how to book your trades in a double-entry system. This discussion will be weaved with detailed examples of how to book these trades in Beancount, wherever possible. There is a related, active proposal for improving the booking methods in Beancount that you might also be interested in. Discussions of basis for tax-deferred accounts will not be treated here, but in the more general cookbook.","title":"Introduction"},{"location":"trading_with_beancount.html#what-is-profit-and-loss","text":"Let\u2019s imagine you have an account at the E*Trade discount broker and you buy some shares of a company, say IBM. If you buy 10 shares of IBM when its price is 160$/share, it will cost you 1600$. That value is what we will call the \u201cbook value\u201d, or equivalently, \u201cthe cost.\u201d This is how much money you had to spend in order to acquire the shares, also called \u201cthe position.\u201d This is how you would enter this transaction in Beancount: 2014-02-16 * \"Buying some IBM\" Assets:US:ETrade:IBM 10 IBM {160.00 USD} Assets:US:ETrade:Cash -1600.00 USD In practice you will probably pay some commission to E*Trade for this service, so let\u2019s put that in for completeness: 2014-02-16 * \"Buying some IBM\" Assets:US:ETrade:IBM 10 IBM {160.00 USD} Assets:US:ETrade:Cash -1609.95 USD Expenses:Financial:Commissions 9.95 USD This is how you tell Beancount to deposit some units \u201cat cost\u201d, in this case, units of \u201cIBM at 160 USD/share\u201d cost. This transaction balances because the sum of its legs is zero: 10 x 160 + -1609.95 + 9.95 = 0. Also note that we\u2019re choosing to use a subaccount dedicated to the shares of IBM; this is not strictly necessary but it is convenient for reporting in, for example, a balance sheet, because it will naturally aggregate all of your shares of each of your positions on their own line. Having a \u201ccash\u201d subaccount also emphasizes that uninvested funds you have there are not providing any return. The next day, the market opens and IBM shares are going for 170$/share. In this context, we will call this \u201cthe price.\u201d 1 The \u201cmarket value\u201d of your position, your shares, is the number of them x the market price, that is, 10 shares x 170$/share = 1700$. The difference between these two amounts is what we will call the P/L: market value - book value = P/L 10 x 170$ - 10 x 160$ = 1700$ - 1600$ = 100$ (profit) We will call a positive amount \u201ca profit\u201d and if the amount is negative, \u201ca loss.\u201d","title":"What is Profit and Loss?"},{"location":"trading_with_beancount.html#realized-and-unrealized-pl","text":"The profit from the previous section is called an \u201cunrealized profit.\u201d That is because the shares have not actually been sold yet - this is a hypothetical profit: if I can sell those shares at the market value, this is how much I would pocket. The 100$ I mentioned in the previous section is actually an \u201cunrealized P/L.\u201d So let\u2019s say you like this unrealized profit and you feel that it\u2019s temporary luck that IBM went up. You decide to sell 3 of these 10 shares to the market at 170$/share. The profit on these share will now be \u201crealized\u201d: market value - book value = P/L 3 x 170$ - 3 x 160$ = 3 x (170 - 160) = 30$ (profit) This 30$ is a \u201crealized P/L.\u201d The remaining portion of your position is still showing an unrealized profit, that is, the price could fluctuate some more until you sell it: market value - book value = P/L 7 x 170$ - 7 x 160$ = 70$ This is how you would book this partial sale of your position in Beancount (again including a commission): 2014-02-17 * \"Selling some IBM\" Assets:US:ETrade:IBM -3 IBM {160.00 USD} Assets:US:ETrade:Cash 500.05 USD Expenses:Financial:Commissions 9.95 USD Do you notice something funny going on here? -3 x 160 = -480, -480 + 500.05 + 9.95 = 30\u2026 This transaction does not balance to zero! The problem is that we received 510$ in cash in exchange for the 3 shares we sold. This is because the actual price we sold them at was 170$: 3 x 170 = 510$. This is where we need to account for the profit, by adding another leg which will absorb this profit, and conveniently enough, automatically calculate and track our profits for us: 2014-02-17 * \"Selling some IBM\" Assets:US:ETrade:IBM -3 IBM {160.00 USD} Assets:US:ETrade:Cash 500.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL The last leg will be automatically filled in by Beancount to -30 USD , as we\u2019re allowed one posting without an amount (and remember that in the double-entry system without credits and debits, a profit is a negative number for \u201cIncome\u201d accounts). This is the number the government is interested in for your taxes. In summary, you now have: A position of 7 \u201cshares at book value of 160$\u201d = 1120$ (its book value) A realized P/L of 30$ An unrealized P/L of 70$ Now at this point, some of you will jump up and down and say: \u201cBut wait, waiiit! I sold at 170$/share, not 160$/share, why do you put 160$ here?\u201d The answer is that you did not have shares held at 170$ to sell. In order to explain this, I need to make a little detour to explain how we keep track of things in accounts... So how do we keep track of these shares? It\u2019s actually easy: when Beancount stores things in accounts, we use something called \u201can inventory.\u201d Imagine that an \u201cinventory\u201d is a bag with the name of that account on it. Each account has one such bag to hold the things in the account at a particular point in time, the \u201cbalance\u201d of this account at that time. Imagine that the things it contains have a little label attached to each of them, with their cost, that is, the price that was paid to acquire them. Whenever you put a thing in the bag, you attach a new label to the thing. For things to work right, all things need to be labeled 2 . In our example, the bag contained 10 items of \u201cshares of IBM bought at 160$/share\u201d. The syntax we used to put the IBM in the account can seem a little misleading; we wrote: Assets:US:ETrade:IBM 10 IBM {160.00 USD} but really, this is understood by Beancount closer to the following syntax: Assets:US:ETrade:IBM 10 {IBM 160.00 USD} But \u2026 it would be annoying to write this, so we use a syntax more intuitive to humans. So the thing is, you can\u2019t subtract units of {IBM at 170.00 USD} ... because there just aren\u2019t any in that bag. What you have in the bag are units of {IBM at 160.00 USD} . You can only take out these ones. Now that being said, do you see how it\u2019s the amount that was exchanged to us for the shares that really helps us track the P/L? Nowhere did we actually need to indicate the price at which we sold the shares. It\u2019s the fact that we received a certain amount of cash that is different than the cost of the position we\u2019re selling that triggers the imbalance, which we book to a capital gain. Hmmm\u2026 Beancount maintains a price database, wouldn\u2019t it be nice to at least record and attach that price to the transaction for documentation purposes? Indeed. Beancount allows you to also attach a price to that posting, but for the purpose of balancing the transaction, it ignores it completely. It is mainly there for documentation, and you can use it if you write scripts. And if you use the beancount.plugins.implicit_prices plugin, it will be used to automatically synthesize a price entry that will enrich our historical price database, which may be used in reporting the market value of the account contents (more details on this follow). So the complete and final transaction for selling those shares should be: 2014-02-17 * \"Selling some IBM\" Assets:US:ETrade:IBM -3 IBM {160.00 USD} @ 170.00 USD Assets:US:ETrade:Cash 500.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL","title":"Realized and Unrealized P/L"},{"location":"trading_with_beancount.html#trade-lots","text":"In practice, the reality of trading gets a tiny bit more complicated than this. You might decide to buy some IBM multiple times, and each time, it is likely that you would buy them at a different price. Let\u2019s see how this works with another example trade. Given your previous position of 7 shares held at 160$ cost, the following day you see that the price went up some more, you change your mind on IBM and decide to \u201cgo long\u201d and buy 5 more shares. The price you get is 180$/share this time: 2014-02-18 * \"I put my chips on big blue!\" Assets:US:ETrade:IBM 5 IBM {180.00 USD} Assets:US:ETrade:Cash -909.95 USD Expenses:Financial:Commissions 9.95 USD Now, what do we have in the bag for Assets:US:ETrade:IBM ? We have two kinds of things: 7 shares of \u201cIBM held at 160 USD/share\u201d, from the first trade 5 shares of \u201cIBM held at 180 USD/share\u201d, from this last trade We will call these \u201clots,\u201d or \u201ctrade lots.\u201d In fact, if you were to sell this entire position, say, a month later, the way to legally sell it in Beancount (that is, without issuing an error), is by specifying both legs. Say the price is 172$/share at that moment: 2014-03-18 * \"Selling all my blue chips.\" Assets:US:ETrade:IBM -7 IBM {160.00 USD} @ 172.00 USD Assets:US:ETrade:IBM -5 IBM {180.00 USD} Assets:US:ETrade:Cash 2054.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Now your final position of IBM would be 0 shares. Alternatively, since you\u2019re selling the entire position, Beancount should be able to unambiguously match all the lots against an unspecified cost. This is equivalent: 2014-03-18 * \"Selling all my blue chips.\" Assets:US:ETrade:IBM -12 IBM {} @ 172.00 USD Assets:US:ETrade:Cash 2054.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Note that this won\u2019t work if the total amount of shares doesn\u2019t match all the lots (this would be ambiguous\u2026 which subset of the lots should be chosen isn\u2019t obvious).","title":"Trade Lots"},{"location":"trading_with_beancount.html#booking-methods","text":"But what if you decided to sell only some of those shares? Say you need some cash to buy a gift to your loved one and you want to sell 4 shares this time. Say the price is now 175$/share. Now you have a choice to make. You can choose to sell the older shares and realize a larger profit: 2014-03-18 * \"Selling my older blue chips.\" Assets:US:ETrade:IBM -4 IBM {160.00 USD} @ 175.00 USD Assets:US:ETrade:Cash 690.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL ;; -60.00 USD (profit) Or you may choose to sell the most recently acquired ones and realize a loss: 2014-03-18 * \"Selling my most recent blue chips.\" Assets:US:ETrade:IBM -4 IBM {180.00 USD} @ 175.00 USD Assets:US:ETrade:Cash 690.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL ;; 20.00 USD (loss) Or you can choose to sell a mix of both: just use two legs. Note that in practice this choice will depend on a number of factors: The tax law of the jurisdiction where you trade the shares may have a defined method for how to book the shares and you may not actually have a choice. For example, they may state that you must trade the oldest lot you bought, a method called \u201cfirst-in-first out.\u201d If you have a choice, the various lots you\u2019re holding may have different taxation characteristics because you\u2019ve held them for a different period of time. In the USA, for example, positions held for more than one year benefit from a lower taxation rate (the \u201clong-term\u201d capital gains rate). You may have other gains or losses that you want to offset in order to minimize your cash flow requirements on your tax liability. This is sometimes called \u201c tax loss harvesting .\u201d There are more\u2026 but I\u2019m not going to elaborate on them here. My goal is to show you how to book these things with the double-entry method.","title":"Booking Methods"},{"location":"trading_with_beancount.html#dated-lots","text":"We\u2019ve almost completed the whole picture of how this works. There is one more rather technical detail to add and it begins with a question: What if I bought multiple lots of share at the same price? As we alluded to in the previous section, the duration for which you held a position may have an impact on your taxation, even if the P/L ends up being the same. How do we differentiate between these lots? Well\u2026 I had simplified things a tiny bit earlier, just to make it simpler to understand. When we put positions in an inventory, on the label that we attach to the things we put in it, we also mark down the date that lot was acquired if you supply it. This is how you would book entering the position this way: 2014-05-20 * \"First trade\" Assets:US:ETrade:IBM 5 IBM {180.00 USD, 2014-05-20} Assets:US:ETrade:Cash -909.95 USD Expenses:Financial:Commissions 9.95 USD 2014-05-21 * \"Second trade\" Assets:US:ETrade:IBM 3 IBM {180.00 USD, 2014-05-21} Assets:US:ETrade:Cash -549.95 USD Expenses:Financial:Commissions 9.95 USD Now when you sell, you can do the same thing to disambiguate which lot\u2019s position you want to reduce: 2014-08-04 * \"Selling off first trade\" Assets:US:ETrade:IBM -5 IBM {180.00 USD, 2014-05-20} Assets:US:ETrade:Cash 815.05 USD Expenses:Financial:Commissions 9.95 USD Income:US:ETrade:PnL Note that it\u2019s really unlikely that your broker will provide the information in the downloadable CSV or OFX files from their website\u2026 you probably won\u2019t be able to automate the lot detail of this transaction, you might have to pick up the PDF trade confirmations your broker provides to enter this manually, if it ever happens. But how often does it happen that you buy two lots at the same price? I trade relatively frequently - about every two weeks - and in 8 years worth of data I don\u2019t have a single occurrence of it. In practice, unless you do thousands of trades per day- and Beancount isn\u2019t really designed to handle that kind of activity, at least not in the most efficient way - it just won\u2019t happen very much. ( Technical Detail : that we\u2019re working on bettering the mechanism for lot selection so that you never have to insert the lot-date yourself, and so that you could disambiguate lot selection by supplying a name instead. See upcoming changes.)","title":"Dated lots"},{"location":"trading_with_beancount.html#reporting-unrealized-pl","text":"Okay, so our account balances are holding the cost of each unit, and that provides us with the book value of these positions. Nice. But what about viewing the market value? The market value of the positions is simply the number of units of these instruments x the market price at the time we\u2019re interested in. This price fluctuates. So we need the price. Beancount supports a type of entry called a price entry that allows you to tell it what the price of an instrument was at a particular point in time, e.g. 2014-05-25 price IBM 182.27 USD In order to keep Beancount simple and with few dependencies, the software does not automatically fetch these prices (you can check out LedgerHub for this purpose, or write your own script that will insert the latest prices in your input file if so desired\u2026 there are many libraries to fetch prices from the internet online). It only knows about market prices from all these price entries. Using these, it builds an in-memory historical database of prices over time and can query it to obtain the most current values. Instead of supporting different reporting modes with options, you can trigger the insertion of unrealized gains by enabling a plugin: plugin \"beancount.plugins.unrealized\" \"Unrealized\" This will create a synthetic transaction at the date of the last of directives, that reflects the unrealized P/L. It books one side as Income and the other side as a change in Asset: 2014-05-25 U \"Unrealized gain for 7 units of IBM (price: 182.2700 USD as of 2014-05-25, average cost: 160.0000 USD)\" Assets:US:ETrade:IBM:Unrealized 155.89 USD Income:US:ETrade:IBM:Unrealized -155.89 USD Note that I used an option in this example to specify a sub-account to book the unrealized gains to. The unrealized P/L shows up on a separate line in the balance sheet and the parent account should show the market value on its balance (which includes that of its sub-accounts).","title":"Reporting Unrealized P/L"},{"location":"trading_with_beancount.html#commissions","text":"So far we have not discussed trading commissions. Depending on the tax law that applies to you, the costs associated with trading may be deductible from the raw capital gain as we\u2019ve calculated it in the previous examples. These are considered expenses by the government, and it is often the case that you can deduct those trading commissions (it\u2019s entirely reasonable from their part, you did not pocket that money after all). In the examples above, the capital gains and commission expenses get tracked into two separate accounts. For example, you could end up with reported balances that look like this: Income:US:ETrade:PnL -645.02 USD Expenses:Financial:Commissions 39.80 USD (Just to be clear, this is to be interpreted as a profit of $645.02 and an expense of $39.80.) You could subtract these numbers to obtain an approximation of the P/L without costs: 645.02 - 39.80 = $605.22. However, this is only an approximation of the correct P/L value. To understand why, we need to look at an example where a partial number of shares are sold across a reporting period. Imagine that we have an account with a commission rate of $10 per trade, 100 shares of ITOT were bought in 2013, 40 of those shares were later sold in that same year, and the remaining 60 were sold the year after, a scenario that looks like this: 2013-09-01 Buy 100 ITOT at $80, commission = 10$ 2013-11-01 Sell 40 ITOT at $82, commission = 10$ 2014-02-01 Sell 60 ITOT at $84, commission = 10$ If you computed the sum of commissions paid at the end of 2013, you would have $20, and using the approximate method outlined previously, for so 2013 and 2014 you would declare 2013: P/L of 40 x ($82 - $80) - ($10 + $10) = $60 2014: P/L of 60 x ($84 - $80) - $10 = $230 However, strictly speaking, this is incorrect. The $10 commission paid on acquiring the 100 shares has to be pro-rated with respect to the number of shares sold. This means that on that first sale of 40 shares only 4$ of the commission is deductible: $10 x (40 shares / 100 shares), and so we obtain: 2013: P/L of 40 x ($82 - $80) - $(4 + 10) = $66 2014: P/L of 60 x ($84 - $80) - $(6 + 10) = $224 As you can see, the P/L declared for each year differs, even if the sum of the P/L for both years is the same ($290). A convenient method to automatically allocate the acquisition costs to the pro-rata value of the number of shares sold is to add the acquisition trading cost to the total book value of the position. In this example, you would say that the position of 100 shares has a book value $8010 instead of $8000: 100 share x $80/share + $10, or equivalently, that the individual shares have a book value of $80.10 each. This would result in the following calculation: 2013: P/L of 40 x ($82 - $80.10) - $10 = $66 2014: P/L of 60 x ($84 - $80.10) - $10 = $224 You could even go one step further and fold the commission on sale into the price of each share sold as well: 2013: P/L of 40 x ($81.75 - $80.10) = $66 2014: P/L of 60 x ($83.8333 - $80.10) = $224 This may seem overkill, but imagine that those costs were much higher, as is the case on large commercial transactions; the details do begin to matter to the tax man. Accurate accounting is important, and we need to develop a method to do this more precisely. We don\u2019t currently have a good method of doing this with our input syntax. A suitable method is currently being developed and a proposal is on the table. Also see mailing-list for details. [June 2014]","title":"Commissions"},{"location":"trading_with_beancount.html#stock-splits","text":"Stock splits are currently dealt with by emptying an account\u2019s positions and recreating the positions at a different price: 2004-12-21 * \"Autodesk stock splits\" Assets:US:MSSB:ADSK -100 ADSK {66.30 USD} Assets:US:MSSB:ADSK 200 ADSK {33.15 USD} The postings balance each other, so the rule is respected. As you can see, this requires no special syntax feature. It also handles more general scenarios, such as the odd split of the Google company that occurred on the NASDAQ exchange in April 2014, into two different classes of stock (voting and non-voting shares, at 50.08% and 49.92%, respectively): 2014-04-07 * \"Stock splits into voting and non-voting shares\" Assets:US:MSSB:GOOG -25 GOOG {1212.51 USD} ; Old GOOG Assets:US:MSSB:GOOG 25 GOOG { 605.2850 USD} ; New GOOG Assets:US:MSSB:GOOGL 25 GOOG { 607.2250 USD} Ultimately, maybe a plug-in module should be provided to more easily create such stock split transactions, as there is some amount of redundancy involved. We need to figure out the most general way to do this. But the above will work for now. One problem with this approach is that the continuity of the trade lots is lost, that is, the purchase date of each lot has now been reset as a result of the transaction above, and it becomes impossible to automatically figure out the duration of the trade and its associated impact on taxation, i.e. long-term vs. short-term trade. Even without this the profit is still calculated correctly, but it is an annoying detail nonetheless. One way to handle this is by using the Dated Lots (see the appropriate section of this doc). That way, the original trade date can be preserved on the new lots. This provides accurate timing information in addition to the capital gain/loss based on the price. Another method for solving this and for easily propagating the lot trade date has been proposed and will be implemented in Beancount later on. A more important problem with the current implementation is that the meaning of a unit of ADSK before and after the stock split is different. The price graph for this commodity unit will show a radical discontinuity! This is a more general problem that has yet to be addressed in both Beancount and Ledger. The Commodity Definition Changes document has a discussion to address this topic.","title":"Stock Splits"},{"location":"trading_with_beancount.html#cost-basis-adjustment-and-return-of-capital","text":"Readjustment in cost basis may occur in managed funds, due to the fund\u2019s internal trading activities. This will typically occur in tax-sheltered accounts where the gain that occurs from such an adjustment has no impact on taxes, and where the cost basis is held at the average cost of all shares in each position. If we have the specific lot prices being adjusted, it is doable to book these in the same manner as we dealt with stock splits: 2014-04-07 * \"Cost basis adjustment for XSP\" Assets:CA:RRSP:XSP -100 ADSK {21.10 CAD} Assets:CA:RRSP:XSP 100 ADSK {23.40 CAD} Income:CA:RRSP:Gains -230.00 CAD However, this is really uncommon. The more common case of this is of an account using the average cost booking method, we don\u2019t currently have a way to deal with this. There is an active proposal in place to make this possible. The cost basis adjustment is commonly found in Return of Capital events. These happen, for example, when funds are returning capital to the shareholders. This can be caused by winding down the operation. From the taxation point of view, these are non-taxable events and affect the cost basis of the equity in the fund. The number of shares might stay the same, but their cost basis needs to be adjusted for potential Gain/Loss calculation at the point of sale in the future.","title":"Cost Basis Adjustment and Return of Capital"},{"location":"trading_with_beancount.html#dividends","text":"Dividends don\u2019t pose a particular problem. They are just income. They can be received as cash: 2014-02-01 * \"Cash dividends received from mutual fund RBF1005\" Assets:Investments:Cash 171.02 CAD Income:Investments:Dividends Or they can be received as stock itself: 2014-02-01 * \"Stock dividends received in shares\" Assets:Investments:RBF1005 7.234 RBF1005 {23.64 CAD} Income:Investments:Dividends In the case of dividends received as stock, as for stock purchases, you provide the cost basis at which the dividend was received (this should be available in your statements). If the account is held at average cost, this posting will simply merge with the other legs at the time an average cost booking is needed to be performed.","title":"Dividends"},{"location":"trading_with_beancount.html#average-cost-booking","text":"At the moment, the only way to perform booking at average cost is painful: you would have to use the method outlined in the Stock Split section in order to revalue your inventory. This is impractical, however. There is an active proposal with an associated syntax to fully solve this problem. Once the proposal is implemented, it will look like this: 2014-02-01 * \"Selling 5 shares at market price 550 USD\" Assets:Investments:Stock -5 GOOG {*} Assets:Investments:Cash 2740.05 USD Expenses:Commissions 9.95 USD Income:Investments:CapitalGains Any posting with a cost of \u201c*\u201d acting on an inventory will select all the shares of that currency (GOOG), merge them into a single one at the average cost, and then reduce that position at this new average cost.","title":"Average Cost Booking"},{"location":"trading_with_beancount.html#future-topics","text":"I\u2019ll be handling the following topics later on: Mark-to-Market : Handling end-of-year mark-to-market for Section 1256 instruments (i.e., futures and options), by re-evaluating the cost basis. This is similar to a cost basis readjustment applied at the end of each year for all of these types of instruments. Short Sales : these require little changes. We just have to allow negative numbers of units held at cost. At the moment we spit a warning when units held at cost go negative in order to detect data entry errors, but it would be easy to extend the Open directive syntax to allow this to occur on specific accounts which can hold short sales, which should just show as negative shares. All the arithmetic should otherwise just work naturally. Interest payments on margins would show up as distinct transactions. Also, when you short the stock, you don\u2019t receive dividends for those positions, but rather you have to pay them out. You would have expense account for this, e.g., Expenses:StockLoans:Dividends . Trading Options : I have no idea how to do this at the moment, but I imagine these could be held like shares of stock, with no distinctions. I don\u2019t foresee any difficulty. Currency Trading : At the moment, I\u2019m not accounting for the positions in my FOREX accounts, just their P/L and interest payments. This poses interesting problems: Positions held in a FOREX account aren\u2019t just long or short the way that stocks are: they are actually offsetting two commodities at the same time. For example, a long position in USD/CAD should increase the exposure of USD and decrease the exposure in CAD, it can be seen as holding a long asset of USD and a short asset in CAD, at the same time. While it is possible to hold these positions as if they were distinct instruments (e.g., units of \u201cUSDCAD\u201d with disregard for its components) but for large positions, especially if held over long periods of time for hedging purposes, it is important to deal with this and somehow allow the user to reflect the net currency exposures of multiple currency positions against the rest of their assets and liabilities. We also need to deal with the gains generated by the closing of these positions: those generate a gain in the currency of the account, after conversion to this currency. For example, if you hold a currency account denominated in USD, and you go long EUR/JPY, when you close the position you will obtain a gain in EUR, and after conversion of the P/L from the EUR into the equivalent number of USD (via EUR/USD) the USD gain will be deposited in your account. This means that two rates are being used to estimate the current market value of any position: the differential between the current rate and the rate at the time of buying, and the rate of the base currency (e.g., EUR) in the account currency (e.g., USD). Some of these involve new features in Beancount, but some not. Ideas welcome. This is a misleading notion, however. In reality, there is no price , there exist only markets where you get a \u201chint\u201d of how much someone else might be willing to exchange your shares for (for different amounts to buy or to sell them, and for some limited number of them, we call this \u201ca market\u201d), but until you\u2019ve actually completed selling your shares, you don\u2019t really know precisely how much you will be able to execute that trade at, only an estimate. If you\u2019re not intimately familiar with trading, this should give you pause, and hopefully a big \u201cah-ha! \u201cmoment about how the world works - there really does not exist a price for anything in the world - but in the context of this discussion, let\u2019s make abstraction of this and assume that you can buy or sell as many shares as you have instantly on the markets at the middle price, such as Google Finance or Yahoo Finance or your broker would report it. The process of deciding that we would be able to sell the shares at 170$ each is called \u201cmarking\u201d, that is, under reasonable assumptions, we believe that we would be able to actually sell those shares at that price (the term of art \u201cmarking to market\u201d refers to the fact that we use the market price as the best indicator to our knowledge of our capability to realize the trade). For an individual buying and selling small amounts of shares that don\u2019t move the market and with an honest broker, this is mostly true in practice. \u21a9 As an aside, putting regular currencies in an account is just the degenerate case of a thing with an empty label. This is an implementation detail that works great in practice. \u21a9","title":"Future Topics"},{"location":"tutorial_example.html","text":"Beancount Example & Tutorial \uf0c1 Martin Blais , October 2014 http://furius.ca/beancount/doc/example Example File Generator Converting to Ledger Input Profile of Example User Future Additions Tutorial Generate an Example File Generating Reports Generating Balances Generating a Balance Sheet and Income Statement Journals Holdings Other Reports Other Formats Viewing Reports through the Web Interface The Future of Beancount Reports Example File Generator \uf0c1 Beancount has a command to generate a few years of a realistic user\u2019s historical entries: bean-example > example.beancount The script checks if the output fails to process cleanly in Beancount (if this occurs, try rerunning or contact the author at blais@furius.ca ). Note that there is an option to fix the random generator\u2019s seed to be used to generate the entries. You can generate multiple years of ledger data for this user; see available options with bean-example --help The purpose of the script is: To provide a realistic corpus of anonymous data for users to experiment with, to kick the tires of Beancount, generate reports, etc. To compare reports with those generated by Ledger. As an example file to be used in this tutorial. As input for stress testing Beancount. Converting to Ledger Input \uf0c1 It should be possible to convert example files generated from the script to Ledger syntax, like this: bean-report example.beancount ledger > example.lgr If you encounter any problems with the conversion, please let me know. While I have an automated test to check that the conversion succeeds, I\u2019m focusing my efforts on Beancount itself and I\u2019m not using Ledger much other than for the occasional comparison or for illustrating something on the mailing-list. Profile of Example User \uf0c1 Here\u2019s a high-level profile of this user. In the text that follows, I\u2019ll use the masculine \u201che\u201d for simplicity\u2019s sake. He lives somewhere in the USA and uses a single operating currency: \u201cUSD\u201d. He rents an apartment. Income : He is a software engineer (sorry I could not resist), receives a modest salary income from a software company. He is paid bi-weekly. He maximizes contributions to his company\u2019s 401k plan. His company offers a match to his contributions. His salary is paid as a direct deposit to his checking account. He benefits from and pays premiums for a health insurance plan provided by his employer. He tracks his vacation hours explicitly. Expenses : Checking account : He pays his apartment\u2019s rent by check. He also pays his electricity, internet ISP and mobile phone bills directly from his checking account. His bank also takes a monthly bank fee. Credit cards : He also has other regular expenses, such as groceries and restaurant outings with friends, which he pays from his credit card. Assets : Banking : He has an account at a major US bank. Retirement account : His 401k account is held at Fidelity or Vanguard and is allocated in its entirety to two funds, a mix of stocks and bonds. There are never any sales in this account. This account is held at average cost. Other investments : Extra money that he is able to save is transferred from his checking account towards a taxable investment account in which he purchases a mix of stocks and ETFs, and occasionally makes sales and realizes profits. Liabilities : Credit cards : He has one or two credit cards issued by another bank. Events/Tags : He takes a short vacation or two per year, where he travels somewhere fun. Future Additions \uf0c1 Note that as I beef up the example generator script, this profile will increase in complexity. If there is a particular situation from the cookbook or otherwise that you\u2019d like to see in the example file, please let me know (a patch is the best way to let me know). Generate fake PDF files for documents (from an option) along with suitable document directives for them, most of them implicit but some explicit. Generate more expenses: Some amount of realistic cash expenses. Add a vehicle: car repairs, gas, etc. Remove tram example, it is too urban-specific. Add donations for charity. Add a second credit card and vary sources of expenses. Add some real uses for links, for the 401k match, for instance, the employer contributions should be linked with the employee contributions. Or other ones. Generate a loan and payments for a student loan. Having a large liability is a common case, I\u2019d like to represent that, even if it\u2019s not a mortgage. Convert retirement accounts to book using the average cost method, and add corresponding fees which would otherwise be impossible to track with explicit lots. Add tracking of health care expenses as per the cookbook. Add foreign accounts in another currency and transactions and transfers in that currency. Tutorial \uf0c1 This section provides basic examples of generating reports with Beancount, on the typical example file that would be output by it. The list of reports here is not meant to be exhaustive. You may follow the tutorial and generate your own example file, or look at the files from beancount/examples/tutorial if you prefer. Generate an Example File \uf0c1 Begin by generating the example file: bean-example > example.beancount Open example.beancount and examine it. Next, before we begin generating reports, verify that the file loads without any errors: bean-check example.beancount It should return quietly, without outputting anything (bean-check only writes errors when there are some, otherwise on success it writes nothing). Generating Reports \uf0c1 Let\u2019s generate a report of the final balances of all accounts [output] : bean-report example.beancount balances As you can see, the bean-report script has subcommands for the various reports it generates. To list the available reports, use --help-reports [output] : bean-report --help-reports To list the options available for a particular report, use --help on it [output] : bean-report example.beancount balances --help The report-specific options vary for each report type. You can also list the global options for the script [output] : bean-report --help The global options allow you to specify the desired output format for the reports. To see which formats are available for which report, use --help-formats [output] : bean-report --help-formats Generating Balances \uf0c1 Good, so we know how to generate a report of balances for all accounts. This is a pretty detailed list of accounts though. Let\u2019s just restrict the output to the accounts that we\u2019re interested in [output] : bean-report example.beancount balances -e ETrade Here you can view the number of units held in each of the investment subaccounts. To render the cost, use the --cost option [output] : bean-report example.beancount balances -e ETrade --cost Formatting Tools \uf0c1 Sometimes it\u2019s nice to render a hierarchical list of accounts as a tree. You can use the \u201ctreeify\u201d tool provided by Beancount to do this: bean-report example.beancount balances | treeify This tool will work on any column of data that looks like a column of account names (you can also configure it work with filenames as well, or other patterns). Generating a Balance Sheet and Income Statement \uf0c1 Let us generate a balance sheet: bean-report example.beancount balsheet Unfortunately, the only output format supported for it at this point is HTML. Also, filtering balance sheet entries from the command-line is not supported. Generate this to a file and open a browser to it [output] : bean-report example.beancount balsheet > /tmp/balsheet.html You can to the same for income statements: bean-report example.beancount income > /tmp/income.html Journals \uf0c1 You can also generate journals (in Ledger parlance, these are \u201cregisters\u201d). Let\u2019s look at a checking account postings, for instance [output] : bean-report example.beancount journal -a Assets:US:BofA:Checking To render a column of running balances, add the --balance option [output] : bean-report example.beancount journal -a Assets:US:BofA:Checking --balance The inventories are rendered in the number of units they\u2019re holding [output] : bean-report example.beancount journal -a Assets:US:ETrade:GLD If you want to render the values at cost, use the --cost option [output] : bean-report example.beancount journal -a Assets:US:ETrade:GLD --cost To render journals, you will typically restrict the set of postings that you want to view. If you did not, the postings would render no change, because all the legs of the transactions would be applied to the running balance and the total would be zero. But try it, nonetheless [output] : bean-report example.beancount journal --balance Holdings \uf0c1 There are a variety of ways to obtain aggregations for the total list of holdings. List the detailed holdings [output] : bean-report example.beancount holdings To aggregate the holdings by account, do this [output] : bean-report example.beancount holdings --by=account Because it\u2019s common practice to create and use a dedicated Beancount subaccount name for each commodity held in a real-world account, we can refine this further to aggregate to the parent (\u201croot\u201d) accounts of those subaccounts [output] : bean-report example.beancount holdings --by=root-account We can aggregate by commodity [output] : bean-report example.beancount holdings --by=commodity Or by the currency in which the commodities are quoted, which gives us a glimpse into our currency exposure [output] : bean-report example.beancount holdings --by=currency Finally, we can aggregate all the holdings to obtain net worth [output] : bean-report example.beancount networth There are a few more options for converting amounts to a common currency. See help for details. Other Reports \uf0c1 There are many other miscellaneous reports available. Try a few of those. Listing all accounts [output] : bean-report example.beancount accounts Listing events [output] : bean-report example.beancount events Number of directives by type [output] : bean-report example.beancount stats-directives Number of postings by type [output] : bean-report example.beancount stats-postings Other Formats \uf0c1 Reports may support various output formats. You can change the output format with the --format (-f) global option [output] : bean-report -f csv example.beancount holdings Note that \u201cbeancount\u201d and \u201cledger\u201d are valid output formats: they refer to the Beancount and Ledger input syntaxes. Viewing Reports through the Web Interface \uf0c1 The original way to access reports in Beancount is via its web interface that serves to a local web server on your machine. Serve the example file like this: bean-web example.beancount Then navigate with a web browser to http://localhost:8080 . From there, you can click on any number of filtered views and access some of the reports previously demonstrated. For example, click on a year view; that will provide balance sheets and income statements and various other reports for this subset of transactions. The bean-web tool has many options for restricting what is being served. (Note that by default the server listens only for connections from your computer; if you want to host this on a web server and accept connections from anywhere, use --public ). Note: There exists a separate project which provides a better web interface than the one which comes with Beancount: Fava . You might want to check it out. The Future of Beancount Reports \uf0c1 I find my command-line reports above to be rather unsatisfying, from the point-of-view of customizability. They bother me a lot. This is largely because I\u2019ve been happy and satisfied with the capabilities of the web interface to Beancount. As I\u2019m beginning to use the command-line interface more and more, I\u2019m finding myself desiring for more explicit and well-defined way to apply all the filters I have available from the web interface, and more. For this reason, I\u2019m currently implementing a query language that looks similar to SQL . This syntax will allow me to remove all the custom reports above and to replace them by a single consistent query syntax. This work is underway. All the holdings reports and even their internal objects will be replaced by the SQL-like query syntax as well. Holdings need not be treated separately: they are simply the list of positions available at a particular point in time, and along with suitable accessors from the query syntax and support for aggregations over their many columns, we should be able to generate all of those reports equivalently and simplify the code. \u2014 Martin Blais, October 2014","title":"Tutorial & Example"},{"location":"tutorial_example.html#beancount-example-tutorial","text":"Martin Blais , October 2014 http://furius.ca/beancount/doc/example Example File Generator Converting to Ledger Input Profile of Example User Future Additions Tutorial Generate an Example File Generating Reports Generating Balances Generating a Balance Sheet and Income Statement Journals Holdings Other Reports Other Formats Viewing Reports through the Web Interface The Future of Beancount Reports","title":"Beancount Example & Tutorial"},{"location":"tutorial_example.html#example-file-generator","text":"Beancount has a command to generate a few years of a realistic user\u2019s historical entries: bean-example > example.beancount The script checks if the output fails to process cleanly in Beancount (if this occurs, try rerunning or contact the author at blais@furius.ca ). Note that there is an option to fix the random generator\u2019s seed to be used to generate the entries. You can generate multiple years of ledger data for this user; see available options with bean-example --help The purpose of the script is: To provide a realistic corpus of anonymous data for users to experiment with, to kick the tires of Beancount, generate reports, etc. To compare reports with those generated by Ledger. As an example file to be used in this tutorial. As input for stress testing Beancount.","title":"Example File Generator"},{"location":"tutorial_example.html#converting-to-ledger-input","text":"It should be possible to convert example files generated from the script to Ledger syntax, like this: bean-report example.beancount ledger > example.lgr If you encounter any problems with the conversion, please let me know. While I have an automated test to check that the conversion succeeds, I\u2019m focusing my efforts on Beancount itself and I\u2019m not using Ledger much other than for the occasional comparison or for illustrating something on the mailing-list.","title":"Converting to Ledger Input"},{"location":"tutorial_example.html#profile-of-example-user","text":"Here\u2019s a high-level profile of this user. In the text that follows, I\u2019ll use the masculine \u201che\u201d for simplicity\u2019s sake. He lives somewhere in the USA and uses a single operating currency: \u201cUSD\u201d. He rents an apartment. Income : He is a software engineer (sorry I could not resist), receives a modest salary income from a software company. He is paid bi-weekly. He maximizes contributions to his company\u2019s 401k plan. His company offers a match to his contributions. His salary is paid as a direct deposit to his checking account. He benefits from and pays premiums for a health insurance plan provided by his employer. He tracks his vacation hours explicitly. Expenses : Checking account : He pays his apartment\u2019s rent by check. He also pays his electricity, internet ISP and mobile phone bills directly from his checking account. His bank also takes a monthly bank fee. Credit cards : He also has other regular expenses, such as groceries and restaurant outings with friends, which he pays from his credit card. Assets : Banking : He has an account at a major US bank. Retirement account : His 401k account is held at Fidelity or Vanguard and is allocated in its entirety to two funds, a mix of stocks and bonds. There are never any sales in this account. This account is held at average cost. Other investments : Extra money that he is able to save is transferred from his checking account towards a taxable investment account in which he purchases a mix of stocks and ETFs, and occasionally makes sales and realizes profits. Liabilities : Credit cards : He has one or two credit cards issued by another bank. Events/Tags : He takes a short vacation or two per year, where he travels somewhere fun.","title":"Profile of Example User"},{"location":"tutorial_example.html#future-additions","text":"Note that as I beef up the example generator script, this profile will increase in complexity. If there is a particular situation from the cookbook or otherwise that you\u2019d like to see in the example file, please let me know (a patch is the best way to let me know). Generate fake PDF files for documents (from an option) along with suitable document directives for them, most of them implicit but some explicit. Generate more expenses: Some amount of realistic cash expenses. Add a vehicle: car repairs, gas, etc. Remove tram example, it is too urban-specific. Add donations for charity. Add a second credit card and vary sources of expenses. Add some real uses for links, for the 401k match, for instance, the employer contributions should be linked with the employee contributions. Or other ones. Generate a loan and payments for a student loan. Having a large liability is a common case, I\u2019d like to represent that, even if it\u2019s not a mortgage. Convert retirement accounts to book using the average cost method, and add corresponding fees which would otherwise be impossible to track with explicit lots. Add tracking of health care expenses as per the cookbook. Add foreign accounts in another currency and transactions and transfers in that currency.","title":"Future Additions"},{"location":"tutorial_example.html#tutorial","text":"This section provides basic examples of generating reports with Beancount, on the typical example file that would be output by it. The list of reports here is not meant to be exhaustive. You may follow the tutorial and generate your own example file, or look at the files from beancount/examples/tutorial if you prefer.","title":"Tutorial"},{"location":"tutorial_example.html#generate-an-example-file","text":"Begin by generating the example file: bean-example > example.beancount Open example.beancount and examine it. Next, before we begin generating reports, verify that the file loads without any errors: bean-check example.beancount It should return quietly, without outputting anything (bean-check only writes errors when there are some, otherwise on success it writes nothing).","title":"Generate an Example File"},{"location":"tutorial_example.html#generating-reports","text":"Let\u2019s generate a report of the final balances of all accounts [output] : bean-report example.beancount balances As you can see, the bean-report script has subcommands for the various reports it generates. To list the available reports, use --help-reports [output] : bean-report --help-reports To list the options available for a particular report, use --help on it [output] : bean-report example.beancount balances --help The report-specific options vary for each report type. You can also list the global options for the script [output] : bean-report --help The global options allow you to specify the desired output format for the reports. To see which formats are available for which report, use --help-formats [output] : bean-report --help-formats","title":"Generating Reports"},{"location":"tutorial_example.html#generating-balances","text":"Good, so we know how to generate a report of balances for all accounts. This is a pretty detailed list of accounts though. Let\u2019s just restrict the output to the accounts that we\u2019re interested in [output] : bean-report example.beancount balances -e ETrade Here you can view the number of units held in each of the investment subaccounts. To render the cost, use the --cost option [output] : bean-report example.beancount balances -e ETrade --cost","title":"Generating Balances"},{"location":"tutorial_example.html#formatting-tools","text":"Sometimes it\u2019s nice to render a hierarchical list of accounts as a tree. You can use the \u201ctreeify\u201d tool provided by Beancount to do this: bean-report example.beancount balances | treeify This tool will work on any column of data that looks like a column of account names (you can also configure it work with filenames as well, or other patterns).","title":"Formatting Tools"},{"location":"tutorial_example.html#generating-a-balance-sheet-and-income-statement","text":"Let us generate a balance sheet: bean-report example.beancount balsheet Unfortunately, the only output format supported for it at this point is HTML. Also, filtering balance sheet entries from the command-line is not supported. Generate this to a file and open a browser to it [output] : bean-report example.beancount balsheet > /tmp/balsheet.html You can to the same for income statements: bean-report example.beancount income > /tmp/income.html","title":"Generating a Balance Sheet and Income Statement"},{"location":"tutorial_example.html#journals","text":"You can also generate journals (in Ledger parlance, these are \u201cregisters\u201d). Let\u2019s look at a checking account postings, for instance [output] : bean-report example.beancount journal -a Assets:US:BofA:Checking To render a column of running balances, add the --balance option [output] : bean-report example.beancount journal -a Assets:US:BofA:Checking --balance The inventories are rendered in the number of units they\u2019re holding [output] : bean-report example.beancount journal -a Assets:US:ETrade:GLD If you want to render the values at cost, use the --cost option [output] : bean-report example.beancount journal -a Assets:US:ETrade:GLD --cost To render journals, you will typically restrict the set of postings that you want to view. If you did not, the postings would render no change, because all the legs of the transactions would be applied to the running balance and the total would be zero. But try it, nonetheless [output] : bean-report example.beancount journal --balance","title":"Journals"},{"location":"tutorial_example.html#holdings","text":"There are a variety of ways to obtain aggregations for the total list of holdings. List the detailed holdings [output] : bean-report example.beancount holdings To aggregate the holdings by account, do this [output] : bean-report example.beancount holdings --by=account Because it\u2019s common practice to create and use a dedicated Beancount subaccount name for each commodity held in a real-world account, we can refine this further to aggregate to the parent (\u201croot\u201d) accounts of those subaccounts [output] : bean-report example.beancount holdings --by=root-account We can aggregate by commodity [output] : bean-report example.beancount holdings --by=commodity Or by the currency in which the commodities are quoted, which gives us a glimpse into our currency exposure [output] : bean-report example.beancount holdings --by=currency Finally, we can aggregate all the holdings to obtain net worth [output] : bean-report example.beancount networth There are a few more options for converting amounts to a common currency. See help for details.","title":"Holdings"},{"location":"tutorial_example.html#other-reports","text":"There are many other miscellaneous reports available. Try a few of those. Listing all accounts [output] : bean-report example.beancount accounts Listing events [output] : bean-report example.beancount events Number of directives by type [output] : bean-report example.beancount stats-directives Number of postings by type [output] : bean-report example.beancount stats-postings","title":"Other Reports"},{"location":"tutorial_example.html#other-formats","text":"Reports may support various output formats. You can change the output format with the --format (-f) global option [output] : bean-report -f csv example.beancount holdings Note that \u201cbeancount\u201d and \u201cledger\u201d are valid output formats: they refer to the Beancount and Ledger input syntaxes.","title":"Other Formats"},{"location":"tutorial_example.html#viewing-reports-through-the-web-interface","text":"The original way to access reports in Beancount is via its web interface that serves to a local web server on your machine. Serve the example file like this: bean-web example.beancount Then navigate with a web browser to http://localhost:8080 . From there, you can click on any number of filtered views and access some of the reports previously demonstrated. For example, click on a year view; that will provide balance sheets and income statements and various other reports for this subset of transactions. The bean-web tool has many options for restricting what is being served. (Note that by default the server listens only for connections from your computer; if you want to host this on a web server and accept connections from anywhere, use --public ). Note: There exists a separate project which provides a better web interface than the one which comes with Beancount: Fava . You might want to check it out.","title":"Viewing Reports through the Web Interface"},{"location":"tutorial_example.html#the-future-of-beancount-reports","text":"I find my command-line reports above to be rather unsatisfying, from the point-of-view of customizability. They bother me a lot. This is largely because I\u2019ve been happy and satisfied with the capabilities of the web interface to Beancount. As I\u2019m beginning to use the command-line interface more and more, I\u2019m finding myself desiring for more explicit and well-defined way to apply all the filters I have available from the web interface, and more. For this reason, I\u2019m currently implementing a query language that looks similar to SQL . This syntax will allow me to remove all the custom reports above and to replace them by a single consistent query syntax. This work is underway. All the holdings reports and even their internal objects will be replaced by the SQL-like query syntax as well. Holdings need not be treated separately: they are simply the list of positions available at a particular point in time, and along with suitable accessors from the query syntax and support for aggregations over their many columns, we should be able to generate all of those reports equivalently and simplify the code. \u2014 Martin Blais, October 2014","title":"The Future of Beancount Reports"},{"location":"api_reference/index.html","text":"beancount \uf0c1 beancount.core beancount.loader beancount.ops beancount.parser beancount.plugins beancount.scripts beancount.tools beancount.utils","title":"beancount"},{"location":"api_reference/index.html#beancount","text":"beancount.core beancount.loader beancount.ops beancount.parser beancount.plugins beancount.scripts beancount.tools beancount.utils","title":"beancount"},{"location":"api_reference/beancount.core.html","text":"beancount.core \uf0c1 Core basic objects and data structures to represent a list of entries. beancount.core.account \uf0c1 Functions that operate on account strings. These account objects are rather simple and dumb; they do not contain the list of their associated postings. This is achieved by building a realization; see realization.py for details. beancount.core.account.AccountTransformer \uf0c1 Account name transformer. This is used to support Win... huh, filesystems and platforms which do not support colon characters. Attributes: Name Type Description rsep A character string, the new separator to use in link names. beancount . core . account . AccountTransformer . parse ( self , transformed_name ) \uf0c1 Convert the transform account name to an account name. Source code in beancount/core/account.py def parse ( self , transformed_name : str ) -> Account : \"Convert the transform account name to an account name.\" return ( transformed_name if self . rsep is None else transformed_name . replace ( self . rsep , sep ) ) beancount . core . account . AccountTransformer . render ( self , account_name ) \uf0c1 Convert the account name to a transformed account name. Source code in beancount/core/account.py def render ( self , account_name : Account ) -> str : \"Convert the account name to a transformed account name.\" return account_name if self . rsep is None else account_name . replace ( sep , self . rsep ) beancount . core . account . commonprefix ( accounts ) \uf0c1 Return the common prefix of a list of account names. Parameters: accounts ( Iterable[str] ) \u2013 A sequence of account name strings. Returns: str \u2013 A string, the common parent account. If none, returns an empty string. Source code in beancount/core/account.py def commonprefix ( accounts : Iterable [ Account ]) -> Account : \"\"\"Return the common prefix of a list of account names. Args: accounts: A sequence of account name strings. Returns: A string, the common parent account. If none, returns an empty string. \"\"\" accounts_lists = [ account_ . split ( sep ) for account_ in accounts ] # Note: the os.path.commonprefix() function just happens to work here. # Inspect its code, and even the special case of no common prefix # works well with str.join() below. common_list = path . commonprefix ( accounts_lists ) return sep . join ( common_list ) beancount . core . account . has_component ( account_name , component ) \uf0c1 Return true if one of the account contains a given component. Parameters: account_name ( str ) \u2013 A string, an account name. component ( str ) \u2013 A string, a component of an account name. For instance, Food in Expenses:Food:Restaurant . All components are considered. Returns: Boolean \u2013 true if the component is in the account. Note that a component name must be whole, that is NY is not in Expenses:Taxes:StateNY . Source code in beancount/core/account.py def has_component ( account_name : Account , component : str ) -> bool : \"\"\"Return true if one of the account contains a given component. Args: account_name: A string, an account name. component: A string, a component of an account name. For instance, ``Food`` in ``Expenses:Food:Restaurant``. All components are considered. Returns: Boolean: true if the component is in the account. Note that a component name must be whole, that is ``NY`` is not in ``Expenses:Taxes:StateNY``. \"\"\" return bool ( re . search ( \"(^|:) {} (:|$)\" . format ( component ), account_name )) beancount . core . account . is_valid ( string ) \uf0c1 Return true if the given string is a valid account name. This does not check for the root account types, just the general syntax. Parameters: string ( str ) \u2013 A string, to be checked for account name pattern. Returns: bool \u2013 A boolean, true if the string has the form of an account's name. Source code in beancount/core/account.py def is_valid ( string : Account ) -> bool : \"\"\"Return true if the given string is a valid account name. This does not check for the root account types, just the general syntax. Args: string: A string, to be checked for account name pattern. Returns: A boolean, true if the string has the form of an account's name. \"\"\" return isinstance ( string , str ) and bool ( regex . match ( \" {} $\" . format ( ACCOUNT_RE ), string )) beancount . core . account . join ( * components ) \uf0c1 Join the names with the account separator. Parameters: *components ( Tuple[str] ) \u2013 Strings, the components of an account name. Returns: str \u2013 A string, joined in a single account name. Source code in beancount/core/account.py def join ( * components : Tuple [ str ]) -> Account : \"\"\"Join the names with the account separator. Args: *components: Strings, the components of an account name. Returns: A string, joined in a single account name. \"\"\" return sep . join ( components ) beancount . core . account . leaf ( account_name ) \uf0c1 Get the name of the leaf of this account. Parameters: account_name ( str ) \u2013 A string, the name of the account whose leaf name to return. Returns: str \u2013 A string, the name of the leaf of the account. Source code in beancount/core/account.py def leaf ( account_name : Account ) -> Account : \"\"\"Get the name of the leaf of this account. Args: account_name: A string, the name of the account whose leaf name to return. Returns: A string, the name of the leaf of the account. \"\"\" assert isinstance ( account_name , str ) return account_name . split ( sep )[ - 1 ] if account_name else None beancount . core . account . parent ( account_name ) \uf0c1 Return the name of the parent account of the given account. Parameters: account_name ( str ) \u2013 A string, the name of the account whose parent to return. Returns: str \u2013 A string, the name of the parent account of this account. Source code in beancount/core/account.py def parent ( account_name : Account ) -> Account : \"\"\"Return the name of the parent account of the given account. Args: account_name: A string, the name of the account whose parent to return. Returns: A string, the name of the parent account of this account. \"\"\" assert isinstance ( account_name , str ), account_name if not account_name : return None components = account_name . split ( sep ) components . pop ( - 1 ) return sep . join ( components ) beancount . core . account . parent_matcher ( account_name ) \uf0c1 Build a predicate that returns whether an account is under the given one. Parameters: account_name ( str ) \u2013 The name of the parent account we want to check for. Returns: Callable[[str], Any] \u2013 A callable, which, when called, will return true if the given account is a child of account_name . Source code in beancount/core/account.py def parent_matcher ( account_name : Account ) -> Callable [[ str ], Any ]: \"\"\"Build a predicate that returns whether an account is under the given one. Args: account_name: The name of the parent account we want to check for. Returns: A callable, which, when called, will return true if the given account is a child of ``account_name``. \"\"\" return re . compile ( r \" {} ($| {} )\" . format ( re . escape ( account_name ), sep )) . match beancount . core . account . parents ( account_name ) \uf0c1 A generator of the names of the parents of this account, including this account. Parameters: account_name ( str ) \u2013 The name of the account we want to start iterating from. Returns: Iterator[str] \u2013 A generator of account name strings. Source code in beancount/core/account.py def parents ( account_name : Account ) -> Iterator [ Account ]: \"\"\"A generator of the names of the parents of this account, including this account. Args: account_name: The name of the account we want to start iterating from. Returns: A generator of account name strings. \"\"\" while account_name : yield account_name account_name = parent ( account_name ) beancount . core . account . root ( num_components , account_name ) \uf0c1 Return the first few components of an account's name. Parameters: num_components ( int ) \u2013 An integer, the number of components to return. account_name ( str ) \u2013 A string, an account name. Returns: str \u2013 A string, the account root up to 'num_components' components. Source code in beancount/core/account.py def root ( num_components : int , account_name : Account ) -> str : \"\"\"Return the first few components of an account's name. Args: num_components: An integer, the number of components to return. account_name: A string, an account name. Returns: A string, the account root up to 'num_components' components. \"\"\" return join ( * ( split ( account_name )[: num_components ])) beancount . core . account . sans_root ( account_name ) \uf0c1 Get the name of the account without the root. For example, an input of 'Assets:BofA:Checking' will produce 'BofA:Checking'. Parameters: account_name ( str ) \u2013 A string, the name of the account whose leaf name to return. Returns: str \u2013 A string, the name of the non-root portion of this account name. Source code in beancount/core/account.py def sans_root ( account_name : Account ) -> Account : \"\"\"Get the name of the account without the root. For example, an input of 'Assets:BofA:Checking' will produce 'BofA:Checking'. Args: account_name: A string, the name of the account whose leaf name to return. Returns: A string, the name of the non-root portion of this account name. \"\"\" assert isinstance ( account_name , str ) components = account_name . split ( sep )[ 1 :] return join ( * components ) if account_name else None beancount . core . account . split ( account_name ) \uf0c1 Split an account's name into its components. Parameters: account_name ( str ) \u2013 A string, an account name. Returns: List[str] \u2013 A list of strings, the components of the account name (without the separators). Source code in beancount/core/account.py def split ( account_name : Account ) -> List [ str ]: \"\"\"Split an account's name into its components. Args: account_name: A string, an account name. Returns: A list of strings, the components of the account name (without the separators). \"\"\" return account_name . split ( sep ) beancount . core . account . walk ( root_directory ) \uf0c1 A version of os.walk() which yields directories that are valid account names. This only yields directories that are accounts... it skips the other ones. For convenience, it also yields you the account's name. Parameters: root_directory ( str ) \u2013 A string, the name of the root of the hierarchy to be walked. Yields: Tuples of (root, account-name, dirs, files), similar to os.walk(). Source code in beancount/core/account.py def walk ( root_directory : Account ) -> Iterator [ Tuple [ str , Account , List [ str ], List [ str ]]]: \"\"\"A version of os.walk() which yields directories that are valid account names. This only yields directories that are accounts... it skips the other ones. For convenience, it also yields you the account's name. Args: root_directory: A string, the name of the root of the hierarchy to be walked. Yields: Tuples of (root, account-name, dirs, files), similar to os.walk(). \"\"\" for root , dirs , files in os . walk ( root_directory ): dirs . sort () files . sort () relroot = root [ len ( root_directory ) + 1 :] account_name = relroot . replace ( os . sep , sep ) # The regex module does not handle Unicode characters in decomposed # form. Python uses the normal form for representing string. However, # some filesystems use the canonical decomposition form. # See https://docs.python.org/3/library/unicodedata.html#unicodedata.normalize account_name = unicodedata . normalize ( \"NFKC\" , account_name ) if is_valid ( account_name ): yield ( root , account_name , dirs , files ) beancount.core.account_types \uf0c1 Definition for global account types. This is where we keep the global account types value and definition. Note that it's unfortunate that we're using globals and side-effect here, but this is the best solution in the short-term, the account types are used in too many places to pass around that state everywhere. Maybe we change this later on. beancount.core.account_types.AccountTypes ( tuple ) \uf0c1 AccountTypes(assets, liabilities, equity, income, expenses) beancount . core . account_types . AccountTypes . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/account_types.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . account_types . AccountTypes . __new__ ( _cls , assets , liabilities , equity , income , expenses ) special staticmethod \uf0c1 Create new instance of AccountTypes(assets, liabilities, equity, income, expenses) beancount . core . account_types . AccountTypes . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/account_types.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . core . account_types . get_account_sign ( account_name , account_types = None ) \uf0c1 Return the sign of the normal balance of a particular account. Parameters: account_name ( str ) \u2013 A string, the name of the account whose sign is to return. account_types ( AccountTypes ) \u2013 An optional instance of the current account_types. Returns: int \u2013 +1 or -1, depending on the account's type. Source code in beancount/core/account_types.py def get_account_sign ( account_name : Account , account_types : AccountTypes = None ) -> int : \"\"\"Return the sign of the normal balance of a particular account. Args: account_name: A string, the name of the account whose sign is to return. account_types: An optional instance of the current account_types. Returns: +1 or -1, depending on the account's type. \"\"\" if account_types is None : account_types = DEFAULT_ACCOUNT_TYPES assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) account_type = get_account_type ( account_name ) return + 1 if account_type in ( account_types . assets , account_types . expenses ) else - 1 beancount . core . account_types . get_account_sort_key ( account_types , account_name ) \uf0c1 Return a tuple that can be used to order/sort account names. Parameters: account_types ( AccountTypes ) \u2013 An instance of AccountTypes, a tuple of account type names. Returns: Tuple[str, str] \u2013 An object to use as the 'key' argument to the sort function. Source code in beancount/core/account_types.py def get_account_sort_key ( account_types : AccountTypes , account_name : Account ) -> Tuple [ str , Account ]: \"\"\"Return a tuple that can be used to order/sort account names. Args: account_types: An instance of AccountTypes, a tuple of account type names. Returns: An object to use as the 'key' argument to the sort function. \"\"\" return ( account_types . index ( get_account_type ( account_name )), account_name ) beancount . core . account_types . get_account_type ( account_name ) \uf0c1 Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Parameters: account_name ( str ) \u2013 A string, the name of the account whose type is to return. Returns: A string, the type of the account in 'account_name'. Source code in beancount/core/account_types.py def get_account_type ( account_name : Account ): \"\"\"Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Args: account_name: A string, the name of the account whose type is to return. Returns: A string, the type of the account in 'account_name'. \"\"\" assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) return account . split ( account_name )[ 0 ] beancount . core . account_types . is_account_type ( account_type , account_name ) \uf0c1 Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Parameters: account_type ( str ) \u2013 A string, the prefix type of the account. account_name ( str ) \u2013 A string, the name of the account whose type is to return. Returns: bool \u2013 A boolean, true if the account is of the given type. Source code in beancount/core/account_types.py def is_account_type ( account_type : str , account_name : Account ) -> bool : \"\"\"Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Args: account_type: A string, the prefix type of the account. account_name: A string, the name of the account whose type is to return. Returns: A boolean, true if the account is of the given type. \"\"\" return bool ( re . match ( \"^ {}{} \" . format ( account_type , account . sep ), account_name )) beancount . core . account_types . is_balance_sheet_account ( account_name , account_types ) \uf0c1 Return true if the given account is a balance sheet account. Assets, liabilities and equity accounts are balance sheet accounts. Parameters: account_name ( str ) \u2013 A string, an account name. account_types ( AccountTypes ) \u2013 An instance of AccountTypes. Returns: bool \u2013 A boolean, true if the account is a balance sheet account. Source code in beancount/core/account_types.py def is_balance_sheet_account ( account_name : Account , account_types : AccountTypes ) -> bool : \"\"\"Return true if the given account is a balance sheet account. Assets, liabilities and equity accounts are balance sheet accounts. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account is a balance sheet account. \"\"\" assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) assert isinstance ( account_types , AccountTypes ), \"Account types has invalid type: {} \" . format ( account_types ) account_type = get_account_type ( account_name ) return account_type in ( account_types . assets , account_types . liabilities , account_types . equity , ) beancount . core . account_types . is_equity_account ( account_name , account_types ) \uf0c1 Return true if the given account is an equity account. Parameters: account_name ( str ) \u2013 A string, an account name. account_types ( AccountTypes ) \u2013 An instance of AccountTypes. Returns: bool \u2013 A boolean, true if the account is an equity account. Source code in beancount/core/account_types.py def is_equity_account ( account_name : Account , account_types : AccountTypes ) -> bool : \"\"\"Return true if the given account is an equity account. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account is an equity account. \"\"\" assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) assert isinstance ( account_types , AccountTypes ), \"Account types has invalid type: {} \" . format ( account_types ) account_type = get_account_type ( account_name ) return account_type == account_types . equity beancount . core . account_types . is_income_statement_account ( account_name , account_types ) \uf0c1 Return true if the given account is an income statement account. Income and expense accounts are income statement accounts. Parameters: account_name ( str ) \u2013 A string, an account name. account_types ( AccountTypes ) \u2013 An instance of AccountTypes. Returns: bool \u2013 A boolean, true if the account is an income statement account. Source code in beancount/core/account_types.py def is_income_statement_account ( account_name : Account , account_types : AccountTypes ) -> bool : \"\"\"Return true if the given account is an income statement account. Income and expense accounts are income statement accounts. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account is an income statement account. \"\"\" assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) assert isinstance ( account_types , AccountTypes ), \"Account types has invalid type: {} \" . format ( account_types ) account_type = get_account_type ( account_name ) return account_type in ( account_types . income , account_types . expenses ) beancount . core . account_types . is_inverted_account ( account_name , account_types ) \uf0c1 Return true if the given account has inverted signs. An inverted sign is the inverse as you'd expect in an external report, i.e., with all positive signs expected. Parameters: account_name ( str ) \u2013 A string, an account name. account_types ( AccountTypes ) \u2013 An instance of AccountTypes. Returns: bool \u2013 A boolean, true if the account has an inverted sign. Source code in beancount/core/account_types.py def is_inverted_account ( account_name : Account , account_types : AccountTypes ) -> bool : \"\"\"Return true if the given account has inverted signs. An inverted sign is the inverse as you'd expect in an external report, i.e., with all positive signs expected. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account has an inverted sign. \"\"\" assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) assert isinstance ( account_types , AccountTypes ), \"Account types has invalid type: {} \" . format ( account_types ) account_type = get_account_type ( account_name ) return account_type in ( account_types . liabilities , account_types . income , account_types . equity , ) beancount . core . account_types . is_root_account ( account_name ) \uf0c1 Return true if the account name is a root account. This function does not verify whether the account root is a valid one, just that it is a root account or not. Parameters: account_name ( str ) \u2013 A string, the name of the account to check for. Returns: bool \u2013 A boolean, true if the account is root account. Source code in beancount/core/account_types.py def is_root_account ( account_name : Account ) -> bool : \"\"\"Return true if the account name is a root account. This function does not verify whether the account root is a valid one, just that it is a root account or not. Args: account_name: A string, the name of the account to check for. Returns: A boolean, true if the account is root account. \"\"\" assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) return account_name and bool ( re . match ( r \"([A-Z][A-Za-z0-9\\-]+)$\" , account_name )) beancount.core.amount \uf0c1 Amount class. This simple class is used to associate a number of units of a currency with its currency: (number, currency). beancount.core.amount.Amount ( _Amount ) \uf0c1 An 'Amount' represents a number of a particular unit of something. It's essentially a typed number, with corresponding manipulation operations defined on it. beancount . core . amount . Amount . __bool__ ( self ) special \uf0c1 Boolean predicate returns true if the number is non-zero. Returns: A boolean, true if non-zero number. Source code in beancount/core/amount.py def __bool__ ( self ): \"\"\"Boolean predicate returns true if the number is non-zero. Returns: A boolean, true if non-zero number. \"\"\" return self . number != ZERO beancount . core . amount . Amount . __eq__ ( self , other ) special \uf0c1 Equality predicate. Returns true if both number and currency are equal. Returns: A boolean. Source code in beancount/core/amount.py def __eq__ ( self , other ): \"\"\"Equality predicate. Returns true if both number and currency are equal. Returns: A boolean. \"\"\" if other is None : return False return ( self . number , self . currency ) == ( other . number , other . currency ) beancount . core . amount . Amount . __hash__ ( self ) special \uf0c1 A hashing function for amounts. The hash includes the currency. Returns: An integer, the hash for this amount. Source code in beancount/core/amount.py def __hash__ ( self ): \"\"\"A hashing function for amounts. The hash includes the currency. Returns: An integer, the hash for this amount. \"\"\" return hash (( self . number , self . currency )) beancount . core . amount . Amount . __lt__ ( self , other ) special \uf0c1 Ordering comparison. This is used in the sorting key of positions. Parameters: other \u2013 An instance of Amount. Returns: True if this is less than the other Amount. Source code in beancount/core/amount.py def __lt__ ( self , other ): \"\"\"Ordering comparison. This is used in the sorting key of positions. Args: other: An instance of Amount. Returns: True if this is less than the other Amount. \"\"\" return sortkey ( self ) < sortkey ( other ) beancount . core . amount . Amount . __neg__ ( self ) special \uf0c1 Return the negative of this amount. Returns: A new instance of Amount, with the negative number of units. Source code in beancount/core/amount.py def __neg__ ( self ): \"\"\"Return the negative of this amount. Returns: A new instance of Amount, with the negative number of units. \"\"\" return Amount ( - self . number , self . currency ) beancount . core . amount . Amount . __new__ ( cls , number , currency ) special staticmethod \uf0c1 Constructor from a number and currency. Parameters: number \u2013 A Decimal instance. currency \u2013 A string, the currency symbol to use. Source code in beancount/core/amount.py def __new__ ( cls , number , currency ): \"\"\"Constructor from a number and currency. Args: number: A Decimal instance. currency: A string, the currency symbol to use. \"\"\" assert isinstance ( number , Amount . valid_types_number ), repr ( number ) assert isinstance ( currency , Amount . valid_types_currency ), repr ( currency ) return _Amount . __new__ ( cls , number , currency ) beancount . core . amount . Amount . __repr__ ( self ) special \uf0c1 Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/amount.py def __str__ ( self ): \"\"\"Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. \"\"\" return self . to_string () beancount . core . amount . Amount . __str__ ( self ) special \uf0c1 Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/amount.py def __str__ ( self ): \"\"\"Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. \"\"\" return self . to_string () beancount . core . amount . Amount . from_string ( string ) staticmethod \uf0c1 Create an amount from a string. This is a miniature parser used for building tests. Parameters: string \u2013 A string of . Returns: A new instance of Amount. Source code in beancount/core/amount.py @staticmethod def from_string ( string ): \"\"\"Create an amount from a string. This is a miniature parser used for building tests. Args: string: A string of . Returns: A new instance of Amount. \"\"\" match = re . match ( r \"\\s*([-+]?[0-9.]+)\\s+( {currency} )\" . format ( currency = CURRENCY_RE ), string ) if not match : raise ValueError ( \"Invalid string for amount: ' {} '\" . format ( string )) number , currency = match . group ( 1 , 2 ) return Amount ( D ( number ), currency ) beancount . core . amount . Amount . to_string ( self , dformat =< beancount . core . display_context . DisplayFormatter object at 0x78fcde6b7290 > ) \uf0c1 Convert an Amount instance to a printable string. Parameters: dformat \u2013 An instance of DisplayFormatter. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/amount.py def to_string ( self , dformat = DEFAULT_FORMATTER ): \"\"\"Convert an Amount instance to a printable string. Args: dformat: An instance of DisplayFormatter. Returns: A formatted string of the quantized amount and symbol. \"\"\" if isinstance ( self . number , Decimal ): number_fmt = dformat . format ( self . number , self . currency ) elif self . number is MISSING : number_fmt = \"\" else : number_fmt = str ( self . number ) return \" {} {} \" . format ( number_fmt , self . currency ) beancount . core . amount . A ( string ) \uf0c1 Create an amount from a string. This is a miniature parser used for building tests. Parameters: string \u2013 A string of . Returns: A new instance of Amount. Source code in beancount/core/amount.py @staticmethod def from_string ( string ): \"\"\"Create an amount from a string. This is a miniature parser used for building tests. Args: string: A string of . Returns: A new instance of Amount. \"\"\" match = re . match ( r \"\\s*([-+]?[0-9.]+)\\s+( {currency} )\" . format ( currency = CURRENCY_RE ), string ) if not match : raise ValueError ( \"Invalid string for amount: ' {} '\" . format ( string )) number , currency = match . group ( 1 , 2 ) return Amount ( D ( number ), currency ) beancount . core . amount . abs ( amount ) \uf0c1 Return the absolute value of the given amount. Parameters: amount \u2013 An instance of Amount. Returns: An instance of Amount. Source code in beancount/core/amount.py def abs ( amount ): \"\"\"Return the absolute value of the given amount. Args: amount: An instance of Amount. Returns: An instance of Amount. \"\"\" return amount if amount . number >= ZERO else Amount ( - amount . number , amount . currency ) beancount . core . amount . add ( amount1 , amount2 ) \uf0c1 Add the given amounts with the same currency. Parameters: amount1 \u2013 An instance of Amount. amount2 \u2013 An instance of Amount. Returns: An instance of Amount, with the sum the two amount's numbers, in the same currency. Source code in beancount/core/amount.py def add ( amount1 , amount2 ): \"\"\"Add the given amounts with the same currency. Args: amount1: An instance of Amount. amount2: An instance of Amount. Returns: An instance of Amount, with the sum the two amount's numbers, in the same currency. \"\"\" assert isinstance ( amount1 . number , Decimal ), \"Amount1's number is not a Decimal instance: {} \" . format ( amount1 . number ) assert isinstance ( amount2 . number , Decimal ), \"Amount2's number is not a Decimal instance: {} \" . format ( amount2 . number ) if amount1 . currency != amount2 . currency : raise ValueError ( \"Unmatching currencies for operation on {} and {} \" . format ( amount1 , amount2 ) ) return Amount ( amount1 . number + amount2 . number , amount1 . currency ) beancount . core . amount . div ( amount , number ) \uf0c1 Divide the given amount by a number. Parameters: amount \u2013 An instance of Amount. number \u2013 A decimal number. Returns: An Amount, with the same currency, but with amount units divided by 'number'. Source code in beancount/core/amount.py def div ( amount , number ): \"\"\"Divide the given amount by a number. Args: amount: An instance of Amount. number: A decimal number. Returns: An Amount, with the same currency, but with amount units divided by 'number'. \"\"\" assert isinstance ( amount . number , Decimal ), \"Amount's number is not a Decimal instance: {} \" . format ( amount . number ) assert isinstance ( number , Decimal ), \"Number is not a Decimal instance: {} \" . format ( number ) return Amount ( amount . number / number , amount . currency ) beancount . core . amount . from_string ( string ) \uf0c1 Create an amount from a string. This is a miniature parser used for building tests. Parameters: string \u2013 A string of . Returns: A new instance of Amount. Source code in beancount/core/amount.py @staticmethod def from_string ( string ): \"\"\"Create an amount from a string. This is a miniature parser used for building tests. Args: string: A string of . Returns: A new instance of Amount. \"\"\" match = re . match ( r \"\\s*([-+]?[0-9.]+)\\s+( {currency} )\" . format ( currency = CURRENCY_RE ), string ) if not match : raise ValueError ( \"Invalid string for amount: ' {} '\" . format ( string )) number , currency = match . group ( 1 , 2 ) return Amount ( D ( number ), currency ) beancount . core . amount . mul ( amount , number ) \uf0c1 Multiply the given amount by a number. Parameters: amount \u2013 An instance of Amount. number \u2013 A decimal number. Returns: An Amount, with the same currency, but with 'number' times units. Source code in beancount/core/amount.py def mul ( amount , number ): \"\"\"Multiply the given amount by a number. Args: amount: An instance of Amount. number: A decimal number. Returns: An Amount, with the same currency, but with 'number' times units. \"\"\" assert isinstance ( amount . number , Decimal ), \"Amount's number is not a Decimal instance: {} \" . format ( amount . number ) assert isinstance ( number , Decimal ), \"Number is not a Decimal instance: {} \" . format ( number ) return Amount ( amount . number * number , amount . currency ) beancount . core . amount . sortkey ( amount ) \uf0c1 A comparison function that sorts by currency first. Parameters: amount \u2013 An instance of Amount. Returns: A sort key, composed of the currency first and then the number. Source code in beancount/core/amount.py def sortkey ( amount ): \"\"\"A comparison function that sorts by currency first. Args: amount: An instance of Amount. Returns: A sort key, composed of the currency first and then the number. \"\"\" return ( amount . currency , amount . number ) beancount . core . amount . sub ( amount1 , amount2 ) \uf0c1 Subtract the given amounts with the same currency. Parameters: amount1 \u2013 An instance of Amount. amount2 \u2013 An instance of Amount. Returns: An instance of Amount, with the difference between the two amount's numbers, in the same currency. Source code in beancount/core/amount.py def sub ( amount1 , amount2 ): \"\"\"Subtract the given amounts with the same currency. Args: amount1: An instance of Amount. amount2: An instance of Amount. Returns: An instance of Amount, with the difference between the two amount's numbers, in the same currency. \"\"\" assert isinstance ( amount1 . number , Decimal ), \"Amount1's number is not a Decimal instance: {} \" . format ( amount1 . number ) assert isinstance ( amount2 . number , Decimal ), \"Amount2's number is not a Decimal instance: {} \" . format ( amount2 . number ) if amount1 . currency != amount2 . currency : raise ValueError ( \"Unmatching currencies for operation on {} and {} \" . format ( amount1 , amount2 ) ) return Amount ( amount1 . number - amount2 . number , amount1 . currency ) beancount.core.compare \uf0c1 Comparison helpers for data objects. beancount.core.compare.CompareError ( tuple ) \uf0c1 CompareError(source, message, entry) beancount . core . compare . CompareError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/compare.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . compare . CompareError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of CompareError(source, message, entry) beancount . core . compare . CompareError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/compare.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . core . compare . compare_entries ( entries1 , entries2 ) \uf0c1 Compare two lists of entries. This is used for testing. The entries are compared with disregard for their file location. Parameters: entries1 \u2013 A list of directives of any type. entries2 \u2013 Another list of directives of any type. Returns: A tuple of (success, not_found1, not_found2), where the fields are \u2013 success: A boolean, true if all the values are equal. missing1: A list of directives from 'entries1' not found in 'entries2'. missing2: A list of directives from 'entries2' not found in 'entries1'. Exceptions: ValueError \u2013 If a duplicate entry is found. Source code in beancount/core/compare.py def compare_entries ( entries1 , entries2 ): \"\"\"Compare two lists of entries. This is used for testing. The entries are compared with disregard for their file location. Args: entries1: A list of directives of any type. entries2: Another list of directives of any type. Returns: A tuple of (success, not_found1, not_found2), where the fields are: success: A boolean, true if all the values are equal. missing1: A list of directives from 'entries1' not found in 'entries2'. missing2: A list of directives from 'entries2' not found in 'entries1'. Raises: ValueError: If a duplicate entry is found. \"\"\" hashes1 , errors1 = hash_entries ( entries1 , exclude_meta = True ) hashes2 , errors2 = hash_entries ( entries2 , exclude_meta = True ) keys1 = set ( hashes1 . keys ()) keys2 = set ( hashes2 . keys ()) if errors1 or errors2 : error = ( errors1 + errors2 )[ 0 ] raise ValueError ( str ( error )) same = keys1 == keys2 missing1 = data . sorted ([ hashes1 [ key ] for key in keys1 - keys2 ]) missing2 = data . sorted ([ hashes2 [ key ] for key in keys2 - keys1 ]) return ( same , missing1 , missing2 ) beancount . core . compare . excludes_entries ( subset_entries , entries ) \uf0c1 Check that a list of entries does not appear in another list. Parameters: subset_entries \u2013 The set of entries to look for in 'entries'. entries \u2013 The larger list of entries that should not include 'subset_entries'. Returns: A boolean and a list of entries that are not supposed to appear. Exceptions: ValueError \u2013 If a duplicate entry is found. Source code in beancount/core/compare.py def excludes_entries ( subset_entries , entries ): \"\"\"Check that a list of entries does not appear in another list. Args: subset_entries: The set of entries to look for in 'entries'. entries: The larger list of entries that should not include 'subset_entries'. Returns: A boolean and a list of entries that are not supposed to appear. Raises: ValueError: If a duplicate entry is found. \"\"\" subset_hashes , subset_errors = hash_entries ( subset_entries , exclude_meta = True ) subset_keys = set ( subset_hashes . keys ()) hashes , errors = hash_entries ( entries , exclude_meta = True ) keys = set ( hashes . keys ()) if subset_errors or errors : error = ( subset_errors + errors )[ 0 ] raise ValueError ( str ( error )) intersection = keys . intersection ( subset_keys ) excludes = not bool ( intersection ) extra = data . sorted ([ subset_hashes [ key ] for key in intersection ]) return ( excludes , extra ) beancount . core . compare . hash_entries ( entries , exclude_meta = False ) \uf0c1 Compute unique hashes of each of the entries and return a map of them. This is used for comparisons between sets of entries. Parameters: entries \u2013 A list of directives. exclude_meta \u2013 If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A dict of hash-value to entry (for all entries) and a list of errors. Errors are created when duplicate entries are found. Source code in beancount/core/compare.py def hash_entries ( entries , exclude_meta = False ): \"\"\"Compute unique hashes of each of the entries and return a map of them. This is used for comparisons between sets of entries. Args: entries: A list of directives. exclude_meta: If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A dict of hash-value to entry (for all entries) and a list of errors. Errors are created when duplicate entries are found. \"\"\" entry_hash_dict = {} errors = [] num_legal_duplicates = 0 for entry in entries : hash_ = hash_entry ( entry , exclude_meta ) if hash_ in entry_hash_dict : if isinstance ( entry , Price ): # Note: Allow duplicate Price entries, they should be common # because of the nature of stock markets (if they're closed, the # data source is likely to return an entry for the previously # available date, which may already have been fetched). num_legal_duplicates += 1 else : other_entry = entry_hash_dict [ hash_ ] errors . append ( CompareError ( entry . meta , \"Duplicate entry: {} == {} \" . format ( entry , other_entry ), entry , ) ) entry_hash_dict [ hash_ ] = entry if not errors : assert len ( entry_hash_dict ) + num_legal_duplicates == len ( entries ), ( len ( entry_hash_dict ), len ( entries ), num_legal_duplicates , ) return entry_hash_dict , errors beancount . core . compare . hash_entry ( entry , exclude_meta = False ) \uf0c1 Compute the stable hash of a single entry. Parameters: entry \u2013 A directive instance. exclude_meta \u2013 If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A stable hexadecimal hash of this entry. Source code in beancount/core/compare.py def hash_entry ( entry , exclude_meta = False ): \"\"\"Compute the stable hash of a single entry. Args: entry: A directive instance. exclude_meta: If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A stable hexadecimal hash of this entry. \"\"\" return stable_hash_namedtuple ( entry , IGNORED_FIELD_NAMES if exclude_meta else frozenset () ) beancount . core . compare . includes_entries ( subset_entries , entries ) \uf0c1 Check if a list of entries is included in another list. Parameters: subset_entries \u2013 The set of entries to look for in 'entries'. entries \u2013 The larger list of entries that could include 'subset_entries'. Returns: A boolean and a list of missing entries. Exceptions: ValueError \u2013 If a duplicate entry is found. Source code in beancount/core/compare.py def includes_entries ( subset_entries , entries ): \"\"\"Check if a list of entries is included in another list. Args: subset_entries: The set of entries to look for in 'entries'. entries: The larger list of entries that could include 'subset_entries'. Returns: A boolean and a list of missing entries. Raises: ValueError: If a duplicate entry is found. \"\"\" subset_hashes , subset_errors = hash_entries ( subset_entries , exclude_meta = True ) subset_keys = set ( subset_hashes . keys ()) hashes , errors = hash_entries ( entries , exclude_meta = True ) keys = set ( hashes . keys ()) if subset_errors or errors : error = ( subset_errors + errors )[ 0 ] raise ValueError ( str ( error )) includes = subset_keys . issubset ( keys ) missing = data . sorted ([ subset_hashes [ key ] for key in subset_keys - keys ]) return ( includes , missing ) beancount . core . compare . stable_hash_namedtuple ( objtuple , ignore = frozenset ()) \uf0c1 Hash the given namedtuple and its child fields. This iterates over all the members of objtuple, skipping the attributes from the 'ignore' set, and computes a unique hash string code. If the elements are lists or sets, sorts them for stability. Parameters: objtuple \u2013 A tuple object or other. ignore \u2013 A set of strings, attribute names to be skipped in computing a stable hash. For instance, circular references to objects or irrelevant data. Source code in beancount/core/compare.py def stable_hash_namedtuple ( objtuple , ignore = frozenset ()): \"\"\"Hash the given namedtuple and its child fields. This iterates over all the members of objtuple, skipping the attributes from the 'ignore' set, and computes a unique hash string code. If the elements are lists or sets, sorts them for stability. Args: objtuple: A tuple object or other. ignore: A set of strings, attribute names to be skipped in computing a stable hash. For instance, circular references to objects or irrelevant data. \"\"\" # Note: this routine is slow and would stand to be implemented in C. hashobj = hashlib . md5 () for attr_name , attr_value in zip ( objtuple . _fields , objtuple ): if attr_name in ignore : continue if isinstance ( attr_value , ( list , set , frozenset )): subhashes = [] for element in attr_value : if isinstance ( element , tuple ): subhashes . append ( stable_hash_namedtuple ( element , ignore )) else : md5 = hashlib . md5 () md5 . update ( str ( element ) . encode ()) subhashes . append ( md5 . hexdigest ()) for subhash in sorted ( subhashes ): hashobj . update ( subhash . encode ()) else : hashobj . update ( str ( attr_value ) . encode ()) return hashobj . hexdigest () beancount.core.convert \uf0c1 Conversions from Position (or Posting) to units, cost, weight, market value. Units: Just the primary amount of the position. Cost: The cost basis of the position, if available. Weight: The cost basis or price of the position. Market Value: The units converted to a value via a price map. To convert an inventory's contents, simply use these functions in conjunction with Inventory.reduce() , like cost_inv = inv.reduce(convert.get_cost) This module equivalently converts Position and Posting instances. Note that we're specifically avoiding to create an import dependency on beancount.core.data in order to keep this module isolatable, but it works on postings due to duck-typing. Function named get_*() are used to compute values from postings to their price currency. Functions named convert_*() are used to convert postings and amounts to any currency. beancount . core . convert . convert_amount ( amt , target_currency , price_map , date = None , via = None ) \uf0c1 Return the market value of an Amount in a particular currency. In addition, if a conversion rate isn't available, you can provide a list of currencies to attempt to synthesize a rate for via implied rates. Parameters: amt \u2013 An instance of Amount. target_currency \u2013 The target currency to convert to. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance to evaluate the value at, or None. via \u2013 A list of currencies to attempt to synthesize an implied rate if the direct conversion fails. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, the amount itself, unmodified. Source code in beancount/core/convert.py def convert_amount ( amt , target_currency , price_map , date = None , via = None ): \"\"\"Return the market value of an Amount in a particular currency. In addition, if a conversion rate isn't available, you can provide a list of currencies to attempt to synthesize a rate for via implied rates. Args: amt: An instance of Amount. target_currency: The target currency to convert to. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. via: A list of currencies to attempt to synthesize an implied rate if the direct conversion fails. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, the amount itself, unmodified. \"\"\" # First, attempt to convert directly. This should be the most # straightforward conversion. base_quote = ( amt . currency , target_currency ) _ , rate = prices . get_price ( price_map , base_quote , date ) if rate is not None : # On success, just make the conversion directly. return Amount ( amt . number * rate , target_currency ) elif via : assert isinstance ( via , ( tuple , list )) # A price is unavailable, attempt to convert via cost/price currency # hop, if the value currency isn't the target currency. for implied_currency in via : if implied_currency == target_currency : continue base_quote1 = ( amt . currency , implied_currency ) _ , rate1 = prices . get_price ( price_map , base_quote1 , date ) if rate1 is not None : base_quote2 = ( implied_currency , target_currency ) _ , rate2 = prices . get_price ( price_map , base_quote2 , date ) if rate2 is not None : return Amount ( amt . number * rate1 * rate2 , target_currency ) # We failed to infer a conversion rate; return the amt. return amt beancount . core . convert . convert_position ( pos , target_currency , price_map , date = None ) \uf0c1 Return the market value of a Position or Posting in a particular currency. In addition, if the rate from the position's currency to target_currency isn't available, an attempt is made to convert from its cost currency, if one is available. Parameters: pos \u2013 An instance of Position or Posting, equivalently. target_currency \u2013 The target currency to convert to. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance to evaluate the value at, or None. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. (See get_value() above for details.) Source code in beancount/core/convert.py def convert_position ( pos , target_currency , price_map , date = None ): \"\"\"Return the market value of a Position or Posting in a particular currency. In addition, if the rate from the position's currency to target_currency isn't available, an attempt is made to convert from its cost currency, if one is available. Args: pos: An instance of Position or Posting, equivalently. target_currency: The target currency to convert to. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. (See get_value() above for details.) \"\"\" cost = pos . cost value_currency = ( ( isinstance ( cost , Cost ) and cost . currency ) or ( hasattr ( pos , \"price\" ) and pos . price and pos . price . currency ) or None ) return convert_amount ( pos . units , target_currency , price_map , date = date , via = ( value_currency ,) ) beancount . core . convert . get_cost ( pos ) \uf0c1 Return the total cost of a Position or Posting. Parameters: pos \u2013 An instance of Position or Posting, equivalently. Returns: An Amount. Source code in beancount/core/convert.py def get_cost ( pos ): \"\"\"Return the total cost of a Position or Posting. Args: pos: An instance of Position or Posting, equivalently. Returns: An Amount. \"\"\" assert isinstance ( pos , Position ) or type ( pos ) . __name__ == \"Posting\" cost = pos . cost return ( Amount ( cost . number * pos . units . number , cost . currency ) if ( isinstance ( cost , Cost ) and isinstance ( cost . number , Decimal )) else pos . units ) beancount . core . convert . get_units ( pos ) \uf0c1 Return the units of a Position or Posting. Parameters: pos \u2013 An instance of Position or Posting, equivalently. Returns: An Amount. Source code in beancount/core/convert.py def get_units ( pos ): \"\"\"Return the units of a Position or Posting. Args: pos: An instance of Position or Posting, equivalently. Returns: An Amount. \"\"\" assert isinstance ( pos , Position ) or type ( pos ) . __name__ == \"Posting\" return pos . units beancount . core . convert . get_value ( pos , price_map , date = None , output_date_prices = None ) \uf0c1 Return the market value of a Position or Posting. Note that if the position is not held at cost, this does not convert anything, even if a price is available in the 'price_map'. We don't specify a target currency here. If you're attempting to make such a conversion, see convert_*() functions below. However, is the object is a posting and it has a price, we will use that price to infer the target currency and those will be converted. Parameters: pos \u2013 An instance of Position or Posting, equivalently. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance to evaluate the value at, or None. output_date_prices \u2013 An optional output list of (date, price). If this list is provided, it will be appended to (mutated) to output the price pulled in making the conversions. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. This is designed so that you could reduce an inventory with this and not lose any information silently in case of failure to convert (possibly due to an empty price map). Compare the returned currency to that of the input position if you need to check for success. Source code in beancount/core/convert.py def get_value ( pos , price_map , date = None , output_date_prices = None ): \"\"\"Return the market value of a Position or Posting. Note that if the position is not held at cost, this does not convert anything, even if a price is available in the 'price_map'. We don't specify a target currency here. If you're attempting to make such a conversion, see ``convert_*()`` functions below. However, is the object is a posting and it has a price, we will use that price to infer the target currency and those will be converted. Args: pos: An instance of Position or Posting, equivalently. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. output_date_prices: An optional output list of (date, price). If this list is provided, it will be appended to (mutated) to output the price pulled in making the conversions. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. This is designed so that you could reduce an inventory with this and not lose any information silently in case of failure to convert (possibly due to an empty price map). Compare the returned currency to that of the input position if you need to check for success. \"\"\" assert isinstance ( pos , Position ) or type ( pos ) . __name__ == \"Posting\" units = pos . units cost = pos . cost # Try to infer what the cost/price currency should be. value_currency = ( ( isinstance ( cost , Cost ) and cost . currency ) or ( hasattr ( pos , \"price\" ) and pos . price and pos . price . currency ) or None ) if isinstance ( value_currency , str ): # We have a value currency; hit the price database. base_quote = ( units . currency , value_currency ) price_date , price_number = prices . get_price ( price_map , base_quote , date ) if output_date_prices is not None : output_date_prices . append (( price_date , price_number )) if price_number is not None : return Amount ( units . number * price_number , value_currency ) # We failed to infer a conversion rate; return the units. return units beancount . core . convert . get_weight ( pos ) \uf0c1 Return the weight of a Position or Posting. This is the amount that will need to be balanced from a posting of a transaction. This is a key element of the semantics of transactions in this software. A balance amount is the amount used to check the balance of a transaction. Here are all relevant examples, with the amounts used to balance the postings: Assets:Account 5234.50 USD -> 5234.50 USD Assets:Account 3877.41 EUR @ 1.35 USD -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} @ 545.60 CAD -> 5234.50 USD Parameters: pos \u2013 An instance of Position or Posting, equivalently. Returns: An Amount. Source code in beancount/core/convert.py def get_weight ( pos ): \"\"\"Return the weight of a Position or Posting. This is the amount that will need to be balanced from a posting of a transaction. This is a *key* element of the semantics of transactions in this software. A balance amount is the amount used to check the balance of a transaction. Here are all relevant examples, with the amounts used to balance the postings: Assets:Account 5234.50 USD -> 5234.50 USD Assets:Account 3877.41 EUR @ 1.35 USD -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} @ 545.60 CAD -> 5234.50 USD Args: pos: An instance of Position or Posting, equivalently. Returns: An Amount. \"\"\" assert isinstance ( pos , Position ) or type ( pos ) . __name__ == \"Posting\" units = pos . units cost = pos . cost # It the object has a cost, use that as the weight, to balance. if isinstance ( cost , Cost ) and isinstance ( cost . number , Decimal ): weight = Amount ( cost . number * pos . units . number , cost . currency ) else : # Otherwise use the postings. weight = units # Unless there is a price available; use that if present. if not isinstance ( pos , Position ): price = pos . price if price is not None : # Note: Here we could assert that price.currency == units.currency. if price . number is MISSING or units . number is MISSING : converted_number = MISSING else : converted_number = price . number * units . number weight = Amount ( converted_number , price . currency ) return weight beancount.core.data \uf0c1 Basic data structures used to represent the Ledger entries. beancount.core.data.Balance ( tuple ) \uf0c1 A \"check the balance of this account\" directive. This directive asserts that the declared account should have a known number of units of a particular currency at the beginning of its date. This is essentially an assertion, and corresponds to the final \"Statement Balance\" line of a real-world statement. These assertions act as checkpoints to help ensure that you have entered your transactions correctly. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the account whose balance to check at the given date. amount Amount An Amount, the number of units of the given currency you're expecting 'account' to have at this date. diff_amount Optional[beancount.core.amount.Amount] None if the balance check succeeds. This value is set to an Amount instance if the balance fails, the amount of the difference. tolerance Optional[decimal.Decimal] A Decimal object, the amount of tolerance to use in the verification. beancount . core . data . Balance . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Balance . __new__ ( _cls , meta , date , account , amount , tolerance , diff_amount ) special staticmethod \uf0c1 Create new instance of Balance(meta, date, account, amount, tolerance, diff_amount) beancount . core . data . Balance . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.Close ( tuple ) \uf0c1 A \"close account\" directive. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the name of the account that is being closed. beancount . core . data . Close . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Close . __new__ ( _cls , meta , date , account ) special staticmethod \uf0c1 Create new instance of Close(meta, date, account) beancount . core . data . Close . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.Commodity ( tuple ) \uf0c1 An optional commodity declaration directive. Commodities generally do not need to be declared, but they may, and this is mainly created as intended to be used to attach meta-data on a commodity name. Whenever a plugin needs per-commodity meta-data, you would define such a commodity directive. Another use is to define a commodity that isn't otherwise (yet) used anywhere in an input file. (At the moment the date is meaningless but is specified for coherence with all the other directives; if you can think of a good use case, let us know). Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. currency str A string, the commodity under consideration. beancount . core . data . Commodity . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Commodity . __new__ ( _cls , meta , date , currency ) special staticmethod \uf0c1 Create new instance of Commodity(meta, date, currency) beancount . core . data . Commodity . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.Custom ( tuple ) \uf0c1 A custom directive. This directive can be used to implement new experimental dated features in the Beancount file. This is meant as an intermediate measure to be used when you would need to implement a new directive in a plugin. These directives will be parsed liberally... any list of tokens are supported. All that is required is some unique name for them that acts as a \"type\". These directives are included in the stream and a plugin should be able to gather them. Attributes: Name Type Description meta Dict[str, Any] See above. type str A string that represents the type of the directive. values List A list of values of various simple types supported by the grammar. (Note that this list is not enforced to be consistent for all directives of the same type by the parser.) beancount . core . data . Custom . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Custom . __new__ ( _cls , meta , date , type , values ) special staticmethod \uf0c1 Create new instance of Custom(meta, date, type, values) beancount . core . data . Custom . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.Document ( tuple ) \uf0c1 A document file declaration directive. This directive is used to attach a statement to an account, at a particular date. A typical usage would be to render PDF files or scans of your bank statements into the account's journal. While you can explicitly create those directives in the input syntax, it is much more convenient to provide Beancount with a root directory to search for filenames in a hierarchy mirroring the chart of accounts, filenames which should match the following dated format: \"YYYY-MM-DD.*\". See options for detail. Beancount will automatically create these documents directives based on the file hierarchy, and you can get them by parsing the list of entries. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the account which the statement or document is associated with. filename str The absolute filename of the document file. tags Optional[Set] A set of tag strings (without the '#'), or None, if an empty set. links Optional[Set] A set of link strings (without the '^'), or None, if an empty set. beancount . core . data . Document . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Document . __new__ ( _cls , meta , date , account , filename , tags , links ) special staticmethod \uf0c1 Create new instance of Document(meta, date, account, filename, tags, links) beancount . core . data . Document . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.Event ( tuple ) \uf0c1 An \"event value change\" directive. These directives are used as string variables that have different values over time. You can use these to track an address, your location, your current employer, anything you like. The kind of reporting that is made of these generic events is based on days and a timeline. For instance, if you need to track the number of days you spend in each country or state, create a \"location\" event and whenever you travel, add an event directive to indicate its new value. You should be able to write simple scripts against those in order to compute if you were present somewhere for a particular number of days. Here's an illustrative example usage, in order to maintain your health insurance coverage in Canada, you need to be present in the country for 183 days or more, excluding trips of less than 30 days. There is a similar test to be done in the US by aliens to figure out if they need to be considered as residents for tax purposes (the so-called \"substantial presence test\"). By integrating these directives into your bookkeeping, you can easily have a little program that computes the tests for you. This is, of course, entirely optional and somewhat auxiliary to the main purpose of double-entry bookkeeping, but correlates strongly with the transactions you insert in it, and so it's a really convenient thing to have in the same input file. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. \"type\" A short string, typically a single lowercase word, that defines a unique variable whose value changes over time. For example, 'location'. description str A free-form string, the value of the variable as of the date of the transaction. beancount . core . data . Event . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Event . __new__ ( _cls , meta , date , type , description ) special staticmethod \uf0c1 Create new instance of Event(meta, date, type, description) beancount . core . data . Event . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.Note ( tuple ) \uf0c1 A note directive, a general note that is attached to an account. These are used to attach text at a particular date in a specific account. The notes can be anything; a typical use would be to jot down an answer from a phone call to the institution represented by the account. It should show up in an account's journal. If you don't want this rendered, use the comment syntax in the input file, which does not get parsed and stored. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the account which the note is to be attached to. This is never None, notes always have an account they correspond to. comment str A free-form string, the text of the note. This can be long if you want it to. beancount . core . data . Note . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Note . __new__ ( _cls , meta , date , account , comment , tags , links ) special staticmethod \uf0c1 Create new instance of Note(meta, date, account, comment, tags, links) beancount . core . data . Note . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.Open ( tuple ) \uf0c1 An \"open account\" directive. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the name of the account that is being opened. currencies List[str] A list of strings, currencies that are allowed in this account. May be None, in which case it means that there are no restrictions on which currencies may be stored in this account. booking Optional[beancount.core.data.Booking] A Booking enum, the booking method to use to disambiguate postings to this account (when zero or more than one postings match the specification), or None if not specified. In practice, this attribute will be should be left unspecified (None) in the vast majority of cases. See Booking below for a selection of valid methods. beancount . core . data . Open . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Open . __new__ ( _cls , meta , date , account , currencies , booking ) special staticmethod \uf0c1 Create new instance of Open(meta, date, account, currencies, booking) beancount . core . data . Open . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.Pad ( tuple ) \uf0c1 A \"pad this account with this other account\" directive. This directive automatically inserts transactions that will make the next chronological balance directive succeeds. It can be used to fill in missing date ranges of transactions, as a convenience. You don't have to use this, it's sugar coating in case you need it, while you're entering past history into your Ledger. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the name of the account which needs to be filled. source_account str A string, the name of the account which is used to debit from in order to fill 'account'. beancount . core . data . Pad . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Pad . __new__ ( _cls , meta , date , account , source_account ) special staticmethod \uf0c1 Create new instance of Pad(meta, date, account, source_account) beancount . core . data . Pad . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.Posting ( tuple ) \uf0c1 Postings are contained in Transaction entries. These represent the individual legs of a transaction. Note: a posting may only appear within a single entry (multiple transactions may not share a Posting instance), and that's what the entry field should be set to. Attributes: Name Type Description account str A string, the account that is modified by this posting. units Optional[beancount.core.amount.Amount] An Amount, the units of the position, or None if it is to be inferred from the other postings in the transaction. cost Union[beancount.core.position.Cost, beancount.core.position.CostSpec] A Cost or CostSpec instances, the units of the position. price Optional[beancount.core.amount.Amount] An Amount, the price at which the position took place, or None, where not relevant. Providing a price member to a posting automatically adds a price in the prices database at the date of the transaction. flag Optional[str] An optional flag, a one-character string or None, which is to be associated with the posting. Most postings don't have a flag, but it can be convenient to mark a particular posting as problematic or pending to be reconciled for a future import of its account. meta Optional[Dict[str, Any]] A dict of strings to values, the metadata that was attached specifically to that posting, or None, if not provided. In practice, most of the instances will be unlikely to have metadata. beancount . core . data . Posting . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Posting . __new__ ( _cls , account , units , cost , price , flag , meta ) special staticmethod \uf0c1 Create new instance of Posting(account, units, cost, price, flag, meta) beancount . core . data . Posting . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.Price ( tuple ) \uf0c1 A price declaration directive. This establishes the price of a currency in terms of another currency as of the directive's date. A history of the prices for each currency pairs is built and can be queried within the bookkeeping system. Note that because Beancount does not store any data at time-of-day resolution, it makes no sense to have multiple price directives at the same date. (Beancount will not attempt to solve this problem; this is beyond the general scope of double-entry bookkeeping and if you need to build a day trading system, you should probably use something else). Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. currency: A string, the currency that is being priced, e.g. HOOL. amount: An instance of Amount, the number of units and currency that 'currency' is worth, for instance 1200.12 USD. beancount . core . data . Price . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Price . __new__ ( _cls , meta , date , currency , amount ) special staticmethod \uf0c1 Create new instance of Price(meta, date, currency, amount) beancount . core . data . Price . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.Query ( tuple ) \uf0c1 A named query declaration. This directive is used to create pre-canned queries that can then be automatically run or made available to the shell, or perhaps be rendered as part of a web interface. The purpose of this routine is to define useful queries for the context of the particular given Beancount input file. Attributes: Name Type Description meta Dict[str, Any] See above. date date The date at which this query should be run. All directives following this date will be ignored automatically. This is essentially equivalent to the CLOSE modifier in the shell syntax. name str A string, the unique identifier for the query. query_string str The SQL query string to be run or made available. beancount . core . data . Query . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Query . __new__ ( _cls , meta , date , name , query_string ) special staticmethod \uf0c1 Create new instance of Query(meta, date, name, query_string) beancount . core . data . Query . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.Transaction ( tuple ) \uf0c1 A transaction! This is the main type of object that we manipulate, and the entire reason this whole project exists in the first place, because representing these types of structures with a spreadsheet is difficult. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. flag str A single-character string or None. This user-specified string represents some custom/user-defined state of the transaction. You can use this for various purposes. Otherwise common, pre-defined flags are defined under beancount.core.flags, to flags transactions that are automatically generated. payee Optional[str] A free-form string that identifies the payee, or None, if absent. narration str A free-form string that provides a description for the transaction. All transactions have at least a narration string, this is never None. tags FrozenSet A set of tag strings (without the '#'), or EMPTY_SET. links FrozenSet A set of link strings (without the '^'), or EMPTY_SET. postings List[beancount.core.data.Posting] A list of Posting instances, the legs of this transaction. See the doc under Posting above. beancount . core . data . Transaction . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . Transaction . __new__ ( _cls , meta , date , flag , payee , narration , tags , links , postings ) special staticmethod \uf0c1 Create new instance of Transaction(meta, date, flag, payee, narration, tags, links, postings) beancount . core . data . Transaction . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.TxnPosting ( tuple ) \uf0c1 A pair of a Posting and its parent Transaction. This is inserted as temporaries in lists of postings-of-entries, which is the product of a realization. Attributes: Name Type Description txn Transaction The parent Transaction instance. posting Posting The Posting instance. beancount . core . data . TxnPosting . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . TxnPosting . __new__ ( _cls , txn , posting ) special staticmethod \uf0c1 Create new instance of TxnPosting(txn, posting) beancount . core . data . TxnPosting . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.dtypes \uf0c1 Types of directives. beancount.core.data.dtypes.Balance ( tuple ) \uf0c1 A \"check the balance of this account\" directive. This directive asserts that the declared account should have a known number of units of a particular currency at the beginning of its date. This is essentially an assertion, and corresponds to the final \"Statement Balance\" line of a real-world statement. These assertions act as checkpoints to help ensure that you have entered your transactions correctly. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the account whose balance to check at the given date. amount Amount An Amount, the number of units of the given currency you're expecting 'account' to have at this date. diff_amount Optional[beancount.core.amount.Amount] None if the balance check succeeds. This value is set to an Amount instance if the balance fails, the amount of the difference. tolerance Optional[decimal.Decimal] A Decimal object, the amount of tolerance to use in the verification. beancount . core . data . dtypes . Balance . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . dtypes . Balance . __new__ ( _cls , meta , date , account , amount , tolerance , diff_amount ) special staticmethod \uf0c1 Create new instance of Balance(meta, date, account, amount, tolerance, diff_amount) beancount . core . data . dtypes . Balance . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.dtypes.Close ( tuple ) \uf0c1 A \"close account\" directive. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the name of the account that is being closed. beancount . core . data . dtypes . Close . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . dtypes . Close . __new__ ( _cls , meta , date , account ) special staticmethod \uf0c1 Create new instance of Close(meta, date, account) beancount . core . data . dtypes . Close . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.dtypes.Commodity ( tuple ) \uf0c1 An optional commodity declaration directive. Commodities generally do not need to be declared, but they may, and this is mainly created as intended to be used to attach meta-data on a commodity name. Whenever a plugin needs per-commodity meta-data, you would define such a commodity directive. Another use is to define a commodity that isn't otherwise (yet) used anywhere in an input file. (At the moment the date is meaningless but is specified for coherence with all the other directives; if you can think of a good use case, let us know). Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. currency str A string, the commodity under consideration. beancount . core . data . dtypes . Commodity . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . dtypes . Commodity . __new__ ( _cls , meta , date , currency ) special staticmethod \uf0c1 Create new instance of Commodity(meta, date, currency) beancount . core . data . dtypes . Commodity . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.dtypes.Custom ( tuple ) \uf0c1 A custom directive. This directive can be used to implement new experimental dated features in the Beancount file. This is meant as an intermediate measure to be used when you would need to implement a new directive in a plugin. These directives will be parsed liberally... any list of tokens are supported. All that is required is some unique name for them that acts as a \"type\". These directives are included in the stream and a plugin should be able to gather them. Attributes: Name Type Description meta Dict[str, Any] See above. type str A string that represents the type of the directive. values List A list of values of various simple types supported by the grammar. (Note that this list is not enforced to be consistent for all directives of the same type by the parser.) beancount . core . data . dtypes . Custom . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . dtypes . Custom . __new__ ( _cls , meta , date , type , values ) special staticmethod \uf0c1 Create new instance of Custom(meta, date, type, values) beancount . core . data . dtypes . Custom . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.dtypes.Document ( tuple ) \uf0c1 A document file declaration directive. This directive is used to attach a statement to an account, at a particular date. A typical usage would be to render PDF files or scans of your bank statements into the account's journal. While you can explicitly create those directives in the input syntax, it is much more convenient to provide Beancount with a root directory to search for filenames in a hierarchy mirroring the chart of accounts, filenames which should match the following dated format: \"YYYY-MM-DD.*\". See options for detail. Beancount will automatically create these documents directives based on the file hierarchy, and you can get them by parsing the list of entries. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the account which the statement or document is associated with. filename str The absolute filename of the document file. tags Optional[Set] A set of tag strings (without the '#'), or None, if an empty set. links Optional[Set] A set of link strings (without the '^'), or None, if an empty set. beancount . core . data . dtypes . Document . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . dtypes . Document . __new__ ( _cls , meta , date , account , filename , tags , links ) special staticmethod \uf0c1 Create new instance of Document(meta, date, account, filename, tags, links) beancount . core . data . dtypes . Document . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.dtypes.Event ( tuple ) \uf0c1 An \"event value change\" directive. These directives are used as string variables that have different values over time. You can use these to track an address, your location, your current employer, anything you like. The kind of reporting that is made of these generic events is based on days and a timeline. For instance, if you need to track the number of days you spend in each country or state, create a \"location\" event and whenever you travel, add an event directive to indicate its new value. You should be able to write simple scripts against those in order to compute if you were present somewhere for a particular number of days. Here's an illustrative example usage, in order to maintain your health insurance coverage in Canada, you need to be present in the country for 183 days or more, excluding trips of less than 30 days. There is a similar test to be done in the US by aliens to figure out if they need to be considered as residents for tax purposes (the so-called \"substantial presence test\"). By integrating these directives into your bookkeeping, you can easily have a little program that computes the tests for you. This is, of course, entirely optional and somewhat auxiliary to the main purpose of double-entry bookkeeping, but correlates strongly with the transactions you insert in it, and so it's a really convenient thing to have in the same input file. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. \"type\" A short string, typically a single lowercase word, that defines a unique variable whose value changes over time. For example, 'location'. description str A free-form string, the value of the variable as of the date of the transaction. beancount . core . data . dtypes . Event . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . dtypes . Event . __new__ ( _cls , meta , date , type , description ) special staticmethod \uf0c1 Create new instance of Event(meta, date, type, description) beancount . core . data . dtypes . Event . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.dtypes.Note ( tuple ) \uf0c1 A note directive, a general note that is attached to an account. These are used to attach text at a particular date in a specific account. The notes can be anything; a typical use would be to jot down an answer from a phone call to the institution represented by the account. It should show up in an account's journal. If you don't want this rendered, use the comment syntax in the input file, which does not get parsed and stored. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the account which the note is to be attached to. This is never None, notes always have an account they correspond to. comment str A free-form string, the text of the note. This can be long if you want it to. beancount . core . data . dtypes . Note . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . dtypes . Note . __new__ ( _cls , meta , date , account , comment , tags , links ) special staticmethod \uf0c1 Create new instance of Note(meta, date, account, comment, tags, links) beancount . core . data . dtypes . Note . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.dtypes.Open ( tuple ) \uf0c1 An \"open account\" directive. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the name of the account that is being opened. currencies List[str] A list of strings, currencies that are allowed in this account. May be None, in which case it means that there are no restrictions on which currencies may be stored in this account. booking Optional[beancount.core.data.Booking] A Booking enum, the booking method to use to disambiguate postings to this account (when zero or more than one postings match the specification), or None if not specified. In practice, this attribute will be should be left unspecified (None) in the vast majority of cases. See Booking below for a selection of valid methods. beancount . core . data . dtypes . Open . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . dtypes . Open . __new__ ( _cls , meta , date , account , currencies , booking ) special staticmethod \uf0c1 Create new instance of Open(meta, date, account, currencies, booking) beancount . core . data . dtypes . Open . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.dtypes.Pad ( tuple ) \uf0c1 A \"pad this account with this other account\" directive. This directive automatically inserts transactions that will make the next chronological balance directive succeeds. It can be used to fill in missing date ranges of transactions, as a convenience. You don't have to use this, it's sugar coating in case you need it, while you're entering past history into your Ledger. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the name of the account which needs to be filled. source_account str A string, the name of the account which is used to debit from in order to fill 'account'. beancount . core . data . dtypes . Pad . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . dtypes . Pad . __new__ ( _cls , meta , date , account , source_account ) special staticmethod \uf0c1 Create new instance of Pad(meta, date, account, source_account) beancount . core . data . dtypes . Pad . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.dtypes.Price ( tuple ) \uf0c1 A price declaration directive. This establishes the price of a currency in terms of another currency as of the directive's date. A history of the prices for each currency pairs is built and can be queried within the bookkeeping system. Note that because Beancount does not store any data at time-of-day resolution, it makes no sense to have multiple price directives at the same date. (Beancount will not attempt to solve this problem; this is beyond the general scope of double-entry bookkeeping and if you need to build a day trading system, you should probably use something else). Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. currency: A string, the currency that is being priced, e.g. HOOL. amount: An instance of Amount, the number of units and currency that 'currency' is worth, for instance 1200.12 USD. beancount . core . data . dtypes . Price . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . dtypes . Price . __new__ ( _cls , meta , date , currency , amount ) special staticmethod \uf0c1 Create new instance of Price(meta, date, currency, amount) beancount . core . data . dtypes . Price . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.dtypes.Query ( tuple ) \uf0c1 A named query declaration. This directive is used to create pre-canned queries that can then be automatically run or made available to the shell, or perhaps be rendered as part of a web interface. The purpose of this routine is to define useful queries for the context of the particular given Beancount input file. Attributes: Name Type Description meta Dict[str, Any] See above. date date The date at which this query should be run. All directives following this date will be ignored automatically. This is essentially equivalent to the CLOSE modifier in the shell syntax. name str A string, the unique identifier for the query. query_string str The SQL query string to be run or made available. beancount . core . data . dtypes . Query . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . dtypes . Query . __new__ ( _cls , meta , date , name , query_string ) special staticmethod \uf0c1 Create new instance of Query(meta, date, name, query_string) beancount . core . data . dtypes . Query . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.data.dtypes.Transaction ( tuple ) \uf0c1 A transaction! This is the main type of object that we manipulate, and the entire reason this whole project exists in the first place, because representing these types of structures with a spreadsheet is difficult. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. flag str A single-character string or None. This user-specified string represents some custom/user-defined state of the transaction. You can use this for various purposes. Otherwise common, pre-defined flags are defined under beancount.core.flags, to flags transactions that are automatically generated. payee Optional[str] A free-form string that identifies the payee, or None, if absent. narration str A free-form string that provides a description for the transaction. All transactions have at least a narration string, this is never None. tags FrozenSet A set of tag strings (without the '#'), or EMPTY_SET. links FrozenSet A set of link strings (without the '^'), or EMPTY_SET. postings List[beancount.core.data.Posting] A list of Posting instances, the legs of this transaction. See the doc under Posting above. beancount . core . data . dtypes . Transaction . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . data . dtypes . Transaction . __new__ ( _cls , meta , date , flag , payee , narration , tags , links , postings ) special staticmethod \uf0c1 Create new instance of Transaction(meta, date, flag, payee, narration, tags, links, postings) beancount . core . data . dtypes . Transaction . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . core . data . create_simple_posting ( entry , account , number , currency ) \uf0c1 Create a simple posting on the entry, with just a number and currency (no cost). Parameters: entry \u2013 The entry instance to add the posting to. account \u2013 A string, the account to use on the posting. number \u2013 A Decimal number or string to use in the posting's Amount. currency \u2013 A string, the currency for the Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. Source code in beancount/core/data.py def create_simple_posting ( entry , account , number , currency ): \"\"\"Create a simple posting on the entry, with just a number and currency (no cost). Args: entry: The entry instance to add the posting to. account: A string, the account to use on the posting. number: A Decimal number or string to use in the posting's Amount. currency: A string, the currency for the Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. \"\"\" if isinstance ( account , str ): pass if number is None : units = None else : if not isinstance ( number , Decimal ): number = D ( number ) units = Amount ( number , currency ) posting = Posting ( account , units , None , None , None , None ) if entry is not None : entry . postings . append ( posting ) return posting beancount . core . data . create_simple_posting_with_cost ( entry , account , number , currency , cost_number , cost_currency ) \uf0c1 Create a simple posting on the entry, with just a number and currency (no cost). Parameters: entry \u2013 The entry instance to add the posting to. account \u2013 A string, the account to use on the posting. number \u2013 A Decimal number or string to use in the posting's Amount. currency \u2013 A string, the currency for the Amount. cost_number \u2013 A Decimal number or string to use for the posting's cost Amount. cost_currency \u2013 a string, the currency for the cost Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. Source code in beancount/core/data.py def create_simple_posting_with_cost ( entry , account , number , currency , cost_number , cost_currency ): \"\"\"Create a simple posting on the entry, with just a number and currency (no cost). Args: entry: The entry instance to add the posting to. account: A string, the account to use on the posting. number: A Decimal number or string to use in the posting's Amount. currency: A string, the currency for the Amount. cost_number: A Decimal number or string to use for the posting's cost Amount. cost_currency: a string, the currency for the cost Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. \"\"\" if isinstance ( account , str ): pass if not isinstance ( number , Decimal ): number = D ( number ) if cost_number and not isinstance ( cost_number , Decimal ): cost_number = D ( cost_number ) units = Amount ( number , currency ) cost = Cost ( cost_number , cost_currency , None , None ) posting = Posting ( account , units , cost , None , None , None ) if entry is not None : entry . postings . append ( posting ) return posting beancount . core . data . entry_sortkey ( entry ) \uf0c1 Sort-key for entries. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Parameters: entry \u2013 An entry instance. Returns: A tuple of (date, integer, integer), that forms the sort key for the entry. Source code in beancount/core/data.py def entry_sortkey ( entry ): \"\"\"Sort-key for entries. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Args: entry: An entry instance. Returns: A tuple of (date, integer, integer), that forms the sort key for the entry. \"\"\" return ( entry . date , SORT_ORDER . get ( type ( entry ), 0 ), entry . meta [ \"lineno\" ]) beancount . core . data . filter_txns ( entries ) \uf0c1 A generator that yields only the Transaction instances. This is such an incredibly common operation that it deserves a terse filtering mechanism. Parameters: entries \u2013 A list of directives. Yields: A sorted list of only the Transaction directives. Source code in beancount/core/data.py def filter_txns ( entries ): \"\"\"A generator that yields only the Transaction instances. This is such an incredibly common operation that it deserves a terse filtering mechanism. Args: entries: A list of directives. Yields: A sorted list of only the Transaction directives. \"\"\" for entry in entries : if isinstance ( entry , Transaction ): yield entry beancount . core . data . find_closest ( entries , filename , lineno ) \uf0c1 Find the closest entry from entries to (filename, lineno). Parameters: entries \u2013 A list of directives. filename \u2013 A string, the name of the ledger file to look for. Be careful to provide the very same filename, and note that the parser stores the absolute path of the filename here. lineno \u2013 An integer, the line number closest after the directive we're looking for. This may be the exact/first line of the directive. Returns: The closest entry found in the given file for the given filename, or None, if none could be found. Source code in beancount/core/data.py def find_closest ( entries , filename , lineno ): \"\"\"Find the closest entry from entries to (filename, lineno). Args: entries: A list of directives. filename: A string, the name of the ledger file to look for. Be careful to provide the very same filename, and note that the parser stores the absolute path of the filename here. lineno: An integer, the line number closest after the directive we're looking for. This may be the exact/first line of the directive. Returns: The closest entry found in the given file for the given filename, or None, if none could be found. \"\"\" min_diffline = sys . maxsize closest_entry = None for entry in entries : emeta = entry . meta if emeta [ \"filename\" ] == filename and emeta [ \"lineno\" ] > 0 : diffline = lineno - emeta [ \"lineno\" ] if 0 <= diffline < min_diffline : min_diffline = diffline closest_entry = entry return closest_entry beancount . core . data . get_entry ( posting_or_entry ) \uf0c1 Return the entry associated with the posting or entry. Parameters: entry \u2013 A TxnPosting or entry instance Returns: A datetime instance. Source code in beancount/core/data.py def get_entry ( posting_or_entry ): \"\"\"Return the entry associated with the posting or entry. Args: entry: A TxnPosting or entry instance Returns: A datetime instance. \"\"\" return ( posting_or_entry . txn if isinstance ( posting_or_entry , TxnPosting ) else posting_or_entry ) beancount . core . data . has_entry_account_component ( entry , component ) \uf0c1 Return true if one of the entry's postings has an account component. Parameters: entry \u2013 A Transaction entry. component \u2013 A string, a component of an account name. For instance, Food in Expenses:Food:Restaurant . All components are considered. Returns: Boolean \u2013 true if the component is in the account. Note that a component name must be whole, that is NY is not in Expenses:Taxes:StateNY . Source code in beancount/core/data.py def has_entry_account_component ( entry , component ): \"\"\"Return true if one of the entry's postings has an account component. Args: entry: A Transaction entry. component: A string, a component of an account name. For instance, ``Food`` in ``Expenses:Food:Restaurant``. All components are considered. Returns: Boolean: true if the component is in the account. Note that a component name must be whole, that is ``NY`` is not in ``Expenses:Taxes:StateNY``. \"\"\" return isinstance ( entry , Transaction ) and any ( has_component ( posting . account , component ) for posting in entry . postings ) beancount . core . data . iter_entry_dates ( entries , date_begin , date_end ) \uf0c1 Iterate over the entries in a date window. Parameters: entries \u2013 A date-sorted list of dated directives. date_begin \u2013 A datetime.date instance, the first date to include. date_end \u2013 A datetime.date instance, one day beyond the last date. Yields: Instances of the dated directives, between the dates, and in the order in which they appear. Source code in beancount/core/data.py def iter_entry_dates ( entries , date_begin , date_end ): \"\"\"Iterate over the entries in a date window. Args: entries: A date-sorted list of dated directives. date_begin: A datetime.date instance, the first date to include. date_end: A datetime.date instance, one day beyond the last date. Yields: Instances of the dated directives, between the dates, and in the order in which they appear. \"\"\" getdate = lambda entry : entry . date index_begin = bisect_left_with_key ( entries , date_begin , key = getdate ) index_end = bisect_left_with_key ( entries , date_end , key = getdate ) for index in range ( index_begin , index_end ): yield entries [ index ] beancount . core . data . new_metadata ( filename , lineno , kvlist = None ) \uf0c1 Create a new metadata container from the filename and line number. Parameters: filename \u2013 A string, the filename for the creator of this directive. lineno \u2013 An integer, the line number where the directive has been created. kvlist \u2013 An optional container of key-values. Returns: A metadata dict. Source code in beancount/core/data.py def new_metadata ( filename , lineno , kvlist = None ): \"\"\"Create a new metadata container from the filename and line number. Args: filename: A string, the filename for the creator of this directive. lineno: An integer, the line number where the directive has been created. kvlist: An optional container of key-values. Returns: A metadata dict. \"\"\" meta = { \"filename\" : filename , \"lineno\" : lineno } if kvlist : meta . update ( kvlist ) return meta beancount . core . data . posting_has_conversion ( posting ) \uf0c1 Return true if this position involves a conversion. A conversion is when there is a price attached to the amount but no cost. This is used on transactions to convert between units. Parameters: posting \u2013 an instance of Posting Returns: A boolean, true if this posting has a price conversion. Source code in beancount/core/data.py def posting_has_conversion ( posting ): \"\"\"Return true if this position involves a conversion. A conversion is when there is a price attached to the amount but no cost. This is used on transactions to convert between units. Args: posting: an instance of Posting Return: A boolean, true if this posting has a price conversion. \"\"\" return posting . cost is None and posting . price is not None beancount . core . data . posting_sortkey ( entry ) \uf0c1 Sort-key for entries or postings. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Parameters: entry \u2013 A Posting or entry instance Returns: A tuple of (date, integer, integer), that forms the sort key for the posting or entry. Source code in beancount/core/data.py def posting_sortkey ( entry ): \"\"\"Sort-key for entries or postings. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Args: entry: A Posting or entry instance Returns: A tuple of (date, integer, integer), that forms the sort key for the posting or entry. \"\"\" if isinstance ( entry , TxnPosting ): entry = entry . txn return ( entry . date , SORT_ORDER . get ( type ( entry ), 0 ), entry . meta [ \"lineno\" ]) beancount . core . data . remove_account_postings ( account , entries ) \uf0c1 Remove all postings with the given account. Parameters: account \u2013 A string, the account name whose postings we want to remove. Returns: A list of entries without the rounding postings. Source code in beancount/core/data.py def remove_account_postings ( account , entries ): \"\"\"Remove all postings with the given account. Args: account: A string, the account name whose postings we want to remove. Returns: A list of entries without the rounding postings. \"\"\" new_entries = [] for entry in entries : if isinstance ( entry , Transaction ) and ( any ( posting . account == account for posting in entry . postings ) ): entry = entry . _replace ( postings = [ posting for posting in entry . postings if posting . account != account ] ) new_entries . append ( entry ) return new_entries beancount . core . data . sanity_check_types ( entry , allow_none_for_tags_and_links = False ) \uf0c1 Check that the entry and its postings has all correct data types. Parameters: entry \u2013 An instance of one of the entries to be checked. allow_none_for_tags_and_links \u2013 A boolean, whether to allow plugins to generate Transaction objects with None as value for the 'tags' or 'links' attributes. Exceptions: AssertionError \u2013 If there is anything that is unexpected, raises an exception. Source code in beancount/core/data.py def sanity_check_types ( entry , allow_none_for_tags_and_links = False ): \"\"\"Check that the entry and its postings has all correct data types. Args: entry: An instance of one of the entries to be checked. allow_none_for_tags_and_links: A boolean, whether to allow plugins to generate Transaction objects with None as value for the 'tags' or 'links' attributes. Raises: AssertionError: If there is anything that is unexpected, raises an exception. \"\"\" assert isinstance ( entry , ALL_DIRECTIVES ), \"Invalid directive type\" assert isinstance ( entry . meta , dict ), \"Invalid type for meta\" assert \"filename\" in entry . meta , \"Missing filename in metadata\" assert \"lineno\" in entry . meta , \"Missing line number in metadata\" assert isinstance ( entry . date , datetime . date ), \"Invalid date type\" if isinstance ( entry , Transaction ): assert isinstance ( entry . flag , ( NoneType , str )), \"Invalid flag type\" assert isinstance ( entry . payee , ( NoneType , str )), \"Invalid payee type\" assert isinstance ( entry . narration , ( NoneType , str )), \"Invalid narration type\" set_types = ( ( NoneType , set , frozenset ) if allow_none_for_tags_and_links else ( set , frozenset ) ) assert isinstance ( entry . tags , set_types ), \"Invalid tags type: {} \" . format ( type ( entry . tags ) ) assert isinstance ( entry . links , set_types ), \"Invalid links type: {} \" . format ( type ( entry . links ) ) assert isinstance ( entry . postings , list ), \"Invalid postings list type\" for posting in entry . postings : assert isinstance ( posting , Posting ), \"Invalid posting type\" assert isinstance ( posting . account , str ), \"Invalid account type\" assert isinstance ( posting . units , ( Amount , NoneType )), \"Invalid units type\" assert isinstance ( posting . cost , ( Cost , CostSpec , NoneType )), \"Invalid cost type\" assert isinstance ( posting . price , ( Amount , NoneType )), \"Invalid price type\" assert isinstance ( posting . flag , ( str , NoneType )), \"Invalid flag type\" beancount . core . data . sorted ( entries ) \uf0c1 A convenience to sort a list of entries, using entry_sortkey(). Parameters: entries \u2013 A list of directives. Returns: A sorted list of directives. Source code in beancount/core/data.py def sorted ( entries ): \"\"\"A convenience to sort a list of entries, using entry_sortkey(). Args: entries: A list of directives. Returns: A sorted list of directives. \"\"\" return builtins . sorted ( entries , key = entry_sortkey ) beancount . core . data . transaction_has_conversion ( transaction ) \uf0c1 Given a Transaction entry, return true if at least one of the postings has a price conversion (without an associated cost). These are the source of non-zero conversion balances. Parameters: transaction \u2013 an instance of a Transaction entry. Returns: A boolean, true if this transaction contains at least one posting with a price conversion. Source code in beancount/core/data.py def transaction_has_conversion ( transaction ): \"\"\"Given a Transaction entry, return true if at least one of the postings has a price conversion (without an associated cost). These are the source of non-zero conversion balances. Args: transaction: an instance of a Transaction entry. Returns: A boolean, true if this transaction contains at least one posting with a price conversion. \"\"\" assert isinstance ( transaction , Transaction ), \"Invalid type of entry for transaction: {} \" . format ( transaction ) for posting in transaction . postings : if posting_has_conversion ( posting ): return True return False beancount.core.display_context \uf0c1 A settings class to offer control over the number of digits rendered. This module contains routines that can accumulate information on the width and precision of numbers to be rendered and derive the precision required to render all of them consistently and under certain common alignment requirements. This is required in order to output neatly lined up columns of numbers in various styles. A common case is that the precision can be observed for numbers present in the input file. This display precision can be used as the \"precision by default\" if we write a routine for which it is inconvenient to feed all the numbers to build such an accumulator. Here are all the aspects supported by this module: PRECISION: Numbers for a particular currency are always rendered to the same precision, and they can be rendered to one of two precisions; either the most common number of fractional digits, or the maximum number of digits seen (this is useful for rendering prices). ALIGNMENT: Several alignment methods are supported. \"natural\": Render the strings as small as possible with no padding, but to their currency's precision. Like this: '1.2345' '764' '-7,409.01' '0.00000125' \"dot-aligned\": The periods will align vertically, the left and right sides are padded so that the column of numbers has the same width: ' 1.2345 ' ' 764 ' '-7,409.01 ' ' 0.00000125' \"right\": The strings are all flushed right, the left side is padded so that the column of numbers has the same width: ' 1.2345' ' 764' ' -7,409.01' ' 0.00000125' SIGN: If a negative sign is present in the input numbers, the rendered numbers reserve a space for it. If not, then we save the space. COMMAS: If the user requests to render commas, commas are rendered in the output. RESERVED: A number of extra integral digits reserved on the left in order to allow rendering novel numbers that haven't yet been seen. For example, balances may contains much larger numbers than the numbers seen in input files, and these need to be accommodated when aligning to the right. beancount.core.display_context.Align ( Enum ) \uf0c1 Alignment style for numbers. beancount.core.display_context.DisplayContext \uf0c1 A builder object used to construct a DisplayContext from a series of numbers. Attributes: Name Type Description ccontexts A dict of currency string to CurrencyContext instance. commas A bool, true if we should render commas. This just gets propagated onwards as the default value of to build with. beancount . core . display_context . DisplayContext . build ( self , alignment =< Align . NATURAL : 1 > , precision =< Precision . MOST_COMMON : 1 > , commas = None , reserved = 0 ) \uf0c1 Build a formatter for the given display context. Parameters: alignment \u2013 The desired alignment. precision \u2013 The desired precision. commas \u2013 Whether to render commas or not. If 'None', the default value carried by the context will be used. reserved \u2013 An integer, the number of extra digits to be allocated in the maximum width calculations. Source code in beancount/core/display_context.py def build ( self , alignment = Align . NATURAL , precision = Precision . MOST_COMMON , commas = None , reserved = 0 , ): \"\"\"Build a formatter for the given display context. Args: alignment: The desired alignment. precision: The desired precision. commas: Whether to render commas or not. If 'None', the default value carried by the context will be used. reserved: An integer, the number of extra digits to be allocated in the maximum width calculations. \"\"\" if commas is None : commas = self . commas if alignment == Align . NATURAL : build_method = self . _build_natural elif alignment == Align . RIGHT : build_method = self . _build_right elif alignment == Align . DOT : build_method = self . _build_dot else : raise ValueError ( \"Unknown alignment: {} \" . format ( alignment )) fmtstrings = build_method ( precision , commas , reserved ) return DisplayFormatter ( self , precision , fmtstrings ) beancount . core . display_context . DisplayContext . quantize ( self , number , currency , precision =< Precision . MOST_COMMON : 1 > ) \uf0c1 Quantize the given number to the given precision. Parameters: number \u2013 A Decimal instance, the number to be quantized. currency \u2013 A currency string. precision \u2013 Which precision to use. Returns: A Decimal instance, the quantized number. Source code in beancount/core/display_context.py def quantize ( self , number , currency , precision = Precision . MOST_COMMON ): \"\"\"Quantize the given number to the given precision. Args: number: A Decimal instance, the number to be quantized. currency: A currency string. precision: Which precision to use. Returns: A Decimal instance, the quantized number. \"\"\" assert isinstance ( number , Decimal ), \"Invalid data: {} \" . format ( number ) ccontext = self . ccontexts [ currency ] num_fractional_digits = ccontext . get_fractional ( precision ) if num_fractional_digits is None : # Note: We could probably logging.warn() this situation here. return number qdigit = Decimal ( 1 ) . scaleb ( - num_fractional_digits ) with decimal . localcontext () as ctx : # Allow precision for numbers as large as 1 billion in addition to # the required number of fractional digits. # # TODO(blais): Review this to assess performance impact, and whether # we could fold this outside a calling loop. ctx . prec = num_fractional_digits + 9 return number . quantize ( qdigit ) beancount . core . display_context . DisplayContext . set_commas ( self , commas ) \uf0c1 Set the default value for rendering commas. Source code in beancount/core/display_context.py def set_commas ( self , commas ): \"\"\"Set the default value for rendering commas.\"\"\" self . commas = commas beancount . core . display_context . DisplayContext . update ( self , number , currency = '__default__' ) \uf0c1 Update the builder with the given number for the given currency. Parameters: number \u2013 An instance of Decimal to consider for this currency. currency \u2013 An optional string, the currency this numbers applies to. Source code in beancount/core/display_context.py def update ( self , number , currency = \"__default__\" ): \"\"\"Update the builder with the given number for the given currency. Args: number: An instance of Decimal to consider for this currency. currency: An optional string, the currency this numbers applies to. \"\"\" self . ccontexts [ currency ] . update ( number ) beancount . core . display_context . DisplayContext . update_from ( self , other ) \uf0c1 Update the builder with the other given DisplayContext. Parameters: other \u2013 Another DisplayContext. Source code in beancount/core/display_context.py def update_from ( self , other ): \"\"\"Update the builder with the other given DisplayContext. Args: other: Another DisplayContext. \"\"\" for currency , ccontext in other . ccontexts . items (): self . ccontexts [ currency ] . update_from ( ccontext ) beancount.core.display_context.DisplayFormatter \uf0c1 A class used to contain various settings that control how we output numbers. In particular, the precision used for each currency, and whether or not commas should be printed. This object is intended to be passed around to all functions that format numbers to strings. Attributes: Name Type Description dcontext A DisplayContext instance. precision An enum of Precision from which it was built. fmtstrings A dict of currency to pre-baked format strings for it. fmtfuncs A dict of currency to pre-baked formatting functions for it. beancount.core.display_context.Precision ( Enum ) \uf0c1 The type of precision required. beancount.core.distribution \uf0c1 A simple accumulator for data about a mathematical distribution. beancount.core.distribution.Distribution \uf0c1 A class that computes a histogram of integer values. This is used to compute a length that will cover at least some decent fraction of the samples. beancount . core . distribution . Distribution . empty ( self ) \uf0c1 Return true if the distribution is empty. Returns: A boolean. Source code in beancount/core/distribution.py def empty ( self ): \"\"\"Return true if the distribution is empty. Returns: A boolean. \"\"\" return len ( self . hist ) == 0 beancount . core . distribution . Distribution . max ( self ) \uf0c1 Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. Source code in beancount/core/distribution.py def max ( self ): \"\"\"Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. \"\"\" if not self . hist : return None value , _ = sorted ( self . hist . items ())[ - 1 ] return value beancount . core . distribution . Distribution . min ( self ) \uf0c1 Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. Source code in beancount/core/distribution.py def min ( self ): \"\"\"Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. \"\"\" if not self . hist : return None value , _ = sorted ( self . hist . items ())[ 0 ] return value beancount . core . distribution . Distribution . mode ( self ) \uf0c1 Return the mode of the distribution. Returns: An element of the value type, or None, if the distribution was empty. Source code in beancount/core/distribution.py def mode ( self ): \"\"\"Return the mode of the distribution. Returns: An element of the value type, or None, if the distribution was empty. \"\"\" if not self . hist : return None max_value = 0 max_count = 0 for value , count in sorted ( self . hist . items ()): if count >= max_count : max_count = count max_value = value return max_value beancount . core . distribution . Distribution . update ( self , value ) \uf0c1 Add a sample to the distribution. Parameters: value \u2013 A value of the function. Source code in beancount/core/distribution.py def update ( self , value ): \"\"\"Add a sample to the distribution. Args: value: A value of the function. \"\"\" self . hist [ value ] += 1 beancount . core . distribution . Distribution . update_from ( self , other ) \uf0c1 Add samples from the other distribution to this one. Parameters: other \u2013 Another distribution. Source code in beancount/core/distribution.py def update_from ( self , other ): \"\"\"Add samples from the other distribution to this one. Args: other: Another distribution. \"\"\" for value , count in other . hist . items (): self . hist [ value ] += count beancount.core.flags \uf0c1 Flag constants. beancount.core.getters \uf0c1 Getter functions that operate on lists of entries to return various lists of things that they reference, accounts, tags, links, currencies, etc. beancount.core.getters.GetAccounts \uf0c1 Accounts gatherer. beancount . core . getters . GetAccounts . Balance ( _ , entry ) \uf0c1 Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one ( _ , entry ): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return ( entry . account ,) beancount . core . getters . GetAccounts . Close ( _ , entry ) \uf0c1 Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one ( _ , entry ): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return ( entry . account ,) beancount . core . getters . GetAccounts . Commodity ( _ , entry ) \uf0c1 Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero ( _ , entry ): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return () beancount . core . getters . GetAccounts . Custom ( _ , entry ) \uf0c1 Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero ( _ , entry ): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return () beancount . core . getters . GetAccounts . Document ( _ , entry ) \uf0c1 Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one ( _ , entry ): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return ( entry . account ,) beancount . core . getters . GetAccounts . Event ( _ , entry ) \uf0c1 Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero ( _ , entry ): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return () beancount . core . getters . GetAccounts . Note ( _ , entry ) \uf0c1 Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one ( _ , entry ): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return ( entry . account ,) beancount . core . getters . GetAccounts . Open ( _ , entry ) \uf0c1 Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one ( _ , entry ): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return ( entry . account ,) beancount . core . getters . GetAccounts . Pad ( _ , entry ) \uf0c1 Process a Pad directive. Parameters: entry \u2013 An instance of Pad. Returns: The two accounts of the Pad directive. Source code in beancount/core/getters.py def Pad ( _ , entry ): \"\"\"Process a Pad directive. Args: entry: An instance of Pad. Returns: The two accounts of the Pad directive. \"\"\" return ( entry . account , entry . source_account ) beancount . core . getters . GetAccounts . Price ( _ , entry ) \uf0c1 Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero ( _ , entry ): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return () beancount . core . getters . GetAccounts . Query ( _ , entry ) \uf0c1 Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero ( _ , entry ): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return () beancount . core . getters . GetAccounts . Transaction ( _ , entry ) \uf0c1 Process a Transaction directive. Parameters: entry \u2013 An instance of Transaction. Yields: The accounts of the legs of the transaction. Source code in beancount/core/getters.py def Transaction ( _ , entry ): \"\"\"Process a Transaction directive. Args: entry: An instance of Transaction. Yields: The accounts of the legs of the transaction. \"\"\" for posting in entry . postings : yield posting . account beancount . core . getters . GetAccounts . get_accounts_use_map ( self , entries ) \uf0c1 Gather the list of accounts from the list of entries. Parameters: entries \u2013 A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. Source code in beancount/core/getters.py def get_accounts_use_map ( self , entries ): \"\"\"Gather the list of accounts from the list of entries. Args: entries: A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. \"\"\" accounts_first = {} accounts_last = {} for entry in entries : method = getattr ( self , entry . __class__ . __name__ ) for account_ in method ( entry ): if account_ not in accounts_first : accounts_first [ account_ ] = entry . date accounts_last [ account_ ] = entry . date return accounts_first , accounts_last beancount . core . getters . GetAccounts . get_entry_accounts ( self , entry ) \uf0c1 Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Parameters: entry \u2013 A directive instance. Returns: A set of account name strings. Source code in beancount/core/getters.py def get_entry_accounts ( self , entry ): \"\"\"Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Args: entry: A directive instance. Returns: A set of account name strings. \"\"\" method = getattr ( self , entry . __class__ . __name__ ) return set ( method ( entry )) beancount . core . getters . get_account_components ( entries ) \uf0c1 Gather all the account components available in the given directives. Parameters: entries \u2013 A list of directive instances. Returns: A list of strings, the unique account components, including the root account names. Source code in beancount/core/getters.py def get_account_components ( entries ): \"\"\"Gather all the account components available in the given directives. Args: entries: A list of directive instances. Returns: A list of strings, the unique account components, including the root account names. \"\"\" accounts = get_accounts ( entries ) components = set () for account_name in accounts : components . update ( account . split ( account_name )) return sorted ( components ) beancount . core . getters . get_account_open_close ( entries ) \uf0c1 Fetch the open/close entries for each of the accounts. If an open or close entry happens to be duplicated, accept the earliest entry (chronologically). Parameters: entries \u2013 A list of directive instances. Returns: A map of account name strings to pairs of (open-directive, close-directive) tuples. Source code in beancount/core/getters.py def get_account_open_close ( entries ): \"\"\"Fetch the open/close entries for each of the accounts. If an open or close entry happens to be duplicated, accept the earliest entry (chronologically). Args: entries: A list of directive instances. Returns: A map of account name strings to pairs of (open-directive, close-directive) tuples. \"\"\" # A dict of account name to (open-entry, close-entry). open_close_map = defaultdict ( lambda : [ None , None ]) for entry in entries : if not isinstance ( entry , ( Open , Close )): continue open_close = open_close_map [ entry . account ] index = 0 if isinstance ( entry , Open ) else 1 previous_entry = open_close [ index ] if previous_entry is not None : if previous_entry . date <= entry . date : entry = previous_entry open_close [ index ] = entry return dict ( open_close_map ) beancount . core . getters . get_accounts ( entries ) \uf0c1 Gather all the accounts references by a list of directives. Parameters: entries \u2013 A list of directive instances. Returns: A set of account strings. Source code in beancount/core/getters.py def get_accounts ( entries ): \"\"\"Gather all the accounts references by a list of directives. Args: entries: A list of directive instances. Returns: A set of account strings. \"\"\" _ , accounts_last = _GetAccounts . get_accounts_use_map ( entries ) return accounts_last . keys () beancount . core . getters . get_accounts_use_map ( entries ) \uf0c1 Gather all the accounts references by a list of directives. Parameters: entries \u2013 A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. Source code in beancount/core/getters.py def get_accounts_use_map ( entries ): \"\"\"Gather all the accounts references by a list of directives. Args: entries: A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. \"\"\" return _GetAccounts . get_accounts_use_map ( entries ) beancount . core . getters . get_active_years ( entries ) \uf0c1 Yield all the years that have at least one entry in them. Parameters: entries \u2013 A list of directive instances. Yields: Unique dates see in the list of directives. Source code in beancount/core/getters.py def get_active_years ( entries ): \"\"\"Yield all the years that have at least one entry in them. Args: entries: A list of directive instances. Yields: Unique dates see in the list of directives. \"\"\" seen = set () prev_year = None for entry in entries : year = entry . date . year if year != prev_year : prev_year = year assert year not in seen seen . add ( year ) yield year beancount . core . getters . get_all_links ( entries ) \uf0c1 Return a list of all the links seen in the given entries. Parameters: entries \u2013 A list of directive instances. Returns: A set of links strings. Source code in beancount/core/getters.py def get_all_links ( entries ): \"\"\"Return a list of all the links seen in the given entries. Args: entries: A list of directive instances. Returns: A set of links strings. \"\"\" all_links = set () for entry in entries : if not isinstance ( entry , Transaction ): continue if entry . links : all_links . update ( entry . links ) return sorted ( all_links ) beancount . core . getters . get_all_payees ( entries ) \uf0c1 Return a list of all the unique payees seen in the given entries. Parameters: entries \u2013 A list of directive instances. Returns: A set of payee strings. Source code in beancount/core/getters.py def get_all_payees ( entries ): \"\"\"Return a list of all the unique payees seen in the given entries. Args: entries: A list of directive instances. Returns: A set of payee strings. \"\"\" all_payees = set () for entry in entries : if not isinstance ( entry , Transaction ): continue all_payees . add ( entry . payee ) all_payees . discard ( None ) return sorted ( all_payees ) beancount . core . getters . get_all_tags ( entries ) \uf0c1 Return a list of all the tags seen in the given entries. Parameters: entries \u2013 A list of directive instances. Returns: A set of tag strings. Source code in beancount/core/getters.py def get_all_tags ( entries ): \"\"\"Return a list of all the tags seen in the given entries. Args: entries: A list of directive instances. Returns: A set of tag strings. \"\"\" all_tags = set () for entry in entries : if not isinstance ( entry , Transaction ): continue if entry . tags : all_tags . update ( entry . tags ) return sorted ( all_tags ) beancount . core . getters . get_commodity_directives ( entries ) \uf0c1 Create map of commodity names to Commodity entries. Parameters: entries \u2013 A list of directive instances. Returns: A map of commodity name strings to Commodity directives. Source code in beancount/core/getters.py def get_commodity_directives ( entries ): \"\"\"Create map of commodity names to Commodity entries. Args: entries: A list of directive instances. Returns: A map of commodity name strings to Commodity directives. \"\"\" return { entry . currency : entry for entry in entries if isinstance ( entry , Commodity )} beancount . core . getters . get_dict_accounts ( account_names ) \uf0c1 Return a nested dict of all the unique leaf names. account names are labelled with LABEL=True Parameters: account_names \u2013 An iterable of account names (strings) Returns: A nested OrderedDict of account leafs Source code in beancount/core/getters.py def get_dict_accounts ( account_names ): \"\"\"Return a nested dict of all the unique leaf names. account names are labelled with LABEL=True Args: account_names: An iterable of account names (strings) Returns: A nested OrderedDict of account leafs \"\"\" leveldict = OrderedDict () for account_name in account_names : nested_dict = leveldict for component in account . split ( account_name ): nested_dict = nested_dict . setdefault ( component , OrderedDict ()) nested_dict [ get_dict_accounts . ACCOUNT_LABEL ] = True return leveldict beancount . core . getters . get_entry_accounts ( entry ) \uf0c1 Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Parameters: entries \u2013 A directive instance. Returns: A set of account strings. Source code in beancount/core/getters.py def get_entry_accounts ( entry ): \"\"\"Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Args: entries: A directive instance. Returns: A set of account strings. \"\"\" return _GetAccounts . get_entry_accounts ( entry ) beancount . core . getters . get_leveln_parent_accounts ( account_names , level , nrepeats = 0 ) \uf0c1 Return a list of all the unique leaf names at level N in an account hierarchy. Parameters: account_names \u2013 A list of account names (strings) level \u2013 The level to cross-cut. 0 is for root accounts. nrepeats \u2013 A minimum number of times a leaf is required to be present in the the list of unique account names in order to be returned by this function. Returns: A list of leaf node names. Source code in beancount/core/getters.py def get_leveln_parent_accounts ( account_names , level , nrepeats = 0 ): \"\"\"Return a list of all the unique leaf names at level N in an account hierarchy. Args: account_names: A list of account names (strings) level: The level to cross-cut. 0 is for root accounts. nrepeats: A minimum number of times a leaf is required to be present in the the list of unique account names in order to be returned by this function. Returns: A list of leaf node names. \"\"\" leveldict = defaultdict ( int ) for account_name in set ( account_names ): components = account . split ( account_name ) if level < len ( components ): leveldict [ components [ level ]] += 1 levels = { level_ for level_ , count in leveldict . items () if count > nrepeats } return sorted ( levels ) beancount . core . getters . get_min_max_dates ( entries , types = None ) \uf0c1 Return the minimum and maximum dates in the list of entries. Parameters: entries \u2013 A list of directive instances. types \u2013 An optional tuple of types to restrict the entries to. Returns: A pair of datetime.date dates, the minimum and maximum dates seen in the directives. Source code in beancount/core/getters.py def get_min_max_dates ( entries , types = None ): \"\"\"Return the minimum and maximum dates in the list of entries. Args: entries: A list of directive instances. types: An optional tuple of types to restrict the entries to. Returns: A pair of datetime.date dates, the minimum and maximum dates seen in the directives. \"\"\" date_first = date_last = None for entry in entries : if types and not isinstance ( entry , types ): continue date_first = entry . date break for entry in reversed ( entries ): if types and not isinstance ( entry , types ): continue date_last = entry . date break return ( date_first , date_last ) beancount . core . getters . get_values_meta ( name_to_entries_map , * meta_keys , * , default = None ) \uf0c1 Get a map of the metadata from a map of entries values. Given a dict of some key to a directive instance (or None), return a mapping of the key to the metadata extracted from each directive, or a default value. This can be used to gather a particular piece of metadata from an accounts map or a commodities map. Parameters: name_to_entries_map \u2013 A dict of something to an entry or None. meta_keys \u2013 A list of strings, the keys to fetch from the metadata. default \u2013 The default value to use if the metadata is not available or if the value/entry is None. Returns: A mapping of the keys of name_to_entries_map to the values of the 'meta_keys' metadata. If there are multiple 'meta_keys', each value is a tuple of them. On the other hand, if there is only a single one, the value itself is returned. Source code in beancount/core/getters.py def get_values_meta ( name_to_entries_map , * meta_keys , default = None ): \"\"\"Get a map of the metadata from a map of entries values. Given a dict of some key to a directive instance (or None), return a mapping of the key to the metadata extracted from each directive, or a default value. This can be used to gather a particular piece of metadata from an accounts map or a commodities map. Args: name_to_entries_map: A dict of something to an entry or None. meta_keys: A list of strings, the keys to fetch from the metadata. default: The default value to use if the metadata is not available or if the value/entry is None. Returns: A mapping of the keys of name_to_entries_map to the values of the 'meta_keys' metadata. If there are multiple 'meta_keys', each value is a tuple of them. On the other hand, if there is only a single one, the value itself is returned. \"\"\" value_map = {} for key , entry in name_to_entries_map . items (): value_list = [] for meta_key in meta_keys : value_list . append ( entry . meta . get ( meta_key , default ) if entry is not None else default ) value_map [ key ] = value_list [ 0 ] if len ( meta_keys ) == 1 else tuple ( value_list ) return value_map beancount.core.interpolate \uf0c1 Code used to automatically complete postings without positions. beancount.core.interpolate.BalanceError ( tuple ) \uf0c1 BalanceError(source, message, entry) beancount . core . interpolate . BalanceError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/interpolate.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . interpolate . BalanceError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of BalanceError(source, message, entry) beancount . core . interpolate . BalanceError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/interpolate.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . core . interpolate . compute_entries_balance ( entries , prefix = None , date = None ) \uf0c1 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. Parameters: entries \u2013 A list of directives. prefix \u2013 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 \u2013 A datetime.date instance at which to stop adding up the balance. The date is exclusive. Returns: An instance of Inventory. Source code in beancount/core/interpolate.py 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 beancount . core . interpolate . compute_entry_context ( entries , context_entry , additional_accounts = None ) \uf0c1 Compute the balances of all accounts referenced by entry up to entry. This provides the inventory of the accounts to which the entry is to be applied, before and after. Parameters: entries \u2013 A list of directives. context_entry \u2013 The entry for which we want to obtain the before and after context. additional_accounts \u2013 Additional list of accounts to include in calculating the balance. This is used when invoked for debugging, in case the booked & interpolated transaction doesn't have all the accounts we need because it had an error (the booking code will remove invalid postings). Returns: Two dicts of account-name to Inventory instance, one which represents the context before the entry is applied, and one that represents the context after it has been applied. Source code in beancount/core/interpolate.py def compute_entry_context ( entries , context_entry , additional_accounts = None ): \"\"\"Compute the balances of all accounts referenced by entry up to entry. This provides the inventory of the accounts to which the entry is to be applied, before and after. Args: entries: A list of directives. context_entry: The entry for which we want to obtain the before and after context. additional_accounts: Additional list of accounts to include in calculating the balance. This is used when invoked for debugging, in case the booked & interpolated transaction doesn't have all the accounts we need because it had an error (the booking code will remove invalid postings). Returns: Two dicts of account-name to Inventory instance, one which represents the context before the entry is applied, and one that represents the context after it has been applied. \"\"\" assert context_entry is not None , \"context_entry is missing.\" # Get the set of accounts for which to compute the context. context_accounts = getters . get_entry_accounts ( context_entry ) if additional_accounts : context_accounts . update ( additional_accounts ) # Iterate over the entries until we find the target one and accumulate the # balance. context_before = collections . defaultdict ( inventory . Inventory ) for entry in entries : if entry is context_entry : break if isinstance ( entry , Transaction ): for posting in entry . postings : if not any ( posting . account == account for account in context_accounts ): continue balance = context_before [ posting . account ] balance . add_position ( posting ) # Compute the after context for the entry. context_after = copy . deepcopy ( context_before ) if isinstance ( context_entry , Transaction ): for posting in entry . postings : balance = context_after [ posting . account ] balance . add_position ( posting ) return context_before , context_after beancount . core . interpolate . compute_residual ( postings ) \uf0c1 Compute the residual of a set of complete postings, and the per-currency precision. This is used to cross-check a balanced transaction. The precision is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. Parameters: postings \u2013 A list of Posting instances. Returns: An instance of Inventory, with the residual of the given list of postings. Source code in beancount/core/interpolate.py def compute_residual ( postings ): \"\"\"Compute the residual of a set of complete postings, and the per-currency precision. This is used to cross-check a balanced transaction. The precision is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. Args: postings: A list of Posting instances. Returns: An instance of Inventory, with the residual of the given list of postings. \"\"\" inventory = Inventory () for posting in postings : # Skip auto-postings inserted to absorb the residual (rounding error). if posting . meta and posting . meta . get ( AUTOMATIC_RESIDUAL , False ): continue # Add to total residual balance. inventory . add_amount ( convert . get_weight ( posting )) return inventory beancount . core . interpolate . fill_residual_posting ( entry , account_rounding ) \uf0c1 If necessary, insert a posting to absorb the residual. This makes the transaction balance exactly. Note: This was developed in order to tweak transactions before exporting them to Ledger. A better method would be to enable the feature that automatically inserts these rounding postings on all transactions, and so maybe this method can be deprecated if we do so. Parameters: entry \u2013 An instance of a Transaction. account_rounding \u2013 A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A possibly new, modified entry with a new posting. If a residual was not needed - the transaction already balanced perfectly - no new leg is inserted. Source code in beancount/core/interpolate.py def fill_residual_posting ( entry , account_rounding ): \"\"\"If necessary, insert a posting to absorb the residual. This makes the transaction balance exactly. Note: This was developed in order to tweak transactions before exporting them to Ledger. A better method would be to enable the feature that automatically inserts these rounding postings on all transactions, and so maybe this method can be deprecated if we do so. Args: entry: An instance of a Transaction. account_rounding: A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A possibly new, modified entry with a new posting. If a residual was not needed - the transaction already balanced perfectly - no new leg is inserted. \"\"\" residual = compute_residual ( entry . postings ) if not residual . is_empty (): new_postings = list ( entry . postings ) new_postings . extend ( get_residual_postings ( residual , account_rounding )) entry = entry . _replace ( postings = new_postings ) return entry beancount . core . interpolate . get_residual_postings ( residual , account_rounding ) \uf0c1 Create postings to book the given residuals. Parameters: residual \u2013 An Inventory, the residual positions. account_rounding \u2013 A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A list of new postings to be inserted to reduce the given residual. Source code in beancount/core/interpolate.py def get_residual_postings ( residual , account_rounding ): \"\"\"Create postings to book the given residuals. Args: residual: An Inventory, the residual positions. account_rounding: A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A list of new postings to be inserted to reduce the given residual. \"\"\" meta = { AUTOMATIC_META : True , AUTOMATIC_RESIDUAL : True } return [ Posting ( account_rounding , - position . units , position . cost , None , None , meta . copy ()) for position in residual . get_positions () ] beancount . core . interpolate . has_nontrivial_balance ( posting ) \uf0c1 Return True if a Posting has a balance amount that would have to be calculated. Parameters: posting \u2013 A Posting instance. Returns: A boolean. Source code in beancount/core/interpolate.py def has_nontrivial_balance ( posting ): \"\"\"Return True if a Posting has a balance amount that would have to be calculated. Args: posting: A Posting instance. Returns: A boolean. \"\"\" return posting . cost or posting . price beancount . core . interpolate . infer_tolerances ( postings , options_map , use_cost = None ) \uf0c1 Infer tolerances from a list of postings. The tolerance is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. The 'use_cost' option allows one to experiment with letting postings at cost and at price influence the maximum value of the tolerance. It's tricky to use and alters the definition of the tolerance in a non-trivial way, if you use it. The tolerance is expanded by the sum of the cost times a fraction 'M' of the smallest digits in the number of units for all postings held at cost. For example, in this transaction: 2006-01-17 * \"Plan Contribution\" Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:Cash -1150.00 USD The tolerance for units of USD will calculated as the MAXIMUM of: 0.01 * M = 0.005 (from the 1150.00 USD leg) The sum of 0.001 * M x 30.96 = 0.01548 + 0.001 * M x 30.96 = 0.01548 = 0.03096 So the tolerance for USD in this case is max(0.005, 0.03096) = 0.03096. Prices contribute similarly to the maximum tolerance allowed. Note that 'M' above is the inferred_tolerance_multiplier and its default value is 0.5. Parameters: postings \u2013 A list of Posting instances. options_map \u2013 A dict of options. use_cost \u2013 A boolean, true if we should be using a combination of the smallest digit of the number times the cost or price in order to infer the tolerance. If the value is left unspecified (as 'None'), the default value can be overridden by setting an option. Returns: A dict of currency to the tolerated difference amount to be used for it, e.g. 0.005. Source code in beancount/core/interpolate.py def infer_tolerances ( postings , options_map , use_cost = None ): \"\"\"Infer tolerances from a list of postings. The tolerance is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. The 'use_cost' option allows one to experiment with letting postings at cost and at price influence the maximum value of the tolerance. It's tricky to use and alters the definition of the tolerance in a non-trivial way, if you use it. The tolerance is expanded by the sum of the cost times a fraction 'M' of the smallest digits in the number of units for all postings held at cost. For example, in this transaction: 2006-01-17 * \"Plan Contribution\" Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:Cash -1150.00 USD The tolerance for units of USD will calculated as the MAXIMUM of: 0.01 * M = 0.005 (from the 1150.00 USD leg) The sum of 0.001 * M x 30.96 = 0.01548 + 0.001 * M x 30.96 = 0.01548 = 0.03096 So the tolerance for USD in this case is max(0.005, 0.03096) = 0.03096. Prices contribute similarly to the maximum tolerance allowed. Note that 'M' above is the inferred_tolerance_multiplier and its default value is 0.5. Args: postings: A list of Posting instances. options_map: A dict of options. use_cost: A boolean, true if we should be using a combination of the smallest digit of the number times the cost or price in order to infer the tolerance. If the value is left unspecified (as 'None'), the default value can be overridden by setting an option. Returns: A dict of currency to the tolerated difference amount to be used for it, e.g. 0.005. \"\"\" if use_cost is None : use_cost = options_map [ \"infer_tolerance_from_cost\" ] inferred_tolerance_multiplier = options_map [ \"inferred_tolerance_multiplier\" ] default_tolerances = options_map [ \"inferred_tolerance_default\" ] tolerances = default_tolerances . copy () cost_tolerances = collections . defaultdict ( D ) for posting in postings : # Skip the precision on automatically inferred postings. if posting . meta and AUTOMATIC_META in posting . meta : continue units = posting . units if not ( isinstance ( units , Amount ) and isinstance ( units . number , Decimal )): continue # Compute bounds on the number. currency = units . currency expo = units . number . as_tuple () . exponent if expo < 0 : # Note: the exponent is a negative value. tolerance = ONE . scaleb ( expo ) * inferred_tolerance_multiplier # Note that we take the max() and not the min() here because the # tolerance has a dual purpose: it's used to infer the resolution # for interpolation (where we might want the min()) and also for # balance checks (where we favor the looser/larger tolerance). tolerances [ currency ] = max ( tolerance , tolerances . get ( currency , - 1024 )) if not use_cost : continue # Compute bounds on the smallest digit of the number implied as cost. cost = posting . cost if cost is not None : cost_currency = cost . currency if isinstance ( cost , Cost ): cost_tolerance = min ( tolerance * cost . number , MAXIMUM_TOLERANCE ) else : assert isinstance ( cost , CostSpec ) cost_tolerance = MAXIMUM_TOLERANCE for cost_number in cost . number_total , cost . number_per : if cost_number is None or cost_number is MISSING : continue cost_tolerance = min ( tolerance * cost_number , cost_tolerance ) cost_tolerances [ cost_currency ] += cost_tolerance # Compute bounds on the smallest digit of the number implied as cost. price = posting . price if isinstance ( price , Amount ) and isinstance ( price . number , Decimal ): price_currency = price . currency price_tolerance = min ( tolerance * price . number , MAXIMUM_TOLERANCE ) cost_tolerances [ price_currency ] += price_tolerance for currency , tolerance in cost_tolerances . items (): tolerances [ currency ] = max ( tolerance , tolerances . get ( currency , - 1024 )) default = tolerances . pop ( \"*\" , ZERO ) return defdict . ImmutableDictWithDefault ( tolerances , default = default ) beancount . core . interpolate . is_tolerance_user_specified ( tolerance ) \uf0c1 Return true if the given tolerance number was user-specified. This would allow the user to provide a tolerance like # 0.1234 but not 0.123456. This is used to detect whether a tolerance value # is input by the user and not inferred automatically. Parameters: tolerance \u2013 An instance of Decimal. Returns: A boolean. Source code in beancount/core/interpolate.py def is_tolerance_user_specified ( tolerance ): \"\"\"Return true if the given tolerance number was user-specified. This would allow the user to provide a tolerance like # 0.1234 but not 0.123456. This is used to detect whether a tolerance value # is input by the user and not inferred automatically. Args: tolerance: An instance of Decimal. Returns: A boolean. \"\"\" return len ( tolerance . as_tuple () . digits ) < MAX_TOLERANCE_DIGITS beancount . core . interpolate . quantize_with_tolerance ( tolerances , currency , number ) \uf0c1 Quantize the units using the tolerance dict. Parameters: tolerances \u2013 A dict of currency to tolerance Decimalvalues. number \u2013 A number to quantize. currency \u2013 A string currency. Returns: A Decimal, the number possibly quantized. Source code in beancount/core/interpolate.py def quantize_with_tolerance ( tolerances , currency , number ): \"\"\"Quantize the units using the tolerance dict. Args: tolerances: A dict of currency to tolerance Decimalvalues. number: A number to quantize. currency: A string currency. Returns: A Decimal, the number possibly quantized. \"\"\" # Applying rounding to the default tolerance, if there is one. tolerance = tolerances . get ( currency ) if tolerance : quantum = ( tolerance * 2 ) . normalize () # If the tolerance is a neat number provided by the user, # quantize the inferred numbers. See doc on quantize(): # # Unlike other operations, if the length of the coefficient # after the quantize operation would be greater than # precision, then an InvalidOperation is signaled. This # guarantees that, unless there is an error condition, the # quantized exponent is always equal to that of the # right-hand operand. if is_tolerance_user_specified ( quantum ): number = number . quantize ( quantum ) return number beancount.core.inventory \uf0c1 A container for an inventory of positions. This module provides a container class that can hold positions. An inventory is a mapping of positions, where each position is keyed by (currency: str, cost: Cost) -> position: Position where 'currency': The commodity under consideration, USD, CAD, or stock units such as HOOL, MSFT, AAPL, etc.; 'cost': None or a Cost instance existing of cost currency, number, date, and label; 'position': A Position object, whose 'units' attribute is guaranteed to have the same currency as 'currency' and whose 'cost' attribute is equal to the 'cost' key. It basically stores the number of units. This is meant to accommodate both booked and non-booked amounts. The clever trick that we pull to do this is that for positions which aren't booked, we simply leave the 'cost' as None. This is the case for most of the transactions. = Conversions = If it often desired to convert this inventory into an equivalent position for its cost, or to just flatten all the positions with the same currency and count the number of units, or to compute the market value for the inventory at a specific date. You do these conversions using the reduce() method: inventory.reduce(convert.get_cost) inventory.reduce(convert.get_units) inventory.reduce(convert.get_value, price_map, date) beancount.core.inventory.Inventory ( dict ) \uf0c1 An Inventory is a set of positions, indexed for efficiency. beancount . core . inventory . Inventory . __abs__ ( self ) special \uf0c1 Return an inventory with the absolute value of each position. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def __abs__ ( self ): \"\"\"Return an inventory with the absolute value of each position. Returns: An instance of Inventory. \"\"\" return Inventory ({ key : abs ( pos ) for key , pos in self . items ()}) beancount . core . inventory . Inventory . __add__ ( self , other ) special \uf0c1 Add another inventory to this one. This inventory is not modified. Parameters: other \u2013 An instance of Inventory. Returns: A new instance of Inventory. Source code in beancount/core/inventory.py def __add__ ( self , other ): \"\"\"Add another inventory to this one. This inventory is not modified. Args: other: An instance of Inventory. Returns: A new instance of Inventory. \"\"\" new_inventory = self . __copy__ () new_inventory . add_inventory ( other ) return new_inventory beancount . core . inventory . Inventory . __copy__ ( self ) special \uf0c1 A shallow copy of this inventory object. Returns: An instance of Inventory, equal to this one. Source code in beancount/core/inventory.py def __copy__ ( self ): \"\"\"A shallow copy of this inventory object. Returns: An instance of Inventory, equal to this one. \"\"\" return Inventory ( self ) beancount . core . inventory . Inventory . __iadd__ ( self , other ) special \uf0c1 Add all the positions of another Inventory instance to this one. Parameters: other \u2013 An instance of Inventory to add to this one. Returns: This inventory, modified. Source code in beancount/core/inventory.py def add_inventory ( self , other ): \"\"\"Add all the positions of another Inventory instance to this one. Args: other: An instance of Inventory to add to this one. Returns: This inventory, modified. \"\"\" if self . is_empty (): # Optimization for empty inventories; if the current one is empty, # adopt all of the other inventory's positions without running # through the full aggregation checks. This should be very cheap. We # can do this because the positions are immutable. self . update ( other ) else : for position in other . get_positions (): self . add_position ( position ) return self beancount . core . inventory . Inventory . __init__ ( self , positions = None ) special \uf0c1 Create a new inventory using a list of existing positions. Parameters: positions \u2013 A list of Position instances or an existing dict or Inventory instance. Source code in beancount/core/inventory.py def __init__ ( self , positions = None ): \"\"\"Create a new inventory using a list of existing positions. Args: positions: A list of Position instances or an existing dict or Inventory instance. \"\"\" if isinstance ( positions , ( dict , Inventory )): dict . __init__ ( self , positions ) else : dict . __init__ ( self ) if positions : assert isinstance ( positions , Iterable ) for position in positions : self . add_position ( position ) beancount . core . inventory . Inventory . __iter__ ( self ) special \uf0c1 Iterate over the positions. Note that there is no guaranteed order. Source code in beancount/core/inventory.py def __iter__ ( self ): \"\"\"Iterate over the positions. Note that there is no guaranteed order.\"\"\" return iter ( self . values ()) beancount . core . inventory . Inventory . __lt__ ( self , other ) special \uf0c1 Inequality comparison operator. Source code in beancount/core/inventory.py def __lt__ ( self , other ): \"\"\"Inequality comparison operator.\"\"\" return sorted ( self ) < sorted ( other ) beancount . core . inventory . Inventory . __mul__ ( self , scalar ) special \uf0c1 Scale/multiply the contents of the inventory. Parameters: scalar \u2013 A Decimal. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def __mul__ ( self , scalar ): \"\"\"Scale/multiply the contents of the inventory. Args: scalar: A Decimal. Returns: An instance of Inventory. \"\"\" return Inventory ({ key : pos * scalar for key , pos in self . items ()}) beancount . core . inventory . Inventory . __neg__ ( self ) special \uf0c1 Return an inventory with the negative of values of this one. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def __neg__ ( self ): \"\"\"Return an inventory with the negative of values of this one. Returns: An instance of Inventory. \"\"\" return Inventory ({ key : - pos for key , pos in self . items ()}) beancount . core . inventory . Inventory . __repr__ ( self ) special \uf0c1 Render as a human-readable string. Returns: A string, for human consumption. Source code in beancount/core/inventory.py def __str__ ( self ): \"\"\"Render as a human-readable string. Returns: A string, for human consumption. \"\"\" return self . to_string () beancount . core . inventory . Inventory . __str__ ( self ) special \uf0c1 Render as a human-readable string. Returns: A string, for human consumption. Source code in beancount/core/inventory.py def __str__ ( self ): \"\"\"Render as a human-readable string. Returns: A string, for human consumption. \"\"\" return self . to_string () beancount . core . inventory . Inventory . add_amount ( self , units , cost = None ) \uf0c1 Add to this inventory using amount and cost. This adds with strict lot matching, that is, no partial matches are done on the arguments to the keys of the inventory. Parameters: units \u2013 An Amount instance to add. cost \u2013 An instance of Cost or None, as a key to the inventory. Returns: A pair of (position, matched) where 'position' is the position that that was modified BEFORE it was modified, and where 'matched' is a MatchResult enum that hints at how the lot was booked to this inventory. Position may be None if there is no corresponding Position object, e.g. the position was deleted. Source code in beancount/core/inventory.py def add_amount ( self , units , cost = None ): \"\"\"Add to this inventory using amount and cost. This adds with strict lot matching, that is, no partial matches are done on the arguments to the keys of the inventory. Args: units: An Amount instance to add. cost: An instance of Cost or None, as a key to the inventory. Returns: A pair of (position, matched) where 'position' is the position that that was modified BEFORE it was modified, and where 'matched' is a MatchResult enum that hints at how the lot was booked to this inventory. Position may be None if there is no corresponding Position object, e.g. the position was deleted. \"\"\" if ASSERTS_TYPES : assert isinstance ( units , Amount ), \"Internal error: {!r} (type: {} )\" . format ( units , type ( units ) . __name__ ) assert cost is None or isinstance ( cost , Cost ), \"Internal error: {!r} (type: {} )\" . format ( cost , type ( cost ) . __name__ ) # Find the position. key = ( units . currency , cost ) pos = self . get ( key , None ) if pos is not None : # Note: In order to augment or reduce, all the fields have to match. # Check if reducing. booking = ( MatchResult . REDUCED if not same_sign ( pos . units . number , units . number ) else MatchResult . AUGMENTED ) # Compute the new number of units. number = pos . units . number + units . number if number == ZERO : # If empty, delete the position. del self [ key ] else : # Otherwise update it. self [ key ] = Position ( Amount ( number , units . currency ), cost ) else : # If not found, create a new one. if units . number == ZERO : booking = MatchResult . IGNORED else : self [ key ] = Position ( units , cost ) booking = MatchResult . CREATED return pos , booking beancount . core . inventory . Inventory . add_inventory ( self , other ) \uf0c1 Add all the positions of another Inventory instance to this one. Parameters: other \u2013 An instance of Inventory to add to this one. Returns: This inventory, modified. Source code in beancount/core/inventory.py def add_inventory ( self , other ): \"\"\"Add all the positions of another Inventory instance to this one. Args: other: An instance of Inventory to add to this one. Returns: This inventory, modified. \"\"\" if self . is_empty (): # Optimization for empty inventories; if the current one is empty, # adopt all of the other inventory's positions without running # through the full aggregation checks. This should be very cheap. We # can do this because the positions are immutable. self . update ( other ) else : for position in other . get_positions (): self . add_position ( position ) return self beancount . core . inventory . Inventory . add_position ( self , position ) \uf0c1 Add using a position (with strict lot matching). Return True if this position was booked against and reduced another. Parameters: position \u2013 The Posting or Position to add to this inventory. Returns: A pair of (position, booking) where 'position' is the position that that was modified, and where 'matched' is a MatchResult enum that hints at how the lot was booked to this inventory. Source code in beancount/core/inventory.py def add_position ( self , position ): \"\"\"Add using a position (with strict lot matching). Return True if this position was booked against and reduced another. Args: position: The Posting or Position to add to this inventory. Returns: A pair of (position, booking) where 'position' is the position that that was modified, and where 'matched' is a MatchResult enum that hints at how the lot was booked to this inventory. \"\"\" if ASSERTS_TYPES : assert hasattr ( position , \"units\" ) and hasattr ( position , \"cost\" ), \"Invalid type for position: {} \" . format ( position ) assert isinstance ( position . cost , ( type ( None ), Cost ) ), \"Invalid type for cost: {} \" . format ( position . cost ) return self . add_amount ( position . units , position . cost ) beancount . core . inventory . Inventory . average ( self ) \uf0c1 Average all lots of the same currency together. Use the minimum date from each aggregated set of lots. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def average ( self ): \"\"\"Average all lots of the same currency together. Use the minimum date from each aggregated set of lots. Returns: An instance of Inventory. \"\"\" groups = collections . defaultdict ( list ) for position in self : key = ( position . units . currency , position . cost . currency if position . cost else None , ) groups [ key ] . append ( position ) average_inventory = Inventory () for ( currency , cost_currency ), positions in groups . items (): total_units = sum ( position . units . number for position in positions ) # Explicitly skip aggregates when resulting in zero units. if total_units == ZERO : continue units_amount = Amount ( total_units , currency ) if cost_currency : total_cost = sum ( convert . get_cost ( position ) . number for position in positions ) cost_number = ( Decimal ( \"Infinity\" ) if total_units == ZERO else ( total_cost / total_units ) ) min_date = None for pos in positions : pos_date = pos . cost . date if pos . cost else None if pos_date is not None : min_date = pos_date if min_date is None else min ( min_date , pos_date ) cost = Cost ( cost_number , cost_currency , min_date , None ) else : cost = None average_inventory . add_amount ( units_amount , cost ) return average_inventory beancount . core . inventory . Inventory . cost_currencies ( self ) \uf0c1 Return the list of unit currencies held in this inventory. Returns: A set of currency strings. Source code in beancount/core/inventory.py def cost_currencies ( self ): \"\"\"Return the list of unit currencies held in this inventory. Returns: A set of currency strings. \"\"\" return set ( cost . currency for _ , cost in self . keys () if cost is not None ) beancount . core . inventory . Inventory . currencies ( self ) \uf0c1 Return the list of unit currencies held in this inventory. Returns: A list of currency strings. Source code in beancount/core/inventory.py def currencies ( self ): \"\"\"Return the list of unit currencies held in this inventory. Returns: A list of currency strings. \"\"\" return set ( currency for currency , _ in self . keys ()) beancount . core . inventory . Inventory . currency_pairs ( self ) \uf0c1 Return the commodities held in this inventory. Returns: A set of currency strings. Source code in beancount/core/inventory.py def currency_pairs ( self ): \"\"\"Return the commodities held in this inventory. Returns: A set of currency strings. \"\"\" return set ( position . currency_pair () for position in self ) beancount . core . inventory . Inventory . from_string ( string ) staticmethod \uf0c1 Create an Inventory from a string. This is useful for writing tests. Parameters: string \u2013 A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. Source code in beancount/core/inventory.py @staticmethod def from_string ( string ): \"\"\"Create an Inventory from a string. This is useful for writing tests. Args: string: A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. \"\"\" new_inventory = Inventory () # We need to split the comma-separated positions but ignore commas # occurring within a {...cost...} specification. position_strs = re . split ( r \"([-+]?[0-9,.]+\\s+[A-Z]+\\s*(?:{[^}]*})?)\\s*,?\\s*\" , string )[ 1 :: 2 ] for position_str in position_strs : new_inventory . add_position ( position_from_string ( position_str )) return new_inventory beancount . core . inventory . Inventory . get_currency_units ( self , currency ) \uf0c1 Fetch the total amount across all the position in the given currency. This may sum multiple lots in the same currency denomination. Parameters: currency \u2013 A string, the currency to filter the positions with. Returns: An instance of Amount, with the given currency. Source code in beancount/core/inventory.py def get_currency_units ( self , currency ): \"\"\"Fetch the total amount across all the position in the given currency. This may sum multiple lots in the same currency denomination. Args: currency: A string, the currency to filter the positions with. Returns: An instance of Amount, with the given currency. \"\"\" total_units = ZERO for position in self : if position . units . currency == currency : total_units += position . units . number return Amount ( total_units , currency ) beancount . core . inventory . Inventory . get_only_position ( self ) \uf0c1 Return the first position and assert there are no more. If the inventory is empty, return None. Source code in beancount/core/inventory.py def get_only_position ( self ): \"\"\"Return the first position and assert there are no more. If the inventory is empty, return None. \"\"\" if len ( self ) > 0 : if len ( self ) > 1 : raise AssertionError ( \"Inventory has more than one expected \" \"position: {} \" . format ( self ) ) return next ( iter ( self )) beancount . core . inventory . Inventory . get_positions ( self ) \uf0c1 Return the positions in this inventory. Returns: A shallow copy of the list of positions. Source code in beancount/core/inventory.py def get_positions ( self ): \"\"\"Return the positions in this inventory. Returns: A shallow copy of the list of positions. \"\"\" return list ( iter ( self )) beancount . core . inventory . Inventory . is_empty ( self ) \uf0c1 Return true if the inventory is empty, that is, has no positions. Returns: A boolean. Source code in beancount/core/inventory.py def is_empty ( self ): \"\"\"Return true if the inventory is empty, that is, has no positions. Returns: A boolean. \"\"\" return len ( self ) == 0 beancount . core . inventory . Inventory . is_mixed ( self ) \uf0c1 Return true if the inventory contains a mix of positive and negative lots for at least one instrument. Returns: A boolean. Source code in beancount/core/inventory.py def is_mixed ( self ): \"\"\"Return true if the inventory contains a mix of positive and negative lots for at least one instrument. Returns: A boolean. \"\"\" signs_map = {} for position in self : sign = position . units . number >= 0 prev_sign = signs_map . setdefault ( position . units . currency , sign ) if sign != prev_sign : return True return False beancount . core . inventory . Inventory . is_reduced_by ( self , ramount ) \uf0c1 Return true if the amount could reduce this inventory. Parameters: ramount \u2013 An instance of Amount. Returns: A boolean. Source code in beancount/core/inventory.py def is_reduced_by ( self , ramount ): \"\"\"Return true if the amount could reduce this inventory. Args: ramount: An instance of Amount. Returns: A boolean. \"\"\" if ramount . number == ZERO : return False for position in self : units = position . units if ramount . currency == units . currency and not same_sign ( ramount . number , units . number ): return True return False beancount . core . inventory . Inventory . is_small ( self , tolerances ) \uf0c1 Return true if all the positions in the inventory are small. Parameters: tolerances \u2013 A Decimal, the small number of units under which a position is considered small, or a dict of currency to such epsilon precision. Returns: A boolean. Source code in beancount/core/inventory.py def is_small ( self , tolerances ): \"\"\"Return true if all the positions in the inventory are small. Args: tolerances: A Decimal, the small number of units under which a position is considered small, or a dict of currency to such epsilon precision. Returns: A boolean. \"\"\" if isinstance ( tolerances , dict ): for position in self : tolerance = tolerances . get ( position . units . currency , ZERO ) if abs ( position . units . number ) > tolerance : return False small = True else : small = not any ( abs ( position . units . number ) > tolerances for position in self ) return small beancount . core . inventory . Inventory . reduce ( self , reducer , * args ) \uf0c1 Reduce an inventory using one of the conversion functions. See functions in beancount.core.convert. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def reduce ( self , reducer , * args ): \"\"\"Reduce an inventory using one of the conversion functions. See functions in beancount.core.convert. Returns: An instance of Inventory. \"\"\" inventory = Inventory () for position in self : inventory . add_amount ( reducer ( position , * args )) return inventory beancount . core . inventory . Inventory . segregate_units ( self , currencies ) \uf0c1 Split up the list of positions to the given currencies. Parameters: currencies \u2013 A list of currency strings, the currencies to isolate. Returns: A dict of currency to Inventory instances. Source code in beancount/core/inventory.py def segregate_units ( self , currencies ): \"\"\"Split up the list of positions to the given currencies. Args: currencies: A list of currency strings, the currencies to isolate. Returns: A dict of currency to Inventory instances. \"\"\" per_currency_dict = { currency : Inventory () for currency in currencies } per_currency_dict [ None ] = Inventory () for position in self : currency = position . units . currency key = currency if currency in currencies else None per_currency_dict [ key ] . add_position ( position ) return per_currency_dict beancount . core . inventory . Inventory . split ( self ) \uf0c1 Split up the list of positions to their corresponding currencies. Returns: A dict of currency to Inventory instances. Source code in beancount/core/inventory.py def split ( self ): \"\"\"Split up the list of positions to their corresponding currencies. Returns: A dict of currency to Inventory instances. \"\"\" per_currency_dict = collections . defaultdict ( Inventory ) for position in self : per_currency_dict [ position . units . currency ] . add_position ( position ) return dict ( per_currency_dict ) beancount . core . inventory . Inventory . to_string ( self , dformat =< beancount . core . display_context . DisplayFormatter object at 0x78fcde6b7290 > , parens = True ) \uf0c1 Convert an Inventory instance to a printable string. Parameters: dformat \u2013 An instance of DisplayFormatter. parents \u2013 A boolean, true if we should surround the results by parentheses. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/inventory.py def to_string ( self , dformat = DEFAULT_FORMATTER , parens = True ): \"\"\"Convert an Inventory instance to a printable string. Args: dformat: An instance of DisplayFormatter. parents: A boolean, true if we should surround the results by parentheses. Returns: A formatted string of the quantized amount and symbol. \"\"\" fmt = \"( {} )\" if parens else \" {} \" return fmt . format ( \", \" . join ( pos . to_string ( dformat ) for pos in sorted ( self ))) beancount.core.inventory.MatchResult ( Enum ) \uf0c1 Result of booking a new lot to an existing inventory. beancount . core . inventory . check_invariants ( inv ) \uf0c1 Check the invariants of the Inventory. Parameters: inventory \u2013 An instance of Inventory. Returns: True if the invariants are respected. Source code in beancount/core/inventory.py def check_invariants ( inv ): \"\"\"Check the invariants of the Inventory. Args: inventory: An instance of Inventory. Returns: True if the invariants are respected. \"\"\" # Check that all the keys are unique. lots = set (( pos . units . currency , pos . cost ) for pos in inv ) assert len ( lots ) == len ( inv ), \"Invalid inventory: {} \" . format ( inv ) # Check that none of the amounts is zero. for pos in inv : assert pos . units . number != ZERO , \"Invalid position size: {} \" . format ( pos ) beancount . core . inventory . from_string ( string ) \uf0c1 Create an Inventory from a string. This is useful for writing tests. Parameters: string \u2013 A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. Source code in beancount/core/inventory.py @staticmethod def from_string ( string ): \"\"\"Create an Inventory from a string. This is useful for writing tests. Args: string: A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. \"\"\" new_inventory = Inventory () # We need to split the comma-separated positions but ignore commas # occurring within a {...cost...} specification. position_strs = re . split ( r \"([-+]?[0-9,.]+\\s+[A-Z]+\\s*(?:{[^}]*})?)\\s*,?\\s*\" , string )[ 1 :: 2 ] for position_str in position_strs : new_inventory . add_position ( position_from_string ( position_str )) return new_inventory beancount.core.number \uf0c1 The module contains the basic Decimal type import. About Decimal usage: Do not import Decimal from the 'decimal' or 'cdecimal' modules; always import your Decimal class from beancount.core.amount. Prefer to use D() to create new instances of Decimal objects, which handles more syntax, e.g., handles None, and numbers with commas. beancount . core . number . D ( strord = None ) \uf0c1 Convert a string into a Decimal object. This is used in parsing amounts from files in the importers. This is the main function you should use to build all numbers the system manipulates (never use floating-point in an accounting system). Commas are stripped and ignored, as they are assumed to be thousands separators (the French comma separator as decimal is not supported). This function just returns the argument if it is already a Decimal object, for convenience. Parameters: strord \u2013 A string or Decimal instance. Returns: A Decimal instance. Source code in beancount/core/number.py def D ( strord = None ): \"\"\"Convert a string into a Decimal object. This is used in parsing amounts from files in the importers. This is the main function you should use to build all numbers the system manipulates (never use floating-point in an accounting system). Commas are stripped and ignored, as they are assumed to be thousands separators (the French comma separator as decimal is not supported). This function just returns the argument if it is already a Decimal object, for convenience. Args: strord: A string or Decimal instance. Returns: A Decimal instance. \"\"\" try : # Note: try a map lookup and optimize performance here. if strord is None or strord == \"\" : return Decimal () elif isinstance ( strord , str ): return Decimal ( _CLEAN_NUMBER_RE . sub ( \"\" , strord )) elif isinstance ( strord , Decimal ): return strord elif isinstance ( strord , ( int , float )): return Decimal ( strord ) else : assert strord is None , \"Invalid value to convert: {} \" . format ( strord ) except Exception as exc : raise ValueError ( \"Impossible to create Decimal instance from {!s} : {} \" . format ( strord , exc ) ) from exc beancount . core . number . auto_quantize ( number , threshold ) \uf0c1 Automatically quantize the number at a given threshold. For example, with a threshold of 0.01, this will convert: 20.899999618530273 20.9 20.290000000000000000000000000000 20.29 110.90 110.9 11.0600004196167 11.06 10.539999961853027 10.54 134.3300018310547 134.33 253.920200000000000000000000000000 253.9202 Source code in beancount/core/number.py def auto_quantize ( number : Decimal , threshold : float ) -> Decimal : \"\"\"Automatically quantize the number at a given threshold. For example, with a threshold of 0.01, this will convert: 20.899999618530273 20.9 20.290000000000000000000000000000 20.29 110.90 110.9 11.0600004196167 11.06 10.539999961853027 10.54 134.3300018310547 134.33 253.920200000000000000000000000000 253.9202 \"\"\" exponent = auto_quantized_exponent ( number , threshold ) if exponent != number . as_tuple () . exponent : quant = TEN ** exponent qnumber = number . quantize ( quant ) . normalize () return qnumber else : return number beancount . core . number . auto_quantized_exponent ( number , threshold ) \uf0c1 Automatically infer the exponent that would be used below a given threshold. Source code in beancount/core/number.py def auto_quantized_exponent ( number : Decimal , threshold : float ) -> int : \"\"\"Automatically infer the exponent that would be used below a given threshold.\"\"\" dtuple = number . normalize () . as_tuple () norm = Decimal ( dtuple . _replace ( sign = 0 , exponent =- len ( dtuple . digits ))) low_threshold = threshold high_threshold = 1.0 - low_threshold while norm != ZERO : if not ( low_threshold <= norm <= high_threshold ): break ntuple = norm . scaleb ( 1 ) . as_tuple () norm = Decimal ( ntuple . _replace ( digits = ntuple . digits [ ntuple . exponent :])) return dtuple . exponent - norm . as_tuple () . exponent beancount . core . number . infer_quantum_from_list ( numbers , threshold = 0.01 ) \uf0c1 Given a list of numbers from floats, infer the common quantization. For a series of numbers provided as floats, e.g., prices from a price source, we'd like to infer what the right quantization that should be used to avoid rounding errors above some threshold. from the numbers. This simple algorithm auto-quantizes all the numbers and quantizes all of them at the maximum precision that would result in rounding under the threshold. Parameters: prices \u2013 A list of float or Decimal prices to infer from. If floats are provided, conversion is done naively. threshold ( float ) \u2013 A fraction, the maximum error to tolerate before stopping the search. Returns: Optional[decimal.Decimal] \u2013 A decimal object to use with decimal.Decimal.quantize(). Source code in beancount/core/number.py def infer_quantum_from_list ( numbers : List [ Decimal ], threshold : float = 0.01 ) -> Optional [ Decimal ]: \"\"\"Given a list of numbers from floats, infer the common quantization. For a series of numbers provided as floats, e.g., prices from a price source, we'd like to infer what the right quantization that should be used to avoid rounding errors above some threshold. from the numbers. This simple algorithm auto-quantizes all the numbers and quantizes all of them at the maximum precision that would result in rounding under the threshold. Args: prices: A list of float or Decimal prices to infer from. If floats are provided, conversion is done naively. threshold: A fraction, the maximum error to tolerate before stopping the search. Returns: A decimal object to use with decimal.Decimal.quantize(). \"\"\" # Auto quantize all the numbers. qnumbers = [ auto_quantize ( num , threshold ) for num in numbers ] exponent = max ( num_fractional_digits ( n ) for n in qnumbers ) return - exponent beancount . core . number . num_fractional_digits ( number ) \uf0c1 Return the number of fractional digits. Source code in beancount/core/number.py def num_fractional_digits ( number : Decimal ) -> int : \"\"\"Return the number of fractional digits.\"\"\" return - number . as_tuple () . exponent beancount . core . number . round_to ( number , increment ) \uf0c1 Round a number down to a particular increment. Parameters: number \u2013 A Decimal, the number to be rounded. increment \u2013 A Decimal, the size of the increment. Returns: A Decimal, the rounded number. Source code in beancount/core/number.py def round_to ( number , increment ): \"\"\"Round a number *down* to a particular increment. Args: number: A Decimal, the number to be rounded. increment: A Decimal, the size of the increment. Returns: A Decimal, the rounded number. \"\"\" return int (( number / increment )) * increment beancount . core . number . same_sign ( number1 , number2 ) \uf0c1 Return true if both numbers have the same sign. Parameters: number1 \u2013 An instance of Decimal. number2 \u2013 An instance of Decimal. Returns: A boolean. Source code in beancount/core/number.py def same_sign ( number1 , number2 ): \"\"\"Return true if both numbers have the same sign. Args: number1: An instance of Decimal. number2: An instance of Decimal. Returns: A boolean. \"\"\" return ( number1 >= 0 ) == ( number2 >= 0 ) beancount.core.position \uf0c1 A position object, which consists of units Amount and cost Cost. See types below for details. beancount.core.position.Cost ( tuple ) \uf0c1 Cost(number, currency, date, label) beancount . core . position . Cost . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/position.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . position . Cost . __new__ ( _cls , number , currency , date , label ) special staticmethod \uf0c1 Create new instance of Cost(number, currency, date, label) beancount . core . position . Cost . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/position.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.position.CostSpec ( tuple ) \uf0c1 CostSpec(number_per, number_total, currency, date, label, merge) beancount . core . position . CostSpec . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/position.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . core . position . CostSpec . __new__ ( _cls , number_per , number_total , currency , date , label , merge ) special staticmethod \uf0c1 Create new instance of CostSpec(number_per, number_total, currency, date, label, merge) beancount . core . position . CostSpec . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/core/position.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.core.position.Position ( _Position ) \uf0c1 A 'Position' is a pair of units and optional cost. This is used to track inventories. Attributes: Name Type Description units Amount An Amount, the number of units and its currency. cost Cost A Cost that represents the lot, or None. beancount . core . position . Position . __abs__ ( self ) special \uf0c1 Return the absolute value of the position. Returns: An instance of Position with the absolute units. Source code in beancount/core/position.py def __abs__ ( self ): \"\"\"Return the absolute value of the position. Returns: An instance of Position with the absolute units. \"\"\" return Position ( amount_abs ( self . units ), self . cost ) beancount . core . position . Position . __copy__ ( self ) special \uf0c1 Shallow copy, except for the lot, which can be shared. This is important for performance reasons; a lot of time is spent here during balancing. Returns: A shallow copy of this position. Source code in beancount/core/position.py def __copy__ ( self ): \"\"\"Shallow copy, except for the lot, which can be shared. This is important for performance reasons; a lot of time is spent here during balancing. Returns: A shallow copy of this position. \"\"\" # Note: We use Decimal() for efficiency. return Position ( copy . copy ( self . units ), copy . copy ( self . cost )) beancount . core . position . Position . __eq__ ( self , other ) special \uf0c1 Equality comparison with another Position. The objects are considered equal if both number and lot are matching, and if the number of units is zero and the other position is None, that is also okay. Parameters: other \u2013 An instance of Position, or None. Returns: A boolean, true if the positions are equal. Source code in beancount/core/position.py def __eq__ ( self , other ): \"\"\"Equality comparison with another Position. The objects are considered equal if both number and lot are matching, and if the number of units is zero and the other position is None, that is also okay. Args: other: An instance of Position, or None. Returns: A boolean, true if the positions are equal. \"\"\" return ( self . units . number == ZERO if other is None else ( self . units == other . units and self . cost == other . cost ) ) beancount . core . position . Position . __hash__ ( self ) special \uf0c1 Compute a hash for this position. Returns: A hash of this position object. Source code in beancount/core/position.py def __hash__ ( self ): \"\"\"Compute a hash for this position. Returns: A hash of this position object. \"\"\" return hash (( self . units , self . cost )) beancount . core . position . Position . __lt__ ( self , other ) special \uf0c1 A less-than comparison operator for positions. Parameters: other \u2013 Another instance of Position. Returns: True if this positions is smaller than the other position. Source code in beancount/core/position.py def __lt__ ( self , other ): \"\"\"A less-than comparison operator for positions. Args: other: Another instance of Position. Returns: True if this positions is smaller than the other position. \"\"\" return self . sortkey () < other . sortkey () beancount . core . position . Position . __mul__ ( self , scalar ) special \uf0c1 Scale/multiply the contents of the position. Parameters: scalar \u2013 A Decimal. Returns: An instance of Inventory. Source code in beancount/core/position.py def __mul__ ( self , scalar ): \"\"\"Scale/multiply the contents of the position. Args: scalar: A Decimal. Returns: An instance of Inventory. \"\"\" return Position ( amount_mul ( self . units , scalar ), self . cost ) beancount . core . position . Position . __neg__ ( self ) special \uf0c1 Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. Source code in beancount/core/position.py def get_negative ( self ): \"\"\"Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. \"\"\" # Note: We use Decimal() for efficiency. return Position ( - self . units , self . cost ) beancount . core . position . Position . __new__ ( cls , units , cost = None ) special staticmethod \uf0c1 Create new instance of _Position(units, cost) Source code in beancount/core/position.py def __new__ ( cls , units , cost = None ): assert isinstance ( units , Amount ), \"Expected an Amount for units; received ' {} '\" . format ( units ) assert cost is None or isinstance ( cost , Position . cost_types ), \"Expected a Cost for cost; received ' {} '\" . format ( cost ) return _Position . __new__ ( cls , units , cost ) beancount . core . position . Position . __repr__ ( self ) special \uf0c1 Return a string representation of the position. Returns: A string, a printable representation of the position. Source code in beancount/core/position.py def __str__ ( self ): \"\"\"Return a string representation of the position. Returns: A string, a printable representation of the position. \"\"\" return self . to_string () beancount . core . position . Position . __str__ ( self ) special \uf0c1 Return a string representation of the position. Returns: A string, a printable representation of the position. Source code in beancount/core/position.py def __str__ ( self ): \"\"\"Return a string representation of the position. Returns: A string, a printable representation of the position. \"\"\" return self . to_string () beancount . core . position . Position . currency_pair ( self ) \uf0c1 Return the currency pair associated with this position. Returns: A pair of a currency string and a cost currency string or None. Source code in beancount/core/position.py def currency_pair ( self ): \"\"\"Return the currency pair associated with this position. Returns: A pair of a currency string and a cost currency string or None. \"\"\" return ( self . units . currency , self . cost . currency if self . cost else None ) beancount . core . position . Position . from_amounts ( units , cost_amount = None ) staticmethod \uf0c1 Create a position from an amount and a cost. Parameters: amount \u2013 An amount, that represents the number of units and the lot currency. cost_amount \u2013 If not None, represents the cost amount. Returns: A Position instance. Source code in beancount/core/position.py @staticmethod def from_amounts ( units , cost_amount = None ): \"\"\"Create a position from an amount and a cost. Args: amount: An amount, that represents the number of units and the lot currency. cost_amount: If not None, represents the cost amount. Returns: A Position instance. \"\"\" assert cost_amount is None or isinstance ( cost_amount , Amount ), \"Invalid type for cost: {} \" . format ( cost_amount ) cost = ( Cost ( cost_amount . number , cost_amount . currency , None , None ) if cost_amount else None ) return Position ( units , cost ) beancount . core . position . Position . from_string ( string ) staticmethod \uf0c1 Create a position from a string specification. This is a miniature parser used for building tests. Parameters: string \u2013 A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. Source code in beancount/core/position.py @staticmethod def from_string ( string ): \"\"\"Create a position from a string specification. This is a miniature parser used for building tests. Args: string: A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. \"\"\" match = re . match ( ( r \"\\s*( {} )\\s+( {} )\" r \"(?:\\s+{{([^}}]*)}})?\" r \"\\s*$\" ) . format ( NUMBER_RE , CURRENCY_RE ), string , ) if not match : raise ValueError ( \"Invalid string for position: ' {} '\" . format ( string )) number = D ( match . group ( 1 )) currency = match . group ( 2 ) # Parse a cost expression. cost_number = None cost_currency = None date = None label = None cost_expression = match . group ( 3 ) if match . group ( 3 ): expressions = [ expr . strip () for expr in re . split ( \"[,/]\" , cost_expression )] for expr in expressions : # Match a compound number. match = re . match ( r \"( {NUMBER_RE} )\\s*(?:#\\s*( {NUMBER_RE} ))?\\s+( {CURRENCY_RE} )$\" . format ( NUMBER_RE = NUMBER_RE , CURRENCY_RE = CURRENCY_RE ), expr , ) if match : per_number , total_number , cost_currency = match . group ( 1 , 2 , 3 ) per_number = D ( per_number ) if per_number else ZERO total_number = D ( total_number ) if total_number else ZERO if total_number : # Calculate the per-unit cost. total = number * per_number + total_number per_number = total / number cost_number = per_number continue # Match a date. match = re . match ( r \"(\\d\\d\\d\\d)[-/](\\d\\d)[-/](\\d\\d)$\" , expr ) if match : date = datetime . date ( * map ( int , match . group ( 1 , 2 , 3 ))) continue # Match a label. match = re . match ( r '\"([^\"]+)*\"$' , expr ) if match : label = match . group ( 1 ) continue # Match a merge-cost marker. match = re . match ( r \"\\*$\" , expr ) if match : raise ValueError ( \"Merge-code not supported in string constructor.\" ) raise ValueError ( \"Invalid cost component: ' {} '\" . format ( expr )) cost = Cost ( cost_number , cost_currency , date , label ) else : cost = None return Position ( Amount ( number , currency ), cost ) beancount . core . position . Position . get_negative ( self ) \uf0c1 Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. Source code in beancount/core/position.py def get_negative ( self ): \"\"\"Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. \"\"\" # Note: We use Decimal() for efficiency. return Position ( - self . units , self . cost ) beancount . core . position . Position . is_negative_at_cost ( self ) \uf0c1 Return true if the position is held at cost and negative. Returns: A boolean. Source code in beancount/core/position.py def is_negative_at_cost ( self ): \"\"\"Return true if the position is held at cost and negative. Returns: A boolean. \"\"\" return self . units . number < ZERO and self . cost is not None beancount . core . position . Position . sortkey ( self ) \uf0c1 Return a key to sort positions by. This key depends on the order of the currency of the lot (we want to order common currencies first) and the number of units. Returns: A tuple, used to sort lists of positions. Source code in beancount/core/position.py def sortkey ( self ): \"\"\"Return a key to sort positions by. This key depends on the order of the currency of the lot (we want to order common currencies first) and the number of units. Returns: A tuple, used to sort lists of positions. \"\"\" currency = self . units . currency order_units = CURRENCY_ORDER . get ( currency , NCURRENCIES + len ( currency )) if self . cost is not None : cost_number = self . cost . number cost_currency = self . cost . currency else : cost_number = ZERO cost_currency = \"\" return ( order_units , cost_number , cost_currency , self . units . number ) beancount . core . position . Position . to_string ( self , dformat =< beancount . core . display_context . DisplayFormatter object at 0x78fcde6b7290 > , detail = True ) \uf0c1 Render the position to a string.See to_string() for details. Source code in beancount/core/position.py def to_string ( self , dformat = DEFAULT_FORMATTER , detail = True ): \"\"\"Render the position to a string.See to_string() for details.\"\"\" return to_string ( self , dformat , detail ) beancount . core . position . cost_to_str ( cost , dformat , detail = True ) \uf0c1 Format an instance of Cost or a CostSpec to a string. Parameters: cost \u2013 An instance of Cost or CostSpec. dformat \u2013 A DisplayFormatter object. detail \u2013 A boolean, true if we should render the non-amount components. Returns: A string, suitable for formatting. Source code in beancount/core/position.py def cost_to_str ( cost , dformat , detail = True ): \"\"\"Format an instance of Cost or a CostSpec to a string. Args: cost: An instance of Cost or CostSpec. dformat: A DisplayFormatter object. detail: A boolean, true if we should render the non-amount components. Returns: A string, suitable for formatting. \"\"\" strlist = [] if isinstance ( cost , Cost ): if isinstance ( cost . number , Decimal ): strlist . append ( Amount ( cost . number , cost . currency ) . to_string ( dformat )) if detail : if cost . date : strlist . append ( cost . date . isoformat ()) if cost . label : strlist . append ( '\" {} \"' . format ( cost . label )) elif isinstance ( cost , CostSpec ): if isinstance ( cost . number_per , Decimal ) or isinstance ( cost . number_total , Decimal ): amountlist = [] if isinstance ( cost . number_per , Decimal ): amountlist . append ( dformat . format ( cost . number_per )) if isinstance ( cost . number_total , Decimal ): amountlist . append ( \"#\" ) amountlist . append ( dformat . format ( cost . number_total )) if isinstance ( cost . currency , str ): amountlist . append ( cost . currency ) strlist . append ( \" \" . join ( amountlist )) if detail : if cost . date : strlist . append ( cost . date . isoformat ()) if cost . label : strlist . append ( '\" {} \"' . format ( cost . label )) if cost . merge : strlist . append ( \"*\" ) return \", \" . join ( strlist ) beancount . core . position . from_amounts ( units , cost_amount = None ) \uf0c1 Create a position from an amount and a cost. Parameters: amount \u2013 An amount, that represents the number of units and the lot currency. cost_amount \u2013 If not None, represents the cost amount. Returns: A Position instance. Source code in beancount/core/position.py @staticmethod def from_amounts ( units , cost_amount = None ): \"\"\"Create a position from an amount and a cost. Args: amount: An amount, that represents the number of units and the lot currency. cost_amount: If not None, represents the cost amount. Returns: A Position instance. \"\"\" assert cost_amount is None or isinstance ( cost_amount , Amount ), \"Invalid type for cost: {} \" . format ( cost_amount ) cost = ( Cost ( cost_amount . number , cost_amount . currency , None , None ) if cost_amount else None ) return Position ( units , cost ) beancount . core . position . from_string ( string ) \uf0c1 Create a position from a string specification. This is a miniature parser used for building tests. Parameters: string \u2013 A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. Source code in beancount/core/position.py @staticmethod def from_string ( string ): \"\"\"Create a position from a string specification. This is a miniature parser used for building tests. Args: string: A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. \"\"\" match = re . match ( ( r \"\\s*( {} )\\s+( {} )\" r \"(?:\\s+{{([^}}]*)}})?\" r \"\\s*$\" ) . format ( NUMBER_RE , CURRENCY_RE ), string , ) if not match : raise ValueError ( \"Invalid string for position: ' {} '\" . format ( string )) number = D ( match . group ( 1 )) currency = match . group ( 2 ) # Parse a cost expression. cost_number = None cost_currency = None date = None label = None cost_expression = match . group ( 3 ) if match . group ( 3 ): expressions = [ expr . strip () for expr in re . split ( \"[,/]\" , cost_expression )] for expr in expressions : # Match a compound number. match = re . match ( r \"( {NUMBER_RE} )\\s*(?:#\\s*( {NUMBER_RE} ))?\\s+( {CURRENCY_RE} )$\" . format ( NUMBER_RE = NUMBER_RE , CURRENCY_RE = CURRENCY_RE ), expr , ) if match : per_number , total_number , cost_currency = match . group ( 1 , 2 , 3 ) per_number = D ( per_number ) if per_number else ZERO total_number = D ( total_number ) if total_number else ZERO if total_number : # Calculate the per-unit cost. total = number * per_number + total_number per_number = total / number cost_number = per_number continue # Match a date. match = re . match ( r \"(\\d\\d\\d\\d)[-/](\\d\\d)[-/](\\d\\d)$\" , expr ) if match : date = datetime . date ( * map ( int , match . group ( 1 , 2 , 3 ))) continue # Match a label. match = re . match ( r '\"([^\"]+)*\"$' , expr ) if match : label = match . group ( 1 ) continue # Match a merge-cost marker. match = re . match ( r \"\\*$\" , expr ) if match : raise ValueError ( \"Merge-code not supported in string constructor.\" ) raise ValueError ( \"Invalid cost component: ' {} '\" . format ( expr )) cost = Cost ( cost_number , cost_currency , date , label ) else : cost = None return Position ( Amount ( number , currency ), cost ) beancount . core . position . get_position ( posting ) \uf0c1 Build a Position instance from a Posting instance. Parameters: posting \u2013 An instance of Posting. Returns: An instance of Position. Source code in beancount/core/position.py def get_position ( posting ): \"\"\"Build a Position instance from a Posting instance. Args: posting: An instance of Posting. Returns: An instance of Position. \"\"\" return Position ( posting . units , posting . cost ) beancount . core . position . to_string ( pos , dformat =< beancount . core . display_context . DisplayFormatter object at 0x78fcde6b7290 > , detail = True ) \uf0c1 Render the Position or Posting instance to a string. Parameters: pos \u2013 An instance of Position or Posting. dformat \u2013 An instance of DisplayFormatter. detail \u2013 A boolean, true if we should only render the lot details beyond the cost (lot-date, label, etc.). If false, we only render the cost, if present. Returns: A string, the rendered position. Source code in beancount/core/position.py def to_string ( pos , dformat = DEFAULT_FORMATTER , detail = True ): \"\"\"Render the Position or Posting instance to a string. Args: pos: An instance of Position or Posting. dformat: An instance of DisplayFormatter. detail: A boolean, true if we should only render the lot details beyond the cost (lot-date, label, etc.). If false, we only render the cost, if present. Returns: A string, the rendered position. \"\"\" pos_str = pos . units . to_string ( dformat ) if pos . cost is not None : pos_str = \" {} {{ {} }}\" . format ( pos_str , cost_to_str ( pos . cost , dformat , detail )) return pos_str beancount.core.prices \uf0c1 This module has code that can build a database of historical prices at various times, from which unrealized capital gains and market value can be deduced. Prices are deduced from Price entries found in the file, or perhaps created by scripts (for example you could build a script that will fetch live prices online and create entries on-the-fly). beancount.core.prices.PriceMap ( dict ) \uf0c1 A price map dictionary. The keys include both the set of forward (base, quote) pairs and their inverse. In order to determine which are the forward pairs, access the 'forward_pairs' attribute Attributes: Name Type Description forward_pairs A list of (base, quote) keys for the forward pairs. beancount . core . prices . build_price_map ( entries ) \uf0c1 Build a price map from a list of arbitrary entries. If multiple prices are found for the same (currency, cost-currency) pair at the same date, the latest date is kept and the earlier ones (for that day) are discarded. If inverse price pairs are found, e.g. USD in AUD and AUD in USD, the inverse that has the smallest number of price points is converted into the one that has the most price points. In that way they are reconciled into a single one. Parameters: entries \u2013 A list of directives, hopefully including some Price and/or Returns: A dict of (currency, cost-currency) keys to sorted lists of (date, number) pairs, where 'date' is the date the price occurs at and 'number' a Decimal that represents the price, or rate, between these two currencies/commodities. Each date occurs only once in the sorted list of prices of a particular key. All of the inverses are automatically generated in the price map. Source code in beancount/core/prices.py def build_price_map ( entries ): \"\"\"Build a price map from a list of arbitrary entries. If multiple prices are found for the same (currency, cost-currency) pair at the same date, the latest date is kept and the earlier ones (for that day) are discarded. If inverse price pairs are found, e.g. USD in AUD and AUD in USD, the inverse that has the smallest number of price points is converted into the one that has the most price points. In that way they are reconciled into a single one. Args: entries: A list of directives, hopefully including some Price and/or Transaction entries. Returns: A dict of (currency, cost-currency) keys to sorted lists of (date, number) pairs, where 'date' is the date the price occurs at and 'number' a Decimal that represents the price, or rate, between these two currencies/commodities. Each date occurs only once in the sorted list of prices of a particular key. All of the inverses are automatically generated in the price map. \"\"\" # Fetch a list of all the price entries seen in the ledger. price_entries = [ entry for entry in entries if isinstance ( entry , Price )] # Build a map of exchange rates between these units. # (base-currency, quote-currency) -> List of (date, rate). price_map = collections . defaultdict ( list ) for price in price_entries : base_quote = ( price . currency , price . amount . currency ) price_map [ base_quote ] . append (( price . date , price . amount . number )) # Find pairs of inversed units. inversed_units = [] for base_quote , values in price_map . items (): base , quote = base_quote if ( quote , base ) in price_map : inversed_units . append ( base_quote ) # Find pairs of inversed units, and swallow the conversion with the smaller # number of rates into the other one. for base , quote in inversed_units : bq_prices = price_map [( base , quote )] qb_prices = price_map [( quote , base )] remove = ( base , quote ) if len ( bq_prices ) < len ( qb_prices ) else ( quote , base ) base , quote = remove remove_list = price_map [ remove ] insert_list = price_map [( quote , base )] del price_map [ remove ] inverted_list = [( date , ONE / rate ) for ( date , rate ) in remove_list if rate != ZERO ] insert_list . extend ( inverted_list ) # Unzip and sort each of the entries and eliminate duplicates on the date. sorted_price_map = PriceMap ( { base_quote : list ( misc_utils . sorted_uniquify ( date_rates , lambda x : x [ 0 ], last = True ) ) for ( base_quote , date_rates ) in price_map . items () } ) # Compute and insert all the inverted rates. forward_pairs = list ( sorted_price_map . keys ()) for ( base , quote ), price_list in list ( sorted_price_map . items ()): # Note: You have to filter out zero prices for zero-cost postings, like # gifted options. sorted_price_map [( quote , base )] = [ ( date , ONE / price ) for date , price in price_list if price != ZERO ] sorted_price_map . forward_pairs = forward_pairs return sorted_price_map beancount . core . prices . get_all_prices ( price_map , base_quote ) \uf0c1 Return a sorted list of all (date, number) price pairs. Parameters: price_map \u2013 A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote \u2013 A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A list of (date, Decimal) pairs, sorted by date. Exceptions: KeyError \u2013 If the base/quote could not be found. Source code in beancount/core/prices.py def get_all_prices ( price_map , base_quote ): \"\"\"Return a sorted list of all (date, number) price pairs. Args: price_map: A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote: A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A list of (date, Decimal) pairs, sorted by date. Raises: KeyError: If the base/quote could not be found. \"\"\" base_quote = normalize_base_quote ( base_quote ) return _lookup_price_and_inverse ( price_map , base_quote ) beancount . core . prices . get_last_price_entries ( entries , date ) \uf0c1 Run through the entries until the given date and return the last Price entry encountered for each (currency, cost-currency) pair. Parameters: entries \u2013 A list of directives. date \u2013 An instance of datetime.date. If None, the very latest price is returned. Returns: A list of price entries. Source code in beancount/core/prices.py def get_last_price_entries ( entries , date ): \"\"\"Run through the entries until the given date and return the last Price entry encountered for each (currency, cost-currency) pair. Args: entries: A list of directives. date: An instance of datetime.date. If None, the very latest price is returned. Returns: A list of price entries. \"\"\" price_entry_map = {} for entry in entries : if date is not None and entry . date >= date : break if isinstance ( entry , Price ): base_quote = ( entry . currency , entry . amount . currency ) price_entry_map [ base_quote ] = entry return sorted ( price_entry_map . values (), key = data . entry_sortkey ) beancount . core . prices . get_latest_price ( price_map , base_quote ) \uf0c1 Return the latest price/rate from a price map for the given base/quote pair. This is often used to just get the 'current' price if you're looking at the entire set of entries. Parameters: price_map \u2013 A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. Returns: A pair of (date, number), where 'date' is a datetime.date instance and 'number' is a Decimal of the price, or rate, at that date. The date is the latest date which we have an available price for in the price map. Source code in beancount/core/prices.py def get_latest_price ( price_map , base_quote ): \"\"\"Return the latest price/rate from a price map for the given base/quote pair. This is often used to just get the 'current' price if you're looking at the entire set of entries. Args: price_map: A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. Returns: A pair of (date, number), where 'date' is a datetime.date instance and 'number' is a Decimal of the price, or rate, at that date. The date is the latest date which we have an available price for in the price map. \"\"\" base_quote = normalize_base_quote ( base_quote ) # Handle the degenerate case of a currency priced into its own. base , quote = base_quote if quote is None or base == quote : return ( None , ONE ) # Look up the list and return the latest element. The lists are assumed to # be sorted. try : price_list = _lookup_price_and_inverse ( price_map , base_quote ) except KeyError : price_list = None if price_list : return price_list [ - 1 ] else : return None , None beancount . core . prices . get_price ( price_map , base_quote , date = None ) \uf0c1 Return the price as of the given date. If the date is unspecified, return the latest price. Parameters: price_map \u2013 A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote \u2013 A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. date \u2013 A datetime.date instance, the date at which we want the conversion rate. Returns: A pair of (datetime.date, Decimal) instance. If no price information could be found, return (None, None). Source code in beancount/core/prices.py def get_price ( price_map , base_quote , date = None ): \"\"\"Return the price as of the given date. If the date is unspecified, return the latest price. Args: price_map: A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote: A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. date: A datetime.date instance, the date at which we want the conversion rate. Returns: A pair of (datetime.date, Decimal) instance. If no price information could be found, return (None, None). \"\"\" if date is None : return get_latest_price ( price_map , base_quote ) base_quote = normalize_base_quote ( base_quote ) # Handle the degenerate case of a currency priced into its own. base , quote = base_quote if quote is None or base == quote : return ( None , ONE ) try : price_list = _lookup_price_and_inverse ( price_map , base_quote ) index = bisect_key . bisect_right_with_key ( price_list , date , key = lambda x : x [ 0 ]) if index == 0 : return None , None else : return price_list [ index - 1 ] except KeyError : return None , None beancount . core . prices . normalize_base_quote ( base_quote ) \uf0c1 Convert a slash-separated string to a pair of strings. Parameters: base_quote \u2013 A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A pair of strings. Source code in beancount/core/prices.py def normalize_base_quote ( base_quote ): \"\"\"Convert a slash-separated string to a pair of strings. Args: base_quote: A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A pair of strings. \"\"\" if isinstance ( base_quote , str ): base_quote_norm = tuple ( base_quote . split ( \"/\" )) assert len ( base_quote_norm ) == 2 , base_quote base_quote = base_quote_norm assert isinstance ( base_quote , tuple ), base_quote return base_quote beancount . core . prices . project ( orig_price_map , from_currency , to_currency , base_currencies = None ) \uf0c1 Project all prices with a quote currency to another quote currency. Say you have a price for HOOL in USD and you'd like to convert HOOL to CAD. If there aren't any (HOOL, CAD) price pairs in the database it will remain unconverted. Projecting from USD to CAD will compute combined rates and insert corresponding prices over all base currencies (like HOOL). In this example, each of the (HOOL, USD) prices would see an inserted (HOOL, CAD) price inserted at the same date. It is common to make these projections when reducing inventories in a ledger that states multiple operating currency pairs, when for example, one wants to compute total value of a set of accounts in one of those currencies. Please note that: Even if the target pair has existing entries, projection will still be applied. For example, is there exist some (HOOL, CAD) prices, the projection in the example above will still insert some new price points to it. However, projected prices colliding existing ones at the same date will not override them. Projection will fail to insert a new price if the conversion between to and from currencies has no existing prices (e.g. before its first price entry). Perhaps most importantly, we only insert price points at dates where the base commodity has a price point. In other words, if we have prices for dates A and C and the rate changes between these dates at date B, we don't synthesize a new price at date B. A more accurate method to get projected prices that takes into account varying rates is to do multiple lookups. We'll eventually add a method to query it via a specified list of intermediate pairs. {c1bd24f8d4b7} Parameters: orig_price_map ( PriceMap ) \u2013 An existing price map. from_currency ( str ) \u2013 The quote currency with existing project points (e.g., USD). to_currency ( str ) \u2013 The quote currency to insert price points for (e.g., CAD). base_currencies ( Optional[Set[str]] ) \u2013 An optional set of commodities to restrict the projections to (e.g., {HOOL}). Returns: PriceMap \u2013 A new price map, with the extra projected prices. The original price map is kept intact. Source code in beancount/core/prices.py def project ( orig_price_map : PriceMap , from_currency : Currency , to_currency : Currency , base_currencies : Optional [ Set [ Currency ]] = None , ) -> PriceMap : \"\"\"Project all prices with a quote currency to another quote currency. Say you have a price for HOOL in USD and you'd like to convert HOOL to CAD. If there aren't any (HOOL, CAD) price pairs in the database it will remain unconverted. Projecting from USD to CAD will compute combined rates and insert corresponding prices over all base currencies (like HOOL). In this example, each of the (HOOL, USD) prices would see an inserted (HOOL, CAD) price inserted at the same date. It is common to make these projections when reducing inventories in a ledger that states multiple operating currency pairs, when for example, one wants to compute total value of a set of accounts in one of those currencies. Please note that: - Even if the target pair has existing entries, projection will still be applied. For example, is there exist some (HOOL, CAD) prices, the projection in the example above will still insert some new price points to it. - However, projected prices colliding existing ones at the same date will not override them. - Projection will fail to insert a new price if the conversion between to and from currencies has no existing prices (e.g. before its first price entry). - Perhaps most importantly, we only insert price points at dates where the base commodity has a price point. In other words, if we have prices for dates A and C and the rate changes between these dates at date B, we don't synthesize a new price at date B. A more accurate method to get projected prices that takes into account varying rates is to do multiple lookups. We'll eventually add a method to query it via a specified list of intermediate pairs. {c1bd24f8d4b7} Args: orig_price_map: An existing price map. from_currency: The quote currency with existing project points (e.g., USD). to_currency: The quote currency to insert price points for (e.g., CAD). base_currencies: An optional set of commodities to restrict the projections to (e.g., {HOOL}). Returns: A new price map, with the extra projected prices. The original price map is kept intact. \"\"\" # If nothing is requested, return the original map. if from_currency == to_currency : return orig_price_map # Avoid mutating the input map. price_map = { key : list ( value ) for key , value in orig_price_map . items ()} # Process the entire database (it's not indexed by quote currency). currency_pair = ( from_currency , to_currency ) for base_quote , prices in list ( price_map . items ()): # Filter just the currencies to convert. base , quote = base_quote if quote != from_currency : continue # Skip currencies not requested if a constraint has been provided. # {4bb702d82c8a} if base_currencies and base not in base_currencies : continue # Create a mapping of existing prices so we can avoid date collisions. existing_prices = ( { date for date , _ in price_map [( base , to_currency )]} if ( base , to_currency ) in price_map else set () ) # Project over each of the prices. new_projected = [] for date , price in prices : rate_date , rate = get_price ( price_map , currency_pair , date ) if rate is None : # There is no conversion rate at this time; skip projection. # {b2b23353275d}. continue if rate_date in existing_prices : # Skip collisions in date. {97a5703ac517} continue # Append the new rate. new_price = price * rate new_projected . append (( date , new_price )) # Make sure the resulting lists are sorted. if new_projected : projected = price_map . setdefault (( base , to_currency ), []) projected . extend ( new_projected ) projected . sort () inverted = price_map . setdefault (( to_currency , base ), []) inverted . extend ( ( date , ZERO if rate == ZERO else ONE / rate ) for date , rate in new_projected ) inverted . sort () return price_map beancount.core.realization \uf0c1 Realization of specific lists of account postings into reports. This code converts a list of entries into a tree of RealAccount nodes (which stands for \"realized accounts\"). The RealAccount objects contain lists of Posting instances instead of Transactions, or other entry types that are attached to an account, such as a Balance or Note entry. The interface of RealAccount corresponds to that of a regular Python dict, where the keys are the names of the individual components of an account's name, and the values are always other RealAccount instances. If you want to get an account by long account name, there are helper functions in this module for this purpose (see realization.get(), for instance). RealAccount instances also contain the final balance of that account, resulting from its list of postings. You should not build RealAccount trees yourself; instead, you should filter the list of desired directives to display and call the realize() function with them. beancount.core.realization.RealAccount ( dict ) \uf0c1 A realized account, inserted in a tree, that contains the list of realized entries. Attributes: Name Type Description account A string, the full name of the corresponding account. postings A list of postings associated with this accounting (does not include the postings of children accounts). balance The final balance of the list of postings associated with this account. beancount . core . realization . RealAccount . __eq__ ( self , other ) special \uf0c1 Equality predicate. All attributes are compared. Parameters: other \u2013 Another instance of RealAccount. Returns: A boolean, True if the two real accounts are equal. Source code in beancount/core/realization.py def __eq__ ( self , other ): \"\"\"Equality predicate. All attributes are compared. Args: other: Another instance of RealAccount. Returns: A boolean, True if the two real accounts are equal. \"\"\" return ( dict . __eq__ ( self , other ) and self . account == other . account and self . balance == other . balance and self . txn_postings == other . txn_postings ) beancount . core . realization . RealAccount . __init__ ( self , account_name , * args , ** kwargs ) special \uf0c1 Create a RealAccount instance. Parameters: account_name \u2013 a string, the name of the account. Maybe not be None. Source code in beancount/core/realization.py def __init__ ( self , account_name , * args , ** kwargs ): \"\"\"Create a RealAccount instance. Args: account_name: a string, the name of the account. Maybe not be None. \"\"\" super () . __init__ ( * args , ** kwargs ) assert isinstance ( account_name , str ) self . account = account_name self . txn_postings = [] self . balance = inventory . Inventory () beancount . core . realization . RealAccount . __ne__ ( self , other ) special \uf0c1 Not-equality predicate. See eq . Parameters: other \u2013 Another instance of RealAccount. Returns: A boolean, True if the two real accounts are not equal. Source code in beancount/core/realization.py def __ne__ ( self , other ): \"\"\"Not-equality predicate. See __eq__. Args: other: Another instance of RealAccount. Returns: A boolean, True if the two real accounts are not equal. \"\"\" return not self . __eq__ ( other ) beancount . core . realization . RealAccount . __setitem__ ( self , key , value ) special \uf0c1 Prevent the setting of non-string or non-empty keys on this dict. Parameters: key \u2013 The dictionary key. Must be a string. value \u2013 The value, must be a RealAccount instance. Exceptions: KeyError \u2013 If the key is not a string, or is invalid. ValueError \u2013 If the value is not a RealAccount instance. Source code in beancount/core/realization.py def __setitem__ ( self , key , value ): \"\"\"Prevent the setting of non-string or non-empty keys on this dict. Args: key: The dictionary key. Must be a string. value: The value, must be a RealAccount instance. Raises: KeyError: If the key is not a string, or is invalid. ValueError: If the value is not a RealAccount instance. \"\"\" if not isinstance ( key , str ) or not key : raise KeyError ( \"Invalid RealAccount key: ' {} '\" . format ( key )) if not isinstance ( value , RealAccount ): raise ValueError ( \"Invalid RealAccount value: ' {} '\" . format ( value )) if not value . account . endswith ( key ): raise ValueError ( \"RealAccount name ' {} ' inconsistent with key: ' {} '\" . format ( value . account , key ) ) return super () . __setitem__ ( key , value ) beancount . core . realization . RealAccount . copy ( self ) \uf0c1 Override dict.copy() to clone a RealAccount. This is only necessary to correctly implement the copy method. Otherwise, calling .copy() on a RealAccount instance invokes the base class' method, which return just a dict. Returns: A cloned instance of RealAccount, with all members shallow-copied. Source code in beancount/core/realization.py def copy ( self ): \"\"\"Override dict.copy() to clone a RealAccount. This is only necessary to correctly implement the copy method. Otherwise, calling .copy() on a RealAccount instance invokes the base class' method, which return just a dict. Returns: A cloned instance of RealAccount, with all members shallow-copied. \"\"\" return copy . copy ( self ) beancount . core . realization . compute_balance ( real_account , leaf_only = False ) \uf0c1 Compute the total balance of this account and all its subaccounts. Parameters: real_account \u2013 A RealAccount instance. leaf_only \u2013 A boolean flag, true if we should yield only leaves. Returns: An Inventory. Source code in beancount/core/realization.py def compute_balance ( real_account , leaf_only = False ): \"\"\"Compute the total balance of this account and all its subaccounts. Args: real_account: A RealAccount instance. leaf_only: A boolean flag, true if we should yield only leaves. Returns: An Inventory. \"\"\" return functools . reduce ( operator . add , [ ra . balance for ra in iter_children ( real_account , leaf_only )] ) beancount . core . realization . compute_postings_balance ( txn_postings ) \uf0c1 Compute the balance of a list of Postings's or TxnPosting's positions. Parameters: postings \u2013 A list of Posting instances and other directives (which are skipped). Returns: An Inventory. Source code in beancount/core/realization.py def compute_postings_balance ( txn_postings ): \"\"\"Compute the balance of a list of Postings's or TxnPosting's positions. Args: postings: A list of Posting instances and other directives (which are skipped). Returns: An Inventory. \"\"\" final_balance = inventory . Inventory () for txn_posting in txn_postings : if isinstance ( txn_posting , Posting ): final_balance . add_position ( txn_posting ) elif isinstance ( txn_posting , TxnPosting ): final_balance . add_position ( txn_posting . posting ) return final_balance beancount . core . realization . contains ( real_account , account_name ) \uf0c1 True if the given account node contains the subaccount name. Parameters: account_name \u2013 A string, the name of a direct or indirect subaccount of this node. Returns: A boolean, true the name is a child of this node. Source code in beancount/core/realization.py def contains ( real_account , account_name ): \"\"\"True if the given account node contains the subaccount name. Args: account_name: A string, the name of a direct or indirect subaccount of this node. Returns: A boolean, true the name is a child of this node. \"\"\" return get ( real_account , account_name ) is not None beancount . core . realization . dump ( root_account ) \uf0c1 Convert a RealAccount node to a line of lines. Note: this is not meant to be used to produce text reports; the reporting code should produce an intermediate object that contains the structure of it, which can then be rendered to ASCII, HTML or CSV formats. This is intended as a convenient little function for dumping trees of data for debugging purposes. Parameters: root_account \u2013 A RealAccount instance. Returns: A list of tuples of (first_line, continuation_line, real_account) where first_line \u2013 A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. real_account: The RealAccount instance which corresponds to this line. Source code in beancount/core/realization.py def dump ( root_account ): \"\"\"Convert a RealAccount node to a line of lines. Note: this is not meant to be used to produce text reports; the reporting code should produce an intermediate object that contains the structure of it, which can then be rendered to ASCII, HTML or CSV formats. This is intended as a convenient little function for dumping trees of data for debugging purposes. Args: root_account: A RealAccount instance. Returns: A list of tuples of (first_line, continuation_line, real_account) where first_line: A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. real_account: The RealAccount instance which corresponds to this line. \"\"\" # Compute all the lines ahead of time in order to calculate the width. lines = [] # Start with the root node. We push the constant prefix before this node, # the account name, and the RealAccount instance. We will maintain a stack # of children nodes to render. stack = [( \"\" , root_account . account , root_account , True )] while stack : prefix , name , real_account , is_last = stack . pop ( - 1 ) if real_account is root_account : # For the root node, we don't want to render any prefix. first = cont = \"\" else : # Compute the string that precedes the name directly and the one below # that for the continuation lines. # | # @@@ Bank1 <---------------- # @@@ | # | |-- Checking if is_last : first = prefix + PREFIX_LEAF_1 cont = prefix + PREFIX_LEAF_C else : first = prefix + PREFIX_CHILD_1 cont = prefix + PREFIX_CHILD_C # Compute the name to render for continuation lines. # | # |-- Bank1 # | @@@ <---------------- # | |-- Checking if len ( real_account ) > 0 : cont_name = PREFIX_CHILD_C else : cont_name = PREFIX_LEAF_C # Add a line for this account. if not ( real_account is root_account and not name ): lines . append (( first + name , cont + cont_name , real_account )) # Push the children onto the stack, being careful with ordering and # marking the last node as such. child_items = sorted ( real_account . items (), reverse = True ) if child_items : child_iter = iter ( child_items ) child_name , child_account = next ( child_iter ) stack . append (( cont , child_name , child_account , True )) for child_name , child_account in child_iter : stack . append (( cont , child_name , child_account , False )) if not lines : return lines # Compute the maximum width of the lines and convert all of them to the same # maximal width. This makes it easy on the client. max_width = max ( len ( first_line ) for first_line , _ , __ in lines ) line_format = \"{{: {width} }}\" . format ( width = max_width ) return [ ( line_format . format ( first_line ), line_format . format ( cont_line ), real_node ) for ( first_line , cont_line , real_node ) in lines ] beancount . core . realization . dump_balances ( real_root , dformat , at_cost = False , fullnames = False , file = None ) \uf0c1 Dump a realization tree with balances. Parameters: real_root \u2013 An instance of RealAccount. dformat \u2013 An instance of DisplayFormatter to format the numbers with. at_cost \u2013 A boolean, if true, render the values at cost. fullnames \u2013 A boolean, if true, don't render a tree of accounts and render the full account names. file \u2013 A file object to dump the output to. If not specified, we return the output as a string. Returns: A string, the rendered tree, or nothing, if 'file' was provided. Source code in beancount/core/realization.py def dump_balances ( real_root , dformat , at_cost = False , fullnames = False , file = None ): \"\"\"Dump a realization tree with balances. Args: real_root: An instance of RealAccount. dformat: An instance of DisplayFormatter to format the numbers with. at_cost: A boolean, if true, render the values at cost. fullnames: A boolean, if true, don't render a tree of accounts and render the full account names. file: A file object to dump the output to. If not specified, we return the output as a string. Returns: A string, the rendered tree, or nothing, if 'file' was provided. \"\"\" if fullnames : # Compute the maximum account name length; maxlen = max ( len ( real_child . account ) for real_child in iter_children ( real_root , leaf_only = True ) ) line_format = \"{{: {width} }} {{}} \\n \" . format ( width = maxlen ) else : line_format = \" {} {} \\n \" output = file or io . StringIO () for first_line , cont_line , real_account in dump ( real_root ): if not real_account . balance . is_empty (): if at_cost : rinv = real_account . balance . reduce ( convert . get_cost ) else : rinv = real_account . balance . reduce ( convert . get_units ) amounts = [ position . units for position in rinv . get_positions ()] positions = [ amount_ . to_string ( dformat ) for amount_ in sorted ( amounts , key = amount . sortkey ) ] else : positions = [ \"\" ] if fullnames : for position in positions : if not position and len ( real_account ) > 0 : continue # Skip parent accounts with no position to render. output . write ( line_format . format ( real_account . account , position )) else : line = first_line for position in positions : output . write ( line_format . format ( line , position )) line = cont_line if file is None : return output . getvalue () beancount . core . realization . filter ( real_account , predicate ) \uf0c1 Filter a RealAccount tree of nodes by the predicate. This function visits the tree and applies the predicate on each node. It returns a partial clone of RealAccount whereby on each node - either the predicate is true, or - for at least one child of the node the predicate is true. All the leaves have the predicate be true. Parameters: real_account \u2013 An instance of RealAccount. predicate \u2013 A callable/function which accepts a RealAccount and returns a boolean. If the function returns True, the node is kept. Returns: A shallow clone of RealAccount is always returned. Source code in beancount/core/realization.py def filter ( real_account , predicate ): \"\"\"Filter a RealAccount tree of nodes by the predicate. This function visits the tree and applies the predicate on each node. It returns a partial clone of RealAccount whereby on each node - either the predicate is true, or - for at least one child of the node the predicate is true. All the leaves have the predicate be true. Args: real_account: An instance of RealAccount. predicate: A callable/function which accepts a RealAccount and returns a boolean. If the function returns True, the node is kept. Returns: A shallow clone of RealAccount is always returned. \"\"\" assert isinstance ( real_account , RealAccount ) real_copy = RealAccount ( real_account . account ) real_copy . balance = real_account . balance real_copy . txn_postings = real_account . txn_postings for child_name , real_child in real_account . items (): real_child_copy = filter ( real_child , predicate ) if real_child_copy is not None : real_copy [ child_name ] = real_child_copy if len ( real_copy ) > 0 or predicate ( real_account ): return real_copy beancount . core . realization . find_last_active_posting ( txn_postings ) \uf0c1 Look at the end of the list of postings, and find the last posting or entry that is not an automatically added directive. Note that if the account is closed, the last posting is assumed to be a Close directive (this is the case if the input is valid and checks without errors. Parameters: txn_postings \u2013 a list of postings or entries. Returns: An entry, or None, if the input list was empty. Source code in beancount/core/realization.py def find_last_active_posting ( txn_postings ): \"\"\"Look at the end of the list of postings, and find the last posting or entry that is not an automatically added directive. Note that if the account is closed, the last posting is assumed to be a Close directive (this is the case if the input is valid and checks without errors. Args: txn_postings: a list of postings or entries. Returns: An entry, or None, if the input list was empty. \"\"\" for txn_posting in reversed ( txn_postings ): assert not isinstance ( txn_posting , Posting ) if not isinstance ( txn_posting , ( TxnPosting , Open , Close , Pad , Balance , Note )): continue return txn_posting beancount . core . realization . get ( real_account , account_name , default = None ) \uf0c1 Fetch the subaccount name from the real_account node. Parameters: real_account \u2013 An instance of RealAccount, the parent node to look for children of. account_name \u2013 A string, the name of a possibly indirect child leaf found down the tree of 'real_account' nodes. default \u2013 The default value that should be returned if the child subaccount is not found. Returns: A RealAccount instance for the child, or the default value, if the child is not found. Source code in beancount/core/realization.py def get ( real_account , account_name , default = None ): \"\"\"Fetch the subaccount name from the real_account node. Args: real_account: An instance of RealAccount, the parent node to look for children of. account_name: A string, the name of a possibly indirect child leaf found down the tree of 'real_account' nodes. default: The default value that should be returned if the child subaccount is not found. Returns: A RealAccount instance for the child, or the default value, if the child is not found. \"\"\" if not isinstance ( account_name , str ): raise ValueError components = account . split ( account_name ) for component in components : real_child = real_account . get ( component , default ) if real_child is default : return default real_account = real_child return real_account beancount . core . realization . get_or_create ( real_account , account_name ) \uf0c1 Fetch the subaccount name from the real_account node. Parameters: real_account \u2013 An instance of RealAccount, the parent node to look for children of, or create under. account_name \u2013 A string, the name of the direct or indirect child leaf to get or create. Returns: A RealAccount instance for the child, or the default value, if the child is not found. Source code in beancount/core/realization.py def get_or_create ( real_account , account_name ): \"\"\"Fetch the subaccount name from the real_account node. Args: real_account: An instance of RealAccount, the parent node to look for children of, or create under. account_name: A string, the name of the direct or indirect child leaf to get or create. Returns: A RealAccount instance for the child, or the default value, if the child is not found. \"\"\" if not isinstance ( account_name , str ): raise ValueError components = account . split ( account_name ) path = [] for component in components : path . append ( component ) real_child = real_account . get ( component , None ) if real_child is None : real_child = RealAccount ( account . join ( * path )) real_account [ component ] = real_child real_account = real_child return real_account beancount . core . realization . get_postings ( real_account ) \uf0c1 Return a sorted list a RealAccount's postings and children. Parameters: real_account \u2013 An instance of RealAccount. Returns: A list of Posting or directories. Source code in beancount/core/realization.py def get_postings ( real_account ): \"\"\"Return a sorted list a RealAccount's postings and children. Args: real_account: An instance of RealAccount. Returns: A list of Posting or directories. \"\"\" # We accumulate all the postings at once here instead of incrementally # because we need to return them sorted. accumulator = [] for real_child in iter_children ( real_account ): accumulator . extend ( real_child . txn_postings ) accumulator . sort ( key = data . posting_sortkey ) return accumulator beancount . core . realization . index_key ( sequence , value , key , cmp ) \uf0c1 Find the index of the first element in 'sequence' which is equal to 'value'. If 'key' is specified, the value compared to the value returned by this function. If the value is not found, return None. Parameters: sequence \u2013 The sequence to search. value \u2013 The value to search for. key \u2013 A predicate to call to obtain the value to compare against. cmp \u2013 A comparison predicate. Returns: The index of the first element found, or None, if the element was not found. Source code in beancount/core/realization.py def index_key ( sequence , value , key , cmp ): \"\"\"Find the index of the first element in 'sequence' which is equal to 'value'. If 'key' is specified, the value compared to the value returned by this function. If the value is not found, return None. Args: sequence: The sequence to search. value: The value to search for. key: A predicate to call to obtain the value to compare against. cmp: A comparison predicate. Returns: The index of the first element found, or None, if the element was not found. \"\"\" for index , element in enumerate ( sequence ): if cmp ( key ( element ), value ): return index return beancount . core . realization . iter_children ( real_account , leaf_only = False ) \uf0c1 Iterate this account node and all its children, depth-first. Parameters: real_account \u2013 An instance of RealAccount. leaf_only \u2013 A boolean flag, true if we should yield only leaves. Yields: Instances of RealAccount, beginning with this account. The order is undetermined. Source code in beancount/core/realization.py def iter_children ( real_account , leaf_only = False ): \"\"\"Iterate this account node and all its children, depth-first. Args: real_account: An instance of RealAccount. leaf_only: A boolean flag, true if we should yield only leaves. Yields: Instances of RealAccount, beginning with this account. The order is undetermined. \"\"\" if leaf_only : if len ( real_account ) == 0 : yield real_account else : for key , real_child in sorted ( real_account . items ()): for real_subchild in iter_children ( real_child , leaf_only ): yield real_subchild else : yield real_account for key , real_child in sorted ( real_account . items ()): for real_subchild in iter_children ( real_child ): yield real_subchild beancount . core . realization . iterate_with_balance ( txn_postings ) \uf0c1 Iterate over the entries, accumulating the running balance. For each entry, this yields tuples of the form: (entry, postings, change, balance) entry: This is the directive for this line. If the list contained Posting instance, this yields the corresponding Transaction object. postings: A list of postings on this entry that affect the balance. Only the postings encountered in the input list are included; only those affect the balance. If 'entry' is not a Transaction directive, this should always be an empty list. We preserve the original ordering of the postings as they appear in the input list. change: An Inventory object that reflects the total change due to the postings from this entry that appear in the list. For example, if a Transaction has three postings and two are in the input list, the sum of the two postings will be in the 'change' Inventory object. However, the position for the transactions' third posting--the one not included in the input list--will not be in this inventory. balance: An Inventory object that reflects the balance after adding the 'change' inventory due to this entry's transaction. The 'balance' yielded is never None, even for entries that do not affect the balance, that is, with an empty 'change' inventory. It's up to the caller, the one rendering the entry, to decide whether to render this entry's change for a particular entry type. Note that if the input list of postings-or-entries is not in sorted date order, two postings for the same entry appearing twice with a different date in between will cause the entry appear twice. This is correct behavior, and it is expected that in practice this should never happen anyway, because the list of postings or entries should always be sorted. This method attempts to detect this and raises an assertion if this is seen. Parameters: txn_postings \u2013 A list of postings or directive instances. Postings affect the balance; other entries do not. Yields: Tuples of (entry, postings, change, balance) as described above. Source code in beancount/core/realization.py def iterate_with_balance ( txn_postings ): \"\"\"Iterate over the entries, accumulating the running balance. For each entry, this yields tuples of the form: (entry, postings, change, balance) entry: This is the directive for this line. If the list contained Posting instance, this yields the corresponding Transaction object. postings: A list of postings on this entry that affect the balance. Only the postings encountered in the input list are included; only those affect the balance. If 'entry' is not a Transaction directive, this should always be an empty list. We preserve the original ordering of the postings as they appear in the input list. change: An Inventory object that reflects the total change due to the postings from this entry that appear in the list. For example, if a Transaction has three postings and two are in the input list, the sum of the two postings will be in the 'change' Inventory object. However, the position for the transactions' third posting--the one not included in the input list--will not be in this inventory. balance: An Inventory object that reflects the balance *after* adding the 'change' inventory due to this entry's transaction. The 'balance' yielded is never None, even for entries that do not affect the balance, that is, with an empty 'change' inventory. It's up to the caller, the one rendering the entry, to decide whether to render this entry's change for a particular entry type. Note that if the input list of postings-or-entries is not in sorted date order, two postings for the same entry appearing twice with a different date in between will cause the entry appear twice. This is correct behavior, and it is expected that in practice this should never happen anyway, because the list of postings or entries should always be sorted. This method attempts to detect this and raises an assertion if this is seen. Args: txn_postings: A list of postings or directive instances. Postings affect the balance; other entries do not. Yields: Tuples of (entry, postings, change, balance) as described above. \"\"\" # The running balance. running_balance = inventory . Inventory () # Previous date. prev_date = None # A list of entries at the current date. date_entries = [] first = lambda pair : pair [ 0 ] for txn_posting in txn_postings : # Get the posting if we are dealing with one. assert not isinstance ( txn_posting , Posting ) if isinstance ( txn_posting , TxnPosting ): posting = txn_posting . posting entry = txn_posting . txn else : posting = None entry = txn_posting if entry . date != prev_date : assert ( prev_date is None or entry . date > prev_date ), \"Invalid date order for postings: {} > {} \" . format ( prev_date , entry . date ) prev_date = entry . date # Flush the dated entries. for date_entry , date_postings in date_entries : change = inventory . Inventory () if date_postings : # Compute the change due to this transaction and update the # total balance at the same time. for date_posting in date_postings : change . add_position ( date_posting ) running_balance . add_position ( date_posting ) yield date_entry , date_postings , change , running_balance date_entries . clear () assert not date_entries if posting is not None : # De-dup multiple postings on the same transaction entry by # grouping their positions together. index = index_key ( date_entries , entry , first , operator . is_ ) if index is None : date_entries . append (( entry , [ posting ])) else : # We are indeed de-duping! postings = date_entries [ index ][ 1 ] postings . append ( posting ) else : # This is a regular entry; nothing to add/remove. date_entries . append (( entry , [])) # Flush the final dated entries if any, same as above. for date_entry , date_postings in date_entries : change = inventory . Inventory () if date_postings : for date_posting in date_postings : change . add_position ( date_posting ) running_balance . add_position ( date_posting ) yield date_entry , date_postings , change , running_balance date_entries . clear () beancount . core . realization . postings_by_account ( entries ) \uf0c1 Create lists of postings and balances by account. This routine aggregates postings and entries grouping them by account name. The resulting lists contain postings in-lieu of Transaction directives, but the other directives are stored as entries. This yields a list of postings or other entries by account. All references to accounts are taken into account. Parameters: entries \u2013 A list of directives. Returns: A mapping of account name to list of TxnPosting instances or non-Transaction directives, sorted in the same order as the entries. Source code in beancount/core/realization.py def postings_by_account ( entries ): \"\"\"Create lists of postings and balances by account. This routine aggregates postings and entries grouping them by account name. The resulting lists contain postings in-lieu of Transaction directives, but the other directives are stored as entries. This yields a list of postings or other entries by account. All references to accounts are taken into account. Args: entries: A list of directives. Returns: A mapping of account name to list of TxnPosting instances or non-Transaction directives, sorted in the same order as the entries. \"\"\" txn_postings_map = collections . defaultdict ( list ) for entry in entries : if isinstance ( entry , Transaction ): # Insert an entry for each of the postings. for posting in entry . postings : txn_postings_map [ posting . account ] . append ( TxnPosting ( entry , posting )) elif isinstance ( entry , ( Open , Close , Balance , Note , Document )): # Append some other entries in the realized list. txn_postings_map [ entry . account ] . append ( entry ) elif isinstance ( entry , Pad ): # Insert the pad entry in both realized accounts. txn_postings_map [ entry . account ] . append ( entry ) txn_postings_map [ entry . source_account ] . append ( entry ) elif isinstance ( entry , Custom ): # Insert custom entry for each account in its values. for custom_value in entry . values : if custom_value . dtype == account . TYPE : txn_postings_map [ custom_value . value ] . append ( entry ) return txn_postings_map beancount . core . realization . realize ( entries , min_accounts = None , compute_balance = True ) \uf0c1 Group entries by account, into a \"tree\" of realized accounts. RealAccount's are essentially containers for lists of postings and the final balance of each account, and may be non-leaf accounts (used strictly for organizing accounts into a hierarchy). This is then used to issue reports. The lists of postings in each account my be any of the entry types, except for Transaction, whereby Transaction entries are replaced by the specific Posting legs that belong to the account. Here's a simple diagram that summarizes this seemingly complex, but rather simple data structure: +-------------+ postings +------+ | RealAccount |---------->| Open | +-------------+ +------+ | v +------------+ +-------------+ | TxnPosting |---->| Transaction | +------------+ +-------------+ | \\ \\\\ v \\.__ +---------+ +-----+ -------->| Posting | | Pad | +---------+ +-----+ | v +---------+ | Balance | +---------+ | v +-------+ | Close | +-------+ | . Parameters: entries \u2013 A list of directives. min_accounts \u2013 A list of strings, account names to ensure we create, regardless of whether there are postings on those accounts or not. This can be used to ensure the root accounts all exist. compute_balance \u2013 A boolean, true if we should compute the final balance on the realization. Returns: The root RealAccount instance. Source code in beancount/core/realization.py def realize ( entries , min_accounts = None , compute_balance = True ): r \"\"\"Group entries by account, into a \"tree\" of realized accounts. RealAccount's are essentially containers for lists of postings and the final balance of each account, and may be non-leaf accounts (used strictly for organizing accounts into a hierarchy). This is then used to issue reports. The lists of postings in each account my be any of the entry types, except for Transaction, whereby Transaction entries are replaced by the specific Posting legs that belong to the account. Here's a simple diagram that summarizes this seemingly complex, but rather simple data structure: +-------------+ postings +------+ | RealAccount |---------->| Open | +-------------+ +------+ | v +------------+ +-------------+ | TxnPosting |---->| Transaction | +------------+ +-------------+ | \\ \\\\\\ v `\\.__ +---------+ +-----+ `-------->| Posting | | Pad | +---------+ +-----+ | v +---------+ | Balance | +---------+ | v +-------+ | Close | +-------+ | . Args: entries: A list of directives. min_accounts: A list of strings, account names to ensure we create, regardless of whether there are postings on those accounts or not. This can be used to ensure the root accounts all exist. compute_balance: A boolean, true if we should compute the final balance on the realization. Returns: The root RealAccount instance. \"\"\" # Create lists of the entries by account. txn_postings_map = postings_by_account ( entries ) # Create a RealAccount tree and compute the balance for each. real_root = RealAccount ( \"\" ) for account_name , txn_postings in txn_postings_map . items (): real_account = get_or_create ( real_root , account_name ) real_account . txn_postings = txn_postings if compute_balance : real_account . balance = compute_postings_balance ( txn_postings ) # Ensure a minimum set of accounts that should exist. This is typically # called with an instance of AccountTypes to make sure that those exist. if min_accounts : for account_name in min_accounts : get_or_create ( real_root , account_name ) return real_root","title":"beancount.core"},{"location":"api_reference/beancount.core.html#beancountcore","text":"Core basic objects and data structures to represent a list of entries.","title":"beancount.core"},{"location":"api_reference/beancount.core.html#beancount.core.account","text":"Functions that operate on account strings. These account objects are rather simple and dumb; they do not contain the list of their associated postings. This is achieved by building a realization; see realization.py for details.","title":"account"},{"location":"api_reference/beancount.core.html#beancount.core.account.AccountTransformer","text":"Account name transformer. This is used to support Win... huh, filesystems and platforms which do not support colon characters. Attributes: Name Type Description rsep A character string, the new separator to use in link names.","title":"AccountTransformer"},{"location":"api_reference/beancount.core.html#beancount.core.account.AccountTransformer.parse","text":"Convert the transform account name to an account name. Source code in beancount/core/account.py def parse ( self , transformed_name : str ) -> Account : \"Convert the transform account name to an account name.\" return ( transformed_name if self . rsep is None else transformed_name . replace ( self . rsep , sep ) )","title":"parse()"},{"location":"api_reference/beancount.core.html#beancount.core.account.AccountTransformer.render","text":"Convert the account name to a transformed account name. Source code in beancount/core/account.py def render ( self , account_name : Account ) -> str : \"Convert the account name to a transformed account name.\" return account_name if self . rsep is None else account_name . replace ( sep , self . rsep )","title":"render()"},{"location":"api_reference/beancount.core.html#beancount.core.account.commonprefix","text":"Return the common prefix of a list of account names. Parameters: accounts ( Iterable[str] ) \u2013 A sequence of account name strings. Returns: str \u2013 A string, the common parent account. If none, returns an empty string. Source code in beancount/core/account.py def commonprefix ( accounts : Iterable [ Account ]) -> Account : \"\"\"Return the common prefix of a list of account names. Args: accounts: A sequence of account name strings. Returns: A string, the common parent account. If none, returns an empty string. \"\"\" accounts_lists = [ account_ . split ( sep ) for account_ in accounts ] # Note: the os.path.commonprefix() function just happens to work here. # Inspect its code, and even the special case of no common prefix # works well with str.join() below. common_list = path . commonprefix ( accounts_lists ) return sep . join ( common_list )","title":"commonprefix()"},{"location":"api_reference/beancount.core.html#beancount.core.account.has_component","text":"Return true if one of the account contains a given component. Parameters: account_name ( str ) \u2013 A string, an account name. component ( str ) \u2013 A string, a component of an account name. For instance, Food in Expenses:Food:Restaurant . All components are considered. Returns: Boolean \u2013 true if the component is in the account. Note that a component name must be whole, that is NY is not in Expenses:Taxes:StateNY . Source code in beancount/core/account.py def has_component ( account_name : Account , component : str ) -> bool : \"\"\"Return true if one of the account contains a given component. Args: account_name: A string, an account name. component: A string, a component of an account name. For instance, ``Food`` in ``Expenses:Food:Restaurant``. All components are considered. Returns: Boolean: true if the component is in the account. Note that a component name must be whole, that is ``NY`` is not in ``Expenses:Taxes:StateNY``. \"\"\" return bool ( re . search ( \"(^|:) {} (:|$)\" . format ( component ), account_name ))","title":"has_component()"},{"location":"api_reference/beancount.core.html#beancount.core.account.is_valid","text":"Return true if the given string is a valid account name. This does not check for the root account types, just the general syntax. Parameters: string ( str ) \u2013 A string, to be checked for account name pattern. Returns: bool \u2013 A boolean, true if the string has the form of an account's name. Source code in beancount/core/account.py def is_valid ( string : Account ) -> bool : \"\"\"Return true if the given string is a valid account name. This does not check for the root account types, just the general syntax. Args: string: A string, to be checked for account name pattern. Returns: A boolean, true if the string has the form of an account's name. \"\"\" return isinstance ( string , str ) and bool ( regex . match ( \" {} $\" . format ( ACCOUNT_RE ), string ))","title":"is_valid()"},{"location":"api_reference/beancount.core.html#beancount.core.account.join","text":"Join the names with the account separator. Parameters: *components ( Tuple[str] ) \u2013 Strings, the components of an account name. Returns: str \u2013 A string, joined in a single account name. Source code in beancount/core/account.py def join ( * components : Tuple [ str ]) -> Account : \"\"\"Join the names with the account separator. Args: *components: Strings, the components of an account name. Returns: A string, joined in a single account name. \"\"\" return sep . join ( components )","title":"join()"},{"location":"api_reference/beancount.core.html#beancount.core.account.leaf","text":"Get the name of the leaf of this account. Parameters: account_name ( str ) \u2013 A string, the name of the account whose leaf name to return. Returns: str \u2013 A string, the name of the leaf of the account. Source code in beancount/core/account.py def leaf ( account_name : Account ) -> Account : \"\"\"Get the name of the leaf of this account. Args: account_name: A string, the name of the account whose leaf name to return. Returns: A string, the name of the leaf of the account. \"\"\" assert isinstance ( account_name , str ) return account_name . split ( sep )[ - 1 ] if account_name else None","title":"leaf()"},{"location":"api_reference/beancount.core.html#beancount.core.account.parent","text":"Return the name of the parent account of the given account. Parameters: account_name ( str ) \u2013 A string, the name of the account whose parent to return. Returns: str \u2013 A string, the name of the parent account of this account. Source code in beancount/core/account.py def parent ( account_name : Account ) -> Account : \"\"\"Return the name of the parent account of the given account. Args: account_name: A string, the name of the account whose parent to return. Returns: A string, the name of the parent account of this account. \"\"\" assert isinstance ( account_name , str ), account_name if not account_name : return None components = account_name . split ( sep ) components . pop ( - 1 ) return sep . join ( components )","title":"parent()"},{"location":"api_reference/beancount.core.html#beancount.core.account.parent_matcher","text":"Build a predicate that returns whether an account is under the given one. Parameters: account_name ( str ) \u2013 The name of the parent account we want to check for. Returns: Callable[[str], Any] \u2013 A callable, which, when called, will return true if the given account is a child of account_name . Source code in beancount/core/account.py def parent_matcher ( account_name : Account ) -> Callable [[ str ], Any ]: \"\"\"Build a predicate that returns whether an account is under the given one. Args: account_name: The name of the parent account we want to check for. Returns: A callable, which, when called, will return true if the given account is a child of ``account_name``. \"\"\" return re . compile ( r \" {} ($| {} )\" . format ( re . escape ( account_name ), sep )) . match","title":"parent_matcher()"},{"location":"api_reference/beancount.core.html#beancount.core.account.parents","text":"A generator of the names of the parents of this account, including this account. Parameters: account_name ( str ) \u2013 The name of the account we want to start iterating from. Returns: Iterator[str] \u2013 A generator of account name strings. Source code in beancount/core/account.py def parents ( account_name : Account ) -> Iterator [ Account ]: \"\"\"A generator of the names of the parents of this account, including this account. Args: account_name: The name of the account we want to start iterating from. Returns: A generator of account name strings. \"\"\" while account_name : yield account_name account_name = parent ( account_name )","title":"parents()"},{"location":"api_reference/beancount.core.html#beancount.core.account.root","text":"Return the first few components of an account's name. Parameters: num_components ( int ) \u2013 An integer, the number of components to return. account_name ( str ) \u2013 A string, an account name. Returns: str \u2013 A string, the account root up to 'num_components' components. Source code in beancount/core/account.py def root ( num_components : int , account_name : Account ) -> str : \"\"\"Return the first few components of an account's name. Args: num_components: An integer, the number of components to return. account_name: A string, an account name. Returns: A string, the account root up to 'num_components' components. \"\"\" return join ( * ( split ( account_name )[: num_components ]))","title":"root()"},{"location":"api_reference/beancount.core.html#beancount.core.account.sans_root","text":"Get the name of the account without the root. For example, an input of 'Assets:BofA:Checking' will produce 'BofA:Checking'. Parameters: account_name ( str ) \u2013 A string, the name of the account whose leaf name to return. Returns: str \u2013 A string, the name of the non-root portion of this account name. Source code in beancount/core/account.py def sans_root ( account_name : Account ) -> Account : \"\"\"Get the name of the account without the root. For example, an input of 'Assets:BofA:Checking' will produce 'BofA:Checking'. Args: account_name: A string, the name of the account whose leaf name to return. Returns: A string, the name of the non-root portion of this account name. \"\"\" assert isinstance ( account_name , str ) components = account_name . split ( sep )[ 1 :] return join ( * components ) if account_name else None","title":"sans_root()"},{"location":"api_reference/beancount.core.html#beancount.core.account.split","text":"Split an account's name into its components. Parameters: account_name ( str ) \u2013 A string, an account name. Returns: List[str] \u2013 A list of strings, the components of the account name (without the separators). Source code in beancount/core/account.py def split ( account_name : Account ) -> List [ str ]: \"\"\"Split an account's name into its components. Args: account_name: A string, an account name. Returns: A list of strings, the components of the account name (without the separators). \"\"\" return account_name . split ( sep )","title":"split()"},{"location":"api_reference/beancount.core.html#beancount.core.account.walk","text":"A version of os.walk() which yields directories that are valid account names. This only yields directories that are accounts... it skips the other ones. For convenience, it also yields you the account's name. Parameters: root_directory ( str ) \u2013 A string, the name of the root of the hierarchy to be walked. Yields: Tuples of (root, account-name, dirs, files), similar to os.walk(). Source code in beancount/core/account.py def walk ( root_directory : Account ) -> Iterator [ Tuple [ str , Account , List [ str ], List [ str ]]]: \"\"\"A version of os.walk() which yields directories that are valid account names. This only yields directories that are accounts... it skips the other ones. For convenience, it also yields you the account's name. Args: root_directory: A string, the name of the root of the hierarchy to be walked. Yields: Tuples of (root, account-name, dirs, files), similar to os.walk(). \"\"\" for root , dirs , files in os . walk ( root_directory ): dirs . sort () files . sort () relroot = root [ len ( root_directory ) + 1 :] account_name = relroot . replace ( os . sep , sep ) # The regex module does not handle Unicode characters in decomposed # form. Python uses the normal form for representing string. However, # some filesystems use the canonical decomposition form. # See https://docs.python.org/3/library/unicodedata.html#unicodedata.normalize account_name = unicodedata . normalize ( \"NFKC\" , account_name ) if is_valid ( account_name ): yield ( root , account_name , dirs , files )","title":"walk()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types","text":"Definition for global account types. This is where we keep the global account types value and definition. Note that it's unfortunate that we're using globals and side-effect here, but this is the best solution in the short-term, the account types are used in too many places to pass around that state everywhere. Maybe we change this later on.","title":"account_types"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.AccountTypes","text":"AccountTypes(assets, liabilities, equity, income, expenses)","title":"AccountTypes"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.AccountTypes.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/account_types.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.AccountTypes.__new__","text":"Create new instance of AccountTypes(assets, liabilities, equity, income, expenses)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.AccountTypes.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/account_types.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.get_account_sign","text":"Return the sign of the normal balance of a particular account. Parameters: account_name ( str ) \u2013 A string, the name of the account whose sign is to return. account_types ( AccountTypes ) \u2013 An optional instance of the current account_types. Returns: int \u2013 +1 or -1, depending on the account's type. Source code in beancount/core/account_types.py def get_account_sign ( account_name : Account , account_types : AccountTypes = None ) -> int : \"\"\"Return the sign of the normal balance of a particular account. Args: account_name: A string, the name of the account whose sign is to return. account_types: An optional instance of the current account_types. Returns: +1 or -1, depending on the account's type. \"\"\" if account_types is None : account_types = DEFAULT_ACCOUNT_TYPES assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) account_type = get_account_type ( account_name ) return + 1 if account_type in ( account_types . assets , account_types . expenses ) else - 1","title":"get_account_sign()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.get_account_sort_key","text":"Return a tuple that can be used to order/sort account names. Parameters: account_types ( AccountTypes ) \u2013 An instance of AccountTypes, a tuple of account type names. Returns: Tuple[str, str] \u2013 An object to use as the 'key' argument to the sort function. Source code in beancount/core/account_types.py def get_account_sort_key ( account_types : AccountTypes , account_name : Account ) -> Tuple [ str , Account ]: \"\"\"Return a tuple that can be used to order/sort account names. Args: account_types: An instance of AccountTypes, a tuple of account type names. Returns: An object to use as the 'key' argument to the sort function. \"\"\" return ( account_types . index ( get_account_type ( account_name )), account_name )","title":"get_account_sort_key()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.get_account_type","text":"Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Parameters: account_name ( str ) \u2013 A string, the name of the account whose type is to return. Returns: A string, the type of the account in 'account_name'. Source code in beancount/core/account_types.py def get_account_type ( account_name : Account ): \"\"\"Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Args: account_name: A string, the name of the account whose type is to return. Returns: A string, the type of the account in 'account_name'. \"\"\" assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) return account . split ( account_name )[ 0 ]","title":"get_account_type()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.is_account_type","text":"Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Parameters: account_type ( str ) \u2013 A string, the prefix type of the account. account_name ( str ) \u2013 A string, the name of the account whose type is to return. Returns: bool \u2013 A boolean, true if the account is of the given type. Source code in beancount/core/account_types.py def is_account_type ( account_type : str , account_name : Account ) -> bool : \"\"\"Return the type of this account's name. Warning: No check is made on the validity of the account type. This merely returns the root account of the corresponding account name. Args: account_type: A string, the prefix type of the account. account_name: A string, the name of the account whose type is to return. Returns: A boolean, true if the account is of the given type. \"\"\" return bool ( re . match ( \"^ {}{} \" . format ( account_type , account . sep ), account_name ))","title":"is_account_type()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.is_balance_sheet_account","text":"Return true if the given account is a balance sheet account. Assets, liabilities and equity accounts are balance sheet accounts. Parameters: account_name ( str ) \u2013 A string, an account name. account_types ( AccountTypes ) \u2013 An instance of AccountTypes. Returns: bool \u2013 A boolean, true if the account is a balance sheet account. Source code in beancount/core/account_types.py def is_balance_sheet_account ( account_name : Account , account_types : AccountTypes ) -> bool : \"\"\"Return true if the given account is a balance sheet account. Assets, liabilities and equity accounts are balance sheet accounts. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account is a balance sheet account. \"\"\" assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) assert isinstance ( account_types , AccountTypes ), \"Account types has invalid type: {} \" . format ( account_types ) account_type = get_account_type ( account_name ) return account_type in ( account_types . assets , account_types . liabilities , account_types . equity , )","title":"is_balance_sheet_account()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.is_equity_account","text":"Return true if the given account is an equity account. Parameters: account_name ( str ) \u2013 A string, an account name. account_types ( AccountTypes ) \u2013 An instance of AccountTypes. Returns: bool \u2013 A boolean, true if the account is an equity account. Source code in beancount/core/account_types.py def is_equity_account ( account_name : Account , account_types : AccountTypes ) -> bool : \"\"\"Return true if the given account is an equity account. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account is an equity account. \"\"\" assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) assert isinstance ( account_types , AccountTypes ), \"Account types has invalid type: {} \" . format ( account_types ) account_type = get_account_type ( account_name ) return account_type == account_types . equity","title":"is_equity_account()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.is_income_statement_account","text":"Return true if the given account is an income statement account. Income and expense accounts are income statement accounts. Parameters: account_name ( str ) \u2013 A string, an account name. account_types ( AccountTypes ) \u2013 An instance of AccountTypes. Returns: bool \u2013 A boolean, true if the account is an income statement account. Source code in beancount/core/account_types.py def is_income_statement_account ( account_name : Account , account_types : AccountTypes ) -> bool : \"\"\"Return true if the given account is an income statement account. Income and expense accounts are income statement accounts. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account is an income statement account. \"\"\" assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) assert isinstance ( account_types , AccountTypes ), \"Account types has invalid type: {} \" . format ( account_types ) account_type = get_account_type ( account_name ) return account_type in ( account_types . income , account_types . expenses )","title":"is_income_statement_account()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.is_inverted_account","text":"Return true if the given account has inverted signs. An inverted sign is the inverse as you'd expect in an external report, i.e., with all positive signs expected. Parameters: account_name ( str ) \u2013 A string, an account name. account_types ( AccountTypes ) \u2013 An instance of AccountTypes. Returns: bool \u2013 A boolean, true if the account has an inverted sign. Source code in beancount/core/account_types.py def is_inverted_account ( account_name : Account , account_types : AccountTypes ) -> bool : \"\"\"Return true if the given account has inverted signs. An inverted sign is the inverse as you'd expect in an external report, i.e., with all positive signs expected. Args: account_name: A string, an account name. account_types: An instance of AccountTypes. Returns: A boolean, true if the account has an inverted sign. \"\"\" assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) assert isinstance ( account_types , AccountTypes ), \"Account types has invalid type: {} \" . format ( account_types ) account_type = get_account_type ( account_name ) return account_type in ( account_types . liabilities , account_types . income , account_types . equity , )","title":"is_inverted_account()"},{"location":"api_reference/beancount.core.html#beancount.core.account_types.is_root_account","text":"Return true if the account name is a root account. This function does not verify whether the account root is a valid one, just that it is a root account or not. Parameters: account_name ( str ) \u2013 A string, the name of the account to check for. Returns: bool \u2013 A boolean, true if the account is root account. Source code in beancount/core/account_types.py def is_root_account ( account_name : Account ) -> bool : \"\"\"Return true if the account name is a root account. This function does not verify whether the account root is a valid one, just that it is a root account or not. Args: account_name: A string, the name of the account to check for. Returns: A boolean, true if the account is root account. \"\"\" assert isinstance ( account_name , str ), \"Account is not a string: {} \" . format ( account_name ) return account_name and bool ( re . match ( r \"([A-Z][A-Za-z0-9\\-]+)$\" , account_name ))","title":"is_root_account()"},{"location":"api_reference/beancount.core.html#beancount.core.amount","text":"Amount class. This simple class is used to associate a number of units of a currency with its currency: (number, currency).","title":"amount"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount","text":"An 'Amount' represents a number of a particular unit of something. It's essentially a typed number, with corresponding manipulation operations defined on it.","title":"Amount"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__bool__","text":"Boolean predicate returns true if the number is non-zero. Returns: A boolean, true if non-zero number. Source code in beancount/core/amount.py def __bool__ ( self ): \"\"\"Boolean predicate returns true if the number is non-zero. Returns: A boolean, true if non-zero number. \"\"\" return self . number != ZERO","title":"__bool__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__eq__","text":"Equality predicate. Returns true if both number and currency are equal. Returns: A boolean. Source code in beancount/core/amount.py def __eq__ ( self , other ): \"\"\"Equality predicate. Returns true if both number and currency are equal. Returns: A boolean. \"\"\" if other is None : return False return ( self . number , self . currency ) == ( other . number , other . currency )","title":"__eq__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__hash__","text":"A hashing function for amounts. The hash includes the currency. Returns: An integer, the hash for this amount. Source code in beancount/core/amount.py def __hash__ ( self ): \"\"\"A hashing function for amounts. The hash includes the currency. Returns: An integer, the hash for this amount. \"\"\" return hash (( self . number , self . currency ))","title":"__hash__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__lt__","text":"Ordering comparison. This is used in the sorting key of positions. Parameters: other \u2013 An instance of Amount. Returns: True if this is less than the other Amount. Source code in beancount/core/amount.py def __lt__ ( self , other ): \"\"\"Ordering comparison. This is used in the sorting key of positions. Args: other: An instance of Amount. Returns: True if this is less than the other Amount. \"\"\" return sortkey ( self ) < sortkey ( other )","title":"__lt__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__neg__","text":"Return the negative of this amount. Returns: A new instance of Amount, with the negative number of units. Source code in beancount/core/amount.py def __neg__ ( self ): \"\"\"Return the negative of this amount. Returns: A new instance of Amount, with the negative number of units. \"\"\" return Amount ( - self . number , self . currency )","title":"__neg__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__new__","text":"Constructor from a number and currency. Parameters: number \u2013 A Decimal instance. currency \u2013 A string, the currency symbol to use. Source code in beancount/core/amount.py def __new__ ( cls , number , currency ): \"\"\"Constructor from a number and currency. Args: number: A Decimal instance. currency: A string, the currency symbol to use. \"\"\" assert isinstance ( number , Amount . valid_types_number ), repr ( number ) assert isinstance ( currency , Amount . valid_types_currency ), repr ( currency ) return _Amount . __new__ ( cls , number , currency )","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__repr__","text":"Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/amount.py def __str__ ( self ): \"\"\"Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. \"\"\" return self . to_string ()","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.__str__","text":"Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/amount.py def __str__ ( self ): \"\"\"Convert an Amount instance to a printable string with the defaults. Returns: A formatted string of the quantized amount and symbol. \"\"\" return self . to_string ()","title":"__str__()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.from_string","text":"Create an amount from a string. This is a miniature parser used for building tests. Parameters: string \u2013 A string of . Returns: A new instance of Amount. Source code in beancount/core/amount.py @staticmethod def from_string ( string ): \"\"\"Create an amount from a string. This is a miniature parser used for building tests. Args: string: A string of . Returns: A new instance of Amount. \"\"\" match = re . match ( r \"\\s*([-+]?[0-9.]+)\\s+( {currency} )\" . format ( currency = CURRENCY_RE ), string ) if not match : raise ValueError ( \"Invalid string for amount: ' {} '\" . format ( string )) number , currency = match . group ( 1 , 2 ) return Amount ( D ( number ), currency )","title":"from_string()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.Amount.to_string","text":"Convert an Amount instance to a printable string. Parameters: dformat \u2013 An instance of DisplayFormatter. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/amount.py def to_string ( self , dformat = DEFAULT_FORMATTER ): \"\"\"Convert an Amount instance to a printable string. Args: dformat: An instance of DisplayFormatter. Returns: A formatted string of the quantized amount and symbol. \"\"\" if isinstance ( self . number , Decimal ): number_fmt = dformat . format ( self . number , self . currency ) elif self . number is MISSING : number_fmt = \"\" else : number_fmt = str ( self . number ) return \" {} {} \" . format ( number_fmt , self . currency )","title":"to_string()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.A","text":"Create an amount from a string. This is a miniature parser used for building tests. Parameters: string \u2013 A string of . Returns: A new instance of Amount. Source code in beancount/core/amount.py @staticmethod def from_string ( string ): \"\"\"Create an amount from a string. This is a miniature parser used for building tests. Args: string: A string of . Returns: A new instance of Amount. \"\"\" match = re . match ( r \"\\s*([-+]?[0-9.]+)\\s+( {currency} )\" . format ( currency = CURRENCY_RE ), string ) if not match : raise ValueError ( \"Invalid string for amount: ' {} '\" . format ( string )) number , currency = match . group ( 1 , 2 ) return Amount ( D ( number ), currency )","title":"A()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.abs","text":"Return the absolute value of the given amount. Parameters: amount \u2013 An instance of Amount. Returns: An instance of Amount. Source code in beancount/core/amount.py def abs ( amount ): \"\"\"Return the absolute value of the given amount. Args: amount: An instance of Amount. Returns: An instance of Amount. \"\"\" return amount if amount . number >= ZERO else Amount ( - amount . number , amount . currency )","title":"abs()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.add","text":"Add the given amounts with the same currency. Parameters: amount1 \u2013 An instance of Amount. amount2 \u2013 An instance of Amount. Returns: An instance of Amount, with the sum the two amount's numbers, in the same currency. Source code in beancount/core/amount.py def add ( amount1 , amount2 ): \"\"\"Add the given amounts with the same currency. Args: amount1: An instance of Amount. amount2: An instance of Amount. Returns: An instance of Amount, with the sum the two amount's numbers, in the same currency. \"\"\" assert isinstance ( amount1 . number , Decimal ), \"Amount1's number is not a Decimal instance: {} \" . format ( amount1 . number ) assert isinstance ( amount2 . number , Decimal ), \"Amount2's number is not a Decimal instance: {} \" . format ( amount2 . number ) if amount1 . currency != amount2 . currency : raise ValueError ( \"Unmatching currencies for operation on {} and {} \" . format ( amount1 , amount2 ) ) return Amount ( amount1 . number + amount2 . number , amount1 . currency )","title":"add()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.div","text":"Divide the given amount by a number. Parameters: amount \u2013 An instance of Amount. number \u2013 A decimal number. Returns: An Amount, with the same currency, but with amount units divided by 'number'. Source code in beancount/core/amount.py def div ( amount , number ): \"\"\"Divide the given amount by a number. Args: amount: An instance of Amount. number: A decimal number. Returns: An Amount, with the same currency, but with amount units divided by 'number'. \"\"\" assert isinstance ( amount . number , Decimal ), \"Amount's number is not a Decimal instance: {} \" . format ( amount . number ) assert isinstance ( number , Decimal ), \"Number is not a Decimal instance: {} \" . format ( number ) return Amount ( amount . number / number , amount . currency )","title":"div()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.from_string","text":"Create an amount from a string. This is a miniature parser used for building tests. Parameters: string \u2013 A string of . Returns: A new instance of Amount. Source code in beancount/core/amount.py @staticmethod def from_string ( string ): \"\"\"Create an amount from a string. This is a miniature parser used for building tests. Args: string: A string of . Returns: A new instance of Amount. \"\"\" match = re . match ( r \"\\s*([-+]?[0-9.]+)\\s+( {currency} )\" . format ( currency = CURRENCY_RE ), string ) if not match : raise ValueError ( \"Invalid string for amount: ' {} '\" . format ( string )) number , currency = match . group ( 1 , 2 ) return Amount ( D ( number ), currency )","title":"from_string()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.mul","text":"Multiply the given amount by a number. Parameters: amount \u2013 An instance of Amount. number \u2013 A decimal number. Returns: An Amount, with the same currency, but with 'number' times units. Source code in beancount/core/amount.py def mul ( amount , number ): \"\"\"Multiply the given amount by a number. Args: amount: An instance of Amount. number: A decimal number. Returns: An Amount, with the same currency, but with 'number' times units. \"\"\" assert isinstance ( amount . number , Decimal ), \"Amount's number is not a Decimal instance: {} \" . format ( amount . number ) assert isinstance ( number , Decimal ), \"Number is not a Decimal instance: {} \" . format ( number ) return Amount ( amount . number * number , amount . currency )","title":"mul()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.sortkey","text":"A comparison function that sorts by currency first. Parameters: amount \u2013 An instance of Amount. Returns: A sort key, composed of the currency first and then the number. Source code in beancount/core/amount.py def sortkey ( amount ): \"\"\"A comparison function that sorts by currency first. Args: amount: An instance of Amount. Returns: A sort key, composed of the currency first and then the number. \"\"\" return ( amount . currency , amount . number )","title":"sortkey()"},{"location":"api_reference/beancount.core.html#beancount.core.amount.sub","text":"Subtract the given amounts with the same currency. Parameters: amount1 \u2013 An instance of Amount. amount2 \u2013 An instance of Amount. Returns: An instance of Amount, with the difference between the two amount's numbers, in the same currency. Source code in beancount/core/amount.py def sub ( amount1 , amount2 ): \"\"\"Subtract the given amounts with the same currency. Args: amount1: An instance of Amount. amount2: An instance of Amount. Returns: An instance of Amount, with the difference between the two amount's numbers, in the same currency. \"\"\" assert isinstance ( amount1 . number , Decimal ), \"Amount1's number is not a Decimal instance: {} \" . format ( amount1 . number ) assert isinstance ( amount2 . number , Decimal ), \"Amount2's number is not a Decimal instance: {} \" . format ( amount2 . number ) if amount1 . currency != amount2 . currency : raise ValueError ( \"Unmatching currencies for operation on {} and {} \" . format ( amount1 , amount2 ) ) return Amount ( amount1 . number - amount2 . number , amount1 . currency )","title":"sub()"},{"location":"api_reference/beancount.core.html#beancount.core.compare","text":"Comparison helpers for data objects.","title":"compare"},{"location":"api_reference/beancount.core.html#beancount.core.compare.CompareError","text":"CompareError(source, message, entry)","title":"CompareError"},{"location":"api_reference/beancount.core.html#beancount.core.compare.CompareError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/compare.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.CompareError.__new__","text":"Create new instance of CompareError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.CompareError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/compare.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.compare_entries","text":"Compare two lists of entries. This is used for testing. The entries are compared with disregard for their file location. Parameters: entries1 \u2013 A list of directives of any type. entries2 \u2013 Another list of directives of any type. Returns: A tuple of (success, not_found1, not_found2), where the fields are \u2013 success: A boolean, true if all the values are equal. missing1: A list of directives from 'entries1' not found in 'entries2'. missing2: A list of directives from 'entries2' not found in 'entries1'. Exceptions: ValueError \u2013 If a duplicate entry is found. Source code in beancount/core/compare.py def compare_entries ( entries1 , entries2 ): \"\"\"Compare two lists of entries. This is used for testing. The entries are compared with disregard for their file location. Args: entries1: A list of directives of any type. entries2: Another list of directives of any type. Returns: A tuple of (success, not_found1, not_found2), where the fields are: success: A boolean, true if all the values are equal. missing1: A list of directives from 'entries1' not found in 'entries2'. missing2: A list of directives from 'entries2' not found in 'entries1'. Raises: ValueError: If a duplicate entry is found. \"\"\" hashes1 , errors1 = hash_entries ( entries1 , exclude_meta = True ) hashes2 , errors2 = hash_entries ( entries2 , exclude_meta = True ) keys1 = set ( hashes1 . keys ()) keys2 = set ( hashes2 . keys ()) if errors1 or errors2 : error = ( errors1 + errors2 )[ 0 ] raise ValueError ( str ( error )) same = keys1 == keys2 missing1 = data . sorted ([ hashes1 [ key ] for key in keys1 - keys2 ]) missing2 = data . sorted ([ hashes2 [ key ] for key in keys2 - keys1 ]) return ( same , missing1 , missing2 )","title":"compare_entries()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.excludes_entries","text":"Check that a list of entries does not appear in another list. Parameters: subset_entries \u2013 The set of entries to look for in 'entries'. entries \u2013 The larger list of entries that should not include 'subset_entries'. Returns: A boolean and a list of entries that are not supposed to appear. Exceptions: ValueError \u2013 If a duplicate entry is found. Source code in beancount/core/compare.py def excludes_entries ( subset_entries , entries ): \"\"\"Check that a list of entries does not appear in another list. Args: subset_entries: The set of entries to look for in 'entries'. entries: The larger list of entries that should not include 'subset_entries'. Returns: A boolean and a list of entries that are not supposed to appear. Raises: ValueError: If a duplicate entry is found. \"\"\" subset_hashes , subset_errors = hash_entries ( subset_entries , exclude_meta = True ) subset_keys = set ( subset_hashes . keys ()) hashes , errors = hash_entries ( entries , exclude_meta = True ) keys = set ( hashes . keys ()) if subset_errors or errors : error = ( subset_errors + errors )[ 0 ] raise ValueError ( str ( error )) intersection = keys . intersection ( subset_keys ) excludes = not bool ( intersection ) extra = data . sorted ([ subset_hashes [ key ] for key in intersection ]) return ( excludes , extra )","title":"excludes_entries()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.hash_entries","text":"Compute unique hashes of each of the entries and return a map of them. This is used for comparisons between sets of entries. Parameters: entries \u2013 A list of directives. exclude_meta \u2013 If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A dict of hash-value to entry (for all entries) and a list of errors. Errors are created when duplicate entries are found. Source code in beancount/core/compare.py def hash_entries ( entries , exclude_meta = False ): \"\"\"Compute unique hashes of each of the entries and return a map of them. This is used for comparisons between sets of entries. Args: entries: A list of directives. exclude_meta: If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A dict of hash-value to entry (for all entries) and a list of errors. Errors are created when duplicate entries are found. \"\"\" entry_hash_dict = {} errors = [] num_legal_duplicates = 0 for entry in entries : hash_ = hash_entry ( entry , exclude_meta ) if hash_ in entry_hash_dict : if isinstance ( entry , Price ): # Note: Allow duplicate Price entries, they should be common # because of the nature of stock markets (if they're closed, the # data source is likely to return an entry for the previously # available date, which may already have been fetched). num_legal_duplicates += 1 else : other_entry = entry_hash_dict [ hash_ ] errors . append ( CompareError ( entry . meta , \"Duplicate entry: {} == {} \" . format ( entry , other_entry ), entry , ) ) entry_hash_dict [ hash_ ] = entry if not errors : assert len ( entry_hash_dict ) + num_legal_duplicates == len ( entries ), ( len ( entry_hash_dict ), len ( entries ), num_legal_duplicates , ) return entry_hash_dict , errors","title":"hash_entries()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.hash_entry","text":"Compute the stable hash of a single entry. Parameters: entry \u2013 A directive instance. exclude_meta \u2013 If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A stable hexadecimal hash of this entry. Source code in beancount/core/compare.py def hash_entry ( entry , exclude_meta = False ): \"\"\"Compute the stable hash of a single entry. Args: entry: A directive instance. exclude_meta: If set, exclude the metadata from the hash. Use this for unit tests comparing entries coming from different sources as the filename and lineno will be distinct. However, when you're using the hashes to uniquely identify transactions, you want to include the filenames and line numbers (the default). Returns: A stable hexadecimal hash of this entry. \"\"\" return stable_hash_namedtuple ( entry , IGNORED_FIELD_NAMES if exclude_meta else frozenset () )","title":"hash_entry()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.includes_entries","text":"Check if a list of entries is included in another list. Parameters: subset_entries \u2013 The set of entries to look for in 'entries'. entries \u2013 The larger list of entries that could include 'subset_entries'. Returns: A boolean and a list of missing entries. Exceptions: ValueError \u2013 If a duplicate entry is found. Source code in beancount/core/compare.py def includes_entries ( subset_entries , entries ): \"\"\"Check if a list of entries is included in another list. Args: subset_entries: The set of entries to look for in 'entries'. entries: The larger list of entries that could include 'subset_entries'. Returns: A boolean and a list of missing entries. Raises: ValueError: If a duplicate entry is found. \"\"\" subset_hashes , subset_errors = hash_entries ( subset_entries , exclude_meta = True ) subset_keys = set ( subset_hashes . keys ()) hashes , errors = hash_entries ( entries , exclude_meta = True ) keys = set ( hashes . keys ()) if subset_errors or errors : error = ( subset_errors + errors )[ 0 ] raise ValueError ( str ( error )) includes = subset_keys . issubset ( keys ) missing = data . sorted ([ subset_hashes [ key ] for key in subset_keys - keys ]) return ( includes , missing )","title":"includes_entries()"},{"location":"api_reference/beancount.core.html#beancount.core.compare.stable_hash_namedtuple","text":"Hash the given namedtuple and its child fields. This iterates over all the members of objtuple, skipping the attributes from the 'ignore' set, and computes a unique hash string code. If the elements are lists or sets, sorts them for stability. Parameters: objtuple \u2013 A tuple object or other. ignore \u2013 A set of strings, attribute names to be skipped in computing a stable hash. For instance, circular references to objects or irrelevant data. Source code in beancount/core/compare.py def stable_hash_namedtuple ( objtuple , ignore = frozenset ()): \"\"\"Hash the given namedtuple and its child fields. This iterates over all the members of objtuple, skipping the attributes from the 'ignore' set, and computes a unique hash string code. If the elements are lists or sets, sorts them for stability. Args: objtuple: A tuple object or other. ignore: A set of strings, attribute names to be skipped in computing a stable hash. For instance, circular references to objects or irrelevant data. \"\"\" # Note: this routine is slow and would stand to be implemented in C. hashobj = hashlib . md5 () for attr_name , attr_value in zip ( objtuple . _fields , objtuple ): if attr_name in ignore : continue if isinstance ( attr_value , ( list , set , frozenset )): subhashes = [] for element in attr_value : if isinstance ( element , tuple ): subhashes . append ( stable_hash_namedtuple ( element , ignore )) else : md5 = hashlib . md5 () md5 . update ( str ( element ) . encode ()) subhashes . append ( md5 . hexdigest ()) for subhash in sorted ( subhashes ): hashobj . update ( subhash . encode ()) else : hashobj . update ( str ( attr_value ) . encode ()) return hashobj . hexdigest ()","title":"stable_hash_namedtuple()"},{"location":"api_reference/beancount.core.html#beancount.core.convert","text":"Conversions from Position (or Posting) to units, cost, weight, market value. Units: Just the primary amount of the position. Cost: The cost basis of the position, if available. Weight: The cost basis or price of the position. Market Value: The units converted to a value via a price map. To convert an inventory's contents, simply use these functions in conjunction with Inventory.reduce() , like cost_inv = inv.reduce(convert.get_cost) This module equivalently converts Position and Posting instances. Note that we're specifically avoiding to create an import dependency on beancount.core.data in order to keep this module isolatable, but it works on postings due to duck-typing. Function named get_*() are used to compute values from postings to their price currency. Functions named convert_*() are used to convert postings and amounts to any currency.","title":"convert"},{"location":"api_reference/beancount.core.html#beancount.core.convert.convert_amount","text":"Return the market value of an Amount in a particular currency. In addition, if a conversion rate isn't available, you can provide a list of currencies to attempt to synthesize a rate for via implied rates. Parameters: amt \u2013 An instance of Amount. target_currency \u2013 The target currency to convert to. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance to evaluate the value at, or None. via \u2013 A list of currencies to attempt to synthesize an implied rate if the direct conversion fails. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, the amount itself, unmodified. Source code in beancount/core/convert.py def convert_amount ( amt , target_currency , price_map , date = None , via = None ): \"\"\"Return the market value of an Amount in a particular currency. In addition, if a conversion rate isn't available, you can provide a list of currencies to attempt to synthesize a rate for via implied rates. Args: amt: An instance of Amount. target_currency: The target currency to convert to. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. via: A list of currencies to attempt to synthesize an implied rate if the direct conversion fails. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, the amount itself, unmodified. \"\"\" # First, attempt to convert directly. This should be the most # straightforward conversion. base_quote = ( amt . currency , target_currency ) _ , rate = prices . get_price ( price_map , base_quote , date ) if rate is not None : # On success, just make the conversion directly. return Amount ( amt . number * rate , target_currency ) elif via : assert isinstance ( via , ( tuple , list )) # A price is unavailable, attempt to convert via cost/price currency # hop, if the value currency isn't the target currency. for implied_currency in via : if implied_currency == target_currency : continue base_quote1 = ( amt . currency , implied_currency ) _ , rate1 = prices . get_price ( price_map , base_quote1 , date ) if rate1 is not None : base_quote2 = ( implied_currency , target_currency ) _ , rate2 = prices . get_price ( price_map , base_quote2 , date ) if rate2 is not None : return Amount ( amt . number * rate1 * rate2 , target_currency ) # We failed to infer a conversion rate; return the amt. return amt","title":"convert_amount()"},{"location":"api_reference/beancount.core.html#beancount.core.convert.convert_position","text":"Return the market value of a Position or Posting in a particular currency. In addition, if the rate from the position's currency to target_currency isn't available, an attempt is made to convert from its cost currency, if one is available. Parameters: pos \u2013 An instance of Position or Posting, equivalently. target_currency \u2013 The target currency to convert to. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance to evaluate the value at, or None. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. (See get_value() above for details.) Source code in beancount/core/convert.py def convert_position ( pos , target_currency , price_map , date = None ): \"\"\"Return the market value of a Position or Posting in a particular currency. In addition, if the rate from the position's currency to target_currency isn't available, an attempt is made to convert from its cost currency, if one is available. Args: pos: An instance of Position or Posting, equivalently. target_currency: The target currency to convert to. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. (See get_value() above for details.) \"\"\" cost = pos . cost value_currency = ( ( isinstance ( cost , Cost ) and cost . currency ) or ( hasattr ( pos , \"price\" ) and pos . price and pos . price . currency ) or None ) return convert_amount ( pos . units , target_currency , price_map , date = date , via = ( value_currency ,) )","title":"convert_position()"},{"location":"api_reference/beancount.core.html#beancount.core.convert.get_cost","text":"Return the total cost of a Position or Posting. Parameters: pos \u2013 An instance of Position or Posting, equivalently. Returns: An Amount. Source code in beancount/core/convert.py def get_cost ( pos ): \"\"\"Return the total cost of a Position or Posting. Args: pos: An instance of Position or Posting, equivalently. Returns: An Amount. \"\"\" assert isinstance ( pos , Position ) or type ( pos ) . __name__ == \"Posting\" cost = pos . cost return ( Amount ( cost . number * pos . units . number , cost . currency ) if ( isinstance ( cost , Cost ) and isinstance ( cost . number , Decimal )) else pos . units )","title":"get_cost()"},{"location":"api_reference/beancount.core.html#beancount.core.convert.get_units","text":"Return the units of a Position or Posting. Parameters: pos \u2013 An instance of Position or Posting, equivalently. Returns: An Amount. Source code in beancount/core/convert.py def get_units ( pos ): \"\"\"Return the units of a Position or Posting. Args: pos: An instance of Position or Posting, equivalently. Returns: An Amount. \"\"\" assert isinstance ( pos , Position ) or type ( pos ) . __name__ == \"Posting\" return pos . units","title":"get_units()"},{"location":"api_reference/beancount.core.html#beancount.core.convert.get_value","text":"Return the market value of a Position or Posting. Note that if the position is not held at cost, this does not convert anything, even if a price is available in the 'price_map'. We don't specify a target currency here. If you're attempting to make such a conversion, see convert_*() functions below. However, is the object is a posting and it has a price, we will use that price to infer the target currency and those will be converted. Parameters: pos \u2013 An instance of Position or Posting, equivalently. price_map \u2013 A dict of prices, as built by prices.build_price_map(). date \u2013 A datetime.date instance to evaluate the value at, or None. output_date_prices \u2013 An optional output list of (date, price). If this list is provided, it will be appended to (mutated) to output the price pulled in making the conversions. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. This is designed so that you could reduce an inventory with this and not lose any information silently in case of failure to convert (possibly due to an empty price map). Compare the returned currency to that of the input position if you need to check for success. Source code in beancount/core/convert.py def get_value ( pos , price_map , date = None , output_date_prices = None ): \"\"\"Return the market value of a Position or Posting. Note that if the position is not held at cost, this does not convert anything, even if a price is available in the 'price_map'. We don't specify a target currency here. If you're attempting to make such a conversion, see ``convert_*()`` functions below. However, is the object is a posting and it has a price, we will use that price to infer the target currency and those will be converted. Args: pos: An instance of Position or Posting, equivalently. price_map: A dict of prices, as built by prices.build_price_map(). date: A datetime.date instance to evaluate the value at, or None. output_date_prices: An optional output list of (date, price). If this list is provided, it will be appended to (mutated) to output the price pulled in making the conversions. Returns: An Amount, either with a successful value currency conversion, or if we could not convert the value, just the units, unmodified. This is designed so that you could reduce an inventory with this and not lose any information silently in case of failure to convert (possibly due to an empty price map). Compare the returned currency to that of the input position if you need to check for success. \"\"\" assert isinstance ( pos , Position ) or type ( pos ) . __name__ == \"Posting\" units = pos . units cost = pos . cost # Try to infer what the cost/price currency should be. value_currency = ( ( isinstance ( cost , Cost ) and cost . currency ) or ( hasattr ( pos , \"price\" ) and pos . price and pos . price . currency ) or None ) if isinstance ( value_currency , str ): # We have a value currency; hit the price database. base_quote = ( units . currency , value_currency ) price_date , price_number = prices . get_price ( price_map , base_quote , date ) if output_date_prices is not None : output_date_prices . append (( price_date , price_number )) if price_number is not None : return Amount ( units . number * price_number , value_currency ) # We failed to infer a conversion rate; return the units. return units","title":"get_value()"},{"location":"api_reference/beancount.core.html#beancount.core.convert.get_weight","text":"Return the weight of a Position or Posting. This is the amount that will need to be balanced from a posting of a transaction. This is a key element of the semantics of transactions in this software. A balance amount is the amount used to check the balance of a transaction. Here are all relevant examples, with the amounts used to balance the postings: Assets:Account 5234.50 USD -> 5234.50 USD Assets:Account 3877.41 EUR @ 1.35 USD -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} @ 545.60 CAD -> 5234.50 USD Parameters: pos \u2013 An instance of Position or Posting, equivalently. Returns: An Amount. Source code in beancount/core/convert.py def get_weight ( pos ): \"\"\"Return the weight of a Position or Posting. This is the amount that will need to be balanced from a posting of a transaction. This is a *key* element of the semantics of transactions in this software. A balance amount is the amount used to check the balance of a transaction. Here are all relevant examples, with the amounts used to balance the postings: Assets:Account 5234.50 USD -> 5234.50 USD Assets:Account 3877.41 EUR @ 1.35 USD -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} -> 5234.50 USD Assets:Account 10 HOOL {523.45 USD} @ 545.60 CAD -> 5234.50 USD Args: pos: An instance of Position or Posting, equivalently. Returns: An Amount. \"\"\" assert isinstance ( pos , Position ) or type ( pos ) . __name__ == \"Posting\" units = pos . units cost = pos . cost # It the object has a cost, use that as the weight, to balance. if isinstance ( cost , Cost ) and isinstance ( cost . number , Decimal ): weight = Amount ( cost . number * pos . units . number , cost . currency ) else : # Otherwise use the postings. weight = units # Unless there is a price available; use that if present. if not isinstance ( pos , Position ): price = pos . price if price is not None : # Note: Here we could assert that price.currency == units.currency. if price . number is MISSING or units . number is MISSING : converted_number = MISSING else : converted_number = price . number * units . number weight = Amount ( converted_number , price . currency ) return weight","title":"get_weight()"},{"location":"api_reference/beancount.core.html#beancount.core.data","text":"Basic data structures used to represent the Ledger entries.","title":"data"},{"location":"api_reference/beancount.core.html#beancount.core.data.Balance","text":"A \"check the balance of this account\" directive. This directive asserts that the declared account should have a known number of units of a particular currency at the beginning of its date. This is essentially an assertion, and corresponds to the final \"Statement Balance\" line of a real-world statement. These assertions act as checkpoints to help ensure that you have entered your transactions correctly. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the account whose balance to check at the given date. amount Amount An Amount, the number of units of the given currency you're expecting 'account' to have at this date. diff_amount Optional[beancount.core.amount.Amount] None if the balance check succeeds. This value is set to an Amount instance if the balance fails, the amount of the difference. tolerance Optional[decimal.Decimal] A Decimal object, the amount of tolerance to use in the verification.","title":"Balance"},{"location":"api_reference/beancount.core.html#beancount.core.data.Balance.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Balance.__new__","text":"Create new instance of Balance(meta, date, account, amount, tolerance, diff_amount)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Balance.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Close","text":"A \"close account\" directive. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the name of the account that is being closed.","title":"Close"},{"location":"api_reference/beancount.core.html#beancount.core.data.Close.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Close.__new__","text":"Create new instance of Close(meta, date, account)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Close.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Commodity","text":"An optional commodity declaration directive. Commodities generally do not need to be declared, but they may, and this is mainly created as intended to be used to attach meta-data on a commodity name. Whenever a plugin needs per-commodity meta-data, you would define such a commodity directive. Another use is to define a commodity that isn't otherwise (yet) used anywhere in an input file. (At the moment the date is meaningless but is specified for coherence with all the other directives; if you can think of a good use case, let us know). Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. currency str A string, the commodity under consideration.","title":"Commodity"},{"location":"api_reference/beancount.core.html#beancount.core.data.Commodity.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Commodity.__new__","text":"Create new instance of Commodity(meta, date, currency)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Commodity.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Custom","text":"A custom directive. This directive can be used to implement new experimental dated features in the Beancount file. This is meant as an intermediate measure to be used when you would need to implement a new directive in a plugin. These directives will be parsed liberally... any list of tokens are supported. All that is required is some unique name for them that acts as a \"type\". These directives are included in the stream and a plugin should be able to gather them. Attributes: Name Type Description meta Dict[str, Any] See above. type str A string that represents the type of the directive. values List A list of values of various simple types supported by the grammar. (Note that this list is not enforced to be consistent for all directives of the same type by the parser.)","title":"Custom"},{"location":"api_reference/beancount.core.html#beancount.core.data.Custom.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Custom.__new__","text":"Create new instance of Custom(meta, date, type, values)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Custom.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Document","text":"A document file declaration directive. This directive is used to attach a statement to an account, at a particular date. A typical usage would be to render PDF files or scans of your bank statements into the account's journal. While you can explicitly create those directives in the input syntax, it is much more convenient to provide Beancount with a root directory to search for filenames in a hierarchy mirroring the chart of accounts, filenames which should match the following dated format: \"YYYY-MM-DD.*\". See options for detail. Beancount will automatically create these documents directives based on the file hierarchy, and you can get them by parsing the list of entries. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the account which the statement or document is associated with. filename str The absolute filename of the document file. tags Optional[Set] A set of tag strings (without the '#'), or None, if an empty set. links Optional[Set] A set of link strings (without the '^'), or None, if an empty set.","title":"Document"},{"location":"api_reference/beancount.core.html#beancount.core.data.Document.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Document.__new__","text":"Create new instance of Document(meta, date, account, filename, tags, links)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Document.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Event","text":"An \"event value change\" directive. These directives are used as string variables that have different values over time. You can use these to track an address, your location, your current employer, anything you like. The kind of reporting that is made of these generic events is based on days and a timeline. For instance, if you need to track the number of days you spend in each country or state, create a \"location\" event and whenever you travel, add an event directive to indicate its new value. You should be able to write simple scripts against those in order to compute if you were present somewhere for a particular number of days. Here's an illustrative example usage, in order to maintain your health insurance coverage in Canada, you need to be present in the country for 183 days or more, excluding trips of less than 30 days. There is a similar test to be done in the US by aliens to figure out if they need to be considered as residents for tax purposes (the so-called \"substantial presence test\"). By integrating these directives into your bookkeeping, you can easily have a little program that computes the tests for you. This is, of course, entirely optional and somewhat auxiliary to the main purpose of double-entry bookkeeping, but correlates strongly with the transactions you insert in it, and so it's a really convenient thing to have in the same input file. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. \"type\" A short string, typically a single lowercase word, that defines a unique variable whose value changes over time. For example, 'location'. description str A free-form string, the value of the variable as of the date of the transaction.","title":"Event"},{"location":"api_reference/beancount.core.html#beancount.core.data.Event.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Event.__new__","text":"Create new instance of Event(meta, date, type, description)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Event.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Note","text":"A note directive, a general note that is attached to an account. These are used to attach text at a particular date in a specific account. The notes can be anything; a typical use would be to jot down an answer from a phone call to the institution represented by the account. It should show up in an account's journal. If you don't want this rendered, use the comment syntax in the input file, which does not get parsed and stored. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the account which the note is to be attached to. This is never None, notes always have an account they correspond to. comment str A free-form string, the text of the note. This can be long if you want it to.","title":"Note"},{"location":"api_reference/beancount.core.html#beancount.core.data.Note.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Note.__new__","text":"Create new instance of Note(meta, date, account, comment, tags, links)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Note.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Open","text":"An \"open account\" directive. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the name of the account that is being opened. currencies List[str] A list of strings, currencies that are allowed in this account. May be None, in which case it means that there are no restrictions on which currencies may be stored in this account. booking Optional[beancount.core.data.Booking] A Booking enum, the booking method to use to disambiguate postings to this account (when zero or more than one postings match the specification), or None if not specified. In practice, this attribute will be should be left unspecified (None) in the vast majority of cases. See Booking below for a selection of valid methods.","title":"Open"},{"location":"api_reference/beancount.core.html#beancount.core.data.Open.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Open.__new__","text":"Create new instance of Open(meta, date, account, currencies, booking)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Open.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Pad","text":"A \"pad this account with this other account\" directive. This directive automatically inserts transactions that will make the next chronological balance directive succeeds. It can be used to fill in missing date ranges of transactions, as a convenience. You don't have to use this, it's sugar coating in case you need it, while you're entering past history into your Ledger. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the name of the account which needs to be filled. source_account str A string, the name of the account which is used to debit from in order to fill 'account'.","title":"Pad"},{"location":"api_reference/beancount.core.html#beancount.core.data.Pad.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Pad.__new__","text":"Create new instance of Pad(meta, date, account, source_account)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Pad.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Posting","text":"Postings are contained in Transaction entries. These represent the individual legs of a transaction. Note: a posting may only appear within a single entry (multiple transactions may not share a Posting instance), and that's what the entry field should be set to. Attributes: Name Type Description account str A string, the account that is modified by this posting. units Optional[beancount.core.amount.Amount] An Amount, the units of the position, or None if it is to be inferred from the other postings in the transaction. cost Union[beancount.core.position.Cost, beancount.core.position.CostSpec] A Cost or CostSpec instances, the units of the position. price Optional[beancount.core.amount.Amount] An Amount, the price at which the position took place, or None, where not relevant. Providing a price member to a posting automatically adds a price in the prices database at the date of the transaction. flag Optional[str] An optional flag, a one-character string or None, which is to be associated with the posting. Most postings don't have a flag, but it can be convenient to mark a particular posting as problematic or pending to be reconciled for a future import of its account. meta Optional[Dict[str, Any]] A dict of strings to values, the metadata that was attached specifically to that posting, or None, if not provided. In practice, most of the instances will be unlikely to have metadata.","title":"Posting"},{"location":"api_reference/beancount.core.html#beancount.core.data.Posting.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Posting.__new__","text":"Create new instance of Posting(account, units, cost, price, flag, meta)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Posting.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Price","text":"A price declaration directive. This establishes the price of a currency in terms of another currency as of the directive's date. A history of the prices for each currency pairs is built and can be queried within the bookkeeping system. Note that because Beancount does not store any data at time-of-day resolution, it makes no sense to have multiple price directives at the same date. (Beancount will not attempt to solve this problem; this is beyond the general scope of double-entry bookkeeping and if you need to build a day trading system, you should probably use something else). Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. currency: A string, the currency that is being priced, e.g. HOOL. amount: An instance of Amount, the number of units and currency that 'currency' is worth, for instance 1200.12 USD.","title":"Price"},{"location":"api_reference/beancount.core.html#beancount.core.data.Price.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Price.__new__","text":"Create new instance of Price(meta, date, currency, amount)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Price.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Query","text":"A named query declaration. This directive is used to create pre-canned queries that can then be automatically run or made available to the shell, or perhaps be rendered as part of a web interface. The purpose of this routine is to define useful queries for the context of the particular given Beancount input file. Attributes: Name Type Description meta Dict[str, Any] See above. date date The date at which this query should be run. All directives following this date will be ignored automatically. This is essentially equivalent to the CLOSE modifier in the shell syntax. name str A string, the unique identifier for the query. query_string str The SQL query string to be run or made available.","title":"Query"},{"location":"api_reference/beancount.core.html#beancount.core.data.Query.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Query.__new__","text":"Create new instance of Query(meta, date, name, query_string)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Query.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Transaction","text":"A transaction! This is the main type of object that we manipulate, and the entire reason this whole project exists in the first place, because representing these types of structures with a spreadsheet is difficult. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. flag str A single-character string or None. This user-specified string represents some custom/user-defined state of the transaction. You can use this for various purposes. Otherwise common, pre-defined flags are defined under beancount.core.flags, to flags transactions that are automatically generated. payee Optional[str] A free-form string that identifies the payee, or None, if absent. narration str A free-form string that provides a description for the transaction. All transactions have at least a narration string, this is never None. tags FrozenSet A set of tag strings (without the '#'), or EMPTY_SET. links FrozenSet A set of link strings (without the '^'), or EMPTY_SET. postings List[beancount.core.data.Posting] A list of Posting instances, the legs of this transaction. See the doc under Posting above.","title":"Transaction"},{"location":"api_reference/beancount.core.html#beancount.core.data.Transaction.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Transaction.__new__","text":"Create new instance of Transaction(meta, date, flag, payee, narration, tags, links, postings)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.Transaction.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.TxnPosting","text":"A pair of a Posting and its parent Transaction. This is inserted as temporaries in lists of postings-of-entries, which is the product of a realization. Attributes: Name Type Description txn Transaction The parent Transaction instance. posting Posting The Posting instance.","title":"TxnPosting"},{"location":"api_reference/beancount.core.html#beancount.core.data.TxnPosting.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.TxnPosting.__new__","text":"Create new instance of TxnPosting(txn, posting)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.TxnPosting.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes","text":"Types of directives.","title":"dtypes"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Balance","text":"A \"check the balance of this account\" directive. This directive asserts that the declared account should have a known number of units of a particular currency at the beginning of its date. This is essentially an assertion, and corresponds to the final \"Statement Balance\" line of a real-world statement. These assertions act as checkpoints to help ensure that you have entered your transactions correctly. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the account whose balance to check at the given date. amount Amount An Amount, the number of units of the given currency you're expecting 'account' to have at this date. diff_amount Optional[beancount.core.amount.Amount] None if the balance check succeeds. This value is set to an Amount instance if the balance fails, the amount of the difference. tolerance Optional[decimal.Decimal] A Decimal object, the amount of tolerance to use in the verification.","title":"Balance"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Balance.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Balance.__new__","text":"Create new instance of Balance(meta, date, account, amount, tolerance, diff_amount)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Balance.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Close","text":"A \"close account\" directive. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the name of the account that is being closed.","title":"Close"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Close.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Close.__new__","text":"Create new instance of Close(meta, date, account)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Close.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Commodity","text":"An optional commodity declaration directive. Commodities generally do not need to be declared, but they may, and this is mainly created as intended to be used to attach meta-data on a commodity name. Whenever a plugin needs per-commodity meta-data, you would define such a commodity directive. Another use is to define a commodity that isn't otherwise (yet) used anywhere in an input file. (At the moment the date is meaningless but is specified for coherence with all the other directives; if you can think of a good use case, let us know). Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. currency str A string, the commodity under consideration.","title":"Commodity"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Commodity.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Commodity.__new__","text":"Create new instance of Commodity(meta, date, currency)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Commodity.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Custom","text":"A custom directive. This directive can be used to implement new experimental dated features in the Beancount file. This is meant as an intermediate measure to be used when you would need to implement a new directive in a plugin. These directives will be parsed liberally... any list of tokens are supported. All that is required is some unique name for them that acts as a \"type\". These directives are included in the stream and a plugin should be able to gather them. Attributes: Name Type Description meta Dict[str, Any] See above. type str A string that represents the type of the directive. values List A list of values of various simple types supported by the grammar. (Note that this list is not enforced to be consistent for all directives of the same type by the parser.)","title":"Custom"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Custom.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Custom.__new__","text":"Create new instance of Custom(meta, date, type, values)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Custom.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Document","text":"A document file declaration directive. This directive is used to attach a statement to an account, at a particular date. A typical usage would be to render PDF files or scans of your bank statements into the account's journal. While you can explicitly create those directives in the input syntax, it is much more convenient to provide Beancount with a root directory to search for filenames in a hierarchy mirroring the chart of accounts, filenames which should match the following dated format: \"YYYY-MM-DD.*\". See options for detail. Beancount will automatically create these documents directives based on the file hierarchy, and you can get them by parsing the list of entries. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the account which the statement or document is associated with. filename str The absolute filename of the document file. tags Optional[Set] A set of tag strings (without the '#'), or None, if an empty set. links Optional[Set] A set of link strings (without the '^'), or None, if an empty set.","title":"Document"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Document.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Document.__new__","text":"Create new instance of Document(meta, date, account, filename, tags, links)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Document.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Event","text":"An \"event value change\" directive. These directives are used as string variables that have different values over time. You can use these to track an address, your location, your current employer, anything you like. The kind of reporting that is made of these generic events is based on days and a timeline. For instance, if you need to track the number of days you spend in each country or state, create a \"location\" event and whenever you travel, add an event directive to indicate its new value. You should be able to write simple scripts against those in order to compute if you were present somewhere for a particular number of days. Here's an illustrative example usage, in order to maintain your health insurance coverage in Canada, you need to be present in the country for 183 days or more, excluding trips of less than 30 days. There is a similar test to be done in the US by aliens to figure out if they need to be considered as residents for tax purposes (the so-called \"substantial presence test\"). By integrating these directives into your bookkeeping, you can easily have a little program that computes the tests for you. This is, of course, entirely optional and somewhat auxiliary to the main purpose of double-entry bookkeeping, but correlates strongly with the transactions you insert in it, and so it's a really convenient thing to have in the same input file. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. \"type\" A short string, typically a single lowercase word, that defines a unique variable whose value changes over time. For example, 'location'. description str A free-form string, the value of the variable as of the date of the transaction.","title":"Event"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Event.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Event.__new__","text":"Create new instance of Event(meta, date, type, description)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Event.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Note","text":"A note directive, a general note that is attached to an account. These are used to attach text at a particular date in a specific account. The notes can be anything; a typical use would be to jot down an answer from a phone call to the institution represented by the account. It should show up in an account's journal. If you don't want this rendered, use the comment syntax in the input file, which does not get parsed and stored. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the account which the note is to be attached to. This is never None, notes always have an account they correspond to. comment str A free-form string, the text of the note. This can be long if you want it to.","title":"Note"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Note.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Note.__new__","text":"Create new instance of Note(meta, date, account, comment, tags, links)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Note.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Open","text":"An \"open account\" directive. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the name of the account that is being opened. currencies List[str] A list of strings, currencies that are allowed in this account. May be None, in which case it means that there are no restrictions on which currencies may be stored in this account. booking Optional[beancount.core.data.Booking] A Booking enum, the booking method to use to disambiguate postings to this account (when zero or more than one postings match the specification), or None if not specified. In practice, this attribute will be should be left unspecified (None) in the vast majority of cases. See Booking below for a selection of valid methods.","title":"Open"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Open.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Open.__new__","text":"Create new instance of Open(meta, date, account, currencies, booking)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Open.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Pad","text":"A \"pad this account with this other account\" directive. This directive automatically inserts transactions that will make the next chronological balance directive succeeds. It can be used to fill in missing date ranges of transactions, as a convenience. You don't have to use this, it's sugar coating in case you need it, while you're entering past history into your Ledger. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. account str A string, the name of the account which needs to be filled. source_account str A string, the name of the account which is used to debit from in order to fill 'account'.","title":"Pad"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Pad.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Pad.__new__","text":"Create new instance of Pad(meta, date, account, source_account)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Pad.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Price","text":"A price declaration directive. This establishes the price of a currency in terms of another currency as of the directive's date. A history of the prices for each currency pairs is built and can be queried within the bookkeeping system. Note that because Beancount does not store any data at time-of-day resolution, it makes no sense to have multiple price directives at the same date. (Beancount will not attempt to solve this problem; this is beyond the general scope of double-entry bookkeeping and if you need to build a day trading system, you should probably use something else). Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. currency: A string, the currency that is being priced, e.g. HOOL. amount: An instance of Amount, the number of units and currency that 'currency' is worth, for instance 1200.12 USD.","title":"Price"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Price.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Price.__new__","text":"Create new instance of Price(meta, date, currency, amount)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Price.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Query","text":"A named query declaration. This directive is used to create pre-canned queries that can then be automatically run or made available to the shell, or perhaps be rendered as part of a web interface. The purpose of this routine is to define useful queries for the context of the particular given Beancount input file. Attributes: Name Type Description meta Dict[str, Any] See above. date date The date at which this query should be run. All directives following this date will be ignored automatically. This is essentially equivalent to the CLOSE modifier in the shell syntax. name str A string, the unique identifier for the query. query_string str The SQL query string to be run or made available.","title":"Query"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Query.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Query.__new__","text":"Create new instance of Query(meta, date, name, query_string)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Query.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Transaction","text":"A transaction! This is the main type of object that we manipulate, and the entire reason this whole project exists in the first place, because representing these types of structures with a spreadsheet is difficult. Attributes: Name Type Description meta Dict[str, Any] See above. date date See above. flag str A single-character string or None. This user-specified string represents some custom/user-defined state of the transaction. You can use this for various purposes. Otherwise common, pre-defined flags are defined under beancount.core.flags, to flags transactions that are automatically generated. payee Optional[str] A free-form string that identifies the payee, or None, if absent. narration str A free-form string that provides a description for the transaction. All transactions have at least a narration string, this is never None. tags FrozenSet A set of tag strings (without the '#'), or EMPTY_SET. links FrozenSet A set of link strings (without the '^'), or EMPTY_SET. postings List[beancount.core.data.Posting] A list of Posting instances, the legs of this transaction. See the doc under Posting above.","title":"Transaction"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Transaction.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/data.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Transaction.__new__","text":"Create new instance of Transaction(meta, date, flag, payee, narration, tags, links, postings)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.dtypes.Transaction.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/data.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.data.create_simple_posting","text":"Create a simple posting on the entry, with just a number and currency (no cost). Parameters: entry \u2013 The entry instance to add the posting to. account \u2013 A string, the account to use on the posting. number \u2013 A Decimal number or string to use in the posting's Amount. currency \u2013 A string, the currency for the Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. Source code in beancount/core/data.py def create_simple_posting ( entry , account , number , currency ): \"\"\"Create a simple posting on the entry, with just a number and currency (no cost). Args: entry: The entry instance to add the posting to. account: A string, the account to use on the posting. number: A Decimal number or string to use in the posting's Amount. currency: A string, the currency for the Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. \"\"\" if isinstance ( account , str ): pass if number is None : units = None else : if not isinstance ( number , Decimal ): number = D ( number ) units = Amount ( number , currency ) posting = Posting ( account , units , None , None , None , None ) if entry is not None : entry . postings . append ( posting ) return posting","title":"create_simple_posting()"},{"location":"api_reference/beancount.core.html#beancount.core.data.create_simple_posting_with_cost","text":"Create a simple posting on the entry, with just a number and currency (no cost). Parameters: entry \u2013 The entry instance to add the posting to. account \u2013 A string, the account to use on the posting. number \u2013 A Decimal number or string to use in the posting's Amount. currency \u2013 A string, the currency for the Amount. cost_number \u2013 A Decimal number or string to use for the posting's cost Amount. cost_currency \u2013 a string, the currency for the cost Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. Source code in beancount/core/data.py def create_simple_posting_with_cost ( entry , account , number , currency , cost_number , cost_currency ): \"\"\"Create a simple posting on the entry, with just a number and currency (no cost). Args: entry: The entry instance to add the posting to. account: A string, the account to use on the posting. number: A Decimal number or string to use in the posting's Amount. currency: A string, the currency for the Amount. cost_number: A Decimal number or string to use for the posting's cost Amount. cost_currency: a string, the currency for the cost Amount. Returns: An instance of Posting, and as a side-effect the entry has had its list of postings modified with the new Posting instance. \"\"\" if isinstance ( account , str ): pass if not isinstance ( number , Decimal ): number = D ( number ) if cost_number and not isinstance ( cost_number , Decimal ): cost_number = D ( cost_number ) units = Amount ( number , currency ) cost = Cost ( cost_number , cost_currency , None , None ) posting = Posting ( account , units , cost , None , None , None ) if entry is not None : entry . postings . append ( posting ) return posting","title":"create_simple_posting_with_cost()"},{"location":"api_reference/beancount.core.html#beancount.core.data.entry_sortkey","text":"Sort-key for entries. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Parameters: entry \u2013 An entry instance. Returns: A tuple of (date, integer, integer), that forms the sort key for the entry. Source code in beancount/core/data.py def entry_sortkey ( entry ): \"\"\"Sort-key for entries. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Args: entry: An entry instance. Returns: A tuple of (date, integer, integer), that forms the sort key for the entry. \"\"\" return ( entry . date , SORT_ORDER . get ( type ( entry ), 0 ), entry . meta [ \"lineno\" ])","title":"entry_sortkey()"},{"location":"api_reference/beancount.core.html#beancount.core.data.filter_txns","text":"A generator that yields only the Transaction instances. This is such an incredibly common operation that it deserves a terse filtering mechanism. Parameters: entries \u2013 A list of directives. Yields: A sorted list of only the Transaction directives. Source code in beancount/core/data.py def filter_txns ( entries ): \"\"\"A generator that yields only the Transaction instances. This is such an incredibly common operation that it deserves a terse filtering mechanism. Args: entries: A list of directives. Yields: A sorted list of only the Transaction directives. \"\"\" for entry in entries : if isinstance ( entry , Transaction ): yield entry","title":"filter_txns()"},{"location":"api_reference/beancount.core.html#beancount.core.data.find_closest","text":"Find the closest entry from entries to (filename, lineno). Parameters: entries \u2013 A list of directives. filename \u2013 A string, the name of the ledger file to look for. Be careful to provide the very same filename, and note that the parser stores the absolute path of the filename here. lineno \u2013 An integer, the line number closest after the directive we're looking for. This may be the exact/first line of the directive. Returns: The closest entry found in the given file for the given filename, or None, if none could be found. Source code in beancount/core/data.py def find_closest ( entries , filename , lineno ): \"\"\"Find the closest entry from entries to (filename, lineno). Args: entries: A list of directives. filename: A string, the name of the ledger file to look for. Be careful to provide the very same filename, and note that the parser stores the absolute path of the filename here. lineno: An integer, the line number closest after the directive we're looking for. This may be the exact/first line of the directive. Returns: The closest entry found in the given file for the given filename, or None, if none could be found. \"\"\" min_diffline = sys . maxsize closest_entry = None for entry in entries : emeta = entry . meta if emeta [ \"filename\" ] == filename and emeta [ \"lineno\" ] > 0 : diffline = lineno - emeta [ \"lineno\" ] if 0 <= diffline < min_diffline : min_diffline = diffline closest_entry = entry return closest_entry","title":"find_closest()"},{"location":"api_reference/beancount.core.html#beancount.core.data.get_entry","text":"Return the entry associated with the posting or entry. Parameters: entry \u2013 A TxnPosting or entry instance Returns: A datetime instance. Source code in beancount/core/data.py def get_entry ( posting_or_entry ): \"\"\"Return the entry associated with the posting or entry. Args: entry: A TxnPosting or entry instance Returns: A datetime instance. \"\"\" return ( posting_or_entry . txn if isinstance ( posting_or_entry , TxnPosting ) else posting_or_entry )","title":"get_entry()"},{"location":"api_reference/beancount.core.html#beancount.core.data.has_entry_account_component","text":"Return true if one of the entry's postings has an account component. Parameters: entry \u2013 A Transaction entry. component \u2013 A string, a component of an account name. For instance, Food in Expenses:Food:Restaurant . All components are considered. Returns: Boolean \u2013 true if the component is in the account. Note that a component name must be whole, that is NY is not in Expenses:Taxes:StateNY . Source code in beancount/core/data.py def has_entry_account_component ( entry , component ): \"\"\"Return true if one of the entry's postings has an account component. Args: entry: A Transaction entry. component: A string, a component of an account name. For instance, ``Food`` in ``Expenses:Food:Restaurant``. All components are considered. Returns: Boolean: true if the component is in the account. Note that a component name must be whole, that is ``NY`` is not in ``Expenses:Taxes:StateNY``. \"\"\" return isinstance ( entry , Transaction ) and any ( has_component ( posting . account , component ) for posting in entry . postings )","title":"has_entry_account_component()"},{"location":"api_reference/beancount.core.html#beancount.core.data.iter_entry_dates","text":"Iterate over the entries in a date window. Parameters: entries \u2013 A date-sorted list of dated directives. date_begin \u2013 A datetime.date instance, the first date to include. date_end \u2013 A datetime.date instance, one day beyond the last date. Yields: Instances of the dated directives, between the dates, and in the order in which they appear. Source code in beancount/core/data.py def iter_entry_dates ( entries , date_begin , date_end ): \"\"\"Iterate over the entries in a date window. Args: entries: A date-sorted list of dated directives. date_begin: A datetime.date instance, the first date to include. date_end: A datetime.date instance, one day beyond the last date. Yields: Instances of the dated directives, between the dates, and in the order in which they appear. \"\"\" getdate = lambda entry : entry . date index_begin = bisect_left_with_key ( entries , date_begin , key = getdate ) index_end = bisect_left_with_key ( entries , date_end , key = getdate ) for index in range ( index_begin , index_end ): yield entries [ index ]","title":"iter_entry_dates()"},{"location":"api_reference/beancount.core.html#beancount.core.data.new_metadata","text":"Create a new metadata container from the filename and line number. Parameters: filename \u2013 A string, the filename for the creator of this directive. lineno \u2013 An integer, the line number where the directive has been created. kvlist \u2013 An optional container of key-values. Returns: A metadata dict. Source code in beancount/core/data.py def new_metadata ( filename , lineno , kvlist = None ): \"\"\"Create a new metadata container from the filename and line number. Args: filename: A string, the filename for the creator of this directive. lineno: An integer, the line number where the directive has been created. kvlist: An optional container of key-values. Returns: A metadata dict. \"\"\" meta = { \"filename\" : filename , \"lineno\" : lineno } if kvlist : meta . update ( kvlist ) return meta","title":"new_metadata()"},{"location":"api_reference/beancount.core.html#beancount.core.data.posting_has_conversion","text":"Return true if this position involves a conversion. A conversion is when there is a price attached to the amount but no cost. This is used on transactions to convert between units. Parameters: posting \u2013 an instance of Posting Returns: A boolean, true if this posting has a price conversion. Source code in beancount/core/data.py def posting_has_conversion ( posting ): \"\"\"Return true if this position involves a conversion. A conversion is when there is a price attached to the amount but no cost. This is used on transactions to convert between units. Args: posting: an instance of Posting Return: A boolean, true if this posting has a price conversion. \"\"\" return posting . cost is None and posting . price is not None","title":"posting_has_conversion()"},{"location":"api_reference/beancount.core.html#beancount.core.data.posting_sortkey","text":"Sort-key for entries or postings. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Parameters: entry \u2013 A Posting or entry instance Returns: A tuple of (date, integer, integer), that forms the sort key for the posting or entry. Source code in beancount/core/data.py def posting_sortkey ( entry ): \"\"\"Sort-key for entries or postings. We sort by date, except that checks should be placed in front of every list of entries of that same day, in order to balance linearly. Args: entry: A Posting or entry instance Returns: A tuple of (date, integer, integer), that forms the sort key for the posting or entry. \"\"\" if isinstance ( entry , TxnPosting ): entry = entry . txn return ( entry . date , SORT_ORDER . get ( type ( entry ), 0 ), entry . meta [ \"lineno\" ])","title":"posting_sortkey()"},{"location":"api_reference/beancount.core.html#beancount.core.data.remove_account_postings","text":"Remove all postings with the given account. Parameters: account \u2013 A string, the account name whose postings we want to remove. Returns: A list of entries without the rounding postings. Source code in beancount/core/data.py def remove_account_postings ( account , entries ): \"\"\"Remove all postings with the given account. Args: account: A string, the account name whose postings we want to remove. Returns: A list of entries without the rounding postings. \"\"\" new_entries = [] for entry in entries : if isinstance ( entry , Transaction ) and ( any ( posting . account == account for posting in entry . postings ) ): entry = entry . _replace ( postings = [ posting for posting in entry . postings if posting . account != account ] ) new_entries . append ( entry ) return new_entries","title":"remove_account_postings()"},{"location":"api_reference/beancount.core.html#beancount.core.data.sanity_check_types","text":"Check that the entry and its postings has all correct data types. Parameters: entry \u2013 An instance of one of the entries to be checked. allow_none_for_tags_and_links \u2013 A boolean, whether to allow plugins to generate Transaction objects with None as value for the 'tags' or 'links' attributes. Exceptions: AssertionError \u2013 If there is anything that is unexpected, raises an exception. Source code in beancount/core/data.py def sanity_check_types ( entry , allow_none_for_tags_and_links = False ): \"\"\"Check that the entry and its postings has all correct data types. Args: entry: An instance of one of the entries to be checked. allow_none_for_tags_and_links: A boolean, whether to allow plugins to generate Transaction objects with None as value for the 'tags' or 'links' attributes. Raises: AssertionError: If there is anything that is unexpected, raises an exception. \"\"\" assert isinstance ( entry , ALL_DIRECTIVES ), \"Invalid directive type\" assert isinstance ( entry . meta , dict ), \"Invalid type for meta\" assert \"filename\" in entry . meta , \"Missing filename in metadata\" assert \"lineno\" in entry . meta , \"Missing line number in metadata\" assert isinstance ( entry . date , datetime . date ), \"Invalid date type\" if isinstance ( entry , Transaction ): assert isinstance ( entry . flag , ( NoneType , str )), \"Invalid flag type\" assert isinstance ( entry . payee , ( NoneType , str )), \"Invalid payee type\" assert isinstance ( entry . narration , ( NoneType , str )), \"Invalid narration type\" set_types = ( ( NoneType , set , frozenset ) if allow_none_for_tags_and_links else ( set , frozenset ) ) assert isinstance ( entry . tags , set_types ), \"Invalid tags type: {} \" . format ( type ( entry . tags ) ) assert isinstance ( entry . links , set_types ), \"Invalid links type: {} \" . format ( type ( entry . links ) ) assert isinstance ( entry . postings , list ), \"Invalid postings list type\" for posting in entry . postings : assert isinstance ( posting , Posting ), \"Invalid posting type\" assert isinstance ( posting . account , str ), \"Invalid account type\" assert isinstance ( posting . units , ( Amount , NoneType )), \"Invalid units type\" assert isinstance ( posting . cost , ( Cost , CostSpec , NoneType )), \"Invalid cost type\" assert isinstance ( posting . price , ( Amount , NoneType )), \"Invalid price type\" assert isinstance ( posting . flag , ( str , NoneType )), \"Invalid flag type\"","title":"sanity_check_types()"},{"location":"api_reference/beancount.core.html#beancount.core.data.sorted","text":"A convenience to sort a list of entries, using entry_sortkey(). Parameters: entries \u2013 A list of directives. Returns: A sorted list of directives. Source code in beancount/core/data.py def sorted ( entries ): \"\"\"A convenience to sort a list of entries, using entry_sortkey(). Args: entries: A list of directives. Returns: A sorted list of directives. \"\"\" return builtins . sorted ( entries , key = entry_sortkey )","title":"sorted()"},{"location":"api_reference/beancount.core.html#beancount.core.data.transaction_has_conversion","text":"Given a Transaction entry, return true if at least one of the postings has a price conversion (without an associated cost). These are the source of non-zero conversion balances. Parameters: transaction \u2013 an instance of a Transaction entry. Returns: A boolean, true if this transaction contains at least one posting with a price conversion. Source code in beancount/core/data.py def transaction_has_conversion ( transaction ): \"\"\"Given a Transaction entry, return true if at least one of the postings has a price conversion (without an associated cost). These are the source of non-zero conversion balances. Args: transaction: an instance of a Transaction entry. Returns: A boolean, true if this transaction contains at least one posting with a price conversion. \"\"\" assert isinstance ( transaction , Transaction ), \"Invalid type of entry for transaction: {} \" . format ( transaction ) for posting in transaction . postings : if posting_has_conversion ( posting ): return True return False","title":"transaction_has_conversion()"},{"location":"api_reference/beancount.core.html#beancount.core.display_context","text":"A settings class to offer control over the number of digits rendered. This module contains routines that can accumulate information on the width and precision of numbers to be rendered and derive the precision required to render all of them consistently and under certain common alignment requirements. This is required in order to output neatly lined up columns of numbers in various styles. A common case is that the precision can be observed for numbers present in the input file. This display precision can be used as the \"precision by default\" if we write a routine for which it is inconvenient to feed all the numbers to build such an accumulator. Here are all the aspects supported by this module: PRECISION: Numbers for a particular currency are always rendered to the same precision, and they can be rendered to one of two precisions; either the most common number of fractional digits, or the maximum number of digits seen (this is useful for rendering prices). ALIGNMENT: Several alignment methods are supported. \"natural\": Render the strings as small as possible with no padding, but to their currency's precision. Like this: '1.2345' '764' '-7,409.01' '0.00000125' \"dot-aligned\": The periods will align vertically, the left and right sides are padded so that the column of numbers has the same width: ' 1.2345 ' ' 764 ' '-7,409.01 ' ' 0.00000125' \"right\": The strings are all flushed right, the left side is padded so that the column of numbers has the same width: ' 1.2345' ' 764' ' -7,409.01' ' 0.00000125' SIGN: If a negative sign is present in the input numbers, the rendered numbers reserve a space for it. If not, then we save the space. COMMAS: If the user requests to render commas, commas are rendered in the output. RESERVED: A number of extra integral digits reserved on the left in order to allow rendering novel numbers that haven't yet been seen. For example, balances may contains much larger numbers than the numbers seen in input files, and these need to be accommodated when aligning to the right.","title":"display_context"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.Align","text":"Alignment style for numbers.","title":"Align"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayContext","text":"A builder object used to construct a DisplayContext from a series of numbers. Attributes: Name Type Description ccontexts A dict of currency string to CurrencyContext instance. commas A bool, true if we should render commas. This just gets propagated onwards as the default value of to build with.","title":"DisplayContext"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayContext.build","text":"Build a formatter for the given display context. Parameters: alignment \u2013 The desired alignment. precision \u2013 The desired precision. commas \u2013 Whether to render commas or not. If 'None', the default value carried by the context will be used. reserved \u2013 An integer, the number of extra digits to be allocated in the maximum width calculations. Source code in beancount/core/display_context.py def build ( self , alignment = Align . NATURAL , precision = Precision . MOST_COMMON , commas = None , reserved = 0 , ): \"\"\"Build a formatter for the given display context. Args: alignment: The desired alignment. precision: The desired precision. commas: Whether to render commas or not. If 'None', the default value carried by the context will be used. reserved: An integer, the number of extra digits to be allocated in the maximum width calculations. \"\"\" if commas is None : commas = self . commas if alignment == Align . NATURAL : build_method = self . _build_natural elif alignment == Align . RIGHT : build_method = self . _build_right elif alignment == Align . DOT : build_method = self . _build_dot else : raise ValueError ( \"Unknown alignment: {} \" . format ( alignment )) fmtstrings = build_method ( precision , commas , reserved ) return DisplayFormatter ( self , precision , fmtstrings )","title":"build()"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayContext.quantize","text":"Quantize the given number to the given precision. Parameters: number \u2013 A Decimal instance, the number to be quantized. currency \u2013 A currency string. precision \u2013 Which precision to use. Returns: A Decimal instance, the quantized number. Source code in beancount/core/display_context.py def quantize ( self , number , currency , precision = Precision . MOST_COMMON ): \"\"\"Quantize the given number to the given precision. Args: number: A Decimal instance, the number to be quantized. currency: A currency string. precision: Which precision to use. Returns: A Decimal instance, the quantized number. \"\"\" assert isinstance ( number , Decimal ), \"Invalid data: {} \" . format ( number ) ccontext = self . ccontexts [ currency ] num_fractional_digits = ccontext . get_fractional ( precision ) if num_fractional_digits is None : # Note: We could probably logging.warn() this situation here. return number qdigit = Decimal ( 1 ) . scaleb ( - num_fractional_digits ) with decimal . localcontext () as ctx : # Allow precision for numbers as large as 1 billion in addition to # the required number of fractional digits. # # TODO(blais): Review this to assess performance impact, and whether # we could fold this outside a calling loop. ctx . prec = num_fractional_digits + 9 return number . quantize ( qdigit )","title":"quantize()"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayContext.set_commas","text":"Set the default value for rendering commas. Source code in beancount/core/display_context.py def set_commas ( self , commas ): \"\"\"Set the default value for rendering commas.\"\"\" self . commas = commas","title":"set_commas()"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayContext.update","text":"Update the builder with the given number for the given currency. Parameters: number \u2013 An instance of Decimal to consider for this currency. currency \u2013 An optional string, the currency this numbers applies to. Source code in beancount/core/display_context.py def update ( self , number , currency = \"__default__\" ): \"\"\"Update the builder with the given number for the given currency. Args: number: An instance of Decimal to consider for this currency. currency: An optional string, the currency this numbers applies to. \"\"\" self . ccontexts [ currency ] . update ( number )","title":"update()"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayContext.update_from","text":"Update the builder with the other given DisplayContext. Parameters: other \u2013 Another DisplayContext. Source code in beancount/core/display_context.py def update_from ( self , other ): \"\"\"Update the builder with the other given DisplayContext. Args: other: Another DisplayContext. \"\"\" for currency , ccontext in other . ccontexts . items (): self . ccontexts [ currency ] . update_from ( ccontext )","title":"update_from()"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.DisplayFormatter","text":"A class used to contain various settings that control how we output numbers. In particular, the precision used for each currency, and whether or not commas should be printed. This object is intended to be passed around to all functions that format numbers to strings. Attributes: Name Type Description dcontext A DisplayContext instance. precision An enum of Precision from which it was built. fmtstrings A dict of currency to pre-baked format strings for it. fmtfuncs A dict of currency to pre-baked formatting functions for it.","title":"DisplayFormatter"},{"location":"api_reference/beancount.core.html#beancount.core.display_context.Precision","text":"The type of precision required.","title":"Precision"},{"location":"api_reference/beancount.core.html#beancount.core.distribution","text":"A simple accumulator for data about a mathematical distribution.","title":"distribution"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution","text":"A class that computes a histogram of integer values. This is used to compute a length that will cover at least some decent fraction of the samples.","title":"Distribution"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution.empty","text":"Return true if the distribution is empty. Returns: A boolean. Source code in beancount/core/distribution.py def empty ( self ): \"\"\"Return true if the distribution is empty. Returns: A boolean. \"\"\" return len ( self . hist ) == 0","title":"empty()"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution.max","text":"Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. Source code in beancount/core/distribution.py def max ( self ): \"\"\"Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. \"\"\" if not self . hist : return None value , _ = sorted ( self . hist . items ())[ - 1 ] return value","title":"max()"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution.min","text":"Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. Source code in beancount/core/distribution.py def min ( self ): \"\"\"Return the minimum value seen in the distribution. Returns: An element of the value type, or None, if the distribution was empty. \"\"\" if not self . hist : return None value , _ = sorted ( self . hist . items ())[ 0 ] return value","title":"min()"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution.mode","text":"Return the mode of the distribution. Returns: An element of the value type, or None, if the distribution was empty. Source code in beancount/core/distribution.py def mode ( self ): \"\"\"Return the mode of the distribution. Returns: An element of the value type, or None, if the distribution was empty. \"\"\" if not self . hist : return None max_value = 0 max_count = 0 for value , count in sorted ( self . hist . items ()): if count >= max_count : max_count = count max_value = value return max_value","title":"mode()"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution.update","text":"Add a sample to the distribution. Parameters: value \u2013 A value of the function. Source code in beancount/core/distribution.py def update ( self , value ): \"\"\"Add a sample to the distribution. Args: value: A value of the function. \"\"\" self . hist [ value ] += 1","title":"update()"},{"location":"api_reference/beancount.core.html#beancount.core.distribution.Distribution.update_from","text":"Add samples from the other distribution to this one. Parameters: other \u2013 Another distribution. Source code in beancount/core/distribution.py def update_from ( self , other ): \"\"\"Add samples from the other distribution to this one. Args: other: Another distribution. \"\"\" for value , count in other . hist . items (): self . hist [ value ] += count","title":"update_from()"},{"location":"api_reference/beancount.core.html#beancount.core.flags","text":"Flag constants.","title":"flags"},{"location":"api_reference/beancount.core.html#beancount.core.getters","text":"Getter functions that operate on lists of entries to return various lists of things that they reference, accounts, tags, links, currencies, etc.","title":"getters"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts","text":"Accounts gatherer.","title":"GetAccounts"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Balance","text":"Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one ( _ , entry ): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return ( entry . account ,)","title":"Balance()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Close","text":"Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one ( _ , entry ): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return ( entry . account ,)","title":"Close()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Commodity","text":"Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero ( _ , entry ): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return ()","title":"Commodity()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Custom","text":"Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero ( _ , entry ): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return ()","title":"Custom()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Document","text":"Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one ( _ , entry ): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return ( entry . account ,)","title":"Document()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Event","text":"Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero ( _ , entry ): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return ()","title":"Event()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Note","text":"Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one ( _ , entry ): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return ( entry . account ,)","title":"Note()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Open","text":"Process directives with a single account attribute. Parameters: entry \u2013 An instance of a directive. Returns: The single account of this directive. Source code in beancount/core/getters.py def _one ( _ , entry ): \"\"\"Process directives with a single account attribute. Args: entry: An instance of a directive. Returns: The single account of this directive. \"\"\" return ( entry . account ,)","title":"Open()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Pad","text":"Process a Pad directive. Parameters: entry \u2013 An instance of Pad. Returns: The two accounts of the Pad directive. Source code in beancount/core/getters.py def Pad ( _ , entry ): \"\"\"Process a Pad directive. Args: entry: An instance of Pad. Returns: The two accounts of the Pad directive. \"\"\" return ( entry . account , entry . source_account )","title":"Pad()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Price","text":"Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero ( _ , entry ): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return ()","title":"Price()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Query","text":"Process directives with no accounts. Parameters: entry \u2013 An instance of a directive. Returns: An empty list Source code in beancount/core/getters.py def _zero ( _ , entry ): \"\"\"Process directives with no accounts. Args: entry: An instance of a directive. Returns: An empty list \"\"\" return ()","title":"Query()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.Transaction","text":"Process a Transaction directive. Parameters: entry \u2013 An instance of Transaction. Yields: The accounts of the legs of the transaction. Source code in beancount/core/getters.py def Transaction ( _ , entry ): \"\"\"Process a Transaction directive. Args: entry: An instance of Transaction. Yields: The accounts of the legs of the transaction. \"\"\" for posting in entry . postings : yield posting . account","title":"Transaction()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.get_accounts_use_map","text":"Gather the list of accounts from the list of entries. Parameters: entries \u2013 A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. Source code in beancount/core/getters.py def get_accounts_use_map ( self , entries ): \"\"\"Gather the list of accounts from the list of entries. Args: entries: A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. \"\"\" accounts_first = {} accounts_last = {} for entry in entries : method = getattr ( self , entry . __class__ . __name__ ) for account_ in method ( entry ): if account_ not in accounts_first : accounts_first [ account_ ] = entry . date accounts_last [ account_ ] = entry . date return accounts_first , accounts_last","title":"get_accounts_use_map()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.GetAccounts.get_entry_accounts","text":"Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Parameters: entry \u2013 A directive instance. Returns: A set of account name strings. Source code in beancount/core/getters.py def get_entry_accounts ( self , entry ): \"\"\"Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Args: entry: A directive instance. Returns: A set of account name strings. \"\"\" method = getattr ( self , entry . __class__ . __name__ ) return set ( method ( entry ))","title":"get_entry_accounts()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_account_components","text":"Gather all the account components available in the given directives. Parameters: entries \u2013 A list of directive instances. Returns: A list of strings, the unique account components, including the root account names. Source code in beancount/core/getters.py def get_account_components ( entries ): \"\"\"Gather all the account components available in the given directives. Args: entries: A list of directive instances. Returns: A list of strings, the unique account components, including the root account names. \"\"\" accounts = get_accounts ( entries ) components = set () for account_name in accounts : components . update ( account . split ( account_name )) return sorted ( components )","title":"get_account_components()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_account_open_close","text":"Fetch the open/close entries for each of the accounts. If an open or close entry happens to be duplicated, accept the earliest entry (chronologically). Parameters: entries \u2013 A list of directive instances. Returns: A map of account name strings to pairs of (open-directive, close-directive) tuples. Source code in beancount/core/getters.py def get_account_open_close ( entries ): \"\"\"Fetch the open/close entries for each of the accounts. If an open or close entry happens to be duplicated, accept the earliest entry (chronologically). Args: entries: A list of directive instances. Returns: A map of account name strings to pairs of (open-directive, close-directive) tuples. \"\"\" # A dict of account name to (open-entry, close-entry). open_close_map = defaultdict ( lambda : [ None , None ]) for entry in entries : if not isinstance ( entry , ( Open , Close )): continue open_close = open_close_map [ entry . account ] index = 0 if isinstance ( entry , Open ) else 1 previous_entry = open_close [ index ] if previous_entry is not None : if previous_entry . date <= entry . date : entry = previous_entry open_close [ index ] = entry return dict ( open_close_map )","title":"get_account_open_close()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_accounts","text":"Gather all the accounts references by a list of directives. Parameters: entries \u2013 A list of directive instances. Returns: A set of account strings. Source code in beancount/core/getters.py def get_accounts ( entries ): \"\"\"Gather all the accounts references by a list of directives. Args: entries: A list of directive instances. Returns: A set of account strings. \"\"\" _ , accounts_last = _GetAccounts . get_accounts_use_map ( entries ) return accounts_last . keys ()","title":"get_accounts()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_accounts_use_map","text":"Gather all the accounts references by a list of directives. Parameters: entries \u2013 A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. Source code in beancount/core/getters.py def get_accounts_use_map ( entries ): \"\"\"Gather all the accounts references by a list of directives. Args: entries: A list of directive instances. Returns: A pair of dictionaries of account name to date, one for first date used and one for last date used. The keys should be identical. \"\"\" return _GetAccounts . get_accounts_use_map ( entries )","title":"get_accounts_use_map()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_active_years","text":"Yield all the years that have at least one entry in them. Parameters: entries \u2013 A list of directive instances. Yields: Unique dates see in the list of directives. Source code in beancount/core/getters.py def get_active_years ( entries ): \"\"\"Yield all the years that have at least one entry in them. Args: entries: A list of directive instances. Yields: Unique dates see in the list of directives. \"\"\" seen = set () prev_year = None for entry in entries : year = entry . date . year if year != prev_year : prev_year = year assert year not in seen seen . add ( year ) yield year","title":"get_active_years()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_all_links","text":"Return a list of all the links seen in the given entries. Parameters: entries \u2013 A list of directive instances. Returns: A set of links strings. Source code in beancount/core/getters.py def get_all_links ( entries ): \"\"\"Return a list of all the links seen in the given entries. Args: entries: A list of directive instances. Returns: A set of links strings. \"\"\" all_links = set () for entry in entries : if not isinstance ( entry , Transaction ): continue if entry . links : all_links . update ( entry . links ) return sorted ( all_links )","title":"get_all_links()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_all_payees","text":"Return a list of all the unique payees seen in the given entries. Parameters: entries \u2013 A list of directive instances. Returns: A set of payee strings. Source code in beancount/core/getters.py def get_all_payees ( entries ): \"\"\"Return a list of all the unique payees seen in the given entries. Args: entries: A list of directive instances. Returns: A set of payee strings. \"\"\" all_payees = set () for entry in entries : if not isinstance ( entry , Transaction ): continue all_payees . add ( entry . payee ) all_payees . discard ( None ) return sorted ( all_payees )","title":"get_all_payees()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_all_tags","text":"Return a list of all the tags seen in the given entries. Parameters: entries \u2013 A list of directive instances. Returns: A set of tag strings. Source code in beancount/core/getters.py def get_all_tags ( entries ): \"\"\"Return a list of all the tags seen in the given entries. Args: entries: A list of directive instances. Returns: A set of tag strings. \"\"\" all_tags = set () for entry in entries : if not isinstance ( entry , Transaction ): continue if entry . tags : all_tags . update ( entry . tags ) return sorted ( all_tags )","title":"get_all_tags()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_commodity_directives","text":"Create map of commodity names to Commodity entries. Parameters: entries \u2013 A list of directive instances. Returns: A map of commodity name strings to Commodity directives. Source code in beancount/core/getters.py def get_commodity_directives ( entries ): \"\"\"Create map of commodity names to Commodity entries. Args: entries: A list of directive instances. Returns: A map of commodity name strings to Commodity directives. \"\"\" return { entry . currency : entry for entry in entries if isinstance ( entry , Commodity )}","title":"get_commodity_directives()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_dict_accounts","text":"Return a nested dict of all the unique leaf names. account names are labelled with LABEL=True Parameters: account_names \u2013 An iterable of account names (strings) Returns: A nested OrderedDict of account leafs Source code in beancount/core/getters.py def get_dict_accounts ( account_names ): \"\"\"Return a nested dict of all the unique leaf names. account names are labelled with LABEL=True Args: account_names: An iterable of account names (strings) Returns: A nested OrderedDict of account leafs \"\"\" leveldict = OrderedDict () for account_name in account_names : nested_dict = leveldict for component in account . split ( account_name ): nested_dict = nested_dict . setdefault ( component , OrderedDict ()) nested_dict [ get_dict_accounts . ACCOUNT_LABEL ] = True return leveldict","title":"get_dict_accounts()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_entry_accounts","text":"Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Parameters: entries \u2013 A directive instance. Returns: A set of account strings. Source code in beancount/core/getters.py def get_entry_accounts ( entry ): \"\"\"Gather all the accounts references by a single directive. Note: This should get replaced by a method on each directive eventually, that would be the clean way to do this. Args: entries: A directive instance. Returns: A set of account strings. \"\"\" return _GetAccounts . get_entry_accounts ( entry )","title":"get_entry_accounts()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_leveln_parent_accounts","text":"Return a list of all the unique leaf names at level N in an account hierarchy. Parameters: account_names \u2013 A list of account names (strings) level \u2013 The level to cross-cut. 0 is for root accounts. nrepeats \u2013 A minimum number of times a leaf is required to be present in the the list of unique account names in order to be returned by this function. Returns: A list of leaf node names. Source code in beancount/core/getters.py def get_leveln_parent_accounts ( account_names , level , nrepeats = 0 ): \"\"\"Return a list of all the unique leaf names at level N in an account hierarchy. Args: account_names: A list of account names (strings) level: The level to cross-cut. 0 is for root accounts. nrepeats: A minimum number of times a leaf is required to be present in the the list of unique account names in order to be returned by this function. Returns: A list of leaf node names. \"\"\" leveldict = defaultdict ( int ) for account_name in set ( account_names ): components = account . split ( account_name ) if level < len ( components ): leveldict [ components [ level ]] += 1 levels = { level_ for level_ , count in leveldict . items () if count > nrepeats } return sorted ( levels )","title":"get_leveln_parent_accounts()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_min_max_dates","text":"Return the minimum and maximum dates in the list of entries. Parameters: entries \u2013 A list of directive instances. types \u2013 An optional tuple of types to restrict the entries to. Returns: A pair of datetime.date dates, the minimum and maximum dates seen in the directives. Source code in beancount/core/getters.py def get_min_max_dates ( entries , types = None ): \"\"\"Return the minimum and maximum dates in the list of entries. Args: entries: A list of directive instances. types: An optional tuple of types to restrict the entries to. Returns: A pair of datetime.date dates, the minimum and maximum dates seen in the directives. \"\"\" date_first = date_last = None for entry in entries : if types and not isinstance ( entry , types ): continue date_first = entry . date break for entry in reversed ( entries ): if types and not isinstance ( entry , types ): continue date_last = entry . date break return ( date_first , date_last )","title":"get_min_max_dates()"},{"location":"api_reference/beancount.core.html#beancount.core.getters.get_values_meta","text":"Get a map of the metadata from a map of entries values. Given a dict of some key to a directive instance (or None), return a mapping of the key to the metadata extracted from each directive, or a default value. This can be used to gather a particular piece of metadata from an accounts map or a commodities map. Parameters: name_to_entries_map \u2013 A dict of something to an entry or None. meta_keys \u2013 A list of strings, the keys to fetch from the metadata. default \u2013 The default value to use if the metadata is not available or if the value/entry is None. Returns: A mapping of the keys of name_to_entries_map to the values of the 'meta_keys' metadata. If there are multiple 'meta_keys', each value is a tuple of them. On the other hand, if there is only a single one, the value itself is returned. Source code in beancount/core/getters.py def get_values_meta ( name_to_entries_map , * meta_keys , default = None ): \"\"\"Get a map of the metadata from a map of entries values. Given a dict of some key to a directive instance (or None), return a mapping of the key to the metadata extracted from each directive, or a default value. This can be used to gather a particular piece of metadata from an accounts map or a commodities map. Args: name_to_entries_map: A dict of something to an entry or None. meta_keys: A list of strings, the keys to fetch from the metadata. default: The default value to use if the metadata is not available or if the value/entry is None. Returns: A mapping of the keys of name_to_entries_map to the values of the 'meta_keys' metadata. If there are multiple 'meta_keys', each value is a tuple of them. On the other hand, if there is only a single one, the value itself is returned. \"\"\" value_map = {} for key , entry in name_to_entries_map . items (): value_list = [] for meta_key in meta_keys : value_list . append ( entry . meta . get ( meta_key , default ) if entry is not None else default ) value_map [ key ] = value_list [ 0 ] if len ( meta_keys ) == 1 else tuple ( value_list ) return value_map","title":"get_values_meta()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate","text":"Code used to automatically complete postings without positions.","title":"interpolate"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.BalanceError","text":"BalanceError(source, message, entry)","title":"BalanceError"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.BalanceError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/interpolate.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.BalanceError.__new__","text":"Create new instance of BalanceError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.BalanceError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/interpolate.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.compute_entries_balance","text":"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. Parameters: entries \u2013 A list of directives. prefix \u2013 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 \u2013 A datetime.date instance at which to stop adding up the balance. The date is exclusive. Returns: An instance of Inventory. Source code in beancount/core/interpolate.py 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","title":"compute_entries_balance()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.compute_entry_context","text":"Compute the balances of all accounts referenced by entry up to entry. This provides the inventory of the accounts to which the entry is to be applied, before and after. Parameters: entries \u2013 A list of directives. context_entry \u2013 The entry for which we want to obtain the before and after context. additional_accounts \u2013 Additional list of accounts to include in calculating the balance. This is used when invoked for debugging, in case the booked & interpolated transaction doesn't have all the accounts we need because it had an error (the booking code will remove invalid postings). Returns: Two dicts of account-name to Inventory instance, one which represents the context before the entry is applied, and one that represents the context after it has been applied. Source code in beancount/core/interpolate.py def compute_entry_context ( entries , context_entry , additional_accounts = None ): \"\"\"Compute the balances of all accounts referenced by entry up to entry. This provides the inventory of the accounts to which the entry is to be applied, before and after. Args: entries: A list of directives. context_entry: The entry for which we want to obtain the before and after context. additional_accounts: Additional list of accounts to include in calculating the balance. This is used when invoked for debugging, in case the booked & interpolated transaction doesn't have all the accounts we need because it had an error (the booking code will remove invalid postings). Returns: Two dicts of account-name to Inventory instance, one which represents the context before the entry is applied, and one that represents the context after it has been applied. \"\"\" assert context_entry is not None , \"context_entry is missing.\" # Get the set of accounts for which to compute the context. context_accounts = getters . get_entry_accounts ( context_entry ) if additional_accounts : context_accounts . update ( additional_accounts ) # Iterate over the entries until we find the target one and accumulate the # balance. context_before = collections . defaultdict ( inventory . Inventory ) for entry in entries : if entry is context_entry : break if isinstance ( entry , Transaction ): for posting in entry . postings : if not any ( posting . account == account for account in context_accounts ): continue balance = context_before [ posting . account ] balance . add_position ( posting ) # Compute the after context for the entry. context_after = copy . deepcopy ( context_before ) if isinstance ( context_entry , Transaction ): for posting in entry . postings : balance = context_after [ posting . account ] balance . add_position ( posting ) return context_before , context_after","title":"compute_entry_context()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.compute_residual","text":"Compute the residual of a set of complete postings, and the per-currency precision. This is used to cross-check a balanced transaction. The precision is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. Parameters: postings \u2013 A list of Posting instances. Returns: An instance of Inventory, with the residual of the given list of postings. Source code in beancount/core/interpolate.py def compute_residual ( postings ): \"\"\"Compute the residual of a set of complete postings, and the per-currency precision. This is used to cross-check a balanced transaction. The precision is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. Args: postings: A list of Posting instances. Returns: An instance of Inventory, with the residual of the given list of postings. \"\"\" inventory = Inventory () for posting in postings : # Skip auto-postings inserted to absorb the residual (rounding error). if posting . meta and posting . meta . get ( AUTOMATIC_RESIDUAL , False ): continue # Add to total residual balance. inventory . add_amount ( convert . get_weight ( posting )) return inventory","title":"compute_residual()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.fill_residual_posting","text":"If necessary, insert a posting to absorb the residual. This makes the transaction balance exactly. Note: This was developed in order to tweak transactions before exporting them to Ledger. A better method would be to enable the feature that automatically inserts these rounding postings on all transactions, and so maybe this method can be deprecated if we do so. Parameters: entry \u2013 An instance of a Transaction. account_rounding \u2013 A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A possibly new, modified entry with a new posting. If a residual was not needed - the transaction already balanced perfectly - no new leg is inserted. Source code in beancount/core/interpolate.py def fill_residual_posting ( entry , account_rounding ): \"\"\"If necessary, insert a posting to absorb the residual. This makes the transaction balance exactly. Note: This was developed in order to tweak transactions before exporting them to Ledger. A better method would be to enable the feature that automatically inserts these rounding postings on all transactions, and so maybe this method can be deprecated if we do so. Args: entry: An instance of a Transaction. account_rounding: A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A possibly new, modified entry with a new posting. If a residual was not needed - the transaction already balanced perfectly - no new leg is inserted. \"\"\" residual = compute_residual ( entry . postings ) if not residual . is_empty (): new_postings = list ( entry . postings ) new_postings . extend ( get_residual_postings ( residual , account_rounding )) entry = entry . _replace ( postings = new_postings ) return entry","title":"fill_residual_posting()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.get_residual_postings","text":"Create postings to book the given residuals. Parameters: residual \u2013 An Inventory, the residual positions. account_rounding \u2013 A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A list of new postings to be inserted to reduce the given residual. Source code in beancount/core/interpolate.py def get_residual_postings ( residual , account_rounding ): \"\"\"Create postings to book the given residuals. Args: residual: An Inventory, the residual positions. account_rounding: A string, the name of the rounding account that absorbs residuals / rounding errors. Returns: A list of new postings to be inserted to reduce the given residual. \"\"\" meta = { AUTOMATIC_META : True , AUTOMATIC_RESIDUAL : True } return [ Posting ( account_rounding , - position . units , position . cost , None , None , meta . copy ()) for position in residual . get_positions () ]","title":"get_residual_postings()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.has_nontrivial_balance","text":"Return True if a Posting has a balance amount that would have to be calculated. Parameters: posting \u2013 A Posting instance. Returns: A boolean. Source code in beancount/core/interpolate.py def has_nontrivial_balance ( posting ): \"\"\"Return True if a Posting has a balance amount that would have to be calculated. Args: posting: A Posting instance. Returns: A boolean. \"\"\" return posting . cost or posting . price","title":"has_nontrivial_balance()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.infer_tolerances","text":"Infer tolerances from a list of postings. The tolerance is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. The 'use_cost' option allows one to experiment with letting postings at cost and at price influence the maximum value of the tolerance. It's tricky to use and alters the definition of the tolerance in a non-trivial way, if you use it. The tolerance is expanded by the sum of the cost times a fraction 'M' of the smallest digits in the number of units for all postings held at cost. For example, in this transaction: 2006-01-17 * \"Plan Contribution\" Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:Cash -1150.00 USD The tolerance for units of USD will calculated as the MAXIMUM of: 0.01 * M = 0.005 (from the 1150.00 USD leg) The sum of 0.001 * M x 30.96 = 0.01548 + 0.001 * M x 30.96 = 0.01548 = 0.03096 So the tolerance for USD in this case is max(0.005, 0.03096) = 0.03096. Prices contribute similarly to the maximum tolerance allowed. Note that 'M' above is the inferred_tolerance_multiplier and its default value is 0.5. Parameters: postings \u2013 A list of Posting instances. options_map \u2013 A dict of options. use_cost \u2013 A boolean, true if we should be using a combination of the smallest digit of the number times the cost or price in order to infer the tolerance. If the value is left unspecified (as 'None'), the default value can be overridden by setting an option. Returns: A dict of currency to the tolerated difference amount to be used for it, e.g. 0.005. Source code in beancount/core/interpolate.py def infer_tolerances ( postings , options_map , use_cost = None ): \"\"\"Infer tolerances from a list of postings. The tolerance is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. The 'use_cost' option allows one to experiment with letting postings at cost and at price influence the maximum value of the tolerance. It's tricky to use and alters the definition of the tolerance in a non-trivial way, if you use it. The tolerance is expanded by the sum of the cost times a fraction 'M' of the smallest digits in the number of units for all postings held at cost. For example, in this transaction: 2006-01-17 * \"Plan Contribution\" Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:Cash -1150.00 USD The tolerance for units of USD will calculated as the MAXIMUM of: 0.01 * M = 0.005 (from the 1150.00 USD leg) The sum of 0.001 * M x 30.96 = 0.01548 + 0.001 * M x 30.96 = 0.01548 = 0.03096 So the tolerance for USD in this case is max(0.005, 0.03096) = 0.03096. Prices contribute similarly to the maximum tolerance allowed. Note that 'M' above is the inferred_tolerance_multiplier and its default value is 0.5. Args: postings: A list of Posting instances. options_map: A dict of options. use_cost: A boolean, true if we should be using a combination of the smallest digit of the number times the cost or price in order to infer the tolerance. If the value is left unspecified (as 'None'), the default value can be overridden by setting an option. Returns: A dict of currency to the tolerated difference amount to be used for it, e.g. 0.005. \"\"\" if use_cost is None : use_cost = options_map [ \"infer_tolerance_from_cost\" ] inferred_tolerance_multiplier = options_map [ \"inferred_tolerance_multiplier\" ] default_tolerances = options_map [ \"inferred_tolerance_default\" ] tolerances = default_tolerances . copy () cost_tolerances = collections . defaultdict ( D ) for posting in postings : # Skip the precision on automatically inferred postings. if posting . meta and AUTOMATIC_META in posting . meta : continue units = posting . units if not ( isinstance ( units , Amount ) and isinstance ( units . number , Decimal )): continue # Compute bounds on the number. currency = units . currency expo = units . number . as_tuple () . exponent if expo < 0 : # Note: the exponent is a negative value. tolerance = ONE . scaleb ( expo ) * inferred_tolerance_multiplier # Note that we take the max() and not the min() here because the # tolerance has a dual purpose: it's used to infer the resolution # for interpolation (where we might want the min()) and also for # balance checks (where we favor the looser/larger tolerance). tolerances [ currency ] = max ( tolerance , tolerances . get ( currency , - 1024 )) if not use_cost : continue # Compute bounds on the smallest digit of the number implied as cost. cost = posting . cost if cost is not None : cost_currency = cost . currency if isinstance ( cost , Cost ): cost_tolerance = min ( tolerance * cost . number , MAXIMUM_TOLERANCE ) else : assert isinstance ( cost , CostSpec ) cost_tolerance = MAXIMUM_TOLERANCE for cost_number in cost . number_total , cost . number_per : if cost_number is None or cost_number is MISSING : continue cost_tolerance = min ( tolerance * cost_number , cost_tolerance ) cost_tolerances [ cost_currency ] += cost_tolerance # Compute bounds on the smallest digit of the number implied as cost. price = posting . price if isinstance ( price , Amount ) and isinstance ( price . number , Decimal ): price_currency = price . currency price_tolerance = min ( tolerance * price . number , MAXIMUM_TOLERANCE ) cost_tolerances [ price_currency ] += price_tolerance for currency , tolerance in cost_tolerances . items (): tolerances [ currency ] = max ( tolerance , tolerances . get ( currency , - 1024 )) default = tolerances . pop ( \"*\" , ZERO ) return defdict . ImmutableDictWithDefault ( tolerances , default = default )","title":"infer_tolerances()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.is_tolerance_user_specified","text":"Return true if the given tolerance number was user-specified. This would allow the user to provide a tolerance like # 0.1234 but not 0.123456. This is used to detect whether a tolerance value # is input by the user and not inferred automatically. Parameters: tolerance \u2013 An instance of Decimal. Returns: A boolean. Source code in beancount/core/interpolate.py def is_tolerance_user_specified ( tolerance ): \"\"\"Return true if the given tolerance number was user-specified. This would allow the user to provide a tolerance like # 0.1234 but not 0.123456. This is used to detect whether a tolerance value # is input by the user and not inferred automatically. Args: tolerance: An instance of Decimal. Returns: A boolean. \"\"\" return len ( tolerance . as_tuple () . digits ) < MAX_TOLERANCE_DIGITS","title":"is_tolerance_user_specified()"},{"location":"api_reference/beancount.core.html#beancount.core.interpolate.quantize_with_tolerance","text":"Quantize the units using the tolerance dict. Parameters: tolerances \u2013 A dict of currency to tolerance Decimalvalues. number \u2013 A number to quantize. currency \u2013 A string currency. Returns: A Decimal, the number possibly quantized. Source code in beancount/core/interpolate.py def quantize_with_tolerance ( tolerances , currency , number ): \"\"\"Quantize the units using the tolerance dict. Args: tolerances: A dict of currency to tolerance Decimalvalues. number: A number to quantize. currency: A string currency. Returns: A Decimal, the number possibly quantized. \"\"\" # Applying rounding to the default tolerance, if there is one. tolerance = tolerances . get ( currency ) if tolerance : quantum = ( tolerance * 2 ) . normalize () # If the tolerance is a neat number provided by the user, # quantize the inferred numbers. See doc on quantize(): # # Unlike other operations, if the length of the coefficient # after the quantize operation would be greater than # precision, then an InvalidOperation is signaled. This # guarantees that, unless there is an error condition, the # quantized exponent is always equal to that of the # right-hand operand. if is_tolerance_user_specified ( quantum ): number = number . quantize ( quantum ) return number","title":"quantize_with_tolerance()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory","text":"A container for an inventory of positions. This module provides a container class that can hold positions. An inventory is a mapping of positions, where each position is keyed by (currency: str, cost: Cost) -> position: Position where 'currency': The commodity under consideration, USD, CAD, or stock units such as HOOL, MSFT, AAPL, etc.; 'cost': None or a Cost instance existing of cost currency, number, date, and label; 'position': A Position object, whose 'units' attribute is guaranteed to have the same currency as 'currency' and whose 'cost' attribute is equal to the 'cost' key. It basically stores the number of units. This is meant to accommodate both booked and non-booked amounts. The clever trick that we pull to do this is that for positions which aren't booked, we simply leave the 'cost' as None. This is the case for most of the transactions. = Conversions = If it often desired to convert this inventory into an equivalent position for its cost, or to just flatten all the positions with the same currency and count the number of units, or to compute the market value for the inventory at a specific date. You do these conversions using the reduce() method: inventory.reduce(convert.get_cost) inventory.reduce(convert.get_units) inventory.reduce(convert.get_value, price_map, date)","title":"inventory"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory","text":"An Inventory is a set of positions, indexed for efficiency.","title":"Inventory"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__abs__","text":"Return an inventory with the absolute value of each position. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def __abs__ ( self ): \"\"\"Return an inventory with the absolute value of each position. Returns: An instance of Inventory. \"\"\" return Inventory ({ key : abs ( pos ) for key , pos in self . items ()})","title":"__abs__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__add__","text":"Add another inventory to this one. This inventory is not modified. Parameters: other \u2013 An instance of Inventory. Returns: A new instance of Inventory. Source code in beancount/core/inventory.py def __add__ ( self , other ): \"\"\"Add another inventory to this one. This inventory is not modified. Args: other: An instance of Inventory. Returns: A new instance of Inventory. \"\"\" new_inventory = self . __copy__ () new_inventory . add_inventory ( other ) return new_inventory","title":"__add__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__copy__","text":"A shallow copy of this inventory object. Returns: An instance of Inventory, equal to this one. Source code in beancount/core/inventory.py def __copy__ ( self ): \"\"\"A shallow copy of this inventory object. Returns: An instance of Inventory, equal to this one. \"\"\" return Inventory ( self )","title":"__copy__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__iadd__","text":"Add all the positions of another Inventory instance to this one. Parameters: other \u2013 An instance of Inventory to add to this one. Returns: This inventory, modified. Source code in beancount/core/inventory.py def add_inventory ( self , other ): \"\"\"Add all the positions of another Inventory instance to this one. Args: other: An instance of Inventory to add to this one. Returns: This inventory, modified. \"\"\" if self . is_empty (): # Optimization for empty inventories; if the current one is empty, # adopt all of the other inventory's positions without running # through the full aggregation checks. This should be very cheap. We # can do this because the positions are immutable. self . update ( other ) else : for position in other . get_positions (): self . add_position ( position ) return self","title":"__iadd__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__init__","text":"Create a new inventory using a list of existing positions. Parameters: positions \u2013 A list of Position instances or an existing dict or Inventory instance. Source code in beancount/core/inventory.py def __init__ ( self , positions = None ): \"\"\"Create a new inventory using a list of existing positions. Args: positions: A list of Position instances or an existing dict or Inventory instance. \"\"\" if isinstance ( positions , ( dict , Inventory )): dict . __init__ ( self , positions ) else : dict . __init__ ( self ) if positions : assert isinstance ( positions , Iterable ) for position in positions : self . add_position ( position )","title":"__init__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__iter__","text":"Iterate over the positions. Note that there is no guaranteed order. Source code in beancount/core/inventory.py def __iter__ ( self ): \"\"\"Iterate over the positions. Note that there is no guaranteed order.\"\"\" return iter ( self . values ())","title":"__iter__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__lt__","text":"Inequality comparison operator. Source code in beancount/core/inventory.py def __lt__ ( self , other ): \"\"\"Inequality comparison operator.\"\"\" return sorted ( self ) < sorted ( other )","title":"__lt__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__mul__","text":"Scale/multiply the contents of the inventory. Parameters: scalar \u2013 A Decimal. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def __mul__ ( self , scalar ): \"\"\"Scale/multiply the contents of the inventory. Args: scalar: A Decimal. Returns: An instance of Inventory. \"\"\" return Inventory ({ key : pos * scalar for key , pos in self . items ()})","title":"__mul__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__neg__","text":"Return an inventory with the negative of values of this one. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def __neg__ ( self ): \"\"\"Return an inventory with the negative of values of this one. Returns: An instance of Inventory. \"\"\" return Inventory ({ key : - pos for key , pos in self . items ()})","title":"__neg__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__repr__","text":"Render as a human-readable string. Returns: A string, for human consumption. Source code in beancount/core/inventory.py def __str__ ( self ): \"\"\"Render as a human-readable string. Returns: A string, for human consumption. \"\"\" return self . to_string ()","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.__str__","text":"Render as a human-readable string. Returns: A string, for human consumption. Source code in beancount/core/inventory.py def __str__ ( self ): \"\"\"Render as a human-readable string. Returns: A string, for human consumption. \"\"\" return self . to_string ()","title":"__str__()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.add_amount","text":"Add to this inventory using amount and cost. This adds with strict lot matching, that is, no partial matches are done on the arguments to the keys of the inventory. Parameters: units \u2013 An Amount instance to add. cost \u2013 An instance of Cost or None, as a key to the inventory. Returns: A pair of (position, matched) where 'position' is the position that that was modified BEFORE it was modified, and where 'matched' is a MatchResult enum that hints at how the lot was booked to this inventory. Position may be None if there is no corresponding Position object, e.g. the position was deleted. Source code in beancount/core/inventory.py def add_amount ( self , units , cost = None ): \"\"\"Add to this inventory using amount and cost. This adds with strict lot matching, that is, no partial matches are done on the arguments to the keys of the inventory. Args: units: An Amount instance to add. cost: An instance of Cost or None, as a key to the inventory. Returns: A pair of (position, matched) where 'position' is the position that that was modified BEFORE it was modified, and where 'matched' is a MatchResult enum that hints at how the lot was booked to this inventory. Position may be None if there is no corresponding Position object, e.g. the position was deleted. \"\"\" if ASSERTS_TYPES : assert isinstance ( units , Amount ), \"Internal error: {!r} (type: {} )\" . format ( units , type ( units ) . __name__ ) assert cost is None or isinstance ( cost , Cost ), \"Internal error: {!r} (type: {} )\" . format ( cost , type ( cost ) . __name__ ) # Find the position. key = ( units . currency , cost ) pos = self . get ( key , None ) if pos is not None : # Note: In order to augment or reduce, all the fields have to match. # Check if reducing. booking = ( MatchResult . REDUCED if not same_sign ( pos . units . number , units . number ) else MatchResult . AUGMENTED ) # Compute the new number of units. number = pos . units . number + units . number if number == ZERO : # If empty, delete the position. del self [ key ] else : # Otherwise update it. self [ key ] = Position ( Amount ( number , units . currency ), cost ) else : # If not found, create a new one. if units . number == ZERO : booking = MatchResult . IGNORED else : self [ key ] = Position ( units , cost ) booking = MatchResult . CREATED return pos , booking","title":"add_amount()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.add_inventory","text":"Add all the positions of another Inventory instance to this one. Parameters: other \u2013 An instance of Inventory to add to this one. Returns: This inventory, modified. Source code in beancount/core/inventory.py def add_inventory ( self , other ): \"\"\"Add all the positions of another Inventory instance to this one. Args: other: An instance of Inventory to add to this one. Returns: This inventory, modified. \"\"\" if self . is_empty (): # Optimization for empty inventories; if the current one is empty, # adopt all of the other inventory's positions without running # through the full aggregation checks. This should be very cheap. We # can do this because the positions are immutable. self . update ( other ) else : for position in other . get_positions (): self . add_position ( position ) return self","title":"add_inventory()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.add_position","text":"Add using a position (with strict lot matching). Return True if this position was booked against and reduced another. Parameters: position \u2013 The Posting or Position to add to this inventory. Returns: A pair of (position, booking) where 'position' is the position that that was modified, and where 'matched' is a MatchResult enum that hints at how the lot was booked to this inventory. Source code in beancount/core/inventory.py def add_position ( self , position ): \"\"\"Add using a position (with strict lot matching). Return True if this position was booked against and reduced another. Args: position: The Posting or Position to add to this inventory. Returns: A pair of (position, booking) where 'position' is the position that that was modified, and where 'matched' is a MatchResult enum that hints at how the lot was booked to this inventory. \"\"\" if ASSERTS_TYPES : assert hasattr ( position , \"units\" ) and hasattr ( position , \"cost\" ), \"Invalid type for position: {} \" . format ( position ) assert isinstance ( position . cost , ( type ( None ), Cost ) ), \"Invalid type for cost: {} \" . format ( position . cost ) return self . add_amount ( position . units , position . cost )","title":"add_position()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.average","text":"Average all lots of the same currency together. Use the minimum date from each aggregated set of lots. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def average ( self ): \"\"\"Average all lots of the same currency together. Use the minimum date from each aggregated set of lots. Returns: An instance of Inventory. \"\"\" groups = collections . defaultdict ( list ) for position in self : key = ( position . units . currency , position . cost . currency if position . cost else None , ) groups [ key ] . append ( position ) average_inventory = Inventory () for ( currency , cost_currency ), positions in groups . items (): total_units = sum ( position . units . number for position in positions ) # Explicitly skip aggregates when resulting in zero units. if total_units == ZERO : continue units_amount = Amount ( total_units , currency ) if cost_currency : total_cost = sum ( convert . get_cost ( position ) . number for position in positions ) cost_number = ( Decimal ( \"Infinity\" ) if total_units == ZERO else ( total_cost / total_units ) ) min_date = None for pos in positions : pos_date = pos . cost . date if pos . cost else None if pos_date is not None : min_date = pos_date if min_date is None else min ( min_date , pos_date ) cost = Cost ( cost_number , cost_currency , min_date , None ) else : cost = None average_inventory . add_amount ( units_amount , cost ) return average_inventory","title":"average()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.cost_currencies","text":"Return the list of unit currencies held in this inventory. Returns: A set of currency strings. Source code in beancount/core/inventory.py def cost_currencies ( self ): \"\"\"Return the list of unit currencies held in this inventory. Returns: A set of currency strings. \"\"\" return set ( cost . currency for _ , cost in self . keys () if cost is not None )","title":"cost_currencies()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.currencies","text":"Return the list of unit currencies held in this inventory. Returns: A list of currency strings. Source code in beancount/core/inventory.py def currencies ( self ): \"\"\"Return the list of unit currencies held in this inventory. Returns: A list of currency strings. \"\"\" return set ( currency for currency , _ in self . keys ())","title":"currencies()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.currency_pairs","text":"Return the commodities held in this inventory. Returns: A set of currency strings. Source code in beancount/core/inventory.py def currency_pairs ( self ): \"\"\"Return the commodities held in this inventory. Returns: A set of currency strings. \"\"\" return set ( position . currency_pair () for position in self )","title":"currency_pairs()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.from_string","text":"Create an Inventory from a string. This is useful for writing tests. Parameters: string \u2013 A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. Source code in beancount/core/inventory.py @staticmethod def from_string ( string ): \"\"\"Create an Inventory from a string. This is useful for writing tests. Args: string: A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. \"\"\" new_inventory = Inventory () # We need to split the comma-separated positions but ignore commas # occurring within a {...cost...} specification. position_strs = re . split ( r \"([-+]?[0-9,.]+\\s+[A-Z]+\\s*(?:{[^}]*})?)\\s*,?\\s*\" , string )[ 1 :: 2 ] for position_str in position_strs : new_inventory . add_position ( position_from_string ( position_str )) return new_inventory","title":"from_string()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.get_currency_units","text":"Fetch the total amount across all the position in the given currency. This may sum multiple lots in the same currency denomination. Parameters: currency \u2013 A string, the currency to filter the positions with. Returns: An instance of Amount, with the given currency. Source code in beancount/core/inventory.py def get_currency_units ( self , currency ): \"\"\"Fetch the total amount across all the position in the given currency. This may sum multiple lots in the same currency denomination. Args: currency: A string, the currency to filter the positions with. Returns: An instance of Amount, with the given currency. \"\"\" total_units = ZERO for position in self : if position . units . currency == currency : total_units += position . units . number return Amount ( total_units , currency )","title":"get_currency_units()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.get_only_position","text":"Return the first position and assert there are no more. If the inventory is empty, return None. Source code in beancount/core/inventory.py def get_only_position ( self ): \"\"\"Return the first position and assert there are no more. If the inventory is empty, return None. \"\"\" if len ( self ) > 0 : if len ( self ) > 1 : raise AssertionError ( \"Inventory has more than one expected \" \"position: {} \" . format ( self ) ) return next ( iter ( self ))","title":"get_only_position()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.get_positions","text":"Return the positions in this inventory. Returns: A shallow copy of the list of positions. Source code in beancount/core/inventory.py def get_positions ( self ): \"\"\"Return the positions in this inventory. Returns: A shallow copy of the list of positions. \"\"\" return list ( iter ( self ))","title":"get_positions()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.is_empty","text":"Return true if the inventory is empty, that is, has no positions. Returns: A boolean. Source code in beancount/core/inventory.py def is_empty ( self ): \"\"\"Return true if the inventory is empty, that is, has no positions. Returns: A boolean. \"\"\" return len ( self ) == 0","title":"is_empty()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.is_mixed","text":"Return true if the inventory contains a mix of positive and negative lots for at least one instrument. Returns: A boolean. Source code in beancount/core/inventory.py def is_mixed ( self ): \"\"\"Return true if the inventory contains a mix of positive and negative lots for at least one instrument. Returns: A boolean. \"\"\" signs_map = {} for position in self : sign = position . units . number >= 0 prev_sign = signs_map . setdefault ( position . units . currency , sign ) if sign != prev_sign : return True return False","title":"is_mixed()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.is_reduced_by","text":"Return true if the amount could reduce this inventory. Parameters: ramount \u2013 An instance of Amount. Returns: A boolean. Source code in beancount/core/inventory.py def is_reduced_by ( self , ramount ): \"\"\"Return true if the amount could reduce this inventory. Args: ramount: An instance of Amount. Returns: A boolean. \"\"\" if ramount . number == ZERO : return False for position in self : units = position . units if ramount . currency == units . currency and not same_sign ( ramount . number , units . number ): return True return False","title":"is_reduced_by()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.is_small","text":"Return true if all the positions in the inventory are small. Parameters: tolerances \u2013 A Decimal, the small number of units under which a position is considered small, or a dict of currency to such epsilon precision. Returns: A boolean. Source code in beancount/core/inventory.py def is_small ( self , tolerances ): \"\"\"Return true if all the positions in the inventory are small. Args: tolerances: A Decimal, the small number of units under which a position is considered small, or a dict of currency to such epsilon precision. Returns: A boolean. \"\"\" if isinstance ( tolerances , dict ): for position in self : tolerance = tolerances . get ( position . units . currency , ZERO ) if abs ( position . units . number ) > tolerance : return False small = True else : small = not any ( abs ( position . units . number ) > tolerances for position in self ) return small","title":"is_small()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.reduce","text":"Reduce an inventory using one of the conversion functions. See functions in beancount.core.convert. Returns: An instance of Inventory. Source code in beancount/core/inventory.py def reduce ( self , reducer , * args ): \"\"\"Reduce an inventory using one of the conversion functions. See functions in beancount.core.convert. Returns: An instance of Inventory. \"\"\" inventory = Inventory () for position in self : inventory . add_amount ( reducer ( position , * args )) return inventory","title":"reduce()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.segregate_units","text":"Split up the list of positions to the given currencies. Parameters: currencies \u2013 A list of currency strings, the currencies to isolate. Returns: A dict of currency to Inventory instances. Source code in beancount/core/inventory.py def segregate_units ( self , currencies ): \"\"\"Split up the list of positions to the given currencies. Args: currencies: A list of currency strings, the currencies to isolate. Returns: A dict of currency to Inventory instances. \"\"\" per_currency_dict = { currency : Inventory () for currency in currencies } per_currency_dict [ None ] = Inventory () for position in self : currency = position . units . currency key = currency if currency in currencies else None per_currency_dict [ key ] . add_position ( position ) return per_currency_dict","title":"segregate_units()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.split","text":"Split up the list of positions to their corresponding currencies. Returns: A dict of currency to Inventory instances. Source code in beancount/core/inventory.py def split ( self ): \"\"\"Split up the list of positions to their corresponding currencies. Returns: A dict of currency to Inventory instances. \"\"\" per_currency_dict = collections . defaultdict ( Inventory ) for position in self : per_currency_dict [ position . units . currency ] . add_position ( position ) return dict ( per_currency_dict )","title":"split()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.Inventory.to_string","text":"Convert an Inventory instance to a printable string. Parameters: dformat \u2013 An instance of DisplayFormatter. parents \u2013 A boolean, true if we should surround the results by parentheses. Returns: A formatted string of the quantized amount and symbol. Source code in beancount/core/inventory.py def to_string ( self , dformat = DEFAULT_FORMATTER , parens = True ): \"\"\"Convert an Inventory instance to a printable string. Args: dformat: An instance of DisplayFormatter. parents: A boolean, true if we should surround the results by parentheses. Returns: A formatted string of the quantized amount and symbol. \"\"\" fmt = \"( {} )\" if parens else \" {} \" return fmt . format ( \", \" . join ( pos . to_string ( dformat ) for pos in sorted ( self )))","title":"to_string()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.MatchResult","text":"Result of booking a new lot to an existing inventory.","title":"MatchResult"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.check_invariants","text":"Check the invariants of the Inventory. Parameters: inventory \u2013 An instance of Inventory. Returns: True if the invariants are respected. Source code in beancount/core/inventory.py def check_invariants ( inv ): \"\"\"Check the invariants of the Inventory. Args: inventory: An instance of Inventory. Returns: True if the invariants are respected. \"\"\" # Check that all the keys are unique. lots = set (( pos . units . currency , pos . cost ) for pos in inv ) assert len ( lots ) == len ( inv ), \"Invalid inventory: {} \" . format ( inv ) # Check that none of the amounts is zero. for pos in inv : assert pos . units . number != ZERO , \"Invalid position size: {} \" . format ( pos )","title":"check_invariants()"},{"location":"api_reference/beancount.core.html#beancount.core.inventory.from_string","text":"Create an Inventory from a string. This is useful for writing tests. Parameters: string \u2013 A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. Source code in beancount/core/inventory.py @staticmethod def from_string ( string ): \"\"\"Create an Inventory from a string. This is useful for writing tests. Args: string: A comma-separated string of with an optional { } for the cost. Returns: A new instance of Inventory with the given balances. \"\"\" new_inventory = Inventory () # We need to split the comma-separated positions but ignore commas # occurring within a {...cost...} specification. position_strs = re . split ( r \"([-+]?[0-9,.]+\\s+[A-Z]+\\s*(?:{[^}]*})?)\\s*,?\\s*\" , string )[ 1 :: 2 ] for position_str in position_strs : new_inventory . add_position ( position_from_string ( position_str )) return new_inventory","title":"from_string()"},{"location":"api_reference/beancount.core.html#beancount.core.number","text":"The module contains the basic Decimal type import. About Decimal usage: Do not import Decimal from the 'decimal' or 'cdecimal' modules; always import your Decimal class from beancount.core.amount. Prefer to use D() to create new instances of Decimal objects, which handles more syntax, e.g., handles None, and numbers with commas.","title":"number"},{"location":"api_reference/beancount.core.html#beancount.core.number.D","text":"Convert a string into a Decimal object. This is used in parsing amounts from files in the importers. This is the main function you should use to build all numbers the system manipulates (never use floating-point in an accounting system). Commas are stripped and ignored, as they are assumed to be thousands separators (the French comma separator as decimal is not supported). This function just returns the argument if it is already a Decimal object, for convenience. Parameters: strord \u2013 A string or Decimal instance. Returns: A Decimal instance. Source code in beancount/core/number.py def D ( strord = None ): \"\"\"Convert a string into a Decimal object. This is used in parsing amounts from files in the importers. This is the main function you should use to build all numbers the system manipulates (never use floating-point in an accounting system). Commas are stripped and ignored, as they are assumed to be thousands separators (the French comma separator as decimal is not supported). This function just returns the argument if it is already a Decimal object, for convenience. Args: strord: A string or Decimal instance. Returns: A Decimal instance. \"\"\" try : # Note: try a map lookup and optimize performance here. if strord is None or strord == \"\" : return Decimal () elif isinstance ( strord , str ): return Decimal ( _CLEAN_NUMBER_RE . sub ( \"\" , strord )) elif isinstance ( strord , Decimal ): return strord elif isinstance ( strord , ( int , float )): return Decimal ( strord ) else : assert strord is None , \"Invalid value to convert: {} \" . format ( strord ) except Exception as exc : raise ValueError ( \"Impossible to create Decimal instance from {!s} : {} \" . format ( strord , exc ) ) from exc","title":"D()"},{"location":"api_reference/beancount.core.html#beancount.core.number.auto_quantize","text":"Automatically quantize the number at a given threshold. For example, with a threshold of 0.01, this will convert: 20.899999618530273 20.9 20.290000000000000000000000000000 20.29 110.90 110.9 11.0600004196167 11.06 10.539999961853027 10.54 134.3300018310547 134.33 253.920200000000000000000000000000 253.9202 Source code in beancount/core/number.py def auto_quantize ( number : Decimal , threshold : float ) -> Decimal : \"\"\"Automatically quantize the number at a given threshold. For example, with a threshold of 0.01, this will convert: 20.899999618530273 20.9 20.290000000000000000000000000000 20.29 110.90 110.9 11.0600004196167 11.06 10.539999961853027 10.54 134.3300018310547 134.33 253.920200000000000000000000000000 253.9202 \"\"\" exponent = auto_quantized_exponent ( number , threshold ) if exponent != number . as_tuple () . exponent : quant = TEN ** exponent qnumber = number . quantize ( quant ) . normalize () return qnumber else : return number","title":"auto_quantize()"},{"location":"api_reference/beancount.core.html#beancount.core.number.auto_quantized_exponent","text":"Automatically infer the exponent that would be used below a given threshold. Source code in beancount/core/number.py def auto_quantized_exponent ( number : Decimal , threshold : float ) -> int : \"\"\"Automatically infer the exponent that would be used below a given threshold.\"\"\" dtuple = number . normalize () . as_tuple () norm = Decimal ( dtuple . _replace ( sign = 0 , exponent =- len ( dtuple . digits ))) low_threshold = threshold high_threshold = 1.0 - low_threshold while norm != ZERO : if not ( low_threshold <= norm <= high_threshold ): break ntuple = norm . scaleb ( 1 ) . as_tuple () norm = Decimal ( ntuple . _replace ( digits = ntuple . digits [ ntuple . exponent :])) return dtuple . exponent - norm . as_tuple () . exponent","title":"auto_quantized_exponent()"},{"location":"api_reference/beancount.core.html#beancount.core.number.infer_quantum_from_list","text":"Given a list of numbers from floats, infer the common quantization. For a series of numbers provided as floats, e.g., prices from a price source, we'd like to infer what the right quantization that should be used to avoid rounding errors above some threshold. from the numbers. This simple algorithm auto-quantizes all the numbers and quantizes all of them at the maximum precision that would result in rounding under the threshold. Parameters: prices \u2013 A list of float or Decimal prices to infer from. If floats are provided, conversion is done naively. threshold ( float ) \u2013 A fraction, the maximum error to tolerate before stopping the search. Returns: Optional[decimal.Decimal] \u2013 A decimal object to use with decimal.Decimal.quantize(). Source code in beancount/core/number.py def infer_quantum_from_list ( numbers : List [ Decimal ], threshold : float = 0.01 ) -> Optional [ Decimal ]: \"\"\"Given a list of numbers from floats, infer the common quantization. For a series of numbers provided as floats, e.g., prices from a price source, we'd like to infer what the right quantization that should be used to avoid rounding errors above some threshold. from the numbers. This simple algorithm auto-quantizes all the numbers and quantizes all of them at the maximum precision that would result in rounding under the threshold. Args: prices: A list of float or Decimal prices to infer from. If floats are provided, conversion is done naively. threshold: A fraction, the maximum error to tolerate before stopping the search. Returns: A decimal object to use with decimal.Decimal.quantize(). \"\"\" # Auto quantize all the numbers. qnumbers = [ auto_quantize ( num , threshold ) for num in numbers ] exponent = max ( num_fractional_digits ( n ) for n in qnumbers ) return - exponent","title":"infer_quantum_from_list()"},{"location":"api_reference/beancount.core.html#beancount.core.number.num_fractional_digits","text":"Return the number of fractional digits. Source code in beancount/core/number.py def num_fractional_digits ( number : Decimal ) -> int : \"\"\"Return the number of fractional digits.\"\"\" return - number . as_tuple () . exponent","title":"num_fractional_digits()"},{"location":"api_reference/beancount.core.html#beancount.core.number.round_to","text":"Round a number down to a particular increment. Parameters: number \u2013 A Decimal, the number to be rounded. increment \u2013 A Decimal, the size of the increment. Returns: A Decimal, the rounded number. Source code in beancount/core/number.py def round_to ( number , increment ): \"\"\"Round a number *down* to a particular increment. Args: number: A Decimal, the number to be rounded. increment: A Decimal, the size of the increment. Returns: A Decimal, the rounded number. \"\"\" return int (( number / increment )) * increment","title":"round_to()"},{"location":"api_reference/beancount.core.html#beancount.core.number.same_sign","text":"Return true if both numbers have the same sign. Parameters: number1 \u2013 An instance of Decimal. number2 \u2013 An instance of Decimal. Returns: A boolean. Source code in beancount/core/number.py def same_sign ( number1 , number2 ): \"\"\"Return true if both numbers have the same sign. Args: number1: An instance of Decimal. number2: An instance of Decimal. Returns: A boolean. \"\"\" return ( number1 >= 0 ) == ( number2 >= 0 )","title":"same_sign()"},{"location":"api_reference/beancount.core.html#beancount.core.position","text":"A position object, which consists of units Amount and cost Cost. See types below for details.","title":"position"},{"location":"api_reference/beancount.core.html#beancount.core.position.Cost","text":"Cost(number, currency, date, label)","title":"Cost"},{"location":"api_reference/beancount.core.html#beancount.core.position.Cost.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/position.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Cost.__new__","text":"Create new instance of Cost(number, currency, date, label)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Cost.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/position.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.CostSpec","text":"CostSpec(number_per, number_total, currency, date, label, merge)","title":"CostSpec"},{"location":"api_reference/beancount.core.html#beancount.core.position.CostSpec.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/core/position.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.CostSpec.__new__","text":"Create new instance of CostSpec(number_per, number_total, currency, date, label, merge)","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.CostSpec.__repr__","text":"Return a nicely formatted representation string Source code in beancount/core/position.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position","text":"A 'Position' is a pair of units and optional cost. This is used to track inventories. Attributes: Name Type Description units Amount An Amount, the number of units and its currency. cost Cost A Cost that represents the lot, or None.","title":"Position"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__abs__","text":"Return the absolute value of the position. Returns: An instance of Position with the absolute units. Source code in beancount/core/position.py def __abs__ ( self ): \"\"\"Return the absolute value of the position. Returns: An instance of Position with the absolute units. \"\"\" return Position ( amount_abs ( self . units ), self . cost )","title":"__abs__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__copy__","text":"Shallow copy, except for the lot, which can be shared. This is important for performance reasons; a lot of time is spent here during balancing. Returns: A shallow copy of this position. Source code in beancount/core/position.py def __copy__ ( self ): \"\"\"Shallow copy, except for the lot, which can be shared. This is important for performance reasons; a lot of time is spent here during balancing. Returns: A shallow copy of this position. \"\"\" # Note: We use Decimal() for efficiency. return Position ( copy . copy ( self . units ), copy . copy ( self . cost ))","title":"__copy__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__eq__","text":"Equality comparison with another Position. The objects are considered equal if both number and lot are matching, and if the number of units is zero and the other position is None, that is also okay. Parameters: other \u2013 An instance of Position, or None. Returns: A boolean, true if the positions are equal. Source code in beancount/core/position.py def __eq__ ( self , other ): \"\"\"Equality comparison with another Position. The objects are considered equal if both number and lot are matching, and if the number of units is zero and the other position is None, that is also okay. Args: other: An instance of Position, or None. Returns: A boolean, true if the positions are equal. \"\"\" return ( self . units . number == ZERO if other is None else ( self . units == other . units and self . cost == other . cost ) )","title":"__eq__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__hash__","text":"Compute a hash for this position. Returns: A hash of this position object. Source code in beancount/core/position.py def __hash__ ( self ): \"\"\"Compute a hash for this position. Returns: A hash of this position object. \"\"\" return hash (( self . units , self . cost ))","title":"__hash__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__lt__","text":"A less-than comparison operator for positions. Parameters: other \u2013 Another instance of Position. Returns: True if this positions is smaller than the other position. Source code in beancount/core/position.py def __lt__ ( self , other ): \"\"\"A less-than comparison operator for positions. Args: other: Another instance of Position. Returns: True if this positions is smaller than the other position. \"\"\" return self . sortkey () < other . sortkey ()","title":"__lt__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__mul__","text":"Scale/multiply the contents of the position. Parameters: scalar \u2013 A Decimal. Returns: An instance of Inventory. Source code in beancount/core/position.py def __mul__ ( self , scalar ): \"\"\"Scale/multiply the contents of the position. Args: scalar: A Decimal. Returns: An instance of Inventory. \"\"\" return Position ( amount_mul ( self . units , scalar ), self . cost )","title":"__mul__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__neg__","text":"Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. Source code in beancount/core/position.py def get_negative ( self ): \"\"\"Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. \"\"\" # Note: We use Decimal() for efficiency. return Position ( - self . units , self . cost )","title":"__neg__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__new__","text":"Create new instance of _Position(units, cost) Source code in beancount/core/position.py def __new__ ( cls , units , cost = None ): assert isinstance ( units , Amount ), \"Expected an Amount for units; received ' {} '\" . format ( units ) assert cost is None or isinstance ( cost , Position . cost_types ), \"Expected a Cost for cost; received ' {} '\" . format ( cost ) return _Position . __new__ ( cls , units , cost )","title":"__new__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__repr__","text":"Return a string representation of the position. Returns: A string, a printable representation of the position. Source code in beancount/core/position.py def __str__ ( self ): \"\"\"Return a string representation of the position. Returns: A string, a printable representation of the position. \"\"\" return self . to_string ()","title":"__repr__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.__str__","text":"Return a string representation of the position. Returns: A string, a printable representation of the position. Source code in beancount/core/position.py def __str__ ( self ): \"\"\"Return a string representation of the position. Returns: A string, a printable representation of the position. \"\"\" return self . to_string ()","title":"__str__()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.currency_pair","text":"Return the currency pair associated with this position. Returns: A pair of a currency string and a cost currency string or None. Source code in beancount/core/position.py def currency_pair ( self ): \"\"\"Return the currency pair associated with this position. Returns: A pair of a currency string and a cost currency string or None. \"\"\" return ( self . units . currency , self . cost . currency if self . cost else None )","title":"currency_pair()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.from_amounts","text":"Create a position from an amount and a cost. Parameters: amount \u2013 An amount, that represents the number of units and the lot currency. cost_amount \u2013 If not None, represents the cost amount. Returns: A Position instance. Source code in beancount/core/position.py @staticmethod def from_amounts ( units , cost_amount = None ): \"\"\"Create a position from an amount and a cost. Args: amount: An amount, that represents the number of units and the lot currency. cost_amount: If not None, represents the cost amount. Returns: A Position instance. \"\"\" assert cost_amount is None or isinstance ( cost_amount , Amount ), \"Invalid type for cost: {} \" . format ( cost_amount ) cost = ( Cost ( cost_amount . number , cost_amount . currency , None , None ) if cost_amount else None ) return Position ( units , cost )","title":"from_amounts()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.from_string","text":"Create a position from a string specification. This is a miniature parser used for building tests. Parameters: string \u2013 A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. Source code in beancount/core/position.py @staticmethod def from_string ( string ): \"\"\"Create a position from a string specification. This is a miniature parser used for building tests. Args: string: A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. \"\"\" match = re . match ( ( r \"\\s*( {} )\\s+( {} )\" r \"(?:\\s+{{([^}}]*)}})?\" r \"\\s*$\" ) . format ( NUMBER_RE , CURRENCY_RE ), string , ) if not match : raise ValueError ( \"Invalid string for position: ' {} '\" . format ( string )) number = D ( match . group ( 1 )) currency = match . group ( 2 ) # Parse a cost expression. cost_number = None cost_currency = None date = None label = None cost_expression = match . group ( 3 ) if match . group ( 3 ): expressions = [ expr . strip () for expr in re . split ( \"[,/]\" , cost_expression )] for expr in expressions : # Match a compound number. match = re . match ( r \"( {NUMBER_RE} )\\s*(?:#\\s*( {NUMBER_RE} ))?\\s+( {CURRENCY_RE} )$\" . format ( NUMBER_RE = NUMBER_RE , CURRENCY_RE = CURRENCY_RE ), expr , ) if match : per_number , total_number , cost_currency = match . group ( 1 , 2 , 3 ) per_number = D ( per_number ) if per_number else ZERO total_number = D ( total_number ) if total_number else ZERO if total_number : # Calculate the per-unit cost. total = number * per_number + total_number per_number = total / number cost_number = per_number continue # Match a date. match = re . match ( r \"(\\d\\d\\d\\d)[-/](\\d\\d)[-/](\\d\\d)$\" , expr ) if match : date = datetime . date ( * map ( int , match . group ( 1 , 2 , 3 ))) continue # Match a label. match = re . match ( r '\"([^\"]+)*\"$' , expr ) if match : label = match . group ( 1 ) continue # Match a merge-cost marker. match = re . match ( r \"\\*$\" , expr ) if match : raise ValueError ( \"Merge-code not supported in string constructor.\" ) raise ValueError ( \"Invalid cost component: ' {} '\" . format ( expr )) cost = Cost ( cost_number , cost_currency , date , label ) else : cost = None return Position ( Amount ( number , currency ), cost )","title":"from_string()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.get_negative","text":"Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. Source code in beancount/core/position.py def get_negative ( self ): \"\"\"Get a copy of this position but with a negative number. Returns: An instance of Position which represents the inverse of this Position. \"\"\" # Note: We use Decimal() for efficiency. return Position ( - self . units , self . cost )","title":"get_negative()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.is_negative_at_cost","text":"Return true if the position is held at cost and negative. Returns: A boolean. Source code in beancount/core/position.py def is_negative_at_cost ( self ): \"\"\"Return true if the position is held at cost and negative. Returns: A boolean. \"\"\" return self . units . number < ZERO and self . cost is not None","title":"is_negative_at_cost()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.sortkey","text":"Return a key to sort positions by. This key depends on the order of the currency of the lot (we want to order common currencies first) and the number of units. Returns: A tuple, used to sort lists of positions. Source code in beancount/core/position.py def sortkey ( self ): \"\"\"Return a key to sort positions by. This key depends on the order of the currency of the lot (we want to order common currencies first) and the number of units. Returns: A tuple, used to sort lists of positions. \"\"\" currency = self . units . currency order_units = CURRENCY_ORDER . get ( currency , NCURRENCIES + len ( currency )) if self . cost is not None : cost_number = self . cost . number cost_currency = self . cost . currency else : cost_number = ZERO cost_currency = \"\" return ( order_units , cost_number , cost_currency , self . units . number )","title":"sortkey()"},{"location":"api_reference/beancount.core.html#beancount.core.position.Position.to_string","text":"Render the position to a string.See to_string() for details. Source code in beancount/core/position.py def to_string ( self , dformat = DEFAULT_FORMATTER , detail = True ): \"\"\"Render the position to a string.See to_string() for details.\"\"\" return to_string ( self , dformat , detail )","title":"to_string()"},{"location":"api_reference/beancount.core.html#beancount.core.position.cost_to_str","text":"Format an instance of Cost or a CostSpec to a string. Parameters: cost \u2013 An instance of Cost or CostSpec. dformat \u2013 A DisplayFormatter object. detail \u2013 A boolean, true if we should render the non-amount components. Returns: A string, suitable for formatting. Source code in beancount/core/position.py def cost_to_str ( cost , dformat , detail = True ): \"\"\"Format an instance of Cost or a CostSpec to a string. Args: cost: An instance of Cost or CostSpec. dformat: A DisplayFormatter object. detail: A boolean, true if we should render the non-amount components. Returns: A string, suitable for formatting. \"\"\" strlist = [] if isinstance ( cost , Cost ): if isinstance ( cost . number , Decimal ): strlist . append ( Amount ( cost . number , cost . currency ) . to_string ( dformat )) if detail : if cost . date : strlist . append ( cost . date . isoformat ()) if cost . label : strlist . append ( '\" {} \"' . format ( cost . label )) elif isinstance ( cost , CostSpec ): if isinstance ( cost . number_per , Decimal ) or isinstance ( cost . number_total , Decimal ): amountlist = [] if isinstance ( cost . number_per , Decimal ): amountlist . append ( dformat . format ( cost . number_per )) if isinstance ( cost . number_total , Decimal ): amountlist . append ( \"#\" ) amountlist . append ( dformat . format ( cost . number_total )) if isinstance ( cost . currency , str ): amountlist . append ( cost . currency ) strlist . append ( \" \" . join ( amountlist )) if detail : if cost . date : strlist . append ( cost . date . isoformat ()) if cost . label : strlist . append ( '\" {} \"' . format ( cost . label )) if cost . merge : strlist . append ( \"*\" ) return \", \" . join ( strlist )","title":"cost_to_str()"},{"location":"api_reference/beancount.core.html#beancount.core.position.from_amounts","text":"Create a position from an amount and a cost. Parameters: amount \u2013 An amount, that represents the number of units and the lot currency. cost_amount \u2013 If not None, represents the cost amount. Returns: A Position instance. Source code in beancount/core/position.py @staticmethod def from_amounts ( units , cost_amount = None ): \"\"\"Create a position from an amount and a cost. Args: amount: An amount, that represents the number of units and the lot currency. cost_amount: If not None, represents the cost amount. Returns: A Position instance. \"\"\" assert cost_amount is None or isinstance ( cost_amount , Amount ), \"Invalid type for cost: {} \" . format ( cost_amount ) cost = ( Cost ( cost_amount . number , cost_amount . currency , None , None ) if cost_amount else None ) return Position ( units , cost )","title":"from_amounts()"},{"location":"api_reference/beancount.core.html#beancount.core.position.from_string","text":"Create a position from a string specification. This is a miniature parser used for building tests. Parameters: string \u2013 A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. Source code in beancount/core/position.py @staticmethod def from_string ( string ): \"\"\"Create a position from a string specification. This is a miniature parser used for building tests. Args: string: A string of with an optional { } for the cost, similar to the parser syntax. Returns: A new instance of Position. \"\"\" match = re . match ( ( r \"\\s*( {} )\\s+( {} )\" r \"(?:\\s+{{([^}}]*)}})?\" r \"\\s*$\" ) . format ( NUMBER_RE , CURRENCY_RE ), string , ) if not match : raise ValueError ( \"Invalid string for position: ' {} '\" . format ( string )) number = D ( match . group ( 1 )) currency = match . group ( 2 ) # Parse a cost expression. cost_number = None cost_currency = None date = None label = None cost_expression = match . group ( 3 ) if match . group ( 3 ): expressions = [ expr . strip () for expr in re . split ( \"[,/]\" , cost_expression )] for expr in expressions : # Match a compound number. match = re . match ( r \"( {NUMBER_RE} )\\s*(?:#\\s*( {NUMBER_RE} ))?\\s+( {CURRENCY_RE} )$\" . format ( NUMBER_RE = NUMBER_RE , CURRENCY_RE = CURRENCY_RE ), expr , ) if match : per_number , total_number , cost_currency = match . group ( 1 , 2 , 3 ) per_number = D ( per_number ) if per_number else ZERO total_number = D ( total_number ) if total_number else ZERO if total_number : # Calculate the per-unit cost. total = number * per_number + total_number per_number = total / number cost_number = per_number continue # Match a date. match = re . match ( r \"(\\d\\d\\d\\d)[-/](\\d\\d)[-/](\\d\\d)$\" , expr ) if match : date = datetime . date ( * map ( int , match . group ( 1 , 2 , 3 ))) continue # Match a label. match = re . match ( r '\"([^\"]+)*\"$' , expr ) if match : label = match . group ( 1 ) continue # Match a merge-cost marker. match = re . match ( r \"\\*$\" , expr ) if match : raise ValueError ( \"Merge-code not supported in string constructor.\" ) raise ValueError ( \"Invalid cost component: ' {} '\" . format ( expr )) cost = Cost ( cost_number , cost_currency , date , label ) else : cost = None return Position ( Amount ( number , currency ), cost )","title":"from_string()"},{"location":"api_reference/beancount.core.html#beancount.core.position.get_position","text":"Build a Position instance from a Posting instance. Parameters: posting \u2013 An instance of Posting. Returns: An instance of Position. Source code in beancount/core/position.py def get_position ( posting ): \"\"\"Build a Position instance from a Posting instance. Args: posting: An instance of Posting. Returns: An instance of Position. \"\"\" return Position ( posting . units , posting . cost )","title":"get_position()"},{"location":"api_reference/beancount.core.html#beancount.core.position.to_string","text":"Render the Position or Posting instance to a string. Parameters: pos \u2013 An instance of Position or Posting. dformat \u2013 An instance of DisplayFormatter. detail \u2013 A boolean, true if we should only render the lot details beyond the cost (lot-date, label, etc.). If false, we only render the cost, if present. Returns: A string, the rendered position. Source code in beancount/core/position.py def to_string ( pos , dformat = DEFAULT_FORMATTER , detail = True ): \"\"\"Render the Position or Posting instance to a string. Args: pos: An instance of Position or Posting. dformat: An instance of DisplayFormatter. detail: A boolean, true if we should only render the lot details beyond the cost (lot-date, label, etc.). If false, we only render the cost, if present. Returns: A string, the rendered position. \"\"\" pos_str = pos . units . to_string ( dformat ) if pos . cost is not None : pos_str = \" {} {{ {} }}\" . format ( pos_str , cost_to_str ( pos . cost , dformat , detail )) return pos_str","title":"to_string()"},{"location":"api_reference/beancount.core.html#beancount.core.prices","text":"This module has code that can build a database of historical prices at various times, from which unrealized capital gains and market value can be deduced. Prices are deduced from Price entries found in the file, or perhaps created by scripts (for example you could build a script that will fetch live prices online and create entries on-the-fly).","title":"prices"},{"location":"api_reference/beancount.core.html#beancount.core.prices.PriceMap","text":"A price map dictionary. The keys include both the set of forward (base, quote) pairs and their inverse. In order to determine which are the forward pairs, access the 'forward_pairs' attribute Attributes: Name Type Description forward_pairs A list of (base, quote) keys for the forward pairs.","title":"PriceMap"},{"location":"api_reference/beancount.core.html#beancount.core.prices.build_price_map","text":"Build a price map from a list of arbitrary entries. If multiple prices are found for the same (currency, cost-currency) pair at the same date, the latest date is kept and the earlier ones (for that day) are discarded. If inverse price pairs are found, e.g. USD in AUD and AUD in USD, the inverse that has the smallest number of price points is converted into the one that has the most price points. In that way they are reconciled into a single one. Parameters: entries \u2013 A list of directives, hopefully including some Price and/or Returns: A dict of (currency, cost-currency) keys to sorted lists of (date, number) pairs, where 'date' is the date the price occurs at and 'number' a Decimal that represents the price, or rate, between these two currencies/commodities. Each date occurs only once in the sorted list of prices of a particular key. All of the inverses are automatically generated in the price map. Source code in beancount/core/prices.py def build_price_map ( entries ): \"\"\"Build a price map from a list of arbitrary entries. If multiple prices are found for the same (currency, cost-currency) pair at the same date, the latest date is kept and the earlier ones (for that day) are discarded. If inverse price pairs are found, e.g. USD in AUD and AUD in USD, the inverse that has the smallest number of price points is converted into the one that has the most price points. In that way they are reconciled into a single one. Args: entries: A list of directives, hopefully including some Price and/or Transaction entries. Returns: A dict of (currency, cost-currency) keys to sorted lists of (date, number) pairs, where 'date' is the date the price occurs at and 'number' a Decimal that represents the price, or rate, between these two currencies/commodities. Each date occurs only once in the sorted list of prices of a particular key. All of the inverses are automatically generated in the price map. \"\"\" # Fetch a list of all the price entries seen in the ledger. price_entries = [ entry for entry in entries if isinstance ( entry , Price )] # Build a map of exchange rates between these units. # (base-currency, quote-currency) -> List of (date, rate). price_map = collections . defaultdict ( list ) for price in price_entries : base_quote = ( price . currency , price . amount . currency ) price_map [ base_quote ] . append (( price . date , price . amount . number )) # Find pairs of inversed units. inversed_units = [] for base_quote , values in price_map . items (): base , quote = base_quote if ( quote , base ) in price_map : inversed_units . append ( base_quote ) # Find pairs of inversed units, and swallow the conversion with the smaller # number of rates into the other one. for base , quote in inversed_units : bq_prices = price_map [( base , quote )] qb_prices = price_map [( quote , base )] remove = ( base , quote ) if len ( bq_prices ) < len ( qb_prices ) else ( quote , base ) base , quote = remove remove_list = price_map [ remove ] insert_list = price_map [( quote , base )] del price_map [ remove ] inverted_list = [( date , ONE / rate ) for ( date , rate ) in remove_list if rate != ZERO ] insert_list . extend ( inverted_list ) # Unzip and sort each of the entries and eliminate duplicates on the date. sorted_price_map = PriceMap ( { base_quote : list ( misc_utils . sorted_uniquify ( date_rates , lambda x : x [ 0 ], last = True ) ) for ( base_quote , date_rates ) in price_map . items () } ) # Compute and insert all the inverted rates. forward_pairs = list ( sorted_price_map . keys ()) for ( base , quote ), price_list in list ( sorted_price_map . items ()): # Note: You have to filter out zero prices for zero-cost postings, like # gifted options. sorted_price_map [( quote , base )] = [ ( date , ONE / price ) for date , price in price_list if price != ZERO ] sorted_price_map . forward_pairs = forward_pairs return sorted_price_map","title":"build_price_map()"},{"location":"api_reference/beancount.core.html#beancount.core.prices.get_all_prices","text":"Return a sorted list of all (date, number) price pairs. Parameters: price_map \u2013 A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote \u2013 A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A list of (date, Decimal) pairs, sorted by date. Exceptions: KeyError \u2013 If the base/quote could not be found. Source code in beancount/core/prices.py def get_all_prices ( price_map , base_quote ): \"\"\"Return a sorted list of all (date, number) price pairs. Args: price_map: A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote: A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A list of (date, Decimal) pairs, sorted by date. Raises: KeyError: If the base/quote could not be found. \"\"\" base_quote = normalize_base_quote ( base_quote ) return _lookup_price_and_inverse ( price_map , base_quote )","title":"get_all_prices()"},{"location":"api_reference/beancount.core.html#beancount.core.prices.get_last_price_entries","text":"Run through the entries until the given date and return the last Price entry encountered for each (currency, cost-currency) pair. Parameters: entries \u2013 A list of directives. date \u2013 An instance of datetime.date. If None, the very latest price is returned. Returns: A list of price entries. Source code in beancount/core/prices.py def get_last_price_entries ( entries , date ): \"\"\"Run through the entries until the given date and return the last Price entry encountered for each (currency, cost-currency) pair. Args: entries: A list of directives. date: An instance of datetime.date. If None, the very latest price is returned. Returns: A list of price entries. \"\"\" price_entry_map = {} for entry in entries : if date is not None and entry . date >= date : break if isinstance ( entry , Price ): base_quote = ( entry . currency , entry . amount . currency ) price_entry_map [ base_quote ] = entry return sorted ( price_entry_map . values (), key = data . entry_sortkey )","title":"get_last_price_entries()"},{"location":"api_reference/beancount.core.html#beancount.core.prices.get_latest_price","text":"Return the latest price/rate from a price map for the given base/quote pair. This is often used to just get the 'current' price if you're looking at the entire set of entries. Parameters: price_map \u2013 A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. Returns: A pair of (date, number), where 'date' is a datetime.date instance and 'number' is a Decimal of the price, or rate, at that date. The date is the latest date which we have an available price for in the price map. Source code in beancount/core/prices.py def get_latest_price ( price_map , base_quote ): \"\"\"Return the latest price/rate from a price map for the given base/quote pair. This is often used to just get the 'current' price if you're looking at the entire set of entries. Args: price_map: A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. Returns: A pair of (date, number), where 'date' is a datetime.date instance and 'number' is a Decimal of the price, or rate, at that date. The date is the latest date which we have an available price for in the price map. \"\"\" base_quote = normalize_base_quote ( base_quote ) # Handle the degenerate case of a currency priced into its own. base , quote = base_quote if quote is None or base == quote : return ( None , ONE ) # Look up the list and return the latest element. The lists are assumed to # be sorted. try : price_list = _lookup_price_and_inverse ( price_map , base_quote ) except KeyError : price_list = None if price_list : return price_list [ - 1 ] else : return None , None","title":"get_latest_price()"},{"location":"api_reference/beancount.core.html#beancount.core.prices.get_price","text":"Return the price as of the given date. If the date is unspecified, return the latest price. Parameters: price_map \u2013 A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote \u2013 A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. date \u2013 A datetime.date instance, the date at which we want the conversion rate. Returns: A pair of (datetime.date, Decimal) instance. If no price information could be found, return (None, None). Source code in beancount/core/prices.py def get_price ( price_map , base_quote , date = None ): \"\"\"Return the price as of the given date. If the date is unspecified, return the latest price. Args: price_map: A price map, which is a dict of (base, quote) -> list of (date, number) tuples, as created by build_price_map. base_quote: A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. date: A datetime.date instance, the date at which we want the conversion rate. Returns: A pair of (datetime.date, Decimal) instance. If no price information could be found, return (None, None). \"\"\" if date is None : return get_latest_price ( price_map , base_quote ) base_quote = normalize_base_quote ( base_quote ) # Handle the degenerate case of a currency priced into its own. base , quote = base_quote if quote is None or base == quote : return ( None , ONE ) try : price_list = _lookup_price_and_inverse ( price_map , base_quote ) index = bisect_key . bisect_right_with_key ( price_list , date , key = lambda x : x [ 0 ]) if index == 0 : return None , None else : return price_list [ index - 1 ] except KeyError : return None , None","title":"get_price()"},{"location":"api_reference/beancount.core.html#beancount.core.prices.normalize_base_quote","text":"Convert a slash-separated string to a pair of strings. Parameters: base_quote \u2013 A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A pair of strings. Source code in beancount/core/prices.py def normalize_base_quote ( base_quote ): \"\"\"Convert a slash-separated string to a pair of strings. Args: base_quote: A pair of strings, the base currency to lookup, and the quote currency to lookup, which expresses which units the base currency is denominated in. This may also just be a string, with a '/' separator. Returns: A pair of strings. \"\"\" if isinstance ( base_quote , str ): base_quote_norm = tuple ( base_quote . split ( \"/\" )) assert len ( base_quote_norm ) == 2 , base_quote base_quote = base_quote_norm assert isinstance ( base_quote , tuple ), base_quote return base_quote","title":"normalize_base_quote()"},{"location":"api_reference/beancount.core.html#beancount.core.prices.project","text":"Project all prices with a quote currency to another quote currency. Say you have a price for HOOL in USD and you'd like to convert HOOL to CAD. If there aren't any (HOOL, CAD) price pairs in the database it will remain unconverted. Projecting from USD to CAD will compute combined rates and insert corresponding prices over all base currencies (like HOOL). In this example, each of the (HOOL, USD) prices would see an inserted (HOOL, CAD) price inserted at the same date. It is common to make these projections when reducing inventories in a ledger that states multiple operating currency pairs, when for example, one wants to compute total value of a set of accounts in one of those currencies. Please note that: Even if the target pair has existing entries, projection will still be applied. For example, is there exist some (HOOL, CAD) prices, the projection in the example above will still insert some new price points to it. However, projected prices colliding existing ones at the same date will not override them. Projection will fail to insert a new price if the conversion between to and from currencies has no existing prices (e.g. before its first price entry). Perhaps most importantly, we only insert price points at dates where the base commodity has a price point. In other words, if we have prices for dates A and C and the rate changes between these dates at date B, we don't synthesize a new price at date B. A more accurate method to get projected prices that takes into account varying rates is to do multiple lookups. We'll eventually add a method to query it via a specified list of intermediate pairs. {c1bd24f8d4b7} Parameters: orig_price_map ( PriceMap ) \u2013 An existing price map. from_currency ( str ) \u2013 The quote currency with existing project points (e.g., USD). to_currency ( str ) \u2013 The quote currency to insert price points for (e.g., CAD). base_currencies ( Optional[Set[str]] ) \u2013 An optional set of commodities to restrict the projections to (e.g., {HOOL}). Returns: PriceMap \u2013 A new price map, with the extra projected prices. The original price map is kept intact. Source code in beancount/core/prices.py def project ( orig_price_map : PriceMap , from_currency : Currency , to_currency : Currency , base_currencies : Optional [ Set [ Currency ]] = None , ) -> PriceMap : \"\"\"Project all prices with a quote currency to another quote currency. Say you have a price for HOOL in USD and you'd like to convert HOOL to CAD. If there aren't any (HOOL, CAD) price pairs in the database it will remain unconverted. Projecting from USD to CAD will compute combined rates and insert corresponding prices over all base currencies (like HOOL). In this example, each of the (HOOL, USD) prices would see an inserted (HOOL, CAD) price inserted at the same date. It is common to make these projections when reducing inventories in a ledger that states multiple operating currency pairs, when for example, one wants to compute total value of a set of accounts in one of those currencies. Please note that: - Even if the target pair has existing entries, projection will still be applied. For example, is there exist some (HOOL, CAD) prices, the projection in the example above will still insert some new price points to it. - However, projected prices colliding existing ones at the same date will not override them. - Projection will fail to insert a new price if the conversion between to and from currencies has no existing prices (e.g. before its first price entry). - Perhaps most importantly, we only insert price points at dates where the base commodity has a price point. In other words, if we have prices for dates A and C and the rate changes between these dates at date B, we don't synthesize a new price at date B. A more accurate method to get projected prices that takes into account varying rates is to do multiple lookups. We'll eventually add a method to query it via a specified list of intermediate pairs. {c1bd24f8d4b7} Args: orig_price_map: An existing price map. from_currency: The quote currency with existing project points (e.g., USD). to_currency: The quote currency to insert price points for (e.g., CAD). base_currencies: An optional set of commodities to restrict the projections to (e.g., {HOOL}). Returns: A new price map, with the extra projected prices. The original price map is kept intact. \"\"\" # If nothing is requested, return the original map. if from_currency == to_currency : return orig_price_map # Avoid mutating the input map. price_map = { key : list ( value ) for key , value in orig_price_map . items ()} # Process the entire database (it's not indexed by quote currency). currency_pair = ( from_currency , to_currency ) for base_quote , prices in list ( price_map . items ()): # Filter just the currencies to convert. base , quote = base_quote if quote != from_currency : continue # Skip currencies not requested if a constraint has been provided. # {4bb702d82c8a} if base_currencies and base not in base_currencies : continue # Create a mapping of existing prices so we can avoid date collisions. existing_prices = ( { date for date , _ in price_map [( base , to_currency )]} if ( base , to_currency ) in price_map else set () ) # Project over each of the prices. new_projected = [] for date , price in prices : rate_date , rate = get_price ( price_map , currency_pair , date ) if rate is None : # There is no conversion rate at this time; skip projection. # {b2b23353275d}. continue if rate_date in existing_prices : # Skip collisions in date. {97a5703ac517} continue # Append the new rate. new_price = price * rate new_projected . append (( date , new_price )) # Make sure the resulting lists are sorted. if new_projected : projected = price_map . setdefault (( base , to_currency ), []) projected . extend ( new_projected ) projected . sort () inverted = price_map . setdefault (( to_currency , base ), []) inverted . extend ( ( date , ZERO if rate == ZERO else ONE / rate ) for date , rate in new_projected ) inverted . sort () return price_map","title":"project()"},{"location":"api_reference/beancount.core.html#beancount.core.realization","text":"Realization of specific lists of account postings into reports. This code converts a list of entries into a tree of RealAccount nodes (which stands for \"realized accounts\"). The RealAccount objects contain lists of Posting instances instead of Transactions, or other entry types that are attached to an account, such as a Balance or Note entry. The interface of RealAccount corresponds to that of a regular Python dict, where the keys are the names of the individual components of an account's name, and the values are always other RealAccount instances. If you want to get an account by long account name, there are helper functions in this module for this purpose (see realization.get(), for instance). RealAccount instances also contain the final balance of that account, resulting from its list of postings. You should not build RealAccount trees yourself; instead, you should filter the list of desired directives to display and call the realize() function with them.","title":"realization"},{"location":"api_reference/beancount.core.html#beancount.core.realization.RealAccount","text":"A realized account, inserted in a tree, that contains the list of realized entries. Attributes: Name Type Description account A string, the full name of the corresponding account. postings A list of postings associated with this accounting (does not include the postings of children accounts). balance The final balance of the list of postings associated with this account.","title":"RealAccount"},{"location":"api_reference/beancount.core.html#beancount.core.realization.RealAccount.__eq__","text":"Equality predicate. All attributes are compared. Parameters: other \u2013 Another instance of RealAccount. Returns: A boolean, True if the two real accounts are equal. Source code in beancount/core/realization.py def __eq__ ( self , other ): \"\"\"Equality predicate. All attributes are compared. Args: other: Another instance of RealAccount. Returns: A boolean, True if the two real accounts are equal. \"\"\" return ( dict . __eq__ ( self , other ) and self . account == other . account and self . balance == other . balance and self . txn_postings == other . txn_postings )","title":"__eq__()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.RealAccount.__init__","text":"Create a RealAccount instance. Parameters: account_name \u2013 a string, the name of the account. Maybe not be None. Source code in beancount/core/realization.py def __init__ ( self , account_name , * args , ** kwargs ): \"\"\"Create a RealAccount instance. Args: account_name: a string, the name of the account. Maybe not be None. \"\"\" super () . __init__ ( * args , ** kwargs ) assert isinstance ( account_name , str ) self . account = account_name self . txn_postings = [] self . balance = inventory . Inventory ()","title":"__init__()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.RealAccount.__ne__","text":"Not-equality predicate. See eq . Parameters: other \u2013 Another instance of RealAccount. Returns: A boolean, True if the two real accounts are not equal. Source code in beancount/core/realization.py def __ne__ ( self , other ): \"\"\"Not-equality predicate. See __eq__. Args: other: Another instance of RealAccount. Returns: A boolean, True if the two real accounts are not equal. \"\"\" return not self . __eq__ ( other )","title":"__ne__()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.RealAccount.__setitem__","text":"Prevent the setting of non-string or non-empty keys on this dict. Parameters: key \u2013 The dictionary key. Must be a string. value \u2013 The value, must be a RealAccount instance. Exceptions: KeyError \u2013 If the key is not a string, or is invalid. ValueError \u2013 If the value is not a RealAccount instance. Source code in beancount/core/realization.py def __setitem__ ( self , key , value ): \"\"\"Prevent the setting of non-string or non-empty keys on this dict. Args: key: The dictionary key. Must be a string. value: The value, must be a RealAccount instance. Raises: KeyError: If the key is not a string, or is invalid. ValueError: If the value is not a RealAccount instance. \"\"\" if not isinstance ( key , str ) or not key : raise KeyError ( \"Invalid RealAccount key: ' {} '\" . format ( key )) if not isinstance ( value , RealAccount ): raise ValueError ( \"Invalid RealAccount value: ' {} '\" . format ( value )) if not value . account . endswith ( key ): raise ValueError ( \"RealAccount name ' {} ' inconsistent with key: ' {} '\" . format ( value . account , key ) ) return super () . __setitem__ ( key , value )","title":"__setitem__()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.RealAccount.copy","text":"Override dict.copy() to clone a RealAccount. This is only necessary to correctly implement the copy method. Otherwise, calling .copy() on a RealAccount instance invokes the base class' method, which return just a dict. Returns: A cloned instance of RealAccount, with all members shallow-copied. Source code in beancount/core/realization.py def copy ( self ): \"\"\"Override dict.copy() to clone a RealAccount. This is only necessary to correctly implement the copy method. Otherwise, calling .copy() on a RealAccount instance invokes the base class' method, which return just a dict. Returns: A cloned instance of RealAccount, with all members shallow-copied. \"\"\" return copy . copy ( self )","title":"copy()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.compute_balance","text":"Compute the total balance of this account and all its subaccounts. Parameters: real_account \u2013 A RealAccount instance. leaf_only \u2013 A boolean flag, true if we should yield only leaves. Returns: An Inventory. Source code in beancount/core/realization.py def compute_balance ( real_account , leaf_only = False ): \"\"\"Compute the total balance of this account and all its subaccounts. Args: real_account: A RealAccount instance. leaf_only: A boolean flag, true if we should yield only leaves. Returns: An Inventory. \"\"\" return functools . reduce ( operator . add , [ ra . balance for ra in iter_children ( real_account , leaf_only )] )","title":"compute_balance()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.compute_postings_balance","text":"Compute the balance of a list of Postings's or TxnPosting's positions. Parameters: postings \u2013 A list of Posting instances and other directives (which are skipped). Returns: An Inventory. Source code in beancount/core/realization.py def compute_postings_balance ( txn_postings ): \"\"\"Compute the balance of a list of Postings's or TxnPosting's positions. Args: postings: A list of Posting instances and other directives (which are skipped). Returns: An Inventory. \"\"\" final_balance = inventory . Inventory () for txn_posting in txn_postings : if isinstance ( txn_posting , Posting ): final_balance . add_position ( txn_posting ) elif isinstance ( txn_posting , TxnPosting ): final_balance . add_position ( txn_posting . posting ) return final_balance","title":"compute_postings_balance()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.contains","text":"True if the given account node contains the subaccount name. Parameters: account_name \u2013 A string, the name of a direct or indirect subaccount of this node. Returns: A boolean, true the name is a child of this node. Source code in beancount/core/realization.py def contains ( real_account , account_name ): \"\"\"True if the given account node contains the subaccount name. Args: account_name: A string, the name of a direct or indirect subaccount of this node. Returns: A boolean, true the name is a child of this node. \"\"\" return get ( real_account , account_name ) is not None","title":"contains()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.dump","text":"Convert a RealAccount node to a line of lines. Note: this is not meant to be used to produce text reports; the reporting code should produce an intermediate object that contains the structure of it, which can then be rendered to ASCII, HTML or CSV formats. This is intended as a convenient little function for dumping trees of data for debugging purposes. Parameters: root_account \u2013 A RealAccount instance. Returns: A list of tuples of (first_line, continuation_line, real_account) where first_line \u2013 A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. real_account: The RealAccount instance which corresponds to this line. Source code in beancount/core/realization.py def dump ( root_account ): \"\"\"Convert a RealAccount node to a line of lines. Note: this is not meant to be used to produce text reports; the reporting code should produce an intermediate object that contains the structure of it, which can then be rendered to ASCII, HTML or CSV formats. This is intended as a convenient little function for dumping trees of data for debugging purposes. Args: root_account: A RealAccount instance. Returns: A list of tuples of (first_line, continuation_line, real_account) where first_line: A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. real_account: The RealAccount instance which corresponds to this line. \"\"\" # Compute all the lines ahead of time in order to calculate the width. lines = [] # Start with the root node. We push the constant prefix before this node, # the account name, and the RealAccount instance. We will maintain a stack # of children nodes to render. stack = [( \"\" , root_account . account , root_account , True )] while stack : prefix , name , real_account , is_last = stack . pop ( - 1 ) if real_account is root_account : # For the root node, we don't want to render any prefix. first = cont = \"\" else : # Compute the string that precedes the name directly and the one below # that for the continuation lines. # | # @@@ Bank1 <---------------- # @@@ | # | |-- Checking if is_last : first = prefix + PREFIX_LEAF_1 cont = prefix + PREFIX_LEAF_C else : first = prefix + PREFIX_CHILD_1 cont = prefix + PREFIX_CHILD_C # Compute the name to render for continuation lines. # | # |-- Bank1 # | @@@ <---------------- # | |-- Checking if len ( real_account ) > 0 : cont_name = PREFIX_CHILD_C else : cont_name = PREFIX_LEAF_C # Add a line for this account. if not ( real_account is root_account and not name ): lines . append (( first + name , cont + cont_name , real_account )) # Push the children onto the stack, being careful with ordering and # marking the last node as such. child_items = sorted ( real_account . items (), reverse = True ) if child_items : child_iter = iter ( child_items ) child_name , child_account = next ( child_iter ) stack . append (( cont , child_name , child_account , True )) for child_name , child_account in child_iter : stack . append (( cont , child_name , child_account , False )) if not lines : return lines # Compute the maximum width of the lines and convert all of them to the same # maximal width. This makes it easy on the client. max_width = max ( len ( first_line ) for first_line , _ , __ in lines ) line_format = \"{{: {width} }}\" . format ( width = max_width ) return [ ( line_format . format ( first_line ), line_format . format ( cont_line ), real_node ) for ( first_line , cont_line , real_node ) in lines ]","title":"dump()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.dump_balances","text":"Dump a realization tree with balances. Parameters: real_root \u2013 An instance of RealAccount. dformat \u2013 An instance of DisplayFormatter to format the numbers with. at_cost \u2013 A boolean, if true, render the values at cost. fullnames \u2013 A boolean, if true, don't render a tree of accounts and render the full account names. file \u2013 A file object to dump the output to. If not specified, we return the output as a string. Returns: A string, the rendered tree, or nothing, if 'file' was provided. Source code in beancount/core/realization.py def dump_balances ( real_root , dformat , at_cost = False , fullnames = False , file = None ): \"\"\"Dump a realization tree with balances. Args: real_root: An instance of RealAccount. dformat: An instance of DisplayFormatter to format the numbers with. at_cost: A boolean, if true, render the values at cost. fullnames: A boolean, if true, don't render a tree of accounts and render the full account names. file: A file object to dump the output to. If not specified, we return the output as a string. Returns: A string, the rendered tree, or nothing, if 'file' was provided. \"\"\" if fullnames : # Compute the maximum account name length; maxlen = max ( len ( real_child . account ) for real_child in iter_children ( real_root , leaf_only = True ) ) line_format = \"{{: {width} }} {{}} \\n \" . format ( width = maxlen ) else : line_format = \" {} {} \\n \" output = file or io . StringIO () for first_line , cont_line , real_account in dump ( real_root ): if not real_account . balance . is_empty (): if at_cost : rinv = real_account . balance . reduce ( convert . get_cost ) else : rinv = real_account . balance . reduce ( convert . get_units ) amounts = [ position . units for position in rinv . get_positions ()] positions = [ amount_ . to_string ( dformat ) for amount_ in sorted ( amounts , key = amount . sortkey ) ] else : positions = [ \"\" ] if fullnames : for position in positions : if not position and len ( real_account ) > 0 : continue # Skip parent accounts with no position to render. output . write ( line_format . format ( real_account . account , position )) else : line = first_line for position in positions : output . write ( line_format . format ( line , position )) line = cont_line if file is None : return output . getvalue ()","title":"dump_balances()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.filter","text":"Filter a RealAccount tree of nodes by the predicate. This function visits the tree and applies the predicate on each node. It returns a partial clone of RealAccount whereby on each node - either the predicate is true, or - for at least one child of the node the predicate is true. All the leaves have the predicate be true. Parameters: real_account \u2013 An instance of RealAccount. predicate \u2013 A callable/function which accepts a RealAccount and returns a boolean. If the function returns True, the node is kept. Returns: A shallow clone of RealAccount is always returned. Source code in beancount/core/realization.py def filter ( real_account , predicate ): \"\"\"Filter a RealAccount tree of nodes by the predicate. This function visits the tree and applies the predicate on each node. It returns a partial clone of RealAccount whereby on each node - either the predicate is true, or - for at least one child of the node the predicate is true. All the leaves have the predicate be true. Args: real_account: An instance of RealAccount. predicate: A callable/function which accepts a RealAccount and returns a boolean. If the function returns True, the node is kept. Returns: A shallow clone of RealAccount is always returned. \"\"\" assert isinstance ( real_account , RealAccount ) real_copy = RealAccount ( real_account . account ) real_copy . balance = real_account . balance real_copy . txn_postings = real_account . txn_postings for child_name , real_child in real_account . items (): real_child_copy = filter ( real_child , predicate ) if real_child_copy is not None : real_copy [ child_name ] = real_child_copy if len ( real_copy ) > 0 or predicate ( real_account ): return real_copy","title":"filter()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.find_last_active_posting","text":"Look at the end of the list of postings, and find the last posting or entry that is not an automatically added directive. Note that if the account is closed, the last posting is assumed to be a Close directive (this is the case if the input is valid and checks without errors. Parameters: txn_postings \u2013 a list of postings or entries. Returns: An entry, or None, if the input list was empty. Source code in beancount/core/realization.py def find_last_active_posting ( txn_postings ): \"\"\"Look at the end of the list of postings, and find the last posting or entry that is not an automatically added directive. Note that if the account is closed, the last posting is assumed to be a Close directive (this is the case if the input is valid and checks without errors. Args: txn_postings: a list of postings or entries. Returns: An entry, or None, if the input list was empty. \"\"\" for txn_posting in reversed ( txn_postings ): assert not isinstance ( txn_posting , Posting ) if not isinstance ( txn_posting , ( TxnPosting , Open , Close , Pad , Balance , Note )): continue return txn_posting","title":"find_last_active_posting()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.get","text":"Fetch the subaccount name from the real_account node. Parameters: real_account \u2013 An instance of RealAccount, the parent node to look for children of. account_name \u2013 A string, the name of a possibly indirect child leaf found down the tree of 'real_account' nodes. default \u2013 The default value that should be returned if the child subaccount is not found. Returns: A RealAccount instance for the child, or the default value, if the child is not found. Source code in beancount/core/realization.py def get ( real_account , account_name , default = None ): \"\"\"Fetch the subaccount name from the real_account node. Args: real_account: An instance of RealAccount, the parent node to look for children of. account_name: A string, the name of a possibly indirect child leaf found down the tree of 'real_account' nodes. default: The default value that should be returned if the child subaccount is not found. Returns: A RealAccount instance for the child, or the default value, if the child is not found. \"\"\" if not isinstance ( account_name , str ): raise ValueError components = account . split ( account_name ) for component in components : real_child = real_account . get ( component , default ) if real_child is default : return default real_account = real_child return real_account","title":"get()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.get_or_create","text":"Fetch the subaccount name from the real_account node. Parameters: real_account \u2013 An instance of RealAccount, the parent node to look for children of, or create under. account_name \u2013 A string, the name of the direct or indirect child leaf to get or create. Returns: A RealAccount instance for the child, or the default value, if the child is not found. Source code in beancount/core/realization.py def get_or_create ( real_account , account_name ): \"\"\"Fetch the subaccount name from the real_account node. Args: real_account: An instance of RealAccount, the parent node to look for children of, or create under. account_name: A string, the name of the direct or indirect child leaf to get or create. Returns: A RealAccount instance for the child, or the default value, if the child is not found. \"\"\" if not isinstance ( account_name , str ): raise ValueError components = account . split ( account_name ) path = [] for component in components : path . append ( component ) real_child = real_account . get ( component , None ) if real_child is None : real_child = RealAccount ( account . join ( * path )) real_account [ component ] = real_child real_account = real_child return real_account","title":"get_or_create()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.get_postings","text":"Return a sorted list a RealAccount's postings and children. Parameters: real_account \u2013 An instance of RealAccount. Returns: A list of Posting or directories. Source code in beancount/core/realization.py def get_postings ( real_account ): \"\"\"Return a sorted list a RealAccount's postings and children. Args: real_account: An instance of RealAccount. Returns: A list of Posting or directories. \"\"\" # We accumulate all the postings at once here instead of incrementally # because we need to return them sorted. accumulator = [] for real_child in iter_children ( real_account ): accumulator . extend ( real_child . txn_postings ) accumulator . sort ( key = data . posting_sortkey ) return accumulator","title":"get_postings()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.index_key","text":"Find the index of the first element in 'sequence' which is equal to 'value'. If 'key' is specified, the value compared to the value returned by this function. If the value is not found, return None. Parameters: sequence \u2013 The sequence to search. value \u2013 The value to search for. key \u2013 A predicate to call to obtain the value to compare against. cmp \u2013 A comparison predicate. Returns: The index of the first element found, or None, if the element was not found. Source code in beancount/core/realization.py def index_key ( sequence , value , key , cmp ): \"\"\"Find the index of the first element in 'sequence' which is equal to 'value'. If 'key' is specified, the value compared to the value returned by this function. If the value is not found, return None. Args: sequence: The sequence to search. value: The value to search for. key: A predicate to call to obtain the value to compare against. cmp: A comparison predicate. Returns: The index of the first element found, or None, if the element was not found. \"\"\" for index , element in enumerate ( sequence ): if cmp ( key ( element ), value ): return index return","title":"index_key()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.iter_children","text":"Iterate this account node and all its children, depth-first. Parameters: real_account \u2013 An instance of RealAccount. leaf_only \u2013 A boolean flag, true if we should yield only leaves. Yields: Instances of RealAccount, beginning with this account. The order is undetermined. Source code in beancount/core/realization.py def iter_children ( real_account , leaf_only = False ): \"\"\"Iterate this account node and all its children, depth-first. Args: real_account: An instance of RealAccount. leaf_only: A boolean flag, true if we should yield only leaves. Yields: Instances of RealAccount, beginning with this account. The order is undetermined. \"\"\" if leaf_only : if len ( real_account ) == 0 : yield real_account else : for key , real_child in sorted ( real_account . items ()): for real_subchild in iter_children ( real_child , leaf_only ): yield real_subchild else : yield real_account for key , real_child in sorted ( real_account . items ()): for real_subchild in iter_children ( real_child ): yield real_subchild","title":"iter_children()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.iterate_with_balance","text":"Iterate over the entries, accumulating the running balance. For each entry, this yields tuples of the form: (entry, postings, change, balance) entry: This is the directive for this line. If the list contained Posting instance, this yields the corresponding Transaction object. postings: A list of postings on this entry that affect the balance. Only the postings encountered in the input list are included; only those affect the balance. If 'entry' is not a Transaction directive, this should always be an empty list. We preserve the original ordering of the postings as they appear in the input list. change: An Inventory object that reflects the total change due to the postings from this entry that appear in the list. For example, if a Transaction has three postings and two are in the input list, the sum of the two postings will be in the 'change' Inventory object. However, the position for the transactions' third posting--the one not included in the input list--will not be in this inventory. balance: An Inventory object that reflects the balance after adding the 'change' inventory due to this entry's transaction. The 'balance' yielded is never None, even for entries that do not affect the balance, that is, with an empty 'change' inventory. It's up to the caller, the one rendering the entry, to decide whether to render this entry's change for a particular entry type. Note that if the input list of postings-or-entries is not in sorted date order, two postings for the same entry appearing twice with a different date in between will cause the entry appear twice. This is correct behavior, and it is expected that in practice this should never happen anyway, because the list of postings or entries should always be sorted. This method attempts to detect this and raises an assertion if this is seen. Parameters: txn_postings \u2013 A list of postings or directive instances. Postings affect the balance; other entries do not. Yields: Tuples of (entry, postings, change, balance) as described above. Source code in beancount/core/realization.py def iterate_with_balance ( txn_postings ): \"\"\"Iterate over the entries, accumulating the running balance. For each entry, this yields tuples of the form: (entry, postings, change, balance) entry: This is the directive for this line. If the list contained Posting instance, this yields the corresponding Transaction object. postings: A list of postings on this entry that affect the balance. Only the postings encountered in the input list are included; only those affect the balance. If 'entry' is not a Transaction directive, this should always be an empty list. We preserve the original ordering of the postings as they appear in the input list. change: An Inventory object that reflects the total change due to the postings from this entry that appear in the list. For example, if a Transaction has three postings and two are in the input list, the sum of the two postings will be in the 'change' Inventory object. However, the position for the transactions' third posting--the one not included in the input list--will not be in this inventory. balance: An Inventory object that reflects the balance *after* adding the 'change' inventory due to this entry's transaction. The 'balance' yielded is never None, even for entries that do not affect the balance, that is, with an empty 'change' inventory. It's up to the caller, the one rendering the entry, to decide whether to render this entry's change for a particular entry type. Note that if the input list of postings-or-entries is not in sorted date order, two postings for the same entry appearing twice with a different date in between will cause the entry appear twice. This is correct behavior, and it is expected that in practice this should never happen anyway, because the list of postings or entries should always be sorted. This method attempts to detect this and raises an assertion if this is seen. Args: txn_postings: A list of postings or directive instances. Postings affect the balance; other entries do not. Yields: Tuples of (entry, postings, change, balance) as described above. \"\"\" # The running balance. running_balance = inventory . Inventory () # Previous date. prev_date = None # A list of entries at the current date. date_entries = [] first = lambda pair : pair [ 0 ] for txn_posting in txn_postings : # Get the posting if we are dealing with one. assert not isinstance ( txn_posting , Posting ) if isinstance ( txn_posting , TxnPosting ): posting = txn_posting . posting entry = txn_posting . txn else : posting = None entry = txn_posting if entry . date != prev_date : assert ( prev_date is None or entry . date > prev_date ), \"Invalid date order for postings: {} > {} \" . format ( prev_date , entry . date ) prev_date = entry . date # Flush the dated entries. for date_entry , date_postings in date_entries : change = inventory . Inventory () if date_postings : # Compute the change due to this transaction and update the # total balance at the same time. for date_posting in date_postings : change . add_position ( date_posting ) running_balance . add_position ( date_posting ) yield date_entry , date_postings , change , running_balance date_entries . clear () assert not date_entries if posting is not None : # De-dup multiple postings on the same transaction entry by # grouping their positions together. index = index_key ( date_entries , entry , first , operator . is_ ) if index is None : date_entries . append (( entry , [ posting ])) else : # We are indeed de-duping! postings = date_entries [ index ][ 1 ] postings . append ( posting ) else : # This is a regular entry; nothing to add/remove. date_entries . append (( entry , [])) # Flush the final dated entries if any, same as above. for date_entry , date_postings in date_entries : change = inventory . Inventory () if date_postings : for date_posting in date_postings : change . add_position ( date_posting ) running_balance . add_position ( date_posting ) yield date_entry , date_postings , change , running_balance date_entries . clear ()","title":"iterate_with_balance()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.postings_by_account","text":"Create lists of postings and balances by account. This routine aggregates postings and entries grouping them by account name. The resulting lists contain postings in-lieu of Transaction directives, but the other directives are stored as entries. This yields a list of postings or other entries by account. All references to accounts are taken into account. Parameters: entries \u2013 A list of directives. Returns: A mapping of account name to list of TxnPosting instances or non-Transaction directives, sorted in the same order as the entries. Source code in beancount/core/realization.py def postings_by_account ( entries ): \"\"\"Create lists of postings and balances by account. This routine aggregates postings and entries grouping them by account name. The resulting lists contain postings in-lieu of Transaction directives, but the other directives are stored as entries. This yields a list of postings or other entries by account. All references to accounts are taken into account. Args: entries: A list of directives. Returns: A mapping of account name to list of TxnPosting instances or non-Transaction directives, sorted in the same order as the entries. \"\"\" txn_postings_map = collections . defaultdict ( list ) for entry in entries : if isinstance ( entry , Transaction ): # Insert an entry for each of the postings. for posting in entry . postings : txn_postings_map [ posting . account ] . append ( TxnPosting ( entry , posting )) elif isinstance ( entry , ( Open , Close , Balance , Note , Document )): # Append some other entries in the realized list. txn_postings_map [ entry . account ] . append ( entry ) elif isinstance ( entry , Pad ): # Insert the pad entry in both realized accounts. txn_postings_map [ entry . account ] . append ( entry ) txn_postings_map [ entry . source_account ] . append ( entry ) elif isinstance ( entry , Custom ): # Insert custom entry for each account in its values. for custom_value in entry . values : if custom_value . dtype == account . TYPE : txn_postings_map [ custom_value . value ] . append ( entry ) return txn_postings_map","title":"postings_by_account()"},{"location":"api_reference/beancount.core.html#beancount.core.realization.realize","text":"Group entries by account, into a \"tree\" of realized accounts. RealAccount's are essentially containers for lists of postings and the final balance of each account, and may be non-leaf accounts (used strictly for organizing accounts into a hierarchy). This is then used to issue reports. The lists of postings in each account my be any of the entry types, except for Transaction, whereby Transaction entries are replaced by the specific Posting legs that belong to the account. Here's a simple diagram that summarizes this seemingly complex, but rather simple data structure: +-------------+ postings +------+ | RealAccount |---------->| Open | +-------------+ +------+ | v +------------+ +-------------+ | TxnPosting |---->| Transaction | +------------+ +-------------+ | \\ \\\\ v \\.__ +---------+ +-----+ -------->| Posting | | Pad | +---------+ +-----+ | v +---------+ | Balance | +---------+ | v +-------+ | Close | +-------+ | . Parameters: entries \u2013 A list of directives. min_accounts \u2013 A list of strings, account names to ensure we create, regardless of whether there are postings on those accounts or not. This can be used to ensure the root accounts all exist. compute_balance \u2013 A boolean, true if we should compute the final balance on the realization. Returns: The root RealAccount instance. Source code in beancount/core/realization.py def realize ( entries , min_accounts = None , compute_balance = True ): r \"\"\"Group entries by account, into a \"tree\" of realized accounts. RealAccount's are essentially containers for lists of postings and the final balance of each account, and may be non-leaf accounts (used strictly for organizing accounts into a hierarchy). This is then used to issue reports. The lists of postings in each account my be any of the entry types, except for Transaction, whereby Transaction entries are replaced by the specific Posting legs that belong to the account. Here's a simple diagram that summarizes this seemingly complex, but rather simple data structure: +-------------+ postings +------+ | RealAccount |---------->| Open | +-------------+ +------+ | v +------------+ +-------------+ | TxnPosting |---->| Transaction | +------------+ +-------------+ | \\ \\\\\\ v `\\.__ +---------+ +-----+ `-------->| Posting | | Pad | +---------+ +-----+ | v +---------+ | Balance | +---------+ | v +-------+ | Close | +-------+ | . Args: entries: A list of directives. min_accounts: A list of strings, account names to ensure we create, regardless of whether there are postings on those accounts or not. This can be used to ensure the root accounts all exist. compute_balance: A boolean, true if we should compute the final balance on the realization. Returns: The root RealAccount instance. \"\"\" # Create lists of the entries by account. txn_postings_map = postings_by_account ( entries ) # Create a RealAccount tree and compute the balance for each. real_root = RealAccount ( \"\" ) for account_name , txn_postings in txn_postings_map . items (): real_account = get_or_create ( real_root , account_name ) real_account . txn_postings = txn_postings if compute_balance : real_account . balance = compute_postings_balance ( txn_postings ) # Ensure a minimum set of accounts that should exist. This is typically # called with an instance of AccountTypes to make sure that those exist. if min_accounts : for account_name in min_accounts : get_or_create ( real_root , account_name ) return real_root","title":"realize()"},{"location":"api_reference/beancount.loader.html","text":"beancount.loader \uf0c1 Loader code. This is the main entry point to load up a file. beancount.loader.LoadError ( tuple ) \uf0c1 LoadError(source, message, entry) beancount . loader . LoadError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/loader.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . loader . LoadError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of LoadError(source, message, entry) beancount . loader . LoadError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/loader.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . loader . aggregate_options_map ( options_map , other_options_map ) \uf0c1 Aggregate some of the attributes of options map. Parameters: options_map \u2013 The target map in which we want to aggregate attributes. Note: This value is mutated in-place. other_options_map \u2013 A list of other options maps, some of whose values we'd like to see aggregated. Source code in beancount/loader.py def aggregate_options_map ( options_map , other_options_map ): \"\"\"Aggregate some of the attributes of options map. Args: options_map: The target map in which we want to aggregate attributes. Note: This value is mutated in-place. other_options_map: A list of other options maps, some of whose values we'd like to see aggregated. \"\"\" options_map = copy . copy ( options_map ) currencies = list ( options_map [ \"operating_currency\" ]) for omap in other_options_map : currencies . extend ( omap [ \"operating_currency\" ]) options_map [ \"dcontext\" ] . update_from ( omap [ \"dcontext\" ]) options_map [ \"operating_currency\" ] = list ( misc_utils . uniquify ( currencies )) # Produce a 'pythonpath' value for transformers. pythonpath = set () for omap in itertools . chain (( options_map ,), other_options_map ): if omap . get ( \"insert_pythonpath\" , False ): pythonpath . add ( path . dirname ( omap [ \"filename\" ])) options_map [ \"pythonpath\" ] = sorted ( pythonpath ) return options_map beancount . loader . combine_plugins ( * plugin_modules ) \uf0c1 Combine the plugins from the given plugin modules. This is used to create plugins of plugins. Parameters: *plugins_modules \u2013 A sequence of module objects. Returns: A list that can be assigned to the new module's plugins attribute. Source code in beancount/loader.py def combine_plugins ( * plugin_modules ): \"\"\"Combine the plugins from the given plugin modules. This is used to create plugins of plugins. Args: *plugins_modules: A sequence of module objects. Returns: A list that can be assigned to the new module's __plugins__ attribute. \"\"\" modules = [] for module in plugin_modules : modules . extend ([ getattr ( module , name ) for name in module . __plugins__ ]) return modules beancount . loader . compute_input_hash ( filenames ) \uf0c1 Compute a hash of the input data. Parameters: filenames \u2013 A list of input files. Order is not relevant. Source code in beancount/loader.py def compute_input_hash ( filenames ): \"\"\"Compute a hash of the input data. Args: filenames: A list of input files. Order is not relevant. \"\"\" md5 = hashlib . md5 () for filename in sorted ( filenames ): md5 . update ( filename . encode ( \"utf8\" )) if not path . exists ( filename ): continue stat = os . stat ( filename ) md5 . update ( struct . pack ( \"dd\" , stat . st_mtime_ns , stat . st_size )) return md5 . hexdigest () beancount . loader . delete_cache_function ( cache_getter , function ) \uf0c1 A wrapper that removes the cached filename. Parameters: cache_getter \u2013 A function of one argument, the top-level filename, which will return the name of the corresponding cache file. function \u2013 A function object to decorate for caching. Returns: A decorated function which will delete the cached filename, if it exists. Source code in beancount/loader.py def delete_cache_function ( cache_getter , function ): \"\"\"A wrapper that removes the cached filename. Args: cache_getter: A function of one argument, the top-level filename, which will return the name of the corresponding cache file. function: A function object to decorate for caching. Returns: A decorated function which will delete the cached filename, if it exists. \"\"\" @functools . wraps ( function ) def wrapped ( toplevel_filename , * args , ** kw ): # Delete the cache. cache_filename = cache_getter ( toplevel_filename ) if path . exists ( cache_filename ): os . remove ( cache_filename ) # Invoke the original function. return function ( toplevel_filename , * args , ** kw ) return wrapped beancount . loader . get_cache_filename ( pattern , filename ) \uf0c1 Compute the cache filename from a given pattern and the top-level filename. Parameters: pattern ( str ) \u2013 A cache filename or pattern. If the pattern contains '{filename}' this will get replaced by the top-level filename. This may be absolute or relative. filename ( str ) \u2013 The top-level filename. Returns: str \u2013 The resolved cache filename. Source code in beancount/loader.py def get_cache_filename ( pattern : str , filename : str ) -> str : \"\"\"Compute the cache filename from a given pattern and the top-level filename. Args: pattern: A cache filename or pattern. If the pattern contains '{filename}' this will get replaced by the top-level filename. This may be absolute or relative. filename: The top-level filename. Returns: The resolved cache filename. \"\"\" abs_filename = path . abspath ( filename ) if path . isabs ( pattern ): abs_pattern = pattern else : abs_pattern = path . join ( path . dirname ( abs_filename ), pattern ) return abs_pattern . format ( filename = path . basename ( filename )) beancount . loader . initialize ( use_cache , cache_filename = None ) \uf0c1 Initialize the loader. Source code in beancount/loader.py def initialize ( use_cache : bool , cache_filename : Optional [ str ] = None ): \"\"\"Initialize the loader.\"\"\" # Unless an environment variable disables it, use the pickle load cache # automatically. Note that this works across all Python programs running the # loader which is why it's located here. global _load_file # Make a function to compute the cache filename. cache_pattern = ( cache_filename or os . getenv ( \"BEANCOUNT_LOAD_CACHE_FILENAME\" ) or PICKLE_CACHE_FILENAME ) cache_getter = functools . partial ( get_cache_filename , cache_pattern ) if use_cache : _load_file = pickle_cache_function ( cache_getter , PICKLE_CACHE_THRESHOLD , _uncached_load_file ) else : if cache_filename is not None : logging . warning ( \"Cache disabled; \" \"Explicitly overridden cache filename %s will be ignored.\" , cache_filename , ) _load_file = delete_cache_function ( cache_getter , _uncached_load_file ) beancount . loader . load_doc ( expect_errors = False ) \uf0c1 A factory of decorators that loads the docstring and calls the function with entries. This is an incredibly convenient tool to write lots of tests. Write a unittest using the standard TestCase class and put the input entries in the function's docstring. Parameters: expect_errors \u2013 A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. Returns: A wrapped method that accepts a single 'self' argument. Source code in beancount/loader.py def load_doc ( expect_errors = False ): \"\"\"A factory of decorators that loads the docstring and calls the function with entries. This is an incredibly convenient tool to write lots of tests. Write a unittest using the standard TestCase class and put the input entries in the function's docstring. Args: expect_errors: A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. Returns: A wrapped method that accepts a single 'self' argument. \"\"\" def decorator ( fun ): \"\"\"A decorator that parses the function's docstring as an argument. Args: fun: A callable method, that accepts the three return arguments that load() returns. Returns: A decorated test function. \"\"\" @functools . wraps ( fun ) def wrapper ( self ): entries , errors , options_map = load_string ( fun . __doc__ , dedent = True ) if expect_errors is not None : if expect_errors is False and errors : oss = io . StringIO () printer . print_errors ( errors , file = oss ) self . fail ( \"Unexpected errors found: \\n {} \" . format ( oss . getvalue ())) elif expect_errors is True and not errors : self . fail ( \"Expected errors, none found:\" ) # Note: Even if we expected no errors, we call this function with an # empty 'errors' list. This is so that the interface does not change # based on the arguments to the decorator, which would be somewhat # ugly and which would require explanation. return fun ( self , entries , errors , options_map ) wrapper . __input__ = wrapper . __doc__ wrapper . __doc__ = None return wrapper return decorator beancount . loader . load_encrypted_file ( filename , log_timings = None , log_errors = None , extra_validations = None , dedent = False , encoding = None ) \uf0c1 Load an encrypted Beancount input file. Parameters: filename \u2013 The name of an encrypted file to be parsed. log_timings \u2013 See load_string(). log_errors \u2013 See load_string(). extra_validations \u2013 See load_string(). dedent \u2013 See load_string(). encoding \u2013 See load_string(). Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. Source code in beancount/loader.py def load_encrypted_file ( filename , log_timings = None , log_errors = None , extra_validations = None , dedent = False , encoding = None , ): \"\"\"Load an encrypted Beancount input file. Args: filename: The name of an encrypted file to be parsed. log_timings: See load_string(). log_errors: See load_string(). extra_validations: See load_string(). dedent: See load_string(). encoding: See load_string(). Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. \"\"\" contents = encryption . read_encrypted_file ( filename ) return load_string ( contents , log_timings = log_timings , log_errors = log_errors , extra_validations = extra_validations , encoding = encoding , ) beancount . loader . load_file ( filename , log_timings = None , log_errors = None , extra_validations = None , encoding = None ) \uf0c1 Open a Beancount input file, parse it, run transformations and validate. Parameters: filename \u2013 The name of the file to be parsed. log_timings \u2013 A file object or function to write timings to, or None, if it should remain quiet. (Note that this is intended to use the logging methods and does not insert a newline.) log_errors \u2013 A file object or function to write errors to, or None, if it should remain quiet. extra_validations \u2013 A list of extra validation functions to run after loading this list of entries. encoding \u2013 A string or None, the encoding to decode the input filename with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. Source code in beancount/loader.py def load_file ( filename , log_timings = None , log_errors = None , extra_validations = None , encoding = None ): \"\"\"Open a Beancount input file, parse it, run transformations and validate. Args: filename: The name of the file to be parsed. log_timings: A file object or function to write timings to, or None, if it should remain quiet. (Note that this is intended to use the logging methods and does not insert a newline.) log_errors: A file object or function to write errors to, or None, if it should remain quiet. extra_validations: A list of extra validation functions to run after loading this list of entries. encoding: A string or None, the encoding to decode the input filename with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. \"\"\" filename = path . expandvars ( path . expanduser ( filename )) if not path . isabs ( filename ): filename = path . normpath ( path . join ( os . getcwd (), filename )) if encryption . is_encrypted_file ( filename ): # Note: Caching is not supported for encrypted files. entries , errors , options_map = load_encrypted_file ( filename , log_timings , log_errors , extra_validations , False , encoding ) else : entries , errors , options_map = _load_file ( filename , log_timings , extra_validations , encoding ) _log_errors ( errors , log_errors ) return entries , errors , options_map beancount . loader . load_string ( string , log_timings = None , log_errors = None , extra_validations = None , dedent = False , encoding = None ) \uf0c1 Open a Beancount input string, parse it, run transformations and validate. Parameters: string \u2013 A Beancount input string. log_timings \u2013 A file object or function to write timings to, or None, if it should remain quiet. log_errors \u2013 A file object or function to write errors to, or None, if it should remain quiet. extra_validations \u2013 A list of extra validation functions to run after loading this list of entries. dedent \u2013 A boolean, if set, remove the whitespace in front of the lines. encoding \u2013 A string or None, the encoding to decode the input string with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the string, \"errors\" a list of error objects generated while parsing and validating the string, and \"options_map\", a dict of the options parsed from the string. Source code in beancount/loader.py def load_string ( string , log_timings = None , log_errors = None , extra_validations = None , dedent = False , encoding = None , ): \"\"\"Open a Beancount input string, parse it, run transformations and validate. Args: string: A Beancount input string. log_timings: A file object or function to write timings to, or None, if it should remain quiet. log_errors: A file object or function to write errors to, or None, if it should remain quiet. extra_validations: A list of extra validation functions to run after loading this list of entries. dedent: A boolean, if set, remove the whitespace in front of the lines. encoding: A string or None, the encoding to decode the input string with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the string, \"errors\" a list of error objects generated while parsing and validating the string, and \"options_map\", a dict of the options parsed from the string. \"\"\" if dedent : string = textwrap . dedent ( string ) entries , errors , options_map = _load ( [( string , False )], log_timings , extra_validations , encoding ) _log_errors ( errors , log_errors ) return entries , errors , options_map beancount . loader . needs_refresh ( options_map ) \uf0c1 Predicate that returns true if at least one of the input files may have changed. Parameters: options_map \u2013 An options dict as per the parser. mtime \u2013 A modified time, to check if it covers the include files in the options_map. Returns: A boolean, true if the input is obsoleted by changes in the input files. Source code in beancount/loader.py def needs_refresh ( options_map ): \"\"\"Predicate that returns true if at least one of the input files may have changed. Args: options_map: An options dict as per the parser. mtime: A modified time, to check if it covers the include files in the options_map. Returns: A boolean, true if the input is obsoleted by changes in the input files. \"\"\" if options_map is None : return True input_hash = compute_input_hash ( options_map [ \"include\" ]) return \"input_hash\" not in options_map or input_hash != options_map [ \"input_hash\" ] beancount . loader . pickle_cache_function ( cache_getter , time_threshold , function ) \uf0c1 Decorate a loader function to make it loads its result from a pickle cache. This considers the first argument as a top-level filename and assumes the function to be cached returns an (entries, errors, options_map) triple. We use the 'include' option value in order to check whether any of the included files has changed. It's essentially a special case for an on-disk memoizer. If any of the included files are more recent than the cache, the function is recomputed and the cache refreshed. Parameters: cache_getter \u2013 A function of one argument, the top-level filename, which will return the name of the corresponding cache file. time_threshold \u2013 A float, the number of seconds below which we don't bother caching. function \u2013 A function object to decorate for caching. Returns: A decorated function which will pull its result from a cache file if it is available. Source code in beancount/loader.py def pickle_cache_function ( cache_getter , time_threshold , function ): \"\"\"Decorate a loader function to make it loads its result from a pickle cache. This considers the first argument as a top-level filename and assumes the function to be cached returns an (entries, errors, options_map) triple. We use the 'include' option value in order to check whether any of the included files has changed. It's essentially a special case for an on-disk memoizer. If any of the included files are more recent than the cache, the function is recomputed and the cache refreshed. Args: cache_getter: A function of one argument, the top-level filename, which will return the name of the corresponding cache file. time_threshold: A float, the number of seconds below which we don't bother caching. function: A function object to decorate for caching. Returns: A decorated function which will pull its result from a cache file if it is available. \"\"\" @functools . wraps ( function ) def wrapped ( toplevel_filename , * args , ** kw ): cache_filename = cache_getter ( toplevel_filename ) # Read the cache if it exists in order to get the list of files whose # timestamps to check. exists = path . exists ( cache_filename ) if exists : with open ( cache_filename , \"rb\" ) as file : try : result = pickle . load ( file ) except Exception as exc : # Note: Not a big fan of doing this, but here we handle all # possible exceptions because unpickling of an old or # corrupted pickle file manifests as a variety of different # exception types. # The cache file is corrupted; ignore it and recompute. logging . error ( \"Cache file is corrupted: %s ; recomputing.\" , exc ) result = None else : # Check that the latest timestamp has not been written after the # cache file. entries , errors , options_map = result if not needs_refresh ( options_map ): # All timestamps are legit; cache hit. return result # We failed; recompute the value. if exists : try : os . remove ( cache_filename ) except OSError as exc : # Warn for errors on read-only filesystems. logging . warning ( \"Could not remove picklecache file %s : %s \" , cache_filename , exc ) time_before = time . time () result = function ( toplevel_filename , * args , ** kw ) time_after = time . time () # Overwrite the cache file if the time it takes to compute it # justifies it. if time_after - time_before > time_threshold : try : with open ( cache_filename , \"wb\" ) as file : pickle . dump ( result , file ) except Exception as exc : logging . warning ( \"Could not write to picklecache file %s : %s \" , cache_filename , exc ) return result return wrapped beancount . loader . run_transformations ( entries , parse_errors , options_map , log_timings ) \uf0c1 Run the various transformations on the entries. This is where entries are being synthesized, checked, plugins are run, etc. Parameters: entries \u2013 A list of directives as read from the parser. parse_errors \u2013 A list of errors so far. options_map \u2013 An options dict as read from the parser. log_timings \u2013 A function to write timing log entries to, or None, if it should be quiet. Returns: A list of modified entries, and a list of errors, also possibly modified. Source code in beancount/loader.py def run_transformations ( entries , parse_errors , options_map , log_timings ): \"\"\"Run the various transformations on the entries. This is where entries are being synthesized, checked, plugins are run, etc. Args: entries: A list of directives as read from the parser. parse_errors: A list of errors so far. options_map: An options dict as read from the parser. log_timings: A function to write timing log entries to, or None, if it should be quiet. Returns: A list of modified entries, and a list of errors, also possibly modified. \"\"\" # A list of errors to extend (make a copy to avoid modifying the input). errors = list ( parse_errors ) # Process the plugins. if options_map [ \"plugin_processing_mode\" ] == \"raw\" : plugins_iter = options_map [ \"plugin\" ] elif options_map [ \"plugin_processing_mode\" ] == \"default\" : plugins_iter = itertools . chain ( PLUGINS_PRE , options_map [ \"plugin\" ], PLUGINS_AUTO , PLUGINS_POST ) else : assert \"Invalid value for plugin_processing_mode: {} \" . format ( options_map [ \"plugin_processing_mode\" ] ) for plugin_name , plugin_config in plugins_iter : # Issue a warning on a renamed module. renamed_name = RENAMED_MODULES . get ( plugin_name , None ) if renamed_name : warnings . warn ( \"Deprecation notice: Module ' {} ' has been renamed to ' {} '; \" \"please adjust your plugin directive.\" . format ( plugin_name , renamed_name ) ) plugin_name = renamed_name # Try to import the module. # # Note: We intercept import errors and continue but let other plugin # import time exceptions fail a run, by choice. try : module = importlib . import_module ( plugin_name ) if not hasattr ( module , \"__plugins__\" ): continue except ImportError : # Upon failure, just issue an error. formatted_traceback = traceback . format_exc () . replace ( \" \\n \" , \" \\n \" ) errors . append ( LoadError ( data . new_metadata ( \"\" , 0 ), 'Error importing \" {} \": {} ' . format ( plugin_name , formatted_traceback ), None , ) ) continue # Apply it. with misc_utils . log_time ( plugin_name , log_timings , indent = 2 ): # Run each transformer function in the plugin. for function_name in module . __plugins__ : if isinstance ( function_name , str ): # Support plugin functions provided by name. callback = getattr ( module , function_name ) else : # Support function types directly, not just names. callback = function_name # Provide arguments if config is provided. # TODO(blais): Make this consistent in v3, not conditional. args = () if plugin_config is None else ( plugin_config ,) # Catch all exceptions raised in running the plugin, except exits. try : entries , plugin_errors = callback ( entries , options_map , * args ) errors . extend ( plugin_errors ) except Exception as exc : # Allow the user to exit in a plugin. if isinstance ( exc , SystemExit ): raise # Upon failure, just issue an error. formatted_traceback = traceback . format_exc () . replace ( \" \\n \" , \" \\n \" ) errors . append ( LoadError ( data . new_metadata ( \"\" , 0 ), 'Error applying plugin \" {} \": {} ' . format ( plugin_name , formatted_traceback ), None , ) ) continue # Ensure that the entries are sorted. Don't trust the plugins # themselves. entries . sort ( key = data . entry_sortkey ) return entries , errors","title":"beancount.loader"},{"location":"api_reference/beancount.loader.html#beancountloader","text":"Loader code. This is the main entry point to load up a file.","title":"beancount.loader"},{"location":"api_reference/beancount.loader.html#beancount.loader.LoadError","text":"LoadError(source, message, entry)","title":"LoadError"},{"location":"api_reference/beancount.loader.html#beancount.loader.LoadError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/loader.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.loader.html#beancount.loader.LoadError.__new__","text":"Create new instance of LoadError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.loader.html#beancount.loader.LoadError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/loader.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.loader.html#beancount.loader.aggregate_options_map","text":"Aggregate some of the attributes of options map. Parameters: options_map \u2013 The target map in which we want to aggregate attributes. Note: This value is mutated in-place. other_options_map \u2013 A list of other options maps, some of whose values we'd like to see aggregated. Source code in beancount/loader.py def aggregate_options_map ( options_map , other_options_map ): \"\"\"Aggregate some of the attributes of options map. Args: options_map: The target map in which we want to aggregate attributes. Note: This value is mutated in-place. other_options_map: A list of other options maps, some of whose values we'd like to see aggregated. \"\"\" options_map = copy . copy ( options_map ) currencies = list ( options_map [ \"operating_currency\" ]) for omap in other_options_map : currencies . extend ( omap [ \"operating_currency\" ]) options_map [ \"dcontext\" ] . update_from ( omap [ \"dcontext\" ]) options_map [ \"operating_currency\" ] = list ( misc_utils . uniquify ( currencies )) # Produce a 'pythonpath' value for transformers. pythonpath = set () for omap in itertools . chain (( options_map ,), other_options_map ): if omap . get ( \"insert_pythonpath\" , False ): pythonpath . add ( path . dirname ( omap [ \"filename\" ])) options_map [ \"pythonpath\" ] = sorted ( pythonpath ) return options_map","title":"aggregate_options_map()"},{"location":"api_reference/beancount.loader.html#beancount.loader.combine_plugins","text":"Combine the plugins from the given plugin modules. This is used to create plugins of plugins. Parameters: *plugins_modules \u2013 A sequence of module objects. Returns: A list that can be assigned to the new module's plugins attribute. Source code in beancount/loader.py def combine_plugins ( * plugin_modules ): \"\"\"Combine the plugins from the given plugin modules. This is used to create plugins of plugins. Args: *plugins_modules: A sequence of module objects. Returns: A list that can be assigned to the new module's __plugins__ attribute. \"\"\" modules = [] for module in plugin_modules : modules . extend ([ getattr ( module , name ) for name in module . __plugins__ ]) return modules","title":"combine_plugins()"},{"location":"api_reference/beancount.loader.html#beancount.loader.compute_input_hash","text":"Compute a hash of the input data. Parameters: filenames \u2013 A list of input files. Order is not relevant. Source code in beancount/loader.py def compute_input_hash ( filenames ): \"\"\"Compute a hash of the input data. Args: filenames: A list of input files. Order is not relevant. \"\"\" md5 = hashlib . md5 () for filename in sorted ( filenames ): md5 . update ( filename . encode ( \"utf8\" )) if not path . exists ( filename ): continue stat = os . stat ( filename ) md5 . update ( struct . pack ( \"dd\" , stat . st_mtime_ns , stat . st_size )) return md5 . hexdigest ()","title":"compute_input_hash()"},{"location":"api_reference/beancount.loader.html#beancount.loader.delete_cache_function","text":"A wrapper that removes the cached filename. Parameters: cache_getter \u2013 A function of one argument, the top-level filename, which will return the name of the corresponding cache file. function \u2013 A function object to decorate for caching. Returns: A decorated function which will delete the cached filename, if it exists. Source code in beancount/loader.py def delete_cache_function ( cache_getter , function ): \"\"\"A wrapper that removes the cached filename. Args: cache_getter: A function of one argument, the top-level filename, which will return the name of the corresponding cache file. function: A function object to decorate for caching. Returns: A decorated function which will delete the cached filename, if it exists. \"\"\" @functools . wraps ( function ) def wrapped ( toplevel_filename , * args , ** kw ): # Delete the cache. cache_filename = cache_getter ( toplevel_filename ) if path . exists ( cache_filename ): os . remove ( cache_filename ) # Invoke the original function. return function ( toplevel_filename , * args , ** kw ) return wrapped","title":"delete_cache_function()"},{"location":"api_reference/beancount.loader.html#beancount.loader.get_cache_filename","text":"Compute the cache filename from a given pattern and the top-level filename. Parameters: pattern ( str ) \u2013 A cache filename or pattern. If the pattern contains '{filename}' this will get replaced by the top-level filename. This may be absolute or relative. filename ( str ) \u2013 The top-level filename. Returns: str \u2013 The resolved cache filename. Source code in beancount/loader.py def get_cache_filename ( pattern : str , filename : str ) -> str : \"\"\"Compute the cache filename from a given pattern and the top-level filename. Args: pattern: A cache filename or pattern. If the pattern contains '{filename}' this will get replaced by the top-level filename. This may be absolute or relative. filename: The top-level filename. Returns: The resolved cache filename. \"\"\" abs_filename = path . abspath ( filename ) if path . isabs ( pattern ): abs_pattern = pattern else : abs_pattern = path . join ( path . dirname ( abs_filename ), pattern ) return abs_pattern . format ( filename = path . basename ( filename ))","title":"get_cache_filename()"},{"location":"api_reference/beancount.loader.html#beancount.loader.initialize","text":"Initialize the loader. Source code in beancount/loader.py def initialize ( use_cache : bool , cache_filename : Optional [ str ] = None ): \"\"\"Initialize the loader.\"\"\" # Unless an environment variable disables it, use the pickle load cache # automatically. Note that this works across all Python programs running the # loader which is why it's located here. global _load_file # Make a function to compute the cache filename. cache_pattern = ( cache_filename or os . getenv ( \"BEANCOUNT_LOAD_CACHE_FILENAME\" ) or PICKLE_CACHE_FILENAME ) cache_getter = functools . partial ( get_cache_filename , cache_pattern ) if use_cache : _load_file = pickle_cache_function ( cache_getter , PICKLE_CACHE_THRESHOLD , _uncached_load_file ) else : if cache_filename is not None : logging . warning ( \"Cache disabled; \" \"Explicitly overridden cache filename %s will be ignored.\" , cache_filename , ) _load_file = delete_cache_function ( cache_getter , _uncached_load_file )","title":"initialize()"},{"location":"api_reference/beancount.loader.html#beancount.loader.load_doc","text":"A factory of decorators that loads the docstring and calls the function with entries. This is an incredibly convenient tool to write lots of tests. Write a unittest using the standard TestCase class and put the input entries in the function's docstring. Parameters: expect_errors \u2013 A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. Returns: A wrapped method that accepts a single 'self' argument. Source code in beancount/loader.py def load_doc ( expect_errors = False ): \"\"\"A factory of decorators that loads the docstring and calls the function with entries. This is an incredibly convenient tool to write lots of tests. Write a unittest using the standard TestCase class and put the input entries in the function's docstring. Args: expect_errors: A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. Returns: A wrapped method that accepts a single 'self' argument. \"\"\" def decorator ( fun ): \"\"\"A decorator that parses the function's docstring as an argument. Args: fun: A callable method, that accepts the three return arguments that load() returns. Returns: A decorated test function. \"\"\" @functools . wraps ( fun ) def wrapper ( self ): entries , errors , options_map = load_string ( fun . __doc__ , dedent = True ) if expect_errors is not None : if expect_errors is False and errors : oss = io . StringIO () printer . print_errors ( errors , file = oss ) self . fail ( \"Unexpected errors found: \\n {} \" . format ( oss . getvalue ())) elif expect_errors is True and not errors : self . fail ( \"Expected errors, none found:\" ) # Note: Even if we expected no errors, we call this function with an # empty 'errors' list. This is so that the interface does not change # based on the arguments to the decorator, which would be somewhat # ugly and which would require explanation. return fun ( self , entries , errors , options_map ) wrapper . __input__ = wrapper . __doc__ wrapper . __doc__ = None return wrapper return decorator","title":"load_doc()"},{"location":"api_reference/beancount.loader.html#beancount.loader.load_encrypted_file","text":"Load an encrypted Beancount input file. Parameters: filename \u2013 The name of an encrypted file to be parsed. log_timings \u2013 See load_string(). log_errors \u2013 See load_string(). extra_validations \u2013 See load_string(). dedent \u2013 See load_string(). encoding \u2013 See load_string(). Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. Source code in beancount/loader.py def load_encrypted_file ( filename , log_timings = None , log_errors = None , extra_validations = None , dedent = False , encoding = None , ): \"\"\"Load an encrypted Beancount input file. Args: filename: The name of an encrypted file to be parsed. log_timings: See load_string(). log_errors: See load_string(). extra_validations: See load_string(). dedent: See load_string(). encoding: See load_string(). Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. \"\"\" contents = encryption . read_encrypted_file ( filename ) return load_string ( contents , log_timings = log_timings , log_errors = log_errors , extra_validations = extra_validations , encoding = encoding , )","title":"load_encrypted_file()"},{"location":"api_reference/beancount.loader.html#beancount.loader.load_file","text":"Open a Beancount input file, parse it, run transformations and validate. Parameters: filename \u2013 The name of the file to be parsed. log_timings \u2013 A file object or function to write timings to, or None, if it should remain quiet. (Note that this is intended to use the logging methods and does not insert a newline.) log_errors \u2013 A file object or function to write errors to, or None, if it should remain quiet. extra_validations \u2013 A list of extra validation functions to run after loading this list of entries. encoding \u2013 A string or None, the encoding to decode the input filename with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. Source code in beancount/loader.py def load_file ( filename , log_timings = None , log_errors = None , extra_validations = None , encoding = None ): \"\"\"Open a Beancount input file, parse it, run transformations and validate. Args: filename: The name of the file to be parsed. log_timings: A file object or function to write timings to, or None, if it should remain quiet. (Note that this is intended to use the logging methods and does not insert a newline.) log_errors: A file object or function to write errors to, or None, if it should remain quiet. extra_validations: A list of extra validation functions to run after loading this list of entries. encoding: A string or None, the encoding to decode the input filename with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the file, \"errors\" a list of error objects generated while parsing and validating the file, and \"options_map\", a dict of the options parsed from the file. \"\"\" filename = path . expandvars ( path . expanduser ( filename )) if not path . isabs ( filename ): filename = path . normpath ( path . join ( os . getcwd (), filename )) if encryption . is_encrypted_file ( filename ): # Note: Caching is not supported for encrypted files. entries , errors , options_map = load_encrypted_file ( filename , log_timings , log_errors , extra_validations , False , encoding ) else : entries , errors , options_map = _load_file ( filename , log_timings , extra_validations , encoding ) _log_errors ( errors , log_errors ) return entries , errors , options_map","title":"load_file()"},{"location":"api_reference/beancount.loader.html#beancount.loader.load_string","text":"Open a Beancount input string, parse it, run transformations and validate. Parameters: string \u2013 A Beancount input string. log_timings \u2013 A file object or function to write timings to, or None, if it should remain quiet. log_errors \u2013 A file object or function to write errors to, or None, if it should remain quiet. extra_validations \u2013 A list of extra validation functions to run after loading this list of entries. dedent \u2013 A boolean, if set, remove the whitespace in front of the lines. encoding \u2013 A string or None, the encoding to decode the input string with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the string, \"errors\" a list of error objects generated while parsing and validating the string, and \"options_map\", a dict of the options parsed from the string. Source code in beancount/loader.py def load_string ( string , log_timings = None , log_errors = None , extra_validations = None , dedent = False , encoding = None , ): \"\"\"Open a Beancount input string, parse it, run transformations and validate. Args: string: A Beancount input string. log_timings: A file object or function to write timings to, or None, if it should remain quiet. log_errors: A file object or function to write errors to, or None, if it should remain quiet. extra_validations: A list of extra validation functions to run after loading this list of entries. dedent: A boolean, if set, remove the whitespace in front of the lines. encoding: A string or None, the encoding to decode the input string with. Returns: A triple of (entries, errors, option_map) where \"entries\" is a date-sorted list of entries from the string, \"errors\" a list of error objects generated while parsing and validating the string, and \"options_map\", a dict of the options parsed from the string. \"\"\" if dedent : string = textwrap . dedent ( string ) entries , errors , options_map = _load ( [( string , False )], log_timings , extra_validations , encoding ) _log_errors ( errors , log_errors ) return entries , errors , options_map","title":"load_string()"},{"location":"api_reference/beancount.loader.html#beancount.loader.needs_refresh","text":"Predicate that returns true if at least one of the input files may have changed. Parameters: options_map \u2013 An options dict as per the parser. mtime \u2013 A modified time, to check if it covers the include files in the options_map. Returns: A boolean, true if the input is obsoleted by changes in the input files. Source code in beancount/loader.py def needs_refresh ( options_map ): \"\"\"Predicate that returns true if at least one of the input files may have changed. Args: options_map: An options dict as per the parser. mtime: A modified time, to check if it covers the include files in the options_map. Returns: A boolean, true if the input is obsoleted by changes in the input files. \"\"\" if options_map is None : return True input_hash = compute_input_hash ( options_map [ \"include\" ]) return \"input_hash\" not in options_map or input_hash != options_map [ \"input_hash\" ]","title":"needs_refresh()"},{"location":"api_reference/beancount.loader.html#beancount.loader.pickle_cache_function","text":"Decorate a loader function to make it loads its result from a pickle cache. This considers the first argument as a top-level filename and assumes the function to be cached returns an (entries, errors, options_map) triple. We use the 'include' option value in order to check whether any of the included files has changed. It's essentially a special case for an on-disk memoizer. If any of the included files are more recent than the cache, the function is recomputed and the cache refreshed. Parameters: cache_getter \u2013 A function of one argument, the top-level filename, which will return the name of the corresponding cache file. time_threshold \u2013 A float, the number of seconds below which we don't bother caching. function \u2013 A function object to decorate for caching. Returns: A decorated function which will pull its result from a cache file if it is available. Source code in beancount/loader.py def pickle_cache_function ( cache_getter , time_threshold , function ): \"\"\"Decorate a loader function to make it loads its result from a pickle cache. This considers the first argument as a top-level filename and assumes the function to be cached returns an (entries, errors, options_map) triple. We use the 'include' option value in order to check whether any of the included files has changed. It's essentially a special case for an on-disk memoizer. If any of the included files are more recent than the cache, the function is recomputed and the cache refreshed. Args: cache_getter: A function of one argument, the top-level filename, which will return the name of the corresponding cache file. time_threshold: A float, the number of seconds below which we don't bother caching. function: A function object to decorate for caching. Returns: A decorated function which will pull its result from a cache file if it is available. \"\"\" @functools . wraps ( function ) def wrapped ( toplevel_filename , * args , ** kw ): cache_filename = cache_getter ( toplevel_filename ) # Read the cache if it exists in order to get the list of files whose # timestamps to check. exists = path . exists ( cache_filename ) if exists : with open ( cache_filename , \"rb\" ) as file : try : result = pickle . load ( file ) except Exception as exc : # Note: Not a big fan of doing this, but here we handle all # possible exceptions because unpickling of an old or # corrupted pickle file manifests as a variety of different # exception types. # The cache file is corrupted; ignore it and recompute. logging . error ( \"Cache file is corrupted: %s ; recomputing.\" , exc ) result = None else : # Check that the latest timestamp has not been written after the # cache file. entries , errors , options_map = result if not needs_refresh ( options_map ): # All timestamps are legit; cache hit. return result # We failed; recompute the value. if exists : try : os . remove ( cache_filename ) except OSError as exc : # Warn for errors on read-only filesystems. logging . warning ( \"Could not remove picklecache file %s : %s \" , cache_filename , exc ) time_before = time . time () result = function ( toplevel_filename , * args , ** kw ) time_after = time . time () # Overwrite the cache file if the time it takes to compute it # justifies it. if time_after - time_before > time_threshold : try : with open ( cache_filename , \"wb\" ) as file : pickle . dump ( result , file ) except Exception as exc : logging . warning ( \"Could not write to picklecache file %s : %s \" , cache_filename , exc ) return result return wrapped","title":"pickle_cache_function()"},{"location":"api_reference/beancount.loader.html#beancount.loader.run_transformations","text":"Run the various transformations on the entries. This is where entries are being synthesized, checked, plugins are run, etc. Parameters: entries \u2013 A list of directives as read from the parser. parse_errors \u2013 A list of errors so far. options_map \u2013 An options dict as read from the parser. log_timings \u2013 A function to write timing log entries to, or None, if it should be quiet. Returns: A list of modified entries, and a list of errors, also possibly modified. Source code in beancount/loader.py def run_transformations ( entries , parse_errors , options_map , log_timings ): \"\"\"Run the various transformations on the entries. This is where entries are being synthesized, checked, plugins are run, etc. Args: entries: A list of directives as read from the parser. parse_errors: A list of errors so far. options_map: An options dict as read from the parser. log_timings: A function to write timing log entries to, or None, if it should be quiet. Returns: A list of modified entries, and a list of errors, also possibly modified. \"\"\" # A list of errors to extend (make a copy to avoid modifying the input). errors = list ( parse_errors ) # Process the plugins. if options_map [ \"plugin_processing_mode\" ] == \"raw\" : plugins_iter = options_map [ \"plugin\" ] elif options_map [ \"plugin_processing_mode\" ] == \"default\" : plugins_iter = itertools . chain ( PLUGINS_PRE , options_map [ \"plugin\" ], PLUGINS_AUTO , PLUGINS_POST ) else : assert \"Invalid value for plugin_processing_mode: {} \" . format ( options_map [ \"plugin_processing_mode\" ] ) for plugin_name , plugin_config in plugins_iter : # Issue a warning on a renamed module. renamed_name = RENAMED_MODULES . get ( plugin_name , None ) if renamed_name : warnings . warn ( \"Deprecation notice: Module ' {} ' has been renamed to ' {} '; \" \"please adjust your plugin directive.\" . format ( plugin_name , renamed_name ) ) plugin_name = renamed_name # Try to import the module. # # Note: We intercept import errors and continue but let other plugin # import time exceptions fail a run, by choice. try : module = importlib . import_module ( plugin_name ) if not hasattr ( module , \"__plugins__\" ): continue except ImportError : # Upon failure, just issue an error. formatted_traceback = traceback . format_exc () . replace ( \" \\n \" , \" \\n \" ) errors . append ( LoadError ( data . new_metadata ( \"\" , 0 ), 'Error importing \" {} \": {} ' . format ( plugin_name , formatted_traceback ), None , ) ) continue # Apply it. with misc_utils . log_time ( plugin_name , log_timings , indent = 2 ): # Run each transformer function in the plugin. for function_name in module . __plugins__ : if isinstance ( function_name , str ): # Support plugin functions provided by name. callback = getattr ( module , function_name ) else : # Support function types directly, not just names. callback = function_name # Provide arguments if config is provided. # TODO(blais): Make this consistent in v3, not conditional. args = () if plugin_config is None else ( plugin_config ,) # Catch all exceptions raised in running the plugin, except exits. try : entries , plugin_errors = callback ( entries , options_map , * args ) errors . extend ( plugin_errors ) except Exception as exc : # Allow the user to exit in a plugin. if isinstance ( exc , SystemExit ): raise # Upon failure, just issue an error. formatted_traceback = traceback . format_exc () . replace ( \" \\n \" , \" \\n \" ) errors . append ( LoadError ( data . new_metadata ( \"\" , 0 ), 'Error applying plugin \" {} \": {} ' . format ( plugin_name , formatted_traceback ), None , ) ) continue # Ensure that the entries are sorted. Don't trust the plugins # themselves. entries . sort ( key = data . entry_sortkey ) return entries , errors","title":"run_transformations()"},{"location":"api_reference/beancount.ops.html","text":"beancount.ops \uf0c1 Operations on the entries defined in the core modules. This package contains various functions which operate on lists of entries. beancount.ops.balance \uf0c1 Automatic padding of gaps between entries. beancount.ops.balance.BalanceError ( tuple ) \uf0c1 BalanceError(source, message, entry) beancount . ops . balance . BalanceError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/balance.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . ops . balance . BalanceError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of BalanceError(source, message, entry) beancount . ops . balance . BalanceError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/ops/balance.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . ops . balance . check ( entries , options_map ) \uf0c1 Process the balance assertion directives. For each Balance directive, check that their expected balance corresponds to the actual balance computed at that time and replace failing ones by new ones with a flag that indicates failure. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, parsed from the input file. Returns: A pair of a list of directives and a list of balance check errors. Source code in beancount/ops/balance.py def check ( entries , options_map ): \"\"\"Process the balance assertion directives. For each Balance directive, check that their expected balance corresponds to the actual balance computed at that time and replace failing ones by new ones with a flag that indicates failure. Args: entries: A list of directives. options_map: A dict of options, parsed from the input file. Returns: A pair of a list of directives and a list of balance check errors. \"\"\" new_entries = [] check_errors = [] # This is similar to realization, but performed in a different order, and # where we only accumulate inventories for accounts that have balance # assertions in them (this saves on time). Here we process the entries one # by one along with the balance checks. We use a temporary realization in # order to hold the incremental tree of balances, so that we can easily get # the amounts of an account's subaccounts for making checks on parent # accounts. real_root = realization . RealAccount ( \"\" ) # Figure out the set of accounts for which we need to compute a running # inventory balance. asserted_accounts = { entry . account for entry in entries if isinstance ( entry , Balance )} # Add all children accounts of an asserted account to be calculated as well, # and pre-create these accounts, and only those (we're just being tight to # make sure). asserted_match_list = [ account . parent_matcher ( account_ ) for account_ in asserted_accounts ] for account_ in getters . get_accounts ( entries ): if account_ in asserted_accounts or any ( match ( account_ ) for match in asserted_match_list ): realization . get_or_create ( real_root , account_ ) # Get the Open directives for each account. open_close_map = getters . get_account_open_close ( entries ) for entry in entries : if isinstance ( entry , Transaction ): # For each of the postings' accounts, update the balance inventory. for posting in entry . postings : real_account = realization . get ( real_root , posting . account ) # The account will have been created only if we're meant to track it. if real_account is not None : # Note: Always allow negative lots for the purpose of balancing. # This error should show up somewhere else than here. real_account . balance . add_position ( posting ) elif isinstance ( entry , Balance ): # Check that the currency of the balance check is one of the allowed # currencies for that account. expected_amount = entry . amount try : open , _ = open_close_map [ entry . account ] except KeyError : check_errors . append ( BalanceError ( entry . meta , \"Invalid reference to unknown account ' {} '\" . format ( entry . account ), entry , ) ) continue if ( expected_amount is not None and open and open . currencies and expected_amount . currency not in open . currencies ): check_errors . append ( BalanceError ( entry . meta , \"Invalid currency ' {} ' for Balance directive: \" . format ( expected_amount . currency ), entry , ) ) # Sum up the current balances for this account and its # sub-accounts. We want to support checks for parent accounts # for the total sum of their subaccounts. # # FIXME: Improve the performance further by computing the balance # for the desired currency only. This won't allow us to cache in # this way but may be faster, if we're not asserting all the # currencies. Furthermore, we could probably avoid recomputing the # balance if a subtree of positions hasn't been invalidated by a new # position added to the realization. Do this. real_account = realization . get ( real_root , entry . account ) assert real_account is not None , \"Missing {} \" . format ( entry . account ) subtree_balance = realization . compute_balance ( real_account , leaf_only = False ) # Get only the amount in the desired currency. balance_amount = subtree_balance . get_currency_units ( expected_amount . currency ) # Check if the amount is within bounds of the expected amount. diff_amount = amount . sub ( balance_amount , expected_amount ) # Use the specified tolerance or automatically infer it. tolerance = get_balance_tolerance ( entry , options_map ) if abs ( diff_amount . number ) > tolerance : check_errors . append ( BalanceError ( entry . meta , ( \"Balance failed for ' {} ': \" \"expected {} != accumulated {} ( {} {} )\" ) . format ( entry . account , expected_amount , balance_amount , abs ( diff_amount . number ), ( \"too much\" if diff_amount . number > 0 else \"too little\" ), ), entry , ) ) # Substitute the entry by a failing entry, with the diff_amount # field set on it. I'm not entirely sure that this is the best # of ideas, maybe leaving the original check intact and insert a # new error entry might be more functional or easier to # understand. entry = entry . _replace ( meta = entry . meta . copy (), diff_amount = diff_amount ) new_entries . append ( entry ) return new_entries , check_errors beancount . ops . balance . get_balance_tolerance ( balance_entry , options_map ) \uf0c1 Get the tolerance amount for a single entry. Parameters: balance_entry \u2013 An instance of data.Balance options_map \u2013 An options dict, as per the parser. Returns: A Decimal, the amount of tolerance implied by the directive. Source code in beancount/ops/balance.py def get_balance_tolerance ( balance_entry , options_map ): \"\"\"Get the tolerance amount for a single entry. Args: balance_entry: An instance of data.Balance options_map: An options dict, as per the parser. Returns: A Decimal, the amount of tolerance implied by the directive. \"\"\" if balance_entry . tolerance is not None : # Use the balance-specific tolerance override if it is provided. tolerance = balance_entry . tolerance else : expo = balance_entry . amount . number . as_tuple () . exponent if expo < 0 : # Be generous and always allow twice the multiplier on Balance and # Pad because the user creates these and the rounding of those # balances may often be further off than those used within a single # transaction. tolerance = options_map [ \"inferred_tolerance_multiplier\" ] * 2 tolerance = ONE . scaleb ( expo ) * tolerance else : tolerance = ZERO return tolerance beancount.ops.basicops \uf0c1 Basic filtering and aggregation operations on lists of entries. This module contains some common basic operations on entries that are complex enough not to belong in core/data.py. beancount . ops . basicops . filter_link ( link , entries ) \uf0c1 Yield all the entries which have the given link. Parameters: link \u2013 A string, the link we are interested in. Yields: Every entry in 'entries' that links to 'link. Source code in beancount/ops/basicops.py def filter_link ( link , entries ): \"\"\"Yield all the entries which have the given link. Args: link: A string, the link we are interested in. Yields: Every entry in 'entries' that links to 'link. \"\"\" for entry in entries : if isinstance ( entry , data . Transaction ) and entry . links and link in entry . links : yield entry beancount . ops . basicops . filter_tag ( tag , entries ) \uf0c1 Yield all the entries which have the given tag. Parameters: tag \u2013 A string, the tag we are interested in. Yields: Every entry in 'entries' that tags to 'tag. Source code in beancount/ops/basicops.py def filter_tag ( tag , entries ): \"\"\"Yield all the entries which have the given tag. Args: tag: A string, the tag we are interested in. Yields: Every entry in 'entries' that tags to 'tag. \"\"\" for entry in entries : if isinstance ( entry , data . Transaction ) and entry . tags and tag in entry . tags : yield entry beancount . ops . basicops . get_common_accounts ( entries ) \uf0c1 Compute the intersection of the accounts on the given entries. Parameters: entries \u2013 A list of Transaction entries to process. Returns: A set of strings, the names of the common accounts from these entries. Source code in beancount/ops/basicops.py def get_common_accounts ( entries ): \"\"\"Compute the intersection of the accounts on the given entries. Args: entries: A list of Transaction entries to process. Returns: A set of strings, the names of the common accounts from these entries. \"\"\" assert all ( isinstance ( entry , data . Transaction ) for entry in entries ) # If there is a single entry, the common accounts to it is all its accounts. # Note that this also works with no entries (yields an empty set). if len ( entries ) < 2 : if entries : intersection = { posting . account for posting in entries [ 0 ] . postings } else : intersection = set () else : entries_iter = iter ( entries ) intersection = set ( posting . account for posting in next ( entries_iter ) . postings ) for entry in entries_iter : accounts = set ( posting . account for posting in entry . postings ) intersection &= accounts if not intersection : break return intersection beancount . ops . basicops . group_entries_by_link ( entries ) \uf0c1 Group the list of entries by link. Parameters: entries \u2013 A list of directives/transactions to process. Returns: A dict of link-name to list of entries. Source code in beancount/ops/basicops.py def group_entries_by_link ( entries ): \"\"\"Group the list of entries by link. Args: entries: A list of directives/transactions to process. Returns: A dict of link-name to list of entries. \"\"\" link_groups = defaultdict ( list ) for entry in entries : if not ( isinstance ( entry , data . Transaction ) and entry . links ): continue for link in entry . links : link_groups [ link ] . append ( entry ) return link_groups beancount.ops.compress \uf0c1 Compress multiple entries into a single one. This can be used during import to compress the effective output, for accounts with a large number of similar entries. For example, I had a trading account which would pay out interest every single day. I have no desire to import the full detail of these daily interests, and compressing these interest-only entries to monthly ones made sense. This is the code that was used to carry this out. beancount . ops . compress . compress ( entries , predicate ) \uf0c1 Compress multiple transactions into single transactions. Replace consecutive sequences of Transaction entries that fulfill the given predicate by a single entry at the date of the last matching entry. 'predicate' is the function that determines if an entry should be compressed. This can be used to simply a list of transactions that are similar and occur frequently. As an example, in a retail FOREX trading account, differential interest of very small amounts is paid every day; it is not relevant to look at the full detail of this interest unless there are other transactions. You can use this to compress it into single entries between other types of transactions. Parameters: entries \u2013 A list of directives. predicate \u2013 A callable which accepts an entry and return true if the entry is intended to be compressed. Returns: A list of directives, with compressible transactions replaced by a summary equivalent. Source code in beancount/ops/compress.py def compress ( entries , predicate ): \"\"\"Compress multiple transactions into single transactions. Replace consecutive sequences of Transaction entries that fulfill the given predicate by a single entry at the date of the last matching entry. 'predicate' is the function that determines if an entry should be compressed. This can be used to simply a list of transactions that are similar and occur frequently. As an example, in a retail FOREX trading account, differential interest of very small amounts is paid every day; it is not relevant to look at the full detail of this interest unless there are other transactions. You can use this to compress it into single entries between other types of transactions. Args: entries: A list of directives. predicate: A callable which accepts an entry and return true if the entry is intended to be compressed. Returns: A list of directives, with compressible transactions replaced by a summary equivalent. \"\"\" new_entries = [] pending = [] for entry in entries : if isinstance ( entry , data . Transaction ) and predicate ( entry ): # Save for compressing later. pending . append ( entry ) else : # Compress and output all the pending entries. if pending : new_entries . append ( merge ( pending , pending [ - 1 ])) pending . clear () # Output the differing entry. new_entries . append ( entry ) if pending : new_entries . append ( merge ( pending , pending [ - 1 ])) return new_entries beancount . ops . compress . merge ( entries , prototype_txn ) \uf0c1 Merge the postings of a list of Transactions into a single one. Merge postings the given entries into a single entry with the Transaction attributes of the prototype. Return the new entry. The combined list of postings are merged if everything about the postings is the same except the number. Parameters: entries \u2013 A list of directives. prototype_txn \u2013 A Transaction which is used to create the compressed Transaction instance. Its list of postings is ignored. Returns: A new Transaction instance which contains all the postings from the input entries merged together. Source code in beancount/ops/compress.py def merge ( entries , prototype_txn ): \"\"\"Merge the postings of a list of Transactions into a single one. Merge postings the given entries into a single entry with the Transaction attributes of the prototype. Return the new entry. The combined list of postings are merged if everything about the postings is the same except the number. Args: entries: A list of directives. prototype_txn: A Transaction which is used to create the compressed Transaction instance. Its list of postings is ignored. Returns: A new Transaction instance which contains all the postings from the input entries merged together. \"\"\" # Aggregate the postings together. This is a mapping of numberless postings # to their number of units. postings_map = collections . defaultdict ( Decimal ) for entry in data . filter_txns ( entries ): for posting in entry . postings : # We strip the number off the posting to act as an aggregation key. key = data . Posting ( posting . account , Amount ( None , posting . units . currency ), posting . cost , posting . price , posting . flag , None , ) postings_map [ key ] += posting . units . number # Create a new transaction with the aggregated postings. new_entry = data . Transaction ( prototype_txn . meta , prototype_txn . date , prototype_txn . flag , prototype_txn . payee , prototype_txn . narration , data . EMPTY_SET , data . EMPTY_SET , [], ) # Sort for at least some stability of output. sorted_items = sorted ( postings_map . items (), key = lambda item : ( item [ 0 ] . account , item [ 0 ] . units . currency , item [ 1 ]), ) # Issue the merged postings. for posting , number in sorted_items : units = Amount ( number , posting . units . currency ) new_entry . postings . append ( data . Posting ( posting . account , units , posting . cost , posting . price , posting . flag , posting . meta , ) ) return new_entry beancount.ops.documents \uf0c1 Everything that relates to creating the Document directives. beancount.ops.documents.DocumentError ( tuple ) \uf0c1 DocumentError(source, message, entry) beancount . ops . documents . DocumentError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/documents.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . ops . documents . DocumentError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of DocumentError(source, message, entry) beancount . ops . documents . DocumentError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/ops/documents.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . ops . documents . find_documents ( directory , input_filename , accounts_only = None , strict = False ) \uf0c1 Find dated document files under the given directory. If a restricting set of accounts is provided in 'accounts_only', only return entries that correspond to one of the given accounts. Parameters: directory \u2013 A string, the name of the root of the directory hierarchy to be searched. input_filename \u2013 The name of the file to be used for the Document directives. This is also used to resolve relative directory names. accounts_only \u2013 A set of valid accounts strings to search for. strict \u2013 A boolean, set to true if you want to generate errors on documents found in accounts not provided in accounts_only. This is only meaningful if accounts_only is specified. Returns: A list of new Document objects that were created from the files found, and a list of new errors generated. Source code in beancount/ops/documents.py def find_documents ( directory , input_filename , accounts_only = None , strict = False ): \"\"\"Find dated document files under the given directory. If a restricting set of accounts is provided in 'accounts_only', only return entries that correspond to one of the given accounts. Args: directory: A string, the name of the root of the directory hierarchy to be searched. input_filename: The name of the file to be used for the Document directives. This is also used to resolve relative directory names. accounts_only: A set of valid accounts strings to search for. strict: A boolean, set to true if you want to generate errors on documents found in accounts not provided in accounts_only. This is only meaningful if accounts_only is specified. Returns: A list of new Document objects that were created from the files found, and a list of new errors generated. \"\"\" errors = [] # Compute the documents directory name relative to the beancount input # file itself. if not path . isabs ( directory ): input_directory = path . dirname ( input_filename ) directory = path . abspath ( path . normpath ( path . join ( input_directory , directory ))) # If the directory does not exist, just generate an error and return. if not path . exists ( directory ): meta = data . new_metadata ( input_filename , 0 ) error = DocumentError ( meta , \"Document root ' {} ' does not exist\" . format ( directory ), None ) return ([], [ error ]) # Walk the hierarchy of files. entries = [] for root , account_name , dirs , files in account . walk ( directory ): # Look for files that have a dated filename. for filename in files : match = re . match ( r \"(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d).(.*)\" , filename ) if not match : continue # If a restricting set of accounts was specified, skip document # directives found in accounts with no corresponding account name. if accounts_only is not None and account_name not in accounts_only : if strict : if any ( account_name . startswith ( account ) for account in accounts_only ): errors . append ( DocumentError ( data . new_metadata ( input_filename , 0 ), \"Document ' {} ' found in child account {} \" . format ( filename , account_name ), None , ) ) elif any ( account . startswith ( account_name ) for account in accounts_only ): errors . append ( DocumentError ( data . new_metadata ( input_filename , 0 ), \"Document ' {} ' found in parent account {} \" . format ( filename , account_name ), None , ) ) continue # Create a new directive. meta = data . new_metadata ( input_filename , 0 ) try : date = datetime . date ( * map ( int , match . group ( 1 , 2 , 3 ))) except ValueError as exc : errors . append ( DocumentError ( data . new_metadata ( input_filename , 0 ), \"Invalid date on document file ' {} ': {} \" . format ( filename , exc ), None , ) ) else : entry = data . Document ( meta , date , account_name , path . join ( root , filename ), data . EMPTY_SET , data . EMPTY_SET , ) entries . append ( entry ) return ( entries , errors ) beancount . ops . documents . process_documents ( entries , options_map ) \uf0c1 Check files for document directives and create documents directives automatically. Parameters: entries \u2013 A list of all directives parsed from the file. options_map \u2013 An options dict, as is output by the parser. We're using its 'filename' option to figure out relative path to search for documents. Returns: A pair of list of all entries (including new ones), and errors generated during the process of creating document directives. Source code in beancount/ops/documents.py def process_documents ( entries , options_map ): \"\"\"Check files for document directives and create documents directives automatically. Args: entries: A list of all directives parsed from the file. options_map: An options dict, as is output by the parser. We're using its 'filename' option to figure out relative path to search for documents. Returns: A pair of list of all entries (including new ones), and errors generated during the process of creating document directives. \"\"\" filename = options_map [ \"filename\" ] # Detect filenames that should convert into entries. autodoc_entries = [] autodoc_errors = [] document_dirs = options_map [ \"documents\" ] if document_dirs : # Restrict to the list of valid accounts only. accounts = getters . get_accounts ( entries ) # Accumulate all the entries. for directory in map ( path . normpath , document_dirs ): new_entries , new_errors = find_documents ( directory , filename , accounts ) autodoc_entries . extend ( new_entries ) autodoc_errors . extend ( new_errors ) # Merge the two lists of entries and errors. Keep the entries sorted. entries . extend ( autodoc_entries ) entries . sort ( key = data . entry_sortkey ) return ( entries , autodoc_errors ) beancount . ops . documents . verify_document_files_exist ( entries , unused_options_map ) \uf0c1 Verify that the document entries point to existing files. Parameters: entries \u2013 a list of directives whose documents need to be validated. unused_options_map \u2013 A parser options dict. We're not using it. Returns: The same list of entries, and a list of new errors, if any were encountered. Source code in beancount/ops/documents.py def verify_document_files_exist ( entries , unused_options_map ): \"\"\"Verify that the document entries point to existing files. Args: entries: a list of directives whose documents need to be validated. unused_options_map: A parser options dict. We're not using it. Returns: The same list of entries, and a list of new errors, if any were encountered. \"\"\" errors = [] for entry in entries : if not isinstance ( entry , data . Document ): continue if not path . exists ( entry . filename ): errors . append ( DocumentError ( entry . meta , 'File does not exist: \" {} \"' . format ( entry . filename ), entry ) ) return entries , errors beancount.ops.find_prices \uf0c1 A library of codes create price fetching jobs from strings and files. beancount . ops . find_prices . find_balance_currencies ( entries , date = None ) \uf0c1 Return currencies relevant for the given date. This computes the account balances as of the date, and returns the union of: a) The currencies held at cost, and b) Currency pairs from previous conversions, but only for currencies with non-zero balances. This is intended to produce the list of currencies whose prices are relevant at a particular date, based on previous history. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A set of (base, quote) currencies. Source code in beancount/ops/find_prices.py def find_balance_currencies ( entries , date = None ): \"\"\"Return currencies relevant for the given date. This computes the account balances as of the date, and returns the union of: a) The currencies held at cost, and b) Currency pairs from previous conversions, but only for currencies with non-zero balances. This is intended to produce the list of currencies whose prices are relevant at a particular date, based on previous history. Args: entries: A list of directives. date: A datetime.date instance. Returns: A set of (base, quote) currencies. \"\"\" # Compute the balances. currencies = set () currencies_on_books = set () balances , _ = summarize . balance_by_account ( entries , date ) for _ , balance in balances . items (): for pos in balance : if pos . cost is not None : # Add currencies held at cost. currencies . add (( pos . units . currency , pos . cost . currency )) else : # Add regular currencies. currencies_on_books . add ( pos . units . currency ) # Create currency pairs from the currencies which are on account balances. # In order to figure out the quote currencies, we use the list of price # conversions until this date. converted = find_currencies_converted ( entries , date ) | find_currencies_priced ( entries , date ) for cbase in currencies_on_books : for base_quote in converted : base , quote = base_quote if base == cbase : currencies . add ( base_quote ) return currencies beancount . ops . find_prices . find_currencies_at_cost ( entries , date = None ) \uf0c1 Return all currencies that were held at cost at some point. This returns all of them, even if not on the books at a particular point in time. This code does not look at account balances. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote) currencies. Source code in beancount/ops/find_prices.py def find_currencies_at_cost ( entries , date = None ): \"\"\"Return all currencies that were held at cost at some point. This returns all of them, even if not on the books at a particular point in time. This code does not look at account balances. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote) currencies. \"\"\" currencies = set () for entry in entries : if not isinstance ( entry , data . Transaction ): continue if date and entry . date >= date : break for posting in entry . postings : if posting . cost is not None and posting . cost . number is not None : currencies . add (( posting . units . currency , posting . cost . currency )) return currencies beancount . ops . find_prices . find_currencies_converted ( entries , date = None ) \uf0c1 Return currencies from price conversions. This function looks at all price conversions that occurred until some date and produces a list of them. Note: This does not include Price directives, only postings with price conversions. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote) currencies. Source code in beancount/ops/find_prices.py def find_currencies_converted ( entries , date = None ): \"\"\"Return currencies from price conversions. This function looks at all price conversions that occurred until some date and produces a list of them. Note: This does not include Price directives, only postings with price conversions. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote) currencies. \"\"\" currencies = set () for entry in entries : if not isinstance ( entry , data . Transaction ): continue if date and entry . date >= date : break for posting in entry . postings : price = posting . price if posting . cost is not None or price is None : continue currencies . add (( posting . units . currency , price . currency )) return currencies beancount . ops . find_prices . find_currencies_priced ( entries , date = None ) \uf0c1 Return currencies seen in Price directives. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote) currencies. Source code in beancount/ops/find_prices.py def find_currencies_priced ( entries , date = None ): \"\"\"Return currencies seen in Price directives. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote) currencies. \"\"\" currencies = set () for entry in entries : if not isinstance ( entry , data . Price ): continue if date and entry . date >= date : break currencies . add (( entry . currency , entry . amount . currency )) return currencies beancount.ops.lifetimes \uf0c1 Given a Beancount ledger, compute time intervals where we hold each commodity. This script computes, for each commodity, which time intervals it is required at. This can then be used to identify a list of dates at which we need to fetch prices in order to properly fill the price database. beancount . ops . lifetimes . compress_intervals_days ( intervals , num_days ) \uf0c1 Compress a list of date pairs to ignore short stretches of unused days. Parameters: intervals \u2013 A list of pairs of datetime.date instances. num_days \u2013 An integer, the number of unused days to require for intervals to be distinct, to allow a gap. Returns: A new dict of lifetimes map where some intervals may have been joined. Source code in beancount/ops/lifetimes.py def compress_intervals_days ( intervals , num_days ): \"\"\"Compress a list of date pairs to ignore short stretches of unused days. Args: intervals: A list of pairs of datetime.date instances. num_days: An integer, the number of unused days to require for intervals to be distinct, to allow a gap. Returns: A new dict of lifetimes map where some intervals may have been joined. \"\"\" ignore_interval = datetime . timedelta ( days = num_days ) new_intervals = [] iter_intervals = iter ( intervals ) last_begin , last_end = next ( iter_intervals ) for date_begin , date_end in iter_intervals : if date_begin - last_end < ignore_interval : # Compress. last_end = date_end continue new_intervals . append (( last_begin , last_end )) last_begin , last_end = date_begin , date_end new_intervals . append (( last_begin , last_end )) return new_intervals beancount . ops . lifetimes . compress_lifetimes_days ( lifetimes_map , num_days ) \uf0c1 Compress a lifetimes map to ignore short stretches of unused days. Parameters: lifetimes_map \u2013 A dict of currency intervals as returned by get_commodity_lifetimes. num_days \u2013 An integer, the number of unused days to ignore. Returns: A new dict of lifetimes map where some intervals may have been joined. Source code in beancount/ops/lifetimes.py def compress_lifetimes_days ( lifetimes_map , num_days ): \"\"\"Compress a lifetimes map to ignore short stretches of unused days. Args: lifetimes_map: A dict of currency intervals as returned by get_commodity_lifetimes. num_days: An integer, the number of unused days to ignore. Returns: A new dict of lifetimes map where some intervals may have been joined. \"\"\" return { currency_pair : compress_intervals_days ( intervals , num_days ) for currency_pair , intervals in lifetimes_map . items () } beancount . ops . lifetimes . get_commodity_lifetimes ( entries ) \uf0c1 Given a list of directives, figure out the life of each commodity. Parameters: entries \u2013 A list of directives. Returns: A dict of (currency, cost-currency) commodity strings to lists of (start, end) datetime.date pairs. The dates are inclusive of the day the commodity was seen; the end/last dates are one day after the last date seen. Source code in beancount/ops/lifetimes.py def get_commodity_lifetimes ( entries ): \"\"\"Given a list of directives, figure out the life of each commodity. Args: entries: A list of directives. Returns: A dict of (currency, cost-currency) commodity strings to lists of (start, end) datetime.date pairs. The dates are inclusive of the day the commodity was seen; the end/last dates are one day _after_ the last date seen. \"\"\" lifetimes = collections . defaultdict ( list ) # The current set of active commodities. commodities = set () # The current balances across all accounts. balances = collections . defaultdict ( inventory . Inventory ) for entry in entries : # Process only transaction entries. if not isinstance ( entry , data . Transaction ): continue # Update the balance of affected accounts and check locally whether that # triggered a change in the set of commodities. commodities_changed = False for posting in entry . postings : balance = balances [ posting . account ] commodities_before = balance . currency_pairs () balance . add_position ( posting ) commodities_after = balance . currency_pairs () if commodities_after != commodities_before : commodities_changed = True # If there was a change in one of the affected account's list of # commodities, recompute the total set globally. This should not # occur very frequently. if commodities_changed : new_commodities = set ( itertools . chain ( * ( inv . currency_pairs () for inv in balances . values ())) ) if new_commodities != commodities : # The new global set of commodities has changed; update our # the dictionary of intervals. for currency in new_commodities - commodities : lifetimes [ currency ] . append (( entry . date , None )) for currency in commodities - new_commodities : lifetime = lifetimes [ currency ] begin_date , end_date = lifetime . pop ( - 1 ) assert end_date is None lifetime . append (( begin_date , entry . date + ONEDAY )) # Update our current set. commodities = new_commodities return lifetimes beancount . ops . lifetimes . required_daily_prices ( lifetimes_map , date_last , weekdays_only = False ) \uf0c1 Enumerate all the commodities and days where the price is required. Given a map of lifetimes for a set of commodities, enumerate all the days for each commodity where it is active. This can be used to connect to a historical price fetcher routine to fill in missing price entries from an existing ledger. Parameters: lifetimes_map \u2013 A dict of currency to active intervals as returned by get_commodity_lifetimes(). date_last \u2013 A datetime.date instance, the last date which we're interested in. weekdays_only \u2013 Option to limit fetching to weekdays only. Returns: Tuples of (date, currency, cost-currency). Source code in beancount/ops/lifetimes.py def required_daily_prices ( lifetimes_map , date_last , weekdays_only = False ): \"\"\"Enumerate all the commodities and days where the price is required. Given a map of lifetimes for a set of commodities, enumerate all the days for each commodity where it is active. This can be used to connect to a historical price fetcher routine to fill in missing price entries from an existing ledger. Args: lifetimes_map: A dict of currency to active intervals as returned by get_commodity_lifetimes(). date_last: A datetime.date instance, the last date which we're interested in. weekdays_only: Option to limit fetching to weekdays only. Returns: Tuples of (date, currency, cost-currency). \"\"\" results = [] for currency_pair , intervals in lifetimes_map . items (): if currency_pair [ 1 ] is None : continue for date_begin , date_end in intervals : # Find first Weekday starting on or before minimum date. date = date_begin if weekdays_only : diff_days = 4 - date_begin . weekday () if diff_days < 0 : date += datetime . timedelta ( days = diff_days ) # Iterate over all weekdays. if date_end is None : date_end = date_last while date < date_end : results . append (( date , currency_pair [ 0 ], currency_pair [ 1 ])) if weekdays_only and date . weekday () == 4 : date += 3 * ONEDAY else : date += ONEDAY return sorted ( results ) beancount . ops . lifetimes . required_weekly_prices ( lifetimes_map , date_last ) \uf0c1 Enumerate all the commodities and Fridays where the price is required. Given a map of lifetimes for a set of commodities, enumerate all the Fridays for each commodity where it is active. This can be used to connect to a historical price fetcher routine to fill in missing price entries from an existing ledger. Parameters: lifetimes_map \u2013 A dict of currency to active intervals as returned by get_commodity_lifetimes(). date_last \u2013 A datetime.date instance, the last date which we're interested in. Returns: Tuples of (date, currency, cost-currency). Source code in beancount/ops/lifetimes.py def required_weekly_prices ( lifetimes_map , date_last ): \"\"\"Enumerate all the commodities and Fridays where the price is required. Given a map of lifetimes for a set of commodities, enumerate all the Fridays for each commodity where it is active. This can be used to connect to a historical price fetcher routine to fill in missing price entries from an existing ledger. Args: lifetimes_map: A dict of currency to active intervals as returned by get_commodity_lifetimes(). date_last: A datetime.date instance, the last date which we're interested in. Returns: Tuples of (date, currency, cost-currency). \"\"\" results = [] for currency_pair , intervals in lifetimes_map . items (): if currency_pair [ 1 ] is None : continue for date_begin , date_end in intervals : # Find first Friday before the minimum date. diff_days = 4 - date_begin . weekday () if diff_days >= 1 : diff_days -= 7 date = date_begin + datetime . timedelta ( days = diff_days ) # Iterate over all Fridays. if date_end is None : date_end = date_last while date < date_end : results . append (( date , currency_pair [ 0 ], currency_pair [ 1 ])) date += ONE_WEEK return sorted ( results ) beancount . ops . lifetimes . trim_intervals ( intervals , trim_start = None , trim_end = None ) \uf0c1 Trim a list of date pairs to be within a start and end date. Useful in update-style price fetching. Parameters: intervals \u2013 A list of pairs of datetime.date instances trim_start \u2013 An inclusive starting date. trim_end \u2013 An exclusive starting date. Returns: A list of new intervals (pairs of (date, date)). Source code in beancount/ops/lifetimes.py def trim_intervals ( intervals , trim_start = None , trim_end = None ): \"\"\"Trim a list of date pairs to be within a start and end date. Useful in update-style price fetching. Args: intervals: A list of pairs of datetime.date instances trim_start: An inclusive starting date. trim_end: An exclusive starting date. Returns: A list of new intervals (pairs of (date, date)). \"\"\" new_intervals = [] iter_intervals = iter ( intervals ) if trim_start is not None and trim_end is not None and trim_end < trim_start : raise ValueError ( \"Trim end date is before start date\" ) for date_begin , date_end in iter_intervals : if trim_start is not None and trim_start > date_begin : date_begin = trim_start if trim_end is not None : if date_end is None or trim_end < date_end : date_end = trim_end if date_end is None or date_begin <= date_end : new_intervals . append (( date_begin , date_end )) return new_intervals beancount.ops.pad \uf0c1 Automatic padding of gaps between entries. beancount.ops.pad.PadError ( tuple ) \uf0c1 PadError(source, message, entry) beancount . ops . pad . PadError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/pad.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . ops . pad . PadError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of PadError(source, message, entry) beancount . ops . pad . PadError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/ops/pad.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . ops . pad . pad ( entries , options_map ) \uf0c1 Insert transaction entries for to fulfill a subsequent balance check. Synthesize and insert Transaction entries right after Pad entries in order to fulfill checks in the padded accounts. Returns a new list of entries. Note that this doesn't pad across parent-child relationships, it is a very simple kind of pad. (I have found this to be sufficient in practice, and simpler to implement and understand.) Furthermore, this pads for a single currency only, that is, balance checks are specified only for one currency at a time, and pads will only be inserted for those currencies. Parameters: entries \u2013 A list of directives. options_map \u2013 A parser options dict. Returns: A new list of directives, with Pad entries inserted, and a list of new errors produced. Source code in beancount/ops/pad.py def pad ( entries , options_map ): \"\"\"Insert transaction entries for to fulfill a subsequent balance check. Synthesize and insert Transaction entries right after Pad entries in order to fulfill checks in the padded accounts. Returns a new list of entries. Note that this doesn't pad across parent-child relationships, it is a very simple kind of pad. (I have found this to be sufficient in practice, and simpler to implement and understand.) Furthermore, this pads for a single currency only, that is, balance checks are specified only for one currency at a time, and pads will only be inserted for those currencies. Args: entries: A list of directives. options_map: A parser options dict. Returns: A new list of directives, with Pad entries inserted, and a list of new errors produced. \"\"\" pad_errors = [] # Find all the pad entries and group them by account. pads = list ( misc_utils . filter_type ( entries , data . Pad )) pad_dict = misc_utils . groupby ( lambda x : x . account , pads ) # Partially realize the postings, so we can iterate them by account. by_account = realization . postings_by_account ( entries ) # A dict of pad -> list of entries to be inserted. new_entries = { id ( pad ): [] for pad in pads } # Process each account that has a padding group. for account_ , pad_list in sorted ( pad_dict . items ()): # Last encountered / currency active pad entry. active_pad = None # Gather all the postings for the account and its children. postings = [] is_child = account . parent_matcher ( account_ ) for item_account , item_postings in by_account . items (): if is_child ( item_account ): postings . extend ( item_postings ) postings . sort ( key = data . posting_sortkey ) # A set of currencies already padded so far in this account. padded_lots = set () pad_balance = inventory . Inventory () for entry in postings : assert not isinstance ( entry , data . Posting ) if isinstance ( entry , data . TxnPosting ): # This is a transaction; update the running balance for this # account. pad_balance . add_position ( entry . posting ) elif isinstance ( entry , data . Pad ): if entry . account == account_ : # Mark this newly encountered pad as active and allow all lots # to be padded heretofore. active_pad = entry padded_lots = set () elif isinstance ( entry , data . Balance ): check_amount = entry . amount # Compare the current balance amount to the expected one from # the check entry. IMPORTANT: You need to understand that this # does not check a single position, but rather checks that the # total amount for a particular currency (which itself is # distinct from the cost). balance_amount = pad_balance . get_currency_units ( check_amount . currency ) diff_amount = amount . sub ( balance_amount , check_amount ) # Use the specified tolerance or automatically infer it. tolerance = balance . get_balance_tolerance ( entry , options_map ) if abs ( diff_amount . number ) > tolerance : # The check fails; we need to pad. # Pad only if pad entry is active and we haven't already # padded that lot since it was last encountered. if active_pad and ( check_amount . currency not in padded_lots ): # Note: we decide that it's an error to try to pad # positions at cost; we check here that all the existing # positions with that currency have no cost. positions = [ pos for pos in pad_balance . get_positions () if pos . units . currency == check_amount . currency ] for position_ in positions : if position_ . cost is not None : pad_errors . append ( PadError ( entry . meta , ( \"Attempt to pad an entry with cost for \" \"balance: {} \" . format ( pad_balance ) ), active_pad , ) ) # Thus our padding lot is without cost by default. diff_position = position . Position . from_amounts ( amount . Amount ( check_amount . number - balance_amount . number , check_amount . currency , ) ) # Synthesize a new transaction entry for the difference. narration = ( \"(Padding inserted for Balance of {} for \" \"difference {} )\" ) . format ( check_amount , diff_position ) new_entry = data . Transaction ( active_pad . meta . copy (), active_pad . date , flags . FLAG_PADDING , None , narration , data . EMPTY_SET , data . EMPTY_SET , [], ) new_entry . postings . append ( data . Posting ( active_pad . account , diff_position . units , diff_position . cost , None , None , {}, ) ) neg_diff_position = - diff_position new_entry . postings . append ( data . Posting ( active_pad . source_account , neg_diff_position . units , neg_diff_position . cost , None , None , {}, ) ) # Save it for later insertion after the active pad. new_entries [ id ( active_pad )] . append ( new_entry ) # Fixup the running balance. pos , _ = pad_balance . add_position ( diff_position ) if pos is not None and pos . is_negative_at_cost (): raise ValueError ( \"Position held at cost goes negative: {} \" . format ( pos ) ) # Mark this lot as padded. Further checks should not pad this lot. padded_lots . add ( check_amount . currency ) # Insert the newly created entries right after the pad entries that created them. padded_entries = [] for entry in entries : padded_entries . append ( entry ) if isinstance ( entry , data . Pad ): entry_list = new_entries [ id ( entry )] if entry_list : padded_entries . extend ( entry_list ) else : # Generate errors on unused pad entries. pad_errors . append ( PadError ( entry . meta , \"Unused Pad entry\" , entry )) return padded_entries , pad_errors beancount.ops.summarize \uf0c1 Summarization of entries. This code is used to summarize a sequence of entries (e.g. during a time period) into a few \"opening balance\" entries. This is when computing a balance sheet for a specific time period: we don't want to see the entries from before some period of time, so we fold them into a single transaction per account that has the sum total amount of that account. beancount . ops . summarize . balance_by_account ( entries , date = None , compress_unbooked = False ) \uf0c1 Sum up the balance per account for all entries strictly before 'date'. Parameters: entries \u2013 A list of directives. date \u2013 An optional datetime.date instance. If provided, stop accumulating on and after this date. This is useful for summarization before a specific date. compress_unbooked \u2013 For accounts that have a booking method of NONE, compress their positions into a single average position. This can be used when you export the full list of positions, because those accounts will have a myriad of small positions from fees at negative cost and what-not. Returns: A pair of a dict of account string to instance Inventory (the balance of this account before the given date), and the index in the list of entries where the date was encountered. If all entries are located before the cutoff date, an index one beyond the last entry is returned. Source code in beancount/ops/summarize.py def balance_by_account ( entries , date = None , compress_unbooked = False ): \"\"\"Sum up the balance per account for all entries strictly before 'date'. Args: entries: A list of directives. date: An optional datetime.date instance. If provided, stop accumulating on and after this date. This is useful for summarization before a specific date. compress_unbooked: For accounts that have a booking method of NONE, compress their positions into a single average position. This can be used when you export the full list of positions, because those accounts will have a myriad of small positions from fees at negative cost and what-not. Returns: A pair of a dict of account string to instance Inventory (the balance of this account before the given date), and the index in the list of entries where the date was encountered. If all entries are located before the cutoff date, an index one beyond the last entry is returned. \"\"\" balances = collections . defaultdict ( inventory . Inventory ) for index , entry in enumerate ( entries ): if date and entry . date >= date : break if isinstance ( entry , Transaction ): for posting in entry . postings : account_balance = balances [ posting . account ] # Note: We must allow negative lots at cost, because this may be # used to reduce a filtered list of entries which may not # include the entries necessary to keep units at cost always # above zero. The only summation that is guaranteed to be above # zero is if all the entries are being summed together, no # entries are filtered, at least for a particular account's # postings. account_balance . add_position ( posting ) else : index = len ( entries ) # If the account has \"NONE\" booking method, merge all its postings # together in order to obtain an accurate cost basis and balance of # units. # # (This is a complex issue.) If you accrued positions without having them # booked properly against existing cost bases, you have not properly accounted # for the profit/loss to other postings. This means that the resulting # profit/loss is merged in the cost basis of the positive and negative # postings. if compress_unbooked : oc_map = getters . get_account_open_close ( entries ) accounts_map = { account : dopen for account , ( dopen , _ ) in oc_map . items ()} for account , balance in balances . items (): dopen = accounts_map . get ( account , None ) if dopen is not None and dopen . booking is data . Booking . NONE : average_balance = balance . average () balances [ account ] = inventory . Inventory ( pos for pos in average_balance ) return balances , index beancount . ops . summarize . cap ( entries , account_types , conversion_currency , account_earnings , account_conversions ) \uf0c1 Transfer net income to equity and insert a final conversion entry. This is used to move and nullify balances from the income and expense accounts to an equity account in order to draw up a balance sheet with a balance of precisely zero. Parameters: entries \u2013 A list of directives. account_types \u2013 An instance of AccountTypes. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_earnings \u2013 A string, the name of the equity account to transfer final balances of the income and expense accounts to. account_conversions \u2013 A string, the name of the equity account to use as the source for currency conversions. Returns: A modified list of entries, with the income and expense accounts transferred. Source code in beancount/ops/summarize.py def cap ( entries , account_types , conversion_currency , account_earnings , account_conversions ): \"\"\"Transfer net income to equity and insert a final conversion entry. This is used to move and nullify balances from the income and expense accounts to an equity account in order to draw up a balance sheet with a balance of precisely zero. Args: entries: A list of directives. account_types: An instance of AccountTypes. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_earnings: A string, the name of the equity account to transfer final balances of the income and expense accounts to. account_conversions: A string, the name of the equity account to use as the source for currency conversions. Returns: A modified list of entries, with the income and expense accounts transferred. \"\"\" # Transfer the balances of income and expense accounts as earnings / net # income. income_statement_account_pred = lambda account : is_income_statement_account ( account , account_types ) entries = transfer_balances ( entries , None , income_statement_account_pred , account_earnings ) # Insert final conversion entries. entries = conversions ( entries , account_conversions , conversion_currency , None ) return entries beancount . ops . summarize . cap_opt ( entries , options_map ) \uf0c1 Close by getting all the parameters from an options map. See cap() for details. Parameters: entries \u2013 See cap(). options_map \u2013 A parser's option_map. Returns: Same as close(). Source code in beancount/ops/summarize.py def cap_opt ( entries , options_map ): \"\"\"Close by getting all the parameters from an options map. See cap() for details. Args: entries: See cap(). options_map: A parser's option_map. Returns: Same as close(). \"\"\" account_types = options . get_account_types ( options_map ) current_accounts = options . get_current_accounts ( options_map ) conversion_currency = options_map [ \"conversion_currency\" ] return cap ( entries , account_types , conversion_currency , * current_accounts ) beancount . ops . summarize . clamp ( entries , begin_date , end_date , account_types , conversion_currency , account_earnings , account_opening , account_conversions ) \uf0c1 Filter entries to include only those during a specified time period. Firstly, this method will transfer all balances for the income and expense accounts occurring before the given period begin date to the 'account_earnings' account (earnings before the period, or \"retained earnings\") and summarize all of the transactions before that date against the 'account_opening' account (usually \"opening balances\"). The resulting income and expense accounts should have no transactions (since their balances have been transferred out and summarization of zero balances should not add any transactions). Secondly, all the entries after the period end date will be truncated and a conversion entry will be added for the resulting transactions that reflect changes occurring between the beginning and end of the exercise period. The resulting balance of all account should be empty. Parameters: entries \u2013 A list of directive tuples. begin_date \u2013 A datetime.date instance, the beginning of the period. end_date \u2013 A datetime.date instance, one day beyond the end of the period. account_types \u2013 An instance of AccountTypes. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_earnings \u2013 A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening \u2013 A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions \u2013 A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. Source code in beancount/ops/summarize.py def clamp ( entries , begin_date , end_date , account_types , conversion_currency , account_earnings , account_opening , account_conversions , ): \"\"\"Filter entries to include only those during a specified time period. Firstly, this method will transfer all balances for the income and expense accounts occurring before the given period begin date to the 'account_earnings' account (earnings before the period, or \"retained earnings\") and summarize all of the transactions before that date against the 'account_opening' account (usually \"opening balances\"). The resulting income and expense accounts should have no transactions (since their balances have been transferred out and summarization of zero balances should not add any transactions). Secondly, all the entries after the period end date will be truncated and a conversion entry will be added for the resulting transactions that reflect changes occurring between the beginning and end of the exercise period. The resulting balance of all account should be empty. Args: entries: A list of directive tuples. begin_date: A datetime.date instance, the beginning of the period. end_date: A datetime.date instance, one day beyond the end of the period. account_types: An instance of AccountTypes. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_earnings: A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening: A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions: A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. \"\"\" # Transfer income and expenses before the period to equity. income_statement_account_pred = lambda account : is_income_statement_account ( account , account_types ) entries = transfer_balances ( entries , begin_date , income_statement_account_pred , account_earnings ) # Summarize all the previous balances, after transferring the income and # expense balances, so all entries for those accounts before the begin date # should now disappear. entries , index = summarize ( entries , begin_date , account_opening ) # Truncate the entries after this. entries = truncate ( entries , end_date ) # Insert conversion entries. entries = conversions ( entries , account_conversions , conversion_currency , end_date ) return entries , index beancount . ops . summarize . clamp_opt ( entries , begin_date , end_date , options_map ) \uf0c1 Clamp by getting all the parameters from an options map. See clamp() for details. Parameters: entries \u2013 See clamp(). begin_date \u2013 See clamp(). end_date \u2013 See clamp(). options_map \u2013 A parser's option_map. Returns: Same as clamp(). Source code in beancount/ops/summarize.py def clamp_opt ( entries , begin_date , end_date , options_map ): \"\"\"Clamp by getting all the parameters from an options map. See clamp() for details. Args: entries: See clamp(). begin_date: See clamp(). end_date: See clamp(). options_map: A parser's option_map. Returns: Same as clamp(). \"\"\" account_types = options . get_account_types ( options_map ) previous_earnings , previous_balances , _ = options . get_previous_accounts ( options_map ) _ , current_conversions = options . get_current_accounts ( options_map ) conversion_currency = options_map [ \"conversion_currency\" ] return clamp ( entries , begin_date , end_date , account_types , conversion_currency , previous_earnings , previous_balances , current_conversions , ) beancount . ops . summarize . clear ( entries , date , account_types , account_earnings ) \uf0c1 Transfer income and expenses balances at the given date to the equity accounts. This method insert entries to zero out balances on income and expenses accounts by transferring them to an equity account. Parameters: entries \u2013 A list of directive tuples. date \u2013 A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. account_types \u2013 An instance of AccountTypes. account_earnings \u2013 A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. Returns: A new list of entries is returned, and the index that points to one before the last original transaction before the transfers. Source code in beancount/ops/summarize.py def clear ( entries , date , account_types , account_earnings ): \"\"\"Transfer income and expenses balances at the given date to the equity accounts. This method insert entries to zero out balances on income and expenses accounts by transferring them to an equity account. Args: entries: A list of directive tuples. date: A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. account_types: An instance of AccountTypes. account_earnings: A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. Returns: A new list of entries is returned, and the index that points to one before the last original transaction before the transfers. \"\"\" index = len ( entries ) # Transfer income and expenses before the period to equity. income_statement_account_pred = lambda account : is_income_statement_account ( account , account_types ) new_entries = transfer_balances ( entries , date , income_statement_account_pred , account_earnings ) return new_entries , index beancount . ops . summarize . clear_opt ( entries , date , options_map ) \uf0c1 Convenience function to clear() using an options map. Source code in beancount/ops/summarize.py def clear_opt ( entries , date , options_map ): \"\"\"Convenience function to clear() using an options map.\"\"\" account_types = options . get_account_types ( options_map ) current_accounts = options . get_current_accounts ( options_map ) return clear ( entries , date , account_types , current_accounts [ 0 ]) beancount . ops . summarize . close ( entries , date , conversion_currency , account_conversions ) \uf0c1 Truncate entries that occur after a particular date and ensure balance. This method essentially removes entries after a date. It truncates the future. To do so, it will Remove all entries which occur after 'date', if given. Insert conversion transactions at the end of the list of entries to ensure that the total balance of all postings sums up to empty. The result is a list of entries with a total balance of zero, with possibly non-zero balances for the income/expense accounts. To produce a final balance sheet, use transfer() to move the net income to the equity accounts. Parameters: entries \u2013 A list of directive tuples. date \u2013 A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_conversions \u2013 A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to one beyond the last original transaction that was provided. Further entries may have been inserted to normalize conversions and ensure the total balance sums to zero. Source code in beancount/ops/summarize.py def close ( entries , date , conversion_currency , account_conversions ): \"\"\"Truncate entries that occur after a particular date and ensure balance. This method essentially removes entries after a date. It truncates the future. To do so, it will 1. Remove all entries which occur after 'date', if given. 2. Insert conversion transactions at the end of the list of entries to ensure that the total balance of all postings sums up to empty. The result is a list of entries with a total balance of zero, with possibly non-zero balances for the income/expense accounts. To produce a final balance sheet, use transfer() to move the net income to the equity accounts. Args: entries: A list of directive tuples. date: A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_conversions: A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to one beyond the last original transaction that was provided. Further entries may have been inserted to normalize conversions and ensure the total balance sums to zero. \"\"\" # Truncate the entries after the date, if a date has been provided. if date is not None : entries = truncate ( entries , date ) # Keep an index to the truncated list of entries (before conversions). index = len ( entries ) # Insert a conversions entry to ensure the total balance of all accounts is # flush zero. entries = conversions ( entries , account_conversions , conversion_currency , date ) return entries , index beancount . ops . summarize . close_opt ( entries , date , options_map ) \uf0c1 Convenience function to close() using an options map. Source code in beancount/ops/summarize.py def close_opt ( entries , date , options_map ): \"\"\"Convenience function to close() using an options map.\"\"\" conversion_currency = options_map [ \"conversion_currency\" ] current_accounts = options . get_current_accounts ( options_map ) return close ( entries , date , conversion_currency , current_accounts [ 1 ]) beancount . ops . summarize . conversions ( entries , conversion_account , conversion_currency , date = None ) \uf0c1 Insert a conversion entry at date 'date' at the given account. Parameters: entries \u2013 A list of entries. conversion_account \u2013 A string, the account to book against. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. date \u2013 The date before which to insert the conversion entry. The new entry will be inserted as the last entry of the date just previous to this date. Returns: A modified list of entries. Source code in beancount/ops/summarize.py def conversions ( entries , conversion_account , conversion_currency , date = None ): \"\"\"Insert a conversion entry at date 'date' at the given account. Args: entries: A list of entries. conversion_account: A string, the account to book against. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. date: The date before which to insert the conversion entry. The new entry will be inserted as the last entry of the date just previous to this date. Returns: A modified list of entries. \"\"\" # Compute the balance at the given date. conversion_balance = interpolate . compute_entries_balance ( entries , date = date ) # Early exit if there is nothing to do. conversion_cost_balance = conversion_balance . reduce ( convert . get_cost ) if conversion_cost_balance . is_empty (): return entries # Calculate the index and the date for the new entry. We want to store it as # the last transaction of the day before. if date is not None : index = bisect_key . bisect_left_with_key ( entries , date , key = lambda entry : entry . date ) last_date = date - datetime . timedelta ( days = 1 ) else : index = len ( entries ) last_date = entries [ - 1 ] . date meta = data . new_metadata ( \"\" , - 1 ) narration = \"Conversion for {} \" . format ( conversion_balance ) conversion_entry = Transaction ( meta , last_date , flags . FLAG_CONVERSIONS , None , narration , data . EMPTY_SET , data . EMPTY_SET , [], ) for position in conversion_cost_balance . get_positions (): # Important note: Set the cost to zero here to maintain the balance # invariant. (This is the only single place we cheat on the balance rule # in the entire system and this is necessary; see documentation on # Conversions.) price = amount . Amount ( ZERO , conversion_currency ) neg_pos = - position conversion_entry . postings . append ( data . Posting ( conversion_account , neg_pos . units , neg_pos . cost , price , None , None ) ) # Make a copy of the list of entries and insert the new transaction into it. new_entries = list ( entries ) new_entries . insert ( index , conversion_entry ) return new_entries beancount . ops . summarize . create_entries_from_balances ( balances , date , source_account , direction , meta , flag , narration_template ) \uf0c1 \"Create a list of entries from a dict of balances. This method creates a list of new entries to transfer the amounts in the 'balances' dict to/from another account specified in 'source_account'. The balancing posting is created with the equivalent at cost. In other words, if you attempt to balance 10 HOOL {500 USD}, this will synthesize a posting with this position on one leg, and with 5000 USD on the 'source_account' leg. Parameters: balances \u2013 A dict of account name strings to Inventory instances. date \u2013 A datetime.date object, the date at which to create the transaction. source_account \u2013 A string, the name of the account to pull the balances from. This is the magician's hat to pull the rabbit from. direction \u2013 If 'direction' is True, the new entries transfer TO the balances account from the source account; otherwise the new entries transfer FROM the balances into the source account. meta \u2013 A dict to use as metadata for the transactions. flag \u2013 A string, the flag to use for the transactions. narration_template \u2013 A format string for creating the narration. It is formatted with 'account' and 'date' replacement variables. Returns: A list of newly synthesizes Transaction entries. Source code in beancount/ops/summarize.py def create_entries_from_balances ( balances , date , source_account , direction , meta , flag , narration_template ): \"\"\" \"Create a list of entries from a dict of balances. This method creates a list of new entries to transfer the amounts in the 'balances' dict to/from another account specified in 'source_account'. The balancing posting is created with the equivalent at cost. In other words, if you attempt to balance 10 HOOL {500 USD}, this will synthesize a posting with this position on one leg, and with 5000 USD on the 'source_account' leg. Args: balances: A dict of account name strings to Inventory instances. date: A datetime.date object, the date at which to create the transaction. source_account: A string, the name of the account to pull the balances from. This is the magician's hat to pull the rabbit from. direction: If 'direction' is True, the new entries transfer TO the balances account from the source account; otherwise the new entries transfer FROM the balances into the source account. meta: A dict to use as metadata for the transactions. flag: A string, the flag to use for the transactions. narration_template: A format string for creating the narration. It is formatted with 'account' and 'date' replacement variables. Returns: A list of newly synthesizes Transaction entries. \"\"\" new_entries = [] for account , account_balance in sorted ( balances . items ()): # Don't create new entries where there is no balance. if account_balance . is_empty (): continue narration = narration_template . format ( account = account , date = date ) if not direction : account_balance = - account_balance postings = [] new_entry = Transaction ( meta , date , flag , None , narration , data . EMPTY_SET , data . EMPTY_SET , postings ) for position in account_balance . get_positions (): postings . append ( data . Posting ( account , position . units , position . cost , None , None , None ) ) cost = - convert . get_cost ( position ) postings . append ( data . Posting ( source_account , cost , None , None , None , None )) new_entries . append ( new_entry ) return new_entries beancount . ops . summarize . get_open_entries ( entries , date ) \uf0c1 Gather the list of active Open entries at date. This returns the list of Open entries that have not been closed at the given date, in the same order they were observed in the document. Parameters: entries \u2013 A list of directives. date \u2013 The date at which to look for an open entry. If not specified, will return the entries still open at the latest date. Returns: A list of Open directives. Source code in beancount/ops/summarize.py def get_open_entries ( entries , date ): \"\"\"Gather the list of active Open entries at date. This returns the list of Open entries that have not been closed at the given date, in the same order they were observed in the document. Args: entries: A list of directives. date: The date at which to look for an open entry. If not specified, will return the entries still open at the latest date. Returns: A list of Open directives. \"\"\" open_entries = {} for index , entry in enumerate ( entries ): if date is not None and entry . date >= date : break if isinstance ( entry , Open ): try : ex_index , ex_entry = open_entries [ entry . account ] if entry . date < ex_entry . date : open_entries [ entry . account ] = ( index , entry ) except KeyError : open_entries [ entry . account ] = ( index , entry ) elif isinstance ( entry , Close ): # If there is no corresponding open, don't raise an error. open_entries . pop ( entry . account , None ) return [ entry for ( index , entry ) in sorted ( open_entries . values ())] beancount . ops . summarize . open ( entries , date , account_types , conversion_currency , account_earnings , account_opening , account_conversions ) \uf0c1 Summarize entries before a date and transfer income/expenses to equity. This method essentially prepares a list of directives to contain only transactions that occur after a particular date. It truncates the past. To do so, it will Insert conversion transactions at the given open date, then Insert transactions at that date to move accumulated balances from before that date from the income and expenses accounts to an equity account, and finally It removes all the transactions previous to the date and replaces them by opening balances entries to bring the balances to the same amount. The result is a list of entries for which the income and expense accounts are beginning with a balance of zero, and all other accounts begin with a transaction that brings their balance to the expected amount. All the past has been summarized at that point. An index is returned to the first transaction past the balance opening transactions, so you can keep just those in order to render a balance sheet for only the opening balances. Parameters: entries \u2013 A list of directive tuples. date \u2013 A datetime.date instance, the date at which to do this. account_types \u2013 An instance of AccountTypes. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_earnings \u2013 A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening \u2013 A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions \u2013 A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. Source code in beancount/ops/summarize.py def open ( entries , date , account_types , conversion_currency , account_earnings , account_opening , account_conversions , ): \"\"\"Summarize entries before a date and transfer income/expenses to equity. This method essentially prepares a list of directives to contain only transactions that occur after a particular date. It truncates the past. To do so, it will 1. Insert conversion transactions at the given open date, then 2. Insert transactions at that date to move accumulated balances from before that date from the income and expenses accounts to an equity account, and finally 3. It removes all the transactions previous to the date and replaces them by opening balances entries to bring the balances to the same amount. The result is a list of entries for which the income and expense accounts are beginning with a balance of zero, and all other accounts begin with a transaction that brings their balance to the expected amount. All the past has been summarized at that point. An index is returned to the first transaction past the balance opening transactions, so you can keep just those in order to render a balance sheet for only the opening balances. Args: entries: A list of directive tuples. date: A datetime.date instance, the date at which to do this. account_types: An instance of AccountTypes. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_earnings: A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening: A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions: A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. \"\"\" # Insert conversion entries. entries = conversions ( entries , account_conversions , conversion_currency , date ) # Transfer income and expenses before the period to equity. entries , _ = clear ( entries , date , account_types , account_earnings ) # Summarize all the previous balances, after transferring the income and # expense balances, so all entries for those accounts before the begin date # should now disappear. entries , index = summarize ( entries , date , account_opening ) return entries , index beancount . ops . summarize . open_opt ( entries , date , options_map ) \uf0c1 Convenience function to open() using an options map. Source code in beancount/ops/summarize.py def open_opt ( entries , date , options_map ): \"\"\"Convenience function to open() using an options map.\"\"\" account_types = options . get_account_types ( options_map ) previous_accounts = options . get_previous_accounts ( options_map ) conversion_currency = options_map [ \"conversion_currency\" ] return open ( entries , date , account_types , conversion_currency , * previous_accounts ) beancount . ops . summarize . summarize ( entries , date , account_opening ) \uf0c1 Summarize all entries before a date by replacing then with summarization entries. This function replaces the transactions up to (and not including) the given date with a opening balance transactions, one for each account. It returns new entries, all of the transactions before the given date having been replaced by a few summarization entries, one for each account. Notes: - Open entries are preserved for active accounts. - The last relevant price entry for each (base, quote) pair is preserved. - All other entries before the cutoff date are culled. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance, the cutoff date before which to summarize. account_opening \u2013 A string, the name of the source account to book summarization entries against. Returns: The function returns a list of new entries and the integer index at which the entries on or after the cutoff date begin. Source code in beancount/ops/summarize.py def summarize ( entries , date , account_opening ): \"\"\"Summarize all entries before a date by replacing then with summarization entries. This function replaces the transactions up to (and not including) the given date with a opening balance transactions, one for each account. It returns new entries, all of the transactions before the given date having been replaced by a few summarization entries, one for each account. Notes: - Open entries are preserved for active accounts. - The last relevant price entry for each (base, quote) pair is preserved. - All other entries before the cutoff date are culled. Args: entries: A list of directives. date: A datetime.date instance, the cutoff date before which to summarize. account_opening: A string, the name of the source account to book summarization entries against. Returns: The function returns a list of new entries and the integer index at which the entries on or after the cutoff date begin. \"\"\" # Compute balances at date. balances , index = balance_by_account ( entries , date ) # We need to insert the entries with a date previous to subsequent checks, # to maintain ensure the open directives show up before any transaction. summarize_date = date - datetime . timedelta ( days = 1 ) # Create summarization / opening balance entries. summarizing_entries = create_entries_from_balances ( balances , summarize_date , account_opening , True , data . new_metadata ( \"\" , 0 ), flags . FLAG_SUMMARIZE , \"Opening balance for ' {account} ' (Summarization)\" , ) # Insert the last price entry for each commodity from before the date. price_entries = prices . get_last_price_entries ( entries , date ) # Gather the list of active open entries at date. open_entries = get_open_entries ( entries , date ) # Compute entries before the date and preserve the entries after the date. before_entries = sorted ( open_entries + price_entries + summarizing_entries , key = data . entry_sortkey ) after_entries = entries [ index :] # Return a new list of entries and the index that points after the entries # were inserted. return ( before_entries + after_entries ), len ( before_entries ) beancount . ops . summarize . transfer_balances ( entries , date , account_pred , transfer_account ) \uf0c1 Synthesize transactions to transfer balances from some accounts at a given date. For all accounts that match the 'account_pred' predicate, create new entries to transfer the balance at the given date from the account to the transfer account. This is used to transfer balances from income and expenses from a previous period to a \"retained earnings\" account. This is accomplished by creating new entries. Note that inserting transfers breaks any following balance checks that are in the transferred accounts. For this reason, all balance assertion entries following the cutoff date for those accounts are removed from the list in output. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance, the date at which to make the transfer. account_pred \u2013 A predicate function that, given an account string, returns true if the account is meant to be transferred. transfer_account \u2013 A string, the name of the source account to be used on the transfer entries to receive balances at the given date. Returns: A new list of entries, with the new transfer entries added in. Source code in beancount/ops/summarize.py def transfer_balances ( entries , date , account_pred , transfer_account ): \"\"\"Synthesize transactions to transfer balances from some accounts at a given date. For all accounts that match the 'account_pred' predicate, create new entries to transfer the balance at the given date from the account to the transfer account. This is used to transfer balances from income and expenses from a previous period to a \"retained earnings\" account. This is accomplished by creating new entries. Note that inserting transfers breaks any following balance checks that are in the transferred accounts. For this reason, all balance assertion entries following the cutoff date for those accounts are removed from the list in output. Args: entries: A list of directives. date: A datetime.date instance, the date at which to make the transfer. account_pred: A predicate function that, given an account string, returns true if the account is meant to be transferred. transfer_account: A string, the name of the source account to be used on the transfer entries to receive balances at the given date. Returns: A new list of entries, with the new transfer entries added in. \"\"\" # Don't bother doing anything if there are no entries. if not entries : return entries # Compute balances at date. balances , index = balance_by_account ( entries , date ) # Filter out to keep only the accounts we want. transfer_balances = { account : balance for account , balance in balances . items () if account_pred ( account ) } # We need to insert the entries at the end of the previous day. if date : transfer_date = date - datetime . timedelta ( days = 1 ) else : transfer_date = entries [ - 1 ] . date # Create transfer entries. transfer_entries = create_entries_from_balances ( transfer_balances , transfer_date , transfer_account , False , data . new_metadata ( \"\" , 0 ), flags . FLAG_TRANSFER , \"Transfer balance for ' {account} ' (Transfer balance)\" , ) # Remove balance assertions that occur after a transfer on an account that # has been transferred away; they would break. after_entries = [ entry for entry in entries [ index :] if not ( isinstance ( entry , data . Balance ) and entry . account in transfer_balances ) ] # Split the new entries in a new list. return entries [: index ] + transfer_entries + after_entries beancount . ops . summarize . truncate ( entries , date ) \uf0c1 Filter out all the entries at and after date. Returns a new list of entries. Parameters: entries \u2013 A sorted list of directives. date \u2013 A datetime.date instance. Returns: A truncated list of directives. Source code in beancount/ops/summarize.py def truncate ( entries , date ): \"\"\"Filter out all the entries at and after date. Returns a new list of entries. Args: entries: A sorted list of directives. date: A datetime.date instance. Returns: A truncated list of directives. \"\"\" index = bisect_key . bisect_left_with_key ( entries , date , key = lambda entry : entry . date ) return entries [: index ] beancount.ops.validation \uf0c1 Validation checks. These checks are intended to be run after all the plugins have transformed the list of entries, just before serving them or generating reports from them. The idea is to ensure a reasonable set of invariants and generate errors if those invariants are violated. They are not sanity checks--user data is subject to constraints which are hopefully detected here and which will result in errors trickled up to the user. beancount.ops.validation.ValidationError ( tuple ) \uf0c1 ValidationError(source, message, entry) beancount . ops . validation . ValidationError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/validation.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . ops . validation . ValidationError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of ValidationError(source, message, entry) beancount . ops . validation . ValidationError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/ops/validation.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . ops . validation . validate ( entries , options_map , log_timings = None , extra_validations = None ) \uf0c1 Perform all the standard checks on parsed contents. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. log_timings \u2013 An optional function to use for logging the time of individual operations. extra_validations \u2013 A list of extra validation functions to run after loading this list of entries. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate ( entries , options_map , log_timings = None , extra_validations = None ): \"\"\"Perform all the standard checks on parsed contents. Args: entries: A list of directives. unused_options_map: An options map. log_timings: An optional function to use for logging the time of individual operations. extra_validations: A list of extra validation functions to run after loading this list of entries. Returns: A list of new errors, if any were found. \"\"\" validation_tests = VALIDATIONS if extra_validations : validation_tests += extra_validations # Run various validation routines define above. errors = [] for validation_function in validation_tests : with misc_utils . log_time ( \"function: {} \" . format ( validation_function . __name__ ), log_timings , indent = 2 ): new_errors = validation_function ( entries , options_map ) errors . extend ( new_errors ) return errors beancount . ops . validation . validate_active_accounts ( entries , unused_options_map ) \uf0c1 Check that all references to accounts occurs on active accounts. We basically check that references to accounts from all directives other than Open and Close occur at dates the open-close interval of that account. This should be good for all of the directive types where we can extract an account name. Note that this is more strict a check than comparing the dates: we actually check that no references to account are made on the same day before the open directive appears for that account. This is a nice property to have, and is supported by our custom sorting routine that will sort open entries before transaction entries, given the same date. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_active_accounts ( entries , unused_options_map ): \"\"\"Check that all references to accounts occurs on active accounts. We basically check that references to accounts from all directives other than Open and Close occur at dates the open-close interval of that account. This should be good for all of the directive types where we can extract an account name. Note that this is more strict a check than comparing the dates: we actually check that no references to account are made on the same day before the open directive appears for that account. This is a nice property to have, and is supported by our custom sorting routine that will sort open entries before transaction entries, given the same date. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" error_pairs = [] active_set = set () opened_accounts = set () for entry in entries : if isinstance ( entry , data . Open ): active_set . add ( entry . account ) opened_accounts . add ( entry . account ) elif isinstance ( entry , data . Close ): active_set . discard ( entry . account ) else : for account in getters . get_entry_accounts ( entry ): if account not in active_set : # Allow document and note directives that occur after an # account is closed. if isinstance ( entry , ALLOW_AFTER_CLOSE ) and account in opened_accounts : continue # Register an error to be logged later, with an appropriate # message. error_pairs . append (( account , entry )) # Refine the error message to disambiguate between the case of an account # that has never been seen and one that was simply not active at the time. errors = [] for account , entry in error_pairs : if account in opened_accounts : message = \"Invalid reference to inactive account ' {} '\" . format ( account ) else : message = \"Invalid reference to unknown account ' {} '\" . format ( account ) errors . append ( ValidationError ( entry . meta , message , entry )) return errors beancount . ops . validation . validate_check_transaction_balances ( entries , options_map ) \uf0c1 Check again that all transaction postings balance, as users may have transformed transactions. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_check_transaction_balances ( entries , options_map ): \"\"\"Check again that all transaction postings balance, as users may have transformed transactions. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" # Note: this is a bit slow; we could limit our checks to the original # transactions by using the hash function in the loader. errors = [] for entry in entries : if isinstance ( entry , Transaction ): # IMPORTANT: This validation is _crucial_ and cannot be skipped. # This is where we actually detect and warn on unbalancing # transactions. This _must_ come after the user routines, because # unbalancing input is legal, as those types of transactions may be # \"fixed up\" by a user-plugin. In other words, we want to allow # users to input unbalancing transactions as long as the final # transactions objects that appear on the stream (after processing # the plugins) are balanced. See {9e6c14b51a59}. # # Detect complete sets of postings that have residual balance; residual = interpolate . compute_residual ( entry . postings ) tolerances = interpolate . infer_tolerances ( entry . postings , options_map ) if not residual . is_small ( tolerances ): errors . append ( ValidationError ( entry . meta , \"Transaction does not balance: {} \" . format ( residual ), entry , ) ) return errors beancount . ops . validation . validate_currency_constraints ( entries , options_map ) \uf0c1 Check the currency constraints from account open declarations. Open directives admit an optional list of currencies that specify the only types of commodities that the running inventory for this account may contain. This function checks that all postings are only made in those commodities. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_currency_constraints ( entries , options_map ): \"\"\"Check the currency constraints from account open declarations. Open directives admit an optional list of currencies that specify the only types of commodities that the running inventory for this account may contain. This function checks that all postings are only made in those commodities. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" # Get all the open entries with currency constraints. open_map = { entry . account : entry for entry in entries if isinstance ( entry , Open ) and entry . currencies } errors = [] for entry in entries : if not isinstance ( entry , Transaction ): continue for posting in entry . postings : # Look up the corresponding account's valid currencies; skip the # check if there are none specified. try : open_entry = open_map [ posting . account ] valid_currencies = open_entry . currencies if not valid_currencies : continue except KeyError : continue # Perform the check. if posting . units . currency not in valid_currencies : errors . append ( ValidationError ( entry . meta , \"Invalid currency {} for account ' {} '\" . format ( posting . units . currency , posting . account ), entry , ) ) return errors beancount . ops . validation . validate_data_types ( entries , options_map ) \uf0c1 Check that all the data types of the attributes of entries are as expected. Users are provided with a means to filter the list of entries. They're able to write code that manipulates those tuple objects without any type constraints. With discipline, this mostly works, but I know better: check, just to make sure. This routine checks all the data types and assumptions on entries. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_data_types ( entries , options_map ): \"\"\"Check that all the data types of the attributes of entries are as expected. Users are provided with a means to filter the list of entries. They're able to write code that manipulates those tuple objects without any type constraints. With discipline, this mostly works, but I know better: check, just to make sure. This routine checks all the data types and assumptions on entries. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] for entry in entries : try : data . sanity_check_types ( entry , options_map [ \"allow_deprecated_none_for_tags_and_links\" ] ) except AssertionError as exc : errors . append ( ValidationError ( entry . meta , \"Invalid data types: {} \" . format ( exc ), entry ) ) return errors beancount . ops . validation . validate_documents_paths ( entries , options_map ) \uf0c1 Check that all filenames in resolved Document entries are absolute filenames. The processing of document entries is assumed to result in absolute paths. Relative paths are resolved at the parsing stage and at point we want to make sure we don't have to do any further processing on them. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_documents_paths ( entries , options_map ): \"\"\"Check that all filenames in resolved Document entries are absolute filenames. The processing of document entries is assumed to result in absolute paths. Relative paths are resolved at the parsing stage and at point we want to make sure we don't have to do any further processing on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" return [ ValidationError ( entry . meta , \"Invalid relative path for entry\" , entry ) for entry in entries if ( isinstance ( entry , Document ) and not path . isabs ( entry . filename )) ] beancount . ops . validation . validate_duplicate_balances ( entries , unused_options_map ) \uf0c1 Check that balance entries occur only once per day. Because we do not support time, and the declaration order of entries is meant to be kept irrelevant, two balance entries with different amounts should not occur in the file. We do allow two identical balance assertions, however, because this may occur during import. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_duplicate_balances ( entries , unused_options_map ): \"\"\"Check that balance entries occur only once per day. Because we do not support time, and the declaration order of entries is meant to be kept irrelevant, two balance entries with different amounts should not occur in the file. We do allow two identical balance assertions, however, because this may occur during import. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] # Mapping of (account, currency, date) to Balance entry. balance_entries = {} for entry in entries : if not isinstance ( entry , data . Balance ): continue key = ( entry . account , entry . amount . currency , entry . date ) try : previous_entry = balance_entries [ key ] if entry . amount != previous_entry . amount : errors . append ( ValidationError ( entry . meta , \"Duplicate balance assertion with different amounts\" , entry , ) ) except KeyError : balance_entries [ key ] = entry return errors beancount . ops . validation . validate_duplicate_commodities ( entries , unused_options_map ) \uf0c1 Check that commodity entries are unique for each commodity. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_duplicate_commodities ( entries , unused_options_map ): \"\"\"Check that commodity entries are unique for each commodity. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] # Mapping of (account, currency, date) to Balance entry. commodity_entries = {} for entry in entries : if not isinstance ( entry , data . Commodity ): continue key = entry . currency try : previous_entry = commodity_entries [ key ] if previous_entry : errors . append ( ValidationError ( entry . meta , \"Duplicate commodity directives for ' {} '\" . format ( key ), entry , ) ) except KeyError : commodity_entries [ key ] = entry return errors beancount . ops . validation . validate_open_close ( entries , unused_options_map ) \uf0c1 Check constraints on open and close directives themselves. This method checks two kinds of constraints: An open or a close directive may only show up once for each account. If a duplicate is detected, an error is generated. Close directives may only appear if an open directive has been seen previously (chronologically). The date of close directives must be strictly greater than their corresponding open directive. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_open_close ( entries , unused_options_map ): \"\"\"Check constraints on open and close directives themselves. This method checks two kinds of constraints: 1. An open or a close directive may only show up once for each account. If a duplicate is detected, an error is generated. 2. Close directives may only appear if an open directive has been seen previously (chronologically). 3. The date of close directives must be strictly greater than their corresponding open directive. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] open_map = {} close_map = {} for entry in entries : if isinstance ( entry , Open ): if entry . account in open_map : errors . append ( ValidationError ( entry . meta , \"Duplicate open directive for {} \" . format ( entry . account ), entry , ) ) else : open_map [ entry . account ] = entry elif isinstance ( entry , Close ): if entry . account in close_map : errors . append ( ValidationError ( entry . meta , \"Duplicate close directive for {} \" . format ( entry . account ), entry , ) ) else : try : open_entry = open_map [ entry . account ] if entry . date < open_entry . date : errors . append ( ValidationError ( entry . meta , \"Internal error: closing date for {} \" \"appears before opening date\" . format ( entry . account ), entry , ) ) except KeyError : errors . append ( ValidationError ( entry . meta , \"Unopened account {} is being closed\" . format ( entry . account ), entry , ) ) close_map [ entry . account ] = entry return errors","title":"beancount.ops"},{"location":"api_reference/beancount.ops.html#beancountops","text":"Operations on the entries defined in the core modules. This package contains various functions which operate on lists of entries.","title":"beancount.ops"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance","text":"Automatic padding of gaps between entries.","title":"balance"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance.BalanceError","text":"BalanceError(source, message, entry)","title":"BalanceError"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance.BalanceError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/balance.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance.BalanceError.__new__","text":"Create new instance of BalanceError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance.BalanceError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/ops/balance.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance.check","text":"Process the balance assertion directives. For each Balance directive, check that their expected balance corresponds to the actual balance computed at that time and replace failing ones by new ones with a flag that indicates failure. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, parsed from the input file. Returns: A pair of a list of directives and a list of balance check errors. Source code in beancount/ops/balance.py def check ( entries , options_map ): \"\"\"Process the balance assertion directives. For each Balance directive, check that their expected balance corresponds to the actual balance computed at that time and replace failing ones by new ones with a flag that indicates failure. Args: entries: A list of directives. options_map: A dict of options, parsed from the input file. Returns: A pair of a list of directives and a list of balance check errors. \"\"\" new_entries = [] check_errors = [] # This is similar to realization, but performed in a different order, and # where we only accumulate inventories for accounts that have balance # assertions in them (this saves on time). Here we process the entries one # by one along with the balance checks. We use a temporary realization in # order to hold the incremental tree of balances, so that we can easily get # the amounts of an account's subaccounts for making checks on parent # accounts. real_root = realization . RealAccount ( \"\" ) # Figure out the set of accounts for which we need to compute a running # inventory balance. asserted_accounts = { entry . account for entry in entries if isinstance ( entry , Balance )} # Add all children accounts of an asserted account to be calculated as well, # and pre-create these accounts, and only those (we're just being tight to # make sure). asserted_match_list = [ account . parent_matcher ( account_ ) for account_ in asserted_accounts ] for account_ in getters . get_accounts ( entries ): if account_ in asserted_accounts or any ( match ( account_ ) for match in asserted_match_list ): realization . get_or_create ( real_root , account_ ) # Get the Open directives for each account. open_close_map = getters . get_account_open_close ( entries ) for entry in entries : if isinstance ( entry , Transaction ): # For each of the postings' accounts, update the balance inventory. for posting in entry . postings : real_account = realization . get ( real_root , posting . account ) # The account will have been created only if we're meant to track it. if real_account is not None : # Note: Always allow negative lots for the purpose of balancing. # This error should show up somewhere else than here. real_account . balance . add_position ( posting ) elif isinstance ( entry , Balance ): # Check that the currency of the balance check is one of the allowed # currencies for that account. expected_amount = entry . amount try : open , _ = open_close_map [ entry . account ] except KeyError : check_errors . append ( BalanceError ( entry . meta , \"Invalid reference to unknown account ' {} '\" . format ( entry . account ), entry , ) ) continue if ( expected_amount is not None and open and open . currencies and expected_amount . currency not in open . currencies ): check_errors . append ( BalanceError ( entry . meta , \"Invalid currency ' {} ' for Balance directive: \" . format ( expected_amount . currency ), entry , ) ) # Sum up the current balances for this account and its # sub-accounts. We want to support checks for parent accounts # for the total sum of their subaccounts. # # FIXME: Improve the performance further by computing the balance # for the desired currency only. This won't allow us to cache in # this way but may be faster, if we're not asserting all the # currencies. Furthermore, we could probably avoid recomputing the # balance if a subtree of positions hasn't been invalidated by a new # position added to the realization. Do this. real_account = realization . get ( real_root , entry . account ) assert real_account is not None , \"Missing {} \" . format ( entry . account ) subtree_balance = realization . compute_balance ( real_account , leaf_only = False ) # Get only the amount in the desired currency. balance_amount = subtree_balance . get_currency_units ( expected_amount . currency ) # Check if the amount is within bounds of the expected amount. diff_amount = amount . sub ( balance_amount , expected_amount ) # Use the specified tolerance or automatically infer it. tolerance = get_balance_tolerance ( entry , options_map ) if abs ( diff_amount . number ) > tolerance : check_errors . append ( BalanceError ( entry . meta , ( \"Balance failed for ' {} ': \" \"expected {} != accumulated {} ( {} {} )\" ) . format ( entry . account , expected_amount , balance_amount , abs ( diff_amount . number ), ( \"too much\" if diff_amount . number > 0 else \"too little\" ), ), entry , ) ) # Substitute the entry by a failing entry, with the diff_amount # field set on it. I'm not entirely sure that this is the best # of ideas, maybe leaving the original check intact and insert a # new error entry might be more functional or easier to # understand. entry = entry . _replace ( meta = entry . meta . copy (), diff_amount = diff_amount ) new_entries . append ( entry ) return new_entries , check_errors","title":"check()"},{"location":"api_reference/beancount.ops.html#beancount.ops.balance.get_balance_tolerance","text":"Get the tolerance amount for a single entry. Parameters: balance_entry \u2013 An instance of data.Balance options_map \u2013 An options dict, as per the parser. Returns: A Decimal, the amount of tolerance implied by the directive. Source code in beancount/ops/balance.py def get_balance_tolerance ( balance_entry , options_map ): \"\"\"Get the tolerance amount for a single entry. Args: balance_entry: An instance of data.Balance options_map: An options dict, as per the parser. Returns: A Decimal, the amount of tolerance implied by the directive. \"\"\" if balance_entry . tolerance is not None : # Use the balance-specific tolerance override if it is provided. tolerance = balance_entry . tolerance else : expo = balance_entry . amount . number . as_tuple () . exponent if expo < 0 : # Be generous and always allow twice the multiplier on Balance and # Pad because the user creates these and the rounding of those # balances may often be further off than those used within a single # transaction. tolerance = options_map [ \"inferred_tolerance_multiplier\" ] * 2 tolerance = ONE . scaleb ( expo ) * tolerance else : tolerance = ZERO return tolerance","title":"get_balance_tolerance()"},{"location":"api_reference/beancount.ops.html#beancount.ops.basicops","text":"Basic filtering and aggregation operations on lists of entries. This module contains some common basic operations on entries that are complex enough not to belong in core/data.py.","title":"basicops"},{"location":"api_reference/beancount.ops.html#beancount.ops.basicops.filter_link","text":"Yield all the entries which have the given link. Parameters: link \u2013 A string, the link we are interested in. Yields: Every entry in 'entries' that links to 'link. Source code in beancount/ops/basicops.py def filter_link ( link , entries ): \"\"\"Yield all the entries which have the given link. Args: link: A string, the link we are interested in. Yields: Every entry in 'entries' that links to 'link. \"\"\" for entry in entries : if isinstance ( entry , data . Transaction ) and entry . links and link in entry . links : yield entry","title":"filter_link()"},{"location":"api_reference/beancount.ops.html#beancount.ops.basicops.filter_tag","text":"Yield all the entries which have the given tag. Parameters: tag \u2013 A string, the tag we are interested in. Yields: Every entry in 'entries' that tags to 'tag. Source code in beancount/ops/basicops.py def filter_tag ( tag , entries ): \"\"\"Yield all the entries which have the given tag. Args: tag: A string, the tag we are interested in. Yields: Every entry in 'entries' that tags to 'tag. \"\"\" for entry in entries : if isinstance ( entry , data . Transaction ) and entry . tags and tag in entry . tags : yield entry","title":"filter_tag()"},{"location":"api_reference/beancount.ops.html#beancount.ops.basicops.get_common_accounts","text":"Compute the intersection of the accounts on the given entries. Parameters: entries \u2013 A list of Transaction entries to process. Returns: A set of strings, the names of the common accounts from these entries. Source code in beancount/ops/basicops.py def get_common_accounts ( entries ): \"\"\"Compute the intersection of the accounts on the given entries. Args: entries: A list of Transaction entries to process. Returns: A set of strings, the names of the common accounts from these entries. \"\"\" assert all ( isinstance ( entry , data . Transaction ) for entry in entries ) # If there is a single entry, the common accounts to it is all its accounts. # Note that this also works with no entries (yields an empty set). if len ( entries ) < 2 : if entries : intersection = { posting . account for posting in entries [ 0 ] . postings } else : intersection = set () else : entries_iter = iter ( entries ) intersection = set ( posting . account for posting in next ( entries_iter ) . postings ) for entry in entries_iter : accounts = set ( posting . account for posting in entry . postings ) intersection &= accounts if not intersection : break return intersection","title":"get_common_accounts()"},{"location":"api_reference/beancount.ops.html#beancount.ops.basicops.group_entries_by_link","text":"Group the list of entries by link. Parameters: entries \u2013 A list of directives/transactions to process. Returns: A dict of link-name to list of entries. Source code in beancount/ops/basicops.py def group_entries_by_link ( entries ): \"\"\"Group the list of entries by link. Args: entries: A list of directives/transactions to process. Returns: A dict of link-name to list of entries. \"\"\" link_groups = defaultdict ( list ) for entry in entries : if not ( isinstance ( entry , data . Transaction ) and entry . links ): continue for link in entry . links : link_groups [ link ] . append ( entry ) return link_groups","title":"group_entries_by_link()"},{"location":"api_reference/beancount.ops.html#beancount.ops.compress","text":"Compress multiple entries into a single one. This can be used during import to compress the effective output, for accounts with a large number of similar entries. For example, I had a trading account which would pay out interest every single day. I have no desire to import the full detail of these daily interests, and compressing these interest-only entries to monthly ones made sense. This is the code that was used to carry this out.","title":"compress"},{"location":"api_reference/beancount.ops.html#beancount.ops.compress.compress","text":"Compress multiple transactions into single transactions. Replace consecutive sequences of Transaction entries that fulfill the given predicate by a single entry at the date of the last matching entry. 'predicate' is the function that determines if an entry should be compressed. This can be used to simply a list of transactions that are similar and occur frequently. As an example, in a retail FOREX trading account, differential interest of very small amounts is paid every day; it is not relevant to look at the full detail of this interest unless there are other transactions. You can use this to compress it into single entries between other types of transactions. Parameters: entries \u2013 A list of directives. predicate \u2013 A callable which accepts an entry and return true if the entry is intended to be compressed. Returns: A list of directives, with compressible transactions replaced by a summary equivalent. Source code in beancount/ops/compress.py def compress ( entries , predicate ): \"\"\"Compress multiple transactions into single transactions. Replace consecutive sequences of Transaction entries that fulfill the given predicate by a single entry at the date of the last matching entry. 'predicate' is the function that determines if an entry should be compressed. This can be used to simply a list of transactions that are similar and occur frequently. As an example, in a retail FOREX trading account, differential interest of very small amounts is paid every day; it is not relevant to look at the full detail of this interest unless there are other transactions. You can use this to compress it into single entries between other types of transactions. Args: entries: A list of directives. predicate: A callable which accepts an entry and return true if the entry is intended to be compressed. Returns: A list of directives, with compressible transactions replaced by a summary equivalent. \"\"\" new_entries = [] pending = [] for entry in entries : if isinstance ( entry , data . Transaction ) and predicate ( entry ): # Save for compressing later. pending . append ( entry ) else : # Compress and output all the pending entries. if pending : new_entries . append ( merge ( pending , pending [ - 1 ])) pending . clear () # Output the differing entry. new_entries . append ( entry ) if pending : new_entries . append ( merge ( pending , pending [ - 1 ])) return new_entries","title":"compress()"},{"location":"api_reference/beancount.ops.html#beancount.ops.compress.merge","text":"Merge the postings of a list of Transactions into a single one. Merge postings the given entries into a single entry with the Transaction attributes of the prototype. Return the new entry. The combined list of postings are merged if everything about the postings is the same except the number. Parameters: entries \u2013 A list of directives. prototype_txn \u2013 A Transaction which is used to create the compressed Transaction instance. Its list of postings is ignored. Returns: A new Transaction instance which contains all the postings from the input entries merged together. Source code in beancount/ops/compress.py def merge ( entries , prototype_txn ): \"\"\"Merge the postings of a list of Transactions into a single one. Merge postings the given entries into a single entry with the Transaction attributes of the prototype. Return the new entry. The combined list of postings are merged if everything about the postings is the same except the number. Args: entries: A list of directives. prototype_txn: A Transaction which is used to create the compressed Transaction instance. Its list of postings is ignored. Returns: A new Transaction instance which contains all the postings from the input entries merged together. \"\"\" # Aggregate the postings together. This is a mapping of numberless postings # to their number of units. postings_map = collections . defaultdict ( Decimal ) for entry in data . filter_txns ( entries ): for posting in entry . postings : # We strip the number off the posting to act as an aggregation key. key = data . Posting ( posting . account , Amount ( None , posting . units . currency ), posting . cost , posting . price , posting . flag , None , ) postings_map [ key ] += posting . units . number # Create a new transaction with the aggregated postings. new_entry = data . Transaction ( prototype_txn . meta , prototype_txn . date , prototype_txn . flag , prototype_txn . payee , prototype_txn . narration , data . EMPTY_SET , data . EMPTY_SET , [], ) # Sort for at least some stability of output. sorted_items = sorted ( postings_map . items (), key = lambda item : ( item [ 0 ] . account , item [ 0 ] . units . currency , item [ 1 ]), ) # Issue the merged postings. for posting , number in sorted_items : units = Amount ( number , posting . units . currency ) new_entry . postings . append ( data . Posting ( posting . account , units , posting . cost , posting . price , posting . flag , posting . meta , ) ) return new_entry","title":"merge()"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents","text":"Everything that relates to creating the Document directives.","title":"documents"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.DocumentError","text":"DocumentError(source, message, entry)","title":"DocumentError"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.DocumentError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/documents.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.DocumentError.__new__","text":"Create new instance of DocumentError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.DocumentError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/ops/documents.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.find_documents","text":"Find dated document files under the given directory. If a restricting set of accounts is provided in 'accounts_only', only return entries that correspond to one of the given accounts. Parameters: directory \u2013 A string, the name of the root of the directory hierarchy to be searched. input_filename \u2013 The name of the file to be used for the Document directives. This is also used to resolve relative directory names. accounts_only \u2013 A set of valid accounts strings to search for. strict \u2013 A boolean, set to true if you want to generate errors on documents found in accounts not provided in accounts_only. This is only meaningful if accounts_only is specified. Returns: A list of new Document objects that were created from the files found, and a list of new errors generated. Source code in beancount/ops/documents.py def find_documents ( directory , input_filename , accounts_only = None , strict = False ): \"\"\"Find dated document files under the given directory. If a restricting set of accounts is provided in 'accounts_only', only return entries that correspond to one of the given accounts. Args: directory: A string, the name of the root of the directory hierarchy to be searched. input_filename: The name of the file to be used for the Document directives. This is also used to resolve relative directory names. accounts_only: A set of valid accounts strings to search for. strict: A boolean, set to true if you want to generate errors on documents found in accounts not provided in accounts_only. This is only meaningful if accounts_only is specified. Returns: A list of new Document objects that were created from the files found, and a list of new errors generated. \"\"\" errors = [] # Compute the documents directory name relative to the beancount input # file itself. if not path . isabs ( directory ): input_directory = path . dirname ( input_filename ) directory = path . abspath ( path . normpath ( path . join ( input_directory , directory ))) # If the directory does not exist, just generate an error and return. if not path . exists ( directory ): meta = data . new_metadata ( input_filename , 0 ) error = DocumentError ( meta , \"Document root ' {} ' does not exist\" . format ( directory ), None ) return ([], [ error ]) # Walk the hierarchy of files. entries = [] for root , account_name , dirs , files in account . walk ( directory ): # Look for files that have a dated filename. for filename in files : match = re . match ( r \"(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d).(.*)\" , filename ) if not match : continue # If a restricting set of accounts was specified, skip document # directives found in accounts with no corresponding account name. if accounts_only is not None and account_name not in accounts_only : if strict : if any ( account_name . startswith ( account ) for account in accounts_only ): errors . append ( DocumentError ( data . new_metadata ( input_filename , 0 ), \"Document ' {} ' found in child account {} \" . format ( filename , account_name ), None , ) ) elif any ( account . startswith ( account_name ) for account in accounts_only ): errors . append ( DocumentError ( data . new_metadata ( input_filename , 0 ), \"Document ' {} ' found in parent account {} \" . format ( filename , account_name ), None , ) ) continue # Create a new directive. meta = data . new_metadata ( input_filename , 0 ) try : date = datetime . date ( * map ( int , match . group ( 1 , 2 , 3 ))) except ValueError as exc : errors . append ( DocumentError ( data . new_metadata ( input_filename , 0 ), \"Invalid date on document file ' {} ': {} \" . format ( filename , exc ), None , ) ) else : entry = data . Document ( meta , date , account_name , path . join ( root , filename ), data . EMPTY_SET , data . EMPTY_SET , ) entries . append ( entry ) return ( entries , errors )","title":"find_documents()"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.process_documents","text":"Check files for document directives and create documents directives automatically. Parameters: entries \u2013 A list of all directives parsed from the file. options_map \u2013 An options dict, as is output by the parser. We're using its 'filename' option to figure out relative path to search for documents. Returns: A pair of list of all entries (including new ones), and errors generated during the process of creating document directives. Source code in beancount/ops/documents.py def process_documents ( entries , options_map ): \"\"\"Check files for document directives and create documents directives automatically. Args: entries: A list of all directives parsed from the file. options_map: An options dict, as is output by the parser. We're using its 'filename' option to figure out relative path to search for documents. Returns: A pair of list of all entries (including new ones), and errors generated during the process of creating document directives. \"\"\" filename = options_map [ \"filename\" ] # Detect filenames that should convert into entries. autodoc_entries = [] autodoc_errors = [] document_dirs = options_map [ \"documents\" ] if document_dirs : # Restrict to the list of valid accounts only. accounts = getters . get_accounts ( entries ) # Accumulate all the entries. for directory in map ( path . normpath , document_dirs ): new_entries , new_errors = find_documents ( directory , filename , accounts ) autodoc_entries . extend ( new_entries ) autodoc_errors . extend ( new_errors ) # Merge the two lists of entries and errors. Keep the entries sorted. entries . extend ( autodoc_entries ) entries . sort ( key = data . entry_sortkey ) return ( entries , autodoc_errors )","title":"process_documents()"},{"location":"api_reference/beancount.ops.html#beancount.ops.documents.verify_document_files_exist","text":"Verify that the document entries point to existing files. Parameters: entries \u2013 a list of directives whose documents need to be validated. unused_options_map \u2013 A parser options dict. We're not using it. Returns: The same list of entries, and a list of new errors, if any were encountered. Source code in beancount/ops/documents.py def verify_document_files_exist ( entries , unused_options_map ): \"\"\"Verify that the document entries point to existing files. Args: entries: a list of directives whose documents need to be validated. unused_options_map: A parser options dict. We're not using it. Returns: The same list of entries, and a list of new errors, if any were encountered. \"\"\" errors = [] for entry in entries : if not isinstance ( entry , data . Document ): continue if not path . exists ( entry . filename ): errors . append ( DocumentError ( entry . meta , 'File does not exist: \" {} \"' . format ( entry . filename ), entry ) ) return entries , errors","title":"verify_document_files_exist()"},{"location":"api_reference/beancount.ops.html#beancount.ops.find_prices","text":"A library of codes create price fetching jobs from strings and files.","title":"find_prices"},{"location":"api_reference/beancount.ops.html#beancount.ops.find_prices.find_balance_currencies","text":"Return currencies relevant for the given date. This computes the account balances as of the date, and returns the union of: a) The currencies held at cost, and b) Currency pairs from previous conversions, but only for currencies with non-zero balances. This is intended to produce the list of currencies whose prices are relevant at a particular date, based on previous history. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A set of (base, quote) currencies. Source code in beancount/ops/find_prices.py def find_balance_currencies ( entries , date = None ): \"\"\"Return currencies relevant for the given date. This computes the account balances as of the date, and returns the union of: a) The currencies held at cost, and b) Currency pairs from previous conversions, but only for currencies with non-zero balances. This is intended to produce the list of currencies whose prices are relevant at a particular date, based on previous history. Args: entries: A list of directives. date: A datetime.date instance. Returns: A set of (base, quote) currencies. \"\"\" # Compute the balances. currencies = set () currencies_on_books = set () balances , _ = summarize . balance_by_account ( entries , date ) for _ , balance in balances . items (): for pos in balance : if pos . cost is not None : # Add currencies held at cost. currencies . add (( pos . units . currency , pos . cost . currency )) else : # Add regular currencies. currencies_on_books . add ( pos . units . currency ) # Create currency pairs from the currencies which are on account balances. # In order to figure out the quote currencies, we use the list of price # conversions until this date. converted = find_currencies_converted ( entries , date ) | find_currencies_priced ( entries , date ) for cbase in currencies_on_books : for base_quote in converted : base , quote = base_quote if base == cbase : currencies . add ( base_quote ) return currencies","title":"find_balance_currencies()"},{"location":"api_reference/beancount.ops.html#beancount.ops.find_prices.find_currencies_at_cost","text":"Return all currencies that were held at cost at some point. This returns all of them, even if not on the books at a particular point in time. This code does not look at account balances. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote) currencies. Source code in beancount/ops/find_prices.py def find_currencies_at_cost ( entries , date = None ): \"\"\"Return all currencies that were held at cost at some point. This returns all of them, even if not on the books at a particular point in time. This code does not look at account balances. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote) currencies. \"\"\" currencies = set () for entry in entries : if not isinstance ( entry , data . Transaction ): continue if date and entry . date >= date : break for posting in entry . postings : if posting . cost is not None and posting . cost . number is not None : currencies . add (( posting . units . currency , posting . cost . currency )) return currencies","title":"find_currencies_at_cost()"},{"location":"api_reference/beancount.ops.html#beancount.ops.find_prices.find_currencies_converted","text":"Return currencies from price conversions. This function looks at all price conversions that occurred until some date and produces a list of them. Note: This does not include Price directives, only postings with price conversions. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote) currencies. Source code in beancount/ops/find_prices.py def find_currencies_converted ( entries , date = None ): \"\"\"Return currencies from price conversions. This function looks at all price conversions that occurred until some date and produces a list of them. Note: This does not include Price directives, only postings with price conversions. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote) currencies. \"\"\" currencies = set () for entry in entries : if not isinstance ( entry , data . Transaction ): continue if date and entry . date >= date : break for posting in entry . postings : price = posting . price if posting . cost is not None or price is None : continue currencies . add (( posting . units . currency , price . currency )) return currencies","title":"find_currencies_converted()"},{"location":"api_reference/beancount.ops.html#beancount.ops.find_prices.find_currencies_priced","text":"Return currencies seen in Price directives. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance. Returns: A list of (base, quote) currencies. Source code in beancount/ops/find_prices.py def find_currencies_priced ( entries , date = None ): \"\"\"Return currencies seen in Price directives. Args: entries: A list of directives. date: A datetime.date instance. Returns: A list of (base, quote) currencies. \"\"\" currencies = set () for entry in entries : if not isinstance ( entry , data . Price ): continue if date and entry . date >= date : break currencies . add (( entry . currency , entry . amount . currency )) return currencies","title":"find_currencies_priced()"},{"location":"api_reference/beancount.ops.html#beancount.ops.lifetimes","text":"Given a Beancount ledger, compute time intervals where we hold each commodity. This script computes, for each commodity, which time intervals it is required at. This can then be used to identify a list of dates at which we need to fetch prices in order to properly fill the price database.","title":"lifetimes"},{"location":"api_reference/beancount.ops.html#beancount.ops.lifetimes.compress_intervals_days","text":"Compress a list of date pairs to ignore short stretches of unused days. Parameters: intervals \u2013 A list of pairs of datetime.date instances. num_days \u2013 An integer, the number of unused days to require for intervals to be distinct, to allow a gap. Returns: A new dict of lifetimes map where some intervals may have been joined. Source code in beancount/ops/lifetimes.py def compress_intervals_days ( intervals , num_days ): \"\"\"Compress a list of date pairs to ignore short stretches of unused days. Args: intervals: A list of pairs of datetime.date instances. num_days: An integer, the number of unused days to require for intervals to be distinct, to allow a gap. Returns: A new dict of lifetimes map where some intervals may have been joined. \"\"\" ignore_interval = datetime . timedelta ( days = num_days ) new_intervals = [] iter_intervals = iter ( intervals ) last_begin , last_end = next ( iter_intervals ) for date_begin , date_end in iter_intervals : if date_begin - last_end < ignore_interval : # Compress. last_end = date_end continue new_intervals . append (( last_begin , last_end )) last_begin , last_end = date_begin , date_end new_intervals . append (( last_begin , last_end )) return new_intervals","title":"compress_intervals_days()"},{"location":"api_reference/beancount.ops.html#beancount.ops.lifetimes.compress_lifetimes_days","text":"Compress a lifetimes map to ignore short stretches of unused days. Parameters: lifetimes_map \u2013 A dict of currency intervals as returned by get_commodity_lifetimes. num_days \u2013 An integer, the number of unused days to ignore. Returns: A new dict of lifetimes map where some intervals may have been joined. Source code in beancount/ops/lifetimes.py def compress_lifetimes_days ( lifetimes_map , num_days ): \"\"\"Compress a lifetimes map to ignore short stretches of unused days. Args: lifetimes_map: A dict of currency intervals as returned by get_commodity_lifetimes. num_days: An integer, the number of unused days to ignore. Returns: A new dict of lifetimes map where some intervals may have been joined. \"\"\" return { currency_pair : compress_intervals_days ( intervals , num_days ) for currency_pair , intervals in lifetimes_map . items () }","title":"compress_lifetimes_days()"},{"location":"api_reference/beancount.ops.html#beancount.ops.lifetimes.get_commodity_lifetimes","text":"Given a list of directives, figure out the life of each commodity. Parameters: entries \u2013 A list of directives. Returns: A dict of (currency, cost-currency) commodity strings to lists of (start, end) datetime.date pairs. The dates are inclusive of the day the commodity was seen; the end/last dates are one day after the last date seen. Source code in beancount/ops/lifetimes.py def get_commodity_lifetimes ( entries ): \"\"\"Given a list of directives, figure out the life of each commodity. Args: entries: A list of directives. Returns: A dict of (currency, cost-currency) commodity strings to lists of (start, end) datetime.date pairs. The dates are inclusive of the day the commodity was seen; the end/last dates are one day _after_ the last date seen. \"\"\" lifetimes = collections . defaultdict ( list ) # The current set of active commodities. commodities = set () # The current balances across all accounts. balances = collections . defaultdict ( inventory . Inventory ) for entry in entries : # Process only transaction entries. if not isinstance ( entry , data . Transaction ): continue # Update the balance of affected accounts and check locally whether that # triggered a change in the set of commodities. commodities_changed = False for posting in entry . postings : balance = balances [ posting . account ] commodities_before = balance . currency_pairs () balance . add_position ( posting ) commodities_after = balance . currency_pairs () if commodities_after != commodities_before : commodities_changed = True # If there was a change in one of the affected account's list of # commodities, recompute the total set globally. This should not # occur very frequently. if commodities_changed : new_commodities = set ( itertools . chain ( * ( inv . currency_pairs () for inv in balances . values ())) ) if new_commodities != commodities : # The new global set of commodities has changed; update our # the dictionary of intervals. for currency in new_commodities - commodities : lifetimes [ currency ] . append (( entry . date , None )) for currency in commodities - new_commodities : lifetime = lifetimes [ currency ] begin_date , end_date = lifetime . pop ( - 1 ) assert end_date is None lifetime . append (( begin_date , entry . date + ONEDAY )) # Update our current set. commodities = new_commodities return lifetimes","title":"get_commodity_lifetimes()"},{"location":"api_reference/beancount.ops.html#beancount.ops.lifetimes.required_daily_prices","text":"Enumerate all the commodities and days where the price is required. Given a map of lifetimes for a set of commodities, enumerate all the days for each commodity where it is active. This can be used to connect to a historical price fetcher routine to fill in missing price entries from an existing ledger. Parameters: lifetimes_map \u2013 A dict of currency to active intervals as returned by get_commodity_lifetimes(). date_last \u2013 A datetime.date instance, the last date which we're interested in. weekdays_only \u2013 Option to limit fetching to weekdays only. Returns: Tuples of (date, currency, cost-currency). Source code in beancount/ops/lifetimes.py def required_daily_prices ( lifetimes_map , date_last , weekdays_only = False ): \"\"\"Enumerate all the commodities and days where the price is required. Given a map of lifetimes for a set of commodities, enumerate all the days for each commodity where it is active. This can be used to connect to a historical price fetcher routine to fill in missing price entries from an existing ledger. Args: lifetimes_map: A dict of currency to active intervals as returned by get_commodity_lifetimes(). date_last: A datetime.date instance, the last date which we're interested in. weekdays_only: Option to limit fetching to weekdays only. Returns: Tuples of (date, currency, cost-currency). \"\"\" results = [] for currency_pair , intervals in lifetimes_map . items (): if currency_pair [ 1 ] is None : continue for date_begin , date_end in intervals : # Find first Weekday starting on or before minimum date. date = date_begin if weekdays_only : diff_days = 4 - date_begin . weekday () if diff_days < 0 : date += datetime . timedelta ( days = diff_days ) # Iterate over all weekdays. if date_end is None : date_end = date_last while date < date_end : results . append (( date , currency_pair [ 0 ], currency_pair [ 1 ])) if weekdays_only and date . weekday () == 4 : date += 3 * ONEDAY else : date += ONEDAY return sorted ( results )","title":"required_daily_prices()"},{"location":"api_reference/beancount.ops.html#beancount.ops.lifetimes.required_weekly_prices","text":"Enumerate all the commodities and Fridays where the price is required. Given a map of lifetimes for a set of commodities, enumerate all the Fridays for each commodity where it is active. This can be used to connect to a historical price fetcher routine to fill in missing price entries from an existing ledger. Parameters: lifetimes_map \u2013 A dict of currency to active intervals as returned by get_commodity_lifetimes(). date_last \u2013 A datetime.date instance, the last date which we're interested in. Returns: Tuples of (date, currency, cost-currency). Source code in beancount/ops/lifetimes.py def required_weekly_prices ( lifetimes_map , date_last ): \"\"\"Enumerate all the commodities and Fridays where the price is required. Given a map of lifetimes for a set of commodities, enumerate all the Fridays for each commodity where it is active. This can be used to connect to a historical price fetcher routine to fill in missing price entries from an existing ledger. Args: lifetimes_map: A dict of currency to active intervals as returned by get_commodity_lifetimes(). date_last: A datetime.date instance, the last date which we're interested in. Returns: Tuples of (date, currency, cost-currency). \"\"\" results = [] for currency_pair , intervals in lifetimes_map . items (): if currency_pair [ 1 ] is None : continue for date_begin , date_end in intervals : # Find first Friday before the minimum date. diff_days = 4 - date_begin . weekday () if diff_days >= 1 : diff_days -= 7 date = date_begin + datetime . timedelta ( days = diff_days ) # Iterate over all Fridays. if date_end is None : date_end = date_last while date < date_end : results . append (( date , currency_pair [ 0 ], currency_pair [ 1 ])) date += ONE_WEEK return sorted ( results )","title":"required_weekly_prices()"},{"location":"api_reference/beancount.ops.html#beancount.ops.lifetimes.trim_intervals","text":"Trim a list of date pairs to be within a start and end date. Useful in update-style price fetching. Parameters: intervals \u2013 A list of pairs of datetime.date instances trim_start \u2013 An inclusive starting date. trim_end \u2013 An exclusive starting date. Returns: A list of new intervals (pairs of (date, date)). Source code in beancount/ops/lifetimes.py def trim_intervals ( intervals , trim_start = None , trim_end = None ): \"\"\"Trim a list of date pairs to be within a start and end date. Useful in update-style price fetching. Args: intervals: A list of pairs of datetime.date instances trim_start: An inclusive starting date. trim_end: An exclusive starting date. Returns: A list of new intervals (pairs of (date, date)). \"\"\" new_intervals = [] iter_intervals = iter ( intervals ) if trim_start is not None and trim_end is not None and trim_end < trim_start : raise ValueError ( \"Trim end date is before start date\" ) for date_begin , date_end in iter_intervals : if trim_start is not None and trim_start > date_begin : date_begin = trim_start if trim_end is not None : if date_end is None or trim_end < date_end : date_end = trim_end if date_end is None or date_begin <= date_end : new_intervals . append (( date_begin , date_end )) return new_intervals","title":"trim_intervals()"},{"location":"api_reference/beancount.ops.html#beancount.ops.pad","text":"Automatic padding of gaps between entries.","title":"pad"},{"location":"api_reference/beancount.ops.html#beancount.ops.pad.PadError","text":"PadError(source, message, entry)","title":"PadError"},{"location":"api_reference/beancount.ops.html#beancount.ops.pad.PadError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/pad.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.pad.PadError.__new__","text":"Create new instance of PadError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.pad.PadError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/ops/pad.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.pad.pad","text":"Insert transaction entries for to fulfill a subsequent balance check. Synthesize and insert Transaction entries right after Pad entries in order to fulfill checks in the padded accounts. Returns a new list of entries. Note that this doesn't pad across parent-child relationships, it is a very simple kind of pad. (I have found this to be sufficient in practice, and simpler to implement and understand.) Furthermore, this pads for a single currency only, that is, balance checks are specified only for one currency at a time, and pads will only be inserted for those currencies. Parameters: entries \u2013 A list of directives. options_map \u2013 A parser options dict. Returns: A new list of directives, with Pad entries inserted, and a list of new errors produced. Source code in beancount/ops/pad.py def pad ( entries , options_map ): \"\"\"Insert transaction entries for to fulfill a subsequent balance check. Synthesize and insert Transaction entries right after Pad entries in order to fulfill checks in the padded accounts. Returns a new list of entries. Note that this doesn't pad across parent-child relationships, it is a very simple kind of pad. (I have found this to be sufficient in practice, and simpler to implement and understand.) Furthermore, this pads for a single currency only, that is, balance checks are specified only for one currency at a time, and pads will only be inserted for those currencies. Args: entries: A list of directives. options_map: A parser options dict. Returns: A new list of directives, with Pad entries inserted, and a list of new errors produced. \"\"\" pad_errors = [] # Find all the pad entries and group them by account. pads = list ( misc_utils . filter_type ( entries , data . Pad )) pad_dict = misc_utils . groupby ( lambda x : x . account , pads ) # Partially realize the postings, so we can iterate them by account. by_account = realization . postings_by_account ( entries ) # A dict of pad -> list of entries to be inserted. new_entries = { id ( pad ): [] for pad in pads } # Process each account that has a padding group. for account_ , pad_list in sorted ( pad_dict . items ()): # Last encountered / currency active pad entry. active_pad = None # Gather all the postings for the account and its children. postings = [] is_child = account . parent_matcher ( account_ ) for item_account , item_postings in by_account . items (): if is_child ( item_account ): postings . extend ( item_postings ) postings . sort ( key = data . posting_sortkey ) # A set of currencies already padded so far in this account. padded_lots = set () pad_balance = inventory . Inventory () for entry in postings : assert not isinstance ( entry , data . Posting ) if isinstance ( entry , data . TxnPosting ): # This is a transaction; update the running balance for this # account. pad_balance . add_position ( entry . posting ) elif isinstance ( entry , data . Pad ): if entry . account == account_ : # Mark this newly encountered pad as active and allow all lots # to be padded heretofore. active_pad = entry padded_lots = set () elif isinstance ( entry , data . Balance ): check_amount = entry . amount # Compare the current balance amount to the expected one from # the check entry. IMPORTANT: You need to understand that this # does not check a single position, but rather checks that the # total amount for a particular currency (which itself is # distinct from the cost). balance_amount = pad_balance . get_currency_units ( check_amount . currency ) diff_amount = amount . sub ( balance_amount , check_amount ) # Use the specified tolerance or automatically infer it. tolerance = balance . get_balance_tolerance ( entry , options_map ) if abs ( diff_amount . number ) > tolerance : # The check fails; we need to pad. # Pad only if pad entry is active and we haven't already # padded that lot since it was last encountered. if active_pad and ( check_amount . currency not in padded_lots ): # Note: we decide that it's an error to try to pad # positions at cost; we check here that all the existing # positions with that currency have no cost. positions = [ pos for pos in pad_balance . get_positions () if pos . units . currency == check_amount . currency ] for position_ in positions : if position_ . cost is not None : pad_errors . append ( PadError ( entry . meta , ( \"Attempt to pad an entry with cost for \" \"balance: {} \" . format ( pad_balance ) ), active_pad , ) ) # Thus our padding lot is without cost by default. diff_position = position . Position . from_amounts ( amount . Amount ( check_amount . number - balance_amount . number , check_amount . currency , ) ) # Synthesize a new transaction entry for the difference. narration = ( \"(Padding inserted for Balance of {} for \" \"difference {} )\" ) . format ( check_amount , diff_position ) new_entry = data . Transaction ( active_pad . meta . copy (), active_pad . date , flags . FLAG_PADDING , None , narration , data . EMPTY_SET , data . EMPTY_SET , [], ) new_entry . postings . append ( data . Posting ( active_pad . account , diff_position . units , diff_position . cost , None , None , {}, ) ) neg_diff_position = - diff_position new_entry . postings . append ( data . Posting ( active_pad . source_account , neg_diff_position . units , neg_diff_position . cost , None , None , {}, ) ) # Save it for later insertion after the active pad. new_entries [ id ( active_pad )] . append ( new_entry ) # Fixup the running balance. pos , _ = pad_balance . add_position ( diff_position ) if pos is not None and pos . is_negative_at_cost (): raise ValueError ( \"Position held at cost goes negative: {} \" . format ( pos ) ) # Mark this lot as padded. Further checks should not pad this lot. padded_lots . add ( check_amount . currency ) # Insert the newly created entries right after the pad entries that created them. padded_entries = [] for entry in entries : padded_entries . append ( entry ) if isinstance ( entry , data . Pad ): entry_list = new_entries [ id ( entry )] if entry_list : padded_entries . extend ( entry_list ) else : # Generate errors on unused pad entries. pad_errors . append ( PadError ( entry . meta , \"Unused Pad entry\" , entry )) return padded_entries , pad_errors","title":"pad()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize","text":"Summarization of entries. This code is used to summarize a sequence of entries (e.g. during a time period) into a few \"opening balance\" entries. This is when computing a balance sheet for a specific time period: we don't want to see the entries from before some period of time, so we fold them into a single transaction per account that has the sum total amount of that account.","title":"summarize"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.balance_by_account","text":"Sum up the balance per account for all entries strictly before 'date'. Parameters: entries \u2013 A list of directives. date \u2013 An optional datetime.date instance. If provided, stop accumulating on and after this date. This is useful for summarization before a specific date. compress_unbooked \u2013 For accounts that have a booking method of NONE, compress their positions into a single average position. This can be used when you export the full list of positions, because those accounts will have a myriad of small positions from fees at negative cost and what-not. Returns: A pair of a dict of account string to instance Inventory (the balance of this account before the given date), and the index in the list of entries where the date was encountered. If all entries are located before the cutoff date, an index one beyond the last entry is returned. Source code in beancount/ops/summarize.py def balance_by_account ( entries , date = None , compress_unbooked = False ): \"\"\"Sum up the balance per account for all entries strictly before 'date'. Args: entries: A list of directives. date: An optional datetime.date instance. If provided, stop accumulating on and after this date. This is useful for summarization before a specific date. compress_unbooked: For accounts that have a booking method of NONE, compress their positions into a single average position. This can be used when you export the full list of positions, because those accounts will have a myriad of small positions from fees at negative cost and what-not. Returns: A pair of a dict of account string to instance Inventory (the balance of this account before the given date), and the index in the list of entries where the date was encountered. If all entries are located before the cutoff date, an index one beyond the last entry is returned. \"\"\" balances = collections . defaultdict ( inventory . Inventory ) for index , entry in enumerate ( entries ): if date and entry . date >= date : break if isinstance ( entry , Transaction ): for posting in entry . postings : account_balance = balances [ posting . account ] # Note: We must allow negative lots at cost, because this may be # used to reduce a filtered list of entries which may not # include the entries necessary to keep units at cost always # above zero. The only summation that is guaranteed to be above # zero is if all the entries are being summed together, no # entries are filtered, at least for a particular account's # postings. account_balance . add_position ( posting ) else : index = len ( entries ) # If the account has \"NONE\" booking method, merge all its postings # together in order to obtain an accurate cost basis and balance of # units. # # (This is a complex issue.) If you accrued positions without having them # booked properly against existing cost bases, you have not properly accounted # for the profit/loss to other postings. This means that the resulting # profit/loss is merged in the cost basis of the positive and negative # postings. if compress_unbooked : oc_map = getters . get_account_open_close ( entries ) accounts_map = { account : dopen for account , ( dopen , _ ) in oc_map . items ()} for account , balance in balances . items (): dopen = accounts_map . get ( account , None ) if dopen is not None and dopen . booking is data . Booking . NONE : average_balance = balance . average () balances [ account ] = inventory . Inventory ( pos for pos in average_balance ) return balances , index","title":"balance_by_account()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.cap","text":"Transfer net income to equity and insert a final conversion entry. This is used to move and nullify balances from the income and expense accounts to an equity account in order to draw up a balance sheet with a balance of precisely zero. Parameters: entries \u2013 A list of directives. account_types \u2013 An instance of AccountTypes. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_earnings \u2013 A string, the name of the equity account to transfer final balances of the income and expense accounts to. account_conversions \u2013 A string, the name of the equity account to use as the source for currency conversions. Returns: A modified list of entries, with the income and expense accounts transferred. Source code in beancount/ops/summarize.py def cap ( entries , account_types , conversion_currency , account_earnings , account_conversions ): \"\"\"Transfer net income to equity and insert a final conversion entry. This is used to move and nullify balances from the income and expense accounts to an equity account in order to draw up a balance sheet with a balance of precisely zero. Args: entries: A list of directives. account_types: An instance of AccountTypes. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_earnings: A string, the name of the equity account to transfer final balances of the income and expense accounts to. account_conversions: A string, the name of the equity account to use as the source for currency conversions. Returns: A modified list of entries, with the income and expense accounts transferred. \"\"\" # Transfer the balances of income and expense accounts as earnings / net # income. income_statement_account_pred = lambda account : is_income_statement_account ( account , account_types ) entries = transfer_balances ( entries , None , income_statement_account_pred , account_earnings ) # Insert final conversion entries. entries = conversions ( entries , account_conversions , conversion_currency , None ) return entries","title":"cap()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.cap_opt","text":"Close by getting all the parameters from an options map. See cap() for details. Parameters: entries \u2013 See cap(). options_map \u2013 A parser's option_map. Returns: Same as close(). Source code in beancount/ops/summarize.py def cap_opt ( entries , options_map ): \"\"\"Close by getting all the parameters from an options map. See cap() for details. Args: entries: See cap(). options_map: A parser's option_map. Returns: Same as close(). \"\"\" account_types = options . get_account_types ( options_map ) current_accounts = options . get_current_accounts ( options_map ) conversion_currency = options_map [ \"conversion_currency\" ] return cap ( entries , account_types , conversion_currency , * current_accounts )","title":"cap_opt()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.clamp","text":"Filter entries to include only those during a specified time period. Firstly, this method will transfer all balances for the income and expense accounts occurring before the given period begin date to the 'account_earnings' account (earnings before the period, or \"retained earnings\") and summarize all of the transactions before that date against the 'account_opening' account (usually \"opening balances\"). The resulting income and expense accounts should have no transactions (since their balances have been transferred out and summarization of zero balances should not add any transactions). Secondly, all the entries after the period end date will be truncated and a conversion entry will be added for the resulting transactions that reflect changes occurring between the beginning and end of the exercise period. The resulting balance of all account should be empty. Parameters: entries \u2013 A list of directive tuples. begin_date \u2013 A datetime.date instance, the beginning of the period. end_date \u2013 A datetime.date instance, one day beyond the end of the period. account_types \u2013 An instance of AccountTypes. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_earnings \u2013 A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening \u2013 A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions \u2013 A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. Source code in beancount/ops/summarize.py def clamp ( entries , begin_date , end_date , account_types , conversion_currency , account_earnings , account_opening , account_conversions , ): \"\"\"Filter entries to include only those during a specified time period. Firstly, this method will transfer all balances for the income and expense accounts occurring before the given period begin date to the 'account_earnings' account (earnings before the period, or \"retained earnings\") and summarize all of the transactions before that date against the 'account_opening' account (usually \"opening balances\"). The resulting income and expense accounts should have no transactions (since their balances have been transferred out and summarization of zero balances should not add any transactions). Secondly, all the entries after the period end date will be truncated and a conversion entry will be added for the resulting transactions that reflect changes occurring between the beginning and end of the exercise period. The resulting balance of all account should be empty. Args: entries: A list of directive tuples. begin_date: A datetime.date instance, the beginning of the period. end_date: A datetime.date instance, one day beyond the end of the period. account_types: An instance of AccountTypes. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_earnings: A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening: A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions: A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. \"\"\" # Transfer income and expenses before the period to equity. income_statement_account_pred = lambda account : is_income_statement_account ( account , account_types ) entries = transfer_balances ( entries , begin_date , income_statement_account_pred , account_earnings ) # Summarize all the previous balances, after transferring the income and # expense balances, so all entries for those accounts before the begin date # should now disappear. entries , index = summarize ( entries , begin_date , account_opening ) # Truncate the entries after this. entries = truncate ( entries , end_date ) # Insert conversion entries. entries = conversions ( entries , account_conversions , conversion_currency , end_date ) return entries , index","title":"clamp()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.clamp_opt","text":"Clamp by getting all the parameters from an options map. See clamp() for details. Parameters: entries \u2013 See clamp(). begin_date \u2013 See clamp(). end_date \u2013 See clamp(). options_map \u2013 A parser's option_map. Returns: Same as clamp(). Source code in beancount/ops/summarize.py def clamp_opt ( entries , begin_date , end_date , options_map ): \"\"\"Clamp by getting all the parameters from an options map. See clamp() for details. Args: entries: See clamp(). begin_date: See clamp(). end_date: See clamp(). options_map: A parser's option_map. Returns: Same as clamp(). \"\"\" account_types = options . get_account_types ( options_map ) previous_earnings , previous_balances , _ = options . get_previous_accounts ( options_map ) _ , current_conversions = options . get_current_accounts ( options_map ) conversion_currency = options_map [ \"conversion_currency\" ] return clamp ( entries , begin_date , end_date , account_types , conversion_currency , previous_earnings , previous_balances , current_conversions , )","title":"clamp_opt()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.clear","text":"Transfer income and expenses balances at the given date to the equity accounts. This method insert entries to zero out balances on income and expenses accounts by transferring them to an equity account. Parameters: entries \u2013 A list of directive tuples. date \u2013 A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. account_types \u2013 An instance of AccountTypes. account_earnings \u2013 A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. Returns: A new list of entries is returned, and the index that points to one before the last original transaction before the transfers. Source code in beancount/ops/summarize.py def clear ( entries , date , account_types , account_earnings ): \"\"\"Transfer income and expenses balances at the given date to the equity accounts. This method insert entries to zero out balances on income and expenses accounts by transferring them to an equity account. Args: entries: A list of directive tuples. date: A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. account_types: An instance of AccountTypes. account_earnings: A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. Returns: A new list of entries is returned, and the index that points to one before the last original transaction before the transfers. \"\"\" index = len ( entries ) # Transfer income and expenses before the period to equity. income_statement_account_pred = lambda account : is_income_statement_account ( account , account_types ) new_entries = transfer_balances ( entries , date , income_statement_account_pred , account_earnings ) return new_entries , index","title":"clear()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.clear_opt","text":"Convenience function to clear() using an options map. Source code in beancount/ops/summarize.py def clear_opt ( entries , date , options_map ): \"\"\"Convenience function to clear() using an options map.\"\"\" account_types = options . get_account_types ( options_map ) current_accounts = options . get_current_accounts ( options_map ) return clear ( entries , date , account_types , current_accounts [ 0 ])","title":"clear_opt()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.close","text":"Truncate entries that occur after a particular date and ensure balance. This method essentially removes entries after a date. It truncates the future. To do so, it will Remove all entries which occur after 'date', if given. Insert conversion transactions at the end of the list of entries to ensure that the total balance of all postings sums up to empty. The result is a list of entries with a total balance of zero, with possibly non-zero balances for the income/expense accounts. To produce a final balance sheet, use transfer() to move the net income to the equity accounts. Parameters: entries \u2013 A list of directive tuples. date \u2013 A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_conversions \u2013 A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to one beyond the last original transaction that was provided. Further entries may have been inserted to normalize conversions and ensure the total balance sums to zero. Source code in beancount/ops/summarize.py def close ( entries , date , conversion_currency , account_conversions ): \"\"\"Truncate entries that occur after a particular date and ensure balance. This method essentially removes entries after a date. It truncates the future. To do so, it will 1. Remove all entries which occur after 'date', if given. 2. Insert conversion transactions at the end of the list of entries to ensure that the total balance of all postings sums up to empty. The result is a list of entries with a total balance of zero, with possibly non-zero balances for the income/expense accounts. To produce a final balance sheet, use transfer() to move the net income to the equity accounts. Args: entries: A list of directive tuples. date: A datetime.date instance, one day beyond the end of the period. This date can be optionally left to None in order to close at the end of the list of entries. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_conversions: A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to one beyond the last original transaction that was provided. Further entries may have been inserted to normalize conversions and ensure the total balance sums to zero. \"\"\" # Truncate the entries after the date, if a date has been provided. if date is not None : entries = truncate ( entries , date ) # Keep an index to the truncated list of entries (before conversions). index = len ( entries ) # Insert a conversions entry to ensure the total balance of all accounts is # flush zero. entries = conversions ( entries , account_conversions , conversion_currency , date ) return entries , index","title":"close()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.close_opt","text":"Convenience function to close() using an options map. Source code in beancount/ops/summarize.py def close_opt ( entries , date , options_map ): \"\"\"Convenience function to close() using an options map.\"\"\" conversion_currency = options_map [ \"conversion_currency\" ] current_accounts = options . get_current_accounts ( options_map ) return close ( entries , date , conversion_currency , current_accounts [ 1 ])","title":"close_opt()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.conversions","text":"Insert a conversion entry at date 'date' at the given account. Parameters: entries \u2013 A list of entries. conversion_account \u2013 A string, the account to book against. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. date \u2013 The date before which to insert the conversion entry. The new entry will be inserted as the last entry of the date just previous to this date. Returns: A modified list of entries. Source code in beancount/ops/summarize.py def conversions ( entries , conversion_account , conversion_currency , date = None ): \"\"\"Insert a conversion entry at date 'date' at the given account. Args: entries: A list of entries. conversion_account: A string, the account to book against. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. date: The date before which to insert the conversion entry. The new entry will be inserted as the last entry of the date just previous to this date. Returns: A modified list of entries. \"\"\" # Compute the balance at the given date. conversion_balance = interpolate . compute_entries_balance ( entries , date = date ) # Early exit if there is nothing to do. conversion_cost_balance = conversion_balance . reduce ( convert . get_cost ) if conversion_cost_balance . is_empty (): return entries # Calculate the index and the date for the new entry. We want to store it as # the last transaction of the day before. if date is not None : index = bisect_key . bisect_left_with_key ( entries , date , key = lambda entry : entry . date ) last_date = date - datetime . timedelta ( days = 1 ) else : index = len ( entries ) last_date = entries [ - 1 ] . date meta = data . new_metadata ( \"\" , - 1 ) narration = \"Conversion for {} \" . format ( conversion_balance ) conversion_entry = Transaction ( meta , last_date , flags . FLAG_CONVERSIONS , None , narration , data . EMPTY_SET , data . EMPTY_SET , [], ) for position in conversion_cost_balance . get_positions (): # Important note: Set the cost to zero here to maintain the balance # invariant. (This is the only single place we cheat on the balance rule # in the entire system and this is necessary; see documentation on # Conversions.) price = amount . Amount ( ZERO , conversion_currency ) neg_pos = - position conversion_entry . postings . append ( data . Posting ( conversion_account , neg_pos . units , neg_pos . cost , price , None , None ) ) # Make a copy of the list of entries and insert the new transaction into it. new_entries = list ( entries ) new_entries . insert ( index , conversion_entry ) return new_entries","title":"conversions()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.create_entries_from_balances","text":"\"Create a list of entries from a dict of balances. This method creates a list of new entries to transfer the amounts in the 'balances' dict to/from another account specified in 'source_account'. The balancing posting is created with the equivalent at cost. In other words, if you attempt to balance 10 HOOL {500 USD}, this will synthesize a posting with this position on one leg, and with 5000 USD on the 'source_account' leg. Parameters: balances \u2013 A dict of account name strings to Inventory instances. date \u2013 A datetime.date object, the date at which to create the transaction. source_account \u2013 A string, the name of the account to pull the balances from. This is the magician's hat to pull the rabbit from. direction \u2013 If 'direction' is True, the new entries transfer TO the balances account from the source account; otherwise the new entries transfer FROM the balances into the source account. meta \u2013 A dict to use as metadata for the transactions. flag \u2013 A string, the flag to use for the transactions. narration_template \u2013 A format string for creating the narration. It is formatted with 'account' and 'date' replacement variables. Returns: A list of newly synthesizes Transaction entries. Source code in beancount/ops/summarize.py def create_entries_from_balances ( balances , date , source_account , direction , meta , flag , narration_template ): \"\"\" \"Create a list of entries from a dict of balances. This method creates a list of new entries to transfer the amounts in the 'balances' dict to/from another account specified in 'source_account'. The balancing posting is created with the equivalent at cost. In other words, if you attempt to balance 10 HOOL {500 USD}, this will synthesize a posting with this position on one leg, and with 5000 USD on the 'source_account' leg. Args: balances: A dict of account name strings to Inventory instances. date: A datetime.date object, the date at which to create the transaction. source_account: A string, the name of the account to pull the balances from. This is the magician's hat to pull the rabbit from. direction: If 'direction' is True, the new entries transfer TO the balances account from the source account; otherwise the new entries transfer FROM the balances into the source account. meta: A dict to use as metadata for the transactions. flag: A string, the flag to use for the transactions. narration_template: A format string for creating the narration. It is formatted with 'account' and 'date' replacement variables. Returns: A list of newly synthesizes Transaction entries. \"\"\" new_entries = [] for account , account_balance in sorted ( balances . items ()): # Don't create new entries where there is no balance. if account_balance . is_empty (): continue narration = narration_template . format ( account = account , date = date ) if not direction : account_balance = - account_balance postings = [] new_entry = Transaction ( meta , date , flag , None , narration , data . EMPTY_SET , data . EMPTY_SET , postings ) for position in account_balance . get_positions (): postings . append ( data . Posting ( account , position . units , position . cost , None , None , None ) ) cost = - convert . get_cost ( position ) postings . append ( data . Posting ( source_account , cost , None , None , None , None )) new_entries . append ( new_entry ) return new_entries","title":"create_entries_from_balances()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.get_open_entries","text":"Gather the list of active Open entries at date. This returns the list of Open entries that have not been closed at the given date, in the same order they were observed in the document. Parameters: entries \u2013 A list of directives. date \u2013 The date at which to look for an open entry. If not specified, will return the entries still open at the latest date. Returns: A list of Open directives. Source code in beancount/ops/summarize.py def get_open_entries ( entries , date ): \"\"\"Gather the list of active Open entries at date. This returns the list of Open entries that have not been closed at the given date, in the same order they were observed in the document. Args: entries: A list of directives. date: The date at which to look for an open entry. If not specified, will return the entries still open at the latest date. Returns: A list of Open directives. \"\"\" open_entries = {} for index , entry in enumerate ( entries ): if date is not None and entry . date >= date : break if isinstance ( entry , Open ): try : ex_index , ex_entry = open_entries [ entry . account ] if entry . date < ex_entry . date : open_entries [ entry . account ] = ( index , entry ) except KeyError : open_entries [ entry . account ] = ( index , entry ) elif isinstance ( entry , Close ): # If there is no corresponding open, don't raise an error. open_entries . pop ( entry . account , None ) return [ entry for ( index , entry ) in sorted ( open_entries . values ())]","title":"get_open_entries()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.open","text":"Summarize entries before a date and transfer income/expenses to equity. This method essentially prepares a list of directives to contain only transactions that occur after a particular date. It truncates the past. To do so, it will Insert conversion transactions at the given open date, then Insert transactions at that date to move accumulated balances from before that date from the income and expenses accounts to an equity account, and finally It removes all the transactions previous to the date and replaces them by opening balances entries to bring the balances to the same amount. The result is a list of entries for which the income and expense accounts are beginning with a balance of zero, and all other accounts begin with a transaction that brings their balance to the expected amount. All the past has been summarized at that point. An index is returned to the first transaction past the balance opening transactions, so you can keep just those in order to render a balance sheet for only the opening balances. Parameters: entries \u2013 A list of directive tuples. date \u2013 A datetime.date instance, the date at which to do this. account_types \u2013 An instance of AccountTypes. conversion_currency \u2013 A string, the transfer currency to use for zero prices on the conversion entry. account_earnings \u2013 A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening \u2013 A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions \u2013 A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. Source code in beancount/ops/summarize.py def open ( entries , date , account_types , conversion_currency , account_earnings , account_opening , account_conversions , ): \"\"\"Summarize entries before a date and transfer income/expenses to equity. This method essentially prepares a list of directives to contain only transactions that occur after a particular date. It truncates the past. To do so, it will 1. Insert conversion transactions at the given open date, then 2. Insert transactions at that date to move accumulated balances from before that date from the income and expenses accounts to an equity account, and finally 3. It removes all the transactions previous to the date and replaces them by opening balances entries to bring the balances to the same amount. The result is a list of entries for which the income and expense accounts are beginning with a balance of zero, and all other accounts begin with a transaction that brings their balance to the expected amount. All the past has been summarized at that point. An index is returned to the first transaction past the balance opening transactions, so you can keep just those in order to render a balance sheet for only the opening balances. Args: entries: A list of directive tuples. date: A datetime.date instance, the date at which to do this. account_types: An instance of AccountTypes. conversion_currency: A string, the transfer currency to use for zero prices on the conversion entry. account_earnings: A string, the name of the account to transfer previous earnings from the income statement accounts to the balance sheet. account_opening: A string, the name of the account in equity to transfer previous balances from, in order to initialize account balances at the beginning of the period. This is typically called an opening balances account. account_conversions: A string, the name of the equity account to book currency conversions against. Returns: A new list of entries is returned, and the index that points to the first original transaction after the beginning date of the period. This index can be used to generate the opening balances report, which is a balance sheet fed with only the summarized entries. \"\"\" # Insert conversion entries. entries = conversions ( entries , account_conversions , conversion_currency , date ) # Transfer income and expenses before the period to equity. entries , _ = clear ( entries , date , account_types , account_earnings ) # Summarize all the previous balances, after transferring the income and # expense balances, so all entries for those accounts before the begin date # should now disappear. entries , index = summarize ( entries , date , account_opening ) return entries , index","title":"open()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.open_opt","text":"Convenience function to open() using an options map. Source code in beancount/ops/summarize.py def open_opt ( entries , date , options_map ): \"\"\"Convenience function to open() using an options map.\"\"\" account_types = options . get_account_types ( options_map ) previous_accounts = options . get_previous_accounts ( options_map ) conversion_currency = options_map [ \"conversion_currency\" ] return open ( entries , date , account_types , conversion_currency , * previous_accounts )","title":"open_opt()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.summarize","text":"Summarize all entries before a date by replacing then with summarization entries. This function replaces the transactions up to (and not including) the given date with a opening balance transactions, one for each account. It returns new entries, all of the transactions before the given date having been replaced by a few summarization entries, one for each account. Notes: - Open entries are preserved for active accounts. - The last relevant price entry for each (base, quote) pair is preserved. - All other entries before the cutoff date are culled. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance, the cutoff date before which to summarize. account_opening \u2013 A string, the name of the source account to book summarization entries against. Returns: The function returns a list of new entries and the integer index at which the entries on or after the cutoff date begin. Source code in beancount/ops/summarize.py def summarize ( entries , date , account_opening ): \"\"\"Summarize all entries before a date by replacing then with summarization entries. This function replaces the transactions up to (and not including) the given date with a opening balance transactions, one for each account. It returns new entries, all of the transactions before the given date having been replaced by a few summarization entries, one for each account. Notes: - Open entries are preserved for active accounts. - The last relevant price entry for each (base, quote) pair is preserved. - All other entries before the cutoff date are culled. Args: entries: A list of directives. date: A datetime.date instance, the cutoff date before which to summarize. account_opening: A string, the name of the source account to book summarization entries against. Returns: The function returns a list of new entries and the integer index at which the entries on or after the cutoff date begin. \"\"\" # Compute balances at date. balances , index = balance_by_account ( entries , date ) # We need to insert the entries with a date previous to subsequent checks, # to maintain ensure the open directives show up before any transaction. summarize_date = date - datetime . timedelta ( days = 1 ) # Create summarization / opening balance entries. summarizing_entries = create_entries_from_balances ( balances , summarize_date , account_opening , True , data . new_metadata ( \"\" , 0 ), flags . FLAG_SUMMARIZE , \"Opening balance for ' {account} ' (Summarization)\" , ) # Insert the last price entry for each commodity from before the date. price_entries = prices . get_last_price_entries ( entries , date ) # Gather the list of active open entries at date. open_entries = get_open_entries ( entries , date ) # Compute entries before the date and preserve the entries after the date. before_entries = sorted ( open_entries + price_entries + summarizing_entries , key = data . entry_sortkey ) after_entries = entries [ index :] # Return a new list of entries and the index that points after the entries # were inserted. return ( before_entries + after_entries ), len ( before_entries )","title":"summarize()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.transfer_balances","text":"Synthesize transactions to transfer balances from some accounts at a given date. For all accounts that match the 'account_pred' predicate, create new entries to transfer the balance at the given date from the account to the transfer account. This is used to transfer balances from income and expenses from a previous period to a \"retained earnings\" account. This is accomplished by creating new entries. Note that inserting transfers breaks any following balance checks that are in the transferred accounts. For this reason, all balance assertion entries following the cutoff date for those accounts are removed from the list in output. Parameters: entries \u2013 A list of directives. date \u2013 A datetime.date instance, the date at which to make the transfer. account_pred \u2013 A predicate function that, given an account string, returns true if the account is meant to be transferred. transfer_account \u2013 A string, the name of the source account to be used on the transfer entries to receive balances at the given date. Returns: A new list of entries, with the new transfer entries added in. Source code in beancount/ops/summarize.py def transfer_balances ( entries , date , account_pred , transfer_account ): \"\"\"Synthesize transactions to transfer balances from some accounts at a given date. For all accounts that match the 'account_pred' predicate, create new entries to transfer the balance at the given date from the account to the transfer account. This is used to transfer balances from income and expenses from a previous period to a \"retained earnings\" account. This is accomplished by creating new entries. Note that inserting transfers breaks any following balance checks that are in the transferred accounts. For this reason, all balance assertion entries following the cutoff date for those accounts are removed from the list in output. Args: entries: A list of directives. date: A datetime.date instance, the date at which to make the transfer. account_pred: A predicate function that, given an account string, returns true if the account is meant to be transferred. transfer_account: A string, the name of the source account to be used on the transfer entries to receive balances at the given date. Returns: A new list of entries, with the new transfer entries added in. \"\"\" # Don't bother doing anything if there are no entries. if not entries : return entries # Compute balances at date. balances , index = balance_by_account ( entries , date ) # Filter out to keep only the accounts we want. transfer_balances = { account : balance for account , balance in balances . items () if account_pred ( account ) } # We need to insert the entries at the end of the previous day. if date : transfer_date = date - datetime . timedelta ( days = 1 ) else : transfer_date = entries [ - 1 ] . date # Create transfer entries. transfer_entries = create_entries_from_balances ( transfer_balances , transfer_date , transfer_account , False , data . new_metadata ( \"\" , 0 ), flags . FLAG_TRANSFER , \"Transfer balance for ' {account} ' (Transfer balance)\" , ) # Remove balance assertions that occur after a transfer on an account that # has been transferred away; they would break. after_entries = [ entry for entry in entries [ index :] if not ( isinstance ( entry , data . Balance ) and entry . account in transfer_balances ) ] # Split the new entries in a new list. return entries [: index ] + transfer_entries + after_entries","title":"transfer_balances()"},{"location":"api_reference/beancount.ops.html#beancount.ops.summarize.truncate","text":"Filter out all the entries at and after date. Returns a new list of entries. Parameters: entries \u2013 A sorted list of directives. date \u2013 A datetime.date instance. Returns: A truncated list of directives. Source code in beancount/ops/summarize.py def truncate ( entries , date ): \"\"\"Filter out all the entries at and after date. Returns a new list of entries. Args: entries: A sorted list of directives. date: A datetime.date instance. Returns: A truncated list of directives. \"\"\" index = bisect_key . bisect_left_with_key ( entries , date , key = lambda entry : entry . date ) return entries [: index ]","title":"truncate()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation","text":"Validation checks. These checks are intended to be run after all the plugins have transformed the list of entries, just before serving them or generating reports from them. The idea is to ensure a reasonable set of invariants and generate errors if those invariants are violated. They are not sanity checks--user data is subject to constraints which are hopefully detected here and which will result in errors trickled up to the user.","title":"validation"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.ValidationError","text":"ValidationError(source, message, entry)","title":"ValidationError"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.ValidationError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/ops/validation.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.ValidationError.__new__","text":"Create new instance of ValidationError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.ValidationError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/ops/validation.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate","text":"Perform all the standard checks on parsed contents. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. log_timings \u2013 An optional function to use for logging the time of individual operations. extra_validations \u2013 A list of extra validation functions to run after loading this list of entries. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate ( entries , options_map , log_timings = None , extra_validations = None ): \"\"\"Perform all the standard checks on parsed contents. Args: entries: A list of directives. unused_options_map: An options map. log_timings: An optional function to use for logging the time of individual operations. extra_validations: A list of extra validation functions to run after loading this list of entries. Returns: A list of new errors, if any were found. \"\"\" validation_tests = VALIDATIONS if extra_validations : validation_tests += extra_validations # Run various validation routines define above. errors = [] for validation_function in validation_tests : with misc_utils . log_time ( \"function: {} \" . format ( validation_function . __name__ ), log_timings , indent = 2 ): new_errors = validation_function ( entries , options_map ) errors . extend ( new_errors ) return errors","title":"validate()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_active_accounts","text":"Check that all references to accounts occurs on active accounts. We basically check that references to accounts from all directives other than Open and Close occur at dates the open-close interval of that account. This should be good for all of the directive types where we can extract an account name. Note that this is more strict a check than comparing the dates: we actually check that no references to account are made on the same day before the open directive appears for that account. This is a nice property to have, and is supported by our custom sorting routine that will sort open entries before transaction entries, given the same date. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_active_accounts ( entries , unused_options_map ): \"\"\"Check that all references to accounts occurs on active accounts. We basically check that references to accounts from all directives other than Open and Close occur at dates the open-close interval of that account. This should be good for all of the directive types where we can extract an account name. Note that this is more strict a check than comparing the dates: we actually check that no references to account are made on the same day before the open directive appears for that account. This is a nice property to have, and is supported by our custom sorting routine that will sort open entries before transaction entries, given the same date. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" error_pairs = [] active_set = set () opened_accounts = set () for entry in entries : if isinstance ( entry , data . Open ): active_set . add ( entry . account ) opened_accounts . add ( entry . account ) elif isinstance ( entry , data . Close ): active_set . discard ( entry . account ) else : for account in getters . get_entry_accounts ( entry ): if account not in active_set : # Allow document and note directives that occur after an # account is closed. if isinstance ( entry , ALLOW_AFTER_CLOSE ) and account in opened_accounts : continue # Register an error to be logged later, with an appropriate # message. error_pairs . append (( account , entry )) # Refine the error message to disambiguate between the case of an account # that has never been seen and one that was simply not active at the time. errors = [] for account , entry in error_pairs : if account in opened_accounts : message = \"Invalid reference to inactive account ' {} '\" . format ( account ) else : message = \"Invalid reference to unknown account ' {} '\" . format ( account ) errors . append ( ValidationError ( entry . meta , message , entry )) return errors","title":"validate_active_accounts()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_check_transaction_balances","text":"Check again that all transaction postings balance, as users may have transformed transactions. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_check_transaction_balances ( entries , options_map ): \"\"\"Check again that all transaction postings balance, as users may have transformed transactions. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" # Note: this is a bit slow; we could limit our checks to the original # transactions by using the hash function in the loader. errors = [] for entry in entries : if isinstance ( entry , Transaction ): # IMPORTANT: This validation is _crucial_ and cannot be skipped. # This is where we actually detect and warn on unbalancing # transactions. This _must_ come after the user routines, because # unbalancing input is legal, as those types of transactions may be # \"fixed up\" by a user-plugin. In other words, we want to allow # users to input unbalancing transactions as long as the final # transactions objects that appear on the stream (after processing # the plugins) are balanced. See {9e6c14b51a59}. # # Detect complete sets of postings that have residual balance; residual = interpolate . compute_residual ( entry . postings ) tolerances = interpolate . infer_tolerances ( entry . postings , options_map ) if not residual . is_small ( tolerances ): errors . append ( ValidationError ( entry . meta , \"Transaction does not balance: {} \" . format ( residual ), entry , ) ) return errors","title":"validate_check_transaction_balances()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_currency_constraints","text":"Check the currency constraints from account open declarations. Open directives admit an optional list of currencies that specify the only types of commodities that the running inventory for this account may contain. This function checks that all postings are only made in those commodities. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_currency_constraints ( entries , options_map ): \"\"\"Check the currency constraints from account open declarations. Open directives admit an optional list of currencies that specify the only types of commodities that the running inventory for this account may contain. This function checks that all postings are only made in those commodities. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" # Get all the open entries with currency constraints. open_map = { entry . account : entry for entry in entries if isinstance ( entry , Open ) and entry . currencies } errors = [] for entry in entries : if not isinstance ( entry , Transaction ): continue for posting in entry . postings : # Look up the corresponding account's valid currencies; skip the # check if there are none specified. try : open_entry = open_map [ posting . account ] valid_currencies = open_entry . currencies if not valid_currencies : continue except KeyError : continue # Perform the check. if posting . units . currency not in valid_currencies : errors . append ( ValidationError ( entry . meta , \"Invalid currency {} for account ' {} '\" . format ( posting . units . currency , posting . account ), entry , ) ) return errors","title":"validate_currency_constraints()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_data_types","text":"Check that all the data types of the attributes of entries are as expected. Users are provided with a means to filter the list of entries. They're able to write code that manipulates those tuple objects without any type constraints. With discipline, this mostly works, but I know better: check, just to make sure. This routine checks all the data types and assumptions on entries. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_data_types ( entries , options_map ): \"\"\"Check that all the data types of the attributes of entries are as expected. Users are provided with a means to filter the list of entries. They're able to write code that manipulates those tuple objects without any type constraints. With discipline, this mostly works, but I know better: check, just to make sure. This routine checks all the data types and assumptions on entries. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] for entry in entries : try : data . sanity_check_types ( entry , options_map [ \"allow_deprecated_none_for_tags_and_links\" ] ) except AssertionError as exc : errors . append ( ValidationError ( entry . meta , \"Invalid data types: {} \" . format ( exc ), entry ) ) return errors","title":"validate_data_types()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_documents_paths","text":"Check that all filenames in resolved Document entries are absolute filenames. The processing of document entries is assumed to result in absolute paths. Relative paths are resolved at the parsing stage and at point we want to make sure we don't have to do any further processing on them. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_documents_paths ( entries , options_map ): \"\"\"Check that all filenames in resolved Document entries are absolute filenames. The processing of document entries is assumed to result in absolute paths. Relative paths are resolved at the parsing stage and at point we want to make sure we don't have to do any further processing on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" return [ ValidationError ( entry . meta , \"Invalid relative path for entry\" , entry ) for entry in entries if ( isinstance ( entry , Document ) and not path . isabs ( entry . filename )) ]","title":"validate_documents_paths()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_duplicate_balances","text":"Check that balance entries occur only once per day. Because we do not support time, and the declaration order of entries is meant to be kept irrelevant, two balance entries with different amounts should not occur in the file. We do allow two identical balance assertions, however, because this may occur during import. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_duplicate_balances ( entries , unused_options_map ): \"\"\"Check that balance entries occur only once per day. Because we do not support time, and the declaration order of entries is meant to be kept irrelevant, two balance entries with different amounts should not occur in the file. We do allow two identical balance assertions, however, because this may occur during import. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] # Mapping of (account, currency, date) to Balance entry. balance_entries = {} for entry in entries : if not isinstance ( entry , data . Balance ): continue key = ( entry . account , entry . amount . currency , entry . date ) try : previous_entry = balance_entries [ key ] if entry . amount != previous_entry . amount : errors . append ( ValidationError ( entry . meta , \"Duplicate balance assertion with different amounts\" , entry , ) ) except KeyError : balance_entries [ key ] = entry return errors","title":"validate_duplicate_balances()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_duplicate_commodities","text":"Check that commodity entries are unique for each commodity. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_duplicate_commodities ( entries , unused_options_map ): \"\"\"Check that commodity entries are unique for each commodity. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] # Mapping of (account, currency, date) to Balance entry. commodity_entries = {} for entry in entries : if not isinstance ( entry , data . Commodity ): continue key = entry . currency try : previous_entry = commodity_entries [ key ] if previous_entry : errors . append ( ValidationError ( entry . meta , \"Duplicate commodity directives for ' {} '\" . format ( key ), entry , ) ) except KeyError : commodity_entries [ key ] = entry return errors","title":"validate_duplicate_commodities()"},{"location":"api_reference/beancount.ops.html#beancount.ops.validation.validate_open_close","text":"Check constraints on open and close directives themselves. This method checks two kinds of constraints: An open or a close directive may only show up once for each account. If a duplicate is detected, an error is generated. Close directives may only appear if an open directive has been seen previously (chronologically). The date of close directives must be strictly greater than their corresponding open directive. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/ops/validation.py def validate_open_close ( entries , unused_options_map ): \"\"\"Check constraints on open and close directives themselves. This method checks two kinds of constraints: 1. An open or a close directive may only show up once for each account. If a duplicate is detected, an error is generated. 2. Close directives may only appear if an open directive has been seen previously (chronologically). 3. The date of close directives must be strictly greater than their corresponding open directive. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] open_map = {} close_map = {} for entry in entries : if isinstance ( entry , Open ): if entry . account in open_map : errors . append ( ValidationError ( entry . meta , \"Duplicate open directive for {} \" . format ( entry . account ), entry , ) ) else : open_map [ entry . account ] = entry elif isinstance ( entry , Close ): if entry . account in close_map : errors . append ( ValidationError ( entry . meta , \"Duplicate close directive for {} \" . format ( entry . account ), entry , ) ) else : try : open_entry = open_map [ entry . account ] if entry . date < open_entry . date : errors . append ( ValidationError ( entry . meta , \"Internal error: closing date for {} \" \"appears before opening date\" . format ( entry . account ), entry , ) ) except KeyError : errors . append ( ValidationError ( entry . meta , \"Unopened account {} is being closed\" . format ( entry . account ), entry , ) ) close_map [ entry . account ] = entry return errors","title":"validate_open_close()"},{"location":"api_reference/beancount.parser.html","text":"beancount.parser \uf0c1 Parser module for beancount input files. beancount.parser.booking \uf0c1 Algorithms for 'booking' inventory, that is, the process of finding a matching lot when reducing the content of an inventory. beancount.parser.booking.BookingError ( tuple ) \uf0c1 BookingError(source, message, entry) beancount . parser . booking . BookingError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . booking . BookingError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of BookingError(source, message, entry) beancount . parser . booking . BookingError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . parser . booking . book ( incomplete_entries , options_map , initial_balances = None ) \uf0c1 Book inventory lots and complete all positions with incomplete numbers. Parameters: incomplete_entries \u2013 A list of directives, with some postings possibly left with incomplete amounts as produced by the parser. options_map \u2013 An options dict as produced by the parser. initial_balances \u2013 A dict of (account, inventory) pairs to start booking from. This is useful when attempting to book on top of an existing state. Returns: A pair of entries \u2013 A list of completed entries with all their postings completed. errors: New errors produced during interpolation. Source code in beancount/parser/booking.py def book ( incomplete_entries , options_map , initial_balances = None ): \"\"\"Book inventory lots and complete all positions with incomplete numbers. Args: incomplete_entries: A list of directives, with some postings possibly left with incomplete amounts as produced by the parser. options_map: An options dict as produced by the parser. initial_balances: A dict of (account, inventory) pairs to start booking from. This is useful when attempting to book on top of an existing state. Returns: A pair of entries: A list of completed entries with all their postings completed. errors: New errors produced during interpolation. \"\"\" # Get the list of booking methods for each account. booking_methods = collections . defaultdict ( lambda : options_map [ \"booking_method\" ]) for entry in incomplete_entries : if isinstance ( entry , data . Open ) and entry . booking : booking_methods [ entry . account ] = entry . booking # Do the booking here! entries , booking_errors = booking_full . book ( incomplete_entries , options_map , booking_methods , initial_balances ) # Check for MISSING elements remaining. missing_errors = validate_missing_eliminated ( entries , options_map ) return entries , ( booking_errors + missing_errors ) beancount . parser . booking . convert_lot_specs_to_lots ( entries ) \uf0c1 For all the entries, convert the posting's position's CostSpec to Cost instances. In the simple method, the data provided in the CostSpec must unambiguously provide a way to compute the cost amount. This essentially replicates the way the old parser used to work, but allowing positions to have the fuzzy lot specifications instead of the resolved ones. We used to simply compute the costs locally, and this gets rid of the CostSpec to produce the Cost without fuzzy matching. This is only there for the sake of transition to the new matching logic. Parameters: entries \u2013 A list of incomplete directives as per the parser. Returns: A list of entries whose postings's position costs have been converted to Cost instances but that may still be incomplete. Exceptions: ValueError \u2013 If there's a unacceptable number. Source code in beancount/parser/booking.py def convert_lot_specs_to_lots ( entries ): \"\"\"For all the entries, convert the posting's position's CostSpec to Cost instances. In the simple method, the data provided in the CostSpec must unambiguously provide a way to compute the cost amount. This essentially replicates the way the old parser used to work, but allowing positions to have the fuzzy lot specifications instead of the resolved ones. We used to simply compute the costs locally, and this gets rid of the CostSpec to produce the Cost without fuzzy matching. This is only there for the sake of transition to the new matching logic. Args: entries: A list of incomplete directives as per the parser. Returns: A list of entries whose postings's position costs have been converted to Cost instances but that may still be incomplete. Raises: ValueError: If there's a unacceptable number. \"\"\" new_entries = [] errors = [] for entry in entries : if not isinstance ( entry , data . Transaction ): new_entries . append ( entry ) continue new_postings = [] for posting in entry . postings : try : units = posting . units cost_spec = posting . cost cost = convert_spec_to_cost ( units , cost_spec ) if cost_spec is not None and cost is None : errors . append ( BookingError ( entry . meta , \"Cost syntax not supported; cost spec ignored\" , None ) ) if cost and isinstance ( units , amount . Amount ): # If there is a cost, we don't allow either a cost value of # zero, nor a zero number of units. Note that we allow a price # of zero as the only special case (for conversion entries), but # never for costs. if units . number == ZERO : raise ValueError ( 'Amount is zero: \" {} \"' . format ( units )) if cost . number is not None and cost . number < ZERO : raise ValueError ( 'Cost is negative: \" {} \"' . format ( cost )) except ValueError as exc : errors . append ( BookingError ( entry . meta , str ( exc ), None )) cost = None new_postings . append ( posting . _replace ( cost = cost )) new_entries . append ( entry . _replace ( postings = new_postings )) return new_entries , errors beancount . parser . booking . convert_spec_to_cost ( units , cost_spec ) \uf0c1 Convert a posting's CostSpec instance to a Cost. Parameters: units \u2013 An instance of Amount. cost_spec \u2013 An instance of CostSpec. Returns: An instance of Cost. Source code in beancount/parser/booking.py def convert_spec_to_cost ( units , cost_spec ): \"\"\"Convert a posting's CostSpec instance to a Cost. Args: units: An instance of Amount. cost_spec: An instance of CostSpec. Returns: An instance of Cost. \"\"\" cost = cost_spec if isinstance ( units , amount . Amount ): if cost_spec is not None : number_per , number_total , cost_currency , date , label , merge = cost_spec # Compute the cost. if number_per is not MISSING or number_total is not None : if number_total is not None : # Compute the per-unit cost if there is some total cost # component involved. units_num = units . number cost_total = number_total if number_per is not MISSING : cost_total += number_per * units_num unit_cost = cost_total / abs ( units_num ) else : unit_cost = number_per cost = position . Cost ( unit_cost , cost_currency , date , label ) else : cost = None return cost beancount . parser . booking . validate_inventory_booking ( entries , unused_options_map , booking_methods ) \uf0c1 Validate that no position at cost is allowed to go negative. This routine checks that when a posting reduces a position, existing or not, that the subsequent inventory does not result in a position with a negative number of units. A negative number of units would only be required for short trades of trading spreads on futures, and right now this is not supported. It would not be difficult to support this, however, but we want to be strict about it, because being pedantic about this is otherwise a great way to detect user data entry mistakes. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. booking_methods \u2013 A mapping of account name to booking method, accumulated in the main loop. Returns: A list of errors. Source code in beancount/parser/booking.py def validate_inventory_booking ( entries , unused_options_map , booking_methods ): \"\"\"Validate that no position at cost is allowed to go negative. This routine checks that when a posting reduces a position, existing or not, that the subsequent inventory does not result in a position with a negative number of units. A negative number of units would only be required for short trades of trading spreads on futures, and right now this is not supported. It would not be difficult to support this, however, but we want to be strict about it, because being pedantic about this is otherwise a great way to detect user data entry mistakes. Args: entries: A list of directives. unused_options_map: An options map. booking_methods: A mapping of account name to booking method, accumulated in the main loop. Returns: A list of errors. \"\"\" errors = [] balances = collections . defaultdict ( inventory . Inventory ) for entry in entries : if isinstance ( entry , data . Transaction ): for posting in entry . postings : # Update the balance of each posting on its respective account # without allowing booking to a negative position, and if an error # is encountered, catch it and return it. running_balance = balances [ posting . account ] position_ , _ = running_balance . add_position ( posting ) # Skip this check if the booking method is set to ignore it. if booking_methods . get ( posting . account , None ) == data . Booking . NONE : continue # Check if the resulting inventory is mixed, which is not # allowed under the STRICT method. if running_balance . is_mixed (): errors . append ( BookingError ( entry . meta , ( \"Reducing position results in inventory with positive \" \"and negative lots: {} \" ) . format ( position_ ), entry , ) ) return errors beancount . parser . booking . validate_missing_eliminated ( entries , unused_options_map ) \uf0c1 Validate that all the missing bits of postings have been eliminated. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of errors. Source code in beancount/parser/booking.py def validate_missing_eliminated ( entries , unused_options_map ): \"\"\"Validate that all the missing bits of postings have been eliminated. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of errors. \"\"\" errors = [] for entry in entries : if isinstance ( entry , data . Transaction ): for posting in entry . postings : units = posting . units cost = posting . cost if ( MISSING in ( units . number , units . currency ) or cost is not None and MISSING in ( cost . number , cost . currency , cost . date , cost . label ) ): errors . append ( BookingError ( entry . meta , \"Transaction has incomplete elements\" , entry ) ) break return errors beancount.parser.booking_full \uf0c1 Full (new) booking implementation. Problem description: Interpolation and booking feed on each other, that is, numbers filled in from interpolation might affect the booking process, and numbers derived from the booking process may help carry out interpolation that would otherwise be under-defined. Here's an example of interpolation helping the booking process: Assume the ante-inventory of Assets:Investments contains two lots of shares of HOOL, one at 100.00 USD and the other at 101.00 USD and apply this transaction: 2015-09-30 * Assets:Investments -10 HOOL {USD} Assets:Cash 1000 USD Income:Gains -200 USD Interpolation is unambiguously able to back out a cost of 100 USD / HOOL, which would then result in an unambiguous booking result. On the other hand, consider this transaction: 2015-09-30 * Assets:Investments -10 HOOL {USD} Assets:Cash 1000 USD Income:Gains Now the interpolation cannot succeed. If the Assets:Investments account is configured to use the FIFO method, the 10 oldest shares would be selected for the cost, and we could then interpolate the capital gains correctly. First observation: The second case is much more frequent than the first, and the first is easily resolved manually by requiring a particular cost be specified. Moreover, in many cases there isn't just a single lot of shares to be reduced from and figuring out the correct set of shares given a target cost is an underspecified problem. Second observation: Booking can only be achieved for inventory reductions, not for augmentations. Therefore, we should carry out booking on inventory reductions and fail early if reduction is undefined there, and leave inventory augmentations with missing numbers undefined, so that interpolation can fill them in at a later stage. Note that one case we'd like to but may not be able to handle is of a reduction with interpolated price, like this: 2015-09-30 * Assets:Investments -10 HOOL {100.00 # USD} Expenses:Commission 9.95 USD Assets:Cash 990.05 USD Therefore we choose to 1) Carry out booking first, on inventory reductions only, and leave inventory augmentations as they are, possibly undefined. The 'cost' attributed of booked postings are converted from CostSpec to Cost. Augmented postings with missing amounts are left as CostSpec instances in order to allow for interpolation of total vs. per-unit amount. 2) Compute interpolations on the resulting postings. Undefined costs for inventory augmentations may be filled in by interpolations at this stage (if possible). 3) Finally, convert the interpolated CostSpec instances to Cost instances. Improving on this algorithm would require running a loop over the booking and interpolation steps until all numbers are resolved or no more inference can occur. We may consider that for later, as an experimental feature. My hunch is that there are so few cases for which this would be useful that we won't bother improving on the algorithm above. beancount.parser.booking_full.CategorizationError ( tuple ) \uf0c1 CategorizationError(source, message, entry) beancount . parser . booking_full . CategorizationError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . booking_full . CategorizationError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of CategorizationError(source, message, entry) beancount . parser . booking_full . CategorizationError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.parser.booking_full.InterpolationError ( tuple ) \uf0c1 InterpolationError(source, message, entry) beancount . parser . booking_full . InterpolationError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . booking_full . InterpolationError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of InterpolationError(source, message, entry) beancount . parser . booking_full . InterpolationError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.parser.booking_full.MissingType ( Enum ) \uf0c1 The type of missing number. beancount.parser.booking_full.ReductionError ( tuple ) \uf0c1 ReductionError(source, message, entry) beancount . parser . booking_full . ReductionError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . booking_full . ReductionError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of ReductionError(source, message, entry) beancount . parser . booking_full . ReductionError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.parser.booking_full.Refer ( tuple ) \uf0c1 Refer(index, units_currency, cost_currency, price_currency) beancount . parser . booking_full . Refer . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . booking_full . Refer . __new__ ( _cls , index , units_currency , cost_currency , price_currency ) special staticmethod \uf0c1 Create new instance of Refer(index, units_currency, cost_currency, price_currency) beancount . parser . booking_full . Refer . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.parser.booking_full.SelfReduxError ( tuple ) \uf0c1 SelfReduxError(source, message, entry) beancount . parser . booking_full . SelfReduxError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . booking_full . SelfReduxError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of SelfReduxError(source, message, entry) beancount . parser . booking_full . SelfReduxError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . parser . booking_full . book ( entries , options_map , methods , initial_balances = None ) \uf0c1 Interpolate missing data from the entries using the full historical algorithm. See the internal implementation _book() for details. This method only stripes some of the return values. See _book() for arguments and return values. Source code in beancount/parser/booking_full.py def book ( entries , options_map , methods , initial_balances = None ): \"\"\"Interpolate missing data from the entries using the full historical algorithm. See the internal implementation _book() for details. This method only stripes some of the return values. See _book() for arguments and return values. \"\"\" entries , errors , _ = _book ( entries , options_map , methods , initial_balances ) return entries , errors beancount . parser . booking_full . book_reductions ( entry , group_postings , balances , methods ) \uf0c1 Book inventory reductions against the ante-balances. This function accepts a dict of (account, Inventory balance) and for each posting that is a reduction against its inventory, attempts to find a corresponding lot or list of lots to reduce the balance with. For reducing lots, the CostSpec instance of the posting is replaced by a Cost instance. For augmenting lots, the CostSpec instance of the posting is left alone, except for its date, which is inherited from the parent Transaction. Parameters: entry \u2013 An instance of Transaction. This is only used to refer to when logging errors. group_postings \u2013 A list of Posting instances for the group. balances \u2013 A dict of account name to inventory contents. methods \u2013 A mapping of account name to their corresponding booking method enum. Returns: A pair of booked_postings \u2013 A list of booked postings, with reducing lots resolved against specific position in the corresponding accounts' ante-inventory balances. Note single reducing posting in the input may result in multiple postings in the output. Also note that augmenting postings held-at-cost will still refer to 'cost' instances of CostSpec, left to be interpolated later. errors: A list of errors, if there were any. Source code in beancount/parser/booking_full.py def book_reductions ( entry , group_postings , balances , methods ): \"\"\"Book inventory reductions against the ante-balances. This function accepts a dict of (account, Inventory balance) and for each posting that is a reduction against its inventory, attempts to find a corresponding lot or list of lots to reduce the balance with. * For reducing lots, the CostSpec instance of the posting is replaced by a Cost instance. * For augmenting lots, the CostSpec instance of the posting is left alone, except for its date, which is inherited from the parent Transaction. Args: entry: An instance of Transaction. This is only used to refer to when logging errors. group_postings: A list of Posting instances for the group. balances: A dict of account name to inventory contents. methods: A mapping of account name to their corresponding booking method enum. Returns: A pair of booked_postings: A list of booked postings, with reducing lots resolved against specific position in the corresponding accounts' ante-inventory balances. Note single reducing posting in the input may result in multiple postings in the output. Also note that augmenting postings held-at-cost will still refer to 'cost' instances of CostSpec, left to be interpolated later. errors: A list of errors, if there were any. \"\"\" errors = [] # A local copy of the balances dictionary which is updated just for the # duration of this function's updates, in order to take into account the # cumulative effect of all the postings inferred here local_balances = {} empty = inventory . Inventory () booked_postings = [] for posting in group_postings : # Process a single posting. units = posting . units costspec = posting . cost account = posting . account # Note: We ensure there is no mutation on 'balances' to keep this # function without side-effects. Note that we may be able to optimize # performance later on by giving up this property. # # Also note that if there is no existing balance, then won't be any lot # reduction because none of the postings will be able to match against # any currencies of the balance. if account not in local_balances : previous_balance = balances . get ( account , empty ) local_balances [ account ] = copy . copy ( previous_balance ) balance = local_balances [ account ] # Check if this is a lot held at cost. if costspec is None or units . number is MISSING : # This posting is not held at cost; we do nothing. booked_postings . append ( posting ) else : # This posting is held at cost; figure out if it's a reduction or an # augmentation. method = methods [ account ] if ( method is not Booking . NONE and balance is not None and balance . is_reduced_by ( units ) ): # This posting is a reduction. # Match the positions. cost_number = compute_cost_number ( costspec , units ) matches = [] for position in balance : # Skip inventory contents of a different currency. if units . currency and position . units . currency != units . currency : continue # Skip balance positions not held at cost. if position . cost is None : continue if cost_number is not None and position . cost . number != cost_number : continue if ( isinstance ( costspec . currency , str ) and position . cost . currency != costspec . currency ): continue if costspec . date and position . cost . date != costspec . date : continue if costspec . label and position . cost . label != costspec . label : continue matches . append ( position ) # Check for ambiguous matches. if len ( matches ) == 0 : errors . append ( ReductionError ( entry . meta , 'No position matches \" {} \" against balance {} ' . format ( posting , balance ), entry , ) ) return [], errors # This is irreconcilable, remove these postings. # TODO(blais): We'll have to change this, as we want to allow # positions crossing from negative to positive and vice-versa in # a simple application. See {d3cbd78f1029}. reduction_postings , matched_postings , ambi_errors = ( booking_method . handle_ambiguous_matches ( entry , posting , matches , method ) ) if ambi_errors : errors . extend ( ambi_errors ) return [], errors # Add the reductions to the resulting list of booked postings. booked_postings . extend ( reduction_postings ) # Update the local balance in order to avoid matching against # the same postings twice when processing multiple postings in # the same transaction. Note that we only do this for postings # held at cost because the other postings may need interpolation # in order to be resolved properly. for posting in reduction_postings : balance . add_position ( posting ) else : # This posting is an augmentation. # # Note that we do not convert the CostSpec instances to Cost # instances, because we want to let the subsequent interpolation # process able to interpolate either the cost per-unit or the # total cost, separately. # Put in the date of the parent Transaction if there is no # explicit date specified on the spec. if costspec . date is None : dated_costspec = costspec . _replace ( date = entry . date ) posting = posting . _replace ( cost = dated_costspec ) # FIXME: Insert unique ids for trade tracking; right now this # creates ambiguous matches errors (and it shouldn't). # # Insert a unique label if there isn't one. # if posting.cost is not None and posting.cost.label is None: # posting = posting._replace( # cost=posting.cost._replace(label=unique_label())) booked_postings . append ( posting ) return booked_postings , errors beancount . parser . booking_full . categorize_by_currency ( entry , balances ) \uf0c1 Group the postings by the currency they declare. This is used to prepare the postings for the next stages: Interpolation and booking will then be carried out separately on each currency group. At the outset of this routine, we should have distinct groups of currencies without any ambiguities regarding which currency they need to balance against. Here's how this works. First we apply the constraint that cost-currency and price-currency must match, if there is both a cost and a price. This reduces the space of possibilities somewhat. If the currency is explicitly specified, we put the posting in that currency's bucket. If not, we have a few methods left to disambiguate the currency: We look at the remaining postings... if they are all of a single currency, the posting must be in that currency too. If we cannot do that, we inspect the contents of the inventory of the account for the posting. If all the contents are of a single currency, we use that one. Parameters: postings \u2013 A list of incomplete postings to categorize. balances \u2013 A dict of currency to inventory contents before the transaction is applied. Returns: A list of (currency string, list of tuples) items describing each postings and its interpolated currencies, and a list of generated errors for currency interpolation. The entry's original postings are left unmodified. Each tuple in the value-list contains \u2013 index: The posting index in the original entry. units_currency: The interpolated currency for units. cost_currency: The interpolated currency for cost. price_currency: The interpolated currency for price. Source code in beancount/parser/booking_full.py def categorize_by_currency ( entry , balances ): \"\"\"Group the postings by the currency they declare. This is used to prepare the postings for the next stages: Interpolation and booking will then be carried out separately on each currency group. At the outset of this routine, we should have distinct groups of currencies without any ambiguities regarding which currency they need to balance against. Here's how this works. - First we apply the constraint that cost-currency and price-currency must match, if there is both a cost and a price. This reduces the space of possibilities somewhat. - If the currency is explicitly specified, we put the posting in that currency's bucket. - If not, we have a few methods left to disambiguate the currency: 1. We look at the remaining postings... if they are all of a single currency, the posting must be in that currency too. 2. If we cannot do that, we inspect the contents of the inventory of the account for the posting. If all the contents are of a single currency, we use that one. Args: postings: A list of incomplete postings to categorize. balances: A dict of currency to inventory contents before the transaction is applied. Returns: A list of (currency string, list of tuples) items describing each postings and its interpolated currencies, and a list of generated errors for currency interpolation. The entry's original postings are left unmodified. Each tuple in the value-list contains: index: The posting index in the original entry. units_currency: The interpolated currency for units. cost_currency: The interpolated currency for cost. price_currency: The interpolated currency for price. \"\"\" errors = [] groups = collections . defaultdict ( list ) sortdict = {} auto_postings = [] unknown = [] for index , posting in enumerate ( entry . postings ): units = posting . units cost = posting . cost price = posting . price # Extract and override the currencies locally. units_currency = ( units . currency if units is not MISSING and units is not None else None ) cost_currency = cost . currency if cost is not MISSING and cost is not None else None price_currency = ( price . currency if price is not MISSING and price is not None else None ) # First we apply the constraint that cost-currency and price-currency # must match, if there is both a cost and a price. This reduces the # space of possibilities somewhat. if cost_currency is MISSING and isinstance ( price_currency , str ): cost_currency = price_currency if price_currency is MISSING and isinstance ( cost_currency , str ): price_currency = cost_currency refer = Refer ( index , units_currency , cost_currency , price_currency ) if units is MISSING and price_currency is None : # Bucket auto-postings separately from unknown. auto_postings . append ( refer ) else : # Bucket with what we know so far. currency = get_bucket_currency ( refer ) if currency is not None : sortdict . setdefault ( currency , index ) groups [ currency ] . append ( refer ) else : # If we need to infer the currency, store in unknown. unknown . append ( refer ) # We look at the remaining postings... if they are all of a single currency, # the posting must be in that currency too. if unknown and len ( unknown ) == 1 and len ( groups ) == 1 : ( index , units_currency , cost_currency , price_currency ) = unknown . pop () other_currency = next ( iter ( groups . keys ())) if price_currency is None and cost_currency is None : # Infer to the units currency. units_currency = other_currency else : # Infer to the cost and price currencies. if price_currency is MISSING : price_currency = other_currency if cost_currency is MISSING : cost_currency = other_currency refer = Refer ( index , units_currency , cost_currency , price_currency ) currency = get_bucket_currency ( refer ) assert currency is not None sortdict . setdefault ( currency , index ) groups [ currency ] . append ( refer ) # Finally, try to resolve all the unknown legs using the inventory contents # of each account. for refer in unknown : ( index , units_currency , cost_currency , price_currency ) = refer posting = entry . postings [ index ] balance = balances . get ( posting . account , None ) if balance is None : balance = inventory . Inventory () if units_currency is MISSING : balance_currencies = balance . currencies () if len ( balance_currencies ) == 1 : units_currency = balance_currencies . pop () if cost_currency is MISSING or price_currency is MISSING : balance_cost_currencies = balance . cost_currencies () if len ( balance_cost_currencies ) == 1 : balance_cost_currency = balance_cost_currencies . pop () if price_currency is MISSING : price_currency = balance_cost_currency if cost_currency is MISSING : cost_currency = balance_cost_currency refer = Refer ( index , units_currency , cost_currency , price_currency ) currency = get_bucket_currency ( refer ) if currency is not None : sortdict . setdefault ( currency , index ) groups [ currency ] . append ( refer ) else : errors . append ( CategorizationError ( posting . meta , \"Failed to categorize posting {} \" . format ( index + 1 ), entry ) ) # Fill in missing units currencies if some remain as missing. This may occur # if we used the cost or price to bucket the currency but the units currency # was missing. for currency , refers in groups . items (): for rindex , refer in enumerate ( refers ): if refer . units_currency is MISSING : posting = entry . postings [ refer . index ] balance = balances . get ( posting . account , None ) if balance is None : continue balance_currencies = balance . currencies () if len ( balance_currencies ) == 1 : refers [ rindex ] = refer . _replace ( units_currency = balance_currencies . pop ()) # Deal with auto-postings. if len ( auto_postings ) > 1 : refer = auto_postings [ - 1 ] posting = entry . postings [ refer . index ] errors . append ( CategorizationError ( posting . meta , \"You may not have more than one auto-posting per currency\" , entry , ) ) auto_postings = auto_postings [ 0 : 1 ] for refer in auto_postings : for currency , glist in groups . items (): sortdict . setdefault ( currency , refer . index ) glist . append ( Refer ( refer . index , currency , None , None )) # Issue error for all currencies which we could not resolve. for currency , refers in groups . items (): for refer in refers : posting = entry . postings [ refer . index ] for currency , name in [ ( refer . units_currency , \"units\" ), ( refer . cost_currency , \"cost\" ), ( refer . price_currency , \"price\" ), ]: if currency is MISSING : errors . append ( CategorizationError ( posting . meta , \"Could not resolve {} currency\" . format ( name ), entry , ) ) sorted_groups = sorted ( groups . items (), key = lambda item : sortdict [ item [ 0 ]]) return sorted_groups , errors beancount . parser . booking_full . compute_cost_number ( costspec , units ) \uf0c1 Given a CostSpec, return the cost number, if possible to compute. Parameters: costspec \u2013 A parsed instance of CostSpec. units \u2013 An instance of Amount for the units of the position. Returns: If it is not possible to calculate the cost, return None. Otherwise, returns a Decimal instance, the per-unit cost. Source code in beancount/parser/booking_full.py def compute_cost_number ( costspec , units ): \"\"\"Given a CostSpec, return the cost number, if possible to compute. Args: costspec: A parsed instance of CostSpec. units: An instance of Amount for the units of the position. Returns: If it is not possible to calculate the cost, return None. Otherwise, returns a Decimal instance, the per-unit cost. \"\"\" number_per = costspec . number_per number_total = costspec . number_total if MISSING in ( number_per , number_total ): return None if number_total is not None : # Compute the per-unit cost if there is some total cost # component involved. cost_total = number_total units_number = abs ( units . number ) if number_per is not None : cost_total += number_per * units_number unit_cost = cost_total / units_number elif number_per is None : return None else : unit_cost = number_per return unit_cost beancount . parser . booking_full . convert_costspec_to_cost ( posting ) \uf0c1 Convert an instance of CostSpec to Cost, if present on the posting. If the posting has no cost, it itself is just returned. Parameters: posting \u2013 An instance of Posting. Returns: An instance of Posting with a possibly replaced 'cost' attribute. Source code in beancount/parser/booking_full.py def convert_costspec_to_cost ( posting ): \"\"\"Convert an instance of CostSpec to Cost, if present on the posting. If the posting has no cost, it itself is just returned. Args: posting: An instance of Posting. Returns: An instance of Posting with a possibly replaced 'cost' attribute. \"\"\" cost = posting . cost if isinstance ( cost , position . CostSpec ): if cost is not None : number_per = cost . number_per number_total = cost . number_total if number_total is not None : # Compute the per-unit cost if there is some total cost # component involved. units_number = abs ( posting . units . number ) cost_total = number_total if number_per is not MISSING : cost_total += number_per * units_number unit_cost = cost_total / units_number else : unit_cost = number_per new_cost = Cost ( unit_cost , cost . currency , cost . date , cost . label ) posting = posting . _replace ( units = posting . units , cost = new_cost ) return posting beancount . parser . booking_full . get_bucket_currency ( refer ) \uf0c1 Given currency references for a posting, return the bucket currency. Parameters: refer \u2013 An instance of Refer. Returns: A currency string. Source code in beancount/parser/booking_full.py def get_bucket_currency ( refer ): \"\"\"Given currency references for a posting, return the bucket currency. Args: refer: An instance of Refer. Returns: A currency string. \"\"\" currency = None if isinstance ( refer . cost_currency , str ): currency = refer . cost_currency elif isinstance ( refer . price_currency , str ): currency = refer . price_currency elif ( refer . cost_currency is None and refer . price_currency is None and isinstance ( refer . units_currency , str ) ): currency = refer . units_currency return currency beancount . parser . booking_full . has_self_reduction ( postings , methods ) \uf0c1 Return true if the postings potentially reduce each other at cost. Parameters: postings \u2013 A list of postings with uninterpolated CostSpec cost instances. methods \u2013 A mapping of account name to their corresponding booking method. Returns: A boolean, true if there's a potential for self-reduction. Source code in beancount/parser/booking_full.py def has_self_reduction ( postings , methods ): \"\"\"Return true if the postings potentially reduce each other at cost. Args: postings: A list of postings with uninterpolated CostSpec cost instances. methods: A mapping of account name to their corresponding booking method. Returns: A boolean, true if there's a potential for self-reduction. \"\"\" # A mapping of (currency, cost-currency) and sign. cost_changes = {} for posting in postings : cost = posting . cost if cost is None : continue if methods [ posting . account ] is Booking . NONE : continue key = ( posting . account , posting . units . currency ) sign = 1 if posting . units . number > ZERO else - 1 if cost_changes . setdefault ( key , sign ) != sign : return True return False beancount . parser . booking_full . interpolate_group ( postings , balances , currency , tolerances ) \uf0c1 Interpolate missing numbers in the set of postings. Parameters: postings \u2013 A list of Posting instances. balances \u2013 A dict of account to its ante-inventory. currency \u2013 The weight currency of this group, used for reporting errors. tolerances \u2013 A dict of currency to tolerance values. Returns: A tuple of postings \u2013 A list of new posting instances. errors: A list of errors generated during interpolation. interpolated: A boolean, true if we did have to interpolate. In the case of an error, this returns the original list of postings, which is still incomplete. If an error is returned, you should probably skip the transaction altogether, or just not include the postings in it. (An alternative behaviour would be to return only the list of valid postings, but that would likely result in an unbalanced transaction. We do it this way by choice.) Source code in beancount/parser/booking_full.py def interpolate_group ( postings , balances , currency , tolerances ): \"\"\"Interpolate missing numbers in the set of postings. Args: postings: A list of Posting instances. balances: A dict of account to its ante-inventory. currency: The weight currency of this group, used for reporting errors. tolerances: A dict of currency to tolerance values. Returns: A tuple of postings: A list of new posting instances. errors: A list of errors generated during interpolation. interpolated: A boolean, true if we did have to interpolate. In the case of an error, this returns the original list of postings, which is still incomplete. If an error is returned, you should probably skip the transaction altogether, or just not include the postings in it. (An alternative behaviour would be to return only the list of valid postings, but that would likely result in an unbalanced transaction. We do it this way by choice.) \"\"\" errors = [] # Figure out which type of amount is missing, by creating a list of # incomplete postings and which type of units is missing. incomplete = [] for index , posting in enumerate ( postings ): units = posting . units cost = posting . cost price = posting . price # Identify incomplete parts of the Posting components. if units . number is MISSING : incomplete . append (( MissingType . UNITS , index )) if isinstance ( cost , CostSpec ): if cost and cost . number_per is MISSING : incomplete . append (( MissingType . COST_PER , index )) if cost and cost . number_total is MISSING : incomplete . append (( MissingType . COST_TOTAL , index )) else : # Check that a resolved instance of Cost never needs interpolation. # # Note that in theory we could support the interpolation of regular # per-unit costs in these if we wanted to; but because they're all # reducing postings that have been booked earlier, those never need # to be interpolated. if cost is not None : assert isinstance ( cost . number , Decimal ), \"Internal error: cost has no number: {} ; on postings: {} \" . format ( cost , postings ) if price and price . number is MISSING : incomplete . append (( MissingType . PRICE , index )) # The replacement posting for the incomplete posting of this group. new_posting = None if len ( incomplete ) == 0 : # If there are no missing numbers, just convert the CostSpec to Cost and # return that. out_postings = [ convert_costspec_to_cost ( posting ) for posting in postings ] elif len ( incomplete ) > 1 : # If there is more than a single value to be interpolated, generate an # error and return no postings. _ , posting_index = incomplete [ 0 ] errors . append ( InterpolationError ( postings [ posting_index ] . meta , \"Too many missing numbers for currency group ' {} '\" . format ( currency ), None , ) ) out_postings = [] else : # If there is a single missing number, calculate it and fill it in here. missing , index = incomplete [ 0 ] incomplete_posting = postings [ index ] # Convert augmenting postings' costs from CostSpec to corresponding Cost # instances, except for the incomplete posting. new_postings = [ ( posting if posting is incomplete_posting else convert_costspec_to_cost ( posting ) ) for posting in postings ] # Compute the balance of the other postings. residual = interpolate . compute_residual ( posting for posting in new_postings if posting is not incomplete_posting ) assert len ( residual ) < 2 , \"Internal error in grouping postings by currencies.\" if not residual . is_empty (): respos = next ( iter ( residual )) assert ( respos . cost is None ), \"Internal error; cost appears in weight calculation.\" assert ( respos . units . currency == currency ), \"Internal error; residual different than currency group.\" weight = - respos . units . number weight_currency = respos . units . currency else : weight = ZERO weight_currency = currency if missing == MissingType . UNITS : units = incomplete_posting . units cost = incomplete_posting . cost if cost : # Handle the special case where we only have total cost. if cost . number_per == ZERO : errors . append ( InterpolationError ( incomplete_posting . meta , \"Cannot infer per-unit cost only from total\" , None , ) ) return postings , errors , True assert ( cost . currency == weight_currency ), \"Internal error; residual currency different than missing currency.\" cost_total = cost . number_total or ZERO units_number = ( weight - cost_total ) / cost . number_per elif incomplete_posting . price : assert ( incomplete_posting . price . currency == weight_currency ), \"Internal error; residual currency different than missing currency.\" units_number = weight / incomplete_posting . price . number else : assert ( units . currency == weight_currency ), \"Internal error; residual currency different than missing currency.\" units_number = weight # Quantize the interpolated units if necessary. units_number = interpolate . quantize_with_tolerance ( tolerances , units . currency , units_number ) if weight != ZERO : new_pos = Position ( Amount ( units_number , units . currency ), cost ) new_posting = incomplete_posting . _replace ( units = new_pos . units , cost = new_pos . cost ) else : new_posting = None elif missing == MissingType . COST_PER : units = incomplete_posting . units cost = incomplete_posting . cost assert ( cost . currency == weight_currency ), \"Internal error; residual currency different than missing currency.\" if units . number != ZERO : number_per = ( weight - ( cost . number_total or ZERO )) / units . number new_cost = cost . _replace ( number_per = number_per ) new_pos = Position ( units , new_cost ) new_posting = incomplete_posting . _replace ( units = new_pos . units , cost = new_pos . cost ) else : new_posting = None elif missing == MissingType . COST_TOTAL : units = incomplete_posting . units cost = incomplete_posting . cost assert ( cost . currency == weight_currency ), \"Internal error; residual currency different than missing currency.\" number_total = weight - cost . number_per * units . number new_cost = cost . _replace ( number_total = number_total ) new_pos = Position ( units , new_cost ) new_posting = incomplete_posting . _replace ( units = new_pos . units , cost = new_pos . cost ) elif missing == MissingType . PRICE : units = incomplete_posting . units cost = incomplete_posting . cost if cost is not None : errors . append ( InterpolationError ( incomplete_posting . meta , \"Cannot infer price for postings with units held at cost\" , None , ) ) return postings , errors , True else : price = incomplete_posting . price assert ( price . currency == weight_currency ), \"Internal error; residual currency different than missing currency.\" new_price_number = abs ( weight / units . number ) new_posting = incomplete_posting . _replace ( price = Amount ( new_price_number , price . currency ) ) else : assert False , \"Internal error; Invalid missing type.\" # Replace the number in the posting. if new_posting is not None : # Set meta-data on the new posting to indicate it was interpolated. if new_posting . meta is None : new_posting = new_posting . _replace ( meta = {}) new_posting . meta [ interpolate . AUTOMATIC_META ] = True # Convert augmenting posting costs from CostSpec to a corresponding # Cost instance. new_postings [ index ] = convert_costspec_to_cost ( new_posting ) else : del new_postings [ index ] out_postings = new_postings assert all ( not isinstance ( posting . cost , CostSpec ) for posting in out_postings ) # Check that units are non-zero and that no cost remains negative; issue an # error if this is the case. for posting in out_postings : if posting . cost is None : continue # If there is a cost, we don't allow either a cost value of zero, # nor a zero number of units. Note that we allow a price of zero as # the only special case allowed (for conversion entries), but never # for costs. if posting . units . number == ZERO : errors . append ( InterpolationError ( posting . meta , 'Amount is zero: \" {} \"' . format ( posting . units ), None ) ) if posting . cost . number is not None and posting . cost . number < ZERO : errors . append ( InterpolationError ( posting . meta , 'Cost is negative: \" {} \"' . format ( posting . cost ), None ) ) return out_postings , errors , ( new_posting is not None ) beancount . parser . booking_full . replace_currencies ( postings , refer_groups ) \uf0c1 Replace resolved currencies in the entry's Postings. This essentially applies the findings of categorize_by_currency() to produce new postings with all currencies resolved. Parameters: postings \u2013 A list of Posting instances to replace. refer_groups \u2013 A list of (currency, list of posting references) items as returned by categorize_by_currency(). Returns: A new list of items of (currency, list of Postings), postings for which the currencies have been replaced by their interpolated currency values. Source code in beancount/parser/booking_full.py def replace_currencies ( postings , refer_groups ): \"\"\"Replace resolved currencies in the entry's Postings. This essentially applies the findings of categorize_by_currency() to produce new postings with all currencies resolved. Args: postings: A list of Posting instances to replace. refer_groups: A list of (currency, list of posting references) items as returned by categorize_by_currency(). Returns: A new list of items of (currency, list of Postings), postings for which the currencies have been replaced by their interpolated currency values. \"\"\" new_groups = [] for currency , refers in refer_groups : new_postings = [] for refer in sorted ( refers , key = lambda r : r . index ): posting = postings [ refer . index ] units = posting . units if units is MISSING or units is None : posting = posting . _replace ( units = Amount ( MISSING , refer . units_currency )) else : replace = False cost = posting . cost price = posting . price if units . currency is MISSING : units = Amount ( units . number , refer . units_currency ) replace = True if cost and cost . currency is MISSING : cost = cost . _replace ( currency = refer . cost_currency ) replace = True if price and price . currency is MISSING : price = Amount ( price . number , refer . price_currency ) replace = True if replace : posting = posting . _replace ( units = units , cost = cost , price = price ) new_postings . append ( posting ) new_groups . append (( currency , new_postings )) return new_groups beancount . parser . booking_full . unique_label () \uf0c1 Return a globally unique label for cost entries. Source code in beancount/parser/booking_full.py def unique_label () -> str : \"Return a globally unique label for cost entries.\" return str ( uuid . uuid4 ()) beancount.parser.booking_method \uf0c1 Implementations of all the particular booking methods. This code is used by the full booking algorithm. beancount.parser.booking_method.AmbiguousMatchError ( tuple ) \uf0c1 AmbiguousMatchError(source, message, entry) beancount . parser . booking_method . AmbiguousMatchError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_method.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . booking_method . AmbiguousMatchError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of AmbiguousMatchError(source, message, entry) beancount . parser . booking_method . AmbiguousMatchError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/booking_method.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . parser . booking_method . booking_method_AVERAGE ( entry , posting , matches ) \uf0c1 AVERAGE booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_AVERAGE ( entry , posting , matches ): \"\"\"AVERAGE booking method implementation.\"\"\" booked_reductions = [] booked_matches = [] errors = [ AmbiguousMatchError ( entry . meta , \"AVERAGE method is not supported\" , entry )] return booked_reductions , booked_matches , errors , False # FIXME: Future implementation here. if False : # DISABLED - This is the code for AVERAGE, which is currently disabled. # If there is more than a single match we need to ultimately merge the # postings. Also, if the reducing posting provides a specific cost, we # need to update the cost basis as well. Both of these cases are carried # out by removing all the matches and readding them later on. if len ( matches ) == 1 and ( not isinstance ( posting . cost . number_per , Decimal ) and not isinstance ( posting . cost . number_total , Decimal ) ): # There is no cost. Just reduce the one leg. This should be the # normal case if we always merge augmentations and the user lets # Beancount deal with the cost. match = matches [ 0 ] sign = - 1 if posting . units . number < ZERO else 1 number = min ( abs ( match . units . number ), abs ( posting . units . number )) match_units = Amount ( number * sign , match . units . currency ) booked_reductions . append ( posting . _replace ( units = match_units , cost = match . cost )) _insufficient = match_units . number != posting . units . number else : # Merge the matching postings to a single one. merged_units = inventory . Inventory () merged_cost = inventory . Inventory () for match in matches : merged_units . add_amount ( match . units ) merged_cost . add_amount ( convert . get_weight ( match )) if len ( merged_units ) != 1 or len ( merged_cost ) != 1 : errors . append ( AmbiguousMatchError ( entry . meta , \"Cannot merge positions in multiple currencies: {} \" . format ( \", \" . join ( position . to_string ( match_posting ) for match_posting in matches ) ), entry , ) ) else : if isinstance ( posting . cost . number_per , Decimal ) or isinstance ( posting . cost . number_total , Decimal ): errors . append ( AmbiguousMatchError ( entry . meta , \"Explicit cost reductions aren't supported yet: {} \" . format ( position . to_string ( posting ) ), entry , ) ) else : # Insert postings to remove all the matches. booked_reductions . extend ( posting . _replace ( units =- match . units , cost = match . cost , flag = flags . FLAG_MERGING ) for match in matches ) units = merged_units [ 0 ] . units date = matches [ 0 ] . cost . date ## FIXME: Select which one, ## oldest or latest. cost_units = merged_cost [ 0 ] . units cost = Cost ( cost_units . number / units . number , cost_units . currency , date , None ) # Insert a posting to refill those with a replacement match. booked_reductions . append ( posting . _replace ( units = units , cost = cost , flag = flags . FLAG_MERGING ) ) # Now, match the reducing request against this lot. booked_reductions . append ( posting . _replace ( units = posting . units , cost = cost ) ) _insufficient = abs ( posting . units . number ) > abs ( units . number ) beancount . parser . booking_method . booking_method_FIFO ( entry , posting , matches ) \uf0c1 FIFO booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_FIFO ( entry , posting , matches ): \"\"\"FIFO booking method implementation.\"\"\" return _booking_method_xifo ( entry , posting , matches , \"date\" , False ) beancount . parser . booking_method . booking_method_HIFO ( entry , posting , matches ) \uf0c1 HIFO booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_HIFO ( entry , posting , matches ): \"\"\"HIFO booking method implementation.\"\"\" return _booking_method_xifo ( entry , posting , matches , \"number\" , True ) beancount . parser . booking_method . booking_method_LIFO ( entry , posting , matches ) \uf0c1 LIFO booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_LIFO ( entry , posting , matches ): \"\"\"LIFO booking method implementation.\"\"\" return _booking_method_xifo ( entry , posting , matches , \"date\" , True ) beancount . parser . booking_method . booking_method_NONE ( entry , posting , matches ) \uf0c1 NONE booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_NONE ( entry , posting , matches ): \"\"\"NONE booking method implementation.\"\"\" # This never needs to match against any existing positions... we # disregard the matches, there's never any error. Note that this never # gets called in practice, we want to treat NONE postings as # augmentations. Default behaviour is to return them with their original # CostSpec, and the augmentation code will handle signaling an error if # there is insufficient detail to carry out the conversion to an # instance of Cost. # Note that it's an interesting question whether a reduction on an # account with NONE method which happens to match a single position # ought to be matched against it. We don't allow it for now. return [ posting ], [], False beancount . parser . booking_method . booking_method_STRICT ( entry , posting , matches ) \uf0c1 Strict booking method. This method fails if there are ambiguous matches. Source code in beancount/parser/booking_method.py def booking_method_STRICT ( entry , posting , matches ): \"\"\"Strict booking method. This method fails if there are ambiguous matches.\"\"\" booked_reductions = [] booked_matches = [] errors = [] insufficient = False # In strict mode, we require at most a single matching posting. if len ( matches ) > 1 : # If the total requested to reduce matches the sum of all the # ambiguous postings, match against all of them. sum_matches = sum ( p . units . number for p in matches ) if sum_matches == - posting . units . number : booked_reductions . extend ( posting . _replace ( units =- match . units , cost = match . cost ) for match in matches ) else : errors . append ( AmbiguousMatchError ( entry . meta , 'Ambiguous matches for \" {} \": {} ' . format ( position . to_string ( posting ), \", \" . join ( position . to_string ( match_posting ) for match_posting in matches ), ), entry , ) ) else : # Replace the posting's units and cost values. match = matches [ 0 ] sign = - 1 if posting . units . number < ZERO else 1 number = min ( abs ( match . units . number ), abs ( posting . units . number )) match_units = Amount ( number * sign , match . units . currency ) booked_reductions . append ( posting . _replace ( units = match_units , cost = match . cost )) booked_matches . append ( match ) insufficient = match_units . number != posting . units . number return booked_reductions , booked_matches , errors , insufficient beancount . parser . booking_method . booking_method_STRICT_WITH_SIZE ( entry , posting , matches ) \uf0c1 Strict booking method, but disambiguate further with sizes. This booking method applies the same algorithm as the STRICT method, but if only one of the ambiguous lots matches the desired size, select that one automatically. Source code in beancount/parser/booking_method.py def booking_method_STRICT_WITH_SIZE ( entry , posting , matches ): \"\"\"Strict booking method, but disambiguate further with sizes. This booking method applies the same algorithm as the STRICT method, but if only one of the ambiguous lots matches the desired size, select that one automatically. \"\"\" ( booked_reductions , booked_matches , errors , insufficient ) = booking_method_STRICT ( entry , posting , matches ) # If we couldn't match strictly, attempt to find a match with the same # number of units. If there is one or more of these, accept the oldest lot. if errors and len ( matches ) > 1 : number = - posting . units . number matching_units = [ match for match in matches if number == match . units . number ] if matching_units : matching_units . sort ( key = lambda match : match . cost . date ) # Replace the posting's units and cost values. match = matching_units [ 0 ] booked_reductions . append ( posting . _replace ( units =- match . units , cost = match . cost )) booked_matches . append ( match ) insufficient = False errors = [] return booked_reductions , booked_matches , errors , insufficient beancount . parser . booking_method . handle_ambiguous_matches ( entry , posting , matches , method ) \uf0c1 Handle ambiguous matches by dispatching to a particular method. Parameters: entry \u2013 The parent Transaction instance. posting \u2013 An instance of Posting, the reducing posting which we're attempting to match. matches \u2013 A list of matching Position instances from the ante-inventory. Those positions are known to already match the 'posting' spec. methods \u2013 A mapping of account name to their corresponding booking method. Returns: A triple of booked_reductions \u2013 A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. errors: A list of errors to be generated. insufficient: A boolean, true if we could not find enough matches to cover the entire position. Source code in beancount/parser/booking_method.py def handle_ambiguous_matches ( entry , posting , matches , method ): \"\"\"Handle ambiguous matches by dispatching to a particular method. Args: entry: The parent Transaction instance. posting: An instance of Posting, the reducing posting which we're attempting to match. matches: A list of matching Position instances from the ante-inventory. Those positions are known to already match the 'posting' spec. methods: A mapping of account name to their corresponding booking method. Returns: A triple of booked_reductions: A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. errors: A list of errors to be generated. insufficient: A boolean, true if we could not find enough matches to cover the entire position. \"\"\" assert isinstance ( method , Booking ), \"Invalid type: {} \" . format ( method ) assert matches , \"Internal error: Invalid call with no matches\" # method = globals()['booking_method_{}'.format(method.name)] method = _BOOKING_METHODS [ method ] ( booked_reductions , booked_matches , errors , insufficient ) = method ( entry , posting , matches ) if insufficient : errors . append ( AmbiguousMatchError ( entry . meta , 'Not enough lots to reduce \" {} \": {} ' . format ( position . to_string ( posting ), \", \" . join ( position . to_string ( match_posting ) for match_posting in matches ), ), entry , ) ) return booked_reductions , booked_matches , errors beancount.parser.cmptest \uf0c1 Support utilities for testing scripts. beancount.parser.cmptest.TestCase ( TestCase ) \uf0c1 beancount . parser . cmptest . TestCase . assertEqualEntries ( self , expected_entries , actual_entries , allow_incomplete = False ) \uf0c1 Check that two lists of entries are equal. Entries can be provided either as a list of directives or as a string. In the latter case, the string is parsed with beancount.parser.parse_string() and the resulting directives list is used. If allow_incomplete is True, light-weight booking is performed before comparing the directive lists, allowing to compare transactions with incomplete postings. Parameters: expected_entries \u2013 Expected entries. actual_entries \u2013 Actual entries. allow_incomplete \u2013 Perform booking before comparison. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/parser/cmptest.py def assertEqualEntries ( self , expected_entries , actual_entries , allow_incomplete = False ): \"\"\"Check that two lists of entries are equal. Entries can be provided either as a list of directives or as a string. In the latter case, the string is parsed with beancount.parser.parse_string() and the resulting directives list is used. If allow_incomplete is True, light-weight booking is performed before comparing the directive lists, allowing to compare transactions with incomplete postings. Args: expected_entries: Expected entries. actual_entries: Actual entries. allow_incomplete: Perform booking before comparison. Raises: AssertionError: If the exception fails. \"\"\" expected_entries = read_string_or_entries ( expected_entries , allow_incomplete ) actual_entries = read_string_or_entries ( actual_entries , allow_incomplete ) same , expected_missing , actual_missing = compare . compare_entries ( expected_entries , actual_entries ) if not same : assert expected_missing or actual_missing , \"Missing is missing: {} , {} \" . format ( expected_missing , actual_missing ) oss = io . StringIO () if expected_missing : oss . write ( \"Present in expected set and not in actual set: \\n\\n \" ) for entry in expected_missing : oss . write ( printer . format_entry ( entry )) oss . write ( \" \\n \" ) if actual_missing : oss . write ( \"Present in actual set and not in expected set: \\n\\n \" ) for entry in actual_missing : oss . write ( printer . format_entry ( entry )) oss . write ( \" \\n \" ) self . fail ( oss . getvalue ()) beancount . parser . cmptest . TestCase . assertExcludesEntries ( self , subset_entries , entries , allow_incomplete = False ) \uf0c1 Check that subset_entries is not included in entries. Entries can be provided either as a list of directives or as a string. In the latter case, the string is parsed with beancount.parser.parse_string() and the resulting directives list is used. If allow_incomplete is True, light-weight booking is performed before comparing the directive lists, allowing to compare transactions with incomplete postings. Parameters: subset_entries \u2013 Subset entries. entries \u2013 Entries. allow_incomplete \u2013 Perform booking before comparison. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/parser/cmptest.py def assertExcludesEntries ( self , subset_entries , entries , allow_incomplete = False ): \"\"\"Check that subset_entries is not included in entries. Entries can be provided either as a list of directives or as a string. In the latter case, the string is parsed with beancount.parser.parse_string() and the resulting directives list is used. If allow_incomplete is True, light-weight booking is performed before comparing the directive lists, allowing to compare transactions with incomplete postings. Args: subset_entries: Subset entries. entries: Entries. allow_incomplete: Perform booking before comparison. Raises: AssertionError: If the exception fails. \"\"\" subset_entries = read_string_or_entries ( subset_entries , allow_incomplete ) entries = read_string_or_entries ( entries ) excludes , extra = compare . excludes_entries ( subset_entries , entries ) if not excludes : assert extra , \"Extra is empty: {} \" . format ( extra ) oss = io . StringIO () if extra : oss . write ( \"Extra from from first/excluded set: \\n\\n \" ) for entry in extra : oss . write ( printer . format_entry ( entry )) oss . write ( \" \\n \" ) self . fail ( oss . getvalue ()) beancount . parser . cmptest . TestCase . assertIncludesEntries ( self , subset_entries , entries , allow_incomplete = False ) \uf0c1 Check that subset_entries is included in entries. Entries can be provided either as a list of directives or as a string. In the latter case, the string is parsed with beancount.parser.parse_string() and the resulting directives list is used. If allow_incomplete is True, light-weight booking is performed before comparing the directive lists, allowing to compare transactions with incomplete postings. Parameters: subset_entries \u2013 Subset entries. entries \u2013 Entries. allow_incomplete \u2013 Perform booking before comparison. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/parser/cmptest.py def assertIncludesEntries ( self , subset_entries , entries , allow_incomplete = False ): \"\"\"Check that subset_entries is included in entries. Entries can be provided either as a list of directives or as a string. In the latter case, the string is parsed with beancount.parser.parse_string() and the resulting directives list is used. If allow_incomplete is True, light-weight booking is performed before comparing the directive lists, allowing to compare transactions with incomplete postings. Args: subset_entries: Subset entries. entries: Entries. allow_incomplete: Perform booking before comparison. Raises: AssertionError: If the exception fails. \"\"\" subset_entries = read_string_or_entries ( subset_entries , allow_incomplete ) entries = read_string_or_entries ( entries ) includes , missing = compare . includes_entries ( subset_entries , entries ) if not includes : assert missing , \"Missing is empty: {} \" . format ( missing ) oss = io . StringIO () if missing : oss . write ( \"Missing from from expected set: \\n\\n \" ) for entry in missing : oss . write ( printer . format_entry ( entry )) oss . write ( \" \\n \" ) self . fail ( oss . getvalue ()) beancount.parser.cmptest.TestError ( Exception ) \uf0c1 Errors within the test implementation itself. These should never occur. beancount . parser . cmptest . read_string_or_entries ( entries_or_str , allow_incomplete = False ) \uf0c1 Read a string of entries or just entries. Parameters: entries_or_str \u2013 Either a list of directives, or a string containing directives. allow_incomplete \u2013 A boolean, true if we allow incomplete inputs and perform light-weight booking. Returns: A list of directives. Source code in beancount/parser/cmptest.py def read_string_or_entries ( entries_or_str , allow_incomplete = False ): \"\"\"Read a string of entries or just entries. Args: entries_or_str: Either a list of directives, or a string containing directives. allow_incomplete: A boolean, true if we allow incomplete inputs and perform light-weight booking. Returns: A list of directives. \"\"\" if isinstance ( entries_or_str , str ): entries , errors , options_map = parser . parse_string ( textwrap . dedent ( entries_or_str )) if allow_incomplete : # Do a simplistic local conversion in order to call the comparison. entries = [ _local_booking ( entry ) for entry in entries ] else : # Don't accept incomplete entries either. if any ( parser . is_entry_incomplete ( entry ) for entry in entries ): raise TestError ( \"Entries in assertions may not use interpolation.\" ) entries , booking_errors = booking . book ( entries , options_map ) errors = errors + booking_errors # Don't tolerate errors. if errors : oss = io . StringIO () printer . print_errors ( errors , file = oss ) raise TestError ( \"Unexpected errors in expected: {} \" . format ( oss . getvalue ())) else : assert isinstance ( entries_or_str , list ), \"Expecting list: {} \" . format ( entries_or_str ) entries = entries_or_str return entries beancount.parser.context \uf0c1 Produce a rendering of the account balances just before and after a particular entry is applied. beancount . parser . context . render_entry_context ( entries , options_map , entry , parsed_entry = None ) \uf0c1 Render the context before and after a particular transaction is applied. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. entry \u2013 The entry instance which should be rendered. (Note that this object is expected to be in the set of entries, not just structurally equal.) parsed_entry \u2013 An optional incomplete, parsed but not booked nor interpolated entry. If this is provided, this is used for inspecting the list of prior accounts and it is also rendered. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. Source code in beancount/parser/context.py def render_entry_context ( entries , options_map , entry , parsed_entry = None ): \"\"\"Render the context before and after a particular transaction is applied. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. entry: The entry instance which should be rendered. (Note that this object is expected to be in the set of entries, not just structurally equal.) parsed_entry: An optional incomplete, parsed but not booked nor interpolated entry. If this is provided, this is used for inspecting the list of prior accounts and it is also rendered. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. \"\"\" oss = io . StringIO () pr = functools . partial ( print , file = oss ) header = \"** {} --------------------------------\" meta = entry . meta pr ( header . format ( \"Transaction Id\" )) pr () pr ( \"Hash: {} \" . format ( compare . hash_entry ( entry ))) pr ( \"Location: {} : {} \" . format ( meta [ \"filename\" ], meta [ \"lineno\" ])) pr () pr () # Get the list of accounts sorted by the order in which they appear in the # closest entry. order = {} if parsed_entry is None : parsed_entry = entry if isinstance ( parsed_entry , data . Transaction ): order = { posting . account : index for index , posting in enumerate ( parsed_entry . postings ) } accounts = sorted ( getters . get_entry_accounts ( parsed_entry ), key = lambda account : order . get ( account , 10000 ), ) # Accumulate the balances of these accounts up to the entry. balance_before , balance_after = interpolate . compute_entry_context ( entries , entry , additional_accounts = accounts ) # Create a format line for printing the contents of account balances. max_account_width = max ( map ( len , accounts )) if accounts else 1 position_line = \"{{:1}} {{: {width} }} {{:>49}}\" . format ( width = max_account_width ) # Print the context before. pr ( header . format ( \"Balances before transaction\" )) pr () before_hashes = set () average_costs = {} for account in accounts : balance = balance_before [ account ] pc_balances = balance . split () for currency , pc_balance in pc_balances . items (): if len ( pc_balance ) > 1 : average_costs [ account ] = pc_balance . average () positions = balance . get_positions () for position in positions : before_hashes . add (( account , hash ( position ))) pr ( position_line . format ( \"\" , account , str ( position ))) if not positions : pr ( position_line . format ( \"\" , account , \"\" )) pr () pr () # Print average cost per account, if relevant. if average_costs : pr ( header . format ( \"Average Costs\" )) pr () for account , average_cost in sorted ( average_costs . items ()): for position in average_cost : pr ( position_line . format ( \"\" , account , str ( position ))) pr () pr () # Print the entry itself. dcontext = options_map [ \"dcontext\" ] pr ( header . format ( \"Unbooked Transaction\" )) pr () if parsed_entry : printer . print_entry ( parsed_entry , dcontext , render_weights = True , file = oss ) pr () pr ( header . format ( \"Transaction\" )) pr () printer . print_entry ( entry , dcontext , render_weights = True , file = oss ) pr () if isinstance ( entry , data . Transaction ): pr ( header . format ( \"Residual and Tolerances\" )) pr () # Print residuals. residual = interpolate . compute_residual ( entry . postings ) if not residual . is_empty (): # Note: We render the residual at maximum precision, for debugging. pr ( \"Residual: {} \" . format ( residual )) # Dump the tolerances used. tolerances = interpolate . infer_tolerances ( entry . postings , options_map ) if tolerances : pr ( \"Tolerances: {} \" . format ( \", \" . join ( \" {} = {} \" . format ( key , value ) for key , value in sorted ( tolerances . items ()) ) ) ) # Compute the total cost basis. cost_basis = inventory . Inventory ( pos for pos in entry . postings if pos . cost is not None ) . reduce ( convert . get_cost ) if not cost_basis . is_empty (): pr ( \"Basis: {} \" . format ( cost_basis )) pr () pr () # Print the context after. pr ( header . format ( \"Balances after transaction\" )) pr () for account in accounts : positions = balance_after [ account ] . get_positions () for position in positions : changed = ( account , hash ( position )) not in before_hashes print ( position_line . format ( \"*\" if changed else \"\" , account , str ( position )), file = oss , ) if not positions : pr ( position_line . format ( \"\" , account , \"\" )) pr () return oss . getvalue () beancount . parser . context . render_file_context ( entries , options_map , filename , lineno ) \uf0c1 Render the context before and after a particular transaction is applied. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. filename \u2013 A string, the name of the file from which the transaction was parsed. lineno \u2013 An integer, the line number in the file the transaction was parsed from. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. Source code in beancount/parser/context.py def render_file_context ( entries , options_map , filename , lineno ): \"\"\"Render the context before and after a particular transaction is applied. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. filename: A string, the name of the file from which the transaction was parsed. lineno: An integer, the line number in the file the transaction was parsed from. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. \"\"\" # Find the closest entry. closest_entry = data . find_closest ( entries , filename , lineno ) if closest_entry is None : raise SystemExit ( \"No entry could be found before {} : {} \" . format ( filename , lineno )) # Run just the parser stage (no booking nor interpolation, which would # remove the postings) on the input file to produced the corresponding # unbooked transaction, so that we can get the list of accounts. if path . exists ( filename ): parsed_entries , _ , __ = parser . parse_file ( filename ) # Note: We cannot bisect as we cannot rely on sorting behavior from the parser. lineno = closest_entry . meta [ \"lineno\" ] closest_parsed_entries = [ parsed_entry for parsed_entry in parsed_entries if parsed_entry . meta [ \"lineno\" ] == lineno ] if len ( closest_parsed_entries ) != 1 : # This is an internal error, this should never occur. raise RuntimeError ( \"Parsed entry corresponding to real entry not found in original filename.\" ) closest_parsed_entry = next ( iter ( closest_parsed_entries )) else : closest_parsed_entry = None return render_entry_context ( entries , options_map , closest_entry , closest_parsed_entry ) beancount.parser.grammar \uf0c1 Builder for Beancount grammar. beancount.parser.grammar.Builder ( LexBuilder ) \uf0c1 A builder used by the lexer and grammar parser as callbacks to create the data objects corresponding to rules parsed from the input file. beancount . parser . grammar . Builder . account ( self , filename , lineno , account ) \uf0c1 Check account name validity. Parameters: account \u2013 a str, the account name. Returns: A string, the account name. Source code in beancount/parser/grammar.py def account ( self , filename , lineno , account ): \"\"\"Check account name validity. Args: account: a str, the account name. Returns: A string, the account name. \"\"\" if not self . account_regexp . match ( account ): meta = new_metadata ( filename , lineno ) self . errors . append ( ParserError ( meta , \"Invalid account name: {} \" . format ( account ), None ) ) # Intern account names. This should reduces memory usage a # fair bit because these strings are repeated liberally. return self . accounts . setdefault ( account , account ) beancount . parser . grammar . Builder . amount ( self , filename , lineno , number , currency ) \uf0c1 Process an amount grammar rule. Parameters: number \u2013 a Decimal instance, the number of the amount. currency \u2013 a currency object (a str, really, see CURRENCY above) Returns: An instance of Amount. Source code in beancount/parser/grammar.py def amount ( self , filename , lineno , number , currency ): \"\"\"Process an amount grammar rule. Args: number: a Decimal instance, the number of the amount. currency: a currency object (a str, really, see CURRENCY above) Returns: An instance of Amount. \"\"\" # Update the mapping that stores the parsed precisions. # Note: This is relatively slow, adds about 70ms because of number.as_tuple(). self . _dcupdate ( number , currency ) return Amount ( number , currency ) beancount . parser . grammar . Builder . balance ( self , filename , lineno , date , account , amount , tolerance , kvlist ) \uf0c1 Process an assertion directive. We produce no errors here by default. We replace the failing ones in the routine that does the verification later one, that these have succeeded or failed. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account to balance. amount \u2013 The expected amount, to be checked. tolerance \u2013 The tolerance number. kvlist \u2013 a list of KeyValue instances. Returns: A new Balance object. Source code in beancount/parser/grammar.py def balance ( self , filename , lineno , date , account , amount , tolerance , kvlist ): \"\"\"Process an assertion directive. We produce no errors here by default. We replace the failing ones in the routine that does the verification later one, that these have succeeded or failed. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to balance. amount: The expected amount, to be checked. tolerance: The tolerance number. kvlist: a list of KeyValue instances. Returns: A new Balance object. \"\"\" diff_amount = None meta = new_metadata ( filename , lineno , kvlist ) return Balance ( meta , date , account , amount , tolerance , diff_amount ) beancount . parser . grammar . Builder . build_grammar_error ( self , filename , lineno , exc_value , exc_type = None , exc_traceback = None ) \uf0c1 Build a grammar error and appends it to the list of pending errors. Parameters: filename \u2013 The current filename lineno \u2013 The current line number excvalue \u2013 The exception value, or a str, the message of the error. exc_type \u2013 An exception type, if an exception occurred. exc_traceback \u2013 A traceback object. Source code in beancount/parser/grammar.py def build_grammar_error ( self , filename , lineno , exc_value , exc_type = None , exc_traceback = None ): \"\"\"Build a grammar error and appends it to the list of pending errors. Args: filename: The current filename lineno: The current line number excvalue: The exception value, or a str, the message of the error. exc_type: An exception type, if an exception occurred. exc_traceback: A traceback object. \"\"\" if exc_type is not None : assert not isinstance ( exc_value , str ) strings = traceback . format_exception_only ( exc_type , exc_value ) tblist = traceback . extract_tb ( exc_traceback ) filename , lineno , _ , __ = tblist [ 0 ] message = \" {} ( {} : {} )\" . format ( strings [ 0 ], filename , lineno ) else : message = str ( exc_value ) meta = new_metadata ( filename , lineno ) self . errors . append ( ParserSyntaxError ( meta , message , None )) beancount . parser . grammar . Builder . close ( self , filename , lineno , date , account , kvlist ) \uf0c1 Process a close directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the name of the account. kvlist \u2013 a list of KeyValue instances. Returns: A new Close object. Source code in beancount/parser/grammar.py def close ( self , filename , lineno , date , account , kvlist ): \"\"\"Process a close directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the name of the account. kvlist: a list of KeyValue instances. Returns: A new Close object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Close ( meta , date , account ) beancount . parser . grammar . Builder . commodity ( self , filename , lineno , date , currency , kvlist ) \uf0c1 Process a close directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. currency \u2013 A string, the commodity being declared. kvlist \u2013 a list of KeyValue instances. Returns: A new Close object. Source code in beancount/parser/grammar.py def commodity ( self , filename , lineno , date , currency , kvlist ): \"\"\"Process a close directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. currency: A string, the commodity being declared. kvlist: a list of KeyValue instances. Returns: A new Close object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Commodity ( meta , date , currency ) beancount . parser . grammar . Builder . compound_amount ( self , filename , lineno , number_per , number_total , currency ) \uf0c1 Process an amount grammar rule. Parameters: number_per \u2013 a Decimal instance, the number of the cost per share. number_total \u2013 a Decimal instance, the number of the cost over all shares. currency \u2013 a currency object (a str, really, see CURRENCY above) Returns: A triple of (Decimal, Decimal, currency string) to be processed further when creating the final per-unit cost number. Source code in beancount/parser/grammar.py def compound_amount ( self , filename , lineno , number_per , number_total , currency ): \"\"\"Process an amount grammar rule. Args: number_per: a Decimal instance, the number of the cost per share. number_total: a Decimal instance, the number of the cost over all shares. currency: a currency object (a str, really, see CURRENCY above) Returns: A triple of (Decimal, Decimal, currency string) to be processed further when creating the final per-unit cost number. \"\"\" # Update the mapping that stores the parsed precisions. # Note: This is relatively slow, adds about 70ms because of number.as_tuple(). self . _dcupdate ( number_per , currency ) self . _dcupdate ( number_total , currency ) # Note that we are not able to reduce the value to a number per-share # here because we only get the number of units in the full lot spec. return CompoundAmount ( number_per , number_total , currency ) beancount . parser . grammar . Builder . cost_merge ( self , filename , lineno , _ ) \uf0c1 Create a 'merge cost' token. Source code in beancount/parser/grammar.py def cost_merge ( self , filename , lineno , _ ): \"\"\"Create a 'merge cost' token.\"\"\" return MERGE_COST beancount . parser . grammar . Builder . cost_spec ( self , filename , lineno , cost_comp_list , is_total ) \uf0c1 Process a cost_spec grammar rule. Parameters: cost_comp_list \u2013 A list of CompoundAmount, a datetime.date, or label ID strings. is_total \u2013 Assume only the total cost is specified; reject the # syntax, that is, no compound amounts may be specified. This is used to support the {{...}} syntax. Returns: A cost-info tuple of CompoundAmount, lot date and label string. Any of these may be set to a sentinel indicating \"unset\". Source code in beancount/parser/grammar.py def cost_spec ( self , filename , lineno , cost_comp_list , is_total ): \"\"\"Process a cost_spec grammar rule. Args: cost_comp_list: A list of CompoundAmount, a datetime.date, or label ID strings. is_total: Assume only the total cost is specified; reject the # syntax, that is, no compound amounts may be specified. This is used to support the {{...}} syntax. Returns: A cost-info tuple of CompoundAmount, lot date and label string. Any of these may be set to a sentinel indicating \"unset\". \"\"\" if not cost_comp_list : return CostSpec ( MISSING , None , MISSING , None , None , False ) assert isinstance ( cost_comp_list , list ), \"Internal error in parser: {} \" . format ( cost_comp_list ) compound_cost = None date_ = None label = None merge = None for comp in cost_comp_list : if isinstance ( comp , CompoundAmount ): if compound_cost is None : compound_cost = comp else : self . errors . append ( ParserError ( new_metadata ( filename , lineno ), \"Duplicate cost: ' {} '.\" . format ( comp ), None , ) ) elif isinstance ( comp , date ): if date_ is None : date_ = comp else : self . errors . append ( ParserError ( new_metadata ( filename , lineno ), \"Duplicate date: ' {} '.\" . format ( comp ), None , ) ) elif comp is MERGE_COST : if merge is None : merge = True self . errors . append ( ParserError ( new_metadata ( filename , lineno ), \"Cost merging is not supported yet\" , None , ) ) else : self . errors . append ( ParserError ( new_metadata ( filename , lineno ), \"Duplicate merge-cost spec\" , None , ) ) else : assert isinstance ( comp , str ), \"Currency component is not string: ' {} '\" . format ( comp ) if label is None : label = comp else : self . errors . append ( ParserError ( new_metadata ( filename , lineno ), \"Duplicate label: ' {} '.\" . format ( comp ), None , ) ) # If there was a cost_comp_list, thus a \"{...}\" cost basis spec, you must # indicate that by creating a CompoundAmount(), always. if compound_cost is None : number_per , number_total , currency = MISSING , None , MISSING else : number_per , number_total , currency = compound_cost if is_total : if number_total is not None : self . errors . append ( ParserError ( new_metadata ( filename , lineno ), ( \"Per-unit cost may not be specified using total cost \" \"syntax: ' {} '; ignoring per-unit cost\" ) . format ( compound_cost ), None , ) ) # Ignore per-unit number. number_per = ZERO else : # There's a single number specified; interpret it as a total cost. number_total = number_per number_per = ZERO if merge is None : merge = False return CostSpec ( number_per , number_total , currency , date_ , label , merge ) beancount . parser . grammar . Builder . custom ( self , filename , lineno , date , dir_type , custom_values , kvlist ) \uf0c1 Process a custom directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. dir_type \u2013 A string, a type for the custom directive being parsed. custom_values \u2013 A list of the various tokens seen on the same line. kvlist \u2013 a list of KeyValue instances. Returns: A new Custom object. Source code in beancount/parser/grammar.py def custom ( self , filename , lineno , date , dir_type , custom_values , kvlist ): \"\"\"Process a custom directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. dir_type: A string, a type for the custom directive being parsed. custom_values: A list of the various tokens seen on the same line. kvlist: a list of KeyValue instances. Returns: A new Custom object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Custom ( meta , date , dir_type , custom_values ) beancount . parser . grammar . Builder . custom_value ( self , filename , lineno , value , dtype = None ) \uf0c1 Create a custom value object, along with its type. Parameters: value \u2013 One of the accepted custom values. Returns: A pair of (value, dtype) where 'dtype' is the datatype is that of the value. Source code in beancount/parser/grammar.py def custom_value ( self , filename , lineno , value , dtype = None ): \"\"\"Create a custom value object, along with its type. Args: value: One of the accepted custom values. Returns: A pair of (value, dtype) where 'dtype' is the datatype is that of the value. \"\"\" if dtype is None : dtype = type ( value ) return ValueType ( value , dtype ) beancount . parser . grammar . Builder . document ( self , filename , lineno , date , account , document_filename , tags_links , kvlist ) \uf0c1 Process a document directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. account \u2013 an Account instance. document_filename \u2013 a str, the name of the document file. tags_links \u2013 The current TagsLinks accumulator. kvlist \u2013 a list of KeyValue instances. Returns: A new Document object. Source code in beancount/parser/grammar.py def document ( self , filename , lineno , date , account , document_filename , tags_links , kvlist ): \"\"\"Process a document directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. account: an Account instance. document_filename: a str, the name of the document file. tags_links: The current TagsLinks accumulator. kvlist: a list of KeyValue instances. Returns: A new Document object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) if not path . isabs ( document_filename ): document_filename = path . abspath ( path . join ( path . dirname ( filename ), document_filename ) ) tags , links = self . _finalize_tags_links ( tags_links . tags , tags_links . links ) return Document ( meta , date , account , document_filename , tags , links ) beancount . parser . grammar . Builder . event ( self , filename , lineno , date , event_type , description , kvlist ) \uf0c1 Process an event directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. event_type \u2013 a str, the name of the event type. description \u2013 a str, the event value, the contents. kvlist \u2013 a list of KeyValue instances. Returns: A new Event object. Source code in beancount/parser/grammar.py def event ( self , filename , lineno , date , event_type , description , kvlist ): \"\"\"Process an event directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. event_type: a str, the name of the event type. description: a str, the event value, the contents. kvlist: a list of KeyValue instances. Returns: A new Event object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Event ( meta , date , event_type , description ) beancount . parser . grammar . Builder . finalize ( self ) \uf0c1 Finalize the parser, check for final errors and return the triple. Returns: A triple of entries \u2013 A list of parsed directives, which may need completion. errors: A list of errors, hopefully empty. options_map: A dict of options. Source code in beancount/parser/grammar.py def finalize ( self ): \"\"\"Finalize the parser, check for final errors and return the triple. Returns: A triple of entries: A list of parsed directives, which may need completion. errors: A list of errors, hopefully empty. options_map: A dict of options. \"\"\" # If the user left some tags unbalanced, issue an error. for tag in self . tags : meta = new_metadata ( self . options [ \"filename\" ], 0 ) self . errors . append ( ParserError ( meta , \"Unbalanced pushed tag: ' {} '\" . format ( tag ), None ) ) # If the user left some metadata unpopped, issue an error. for key , value_list in self . meta . items (): meta = new_metadata ( self . options [ \"filename\" ], 0 ) self . errors . append ( ParserError ( meta , ( \"Unbalanced metadata key ' {} '; leftover metadata ' {} '\" ) . format ( key , \", \" . join ( value_list ) ), None , ) ) # Weave the commas option in the DisplayContext itself, so it propagates # everywhere it is used automatically. self . dcontext . set_commas ( self . options [ \"render_commas\" ]) return ( self . get_entries (), self . errors , self . get_options ()) beancount . parser . grammar . Builder . get_entries ( self ) \uf0c1 Return the accumulated entries. Returns: A list of sorted directives. Source code in beancount/parser/grammar.py def get_entries ( self ): \"\"\"Return the accumulated entries. Returns: A list of sorted directives. \"\"\" return sorted ( self . entries , key = data . entry_sortkey ) beancount . parser . grammar . Builder . get_long_string_maxlines ( self ) \uf0c1 See base class. Source code in beancount/parser/grammar.py def get_long_string_maxlines ( self ): \"\"\"See base class.\"\"\" return self . options [ \"long_string_maxlines\" ] beancount . parser . grammar . Builder . get_options ( self ) \uf0c1 Return the final options map. Returns: A dict of option names to options. Source code in beancount/parser/grammar.py def get_options ( self ): \"\"\"Return the final options map. Returns: A dict of option names to options. \"\"\" # Build and store the inferred DisplayContext instance. self . options [ \"dcontext\" ] = self . dcontext return self . options beancount . parser . grammar . Builder . handle_list ( self , filename , lineno , object_list , new_object ) \uf0c1 Handle a recursive list grammar rule, generically. Parameters: object_list \u2013 the current list of objects. new_object \u2013 the new object to be added. Returns: The new, updated list of objects. Source code in beancount/parser/grammar.py def handle_list ( self , filename , lineno , object_list , new_object ): \"\"\"Handle a recursive list grammar rule, generically. Args: object_list: the current list of objects. new_object: the new object to be added. Returns: The new, updated list of objects. \"\"\" if object_list is None : object_list = [] if new_object is not None : object_list . append ( new_object ) return object_list beancount . parser . grammar . Builder . include ( self , filename , lineno , include_filename ) \uf0c1 Process an include directive. Parameters: filename \u2013 current filename. lineno \u2013 current line number. include_name \u2013 A string, the name of the file to include. Source code in beancount/parser/grammar.py def include ( self , filename , lineno , include_filename ): \"\"\"Process an include directive. Args: filename: current filename. lineno: current line number. include_name: A string, the name of the file to include. \"\"\" self . options [ \"include\" ] . append ( include_filename ) beancount . parser . grammar . Builder . key_value ( self , filename , lineno , key , value ) \uf0c1 Process a document directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account the document relates to. document_filename \u2013 A str, the name of the document file. Returns: A new KeyValue object. Source code in beancount/parser/grammar.py def key_value ( self , filename , lineno , key , value ): \"\"\"Process a document directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account the document relates to. document_filename: A str, the name of the document file. Returns: A new KeyValue object. \"\"\" return KeyValue ( key , value ) beancount . parser . grammar . Builder . note ( self , filename , lineno , date , account , comment , tags_links , kvlist ) \uf0c1 Process a note directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account to attach the note to. comment \u2013 A str, the note's comments contents. kvlist \u2013 a list of KeyValue instances. Returns: A new Note object. Source code in beancount/parser/grammar.py def note ( self , filename , lineno , date , account , comment , tags_links , kvlist ): \"\"\"Process a note directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to attach the note to. comment: A str, the note's comments contents. kvlist: a list of KeyValue instances. Returns: A new Note object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) tags , links = self . _finalize_tags_links ( tags_links . tags , tags_links . links ) return Note ( meta , date , account , comment , tags , links ) beancount . parser . grammar . Builder . open ( self , filename , lineno , date , account , currencies , booking_str , kvlist ) \uf0c1 Process an open directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the name of the account. currencies \u2013 A list of constraint currencies. booking_str \u2013 A string, the booking method, or None if none was specified. kvlist \u2013 a list of KeyValue instances. Returns: A new Open object. Source code in beancount/parser/grammar.py def open ( self , filename , lineno , date , account , currencies , booking_str , kvlist ): \"\"\"Process an open directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the name of the account. currencies: A list of constraint currencies. booking_str: A string, the booking method, or None if none was specified. kvlist: a list of KeyValue instances. Returns: A new Open object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) error = False if booking_str : try : # Note: Somehow the 'in' membership operator is not defined on Enum. booking = Booking [ booking_str ] except KeyError : # If the per-account method is invalid, set it to the global # default method and continue. booking = self . options [ \"booking_method\" ] error = True else : booking = None entry = Open ( meta , date , account , currencies , booking ) if error : self . errors . append ( ParserError ( meta , \"Invalid booking method: {} \" . format ( booking_str ), entry ) ) return entry beancount . parser . grammar . Builder . option ( self , filename , lineno , key , value ) \uf0c1 Process an option directive. Parameters: filename \u2013 current filename. lineno \u2013 current line number. key \u2013 option's key (str) value \u2013 option's value Source code in beancount/parser/grammar.py def option ( self , filename , lineno , key , value ): \"\"\"Process an option directive. Args: filename: current filename. lineno: current line number. key: option's key (str) value: option's value \"\"\" if key not in self . options : meta = new_metadata ( filename , lineno ) self . errors . append ( ParserError ( meta , \"Invalid option: ' {} '\" . format ( key ), None )) elif key in options . READ_ONLY_OPTIONS : meta = new_metadata ( filename , lineno ) self . errors . append ( ParserError ( meta , \"Option ' {} ' may not be set\" . format ( key ), None ) ) else : option_descriptor = options . OPTIONS [ key ] # Issue a warning if the option is deprecated. if option_descriptor . deprecated : assert isinstance ( option_descriptor . deprecated , str ), \"Internal error.\" meta = new_metadata ( filename , lineno ) self . errors . append ( DeprecatedError ( meta , option_descriptor . deprecated , None ) ) # Rename the option if it has an alias. if option_descriptor . alias : key = option_descriptor . alias option_descriptor = options . OPTIONS [ key ] # Convert the value, if necessary. if option_descriptor . converter : try : value = option_descriptor . converter ( value ) except ValueError as exc : meta = new_metadata ( filename , lineno ) self . errors . append ( ParserError ( meta , \"Error for option ' {} ': {} \" . format ( key , exc ), None ) ) return option = self . options [ key ] if isinstance ( option , list ): # Append to a list of values. option . append ( value ) elif isinstance ( option , dict ): # Set to a dict of values. if not ( isinstance ( value , tuple ) and len ( value ) == 2 ): self . errors . append ( ParserError ( meta , \"Error for option ' {} ': {} \" . format ( key , value ), None ) ) return dict_key , dict_value = value option [ dict_key ] = dict_value elif isinstance ( option , bool ): # Convert to a boolean. if not isinstance ( value , bool ): value = ( value . lower () in { \"true\" , \"on\" }) or ( value == \"1\" ) self . options [ key ] = value else : # Set the value. self . options [ key ] = value # Refresh the list of valid account regexps as we go along. if key . startswith ( \"name_\" ): # Update the set of valid account types. self . account_regexp = valid_account_regexp ( self . options ) beancount . parser . grammar . Builder . pad ( self , filename , lineno , date , account , source_account , kvlist ) \uf0c1 Process a pad directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account to be padded. source_account \u2013 A string, the account to pad from. kvlist \u2013 a list of KeyValue instances. Returns: A new Pad object. Source code in beancount/parser/grammar.py def pad ( self , filename , lineno , date , account , source_account , kvlist ): \"\"\"Process a pad directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to be padded. source_account: A string, the account to pad from. kvlist: a list of KeyValue instances. Returns: A new Pad object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Pad ( meta , date , account , source_account ) beancount . parser . grammar . Builder . pipe_deprecated_error ( self , filename , lineno ) \uf0c1 Issue a 'Pipe deprecated' error. Parameters: filename \u2013 The current filename lineno \u2013 The current line number Source code in beancount/parser/grammar.py def pipe_deprecated_error ( self , filename , lineno ): \"\"\"Issue a 'Pipe deprecated' error. Args: filename: The current filename lineno: The current line number \"\"\" if self . options [ \"allow_pipe_separator\" ]: return meta = new_metadata ( filename , lineno ) self . errors . append ( ParserSyntaxError ( meta , \"Pipe symbol is deprecated.\" , None )) beancount . parser . grammar . Builder . plugin ( self , filename , lineno , plugin_name , plugin_config ) \uf0c1 Process a plugin directive. Parameters: filename \u2013 current filename. lineno \u2013 current line number. plugin_name \u2013 A string, the name of the plugin module to import. plugin_config \u2013 A string or None, an optional configuration string to pass in to the plugin module. Source code in beancount/parser/grammar.py def plugin ( self , filename , lineno , plugin_name , plugin_config ): \"\"\"Process a plugin directive. Args: filename: current filename. lineno: current line number. plugin_name: A string, the name of the plugin module to import. plugin_config: A string or None, an optional configuration string to pass in to the plugin module. \"\"\" self . options [ \"plugin\" ] . append (( plugin_name , plugin_config )) beancount . parser . grammar . Builder . popmeta ( self , filename , lineno , key ) \uf0c1 Removed a key off the current set of stacks. Parameters: key \u2013 A string, a key to be removed from the meta dict. Source code in beancount/parser/grammar.py def popmeta ( self , filename , lineno , key ): \"\"\"Removed a key off the current set of stacks. Args: key: A string, a key to be removed from the meta dict. \"\"\" try : if key not in self . meta : raise IndexError value_list = self . meta [ key ] value_list . pop ( - 1 ) if not value_list : self . meta . pop ( key ) except IndexError : meta = new_metadata ( filename , lineno ) self . errors . append ( ParserError ( meta , \"Attempting to pop absent metadata key: ' {} '\" . format ( key ), None ) ) beancount . parser . grammar . Builder . poptag ( self , filename , lineno , tag ) \uf0c1 Pop a tag off the current set of stacks. Parameters: tag \u2013 A string, a tag to be removed from the current set of tags. Source code in beancount/parser/grammar.py def poptag ( self , filename , lineno , tag ): \"\"\"Pop a tag off the current set of stacks. Args: tag: A string, a tag to be removed from the current set of tags. \"\"\" try : self . tags . remove ( tag ) except ValueError : meta = new_metadata ( filename , lineno ) self . errors . append ( ParserError ( meta , \"Attempting to pop absent tag: ' {} '\" . format ( tag ), None ) ) beancount . parser . grammar . Builder . posting ( self , filename , lineno , account , units , cost , price , istotal , flag ) \uf0c1 Process a posting grammar rule. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. account \u2013 A string, the account of the posting. units \u2013 An instance of Amount for the units. cost \u2013 An instance of CostSpec for the cost. price \u2013 Either None, or an instance of Amount that is the cost of the position. istotal \u2013 A bool, True if the price is for the total amount being parsed, or False if the price is for each lot of the position. flag \u2013 A string, one-character, the flag associated with this posting. Returns: A new Posting object, with no parent entry. Source code in beancount/parser/grammar.py def posting ( self , filename , lineno , account , units , cost , price , istotal , flag ): \"\"\"Process a posting grammar rule. Args: filename: the current filename. lineno: the current line number. account: A string, the account of the posting. units: An instance of Amount for the units. cost: An instance of CostSpec for the cost. price: Either None, or an instance of Amount that is the cost of the position. istotal: A bool, True if the price is for the total amount being parsed, or False if the price is for each lot of the position. flag: A string, one-character, the flag associated with this posting. Returns: A new Posting object, with no parent entry. \"\"\" meta = new_metadata ( filename , lineno ) # Prices may not be negative. if price and isinstance ( price . number , Decimal ) and price . number < ZERO : self . errors . append ( ParserError ( meta , ( \"Negative prices are not allowed: {} \" \"(see http://furius.ca/beancount/doc/bug-negative-prices \" \"for workaround)\" ) . format ( price ), None , ) ) # Fix it and continue. price = Amount ( abs ( price . number ), price . currency ) # If the price is specified for the entire amount, compute the effective # price here and forget about that detail of the input syntax. if istotal : if units . number is MISSING : # Note: we could potentially do a better job and attempt to fix # this up after interpolation, but this syntax is pretty rare # anyway. self . errors . append ( ParserError ( meta , ( \"Total price on a posting without units: {} .\" ) . format ( price ), None , ) ) price = None else : price_number = price . number if price_number is not MISSING : price_number = ( ZERO if units . number == ZERO else price_number / abs ( units . number ) ) price = Amount ( price_number , price . currency ) # Note: Allow zero prices because we need them for round-trips for # conversion entries. # # if price is not None and price.number == ZERO: # self.errors.append( # ParserError(meta, \"Price is zero: {}\".format(price), None)) # If both cost and price are specified, the currencies must match, or # that is an error. if ( cost is not None and price is not None and isinstance ( cost . currency , str ) and isinstance ( price . currency , str ) and cost . currency != price . currency ): self . errors . append ( ParserError ( meta , \"Cost and price currencies must match: {} != {} \" . format ( cost . currency , price . currency ), None , ) ) return Posting ( account , units , cost , price , chr ( flag ) if flag else None , meta ) beancount . parser . grammar . Builder . price ( self , filename , lineno , date , currency , amount , kvlist ) \uf0c1 Process a price directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. currency \u2013 the currency to be priced. amount \u2013 an instance of Amount, that is the price of the currency. kvlist \u2013 a list of KeyValue instances. Returns: A new Price object. Source code in beancount/parser/grammar.py def price ( self , filename , lineno , date , currency , amount , kvlist ): \"\"\"Process a price directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. currency: the currency to be priced. amount: an instance of Amount, that is the price of the currency. kvlist: a list of KeyValue instances. Returns: A new Price object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Price ( meta , date , currency , amount ) beancount . parser . grammar . Builder . pushmeta ( self , filename , lineno , key_value ) \uf0c1 Set a metadata field on the current key-value pairs to be added to transactions. Parameters: key_value \u2013 A KeyValue instance, to be added to the dict of metadata. Source code in beancount/parser/grammar.py def pushmeta ( self , filename , lineno , key_value ): \"\"\"Set a metadata field on the current key-value pairs to be added to transactions. Args: key_value: A KeyValue instance, to be added to the dict of metadata. \"\"\" key , value = key_value self . meta [ key ] . append ( value ) beancount . parser . grammar . Builder . pushtag ( self , filename , lineno , tag ) \uf0c1 Push a tag on the current set of tags. Note that this does not need to be stack ordered. Parameters: tag \u2013 A string, a tag to be added. Source code in beancount/parser/grammar.py def pushtag ( self , filename , lineno , tag ): \"\"\"Push a tag on the current set of tags. Note that this does not need to be stack ordered. Args: tag: A string, a tag to be added. \"\"\" self . tags . append ( tag ) beancount . parser . grammar . Builder . query ( self , filename , lineno , date , query_name , query_string , kvlist ) \uf0c1 Process a document directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. query_name \u2013 a str, the name of the query. query_string \u2013 a str, the SQL query itself. kvlist \u2013 a list of KeyValue instances. Returns: A new Query object. Source code in beancount/parser/grammar.py def query ( self , filename , lineno , date , query_name , query_string , kvlist ): \"\"\"Process a document directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. query_name: a str, the name of the query. query_string: a str, the SQL query itself. kvlist: a list of KeyValue instances. Returns: A new Query object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Query ( meta , date , query_name , query_string ) beancount . parser . grammar . Builder . store_result ( self , filename , lineno , entries ) \uf0c1 Start rule stores the final result here. Parameters: entries \u2013 A list of entries to store. Source code in beancount/parser/grammar.py def store_result ( self , filename , lineno , entries ): \"\"\"Start rule stores the final result here. Args: entries: A list of entries to store. \"\"\" if entries : self . entries = entries # Also record the name of the processed file. self . options [ \"filename\" ] = filename beancount . parser . grammar . Builder . tag_link_LINK ( self , filename , lineno , tags_links , link ) \uf0c1 Add a link to the TagsLinks accumulator. Parameters: tags_links \u2013 The current TagsLinks accumulator. link \u2013 A string, the new link to insert. Returns: An updated TagsLinks instance. Source code in beancount/parser/grammar.py def tag_link_LINK ( self , filename , lineno , tags_links , link ): \"\"\"Add a link to the TagsLinks accumulator. Args: tags_links: The current TagsLinks accumulator. link: A string, the new link to insert. Returns: An updated TagsLinks instance. \"\"\" tags_links . links . add ( link ) return tags_links beancount . parser . grammar . Builder . tag_link_TAG ( self , filename , lineno , tags_links , tag ) \uf0c1 Add a tag to the TagsLinks accumulator. Parameters: tags_links \u2013 The current TagsLinks accumulator. tag \u2013 A string, the new tag to insert. Returns: An updated TagsLinks instance. Source code in beancount/parser/grammar.py def tag_link_TAG ( self , filename , lineno , tags_links , tag ): \"\"\"Add a tag to the TagsLinks accumulator. Args: tags_links: The current TagsLinks accumulator. tag: A string, the new tag to insert. Returns: An updated TagsLinks instance. \"\"\" tags_links . tags . add ( tag ) return tags_links beancount . parser . grammar . Builder . tag_link_new ( self , filename , lineno ) \uf0c1 Create a new TagsLinks instance. Returns: An instance of TagsLinks, initialized with expected attributes. Source code in beancount/parser/grammar.py def tag_link_new ( self , filename , lineno ): \"\"\"Create a new TagsLinks instance. Returns: An instance of TagsLinks, initialized with expected attributes. \"\"\" return TagsLinks ( set (), set ()) beancount . parser . grammar . Builder . transaction ( self , filename , lineno , date , flag , txn_strings , tags_links , posting_or_kv_list ) \uf0c1 Process a transaction directive. All the postings of the transaction are available at this point, and so the the transaction is balanced here, incomplete postings are completed with the appropriate position, and errors are being accumulated on the builder to be reported later on. This is the main routine that takes up most of the parsing time; be very careful with modifications here, they have an impact on performance. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. flag \u2013 a str, one-character, the flag associated with this transaction. txn_strings \u2013 A list of strings, possibly empty, possibly longer. tags_links \u2013 A TagsLinks namedtuple of tags, and/or links. posting_or_kv_list \u2013 a list of Posting or KeyValue instances, to be inserted in this transaction, or None, if no postings have been declared. Returns: A new Transaction object. Source code in beancount/parser/grammar.py def transaction ( self , filename , lineno , date , flag , txn_strings , tags_links , posting_or_kv_list ): \"\"\"Process a transaction directive. All the postings of the transaction are available at this point, and so the the transaction is balanced here, incomplete postings are completed with the appropriate position, and errors are being accumulated on the builder to be reported later on. This is the main routine that takes up most of the parsing time; be very careful with modifications here, they have an impact on performance. Args: filename: the current filename. lineno: the current line number. date: a datetime object. flag: a str, one-character, the flag associated with this transaction. txn_strings: A list of strings, possibly empty, possibly longer. tags_links: A TagsLinks namedtuple of tags, and/or links. posting_or_kv_list: a list of Posting or KeyValue instances, to be inserted in this transaction, or None, if no postings have been declared. Returns: A new Transaction object. \"\"\" meta = new_metadata ( filename , lineno ) # Separate postings and key-values. explicit_meta = {} postings = [] tags , links = tags_links . tags , tags_links . links if posting_or_kv_list : last_posting = None for posting_or_kv in posting_or_kv_list : if isinstance ( posting_or_kv , Posting ): postings . append ( posting_or_kv ) last_posting = posting_or_kv elif isinstance ( posting_or_kv , TagsLinks ): if postings : self . errors . append ( ParserError ( meta , \"Tags or links not allowed after first \" + \"Posting: {} \" . format ( posting_or_kv ), None , ) ) else : tags . update ( posting_or_kv . tags ) links . update ( posting_or_kv . links ) else : if last_posting is None : value = explicit_meta . setdefault ( posting_or_kv . key , posting_or_kv . value ) if value is not posting_or_kv . value : self . errors . append ( ParserError ( meta , \"Duplicate metadata field on entry: {} \" . format ( posting_or_kv ), None , ) ) else : if last_posting . meta is None : last_posting = last_posting . _replace ( meta = {}) postings . pop ( - 1 ) postings . append ( last_posting ) value = last_posting . meta . setdefault ( posting_or_kv . key , posting_or_kv . value ) if value is not posting_or_kv . value : self . errors . append ( ParserError ( meta , \"Duplicate posting metadata field: {} \" . format ( posting_or_kv ), None , ) ) # Freeze the tags & links or set to default empty values. tags , links = self . _finalize_tags_links ( tags , links ) # Initialize the metadata fields from the set of active values. if self . meta : for key , value_list in self . meta . items (): meta [ key ] = value_list [ - 1 ] # Add on explicitly defined values. if explicit_meta : meta . update ( explicit_meta ) # Unpack the transaction fields. payee_narration = self . _unpack_txn_strings ( txn_strings , meta ) if payee_narration is None : return None payee , narration = payee_narration # We now allow a single posting when its balance is zero, so we # commented out the check below. If a transaction has a single posting # with a non-zero balance, it'll get caught below in the booking code. # # # Detect when a transaction does not have at least two legs. # if postings is None or len(postings) < 2: # self.errors.append( # ParserError(meta, # \"Transaction with only one posting: {}\".format(postings), # None)) # return None # If there are no postings, make sure we insert a list object. if postings is None : postings = [] # Create the transaction. return Transaction ( meta , date , chr ( flag ), payee , narration , tags , links , postings ) beancount.parser.grammar.CompoundAmount ( tuple ) \uf0c1 CompoundAmount(number_per, number_total, currency) beancount . parser . grammar . CompoundAmount . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . grammar . CompoundAmount . __new__ ( _cls , number_per , number_total , currency ) special staticmethod \uf0c1 Create new instance of CompoundAmount(number_per, number_total, currency) beancount . parser . grammar . CompoundAmount . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.parser.grammar.DeprecatedError ( tuple ) \uf0c1 DeprecatedError(source, message, entry) beancount . parser . grammar . DeprecatedError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . grammar . DeprecatedError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of DeprecatedError(source, message, entry) beancount . parser . grammar . DeprecatedError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.parser.grammar.KeyValue ( tuple ) \uf0c1 KeyValue(key, value) beancount . parser . grammar . KeyValue . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . grammar . KeyValue . __new__ ( _cls , key , value ) special staticmethod \uf0c1 Create new instance of KeyValue(key, value) beancount . parser . grammar . KeyValue . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.parser.grammar.ParserError ( tuple ) \uf0c1 ParserError(source, message, entry) beancount . parser . grammar . ParserError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . grammar . ParserError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of ParserError(source, message, entry) beancount . parser . grammar . ParserError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.parser.grammar.ParserSyntaxError ( tuple ) \uf0c1 ParserSyntaxError(source, message, entry) beancount . parser . grammar . ParserSyntaxError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . grammar . ParserSyntaxError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of ParserSyntaxError(source, message, entry) beancount . parser . grammar . ParserSyntaxError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.parser.grammar.TagsLinks ( tuple ) \uf0c1 TagsLinks(tags, links) beancount . parser . grammar . TagsLinks . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . grammar . TagsLinks . __new__ ( _cls , tags , links ) special staticmethod \uf0c1 Create new instance of TagsLinks(tags, links) beancount . parser . grammar . TagsLinks . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.parser.grammar.ValueType ( tuple ) \uf0c1 ValueType(value, dtype) beancount . parser . grammar . ValueType . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . grammar . ValueType . __new__ ( _cls , value , dtype ) special staticmethod \uf0c1 Create new instance of ValueType(value, dtype) beancount . parser . grammar . ValueType . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . parser . grammar . valid_account_regexp ( options ) \uf0c1 Build a regexp to validate account names from the options. Parameters: options \u2013 A dict of options, as per beancount.parser.options. Returns: A string, a regular expression that will match all account names. Source code in beancount/parser/grammar.py def valid_account_regexp ( options ): \"\"\"Build a regexp to validate account names from the options. Args: options: A dict of options, as per beancount.parser.options. Returns: A string, a regular expression that will match all account names. \"\"\" names = map ( options . __getitem__ , ( \"name_assets\" , \"name_liabilities\" , \"name_equity\" , \"name_income\" , \"name_expenses\" ), ) # Replace the first term of the account regular expression with the specific # names allowed under the options configuration. This code is kept in sync # with {5672c7270e1e}. return regex . compile ( \"(?: {} )(?: {}{} )+\" . format ( \"|\" . join ( names ), account . sep , account . ACC_COMP_NAME_RE ) ) beancount.parser.hashsrc \uf0c1 Compute a hash of the source files in order to warn when the source goes out of date. beancount . parser . hashsrc . check_parser_source_files ( parser_module ) \uf0c1 Check the extension module's source hash and issue a warning if the current source differs from that of the module. If the source files aren't located in the Python source directory, ignore the warning, we're probably running this from an installed based, in which case we don't need to check anything (this check is useful only for people running directly from source). Source code in beancount/parser/hashsrc.py def check_parser_source_files ( parser_module : types . ModuleType ): \"\"\"Check the extension module's source hash and issue a warning if the current source differs from that of the module. If the source files aren't located in the Python source directory, ignore the warning, we're probably running this from an installed based, in which case we don't need to check anything (this check is useful only for people running directly from source). \"\"\" parser_source_hash = hash_parser_source_files () if parser_source_hash is None : return if parser_module . SOURCE_HASH and parser_module . SOURCE_HASH != parser_source_hash : warnings . warn ( ( \"The Beancount parser C extension module is out-of-date (' {} ' != ' {} '). \" \"You need to rebuild.\" ) . format ( parser_module . SOURCE_HASH , parser_source_hash ) ) beancount . parser . hashsrc . gen_include () \uf0c1 Generate an include file for the parser source hash. Source code in beancount/parser/hashsrc.py def gen_include (): \"\"\"Generate an include file for the parser source hash.\"\"\" return textwrap . dedent ( \"\"\"\\ #ifndef __BEANCOUNT_PARSER_PARSE_SOURCE_HASH_H__ #define __BEANCOUNT_PARSER_PARSE_SOURCE_HASH_H__ #define PARSER_SOURCE_HASH {source_hash} #endif // __BEANCOUNT_PARSER_PARSE_SOURCE_HASH_H__ \"\"\" . format ( source_hash = hash_parser_source_files ()) ) beancount . parser . hashsrc . hash_parser_source_files () \uf0c1 Compute a unique hash of the parser's Python code in order to bake that into the extension module. This is used at load-time to verify that the extension module and the corresponding Python codes match each other. If not, it issues a warning that you should rebuild your extension module. Returns: A string, the hexadecimal unique hash of relevant source code that should trigger a recompilation. Source code in beancount/parser/hashsrc.py def hash_parser_source_files (): \"\"\"Compute a unique hash of the parser's Python code in order to bake that into the extension module. This is used at load-time to verify that the extension module and the corresponding Python codes match each other. If not, it issues a warning that you should rebuild your extension module. Returns: A string, the hexadecimal unique hash of relevant source code that should trigger a recompilation. \"\"\" md5 = hashlib . md5 () for filename in PARSER_SOURCE_FILES : fullname = path . join ( path . dirname ( __file__ ), filename ) if not path . exists ( fullname ): return None with open ( fullname , \"rb\" ) as file : md5 . update ( file . read ()) # Note: Prepend a character in front of the hash because under Windows MSDEV # removes escapes, and if the hash starts with a number it fails to # recognize this is a string. A small compromise for portability. return md5 . hexdigest () beancount.parser.lexer \uf0c1 Beancount syntax lexer. beancount.parser.lexer.LexBuilder \uf0c1 A builder used only for building lexer objects. beancount . parser . lexer . LexBuilder . build_lexer_error ( self , filename , lineno , message ) \uf0c1 Build a lexer error and appends it to the list of pending errors. Parameters: message \u2013 The message of the error. Source code in beancount/parser/lexer.py def build_lexer_error ( self , filename , lineno , message ): # {0e31aeca3363} \"\"\"Build a lexer error and appends it to the list of pending errors. Args: message: The message of the error. \"\"\" self . errors . append ( LexerError ( new_metadata ( filename , lineno ), str ( message ), None )) beancount.parser.lexer.LexerError ( tuple ) \uf0c1 LexerError(source, message, entry) beancount . parser . lexer . LexerError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/lexer.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . lexer . LexerError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of LexerError(source, message, entry) beancount . parser . lexer . LexerError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/lexer.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . parser . lexer . lex_iter ( file , builder = None ) \uf0c1 An iterator that yields all the tokens in the given file. Parameters: file \u2013 A string, the filename to run the lexer on, or a file object. builder \u2013 A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors). Yields: All the tokens in the input file as (token, lineno, text, value) tuples where token is a string representing the token kind, lineno is the line number in the input file where the token was matched, mathed is a bytes object containing the exact text matched, and value is the semantic value of the token or None. Source code in beancount/parser/lexer.py def lex_iter ( file , builder = None ): \"\"\"An iterator that yields all the tokens in the given file. Args: file: A string, the filename to run the lexer on, or a file object. builder: A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors). Yields: All the tokens in the input file as ``(token, lineno, text, value)`` tuples where ``token`` is a string representing the token kind, ``lineno`` is the line number in the input file where the token was matched, ``mathed`` is a bytes object containing the exact text matched, and ``value`` is the semantic value of the token or None. \"\"\" with contextlib . ExitStack () as ctx : # It would be more appropriate here to check for io.RawIOBase but # that does not work for io.BytesIO despite it implementing the # readinto() method. if not isinstance ( file , io . IOBase ): file = ctx . enter_context ( open ( file , \"rb\" )) if builder is None : builder = LexBuilder () parser = _parser . Parser ( builder ) yield from parser . lex ( file ) beancount . parser . lexer . lex_iter_string ( string , builder = None , ** kwargs ) \uf0c1 An iterator that yields all the tokens in the given string. Parameters: string \u2013 a str or bytes, the contents of the ledger to be parsed. Returns: An iterator, see lex_iter() for details. Source code in beancount/parser/lexer.py def lex_iter_string ( string , builder = None , ** kwargs ): \"\"\"An iterator that yields all the tokens in the given string. Args: string: a str or bytes, the contents of the ledger to be parsed. Returns: An iterator, see ``lex_iter()`` for details. \"\"\" if not isinstance ( string , bytes ): string = string . encode ( \"utf8\" ) file = io . BytesIO ( string ) yield from lex_iter ( file , builder = builder , ** kwargs ) beancount.parser.options \uf0c1 Declaration of options and their default values. beancount.parser.options.OptDesc ( tuple ) \uf0c1 OptDesc(name, default_value, example_value, converter, deprecated, alias) beancount . parser . options . OptDesc . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/options.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . options . OptDesc . __new__ ( _cls , name , default_value , example_value , converter , deprecated , alias ) special staticmethod \uf0c1 Create new instance of OptDesc(name, default_value, example_value, converter, deprecated, alias) beancount . parser . options . OptDesc . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/options.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.parser.options.OptGroup ( tuple ) \uf0c1 OptGroup(description, options) beancount . parser . options . OptGroup . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/options.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . parser . options . OptGroup . __new__ ( _cls , description , options ) special staticmethod \uf0c1 Create new instance of OptGroup(description, options) beancount . parser . options . OptGroup . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/parser/options.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . parser . options . Opt ( name , default_value , example_value =< object object at 0x78fcdfec0a20 > , converter = None , deprecated = False , alias = None ) \uf0c1 Alternative constructor for OptDesc, with default values. Parameters: name \u2013 See OptDesc. default_value \u2013 See OptDesc. example_value \u2013 See OptDesc. converter \u2013 See OptDesc. deprecated \u2013 See OptDesc. alias \u2013 See OptDesc. Returns: An instance of OptDesc. Source code in beancount/parser/options.py def Opt ( name , default_value , example_value = UNSET , converter = None , deprecated = False , alias = None ): \"\"\"Alternative constructor for OptDesc, with default values. Args: name: See OptDesc. default_value: See OptDesc. example_value: See OptDesc. converter: See OptDesc. deprecated: See OptDesc. alias: See OptDesc. Returns: An instance of OptDesc. \"\"\" if example_value is UNSET : example_value = default_value return OptDesc ( name , default_value , example_value , converter , deprecated , alias ) beancount . parser . options . get_account_types ( options ) \uf0c1 Extract the account type names from the parser's options. Parameters: options \u2013 a dict of ledger options. Returns: An instance of AccountTypes, that contains all the prefixes. Source code in beancount/parser/options.py def get_account_types ( options ): \"\"\"Extract the account type names from the parser's options. Args: options: a dict of ledger options. Returns: An instance of AccountTypes, that contains all the prefixes. \"\"\" return account_types . AccountTypes ( * [ options [ key ] for key in ( \"name_assets\" , \"name_liabilities\" , \"name_equity\" , \"name_income\" , \"name_expenses\" , ) ] ) beancount . parser . options . get_current_accounts ( options ) \uf0c1 Return account names for the current earnings and conversion accounts. Parameters: options \u2013 a dict of ledger options. Returns: A tuple of 2 account objects, one for booking current earnings, and one for current conversions. Source code in beancount/parser/options.py def get_current_accounts ( options ): \"\"\"Return account names for the current earnings and conversion accounts. Args: options: a dict of ledger options. Returns: A tuple of 2 account objects, one for booking current earnings, and one for current conversions. \"\"\" equity = options [ \"name_equity\" ] account_current_earnings = account . join ( equity , options [ \"account_current_earnings\" ]) account_current_conversions = account . join ( equity , options [ \"account_current_conversions\" ] ) return ( account_current_earnings , account_current_conversions ) beancount . parser . options . get_previous_accounts ( options ) \uf0c1 Return account names for the previous earnings, balances and conversion accounts. Parameters: options \u2013 a dict of ledger options. Returns: A tuple of 3 account objects, for booking previous earnings, previous balances, and previous conversions. Source code in beancount/parser/options.py def get_previous_accounts ( options ): \"\"\"Return account names for the previous earnings, balances and conversion accounts. Args: options: a dict of ledger options. Returns: A tuple of 3 account objects, for booking previous earnings, previous balances, and previous conversions. \"\"\" equity = options [ \"name_equity\" ] account_previous_earnings = account . join ( equity , options [ \"account_previous_earnings\" ]) account_previous_balances = account . join ( equity , options [ \"account_previous_balances\" ]) account_previous_conversions = account . join ( equity , options [ \"account_previous_conversions\" ] ) return ( account_previous_earnings , account_previous_balances , account_previous_conversions , ) beancount . parser . options . get_unrealized_account ( options ) \uf0c1 Return the full account name for the unrealized account. Parameters: options \u2013 a dict of ledger options. Returns: A tuple of 2 account objects, one for booking current earnings, and one for current conversions. Source code in beancount/parser/options.py def get_unrealized_account ( options ): \"\"\"Return the full account name for the unrealized account. Args: options: a dict of ledger options. Returns: A tuple of 2 account objects, one for booking current earnings, and one for current conversions. \"\"\" income = options [ \"name_income\" ] return account . join ( income , options [ \"account_unrealized_gains\" ]) beancount . parser . options . list_options () \uf0c1 Produce a formatted text of the available options and their description. Returns: A string, formatted nicely to be printed in 80 columns. Source code in beancount/parser/options.py def list_options (): \"\"\"Produce a formatted text of the available options and their description. Returns: A string, formatted nicely to be printed in 80 columns. \"\"\" oss = io . StringIO () for group in PUBLIC_OPTION_GROUPS : for desc in group . options : oss . write ( 'option \" {} \" \" {} \" \\n ' . format ( desc . name , desc . example_value )) if desc . deprecated : oss . write ( textwrap . fill ( \"THIS OPTION IS DEPRECATED: {} \" . format ( desc . deprecated ), initial_indent = \" \" , subsequent_indent = \" \" , ) ) oss . write ( \" \\n\\n \" ) description = \" \" . join ( line . strip () for line in group . description . strip () . splitlines () ) oss . write ( textwrap . fill ( description , initial_indent = \" \" , subsequent_indent = \" \" )) oss . write ( \" \\n \" ) if isinstance ( desc . default_value , ( list , dict , set )): oss . write ( \" \\n \" ) oss . write ( \" (This option may be supplied multiple times.) \\n \" ) oss . write ( \" \\n\\n \" ) return oss . getvalue () beancount . parser . options . options_validate_booking_method ( value ) \uf0c1 Validate a booking method name. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_booking_method ( value ): \"\"\"Validate a booking method name. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" try : return data . Booking [ value ] except KeyError as exc : raise ValueError ( str ( exc )) from exc beancount . parser . options . options_validate_boolean ( value ) \uf0c1 Validate a boolean option. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_boolean ( value ): \"\"\"Validate a boolean option. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" return value . lower () in ( \"1\" , \"true\" , \"yes\" ) beancount . parser . options . options_validate_plugin ( value ) \uf0c1 Validate the plugin option. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_plugin ( value ): \"\"\"Validate the plugin option. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" # Process the 'plugin' option specially: accept an optional # argument from it. NOTE: We will eventually phase this out and # replace it by a dedicated 'plugin' directive. match = re . match ( \"(.*):(.*)\" , value ) if match : plugin_name , plugin_config = match . groups () else : plugin_name , plugin_config = value , None return ( plugin_name , plugin_config ) beancount . parser . options . options_validate_processing_mode ( value ) \uf0c1 Validate the options processing mode. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_processing_mode ( value ): \"\"\"Validate the options processing mode. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" if value not in ( \"raw\" , \"default\" ): raise ValueError ( \"Invalid value ' {} '\" . format ( value )) return value beancount . parser . options . options_validate_tolerance ( value ) \uf0c1 Validate the tolerance option. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_tolerance ( value ): \"\"\"Validate the tolerance option. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" return D ( value ) beancount . parser . options . options_validate_tolerance_map ( value ) \uf0c1 Validate an option with a map of currency/tolerance pairs in a string. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_tolerance_map ( value ): \"\"\"Validate an option with a map of currency/tolerance pairs in a string. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" # Process the setting of a key-value, whereby the value is a Decimal # representation. match = re . match ( \"(.*):(.*)\" , value ) if not match : raise ValueError ( \"Invalid value ' {} '\" . format ( value )) currency , tolerance_str = match . groups () return ( currency , D ( tolerance_str )) beancount.parser.parser \uf0c1 Beancount syntax parser. IMPORTANT: The parser (and its grammar builder) produces \"incomplete\" Transaction objects. This means that some of the data can be found missing from the output of the parser and some of the data types vary slightly. Missing components are replaced not by None, but by a special constant 'NA' which helps diagnose problems if a user inadvertently attempts to work on an incomplete posting instead of a complete one. Those incomplete entries are then run through the \"booking\" routines which do two things simultaneously: They find matching lots for reducing inventory positions, and They interpolate missing numbers. In doing so they normalize the entries to \"complete\" entries by converting a position/lot's \"cost\" attribute from a CostSpec to a Cost. A Cost is similar to an Amount in that it shares \"number\" and \"currency\" attributes, but also has a label and a lot date. A CostSpec is similar to a Cost, but has all optional data; it consists in a specification for matching against a particular inventory lot. Other parts of a posting may also be missing, not just parts of the cost. Leaving out some parts of the input is used to invoke interpolation, to tell Beancount to automatically compute the missing numbers (if possible). Missing components will be set to the special value \"beancount.core.number.MISSING\" until inventory booking and number interpolation has been completed. The \"MISSING\" value should never appear in completed, loaded transaction postings. For instance, all of the units may be missing: INPUT: Assets:Account posting.units = MISSING Or just the number of the units: INPUT: Assets:Account USD posting.units = Amount(MISSING, \"USD\") You must always specify the currency. If a price annotation is simply absent, it appears as None: INPUT: Assets:Account 2 MXN posting.price = None However, you may indicate that there is a price but have Beancount compute it automatically: INPUT: Assets:Account 2 MXN @ posting.price = Amount(MISSING, MISSING) Indicating the conversion currency is also possible (and recommended): INPUT: Assets:Account 2 MXN @ USD posting.price = Amount(MISSING, \"USD\") If a cost specification is provided, a \"cost\" attribute it set but it does not refer to a Cost instance (as in complete entries) but rather to a CostSpec instance. Some of the fields of a CostSpec may be MISSING if they were not specified in the input. For example: INPUT: Assets:Account 1 HOOL {100 # 5 USD} posting.cost = CostSpec(Decimal(\"100\"), Decimal(\"5\"), \"USD\", None, None, False)) Note how we never consider the label of date override to be MISSING; this is because those inputs are optional: A missing label is simply left unset in the computed Cost, and a missing date override uses the date of the transaction that contains the posting. You can indicate that there is a total number to be filled in like this: INPUT: Assets:Account 1 HOOL {100 # USD} posting.cost = CostSpec(Decimal(\"100\"), MISSING, \"USD\", None, None, False)) This is in contrast to the total value simple not being used: INPUT: Assets:Account 1 HOOL {100 USD} posting.cost = CostSpec(Decimal(\"100\"), None, \"USD\", None, None, False)) Both per-unit and total numbers may be omitted as well, in which case, only the number-per-unit portion of the CostSpec will appear as MISSING: INPUT: Assets:Account 1 HOOL {USD} posting.cost = CostSpec(MISSING, None, \"USD\", None, None, False)) And furthermore, all the cost basis may be missing: INPUT: Assets:Account 1 HOOL {} posting.cost = CostSpec(MISSING, None, MISSING, None, None, False)) If you ask for the lots to be merged, you get this: INPUT: Assets:Account 1 HOOL {*} posting.cost = CostSpec(MISSING, None, MISSING, None, None, True)) The numbers have to be computed by Beancount, so we output this with MISSING values. Of course, you can provide only the non-basis information, like just the date or label: INPUT: Assets:Account 1 HOOL {2015-09-21} posting.cost = CostSpec(MISSING, None, MISSING, date(2015, 9, 21), None, True) See the test beancount.parser.grammar_test.TestIncompleteInputs for examples and corresponding expected values. beancount . parser . parser . is_entry_incomplete ( entry ) \uf0c1 Detect the presence of elided amounts in Transactions. Parameters: entries \u2013 A directive. Returns: A boolean, true if there are some missing portions of any postings found. Source code in beancount/parser/parser.py def is_entry_incomplete ( entry ): \"\"\"Detect the presence of elided amounts in Transactions. Args: entries: A directive. Returns: A boolean, true if there are some missing portions of any postings found. \"\"\" if isinstance ( entry , data . Transaction ): if any ( is_posting_incomplete ( posting ) for posting in entry . postings ): return True return False beancount . parser . parser . is_posting_incomplete ( posting ) \uf0c1 Detect the presence of any elided amounts in a Posting. If any of the possible amounts are missing, this returns True. Parameters: entries \u2013 A directive. Returns: A boolean, true if there are some missing portions of any postings found. Source code in beancount/parser/parser.py def is_posting_incomplete ( posting ): \"\"\"Detect the presence of any elided amounts in a Posting. If any of the possible amounts are missing, this returns True. Args: entries: A directive. Returns: A boolean, true if there are some missing portions of any postings found. \"\"\" units = posting . units if units is MISSING or units . number is MISSING or units . currency is MISSING : return True price = posting . price if ( price is MISSING or price is not None and ( price . number is MISSING or price . currency is MISSING ) ): return True cost = posting . cost if cost is not None and ( cost . number_per is MISSING or cost . number_total is MISSING or cost . currency is MISSING ): return True return False beancount . parser . parser . parse_doc ( expect_errors = False , allow_incomplete = False ) \uf0c1 Factory of decorators that parse the function's docstring as an argument. Note that the decorators thus generated only run the parser on the tests, not the loader, so is no validation, balance checks, nor plugins applied to the parsed text. Parameters: expect_errors \u2013 A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. allow_incomplete \u2013 A boolean, if true, allow incomplete input. Otherwise barf if the input would require interpolation. The default value is set not to allow it because we want to minimize the features tests depend on. Returns: A decorator for test functions. Source code in beancount/parser/parser.py def parse_doc ( expect_errors = False , allow_incomplete = False ): \"\"\"Factory of decorators that parse the function's docstring as an argument. Note that the decorators thus generated only run the parser on the tests, not the loader, so is no validation, balance checks, nor plugins applied to the parsed text. Args: expect_errors: A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. allow_incomplete: A boolean, if true, allow incomplete input. Otherwise barf if the input would require interpolation. The default value is set not to allow it because we want to minimize the features tests depend on. Returns: A decorator for test functions. \"\"\" def decorator ( fun ): \"\"\"A decorator that parses the function's docstring as an argument. Args: fun: the function object to be decorated. Returns: A decorated test function. \"\"\" filename = inspect . getfile ( fun ) lines , lineno = inspect . getsourcelines ( fun ) # Skip over decorator invocation and function definition. This # is imperfect as it assumes that each consumes exactly one # line, but this is by far the most common case, and this is # mainly used in test, thus it is good enough. lineno += 2 @functools . wraps ( fun ) def wrapper ( self ): assert fun . __doc__ is not None , \"You need to insert a docstring on {} \" . format ( fun . __name__ ) entries , errors , options_map = parse_string ( fun . __doc__ , report_filename = filename , report_firstline = lineno , dedent = True ) if not allow_incomplete and any ( is_entry_incomplete ( entry ) for entry in entries ): self . fail ( \"parse_doc() may not use interpolation.\" ) if expect_errors is not None : if expect_errors is False and errors : oss = io . StringIO () printer . print_errors ( errors , file = oss ) self . fail ( \"Unexpected errors found: \\n {} \" . format ( oss . getvalue ())) elif expect_errors is True and not errors : self . fail ( \"Expected errors, none found:\" ) return fun ( self , entries , errors , options_map ) return wrapper return decorator beancount . parser . parser . parse_file ( file , report_filename = None , report_firstline = 1 , encoding = None , debug = False , ** kw ) \uf0c1 Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Parameters: file \u2013 file object or path to the file to be parsed. kw \u2013 a dict of keywords to be applied to the C parser. Returns: A tuple of ( list of entries parsed in the file, list of errors that were encountered during parsing, and a dict of the option values that were parsed from the file.) Source code in beancount/parser/parser.py def parse_file ( file , report_filename = None , report_firstline = 1 , encoding = None , debug = False , ** kw ): \"\"\"Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Args: file: file object or path to the file to be parsed. kw: a dict of keywords to be applied to the C parser. Returns: A tuple of ( list of entries parsed in the file, list of errors that were encountered during parsing, and a dict of the option values that were parsed from the file.) \"\"\" if encoding is not None and codecs . lookup ( encoding ) . name != \"utf-8\" : raise ValueError ( \"Only UTF-8 encoded files are supported.\" ) with contextlib . ExitStack () as ctx : if file == \"-\" : file = sys . stdin . buffer # It would be more appropriate here to check for io.RawIOBase but # that does not work for io.BytesIO despite it implementing the # readinto() method. elif not isinstance ( file , io . IOBase ): file = ctx . enter_context ( open ( file , \"rb\" )) builder = grammar . Builder () parser = _parser . Parser ( builder , debug = debug ) parser . parse ( file , filename = report_filename , lineno = report_firstline , ** kw ) return builder . finalize () beancount . parser . parser . parse_many ( string , level = 0 ) \uf0c1 Parse a string with a snippet of Beancount input and replace vars from caller. Parameters: string \u2013 A string with some Beancount input. level \u2013 The number of extra stacks to ignore. Returns: A list of entries. Exceptions: AssertionError \u2013 If there are any errors. Source code in beancount/parser/parser.py def parse_many ( string , level = 0 ): \"\"\"Parse a string with a snippet of Beancount input and replace vars from caller. Args: string: A string with some Beancount input. level: The number of extra stacks to ignore. Returns: A list of entries. Raises: AssertionError: If there are any errors. \"\"\" # Get the locals in the stack for the callers and produce the final text. frame = inspect . stack ()[ level + 1 ] varkwds = frame [ 0 ] . f_locals input_string = textwrap . dedent ( string . format ( ** varkwds )) # Parse entries and check there are no errors. entries , errors , __ = parse_string ( input_string ) assert not errors return entries beancount . parser . parser . parse_one ( string ) \uf0c1 Parse a string with single Beancount directive and replace vars from caller. Parameters: string \u2013 A string with some Beancount input. level \u2013 The number of extra stacks to ignore. Returns: A list of entries. Exceptions: AssertionError \u2013 If there are any errors. Source code in beancount/parser/parser.py def parse_one ( string ): \"\"\"Parse a string with single Beancount directive and replace vars from caller. Args: string: A string with some Beancount input. level: The number of extra stacks to ignore. Returns: A list of entries. Raises: AssertionError: If there are any errors. \"\"\" entries = parse_many ( string , level = 1 ) assert len ( entries ) == 1 return entries [ 0 ] beancount . parser . parser . parse_string ( string , report_filename = None , dedent = False , ** kw ) \uf0c1 Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Parameters: string \u2013 A string, the contents to be parsed instead of a file's. report_filename \u2013 A string, the source filename from which this string has been extracted, if any. This is stored in the metadata of the parsed entries. dedent \u2013 Whether to run textwrap.dedent() on the string before parsing. **kw \u2013 See parse.c. Returns: Same as the output of parse_file(). Source code in beancount/parser/parser.py def parse_string ( string , report_filename = None , dedent = False , ** kw ): \"\"\"Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Args: string: A string, the contents to be parsed instead of a file's. report_filename: A string, the source filename from which this string has been extracted, if any. This is stored in the metadata of the parsed entries. dedent: Whether to run textwrap.dedent() on the string before parsing. **kw: See parse.c. Return: Same as the output of parse_file(). \"\"\" if dedent : string = textwrap . dedent ( string ) if isinstance ( string , str ): string = string . encode ( \"utf8\" ) if report_filename is None : report_filename = \"\" file = io . BytesIO ( string ) return parse_file ( file , report_filename = report_filename , ** kw ) beancount.parser.printer \uf0c1 Conversion from internal data structures to text. beancount.parser.printer.EntryPrinter \uf0c1 A multi-method interface for printing all directive types. Attributes: Name Type Description dcontext An instance of DisplayContext with which to render all the numbers. render_weight A boolean, true if we should render the weight of the postings as a comment, for debugging. min_width_account An integer, the minimum width to leave for the account name. prefix User-specific prefix for custom indentation (for Fava). stringify_invalid_types If a metadata value is invalid, force a conversion to string for printout. beancount . parser . printer . EntryPrinter . __call__ ( self , obj ) special \uf0c1 Render a directive. Parameters: obj \u2013 The directive to be rendered. Returns: A string, the rendered directive. Source code in beancount/parser/printer.py def __call__ ( self , obj ): \"\"\"Render a directive. Args: obj: The directive to be rendered. Returns: A string, the rendered directive. \"\"\" oss = io . StringIO () # We write optional entry source for every entry type, hence writing it here self . write_entry_source ( obj . meta , oss , prefix = \"\" ) method = getattr ( self , obj . __class__ . __name__ ) method ( obj , oss ) return oss . getvalue () beancount . parser . printer . EntryPrinter . render_posting_strings ( self , posting ) \uf0c1 This renders the three components of a posting: the account and its optional posting flag, the position, and finally, the weight of the position. The purpose is to align these in the caller. Parameters: posting \u2013 An instance of Posting, the posting to render. Returns: A tuple of flag_account \u2013 A string, the account name including the flag. position_str: A string, the rendered position string. weight_str: A string, the rendered weight of the posting. Source code in beancount/parser/printer.py def render_posting_strings ( self , posting ): \"\"\"This renders the three components of a posting: the account and its optional posting flag, the position, and finally, the weight of the position. The purpose is to align these in the caller. Args: posting: An instance of Posting, the posting to render. Returns: A tuple of flag_account: A string, the account name including the flag. position_str: A string, the rendered position string. weight_str: A string, the rendered weight of the posting. \"\"\" # Render a string of the flag and the account. flag = \" {} \" . format ( render_flag ( posting . flag )) if posting . flag else \"\" flag_account = flag + posting . account # Render a string with the amount and cost and optional price, if # present. Also render a string with the weight. weight_str = \"\" if isinstance ( posting . units , amount . Amount ): position_str = position . to_string ( posting , self . dformat ) # Note: we render weights at maximum precision, for debugging. if posting . cost is None or ( isinstance ( posting . cost , position . Cost ) and isinstance ( posting . cost . number , Decimal ) ): weight_str = str ( convert . get_weight ( posting )) else : position_str = \"\" if posting . price is not None : position_str += \" @ {} \" . format ( posting . price . to_string ( self . dformat_max )) return flag_account , position_str , weight_str beancount . parser . printer . EntryPrinter . write_entry_source ( self , meta , oss , prefix = None ) \uf0c1 Write source file and line number in a format interpretable as a message location for Emacs, VSCode or other editors. As this is for \"debugging\" purposes, this information will be commented out by a semicolon. Parameters: meta \u2013 A dict that contains the metadata for this directive. oss \u2013 A file object to write to. prefix \u2013 User-specific prefix for custom indentation Source code in beancount/parser/printer.py def write_entry_source ( self , meta , oss , prefix = None ): \"\"\"Write source file and line number in a format interpretable as a message location for Emacs, VSCode or other editors. As this is for \"debugging\" purposes, this information will be commented out by a semicolon. Args: meta: A dict that contains the metadata for this directive. oss: A file object to write to. prefix: User-specific prefix for custom indentation \"\"\" if not self . write_source : return if prefix is None : prefix = self . prefix oss . write ( \" {} ; source: {} \\n \" . format ( prefix , render_source ( meta ))) beancount . parser . printer . EntryPrinter . write_metadata ( self , meta , oss , prefix = None ) \uf0c1 Write metadata to the file object, excluding filename and line number. Parameters: meta \u2013 A dict that contains the metadata for this directive. oss \u2013 A file object to write to. Source code in beancount/parser/printer.py def write_metadata ( self , meta , oss , prefix = None ): \"\"\"Write metadata to the file object, excluding filename and line number. Args: meta: A dict that contains the metadata for this directive. oss: A file object to write to. \"\"\" if meta is None : return if prefix is None : prefix = self . prefix # Note: meta.items() is assumed stable from 3.7 onwards; we're not sorting # on purpose in order to keep the original insertion order in print. for key , value in meta . items (): if key not in self . META_IGNORE and not key . startswith ( \"__\" ): value_str = None if isinstance ( value , str ): value_str = '\" {} \"' . format ( misc_utils . escape_string ( value )) elif isinstance ( value , ( Decimal , datetime . date , amount . Amount , enum . Enum )): value_str = str ( value ) elif isinstance ( value , bool ): value_str = \"TRUE\" if value else \"FALSE\" elif isinstance ( value , ( dict , inventory . Inventory )): pass # Ignore dicts, don't print them out. elif value is None : value_str = \"\" # Render null metadata as empty, on purpose. else : if self . stringify_invalid_types : # This is only intended to be used during development, # when debugging for custom values of data types # attached directly and not coming from the parser. value_str = str ( value ) else : raise ValueError ( \"Unexpected value: ' {!r} '\" . format ( value )) if value_str is not None : oss . write ( \" {}{} : {} \\n \" . format ( prefix , key , value_str )) beancount . parser . printer . align_position_strings ( strings ) \uf0c1 A helper used to align rendered amounts positions to their first currency character (an uppercase letter). This class accepts a list of rendered positions and calculates the necessary width to render them stacked in a column so that the first currency word aligns. It does not go beyond that (further currencies, e.g. for the price or cost, are not aligned). This is perhaps best explained with an example. The following positions will be aligned around the column marked with '^': 45 HOOL {504.30 USD} 4 HOOL {504.30 USD, 2014-11-11} 9.95 USD -22473.32 CAD @ 1.10 USD ^ Strings without a currency character will be rendered flush left. Parameters: strings \u2013 A list of rendered position or amount strings. Returns: A pair of a list of aligned strings and the width of the aligned strings. Source code in beancount/parser/printer.py def align_position_strings ( strings ): \"\"\"A helper used to align rendered amounts positions to their first currency character (an uppercase letter). This class accepts a list of rendered positions and calculates the necessary width to render them stacked in a column so that the first currency word aligns. It does not go beyond that (further currencies, e.g. for the price or cost, are not aligned). This is perhaps best explained with an example. The following positions will be aligned around the column marked with '^': 45 HOOL {504.30 USD} 4 HOOL {504.30 USD, 2014-11-11} 9.95 USD -22473.32 CAD @ 1.10 USD ^ Strings without a currency character will be rendered flush left. Args: strings: A list of rendered position or amount strings. Returns: A pair of a list of aligned strings and the width of the aligned strings. \"\"\" # Maximum length before the alignment character. max_before = 0 # Maximum length after the alignment character. max_after = 0 # Maximum length of unknown strings. max_unknown = 0 string_items = [] search = re . compile ( \"[A-Z]\" ) . search for string in strings : match = search ( string ) if match : index = match . start () if index != 0 : max_before = max ( index , max_before ) max_after = max ( len ( string ) - index , max_after ) string_items . append (( index , string )) continue # else max_unknown = max ( len ( string ), max_unknown ) string_items . append (( None , string )) # Compute formatting string. max_total = max ( max_before + max_after , max_unknown ) max_after_prime = max_total - max_before fmt = \"{{:> {0} }}{{: {1} }}\" . format ( max_before , max_after_prime ) . format fmt_unknown = \"{{:< {0} }}\" . format ( max_total ) . format # Align the strings and return them. aligned_strings = [] for index , string in string_items : if index is not None : string = fmt ( string [: index ], string [ index :]) else : string = fmt_unknown ( string ) aligned_strings . append ( string ) return aligned_strings , max_total beancount . parser . printer . format_entry ( entry , dcontext = None , render_weights = False , prefix = None , write_source = False ) \uf0c1 Format an entry into a string in the same input syntax the parser accepts. Parameters: entry \u2013 An entry instance. dcontext \u2013 An instance of DisplayContext used to format the numbers. render_weights \u2013 A boolean, true to render the weights for debugging. write_source \u2013 If true a source file and line number will be written for each entry in a format interpretable as a message location for Emacs, VSCode or other editors. As this is for \"debugging\" purposes, this information will be commented out by a semicolon. Returns: A string, the formatted entry. Source code in beancount/parser/printer.py def format_entry ( entry , dcontext = None , render_weights = False , prefix = None , write_source = False ): \"\"\"Format an entry into a string in the same input syntax the parser accepts. Args: entry: An entry instance. dcontext: An instance of DisplayContext used to format the numbers. render_weights: A boolean, true to render the weights for debugging. write_source: If true a source file and line number will be written for each entry in a format interpretable as a message location for Emacs, VSCode or other editors. As this is for \"debugging\" purposes, this information will be commented out by a semicolon. Returns: A string, the formatted entry. \"\"\" return EntryPrinter ( dcontext , render_weights , prefix = prefix , write_source = write_source )( entry ) beancount . parser . printer . format_error ( error ) \uf0c1 Given an error objects, return a formatted string for it. Parameters: error \u2013 a namedtuple objects representing an error. It has to have an 'entry' attribute that may be either a single directive object or a list of directive objects. Returns: A string, the errors rendered. Source code in beancount/parser/printer.py def format_error ( error ): \"\"\"Given an error objects, return a formatted string for it. Args: error: a namedtuple objects representing an error. It has to have an 'entry' attribute that may be either a single directive object or a list of directive objects. Returns: A string, the errors rendered. \"\"\" oss = io . StringIO () oss . write ( \" {} {} \\n \" . format ( render_source ( error . source ), error . message )) if error . entry is not None : entries = error . entry if isinstance ( error . entry , list ) else [ error . entry ] error_string = \" \\n \" . join ( format_entry ( entry ) for entry in entries ) oss . write ( \" \\n \" ) oss . write ( textwrap . indent ( error_string , \" \" )) oss . write ( \" \\n \" ) return oss . getvalue () beancount . parser . printer . print_entries ( entries , dcontext = None , render_weights = False , file = None , prefix = None , write_source = False ) \uf0c1 A convenience function that prints a list of entries to a file. Parameters: entries \u2013 A list of directives. dcontext \u2013 An instance of DisplayContext used to format the numbers. render_weights \u2013 A boolean, true to render the weights for debugging. file \u2013 An optional file object to write the entries to. prefix \u2013 User-specific prefix for custom indentation (for Fava). write_source \u2013 If true a source file and line number will be written for each entry in a format interpretable as a message location for Emacs, VSCode or other editors. This is usefull for \"debugging\" peurposes, especially in a multi-file setup Source code in beancount/parser/printer.py def print_entries ( entries , dcontext = None , render_weights = False , file = None , prefix = None , write_source = False ): \"\"\"A convenience function that prints a list of entries to a file. Args: entries: A list of directives. dcontext: An instance of DisplayContext used to format the numbers. render_weights: A boolean, true to render the weights for debugging. file: An optional file object to write the entries to. prefix: User-specific prefix for custom indentation (for Fava). write_source: If true a source file and line number will be written for each entry in a format interpretable as a message location for Emacs, VSCode or other editors. This is usefull for \"debugging\" peurposes, especially in a multi-file setup \"\"\" assert isinstance ( entries , list ), \"Entries is not a list: {} \" . format ( entries ) output = file or ( codecs . getwriter ( \"utf-8\" )( sys . stdout . buffer ) if hasattr ( sys . stdout , \"buffer\" ) else sys . stdout ) if prefix : output . write ( prefix ) previous_type = type ( entries [ 0 ]) if entries else None eprinter = EntryPrinter ( dcontext , render_weights , write_source = write_source ) for entry in entries : # Insert a newline between transactions and between blocks of directives # of the same type. entry_type = type ( entry ) if ( entry_type in ( data . Transaction , data . Commodity ) or entry_type is not previous_type or write_source ): output . write ( \" \\n \" ) previous_type = entry_type string = eprinter ( entry ) output . write ( string ) beancount . parser . printer . print_entry ( entry , dcontext = None , render_weights = False , file = None , write_source = False ) \uf0c1 A convenience function that prints a single entry to a file. Parameters: entry \u2013 A directive entry. dcontext \u2013 An instance of DisplayContext used to format the numbers. render_weights \u2013 A boolean, true to render the weights for debugging. file \u2013 An optional file object to write the entries to. write_source \u2013 If true a source file and line number will be written for each entry in a format interpretable as a message location for Emacs, VSCode or other editors. This is usefull for \"debugging\" purposes, especially in a multi-file setup Source code in beancount/parser/printer.py def print_entry ( entry , dcontext = None , render_weights = False , file = None , write_source = False ): \"\"\"A convenience function that prints a single entry to a file. Args: entry: A directive entry. dcontext: An instance of DisplayContext used to format the numbers. render_weights: A boolean, true to render the weights for debugging. file: An optional file object to write the entries to. write_source: If true a source file and line number will be written for each entry in a format interpretable as a message location for Emacs, VSCode or other editors. This is usefull for \"debugging\" purposes, especially in a multi-file setup \"\"\" # TODO(blais): DO remove this now, it's a huge annoyance not to be able to # print in-between other statements. output = file or ( codecs . getwriter ( \"utf-8\" )( sys . stdout . buffer ) if hasattr ( sys . stdout , \"buffer\" ) else sys . stdout ) output . write ( format_entry ( entry , dcontext , render_weights , write_source = write_source )) output . write ( \" \\n \" ) beancount . parser . printer . print_error ( error , file = None ) \uf0c1 A convenience function that prints a single error to a file. Parameters: error \u2013 An error object. file \u2013 An optional file object to write the errors to. Source code in beancount/parser/printer.py def print_error ( error , file = None ): \"\"\"A convenience function that prints a single error to a file. Args: error: An error object. file: An optional file object to write the errors to. \"\"\" output = file or sys . stdout output . write ( format_error ( error )) output . write ( \" \\n \" ) beancount . parser . printer . print_errors ( errors , file = None , prefix = None ) \uf0c1 A convenience function that prints a list of errors to a file. Parameters: errors \u2013 A list of errors. file \u2013 An optional file object to write the errors to. Source code in beancount/parser/printer.py def print_errors ( errors , file = None , prefix = None ): \"\"\"A convenience function that prints a list of errors to a file. Args: errors: A list of errors. file: An optional file object to write the errors to. \"\"\" output = file or sys . stdout if prefix : output . write ( prefix ) for error in errors : output . write ( format_error ( error )) output . write ( \" \\n \" ) beancount . parser . printer . render_flag ( inflag ) \uf0c1 Render a flag, which can be None, a symbol of a character to a string. Source code in beancount/parser/printer.py def render_flag ( inflag : Optional [ str ]) -> str : \"\"\"Render a flag, which can be None, a symbol of a character to a string.\"\"\" if not inflag : return \"\" return inflag beancount . parser . printer . render_source ( meta ) \uf0c1 Render the source for errors in a way that it will be both detected by Emacs and align and rendered nicely. Parameters: meta \u2013 A dict with the metadata. Returns: A string, rendered to be interpretable as a message location for Emacs or other editors. Source code in beancount/parser/printer.py def render_source ( meta ): \"\"\"Render the source for errors in a way that it will be both detected by Emacs and align and rendered nicely. Args: meta: A dict with the metadata. Returns: A string, rendered to be interpretable as a message location for Emacs or other editors. \"\"\" return \" {} : {:8} \" . format ( meta [ \"filename\" ], \" {} :\" . format ( meta [ \"lineno\" ])) beancount.parser.version \uf0c1 Implement common options across all programs. beancount . parser . version . compute_version_string ( version , changeset , timestamp ) \uf0c1 Compute a version string from the changeset and timestamp baked in the parser. Parameters: version \u2013 A string, the version number. changeset \u2013 A string, a version control string identifying the commit of the version. timestamp \u2013 An integer, the UNIX epoch timestamp of the changeset. Returns: A human-readable string for the version. Source code in beancount/parser/version.py def compute_version_string ( version , changeset , timestamp ): \"\"\"Compute a version string from the changeset and timestamp baked in the parser. Args: version: A string, the version number. changeset: A string, a version control string identifying the commit of the version. timestamp: An integer, the UNIX epoch timestamp of the changeset. Returns: A human-readable string for the version. \"\"\" # Shorten changeset. if changeset : if re . match ( \"hg:\" , changeset ): changeset = changeset [: 15 ] elif re . match ( \"git:\" , changeset ): changeset = changeset [: 12 ] # Convert timestamp to a date. date = None if timestamp > 0 : date = datetime . datetime . fromtimestamp ( timestamp , datetime . timezone . utc ) . date () version = \"Beancount {} \" . format ( version ) if changeset or date : version = \" {} ( {} )\" . format ( version , \"; \" . join ( map ( str , filter ( None , [ changeset , date ]))) ) return version","title":"beancount.parser"},{"location":"api_reference/beancount.parser.html#beancountparser","text":"Parser module for beancount input files.","title":"beancount.parser"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking","text":"Algorithms for 'booking' inventory, that is, the process of finding a matching lot when reducing the content of an inventory.","title":"booking"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.BookingError","text":"BookingError(source, message, entry)","title":"BookingError"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.BookingError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.BookingError.__new__","text":"Create new instance of BookingError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.BookingError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.book","text":"Book inventory lots and complete all positions with incomplete numbers. Parameters: incomplete_entries \u2013 A list of directives, with some postings possibly left with incomplete amounts as produced by the parser. options_map \u2013 An options dict as produced by the parser. initial_balances \u2013 A dict of (account, inventory) pairs to start booking from. This is useful when attempting to book on top of an existing state. Returns: A pair of entries \u2013 A list of completed entries with all their postings completed. errors: New errors produced during interpolation. Source code in beancount/parser/booking.py def book ( incomplete_entries , options_map , initial_balances = None ): \"\"\"Book inventory lots and complete all positions with incomplete numbers. Args: incomplete_entries: A list of directives, with some postings possibly left with incomplete amounts as produced by the parser. options_map: An options dict as produced by the parser. initial_balances: A dict of (account, inventory) pairs to start booking from. This is useful when attempting to book on top of an existing state. Returns: A pair of entries: A list of completed entries with all their postings completed. errors: New errors produced during interpolation. \"\"\" # Get the list of booking methods for each account. booking_methods = collections . defaultdict ( lambda : options_map [ \"booking_method\" ]) for entry in incomplete_entries : if isinstance ( entry , data . Open ) and entry . booking : booking_methods [ entry . account ] = entry . booking # Do the booking here! entries , booking_errors = booking_full . book ( incomplete_entries , options_map , booking_methods , initial_balances ) # Check for MISSING elements remaining. missing_errors = validate_missing_eliminated ( entries , options_map ) return entries , ( booking_errors + missing_errors )","title":"book()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.convert_lot_specs_to_lots","text":"For all the entries, convert the posting's position's CostSpec to Cost instances. In the simple method, the data provided in the CostSpec must unambiguously provide a way to compute the cost amount. This essentially replicates the way the old parser used to work, but allowing positions to have the fuzzy lot specifications instead of the resolved ones. We used to simply compute the costs locally, and this gets rid of the CostSpec to produce the Cost without fuzzy matching. This is only there for the sake of transition to the new matching logic. Parameters: entries \u2013 A list of incomplete directives as per the parser. Returns: A list of entries whose postings's position costs have been converted to Cost instances but that may still be incomplete. Exceptions: ValueError \u2013 If there's a unacceptable number. Source code in beancount/parser/booking.py def convert_lot_specs_to_lots ( entries ): \"\"\"For all the entries, convert the posting's position's CostSpec to Cost instances. In the simple method, the data provided in the CostSpec must unambiguously provide a way to compute the cost amount. This essentially replicates the way the old parser used to work, but allowing positions to have the fuzzy lot specifications instead of the resolved ones. We used to simply compute the costs locally, and this gets rid of the CostSpec to produce the Cost without fuzzy matching. This is only there for the sake of transition to the new matching logic. Args: entries: A list of incomplete directives as per the parser. Returns: A list of entries whose postings's position costs have been converted to Cost instances but that may still be incomplete. Raises: ValueError: If there's a unacceptable number. \"\"\" new_entries = [] errors = [] for entry in entries : if not isinstance ( entry , data . Transaction ): new_entries . append ( entry ) continue new_postings = [] for posting in entry . postings : try : units = posting . units cost_spec = posting . cost cost = convert_spec_to_cost ( units , cost_spec ) if cost_spec is not None and cost is None : errors . append ( BookingError ( entry . meta , \"Cost syntax not supported; cost spec ignored\" , None ) ) if cost and isinstance ( units , amount . Amount ): # If there is a cost, we don't allow either a cost value of # zero, nor a zero number of units. Note that we allow a price # of zero as the only special case (for conversion entries), but # never for costs. if units . number == ZERO : raise ValueError ( 'Amount is zero: \" {} \"' . format ( units )) if cost . number is not None and cost . number < ZERO : raise ValueError ( 'Cost is negative: \" {} \"' . format ( cost )) except ValueError as exc : errors . append ( BookingError ( entry . meta , str ( exc ), None )) cost = None new_postings . append ( posting . _replace ( cost = cost )) new_entries . append ( entry . _replace ( postings = new_postings )) return new_entries , errors","title":"convert_lot_specs_to_lots()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.convert_spec_to_cost","text":"Convert a posting's CostSpec instance to a Cost. Parameters: units \u2013 An instance of Amount. cost_spec \u2013 An instance of CostSpec. Returns: An instance of Cost. Source code in beancount/parser/booking.py def convert_spec_to_cost ( units , cost_spec ): \"\"\"Convert a posting's CostSpec instance to a Cost. Args: units: An instance of Amount. cost_spec: An instance of CostSpec. Returns: An instance of Cost. \"\"\" cost = cost_spec if isinstance ( units , amount . Amount ): if cost_spec is not None : number_per , number_total , cost_currency , date , label , merge = cost_spec # Compute the cost. if number_per is not MISSING or number_total is not None : if number_total is not None : # Compute the per-unit cost if there is some total cost # component involved. units_num = units . number cost_total = number_total if number_per is not MISSING : cost_total += number_per * units_num unit_cost = cost_total / abs ( units_num ) else : unit_cost = number_per cost = position . Cost ( unit_cost , cost_currency , date , label ) else : cost = None return cost","title":"convert_spec_to_cost()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.validate_inventory_booking","text":"Validate that no position at cost is allowed to go negative. This routine checks that when a posting reduces a position, existing or not, that the subsequent inventory does not result in a position with a negative number of units. A negative number of units would only be required for short trades of trading spreads on futures, and right now this is not supported. It would not be difficult to support this, however, but we want to be strict about it, because being pedantic about this is otherwise a great way to detect user data entry mistakes. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. booking_methods \u2013 A mapping of account name to booking method, accumulated in the main loop. Returns: A list of errors. Source code in beancount/parser/booking.py def validate_inventory_booking ( entries , unused_options_map , booking_methods ): \"\"\"Validate that no position at cost is allowed to go negative. This routine checks that when a posting reduces a position, existing or not, that the subsequent inventory does not result in a position with a negative number of units. A negative number of units would only be required for short trades of trading spreads on futures, and right now this is not supported. It would not be difficult to support this, however, but we want to be strict about it, because being pedantic about this is otherwise a great way to detect user data entry mistakes. Args: entries: A list of directives. unused_options_map: An options map. booking_methods: A mapping of account name to booking method, accumulated in the main loop. Returns: A list of errors. \"\"\" errors = [] balances = collections . defaultdict ( inventory . Inventory ) for entry in entries : if isinstance ( entry , data . Transaction ): for posting in entry . postings : # Update the balance of each posting on its respective account # without allowing booking to a negative position, and if an error # is encountered, catch it and return it. running_balance = balances [ posting . account ] position_ , _ = running_balance . add_position ( posting ) # Skip this check if the booking method is set to ignore it. if booking_methods . get ( posting . account , None ) == data . Booking . NONE : continue # Check if the resulting inventory is mixed, which is not # allowed under the STRICT method. if running_balance . is_mixed (): errors . append ( BookingError ( entry . meta , ( \"Reducing position results in inventory with positive \" \"and negative lots: {} \" ) . format ( position_ ), entry , ) ) return errors","title":"validate_inventory_booking()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking.validate_missing_eliminated","text":"Validate that all the missing bits of postings have been eliminated. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of errors. Source code in beancount/parser/booking.py def validate_missing_eliminated ( entries , unused_options_map ): \"\"\"Validate that all the missing bits of postings have been eliminated. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of errors. \"\"\" errors = [] for entry in entries : if isinstance ( entry , data . Transaction ): for posting in entry . postings : units = posting . units cost = posting . cost if ( MISSING in ( units . number , units . currency ) or cost is not None and MISSING in ( cost . number , cost . currency , cost . date , cost . label ) ): errors . append ( BookingError ( entry . meta , \"Transaction has incomplete elements\" , entry ) ) break return errors","title":"validate_missing_eliminated()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full","text":"Full (new) booking implementation. Problem description: Interpolation and booking feed on each other, that is, numbers filled in from interpolation might affect the booking process, and numbers derived from the booking process may help carry out interpolation that would otherwise be under-defined. Here's an example of interpolation helping the booking process: Assume the ante-inventory of Assets:Investments contains two lots of shares of HOOL, one at 100.00 USD and the other at 101.00 USD and apply this transaction: 2015-09-30 * Assets:Investments -10 HOOL {USD} Assets:Cash 1000 USD Income:Gains -200 USD Interpolation is unambiguously able to back out a cost of 100 USD / HOOL, which would then result in an unambiguous booking result. On the other hand, consider this transaction: 2015-09-30 * Assets:Investments -10 HOOL {USD} Assets:Cash 1000 USD Income:Gains Now the interpolation cannot succeed. If the Assets:Investments account is configured to use the FIFO method, the 10 oldest shares would be selected for the cost, and we could then interpolate the capital gains correctly. First observation: The second case is much more frequent than the first, and the first is easily resolved manually by requiring a particular cost be specified. Moreover, in many cases there isn't just a single lot of shares to be reduced from and figuring out the correct set of shares given a target cost is an underspecified problem. Second observation: Booking can only be achieved for inventory reductions, not for augmentations. Therefore, we should carry out booking on inventory reductions and fail early if reduction is undefined there, and leave inventory augmentations with missing numbers undefined, so that interpolation can fill them in at a later stage. Note that one case we'd like to but may not be able to handle is of a reduction with interpolated price, like this: 2015-09-30 * Assets:Investments -10 HOOL {100.00 # USD} Expenses:Commission 9.95 USD Assets:Cash 990.05 USD Therefore we choose to 1) Carry out booking first, on inventory reductions only, and leave inventory augmentations as they are, possibly undefined. The 'cost' attributed of booked postings are converted from CostSpec to Cost. Augmented postings with missing amounts are left as CostSpec instances in order to allow for interpolation of total vs. per-unit amount. 2) Compute interpolations on the resulting postings. Undefined costs for inventory augmentations may be filled in by interpolations at this stage (if possible). 3) Finally, convert the interpolated CostSpec instances to Cost instances. Improving on this algorithm would require running a loop over the booking and interpolation steps until all numbers are resolved or no more inference can occur. We may consider that for later, as an experimental feature. My hunch is that there are so few cases for which this would be useful that we won't bother improving on the algorithm above.","title":"booking_full"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.CategorizationError","text":"CategorizationError(source, message, entry)","title":"CategorizationError"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.CategorizationError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.CategorizationError.__new__","text":"Create new instance of CategorizationError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.CategorizationError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.InterpolationError","text":"InterpolationError(source, message, entry)","title":"InterpolationError"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.InterpolationError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.InterpolationError.__new__","text":"Create new instance of InterpolationError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.InterpolationError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.MissingType","text":"The type of missing number.","title":"MissingType"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.ReductionError","text":"ReductionError(source, message, entry)","title":"ReductionError"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.ReductionError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.ReductionError.__new__","text":"Create new instance of ReductionError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.ReductionError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.Refer","text":"Refer(index, units_currency, cost_currency, price_currency)","title":"Refer"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.Refer.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.Refer.__new__","text":"Create new instance of Refer(index, units_currency, cost_currency, price_currency)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.Refer.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.SelfReduxError","text":"SelfReduxError(source, message, entry)","title":"SelfReduxError"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.SelfReduxError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_full.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.SelfReduxError.__new__","text":"Create new instance of SelfReduxError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.SelfReduxError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking_full.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.book","text":"Interpolate missing data from the entries using the full historical algorithm. See the internal implementation _book() for details. This method only stripes some of the return values. See _book() for arguments and return values. Source code in beancount/parser/booking_full.py def book ( entries , options_map , methods , initial_balances = None ): \"\"\"Interpolate missing data from the entries using the full historical algorithm. See the internal implementation _book() for details. This method only stripes some of the return values. See _book() for arguments and return values. \"\"\" entries , errors , _ = _book ( entries , options_map , methods , initial_balances ) return entries , errors","title":"book()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.book_reductions","text":"Book inventory reductions against the ante-balances. This function accepts a dict of (account, Inventory balance) and for each posting that is a reduction against its inventory, attempts to find a corresponding lot or list of lots to reduce the balance with. For reducing lots, the CostSpec instance of the posting is replaced by a Cost instance. For augmenting lots, the CostSpec instance of the posting is left alone, except for its date, which is inherited from the parent Transaction. Parameters: entry \u2013 An instance of Transaction. This is only used to refer to when logging errors. group_postings \u2013 A list of Posting instances for the group. balances \u2013 A dict of account name to inventory contents. methods \u2013 A mapping of account name to their corresponding booking method enum. Returns: A pair of booked_postings \u2013 A list of booked postings, with reducing lots resolved against specific position in the corresponding accounts' ante-inventory balances. Note single reducing posting in the input may result in multiple postings in the output. Also note that augmenting postings held-at-cost will still refer to 'cost' instances of CostSpec, left to be interpolated later. errors: A list of errors, if there were any. Source code in beancount/parser/booking_full.py def book_reductions ( entry , group_postings , balances , methods ): \"\"\"Book inventory reductions against the ante-balances. This function accepts a dict of (account, Inventory balance) and for each posting that is a reduction against its inventory, attempts to find a corresponding lot or list of lots to reduce the balance with. * For reducing lots, the CostSpec instance of the posting is replaced by a Cost instance. * For augmenting lots, the CostSpec instance of the posting is left alone, except for its date, which is inherited from the parent Transaction. Args: entry: An instance of Transaction. This is only used to refer to when logging errors. group_postings: A list of Posting instances for the group. balances: A dict of account name to inventory contents. methods: A mapping of account name to their corresponding booking method enum. Returns: A pair of booked_postings: A list of booked postings, with reducing lots resolved against specific position in the corresponding accounts' ante-inventory balances. Note single reducing posting in the input may result in multiple postings in the output. Also note that augmenting postings held-at-cost will still refer to 'cost' instances of CostSpec, left to be interpolated later. errors: A list of errors, if there were any. \"\"\" errors = [] # A local copy of the balances dictionary which is updated just for the # duration of this function's updates, in order to take into account the # cumulative effect of all the postings inferred here local_balances = {} empty = inventory . Inventory () booked_postings = [] for posting in group_postings : # Process a single posting. units = posting . units costspec = posting . cost account = posting . account # Note: We ensure there is no mutation on 'balances' to keep this # function without side-effects. Note that we may be able to optimize # performance later on by giving up this property. # # Also note that if there is no existing balance, then won't be any lot # reduction because none of the postings will be able to match against # any currencies of the balance. if account not in local_balances : previous_balance = balances . get ( account , empty ) local_balances [ account ] = copy . copy ( previous_balance ) balance = local_balances [ account ] # Check if this is a lot held at cost. if costspec is None or units . number is MISSING : # This posting is not held at cost; we do nothing. booked_postings . append ( posting ) else : # This posting is held at cost; figure out if it's a reduction or an # augmentation. method = methods [ account ] if ( method is not Booking . NONE and balance is not None and balance . is_reduced_by ( units ) ): # This posting is a reduction. # Match the positions. cost_number = compute_cost_number ( costspec , units ) matches = [] for position in balance : # Skip inventory contents of a different currency. if units . currency and position . units . currency != units . currency : continue # Skip balance positions not held at cost. if position . cost is None : continue if cost_number is not None and position . cost . number != cost_number : continue if ( isinstance ( costspec . currency , str ) and position . cost . currency != costspec . currency ): continue if costspec . date and position . cost . date != costspec . date : continue if costspec . label and position . cost . label != costspec . label : continue matches . append ( position ) # Check for ambiguous matches. if len ( matches ) == 0 : errors . append ( ReductionError ( entry . meta , 'No position matches \" {} \" against balance {} ' . format ( posting , balance ), entry , ) ) return [], errors # This is irreconcilable, remove these postings. # TODO(blais): We'll have to change this, as we want to allow # positions crossing from negative to positive and vice-versa in # a simple application. See {d3cbd78f1029}. reduction_postings , matched_postings , ambi_errors = ( booking_method . handle_ambiguous_matches ( entry , posting , matches , method ) ) if ambi_errors : errors . extend ( ambi_errors ) return [], errors # Add the reductions to the resulting list of booked postings. booked_postings . extend ( reduction_postings ) # Update the local balance in order to avoid matching against # the same postings twice when processing multiple postings in # the same transaction. Note that we only do this for postings # held at cost because the other postings may need interpolation # in order to be resolved properly. for posting in reduction_postings : balance . add_position ( posting ) else : # This posting is an augmentation. # # Note that we do not convert the CostSpec instances to Cost # instances, because we want to let the subsequent interpolation # process able to interpolate either the cost per-unit or the # total cost, separately. # Put in the date of the parent Transaction if there is no # explicit date specified on the spec. if costspec . date is None : dated_costspec = costspec . _replace ( date = entry . date ) posting = posting . _replace ( cost = dated_costspec ) # FIXME: Insert unique ids for trade tracking; right now this # creates ambiguous matches errors (and it shouldn't). # # Insert a unique label if there isn't one. # if posting.cost is not None and posting.cost.label is None: # posting = posting._replace( # cost=posting.cost._replace(label=unique_label())) booked_postings . append ( posting ) return booked_postings , errors","title":"book_reductions()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.categorize_by_currency","text":"Group the postings by the currency they declare. This is used to prepare the postings for the next stages: Interpolation and booking will then be carried out separately on each currency group. At the outset of this routine, we should have distinct groups of currencies without any ambiguities regarding which currency they need to balance against. Here's how this works. First we apply the constraint that cost-currency and price-currency must match, if there is both a cost and a price. This reduces the space of possibilities somewhat. If the currency is explicitly specified, we put the posting in that currency's bucket. If not, we have a few methods left to disambiguate the currency: We look at the remaining postings... if they are all of a single currency, the posting must be in that currency too. If we cannot do that, we inspect the contents of the inventory of the account for the posting. If all the contents are of a single currency, we use that one. Parameters: postings \u2013 A list of incomplete postings to categorize. balances \u2013 A dict of currency to inventory contents before the transaction is applied. Returns: A list of (currency string, list of tuples) items describing each postings and its interpolated currencies, and a list of generated errors for currency interpolation. The entry's original postings are left unmodified. Each tuple in the value-list contains \u2013 index: The posting index in the original entry. units_currency: The interpolated currency for units. cost_currency: The interpolated currency for cost. price_currency: The interpolated currency for price. Source code in beancount/parser/booking_full.py def categorize_by_currency ( entry , balances ): \"\"\"Group the postings by the currency they declare. This is used to prepare the postings for the next stages: Interpolation and booking will then be carried out separately on each currency group. At the outset of this routine, we should have distinct groups of currencies without any ambiguities regarding which currency they need to balance against. Here's how this works. - First we apply the constraint that cost-currency and price-currency must match, if there is both a cost and a price. This reduces the space of possibilities somewhat. - If the currency is explicitly specified, we put the posting in that currency's bucket. - If not, we have a few methods left to disambiguate the currency: 1. We look at the remaining postings... if they are all of a single currency, the posting must be in that currency too. 2. If we cannot do that, we inspect the contents of the inventory of the account for the posting. If all the contents are of a single currency, we use that one. Args: postings: A list of incomplete postings to categorize. balances: A dict of currency to inventory contents before the transaction is applied. Returns: A list of (currency string, list of tuples) items describing each postings and its interpolated currencies, and a list of generated errors for currency interpolation. The entry's original postings are left unmodified. Each tuple in the value-list contains: index: The posting index in the original entry. units_currency: The interpolated currency for units. cost_currency: The interpolated currency for cost. price_currency: The interpolated currency for price. \"\"\" errors = [] groups = collections . defaultdict ( list ) sortdict = {} auto_postings = [] unknown = [] for index , posting in enumerate ( entry . postings ): units = posting . units cost = posting . cost price = posting . price # Extract and override the currencies locally. units_currency = ( units . currency if units is not MISSING and units is not None else None ) cost_currency = cost . currency if cost is not MISSING and cost is not None else None price_currency = ( price . currency if price is not MISSING and price is not None else None ) # First we apply the constraint that cost-currency and price-currency # must match, if there is both a cost and a price. This reduces the # space of possibilities somewhat. if cost_currency is MISSING and isinstance ( price_currency , str ): cost_currency = price_currency if price_currency is MISSING and isinstance ( cost_currency , str ): price_currency = cost_currency refer = Refer ( index , units_currency , cost_currency , price_currency ) if units is MISSING and price_currency is None : # Bucket auto-postings separately from unknown. auto_postings . append ( refer ) else : # Bucket with what we know so far. currency = get_bucket_currency ( refer ) if currency is not None : sortdict . setdefault ( currency , index ) groups [ currency ] . append ( refer ) else : # If we need to infer the currency, store in unknown. unknown . append ( refer ) # We look at the remaining postings... if they are all of a single currency, # the posting must be in that currency too. if unknown and len ( unknown ) == 1 and len ( groups ) == 1 : ( index , units_currency , cost_currency , price_currency ) = unknown . pop () other_currency = next ( iter ( groups . keys ())) if price_currency is None and cost_currency is None : # Infer to the units currency. units_currency = other_currency else : # Infer to the cost and price currencies. if price_currency is MISSING : price_currency = other_currency if cost_currency is MISSING : cost_currency = other_currency refer = Refer ( index , units_currency , cost_currency , price_currency ) currency = get_bucket_currency ( refer ) assert currency is not None sortdict . setdefault ( currency , index ) groups [ currency ] . append ( refer ) # Finally, try to resolve all the unknown legs using the inventory contents # of each account. for refer in unknown : ( index , units_currency , cost_currency , price_currency ) = refer posting = entry . postings [ index ] balance = balances . get ( posting . account , None ) if balance is None : balance = inventory . Inventory () if units_currency is MISSING : balance_currencies = balance . currencies () if len ( balance_currencies ) == 1 : units_currency = balance_currencies . pop () if cost_currency is MISSING or price_currency is MISSING : balance_cost_currencies = balance . cost_currencies () if len ( balance_cost_currencies ) == 1 : balance_cost_currency = balance_cost_currencies . pop () if price_currency is MISSING : price_currency = balance_cost_currency if cost_currency is MISSING : cost_currency = balance_cost_currency refer = Refer ( index , units_currency , cost_currency , price_currency ) currency = get_bucket_currency ( refer ) if currency is not None : sortdict . setdefault ( currency , index ) groups [ currency ] . append ( refer ) else : errors . append ( CategorizationError ( posting . meta , \"Failed to categorize posting {} \" . format ( index + 1 ), entry ) ) # Fill in missing units currencies if some remain as missing. This may occur # if we used the cost or price to bucket the currency but the units currency # was missing. for currency , refers in groups . items (): for rindex , refer in enumerate ( refers ): if refer . units_currency is MISSING : posting = entry . postings [ refer . index ] balance = balances . get ( posting . account , None ) if balance is None : continue balance_currencies = balance . currencies () if len ( balance_currencies ) == 1 : refers [ rindex ] = refer . _replace ( units_currency = balance_currencies . pop ()) # Deal with auto-postings. if len ( auto_postings ) > 1 : refer = auto_postings [ - 1 ] posting = entry . postings [ refer . index ] errors . append ( CategorizationError ( posting . meta , \"You may not have more than one auto-posting per currency\" , entry , ) ) auto_postings = auto_postings [ 0 : 1 ] for refer in auto_postings : for currency , glist in groups . items (): sortdict . setdefault ( currency , refer . index ) glist . append ( Refer ( refer . index , currency , None , None )) # Issue error for all currencies which we could not resolve. for currency , refers in groups . items (): for refer in refers : posting = entry . postings [ refer . index ] for currency , name in [ ( refer . units_currency , \"units\" ), ( refer . cost_currency , \"cost\" ), ( refer . price_currency , \"price\" ), ]: if currency is MISSING : errors . append ( CategorizationError ( posting . meta , \"Could not resolve {} currency\" . format ( name ), entry , ) ) sorted_groups = sorted ( groups . items (), key = lambda item : sortdict [ item [ 0 ]]) return sorted_groups , errors","title":"categorize_by_currency()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.compute_cost_number","text":"Given a CostSpec, return the cost number, if possible to compute. Parameters: costspec \u2013 A parsed instance of CostSpec. units \u2013 An instance of Amount for the units of the position. Returns: If it is not possible to calculate the cost, return None. Otherwise, returns a Decimal instance, the per-unit cost. Source code in beancount/parser/booking_full.py def compute_cost_number ( costspec , units ): \"\"\"Given a CostSpec, return the cost number, if possible to compute. Args: costspec: A parsed instance of CostSpec. units: An instance of Amount for the units of the position. Returns: If it is not possible to calculate the cost, return None. Otherwise, returns a Decimal instance, the per-unit cost. \"\"\" number_per = costspec . number_per number_total = costspec . number_total if MISSING in ( number_per , number_total ): return None if number_total is not None : # Compute the per-unit cost if there is some total cost # component involved. cost_total = number_total units_number = abs ( units . number ) if number_per is not None : cost_total += number_per * units_number unit_cost = cost_total / units_number elif number_per is None : return None else : unit_cost = number_per return unit_cost","title":"compute_cost_number()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.convert_costspec_to_cost","text":"Convert an instance of CostSpec to Cost, if present on the posting. If the posting has no cost, it itself is just returned. Parameters: posting \u2013 An instance of Posting. Returns: An instance of Posting with a possibly replaced 'cost' attribute. Source code in beancount/parser/booking_full.py def convert_costspec_to_cost ( posting ): \"\"\"Convert an instance of CostSpec to Cost, if present on the posting. If the posting has no cost, it itself is just returned. Args: posting: An instance of Posting. Returns: An instance of Posting with a possibly replaced 'cost' attribute. \"\"\" cost = posting . cost if isinstance ( cost , position . CostSpec ): if cost is not None : number_per = cost . number_per number_total = cost . number_total if number_total is not None : # Compute the per-unit cost if there is some total cost # component involved. units_number = abs ( posting . units . number ) cost_total = number_total if number_per is not MISSING : cost_total += number_per * units_number unit_cost = cost_total / units_number else : unit_cost = number_per new_cost = Cost ( unit_cost , cost . currency , cost . date , cost . label ) posting = posting . _replace ( units = posting . units , cost = new_cost ) return posting","title":"convert_costspec_to_cost()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.get_bucket_currency","text":"Given currency references for a posting, return the bucket currency. Parameters: refer \u2013 An instance of Refer. Returns: A currency string. Source code in beancount/parser/booking_full.py def get_bucket_currency ( refer ): \"\"\"Given currency references for a posting, return the bucket currency. Args: refer: An instance of Refer. Returns: A currency string. \"\"\" currency = None if isinstance ( refer . cost_currency , str ): currency = refer . cost_currency elif isinstance ( refer . price_currency , str ): currency = refer . price_currency elif ( refer . cost_currency is None and refer . price_currency is None and isinstance ( refer . units_currency , str ) ): currency = refer . units_currency return currency","title":"get_bucket_currency()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.has_self_reduction","text":"Return true if the postings potentially reduce each other at cost. Parameters: postings \u2013 A list of postings with uninterpolated CostSpec cost instances. methods \u2013 A mapping of account name to their corresponding booking method. Returns: A boolean, true if there's a potential for self-reduction. Source code in beancount/parser/booking_full.py def has_self_reduction ( postings , methods ): \"\"\"Return true if the postings potentially reduce each other at cost. Args: postings: A list of postings with uninterpolated CostSpec cost instances. methods: A mapping of account name to their corresponding booking method. Returns: A boolean, true if there's a potential for self-reduction. \"\"\" # A mapping of (currency, cost-currency) and sign. cost_changes = {} for posting in postings : cost = posting . cost if cost is None : continue if methods [ posting . account ] is Booking . NONE : continue key = ( posting . account , posting . units . currency ) sign = 1 if posting . units . number > ZERO else - 1 if cost_changes . setdefault ( key , sign ) != sign : return True return False","title":"has_self_reduction()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.interpolate_group","text":"Interpolate missing numbers in the set of postings. Parameters: postings \u2013 A list of Posting instances. balances \u2013 A dict of account to its ante-inventory. currency \u2013 The weight currency of this group, used for reporting errors. tolerances \u2013 A dict of currency to tolerance values. Returns: A tuple of postings \u2013 A list of new posting instances. errors: A list of errors generated during interpolation. interpolated: A boolean, true if we did have to interpolate. In the case of an error, this returns the original list of postings, which is still incomplete. If an error is returned, you should probably skip the transaction altogether, or just not include the postings in it. (An alternative behaviour would be to return only the list of valid postings, but that would likely result in an unbalanced transaction. We do it this way by choice.) Source code in beancount/parser/booking_full.py def interpolate_group ( postings , balances , currency , tolerances ): \"\"\"Interpolate missing numbers in the set of postings. Args: postings: A list of Posting instances. balances: A dict of account to its ante-inventory. currency: The weight currency of this group, used for reporting errors. tolerances: A dict of currency to tolerance values. Returns: A tuple of postings: A list of new posting instances. errors: A list of errors generated during interpolation. interpolated: A boolean, true if we did have to interpolate. In the case of an error, this returns the original list of postings, which is still incomplete. If an error is returned, you should probably skip the transaction altogether, or just not include the postings in it. (An alternative behaviour would be to return only the list of valid postings, but that would likely result in an unbalanced transaction. We do it this way by choice.) \"\"\" errors = [] # Figure out which type of amount is missing, by creating a list of # incomplete postings and which type of units is missing. incomplete = [] for index , posting in enumerate ( postings ): units = posting . units cost = posting . cost price = posting . price # Identify incomplete parts of the Posting components. if units . number is MISSING : incomplete . append (( MissingType . UNITS , index )) if isinstance ( cost , CostSpec ): if cost and cost . number_per is MISSING : incomplete . append (( MissingType . COST_PER , index )) if cost and cost . number_total is MISSING : incomplete . append (( MissingType . COST_TOTAL , index )) else : # Check that a resolved instance of Cost never needs interpolation. # # Note that in theory we could support the interpolation of regular # per-unit costs in these if we wanted to; but because they're all # reducing postings that have been booked earlier, those never need # to be interpolated. if cost is not None : assert isinstance ( cost . number , Decimal ), \"Internal error: cost has no number: {} ; on postings: {} \" . format ( cost , postings ) if price and price . number is MISSING : incomplete . append (( MissingType . PRICE , index )) # The replacement posting for the incomplete posting of this group. new_posting = None if len ( incomplete ) == 0 : # If there are no missing numbers, just convert the CostSpec to Cost and # return that. out_postings = [ convert_costspec_to_cost ( posting ) for posting in postings ] elif len ( incomplete ) > 1 : # If there is more than a single value to be interpolated, generate an # error and return no postings. _ , posting_index = incomplete [ 0 ] errors . append ( InterpolationError ( postings [ posting_index ] . meta , \"Too many missing numbers for currency group ' {} '\" . format ( currency ), None , ) ) out_postings = [] else : # If there is a single missing number, calculate it and fill it in here. missing , index = incomplete [ 0 ] incomplete_posting = postings [ index ] # Convert augmenting postings' costs from CostSpec to corresponding Cost # instances, except for the incomplete posting. new_postings = [ ( posting if posting is incomplete_posting else convert_costspec_to_cost ( posting ) ) for posting in postings ] # Compute the balance of the other postings. residual = interpolate . compute_residual ( posting for posting in new_postings if posting is not incomplete_posting ) assert len ( residual ) < 2 , \"Internal error in grouping postings by currencies.\" if not residual . is_empty (): respos = next ( iter ( residual )) assert ( respos . cost is None ), \"Internal error; cost appears in weight calculation.\" assert ( respos . units . currency == currency ), \"Internal error; residual different than currency group.\" weight = - respos . units . number weight_currency = respos . units . currency else : weight = ZERO weight_currency = currency if missing == MissingType . UNITS : units = incomplete_posting . units cost = incomplete_posting . cost if cost : # Handle the special case where we only have total cost. if cost . number_per == ZERO : errors . append ( InterpolationError ( incomplete_posting . meta , \"Cannot infer per-unit cost only from total\" , None , ) ) return postings , errors , True assert ( cost . currency == weight_currency ), \"Internal error; residual currency different than missing currency.\" cost_total = cost . number_total or ZERO units_number = ( weight - cost_total ) / cost . number_per elif incomplete_posting . price : assert ( incomplete_posting . price . currency == weight_currency ), \"Internal error; residual currency different than missing currency.\" units_number = weight / incomplete_posting . price . number else : assert ( units . currency == weight_currency ), \"Internal error; residual currency different than missing currency.\" units_number = weight # Quantize the interpolated units if necessary. units_number = interpolate . quantize_with_tolerance ( tolerances , units . currency , units_number ) if weight != ZERO : new_pos = Position ( Amount ( units_number , units . currency ), cost ) new_posting = incomplete_posting . _replace ( units = new_pos . units , cost = new_pos . cost ) else : new_posting = None elif missing == MissingType . COST_PER : units = incomplete_posting . units cost = incomplete_posting . cost assert ( cost . currency == weight_currency ), \"Internal error; residual currency different than missing currency.\" if units . number != ZERO : number_per = ( weight - ( cost . number_total or ZERO )) / units . number new_cost = cost . _replace ( number_per = number_per ) new_pos = Position ( units , new_cost ) new_posting = incomplete_posting . _replace ( units = new_pos . units , cost = new_pos . cost ) else : new_posting = None elif missing == MissingType . COST_TOTAL : units = incomplete_posting . units cost = incomplete_posting . cost assert ( cost . currency == weight_currency ), \"Internal error; residual currency different than missing currency.\" number_total = weight - cost . number_per * units . number new_cost = cost . _replace ( number_total = number_total ) new_pos = Position ( units , new_cost ) new_posting = incomplete_posting . _replace ( units = new_pos . units , cost = new_pos . cost ) elif missing == MissingType . PRICE : units = incomplete_posting . units cost = incomplete_posting . cost if cost is not None : errors . append ( InterpolationError ( incomplete_posting . meta , \"Cannot infer price for postings with units held at cost\" , None , ) ) return postings , errors , True else : price = incomplete_posting . price assert ( price . currency == weight_currency ), \"Internal error; residual currency different than missing currency.\" new_price_number = abs ( weight / units . number ) new_posting = incomplete_posting . _replace ( price = Amount ( new_price_number , price . currency ) ) else : assert False , \"Internal error; Invalid missing type.\" # Replace the number in the posting. if new_posting is not None : # Set meta-data on the new posting to indicate it was interpolated. if new_posting . meta is None : new_posting = new_posting . _replace ( meta = {}) new_posting . meta [ interpolate . AUTOMATIC_META ] = True # Convert augmenting posting costs from CostSpec to a corresponding # Cost instance. new_postings [ index ] = convert_costspec_to_cost ( new_posting ) else : del new_postings [ index ] out_postings = new_postings assert all ( not isinstance ( posting . cost , CostSpec ) for posting in out_postings ) # Check that units are non-zero and that no cost remains negative; issue an # error if this is the case. for posting in out_postings : if posting . cost is None : continue # If there is a cost, we don't allow either a cost value of zero, # nor a zero number of units. Note that we allow a price of zero as # the only special case allowed (for conversion entries), but never # for costs. if posting . units . number == ZERO : errors . append ( InterpolationError ( posting . meta , 'Amount is zero: \" {} \"' . format ( posting . units ), None ) ) if posting . cost . number is not None and posting . cost . number < ZERO : errors . append ( InterpolationError ( posting . meta , 'Cost is negative: \" {} \"' . format ( posting . cost ), None ) ) return out_postings , errors , ( new_posting is not None )","title":"interpolate_group()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.replace_currencies","text":"Replace resolved currencies in the entry's Postings. This essentially applies the findings of categorize_by_currency() to produce new postings with all currencies resolved. Parameters: postings \u2013 A list of Posting instances to replace. refer_groups \u2013 A list of (currency, list of posting references) items as returned by categorize_by_currency(). Returns: A new list of items of (currency, list of Postings), postings for which the currencies have been replaced by their interpolated currency values. Source code in beancount/parser/booking_full.py def replace_currencies ( postings , refer_groups ): \"\"\"Replace resolved currencies in the entry's Postings. This essentially applies the findings of categorize_by_currency() to produce new postings with all currencies resolved. Args: postings: A list of Posting instances to replace. refer_groups: A list of (currency, list of posting references) items as returned by categorize_by_currency(). Returns: A new list of items of (currency, list of Postings), postings for which the currencies have been replaced by their interpolated currency values. \"\"\" new_groups = [] for currency , refers in refer_groups : new_postings = [] for refer in sorted ( refers , key = lambda r : r . index ): posting = postings [ refer . index ] units = posting . units if units is MISSING or units is None : posting = posting . _replace ( units = Amount ( MISSING , refer . units_currency )) else : replace = False cost = posting . cost price = posting . price if units . currency is MISSING : units = Amount ( units . number , refer . units_currency ) replace = True if cost and cost . currency is MISSING : cost = cost . _replace ( currency = refer . cost_currency ) replace = True if price and price . currency is MISSING : price = Amount ( price . number , refer . price_currency ) replace = True if replace : posting = posting . _replace ( units = units , cost = cost , price = price ) new_postings . append ( posting ) new_groups . append (( currency , new_postings )) return new_groups","title":"replace_currencies()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_full.unique_label","text":"Return a globally unique label for cost entries. Source code in beancount/parser/booking_full.py def unique_label () -> str : \"Return a globally unique label for cost entries.\" return str ( uuid . uuid4 ())","title":"unique_label()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method","text":"Implementations of all the particular booking methods. This code is used by the full booking algorithm.","title":"booking_method"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.AmbiguousMatchError","text":"AmbiguousMatchError(source, message, entry)","title":"AmbiguousMatchError"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.AmbiguousMatchError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/booking_method.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.AmbiguousMatchError.__new__","text":"Create new instance of AmbiguousMatchError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.AmbiguousMatchError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/booking_method.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.booking_method_AVERAGE","text":"AVERAGE booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_AVERAGE ( entry , posting , matches ): \"\"\"AVERAGE booking method implementation.\"\"\" booked_reductions = [] booked_matches = [] errors = [ AmbiguousMatchError ( entry . meta , \"AVERAGE method is not supported\" , entry )] return booked_reductions , booked_matches , errors , False # FIXME: Future implementation here. if False : # DISABLED - This is the code for AVERAGE, which is currently disabled. # If there is more than a single match we need to ultimately merge the # postings. Also, if the reducing posting provides a specific cost, we # need to update the cost basis as well. Both of these cases are carried # out by removing all the matches and readding them later on. if len ( matches ) == 1 and ( not isinstance ( posting . cost . number_per , Decimal ) and not isinstance ( posting . cost . number_total , Decimal ) ): # There is no cost. Just reduce the one leg. This should be the # normal case if we always merge augmentations and the user lets # Beancount deal with the cost. match = matches [ 0 ] sign = - 1 if posting . units . number < ZERO else 1 number = min ( abs ( match . units . number ), abs ( posting . units . number )) match_units = Amount ( number * sign , match . units . currency ) booked_reductions . append ( posting . _replace ( units = match_units , cost = match . cost )) _insufficient = match_units . number != posting . units . number else : # Merge the matching postings to a single one. merged_units = inventory . Inventory () merged_cost = inventory . Inventory () for match in matches : merged_units . add_amount ( match . units ) merged_cost . add_amount ( convert . get_weight ( match )) if len ( merged_units ) != 1 or len ( merged_cost ) != 1 : errors . append ( AmbiguousMatchError ( entry . meta , \"Cannot merge positions in multiple currencies: {} \" . format ( \", \" . join ( position . to_string ( match_posting ) for match_posting in matches ) ), entry , ) ) else : if isinstance ( posting . cost . number_per , Decimal ) or isinstance ( posting . cost . number_total , Decimal ): errors . append ( AmbiguousMatchError ( entry . meta , \"Explicit cost reductions aren't supported yet: {} \" . format ( position . to_string ( posting ) ), entry , ) ) else : # Insert postings to remove all the matches. booked_reductions . extend ( posting . _replace ( units =- match . units , cost = match . cost , flag = flags . FLAG_MERGING ) for match in matches ) units = merged_units [ 0 ] . units date = matches [ 0 ] . cost . date ## FIXME: Select which one, ## oldest or latest. cost_units = merged_cost [ 0 ] . units cost = Cost ( cost_units . number / units . number , cost_units . currency , date , None ) # Insert a posting to refill those with a replacement match. booked_reductions . append ( posting . _replace ( units = units , cost = cost , flag = flags . FLAG_MERGING ) ) # Now, match the reducing request against this lot. booked_reductions . append ( posting . _replace ( units = posting . units , cost = cost ) ) _insufficient = abs ( posting . units . number ) > abs ( units . number )","title":"booking_method_AVERAGE()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.booking_method_FIFO","text":"FIFO booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_FIFO ( entry , posting , matches ): \"\"\"FIFO booking method implementation.\"\"\" return _booking_method_xifo ( entry , posting , matches , \"date\" , False )","title":"booking_method_FIFO()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.booking_method_HIFO","text":"HIFO booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_HIFO ( entry , posting , matches ): \"\"\"HIFO booking method implementation.\"\"\" return _booking_method_xifo ( entry , posting , matches , \"number\" , True )","title":"booking_method_HIFO()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.booking_method_LIFO","text":"LIFO booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_LIFO ( entry , posting , matches ): \"\"\"LIFO booking method implementation.\"\"\" return _booking_method_xifo ( entry , posting , matches , \"date\" , True )","title":"booking_method_LIFO()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.booking_method_NONE","text":"NONE booking method implementation. Source code in beancount/parser/booking_method.py def booking_method_NONE ( entry , posting , matches ): \"\"\"NONE booking method implementation.\"\"\" # This never needs to match against any existing positions... we # disregard the matches, there's never any error. Note that this never # gets called in practice, we want to treat NONE postings as # augmentations. Default behaviour is to return them with their original # CostSpec, and the augmentation code will handle signaling an error if # there is insufficient detail to carry out the conversion to an # instance of Cost. # Note that it's an interesting question whether a reduction on an # account with NONE method which happens to match a single position # ought to be matched against it. We don't allow it for now. return [ posting ], [], False","title":"booking_method_NONE()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.booking_method_STRICT","text":"Strict booking method. This method fails if there are ambiguous matches. Source code in beancount/parser/booking_method.py def booking_method_STRICT ( entry , posting , matches ): \"\"\"Strict booking method. This method fails if there are ambiguous matches.\"\"\" booked_reductions = [] booked_matches = [] errors = [] insufficient = False # In strict mode, we require at most a single matching posting. if len ( matches ) > 1 : # If the total requested to reduce matches the sum of all the # ambiguous postings, match against all of them. sum_matches = sum ( p . units . number for p in matches ) if sum_matches == - posting . units . number : booked_reductions . extend ( posting . _replace ( units =- match . units , cost = match . cost ) for match in matches ) else : errors . append ( AmbiguousMatchError ( entry . meta , 'Ambiguous matches for \" {} \": {} ' . format ( position . to_string ( posting ), \", \" . join ( position . to_string ( match_posting ) for match_posting in matches ), ), entry , ) ) else : # Replace the posting's units and cost values. match = matches [ 0 ] sign = - 1 if posting . units . number < ZERO else 1 number = min ( abs ( match . units . number ), abs ( posting . units . number )) match_units = Amount ( number * sign , match . units . currency ) booked_reductions . append ( posting . _replace ( units = match_units , cost = match . cost )) booked_matches . append ( match ) insufficient = match_units . number != posting . units . number return booked_reductions , booked_matches , errors , insufficient","title":"booking_method_STRICT()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.booking_method_STRICT_WITH_SIZE","text":"Strict booking method, but disambiguate further with sizes. This booking method applies the same algorithm as the STRICT method, but if only one of the ambiguous lots matches the desired size, select that one automatically. Source code in beancount/parser/booking_method.py def booking_method_STRICT_WITH_SIZE ( entry , posting , matches ): \"\"\"Strict booking method, but disambiguate further with sizes. This booking method applies the same algorithm as the STRICT method, but if only one of the ambiguous lots matches the desired size, select that one automatically. \"\"\" ( booked_reductions , booked_matches , errors , insufficient ) = booking_method_STRICT ( entry , posting , matches ) # If we couldn't match strictly, attempt to find a match with the same # number of units. If there is one or more of these, accept the oldest lot. if errors and len ( matches ) > 1 : number = - posting . units . number matching_units = [ match for match in matches if number == match . units . number ] if matching_units : matching_units . sort ( key = lambda match : match . cost . date ) # Replace the posting's units and cost values. match = matching_units [ 0 ] booked_reductions . append ( posting . _replace ( units =- match . units , cost = match . cost )) booked_matches . append ( match ) insufficient = False errors = [] return booked_reductions , booked_matches , errors , insufficient","title":"booking_method_STRICT_WITH_SIZE()"},{"location":"api_reference/beancount.parser.html#beancount.parser.booking_method.handle_ambiguous_matches","text":"Handle ambiguous matches by dispatching to a particular method. Parameters: entry \u2013 The parent Transaction instance. posting \u2013 An instance of Posting, the reducing posting which we're attempting to match. matches \u2013 A list of matching Position instances from the ante-inventory. Those positions are known to already match the 'posting' spec. methods \u2013 A mapping of account name to their corresponding booking method. Returns: A triple of booked_reductions \u2013 A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. errors: A list of errors to be generated. insufficient: A boolean, true if we could not find enough matches to cover the entire position. Source code in beancount/parser/booking_method.py def handle_ambiguous_matches ( entry , posting , matches , method ): \"\"\"Handle ambiguous matches by dispatching to a particular method. Args: entry: The parent Transaction instance. posting: An instance of Posting, the reducing posting which we're attempting to match. matches: A list of matching Position instances from the ante-inventory. Those positions are known to already match the 'posting' spec. methods: A mapping of account name to their corresponding booking method. Returns: A triple of booked_reductions: A list of matched Posting instances, whose 'cost' attributes are ensured to be of type Cost. errors: A list of errors to be generated. insufficient: A boolean, true if we could not find enough matches to cover the entire position. \"\"\" assert isinstance ( method , Booking ), \"Invalid type: {} \" . format ( method ) assert matches , \"Internal error: Invalid call with no matches\" # method = globals()['booking_method_{}'.format(method.name)] method = _BOOKING_METHODS [ method ] ( booked_reductions , booked_matches , errors , insufficient ) = method ( entry , posting , matches ) if insufficient : errors . append ( AmbiguousMatchError ( entry . meta , 'Not enough lots to reduce \" {} \": {} ' . format ( position . to_string ( posting ), \", \" . join ( position . to_string ( match_posting ) for match_posting in matches ), ), entry , ) ) return booked_reductions , booked_matches , errors","title":"handle_ambiguous_matches()"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest","text":"Support utilities for testing scripts.","title":"cmptest"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest.TestCase","text":"","title":"TestCase"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest.TestCase.assertEqualEntries","text":"Check that two lists of entries are equal. Entries can be provided either as a list of directives or as a string. In the latter case, the string is parsed with beancount.parser.parse_string() and the resulting directives list is used. If allow_incomplete is True, light-weight booking is performed before comparing the directive lists, allowing to compare transactions with incomplete postings. Parameters: expected_entries \u2013 Expected entries. actual_entries \u2013 Actual entries. allow_incomplete \u2013 Perform booking before comparison. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/parser/cmptest.py def assertEqualEntries ( self , expected_entries , actual_entries , allow_incomplete = False ): \"\"\"Check that two lists of entries are equal. Entries can be provided either as a list of directives or as a string. In the latter case, the string is parsed with beancount.parser.parse_string() and the resulting directives list is used. If allow_incomplete is True, light-weight booking is performed before comparing the directive lists, allowing to compare transactions with incomplete postings. Args: expected_entries: Expected entries. actual_entries: Actual entries. allow_incomplete: Perform booking before comparison. Raises: AssertionError: If the exception fails. \"\"\" expected_entries = read_string_or_entries ( expected_entries , allow_incomplete ) actual_entries = read_string_or_entries ( actual_entries , allow_incomplete ) same , expected_missing , actual_missing = compare . compare_entries ( expected_entries , actual_entries ) if not same : assert expected_missing or actual_missing , \"Missing is missing: {} , {} \" . format ( expected_missing , actual_missing ) oss = io . StringIO () if expected_missing : oss . write ( \"Present in expected set and not in actual set: \\n\\n \" ) for entry in expected_missing : oss . write ( printer . format_entry ( entry )) oss . write ( \" \\n \" ) if actual_missing : oss . write ( \"Present in actual set and not in expected set: \\n\\n \" ) for entry in actual_missing : oss . write ( printer . format_entry ( entry )) oss . write ( \" \\n \" ) self . fail ( oss . getvalue ())","title":"assertEqualEntries()"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest.TestCase.assertExcludesEntries","text":"Check that subset_entries is not included in entries. Entries can be provided either as a list of directives or as a string. In the latter case, the string is parsed with beancount.parser.parse_string() and the resulting directives list is used. If allow_incomplete is True, light-weight booking is performed before comparing the directive lists, allowing to compare transactions with incomplete postings. Parameters: subset_entries \u2013 Subset entries. entries \u2013 Entries. allow_incomplete \u2013 Perform booking before comparison. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/parser/cmptest.py def assertExcludesEntries ( self , subset_entries , entries , allow_incomplete = False ): \"\"\"Check that subset_entries is not included in entries. Entries can be provided either as a list of directives or as a string. In the latter case, the string is parsed with beancount.parser.parse_string() and the resulting directives list is used. If allow_incomplete is True, light-weight booking is performed before comparing the directive lists, allowing to compare transactions with incomplete postings. Args: subset_entries: Subset entries. entries: Entries. allow_incomplete: Perform booking before comparison. Raises: AssertionError: If the exception fails. \"\"\" subset_entries = read_string_or_entries ( subset_entries , allow_incomplete ) entries = read_string_or_entries ( entries ) excludes , extra = compare . excludes_entries ( subset_entries , entries ) if not excludes : assert extra , \"Extra is empty: {} \" . format ( extra ) oss = io . StringIO () if extra : oss . write ( \"Extra from from first/excluded set: \\n\\n \" ) for entry in extra : oss . write ( printer . format_entry ( entry )) oss . write ( \" \\n \" ) self . fail ( oss . getvalue ())","title":"assertExcludesEntries()"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest.TestCase.assertIncludesEntries","text":"Check that subset_entries is included in entries. Entries can be provided either as a list of directives or as a string. In the latter case, the string is parsed with beancount.parser.parse_string() and the resulting directives list is used. If allow_incomplete is True, light-weight booking is performed before comparing the directive lists, allowing to compare transactions with incomplete postings. Parameters: subset_entries \u2013 Subset entries. entries \u2013 Entries. allow_incomplete \u2013 Perform booking before comparison. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/parser/cmptest.py def assertIncludesEntries ( self , subset_entries , entries , allow_incomplete = False ): \"\"\"Check that subset_entries is included in entries. Entries can be provided either as a list of directives or as a string. In the latter case, the string is parsed with beancount.parser.parse_string() and the resulting directives list is used. If allow_incomplete is True, light-weight booking is performed before comparing the directive lists, allowing to compare transactions with incomplete postings. Args: subset_entries: Subset entries. entries: Entries. allow_incomplete: Perform booking before comparison. Raises: AssertionError: If the exception fails. \"\"\" subset_entries = read_string_or_entries ( subset_entries , allow_incomplete ) entries = read_string_or_entries ( entries ) includes , missing = compare . includes_entries ( subset_entries , entries ) if not includes : assert missing , \"Missing is empty: {} \" . format ( missing ) oss = io . StringIO () if missing : oss . write ( \"Missing from from expected set: \\n\\n \" ) for entry in missing : oss . write ( printer . format_entry ( entry )) oss . write ( \" \\n \" ) self . fail ( oss . getvalue ())","title":"assertIncludesEntries()"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest.TestError","text":"Errors within the test implementation itself. These should never occur.","title":"TestError"},{"location":"api_reference/beancount.parser.html#beancount.parser.cmptest.read_string_or_entries","text":"Read a string of entries or just entries. Parameters: entries_or_str \u2013 Either a list of directives, or a string containing directives. allow_incomplete \u2013 A boolean, true if we allow incomplete inputs and perform light-weight booking. Returns: A list of directives. Source code in beancount/parser/cmptest.py def read_string_or_entries ( entries_or_str , allow_incomplete = False ): \"\"\"Read a string of entries or just entries. Args: entries_or_str: Either a list of directives, or a string containing directives. allow_incomplete: A boolean, true if we allow incomplete inputs and perform light-weight booking. Returns: A list of directives. \"\"\" if isinstance ( entries_or_str , str ): entries , errors , options_map = parser . parse_string ( textwrap . dedent ( entries_or_str )) if allow_incomplete : # Do a simplistic local conversion in order to call the comparison. entries = [ _local_booking ( entry ) for entry in entries ] else : # Don't accept incomplete entries either. if any ( parser . is_entry_incomplete ( entry ) for entry in entries ): raise TestError ( \"Entries in assertions may not use interpolation.\" ) entries , booking_errors = booking . book ( entries , options_map ) errors = errors + booking_errors # Don't tolerate errors. if errors : oss = io . StringIO () printer . print_errors ( errors , file = oss ) raise TestError ( \"Unexpected errors in expected: {} \" . format ( oss . getvalue ())) else : assert isinstance ( entries_or_str , list ), \"Expecting list: {} \" . format ( entries_or_str ) entries = entries_or_str return entries","title":"read_string_or_entries()"},{"location":"api_reference/beancount.parser.html#beancount.parser.context","text":"Produce a rendering of the account balances just before and after a particular entry is applied.","title":"context"},{"location":"api_reference/beancount.parser.html#beancount.parser.context.render_entry_context","text":"Render the context before and after a particular transaction is applied. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. entry \u2013 The entry instance which should be rendered. (Note that this object is expected to be in the set of entries, not just structurally equal.) parsed_entry \u2013 An optional incomplete, parsed but not booked nor interpolated entry. If this is provided, this is used for inspecting the list of prior accounts and it is also rendered. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. Source code in beancount/parser/context.py def render_entry_context ( entries , options_map , entry , parsed_entry = None ): \"\"\"Render the context before and after a particular transaction is applied. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. entry: The entry instance which should be rendered. (Note that this object is expected to be in the set of entries, not just structurally equal.) parsed_entry: An optional incomplete, parsed but not booked nor interpolated entry. If this is provided, this is used for inspecting the list of prior accounts and it is also rendered. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. \"\"\" oss = io . StringIO () pr = functools . partial ( print , file = oss ) header = \"** {} --------------------------------\" meta = entry . meta pr ( header . format ( \"Transaction Id\" )) pr () pr ( \"Hash: {} \" . format ( compare . hash_entry ( entry ))) pr ( \"Location: {} : {} \" . format ( meta [ \"filename\" ], meta [ \"lineno\" ])) pr () pr () # Get the list of accounts sorted by the order in which they appear in the # closest entry. order = {} if parsed_entry is None : parsed_entry = entry if isinstance ( parsed_entry , data . Transaction ): order = { posting . account : index for index , posting in enumerate ( parsed_entry . postings ) } accounts = sorted ( getters . get_entry_accounts ( parsed_entry ), key = lambda account : order . get ( account , 10000 ), ) # Accumulate the balances of these accounts up to the entry. balance_before , balance_after = interpolate . compute_entry_context ( entries , entry , additional_accounts = accounts ) # Create a format line for printing the contents of account balances. max_account_width = max ( map ( len , accounts )) if accounts else 1 position_line = \"{{:1}} {{: {width} }} {{:>49}}\" . format ( width = max_account_width ) # Print the context before. pr ( header . format ( \"Balances before transaction\" )) pr () before_hashes = set () average_costs = {} for account in accounts : balance = balance_before [ account ] pc_balances = balance . split () for currency , pc_balance in pc_balances . items (): if len ( pc_balance ) > 1 : average_costs [ account ] = pc_balance . average () positions = balance . get_positions () for position in positions : before_hashes . add (( account , hash ( position ))) pr ( position_line . format ( \"\" , account , str ( position ))) if not positions : pr ( position_line . format ( \"\" , account , \"\" )) pr () pr () # Print average cost per account, if relevant. if average_costs : pr ( header . format ( \"Average Costs\" )) pr () for account , average_cost in sorted ( average_costs . items ()): for position in average_cost : pr ( position_line . format ( \"\" , account , str ( position ))) pr () pr () # Print the entry itself. dcontext = options_map [ \"dcontext\" ] pr ( header . format ( \"Unbooked Transaction\" )) pr () if parsed_entry : printer . print_entry ( parsed_entry , dcontext , render_weights = True , file = oss ) pr () pr ( header . format ( \"Transaction\" )) pr () printer . print_entry ( entry , dcontext , render_weights = True , file = oss ) pr () if isinstance ( entry , data . Transaction ): pr ( header . format ( \"Residual and Tolerances\" )) pr () # Print residuals. residual = interpolate . compute_residual ( entry . postings ) if not residual . is_empty (): # Note: We render the residual at maximum precision, for debugging. pr ( \"Residual: {} \" . format ( residual )) # Dump the tolerances used. tolerances = interpolate . infer_tolerances ( entry . postings , options_map ) if tolerances : pr ( \"Tolerances: {} \" . format ( \", \" . join ( \" {} = {} \" . format ( key , value ) for key , value in sorted ( tolerances . items ()) ) ) ) # Compute the total cost basis. cost_basis = inventory . Inventory ( pos for pos in entry . postings if pos . cost is not None ) . reduce ( convert . get_cost ) if not cost_basis . is_empty (): pr ( \"Basis: {} \" . format ( cost_basis )) pr () pr () # Print the context after. pr ( header . format ( \"Balances after transaction\" )) pr () for account in accounts : positions = balance_after [ account ] . get_positions () for position in positions : changed = ( account , hash ( position )) not in before_hashes print ( position_line . format ( \"*\" if changed else \"\" , account , str ( position )), file = oss , ) if not positions : pr ( position_line . format ( \"\" , account , \"\" )) pr () return oss . getvalue ()","title":"render_entry_context()"},{"location":"api_reference/beancount.parser.html#beancount.parser.context.render_file_context","text":"Render the context before and after a particular transaction is applied. Parameters: entries \u2013 A list of directives. options_map \u2013 A dict of options, as produced by the parser. filename \u2013 A string, the name of the file from which the transaction was parsed. lineno \u2013 An integer, the line number in the file the transaction was parsed from. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. Source code in beancount/parser/context.py def render_file_context ( entries , options_map , filename , lineno ): \"\"\"Render the context before and after a particular transaction is applied. Args: entries: A list of directives. options_map: A dict of options, as produced by the parser. filename: A string, the name of the file from which the transaction was parsed. lineno: An integer, the line number in the file the transaction was parsed from. Returns: A multiline string of text, which consists of the context before the transaction is applied, the transaction itself, and the context after it is applied. You can just print that, it is in form that is intended to be consumed by the user. \"\"\" # Find the closest entry. closest_entry = data . find_closest ( entries , filename , lineno ) if closest_entry is None : raise SystemExit ( \"No entry could be found before {} : {} \" . format ( filename , lineno )) # Run just the parser stage (no booking nor interpolation, which would # remove the postings) on the input file to produced the corresponding # unbooked transaction, so that we can get the list of accounts. if path . exists ( filename ): parsed_entries , _ , __ = parser . parse_file ( filename ) # Note: We cannot bisect as we cannot rely on sorting behavior from the parser. lineno = closest_entry . meta [ \"lineno\" ] closest_parsed_entries = [ parsed_entry for parsed_entry in parsed_entries if parsed_entry . meta [ \"lineno\" ] == lineno ] if len ( closest_parsed_entries ) != 1 : # This is an internal error, this should never occur. raise RuntimeError ( \"Parsed entry corresponding to real entry not found in original filename.\" ) closest_parsed_entry = next ( iter ( closest_parsed_entries )) else : closest_parsed_entry = None return render_entry_context ( entries , options_map , closest_entry , closest_parsed_entry )","title":"render_file_context()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar","text":"Builder for Beancount grammar.","title":"grammar"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder","text":"A builder used by the lexer and grammar parser as callbacks to create the data objects corresponding to rules parsed from the input file.","title":"Builder"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.account","text":"Check account name validity. Parameters: account \u2013 a str, the account name. Returns: A string, the account name. Source code in beancount/parser/grammar.py def account ( self , filename , lineno , account ): \"\"\"Check account name validity. Args: account: a str, the account name. Returns: A string, the account name. \"\"\" if not self . account_regexp . match ( account ): meta = new_metadata ( filename , lineno ) self . errors . append ( ParserError ( meta , \"Invalid account name: {} \" . format ( account ), None ) ) # Intern account names. This should reduces memory usage a # fair bit because these strings are repeated liberally. return self . accounts . setdefault ( account , account )","title":"account()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.amount","text":"Process an amount grammar rule. Parameters: number \u2013 a Decimal instance, the number of the amount. currency \u2013 a currency object (a str, really, see CURRENCY above) Returns: An instance of Amount. Source code in beancount/parser/grammar.py def amount ( self , filename , lineno , number , currency ): \"\"\"Process an amount grammar rule. Args: number: a Decimal instance, the number of the amount. currency: a currency object (a str, really, see CURRENCY above) Returns: An instance of Amount. \"\"\" # Update the mapping that stores the parsed precisions. # Note: This is relatively slow, adds about 70ms because of number.as_tuple(). self . _dcupdate ( number , currency ) return Amount ( number , currency )","title":"amount()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.balance","text":"Process an assertion directive. We produce no errors here by default. We replace the failing ones in the routine that does the verification later one, that these have succeeded or failed. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account to balance. amount \u2013 The expected amount, to be checked. tolerance \u2013 The tolerance number. kvlist \u2013 a list of KeyValue instances. Returns: A new Balance object. Source code in beancount/parser/grammar.py def balance ( self , filename , lineno , date , account , amount , tolerance , kvlist ): \"\"\"Process an assertion directive. We produce no errors here by default. We replace the failing ones in the routine that does the verification later one, that these have succeeded or failed. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to balance. amount: The expected amount, to be checked. tolerance: The tolerance number. kvlist: a list of KeyValue instances. Returns: A new Balance object. \"\"\" diff_amount = None meta = new_metadata ( filename , lineno , kvlist ) return Balance ( meta , date , account , amount , tolerance , diff_amount )","title":"balance()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.build_grammar_error","text":"Build a grammar error and appends it to the list of pending errors. Parameters: filename \u2013 The current filename lineno \u2013 The current line number excvalue \u2013 The exception value, or a str, the message of the error. exc_type \u2013 An exception type, if an exception occurred. exc_traceback \u2013 A traceback object. Source code in beancount/parser/grammar.py def build_grammar_error ( self , filename , lineno , exc_value , exc_type = None , exc_traceback = None ): \"\"\"Build a grammar error and appends it to the list of pending errors. Args: filename: The current filename lineno: The current line number excvalue: The exception value, or a str, the message of the error. exc_type: An exception type, if an exception occurred. exc_traceback: A traceback object. \"\"\" if exc_type is not None : assert not isinstance ( exc_value , str ) strings = traceback . format_exception_only ( exc_type , exc_value ) tblist = traceback . extract_tb ( exc_traceback ) filename , lineno , _ , __ = tblist [ 0 ] message = \" {} ( {} : {} )\" . format ( strings [ 0 ], filename , lineno ) else : message = str ( exc_value ) meta = new_metadata ( filename , lineno ) self . errors . append ( ParserSyntaxError ( meta , message , None ))","title":"build_grammar_error()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.close","text":"Process a close directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the name of the account. kvlist \u2013 a list of KeyValue instances. Returns: A new Close object. Source code in beancount/parser/grammar.py def close ( self , filename , lineno , date , account , kvlist ): \"\"\"Process a close directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the name of the account. kvlist: a list of KeyValue instances. Returns: A new Close object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Close ( meta , date , account )","title":"close()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.commodity","text":"Process a close directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. currency \u2013 A string, the commodity being declared. kvlist \u2013 a list of KeyValue instances. Returns: A new Close object. Source code in beancount/parser/grammar.py def commodity ( self , filename , lineno , date , currency , kvlist ): \"\"\"Process a close directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. currency: A string, the commodity being declared. kvlist: a list of KeyValue instances. Returns: A new Close object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Commodity ( meta , date , currency )","title":"commodity()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.compound_amount","text":"Process an amount grammar rule. Parameters: number_per \u2013 a Decimal instance, the number of the cost per share. number_total \u2013 a Decimal instance, the number of the cost over all shares. currency \u2013 a currency object (a str, really, see CURRENCY above) Returns: A triple of (Decimal, Decimal, currency string) to be processed further when creating the final per-unit cost number. Source code in beancount/parser/grammar.py def compound_amount ( self , filename , lineno , number_per , number_total , currency ): \"\"\"Process an amount grammar rule. Args: number_per: a Decimal instance, the number of the cost per share. number_total: a Decimal instance, the number of the cost over all shares. currency: a currency object (a str, really, see CURRENCY above) Returns: A triple of (Decimal, Decimal, currency string) to be processed further when creating the final per-unit cost number. \"\"\" # Update the mapping that stores the parsed precisions. # Note: This is relatively slow, adds about 70ms because of number.as_tuple(). self . _dcupdate ( number_per , currency ) self . _dcupdate ( number_total , currency ) # Note that we are not able to reduce the value to a number per-share # here because we only get the number of units in the full lot spec. return CompoundAmount ( number_per , number_total , currency )","title":"compound_amount()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.cost_merge","text":"Create a 'merge cost' token. Source code in beancount/parser/grammar.py def cost_merge ( self , filename , lineno , _ ): \"\"\"Create a 'merge cost' token.\"\"\" return MERGE_COST","title":"cost_merge()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.cost_spec","text":"Process a cost_spec grammar rule. Parameters: cost_comp_list \u2013 A list of CompoundAmount, a datetime.date, or label ID strings. is_total \u2013 Assume only the total cost is specified; reject the # syntax, that is, no compound amounts may be specified. This is used to support the {{...}} syntax. Returns: A cost-info tuple of CompoundAmount, lot date and label string. Any of these may be set to a sentinel indicating \"unset\". Source code in beancount/parser/grammar.py def cost_spec ( self , filename , lineno , cost_comp_list , is_total ): \"\"\"Process a cost_spec grammar rule. Args: cost_comp_list: A list of CompoundAmount, a datetime.date, or label ID strings. is_total: Assume only the total cost is specified; reject the # syntax, that is, no compound amounts may be specified. This is used to support the {{...}} syntax. Returns: A cost-info tuple of CompoundAmount, lot date and label string. Any of these may be set to a sentinel indicating \"unset\". \"\"\" if not cost_comp_list : return CostSpec ( MISSING , None , MISSING , None , None , False ) assert isinstance ( cost_comp_list , list ), \"Internal error in parser: {} \" . format ( cost_comp_list ) compound_cost = None date_ = None label = None merge = None for comp in cost_comp_list : if isinstance ( comp , CompoundAmount ): if compound_cost is None : compound_cost = comp else : self . errors . append ( ParserError ( new_metadata ( filename , lineno ), \"Duplicate cost: ' {} '.\" . format ( comp ), None , ) ) elif isinstance ( comp , date ): if date_ is None : date_ = comp else : self . errors . append ( ParserError ( new_metadata ( filename , lineno ), \"Duplicate date: ' {} '.\" . format ( comp ), None , ) ) elif comp is MERGE_COST : if merge is None : merge = True self . errors . append ( ParserError ( new_metadata ( filename , lineno ), \"Cost merging is not supported yet\" , None , ) ) else : self . errors . append ( ParserError ( new_metadata ( filename , lineno ), \"Duplicate merge-cost spec\" , None , ) ) else : assert isinstance ( comp , str ), \"Currency component is not string: ' {} '\" . format ( comp ) if label is None : label = comp else : self . errors . append ( ParserError ( new_metadata ( filename , lineno ), \"Duplicate label: ' {} '.\" . format ( comp ), None , ) ) # If there was a cost_comp_list, thus a \"{...}\" cost basis spec, you must # indicate that by creating a CompoundAmount(), always. if compound_cost is None : number_per , number_total , currency = MISSING , None , MISSING else : number_per , number_total , currency = compound_cost if is_total : if number_total is not None : self . errors . append ( ParserError ( new_metadata ( filename , lineno ), ( \"Per-unit cost may not be specified using total cost \" \"syntax: ' {} '; ignoring per-unit cost\" ) . format ( compound_cost ), None , ) ) # Ignore per-unit number. number_per = ZERO else : # There's a single number specified; interpret it as a total cost. number_total = number_per number_per = ZERO if merge is None : merge = False return CostSpec ( number_per , number_total , currency , date_ , label , merge )","title":"cost_spec()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.custom","text":"Process a custom directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. dir_type \u2013 A string, a type for the custom directive being parsed. custom_values \u2013 A list of the various tokens seen on the same line. kvlist \u2013 a list of KeyValue instances. Returns: A new Custom object. Source code in beancount/parser/grammar.py def custom ( self , filename , lineno , date , dir_type , custom_values , kvlist ): \"\"\"Process a custom directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. dir_type: A string, a type for the custom directive being parsed. custom_values: A list of the various tokens seen on the same line. kvlist: a list of KeyValue instances. Returns: A new Custom object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Custom ( meta , date , dir_type , custom_values )","title":"custom()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.custom_value","text":"Create a custom value object, along with its type. Parameters: value \u2013 One of the accepted custom values. Returns: A pair of (value, dtype) where 'dtype' is the datatype is that of the value. Source code in beancount/parser/grammar.py def custom_value ( self , filename , lineno , value , dtype = None ): \"\"\"Create a custom value object, along with its type. Args: value: One of the accepted custom values. Returns: A pair of (value, dtype) where 'dtype' is the datatype is that of the value. \"\"\" if dtype is None : dtype = type ( value ) return ValueType ( value , dtype )","title":"custom_value()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.document","text":"Process a document directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. account \u2013 an Account instance. document_filename \u2013 a str, the name of the document file. tags_links \u2013 The current TagsLinks accumulator. kvlist \u2013 a list of KeyValue instances. Returns: A new Document object. Source code in beancount/parser/grammar.py def document ( self , filename , lineno , date , account , document_filename , tags_links , kvlist ): \"\"\"Process a document directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. account: an Account instance. document_filename: a str, the name of the document file. tags_links: The current TagsLinks accumulator. kvlist: a list of KeyValue instances. Returns: A new Document object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) if not path . isabs ( document_filename ): document_filename = path . abspath ( path . join ( path . dirname ( filename ), document_filename ) ) tags , links = self . _finalize_tags_links ( tags_links . tags , tags_links . links ) return Document ( meta , date , account , document_filename , tags , links )","title":"document()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.event","text":"Process an event directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. event_type \u2013 a str, the name of the event type. description \u2013 a str, the event value, the contents. kvlist \u2013 a list of KeyValue instances. Returns: A new Event object. Source code in beancount/parser/grammar.py def event ( self , filename , lineno , date , event_type , description , kvlist ): \"\"\"Process an event directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. event_type: a str, the name of the event type. description: a str, the event value, the contents. kvlist: a list of KeyValue instances. Returns: A new Event object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Event ( meta , date , event_type , description )","title":"event()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.finalize","text":"Finalize the parser, check for final errors and return the triple. Returns: A triple of entries \u2013 A list of parsed directives, which may need completion. errors: A list of errors, hopefully empty. options_map: A dict of options. Source code in beancount/parser/grammar.py def finalize ( self ): \"\"\"Finalize the parser, check for final errors and return the triple. Returns: A triple of entries: A list of parsed directives, which may need completion. errors: A list of errors, hopefully empty. options_map: A dict of options. \"\"\" # If the user left some tags unbalanced, issue an error. for tag in self . tags : meta = new_metadata ( self . options [ \"filename\" ], 0 ) self . errors . append ( ParserError ( meta , \"Unbalanced pushed tag: ' {} '\" . format ( tag ), None ) ) # If the user left some metadata unpopped, issue an error. for key , value_list in self . meta . items (): meta = new_metadata ( self . options [ \"filename\" ], 0 ) self . errors . append ( ParserError ( meta , ( \"Unbalanced metadata key ' {} '; leftover metadata ' {} '\" ) . format ( key , \", \" . join ( value_list ) ), None , ) ) # Weave the commas option in the DisplayContext itself, so it propagates # everywhere it is used automatically. self . dcontext . set_commas ( self . options [ \"render_commas\" ]) return ( self . get_entries (), self . errors , self . get_options ())","title":"finalize()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.get_entries","text":"Return the accumulated entries. Returns: A list of sorted directives. Source code in beancount/parser/grammar.py def get_entries ( self ): \"\"\"Return the accumulated entries. Returns: A list of sorted directives. \"\"\" return sorted ( self . entries , key = data . entry_sortkey )","title":"get_entries()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.get_long_string_maxlines","text":"See base class. Source code in beancount/parser/grammar.py def get_long_string_maxlines ( self ): \"\"\"See base class.\"\"\" return self . options [ \"long_string_maxlines\" ]","title":"get_long_string_maxlines()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.get_options","text":"Return the final options map. Returns: A dict of option names to options. Source code in beancount/parser/grammar.py def get_options ( self ): \"\"\"Return the final options map. Returns: A dict of option names to options. \"\"\" # Build and store the inferred DisplayContext instance. self . options [ \"dcontext\" ] = self . dcontext return self . options","title":"get_options()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.handle_list","text":"Handle a recursive list grammar rule, generically. Parameters: object_list \u2013 the current list of objects. new_object \u2013 the new object to be added. Returns: The new, updated list of objects. Source code in beancount/parser/grammar.py def handle_list ( self , filename , lineno , object_list , new_object ): \"\"\"Handle a recursive list grammar rule, generically. Args: object_list: the current list of objects. new_object: the new object to be added. Returns: The new, updated list of objects. \"\"\" if object_list is None : object_list = [] if new_object is not None : object_list . append ( new_object ) return object_list","title":"handle_list()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.include","text":"Process an include directive. Parameters: filename \u2013 current filename. lineno \u2013 current line number. include_name \u2013 A string, the name of the file to include. Source code in beancount/parser/grammar.py def include ( self , filename , lineno , include_filename ): \"\"\"Process an include directive. Args: filename: current filename. lineno: current line number. include_name: A string, the name of the file to include. \"\"\" self . options [ \"include\" ] . append ( include_filename )","title":"include()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.key_value","text":"Process a document directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account the document relates to. document_filename \u2013 A str, the name of the document file. Returns: A new KeyValue object. Source code in beancount/parser/grammar.py def key_value ( self , filename , lineno , key , value ): \"\"\"Process a document directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account the document relates to. document_filename: A str, the name of the document file. Returns: A new KeyValue object. \"\"\" return KeyValue ( key , value )","title":"key_value()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.note","text":"Process a note directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account to attach the note to. comment \u2013 A str, the note's comments contents. kvlist \u2013 a list of KeyValue instances. Returns: A new Note object. Source code in beancount/parser/grammar.py def note ( self , filename , lineno , date , account , comment , tags_links , kvlist ): \"\"\"Process a note directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to attach the note to. comment: A str, the note's comments contents. kvlist: a list of KeyValue instances. Returns: A new Note object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) tags , links = self . _finalize_tags_links ( tags_links . tags , tags_links . links ) return Note ( meta , date , account , comment , tags , links )","title":"note()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.open","text":"Process an open directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the name of the account. currencies \u2013 A list of constraint currencies. booking_str \u2013 A string, the booking method, or None if none was specified. kvlist \u2013 a list of KeyValue instances. Returns: A new Open object. Source code in beancount/parser/grammar.py def open ( self , filename , lineno , date , account , currencies , booking_str , kvlist ): \"\"\"Process an open directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the name of the account. currencies: A list of constraint currencies. booking_str: A string, the booking method, or None if none was specified. kvlist: a list of KeyValue instances. Returns: A new Open object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) error = False if booking_str : try : # Note: Somehow the 'in' membership operator is not defined on Enum. booking = Booking [ booking_str ] except KeyError : # If the per-account method is invalid, set it to the global # default method and continue. booking = self . options [ \"booking_method\" ] error = True else : booking = None entry = Open ( meta , date , account , currencies , booking ) if error : self . errors . append ( ParserError ( meta , \"Invalid booking method: {} \" . format ( booking_str ), entry ) ) return entry","title":"open()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.option","text":"Process an option directive. Parameters: filename \u2013 current filename. lineno \u2013 current line number. key \u2013 option's key (str) value \u2013 option's value Source code in beancount/parser/grammar.py def option ( self , filename , lineno , key , value ): \"\"\"Process an option directive. Args: filename: current filename. lineno: current line number. key: option's key (str) value: option's value \"\"\" if key not in self . options : meta = new_metadata ( filename , lineno ) self . errors . append ( ParserError ( meta , \"Invalid option: ' {} '\" . format ( key ), None )) elif key in options . READ_ONLY_OPTIONS : meta = new_metadata ( filename , lineno ) self . errors . append ( ParserError ( meta , \"Option ' {} ' may not be set\" . format ( key ), None ) ) else : option_descriptor = options . OPTIONS [ key ] # Issue a warning if the option is deprecated. if option_descriptor . deprecated : assert isinstance ( option_descriptor . deprecated , str ), \"Internal error.\" meta = new_metadata ( filename , lineno ) self . errors . append ( DeprecatedError ( meta , option_descriptor . deprecated , None ) ) # Rename the option if it has an alias. if option_descriptor . alias : key = option_descriptor . alias option_descriptor = options . OPTIONS [ key ] # Convert the value, if necessary. if option_descriptor . converter : try : value = option_descriptor . converter ( value ) except ValueError as exc : meta = new_metadata ( filename , lineno ) self . errors . append ( ParserError ( meta , \"Error for option ' {} ': {} \" . format ( key , exc ), None ) ) return option = self . options [ key ] if isinstance ( option , list ): # Append to a list of values. option . append ( value ) elif isinstance ( option , dict ): # Set to a dict of values. if not ( isinstance ( value , tuple ) and len ( value ) == 2 ): self . errors . append ( ParserError ( meta , \"Error for option ' {} ': {} \" . format ( key , value ), None ) ) return dict_key , dict_value = value option [ dict_key ] = dict_value elif isinstance ( option , bool ): # Convert to a boolean. if not isinstance ( value , bool ): value = ( value . lower () in { \"true\" , \"on\" }) or ( value == \"1\" ) self . options [ key ] = value else : # Set the value. self . options [ key ] = value # Refresh the list of valid account regexps as we go along. if key . startswith ( \"name_\" ): # Update the set of valid account types. self . account_regexp = valid_account_regexp ( self . options )","title":"option()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.pad","text":"Process a pad directive. Parameters: filename \u2013 The current filename. lineno \u2013 The current line number. date \u2013 A datetime object. account \u2013 A string, the account to be padded. source_account \u2013 A string, the account to pad from. kvlist \u2013 a list of KeyValue instances. Returns: A new Pad object. Source code in beancount/parser/grammar.py def pad ( self , filename , lineno , date , account , source_account , kvlist ): \"\"\"Process a pad directive. Args: filename: The current filename. lineno: The current line number. date: A datetime object. account: A string, the account to be padded. source_account: A string, the account to pad from. kvlist: a list of KeyValue instances. Returns: A new Pad object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Pad ( meta , date , account , source_account )","title":"pad()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.pipe_deprecated_error","text":"Issue a 'Pipe deprecated' error. Parameters: filename \u2013 The current filename lineno \u2013 The current line number Source code in beancount/parser/grammar.py def pipe_deprecated_error ( self , filename , lineno ): \"\"\"Issue a 'Pipe deprecated' error. Args: filename: The current filename lineno: The current line number \"\"\" if self . options [ \"allow_pipe_separator\" ]: return meta = new_metadata ( filename , lineno ) self . errors . append ( ParserSyntaxError ( meta , \"Pipe symbol is deprecated.\" , None ))","title":"pipe_deprecated_error()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.plugin","text":"Process a plugin directive. Parameters: filename \u2013 current filename. lineno \u2013 current line number. plugin_name \u2013 A string, the name of the plugin module to import. plugin_config \u2013 A string or None, an optional configuration string to pass in to the plugin module. Source code in beancount/parser/grammar.py def plugin ( self , filename , lineno , plugin_name , plugin_config ): \"\"\"Process a plugin directive. Args: filename: current filename. lineno: current line number. plugin_name: A string, the name of the plugin module to import. plugin_config: A string or None, an optional configuration string to pass in to the plugin module. \"\"\" self . options [ \"plugin\" ] . append (( plugin_name , plugin_config ))","title":"plugin()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.popmeta","text":"Removed a key off the current set of stacks. Parameters: key \u2013 A string, a key to be removed from the meta dict. Source code in beancount/parser/grammar.py def popmeta ( self , filename , lineno , key ): \"\"\"Removed a key off the current set of stacks. Args: key: A string, a key to be removed from the meta dict. \"\"\" try : if key not in self . meta : raise IndexError value_list = self . meta [ key ] value_list . pop ( - 1 ) if not value_list : self . meta . pop ( key ) except IndexError : meta = new_metadata ( filename , lineno ) self . errors . append ( ParserError ( meta , \"Attempting to pop absent metadata key: ' {} '\" . format ( key ), None ) )","title":"popmeta()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.poptag","text":"Pop a tag off the current set of stacks. Parameters: tag \u2013 A string, a tag to be removed from the current set of tags. Source code in beancount/parser/grammar.py def poptag ( self , filename , lineno , tag ): \"\"\"Pop a tag off the current set of stacks. Args: tag: A string, a tag to be removed from the current set of tags. \"\"\" try : self . tags . remove ( tag ) except ValueError : meta = new_metadata ( filename , lineno ) self . errors . append ( ParserError ( meta , \"Attempting to pop absent tag: ' {} '\" . format ( tag ), None ) )","title":"poptag()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.posting","text":"Process a posting grammar rule. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. account \u2013 A string, the account of the posting. units \u2013 An instance of Amount for the units. cost \u2013 An instance of CostSpec for the cost. price \u2013 Either None, or an instance of Amount that is the cost of the position. istotal \u2013 A bool, True if the price is for the total amount being parsed, or False if the price is for each lot of the position. flag \u2013 A string, one-character, the flag associated with this posting. Returns: A new Posting object, with no parent entry. Source code in beancount/parser/grammar.py def posting ( self , filename , lineno , account , units , cost , price , istotal , flag ): \"\"\"Process a posting grammar rule. Args: filename: the current filename. lineno: the current line number. account: A string, the account of the posting. units: An instance of Amount for the units. cost: An instance of CostSpec for the cost. price: Either None, or an instance of Amount that is the cost of the position. istotal: A bool, True if the price is for the total amount being parsed, or False if the price is for each lot of the position. flag: A string, one-character, the flag associated with this posting. Returns: A new Posting object, with no parent entry. \"\"\" meta = new_metadata ( filename , lineno ) # Prices may not be negative. if price and isinstance ( price . number , Decimal ) and price . number < ZERO : self . errors . append ( ParserError ( meta , ( \"Negative prices are not allowed: {} \" \"(see http://furius.ca/beancount/doc/bug-negative-prices \" \"for workaround)\" ) . format ( price ), None , ) ) # Fix it and continue. price = Amount ( abs ( price . number ), price . currency ) # If the price is specified for the entire amount, compute the effective # price here and forget about that detail of the input syntax. if istotal : if units . number is MISSING : # Note: we could potentially do a better job and attempt to fix # this up after interpolation, but this syntax is pretty rare # anyway. self . errors . append ( ParserError ( meta , ( \"Total price on a posting without units: {} .\" ) . format ( price ), None , ) ) price = None else : price_number = price . number if price_number is not MISSING : price_number = ( ZERO if units . number == ZERO else price_number / abs ( units . number ) ) price = Amount ( price_number , price . currency ) # Note: Allow zero prices because we need them for round-trips for # conversion entries. # # if price is not None and price.number == ZERO: # self.errors.append( # ParserError(meta, \"Price is zero: {}\".format(price), None)) # If both cost and price are specified, the currencies must match, or # that is an error. if ( cost is not None and price is not None and isinstance ( cost . currency , str ) and isinstance ( price . currency , str ) and cost . currency != price . currency ): self . errors . append ( ParserError ( meta , \"Cost and price currencies must match: {} != {} \" . format ( cost . currency , price . currency ), None , ) ) return Posting ( account , units , cost , price , chr ( flag ) if flag else None , meta )","title":"posting()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.price","text":"Process a price directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. currency \u2013 the currency to be priced. amount \u2013 an instance of Amount, that is the price of the currency. kvlist \u2013 a list of KeyValue instances. Returns: A new Price object. Source code in beancount/parser/grammar.py def price ( self , filename , lineno , date , currency , amount , kvlist ): \"\"\"Process a price directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. currency: the currency to be priced. amount: an instance of Amount, that is the price of the currency. kvlist: a list of KeyValue instances. Returns: A new Price object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Price ( meta , date , currency , amount )","title":"price()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.pushmeta","text":"Set a metadata field on the current key-value pairs to be added to transactions. Parameters: key_value \u2013 A KeyValue instance, to be added to the dict of metadata. Source code in beancount/parser/grammar.py def pushmeta ( self , filename , lineno , key_value ): \"\"\"Set a metadata field on the current key-value pairs to be added to transactions. Args: key_value: A KeyValue instance, to be added to the dict of metadata. \"\"\" key , value = key_value self . meta [ key ] . append ( value )","title":"pushmeta()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.pushtag","text":"Push a tag on the current set of tags. Note that this does not need to be stack ordered. Parameters: tag \u2013 A string, a tag to be added. Source code in beancount/parser/grammar.py def pushtag ( self , filename , lineno , tag ): \"\"\"Push a tag on the current set of tags. Note that this does not need to be stack ordered. Args: tag: A string, a tag to be added. \"\"\" self . tags . append ( tag )","title":"pushtag()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.query","text":"Process a document directive. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. query_name \u2013 a str, the name of the query. query_string \u2013 a str, the SQL query itself. kvlist \u2013 a list of KeyValue instances. Returns: A new Query object. Source code in beancount/parser/grammar.py def query ( self , filename , lineno , date , query_name , query_string , kvlist ): \"\"\"Process a document directive. Args: filename: the current filename. lineno: the current line number. date: a datetime object. query_name: a str, the name of the query. query_string: a str, the SQL query itself. kvlist: a list of KeyValue instances. Returns: A new Query object. \"\"\" meta = new_metadata ( filename , lineno , kvlist ) return Query ( meta , date , query_name , query_string )","title":"query()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.store_result","text":"Start rule stores the final result here. Parameters: entries \u2013 A list of entries to store. Source code in beancount/parser/grammar.py def store_result ( self , filename , lineno , entries ): \"\"\"Start rule stores the final result here. Args: entries: A list of entries to store. \"\"\" if entries : self . entries = entries # Also record the name of the processed file. self . options [ \"filename\" ] = filename","title":"store_result()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.tag_link_LINK","text":"Add a link to the TagsLinks accumulator. Parameters: tags_links \u2013 The current TagsLinks accumulator. link \u2013 A string, the new link to insert. Returns: An updated TagsLinks instance. Source code in beancount/parser/grammar.py def tag_link_LINK ( self , filename , lineno , tags_links , link ): \"\"\"Add a link to the TagsLinks accumulator. Args: tags_links: The current TagsLinks accumulator. link: A string, the new link to insert. Returns: An updated TagsLinks instance. \"\"\" tags_links . links . add ( link ) return tags_links","title":"tag_link_LINK()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.tag_link_TAG","text":"Add a tag to the TagsLinks accumulator. Parameters: tags_links \u2013 The current TagsLinks accumulator. tag \u2013 A string, the new tag to insert. Returns: An updated TagsLinks instance. Source code in beancount/parser/grammar.py def tag_link_TAG ( self , filename , lineno , tags_links , tag ): \"\"\"Add a tag to the TagsLinks accumulator. Args: tags_links: The current TagsLinks accumulator. tag: A string, the new tag to insert. Returns: An updated TagsLinks instance. \"\"\" tags_links . tags . add ( tag ) return tags_links","title":"tag_link_TAG()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.tag_link_new","text":"Create a new TagsLinks instance. Returns: An instance of TagsLinks, initialized with expected attributes. Source code in beancount/parser/grammar.py def tag_link_new ( self , filename , lineno ): \"\"\"Create a new TagsLinks instance. Returns: An instance of TagsLinks, initialized with expected attributes. \"\"\" return TagsLinks ( set (), set ())","title":"tag_link_new()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.Builder.transaction","text":"Process a transaction directive. All the postings of the transaction are available at this point, and so the the transaction is balanced here, incomplete postings are completed with the appropriate position, and errors are being accumulated on the builder to be reported later on. This is the main routine that takes up most of the parsing time; be very careful with modifications here, they have an impact on performance. Parameters: filename \u2013 the current filename. lineno \u2013 the current line number. date \u2013 a datetime object. flag \u2013 a str, one-character, the flag associated with this transaction. txn_strings \u2013 A list of strings, possibly empty, possibly longer. tags_links \u2013 A TagsLinks namedtuple of tags, and/or links. posting_or_kv_list \u2013 a list of Posting or KeyValue instances, to be inserted in this transaction, or None, if no postings have been declared. Returns: A new Transaction object. Source code in beancount/parser/grammar.py def transaction ( self , filename , lineno , date , flag , txn_strings , tags_links , posting_or_kv_list ): \"\"\"Process a transaction directive. All the postings of the transaction are available at this point, and so the the transaction is balanced here, incomplete postings are completed with the appropriate position, and errors are being accumulated on the builder to be reported later on. This is the main routine that takes up most of the parsing time; be very careful with modifications here, they have an impact on performance. Args: filename: the current filename. lineno: the current line number. date: a datetime object. flag: a str, one-character, the flag associated with this transaction. txn_strings: A list of strings, possibly empty, possibly longer. tags_links: A TagsLinks namedtuple of tags, and/or links. posting_or_kv_list: a list of Posting or KeyValue instances, to be inserted in this transaction, or None, if no postings have been declared. Returns: A new Transaction object. \"\"\" meta = new_metadata ( filename , lineno ) # Separate postings and key-values. explicit_meta = {} postings = [] tags , links = tags_links . tags , tags_links . links if posting_or_kv_list : last_posting = None for posting_or_kv in posting_or_kv_list : if isinstance ( posting_or_kv , Posting ): postings . append ( posting_or_kv ) last_posting = posting_or_kv elif isinstance ( posting_or_kv , TagsLinks ): if postings : self . errors . append ( ParserError ( meta , \"Tags or links not allowed after first \" + \"Posting: {} \" . format ( posting_or_kv ), None , ) ) else : tags . update ( posting_or_kv . tags ) links . update ( posting_or_kv . links ) else : if last_posting is None : value = explicit_meta . setdefault ( posting_or_kv . key , posting_or_kv . value ) if value is not posting_or_kv . value : self . errors . append ( ParserError ( meta , \"Duplicate metadata field on entry: {} \" . format ( posting_or_kv ), None , ) ) else : if last_posting . meta is None : last_posting = last_posting . _replace ( meta = {}) postings . pop ( - 1 ) postings . append ( last_posting ) value = last_posting . meta . setdefault ( posting_or_kv . key , posting_or_kv . value ) if value is not posting_or_kv . value : self . errors . append ( ParserError ( meta , \"Duplicate posting metadata field: {} \" . format ( posting_or_kv ), None , ) ) # Freeze the tags & links or set to default empty values. tags , links = self . _finalize_tags_links ( tags , links ) # Initialize the metadata fields from the set of active values. if self . meta : for key , value_list in self . meta . items (): meta [ key ] = value_list [ - 1 ] # Add on explicitly defined values. if explicit_meta : meta . update ( explicit_meta ) # Unpack the transaction fields. payee_narration = self . _unpack_txn_strings ( txn_strings , meta ) if payee_narration is None : return None payee , narration = payee_narration # We now allow a single posting when its balance is zero, so we # commented out the check below. If a transaction has a single posting # with a non-zero balance, it'll get caught below in the booking code. # # # Detect when a transaction does not have at least two legs. # if postings is None or len(postings) < 2: # self.errors.append( # ParserError(meta, # \"Transaction with only one posting: {}\".format(postings), # None)) # return None # If there are no postings, make sure we insert a list object. if postings is None : postings = [] # Create the transaction. return Transaction ( meta , date , chr ( flag ), payee , narration , tags , links , postings )","title":"transaction()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.CompoundAmount","text":"CompoundAmount(number_per, number_total, currency)","title":"CompoundAmount"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.CompoundAmount.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.CompoundAmount.__new__","text":"Create new instance of CompoundAmount(number_per, number_total, currency)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.CompoundAmount.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.DeprecatedError","text":"DeprecatedError(source, message, entry)","title":"DeprecatedError"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.DeprecatedError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.DeprecatedError.__new__","text":"Create new instance of DeprecatedError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.DeprecatedError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.KeyValue","text":"KeyValue(key, value)","title":"KeyValue"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.KeyValue.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.KeyValue.__new__","text":"Create new instance of KeyValue(key, value)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.KeyValue.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserError","text":"ParserError(source, message, entry)","title":"ParserError"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserError.__new__","text":"Create new instance of ParserError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserSyntaxError","text":"ParserSyntaxError(source, message, entry)","title":"ParserSyntaxError"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserSyntaxError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserSyntaxError.__new__","text":"Create new instance of ParserSyntaxError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ParserSyntaxError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.TagsLinks","text":"TagsLinks(tags, links)","title":"TagsLinks"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.TagsLinks.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.TagsLinks.__new__","text":"Create new instance of TagsLinks(tags, links)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.TagsLinks.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ValueType","text":"ValueType(value, dtype)","title":"ValueType"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ValueType.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/grammar.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ValueType.__new__","text":"Create new instance of ValueType(value, dtype)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.ValueType.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/grammar.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.grammar.valid_account_regexp","text":"Build a regexp to validate account names from the options. Parameters: options \u2013 A dict of options, as per beancount.parser.options. Returns: A string, a regular expression that will match all account names. Source code in beancount/parser/grammar.py def valid_account_regexp ( options ): \"\"\"Build a regexp to validate account names from the options. Args: options: A dict of options, as per beancount.parser.options. Returns: A string, a regular expression that will match all account names. \"\"\" names = map ( options . __getitem__ , ( \"name_assets\" , \"name_liabilities\" , \"name_equity\" , \"name_income\" , \"name_expenses\" ), ) # Replace the first term of the account regular expression with the specific # names allowed under the options configuration. This code is kept in sync # with {5672c7270e1e}. return regex . compile ( \"(?: {} )(?: {}{} )+\" . format ( \"|\" . join ( names ), account . sep , account . ACC_COMP_NAME_RE ) )","title":"valid_account_regexp()"},{"location":"api_reference/beancount.parser.html#beancount.parser.hashsrc","text":"Compute a hash of the source files in order to warn when the source goes out of date.","title":"hashsrc"},{"location":"api_reference/beancount.parser.html#beancount.parser.hashsrc.check_parser_source_files","text":"Check the extension module's source hash and issue a warning if the current source differs from that of the module. If the source files aren't located in the Python source directory, ignore the warning, we're probably running this from an installed based, in which case we don't need to check anything (this check is useful only for people running directly from source). Source code in beancount/parser/hashsrc.py def check_parser_source_files ( parser_module : types . ModuleType ): \"\"\"Check the extension module's source hash and issue a warning if the current source differs from that of the module. If the source files aren't located in the Python source directory, ignore the warning, we're probably running this from an installed based, in which case we don't need to check anything (this check is useful only for people running directly from source). \"\"\" parser_source_hash = hash_parser_source_files () if parser_source_hash is None : return if parser_module . SOURCE_HASH and parser_module . SOURCE_HASH != parser_source_hash : warnings . warn ( ( \"The Beancount parser C extension module is out-of-date (' {} ' != ' {} '). \" \"You need to rebuild.\" ) . format ( parser_module . SOURCE_HASH , parser_source_hash ) )","title":"check_parser_source_files()"},{"location":"api_reference/beancount.parser.html#beancount.parser.hashsrc.gen_include","text":"Generate an include file for the parser source hash. Source code in beancount/parser/hashsrc.py def gen_include (): \"\"\"Generate an include file for the parser source hash.\"\"\" return textwrap . dedent ( \"\"\"\\ #ifndef __BEANCOUNT_PARSER_PARSE_SOURCE_HASH_H__ #define __BEANCOUNT_PARSER_PARSE_SOURCE_HASH_H__ #define PARSER_SOURCE_HASH {source_hash} #endif // __BEANCOUNT_PARSER_PARSE_SOURCE_HASH_H__ \"\"\" . format ( source_hash = hash_parser_source_files ()) )","title":"gen_include()"},{"location":"api_reference/beancount.parser.html#beancount.parser.hashsrc.hash_parser_source_files","text":"Compute a unique hash of the parser's Python code in order to bake that into the extension module. This is used at load-time to verify that the extension module and the corresponding Python codes match each other. If not, it issues a warning that you should rebuild your extension module. Returns: A string, the hexadecimal unique hash of relevant source code that should trigger a recompilation. Source code in beancount/parser/hashsrc.py def hash_parser_source_files (): \"\"\"Compute a unique hash of the parser's Python code in order to bake that into the extension module. This is used at load-time to verify that the extension module and the corresponding Python codes match each other. If not, it issues a warning that you should rebuild your extension module. Returns: A string, the hexadecimal unique hash of relevant source code that should trigger a recompilation. \"\"\" md5 = hashlib . md5 () for filename in PARSER_SOURCE_FILES : fullname = path . join ( path . dirname ( __file__ ), filename ) if not path . exists ( fullname ): return None with open ( fullname , \"rb\" ) as file : md5 . update ( file . read ()) # Note: Prepend a character in front of the hash because under Windows MSDEV # removes escapes, and if the hash starts with a number it fails to # recognize this is a string. A small compromise for portability. return md5 . hexdigest ()","title":"hash_parser_source_files()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer","text":"Beancount syntax lexer.","title":"lexer"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder","text":"A builder used only for building lexer objects.","title":"LexBuilder"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexBuilder.build_lexer_error","text":"Build a lexer error and appends it to the list of pending errors. Parameters: message \u2013 The message of the error. Source code in beancount/parser/lexer.py def build_lexer_error ( self , filename , lineno , message ): # {0e31aeca3363} \"\"\"Build a lexer error and appends it to the list of pending errors. Args: message: The message of the error. \"\"\" self . errors . append ( LexerError ( new_metadata ( filename , lineno ), str ( message ), None ))","title":"build_lexer_error()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexerError","text":"LexerError(source, message, entry)","title":"LexerError"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexerError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/lexer.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexerError.__new__","text":"Create new instance of LexerError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.LexerError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/lexer.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.lex_iter","text":"An iterator that yields all the tokens in the given file. Parameters: file \u2013 A string, the filename to run the lexer on, or a file object. builder \u2013 A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors). Yields: All the tokens in the input file as (token, lineno, text, value) tuples where token is a string representing the token kind, lineno is the line number in the input file where the token was matched, mathed is a bytes object containing the exact text matched, and value is the semantic value of the token or None. Source code in beancount/parser/lexer.py def lex_iter ( file , builder = None ): \"\"\"An iterator that yields all the tokens in the given file. Args: file: A string, the filename to run the lexer on, or a file object. builder: A builder of your choice. If not specified, a LexBuilder is used and discarded (along with its errors). Yields: All the tokens in the input file as ``(token, lineno, text, value)`` tuples where ``token`` is a string representing the token kind, ``lineno`` is the line number in the input file where the token was matched, ``mathed`` is a bytes object containing the exact text matched, and ``value`` is the semantic value of the token or None. \"\"\" with contextlib . ExitStack () as ctx : # It would be more appropriate here to check for io.RawIOBase but # that does not work for io.BytesIO despite it implementing the # readinto() method. if not isinstance ( file , io . IOBase ): file = ctx . enter_context ( open ( file , \"rb\" )) if builder is None : builder = LexBuilder () parser = _parser . Parser ( builder ) yield from parser . lex ( file )","title":"lex_iter()"},{"location":"api_reference/beancount.parser.html#beancount.parser.lexer.lex_iter_string","text":"An iterator that yields all the tokens in the given string. Parameters: string \u2013 a str or bytes, the contents of the ledger to be parsed. Returns: An iterator, see lex_iter() for details. Source code in beancount/parser/lexer.py def lex_iter_string ( string , builder = None , ** kwargs ): \"\"\"An iterator that yields all the tokens in the given string. Args: string: a str or bytes, the contents of the ledger to be parsed. Returns: An iterator, see ``lex_iter()`` for details. \"\"\" if not isinstance ( string , bytes ): string = string . encode ( \"utf8\" ) file = io . BytesIO ( string ) yield from lex_iter ( file , builder = builder , ** kwargs )","title":"lex_iter_string()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options","text":"Declaration of options and their default values.","title":"options"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptDesc","text":"OptDesc(name, default_value, example_value, converter, deprecated, alias)","title":"OptDesc"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptDesc.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/options.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptDesc.__new__","text":"Create new instance of OptDesc(name, default_value, example_value, converter, deprecated, alias)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptDesc.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/options.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptGroup","text":"OptGroup(description, options)","title":"OptGroup"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptGroup.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/parser/options.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptGroup.__new__","text":"Create new instance of OptGroup(description, options)","title":"__new__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.OptGroup.__repr__","text":"Return a nicely formatted representation string Source code in beancount/parser/options.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.Opt","text":"Alternative constructor for OptDesc, with default values. Parameters: name \u2013 See OptDesc. default_value \u2013 See OptDesc. example_value \u2013 See OptDesc. converter \u2013 See OptDesc. deprecated \u2013 See OptDesc. alias \u2013 See OptDesc. Returns: An instance of OptDesc. Source code in beancount/parser/options.py def Opt ( name , default_value , example_value = UNSET , converter = None , deprecated = False , alias = None ): \"\"\"Alternative constructor for OptDesc, with default values. Args: name: See OptDesc. default_value: See OptDesc. example_value: See OptDesc. converter: See OptDesc. deprecated: See OptDesc. alias: See OptDesc. Returns: An instance of OptDesc. \"\"\" if example_value is UNSET : example_value = default_value return OptDesc ( name , default_value , example_value , converter , deprecated , alias )","title":"Opt()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.get_account_types","text":"Extract the account type names from the parser's options. Parameters: options \u2013 a dict of ledger options. Returns: An instance of AccountTypes, that contains all the prefixes. Source code in beancount/parser/options.py def get_account_types ( options ): \"\"\"Extract the account type names from the parser's options. Args: options: a dict of ledger options. Returns: An instance of AccountTypes, that contains all the prefixes. \"\"\" return account_types . AccountTypes ( * [ options [ key ] for key in ( \"name_assets\" , \"name_liabilities\" , \"name_equity\" , \"name_income\" , \"name_expenses\" , ) ] )","title":"get_account_types()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.get_current_accounts","text":"Return account names for the current earnings and conversion accounts. Parameters: options \u2013 a dict of ledger options. Returns: A tuple of 2 account objects, one for booking current earnings, and one for current conversions. Source code in beancount/parser/options.py def get_current_accounts ( options ): \"\"\"Return account names for the current earnings and conversion accounts. Args: options: a dict of ledger options. Returns: A tuple of 2 account objects, one for booking current earnings, and one for current conversions. \"\"\" equity = options [ \"name_equity\" ] account_current_earnings = account . join ( equity , options [ \"account_current_earnings\" ]) account_current_conversions = account . join ( equity , options [ \"account_current_conversions\" ] ) return ( account_current_earnings , account_current_conversions )","title":"get_current_accounts()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.get_previous_accounts","text":"Return account names for the previous earnings, balances and conversion accounts. Parameters: options \u2013 a dict of ledger options. Returns: A tuple of 3 account objects, for booking previous earnings, previous balances, and previous conversions. Source code in beancount/parser/options.py def get_previous_accounts ( options ): \"\"\"Return account names for the previous earnings, balances and conversion accounts. Args: options: a dict of ledger options. Returns: A tuple of 3 account objects, for booking previous earnings, previous balances, and previous conversions. \"\"\" equity = options [ \"name_equity\" ] account_previous_earnings = account . join ( equity , options [ \"account_previous_earnings\" ]) account_previous_balances = account . join ( equity , options [ \"account_previous_balances\" ]) account_previous_conversions = account . join ( equity , options [ \"account_previous_conversions\" ] ) return ( account_previous_earnings , account_previous_balances , account_previous_conversions , )","title":"get_previous_accounts()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.get_unrealized_account","text":"Return the full account name for the unrealized account. Parameters: options \u2013 a dict of ledger options. Returns: A tuple of 2 account objects, one for booking current earnings, and one for current conversions. Source code in beancount/parser/options.py def get_unrealized_account ( options ): \"\"\"Return the full account name for the unrealized account. Args: options: a dict of ledger options. Returns: A tuple of 2 account objects, one for booking current earnings, and one for current conversions. \"\"\" income = options [ \"name_income\" ] return account . join ( income , options [ \"account_unrealized_gains\" ])","title":"get_unrealized_account()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.list_options","text":"Produce a formatted text of the available options and their description. Returns: A string, formatted nicely to be printed in 80 columns. Source code in beancount/parser/options.py def list_options (): \"\"\"Produce a formatted text of the available options and their description. Returns: A string, formatted nicely to be printed in 80 columns. \"\"\" oss = io . StringIO () for group in PUBLIC_OPTION_GROUPS : for desc in group . options : oss . write ( 'option \" {} \" \" {} \" \\n ' . format ( desc . name , desc . example_value )) if desc . deprecated : oss . write ( textwrap . fill ( \"THIS OPTION IS DEPRECATED: {} \" . format ( desc . deprecated ), initial_indent = \" \" , subsequent_indent = \" \" , ) ) oss . write ( \" \\n\\n \" ) description = \" \" . join ( line . strip () for line in group . description . strip () . splitlines () ) oss . write ( textwrap . fill ( description , initial_indent = \" \" , subsequent_indent = \" \" )) oss . write ( \" \\n \" ) if isinstance ( desc . default_value , ( list , dict , set )): oss . write ( \" \\n \" ) oss . write ( \" (This option may be supplied multiple times.) \\n \" ) oss . write ( \" \\n\\n \" ) return oss . getvalue ()","title":"list_options()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.options_validate_booking_method","text":"Validate a booking method name. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_booking_method ( value ): \"\"\"Validate a booking method name. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" try : return data . Booking [ value ] except KeyError as exc : raise ValueError ( str ( exc )) from exc","title":"options_validate_booking_method()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.options_validate_boolean","text":"Validate a boolean option. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_boolean ( value ): \"\"\"Validate a boolean option. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" return value . lower () in ( \"1\" , \"true\" , \"yes\" )","title":"options_validate_boolean()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.options_validate_plugin","text":"Validate the plugin option. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_plugin ( value ): \"\"\"Validate the plugin option. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" # Process the 'plugin' option specially: accept an optional # argument from it. NOTE: We will eventually phase this out and # replace it by a dedicated 'plugin' directive. match = re . match ( \"(.*):(.*)\" , value ) if match : plugin_name , plugin_config = match . groups () else : plugin_name , plugin_config = value , None return ( plugin_name , plugin_config )","title":"options_validate_plugin()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.options_validate_processing_mode","text":"Validate the options processing mode. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_processing_mode ( value ): \"\"\"Validate the options processing mode. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" if value not in ( \"raw\" , \"default\" ): raise ValueError ( \"Invalid value ' {} '\" . format ( value )) return value","title":"options_validate_processing_mode()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.options_validate_tolerance","text":"Validate the tolerance option. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_tolerance ( value ): \"\"\"Validate the tolerance option. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" return D ( value )","title":"options_validate_tolerance()"},{"location":"api_reference/beancount.parser.html#beancount.parser.options.options_validate_tolerance_map","text":"Validate an option with a map of currency/tolerance pairs in a string. Parameters: value \u2013 A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Exceptions: ValueError \u2013 If the value is invalid. Source code in beancount/parser/options.py def options_validate_tolerance_map ( value ): \"\"\"Validate an option with a map of currency/tolerance pairs in a string. Args: value: A string, the value provided as option. Returns: The new value, converted, if the conversion is successful. Raises: ValueError: If the value is invalid. \"\"\" # Process the setting of a key-value, whereby the value is a Decimal # representation. match = re . match ( \"(.*):(.*)\" , value ) if not match : raise ValueError ( \"Invalid value ' {} '\" . format ( value )) currency , tolerance_str = match . groups () return ( currency , D ( tolerance_str ))","title":"options_validate_tolerance_map()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser","text":"Beancount syntax parser. IMPORTANT: The parser (and its grammar builder) produces \"incomplete\" Transaction objects. This means that some of the data can be found missing from the output of the parser and some of the data types vary slightly. Missing components are replaced not by None, but by a special constant 'NA' which helps diagnose problems if a user inadvertently attempts to work on an incomplete posting instead of a complete one. Those incomplete entries are then run through the \"booking\" routines which do two things simultaneously: They find matching lots for reducing inventory positions, and They interpolate missing numbers. In doing so they normalize the entries to \"complete\" entries by converting a position/lot's \"cost\" attribute from a CostSpec to a Cost. A Cost is similar to an Amount in that it shares \"number\" and \"currency\" attributes, but also has a label and a lot date. A CostSpec is similar to a Cost, but has all optional data; it consists in a specification for matching against a particular inventory lot. Other parts of a posting may also be missing, not just parts of the cost. Leaving out some parts of the input is used to invoke interpolation, to tell Beancount to automatically compute the missing numbers (if possible). Missing components will be set to the special value \"beancount.core.number.MISSING\" until inventory booking and number interpolation has been completed. The \"MISSING\" value should never appear in completed, loaded transaction postings. For instance, all of the units may be missing: INPUT: Assets:Account posting.units = MISSING Or just the number of the units: INPUT: Assets:Account USD posting.units = Amount(MISSING, \"USD\") You must always specify the currency. If a price annotation is simply absent, it appears as None: INPUT: Assets:Account 2 MXN posting.price = None However, you may indicate that there is a price but have Beancount compute it automatically: INPUT: Assets:Account 2 MXN @ posting.price = Amount(MISSING, MISSING) Indicating the conversion currency is also possible (and recommended): INPUT: Assets:Account 2 MXN @ USD posting.price = Amount(MISSING, \"USD\") If a cost specification is provided, a \"cost\" attribute it set but it does not refer to a Cost instance (as in complete entries) but rather to a CostSpec instance. Some of the fields of a CostSpec may be MISSING if they were not specified in the input. For example: INPUT: Assets:Account 1 HOOL {100 # 5 USD} posting.cost = CostSpec(Decimal(\"100\"), Decimal(\"5\"), \"USD\", None, None, False)) Note how we never consider the label of date override to be MISSING; this is because those inputs are optional: A missing label is simply left unset in the computed Cost, and a missing date override uses the date of the transaction that contains the posting. You can indicate that there is a total number to be filled in like this: INPUT: Assets:Account 1 HOOL {100 # USD} posting.cost = CostSpec(Decimal(\"100\"), MISSING, \"USD\", None, None, False)) This is in contrast to the total value simple not being used: INPUT: Assets:Account 1 HOOL {100 USD} posting.cost = CostSpec(Decimal(\"100\"), None, \"USD\", None, None, False)) Both per-unit and total numbers may be omitted as well, in which case, only the number-per-unit portion of the CostSpec will appear as MISSING: INPUT: Assets:Account 1 HOOL {USD} posting.cost = CostSpec(MISSING, None, \"USD\", None, None, False)) And furthermore, all the cost basis may be missing: INPUT: Assets:Account 1 HOOL {} posting.cost = CostSpec(MISSING, None, MISSING, None, None, False)) If you ask for the lots to be merged, you get this: INPUT: Assets:Account 1 HOOL {*} posting.cost = CostSpec(MISSING, None, MISSING, None, None, True)) The numbers have to be computed by Beancount, so we output this with MISSING values. Of course, you can provide only the non-basis information, like just the date or label: INPUT: Assets:Account 1 HOOL {2015-09-21} posting.cost = CostSpec(MISSING, None, MISSING, date(2015, 9, 21), None, True) See the test beancount.parser.grammar_test.TestIncompleteInputs for examples and corresponding expected values.","title":"parser"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.is_entry_incomplete","text":"Detect the presence of elided amounts in Transactions. Parameters: entries \u2013 A directive. Returns: A boolean, true if there are some missing portions of any postings found. Source code in beancount/parser/parser.py def is_entry_incomplete ( entry ): \"\"\"Detect the presence of elided amounts in Transactions. Args: entries: A directive. Returns: A boolean, true if there are some missing portions of any postings found. \"\"\" if isinstance ( entry , data . Transaction ): if any ( is_posting_incomplete ( posting ) for posting in entry . postings ): return True return False","title":"is_entry_incomplete()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.is_posting_incomplete","text":"Detect the presence of any elided amounts in a Posting. If any of the possible amounts are missing, this returns True. Parameters: entries \u2013 A directive. Returns: A boolean, true if there are some missing portions of any postings found. Source code in beancount/parser/parser.py def is_posting_incomplete ( posting ): \"\"\"Detect the presence of any elided amounts in a Posting. If any of the possible amounts are missing, this returns True. Args: entries: A directive. Returns: A boolean, true if there are some missing portions of any postings found. \"\"\" units = posting . units if units is MISSING or units . number is MISSING or units . currency is MISSING : return True price = posting . price if ( price is MISSING or price is not None and ( price . number is MISSING or price . currency is MISSING ) ): return True cost = posting . cost if cost is not None and ( cost . number_per is MISSING or cost . number_total is MISSING or cost . currency is MISSING ): return True return False","title":"is_posting_incomplete()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.parse_doc","text":"Factory of decorators that parse the function's docstring as an argument. Note that the decorators thus generated only run the parser on the tests, not the loader, so is no validation, balance checks, nor plugins applied to the parsed text. Parameters: expect_errors \u2013 A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. allow_incomplete \u2013 A boolean, if true, allow incomplete input. Otherwise barf if the input would require interpolation. The default value is set not to allow it because we want to minimize the features tests depend on. Returns: A decorator for test functions. Source code in beancount/parser/parser.py def parse_doc ( expect_errors = False , allow_incomplete = False ): \"\"\"Factory of decorators that parse the function's docstring as an argument. Note that the decorators thus generated only run the parser on the tests, not the loader, so is no validation, balance checks, nor plugins applied to the parsed text. Args: expect_errors: A boolean or None, with the following semantics, True: Expect errors and fail if there are none. False: Expect no errors and fail if there are some. None: Do nothing, no check. allow_incomplete: A boolean, if true, allow incomplete input. Otherwise barf if the input would require interpolation. The default value is set not to allow it because we want to minimize the features tests depend on. Returns: A decorator for test functions. \"\"\" def decorator ( fun ): \"\"\"A decorator that parses the function's docstring as an argument. Args: fun: the function object to be decorated. Returns: A decorated test function. \"\"\" filename = inspect . getfile ( fun ) lines , lineno = inspect . getsourcelines ( fun ) # Skip over decorator invocation and function definition. This # is imperfect as it assumes that each consumes exactly one # line, but this is by far the most common case, and this is # mainly used in test, thus it is good enough. lineno += 2 @functools . wraps ( fun ) def wrapper ( self ): assert fun . __doc__ is not None , \"You need to insert a docstring on {} \" . format ( fun . __name__ ) entries , errors , options_map = parse_string ( fun . __doc__ , report_filename = filename , report_firstline = lineno , dedent = True ) if not allow_incomplete and any ( is_entry_incomplete ( entry ) for entry in entries ): self . fail ( \"parse_doc() may not use interpolation.\" ) if expect_errors is not None : if expect_errors is False and errors : oss = io . StringIO () printer . print_errors ( errors , file = oss ) self . fail ( \"Unexpected errors found: \\n {} \" . format ( oss . getvalue ())) elif expect_errors is True and not errors : self . fail ( \"Expected errors, none found:\" ) return fun ( self , entries , errors , options_map ) return wrapper return decorator","title":"parse_doc()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.parse_file","text":"Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Parameters: file \u2013 file object or path to the file to be parsed. kw \u2013 a dict of keywords to be applied to the C parser. Returns: A tuple of ( list of entries parsed in the file, list of errors that were encountered during parsing, and a dict of the option values that were parsed from the file.) Source code in beancount/parser/parser.py def parse_file ( file , report_filename = None , report_firstline = 1 , encoding = None , debug = False , ** kw ): \"\"\"Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Args: file: file object or path to the file to be parsed. kw: a dict of keywords to be applied to the C parser. Returns: A tuple of ( list of entries parsed in the file, list of errors that were encountered during parsing, and a dict of the option values that were parsed from the file.) \"\"\" if encoding is not None and codecs . lookup ( encoding ) . name != \"utf-8\" : raise ValueError ( \"Only UTF-8 encoded files are supported.\" ) with contextlib . ExitStack () as ctx : if file == \"-\" : file = sys . stdin . buffer # It would be more appropriate here to check for io.RawIOBase but # that does not work for io.BytesIO despite it implementing the # readinto() method. elif not isinstance ( file , io . IOBase ): file = ctx . enter_context ( open ( file , \"rb\" )) builder = grammar . Builder () parser = _parser . Parser ( builder , debug = debug ) parser . parse ( file , filename = report_filename , lineno = report_firstline , ** kw ) return builder . finalize ()","title":"parse_file()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.parse_many","text":"Parse a string with a snippet of Beancount input and replace vars from caller. Parameters: string \u2013 A string with some Beancount input. level \u2013 The number of extra stacks to ignore. Returns: A list of entries. Exceptions: AssertionError \u2013 If there are any errors. Source code in beancount/parser/parser.py def parse_many ( string , level = 0 ): \"\"\"Parse a string with a snippet of Beancount input and replace vars from caller. Args: string: A string with some Beancount input. level: The number of extra stacks to ignore. Returns: A list of entries. Raises: AssertionError: If there are any errors. \"\"\" # Get the locals in the stack for the callers and produce the final text. frame = inspect . stack ()[ level + 1 ] varkwds = frame [ 0 ] . f_locals input_string = textwrap . dedent ( string . format ( ** varkwds )) # Parse entries and check there are no errors. entries , errors , __ = parse_string ( input_string ) assert not errors return entries","title":"parse_many()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.parse_one","text":"Parse a string with single Beancount directive and replace vars from caller. Parameters: string \u2013 A string with some Beancount input. level \u2013 The number of extra stacks to ignore. Returns: A list of entries. Exceptions: AssertionError \u2013 If there are any errors. Source code in beancount/parser/parser.py def parse_one ( string ): \"\"\"Parse a string with single Beancount directive and replace vars from caller. Args: string: A string with some Beancount input. level: The number of extra stacks to ignore. Returns: A list of entries. Raises: AssertionError: If there are any errors. \"\"\" entries = parse_many ( string , level = 1 ) assert len ( entries ) == 1 return entries [ 0 ]","title":"parse_one()"},{"location":"api_reference/beancount.parser.html#beancount.parser.parser.parse_string","text":"Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Parameters: string \u2013 A string, the contents to be parsed instead of a file's. report_filename \u2013 A string, the source filename from which this string has been extracted, if any. This is stored in the metadata of the parsed entries. dedent \u2013 Whether to run textwrap.dedent() on the string before parsing. **kw \u2013 See parse.c. Returns: Same as the output of parse_file(). Source code in beancount/parser/parser.py def parse_string ( string , report_filename = None , dedent = False , ** kw ): \"\"\"Parse a beancount input file and return Ledger with the list of transactions and tree of accounts. Args: string: A string, the contents to be parsed instead of a file's. report_filename: A string, the source filename from which this string has been extracted, if any. This is stored in the metadata of the parsed entries. dedent: Whether to run textwrap.dedent() on the string before parsing. **kw: See parse.c. Return: Same as the output of parse_file(). \"\"\" if dedent : string = textwrap . dedent ( string ) if isinstance ( string , str ): string = string . encode ( \"utf8\" ) if report_filename is None : report_filename = \"\" file = io . BytesIO ( string ) return parse_file ( file , report_filename = report_filename , ** kw )","title":"parse_string()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer","text":"Conversion from internal data structures to text.","title":"printer"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.EntryPrinter","text":"A multi-method interface for printing all directive types. Attributes: Name Type Description dcontext An instance of DisplayContext with which to render all the numbers. render_weight A boolean, true if we should render the weight of the postings as a comment, for debugging. min_width_account An integer, the minimum width to leave for the account name. prefix User-specific prefix for custom indentation (for Fava). stringify_invalid_types If a metadata value is invalid, force a conversion to string for printout.","title":"EntryPrinter"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.EntryPrinter.__call__","text":"Render a directive. Parameters: obj \u2013 The directive to be rendered. Returns: A string, the rendered directive. Source code in beancount/parser/printer.py def __call__ ( self , obj ): \"\"\"Render a directive. Args: obj: The directive to be rendered. Returns: A string, the rendered directive. \"\"\" oss = io . StringIO () # We write optional entry source for every entry type, hence writing it here self . write_entry_source ( obj . meta , oss , prefix = \"\" ) method = getattr ( self , obj . __class__ . __name__ ) method ( obj , oss ) return oss . getvalue ()","title":"__call__()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.EntryPrinter.render_posting_strings","text":"This renders the three components of a posting: the account and its optional posting flag, the position, and finally, the weight of the position. The purpose is to align these in the caller. Parameters: posting \u2013 An instance of Posting, the posting to render. Returns: A tuple of flag_account \u2013 A string, the account name including the flag. position_str: A string, the rendered position string. weight_str: A string, the rendered weight of the posting. Source code in beancount/parser/printer.py def render_posting_strings ( self , posting ): \"\"\"This renders the three components of a posting: the account and its optional posting flag, the position, and finally, the weight of the position. The purpose is to align these in the caller. Args: posting: An instance of Posting, the posting to render. Returns: A tuple of flag_account: A string, the account name including the flag. position_str: A string, the rendered position string. weight_str: A string, the rendered weight of the posting. \"\"\" # Render a string of the flag and the account. flag = \" {} \" . format ( render_flag ( posting . flag )) if posting . flag else \"\" flag_account = flag + posting . account # Render a string with the amount and cost and optional price, if # present. Also render a string with the weight. weight_str = \"\" if isinstance ( posting . units , amount . Amount ): position_str = position . to_string ( posting , self . dformat ) # Note: we render weights at maximum precision, for debugging. if posting . cost is None or ( isinstance ( posting . cost , position . Cost ) and isinstance ( posting . cost . number , Decimal ) ): weight_str = str ( convert . get_weight ( posting )) else : position_str = \"\" if posting . price is not None : position_str += \" @ {} \" . format ( posting . price . to_string ( self . dformat_max )) return flag_account , position_str , weight_str","title":"render_posting_strings()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.EntryPrinter.write_entry_source","text":"Write source file and line number in a format interpretable as a message location for Emacs, VSCode or other editors. As this is for \"debugging\" purposes, this information will be commented out by a semicolon. Parameters: meta \u2013 A dict that contains the metadata for this directive. oss \u2013 A file object to write to. prefix \u2013 User-specific prefix for custom indentation Source code in beancount/parser/printer.py def write_entry_source ( self , meta , oss , prefix = None ): \"\"\"Write source file and line number in a format interpretable as a message location for Emacs, VSCode or other editors. As this is for \"debugging\" purposes, this information will be commented out by a semicolon. Args: meta: A dict that contains the metadata for this directive. oss: A file object to write to. prefix: User-specific prefix for custom indentation \"\"\" if not self . write_source : return if prefix is None : prefix = self . prefix oss . write ( \" {} ; source: {} \\n \" . format ( prefix , render_source ( meta )))","title":"write_entry_source()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.EntryPrinter.write_metadata","text":"Write metadata to the file object, excluding filename and line number. Parameters: meta \u2013 A dict that contains the metadata for this directive. oss \u2013 A file object to write to. Source code in beancount/parser/printer.py def write_metadata ( self , meta , oss , prefix = None ): \"\"\"Write metadata to the file object, excluding filename and line number. Args: meta: A dict that contains the metadata for this directive. oss: A file object to write to. \"\"\" if meta is None : return if prefix is None : prefix = self . prefix # Note: meta.items() is assumed stable from 3.7 onwards; we're not sorting # on purpose in order to keep the original insertion order in print. for key , value in meta . items (): if key not in self . META_IGNORE and not key . startswith ( \"__\" ): value_str = None if isinstance ( value , str ): value_str = '\" {} \"' . format ( misc_utils . escape_string ( value )) elif isinstance ( value , ( Decimal , datetime . date , amount . Amount , enum . Enum )): value_str = str ( value ) elif isinstance ( value , bool ): value_str = \"TRUE\" if value else \"FALSE\" elif isinstance ( value , ( dict , inventory . Inventory )): pass # Ignore dicts, don't print them out. elif value is None : value_str = \"\" # Render null metadata as empty, on purpose. else : if self . stringify_invalid_types : # This is only intended to be used during development, # when debugging for custom values of data types # attached directly and not coming from the parser. value_str = str ( value ) else : raise ValueError ( \"Unexpected value: ' {!r} '\" . format ( value )) if value_str is not None : oss . write ( \" {}{} : {} \\n \" . format ( prefix , key , value_str ))","title":"write_metadata()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.align_position_strings","text":"A helper used to align rendered amounts positions to their first currency character (an uppercase letter). This class accepts a list of rendered positions and calculates the necessary width to render them stacked in a column so that the first currency word aligns. It does not go beyond that (further currencies, e.g. for the price or cost, are not aligned). This is perhaps best explained with an example. The following positions will be aligned around the column marked with '^': 45 HOOL {504.30 USD} 4 HOOL {504.30 USD, 2014-11-11} 9.95 USD -22473.32 CAD @ 1.10 USD ^ Strings without a currency character will be rendered flush left. Parameters: strings \u2013 A list of rendered position or amount strings. Returns: A pair of a list of aligned strings and the width of the aligned strings. Source code in beancount/parser/printer.py def align_position_strings ( strings ): \"\"\"A helper used to align rendered amounts positions to their first currency character (an uppercase letter). This class accepts a list of rendered positions and calculates the necessary width to render them stacked in a column so that the first currency word aligns. It does not go beyond that (further currencies, e.g. for the price or cost, are not aligned). This is perhaps best explained with an example. The following positions will be aligned around the column marked with '^': 45 HOOL {504.30 USD} 4 HOOL {504.30 USD, 2014-11-11} 9.95 USD -22473.32 CAD @ 1.10 USD ^ Strings without a currency character will be rendered flush left. Args: strings: A list of rendered position or amount strings. Returns: A pair of a list of aligned strings and the width of the aligned strings. \"\"\" # Maximum length before the alignment character. max_before = 0 # Maximum length after the alignment character. max_after = 0 # Maximum length of unknown strings. max_unknown = 0 string_items = [] search = re . compile ( \"[A-Z]\" ) . search for string in strings : match = search ( string ) if match : index = match . start () if index != 0 : max_before = max ( index , max_before ) max_after = max ( len ( string ) - index , max_after ) string_items . append (( index , string )) continue # else max_unknown = max ( len ( string ), max_unknown ) string_items . append (( None , string )) # Compute formatting string. max_total = max ( max_before + max_after , max_unknown ) max_after_prime = max_total - max_before fmt = \"{{:> {0} }}{{: {1} }}\" . format ( max_before , max_after_prime ) . format fmt_unknown = \"{{:< {0} }}\" . format ( max_total ) . format # Align the strings and return them. aligned_strings = [] for index , string in string_items : if index is not None : string = fmt ( string [: index ], string [ index :]) else : string = fmt_unknown ( string ) aligned_strings . append ( string ) return aligned_strings , max_total","title":"align_position_strings()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.format_entry","text":"Format an entry into a string in the same input syntax the parser accepts. Parameters: entry \u2013 An entry instance. dcontext \u2013 An instance of DisplayContext used to format the numbers. render_weights \u2013 A boolean, true to render the weights for debugging. write_source \u2013 If true a source file and line number will be written for each entry in a format interpretable as a message location for Emacs, VSCode or other editors. As this is for \"debugging\" purposes, this information will be commented out by a semicolon. Returns: A string, the formatted entry. Source code in beancount/parser/printer.py def format_entry ( entry , dcontext = None , render_weights = False , prefix = None , write_source = False ): \"\"\"Format an entry into a string in the same input syntax the parser accepts. Args: entry: An entry instance. dcontext: An instance of DisplayContext used to format the numbers. render_weights: A boolean, true to render the weights for debugging. write_source: If true a source file and line number will be written for each entry in a format interpretable as a message location for Emacs, VSCode or other editors. As this is for \"debugging\" purposes, this information will be commented out by a semicolon. Returns: A string, the formatted entry. \"\"\" return EntryPrinter ( dcontext , render_weights , prefix = prefix , write_source = write_source )( entry )","title":"format_entry()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.format_error","text":"Given an error objects, return a formatted string for it. Parameters: error \u2013 a namedtuple objects representing an error. It has to have an 'entry' attribute that may be either a single directive object or a list of directive objects. Returns: A string, the errors rendered. Source code in beancount/parser/printer.py def format_error ( error ): \"\"\"Given an error objects, return a formatted string for it. Args: error: a namedtuple objects representing an error. It has to have an 'entry' attribute that may be either a single directive object or a list of directive objects. Returns: A string, the errors rendered. \"\"\" oss = io . StringIO () oss . write ( \" {} {} \\n \" . format ( render_source ( error . source ), error . message )) if error . entry is not None : entries = error . entry if isinstance ( error . entry , list ) else [ error . entry ] error_string = \" \\n \" . join ( format_entry ( entry ) for entry in entries ) oss . write ( \" \\n \" ) oss . write ( textwrap . indent ( error_string , \" \" )) oss . write ( \" \\n \" ) return oss . getvalue ()","title":"format_error()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.print_entries","text":"A convenience function that prints a list of entries to a file. Parameters: entries \u2013 A list of directives. dcontext \u2013 An instance of DisplayContext used to format the numbers. render_weights \u2013 A boolean, true to render the weights for debugging. file \u2013 An optional file object to write the entries to. prefix \u2013 User-specific prefix for custom indentation (for Fava). write_source \u2013 If true a source file and line number will be written for each entry in a format interpretable as a message location for Emacs, VSCode or other editors. This is usefull for \"debugging\" peurposes, especially in a multi-file setup Source code in beancount/parser/printer.py def print_entries ( entries , dcontext = None , render_weights = False , file = None , prefix = None , write_source = False ): \"\"\"A convenience function that prints a list of entries to a file. Args: entries: A list of directives. dcontext: An instance of DisplayContext used to format the numbers. render_weights: A boolean, true to render the weights for debugging. file: An optional file object to write the entries to. prefix: User-specific prefix for custom indentation (for Fava). write_source: If true a source file and line number will be written for each entry in a format interpretable as a message location for Emacs, VSCode or other editors. This is usefull for \"debugging\" peurposes, especially in a multi-file setup \"\"\" assert isinstance ( entries , list ), \"Entries is not a list: {} \" . format ( entries ) output = file or ( codecs . getwriter ( \"utf-8\" )( sys . stdout . buffer ) if hasattr ( sys . stdout , \"buffer\" ) else sys . stdout ) if prefix : output . write ( prefix ) previous_type = type ( entries [ 0 ]) if entries else None eprinter = EntryPrinter ( dcontext , render_weights , write_source = write_source ) for entry in entries : # Insert a newline between transactions and between blocks of directives # of the same type. entry_type = type ( entry ) if ( entry_type in ( data . Transaction , data . Commodity ) or entry_type is not previous_type or write_source ): output . write ( \" \\n \" ) previous_type = entry_type string = eprinter ( entry ) output . write ( string )","title":"print_entries()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.print_entry","text":"A convenience function that prints a single entry to a file. Parameters: entry \u2013 A directive entry. dcontext \u2013 An instance of DisplayContext used to format the numbers. render_weights \u2013 A boolean, true to render the weights for debugging. file \u2013 An optional file object to write the entries to. write_source \u2013 If true a source file and line number will be written for each entry in a format interpretable as a message location for Emacs, VSCode or other editors. This is usefull for \"debugging\" purposes, especially in a multi-file setup Source code in beancount/parser/printer.py def print_entry ( entry , dcontext = None , render_weights = False , file = None , write_source = False ): \"\"\"A convenience function that prints a single entry to a file. Args: entry: A directive entry. dcontext: An instance of DisplayContext used to format the numbers. render_weights: A boolean, true to render the weights for debugging. file: An optional file object to write the entries to. write_source: If true a source file and line number will be written for each entry in a format interpretable as a message location for Emacs, VSCode or other editors. This is usefull for \"debugging\" purposes, especially in a multi-file setup \"\"\" # TODO(blais): DO remove this now, it's a huge annoyance not to be able to # print in-between other statements. output = file or ( codecs . getwriter ( \"utf-8\" )( sys . stdout . buffer ) if hasattr ( sys . stdout , \"buffer\" ) else sys . stdout ) output . write ( format_entry ( entry , dcontext , render_weights , write_source = write_source )) output . write ( \" \\n \" )","title":"print_entry()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.print_error","text":"A convenience function that prints a single error to a file. Parameters: error \u2013 An error object. file \u2013 An optional file object to write the errors to. Source code in beancount/parser/printer.py def print_error ( error , file = None ): \"\"\"A convenience function that prints a single error to a file. Args: error: An error object. file: An optional file object to write the errors to. \"\"\" output = file or sys . stdout output . write ( format_error ( error )) output . write ( \" \\n \" )","title":"print_error()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.print_errors","text":"A convenience function that prints a list of errors to a file. Parameters: errors \u2013 A list of errors. file \u2013 An optional file object to write the errors to. Source code in beancount/parser/printer.py def print_errors ( errors , file = None , prefix = None ): \"\"\"A convenience function that prints a list of errors to a file. Args: errors: A list of errors. file: An optional file object to write the errors to. \"\"\" output = file or sys . stdout if prefix : output . write ( prefix ) for error in errors : output . write ( format_error ( error )) output . write ( \" \\n \" )","title":"print_errors()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.render_flag","text":"Render a flag, which can be None, a symbol of a character to a string. Source code in beancount/parser/printer.py def render_flag ( inflag : Optional [ str ]) -> str : \"\"\"Render a flag, which can be None, a symbol of a character to a string.\"\"\" if not inflag : return \"\" return inflag","title":"render_flag()"},{"location":"api_reference/beancount.parser.html#beancount.parser.printer.render_source","text":"Render the source for errors in a way that it will be both detected by Emacs and align and rendered nicely. Parameters: meta \u2013 A dict with the metadata. Returns: A string, rendered to be interpretable as a message location for Emacs or other editors. Source code in beancount/parser/printer.py def render_source ( meta ): \"\"\"Render the source for errors in a way that it will be both detected by Emacs and align and rendered nicely. Args: meta: A dict with the metadata. Returns: A string, rendered to be interpretable as a message location for Emacs or other editors. \"\"\" return \" {} : {:8} \" . format ( meta [ \"filename\" ], \" {} :\" . format ( meta [ \"lineno\" ]))","title":"render_source()"},{"location":"api_reference/beancount.parser.html#beancount.parser.version","text":"Implement common options across all programs.","title":"version"},{"location":"api_reference/beancount.parser.html#beancount.parser.version.compute_version_string","text":"Compute a version string from the changeset and timestamp baked in the parser. Parameters: version \u2013 A string, the version number. changeset \u2013 A string, a version control string identifying the commit of the version. timestamp \u2013 An integer, the UNIX epoch timestamp of the changeset. Returns: A human-readable string for the version. Source code in beancount/parser/version.py def compute_version_string ( version , changeset , timestamp ): \"\"\"Compute a version string from the changeset and timestamp baked in the parser. Args: version: A string, the version number. changeset: A string, a version control string identifying the commit of the version. timestamp: An integer, the UNIX epoch timestamp of the changeset. Returns: A human-readable string for the version. \"\"\" # Shorten changeset. if changeset : if re . match ( \"hg:\" , changeset ): changeset = changeset [: 15 ] elif re . match ( \"git:\" , changeset ): changeset = changeset [: 12 ] # Convert timestamp to a date. date = None if timestamp > 0 : date = datetime . datetime . fromtimestamp ( timestamp , datetime . timezone . utc ) . date () version = \"Beancount {} \" . format ( version ) if changeset or date : version = \" {} ( {} )\" . format ( version , \"; \" . join ( map ( str , filter ( None , [ changeset , date ]))) ) return version","title":"compute_version_string()"},{"location":"api_reference/beancount.plugins.html","text":"beancount.plugins \uf0c1 Example plugins for filtering transactions. These are various examples of how to filter entries in various creative ways. IMPORTANT: These are not meant to be complete features, rather just experiments in problem-solving using Beancount, work-in-progress that can be selectively installed via a --plugin option, or one-offs to answer questions on the mailing-list. beancount.plugins.auto \uf0c1 A plugin of plugins which triggers are all the automatic and lax plugins. In a sense, this is the inverse of \"pedantic.\" This is useful when doing some types of quick and dirty tests. You can just import the \"auto\" plugin. Put that in a macro. Also see: the 'pedantic' plugin. beancount.plugins.auto_accounts \uf0c1 This module automatically inserts Open directives for accounts not opened (at the date of the first entry) and automatically removes open directives for unused accounts. This can be used as a convenience for doing demos, or when setting up your initial transactions, as an intermediate step. beancount . plugins . auto_accounts . auto_insert_open ( entries , unused_options_map ) \uf0c1 Insert Open directives for accounts not opened. Open directives are inserted at the date of the first entry. Open directives for unused accounts are removed. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 A parser options dict. Returns: A list of entries, possibly with more Open entries than before, and a list of errors. Source code in beancount/plugins/auto_accounts.py def auto_insert_open ( entries , unused_options_map ): \"\"\"Insert Open directives for accounts not opened. Open directives are inserted at the date of the first entry. Open directives for unused accounts are removed. Args: entries: A list of directives. unused_options_map: A parser options dict. Returns: A list of entries, possibly with more Open entries than before, and a list of errors. \"\"\" opened_accounts = { entry . account for entry in entries if isinstance ( entry , data . Open )} new_entries = [] accounts_first , _ = getters . get_accounts_use_map ( entries ) for index , ( account , date_first_used ) in enumerate ( sorted ( accounts_first . items ())): if account not in opened_accounts : meta = data . new_metadata ( \"\" , index ) new_entries . append ( data . Open ( meta , date_first_used , account , None , None )) if new_entries : new_entries . extend ( entries ) new_entries . sort ( key = data . entry_sortkey ) else : new_entries = entries return new_entries , [] beancount.plugins.check_average_cost \uf0c1 A plugin that ensures cost basis is preserved in unbooked transactions. This is intended to be used in accounts using the \"NONE\" booking method, to manually ensure that the sum total of the cost basis of reducing legs matches the average of what's in the account inventory. This is a partial first step toward implementing the \"AVERAGE\" booking method. In other words, this plugins provides assertions that will constrain you to approximate what the \"AVERAGE\" booking method will do, manually, and not to leak too much cost basis through unmatching bookings without checks. (Note the contrived context here: Ideally the \"NONE\" booking method would simply not exist.) beancount.plugins.check_average_cost.MatchBasisError ( tuple ) \uf0c1 MatchBasisError(source, message, entry) beancount . plugins . check_average_cost . MatchBasisError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/check_average_cost.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . plugins . check_average_cost . MatchBasisError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of MatchBasisError(source, message, entry) beancount . plugins . check_average_cost . MatchBasisError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/check_average_cost.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . plugins . check_average_cost . validate_average_cost ( entries , options_map , config_str = None ) \uf0c1 Check that reducing legs on unbooked postings are near the average cost basis. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config_str \u2013 The configuration as a string version of a float. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_average_cost.py def validate_average_cost ( entries , options_map , config_str = None ): \"\"\"Check that reducing legs on unbooked postings are near the average cost basis. Args: entries: A list of directives. unused_options_map: An options map. config_str: The configuration as a string version of a float. Returns: A list of new errors, if any were found. \"\"\" # Initialize tolerance bounds. if config_str and config_str . strip (): config_obj = eval ( config_str , {}, {}) if not isinstance ( config_obj , float ): raise RuntimeError ( \"Invalid configuration for check_average_cost: \" \"must be a float\" ) tolerance = config_obj else : tolerance = DEFAULT_TOLERANCE min_tolerance = D ( 1 - tolerance ) max_tolerance = D ( 1 + tolerance ) errors = [] ocmap = getters . get_account_open_close ( entries ) balances = collections . defaultdict ( inventory . Inventory ) for entry in entries : if isinstance ( entry , Transaction ): for posting in entry . postings : dopen = ocmap . get ( posting . account , None ) # Only process accounts with a NONE booking value. if dopen and dopen [ 0 ] and dopen [ 0 ] . booking == Booking . NONE : balance = balances [ ( posting . account , posting . units . currency , posting . cost . currency if posting . cost else None , ) ] if posting . units . number < ZERO : average = balance . average () . get_only_position () if average is not None : number = average . cost . number min_valid = number * min_tolerance max_valid = number * max_tolerance if not ( min_valid <= posting . cost . number <= max_valid ): errors . append ( MatchBasisError ( entry . meta , ( \"Cost basis on reducing posting is too far from \" \"the average cost ( {} vs. {} )\" . format ( posting . cost . number , average . cost . number ) ), entry , ) ) balance . add_position ( posting ) return entries , errors beancount.plugins.check_closing \uf0c1 A plugin that automatically inserts a balance check on a tagged closing posting. Some postings are known to the user to be \"closing trades\", which means that the resulting position of the instrument just after the trade should be zero. For instance, this is the case for most ordinary options trading, only one lot of a particular instrument is held, and eventually expires or gets sold off in its entirely. One would like to confirm that, and the way to do this in Beancount is to insert a balance check. This plugin allows you to do that more simply, by inserting metadata. For example, this transaction: 2018-02-16 * \"SOLD -14 QQQ 100 16 FEB 18 160 CALL @5.31\" Assets:US:Brokerage:Main:Options -1400 QQQ180216C160 {2.70 USD} @ 5.31 USD closing: TRUE Expenses:Financial:Commissions 17.45 USD Expenses:Financial:Fees 0.42 USD Assets:US:Brokerage:Main:Cash 7416.13 USD Income:US:Brokerage:Main:PnL Would expand into the following two directives: 2018-02-16 * \"SOLD -14 QQQ 100 16 FEB 18 160 CALL @5.31\" Assets:US:Brokerage:Main:Options -1400 QQQ180216C160 {2.70 USD} @ 5.31 USD Expenses:Financial:Commissions 17.45 USD Expenses:Financial:Fees 0.42 USD Assets:US:Brokerage:Main:Cash 7416.13 USD Income:US:Brokerage:Main:PnL 2018-02-17 balance Assets:US:Brokerage:Main:Options 0 QQQ180216C160 Insert the closing line when you know you're closing the position. beancount . plugins . check_closing . check_closing ( entries , options_map ) \uf0c1 Expand 'closing' metadata to a zero balance check. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_closing.py def check_closing ( entries , options_map ): \"\"\"Expand 'closing' metadata to a zero balance check. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" new_entries = [] for entry in entries : if isinstance ( entry , data . Transaction ): for i , posting in enumerate ( entry . postings ): if posting . meta and posting . meta . get ( \"closing\" , False ): # Remove the metadata. meta = posting . meta . copy () del meta [ \"closing\" ] posting = posting . _replace ( meta = meta ) entry . postings [ i ] = posting # Insert a balance. date = entry . date + datetime . timedelta ( days = 1 ) balance = data . Balance ( data . new_metadata ( \"\" , 0 ), date , posting . account , amount . Amount ( ZERO , posting . units . currency ), None , None , ) new_entries . append ( balance ) new_entries . append ( entry ) return new_entries , [] beancount.plugins.check_commodity \uf0c1 A plugin that verifies that all seen commodities have a Commodity directive. This is useful if you're a bit pedantic and like to make sure that you're declared attributes for each of the commodities you use. It's useful if you use the portfolio export, for example. You can provide a mapping of (account-regexp, currency-regexp) as options, to specify which commodities to ignore from this plugin selectively. Use this sparingly, as it is an out from the checks that this plugin provides. However, in an active options trading account, a ton of products get inserted and the number of commodity directives can be overwhelming and it's not productive to declare each of the options contracts - names with an embedded strike and expiration date, such as 'SPX_121622P3300' - individually. Note that if a symbol has been ignored in at least one account, it will therefore be further in all Price directives and Metadata values. beancount.plugins.check_commodity.CheckCommodityError ( tuple ) \uf0c1 CheckCommodityError(source, message, entry) beancount . plugins . check_commodity . CheckCommodityError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/check_commodity.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . plugins . check_commodity . CheckCommodityError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of CheckCommodityError(source, message, entry) beancount . plugins . check_commodity . CheckCommodityError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/check_commodity.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.plugins.check_commodity.ConfigError ( tuple ) \uf0c1 ConfigError(source, message, entry) beancount . plugins . check_commodity . ConfigError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/check_commodity.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . plugins . check_commodity . ConfigError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of ConfigError(source, message, entry) beancount . plugins . check_commodity . ConfigError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/check_commodity.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . plugins . check_commodity . get_commodity_map_ex ( entries , metadata = False ) \uf0c1 Find and extract commodities in the stream of directives. Source code in beancount/plugins/check_commodity.py def get_commodity_map_ex ( entries , metadata = False ): \"\"\"Find and extract commodities in the stream of directives.\"\"\" # Find commodity names in metadata. # # TODO(dnicolodi) Unfortunately detecting commodities in metadata # values may result in false positives: common used string are # matched by the regular expression. Revisit this when commodities # will be represented with their own type. ignore = set ([ \"filename\" , \"lineno\" , \"__automatic__\" ]) regexp = re . compile ( CURRENCY_RE ) def currencies_in_meta ( entry ): if entry . meta is not None : for key , value in entry . meta . items (): if isinstance ( value , str ) and key not in ignore : if regexp . fullmatch ( value ): yield value commodities_map = {} occurrences = set () for entry in entries : if isinstance ( entry , data . Commodity ): commodities_map [ entry . currency ] = entry elif isinstance ( entry , data . Open ): if entry . currencies : for currency in entry . currencies : occurrences . add (( entry . account , currency )) elif isinstance ( entry , data . Transaction ): for posting in entry . postings : # Main currency. units = posting . units occurrences . add (( posting . account , units . currency )) # Currency in cost. cost = posting . cost if cost : occurrences . add (( posting . account , cost . currency )) # Currency in price. price = posting . price if price : occurrences . add (( posting . account , price . currency )) # Currency in posting metadata. if metadata : for currency in currencies_in_meta ( posting ): occurrences . add (( posting . account , currency )) elif isinstance ( entry , data . Balance ): occurrences . add (( entry . account , entry . amount . currency )) elif isinstance ( entry , data . Price ): occurrences . add (( PRICE_CONTEXT , entry . currency )) occurrences . add (( PRICE_CONTEXT , entry . amount . currency )) # Entry metadata. if metadata : for currency in currencies_in_meta ( entry ): occurrences . add (( METADATA_CONTEXT , currency )) return occurrences , commodities_map beancount . plugins . check_commodity . validate_commodity_directives ( entries , options_map , config_str = None ) \uf0c1 Find all commodities used and ensure they have a corresponding Commodity directive. Parameters: entries \u2013 A list of directives. options_map \u2013 An options map. config_str \u2013 The configuration as a string version of a float. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_commodity.py def validate_commodity_directives ( entries , options_map , config_str = None ): \"\"\"Find all commodities used and ensure they have a corresponding Commodity directive. Args: entries: A list of directives. options_map: An options map. config_str: The configuration as a string version of a float. Returns: A list of new errors, if any were found. \"\"\" errors = [] if config_str : config_obj = eval ( config_str , {}, {}) if not isinstance ( config_obj , dict ): errors . append ( ConfigError ( data . new_metadata ( \"\" , 0 ), \"Invalid configuration for check_commodity plugin; skipping.\" , None , ) ) return entries , errors else : config_obj = {} # Compile the regular expressions, producing an error if invalid. ignore_map = {} for key , value in config_obj . items (): kv = [] for pattern in key , value : try : kv . append ( re . compile ( pattern ) . match ) except re . error : meta = data . new_metadata ( \"\" , 0 ) errors . append ( CheckCommodityError ( meta , \"Invalid regexp: ' {} ' for {} \" . format ( value , key ), None ) ) if len ( kv ) == 2 : ignore_map [ kv [ 0 ]] = kv [ 1 ] # Get all the occurrences of commodities and a mapping of the directives. # # TODO(blais): Establish a distinction at the parser level for commodities # and strings, so that we can turn detection of them in metadata. occurrences , commodity_map = get_commodity_map_ex ( entries , metadata = False ) # Process all currencies with context. issued = set () ignored = set () anonymous = set () for context , currency in sorted ( occurrences ): if context in ANONYMOUS : anonymous . add (( context , currency )) continue commodity_entry = commodity_map . get ( currency , None ) # Skip if the commodity was declared, or if an error for that commodity # has already been issued. if commodity_entry is not None or currency in issued : continue # If any of the ignore patterns matches, ignore and record ignored. if any ( ( context_re ( context ) and currency_re ( currency )) for context_re , currency_re in ignore_map . items () ): ignored . add ( currency ) continue # Issue error. meta = data . new_metadata ( \"\" , 0 ) errors . append ( CheckCommodityError ( meta , \"Missing Commodity directive for ' {} ' in ' {} '\" . format ( currency , context ), None , ) ) # Process it only once. issued . add ( currency ) # Process all currencies out of context, automatically ignoring those which # have already been issued with account context.. for context , currency in sorted ( anonymous ): commodity_entry = commodity_map . get ( currency , None ) # Skip if (a) the commodity was declared, any of the ignore patterns # matches, or an error for that commodity has already been issued. if commodity_entry is not None or currency in issued or currency in ignored : continue # Issue error. meta = data . new_metadata ( \"\" , 0 ) errors . append ( CheckCommodityError ( meta , \"Missing Commodity directive for ' {} ' in ' {} '\" . format ( currency , context ), None , ) ) return entries , errors beancount.plugins.check_drained \uf0c1 Insert a balance check for zero before balance sheets accounts are closed. For balance sheet accounts with a Close directive (Assets, Liabilities & Equity), insert Balance directives just after its closing date, for all the commodities that have appeared in that account and that are declared as legal on it as well. This performs the equivalent of the following transformation: 2018-02-01 open Assets:Project:Cash USD,CAD ... 2020-02-01 close Assets:Project:Cash !!! to 2018-02-01 open Assets:Project:Cash USD,CAD ... 2020-02-01 close Assets:Project:Cash 2020-02-02 balance Assets:Project:Cash 0 USD 2020-02-02 balance Assets:Project:Cash 0 CAD beancount . plugins . check_drained . check_drained ( entries , options_map ) \uf0c1 Check that closed accounts are empty. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_drained.py def check_drained ( entries , options_map ): \"\"\"Check that closed accounts are empty. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" acctypes = options . get_account_types ( options_map ) is_covered = functools . partial ( account_types . is_balance_sheet_account , account_types = acctypes ) new_entries = [] currencies = collections . defaultdict ( set ) balances = collections . defaultdict ( set ) for entry in entries : if isinstance ( entry , data . Transaction ): # Accumulate all the currencies seen in each account over time. for posting in entry . postings : if is_covered ( posting . account ): currencies [ posting . account ] . add ( posting . units . currency ) elif isinstance ( entry , data . Open ): # Accumulate all the currencies declared in the account opening. if is_covered ( entry . account ) and entry . currencies : for currency in entry . currencies : currencies [ entry . account ] . add ( currency ) elif isinstance ( entry , data . Balance ): # Ignore balances where directives are present. if is_covered ( entry . account ): balances [ entry . account ] . add (( entry . date , entry . amount . currency )) if isinstance ( entry , data . Close ): if is_covered ( entry . account ): for currency in currencies [ entry . account ]: # Skip balance insertion due to the presence of an explicit one. if ( entry . date , currency ) in balances [ entry . account ]: continue # Insert a balance directive. balance_entry = data . Balance ( # Note: We use the close directive's meta so that # balance errors direct the user to the corresponding # close directive. entry . meta , entry . date + ONE_DAY , entry . account , amount . Amount ( ZERO , currency ), None , None , ) new_entries . append ( balance_entry ) new_entries . append ( entry ) return new_entries , [] beancount.plugins.close_tree \uf0c1 This plugin inserts close directives for all of an account's descendants when an account is closed. Unopened parent accounts can also be closed. Any explicitly specified close is left untouched. For example, given this:: 2017-11-10 open Assets:Brokerage:AAPL 2017-11-10 open Assets:Brokerage:ORNG 2018-11-10 close Assets:Brokerage ; this does not necessarily need to be opened the plugin turns it into:: 2017-11-10 open Assets:Brokerage:AAPL 2017-11-10 open Assets:Brokerage:ORNG 2018-11-10 close Assets:Brokerage:AAPL 2018-11-10 close Assets:Brokerage:ORNG Invoke this plugin after any plugins that generate open directives for account trees that you want to auto close. An example is the auto_accounts plugin that ships with Beancount:: plugin \"beancount.plugins.auto_accounts\" plugin \"beancount.plugins.close_tree\" beancount . plugins . close_tree . close_tree ( entries , unused_options_map ) \uf0c1 Insert close entries for all subaccounts of a closed account. Parameters: entries \u2013 A list of directives. We're interested only in the Open/Close instances. unused_options_map \u2013 A parser options dict. Returns: A tuple of entries and errors. Source code in beancount/plugins/close_tree.py def close_tree ( entries , unused_options_map ): \"\"\"Insert close entries for all subaccounts of a closed account. Args: entries: A list of directives. We're interested only in the Open/Close instances. unused_options_map: A parser options dict. Returns: A tuple of entries and errors. \"\"\" new_entries = [] errors = [] opens = set ( entry . account for entry in entries if isinstance ( entry , Open )) closes = set ( entry . account for entry in entries if isinstance ( entry , Close )) for entry in entries : if isinstance ( entry , Close ): subaccounts = [ account for account in opens if account . startswith ( entry . account + \":\" ) and account not in closes ] for subacc in subaccounts : meta = data . new_metadata ( \"\" , 0 ) close_entry = data . Close ( meta , entry . date , subacc ) new_entries . append ( close_entry ) # So we don't attempt to re-close a grandchild that a child closed closes . add ( subacc ) if entry . account in opens : new_entries . append ( entry ) else : new_entries . append ( entry ) return new_entries , errors beancount.plugins.coherent_cost \uf0c1 This plugin validates that currencies held at cost aren't ever converted at price and vice-versa. This is usually the case, and using it will prevent users from making the mistake of selling a lot without specifying it via its cost basis. beancount.plugins.coherent_cost.CoherentCostError ( tuple ) \uf0c1 CoherentCostError(source, message, entry) beancount . plugins . coherent_cost . CoherentCostError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/coherent_cost.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . plugins . coherent_cost . CoherentCostError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of CoherentCostError(source, message, entry) beancount . plugins . coherent_cost . CoherentCostError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/coherent_cost.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . plugins . coherent_cost . validate_coherent_cost ( entries , unused_options_map ) \uf0c1 Check that all currencies are either used at cost or not at all, but never both. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/coherent_cost.py def validate_coherent_cost ( entries , unused_options_map ): \"\"\"Check that all currencies are either used at cost or not at all, but never both. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] with_cost = {} without_cost = {} for entry in data . filter_txns ( entries ): for posting in entry . postings : target_set = without_cost if posting . cost is None else with_cost currency = posting . units . currency target_set . setdefault ( currency , entry ) for currency in set ( with_cost ) & set ( without_cost ): errors . append ( CoherentCostError ( without_cost [ currency ] . meta , \"Currency ' {} ' is used both with and without cost\" . format ( currency ), with_cost [ currency ], ) ) # Note: We really ought to include both of the first transactions here. return entries , errors beancount.plugins.commodity_attr \uf0c1 A plugin that asserts that all Commodity directives have a particular attribute and that it is part of a set of enum values. The configuration must be a mapping of attribute name to list of valid values, like this: plugin \"beancount.plugins.commodity_attr\" \"{ 'sector': ['Technology', 'Financials', 'Energy'], 'name': None, }\" The plugin issues an error if a Commodity directive is missing the attribute, or if the attribute value is not in the valid set. If you'd like to just ensure the attribute is set, set the list of valid values to None, as in the 'name' attribute in the example above. beancount.plugins.commodity_attr.CommodityError ( tuple ) \uf0c1 CommodityError(source, message, entry) beancount . plugins . commodity_attr . CommodityError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/commodity_attr.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . plugins . commodity_attr . CommodityError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of CommodityError(source, message, entry) beancount . plugins . commodity_attr . CommodityError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/commodity_attr.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.plugins.commodity_attr.ConfigError ( tuple ) \uf0c1 ConfigError(source, message, entry) beancount . plugins . commodity_attr . ConfigError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/commodity_attr.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . plugins . commodity_attr . ConfigError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of ConfigError(source, message, entry) beancount . plugins . commodity_attr . ConfigError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/commodity_attr.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . plugins . commodity_attr . validate_commodity_attr ( entries , unused_options_map , config_str ) \uf0c1 Check that all Commodity directives have a valid attribute. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config_str \u2013 A configuration string. Returns: A list of new errors, if any were found. Source code in beancount/plugins/commodity_attr.py def validate_commodity_attr ( entries , unused_options_map , config_str ): \"\"\"Check that all Commodity directives have a valid attribute. Args: entries: A list of directives. unused_options_map: An options map. config_str: A configuration string. Returns: A list of new errors, if any were found. \"\"\" errors = [] config_obj = eval ( config_str , {}, {}) if not isinstance ( config_obj , dict ): errors . append ( ConfigError ( data . new_metadata ( \"\" , 0 ), \"Invalid configuration for commodity_attr plugin; skipping.\" , None , ) ) return entries , errors validmap = { attr : frozenset ( values ) if values is not None else None for attr , values in config_obj . items () } for entry in entries : if not isinstance ( entry , data . Commodity ): continue for attr , values in validmap . items (): value = entry . meta . get ( attr , None ) if value is None : errors . append ( CommodityError ( entry . meta , \"Missing attribute ' {} ' for Commodity directive {} \" . format ( attr , entry . currency ), None , ) ) continue if values and value not in values : errors . append ( CommodityError ( entry . meta , \"Invalid value ' {} ' for attribute {} , Commodity\" . format ( value , attr ) + \" directive {} ; valid options: {} \" . format ( entry . currency , \", \" . join ( values ) ), None , ) ) return entries , errors beancount.plugins.currency_accounts \uf0c1 An implementation of currency accounts. This is an automatic implementation of the method described here: https://www.mathstat.dal.ca/~selinger/accounting/tutorial.html You enable it just like this: plugin \"beancount.plugins.currency_accounts\" \"Equity:CurrencyAccounts\" Accounts will be automatically created under the given base account, with the currency name appended to it, e.g., Equity:CurrencyAccounts:CAD Equity:CurrencyAccounts:USD etc., where used. You can have a look at the account balances with a query like this: bean-query $L \"select account, sum(position), convert(sum(position), 'USD') where date >= 2018-01-01 and account ~ 'CurrencyAccounts' \" The sum total of the converted amounts should be a number not too large: bean-query $L \"select convert(sum(position), 'USD') where date >= 2018-01-01 and account ~ 'CurrencyAccounts'\" WARNING: This is a prototype. Note the FIXMEs in the code below, which indicate some potential problems. beancount . plugins . currency_accounts . get_neutralizing_postings ( curmap , base_account , new_accounts ) \uf0c1 Process an entry. Parameters: curmap \u2013 A dict of currency to a list of Postings of this transaction. base_account \u2013 A string, the root account name to insert. new_accounts \u2013 A set, a mutable accumulator of new account names. Returns: A modified entry, with new postings inserted to rebalance currency trading accounts. Source code in beancount/plugins/currency_accounts.py def get_neutralizing_postings ( curmap , base_account , new_accounts ): \"\"\"Process an entry. Args: curmap: A dict of currency to a list of Postings of this transaction. base_account: A string, the root account name to insert. new_accounts: A set, a mutable accumulator of new account names. Returns: A modified entry, with new postings inserted to rebalance currency trading accounts. \"\"\" new_postings = [] for currency , postings in curmap . items (): # Compute the per-currency balance. inv = inventory . Inventory () for posting in postings : inv . add_amount ( convert . get_cost ( posting )) if inv . is_empty (): new_postings . extend ( postings ) continue # Re-insert original postings and remove price conversions. # # Note: This may cause problems if the implicit_prices plugin is # configured to run after this one, or if you need the price annotations # for some scripting or serious work. # # FIXME: We need to handle these important cases (they're not frivolous, # this is a prototype), probably by inserting some exceptions with # collaborating code in the booking (e.g. insert some metadata that # disables price conversions on those postings). # # FIXME(2): Ouch! Some of the residual seeps through here, where there # are more than a single currency block. This needs fixing too. You can # easily mitigate some of this to some extent, by excluding transactions # which don't have any price conversion in them. for pos in postings : if pos . price is not None : pos = pos . _replace ( price = None ) new_postings . append ( pos ) # Insert the currency trading accounts postings. amount = inv . get_only_position () . units acc = account . join ( base_account , currency ) new_accounts . add ( acc ) new_postings . append ( Posting ( acc , - amount , None , None , None , None )) return new_postings beancount . plugins . currency_accounts . group_postings_by_weight_currency ( entry ) \uf0c1 Return where this entry might require adjustment. Source code in beancount/plugins/currency_accounts.py def group_postings_by_weight_currency ( entry : Transaction ): \"\"\"Return where this entry might require adjustment.\"\"\" curmap = collections . defaultdict ( list ) has_price = False for posting in entry . postings : currency = posting . units . currency if posting . cost : currency = posting . cost . currency if posting . price : assert posting . price . currency == currency elif posting . price : has_price = True currency = posting . price . currency if posting . price : has_price = True curmap [ currency ] . append ( posting ) return curmap , has_price beancount . plugins . currency_accounts . insert_currency_trading_postings ( entries , options_map , config ) \uf0c1 Insert currency trading postings. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config \u2013 The base account name for currency trading accounts. Returns: A list of new errors, if any were found. Source code in beancount/plugins/currency_accounts.py 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 ( \"\" , index ), earliest_date , acc , None , None ) for index , acc in enumerate ( sorted ( new_accounts )) ] return open_entries + new_entries , errors beancount.plugins.implicit_prices \uf0c1 This plugin synthesizes Price directives for all Postings with a price or directive or if it is an augmenting posting, has a cost directive. beancount.plugins.implicit_prices.ImplicitPriceError ( tuple ) \uf0c1 ImplicitPriceError(source, message, entry) beancount . plugins . implicit_prices . ImplicitPriceError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/implicit_prices.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . plugins . implicit_prices . ImplicitPriceError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of ImplicitPriceError(source, message, entry) beancount . plugins . implicit_prices . ImplicitPriceError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/implicit_prices.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . plugins . implicit_prices . add_implicit_prices ( entries , unused_options_map ) \uf0c1 Insert implicitly defined prices from Transactions. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. Source code in beancount/plugins/implicit_prices.py def add_implicit_prices ( entries , unused_options_map ): \"\"\"Insert implicitly defined prices from Transactions. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. \"\"\" new_entries = [] errors = [] # A dict of (date, currency, cost-currency) to price entry. new_price_entry_map = {} balances = collections . defaultdict ( inventory . Inventory ) for entry in entries : # Always replicate the existing entries. new_entries . append ( entry ) if isinstance ( entry , Transaction ): # Inspect all the postings in the transaction. for posting in entry . postings : units = posting . units cost = posting . cost # Check if the position is matching against an existing # position. _ , booking = balances [ posting . account ] . add_position ( posting ) # Add prices when they're explicitly specified on a posting. An # explicitly specified price may occur in a conversion, e.g. # Assets:Account 100 USD @ 1.10 CAD # or, if a cost is also specified, as the current price of the # underlying instrument, e.g. # Assets:Account 100 HOOL {564.20} @ {581.97} USD if posting . price is not None : meta = data . new_metadata ( entry . meta [ \"filename\" ], entry . meta [ \"lineno\" ]) meta [ METADATA_FIELD ] = \"from_price\" price_entry = data . Price ( meta , entry . date , units . currency , posting . price ) # Add costs, when we're not matching against an existing # position. This happens when we're just specifying the cost, # e.g. # Assets:Account 100 HOOL {564.20} elif cost is not None and booking != inventory . MatchResult . REDUCED : # TODO(blais): What happens here if the account has no # booking strategy? Do we end up inserting a price for the # reducing leg? Check. meta = data . new_metadata ( entry . meta [ \"filename\" ], entry . meta [ \"lineno\" ]) meta [ METADATA_FIELD ] = \"from_cost\" price_entry = data . Price ( meta , entry . date , units . currency , amount . Amount ( cost . number , cost . currency ), ) else : price_entry = None if price_entry is not None : key = ( price_entry . date , price_entry . currency , price_entry . amount . number , # Ideally should be removed. price_entry . amount . currency , ) try : new_price_entry_map [ key ] ## Do not fail for now. We still have many valid use ## cases of duplicate prices on the same date, for ## example, stock splits, or trades on two dates with ## two separate reported prices. We need to figure out a ## more elegant solution for this in the long term. ## Keeping both for now. We should ideally not use the ## number in the de-dup key above. # # dup_entry = new_price_entry_map[key] # if price_entry.amount.number == dup_entry.amount.number: # # Skip duplicates. # continue # else: # errors.append( # ImplicitPriceError( # entry.meta, # \"Duplicate prices for {} on {}\".format(entry, # dup_entry), # entry)) except KeyError : new_price_entry_map [ key ] = price_entry new_entries . append ( price_entry ) return new_entries , errors beancount.plugins.leafonly \uf0c1 A plugin that issues errors when amounts are posted to non-leaf accounts, that is, accounts with child accounts. This is an extra constraint that you may want to apply optionally. If you install this plugin, it will issue errors for all accounts that have postings to non-leaf accounts. Some users may want to disallow this and enforce that only leaf accounts may have postings on them. beancount.plugins.leafonly.LeafOnlyError ( tuple ) \uf0c1 LeafOnlyError(source, message, entry) beancount . plugins . leafonly . LeafOnlyError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/leafonly.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . plugins . leafonly . LeafOnlyError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of LeafOnlyError(source, message, entry) beancount . plugins . leafonly . LeafOnlyError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/leafonly.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . plugins . leafonly . validate_leaf_only ( entries , unused_options_map ) \uf0c1 Check for non-leaf accounts that have postings on them. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/leafonly.py def validate_leaf_only ( entries , unused_options_map ): \"\"\"Check for non-leaf accounts that have postings on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" real_root = realization . realize ( entries , compute_balance = False ) default_meta = data . new_metadata ( \"\" , 0 ) open_close_map = None # Lazily computed. errors = [] for real_account in realization . iter_children ( real_root ): if len ( real_account ) > 0 and real_account . txn_postings : if open_close_map is None : open_close_map = getters . get_account_open_close ( entries ) try : open_entry = open_close_map [ real_account . account ][ 0 ] except KeyError : open_entry = None errors . append ( LeafOnlyError ( open_entry . meta if open_entry else default_meta , \"Non-leaf account ' {} ' has postings on it\" . format ( real_account . account ), open_entry , ) ) return entries , errors beancount.plugins.noduplicates \uf0c1 This plugin validates that there are no duplicate transactions. beancount . plugins . noduplicates . validate_no_duplicates ( entries , unused_options_map ) \uf0c1 Check that the entries are unique, by computing hashes. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/noduplicates.py def validate_no_duplicates ( entries , unused_options_map ): \"\"\"Check that the entries are unique, by computing hashes. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" unused_hashes , errors = compare . hash_entries ( entries , exclude_meta = True ) return entries , errors beancount.plugins.nounused \uf0c1 This plugin validates that there are no unused accounts. beancount.plugins.nounused.UnusedAccountError ( tuple ) \uf0c1 UnusedAccountError(source, message, entry) beancount . plugins . nounused . UnusedAccountError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/nounused.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . plugins . nounused . UnusedAccountError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of UnusedAccountError(source, message, entry) beancount . plugins . nounused . UnusedAccountError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/nounused.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . plugins . nounused . validate_unused_accounts ( entries , unused_options_map ) \uf0c1 Check that all accounts declared open are actually used. We check that all of the accounts that are open are at least referred to by another directive. These are probably unused, so issue a warning (we like to be pedantic). Note that an account that is open and then closed is considered used--this is a valid use case that may occur in reality. If you have a use case for an account to be open but never used, you can quiet that warning by initializing the account with a balance asserts or a pad directive, or even use a note will be sufficient. (This is probably a good candidate for optional inclusion as a \"pedantic\" plugin.) Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/nounused.py def validate_unused_accounts ( entries , unused_options_map ): \"\"\"Check that all accounts declared open are actually used. We check that all of the accounts that are open are at least referred to by another directive. These are probably unused, so issue a warning (we like to be pedantic). Note that an account that is open and then closed is considered used--this is a valid use case that may occur in reality. If you have a use case for an account to be open but never used, you can quiet that warning by initializing the account with a balance asserts or a pad directive, or even use a note will be sufficient. (This is probably a good candidate for optional inclusion as a \"pedantic\" plugin.) Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" # Find all the accounts referenced by entries which are not Open, and the # open directives for error reporting below. open_map = {} referenced_accounts = set () for entry in entries : if isinstance ( entry , data . Open ): open_map [ entry . account ] = entry continue referenced_accounts . update ( getters . get_entry_accounts ( entry )) # Create a list of suitable errors, with the location of the Open directives # corresponding to the unused accounts. errors = [ UnusedAccountError ( open_entry . meta , \"Unused account ' {} '\" . format ( account ), open_entry ) for account , open_entry in open_map . items () if account not in referenced_accounts ] return entries , errors beancount.plugins.onecommodity \uf0c1 A plugin that issues errors when more than one commodity is used in an account. For investments or trading accounts, it can make it easier to filter the action around a single stock by using the name of the stock as the leaf of the account name. Notes: The plugin will automatically skip accounts that have explicitly declared commodities in their Open directive. You can also set the metadata \"onecommodity: FALSE\" on an account's Open directive to skip the checks for that account. If provided, the configuration should be a regular expression restricting the set of accounts to check. beancount.plugins.onecommodity.OneCommodityError ( tuple ) \uf0c1 OneCommodityError(source, message, entry) beancount . plugins . onecommodity . OneCommodityError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/onecommodity.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . plugins . onecommodity . OneCommodityError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of OneCommodityError(source, message, entry) beancount . plugins . onecommodity . OneCommodityError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/onecommodity.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . plugins . onecommodity . validate_one_commodity ( entries , unused_options_map , config = None ) \uf0c1 Check that each account has units in only a single commodity. This is an extra constraint that you may want to apply optionally, despite Beancount's ability to support inventories and aggregations with more than one commodity. I believe this also matches GnuCash's model, where each account has a single commodity attached to it. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config \u2013 The plugin configuration string, a regular expression to match against the subset of accounts to check. Returns: A list of new errors, if any were found. Source code in beancount/plugins/onecommodity.py def validate_one_commodity ( entries , unused_options_map , config = None ): \"\"\"Check that each account has units in only a single commodity. This is an extra constraint that you may want to apply optionally, despite Beancount's ability to support inventories and aggregations with more than one commodity. I believe this also matches GnuCash's model, where each account has a single commodity attached to it. Args: entries: A list of directives. unused_options_map: An options map. config: The plugin configuration string, a regular expression to match against the subset of accounts to check. Returns: A list of new errors, if any were found. \"\"\" accounts_re = re . compile ( config ) if config else None # Mappings of account name to lists of currencies for each units and cost. units_map = collections . defaultdict ( set ) cost_map = collections . defaultdict ( set ) # Mappings to use just for getting a relevant source. units_source_map = {} cost_source_map = {} # Gather the set of accounts to skip from the Open directives. skip_accounts = set () for entry in entries : if not isinstance ( entry , data . Open ): continue if ( not entry . meta . get ( \"onecommodity\" , True ) or ( accounts_re and not accounts_re . match ( entry . account )) or ( entry . currencies and len ( entry . currencies ) > 1 ) ): skip_accounts . add ( entry . account ) # Accumulate all the commodities used. for entry in entries : if isinstance ( entry , data . Transaction ): for posting in entry . postings : if posting . account in skip_accounts : continue units = posting . units units_map [ posting . account ] . add ( units . currency ) if len ( units_map [ posting . account ]) > 1 : units_source_map [ posting . account ] = entry cost = posting . cost if cost : cost_map [ posting . account ] . add ( cost . currency ) if len ( cost_map [ posting . account ]) > 1 : cost_source_map [ posting . account ] = entry elif isinstance ( entry , data . Balance ): if entry . account in skip_accounts : continue units_map [ entry . account ] . add ( entry . amount . currency ) if len ( units_map [ entry . account ]) > 1 : units_source_map [ entry . account ] = entry elif isinstance ( entry , data . Open ): if entry . currencies and len ( entry . currencies ) > 1 : skip_accounts . add ( entry . account ) # Check units. errors = [] for account , currencies in units_map . items (): if account in skip_accounts : continue if len ( currencies ) > 1 : errors . append ( OneCommodityError ( units_source_map [ account ] . meta , \"More than one currency in account ' {} ': {} \" . format ( account , \",\" . join ( currencies ) ), None , ) ) # Check costs. for account , currencies in cost_map . items (): if account in skip_accounts : continue if len ( currencies ) > 1 : errors . append ( OneCommodityError ( cost_source_map [ account ] . meta , \"More than one cost currency in account ' {} ': {} \" . format ( account , \",\" . join ( currencies ) ), None , ) ) return entries , errors beancount.plugins.pedantic \uf0c1 A plugin of plugins which triggers all the pedantic plugins. beancount.plugins.sellgains \uf0c1 A plugin that cross-checks declared gains against prices on lot sales. When you sell stock, the gains can be automatically implied by the corresponding cash amounts. For example, in the following transaction the 2nd and 3rd postings should match the value of the stock sold: 1999-07-31 * \"Sell\" Assets:US:BRS:Company:ESPP -81 ADSK {26.3125 USD} Assets:US:BRS:Company:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:US:Company:ESPP:PnL -10.125 USD The cost basis is checked against: 2141.36 + 008 + -10.125. That is, the balance checks computes -81 x 26.3125 = -2131.3125 + 2141.36 + 0.08 + -10.125 and checks that the residual is below a small tolerance. But... usually the income leg isn't given to you in statements. Beancount can automatically infer it using the balance, which is convenient, like this: 1999-07-31 * \"Sell\" Assets:US:BRS:Company:ESPP -81 ADSK {26.3125 USD} Assets:US:BRS:Company:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:US:Company:ESPP:PnL Additionally, most often you have the sales prices given to you on your transaction confirmation statement, so you can enter this: 1999-07-31 * \"Sell\" Assets:US:BRS:Company:ESPP -81 ADSK {26.3125 USD} @ 26.4375 USD Assets:US:BRS:Company:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:US:Company:ESPP:PnL So in theory, if the price is given (26.4375 USD), we could verify that the proceeds from the sale at the given price match non-Income postings. That is, verify that -81 x 26.4375 = -2141.4375 + 2141.36 + 0.08 + is below a small tolerance value. So this plugin does this. In general terms, it does the following: For transactions with postings that have a cost and a price, it verifies that the sum of the positions on all postings to non-income accounts is below tolerance. This provides yet another level of verification and allows you to elide the income amounts, knowing that the price is there to provide an extra level of error-checking in case you enter a typo. beancount.plugins.sellgains.SellGainsError ( tuple ) \uf0c1 SellGainsError(source, message, entry) beancount . plugins . sellgains . SellGainsError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/sellgains.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . plugins . sellgains . SellGainsError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of SellGainsError(source, message, entry) beancount . plugins . sellgains . SellGainsError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/sellgains.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . plugins . sellgains . validate_sell_gains ( entries , options_map ) \uf0c1 Check the sum of asset account totals for lots sold with a price on them. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/sellgains.py def validate_sell_gains ( entries , options_map ): \"\"\"Check the sum of asset account totals for lots sold with a price on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] acc_types = options . get_account_types ( options_map ) proceed_types = set ( [ acc_types . assets , acc_types . liabilities , acc_types . equity , acc_types . expenses ] ) for entry in entries : if not isinstance ( entry , data . Transaction ): continue # Find transactions whose lots at cost all have a price. postings_at_cost = [ posting for posting in entry . postings if posting . cost is not None ] if not postings_at_cost or not all ( posting . price is not None for posting in postings_at_cost ): continue # Accumulate the total expected proceeds and the sum of the asset and # expenses legs. total_price = inventory . Inventory () total_proceeds = inventory . Inventory () for posting in entry . postings : # If the posting is held at cost, add the priced value to the balance. if posting . cost is not None : assert posting . price is not None price = posting . price total_price . add_amount ( amount . mul ( price , - posting . units . number )) else : # Otherwise, use the weight and ignore postings to Income accounts. atype = account_types . get_account_type ( posting . account ) if atype in proceed_types : total_proceeds . add_amount ( convert . get_weight ( posting )) # Compare inventories, currency by currency. dict_price = { pos . units . currency : pos . units . number for pos in total_price } dict_proceeds = { pos . units . currency : pos . units . number for pos in total_proceeds } tolerances = interpolate . infer_tolerances ( entry . postings , options_map ) invalid = False for currency , price_number in dict_price . items (): # Accept a looser than usual tolerance because rounding occurs # differently. Also, it would be difficult for the user to satisfy # two sets of constraints manually. tolerance = tolerances . get ( currency ) * EXTRA_TOLERANCE_MULTIPLIER proceeds_number = dict_proceeds . pop ( currency , ZERO ) diff = abs ( price_number - proceeds_number ) if diff > tolerance : invalid = True break if invalid or dict_proceeds : errors . append ( SellGainsError ( entry . meta , \"Invalid price vs. proceeds/gains: {} vs. {} ; difference: {} \" . format ( total_price , total_proceeds , ( total_price + - total_proceeds ) ), entry , ) ) return entries , errors beancount.plugins.unique_prices \uf0c1 This module adds validation that there is a single price defined per date and base/quote currencies. If multiple conflicting price values are declared, an error is generated. Note that multiple price entries with the same number do not generate an error. This is meant to be turned on if you want to use a very strict mode for entering prices, and may not be realistic usage. For example, if you have (1) a transaction with an implicitly generated price during the day (from its cost) and (2) a separate explicit price directive that declares a different price for the day's closing price, this would generate an error. I'm not certain this will be useful in the long run, so placing it in a plugin. beancount.plugins.unique_prices.UniquePricesError ( tuple ) \uf0c1 UniquePricesError(source, message, entry) beancount . plugins . unique_prices . UniquePricesError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/unique_prices.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . plugins . unique_prices . UniquePricesError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of UniquePricesError(source, message, entry) beancount . plugins . unique_prices . UniquePricesError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/plugins/unique_prices.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . plugins . unique_prices . validate_unique_prices ( entries , unused_options_map ) \uf0c1 Check that there is only a single price per day for a particular base/quote. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. Returns: The list of input entries, and a list of new UniquePricesError instances generated. Source code in beancount/plugins/unique_prices.py def validate_unique_prices ( entries , unused_options_map ): \"\"\"Check that there is only a single price per day for a particular base/quote. Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. Returns: The list of input entries, and a list of new UniquePricesError instances generated. \"\"\" errors = [] prices = collections . defaultdict ( list ) for entry in entries : if not isinstance ( entry , data . Price ): continue key = ( entry . date , entry . currency , entry . amount . currency ) prices [ key ] . append ( entry ) errors = [] for price_entries in prices . values (): if len ( price_entries ) > 1 : number_map = { price_entry . amount . number : price_entry for price_entry in price_entries } if len ( number_map ) > 1 : # Note: This should be a list of entries for better error # reporting. (Later.) error_entry = next ( iter ( number_map . values ())) errors . append ( UniquePricesError ( error_entry . meta , \"Disagreeing price entries\" , price_entries ) ) return entries , errors","title":"beancount.plugins"},{"location":"api_reference/beancount.plugins.html#beancountplugins","text":"Example plugins for filtering transactions. These are various examples of how to filter entries in various creative ways. IMPORTANT: These are not meant to be complete features, rather just experiments in problem-solving using Beancount, work-in-progress that can be selectively installed via a --plugin option, or one-offs to answer questions on the mailing-list.","title":"beancount.plugins"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.auto","text":"A plugin of plugins which triggers are all the automatic and lax plugins. In a sense, this is the inverse of \"pedantic.\" This is useful when doing some types of quick and dirty tests. You can just import the \"auto\" plugin. Put that in a macro. Also see: the 'pedantic' plugin.","title":"auto"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.auto_accounts","text":"This module automatically inserts Open directives for accounts not opened (at the date of the first entry) and automatically removes open directives for unused accounts. This can be used as a convenience for doing demos, or when setting up your initial transactions, as an intermediate step.","title":"auto_accounts"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.auto_accounts.auto_insert_open","text":"Insert Open directives for accounts not opened. Open directives are inserted at the date of the first entry. Open directives for unused accounts are removed. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 A parser options dict. Returns: A list of entries, possibly with more Open entries than before, and a list of errors. Source code in beancount/plugins/auto_accounts.py def auto_insert_open ( entries , unused_options_map ): \"\"\"Insert Open directives for accounts not opened. Open directives are inserted at the date of the first entry. Open directives for unused accounts are removed. Args: entries: A list of directives. unused_options_map: A parser options dict. Returns: A list of entries, possibly with more Open entries than before, and a list of errors. \"\"\" opened_accounts = { entry . account for entry in entries if isinstance ( entry , data . Open )} new_entries = [] accounts_first , _ = getters . get_accounts_use_map ( entries ) for index , ( account , date_first_used ) in enumerate ( sorted ( accounts_first . items ())): if account not in opened_accounts : meta = data . new_metadata ( \"\" , index ) new_entries . append ( data . Open ( meta , date_first_used , account , None , None )) if new_entries : new_entries . extend ( entries ) new_entries . sort ( key = data . entry_sortkey ) else : new_entries = entries return new_entries , []","title":"auto_insert_open()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_average_cost","text":"A plugin that ensures cost basis is preserved in unbooked transactions. This is intended to be used in accounts using the \"NONE\" booking method, to manually ensure that the sum total of the cost basis of reducing legs matches the average of what's in the account inventory. This is a partial first step toward implementing the \"AVERAGE\" booking method. In other words, this plugins provides assertions that will constrain you to approximate what the \"AVERAGE\" booking method will do, manually, and not to leak too much cost basis through unmatching bookings without checks. (Note the contrived context here: Ideally the \"NONE\" booking method would simply not exist.)","title":"check_average_cost"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_average_cost.MatchBasisError","text":"MatchBasisError(source, message, entry)","title":"MatchBasisError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_average_cost.MatchBasisError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/check_average_cost.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_average_cost.MatchBasisError.__new__","text":"Create new instance of MatchBasisError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_average_cost.MatchBasisError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/check_average_cost.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_average_cost.validate_average_cost","text":"Check that reducing legs on unbooked postings are near the average cost basis. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config_str \u2013 The configuration as a string version of a float. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_average_cost.py def validate_average_cost ( entries , options_map , config_str = None ): \"\"\"Check that reducing legs on unbooked postings are near the average cost basis. Args: entries: A list of directives. unused_options_map: An options map. config_str: The configuration as a string version of a float. Returns: A list of new errors, if any were found. \"\"\" # Initialize tolerance bounds. if config_str and config_str . strip (): config_obj = eval ( config_str , {}, {}) if not isinstance ( config_obj , float ): raise RuntimeError ( \"Invalid configuration for check_average_cost: \" \"must be a float\" ) tolerance = config_obj else : tolerance = DEFAULT_TOLERANCE min_tolerance = D ( 1 - tolerance ) max_tolerance = D ( 1 + tolerance ) errors = [] ocmap = getters . get_account_open_close ( entries ) balances = collections . defaultdict ( inventory . Inventory ) for entry in entries : if isinstance ( entry , Transaction ): for posting in entry . postings : dopen = ocmap . get ( posting . account , None ) # Only process accounts with a NONE booking value. if dopen and dopen [ 0 ] and dopen [ 0 ] . booking == Booking . NONE : balance = balances [ ( posting . account , posting . units . currency , posting . cost . currency if posting . cost else None , ) ] if posting . units . number < ZERO : average = balance . average () . get_only_position () if average is not None : number = average . cost . number min_valid = number * min_tolerance max_valid = number * max_tolerance if not ( min_valid <= posting . cost . number <= max_valid ): errors . append ( MatchBasisError ( entry . meta , ( \"Cost basis on reducing posting is too far from \" \"the average cost ( {} vs. {} )\" . format ( posting . cost . number , average . cost . number ) ), entry , ) ) balance . add_position ( posting ) return entries , errors","title":"validate_average_cost()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_closing","text":"A plugin that automatically inserts a balance check on a tagged closing posting. Some postings are known to the user to be \"closing trades\", which means that the resulting position of the instrument just after the trade should be zero. For instance, this is the case for most ordinary options trading, only one lot of a particular instrument is held, and eventually expires or gets sold off in its entirely. One would like to confirm that, and the way to do this in Beancount is to insert a balance check. This plugin allows you to do that more simply, by inserting metadata. For example, this transaction: 2018-02-16 * \"SOLD -14 QQQ 100 16 FEB 18 160 CALL @5.31\" Assets:US:Brokerage:Main:Options -1400 QQQ180216C160 {2.70 USD} @ 5.31 USD closing: TRUE Expenses:Financial:Commissions 17.45 USD Expenses:Financial:Fees 0.42 USD Assets:US:Brokerage:Main:Cash 7416.13 USD Income:US:Brokerage:Main:PnL Would expand into the following two directives: 2018-02-16 * \"SOLD -14 QQQ 100 16 FEB 18 160 CALL @5.31\" Assets:US:Brokerage:Main:Options -1400 QQQ180216C160 {2.70 USD} @ 5.31 USD Expenses:Financial:Commissions 17.45 USD Expenses:Financial:Fees 0.42 USD Assets:US:Brokerage:Main:Cash 7416.13 USD Income:US:Brokerage:Main:PnL 2018-02-17 balance Assets:US:Brokerage:Main:Options 0 QQQ180216C160 Insert the closing line when you know you're closing the position.","title":"check_closing"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_closing.check_closing","text":"Expand 'closing' metadata to a zero balance check. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_closing.py def check_closing ( entries , options_map ): \"\"\"Expand 'closing' metadata to a zero balance check. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" new_entries = [] for entry in entries : if isinstance ( entry , data . Transaction ): for i , posting in enumerate ( entry . postings ): if posting . meta and posting . meta . get ( \"closing\" , False ): # Remove the metadata. meta = posting . meta . copy () del meta [ \"closing\" ] posting = posting . _replace ( meta = meta ) entry . postings [ i ] = posting # Insert a balance. date = entry . date + datetime . timedelta ( days = 1 ) balance = data . Balance ( data . new_metadata ( \"\" , 0 ), date , posting . account , amount . Amount ( ZERO , posting . units . currency ), None , None , ) new_entries . append ( balance ) new_entries . append ( entry ) return new_entries , []","title":"check_closing()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity","text":"A plugin that verifies that all seen commodities have a Commodity directive. This is useful if you're a bit pedantic and like to make sure that you're declared attributes for each of the commodities you use. It's useful if you use the portfolio export, for example. You can provide a mapping of (account-regexp, currency-regexp) as options, to specify which commodities to ignore from this plugin selectively. Use this sparingly, as it is an out from the checks that this plugin provides. However, in an active options trading account, a ton of products get inserted and the number of commodity directives can be overwhelming and it's not productive to declare each of the options contracts - names with an embedded strike and expiration date, such as 'SPX_121622P3300' - individually. Note that if a symbol has been ignored in at least one account, it will therefore be further in all Price directives and Metadata values.","title":"check_commodity"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.CheckCommodityError","text":"CheckCommodityError(source, message, entry)","title":"CheckCommodityError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.CheckCommodityError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/check_commodity.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.CheckCommodityError.__new__","text":"Create new instance of CheckCommodityError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.CheckCommodityError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/check_commodity.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.ConfigError","text":"ConfigError(source, message, entry)","title":"ConfigError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.ConfigError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/check_commodity.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.ConfigError.__new__","text":"Create new instance of ConfigError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.ConfigError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/check_commodity.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.get_commodity_map_ex","text":"Find and extract commodities in the stream of directives. Source code in beancount/plugins/check_commodity.py def get_commodity_map_ex ( entries , metadata = False ): \"\"\"Find and extract commodities in the stream of directives.\"\"\" # Find commodity names in metadata. # # TODO(dnicolodi) Unfortunately detecting commodities in metadata # values may result in false positives: common used string are # matched by the regular expression. Revisit this when commodities # will be represented with their own type. ignore = set ([ \"filename\" , \"lineno\" , \"__automatic__\" ]) regexp = re . compile ( CURRENCY_RE ) def currencies_in_meta ( entry ): if entry . meta is not None : for key , value in entry . meta . items (): if isinstance ( value , str ) and key not in ignore : if regexp . fullmatch ( value ): yield value commodities_map = {} occurrences = set () for entry in entries : if isinstance ( entry , data . Commodity ): commodities_map [ entry . currency ] = entry elif isinstance ( entry , data . Open ): if entry . currencies : for currency in entry . currencies : occurrences . add (( entry . account , currency )) elif isinstance ( entry , data . Transaction ): for posting in entry . postings : # Main currency. units = posting . units occurrences . add (( posting . account , units . currency )) # Currency in cost. cost = posting . cost if cost : occurrences . add (( posting . account , cost . currency )) # Currency in price. price = posting . price if price : occurrences . add (( posting . account , price . currency )) # Currency in posting metadata. if metadata : for currency in currencies_in_meta ( posting ): occurrences . add (( posting . account , currency )) elif isinstance ( entry , data . Balance ): occurrences . add (( entry . account , entry . amount . currency )) elif isinstance ( entry , data . Price ): occurrences . add (( PRICE_CONTEXT , entry . currency )) occurrences . add (( PRICE_CONTEXT , entry . amount . currency )) # Entry metadata. if metadata : for currency in currencies_in_meta ( entry ): occurrences . add (( METADATA_CONTEXT , currency )) return occurrences , commodities_map","title":"get_commodity_map_ex()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_commodity.validate_commodity_directives","text":"Find all commodities used and ensure they have a corresponding Commodity directive. Parameters: entries \u2013 A list of directives. options_map \u2013 An options map. config_str \u2013 The configuration as a string version of a float. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_commodity.py def validate_commodity_directives ( entries , options_map , config_str = None ): \"\"\"Find all commodities used and ensure they have a corresponding Commodity directive. Args: entries: A list of directives. options_map: An options map. config_str: The configuration as a string version of a float. Returns: A list of new errors, if any were found. \"\"\" errors = [] if config_str : config_obj = eval ( config_str , {}, {}) if not isinstance ( config_obj , dict ): errors . append ( ConfigError ( data . new_metadata ( \"\" , 0 ), \"Invalid configuration for check_commodity plugin; skipping.\" , None , ) ) return entries , errors else : config_obj = {} # Compile the regular expressions, producing an error if invalid. ignore_map = {} for key , value in config_obj . items (): kv = [] for pattern in key , value : try : kv . append ( re . compile ( pattern ) . match ) except re . error : meta = data . new_metadata ( \"\" , 0 ) errors . append ( CheckCommodityError ( meta , \"Invalid regexp: ' {} ' for {} \" . format ( value , key ), None ) ) if len ( kv ) == 2 : ignore_map [ kv [ 0 ]] = kv [ 1 ] # Get all the occurrences of commodities and a mapping of the directives. # # TODO(blais): Establish a distinction at the parser level for commodities # and strings, so that we can turn detection of them in metadata. occurrences , commodity_map = get_commodity_map_ex ( entries , metadata = False ) # Process all currencies with context. issued = set () ignored = set () anonymous = set () for context , currency in sorted ( occurrences ): if context in ANONYMOUS : anonymous . add (( context , currency )) continue commodity_entry = commodity_map . get ( currency , None ) # Skip if the commodity was declared, or if an error for that commodity # has already been issued. if commodity_entry is not None or currency in issued : continue # If any of the ignore patterns matches, ignore and record ignored. if any ( ( context_re ( context ) and currency_re ( currency )) for context_re , currency_re in ignore_map . items () ): ignored . add ( currency ) continue # Issue error. meta = data . new_metadata ( \"\" , 0 ) errors . append ( CheckCommodityError ( meta , \"Missing Commodity directive for ' {} ' in ' {} '\" . format ( currency , context ), None , ) ) # Process it only once. issued . add ( currency ) # Process all currencies out of context, automatically ignoring those which # have already been issued with account context.. for context , currency in sorted ( anonymous ): commodity_entry = commodity_map . get ( currency , None ) # Skip if (a) the commodity was declared, any of the ignore patterns # matches, or an error for that commodity has already been issued. if commodity_entry is not None or currency in issued or currency in ignored : continue # Issue error. meta = data . new_metadata ( \"\" , 0 ) errors . append ( CheckCommodityError ( meta , \"Missing Commodity directive for ' {} ' in ' {} '\" . format ( currency , context ), None , ) ) return entries , errors","title":"validate_commodity_directives()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_drained","text":"Insert a balance check for zero before balance sheets accounts are closed. For balance sheet accounts with a Close directive (Assets, Liabilities & Equity), insert Balance directives just after its closing date, for all the commodities that have appeared in that account and that are declared as legal on it as well. This performs the equivalent of the following transformation: 2018-02-01 open Assets:Project:Cash USD,CAD ... 2020-02-01 close Assets:Project:Cash !!! to 2018-02-01 open Assets:Project:Cash USD,CAD ... 2020-02-01 close Assets:Project:Cash 2020-02-02 balance Assets:Project:Cash 0 USD 2020-02-02 balance Assets:Project:Cash 0 CAD","title":"check_drained"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.check_drained.check_drained","text":"Check that closed accounts are empty. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/check_drained.py def check_drained ( entries , options_map ): \"\"\"Check that closed accounts are empty. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" acctypes = options . get_account_types ( options_map ) is_covered = functools . partial ( account_types . is_balance_sheet_account , account_types = acctypes ) new_entries = [] currencies = collections . defaultdict ( set ) balances = collections . defaultdict ( set ) for entry in entries : if isinstance ( entry , data . Transaction ): # Accumulate all the currencies seen in each account over time. for posting in entry . postings : if is_covered ( posting . account ): currencies [ posting . account ] . add ( posting . units . currency ) elif isinstance ( entry , data . Open ): # Accumulate all the currencies declared in the account opening. if is_covered ( entry . account ) and entry . currencies : for currency in entry . currencies : currencies [ entry . account ] . add ( currency ) elif isinstance ( entry , data . Balance ): # Ignore balances where directives are present. if is_covered ( entry . account ): balances [ entry . account ] . add (( entry . date , entry . amount . currency )) if isinstance ( entry , data . Close ): if is_covered ( entry . account ): for currency in currencies [ entry . account ]: # Skip balance insertion due to the presence of an explicit one. if ( entry . date , currency ) in balances [ entry . account ]: continue # Insert a balance directive. balance_entry = data . Balance ( # Note: We use the close directive's meta so that # balance errors direct the user to the corresponding # close directive. entry . meta , entry . date + ONE_DAY , entry . account , amount . Amount ( ZERO , currency ), None , None , ) new_entries . append ( balance_entry ) new_entries . append ( entry ) return new_entries , []","title":"check_drained()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.close_tree","text":"This plugin inserts close directives for all of an account's descendants when an account is closed. Unopened parent accounts can also be closed. Any explicitly specified close is left untouched. For example, given this:: 2017-11-10 open Assets:Brokerage:AAPL 2017-11-10 open Assets:Brokerage:ORNG 2018-11-10 close Assets:Brokerage ; this does not necessarily need to be opened the plugin turns it into:: 2017-11-10 open Assets:Brokerage:AAPL 2017-11-10 open Assets:Brokerage:ORNG 2018-11-10 close Assets:Brokerage:AAPL 2018-11-10 close Assets:Brokerage:ORNG Invoke this plugin after any plugins that generate open directives for account trees that you want to auto close. An example is the auto_accounts plugin that ships with Beancount:: plugin \"beancount.plugins.auto_accounts\" plugin \"beancount.plugins.close_tree\"","title":"close_tree"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.close_tree.close_tree","text":"Insert close entries for all subaccounts of a closed account. Parameters: entries \u2013 A list of directives. We're interested only in the Open/Close instances. unused_options_map \u2013 A parser options dict. Returns: A tuple of entries and errors. Source code in beancount/plugins/close_tree.py def close_tree ( entries , unused_options_map ): \"\"\"Insert close entries for all subaccounts of a closed account. Args: entries: A list of directives. We're interested only in the Open/Close instances. unused_options_map: A parser options dict. Returns: A tuple of entries and errors. \"\"\" new_entries = [] errors = [] opens = set ( entry . account for entry in entries if isinstance ( entry , Open )) closes = set ( entry . account for entry in entries if isinstance ( entry , Close )) for entry in entries : if isinstance ( entry , Close ): subaccounts = [ account for account in opens if account . startswith ( entry . account + \":\" ) and account not in closes ] for subacc in subaccounts : meta = data . new_metadata ( \"\" , 0 ) close_entry = data . Close ( meta , entry . date , subacc ) new_entries . append ( close_entry ) # So we don't attempt to re-close a grandchild that a child closed closes . add ( subacc ) if entry . account in opens : new_entries . append ( entry ) else : new_entries . append ( entry ) return new_entries , errors","title":"close_tree()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.coherent_cost","text":"This plugin validates that currencies held at cost aren't ever converted at price and vice-versa. This is usually the case, and using it will prevent users from making the mistake of selling a lot without specifying it via its cost basis.","title":"coherent_cost"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.coherent_cost.CoherentCostError","text":"CoherentCostError(source, message, entry)","title":"CoherentCostError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.coherent_cost.CoherentCostError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/coherent_cost.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.coherent_cost.CoherentCostError.__new__","text":"Create new instance of CoherentCostError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.coherent_cost.CoherentCostError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/coherent_cost.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.coherent_cost.validate_coherent_cost","text":"Check that all currencies are either used at cost or not at all, but never both. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/coherent_cost.py def validate_coherent_cost ( entries , unused_options_map ): \"\"\"Check that all currencies are either used at cost or not at all, but never both. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] with_cost = {} without_cost = {} for entry in data . filter_txns ( entries ): for posting in entry . postings : target_set = without_cost if posting . cost is None else with_cost currency = posting . units . currency target_set . setdefault ( currency , entry ) for currency in set ( with_cost ) & set ( without_cost ): errors . append ( CoherentCostError ( without_cost [ currency ] . meta , \"Currency ' {} ' is used both with and without cost\" . format ( currency ), with_cost [ currency ], ) ) # Note: We really ought to include both of the first transactions here. return entries , errors","title":"validate_coherent_cost()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr","text":"A plugin that asserts that all Commodity directives have a particular attribute and that it is part of a set of enum values. The configuration must be a mapping of attribute name to list of valid values, like this: plugin \"beancount.plugins.commodity_attr\" \"{ 'sector': ['Technology', 'Financials', 'Energy'], 'name': None, }\" The plugin issues an error if a Commodity directive is missing the attribute, or if the attribute value is not in the valid set. If you'd like to just ensure the attribute is set, set the list of valid values to None, as in the 'name' attribute in the example above.","title":"commodity_attr"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.CommodityError","text":"CommodityError(source, message, entry)","title":"CommodityError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.CommodityError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/commodity_attr.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.CommodityError.__new__","text":"Create new instance of CommodityError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.CommodityError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/commodity_attr.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.ConfigError","text":"ConfigError(source, message, entry)","title":"ConfigError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.ConfigError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/commodity_attr.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.ConfigError.__new__","text":"Create new instance of ConfigError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.ConfigError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/commodity_attr.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.commodity_attr.validate_commodity_attr","text":"Check that all Commodity directives have a valid attribute. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config_str \u2013 A configuration string. Returns: A list of new errors, if any were found. Source code in beancount/plugins/commodity_attr.py def validate_commodity_attr ( entries , unused_options_map , config_str ): \"\"\"Check that all Commodity directives have a valid attribute. Args: entries: A list of directives. unused_options_map: An options map. config_str: A configuration string. Returns: A list of new errors, if any were found. \"\"\" errors = [] config_obj = eval ( config_str , {}, {}) if not isinstance ( config_obj , dict ): errors . append ( ConfigError ( data . new_metadata ( \"\" , 0 ), \"Invalid configuration for commodity_attr plugin; skipping.\" , None , ) ) return entries , errors validmap = { attr : frozenset ( values ) if values is not None else None for attr , values in config_obj . items () } for entry in entries : if not isinstance ( entry , data . Commodity ): continue for attr , values in validmap . items (): value = entry . meta . get ( attr , None ) if value is None : errors . append ( CommodityError ( entry . meta , \"Missing attribute ' {} ' for Commodity directive {} \" . format ( attr , entry . currency ), None , ) ) continue if values and value not in values : errors . append ( CommodityError ( entry . meta , \"Invalid value ' {} ' for attribute {} , Commodity\" . format ( value , attr ) + \" directive {} ; valid options: {} \" . format ( entry . currency , \", \" . join ( values ) ), None , ) ) return entries , errors","title":"validate_commodity_attr()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.currency_accounts","text":"An implementation of currency accounts. This is an automatic implementation of the method described here: https://www.mathstat.dal.ca/~selinger/accounting/tutorial.html You enable it just like this: plugin \"beancount.plugins.currency_accounts\" \"Equity:CurrencyAccounts\" Accounts will be automatically created under the given base account, with the currency name appended to it, e.g., Equity:CurrencyAccounts:CAD Equity:CurrencyAccounts:USD etc., where used. You can have a look at the account balances with a query like this: bean-query $L \"select account, sum(position), convert(sum(position), 'USD') where date >= 2018-01-01 and account ~ 'CurrencyAccounts' \" The sum total of the converted amounts should be a number not too large: bean-query $L \"select convert(sum(position), 'USD') where date >= 2018-01-01 and account ~ 'CurrencyAccounts'\" WARNING: This is a prototype. Note the FIXMEs in the code below, which indicate some potential problems.","title":"currency_accounts"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.currency_accounts.get_neutralizing_postings","text":"Process an entry. Parameters: curmap \u2013 A dict of currency to a list of Postings of this transaction. base_account \u2013 A string, the root account name to insert. new_accounts \u2013 A set, a mutable accumulator of new account names. Returns: A modified entry, with new postings inserted to rebalance currency trading accounts. Source code in beancount/plugins/currency_accounts.py def get_neutralizing_postings ( curmap , base_account , new_accounts ): \"\"\"Process an entry. Args: curmap: A dict of currency to a list of Postings of this transaction. base_account: A string, the root account name to insert. new_accounts: A set, a mutable accumulator of new account names. Returns: A modified entry, with new postings inserted to rebalance currency trading accounts. \"\"\" new_postings = [] for currency , postings in curmap . items (): # Compute the per-currency balance. inv = inventory . Inventory () for posting in postings : inv . add_amount ( convert . get_cost ( posting )) if inv . is_empty (): new_postings . extend ( postings ) continue # Re-insert original postings and remove price conversions. # # Note: This may cause problems if the implicit_prices plugin is # configured to run after this one, or if you need the price annotations # for some scripting or serious work. # # FIXME: We need to handle these important cases (they're not frivolous, # this is a prototype), probably by inserting some exceptions with # collaborating code in the booking (e.g. insert some metadata that # disables price conversions on those postings). # # FIXME(2): Ouch! Some of the residual seeps through here, where there # are more than a single currency block. This needs fixing too. You can # easily mitigate some of this to some extent, by excluding transactions # which don't have any price conversion in them. for pos in postings : if pos . price is not None : pos = pos . _replace ( price = None ) new_postings . append ( pos ) # Insert the currency trading accounts postings. amount = inv . get_only_position () . units acc = account . join ( base_account , currency ) new_accounts . add ( acc ) new_postings . append ( Posting ( acc , - amount , None , None , None , None )) return new_postings","title":"get_neutralizing_postings()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.currency_accounts.group_postings_by_weight_currency","text":"Return where this entry might require adjustment. Source code in beancount/plugins/currency_accounts.py def group_postings_by_weight_currency ( entry : Transaction ): \"\"\"Return where this entry might require adjustment.\"\"\" curmap = collections . defaultdict ( list ) has_price = False for posting in entry . postings : currency = posting . units . currency if posting . cost : currency = posting . cost . currency if posting . price : assert posting . price . currency == currency elif posting . price : has_price = True currency = posting . price . currency if posting . price : has_price = True curmap [ currency ] . append ( posting ) return curmap , has_price","title":"group_postings_by_weight_currency()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.currency_accounts.insert_currency_trading_postings","text":"Insert currency trading postings. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config \u2013 The base account name for currency trading accounts. Returns: A list of new errors, if any were found. Source code in beancount/plugins/currency_accounts.py 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 ( \"\" , index ), earliest_date , acc , None , None ) for index , acc in enumerate ( sorted ( new_accounts )) ] return open_entries + new_entries , errors","title":"insert_currency_trading_postings()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.implicit_prices","text":"This plugin synthesizes Price directives for all Postings with a price or directive or if it is an augmenting posting, has a cost directive.","title":"implicit_prices"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.implicit_prices.ImplicitPriceError","text":"ImplicitPriceError(source, message, entry)","title":"ImplicitPriceError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.implicit_prices.ImplicitPriceError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/implicit_prices.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.implicit_prices.ImplicitPriceError.__new__","text":"Create new instance of ImplicitPriceError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.implicit_prices.ImplicitPriceError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/implicit_prices.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.implicit_prices.add_implicit_prices","text":"Insert implicitly defined prices from Transactions. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. Source code in beancount/plugins/implicit_prices.py def add_implicit_prices ( entries , unused_options_map ): \"\"\"Insert implicitly defined prices from Transactions. Explicit price entries are simply maintained in the output list. Prices from postings with costs or with prices from Transaction entries are synthesized as new Price entries in the list of entries output. Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. Returns: A list of entries, possibly with more Price entries than before, and a list of errors. \"\"\" new_entries = [] errors = [] # A dict of (date, currency, cost-currency) to price entry. new_price_entry_map = {} balances = collections . defaultdict ( inventory . Inventory ) for entry in entries : # Always replicate the existing entries. new_entries . append ( entry ) if isinstance ( entry , Transaction ): # Inspect all the postings in the transaction. for posting in entry . postings : units = posting . units cost = posting . cost # Check if the position is matching against an existing # position. _ , booking = balances [ posting . account ] . add_position ( posting ) # Add prices when they're explicitly specified on a posting. An # explicitly specified price may occur in a conversion, e.g. # Assets:Account 100 USD @ 1.10 CAD # or, if a cost is also specified, as the current price of the # underlying instrument, e.g. # Assets:Account 100 HOOL {564.20} @ {581.97} USD if posting . price is not None : meta = data . new_metadata ( entry . meta [ \"filename\" ], entry . meta [ \"lineno\" ]) meta [ METADATA_FIELD ] = \"from_price\" price_entry = data . Price ( meta , entry . date , units . currency , posting . price ) # Add costs, when we're not matching against an existing # position. This happens when we're just specifying the cost, # e.g. # Assets:Account 100 HOOL {564.20} elif cost is not None and booking != inventory . MatchResult . REDUCED : # TODO(blais): What happens here if the account has no # booking strategy? Do we end up inserting a price for the # reducing leg? Check. meta = data . new_metadata ( entry . meta [ \"filename\" ], entry . meta [ \"lineno\" ]) meta [ METADATA_FIELD ] = \"from_cost\" price_entry = data . Price ( meta , entry . date , units . currency , amount . Amount ( cost . number , cost . currency ), ) else : price_entry = None if price_entry is not None : key = ( price_entry . date , price_entry . currency , price_entry . amount . number , # Ideally should be removed. price_entry . amount . currency , ) try : new_price_entry_map [ key ] ## Do not fail for now. We still have many valid use ## cases of duplicate prices on the same date, for ## example, stock splits, or trades on two dates with ## two separate reported prices. We need to figure out a ## more elegant solution for this in the long term. ## Keeping both for now. We should ideally not use the ## number in the de-dup key above. # # dup_entry = new_price_entry_map[key] # if price_entry.amount.number == dup_entry.amount.number: # # Skip duplicates. # continue # else: # errors.append( # ImplicitPriceError( # entry.meta, # \"Duplicate prices for {} on {}\".format(entry, # dup_entry), # entry)) except KeyError : new_price_entry_map [ key ] = price_entry new_entries . append ( price_entry ) return new_entries , errors","title":"add_implicit_prices()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.leafonly","text":"A plugin that issues errors when amounts are posted to non-leaf accounts, that is, accounts with child accounts. This is an extra constraint that you may want to apply optionally. If you install this plugin, it will issue errors for all accounts that have postings to non-leaf accounts. Some users may want to disallow this and enforce that only leaf accounts may have postings on them.","title":"leafonly"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.leafonly.LeafOnlyError","text":"LeafOnlyError(source, message, entry)","title":"LeafOnlyError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.leafonly.LeafOnlyError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/leafonly.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.leafonly.LeafOnlyError.__new__","text":"Create new instance of LeafOnlyError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.leafonly.LeafOnlyError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/leafonly.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.leafonly.validate_leaf_only","text":"Check for non-leaf accounts that have postings on them. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/leafonly.py def validate_leaf_only ( entries , unused_options_map ): \"\"\"Check for non-leaf accounts that have postings on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" real_root = realization . realize ( entries , compute_balance = False ) default_meta = data . new_metadata ( \"\" , 0 ) open_close_map = None # Lazily computed. errors = [] for real_account in realization . iter_children ( real_root ): if len ( real_account ) > 0 and real_account . txn_postings : if open_close_map is None : open_close_map = getters . get_account_open_close ( entries ) try : open_entry = open_close_map [ real_account . account ][ 0 ] except KeyError : open_entry = None errors . append ( LeafOnlyError ( open_entry . meta if open_entry else default_meta , \"Non-leaf account ' {} ' has postings on it\" . format ( real_account . account ), open_entry , ) ) return entries , errors","title":"validate_leaf_only()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.noduplicates","text":"This plugin validates that there are no duplicate transactions.","title":"noduplicates"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.noduplicates.validate_no_duplicates","text":"Check that the entries are unique, by computing hashes. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/noduplicates.py def validate_no_duplicates ( entries , unused_options_map ): \"\"\"Check that the entries are unique, by computing hashes. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" unused_hashes , errors = compare . hash_entries ( entries , exclude_meta = True ) return entries , errors","title":"validate_no_duplicates()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.nounused","text":"This plugin validates that there are no unused accounts.","title":"nounused"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.nounused.UnusedAccountError","text":"UnusedAccountError(source, message, entry)","title":"UnusedAccountError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.nounused.UnusedAccountError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/nounused.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.nounused.UnusedAccountError.__new__","text":"Create new instance of UnusedAccountError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.nounused.UnusedAccountError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/nounused.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.nounused.validate_unused_accounts","text":"Check that all accounts declared open are actually used. We check that all of the accounts that are open are at least referred to by another directive. These are probably unused, so issue a warning (we like to be pedantic). Note that an account that is open and then closed is considered used--this is a valid use case that may occur in reality. If you have a use case for an account to be open but never used, you can quiet that warning by initializing the account with a balance asserts or a pad directive, or even use a note will be sufficient. (This is probably a good candidate for optional inclusion as a \"pedantic\" plugin.) Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/nounused.py def validate_unused_accounts ( entries , unused_options_map ): \"\"\"Check that all accounts declared open are actually used. We check that all of the accounts that are open are at least referred to by another directive. These are probably unused, so issue a warning (we like to be pedantic). Note that an account that is open and then closed is considered used--this is a valid use case that may occur in reality. If you have a use case for an account to be open but never used, you can quiet that warning by initializing the account with a balance asserts or a pad directive, or even use a note will be sufficient. (This is probably a good candidate for optional inclusion as a \"pedantic\" plugin.) Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" # Find all the accounts referenced by entries which are not Open, and the # open directives for error reporting below. open_map = {} referenced_accounts = set () for entry in entries : if isinstance ( entry , data . Open ): open_map [ entry . account ] = entry continue referenced_accounts . update ( getters . get_entry_accounts ( entry )) # Create a list of suitable errors, with the location of the Open directives # corresponding to the unused accounts. errors = [ UnusedAccountError ( open_entry . meta , \"Unused account ' {} '\" . format ( account ), open_entry ) for account , open_entry in open_map . items () if account not in referenced_accounts ] return entries , errors","title":"validate_unused_accounts()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.onecommodity","text":"A plugin that issues errors when more than one commodity is used in an account. For investments or trading accounts, it can make it easier to filter the action around a single stock by using the name of the stock as the leaf of the account name. Notes: The plugin will automatically skip accounts that have explicitly declared commodities in their Open directive. You can also set the metadata \"onecommodity: FALSE\" on an account's Open directive to skip the checks for that account. If provided, the configuration should be a regular expression restricting the set of accounts to check.","title":"onecommodity"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.onecommodity.OneCommodityError","text":"OneCommodityError(source, message, entry)","title":"OneCommodityError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.onecommodity.OneCommodityError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/onecommodity.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.onecommodity.OneCommodityError.__new__","text":"Create new instance of OneCommodityError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.onecommodity.OneCommodityError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/onecommodity.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.onecommodity.validate_one_commodity","text":"Check that each account has units in only a single commodity. This is an extra constraint that you may want to apply optionally, despite Beancount's ability to support inventories and aggregations with more than one commodity. I believe this also matches GnuCash's model, where each account has a single commodity attached to it. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. config \u2013 The plugin configuration string, a regular expression to match against the subset of accounts to check. Returns: A list of new errors, if any were found. Source code in beancount/plugins/onecommodity.py def validate_one_commodity ( entries , unused_options_map , config = None ): \"\"\"Check that each account has units in only a single commodity. This is an extra constraint that you may want to apply optionally, despite Beancount's ability to support inventories and aggregations with more than one commodity. I believe this also matches GnuCash's model, where each account has a single commodity attached to it. Args: entries: A list of directives. unused_options_map: An options map. config: The plugin configuration string, a regular expression to match against the subset of accounts to check. Returns: A list of new errors, if any were found. \"\"\" accounts_re = re . compile ( config ) if config else None # Mappings of account name to lists of currencies for each units and cost. units_map = collections . defaultdict ( set ) cost_map = collections . defaultdict ( set ) # Mappings to use just for getting a relevant source. units_source_map = {} cost_source_map = {} # Gather the set of accounts to skip from the Open directives. skip_accounts = set () for entry in entries : if not isinstance ( entry , data . Open ): continue if ( not entry . meta . get ( \"onecommodity\" , True ) or ( accounts_re and not accounts_re . match ( entry . account )) or ( entry . currencies and len ( entry . currencies ) > 1 ) ): skip_accounts . add ( entry . account ) # Accumulate all the commodities used. for entry in entries : if isinstance ( entry , data . Transaction ): for posting in entry . postings : if posting . account in skip_accounts : continue units = posting . units units_map [ posting . account ] . add ( units . currency ) if len ( units_map [ posting . account ]) > 1 : units_source_map [ posting . account ] = entry cost = posting . cost if cost : cost_map [ posting . account ] . add ( cost . currency ) if len ( cost_map [ posting . account ]) > 1 : cost_source_map [ posting . account ] = entry elif isinstance ( entry , data . Balance ): if entry . account in skip_accounts : continue units_map [ entry . account ] . add ( entry . amount . currency ) if len ( units_map [ entry . account ]) > 1 : units_source_map [ entry . account ] = entry elif isinstance ( entry , data . Open ): if entry . currencies and len ( entry . currencies ) > 1 : skip_accounts . add ( entry . account ) # Check units. errors = [] for account , currencies in units_map . items (): if account in skip_accounts : continue if len ( currencies ) > 1 : errors . append ( OneCommodityError ( units_source_map [ account ] . meta , \"More than one currency in account ' {} ': {} \" . format ( account , \",\" . join ( currencies ) ), None , ) ) # Check costs. for account , currencies in cost_map . items (): if account in skip_accounts : continue if len ( currencies ) > 1 : errors . append ( OneCommodityError ( cost_source_map [ account ] . meta , \"More than one cost currency in account ' {} ': {} \" . format ( account , \",\" . join ( currencies ) ), None , ) ) return entries , errors","title":"validate_one_commodity()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.pedantic","text":"A plugin of plugins which triggers all the pedantic plugins.","title":"pedantic"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.sellgains","text":"A plugin that cross-checks declared gains against prices on lot sales. When you sell stock, the gains can be automatically implied by the corresponding cash amounts. For example, in the following transaction the 2nd and 3rd postings should match the value of the stock sold: 1999-07-31 * \"Sell\" Assets:US:BRS:Company:ESPP -81 ADSK {26.3125 USD} Assets:US:BRS:Company:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:US:Company:ESPP:PnL -10.125 USD The cost basis is checked against: 2141.36 + 008 + -10.125. That is, the balance checks computes -81 x 26.3125 = -2131.3125 + 2141.36 + 0.08 + -10.125 and checks that the residual is below a small tolerance. But... usually the income leg isn't given to you in statements. Beancount can automatically infer it using the balance, which is convenient, like this: 1999-07-31 * \"Sell\" Assets:US:BRS:Company:ESPP -81 ADSK {26.3125 USD} Assets:US:BRS:Company:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:US:Company:ESPP:PnL Additionally, most often you have the sales prices given to you on your transaction confirmation statement, so you can enter this: 1999-07-31 * \"Sell\" Assets:US:BRS:Company:ESPP -81 ADSK {26.3125 USD} @ 26.4375 USD Assets:US:BRS:Company:Cash 2141.36 USD Expenses:Financial:Fees 0.08 USD Income:US:Company:ESPP:PnL So in theory, if the price is given (26.4375 USD), we could verify that the proceeds from the sale at the given price match non-Income postings. That is, verify that -81 x 26.4375 = -2141.4375 + 2141.36 + 0.08 + is below a small tolerance value. So this plugin does this. In general terms, it does the following: For transactions with postings that have a cost and a price, it verifies that the sum of the positions on all postings to non-income accounts is below tolerance. This provides yet another level of verification and allows you to elide the income amounts, knowing that the price is there to provide an extra level of error-checking in case you enter a typo.","title":"sellgains"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.sellgains.SellGainsError","text":"SellGainsError(source, message, entry)","title":"SellGainsError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.sellgains.SellGainsError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/sellgains.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.sellgains.SellGainsError.__new__","text":"Create new instance of SellGainsError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.sellgains.SellGainsError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/sellgains.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.sellgains.validate_sell_gains","text":"Check the sum of asset account totals for lots sold with a price on them. Parameters: entries \u2013 A list of directives. unused_options_map \u2013 An options map. Returns: A list of new errors, if any were found. Source code in beancount/plugins/sellgains.py def validate_sell_gains ( entries , options_map ): \"\"\"Check the sum of asset account totals for lots sold with a price on them. Args: entries: A list of directives. unused_options_map: An options map. Returns: A list of new errors, if any were found. \"\"\" errors = [] acc_types = options . get_account_types ( options_map ) proceed_types = set ( [ acc_types . assets , acc_types . liabilities , acc_types . equity , acc_types . expenses ] ) for entry in entries : if not isinstance ( entry , data . Transaction ): continue # Find transactions whose lots at cost all have a price. postings_at_cost = [ posting for posting in entry . postings if posting . cost is not None ] if not postings_at_cost or not all ( posting . price is not None for posting in postings_at_cost ): continue # Accumulate the total expected proceeds and the sum of the asset and # expenses legs. total_price = inventory . Inventory () total_proceeds = inventory . Inventory () for posting in entry . postings : # If the posting is held at cost, add the priced value to the balance. if posting . cost is not None : assert posting . price is not None price = posting . price total_price . add_amount ( amount . mul ( price , - posting . units . number )) else : # Otherwise, use the weight and ignore postings to Income accounts. atype = account_types . get_account_type ( posting . account ) if atype in proceed_types : total_proceeds . add_amount ( convert . get_weight ( posting )) # Compare inventories, currency by currency. dict_price = { pos . units . currency : pos . units . number for pos in total_price } dict_proceeds = { pos . units . currency : pos . units . number for pos in total_proceeds } tolerances = interpolate . infer_tolerances ( entry . postings , options_map ) invalid = False for currency , price_number in dict_price . items (): # Accept a looser than usual tolerance because rounding occurs # differently. Also, it would be difficult for the user to satisfy # two sets of constraints manually. tolerance = tolerances . get ( currency ) * EXTRA_TOLERANCE_MULTIPLIER proceeds_number = dict_proceeds . pop ( currency , ZERO ) diff = abs ( price_number - proceeds_number ) if diff > tolerance : invalid = True break if invalid or dict_proceeds : errors . append ( SellGainsError ( entry . meta , \"Invalid price vs. proceeds/gains: {} vs. {} ; difference: {} \" . format ( total_price , total_proceeds , ( total_price + - total_proceeds ) ), entry , ) ) return entries , errors","title":"validate_sell_gains()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unique_prices","text":"This module adds validation that there is a single price defined per date and base/quote currencies. If multiple conflicting price values are declared, an error is generated. Note that multiple price entries with the same number do not generate an error. This is meant to be turned on if you want to use a very strict mode for entering prices, and may not be realistic usage. For example, if you have (1) a transaction with an implicitly generated price during the day (from its cost) and (2) a separate explicit price directive that declares a different price for the day's closing price, this would generate an error. I'm not certain this will be useful in the long run, so placing it in a plugin.","title":"unique_prices"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unique_prices.UniquePricesError","text":"UniquePricesError(source, message, entry)","title":"UniquePricesError"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unique_prices.UniquePricesError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/plugins/unique_prices.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unique_prices.UniquePricesError.__new__","text":"Create new instance of UniquePricesError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unique_prices.UniquePricesError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/plugins/unique_prices.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.plugins.html#beancount.plugins.unique_prices.validate_unique_prices","text":"Check that there is only a single price per day for a particular base/quote. Parameters: entries \u2013 A list of directives. We're interested only in the Transaction instances. unused_options_map \u2013 A parser options dict. Returns: The list of input entries, and a list of new UniquePricesError instances generated. Source code in beancount/plugins/unique_prices.py def validate_unique_prices ( entries , unused_options_map ): \"\"\"Check that there is only a single price per day for a particular base/quote. Args: entries: A list of directives. We're interested only in the Transaction instances. unused_options_map: A parser options dict. Returns: The list of input entries, and a list of new UniquePricesError instances generated. \"\"\" errors = [] prices = collections . defaultdict ( list ) for entry in entries : if not isinstance ( entry , data . Price ): continue key = ( entry . date , entry . currency , entry . amount . currency ) prices [ key ] . append ( entry ) errors = [] for price_entries in prices . values (): if len ( price_entries ) > 1 : number_map = { price_entry . amount . number : price_entry for price_entry in price_entries } if len ( number_map ) > 1 : # Note: This should be a list of entries for better error # reporting. (Later.) error_entry = next ( iter ( number_map . values ())) errors . append ( UniquePricesError ( error_entry . meta , \"Disagreeing price entries\" , price_entries ) ) return entries , errors","title":"validate_unique_prices()"},{"location":"api_reference/beancount.scripts.html","text":"beancount.scripts \uf0c1 Implementation of the various scripts available from bin. This is structured this way because we want all the significant codes under a single directory, for analysis, grepping and unit testing. beancount.scripts.deps \uf0c1 Check the installation dependencies and report the version numbers of each. This is meant to be used as an error diagnostic tool. beancount . scripts . deps . check_cdecimal () \uf0c1 Check that Python 3.3 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_cdecimal (): \"\"\"Check that Python 3.3 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" # Note: this code mirrors and should be kept in-sync with that at the top of # beancount.core.number. # Try the built-in installation. import decimal if is_fast_decimal ( decimal ): return ( \"cdecimal\" , \" {} (built-in)\" . format ( decimal . __version__ ), True ) # Try an explicitly installed version. try : import cdecimal if is_fast_decimal ( cdecimal ): return ( \"cdecimal\" , getattr ( cdecimal , \"__version__\" , \"OKAY\" ), True ) except ImportError : pass # Not found. return ( \"cdecimal\" , None , False ) beancount . scripts . deps . check_dependencies () \uf0c1 Check the runtime dependencies and report their version numbers. Returns: A list of pairs of (package-name, version-number, sufficient) whereby if a package has not been installed, its 'version-number' will be set to None. Otherwise, it will be a string with the version number in it. 'sufficient' will be True if the version if sufficient for this installation of Beancount. Source code in beancount/scripts/deps.py def check_dependencies (): \"\"\"Check the runtime dependencies and report their version numbers. Returns: A list of pairs of (package-name, version-number, sufficient) whereby if a package has not been installed, its 'version-number' will be set to None. Otherwise, it will be a string with the version number in it. 'sufficient' will be True if the version if sufficient for this installation of Beancount. \"\"\" return [ check_python (), check_cdecimal (), check_import ( \"dateutil\" ), ] beancount . scripts . deps . check_import ( package_name , min_version = None , module_name = None ) \uf0c1 Check that a particular module name is installed. Parameters: package_name \u2013 A string, the name of the package and module to be imported to verify this works. This should have a version attribute on it. min_version \u2013 If not None, a string, the minimum version number we require. module_name \u2013 The name of the module to import if it differs from the package name. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_import ( package_name , min_version = None , module_name = None ): \"\"\"Check that a particular module name is installed. Args: package_name: A string, the name of the package and module to be imported to verify this works. This should have a __version__ attribute on it. min_version: If not None, a string, the minimum version number we require. module_name: The name of the module to import if it differs from the package name. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" if module_name is None : module_name = package_name try : __import__ ( module_name ) module = sys . modules [ module_name ] if min_version is not None : version = module . __version__ assert isinstance ( version , str ) is_sufficient = ( parse_version ( version ) >= parse_version ( min_version ) if min_version else True ) else : version , is_sufficient = None , True except ImportError : version , is_sufficient = None , False return ( package_name , version , is_sufficient ) beancount . scripts . deps . check_python () \uf0c1 Check that Python 3.7 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_python (): \"\"\"Check that Python 3.7 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" return ( \"python3\" , \".\" . join ( map ( str , sys . version_info [: 3 ])), sys . version_info [: 2 ] >= ( 3 , 7 ), ) beancount . scripts . deps . check_python_magic () \uf0c1 Check that a recent-enough version of python-magic is installed. python-magic is an interface to libmagic, which is used by the 'file' tool and UNIX to identify file types. Note that there are two Python wrappers which provide the 'magic' import: python-magic and filemagic. The former is what we need, which appears to be more recently maintained. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_python_magic (): \"\"\"Check that a recent-enough version of python-magic is installed. python-magic is an interface to libmagic, which is used by the 'file' tool and UNIX to identify file types. Note that there are two Python wrappers which provide the 'magic' import: python-magic and filemagic. The former is what we need, which appears to be more recently maintained. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" try : import magic # Check that python-magic and not filemagic is installed. if not hasattr ( magic , \"from_file\" ): # 'filemagic' is installed; install python-magic. raise ImportError return ( \"python-magic\" , \"OK\" , True ) except ( ImportError , OSError ): return ( \"python-magic\" , None , False ) beancount . scripts . deps . is_fast_decimal ( decimal_module ) \uf0c1 Return true if a fast C decimal implementation is installed. Source code in beancount/scripts/deps.py def is_fast_decimal ( decimal_module ): \"Return true if a fast C decimal implementation is installed.\" return isinstance ( decimal_module . Decimal () . sqrt , types . BuiltinFunctionType ) beancount . scripts . deps . list_dependencies ( file =< _io . TextIOWrapper name = '' mode = 'w' encoding = 'utf-8' > ) \uf0c1 Check the dependencies and produce a listing on the given file. Parameters: file \u2013 A file object to write the output to. Source code in beancount/scripts/deps.py def list_dependencies ( file = sys . stderr ): \"\"\"Check the dependencies and produce a listing on the given file. Args: file: A file object to write the output to. \"\"\" print ( \"Dependencies:\" ) for package , version , sufficient in check_dependencies (): print ( \" {:16} : {} {} \" . format ( package , version or \"NOT INSTALLED\" , \"(INSUFFICIENT)\" if version and not sufficient else \"\" , ), file = file , ) beancount . scripts . deps . parse_version ( version_str ) \uf0c1 Parse the version string into a comparable tuple. Source code in beancount/scripts/deps.py def parse_version ( version_str : str ) -> str : \"\"\"Parse the version string into a comparable tuple.\"\"\" return [ int ( v ) for v in version_str . split ( \".\" )] beancount.scripts.directories \uf0c1 Check that document directories mirror a list of accounts correctly. beancount.scripts.directories.ValidateDirectoryError ( Exception ) \uf0c1 A directory validation error. beancount . scripts . directories . validate_directories ( entries , document_dirs ) \uf0c1 Validate a directory hierarchy against a ledger's account names. Read a ledger's list of account names and check that all the capitalized subdirectory names under the given roots match the account names. Parameters: entries \u2013 A list of directives. document_dirs \u2013 A list of string, the directory roots to walk and validate. Source code in beancount/scripts/directories.py def validate_directories ( entries , document_dirs ): \"\"\"Validate a directory hierarchy against a ledger's account names. Read a ledger's list of account names and check that all the capitalized subdirectory names under the given roots match the account names. Args: entries: A list of directives. document_dirs: A list of string, the directory roots to walk and validate. \"\"\" # Get the list of accounts declared in the ledge. accounts = getters . get_accounts ( entries ) # For each of the roots, validate the hierarchy of directories. for document_dir in document_dirs : errors = validate_directory ( accounts , document_dir ) for error in errors : print ( \"ERROR: {} \" . format ( error )) beancount . scripts . directories . validate_directory ( accounts , document_dir ) \uf0c1 Check a directory hierarchy against a list of valid accounts. Walk the directory hierarchy, and for all directories with names matching that of accounts (with \":\" replaced with \"/\"), check that they refer to an account name declared in the given list. Parameters: account \u2013 A set or dict of account names. document_dir \u2013 A string, the root directory to walk and validate. Returns: An errors for each invalid directory name found. Source code in beancount/scripts/directories.py def validate_directory ( accounts , document_dir ): \"\"\"Check a directory hierarchy against a list of valid accounts. Walk the directory hierarchy, and for all directories with names matching that of accounts (with \":\" replaced with \"/\"), check that they refer to an account name declared in the given list. Args: account: A set or dict of account names. document_dir: A string, the root directory to walk and validate. Returns: An errors for each invalid directory name found. \"\"\" # Generate all parent accounts in the account_set we're checking against, so # that parent directories with no corresponding account don't warn. accounts_with_parents = set ( accounts ) for account_ in accounts : while True : parent = account . parent ( account_ ) if not parent : break if parent in accounts_with_parents : break accounts_with_parents . add ( parent ) account_ = parent errors = [] for directory , account_name , _ , _ in account . walk ( document_dir ): if account_name not in accounts_with_parents : errors . append ( ValidateDirectoryError ( \"Invalid directory ' {} ': no corresponding account ' {} '\" . format ( directory , account_name ) ) ) return errors beancount.scripts.doctor \uf0c1 Debugging tool for those finding bugs in Beancount. This tool is able to dump lexer/parser state, and will provide other services in the name of debugging. beancount.scripts.doctor.FileLocation ( ParamType ) \uf0c1 beancount . scripts . doctor . FileLocation . convert ( self , value , param , ctx ) \uf0c1 Convert the value to the correct type. This is not called if the value is None (the missing value). This must accept string values from the command line, as well as values that are already the correct type. It may also convert other compatible types. The param and ctx arguments may be None in certain situations, such as when converting prompt input. If the value cannot be converted, call :meth: fail with a descriptive message. :param value: The value to convert. :param param: The parameter that is using this type to convert its value. May be None . :param ctx: The current context that arrived at this value. May be None . Source code in beancount/scripts/doctor.py def convert ( self , value , param , ctx ): match = re . match ( r \"(?:(.+):)?(\\d+)$\" , value ) if not match : self . fail ( \" {!r} is not a valid location\" . format ( value ), param , ctx ) filename , lineno = match . groups () if filename : filename = os . path . abspath ( filename ) return filename , int ( lineno ) beancount.scripts.doctor.FileRegion ( ParamType ) \uf0c1 beancount . scripts . doctor . FileRegion . convert ( self , value , param , ctx ) \uf0c1 Convert the value to the correct type. This is not called if the value is None (the missing value). This must accept string values from the command line, as well as values that are already the correct type. It may also convert other compatible types. The param and ctx arguments may be None in certain situations, such as when converting prompt input. If the value cannot be converted, call :meth: fail with a descriptive message. :param value: The value to convert. :param param: The parameter that is using this type to convert its value. May be None . :param ctx: The current context that arrived at this value. May be None . Source code in beancount/scripts/doctor.py def convert ( self , value , param , ctx ): match = re . match ( r \"(?:(.+):)?(\\d+):(\\d+)$\" , value ) if not match : self . fail ( \" {!r} is not a valid region\" . format ( value ), param , ctx ) filename , start_lineno , end_lineno = match . groups () if filename : filename = os . path . abspath ( filename ) return filename , int ( start_lineno ), int ( end_lineno ) beancount.scripts.doctor.Group ( Group ) \uf0c1 beancount . scripts . doctor . Group . command ( self , * args , * , alias = None , ** kwargs ) \uf0c1 A shortcut decorator for declaring and attaching a command to the group. This takes the same arguments as :func: command and immediately registers the created command with this group by calling :meth: add_command . To customize the command class used, set the :attr: command_class attribute. .. versionchanged:: 8.1 This decorator can be applied without parentheses. .. versionchanged:: 8.0 Added the :attr: command_class attribute. Source code in beancount/scripts/doctor.py def command ( self , * args , alias = None , ** kwargs ): wrap = click . Group . command ( self , * args , ** kwargs ) def decorator ( f ): cmd = wrap ( f ) if alias : self . aliases [ alias ] = cmd . name return cmd return decorator beancount . scripts . doctor . Group . get_command ( self , ctx , cmd_name ) \uf0c1 Given a context and a command name, this returns a :class: Command object if it exists or returns None . Source code in beancount/scripts/doctor.py def get_command ( self , ctx , cmd_name ): # aliases name = self . aliases . get ( cmd_name , cmd_name ) # allow to use '_' or '-' in command names. name = name . replace ( \"_\" , \"-\" ) return click . Group . get_command ( self , ctx , name ) beancount.scripts.doctor.RenderError ( tuple ) \uf0c1 RenderError(source, message, entry) beancount . scripts . doctor . RenderError . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/doctor.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . scripts . doctor . RenderError . __new__ ( _cls , source , message , entry ) special staticmethod \uf0c1 Create new instance of RenderError(source, message, entry) beancount . scripts . doctor . RenderError . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/scripts/doctor.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . scripts . doctor . find_linked_entries ( entries , links , follow_links ) \uf0c1 Find all linked entries. Note that there is an option here: You can either just look at the links on the closest entry, or you can include the links of the linked transactions as well. Whichever one you want depends on how you use your links. Best would be to query the user (in Emacs) when there are many links present. Source code in beancount/scripts/doctor.py def find_linked_entries ( entries , links , follow_links : bool ): \"\"\"Find all linked entries. Note that there is an option here: You can either just look at the links on the closest entry, or you can include the links of the linked transactions as well. Whichever one you want depends on how you use your links. Best would be to query the user (in Emacs) when there are many links present. \"\"\" linked_entries = [] if not follow_links : linked_entries = [ entry for entry in entries if ( isinstance ( entry , data . Transaction ) and entry . links and entry . links & links ) ] else : links = set ( links ) linked_entries = [] while True : num_linked = len ( linked_entries ) linked_entries = [ entry for entry in entries if ( isinstance ( entry , data . Transaction ) and entry . links and entry . links & links ) ] if len ( linked_entries ) == num_linked : break for entry in linked_entries : if entry . links : links . update ( entry . links ) return linked_entries beancount . scripts . doctor . find_tagged_entries ( entries , tag ) \uf0c1 Find all entries with the given tag. Source code in beancount/scripts/doctor.py def find_tagged_entries ( entries , tag ): \"\"\"Find all entries with the given tag.\"\"\" return [ entry for entry in entries if ( isinstance ( entry , data . Transaction ) and entry . tags and tag in entry . tags ) ] beancount . scripts . doctor . render_mini_balances ( entries , options_map , conversion = None , price_map = None ) \uf0c1 Render a treeified list of the balances for the given transactions. Parameters: entries \u2013 A list of selected transactions to render. options_map \u2013 The parsed options. conversion \u2013 Conversion method string, None, 'value' or 'cost'. price_map \u2013 A price map from the original entries. If this isn't provided, the inventories are rendered directly. If it is, their contents are converted to market value. Source code in beancount/scripts/doctor.py def render_mini_balances ( entries , options_map , conversion = None , price_map = None ): \"\"\"Render a treeified list of the balances for the given transactions. Args: entries: A list of selected transactions to render. options_map: The parsed options. conversion: Conversion method string, None, 'value' or 'cost'. price_map: A price map from the original entries. If this isn't provided, the inventories are rendered directly. If it is, their contents are converted to market value. \"\"\" # Render linked entries (in date order) as errors (for Emacs). errors = [ RenderError ( entry . meta , \"\" , entry ) for entry in entries ] printer . print_errors ( errors ) # Print out balances. real_root = realization . realize ( entries ) dformat = options_map [ \"dcontext\" ] . build ( alignment = Align . DOT , reserved = 2 ) # TODO(blais): I always want to be able to convert at cost. We need # arguments capability. # # TODO(blais): Ideally this conversion inserts a new transactions to # 'Unrealized' to account for the difference between cost and market value. # Insert one and update the realization. Add an update() method to the # realization, given a transaction. acctypes = options . get_account_types ( options_map ) if conversion == \"value\" : assert price_map is not None # Warning: Mutate the inventories in-place, converting them to market # value. balance_diff = inventory . Inventory () for real_account in realization . iter_children ( real_root ): balance_cost = real_account . balance . reduce ( convert . get_cost ) balance_value = real_account . balance . reduce ( convert . get_value , price_map ) real_account . balance = balance_value balance_diff . add_inventory ( balance_cost ) balance_diff . add_inventory ( - balance_value ) if not balance_diff . is_empty (): account_unrealized = account . join ( acctypes . income , options_map [ \"account_unrealized_gains\" ] ) unrealized = realization . get_or_create ( real_root , account_unrealized ) unrealized . balance . add_inventory ( balance_diff ) elif conversion == \"cost\" : for real_account in realization . iter_children ( real_root ): real_account . balance = real_account . balance . reduce ( convert . get_cost ) realization . dump_balances ( real_root , dformat , file = sys . stdout ) # Print out net income change. net_income = inventory . Inventory () for real_node in realization . iter_children ( real_root ): if account_types . is_income_statement_account ( real_node . account , acctypes ): net_income . add_inventory ( real_node . balance ) print () print ( \"Net Income: {} \" . format ( - net_income )) beancount . scripts . doctor . resolve_region_to_entries ( entries , filename , region ) \uf0c1 Resolve a filename and region to a list of entries. Source code in beancount/scripts/doctor.py def resolve_region_to_entries ( entries : List [ data . Entries ], filename : str , region : Tuple [ str , int , int ] ) -> List [ data . Entries ]: \"\"\"Resolve a filename and region to a list of entries.\"\"\" search_filename , first_lineno , last_lineno = region if search_filename is None : search_filename = filename # Find all the entries in the region. (To be clear, this isn't like the # 'linked' command, none of the links are followed.) region_entries = [ entry for entry in data . filter_txns ( entries ) if ( entry . meta [ \"filename\" ] == search_filename and first_lineno <= entry . meta [ \"lineno\" ] <= last_lineno ) ] return region_entries beancount.scripts.example \uf0c1 beancount.scripts.example.LiberalDate ( ParamType ) \uf0c1 beancount . scripts . example . LiberalDate . convert ( self , value , param , ctx ) \uf0c1 Convert the value to the correct type. This is not called if the value is None (the missing value). This must accept string values from the command line, as well as values that are already the correct type. It may also convert other compatible types. The param and ctx arguments may be None in certain situations, such as when converting prompt input. If the value cannot be converted, call :meth: fail with a descriptive message. :param value: The value to convert. :param param: The parameter that is using this type to convert its value. May be None . :param ctx: The current context that arrived at this value. May be None . Source code in beancount/scripts/example.py def convert ( self , value , param , ctx ): try : date_utils . parse_date_liberally ( value ) except ValueError : self . fail ( \" {!r} is not a valid date\" . format ( value ), param , ctx ) beancount . scripts . example . check_non_negative ( entries , account , currency ) \uf0c1 Check that the balance of the given account never goes negative. Parameters: entries \u2013 A list of directives. account \u2013 An account string, the account to check the balance for. currency \u2013 A string, the currency to check minimums for. Exceptions: AssertionError \u2013 if the balance goes negative. Source code in beancount/scripts/example.py def check_non_negative ( entries , account , currency ): \"\"\"Check that the balance of the given account never goes negative. Args: entries: A list of directives. account: An account string, the account to check the balance for. currency: A string, the currency to check minimums for. Raises: AssertionError: if the balance goes negative. \"\"\" previous_date = None for txn_posting , balances in postings_for ( data . sorted ( entries ), [ account ], before = True ): balance = balances [ account ] date = txn_posting . txn . date if date != previous_date : assert all ( pos . units . number >= ZERO for pos in balance . get_positions () ), \"Negative balance: {} at: {} \" . format ( balance , txn_posting . txn . date ) previous_date = date beancount . scripts . example . compute_trip_dates ( date_begin , date_end ) \uf0c1 Generate dates at reasonable intervals for trips during the given time period. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Pairs of dates for the trips within the period. Source code in beancount/scripts/example.py def compute_trip_dates ( date_begin , date_end ): \"\"\"Generate dates at reasonable intervals for trips during the given time period. Args: date_begin: The start date. date_end: The end date. Yields: Pairs of dates for the trips within the period. \"\"\" # Min and max number of days remaining at home. days_at_home = ( 4 * 30 , 13 * 30 ) # Length of trip. days_trip = ( 8 , 22 ) # Number of days to ensure no trip at the beginning and the end. days_buffer = 21 date_begin += datetime . timedelta ( days = days_buffer ) date_end -= datetime . timedelta ( days = days_buffer ) date = date_begin while 1 : duration_at_home = datetime . timedelta ( days = random . randint ( * days_at_home )) duration_trip = datetime . timedelta ( days = random . randint ( * days_trip )) date_trip_begin = date + duration_at_home date_trip_end = date_trip_begin + duration_trip if date_trip_end >= date_end : break yield ( date_trip_begin , date_trip_end ) date = date_trip_end beancount . scripts . example . contextualize_file ( contents , employer ) \uf0c1 Replace generic strings in the generated file with realistic strings. Parameters: contents \u2013 A string, the generic file contents. Returns: A string, the contextualized version. Source code in beancount/scripts/example.py def contextualize_file ( contents , employer ): \"\"\"Replace generic strings in the generated file with realistic strings. Args: contents: A string, the generic file contents. Returns: A string, the contextualized version. \"\"\" replacements = { \"CC\" : \"US\" , \"Bank1\" : \"BofA\" , \"Bank1_Institution\" : \"Bank of America\" , \"Bank1_Address\" : \"123 America Street, LargeTown, USA\" , \"Bank1_Phone\" : \"+1.012.345.6789\" , \"CreditCard1\" : \"Chase:Slate\" , \"CreditCard2\" : \"Amex:BlueCash\" , \"Employer1\" : employer , \"Retirement\" : \"Vanguard\" , \"Retirement_Institution\" : \"Vanguard Group\" , \"Retirement_Address\" : \"P.O. Box 1110, Valley Forge, PA 19482-1110\" , \"Retirement_Phone\" : \"+1.800.523.1188\" , \"Investment\" : \"ETrade\" , # Commodities \"CCY\" : \"USD\" , \"VACHR\" : \"VACHR\" , \"DEFCCY\" : \"IRAUSD\" , \"MFUND1\" : \"VBMPX\" , \"MFUND2\" : \"RGAGX\" , \"STK1\" : \"ITOT\" , \"STK2\" : \"VEA\" , \"STK3\" : \"VHT\" , \"STK4\" : \"GLD\" , } new_contents = replace ( contents , replacements ) return new_contents , replacements beancount . scripts . example . date_iter ( date_begin , date_end ) \uf0c1 Generate a sequence of dates. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Instances of datetime.date. Source code in beancount/scripts/example.py def date_iter ( date_begin , date_end ): \"\"\"Generate a sequence of dates. Args: date_begin: The start date. date_end: The end date. Yields: Instances of datetime.date. \"\"\" assert date_begin <= date_end date = date_begin one_day = datetime . timedelta ( days = 1 ) while date < date_end : date += one_day yield date beancount . scripts . example . date_random_seq ( date_begin , date_end , days_min , days_max ) \uf0c1 Generate a sequence of dates with some random increase in days. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. days_min \u2013 The minimum number of days to advance on each iteration. days_max \u2013 The maximum number of days to advance on each iteration. Yields: Instances of datetime.date. Source code in beancount/scripts/example.py def date_random_seq ( date_begin , date_end , days_min , days_max ): \"\"\"Generate a sequence of dates with some random increase in days. Args: date_begin: The start date. date_end: The end date. days_min: The minimum number of days to advance on each iteration. days_max: The maximum number of days to advance on each iteration. Yields: Instances of datetime.date. \"\"\" assert days_min > 0 assert days_min <= days_max date = date_begin while date < date_end : nb_days_forward = random . randint ( days_min , days_max ) date += datetime . timedelta ( days = nb_days_forward ) if date >= date_end : break yield date beancount . scripts . example . delay_dates ( date_iter , delay_days_min , delay_days_max ) \uf0c1 Delay the dates from the given iterator by some uniformly drawn number of days. Parameters: date_iter \u2013 An iterator of datetime.date instances. delay_days_min \u2013 The minimum amount of advance days for the transaction. delay_days_max \u2013 The maximum amount of advance days for the transaction. Yields: datetime.date instances. Source code in beancount/scripts/example.py def delay_dates ( date_iter , delay_days_min , delay_days_max ): \"\"\"Delay the dates from the given iterator by some uniformly drawn number of days. Args: date_iter: An iterator of datetime.date instances. delay_days_min: The minimum amount of advance days for the transaction. delay_days_max: The maximum amount of advance days for the transaction. Yields: datetime.date instances. \"\"\" dates = list ( date_iter ) last_date = dates [ - 1 ] last_date = last_date . date () if isinstance ( last_date , datetime . datetime ) else last_date for dtime in dates : date = dtime . date () if isinstance ( dtime , datetime . datetime ) else dtime date += datetime . timedelta ( days = random . randint ( delay_days_min , delay_days_max )) if date >= last_date : break yield date beancount . scripts . example . generate_balance_checks ( entries , account , date_iter ) \uf0c1 Generate balance check entries to the given frequency. Parameters: entries \u2013 A list of directives that contain all the transactions for the accounts. account \u2013 The name of the account for which to generate. date_iter \u2013 Iterator of dates. We generate balance checks at these dates. Returns: A list of balance check entries. Source code in beancount/scripts/example.py def generate_balance_checks ( entries , account , date_iter ): \"\"\"Generate balance check entries to the given frequency. Args: entries: A list of directives that contain all the transactions for the accounts. account: The name of the account for which to generate. date_iter: Iterator of dates. We generate balance checks at these dates. Returns: A list of balance check entries. \"\"\" balance_checks = [] date_iter = iter ( date_iter ) next_date = next ( date_iter ) with misc_utils . swallow ( StopIteration ): for txn_posting , balance in postings_for ( entries , [ account ], before = True ): while txn_posting . txn . date >= next_date : amount = balance [ account ] . get_currency_units ( \"CCY\" ) . number balance_checks . extend ( parse ( f \"\"\" { next_date } balance { account } { amount } CCY \"\"\" ) ) next_date = next ( date_iter ) return balance_checks beancount . scripts . example . generate_banking ( entries , date_begin , date_end , amount_initial ) \uf0c1 Generate a checking account opening. Parameters: entries \u2013 A list of entries which affect this account. date_begin \u2013 A date instance, the beginning date. date_end \u2013 A date instance, the end date. amount_initial \u2013 A Decimal instance, the amount to initialize the checking account with. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_banking ( entries , date_begin , date_end , amount_initial ): \"\"\"Generate a checking account opening. Args: entries: A list of entries which affect this account. date_begin: A date instance, the beginning date. date_end: A date instance, the end date. amount_initial: A Decimal instance, the amount to initialize the checking account with. Returns: A list of directives. \"\"\" amount_initial_neg = - amount_initial new_entries = parse ( f \"\"\" { date_begin } open Assets:CC:Bank1 institution: \"Bank1_Institution\" address: \"Bank1_Address\" phone: \"Bank1_Phone\" { date_begin } open Assets:CC:Bank1:Checking CCY account: \"00234-48574897\" ;; { date_begin } open Assets:CC:Bank1:Savings CCY { date_begin } * \"Opening Balance for checking account\" Assets:CC:Bank1:Checking { amount_initial } CCY Equity:Opening-Balances { amount_initial_neg } CCY \"\"\" ) date_balance = date_begin + datetime . timedelta ( days = 1 ) account = \"Assets:CC:Bank1:Checking\" for txn_posting , balances in postings_for ( data . sorted ( entries + new_entries ), [ account ], before = True ): if txn_posting . txn . date >= date_balance : break amount_balance = balances [ account ] . get_currency_units ( \"CCY\" ) . number bal_entries = parse ( f \"\"\" { date_balance } balance Assets:CC:Bank1:Checking { amount_balance } CCY \"\"\" ) return new_entries + bal_entries beancount . scripts . example . generate_banking_expenses ( date_begin , date_end , account , rent_amount ) \uf0c1 Generate expenses paid out of a checking account, typically living expenses. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. account \u2013 The checking account to generate expenses to. rent_amount \u2013 The amount of rent. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_banking_expenses ( date_begin , date_end , account , rent_amount ): \"\"\"Generate expenses paid out of a checking account, typically living expenses. Args: date_begin: The start date. date_end: The end date. account: The checking account to generate expenses to. rent_amount: The amount of rent. Returns: A list of directives. \"\"\" fee_expenses = generate_periodic_expenses ( rrule . rrule ( rrule . MONTHLY , bymonthday = 4 , dtstart = date_begin , until = date_end ), \"BANK FEES\" , \"Monthly bank fee\" , account , \"Expenses:Financial:Fees\" , lambda : D ( \"4.00\" ), ) rent_expenses = generate_periodic_expenses ( delay_dates ( rrule . rrule ( rrule . MONTHLY , dtstart = date_begin , until = date_end ), 2 , 5 ), \"RiverBank Properties\" , \"Paying the rent\" , account , \"Expenses:Home:Rent\" , lambda : random . normalvariate ( float ( rent_amount ), 0 ), ) electricity_expenses = generate_periodic_expenses ( delay_dates ( rrule . rrule ( rrule . MONTHLY , dtstart = date_begin , until = date_end ), 7 , 8 ), \"EDISON POWER\" , \"\" , account , \"Expenses:Home:Electricity\" , lambda : random . normalvariate ( 65 , 0 ), ) internet_expenses = generate_periodic_expenses ( delay_dates ( rrule . rrule ( rrule . MONTHLY , dtstart = date_begin , until = date_end ), 20 , 22 ), \"Wine-Tarner Cable\" , \"\" , account , \"Expenses:Home:Internet\" , lambda : random . normalvariate ( 80 , 0.10 ), ) phone_expenses = generate_periodic_expenses ( delay_dates ( rrule . rrule ( rrule . MONTHLY , dtstart = date_begin , until = date_end ), 17 , 19 ), \"Verizon Wireless\" , \"\" , account , \"Expenses:Home:Phone\" , lambda : random . normalvariate ( 60 , 10 ), ) return data . sorted ( fee_expenses + rent_expenses + electricity_expenses + internet_expenses + phone_expenses ) beancount . scripts . example . generate_clearing_entries ( date_iter , payee , narration , entries , account_clear , account_from ) \uf0c1 Generate entries to clear the value of an account. Parameters: date_iter \u2013 An iterator of datetime.date instances. payee \u2013 A string, the payee name to use on the transactions. narration \u2013 A string, the narration to use on the transactions. entries \u2013 A list of entries. account_clear \u2013 The account to clear. account_from \u2013 The source account to clear 'account_clear' from. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_clearing_entries ( date_iter , payee , narration , entries , account_clear , account_from ): \"\"\"Generate entries to clear the value of an account. Args: date_iter: An iterator of datetime.date instances. payee: A string, the payee name to use on the transactions. narration: A string, the narration to use on the transactions. entries: A list of entries. account_clear: The account to clear. account_from: The source account to clear 'account_clear' from. Returns: A list of directives. \"\"\" new_entries = [] # The next date we're looking for. date_iter = iter ( date_iter ) next_date = next ( date_iter , None ) if not next_date : return new_entries # Iterate over all the postings of the account to clear. for txn_posting , balances in postings_for ( entries , [ account_clear ]): balance_clear = balances [ account_clear ] # Check if we need to clear. if next_date <= txn_posting . txn . date : pos_amount = balance_clear . get_currency_units ( \"CCY\" ) neg_amount = - pos_amount new_entries . extend ( parse ( f \"\"\" { next_date } * \" { payee } \" \" { narration } \" { account_clear } { neg_amount . number : .2f } CCY { account_from } { pos_amount . number : .2f } CCY \"\"\" ) ) balance_clear . add_amount ( neg_amount ) # Advance to the next date we're looking for. next_date = next ( date_iter , None ) if not next_date : break return new_entries beancount . scripts . example . generate_commodity_entries ( date_birth ) \uf0c1 Create a list of Commodity entries for all the currencies we're using. Parameters: date_birth \u2013 A datetime.date instance, the date of birth of the user. Returns: A list of Commodity entries for all the commodities in use. Source code in beancount/scripts/example.py def generate_commodity_entries ( date_birth ): \"\"\"Create a list of Commodity entries for all the currencies we're using. Args: date_birth: A datetime.date instance, the date of birth of the user. Returns: A list of Commodity entries for all the commodities in use. \"\"\" return parse ( f \"\"\" 1792-01-01 commodity USD name: \"US Dollar\" export: \"CASH\" { date_birth } commodity VACHR name: \"Employer Vacation Hours\" export: \"IGNORE\" { date_birth } commodity IRAUSD name: \"US 401k and IRA Contributions\" export: \"IGNORE\" 2009-05-01 commodity RGAGX name: \"American Funds The Growth Fund of America Class R-6\" export: \"MUTF:RGAGX\" price: \"USD:google/MUTF:RGAGX\" 1995-09-18 commodity VBMPX name: \"Vanguard Total Bond Market Index Fund Institutional Plus Shares\" export: \"MUTF:VBMPX\" price: \"USD:google/MUTF:VBMPX\" 2004-01-20 commodity ITOT name: \"iShares Core S&P Total U.S. Stock Market ETF\" export: \"NYSEARCA:ITOT\" price: \"USD:google/NYSEARCA:ITOT\" 2007-07-20 commodity VEA name: \"Vanguard FTSE Developed Markets ETF\" export: \"NYSEARCA:VEA\" price: \"USD:google/NYSEARCA:VEA\" 2004-01-26 commodity VHT name: \"Vanguard Health Care ETF\" export: \"NYSEARCA:VHT\" price: \"USD:google/NYSEARCA:VHT\" 2004-11-01 commodity GLD name: \"SPDR Gold Trust (ETF)\" export: \"NYSEARCA:GLD\" price: \"USD:google/NYSEARCA:GLD\" 1900-01-01 commodity VMMXX export: \"MUTF:VMMXX (MONEY:USD)\" \"\"\" ) beancount . scripts . example . generate_employment_income ( employer_name , employer_address , annual_salary , account_deposit , account_retirement , date_begin , date_end ) \uf0c1 Generate bi-weekly entries for payroll salary income. Parameters: employer_name \u2013 A string, the human-readable name of the employer. employer_address \u2013 A string, the address of the employer. annual_salary \u2013 A Decimal, the annual salary of the employee. account_deposit \u2013 An account string, the account to deposit the salary to. account_retirement \u2013 An account string, the account to deposit retirement contributions to. date_begin \u2013 The start date. date_end \u2013 The end date. Returns: A list of directives, including open directives for the account. Source code in beancount/scripts/example.py def generate_employment_income ( employer_name , employer_address , annual_salary , account_deposit , account_retirement , date_begin , date_end , ): \"\"\"Generate bi-weekly entries for payroll salary income. Args: employer_name: A string, the human-readable name of the employer. employer_address: A string, the address of the employer. annual_salary: A Decimal, the annual salary of the employee. account_deposit: An account string, the account to deposit the salary to. account_retirement: An account string, the account to deposit retirement contributions to. date_begin: The start date. date_end: The end date. Returns: A list of directives, including open directives for the account. \"\"\" preamble = parse ( f \"\"\" { date_begin } event \"employer\" \" { employer_name } , { employer_address } \" { date_begin } open Income:CC:Employer1:Salary CCY ; { date_begin } open Income:CC:Employer1:AnnualBonus CCY { date_begin } open Income:CC:Employer1:GroupTermLife CCY { date_begin } open Income:CC:Employer1:Vacation VACHR { date_begin } open Assets:CC:Employer1:Vacation VACHR { date_begin } open Expenses:Vacation VACHR { date_begin } open Expenses:Health:Life:GroupTermLife { date_begin } open Expenses:Health:Medical:Insurance { date_begin } open Expenses:Health:Dental:Insurance { date_begin } open Expenses:Health:Vision:Insurance ; { date_begin } open Expenses:Vacation:Employer \"\"\" ) date_prev = None contrib_retirement = ZERO contrib_socsec = ZERO biweekly_pay = annual_salary / 26 gross = biweekly_pay medicare = gross * D ( \"0.0231\" ) federal = gross * D ( \"0.2303\" ) state = gross * D ( \"0.0791\" ) city = gross * D ( \"0.0379\" ) sdi = D ( \"1.12\" ) lifeinsurance = D ( \"24.32\" ) dental = D ( \"2.90\" ) medical = D ( \"27.38\" ) vision = D ( \"42.30\" ) fixed = medicare + federal + state + city + sdi + dental + medical + vision # Calculate vacation hours per-pay. with decimal . localcontext () as ctx : ctx . prec = 4 vacation_hrs = ( ANNUAL_VACATION_DAYS * D ( \"8\" )) / D ( \"26\" ) transactions = [] for dtime in misc_utils . skipiter ( rrule . rrule ( rrule . WEEKLY , byweekday = rrule . TH , dtstart = date_begin , until = date_end ), 2 ): date = dtime . date () year = date . year if not date_prev or date_prev . year != date . year : contrib_retirement = RETIREMENT_LIMITS . get ( date . year , RETIREMENT_LIMITS [ None ]) contrib_socsec = D ( \"7000\" ) date_prev = date retirement_uncapped = math . ceil (( gross * D ( \"0.25\" )) / 100 ) * 100 retirement = min ( contrib_retirement , retirement_uncapped ) contrib_retirement -= retirement socsec_uncapped = gross * D ( \"0.0610\" ) socsec = min ( contrib_socsec , socsec_uncapped ) contrib_socsec -= socsec with decimal . localcontext () as ctx : ctx . prec = 6 deposit = gross - retirement - fixed - socsec retirement_neg = - retirement gross_neg = - gross lifeinsurance_neg = - lifeinsurance vacation_hrs_neg = - vacation_hrs transactions . extend ( parse ( f \"\"\" { date } * \" { employer_name } \" \"Payroll\" { account_deposit } { deposit : .2f } CCY \"\"\" + ( \"\" if retirement == ZERO else f \"\"\" \\ { account_retirement } { retirement : .2f } CCY Assets:CC:Federal:PreTax401k { retirement_neg : .2f } DEFCCY Expenses:Taxes:Y { year } :CC:Federal:PreTax401k { retirement : .2f } DEFCCY \"\"\" ) + f \"\"\" \\ Income:CC:Employer1:Salary { gross_neg : .2f } CCY Income:CC:Employer1:GroupTermLife { lifeinsurance_neg : .2f } CCY Expenses:Health:Life:GroupTermLife { lifeinsurance : .2f } CCY Expenses:Health:Dental:Insurance { dental } CCY Expenses:Health:Medical:Insurance { medical } CCY Expenses:Health:Vision:Insurance { vision } CCY Expenses:Taxes:Y { year } :CC:Medicare { medicare : .2f } CCY Expenses:Taxes:Y { year } :CC:Federal { federal : .2f } CCY Expenses:Taxes:Y { year } :CC:State { state : .2f } CCY Expenses:Taxes:Y { year } :CC:CityNYC { city : .2f } CCY Expenses:Taxes:Y { year } :CC:SDI { sdi : .2f } CCY Expenses:Taxes:Y { year } :CC:SocSec { socsec : .2f } CCY Assets:CC:Employer1:Vacation { vacation_hrs : .2f } VACHR Income:CC:Employer1:Vacation { vacation_hrs_neg : .2f } VACHR \"\"\" ) ) return preamble + transactions beancount . scripts . example . generate_expense_accounts ( date_birth ) \uf0c1 Generate directives for expense accounts. Parameters: date_birth \u2013 Birth date of the character. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_expense_accounts ( date_birth ): \"\"\"Generate directives for expense accounts. Args: date_birth: Birth date of the character. Returns: A list of directives. \"\"\" return parse ( f \"\"\" { date_birth } open Expenses:Food:Groceries { date_birth } open Expenses:Food:Restaurant { date_birth } open Expenses:Food:Coffee { date_birth } open Expenses:Food:Alcohol { date_birth } open Expenses:Transport:Tram { date_birth } open Expenses:Home:Rent { date_birth } open Expenses:Home:Electricity { date_birth } open Expenses:Home:Internet { date_birth } open Expenses:Home:Phone { date_birth } open Expenses:Financial:Fees { date_birth } open Expenses:Financial:Commissions \"\"\" ) beancount . scripts . example . generate_open_entries ( date , accounts , currency = None ) \uf0c1 Generate a list of Open entries for the given accounts: Parameters: date \u2013 A datetime.date instance for the open entries. accounts \u2013 A list of account strings. currency \u2013 An optional currency constraint. Returns: A list of Open directives. Source code in beancount/scripts/example.py def generate_open_entries ( date , accounts , currency = None ): \"\"\"Generate a list of Open entries for the given accounts: Args: date: A datetime.date instance for the open entries. accounts: A list of account strings. currency: An optional currency constraint. Returns: A list of Open directives. \"\"\" assert isinstance ( accounts , ( list , tuple )) return parse ( \"\" . join ( \" {date} open {account} {currency} \\n \" . format ( date = date , account = account , currency = currency or \"\" ) for account in accounts ) ) beancount . scripts . example . generate_outgoing_transfers ( entries , account , account_out , transfer_minimum , transfer_threshold , transfer_increment ) \uf0c1 Generate transfers of accumulated funds out of an account. This monitors the balance of an account and when it is beyond a threshold, generate out transfers form that account to another account. Parameters: entries \u2013 A list of existing entries that affect this account so far. The generated entries will also affect this account. account \u2013 An account string, the account to monitor. account_out \u2013 An account string, the savings account to make transfers to. transfer_minimum \u2013 The minimum amount of funds to always leave in this account after a transfer. transfer_threshold \u2013 The minimum amount of funds to be able to transfer out without breaking the minimum. transfer_increment \u2013 A Decimal, the increment to round transfers to. Returns: A list of new directives, the transfers to add to the given account. Source code in beancount/scripts/example.py def generate_outgoing_transfers ( entries , account , account_out , transfer_minimum , transfer_threshold , transfer_increment ): \"\"\"Generate transfers of accumulated funds out of an account. This monitors the balance of an account and when it is beyond a threshold, generate out transfers form that account to another account. Args: entries: A list of existing entries that affect this account so far. The generated entries will also affect this account. account: An account string, the account to monitor. account_out: An account string, the savings account to make transfers to. transfer_minimum: The minimum amount of funds to always leave in this account after a transfer. transfer_threshold: The minimum amount of funds to be able to transfer out without breaking the minimum. transfer_increment: A Decimal, the increment to round transfers to. Returns: A list of new directives, the transfers to add to the given account. \"\"\" last_date = entries [ - 1 ] . date # Reverse the balance amounts taking into account the minimum balance for # all time in the future. amounts = [ ( balances [ account ] . get_currency_units ( \"CCY\" ) . number , txn_posting ) for txn_posting , balances in postings_for ( entries , [ account ]) ] reversed_amounts = [] last_amount , _ = amounts [ - 1 ] for current_amount , _ in reversed ( amounts ): if current_amount < last_amount : reversed_amounts . append ( current_amount ) last_amount = current_amount else : reversed_amounts . append ( last_amount ) capped_amounts = reversed ( reversed_amounts ) # Create transfers outward where the future allows it. new_entries = [] offset_amount = ZERO for current_amount , ( _ , txn_posting ) in zip ( capped_amounts , amounts ): if txn_posting . txn . date >= last_date : break adjusted_amount = current_amount - offset_amount if adjusted_amount > ( transfer_minimum + transfer_threshold ): amount_transfer = round_to ( adjusted_amount - transfer_minimum , transfer_increment ) date = txn_posting . txn . date + datetime . timedelta ( days = 1 ) amount_transfer_neg = - amount_transfer new_entries . extend ( parse ( f \"\"\" { date } * \"Transfering accumulated savings to other account\" { account } { amount_transfer_neg : 2f } CCY { account_out } { amount_transfer : 2f } CCY \"\"\" ) ) offset_amount += amount_transfer return new_entries beancount . scripts . example . generate_periodic_expenses ( date_iter , payee , narration , account_from , account_to , amount_generator ) \uf0c1 Generate periodic expense transactions. Parameters: date_iter \u2013 An iterator for dates or datetimes. payee \u2013 A string, the payee name to use on the transactions, or a set of such strings to randomly choose from narration \u2013 A string, the narration to use on the transactions. account_from \u2013 An account string the debited account. account_to \u2013 An account string the credited account. amount_generator \u2013 A callable object to generate variates. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_periodic_expenses ( date_iter , payee , narration , account_from , account_to , amount_generator ): \"\"\"Generate periodic expense transactions. Args: date_iter: An iterator for dates or datetimes. payee: A string, the payee name to use on the transactions, or a set of such strings to randomly choose from narration: A string, the narration to use on the transactions. account_from: An account string the debited account. account_to: An account string the credited account. amount_generator: A callable object to generate variates. Returns: A list of directives. \"\"\" new_entries = [] for dtime in date_iter : date = dtime . date () if isinstance ( dtime , datetime . datetime ) else dtime amount = D ( amount_generator ()) txn_payee = payee if isinstance ( payee , str ) else random . choice ( payee ) txn_narration = ( narration if isinstance ( narration , str ) else random . choice ( narration ) ) amount_neg = - amount new_entries . extend ( parse ( f \"\"\" { date } * \" { txn_payee } \" \" { txn_narration } \" { account_from } { amount_neg : .2f } CCY { account_to } { amount : .2f } CCY \"\"\" ) ) return new_entries beancount . scripts . example . generate_prices ( date_begin , date_end , currencies , cost_currency ) \uf0c1 Generate weekly or monthly price entries for the given currencies. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. currencies \u2013 A list of currency strings to generate prices for. cost_currency \u2013 A string, the cost currency. Returns: A list of Price directives. Source code in beancount/scripts/example.py def generate_prices ( date_begin , date_end , currencies , cost_currency ): \"\"\"Generate weekly or monthly price entries for the given currencies. Args: date_begin: The start date. date_end: The end date. currencies: A list of currency strings to generate prices for. cost_currency: A string, the cost currency. Returns: A list of Price directives. \"\"\" digits = D ( \"0.01\" ) entries = [] counter = itertools . count () for currency in currencies : start_price = random . uniform ( 30 , 200 ) growth = random . uniform ( 0.02 , 0.13 ) # %/year mu = growth * ( 7 / 365 ) sigma = random . uniform ( 0.005 , 0.02 ) # Vol for dtime , price_float in zip ( rrule . rrule ( rrule . WEEKLY , byweekday = rrule . FR , dtstart = date_begin , until = date_end ), price_series ( start_price , mu , sigma ), ): price = D ( price_float ) . quantize ( digits ) meta = data . new_metadata ( generate_prices . __name__ , next ( counter )) entry = data . Price ( meta , dtime . date (), currency , amount . Amount ( price , cost_currency ) ) entries . append ( entry ) return entries beancount . scripts . example . generate_regular_credit_expenses ( date_birth , date_begin , date_end , account_credit , account_checking ) \uf0c1 Generate expenses paid out of a credit card account, including payments to the credit card. Parameters: date_birth \u2013 The user's birth date. date_begin \u2013 The start date. date_end \u2013 The end date. account_credit \u2013 The credit card account to generate expenses against. account_checking \u2013 The checking account to generate payments from. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_regular_credit_expenses ( date_birth , date_begin , date_end , account_credit , account_checking ): \"\"\"Generate expenses paid out of a credit card account, including payments to the credit card. Args: date_birth: The user's birth date. date_begin: The start date. date_end: The end date. account_credit: The credit card account to generate expenses against. account_checking: The checking account to generate payments from. Returns: A list of directives. \"\"\" restaurant_expenses = generate_periodic_expenses ( date_random_seq ( date_begin , date_end , 1 , 5 ), RESTAURANT_NAMES , RESTAURANT_NARRATIONS , account_credit , \"Expenses:Food:Restaurant\" , lambda : min ( random . lognormvariate ( math . log ( 30 ), math . log ( 1.5 )), random . randint ( 200 , 220 ) ), ) groceries_expenses = generate_periodic_expenses ( date_random_seq ( date_begin , date_end , 5 , 20 ), GROCERIES_NAMES , \"Buying groceries\" , account_credit , \"Expenses:Food:Groceries\" , lambda : min ( random . lognormvariate ( math . log ( 80 ), math . log ( 1.3 )), random . randint ( 250 , 300 ) ), ) subway_expenses = generate_periodic_expenses ( date_random_seq ( date_begin , date_end , 27 , 33 ), \"Metro Transport Authority\" , \"Tram tickets\" , account_credit , \"Expenses:Transport:Tram\" , lambda : D ( \"120.00\" ), ) credit_expenses = data . sorted ( restaurant_expenses + groceries_expenses + subway_expenses ) # Entries to open accounts. credit_preamble = generate_open_entries ( date_birth , [ account_credit ], \"CCY\" ) return data . sorted ( credit_preamble + credit_expenses ) beancount . scripts . example . generate_retirement_employer_match ( entries , account_invest , account_income ) \uf0c1 Generate employer matching contributions into a retirement account. Parameters: entries \u2013 A list of directives that cover the retirement account. account_invest \u2013 The name of the retirement cash account. account_income \u2013 The name of the income account. Returns: A list of new entries generated for employer contributions. Source code in beancount/scripts/example.py def generate_retirement_employer_match ( entries , account_invest , account_income ): \"\"\"Generate employer matching contributions into a retirement account. Args: entries: A list of directives that cover the retirement account. account_invest: The name of the retirement cash account. account_income: The name of the income account. Returns: A list of new entries generated for employer contributions. \"\"\" match_frac = D ( \"0.50\" ) new_entries = parse ( f \"\"\" { entries [ 0 ] . date } open { account_income } CCY \"\"\" ) for txn_posting , balances in postings_for ( entries , [ account_invest ]): amount = txn_posting . posting . units . number * match_frac amount_neg = - amount date = txn_posting . txn . date + ONE_DAY new_entries . extend ( parse ( f \"\"\" { date } * \"Employer match for contribution\" { account_invest } { amount : .2f } CCY { account_income } { amount_neg : .2f } CCY \"\"\" ) ) return new_entries beancount . scripts . example . generate_retirement_investments ( entries , account , commodities_items , price_map ) \uf0c1 Invest money deposited to the given retirement account. Parameters: entries \u2013 A list of directives account \u2013 The root account for all retirement investment sub-accounts. commodities_items \u2013 A list of (commodity, fraction to be invested in) items. price_map \u2013 A dict of prices, as per beancount.core.prices.build_price_map(). Returns: A list of new directives for the given investments. This also generates account opening directives for the desired investment commodities. Source code in beancount/scripts/example.py def generate_retirement_investments ( entries , account , commodities_items , price_map ): \"\"\"Invest money deposited to the given retirement account. Args: entries: A list of directives account: The root account for all retirement investment sub-accounts. commodities_items: A list of (commodity, fraction to be invested in) items. price_map: A dict of prices, as per beancount.core.prices.build_price_map(). Returns: A list of new directives for the given investments. This also generates account opening directives for the desired investment commodities. \"\"\" open_entries = [] account_cash = join ( account , \"Cash\" ) date_origin = entries [ 0 ] . date open_entries . extend ( parse ( f \"\"\" { date_origin } open { account } CCY institution: \"Retirement_Institution\" address: \"Retirement_Address\" phone: \"Retirement_Phone\" { date_origin } open { account_cash } CCY number: \"882882\" \"\"\" ) ) for currency , _ in commodities_items : open_entries . extend ( parse ( f \"\"\" { date_origin } open { account } : { currency } { currency } number: \"882882\" \"\"\" ) ) new_entries = [] for txn_posting , balances in postings_for ( entries , [ account_cash ]): balance = balances [ account_cash ] amount_to_invest = balance . get_currency_units ( \"CCY\" ) . number # Find the date the following Monday, the date to invest. txn_date = txn_posting . txn . date while txn_date . weekday () != calendar . MONDAY : txn_date += ONE_DAY for commodity , fraction in commodities_items : amount_fraction = amount_to_invest * D ( fraction ) # Find the price at that date. _ , price = prices . get_price ( price_map , ( commodity , \"CCY\" ), txn_date ) units = ( amount_fraction / price ) . quantize ( D ( \"0.001\" )) amount_cash = ( units * price ) . quantize ( D ( \"0.01\" )) amount_cash_neg = - amount_cash new_entries . extend ( parse ( f \"\"\" { txn_date } * \"Investing { fraction : .0% } of cash in { commodity } \" { account } : { commodity } { units : .3f } { commodity } {{ { price : .2f } CCY }} { account } :Cash { amount_cash_neg : .2f } CCY \"\"\" ) ) balance . add_amount ( amount . Amount ( - amount_cash , \"CCY\" )) return data . sorted ( open_entries + new_entries ) beancount . scripts . example . generate_tax_accounts ( year , date_max ) \uf0c1 Generate accounts and contribution directives for a particular tax year. Parameters: year \u2013 An integer, the year we're to generate this for. date_max \u2013 The maximum date to produce an entry for. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_tax_accounts ( year , date_max ): \"\"\"Generate accounts and contribution directives for a particular tax year. Args: year: An integer, the year we're to generate this for. date_max: The maximum date to produce an entry for. Returns: A list of directives. \"\"\" date_year = datetime . date ( year , 1 , 1 ) date_filing = datetime . date ( year + 1 , 3 , 20 ) + datetime . timedelta ( days = random . randint ( 0 , 5 ) ) date_federal = date_filing + datetime . timedelta ( days = random . randint ( 0 , 4 )) date_state = date_filing + datetime . timedelta ( days = random . randint ( 0 , 4 )) quantum = D ( \"0.01\" ) amount_federal = D ( max ( random . normalvariate ( 500 , 120 ), 12 )) . quantize ( quantum ) amount_federal_neg = - amount_federal amount_state = D ( max ( random . normalvariate ( 300 , 100 ), 10 )) . quantize ( quantum ) amount_state_neg = - amount_state amount_payable = - ( amount_federal + amount_state ) amount_limit = RETIREMENT_LIMITS . get ( year , RETIREMENT_LIMITS [ None ]) amount_limit_neg = - amount_limit entries = parse ( f \"\"\" ;; Open tax accounts for that year. { date_year } open Expenses:Taxes:Y { year } :CC:Federal:PreTax401k DEFCCY { date_year } open Expenses:Taxes:Y { year } :CC:Medicare CCY { date_year } open Expenses:Taxes:Y { year } :CC:Federal CCY { date_year } open Expenses:Taxes:Y { year } :CC:CityNYC CCY { date_year } open Expenses:Taxes:Y { year } :CC:SDI CCY { date_year } open Expenses:Taxes:Y { year } :CC:State CCY { date_year } open Expenses:Taxes:Y { year } :CC:SocSec CCY ;; Check that the tax amounts have been fully used. { date_year } balance Assets:CC:Federal:PreTax401k 0 DEFCCY { date_year } * \"Allowed contributions for one year\" Income:CC:Federal:PreTax401k { amount_limit_neg } DEFCCY Assets:CC:Federal:PreTax401k { amount_limit } DEFCCY { date_filing } * \"Filing taxes for { year } \" Expenses:Taxes:Y { year } :CC:Federal { amount_federal : .2f } CCY Expenses:Taxes:Y { year } :CC:State { amount_state : .2f } CCY Liabilities:AccountsPayable { amount_payable : .2f } CCY { date_federal } * \"FEDERAL TAXPYMT\" Assets:CC:Bank1:Checking { amount_federal_neg : .2f } CCY Liabilities:AccountsPayable { amount_federal : .2f } CCY { date_state } * \"STATE TAX & FINANC PYMT\" Assets:CC:Bank1:Checking { amount_state_neg : .2f } CCY Liabilities:AccountsPayable { amount_state : .2f } CCY \"\"\" ) return [ entry for entry in entries if entry . date < date_max ] beancount . scripts . example . generate_tax_preamble ( date_birth ) \uf0c1 Generate tax declarations not specific to any particular year. Parameters: date_birth \u2013 A date instance, the birth date of the character. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_tax_preamble ( date_birth ): \"\"\"Generate tax declarations not specific to any particular year. Args: date_birth: A date instance, the birth date of the character. Returns: A list of directives. \"\"\" return parse ( f \"\"\" ;; Tax accounts not specific to a year. { date_birth } open Income:CC:Federal:PreTax401k DEFCCY { date_birth } open Assets:CC:Federal:PreTax401k DEFCCY \"\"\" ) beancount . scripts . example . generate_taxable_investment ( date_begin , date_end , entries , price_map , stocks ) \uf0c1 Generate opening directives and transactions for an investment account. Parameters: date_begin \u2013 A date instance, the beginning date. date_end \u2013 A date instance, the end date. entries \u2013 A list of entries that contains at least the transfers to the investment account's cash account. price_map \u2013 A dict of prices, as per beancount.core.prices.build_price_map(). stocks \u2013 A list of strings, the list of commodities to invest in. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_taxable_investment ( date_begin , date_end , entries , price_map , stocks ): \"\"\"Generate opening directives and transactions for an investment account. Args: date_begin: A date instance, the beginning date. date_end: A date instance, the end date. entries: A list of entries that contains at least the transfers to the investment account's cash account. price_map: A dict of prices, as per beancount.core.prices.build_price_map(). stocks: A list of strings, the list of commodities to invest in. Returns: A list of directives. \"\"\" account = \"Assets:CC:Investment\" income = \"Income:CC:Investment\" account_cash = join ( account , \"Cash\" ) account_gains = \" {income} :PnL\" . format ( income = income ) dividends = \"Dividend\" accounts_stocks = [ \"Assets:CC:Investment: {} \" . format ( commodity ) for commodity in stocks ] open_entries = parse ( f \"\"\" { date_begin } open { account } :Cash CCY { date_begin } open { account_gains } CCY \"\"\" ) for stock in stocks : open_entries . extend ( parse ( f \"\"\" { date_begin } open { account } : { stock } { stock } { date_begin } open { income } : { stock } : { dividends } CCY \"\"\" ) ) # Figure out dates at which dividends should be distributed, near the end of # each quarter. days_to = datetime . timedelta ( days = 3 * 90 - 10 ) dividend_dates = [] for quarter_begin in iter_quarters ( date_begin , date_end ): end_of_quarter = quarter_begin + days_to if not ( date_begin < end_of_quarter < date_end ): continue dividend_dates . append ( end_of_quarter ) # Iterate over all the dates, but merging in the postings for the cash # account. min_amount = D ( \"1000.00\" ) round_amount = D ( \"100.00\" ) commission = D ( \"8.95\" ) round_units = D ( \"1\" ) frac_invest = D ( \"1.00\" ) frac_dividend = D ( \"0.004\" ) p_daily_buy = 1.0 / 15 # days p_daily_sell = 1.0 / 90 # days stocks_inventory = inventory . Inventory () new_entries = [] dividend_date_iter = iter ( dividend_dates ) next_dividend_date = next ( dividend_date_iter , None ) for date , balances in iter_dates_with_balance ( date_begin , date_end , entries , [ account_cash ] ): # Check if we should insert a dividend. Note that we could not factor # this out because we want to explicitly reinvest the cash dividends and # we also want the dividends to be proportional to the amount of # invested stock, so one feeds on the other and vice-versa. if next_dividend_date and date > next_dividend_date : # Compute the total balances for the stock accounts in order to # create a realistic dividend. total = inventory . Inventory () for account_stock in accounts_stocks : total . add_inventory ( balances [ account_stock ]) # Create an entry offering dividends of 1% of the portfolio. portfolio_cost = total . reduce ( convert . get_cost ) . get_currency_units ( \"CCY\" ) . number amount_cash = ( frac_dividend * portfolio_cost ) . quantize ( D ( \"0.01\" )) amount_cash_neg = - amount_cash stock = random . choice ( stocks ) cash_dividend = parse ( f \"\"\" { next_dividend_date } * \"Dividends on portfolio\" { account } :Cash { amount_cash : .2f } CCY { income } : { stock } : { dividends } { amount_cash_neg : .2f } CCY \"\"\" )[ 0 ] new_entries . append ( cash_dividend ) # Advance the next dividend date. next_dividend_date = next ( dividend_date_iter , None ) # If the balance is high, buy with high probability. balance = balances [ account_cash ] total_cash = balance . get_currency_units ( \"CCY\" ) . number assert total_cash >= ZERO , \"Cash balance is negative: {} \" . format ( total_cash ) invest_cash = total_cash * frac_invest - commission if invest_cash > min_amount : if random . random () < p_daily_buy : commodities = random . sample ( stocks , random . randint ( 1 , len ( stocks ))) lot_amount = round_to ( invest_cash / len ( commodities ), round_amount ) for stock in commodities : # Find the price at that date. _ , price = prices . get_price ( price_map , ( stock , \"CCY\" ), date ) units = round_to (( lot_amount / price ), round_units ) if units <= ZERO : continue amount_cash = - ( units * price + commission ) # logging.info('Buying %s %s @ %s CCY = %s CCY', # units, stock, price, units * price) buy = parse ( f \"\"\" { date } * \"Buy shares of { stock } \" { account } :Cash { amount_cash : .2f } CCY { account } : { stock } { units : .0f } { stock } {{ { price : .2f } CCY }} Expenses:Financial:Commissions { commission : .2f } CCY \"\"\" )[ 0 ] new_entries . append ( buy ) account_stock = \":\" . join ([ account , stock ]) balances [ account_cash ] . add_position ( buy . postings [ 0 ]) balances [ account_stock ] . add_position ( buy . postings [ 1 ]) stocks_inventory . add_position ( buy . postings [ 1 ]) # Don't sell on days you buy. continue # Otherwise, sell with low probability. if not stocks_inventory . is_empty () and random . random () < p_daily_sell : # Choose the lot with the highest gain or highest loss. gains = [] for position in stocks_inventory . get_positions (): base_quote = ( position . units . currency , position . cost . currency ) _ , price = prices . get_price ( price_map , base_quote , date ) if price == position . cost . number : continue # Skip lots without movement. market_value = position . units . number * price book_value = convert . get_cost ( position ) . number gain = market_value - book_value gains . append (( gain , market_value , price , position )) if not gains : continue # Sell either biggest winner or biggest loser. biggest = bool ( random . random () < 0.5 ) lot_tuple = sorted ( gains )[ 0 if biggest else - 1 ] gain , market_value , price , sell_position = lot_tuple # logging.info('Selling {} for {}'.format(sell_position, market_value)) sell_position = - sell_position stock = sell_position . units . currency amount_cash = market_value - commission amount_gain = - gain sell = parse ( f \"\"\" { date } * \"Sell shares of { stock } \" { account } : { stock } { sell_position } @ { price : .2f } CCY { account } :Cash { amount_cash : .2f } CCY Expenses:Financial:Commissions { commission : .2f } CCY { account_gains } { amount_gain : .2f } CCY \"\"\" )[ 0 ] new_entries . append ( sell ) balances [ account_cash ] . add_position ( sell . postings [ 1 ]) stocks_inventory . add_position ( sell . postings [ 0 ]) continue return open_entries + new_entries beancount . scripts . example . generate_trip_entries ( date_begin , date_end , tag , config , trip_city , home_city , account_credit ) \uf0c1 Generate more dense expenses for a trip. Parameters: date_begin \u2013 A datetime.date instance, the beginning of the trip. date_end \u2013 A datetime.date instance, the end of the trip. tag \u2013 A string, the name of the tag. config \u2013 A list of (payee name, account name, (mu, 3sigma)), where mu is the mean of the prices to generate and 3sigma is 3 times the standard deviation. trip_city \u2013 A string, the capitalized name of the destination city. home_city \u2013 A string, the name of the home city. account_credit \u2013 A string, the name of the credit card account to pay the expenses from. Returns: A list of entries for the trip, all tagged with the given tag. Source code in beancount/scripts/example.py def generate_trip_entries ( date_begin , date_end , tag , config , trip_city , home_city , account_credit ): \"\"\"Generate more dense expenses for a trip. Args: date_begin: A datetime.date instance, the beginning of the trip. date_end: A datetime.date instance, the end of the trip. tag: A string, the name of the tag. config: A list of (payee name, account name, (mu, 3sigma)), where mu is the mean of the prices to generate and 3sigma is 3 times the standard deviation. trip_city: A string, the capitalized name of the destination city. home_city: A string, the name of the home city. account_credit: A string, the name of the credit card account to pay the expenses from. Returns: A list of entries for the trip, all tagged with the given tag. \"\"\" p_day_generate = 0.3 new_entries = [] for date in date_iter ( date_begin , date_end ): for payee , account_expense , ( mu , sigma3 ) in config : if random . random () < p_day_generate : amount = random . normalvariate ( mu , sigma3 / 3.0 ) amount_neg = - amount new_entries . extend ( parse ( f \"\"\" { date } * \" { payee } \" \"\" # { tag } { account_credit } { amount_neg : .2f } CCY { account_expense } { amount : .2f } CCY \"\"\" ) ) # Consume the vacation days. vacation_hrs = ( date_end - date_begin ) . days * 8 # hrs/day new_entries . extend ( parse ( f \"\"\" { date_end } * \"Consume vacation days\" Assets:CC:Employer1:Vacation - { vacation_hrs : .2f } VACHR Expenses:Vacation { vacation_hrs : .2f } VACHR \"\"\" ) ) # Generate events for the trip. new_entries . extend ( parse ( f \"\"\" { date_begin } event \"location\" \" { trip_city } \" { date_end } event \"location\" \" { home_city } \" \"\"\" ) ) return new_entries beancount . scripts . example . get_minimum_balance ( entries , account , currency ) \uf0c1 Compute the minimum balance of the given account according to the entries history. Parameters: entries \u2013 A list of directives. account \u2013 An account string. currency \u2013 A currency string, for which we want to compute the minimum. Returns: A Decimal number, the minimum amount throughout the history of this account. Source code in beancount/scripts/example.py def get_minimum_balance ( entries , account , currency ): \"\"\"Compute the minimum balance of the given account according to the entries history. Args: entries: A list of directives. account: An account string. currency: A currency string, for which we want to compute the minimum. Returns: A Decimal number, the minimum amount throughout the history of this account. \"\"\" min_amount = ZERO for _ , balances in postings_for ( entries , [ account ]): balance = balances [ account ] current = balance . get_currency_units ( currency ) . number if current < min_amount : min_amount = current return min_amount beancount . scripts . example . iter_dates_with_balance ( date_begin , date_end , entries , accounts ) \uf0c1 Iterate over dates, including the balances of the postings iterator. Parameters: postings_iter \u2013 An iterator of postings as per postings_for(). date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Pairs of (data, balances) objects, with date: A datetime.date instance balances: An Inventory object, representing the current balance. You can modify the inventory object to feed back changes in the balance. Source code in beancount/scripts/example.py def iter_dates_with_balance ( date_begin , date_end , entries , accounts ): \"\"\"Iterate over dates, including the balances of the postings iterator. Args: postings_iter: An iterator of postings as per postings_for(). date_begin: The start date. date_end: The end date. Yields: Pairs of (data, balances) objects, with date: A datetime.date instance balances: An Inventory object, representing the current balance. You can modify the inventory object to feed back changes in the balance. \"\"\" balances = collections . defaultdict ( inventory . Inventory ) merged_txn_postings = iter ( merge_postings ( entries , accounts )) txn_posting = next ( merged_txn_postings , None ) for date in date_iter ( date_begin , date_end ): while txn_posting and txn_posting . txn . date == date : posting = txn_posting . posting balances [ posting . account ] . add_position ( posting ) txn_posting = next ( merged_txn_postings , None ) yield date , balances beancount . scripts . example . iter_quarters ( date_begin , date_end ) \uf0c1 Iterate over all quarters between begin and end dates. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Instances of datetime.date at the beginning of the quarters. This will include the quarter of the beginning date and of the end date. Source code in beancount/scripts/example.py def iter_quarters ( date_begin , date_end ): \"\"\"Iterate over all quarters between begin and end dates. Args: date_begin: The start date. date_end: The end date. Yields: Instances of datetime.date at the beginning of the quarters. This will include the quarter of the beginning date and of the end date. \"\"\" quarter = ( date_begin . year , ( date_begin . month - 1 ) // 3 ) quarter_last = ( date_end . year , ( date_end . month - 1 ) // 3 ) assert quarter <= quarter_last while True : year , trimester = quarter yield datetime . date ( year , trimester * 3 + 1 , 1 ) if quarter == quarter_last : break trimester = ( trimester + 1 ) % 4 if trimester == 0 : year += 1 quarter = ( year , trimester ) beancount . scripts . example . merge_postings ( entries , accounts ) \uf0c1 Merge all the postings from the given account names. Parameters: entries \u2013 A list of directives. accounts \u2013 A list of account strings to get the balances for. Yields: A list of TxnPosting's for all the accounts, in sorted order. Source code in beancount/scripts/example.py def merge_postings ( entries , accounts ): \"\"\"Merge all the postings from the given account names. Args: entries: A list of directives. accounts: A list of account strings to get the balances for. Yields: A list of TxnPosting's for all the accounts, in sorted order. \"\"\" real_root = realization . realize ( entries ) merged_postings = [] for account in accounts : real_account = realization . get ( real_root , account ) if real_account is None : continue merged_postings . extend ( txn_posting for txn_posting in real_account . txn_postings if isinstance ( txn_posting , data . TxnPosting ) ) merged_postings . sort ( key = lambda txn_posting : txn_posting . txn . date ) return merged_postings beancount . scripts . example . parse ( input_string ) \uf0c1 Parse some input string and assert no errors. This parse function does not just create the object, it also triggers local interpolation to fill in the missing amounts. Parameters: input_string \u2013 Beancount input text. Returns: A list of directive objects. Source code in beancount/scripts/example.py def parse ( input_string ): \"\"\"Parse some input string and assert no errors. This parse function does not just create the object, it also triggers local interpolation to fill in the missing amounts. Args: input_string: Beancount input text. Returns: A list of directive objects. \"\"\" entries , errors , options_map = parser . parse_string ( textwrap . dedent ( input_string )) if errors : printer . print_errors ( errors , file = sys . stderr ) raise ValueError ( \"Parsed text has errors\" ) # Interpolation. entries , unused_balance_errors = booking . book ( entries , options_map ) return data . sorted ( entries ) beancount . scripts . example . postings_for ( entries , accounts , before = False ) \uf0c1 Realize the entries and get the list of postings for the given accounts. All the non-Posting directives are already filtered out. Parameters: entries \u2013 A list of directives. accounts \u2013 A list of account strings to get the balances for. before \u2013 A boolean, if true, yield the balance before the position is applied. The default is to yield the balance after applying the position. Yields: Tuples of: posting: An instance of TxnPosting balances: A dict of Inventory balances for the given accounts after applying the posting. These inventory objects can be mutated to adjust the balance due to generated transactions to be applied later. Source code in beancount/scripts/example.py def postings_for ( entries , accounts , before = False ): \"\"\"Realize the entries and get the list of postings for the given accounts. All the non-Posting directives are already filtered out. Args: entries: A list of directives. accounts: A list of account strings to get the balances for. before: A boolean, if true, yield the balance before the position is applied. The default is to yield the balance after applying the position. Yields: Tuples of: posting: An instance of TxnPosting balances: A dict of Inventory balances for the given accounts _after_ applying the posting. These inventory objects can be mutated to adjust the balance due to generated transactions to be applied later. \"\"\" assert isinstance ( accounts , list ) merged_txn_postings = merge_postings ( entries , accounts ) balances = collections . defaultdict ( inventory . Inventory ) for txn_posting in merged_txn_postings : if before : yield txn_posting , balances posting = txn_posting . posting balances [ posting . account ] . add_position ( posting ) if not before : yield txn_posting , balances beancount . scripts . example . price_series ( start , mu , sigma ) \uf0c1 Generate a price series based on a simple stochastic model. Parameters: start \u2013 The beginning value. mu \u2013 The per-step drift, in units of value. sigma \u2013 Volatility of the changes. Yields: Floats, at each step. Source code in beancount/scripts/example.py def price_series ( start , mu , sigma ): \"\"\"Generate a price series based on a simple stochastic model. Args: start: The beginning value. mu: The per-step drift, in units of value. sigma: Volatility of the changes. Yields: Floats, at each step. \"\"\" value = start while 1 : yield value value += random . normalvariate ( mu , sigma ) * value beancount . scripts . example . replace ( string , replacements , strip = False ) \uf0c1 Apply word-boundaried regular expression replacements to an indented string. Parameters: string \u2013 Some input template string. replacements \u2013 A dict of regexp to replacement value. strip \u2013 A boolean, true if we should strip the input. Returns: The input string with the replacements applied to it, with the indentation removed. Source code in beancount/scripts/example.py def replace ( string , replacements , strip = False ): \"\"\"Apply word-boundaried regular expression replacements to an indented string. Args: string: Some input template string. replacements: A dict of regexp to replacement value. strip: A boolean, true if we should strip the input. Returns: The input string with the replacements applied to it, with the indentation removed. \"\"\" output = textwrap . dedent ( string ) if strip : output = output . strip () for from_ , to_ in replacements . items (): if not isinstance ( to_ , str ) and not callable ( to_ ): to_ = str ( to_ ) output = re . sub ( r \"\\b {} \\b\" . format ( from_ ), to_ , output ) return output beancount . scripts . example . validate_output ( contents , positive_accounts , currency ) \uf0c1 Check that the output file validates. Parameters: contents \u2013 A string, the output file. positive_accounts \u2013 A list of strings, account names to check for non-negative balances. currency \u2013 A string, the currency to check minimums for. Exceptions: AssertionError \u2013 If the output does not validate. Source code in beancount/scripts/example.py def validate_output ( contents , positive_accounts , currency ): \"\"\"Check that the output file validates. Args: contents: A string, the output file. positive_accounts: A list of strings, account names to check for non-negative balances. currency: A string, the currency to check minimums for. Raises: AssertionError: If the output does not validate. \"\"\" loaded_entries , _ , _ = loader . load_string ( contents , log_errors = sys . stderr , extra_validations = validation . HARDCORE_VALIDATIONS ) # Sanity checks: Check that the checking balance never goes below zero. for account in positive_accounts : check_non_negative ( loaded_entries , account , currency ) beancount . scripts . example . write_example_file ( date_birth , date_begin , date_end , reformat , file ) \uf0c1 Generate the example file. Parameters: date_birth \u2013 A datetime.date instance, the birth date of our character. date_begin \u2013 A datetime.date instance, the beginning date at which to generate transactions. date_end \u2013 A datetime.date instance, the end date at which to generate transactions. reformat \u2013 A boolean, true if we should apply global reformatting to this file. file \u2013 A file object, where to write out the output. Source code in beancount/scripts/example.py def write_example_file ( date_birth , date_begin , date_end , reformat , file ): \"\"\"Generate the example file. Args: date_birth: A datetime.date instance, the birth date of our character. date_begin: A datetime.date instance, the beginning date at which to generate transactions. date_end: A datetime.date instance, the end date at which to generate transactions. reformat: A boolean, true if we should apply global reformatting to this file. file: A file object, where to write out the output. \"\"\" # The following code entirely writes out the output to generic names, such # as \"Employer1\", \"Bank1\", and \"CCY\" (for principal currency). Those names # are purposely chosen to be unique, and only near the very end do we make # renamings to more specific and realistic names. # Name of the checking account. account_opening = \"Equity:Opening-Balances\" account_payable = \"Liabilities:AccountsPayable\" account_checking = \"Assets:CC:Bank1:Checking\" account_credit = \"Liabilities:CC:CreditCard1\" account_retirement = \"Assets:CC:Retirement\" account_investing = \"Assets:CC:Investment:Cash\" # Commodities. commodity_entries = generate_commodity_entries ( date_birth ) # Estimate the rent. rent_amount = round_to ( ANNUAL_SALARY / RENT_DIVISOR , RENT_INCREMENT ) # Get a random employer. employer_name , employer_address = random . choice ( EMPLOYERS ) logging . info ( \"Generating Salary Employment Income\" ) income_entries = generate_employment_income ( employer_name , employer_address , ANNUAL_SALARY , account_checking , join ( account_retirement , \"Cash\" ), date_begin , date_end , ) logging . info ( \"Generating Expenses from Banking Accounts\" ) banking_expenses = generate_banking_expenses ( date_begin , date_end , account_checking , rent_amount ) logging . info ( \"Generating Regular Expenses via Credit Card\" ) credit_regular_entries = generate_regular_credit_expenses ( date_birth , date_begin , date_end , account_credit , account_checking ) logging . info ( \"Generating Credit Card Expenses for Trips\" ) trip_entries = [] destinations = sorted ( TRIP_DESTINATIONS . items ()) destinations . extend ( destinations ) random . shuffle ( destinations ) for ( date_trip_begin , date_trip_end ), ( destination_name , config ) in zip ( compute_trip_dates ( date_begin , date_end ), destinations ): # Compute a suitable tag. tag = \"trip- {} - {} \" . format ( destination_name . lower () . replace ( \" \" , \"-\" ), date_trip_begin . year ) # logging.info(\"%s -- %s %s\", tag, date_trip_begin, date_trip_end) # Remove regular entries during this trip. credit_regular_entries = [ entry for entry in credit_regular_entries if not ( date_trip_begin <= entry . date < date_trip_end ) ] # Generate entries for the trip. this_trip_entries = generate_trip_entries ( date_trip_begin , date_trip_end , tag , config , destination_name . replace ( \"-\" , \" \" ) . title (), HOME_NAME , account_credit , ) trip_entries . extend ( this_trip_entries ) logging . info ( \"Generating Credit Card Payment Entries\" ) credit_payments = generate_clearing_entries ( delay_dates ( rrule . rrule ( rrule . MONTHLY , dtstart = date_begin , until = date_end , bymonthday = 7 ), 0 , 4 , ), \"CreditCard1\" , \"Paying off credit card\" , credit_regular_entries , account_credit , account_checking , ) credit_entries = credit_regular_entries + trip_entries + credit_payments logging . info ( \"Generating Tax Filings and Payments\" ) tax_preamble = generate_tax_preamble ( date_birth ) # Figure out all the years we need tax accounts for. years = set () for account_name in getters . get_accounts ( income_entries ): match = re . match ( r \"Expenses:Taxes:Y(\\d\\d\\d\\d)\" , account_name ) if match : years . add ( int ( match . group ( 1 ))) taxes = [( year , generate_tax_accounts ( year , date_end )) for year in sorted ( years )] tax_entries = tax_preamble + functools . reduce ( operator . add , ( entries for _ , entries in taxes ) ) logging . info ( \"Generating Opening of Banking Accounts\" ) # Open banking accounts and gift the checking account with a balance that # will offset all the amounts to ensure a positive balance throughout its # lifetime. entries_for_banking = data . sorted ( income_entries + banking_expenses + credit_entries + tax_entries ) minimum = get_minimum_balance ( entries_for_banking , account_checking , \"CCY\" ) banking_entries = generate_banking ( entries_for_banking , date_begin , date_end , max ( - minimum , ZERO ) ) logging . info ( \"Generating Transfers to Investment Account\" ) banking_transfers = generate_outgoing_transfers ( data . sorted ( income_entries + banking_entries + banking_expenses + credit_entries + tax_entries ), account_checking , account_investing , transfer_minimum = D ( \"200\" ), transfer_threshold = D ( \"3000\" ), transfer_increment = D ( \"500\" ), ) logging . info ( \"Generating Prices\" ) # Generate price entries for investment currencies and create a price map to # use for later for generating investment transactions. funds_allocation = { \"MFUND1\" : 0.40 , \"MFUND2\" : 0.60 } stocks = [ \"STK1\" , \"STK2\" , \"STK3\" , \"STK4\" ] price_entries = generate_prices ( date_begin , date_end , sorted ( funds_allocation . keys ()) + stocks , \"CCY\" ) price_map = prices . build_price_map ( price_entries ) logging . info ( \"Generating Employer Match Contribution\" ) account_match = \"Income:US:Employer1:Match401k\" retirement_match = generate_retirement_employer_match ( income_entries , join ( account_retirement , \"Cash\" ), account_match ) logging . info ( \"Generating Retirement Investments\" ) retirement_entries = generate_retirement_investments ( income_entries + retirement_match , account_retirement , sorted ( funds_allocation . items ()), price_map , ) logging . info ( \"Generating Taxes Investments\" ) investment_entries = generate_taxable_investment ( date_begin , date_end , banking_transfers , price_map , stocks ) logging . info ( \"Generating Expense Accounts\" ) expense_accounts_entries = generate_expense_accounts ( date_birth ) logging . info ( \"Generating Equity Accounts\" ) equity_entries = generate_open_entries ( date_birth , [ account_opening , account_payable ]) logging . info ( \"Generating Balance Checks\" ) credit_checks = generate_balance_checks ( credit_entries , account_credit , date_random_seq ( date_begin , date_end , 20 , 30 ) ) banking_checks = generate_balance_checks ( data . sorted ( income_entries + banking_entries + banking_expenses + banking_transfers + credit_entries + tax_entries ), account_checking , date_random_seq ( date_begin , date_end , 20 , 30 ), ) logging . info ( \"Outputting and Formatting Entries\" ) dcontext = display_context . DisplayContext () default_int_digits = 8 for currency , precision in { \"USD\" : 2 , \"CAD\" : 2 , \"VACHR\" : 0 , \"IRAUSD\" : 2 , \"VBMPX\" : 3 , \"RGAGX\" : 3 , \"ITOT\" : 0 , \"VEA\" : 0 , \"VHT\" : 0 , \"GLD\" : 0 , } . items (): int_digits = default_int_digits if precision > 0 : int_digits += 1 + precision dcontext . update ( D ( \"{{:0 {} . {} f}}\" . format ( int_digits , precision ) . format ( 0 )), currency ) output = io . StringIO () def output_section ( title , entries ): output . write ( \" \\n\\n\\n {} \\n\\n \" . format ( title )) printer . print_entries ( data . sorted ( entries ), dcontext , file = output ) output . write ( FILE_PREAMBLE . format ( ** locals ())) output_section ( \"* Commodities\" , commodity_entries ) output_section ( \"* Equity Accounts\" , equity_entries ) output_section ( \"* Banking\" , data . sorted ( banking_entries + banking_expenses + banking_transfers + banking_checks ), ) output_section ( \"* Credit-Cards\" , data . sorted ( credit_entries + credit_checks )) output_section ( \"* Taxable Investments\" , investment_entries ) output_section ( \"* Retirement Investments\" , data . sorted ( retirement_entries + retirement_match ) ) output_section ( \"* Sources of Income\" , income_entries ) output_section ( \"* Taxes\" , tax_preamble ) for year , entries in taxes : output_section ( \"** Tax Year {} \" . format ( year ), entries ) output_section ( \"* Expenses\" , expense_accounts_entries ) output_section ( \"* Prices\" , price_entries ) output_section ( \"* Cash\" , []) logging . info ( \"Contextualizing to Realistic Names\" ) contents , replacements = contextualize_file ( output . getvalue (), employer_name ) if reformat : contents = format . align_beancount ( contents ) logging . info ( \"Writing contents\" ) file . write ( contents ) logging . info ( \"Validating Results\" ) validate_output ( contents , [ replace ( account , replacements ) for account in [ account_checking ]], replace ( \"CCY\" , replacements ), ) beancount.scripts.format \uf0c1 beancount . scripts . format . align_beancount ( contents , prefix_width = None , num_width = None , currency_column = None ) \uf0c1 Reformat Beancount input to align all the numbers at the same column. Parameters: contents \u2013 A string, Beancount input syntax to reformat. prefix_width \u2013 An integer, the width in characters to render the account name to. If this is not specified, a good value is selected automatically from the contents of the file. num_width \u2013 An integer, the width to render each number. If this is not specified, a good value is selected automatically from the contents of the file. currency_column \u2013 An integer, the column at which to align the currencies. If given, this overrides the other options. Returns: A string, reformatted Beancount input with all the number aligned. No other changes than whitespace changes should be present between that return value and the input contents. Source code in beancount/scripts/format.py def align_beancount ( contents , prefix_width = None , num_width = None , currency_column = None ): \"\"\"Reformat Beancount input to align all the numbers at the same column. Args: contents: A string, Beancount input syntax to reformat. prefix_width: An integer, the width in characters to render the account name to. If this is not specified, a good value is selected automatically from the contents of the file. num_width: An integer, the width to render each number. If this is not specified, a good value is selected automatically from the contents of the file. currency_column: An integer, the column at which to align the currencies. If given, this overrides the other options. Returns: A string, reformatted Beancount input with all the number aligned. No other changes than whitespace changes should be present between that return value and the input contents. \"\"\" # Find all lines that have a number in them and calculate the maximum length # of the stripped prefix and the number. match_pairs = [] for line in contents . splitlines (): match = regex . match ( rf '(^\\d[^\";]*?|\\s+ { account . ACCOUNT_RE } )\\s+' rf \"( { PARENTHESIZED_BINARY_OP_RE } | { NUMBER_RE } )\\s+\" rf \"((?: { amount . CURRENCY_RE } )\\b.*)\" , line , ) if match : prefix , number , rest = match . groups () match_pairs . append (( prefix , number , rest )) else : match_pairs . append (( line , None , None )) # Normalize whitespace before lines that has some indent and an account # name. norm_match_pairs = normalize_indent_whitespace ( match_pairs ) if currency_column : output = io . StringIO () for prefix , number , rest in norm_match_pairs : if number is None : output . write ( prefix ) else : num_of_spaces = currency_column - len ( prefix ) - len ( number ) - 4 spaces = \" \" * num_of_spaces output . write ( prefix + spaces + \" \" + number + \" \" + rest ) output . write ( \" \\n \" ) return output . getvalue () # Compute the maximum widths. filtered_pairs = [ ( prefix , number ) for prefix , number , _ in match_pairs if number is not None ] if filtered_pairs : max_prefix_width = max ( len ( prefix ) for prefix , _ in filtered_pairs ) max_num_width = max ( len ( number ) for _ , number in filtered_pairs ) else : max_prefix_width = 0 max_num_width = 0 # Use user-supplied overrides, if available if prefix_width : max_prefix_width = prefix_width if num_width : max_num_width = num_width # Create a format that will admit the maximum width of all prefixes equally. line_format = \"{{:< {prefix_width} }} {{:> {num_width} }} {{}}\" . format ( prefix_width = max_prefix_width , num_width = max_num_width ) # Process each line to an output buffer. output = io . StringIO () for prefix , number , rest in norm_match_pairs : if number is None : output . write ( prefix ) else : output . write ( line_format . format ( prefix . rstrip (), number , rest )) output . write ( \" \\n \" ) formatted_contents = output . getvalue () # Ensure that the file before and after have only whitespace differences. # This is a sanity check, to make really sure we never change anything but whitespace, # so it's safe. # open('/tmp/before', 'w').write(regex.sub(r'[ \\t]+', ' ', contents)) # open('/tmp/after', 'w').write(regex.sub(r'[ \\t]+', ' ', formatted_contents)) old_stripped = regex . sub ( r \"[ \\t\\n]+\" , \" \" , contents . rstrip ()) new_stripped = regex . sub ( r \"[ \\t\\n]+\" , \" \" , formatted_contents . rstrip ()) assert old_stripped == new_stripped , ( old_stripped , new_stripped ) return formatted_contents beancount . scripts . format . compute_most_frequent ( iterable ) \uf0c1 Compute the frequencies of the given elements and return the most frequent. Parameters: iterable \u2013 A collection of hashable elements. Returns: The most frequent element. If there are no elements in the iterable, return None. Source code in beancount/scripts/format.py def compute_most_frequent ( iterable ): \"\"\"Compute the frequencies of the given elements and return the most frequent. Args: iterable: A collection of hashable elements. Returns: The most frequent element. If there are no elements in the iterable, return None. \"\"\" frequencies = collections . Counter ( iterable ) if not frequencies : return None counts = sorted (( count , element ) for element , count in frequencies . items ()) # Note: In case of a tie, this chooses the longest width. # We could eventually make this an option. return counts [ - 1 ][ 1 ] beancount . scripts . format . normalize_indent_whitespace ( match_pairs ) \uf0c1 Normalize whitespace before lines that has some indent and an account name. Parameters: match_pairs \u2013 A list of (prefix, number, rest) tuples. Returns: Another list of (prefix, number, rest) tuples, where prefix may have been adjusted with a different whitespace prefix. Source code in beancount/scripts/format.py def normalize_indent_whitespace ( match_pairs ): \"\"\"Normalize whitespace before lines that has some indent and an account name. Args: match_pairs: A list of (prefix, number, rest) tuples. Returns: Another list of (prefix, number, rest) tuples, where prefix may have been adjusted with a different whitespace prefix. \"\"\" # Compute most frequent account name prefix. match_posting = regex . compile ( r \"([ \\t]+)( {} .*)\" . format ( account . ACCOUNT_RE )) . match width = compute_most_frequent ( len ( match . group ( 1 )) for match in ( match_posting ( prefix ) for prefix , _ , _ in match_pairs ) if match is not None ) norm_format = \" \" * ( width or 0 ) + \" {} \" # Make the necessary adjustments. adjusted_pairs = [] for tup in match_pairs : prefix , number , rest = tup match = match_posting ( prefix ) if match is not None : tup = ( norm_format . format ( match . group ( 2 )), number , rest ) adjusted_pairs . append ( tup ) return adjusted_pairs","title":"beancount.scripts"},{"location":"api_reference/beancount.scripts.html#beancountscripts","text":"Implementation of the various scripts available from bin. This is structured this way because we want all the significant codes under a single directory, for analysis, grepping and unit testing.","title":"beancount.scripts"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps","text":"Check the installation dependencies and report the version numbers of each. This is meant to be used as an error diagnostic tool.","title":"deps"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.check_cdecimal","text":"Check that Python 3.3 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_cdecimal (): \"\"\"Check that Python 3.3 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" # Note: this code mirrors and should be kept in-sync with that at the top of # beancount.core.number. # Try the built-in installation. import decimal if is_fast_decimal ( decimal ): return ( \"cdecimal\" , \" {} (built-in)\" . format ( decimal . __version__ ), True ) # Try an explicitly installed version. try : import cdecimal if is_fast_decimal ( cdecimal ): return ( \"cdecimal\" , getattr ( cdecimal , \"__version__\" , \"OKAY\" ), True ) except ImportError : pass # Not found. return ( \"cdecimal\" , None , False )","title":"check_cdecimal()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.check_dependencies","text":"Check the runtime dependencies and report their version numbers. Returns: A list of pairs of (package-name, version-number, sufficient) whereby if a package has not been installed, its 'version-number' will be set to None. Otherwise, it will be a string with the version number in it. 'sufficient' will be True if the version if sufficient for this installation of Beancount. Source code in beancount/scripts/deps.py def check_dependencies (): \"\"\"Check the runtime dependencies and report their version numbers. Returns: A list of pairs of (package-name, version-number, sufficient) whereby if a package has not been installed, its 'version-number' will be set to None. Otherwise, it will be a string with the version number in it. 'sufficient' will be True if the version if sufficient for this installation of Beancount. \"\"\" return [ check_python (), check_cdecimal (), check_import ( \"dateutil\" ), ]","title":"check_dependencies()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.check_import","text":"Check that a particular module name is installed. Parameters: package_name \u2013 A string, the name of the package and module to be imported to verify this works. This should have a version attribute on it. min_version \u2013 If not None, a string, the minimum version number we require. module_name \u2013 The name of the module to import if it differs from the package name. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_import ( package_name , min_version = None , module_name = None ): \"\"\"Check that a particular module name is installed. Args: package_name: A string, the name of the package and module to be imported to verify this works. This should have a __version__ attribute on it. min_version: If not None, a string, the minimum version number we require. module_name: The name of the module to import if it differs from the package name. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" if module_name is None : module_name = package_name try : __import__ ( module_name ) module = sys . modules [ module_name ] if min_version is not None : version = module . __version__ assert isinstance ( version , str ) is_sufficient = ( parse_version ( version ) >= parse_version ( min_version ) if min_version else True ) else : version , is_sufficient = None , True except ImportError : version , is_sufficient = None , False return ( package_name , version , is_sufficient )","title":"check_import()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.check_python","text":"Check that Python 3.7 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_python (): \"\"\"Check that Python 3.7 or above is installed. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" return ( \"python3\" , \".\" . join ( map ( str , sys . version_info [: 3 ])), sys . version_info [: 2 ] >= ( 3 , 7 ), )","title":"check_python()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.check_python_magic","text":"Check that a recent-enough version of python-magic is installed. python-magic is an interface to libmagic, which is used by the 'file' tool and UNIX to identify file types. Note that there are two Python wrappers which provide the 'magic' import: python-magic and filemagic. The former is what we need, which appears to be more recently maintained. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). Source code in beancount/scripts/deps.py def check_python_magic (): \"\"\"Check that a recent-enough version of python-magic is installed. python-magic is an interface to libmagic, which is used by the 'file' tool and UNIX to identify file types. Note that there are two Python wrappers which provide the 'magic' import: python-magic and filemagic. The former is what we need, which appears to be more recently maintained. Returns: A triple of (package-name, version-number, sufficient) as per check_dependencies(). \"\"\" try : import magic # Check that python-magic and not filemagic is installed. if not hasattr ( magic , \"from_file\" ): # 'filemagic' is installed; install python-magic. raise ImportError return ( \"python-magic\" , \"OK\" , True ) except ( ImportError , OSError ): return ( \"python-magic\" , None , False )","title":"check_python_magic()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.is_fast_decimal","text":"Return true if a fast C decimal implementation is installed. Source code in beancount/scripts/deps.py def is_fast_decimal ( decimal_module ): \"Return true if a fast C decimal implementation is installed.\" return isinstance ( decimal_module . Decimal () . sqrt , types . BuiltinFunctionType )","title":"is_fast_decimal()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.list_dependencies","text":"Check the dependencies and produce a listing on the given file. Parameters: file \u2013 A file object to write the output to. Source code in beancount/scripts/deps.py def list_dependencies ( file = sys . stderr ): \"\"\"Check the dependencies and produce a listing on the given file. Args: file: A file object to write the output to. \"\"\" print ( \"Dependencies:\" ) for package , version , sufficient in check_dependencies (): print ( \" {:16} : {} {} \" . format ( package , version or \"NOT INSTALLED\" , \"(INSUFFICIENT)\" if version and not sufficient else \"\" , ), file = file , )","title":"list_dependencies()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.deps.parse_version","text":"Parse the version string into a comparable tuple. Source code in beancount/scripts/deps.py def parse_version ( version_str : str ) -> str : \"\"\"Parse the version string into a comparable tuple.\"\"\" return [ int ( v ) for v in version_str . split ( \".\" )]","title":"parse_version()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.directories","text":"Check that document directories mirror a list of accounts correctly.","title":"directories"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.directories.ValidateDirectoryError","text":"A directory validation error.","title":"ValidateDirectoryError"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.directories.validate_directories","text":"Validate a directory hierarchy against a ledger's account names. Read a ledger's list of account names and check that all the capitalized subdirectory names under the given roots match the account names. Parameters: entries \u2013 A list of directives. document_dirs \u2013 A list of string, the directory roots to walk and validate. Source code in beancount/scripts/directories.py def validate_directories ( entries , document_dirs ): \"\"\"Validate a directory hierarchy against a ledger's account names. Read a ledger's list of account names and check that all the capitalized subdirectory names under the given roots match the account names. Args: entries: A list of directives. document_dirs: A list of string, the directory roots to walk and validate. \"\"\" # Get the list of accounts declared in the ledge. accounts = getters . get_accounts ( entries ) # For each of the roots, validate the hierarchy of directories. for document_dir in document_dirs : errors = validate_directory ( accounts , document_dir ) for error in errors : print ( \"ERROR: {} \" . format ( error ))","title":"validate_directories()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.directories.validate_directory","text":"Check a directory hierarchy against a list of valid accounts. Walk the directory hierarchy, and for all directories with names matching that of accounts (with \":\" replaced with \"/\"), check that they refer to an account name declared in the given list. Parameters: account \u2013 A set or dict of account names. document_dir \u2013 A string, the root directory to walk and validate. Returns: An errors for each invalid directory name found. Source code in beancount/scripts/directories.py def validate_directory ( accounts , document_dir ): \"\"\"Check a directory hierarchy against a list of valid accounts. Walk the directory hierarchy, and for all directories with names matching that of accounts (with \":\" replaced with \"/\"), check that they refer to an account name declared in the given list. Args: account: A set or dict of account names. document_dir: A string, the root directory to walk and validate. Returns: An errors for each invalid directory name found. \"\"\" # Generate all parent accounts in the account_set we're checking against, so # that parent directories with no corresponding account don't warn. accounts_with_parents = set ( accounts ) for account_ in accounts : while True : parent = account . parent ( account_ ) if not parent : break if parent in accounts_with_parents : break accounts_with_parents . add ( parent ) account_ = parent errors = [] for directory , account_name , _ , _ in account . walk ( document_dir ): if account_name not in accounts_with_parents : errors . append ( ValidateDirectoryError ( \"Invalid directory ' {} ': no corresponding account ' {} '\" . format ( directory , account_name ) ) ) return errors","title":"validate_directory()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor","text":"Debugging tool for those finding bugs in Beancount. This tool is able to dump lexer/parser state, and will provide other services in the name of debugging.","title":"doctor"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.FileLocation","text":"","title":"FileLocation"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.FileLocation.convert","text":"Convert the value to the correct type. This is not called if the value is None (the missing value). This must accept string values from the command line, as well as values that are already the correct type. It may also convert other compatible types. The param and ctx arguments may be None in certain situations, such as when converting prompt input. If the value cannot be converted, call :meth: fail with a descriptive message. :param value: The value to convert. :param param: The parameter that is using this type to convert its value. May be None . :param ctx: The current context that arrived at this value. May be None . Source code in beancount/scripts/doctor.py def convert ( self , value , param , ctx ): match = re . match ( r \"(?:(.+):)?(\\d+)$\" , value ) if not match : self . fail ( \" {!r} is not a valid location\" . format ( value ), param , ctx ) filename , lineno = match . groups () if filename : filename = os . path . abspath ( filename ) return filename , int ( lineno )","title":"convert()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.FileRegion","text":"","title":"FileRegion"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.FileRegion.convert","text":"Convert the value to the correct type. This is not called if the value is None (the missing value). This must accept string values from the command line, as well as values that are already the correct type. It may also convert other compatible types. The param and ctx arguments may be None in certain situations, such as when converting prompt input. If the value cannot be converted, call :meth: fail with a descriptive message. :param value: The value to convert. :param param: The parameter that is using this type to convert its value. May be None . :param ctx: The current context that arrived at this value. May be None . Source code in beancount/scripts/doctor.py def convert ( self , value , param , ctx ): match = re . match ( r \"(?:(.+):)?(\\d+):(\\d+)$\" , value ) if not match : self . fail ( \" {!r} is not a valid region\" . format ( value ), param , ctx ) filename , start_lineno , end_lineno = match . groups () if filename : filename = os . path . abspath ( filename ) return filename , int ( start_lineno ), int ( end_lineno )","title":"convert()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.Group","text":"","title":"Group"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.Group.command","text":"A shortcut decorator for declaring and attaching a command to the group. This takes the same arguments as :func: command and immediately registers the created command with this group by calling :meth: add_command . To customize the command class used, set the :attr: command_class attribute. .. versionchanged:: 8.1 This decorator can be applied without parentheses. .. versionchanged:: 8.0 Added the :attr: command_class attribute. Source code in beancount/scripts/doctor.py def command ( self , * args , alias = None , ** kwargs ): wrap = click . Group . command ( self , * args , ** kwargs ) def decorator ( f ): cmd = wrap ( f ) if alias : self . aliases [ alias ] = cmd . name return cmd return decorator","title":"command()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.Group.get_command","text":"Given a context and a command name, this returns a :class: Command object if it exists or returns None . Source code in beancount/scripts/doctor.py def get_command ( self , ctx , cmd_name ): # aliases name = self . aliases . get ( cmd_name , cmd_name ) # allow to use '_' or '-' in command names. name = name . replace ( \"_\" , \"-\" ) return click . Group . get_command ( self , ctx , name )","title":"get_command()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.RenderError","text":"RenderError(source, message, entry)","title":"RenderError"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.RenderError.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/scripts/doctor.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.RenderError.__new__","text":"Create new instance of RenderError(source, message, entry)","title":"__new__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.RenderError.__repr__","text":"Return a nicely formatted representation string Source code in beancount/scripts/doctor.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.find_linked_entries","text":"Find all linked entries. Note that there is an option here: You can either just look at the links on the closest entry, or you can include the links of the linked transactions as well. Whichever one you want depends on how you use your links. Best would be to query the user (in Emacs) when there are many links present. Source code in beancount/scripts/doctor.py def find_linked_entries ( entries , links , follow_links : bool ): \"\"\"Find all linked entries. Note that there is an option here: You can either just look at the links on the closest entry, or you can include the links of the linked transactions as well. Whichever one you want depends on how you use your links. Best would be to query the user (in Emacs) when there are many links present. \"\"\" linked_entries = [] if not follow_links : linked_entries = [ entry for entry in entries if ( isinstance ( entry , data . Transaction ) and entry . links and entry . links & links ) ] else : links = set ( links ) linked_entries = [] while True : num_linked = len ( linked_entries ) linked_entries = [ entry for entry in entries if ( isinstance ( entry , data . Transaction ) and entry . links and entry . links & links ) ] if len ( linked_entries ) == num_linked : break for entry in linked_entries : if entry . links : links . update ( entry . links ) return linked_entries","title":"find_linked_entries()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.find_tagged_entries","text":"Find all entries with the given tag. Source code in beancount/scripts/doctor.py def find_tagged_entries ( entries , tag ): \"\"\"Find all entries with the given tag.\"\"\" return [ entry for entry in entries if ( isinstance ( entry , data . Transaction ) and entry . tags and tag in entry . tags ) ]","title":"find_tagged_entries()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.render_mini_balances","text":"Render a treeified list of the balances for the given transactions. Parameters: entries \u2013 A list of selected transactions to render. options_map \u2013 The parsed options. conversion \u2013 Conversion method string, None, 'value' or 'cost'. price_map \u2013 A price map from the original entries. If this isn't provided, the inventories are rendered directly. If it is, their contents are converted to market value. Source code in beancount/scripts/doctor.py def render_mini_balances ( entries , options_map , conversion = None , price_map = None ): \"\"\"Render a treeified list of the balances for the given transactions. Args: entries: A list of selected transactions to render. options_map: The parsed options. conversion: Conversion method string, None, 'value' or 'cost'. price_map: A price map from the original entries. If this isn't provided, the inventories are rendered directly. If it is, their contents are converted to market value. \"\"\" # Render linked entries (in date order) as errors (for Emacs). errors = [ RenderError ( entry . meta , \"\" , entry ) for entry in entries ] printer . print_errors ( errors ) # Print out balances. real_root = realization . realize ( entries ) dformat = options_map [ \"dcontext\" ] . build ( alignment = Align . DOT , reserved = 2 ) # TODO(blais): I always want to be able to convert at cost. We need # arguments capability. # # TODO(blais): Ideally this conversion inserts a new transactions to # 'Unrealized' to account for the difference between cost and market value. # Insert one and update the realization. Add an update() method to the # realization, given a transaction. acctypes = options . get_account_types ( options_map ) if conversion == \"value\" : assert price_map is not None # Warning: Mutate the inventories in-place, converting them to market # value. balance_diff = inventory . Inventory () for real_account in realization . iter_children ( real_root ): balance_cost = real_account . balance . reduce ( convert . get_cost ) balance_value = real_account . balance . reduce ( convert . get_value , price_map ) real_account . balance = balance_value balance_diff . add_inventory ( balance_cost ) balance_diff . add_inventory ( - balance_value ) if not balance_diff . is_empty (): account_unrealized = account . join ( acctypes . income , options_map [ \"account_unrealized_gains\" ] ) unrealized = realization . get_or_create ( real_root , account_unrealized ) unrealized . balance . add_inventory ( balance_diff ) elif conversion == \"cost\" : for real_account in realization . iter_children ( real_root ): real_account . balance = real_account . balance . reduce ( convert . get_cost ) realization . dump_balances ( real_root , dformat , file = sys . stdout ) # Print out net income change. net_income = inventory . Inventory () for real_node in realization . iter_children ( real_root ): if account_types . is_income_statement_account ( real_node . account , acctypes ): net_income . add_inventory ( real_node . balance ) print () print ( \"Net Income: {} \" . format ( - net_income ))","title":"render_mini_balances()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.doctor.resolve_region_to_entries","text":"Resolve a filename and region to a list of entries. Source code in beancount/scripts/doctor.py def resolve_region_to_entries ( entries : List [ data . Entries ], filename : str , region : Tuple [ str , int , int ] ) -> List [ data . Entries ]: \"\"\"Resolve a filename and region to a list of entries.\"\"\" search_filename , first_lineno , last_lineno = region if search_filename is None : search_filename = filename # Find all the entries in the region. (To be clear, this isn't like the # 'linked' command, none of the links are followed.) region_entries = [ entry for entry in data . filter_txns ( entries ) if ( entry . meta [ \"filename\" ] == search_filename and first_lineno <= entry . meta [ \"lineno\" ] <= last_lineno ) ] return region_entries","title":"resolve_region_to_entries()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example","text":"","title":"example"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.LiberalDate","text":"","title":"LiberalDate"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.LiberalDate.convert","text":"Convert the value to the correct type. This is not called if the value is None (the missing value). This must accept string values from the command line, as well as values that are already the correct type. It may also convert other compatible types. The param and ctx arguments may be None in certain situations, such as when converting prompt input. If the value cannot be converted, call :meth: fail with a descriptive message. :param value: The value to convert. :param param: The parameter that is using this type to convert its value. May be None . :param ctx: The current context that arrived at this value. May be None . Source code in beancount/scripts/example.py def convert ( self , value , param , ctx ): try : date_utils . parse_date_liberally ( value ) except ValueError : self . fail ( \" {!r} is not a valid date\" . format ( value ), param , ctx )","title":"convert()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.check_non_negative","text":"Check that the balance of the given account never goes negative. Parameters: entries \u2013 A list of directives. account \u2013 An account string, the account to check the balance for. currency \u2013 A string, the currency to check minimums for. Exceptions: AssertionError \u2013 if the balance goes negative. Source code in beancount/scripts/example.py def check_non_negative ( entries , account , currency ): \"\"\"Check that the balance of the given account never goes negative. Args: entries: A list of directives. account: An account string, the account to check the balance for. currency: A string, the currency to check minimums for. Raises: AssertionError: if the balance goes negative. \"\"\" previous_date = None for txn_posting , balances in postings_for ( data . sorted ( entries ), [ account ], before = True ): balance = balances [ account ] date = txn_posting . txn . date if date != previous_date : assert all ( pos . units . number >= ZERO for pos in balance . get_positions () ), \"Negative balance: {} at: {} \" . format ( balance , txn_posting . txn . date ) previous_date = date","title":"check_non_negative()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.compute_trip_dates","text":"Generate dates at reasonable intervals for trips during the given time period. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Pairs of dates for the trips within the period. Source code in beancount/scripts/example.py def compute_trip_dates ( date_begin , date_end ): \"\"\"Generate dates at reasonable intervals for trips during the given time period. Args: date_begin: The start date. date_end: The end date. Yields: Pairs of dates for the trips within the period. \"\"\" # Min and max number of days remaining at home. days_at_home = ( 4 * 30 , 13 * 30 ) # Length of trip. days_trip = ( 8 , 22 ) # Number of days to ensure no trip at the beginning and the end. days_buffer = 21 date_begin += datetime . timedelta ( days = days_buffer ) date_end -= datetime . timedelta ( days = days_buffer ) date = date_begin while 1 : duration_at_home = datetime . timedelta ( days = random . randint ( * days_at_home )) duration_trip = datetime . timedelta ( days = random . randint ( * days_trip )) date_trip_begin = date + duration_at_home date_trip_end = date_trip_begin + duration_trip if date_trip_end >= date_end : break yield ( date_trip_begin , date_trip_end ) date = date_trip_end","title":"compute_trip_dates()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.contextualize_file","text":"Replace generic strings in the generated file with realistic strings. Parameters: contents \u2013 A string, the generic file contents. Returns: A string, the contextualized version. Source code in beancount/scripts/example.py def contextualize_file ( contents , employer ): \"\"\"Replace generic strings in the generated file with realistic strings. Args: contents: A string, the generic file contents. Returns: A string, the contextualized version. \"\"\" replacements = { \"CC\" : \"US\" , \"Bank1\" : \"BofA\" , \"Bank1_Institution\" : \"Bank of America\" , \"Bank1_Address\" : \"123 America Street, LargeTown, USA\" , \"Bank1_Phone\" : \"+1.012.345.6789\" , \"CreditCard1\" : \"Chase:Slate\" , \"CreditCard2\" : \"Amex:BlueCash\" , \"Employer1\" : employer , \"Retirement\" : \"Vanguard\" , \"Retirement_Institution\" : \"Vanguard Group\" , \"Retirement_Address\" : \"P.O. Box 1110, Valley Forge, PA 19482-1110\" , \"Retirement_Phone\" : \"+1.800.523.1188\" , \"Investment\" : \"ETrade\" , # Commodities \"CCY\" : \"USD\" , \"VACHR\" : \"VACHR\" , \"DEFCCY\" : \"IRAUSD\" , \"MFUND1\" : \"VBMPX\" , \"MFUND2\" : \"RGAGX\" , \"STK1\" : \"ITOT\" , \"STK2\" : \"VEA\" , \"STK3\" : \"VHT\" , \"STK4\" : \"GLD\" , } new_contents = replace ( contents , replacements ) return new_contents , replacements","title":"contextualize_file()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.date_iter","text":"Generate a sequence of dates. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Instances of datetime.date. Source code in beancount/scripts/example.py def date_iter ( date_begin , date_end ): \"\"\"Generate a sequence of dates. Args: date_begin: The start date. date_end: The end date. Yields: Instances of datetime.date. \"\"\" assert date_begin <= date_end date = date_begin one_day = datetime . timedelta ( days = 1 ) while date < date_end : date += one_day yield date","title":"date_iter()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.date_random_seq","text":"Generate a sequence of dates with some random increase in days. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. days_min \u2013 The minimum number of days to advance on each iteration. days_max \u2013 The maximum number of days to advance on each iteration. Yields: Instances of datetime.date. Source code in beancount/scripts/example.py def date_random_seq ( date_begin , date_end , days_min , days_max ): \"\"\"Generate a sequence of dates with some random increase in days. Args: date_begin: The start date. date_end: The end date. days_min: The minimum number of days to advance on each iteration. days_max: The maximum number of days to advance on each iteration. Yields: Instances of datetime.date. \"\"\" assert days_min > 0 assert days_min <= days_max date = date_begin while date < date_end : nb_days_forward = random . randint ( days_min , days_max ) date += datetime . timedelta ( days = nb_days_forward ) if date >= date_end : break yield date","title":"date_random_seq()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.delay_dates","text":"Delay the dates from the given iterator by some uniformly drawn number of days. Parameters: date_iter \u2013 An iterator of datetime.date instances. delay_days_min \u2013 The minimum amount of advance days for the transaction. delay_days_max \u2013 The maximum amount of advance days for the transaction. Yields: datetime.date instances. Source code in beancount/scripts/example.py def delay_dates ( date_iter , delay_days_min , delay_days_max ): \"\"\"Delay the dates from the given iterator by some uniformly drawn number of days. Args: date_iter: An iterator of datetime.date instances. delay_days_min: The minimum amount of advance days for the transaction. delay_days_max: The maximum amount of advance days for the transaction. Yields: datetime.date instances. \"\"\" dates = list ( date_iter ) last_date = dates [ - 1 ] last_date = last_date . date () if isinstance ( last_date , datetime . datetime ) else last_date for dtime in dates : date = dtime . date () if isinstance ( dtime , datetime . datetime ) else dtime date += datetime . timedelta ( days = random . randint ( delay_days_min , delay_days_max )) if date >= last_date : break yield date","title":"delay_dates()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_balance_checks","text":"Generate balance check entries to the given frequency. Parameters: entries \u2013 A list of directives that contain all the transactions for the accounts. account \u2013 The name of the account for which to generate. date_iter \u2013 Iterator of dates. We generate balance checks at these dates. Returns: A list of balance check entries. Source code in beancount/scripts/example.py def generate_balance_checks ( entries , account , date_iter ): \"\"\"Generate balance check entries to the given frequency. Args: entries: A list of directives that contain all the transactions for the accounts. account: The name of the account for which to generate. date_iter: Iterator of dates. We generate balance checks at these dates. Returns: A list of balance check entries. \"\"\" balance_checks = [] date_iter = iter ( date_iter ) next_date = next ( date_iter ) with misc_utils . swallow ( StopIteration ): for txn_posting , balance in postings_for ( entries , [ account ], before = True ): while txn_posting . txn . date >= next_date : amount = balance [ account ] . get_currency_units ( \"CCY\" ) . number balance_checks . extend ( parse ( f \"\"\" { next_date } balance { account } { amount } CCY \"\"\" ) ) next_date = next ( date_iter ) return balance_checks","title":"generate_balance_checks()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_banking","text":"Generate a checking account opening. Parameters: entries \u2013 A list of entries which affect this account. date_begin \u2013 A date instance, the beginning date. date_end \u2013 A date instance, the end date. amount_initial \u2013 A Decimal instance, the amount to initialize the checking account with. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_banking ( entries , date_begin , date_end , amount_initial ): \"\"\"Generate a checking account opening. Args: entries: A list of entries which affect this account. date_begin: A date instance, the beginning date. date_end: A date instance, the end date. amount_initial: A Decimal instance, the amount to initialize the checking account with. Returns: A list of directives. \"\"\" amount_initial_neg = - amount_initial new_entries = parse ( f \"\"\" { date_begin } open Assets:CC:Bank1 institution: \"Bank1_Institution\" address: \"Bank1_Address\" phone: \"Bank1_Phone\" { date_begin } open Assets:CC:Bank1:Checking CCY account: \"00234-48574897\" ;; { date_begin } open Assets:CC:Bank1:Savings CCY { date_begin } * \"Opening Balance for checking account\" Assets:CC:Bank1:Checking { amount_initial } CCY Equity:Opening-Balances { amount_initial_neg } CCY \"\"\" ) date_balance = date_begin + datetime . timedelta ( days = 1 ) account = \"Assets:CC:Bank1:Checking\" for txn_posting , balances in postings_for ( data . sorted ( entries + new_entries ), [ account ], before = True ): if txn_posting . txn . date >= date_balance : break amount_balance = balances [ account ] . get_currency_units ( \"CCY\" ) . number bal_entries = parse ( f \"\"\" { date_balance } balance Assets:CC:Bank1:Checking { amount_balance } CCY \"\"\" ) return new_entries + bal_entries","title":"generate_banking()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_banking_expenses","text":"Generate expenses paid out of a checking account, typically living expenses. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. account \u2013 The checking account to generate expenses to. rent_amount \u2013 The amount of rent. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_banking_expenses ( date_begin , date_end , account , rent_amount ): \"\"\"Generate expenses paid out of a checking account, typically living expenses. Args: date_begin: The start date. date_end: The end date. account: The checking account to generate expenses to. rent_amount: The amount of rent. Returns: A list of directives. \"\"\" fee_expenses = generate_periodic_expenses ( rrule . rrule ( rrule . MONTHLY , bymonthday = 4 , dtstart = date_begin , until = date_end ), \"BANK FEES\" , \"Monthly bank fee\" , account , \"Expenses:Financial:Fees\" , lambda : D ( \"4.00\" ), ) rent_expenses = generate_periodic_expenses ( delay_dates ( rrule . rrule ( rrule . MONTHLY , dtstart = date_begin , until = date_end ), 2 , 5 ), \"RiverBank Properties\" , \"Paying the rent\" , account , \"Expenses:Home:Rent\" , lambda : random . normalvariate ( float ( rent_amount ), 0 ), ) electricity_expenses = generate_periodic_expenses ( delay_dates ( rrule . rrule ( rrule . MONTHLY , dtstart = date_begin , until = date_end ), 7 , 8 ), \"EDISON POWER\" , \"\" , account , \"Expenses:Home:Electricity\" , lambda : random . normalvariate ( 65 , 0 ), ) internet_expenses = generate_periodic_expenses ( delay_dates ( rrule . rrule ( rrule . MONTHLY , dtstart = date_begin , until = date_end ), 20 , 22 ), \"Wine-Tarner Cable\" , \"\" , account , \"Expenses:Home:Internet\" , lambda : random . normalvariate ( 80 , 0.10 ), ) phone_expenses = generate_periodic_expenses ( delay_dates ( rrule . rrule ( rrule . MONTHLY , dtstart = date_begin , until = date_end ), 17 , 19 ), \"Verizon Wireless\" , \"\" , account , \"Expenses:Home:Phone\" , lambda : random . normalvariate ( 60 , 10 ), ) return data . sorted ( fee_expenses + rent_expenses + electricity_expenses + internet_expenses + phone_expenses )","title":"generate_banking_expenses()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_clearing_entries","text":"Generate entries to clear the value of an account. Parameters: date_iter \u2013 An iterator of datetime.date instances. payee \u2013 A string, the payee name to use on the transactions. narration \u2013 A string, the narration to use on the transactions. entries \u2013 A list of entries. account_clear \u2013 The account to clear. account_from \u2013 The source account to clear 'account_clear' from. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_clearing_entries ( date_iter , payee , narration , entries , account_clear , account_from ): \"\"\"Generate entries to clear the value of an account. Args: date_iter: An iterator of datetime.date instances. payee: A string, the payee name to use on the transactions. narration: A string, the narration to use on the transactions. entries: A list of entries. account_clear: The account to clear. account_from: The source account to clear 'account_clear' from. Returns: A list of directives. \"\"\" new_entries = [] # The next date we're looking for. date_iter = iter ( date_iter ) next_date = next ( date_iter , None ) if not next_date : return new_entries # Iterate over all the postings of the account to clear. for txn_posting , balances in postings_for ( entries , [ account_clear ]): balance_clear = balances [ account_clear ] # Check if we need to clear. if next_date <= txn_posting . txn . date : pos_amount = balance_clear . get_currency_units ( \"CCY\" ) neg_amount = - pos_amount new_entries . extend ( parse ( f \"\"\" { next_date } * \" { payee } \" \" { narration } \" { account_clear } { neg_amount . number : .2f } CCY { account_from } { pos_amount . number : .2f } CCY \"\"\" ) ) balance_clear . add_amount ( neg_amount ) # Advance to the next date we're looking for. next_date = next ( date_iter , None ) if not next_date : break return new_entries","title":"generate_clearing_entries()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_commodity_entries","text":"Create a list of Commodity entries for all the currencies we're using. Parameters: date_birth \u2013 A datetime.date instance, the date of birth of the user. Returns: A list of Commodity entries for all the commodities in use. Source code in beancount/scripts/example.py def generate_commodity_entries ( date_birth ): \"\"\"Create a list of Commodity entries for all the currencies we're using. Args: date_birth: A datetime.date instance, the date of birth of the user. Returns: A list of Commodity entries for all the commodities in use. \"\"\" return parse ( f \"\"\" 1792-01-01 commodity USD name: \"US Dollar\" export: \"CASH\" { date_birth } commodity VACHR name: \"Employer Vacation Hours\" export: \"IGNORE\" { date_birth } commodity IRAUSD name: \"US 401k and IRA Contributions\" export: \"IGNORE\" 2009-05-01 commodity RGAGX name: \"American Funds The Growth Fund of America Class R-6\" export: \"MUTF:RGAGX\" price: \"USD:google/MUTF:RGAGX\" 1995-09-18 commodity VBMPX name: \"Vanguard Total Bond Market Index Fund Institutional Plus Shares\" export: \"MUTF:VBMPX\" price: \"USD:google/MUTF:VBMPX\" 2004-01-20 commodity ITOT name: \"iShares Core S&P Total U.S. Stock Market ETF\" export: \"NYSEARCA:ITOT\" price: \"USD:google/NYSEARCA:ITOT\" 2007-07-20 commodity VEA name: \"Vanguard FTSE Developed Markets ETF\" export: \"NYSEARCA:VEA\" price: \"USD:google/NYSEARCA:VEA\" 2004-01-26 commodity VHT name: \"Vanguard Health Care ETF\" export: \"NYSEARCA:VHT\" price: \"USD:google/NYSEARCA:VHT\" 2004-11-01 commodity GLD name: \"SPDR Gold Trust (ETF)\" export: \"NYSEARCA:GLD\" price: \"USD:google/NYSEARCA:GLD\" 1900-01-01 commodity VMMXX export: \"MUTF:VMMXX (MONEY:USD)\" \"\"\" )","title":"generate_commodity_entries()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_employment_income","text":"Generate bi-weekly entries for payroll salary income. Parameters: employer_name \u2013 A string, the human-readable name of the employer. employer_address \u2013 A string, the address of the employer. annual_salary \u2013 A Decimal, the annual salary of the employee. account_deposit \u2013 An account string, the account to deposit the salary to. account_retirement \u2013 An account string, the account to deposit retirement contributions to. date_begin \u2013 The start date. date_end \u2013 The end date. Returns: A list of directives, including open directives for the account. Source code in beancount/scripts/example.py def generate_employment_income ( employer_name , employer_address , annual_salary , account_deposit , account_retirement , date_begin , date_end , ): \"\"\"Generate bi-weekly entries for payroll salary income. Args: employer_name: A string, the human-readable name of the employer. employer_address: A string, the address of the employer. annual_salary: A Decimal, the annual salary of the employee. account_deposit: An account string, the account to deposit the salary to. account_retirement: An account string, the account to deposit retirement contributions to. date_begin: The start date. date_end: The end date. Returns: A list of directives, including open directives for the account. \"\"\" preamble = parse ( f \"\"\" { date_begin } event \"employer\" \" { employer_name } , { employer_address } \" { date_begin } open Income:CC:Employer1:Salary CCY ; { date_begin } open Income:CC:Employer1:AnnualBonus CCY { date_begin } open Income:CC:Employer1:GroupTermLife CCY { date_begin } open Income:CC:Employer1:Vacation VACHR { date_begin } open Assets:CC:Employer1:Vacation VACHR { date_begin } open Expenses:Vacation VACHR { date_begin } open Expenses:Health:Life:GroupTermLife { date_begin } open Expenses:Health:Medical:Insurance { date_begin } open Expenses:Health:Dental:Insurance { date_begin } open Expenses:Health:Vision:Insurance ; { date_begin } open Expenses:Vacation:Employer \"\"\" ) date_prev = None contrib_retirement = ZERO contrib_socsec = ZERO biweekly_pay = annual_salary / 26 gross = biweekly_pay medicare = gross * D ( \"0.0231\" ) federal = gross * D ( \"0.2303\" ) state = gross * D ( \"0.0791\" ) city = gross * D ( \"0.0379\" ) sdi = D ( \"1.12\" ) lifeinsurance = D ( \"24.32\" ) dental = D ( \"2.90\" ) medical = D ( \"27.38\" ) vision = D ( \"42.30\" ) fixed = medicare + federal + state + city + sdi + dental + medical + vision # Calculate vacation hours per-pay. with decimal . localcontext () as ctx : ctx . prec = 4 vacation_hrs = ( ANNUAL_VACATION_DAYS * D ( \"8\" )) / D ( \"26\" ) transactions = [] for dtime in misc_utils . skipiter ( rrule . rrule ( rrule . WEEKLY , byweekday = rrule . TH , dtstart = date_begin , until = date_end ), 2 ): date = dtime . date () year = date . year if not date_prev or date_prev . year != date . year : contrib_retirement = RETIREMENT_LIMITS . get ( date . year , RETIREMENT_LIMITS [ None ]) contrib_socsec = D ( \"7000\" ) date_prev = date retirement_uncapped = math . ceil (( gross * D ( \"0.25\" )) / 100 ) * 100 retirement = min ( contrib_retirement , retirement_uncapped ) contrib_retirement -= retirement socsec_uncapped = gross * D ( \"0.0610\" ) socsec = min ( contrib_socsec , socsec_uncapped ) contrib_socsec -= socsec with decimal . localcontext () as ctx : ctx . prec = 6 deposit = gross - retirement - fixed - socsec retirement_neg = - retirement gross_neg = - gross lifeinsurance_neg = - lifeinsurance vacation_hrs_neg = - vacation_hrs transactions . extend ( parse ( f \"\"\" { date } * \" { employer_name } \" \"Payroll\" { account_deposit } { deposit : .2f } CCY \"\"\" + ( \"\" if retirement == ZERO else f \"\"\" \\ { account_retirement } { retirement : .2f } CCY Assets:CC:Federal:PreTax401k { retirement_neg : .2f } DEFCCY Expenses:Taxes:Y { year } :CC:Federal:PreTax401k { retirement : .2f } DEFCCY \"\"\" ) + f \"\"\" \\ Income:CC:Employer1:Salary { gross_neg : .2f } CCY Income:CC:Employer1:GroupTermLife { lifeinsurance_neg : .2f } CCY Expenses:Health:Life:GroupTermLife { lifeinsurance : .2f } CCY Expenses:Health:Dental:Insurance { dental } CCY Expenses:Health:Medical:Insurance { medical } CCY Expenses:Health:Vision:Insurance { vision } CCY Expenses:Taxes:Y { year } :CC:Medicare { medicare : .2f } CCY Expenses:Taxes:Y { year } :CC:Federal { federal : .2f } CCY Expenses:Taxes:Y { year } :CC:State { state : .2f } CCY Expenses:Taxes:Y { year } :CC:CityNYC { city : .2f } CCY Expenses:Taxes:Y { year } :CC:SDI { sdi : .2f } CCY Expenses:Taxes:Y { year } :CC:SocSec { socsec : .2f } CCY Assets:CC:Employer1:Vacation { vacation_hrs : .2f } VACHR Income:CC:Employer1:Vacation { vacation_hrs_neg : .2f } VACHR \"\"\" ) ) return preamble + transactions","title":"generate_employment_income()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_expense_accounts","text":"Generate directives for expense accounts. Parameters: date_birth \u2013 Birth date of the character. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_expense_accounts ( date_birth ): \"\"\"Generate directives for expense accounts. Args: date_birth: Birth date of the character. Returns: A list of directives. \"\"\" return parse ( f \"\"\" { date_birth } open Expenses:Food:Groceries { date_birth } open Expenses:Food:Restaurant { date_birth } open Expenses:Food:Coffee { date_birth } open Expenses:Food:Alcohol { date_birth } open Expenses:Transport:Tram { date_birth } open Expenses:Home:Rent { date_birth } open Expenses:Home:Electricity { date_birth } open Expenses:Home:Internet { date_birth } open Expenses:Home:Phone { date_birth } open Expenses:Financial:Fees { date_birth } open Expenses:Financial:Commissions \"\"\" )","title":"generate_expense_accounts()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_open_entries","text":"Generate a list of Open entries for the given accounts: Parameters: date \u2013 A datetime.date instance for the open entries. accounts \u2013 A list of account strings. currency \u2013 An optional currency constraint. Returns: A list of Open directives. Source code in beancount/scripts/example.py def generate_open_entries ( date , accounts , currency = None ): \"\"\"Generate a list of Open entries for the given accounts: Args: date: A datetime.date instance for the open entries. accounts: A list of account strings. currency: An optional currency constraint. Returns: A list of Open directives. \"\"\" assert isinstance ( accounts , ( list , tuple )) return parse ( \"\" . join ( \" {date} open {account} {currency} \\n \" . format ( date = date , account = account , currency = currency or \"\" ) for account in accounts ) )","title":"generate_open_entries()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_outgoing_transfers","text":"Generate transfers of accumulated funds out of an account. This monitors the balance of an account and when it is beyond a threshold, generate out transfers form that account to another account. Parameters: entries \u2013 A list of existing entries that affect this account so far. The generated entries will also affect this account. account \u2013 An account string, the account to monitor. account_out \u2013 An account string, the savings account to make transfers to. transfer_minimum \u2013 The minimum amount of funds to always leave in this account after a transfer. transfer_threshold \u2013 The minimum amount of funds to be able to transfer out without breaking the minimum. transfer_increment \u2013 A Decimal, the increment to round transfers to. Returns: A list of new directives, the transfers to add to the given account. Source code in beancount/scripts/example.py def generate_outgoing_transfers ( entries , account , account_out , transfer_minimum , transfer_threshold , transfer_increment ): \"\"\"Generate transfers of accumulated funds out of an account. This monitors the balance of an account and when it is beyond a threshold, generate out transfers form that account to another account. Args: entries: A list of existing entries that affect this account so far. The generated entries will also affect this account. account: An account string, the account to monitor. account_out: An account string, the savings account to make transfers to. transfer_minimum: The minimum amount of funds to always leave in this account after a transfer. transfer_threshold: The minimum amount of funds to be able to transfer out without breaking the minimum. transfer_increment: A Decimal, the increment to round transfers to. Returns: A list of new directives, the transfers to add to the given account. \"\"\" last_date = entries [ - 1 ] . date # Reverse the balance amounts taking into account the minimum balance for # all time in the future. amounts = [ ( balances [ account ] . get_currency_units ( \"CCY\" ) . number , txn_posting ) for txn_posting , balances in postings_for ( entries , [ account ]) ] reversed_amounts = [] last_amount , _ = amounts [ - 1 ] for current_amount , _ in reversed ( amounts ): if current_amount < last_amount : reversed_amounts . append ( current_amount ) last_amount = current_amount else : reversed_amounts . append ( last_amount ) capped_amounts = reversed ( reversed_amounts ) # Create transfers outward where the future allows it. new_entries = [] offset_amount = ZERO for current_amount , ( _ , txn_posting ) in zip ( capped_amounts , amounts ): if txn_posting . txn . date >= last_date : break adjusted_amount = current_amount - offset_amount if adjusted_amount > ( transfer_minimum + transfer_threshold ): amount_transfer = round_to ( adjusted_amount - transfer_minimum , transfer_increment ) date = txn_posting . txn . date + datetime . timedelta ( days = 1 ) amount_transfer_neg = - amount_transfer new_entries . extend ( parse ( f \"\"\" { date } * \"Transfering accumulated savings to other account\" { account } { amount_transfer_neg : 2f } CCY { account_out } { amount_transfer : 2f } CCY \"\"\" ) ) offset_amount += amount_transfer return new_entries","title":"generate_outgoing_transfers()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_periodic_expenses","text":"Generate periodic expense transactions. Parameters: date_iter \u2013 An iterator for dates or datetimes. payee \u2013 A string, the payee name to use on the transactions, or a set of such strings to randomly choose from narration \u2013 A string, the narration to use on the transactions. account_from \u2013 An account string the debited account. account_to \u2013 An account string the credited account. amount_generator \u2013 A callable object to generate variates. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_periodic_expenses ( date_iter , payee , narration , account_from , account_to , amount_generator ): \"\"\"Generate periodic expense transactions. Args: date_iter: An iterator for dates or datetimes. payee: A string, the payee name to use on the transactions, or a set of such strings to randomly choose from narration: A string, the narration to use on the transactions. account_from: An account string the debited account. account_to: An account string the credited account. amount_generator: A callable object to generate variates. Returns: A list of directives. \"\"\" new_entries = [] for dtime in date_iter : date = dtime . date () if isinstance ( dtime , datetime . datetime ) else dtime amount = D ( amount_generator ()) txn_payee = payee if isinstance ( payee , str ) else random . choice ( payee ) txn_narration = ( narration if isinstance ( narration , str ) else random . choice ( narration ) ) amount_neg = - amount new_entries . extend ( parse ( f \"\"\" { date } * \" { txn_payee } \" \" { txn_narration } \" { account_from } { amount_neg : .2f } CCY { account_to } { amount : .2f } CCY \"\"\" ) ) return new_entries","title":"generate_periodic_expenses()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_prices","text":"Generate weekly or monthly price entries for the given currencies. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. currencies \u2013 A list of currency strings to generate prices for. cost_currency \u2013 A string, the cost currency. Returns: A list of Price directives. Source code in beancount/scripts/example.py def generate_prices ( date_begin , date_end , currencies , cost_currency ): \"\"\"Generate weekly or monthly price entries for the given currencies. Args: date_begin: The start date. date_end: The end date. currencies: A list of currency strings to generate prices for. cost_currency: A string, the cost currency. Returns: A list of Price directives. \"\"\" digits = D ( \"0.01\" ) entries = [] counter = itertools . count () for currency in currencies : start_price = random . uniform ( 30 , 200 ) growth = random . uniform ( 0.02 , 0.13 ) # %/year mu = growth * ( 7 / 365 ) sigma = random . uniform ( 0.005 , 0.02 ) # Vol for dtime , price_float in zip ( rrule . rrule ( rrule . WEEKLY , byweekday = rrule . FR , dtstart = date_begin , until = date_end ), price_series ( start_price , mu , sigma ), ): price = D ( price_float ) . quantize ( digits ) meta = data . new_metadata ( generate_prices . __name__ , next ( counter )) entry = data . Price ( meta , dtime . date (), currency , amount . Amount ( price , cost_currency ) ) entries . append ( entry ) return entries","title":"generate_prices()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_regular_credit_expenses","text":"Generate expenses paid out of a credit card account, including payments to the credit card. Parameters: date_birth \u2013 The user's birth date. date_begin \u2013 The start date. date_end \u2013 The end date. account_credit \u2013 The credit card account to generate expenses against. account_checking \u2013 The checking account to generate payments from. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_regular_credit_expenses ( date_birth , date_begin , date_end , account_credit , account_checking ): \"\"\"Generate expenses paid out of a credit card account, including payments to the credit card. Args: date_birth: The user's birth date. date_begin: The start date. date_end: The end date. account_credit: The credit card account to generate expenses against. account_checking: The checking account to generate payments from. Returns: A list of directives. \"\"\" restaurant_expenses = generate_periodic_expenses ( date_random_seq ( date_begin , date_end , 1 , 5 ), RESTAURANT_NAMES , RESTAURANT_NARRATIONS , account_credit , \"Expenses:Food:Restaurant\" , lambda : min ( random . lognormvariate ( math . log ( 30 ), math . log ( 1.5 )), random . randint ( 200 , 220 ) ), ) groceries_expenses = generate_periodic_expenses ( date_random_seq ( date_begin , date_end , 5 , 20 ), GROCERIES_NAMES , \"Buying groceries\" , account_credit , \"Expenses:Food:Groceries\" , lambda : min ( random . lognormvariate ( math . log ( 80 ), math . log ( 1.3 )), random . randint ( 250 , 300 ) ), ) subway_expenses = generate_periodic_expenses ( date_random_seq ( date_begin , date_end , 27 , 33 ), \"Metro Transport Authority\" , \"Tram tickets\" , account_credit , \"Expenses:Transport:Tram\" , lambda : D ( \"120.00\" ), ) credit_expenses = data . sorted ( restaurant_expenses + groceries_expenses + subway_expenses ) # Entries to open accounts. credit_preamble = generate_open_entries ( date_birth , [ account_credit ], \"CCY\" ) return data . sorted ( credit_preamble + credit_expenses )","title":"generate_regular_credit_expenses()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_retirement_employer_match","text":"Generate employer matching contributions into a retirement account. Parameters: entries \u2013 A list of directives that cover the retirement account. account_invest \u2013 The name of the retirement cash account. account_income \u2013 The name of the income account. Returns: A list of new entries generated for employer contributions. Source code in beancount/scripts/example.py def generate_retirement_employer_match ( entries , account_invest , account_income ): \"\"\"Generate employer matching contributions into a retirement account. Args: entries: A list of directives that cover the retirement account. account_invest: The name of the retirement cash account. account_income: The name of the income account. Returns: A list of new entries generated for employer contributions. \"\"\" match_frac = D ( \"0.50\" ) new_entries = parse ( f \"\"\" { entries [ 0 ] . date } open { account_income } CCY \"\"\" ) for txn_posting , balances in postings_for ( entries , [ account_invest ]): amount = txn_posting . posting . units . number * match_frac amount_neg = - amount date = txn_posting . txn . date + ONE_DAY new_entries . extend ( parse ( f \"\"\" { date } * \"Employer match for contribution\" { account_invest } { amount : .2f } CCY { account_income } { amount_neg : .2f } CCY \"\"\" ) ) return new_entries","title":"generate_retirement_employer_match()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_retirement_investments","text":"Invest money deposited to the given retirement account. Parameters: entries \u2013 A list of directives account \u2013 The root account for all retirement investment sub-accounts. commodities_items \u2013 A list of (commodity, fraction to be invested in) items. price_map \u2013 A dict of prices, as per beancount.core.prices.build_price_map(). Returns: A list of new directives for the given investments. This also generates account opening directives for the desired investment commodities. Source code in beancount/scripts/example.py def generate_retirement_investments ( entries , account , commodities_items , price_map ): \"\"\"Invest money deposited to the given retirement account. Args: entries: A list of directives account: The root account for all retirement investment sub-accounts. commodities_items: A list of (commodity, fraction to be invested in) items. price_map: A dict of prices, as per beancount.core.prices.build_price_map(). Returns: A list of new directives for the given investments. This also generates account opening directives for the desired investment commodities. \"\"\" open_entries = [] account_cash = join ( account , \"Cash\" ) date_origin = entries [ 0 ] . date open_entries . extend ( parse ( f \"\"\" { date_origin } open { account } CCY institution: \"Retirement_Institution\" address: \"Retirement_Address\" phone: \"Retirement_Phone\" { date_origin } open { account_cash } CCY number: \"882882\" \"\"\" ) ) for currency , _ in commodities_items : open_entries . extend ( parse ( f \"\"\" { date_origin } open { account } : { currency } { currency } number: \"882882\" \"\"\" ) ) new_entries = [] for txn_posting , balances in postings_for ( entries , [ account_cash ]): balance = balances [ account_cash ] amount_to_invest = balance . get_currency_units ( \"CCY\" ) . number # Find the date the following Monday, the date to invest. txn_date = txn_posting . txn . date while txn_date . weekday () != calendar . MONDAY : txn_date += ONE_DAY for commodity , fraction in commodities_items : amount_fraction = amount_to_invest * D ( fraction ) # Find the price at that date. _ , price = prices . get_price ( price_map , ( commodity , \"CCY\" ), txn_date ) units = ( amount_fraction / price ) . quantize ( D ( \"0.001\" )) amount_cash = ( units * price ) . quantize ( D ( \"0.01\" )) amount_cash_neg = - amount_cash new_entries . extend ( parse ( f \"\"\" { txn_date } * \"Investing { fraction : .0% } of cash in { commodity } \" { account } : { commodity } { units : .3f } { commodity } {{ { price : .2f } CCY }} { account } :Cash { amount_cash_neg : .2f } CCY \"\"\" ) ) balance . add_amount ( amount . Amount ( - amount_cash , \"CCY\" )) return data . sorted ( open_entries + new_entries )","title":"generate_retirement_investments()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_tax_accounts","text":"Generate accounts and contribution directives for a particular tax year. Parameters: year \u2013 An integer, the year we're to generate this for. date_max \u2013 The maximum date to produce an entry for. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_tax_accounts ( year , date_max ): \"\"\"Generate accounts and contribution directives for a particular tax year. Args: year: An integer, the year we're to generate this for. date_max: The maximum date to produce an entry for. Returns: A list of directives. \"\"\" date_year = datetime . date ( year , 1 , 1 ) date_filing = datetime . date ( year + 1 , 3 , 20 ) + datetime . timedelta ( days = random . randint ( 0 , 5 ) ) date_federal = date_filing + datetime . timedelta ( days = random . randint ( 0 , 4 )) date_state = date_filing + datetime . timedelta ( days = random . randint ( 0 , 4 )) quantum = D ( \"0.01\" ) amount_federal = D ( max ( random . normalvariate ( 500 , 120 ), 12 )) . quantize ( quantum ) amount_federal_neg = - amount_federal amount_state = D ( max ( random . normalvariate ( 300 , 100 ), 10 )) . quantize ( quantum ) amount_state_neg = - amount_state amount_payable = - ( amount_federal + amount_state ) amount_limit = RETIREMENT_LIMITS . get ( year , RETIREMENT_LIMITS [ None ]) amount_limit_neg = - amount_limit entries = parse ( f \"\"\" ;; Open tax accounts for that year. { date_year } open Expenses:Taxes:Y { year } :CC:Federal:PreTax401k DEFCCY { date_year } open Expenses:Taxes:Y { year } :CC:Medicare CCY { date_year } open Expenses:Taxes:Y { year } :CC:Federal CCY { date_year } open Expenses:Taxes:Y { year } :CC:CityNYC CCY { date_year } open Expenses:Taxes:Y { year } :CC:SDI CCY { date_year } open Expenses:Taxes:Y { year } :CC:State CCY { date_year } open Expenses:Taxes:Y { year } :CC:SocSec CCY ;; Check that the tax amounts have been fully used. { date_year } balance Assets:CC:Federal:PreTax401k 0 DEFCCY { date_year } * \"Allowed contributions for one year\" Income:CC:Federal:PreTax401k { amount_limit_neg } DEFCCY Assets:CC:Federal:PreTax401k { amount_limit } DEFCCY { date_filing } * \"Filing taxes for { year } \" Expenses:Taxes:Y { year } :CC:Federal { amount_federal : .2f } CCY Expenses:Taxes:Y { year } :CC:State { amount_state : .2f } CCY Liabilities:AccountsPayable { amount_payable : .2f } CCY { date_federal } * \"FEDERAL TAXPYMT\" Assets:CC:Bank1:Checking { amount_federal_neg : .2f } CCY Liabilities:AccountsPayable { amount_federal : .2f } CCY { date_state } * \"STATE TAX & FINANC PYMT\" Assets:CC:Bank1:Checking { amount_state_neg : .2f } CCY Liabilities:AccountsPayable { amount_state : .2f } CCY \"\"\" ) return [ entry for entry in entries if entry . date < date_max ]","title":"generate_tax_accounts()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_tax_preamble","text":"Generate tax declarations not specific to any particular year. Parameters: date_birth \u2013 A date instance, the birth date of the character. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_tax_preamble ( date_birth ): \"\"\"Generate tax declarations not specific to any particular year. Args: date_birth: A date instance, the birth date of the character. Returns: A list of directives. \"\"\" return parse ( f \"\"\" ;; Tax accounts not specific to a year. { date_birth } open Income:CC:Federal:PreTax401k DEFCCY { date_birth } open Assets:CC:Federal:PreTax401k DEFCCY \"\"\" )","title":"generate_tax_preamble()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_taxable_investment","text":"Generate opening directives and transactions for an investment account. Parameters: date_begin \u2013 A date instance, the beginning date. date_end \u2013 A date instance, the end date. entries \u2013 A list of entries that contains at least the transfers to the investment account's cash account. price_map \u2013 A dict of prices, as per beancount.core.prices.build_price_map(). stocks \u2013 A list of strings, the list of commodities to invest in. Returns: A list of directives. Source code in beancount/scripts/example.py def generate_taxable_investment ( date_begin , date_end , entries , price_map , stocks ): \"\"\"Generate opening directives and transactions for an investment account. Args: date_begin: A date instance, the beginning date. date_end: A date instance, the end date. entries: A list of entries that contains at least the transfers to the investment account's cash account. price_map: A dict of prices, as per beancount.core.prices.build_price_map(). stocks: A list of strings, the list of commodities to invest in. Returns: A list of directives. \"\"\" account = \"Assets:CC:Investment\" income = \"Income:CC:Investment\" account_cash = join ( account , \"Cash\" ) account_gains = \" {income} :PnL\" . format ( income = income ) dividends = \"Dividend\" accounts_stocks = [ \"Assets:CC:Investment: {} \" . format ( commodity ) for commodity in stocks ] open_entries = parse ( f \"\"\" { date_begin } open { account } :Cash CCY { date_begin } open { account_gains } CCY \"\"\" ) for stock in stocks : open_entries . extend ( parse ( f \"\"\" { date_begin } open { account } : { stock } { stock } { date_begin } open { income } : { stock } : { dividends } CCY \"\"\" ) ) # Figure out dates at which dividends should be distributed, near the end of # each quarter. days_to = datetime . timedelta ( days = 3 * 90 - 10 ) dividend_dates = [] for quarter_begin in iter_quarters ( date_begin , date_end ): end_of_quarter = quarter_begin + days_to if not ( date_begin < end_of_quarter < date_end ): continue dividend_dates . append ( end_of_quarter ) # Iterate over all the dates, but merging in the postings for the cash # account. min_amount = D ( \"1000.00\" ) round_amount = D ( \"100.00\" ) commission = D ( \"8.95\" ) round_units = D ( \"1\" ) frac_invest = D ( \"1.00\" ) frac_dividend = D ( \"0.004\" ) p_daily_buy = 1.0 / 15 # days p_daily_sell = 1.0 / 90 # days stocks_inventory = inventory . Inventory () new_entries = [] dividend_date_iter = iter ( dividend_dates ) next_dividend_date = next ( dividend_date_iter , None ) for date , balances in iter_dates_with_balance ( date_begin , date_end , entries , [ account_cash ] ): # Check if we should insert a dividend. Note that we could not factor # this out because we want to explicitly reinvest the cash dividends and # we also want the dividends to be proportional to the amount of # invested stock, so one feeds on the other and vice-versa. if next_dividend_date and date > next_dividend_date : # Compute the total balances for the stock accounts in order to # create a realistic dividend. total = inventory . Inventory () for account_stock in accounts_stocks : total . add_inventory ( balances [ account_stock ]) # Create an entry offering dividends of 1% of the portfolio. portfolio_cost = total . reduce ( convert . get_cost ) . get_currency_units ( \"CCY\" ) . number amount_cash = ( frac_dividend * portfolio_cost ) . quantize ( D ( \"0.01\" )) amount_cash_neg = - amount_cash stock = random . choice ( stocks ) cash_dividend = parse ( f \"\"\" { next_dividend_date } * \"Dividends on portfolio\" { account } :Cash { amount_cash : .2f } CCY { income } : { stock } : { dividends } { amount_cash_neg : .2f } CCY \"\"\" )[ 0 ] new_entries . append ( cash_dividend ) # Advance the next dividend date. next_dividend_date = next ( dividend_date_iter , None ) # If the balance is high, buy with high probability. balance = balances [ account_cash ] total_cash = balance . get_currency_units ( \"CCY\" ) . number assert total_cash >= ZERO , \"Cash balance is negative: {} \" . format ( total_cash ) invest_cash = total_cash * frac_invest - commission if invest_cash > min_amount : if random . random () < p_daily_buy : commodities = random . sample ( stocks , random . randint ( 1 , len ( stocks ))) lot_amount = round_to ( invest_cash / len ( commodities ), round_amount ) for stock in commodities : # Find the price at that date. _ , price = prices . get_price ( price_map , ( stock , \"CCY\" ), date ) units = round_to (( lot_amount / price ), round_units ) if units <= ZERO : continue amount_cash = - ( units * price + commission ) # logging.info('Buying %s %s @ %s CCY = %s CCY', # units, stock, price, units * price) buy = parse ( f \"\"\" { date } * \"Buy shares of { stock } \" { account } :Cash { amount_cash : .2f } CCY { account } : { stock } { units : .0f } { stock } {{ { price : .2f } CCY }} Expenses:Financial:Commissions { commission : .2f } CCY \"\"\" )[ 0 ] new_entries . append ( buy ) account_stock = \":\" . join ([ account , stock ]) balances [ account_cash ] . add_position ( buy . postings [ 0 ]) balances [ account_stock ] . add_position ( buy . postings [ 1 ]) stocks_inventory . add_position ( buy . postings [ 1 ]) # Don't sell on days you buy. continue # Otherwise, sell with low probability. if not stocks_inventory . is_empty () and random . random () < p_daily_sell : # Choose the lot with the highest gain or highest loss. gains = [] for position in stocks_inventory . get_positions (): base_quote = ( position . units . currency , position . cost . currency ) _ , price = prices . get_price ( price_map , base_quote , date ) if price == position . cost . number : continue # Skip lots without movement. market_value = position . units . number * price book_value = convert . get_cost ( position ) . number gain = market_value - book_value gains . append (( gain , market_value , price , position )) if not gains : continue # Sell either biggest winner or biggest loser. biggest = bool ( random . random () < 0.5 ) lot_tuple = sorted ( gains )[ 0 if biggest else - 1 ] gain , market_value , price , sell_position = lot_tuple # logging.info('Selling {} for {}'.format(sell_position, market_value)) sell_position = - sell_position stock = sell_position . units . currency amount_cash = market_value - commission amount_gain = - gain sell = parse ( f \"\"\" { date } * \"Sell shares of { stock } \" { account } : { stock } { sell_position } @ { price : .2f } CCY { account } :Cash { amount_cash : .2f } CCY Expenses:Financial:Commissions { commission : .2f } CCY { account_gains } { amount_gain : .2f } CCY \"\"\" )[ 0 ] new_entries . append ( sell ) balances [ account_cash ] . add_position ( sell . postings [ 1 ]) stocks_inventory . add_position ( sell . postings [ 0 ]) continue return open_entries + new_entries","title":"generate_taxable_investment()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.generate_trip_entries","text":"Generate more dense expenses for a trip. Parameters: date_begin \u2013 A datetime.date instance, the beginning of the trip. date_end \u2013 A datetime.date instance, the end of the trip. tag \u2013 A string, the name of the tag. config \u2013 A list of (payee name, account name, (mu, 3sigma)), where mu is the mean of the prices to generate and 3sigma is 3 times the standard deviation. trip_city \u2013 A string, the capitalized name of the destination city. home_city \u2013 A string, the name of the home city. account_credit \u2013 A string, the name of the credit card account to pay the expenses from. Returns: A list of entries for the trip, all tagged with the given tag. Source code in beancount/scripts/example.py def generate_trip_entries ( date_begin , date_end , tag , config , trip_city , home_city , account_credit ): \"\"\"Generate more dense expenses for a trip. Args: date_begin: A datetime.date instance, the beginning of the trip. date_end: A datetime.date instance, the end of the trip. tag: A string, the name of the tag. config: A list of (payee name, account name, (mu, 3sigma)), where mu is the mean of the prices to generate and 3sigma is 3 times the standard deviation. trip_city: A string, the capitalized name of the destination city. home_city: A string, the name of the home city. account_credit: A string, the name of the credit card account to pay the expenses from. Returns: A list of entries for the trip, all tagged with the given tag. \"\"\" p_day_generate = 0.3 new_entries = [] for date in date_iter ( date_begin , date_end ): for payee , account_expense , ( mu , sigma3 ) in config : if random . random () < p_day_generate : amount = random . normalvariate ( mu , sigma3 / 3.0 ) amount_neg = - amount new_entries . extend ( parse ( f \"\"\" { date } * \" { payee } \" \"\" # { tag } { account_credit } { amount_neg : .2f } CCY { account_expense } { amount : .2f } CCY \"\"\" ) ) # Consume the vacation days. vacation_hrs = ( date_end - date_begin ) . days * 8 # hrs/day new_entries . extend ( parse ( f \"\"\" { date_end } * \"Consume vacation days\" Assets:CC:Employer1:Vacation - { vacation_hrs : .2f } VACHR Expenses:Vacation { vacation_hrs : .2f } VACHR \"\"\" ) ) # Generate events for the trip. new_entries . extend ( parse ( f \"\"\" { date_begin } event \"location\" \" { trip_city } \" { date_end } event \"location\" \" { home_city } \" \"\"\" ) ) return new_entries","title":"generate_trip_entries()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.get_minimum_balance","text":"Compute the minimum balance of the given account according to the entries history. Parameters: entries \u2013 A list of directives. account \u2013 An account string. currency \u2013 A currency string, for which we want to compute the minimum. Returns: A Decimal number, the minimum amount throughout the history of this account. Source code in beancount/scripts/example.py def get_minimum_balance ( entries , account , currency ): \"\"\"Compute the minimum balance of the given account according to the entries history. Args: entries: A list of directives. account: An account string. currency: A currency string, for which we want to compute the minimum. Returns: A Decimal number, the minimum amount throughout the history of this account. \"\"\" min_amount = ZERO for _ , balances in postings_for ( entries , [ account ]): balance = balances [ account ] current = balance . get_currency_units ( currency ) . number if current < min_amount : min_amount = current return min_amount","title":"get_minimum_balance()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.iter_dates_with_balance","text":"Iterate over dates, including the balances of the postings iterator. Parameters: postings_iter \u2013 An iterator of postings as per postings_for(). date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Pairs of (data, balances) objects, with date: A datetime.date instance balances: An Inventory object, representing the current balance. You can modify the inventory object to feed back changes in the balance. Source code in beancount/scripts/example.py def iter_dates_with_balance ( date_begin , date_end , entries , accounts ): \"\"\"Iterate over dates, including the balances of the postings iterator. Args: postings_iter: An iterator of postings as per postings_for(). date_begin: The start date. date_end: The end date. Yields: Pairs of (data, balances) objects, with date: A datetime.date instance balances: An Inventory object, representing the current balance. You can modify the inventory object to feed back changes in the balance. \"\"\" balances = collections . defaultdict ( inventory . Inventory ) merged_txn_postings = iter ( merge_postings ( entries , accounts )) txn_posting = next ( merged_txn_postings , None ) for date in date_iter ( date_begin , date_end ): while txn_posting and txn_posting . txn . date == date : posting = txn_posting . posting balances [ posting . account ] . add_position ( posting ) txn_posting = next ( merged_txn_postings , None ) yield date , balances","title":"iter_dates_with_balance()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.iter_quarters","text":"Iterate over all quarters between begin and end dates. Parameters: date_begin \u2013 The start date. date_end \u2013 The end date. Yields: Instances of datetime.date at the beginning of the quarters. This will include the quarter of the beginning date and of the end date. Source code in beancount/scripts/example.py def iter_quarters ( date_begin , date_end ): \"\"\"Iterate over all quarters between begin and end dates. Args: date_begin: The start date. date_end: The end date. Yields: Instances of datetime.date at the beginning of the quarters. This will include the quarter of the beginning date and of the end date. \"\"\" quarter = ( date_begin . year , ( date_begin . month - 1 ) // 3 ) quarter_last = ( date_end . year , ( date_end . month - 1 ) // 3 ) assert quarter <= quarter_last while True : year , trimester = quarter yield datetime . date ( year , trimester * 3 + 1 , 1 ) if quarter == quarter_last : break trimester = ( trimester + 1 ) % 4 if trimester == 0 : year += 1 quarter = ( year , trimester )","title":"iter_quarters()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.merge_postings","text":"Merge all the postings from the given account names. Parameters: entries \u2013 A list of directives. accounts \u2013 A list of account strings to get the balances for. Yields: A list of TxnPosting's for all the accounts, in sorted order. Source code in beancount/scripts/example.py def merge_postings ( entries , accounts ): \"\"\"Merge all the postings from the given account names. Args: entries: A list of directives. accounts: A list of account strings to get the balances for. Yields: A list of TxnPosting's for all the accounts, in sorted order. \"\"\" real_root = realization . realize ( entries ) merged_postings = [] for account in accounts : real_account = realization . get ( real_root , account ) if real_account is None : continue merged_postings . extend ( txn_posting for txn_posting in real_account . txn_postings if isinstance ( txn_posting , data . TxnPosting ) ) merged_postings . sort ( key = lambda txn_posting : txn_posting . txn . date ) return merged_postings","title":"merge_postings()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.parse","text":"Parse some input string and assert no errors. This parse function does not just create the object, it also triggers local interpolation to fill in the missing amounts. Parameters: input_string \u2013 Beancount input text. Returns: A list of directive objects. Source code in beancount/scripts/example.py def parse ( input_string ): \"\"\"Parse some input string and assert no errors. This parse function does not just create the object, it also triggers local interpolation to fill in the missing amounts. Args: input_string: Beancount input text. Returns: A list of directive objects. \"\"\" entries , errors , options_map = parser . parse_string ( textwrap . dedent ( input_string )) if errors : printer . print_errors ( errors , file = sys . stderr ) raise ValueError ( \"Parsed text has errors\" ) # Interpolation. entries , unused_balance_errors = booking . book ( entries , options_map ) return data . sorted ( entries )","title":"parse()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.postings_for","text":"Realize the entries and get the list of postings for the given accounts. All the non-Posting directives are already filtered out. Parameters: entries \u2013 A list of directives. accounts \u2013 A list of account strings to get the balances for. before \u2013 A boolean, if true, yield the balance before the position is applied. The default is to yield the balance after applying the position. Yields: Tuples of: posting: An instance of TxnPosting balances: A dict of Inventory balances for the given accounts after applying the posting. These inventory objects can be mutated to adjust the balance due to generated transactions to be applied later. Source code in beancount/scripts/example.py def postings_for ( entries , accounts , before = False ): \"\"\"Realize the entries and get the list of postings for the given accounts. All the non-Posting directives are already filtered out. Args: entries: A list of directives. accounts: A list of account strings to get the balances for. before: A boolean, if true, yield the balance before the position is applied. The default is to yield the balance after applying the position. Yields: Tuples of: posting: An instance of TxnPosting balances: A dict of Inventory balances for the given accounts _after_ applying the posting. These inventory objects can be mutated to adjust the balance due to generated transactions to be applied later. \"\"\" assert isinstance ( accounts , list ) merged_txn_postings = merge_postings ( entries , accounts ) balances = collections . defaultdict ( inventory . Inventory ) for txn_posting in merged_txn_postings : if before : yield txn_posting , balances posting = txn_posting . posting balances [ posting . account ] . add_position ( posting ) if not before : yield txn_posting , balances","title":"postings_for()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.price_series","text":"Generate a price series based on a simple stochastic model. Parameters: start \u2013 The beginning value. mu \u2013 The per-step drift, in units of value. sigma \u2013 Volatility of the changes. Yields: Floats, at each step. Source code in beancount/scripts/example.py def price_series ( start , mu , sigma ): \"\"\"Generate a price series based on a simple stochastic model. Args: start: The beginning value. mu: The per-step drift, in units of value. sigma: Volatility of the changes. Yields: Floats, at each step. \"\"\" value = start while 1 : yield value value += random . normalvariate ( mu , sigma ) * value","title":"price_series()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.replace","text":"Apply word-boundaried regular expression replacements to an indented string. Parameters: string \u2013 Some input template string. replacements \u2013 A dict of regexp to replacement value. strip \u2013 A boolean, true if we should strip the input. Returns: The input string with the replacements applied to it, with the indentation removed. Source code in beancount/scripts/example.py def replace ( string , replacements , strip = False ): \"\"\"Apply word-boundaried regular expression replacements to an indented string. Args: string: Some input template string. replacements: A dict of regexp to replacement value. strip: A boolean, true if we should strip the input. Returns: The input string with the replacements applied to it, with the indentation removed. \"\"\" output = textwrap . dedent ( string ) if strip : output = output . strip () for from_ , to_ in replacements . items (): if not isinstance ( to_ , str ) and not callable ( to_ ): to_ = str ( to_ ) output = re . sub ( r \"\\b {} \\b\" . format ( from_ ), to_ , output ) return output","title":"replace()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.validate_output","text":"Check that the output file validates. Parameters: contents \u2013 A string, the output file. positive_accounts \u2013 A list of strings, account names to check for non-negative balances. currency \u2013 A string, the currency to check minimums for. Exceptions: AssertionError \u2013 If the output does not validate. Source code in beancount/scripts/example.py def validate_output ( contents , positive_accounts , currency ): \"\"\"Check that the output file validates. Args: contents: A string, the output file. positive_accounts: A list of strings, account names to check for non-negative balances. currency: A string, the currency to check minimums for. Raises: AssertionError: If the output does not validate. \"\"\" loaded_entries , _ , _ = loader . load_string ( contents , log_errors = sys . stderr , extra_validations = validation . HARDCORE_VALIDATIONS ) # Sanity checks: Check that the checking balance never goes below zero. for account in positive_accounts : check_non_negative ( loaded_entries , account , currency )","title":"validate_output()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.example.write_example_file","text":"Generate the example file. Parameters: date_birth \u2013 A datetime.date instance, the birth date of our character. date_begin \u2013 A datetime.date instance, the beginning date at which to generate transactions. date_end \u2013 A datetime.date instance, the end date at which to generate transactions. reformat \u2013 A boolean, true if we should apply global reformatting to this file. file \u2013 A file object, where to write out the output. Source code in beancount/scripts/example.py def write_example_file ( date_birth , date_begin , date_end , reformat , file ): \"\"\"Generate the example file. Args: date_birth: A datetime.date instance, the birth date of our character. date_begin: A datetime.date instance, the beginning date at which to generate transactions. date_end: A datetime.date instance, the end date at which to generate transactions. reformat: A boolean, true if we should apply global reformatting to this file. file: A file object, where to write out the output. \"\"\" # The following code entirely writes out the output to generic names, such # as \"Employer1\", \"Bank1\", and \"CCY\" (for principal currency). Those names # are purposely chosen to be unique, and only near the very end do we make # renamings to more specific and realistic names. # Name of the checking account. account_opening = \"Equity:Opening-Balances\" account_payable = \"Liabilities:AccountsPayable\" account_checking = \"Assets:CC:Bank1:Checking\" account_credit = \"Liabilities:CC:CreditCard1\" account_retirement = \"Assets:CC:Retirement\" account_investing = \"Assets:CC:Investment:Cash\" # Commodities. commodity_entries = generate_commodity_entries ( date_birth ) # Estimate the rent. rent_amount = round_to ( ANNUAL_SALARY / RENT_DIVISOR , RENT_INCREMENT ) # Get a random employer. employer_name , employer_address = random . choice ( EMPLOYERS ) logging . info ( \"Generating Salary Employment Income\" ) income_entries = generate_employment_income ( employer_name , employer_address , ANNUAL_SALARY , account_checking , join ( account_retirement , \"Cash\" ), date_begin , date_end , ) logging . info ( \"Generating Expenses from Banking Accounts\" ) banking_expenses = generate_banking_expenses ( date_begin , date_end , account_checking , rent_amount ) logging . info ( \"Generating Regular Expenses via Credit Card\" ) credit_regular_entries = generate_regular_credit_expenses ( date_birth , date_begin , date_end , account_credit , account_checking ) logging . info ( \"Generating Credit Card Expenses for Trips\" ) trip_entries = [] destinations = sorted ( TRIP_DESTINATIONS . items ()) destinations . extend ( destinations ) random . shuffle ( destinations ) for ( date_trip_begin , date_trip_end ), ( destination_name , config ) in zip ( compute_trip_dates ( date_begin , date_end ), destinations ): # Compute a suitable tag. tag = \"trip- {} - {} \" . format ( destination_name . lower () . replace ( \" \" , \"-\" ), date_trip_begin . year ) # logging.info(\"%s -- %s %s\", tag, date_trip_begin, date_trip_end) # Remove regular entries during this trip. credit_regular_entries = [ entry for entry in credit_regular_entries if not ( date_trip_begin <= entry . date < date_trip_end ) ] # Generate entries for the trip. this_trip_entries = generate_trip_entries ( date_trip_begin , date_trip_end , tag , config , destination_name . replace ( \"-\" , \" \" ) . title (), HOME_NAME , account_credit , ) trip_entries . extend ( this_trip_entries ) logging . info ( \"Generating Credit Card Payment Entries\" ) credit_payments = generate_clearing_entries ( delay_dates ( rrule . rrule ( rrule . MONTHLY , dtstart = date_begin , until = date_end , bymonthday = 7 ), 0 , 4 , ), \"CreditCard1\" , \"Paying off credit card\" , credit_regular_entries , account_credit , account_checking , ) credit_entries = credit_regular_entries + trip_entries + credit_payments logging . info ( \"Generating Tax Filings and Payments\" ) tax_preamble = generate_tax_preamble ( date_birth ) # Figure out all the years we need tax accounts for. years = set () for account_name in getters . get_accounts ( income_entries ): match = re . match ( r \"Expenses:Taxes:Y(\\d\\d\\d\\d)\" , account_name ) if match : years . add ( int ( match . group ( 1 ))) taxes = [( year , generate_tax_accounts ( year , date_end )) for year in sorted ( years )] tax_entries = tax_preamble + functools . reduce ( operator . add , ( entries for _ , entries in taxes ) ) logging . info ( \"Generating Opening of Banking Accounts\" ) # Open banking accounts and gift the checking account with a balance that # will offset all the amounts to ensure a positive balance throughout its # lifetime. entries_for_banking = data . sorted ( income_entries + banking_expenses + credit_entries + tax_entries ) minimum = get_minimum_balance ( entries_for_banking , account_checking , \"CCY\" ) banking_entries = generate_banking ( entries_for_banking , date_begin , date_end , max ( - minimum , ZERO ) ) logging . info ( \"Generating Transfers to Investment Account\" ) banking_transfers = generate_outgoing_transfers ( data . sorted ( income_entries + banking_entries + banking_expenses + credit_entries + tax_entries ), account_checking , account_investing , transfer_minimum = D ( \"200\" ), transfer_threshold = D ( \"3000\" ), transfer_increment = D ( \"500\" ), ) logging . info ( \"Generating Prices\" ) # Generate price entries for investment currencies and create a price map to # use for later for generating investment transactions. funds_allocation = { \"MFUND1\" : 0.40 , \"MFUND2\" : 0.60 } stocks = [ \"STK1\" , \"STK2\" , \"STK3\" , \"STK4\" ] price_entries = generate_prices ( date_begin , date_end , sorted ( funds_allocation . keys ()) + stocks , \"CCY\" ) price_map = prices . build_price_map ( price_entries ) logging . info ( \"Generating Employer Match Contribution\" ) account_match = \"Income:US:Employer1:Match401k\" retirement_match = generate_retirement_employer_match ( income_entries , join ( account_retirement , \"Cash\" ), account_match ) logging . info ( \"Generating Retirement Investments\" ) retirement_entries = generate_retirement_investments ( income_entries + retirement_match , account_retirement , sorted ( funds_allocation . items ()), price_map , ) logging . info ( \"Generating Taxes Investments\" ) investment_entries = generate_taxable_investment ( date_begin , date_end , banking_transfers , price_map , stocks ) logging . info ( \"Generating Expense Accounts\" ) expense_accounts_entries = generate_expense_accounts ( date_birth ) logging . info ( \"Generating Equity Accounts\" ) equity_entries = generate_open_entries ( date_birth , [ account_opening , account_payable ]) logging . info ( \"Generating Balance Checks\" ) credit_checks = generate_balance_checks ( credit_entries , account_credit , date_random_seq ( date_begin , date_end , 20 , 30 ) ) banking_checks = generate_balance_checks ( data . sorted ( income_entries + banking_entries + banking_expenses + banking_transfers + credit_entries + tax_entries ), account_checking , date_random_seq ( date_begin , date_end , 20 , 30 ), ) logging . info ( \"Outputting and Formatting Entries\" ) dcontext = display_context . DisplayContext () default_int_digits = 8 for currency , precision in { \"USD\" : 2 , \"CAD\" : 2 , \"VACHR\" : 0 , \"IRAUSD\" : 2 , \"VBMPX\" : 3 , \"RGAGX\" : 3 , \"ITOT\" : 0 , \"VEA\" : 0 , \"VHT\" : 0 , \"GLD\" : 0 , } . items (): int_digits = default_int_digits if precision > 0 : int_digits += 1 + precision dcontext . update ( D ( \"{{:0 {} . {} f}}\" . format ( int_digits , precision ) . format ( 0 )), currency ) output = io . StringIO () def output_section ( title , entries ): output . write ( \" \\n\\n\\n {} \\n\\n \" . format ( title )) printer . print_entries ( data . sorted ( entries ), dcontext , file = output ) output . write ( FILE_PREAMBLE . format ( ** locals ())) output_section ( \"* Commodities\" , commodity_entries ) output_section ( \"* Equity Accounts\" , equity_entries ) output_section ( \"* Banking\" , data . sorted ( banking_entries + banking_expenses + banking_transfers + banking_checks ), ) output_section ( \"* Credit-Cards\" , data . sorted ( credit_entries + credit_checks )) output_section ( \"* Taxable Investments\" , investment_entries ) output_section ( \"* Retirement Investments\" , data . sorted ( retirement_entries + retirement_match ) ) output_section ( \"* Sources of Income\" , income_entries ) output_section ( \"* Taxes\" , tax_preamble ) for year , entries in taxes : output_section ( \"** Tax Year {} \" . format ( year ), entries ) output_section ( \"* Expenses\" , expense_accounts_entries ) output_section ( \"* Prices\" , price_entries ) output_section ( \"* Cash\" , []) logging . info ( \"Contextualizing to Realistic Names\" ) contents , replacements = contextualize_file ( output . getvalue (), employer_name ) if reformat : contents = format . align_beancount ( contents ) logging . info ( \"Writing contents\" ) file . write ( contents ) logging . info ( \"Validating Results\" ) validate_output ( contents , [ replace ( account , replacements ) for account in [ account_checking ]], replace ( \"CCY\" , replacements ), )","title":"write_example_file()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.format","text":"","title":"format"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.format.align_beancount","text":"Reformat Beancount input to align all the numbers at the same column. Parameters: contents \u2013 A string, Beancount input syntax to reformat. prefix_width \u2013 An integer, the width in characters to render the account name to. If this is not specified, a good value is selected automatically from the contents of the file. num_width \u2013 An integer, the width to render each number. If this is not specified, a good value is selected automatically from the contents of the file. currency_column \u2013 An integer, the column at which to align the currencies. If given, this overrides the other options. Returns: A string, reformatted Beancount input with all the number aligned. No other changes than whitespace changes should be present between that return value and the input contents. Source code in beancount/scripts/format.py def align_beancount ( contents , prefix_width = None , num_width = None , currency_column = None ): \"\"\"Reformat Beancount input to align all the numbers at the same column. Args: contents: A string, Beancount input syntax to reformat. prefix_width: An integer, the width in characters to render the account name to. If this is not specified, a good value is selected automatically from the contents of the file. num_width: An integer, the width to render each number. If this is not specified, a good value is selected automatically from the contents of the file. currency_column: An integer, the column at which to align the currencies. If given, this overrides the other options. Returns: A string, reformatted Beancount input with all the number aligned. No other changes than whitespace changes should be present between that return value and the input contents. \"\"\" # Find all lines that have a number in them and calculate the maximum length # of the stripped prefix and the number. match_pairs = [] for line in contents . splitlines (): match = regex . match ( rf '(^\\d[^\";]*?|\\s+ { account . ACCOUNT_RE } )\\s+' rf \"( { PARENTHESIZED_BINARY_OP_RE } | { NUMBER_RE } )\\s+\" rf \"((?: { amount . CURRENCY_RE } )\\b.*)\" , line , ) if match : prefix , number , rest = match . groups () match_pairs . append (( prefix , number , rest )) else : match_pairs . append (( line , None , None )) # Normalize whitespace before lines that has some indent and an account # name. norm_match_pairs = normalize_indent_whitespace ( match_pairs ) if currency_column : output = io . StringIO () for prefix , number , rest in norm_match_pairs : if number is None : output . write ( prefix ) else : num_of_spaces = currency_column - len ( prefix ) - len ( number ) - 4 spaces = \" \" * num_of_spaces output . write ( prefix + spaces + \" \" + number + \" \" + rest ) output . write ( \" \\n \" ) return output . getvalue () # Compute the maximum widths. filtered_pairs = [ ( prefix , number ) for prefix , number , _ in match_pairs if number is not None ] if filtered_pairs : max_prefix_width = max ( len ( prefix ) for prefix , _ in filtered_pairs ) max_num_width = max ( len ( number ) for _ , number in filtered_pairs ) else : max_prefix_width = 0 max_num_width = 0 # Use user-supplied overrides, if available if prefix_width : max_prefix_width = prefix_width if num_width : max_num_width = num_width # Create a format that will admit the maximum width of all prefixes equally. line_format = \"{{:< {prefix_width} }} {{:> {num_width} }} {{}}\" . format ( prefix_width = max_prefix_width , num_width = max_num_width ) # Process each line to an output buffer. output = io . StringIO () for prefix , number , rest in norm_match_pairs : if number is None : output . write ( prefix ) else : output . write ( line_format . format ( prefix . rstrip (), number , rest )) output . write ( \" \\n \" ) formatted_contents = output . getvalue () # Ensure that the file before and after have only whitespace differences. # This is a sanity check, to make really sure we never change anything but whitespace, # so it's safe. # open('/tmp/before', 'w').write(regex.sub(r'[ \\t]+', ' ', contents)) # open('/tmp/after', 'w').write(regex.sub(r'[ \\t]+', ' ', formatted_contents)) old_stripped = regex . sub ( r \"[ \\t\\n]+\" , \" \" , contents . rstrip ()) new_stripped = regex . sub ( r \"[ \\t\\n]+\" , \" \" , formatted_contents . rstrip ()) assert old_stripped == new_stripped , ( old_stripped , new_stripped ) return formatted_contents","title":"align_beancount()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.format.compute_most_frequent","text":"Compute the frequencies of the given elements and return the most frequent. Parameters: iterable \u2013 A collection of hashable elements. Returns: The most frequent element. If there are no elements in the iterable, return None. Source code in beancount/scripts/format.py def compute_most_frequent ( iterable ): \"\"\"Compute the frequencies of the given elements and return the most frequent. Args: iterable: A collection of hashable elements. Returns: The most frequent element. If there are no elements in the iterable, return None. \"\"\" frequencies = collections . Counter ( iterable ) if not frequencies : return None counts = sorted (( count , element ) for element , count in frequencies . items ()) # Note: In case of a tie, this chooses the longest width. # We could eventually make this an option. return counts [ - 1 ][ 1 ]","title":"compute_most_frequent()"},{"location":"api_reference/beancount.scripts.html#beancount.scripts.format.normalize_indent_whitespace","text":"Normalize whitespace before lines that has some indent and an account name. Parameters: match_pairs \u2013 A list of (prefix, number, rest) tuples. Returns: Another list of (prefix, number, rest) tuples, where prefix may have been adjusted with a different whitespace prefix. Source code in beancount/scripts/format.py def normalize_indent_whitespace ( match_pairs ): \"\"\"Normalize whitespace before lines that has some indent and an account name. Args: match_pairs: A list of (prefix, number, rest) tuples. Returns: Another list of (prefix, number, rest) tuples, where prefix may have been adjusted with a different whitespace prefix. \"\"\" # Compute most frequent account name prefix. match_posting = regex . compile ( r \"([ \\t]+)( {} .*)\" . format ( account . ACCOUNT_RE )) . match width = compute_most_frequent ( len ( match . group ( 1 )) for match in ( match_posting ( prefix ) for prefix , _ , _ in match_pairs ) if match is not None ) norm_format = \" \" * ( width or 0 ) + \" {} \" # Make the necessary adjustments. adjusted_pairs = [] for tup in match_pairs : prefix , number , rest = tup match = match_posting ( prefix ) if match is not None : tup = ( norm_format . format ( match . group ( 2 )), number , rest ) adjusted_pairs . append ( tup ) return adjusted_pairs","title":"normalize_indent_whitespace()"},{"location":"api_reference/beancount.tools.html","text":"beancount.tools \uf0c1 Standalone tools that aren't linked to Beancount but that are useful with it. The beancount.scripts package contains the implementation of scripts which invoke the Beancount library code. This beancount.tools package implements other tools which aren't directly invoking Beancount library code and which could be theoretically copied and used independently. However, these are to be distributed with Beancount and in order to maintain all the source code together they are put in this package and invokes from stubs under beancount/bin/, just like the other scripts. beancount.tools.treeify \uf0c1 Identify a column of text that contains hierarchical id and treeify that column. This script will inspect a text file and attempt to find a vertically left-aligned column of text that contains identifiers with multiple components, such as \"Assets:US:Bank:Checking\", and replace those by a tree-like structure rendered in ASCII, inserting new empty lines where necessary to create the tree. Note: If your paths have spaces in them, this will not work. Space is used as a delimiter to detect the end of a column. You can customize the delimiter with an option. beancount.tools.treeify.Node ( list ) \uf0c1 A node with a name attribute, a list of line numbers and a list of children (from its parent class). beancount . tools . treeify . Node . __repr__ ( self ) special \uf0c1 Return str(self). Source code in beancount/tools/treeify.py def __str__ ( self ): return \"\" . format ( self . name , [ node . name for node in self ]) beancount . tools . treeify . create_tree ( column_matches , regexp_split ) \uf0c1 Build up a tree from a list of matches. Parameters: column_matches \u2013 A list of (line-number, name) pairs. regexp_split \u2013 A regular expression string, to use for splitting the names of components. Returns: An instance of Node, the root node of the created tree. Source code in beancount/tools/treeify.py def create_tree ( column_matches , regexp_split ): \"\"\"Build up a tree from a list of matches. Args: column_matches: A list of (line-number, name) pairs. regexp_split: A regular expression string, to use for splitting the names of components. Returns: An instance of Node, the root node of the created tree. \"\"\" root = Node ( \"\" ) for no , name in column_matches : parts = re . split ( regexp_split , name ) node = root for part in parts : last_node = node [ - 1 ] if node else None if last_node is None or last_node . name != part : last_node = Node ( part ) node . append ( last_node ) node = last_node node . nos . append ( no ) return root beancount . tools . treeify . dump_tree ( node , file =< _io . StringIO object at 0x78fcdeaef7c0 > , prefix = '' ) \uf0c1 Render a tree as a tree. Parameters: node \u2013 An instance of Node. file \u2013 A file object to write to. prefix \u2013 A prefix string for each of the lines of the children. Source code in beancount/tools/treeify.py def dump_tree ( node , file = sys . stdout , prefix = \"\" ): \"\"\"Render a tree as a tree. Args: node: An instance of Node. file: A file object to write to. prefix: A prefix string for each of the lines of the children. \"\"\" file . write ( prefix ) file . write ( node . name ) file . write ( \" \\n \" ) for child in node : dump_tree ( child , file , prefix + \"... \" ) beancount . tools . treeify . enum_tree_by_input_line_num ( tree_lines ) \uf0c1 Accumulate the lines of a tree until a line number is found. Parameters: tree_lines \u2013 A list of lines as returned by render_tree. Yields: Pairs of (line number, list of (line, node)). Source code in beancount/tools/treeify.py def enum_tree_by_input_line_num ( tree_lines ): \"\"\"Accumulate the lines of a tree until a line number is found. Args: tree_lines: A list of lines as returned by render_tree. Yields: Pairs of (line number, list of (line, node)). \"\"\" pending = [] for first_line , cont_line , node in tree_lines : if not node . nos : pending . append (( first_line , node )) else : line = first_line for no in node . nos : pending . append (( line , node )) line = cont_line yield ( no , pending ) pending = [] if pending : yield ( None , pending ) beancount . tools . treeify . find_column ( lines , pattern , delimiter ) \uf0c1 Find a valid column with hierarchical data in the text lines. Parameters: lines \u2013 A list of strings, the contents of the input. pattern \u2013 A regular expression for the hierarchical entries. delimiter \u2013 A regular expression that dictates how we detect the end of a column. Normally this is a single space. If the patterns contain spaces, you will need to increase this. Returns: A tuple of matches \u2013 A list of (line-number, name) tuples where 'name' is the hierarchical string to treeify and line-number is an integer, the line number where this applies. left: An integer, the leftmost column. right: An integer, the rightmost column. Note that not all line numbers may be present, so you may need to skip some. However, they are in guaranteed in sorted order. Source code in beancount/tools/treeify.py def find_column ( lines , pattern , delimiter ): \"\"\"Find a valid column with hierarchical data in the text lines. Args: lines: A list of strings, the contents of the input. pattern: A regular expression for the hierarchical entries. delimiter: A regular expression that dictates how we detect the end of a column. Normally this is a single space. If the patterns contain spaces, you will need to increase this. Returns: A tuple of matches: A list of (line-number, name) tuples where 'name' is the hierarchical string to treeify and line-number is an integer, the line number where this applies. left: An integer, the leftmost column. right: An integer, the rightmost column. Note that not all line numbers may be present, so you may need to skip some. However, they are in guaranteed in sorted order. \"\"\" # A mapping of the line beginning position to its match object. beginnings = collections . defaultdict ( list ) pattern_and_whitespace = \"( {} )(?P {} .|$)\" . format ( pattern , delimiter ) for no , line in enumerate ( lines ): for match in re . finditer ( pattern_and_whitespace , line ): beginnings [ match . start ()] . append (( no , line , match )) # For each potential column found, verify that it is valid. A valid column # will have the maximum of its content text not overlap with any of the # following text. We assume that a column will have been formatted to full # width and that no text following the line overlap with the column, even in # its trailing whitespace. # # In other words, the following example is a violation because \"10,990.74\" # overlaps with the end of \"Insurance\" and so this would not be recognized # as a valid column: # # Expenses:Food:Restaurant 10,990.74 USD # Expenses:Health:Dental:Insurance 208.80 USD # for leftmost_column , column_matches in sorted ( beginnings . items ()): # Compute the location of the rightmost column of text. rightmost_column = max ( match . end ( 1 ) for _ , _ , match in column_matches ) # Compute the leftmost location of the content following the column text # and past its whitespace. following_column = min ( match . end () if match . group ( \"ws\" ) else 10000 for _ , _ , match in column_matches ) if rightmost_column < following_column : # We process only the very first match. return_matches = [ ( no , match . group ( 1 ) . rstrip ()) for no , _ , match in column_matches ] return return_matches , leftmost_column , rightmost_column beancount . tools . treeify . render_tree ( root ) \uf0c1 Render a tree of nodes. Returns: A list of tuples of (first_line, continuation_line, node) where first_line \u2013 A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. node: The Node instance which corresponds to this line. and an integer, the width of the new columns. Source code in beancount/tools/treeify.py def render_tree ( root ): \"\"\"Render a tree of nodes. Returns: A list of tuples of (first_line, continuation_line, node) where first_line: A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. node: The Node instance which corresponds to this line. and an integer, the width of the new columns. \"\"\" # Compute all the lines ahead of time in order to calculate the width. lines = [] # Start with the root node. We push the constant prefix before this node, # the account name, and the RealAccount instance. We will maintain a stack # of children nodes to render. stack = [( \"\" , root . name , root , True )] while stack : prefix , name , node , is_last = stack . pop ( - 1 ) if node is root : # For the root node, we don't want to render any prefix. first = cont = \"\" else : # Compute the string that precedes the name directly and the one below # that for the continuation lines. # | # @@@ Bank1 <---------------- # @@@ | # | |-- Checking if is_last : first = prefix + PREFIX_LEAF_1 cont = prefix + PREFIX_LEAF_C else : first = prefix + PREFIX_CHILD_1 cont = prefix + PREFIX_CHILD_C # Compute the name to render for continuation lines. # | # |-- Bank1 # | @@@ <---------------- # | |-- Checking if len ( node ) > 0 : cont_name = PREFIX_CHILD_C else : cont_name = PREFIX_LEAF_C # Add a line for this account. if not ( node is root and not name ): lines . append (( first + name , cont + cont_name , node )) # Push the children onto the stack, being careful with ordering and # marking the last node as such. if node : child_items = reversed ( node ) child_iter = iter ( child_items ) child_node = next ( child_iter ) stack . append (( cont , child_node . name , child_node , True )) for child_node in child_iter : stack . append (( cont , child_node . name , child_node , False )) if not lines : return lines # Compute the maximum width of the lines and convert all of them to the same # maximal width. This makes it easy on the client. max_width = max ( len ( first_line ) for first_line , _ , __ in lines ) line_format = \"{{: {width} }}\" . format ( width = max_width ) return [ ( line_format . format ( first_line ), line_format . format ( cont_line ), node ) for ( first_line , cont_line , node ) in lines ], max_width beancount.tools.treeify_test \uf0c1 Unit tests for treeify tool. beancount.tools.treeify_test.TestTreeifyBase ( TestCase ) \uf0c1 beancount . tools . treeify_test . TestTreeifyBase . treeify ( self , string , expect_errors = False , options = None ) \uf0c1 Run treeify on the given string and assert no errors. Parameters: string \u2013 A string, the input contents to run on. expect_errors \u2013 A boolean, true if we should expect there to be errors. options \u2013 An optional list of options for the subprogram. Returns: The converted output string. This fails the test if there were any errors. Source code in beancount/tools/treeify_test.py def treeify ( self , string , expect_errors = False , options = None ): \"\"\"Run treeify on the given string and assert no errors. Args: string: A string, the input contents to run on. expect_errors: A boolean, true if we should expect there to be errors. options: An optional list of options for the subprogram. Returns: The converted output string. This fails the test if there were any errors. \"\"\" returncode , output , errors = treeify ( string , options ) actual_errors = returncode != 0 or bool ( errors ) if actual_errors != expect_errors : if expect_errors : self . fail ( \"Missing expected errors\" ) else : self . fail ( \"Unexpected errors: {} \" . format ( errors )) return output beancount . tools . treeify_test . TestTreeifyBase . treeify_equal ( self , string , expected , expect_errors = False , options = None ) \uf0c1 Assert an expected treeification result. Parameters: string \u2013 An expected input contents string to treeify. expected \u2013 A string, the expected treeified output. expect_errors \u2013 A boolean, true if we should expect there to be errors. options \u2013 An optional list of options for the subprogram. Returns: The actual output string. This fails the test if there the expected output differed from the actual. Source code in beancount/tools/treeify_test.py def treeify_equal ( self , string , expected , expect_errors = False , options = None ): \"\"\"Assert an expected treeification result. Args: string: An expected input contents string to treeify. expected: A string, the expected treeified output. expect_errors: A boolean, true if we should expect there to be errors. options: An optional list of options for the subprogram. Returns: The actual output string. This fails the test if there the expected output differed from the actual. \"\"\" input_ = textwrap . dedent ( string ) output = self . treeify ( input_ , expect_errors , options ) expected = textwrap . dedent ( expected ) if DEBUG : print ( \"-(input)----------------------------------\" ) print ( input_ ) print ( \"-(output)---------------------------------\" ) print ( output ) print ( \"-(expected)-------------------------------\" ) print ( expected ) print ( \"------------------------------------------\" ) self . assertEqual ( expected , output ) return output beancount . tools . treeify_test . treeify ( string , options = None ) \uf0c1 Run treeify on the string. Parameters: string \u2013 The input string to feed treeify. options \u2013 An optional list of options for the subprogram. Returns: The treeified string. Source code in beancount/tools/treeify_test.py def treeify ( string , options = None ): \"\"\"Run treeify on the string. Args: string: The input string to feed treeify. options: An optional list of options for the subprogram. Returns: The treeified string. \"\"\" pipe = subprocess . Popen ( [ PROGRAM ] + ( options or []), shell = False , stdin = subprocess . PIPE , stdout = subprocess . PIPE , stderr = subprocess . PIPE , ) output , errors = pipe . communicate ( string . encode ( \"utf-8\" )) return ( pipe . returncode , output . decode ( \"utf-8\" ) if output else \"\" , errors . decode ( \"utf-8\" ) if errors else \"\" , )","title":"beancount.tools"},{"location":"api_reference/beancount.tools.html#beancounttools","text":"Standalone tools that aren't linked to Beancount but that are useful with it. The beancount.scripts package contains the implementation of scripts which invoke the Beancount library code. This beancount.tools package implements other tools which aren't directly invoking Beancount library code and which could be theoretically copied and used independently. However, these are to be distributed with Beancount and in order to maintain all the source code together they are put in this package and invokes from stubs under beancount/bin/, just like the other scripts.","title":"beancount.tools"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify","text":"Identify a column of text that contains hierarchical id and treeify that column. This script will inspect a text file and attempt to find a vertically left-aligned column of text that contains identifiers with multiple components, such as \"Assets:US:Bank:Checking\", and replace those by a tree-like structure rendered in ASCII, inserting new empty lines where necessary to create the tree. Note: If your paths have spaces in them, this will not work. Space is used as a delimiter to detect the end of a column. You can customize the delimiter with an option.","title":"treeify"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.Node","text":"A node with a name attribute, a list of line numbers and a list of children (from its parent class).","title":"Node"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.Node.__repr__","text":"Return str(self). Source code in beancount/tools/treeify.py def __str__ ( self ): return \"\" . format ( self . name , [ node . name for node in self ])","title":"__repr__()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.create_tree","text":"Build up a tree from a list of matches. Parameters: column_matches \u2013 A list of (line-number, name) pairs. regexp_split \u2013 A regular expression string, to use for splitting the names of components. Returns: An instance of Node, the root node of the created tree. Source code in beancount/tools/treeify.py def create_tree ( column_matches , regexp_split ): \"\"\"Build up a tree from a list of matches. Args: column_matches: A list of (line-number, name) pairs. regexp_split: A regular expression string, to use for splitting the names of components. Returns: An instance of Node, the root node of the created tree. \"\"\" root = Node ( \"\" ) for no , name in column_matches : parts = re . split ( regexp_split , name ) node = root for part in parts : last_node = node [ - 1 ] if node else None if last_node is None or last_node . name != part : last_node = Node ( part ) node . append ( last_node ) node = last_node node . nos . append ( no ) return root","title":"create_tree()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.dump_tree","text":"Render a tree as a tree. Parameters: node \u2013 An instance of Node. file \u2013 A file object to write to. prefix \u2013 A prefix string for each of the lines of the children. Source code in beancount/tools/treeify.py def dump_tree ( node , file = sys . stdout , prefix = \"\" ): \"\"\"Render a tree as a tree. Args: node: An instance of Node. file: A file object to write to. prefix: A prefix string for each of the lines of the children. \"\"\" file . write ( prefix ) file . write ( node . name ) file . write ( \" \\n \" ) for child in node : dump_tree ( child , file , prefix + \"... \" )","title":"dump_tree()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.enum_tree_by_input_line_num","text":"Accumulate the lines of a tree until a line number is found. Parameters: tree_lines \u2013 A list of lines as returned by render_tree. Yields: Pairs of (line number, list of (line, node)). Source code in beancount/tools/treeify.py def enum_tree_by_input_line_num ( tree_lines ): \"\"\"Accumulate the lines of a tree until a line number is found. Args: tree_lines: A list of lines as returned by render_tree. Yields: Pairs of (line number, list of (line, node)). \"\"\" pending = [] for first_line , cont_line , node in tree_lines : if not node . nos : pending . append (( first_line , node )) else : line = first_line for no in node . nos : pending . append (( line , node )) line = cont_line yield ( no , pending ) pending = [] if pending : yield ( None , pending )","title":"enum_tree_by_input_line_num()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.find_column","text":"Find a valid column with hierarchical data in the text lines. Parameters: lines \u2013 A list of strings, the contents of the input. pattern \u2013 A regular expression for the hierarchical entries. delimiter \u2013 A regular expression that dictates how we detect the end of a column. Normally this is a single space. If the patterns contain spaces, you will need to increase this. Returns: A tuple of matches \u2013 A list of (line-number, name) tuples where 'name' is the hierarchical string to treeify and line-number is an integer, the line number where this applies. left: An integer, the leftmost column. right: An integer, the rightmost column. Note that not all line numbers may be present, so you may need to skip some. However, they are in guaranteed in sorted order. Source code in beancount/tools/treeify.py def find_column ( lines , pattern , delimiter ): \"\"\"Find a valid column with hierarchical data in the text lines. Args: lines: A list of strings, the contents of the input. pattern: A regular expression for the hierarchical entries. delimiter: A regular expression that dictates how we detect the end of a column. Normally this is a single space. If the patterns contain spaces, you will need to increase this. Returns: A tuple of matches: A list of (line-number, name) tuples where 'name' is the hierarchical string to treeify and line-number is an integer, the line number where this applies. left: An integer, the leftmost column. right: An integer, the rightmost column. Note that not all line numbers may be present, so you may need to skip some. However, they are in guaranteed in sorted order. \"\"\" # A mapping of the line beginning position to its match object. beginnings = collections . defaultdict ( list ) pattern_and_whitespace = \"( {} )(?P {} .|$)\" . format ( pattern , delimiter ) for no , line in enumerate ( lines ): for match in re . finditer ( pattern_and_whitespace , line ): beginnings [ match . start ()] . append (( no , line , match )) # For each potential column found, verify that it is valid. A valid column # will have the maximum of its content text not overlap with any of the # following text. We assume that a column will have been formatted to full # width and that no text following the line overlap with the column, even in # its trailing whitespace. # # In other words, the following example is a violation because \"10,990.74\" # overlaps with the end of \"Insurance\" and so this would not be recognized # as a valid column: # # Expenses:Food:Restaurant 10,990.74 USD # Expenses:Health:Dental:Insurance 208.80 USD # for leftmost_column , column_matches in sorted ( beginnings . items ()): # Compute the location of the rightmost column of text. rightmost_column = max ( match . end ( 1 ) for _ , _ , match in column_matches ) # Compute the leftmost location of the content following the column text # and past its whitespace. following_column = min ( match . end () if match . group ( \"ws\" ) else 10000 for _ , _ , match in column_matches ) if rightmost_column < following_column : # We process only the very first match. return_matches = [ ( no , match . group ( 1 ) . rstrip ()) for no , _ , match in column_matches ] return return_matches , leftmost_column , rightmost_column","title":"find_column()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify.render_tree","text":"Render a tree of nodes. Returns: A list of tuples of (first_line, continuation_line, node) where first_line \u2013 A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. node: The Node instance which corresponds to this line. and an integer, the width of the new columns. Source code in beancount/tools/treeify.py def render_tree ( root ): \"\"\"Render a tree of nodes. Returns: A list of tuples of (first_line, continuation_line, node) where first_line: A string, the first line to render, which includes the account name. continuation_line: A string, further line to render if necessary. node: The Node instance which corresponds to this line. and an integer, the width of the new columns. \"\"\" # Compute all the lines ahead of time in order to calculate the width. lines = [] # Start with the root node. We push the constant prefix before this node, # the account name, and the RealAccount instance. We will maintain a stack # of children nodes to render. stack = [( \"\" , root . name , root , True )] while stack : prefix , name , node , is_last = stack . pop ( - 1 ) if node is root : # For the root node, we don't want to render any prefix. first = cont = \"\" else : # Compute the string that precedes the name directly and the one below # that for the continuation lines. # | # @@@ Bank1 <---------------- # @@@ | # | |-- Checking if is_last : first = prefix + PREFIX_LEAF_1 cont = prefix + PREFIX_LEAF_C else : first = prefix + PREFIX_CHILD_1 cont = prefix + PREFIX_CHILD_C # Compute the name to render for continuation lines. # | # |-- Bank1 # | @@@ <---------------- # | |-- Checking if len ( node ) > 0 : cont_name = PREFIX_CHILD_C else : cont_name = PREFIX_LEAF_C # Add a line for this account. if not ( node is root and not name ): lines . append (( first + name , cont + cont_name , node )) # Push the children onto the stack, being careful with ordering and # marking the last node as such. if node : child_items = reversed ( node ) child_iter = iter ( child_items ) child_node = next ( child_iter ) stack . append (( cont , child_node . name , child_node , True )) for child_node in child_iter : stack . append (( cont , child_node . name , child_node , False )) if not lines : return lines # Compute the maximum width of the lines and convert all of them to the same # maximal width. This makes it easy on the client. max_width = max ( len ( first_line ) for first_line , _ , __ in lines ) line_format = \"{{: {width} }}\" . format ( width = max_width ) return [ ( line_format . format ( first_line ), line_format . format ( cont_line ), node ) for ( first_line , cont_line , node ) in lines ], max_width","title":"render_tree()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify_test","text":"Unit tests for treeify tool.","title":"treeify_test"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify_test.TestTreeifyBase","text":"","title":"TestTreeifyBase"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify_test.TestTreeifyBase.treeify","text":"Run treeify on the given string and assert no errors. Parameters: string \u2013 A string, the input contents to run on. expect_errors \u2013 A boolean, true if we should expect there to be errors. options \u2013 An optional list of options for the subprogram. Returns: The converted output string. This fails the test if there were any errors. Source code in beancount/tools/treeify_test.py def treeify ( self , string , expect_errors = False , options = None ): \"\"\"Run treeify on the given string and assert no errors. Args: string: A string, the input contents to run on. expect_errors: A boolean, true if we should expect there to be errors. options: An optional list of options for the subprogram. Returns: The converted output string. This fails the test if there were any errors. \"\"\" returncode , output , errors = treeify ( string , options ) actual_errors = returncode != 0 or bool ( errors ) if actual_errors != expect_errors : if expect_errors : self . fail ( \"Missing expected errors\" ) else : self . fail ( \"Unexpected errors: {} \" . format ( errors )) return output","title":"treeify()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify_test.TestTreeifyBase.treeify_equal","text":"Assert an expected treeification result. Parameters: string \u2013 An expected input contents string to treeify. expected \u2013 A string, the expected treeified output. expect_errors \u2013 A boolean, true if we should expect there to be errors. options \u2013 An optional list of options for the subprogram. Returns: The actual output string. This fails the test if there the expected output differed from the actual. Source code in beancount/tools/treeify_test.py def treeify_equal ( self , string , expected , expect_errors = False , options = None ): \"\"\"Assert an expected treeification result. Args: string: An expected input contents string to treeify. expected: A string, the expected treeified output. expect_errors: A boolean, true if we should expect there to be errors. options: An optional list of options for the subprogram. Returns: The actual output string. This fails the test if there the expected output differed from the actual. \"\"\" input_ = textwrap . dedent ( string ) output = self . treeify ( input_ , expect_errors , options ) expected = textwrap . dedent ( expected ) if DEBUG : print ( \"-(input)----------------------------------\" ) print ( input_ ) print ( \"-(output)---------------------------------\" ) print ( output ) print ( \"-(expected)-------------------------------\" ) print ( expected ) print ( \"------------------------------------------\" ) self . assertEqual ( expected , output ) return output","title":"treeify_equal()"},{"location":"api_reference/beancount.tools.html#beancount.tools.treeify_test.treeify","text":"Run treeify on the string. Parameters: string \u2013 The input string to feed treeify. options \u2013 An optional list of options for the subprogram. Returns: The treeified string. Source code in beancount/tools/treeify_test.py def treeify ( string , options = None ): \"\"\"Run treeify on the string. Args: string: The input string to feed treeify. options: An optional list of options for the subprogram. Returns: The treeified string. \"\"\" pipe = subprocess . Popen ( [ PROGRAM ] + ( options or []), shell = False , stdin = subprocess . PIPE , stdout = subprocess . PIPE , stderr = subprocess . PIPE , ) output , errors = pipe . communicate ( string . encode ( \"utf-8\" )) return ( pipe . returncode , output . decode ( \"utf-8\" ) if output else \"\" , errors . decode ( \"utf-8\" ) if errors else \"\" , )","title":"treeify()"},{"location":"api_reference/beancount.utils.html","text":"beancount.utils \uf0c1 Generic utility packages and functions. beancount.utils.bisect_key \uf0c1 A version of bisect that accepts a custom key function, like the sorting ones do. beancount . utils . bisect_key . bisect_left_with_key ( sequence , value , key = None ) \uf0c1 Find the last element before the given value in a sorted list. Parameters: sequence \u2013 A sorted sequence of elements. value \u2013 The value to search for. key \u2013 An optional function used to extract the value from the elements of sequence. Returns: Return the index. May return None. Source code in beancount/utils/bisect_key.py def bisect_left_with_key ( sequence , value , key = None ): \"\"\"Find the last element before the given value in a sorted list. Args: sequence: A sorted sequence of elements. value: The value to search for. key: An optional function used to extract the value from the elements of sequence. Returns: Return the index. May return None. \"\"\" if key is None : key = lambda x : x # Identity. lo = 0 hi = len ( sequence ) while lo < hi : mid = ( lo + hi ) // 2 if key ( sequence [ mid ]) < value : lo = mid + 1 else : hi = mid return lo beancount . utils . bisect_key . bisect_right_with_key ( a , x , key , lo = 0 , hi = None ) \uf0c1 Like bisect.bisect_right, but with a key lookup parameter. Parameters: a \u2013 The list to search in. x \u2013 The element to search for. key \u2013 A function, to extract the value from the list. lo \u2013 The smallest index to search. hi \u2013 The largest index to search. Returns: As in bisect.bisect_right, an element from list 'a'. Source code in beancount/utils/bisect_key.py def bisect_right_with_key ( a , x , key , lo = 0 , hi = None ): \"\"\"Like bisect.bisect_right, but with a key lookup parameter. Args: a: The list to search in. x: The element to search for. key: A function, to extract the value from the list. lo: The smallest index to search. hi: The largest index to search. Returns: As in bisect.bisect_right, an element from list 'a'. \"\"\" if lo < 0 : raise ValueError ( \"lo must be non-negative\" ) if hi is None : hi = len ( a ) while lo < hi : mid = ( lo + hi ) // 2 if x < key ( a [ mid ]): hi = mid else : lo = mid + 1 return lo beancount.utils.date_utils \uf0c1 Parse the date from various formats. beancount . utils . date_utils . intimezone ( tz_value ) \uf0c1 Temporarily reset the value of TZ. This is used for testing. Parameters: tz_value ( str ) \u2013 The value of TZ to set for the duration of this context. Returns: A contextmanager in the given timezone locale. Source code in beancount/utils/date_utils.py @contextlib . contextmanager def intimezone ( tz_value : str ): \"\"\"Temporarily reset the value of TZ. This is used for testing. Args: tz_value: The value of TZ to set for the duration of this context. Returns: A contextmanager in the given timezone locale. \"\"\" tz_old = os . environ . get ( \"TZ\" , None ) os . environ [ \"TZ\" ] = tz_value time . tzset () try : yield finally : if tz_old is None : del os . environ [ \"TZ\" ] else : os . environ [ \"TZ\" ] = tz_old time . tzset () beancount . utils . date_utils . iter_dates ( start_date , end_date ) \uf0c1 Yield all the dates between 'start_date' and 'end_date'. Parameters: start_date \u2013 An instance of datetime.date. end_date \u2013 An instance of datetime.date. Yields: Instances of datetime.date. Source code in beancount/utils/date_utils.py def iter_dates ( start_date , end_date ): \"\"\"Yield all the dates between 'start_date' and 'end_date'. Args: start_date: An instance of datetime.date. end_date: An instance of datetime.date. Yields: Instances of datetime.date. \"\"\" oneday = datetime . timedelta ( days = 1 ) date = start_date while date < end_date : yield date date += oneday beancount . utils . date_utils . next_month ( date ) \uf0c1 Compute the date at the beginning of the following month from the given date. Parameters: date \u2013 A datetime.date instance. Returns: A datetime.date instance, the first day of the month following 'date'. Source code in beancount/utils/date_utils.py def next_month ( date ): \"\"\"Compute the date at the beginning of the following month from the given date. Args: date: A datetime.date instance. Returns: A datetime.date instance, the first day of the month following 'date'. \"\"\" # Compute the date at the beginning of the following month. year = date . year month = date . month + 1 if date . month == 12 : year += 1 month = 1 return datetime . date ( year , month , 1 ) beancount . utils . date_utils . render_ofx_date ( dtime ) \uf0c1 Render a datetime to the OFX format. Parameters: dtime \u2013 A datetime.datetime instance. Returns: A string, rendered to milliseconds. Source code in beancount/utils/date_utils.py def render_ofx_date ( dtime ): \"\"\"Render a datetime to the OFX format. Args: dtime: A datetime.datetime instance. Returns: A string, rendered to milliseconds. \"\"\" return \" {} . {:03d} \" . format ( dtime . strftime ( \"%Y%m %d %H%M%S\" ), int ( dtime . microsecond / 1000 )) beancount.utils.defdict \uf0c1 An instance of collections.defaultdict whose factory accepts a key. Note: This really ought to be an enhancement to Python itself. I should bother adding this in eventually. beancount.utils.defdict.DefaultDictWithKey ( defaultdict ) \uf0c1 A version of defaultdict whose factory accepts the key as an argument. Note: collections.defaultdict would be improved by supporting this directly, this is a common occurrence. beancount.utils.defdict.ImmutableDictWithDefault ( dict ) \uf0c1 An immutable dict which returns a default value for missing keys. This differs from a defaultdict in that it does not insert a missing default value when one is materialized (from a missing fetch), and furthermore, the set method is make unavailable to prevent mutation beyond construction. beancount . utils . defdict . ImmutableDictWithDefault . __setitem__ ( self , key , value ) special \uf0c1 Disallow mutating the dict in the usual way. Source code in beancount/utils/defdict.py def __setitem__ ( self , key , value ): \"\"\"Disallow mutating the dict in the usual way.\"\"\" raise NotImplementedError beancount . utils . defdict . ImmutableDictWithDefault . get ( self , key , _ = None ) \uf0c1 Return the value for key if key is in the dictionary, else default. Source code in beancount/utils/defdict.py def get ( self , key , _ = None ): return self . __getitem__ ( key ) beancount.utils.encryption \uf0c1 Support for encrypted tests. beancount . utils . encryption . is_encrypted_file ( filename ) \uf0c1 Return true if the given filename contains an encrypted file. Parameters: filename \u2013 A path string. Returns: A boolean, true if the file contains an encrypted file. Source code in beancount/utils/encryption.py def is_encrypted_file ( filename ): \"\"\"Return true if the given filename contains an encrypted file. Args: filename: A path string. Returns: A boolean, true if the file contains an encrypted file. \"\"\" _ , ext = path . splitext ( filename ) if ext == \".gpg\" : return True if ext == \".asc\" : with open ( filename ) as encfile : head = encfile . read ( 1024 ) if re . search ( \"--BEGIN PGP MESSAGE--\" , head ): return True return False beancount . utils . encryption . is_gpg_installed () \uf0c1 Return true if GPG 1.4.x or 2.x are installed, which is what we use and support. Source code in beancount/utils/encryption.py def is_gpg_installed (): \"\"\"Return true if GPG 1.4.x or 2.x are installed, which is what we use and support.\"\"\" try : pipe = subprocess . Popen ( [ \"gpg\" , \"--version\" ], shell = 0 , stdout = subprocess . PIPE , stderr = subprocess . PIPE ) out , err = pipe . communicate () version_text = out . decode ( \"utf8\" ) return pipe . returncode == 0 and re . match ( r \"gpg \\(GnuPG\\) (1\\.4|2)\\.\" , version_text ) except OSError : return False beancount . utils . encryption . read_encrypted_file ( filename ) \uf0c1 Decrypt and read an encrypted file without temporary storage. Parameters: filename \u2013 A string, the path to the encrypted file. Returns: A string, the contents of the file. Exceptions: OSError \u2013 If we could not properly decrypt the file. Source code in beancount/utils/encryption.py def read_encrypted_file ( filename ): \"\"\"Decrypt and read an encrypted file without temporary storage. Args: filename: A string, the path to the encrypted file. Returns: A string, the contents of the file. Raises: OSError: If we could not properly decrypt the file. \"\"\" command = [ \"gpg\" , \"--batch\" , \"--decrypt\" , path . realpath ( filename )] pipe = subprocess . Popen ( command , shell = False , stdout = subprocess . PIPE , stderr = subprocess . PIPE ) contents , errors = pipe . communicate () if pipe . returncode != 0 : raise OSError ( \"Could not decrypt file ( {} ): {} \" . format ( pipe . returncode , errors . decode ( \"utf8\" )) ) return contents . decode ( \"utf-8\" ) beancount.utils.file_utils \uf0c1 File utilities. beancount . utils . file_utils . chdir ( directory ) \uf0c1 Temporarily chdir to the given directory. Parameters: directory \u2013 The directory to switch do. Returns: A context manager which restores the cwd after running. Source code in beancount/utils/file_utils.py @contextlib . contextmanager def chdir ( directory ): \"\"\"Temporarily chdir to the given directory. Args: directory: The directory to switch do. Returns: A context manager which restores the cwd after running. \"\"\" cwd = os . getcwd () os . chdir ( directory ) try : yield cwd finally : os . chdir ( cwd ) beancount . utils . file_utils . find_files ( fords , ignore_dirs = ( '.hg' , '.svn' , '.git' ), ignore_files = ( '.DS_Store' ,)) \uf0c1 Enumerate the files under the given directories, stably. Invalid file or directory names will be logged to the error log. Parameters: fords \u2013 A list of strings, file or directory names. ignore_dirs \u2013 A list of strings, filenames or directories to be ignored. Yields: Strings, full filenames from the given roots. Source code in beancount/utils/file_utils.py def find_files ( fords , ignore_dirs = ( \".hg\" , \".svn\" , \".git\" ), ignore_files = ( \".DS_Store\" ,)): \"\"\"Enumerate the files under the given directories, stably. Invalid file or directory names will be logged to the error log. Args: fords: A list of strings, file or directory names. ignore_dirs: A list of strings, filenames or directories to be ignored. Yields: Strings, full filenames from the given roots. \"\"\" if isinstance ( fords , str ): fords = [ fords ] assert isinstance ( fords , ( list , tuple )) for ford in fords : if path . isdir ( ford ): for root , dirs , filenames in os . walk ( ford ): dirs [:] = sorted ( dirname for dirname in dirs if dirname not in ignore_dirs ) for filename in sorted ( filenames ): if filename in ignore_files : continue yield path . join ( root , filename ) elif path . isfile ( ford ) or path . islink ( ford ): yield ford elif not path . exists ( ford ): logging . error ( \"File or directory ' {} ' does not exist.\" . format ( ford )) beancount . utils . file_utils . guess_file_format ( filename , default = None ) \uf0c1 Guess the file format from the filename. Parameters: filename \u2013 A string, the name of the file. This can be None. Returns: A string, the extension of the format, without a leading period. Source code in beancount/utils/file_utils.py def guess_file_format ( filename , default = None ): \"\"\"Guess the file format from the filename. Args: filename: A string, the name of the file. This can be None. Returns: A string, the extension of the format, without a leading period. \"\"\" if filename : if filename . endswith ( \".txt\" ) or filename . endswith ( \".text\" ): format = \"text\" elif filename . endswith ( \".csv\" ): format = \"csv\" elif filename . endswith ( \".html\" ) or filename . endswith ( \".xhtml\" ): format = \"html\" else : format = default else : format = default return format beancount . utils . file_utils . path_greedy_split ( filename ) \uf0c1 Split a path, returning the longest possible extension. Parameters: filename \u2013 A string, the filename to split. Returns: A pair of basename, extension (which includes the leading period). Source code in beancount/utils/file_utils.py def path_greedy_split ( filename ): \"\"\"Split a path, returning the longest possible extension. Args: filename: A string, the filename to split. Returns: A pair of basename, extension (which includes the leading period). \"\"\" basename = path . basename ( filename ) index = basename . find ( \".\" ) if index == - 1 : extension = None else : extension = basename [ index :] basename = basename [: index ] return ( path . join ( path . dirname ( filename ), basename ), extension ) beancount . utils . file_utils . touch_file ( filename , * otherfiles ) \uf0c1 Touch a file and wait until its timestamp has been changed. Parameters: filename \u2013 A string path, the name of the file to touch. otherfiles \u2013 A list of other files to ensure the timestamp is beyond of. Source code in beancount/utils/file_utils.py def touch_file ( filename , * otherfiles ): \"\"\"Touch a file and wait until its timestamp has been changed. Args: filename: A string path, the name of the file to touch. otherfiles: A list of other files to ensure the timestamp is beyond of. \"\"\" # Note: You could set os.stat_float_times() but then the main function would # have to set that up as well. It doesn't help so much, however, since # filesystems tend to have low resolutions, e.g. one second. orig_mtime_ns = max ( os . stat ( minfile ) . st_mtime_ns for minfile in ( filename ,) + otherfiles ) delay_secs = 0.05 while True : with open ( filename , \"a\" ): os . utime ( filename ) time . sleep ( delay_secs ) new_stat = os . stat ( filename ) if new_stat . st_mtime_ns > orig_mtime_ns : break beancount.utils.import_utils \uf0c1 Utilities for importing symbols programmatically. beancount . utils . import_utils . import_symbol ( dotted_name ) \uf0c1 Import a symbol in an arbitrary module. Parameters: dotted_name \u2013 A dotted path to a symbol. Returns: The object referenced by the given name. Exceptions: ImportError \u2013 If the module not not be imported. AttributeError \u2013 If the symbol could not be found in the module. Source code in beancount/utils/import_utils.py def import_symbol ( dotted_name ): \"\"\"Import a symbol in an arbitrary module. Args: dotted_name: A dotted path to a symbol. Returns: The object referenced by the given name. Raises: ImportError: If the module not not be imported. AttributeError: If the symbol could not be found in the module. \"\"\" comps = dotted_name . split ( \".\" ) module_name = \".\" . join ( comps [: - 1 ]) symbol_name = comps [ - 1 ] module = importlib . import_module ( module_name ) return getattr ( module , symbol_name ) beancount.utils.invariants \uf0c1 Functions to register auxiliary functions on a class' methods to check for invariants. This is intended to be used in a test, whereby your test will setup a class to automatically run invariant verification functions before and after each function call, to ensure some extra sanity checks that wouldn't be used in non-tests. Example: Instrument the Inventory class with the check_inventory_invariants() function. def setUp(module): instrument_invariants(Inventory, check_inventory_invariants, check_inventory_invariants) def tearDown(module): uninstrument_invariants(Inventory) beancount . utils . invariants . instrument_invariants ( klass , prefun , postfun ) \uf0c1 Instrument the class 'klass' with pre/post invariant checker functions. Parameters: klass \u2013 A class object, whose methods to be instrumented. prefun \u2013 A function that checks invariants pre-call. postfun \u2013 A function that checks invariants pre-call. Source code in beancount/utils/invariants.py def instrument_invariants ( klass , prefun , postfun ): \"\"\"Instrument the class 'klass' with pre/post invariant checker functions. Args: klass: A class object, whose methods to be instrumented. prefun: A function that checks invariants pre-call. postfun: A function that checks invariants pre-call. \"\"\" instrumented = {} for attrname , object_ in klass . __dict__ . items (): if attrname . startswith ( \"_\" ): continue if not isinstance ( object_ , types . FunctionType ): continue instrumented [ attrname ] = object_ setattr ( klass , attrname , invariant_check ( object_ , prefun , postfun )) klass . __instrumented = instrumented beancount . utils . invariants . invariant_check ( method , prefun , postfun ) \uf0c1 Decorate a method with the pre/post invariant checkers. Parameters: method \u2013 An unbound method to instrument. prefun \u2013 A function that checks invariants pre-call. postfun \u2013 A function that checks invariants post-call. Returns: An unbound method, decorated. Source code in beancount/utils/invariants.py def invariant_check ( method , prefun , postfun ): \"\"\"Decorate a method with the pre/post invariant checkers. Args: method: An unbound method to instrument. prefun: A function that checks invariants pre-call. postfun: A function that checks invariants post-call. Returns: An unbound method, decorated. \"\"\" reentrant = [] def new_method ( self , * args , ** kw ): reentrant . append ( None ) if len ( reentrant ) == 1 : prefun ( self ) result = method ( self , * args , ** kw ) if len ( reentrant ) == 1 : postfun ( self ) reentrant . pop () return result return new_method beancount . utils . invariants . uninstrument_invariants ( klass ) \uf0c1 Undo the instrumentation for invariants. Parameters: klass \u2013 A class object, whose methods to be uninstrumented. Source code in beancount/utils/invariants.py def uninstrument_invariants ( klass ): \"\"\"Undo the instrumentation for invariants. Args: klass: A class object, whose methods to be uninstrumented. \"\"\" instrumented = getattr ( klass , \"__instrumented\" , None ) if instrumented : for attrname , object_ in instrumented . items (): setattr ( klass , attrname , object_ ) del klass . __instrumented beancount.utils.memo \uf0c1 Memoization utilities. beancount . utils . memo . memoize_recent_fileobj ( function , cache_filename , expiration = None ) \uf0c1 Memoize recent calls to the given function which returns a file object. The results of the cache expire after some time. Parameters: function \u2013 A callable object. cache_filename \u2013 A string, the path to the database file to cache to. expiration \u2013 The time during which the results will be kept valid. Use 'None' to never expire the cache (this is the default). Returns: A memoized version of the function. Source code in beancount/utils/memo.py def memoize_recent_fileobj ( function , cache_filename , expiration = None ): \"\"\"Memoize recent calls to the given function which returns a file object. The results of the cache expire after some time. Args: function: A callable object. cache_filename: A string, the path to the database file to cache to. expiration: The time during which the results will be kept valid. Use 'None' to never expire the cache (this is the default). Returns: A memoized version of the function. \"\"\" urlcache = shelve . open ( cache_filename , \"c\" ) urlcache . lock = threading . Lock () # Note: 'shelve' is not thread-safe. @functools . wraps ( function ) def memoized ( * args , ** kw ): # Encode the arguments, including a date string in order to invalidate # results over some time. md5 = hashlib . md5 () md5 . update ( str ( args ) . encode ( \"utf-8\" )) md5 . update ( str ( sorted ( kw . items ())) . encode ( \"utf-8\" )) hash_ = md5 . hexdigest () time_now = now () try : with urlcache . lock : time_orig , contents = urlcache [ hash_ ] if expiration is not None and ( time_now - time_orig ) > expiration : raise KeyError except KeyError : fileobj = function ( * args , ** kw ) if fileobj : contents = fileobj . read () with urlcache . lock : urlcache [ hash_ ] = ( time_now , contents ) else : contents = None return io . BytesIO ( contents ) if contents else None return memoized beancount . utils . memo . now () \uf0c1 Indirection on datetime.datetime.now() for testing. Source code in beancount/utils/memo.py def now (): \"Indirection on datetime.datetime.now() for testing.\" return datetime . datetime . now () beancount.utils.misc_utils \uf0c1 Generic utility packages and functions. beancount.utils.misc_utils.LineFileProxy \uf0c1 A file object that will delegate writing full lines to another logging function. This may be used for writing data to a logging level without having to worry about lines. beancount . utils . misc_utils . LineFileProxy . __init__ ( self , line_writer , prefix = None , write_newlines = False ) special \uf0c1 Construct a new line delegator file object proxy. Parameters: line_writer \u2013 A callable function, used to write to the delegated output. prefix \u2013 An optional string, the prefix to insert before every line. write_newlines \u2013 A boolean, true if we should output the newline characters. Source code in beancount/utils/misc_utils.py def __init__ ( self , line_writer , prefix = None , write_newlines = False ): \"\"\"Construct a new line delegator file object proxy. Args: line_writer: A callable function, used to write to the delegated output. prefix: An optional string, the prefix to insert before every line. write_newlines: A boolean, true if we should output the newline characters. \"\"\" self . line_writer = line_writer self . prefix = prefix self . write_newlines = write_newlines self . data = [] beancount . utils . misc_utils . LineFileProxy . close ( self ) \uf0c1 Close the line delegator. Source code in beancount/utils/misc_utils.py def close ( self ): \"\"\"Close the line delegator.\"\"\" self . flush () beancount . utils . misc_utils . LineFileProxy . flush ( self ) \uf0c1 Flush the data to the line writer. Source code in beancount/utils/misc_utils.py def flush ( self ): \"\"\"Flush the data to the line writer.\"\"\" data = \"\" . join ( self . data ) if data : lines = data . splitlines () self . data = [ lines . pop ( - 1 )] if data [ - 1 ] != \" \\n \" else [] for line in lines : if self . prefix : line = self . prefix + line if self . write_newlines : line += \" \\n \" self . line_writer ( line ) beancount . utils . misc_utils . LineFileProxy . write ( self , data ) \uf0c1 Write some string data to the output. Parameters: data \u2013 A string, with or without newlines. Source code in beancount/utils/misc_utils.py def write ( self , data ): \"\"\"Write some string data to the output. Args: data: A string, with or without newlines. \"\"\" if \" \\n \" in data : self . data . append ( data ) self . flush () else : self . data . append ( data ) beancount.utils.misc_utils.TypeComparable \uf0c1 A base class whose equality comparison includes comparing the type of the instance itself. beancount . utils . misc_utils . box ( name = None , file = None ) \uf0c1 A context manager that prints out a box around a block. This is useful for printing out stuff from tests in a way that is readable. Parameters: name \u2013 A string, the name of the box to use. file \u2013 The file object to print to. Yields: None. Source code in beancount/utils/misc_utils.py @contextlib . contextmanager def box ( name = None , file = None ): \"\"\"A context manager that prints out a box around a block. This is useful for printing out stuff from tests in a way that is readable. Args: name: A string, the name of the box to use. file: The file object to print to. Yields: None. \"\"\" file = file or sys . stdout file . write ( \" \\n \" ) if name : header = \",--------( {} )-------- \\n \" . format ( name ) footer = \"` {} \\n \" . format ( \"-\" * ( len ( header ) - 2 )) else : header = \",---------------- \\n \" footer = \"`---------------- \\n \" file . write ( header ) yield file . write ( footer ) file . flush () beancount . utils . misc_utils . cmptuple ( name , attributes ) \uf0c1 Manufacture a comparable namedtuple class, similar to collections.namedtuple. A comparable named tuple is a tuple which compares to False if contents are equal but the data types are different. We define this to supplement collections.namedtuple because by default a namedtuple disregards the type and we want to make precise comparisons for tests. Parameters: name \u2013 The given name of the class. attributes \u2013 A string or tuple of strings, with the names of the attributes. Returns: A new namedtuple-derived type that compares False with other tuples with same contents. Source code in beancount/utils/misc_utils.py def cmptuple ( name , attributes ): \"\"\"Manufacture a comparable namedtuple class, similar to collections.namedtuple. A comparable named tuple is a tuple which compares to False if contents are equal but the data types are different. We define this to supplement collections.namedtuple because by default a namedtuple disregards the type and we want to make precise comparisons for tests. Args: name: The given name of the class. attributes: A string or tuple of strings, with the names of the attributes. Returns: A new namedtuple-derived type that compares False with other tuples with same contents. \"\"\" base = collections . namedtuple ( \"_ {} \" . format ( name ), attributes ) return type ( name , ( TypeComparable , base , ), {}, ) beancount . utils . misc_utils . compute_unique_clean_ids ( strings ) \uf0c1 Given a sequence of strings, reduce them to corresponding ids without any funny characters and insure that the list of ids is unique. Yields pairs of (id, string) for the result. Parameters: strings \u2013 A list of strings. Returns: A list of (id, string) pairs. Source code in beancount/utils/misc_utils.py def compute_unique_clean_ids ( strings ): \"\"\"Given a sequence of strings, reduce them to corresponding ids without any funny characters and insure that the list of ids is unique. Yields pairs of (id, string) for the result. Args: strings: A list of strings. Returns: A list of (id, string) pairs. \"\"\" string_set = set ( strings ) # Try multiple methods until we get one that has no collisions. for regexp , replacement in [ ( r \"[^A-Za-z0-9.-]\" , \"_\" ), ( r \"[^A-Za-z0-9_]\" , \"\" ), ]: seen = set () idmap = {} mre = re . compile ( regexp ) for string in string_set : id_ = mre . sub ( replacement , string ) if id_ in seen : break # Collision. seen . add ( id_ ) idmap [ id_ ] = string else : break else : return None # Could not find a unique mapping. return idmap beancount . utils . misc_utils . deprecated ( message ) \uf0c1 A decorator generator to mark functions as deprecated and log a warning. Source code in beancount/utils/misc_utils.py def deprecated ( message ): \"\"\"A decorator generator to mark functions as deprecated and log a warning.\"\"\" def decorator ( func ): @functools . wraps ( func ) def new_func ( * args , ** kwargs ): warnings . warn ( \"Call to deprecated function {} : {} \" . format ( func . __name__ , message ), category = DeprecationWarning , stacklevel = 2 , ) return func ( * args , ** kwargs ) return new_func return decorator beancount . utils . misc_utils . dictmap ( mdict , keyfun = None , valfun = None ) \uf0c1 Map a dictionary's value. Parameters: mdict \u2013 A dict. key \u2013 A callable to apply to the keys. value \u2013 A callable to apply to the values. Source code in beancount/utils/misc_utils.py def dictmap ( mdict , keyfun = None , valfun = None ): \"\"\"Map a dictionary's value. Args: mdict: A dict. key: A callable to apply to the keys. value: A callable to apply to the values. \"\"\" if keyfun is None : keyfun = lambda x : x if valfun is None : valfun = lambda x : x return { keyfun ( key ): valfun ( val ) for key , val in mdict . items ()} beancount . utils . misc_utils . escape_string ( string ) \uf0c1 Escape quotes and backslashes in payee and narration. Parameters: string \u2013 Any string. Returns. The input string, with offending characters replaced. Source code in beancount/utils/misc_utils.py def escape_string ( string ): \"\"\"Escape quotes and backslashes in payee and narration. Args: string: Any string. Returns. The input string, with offending characters replaced. \"\"\" return string . replace ( \" \\\\ \" , r \" \\\\ \" ) . replace ( '\"' , r \" \\\" \" ) beancount . utils . misc_utils . filter_type ( elist , types ) \uf0c1 Filter the given list to yield only instances of the given types. Parameters: elist \u2013 A sequence of elements. types \u2013 A sequence of types to include in the output list. Yields: Each element, if it is an instance of 'types'. Source code in beancount/utils/misc_utils.py def filter_type ( elist , types ): \"\"\"Filter the given list to yield only instances of the given types. Args: elist: A sequence of elements. types: A sequence of types to include in the output list. Yields: Each element, if it is an instance of 'types'. \"\"\" for element in elist : if not isinstance ( element , types ): continue yield element beancount . utils . misc_utils . first_paragraph ( docstring ) \uf0c1 Return the first sentence of a docstring. The sentence has to be delimited by an empty line. Parameters: docstring \u2013 A doc string. Returns: A string with just the first sentence on a single line. Source code in beancount/utils/misc_utils.py def first_paragraph ( docstring ): \"\"\"Return the first sentence of a docstring. The sentence has to be delimited by an empty line. Args: docstring: A doc string. Returns: A string with just the first sentence on a single line. \"\"\" lines = [] for line in docstring . strip () . splitlines (): if not line : break lines . append ( line . rstrip ()) return \" \" . join ( lines ) beancount . utils . misc_utils . get_screen_height () \uf0c1 Return the height of the terminal that runs this program. Returns: An integer, the number of characters the screen is high. Return 0 if the terminal cannot be initialized. Source code in beancount/utils/misc_utils.py def get_screen_height (): \"\"\"Return the height of the terminal that runs this program. Returns: An integer, the number of characters the screen is high. Return 0 if the terminal cannot be initialized. \"\"\" return _get_screen_value ( \"lines\" , 0 ) beancount . utils . misc_utils . get_screen_width () \uf0c1 Return the width of the terminal that runs this program. Returns: An integer, the number of characters the screen is wide. Return 0 if the terminal cannot be initialized. Source code in beancount/utils/misc_utils.py def get_screen_width (): \"\"\"Return the width of the terminal that runs this program. Returns: An integer, the number of characters the screen is wide. Return 0 if the terminal cannot be initialized. \"\"\" return _get_screen_value ( \"cols\" , 0 ) beancount . utils . misc_utils . get_tuple_values ( ntuple , predicate , memo = None ) \uf0c1 Return all members referred to by this namedtuple instance that satisfy the given predicate. This function also works recursively on its members which are lists or tuples, and so it can be used for Transaction instances. Parameters: ntuple \u2013 A tuple or namedtuple. predicate \u2013 A predicate function that returns true if an attribute is to be output. memo \u2013 An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. Source code in beancount/utils/misc_utils.py def get_tuple_values ( ntuple , predicate , memo = None ): \"\"\"Return all members referred to by this namedtuple instance that satisfy the given predicate. This function also works recursively on its members which are lists or tuples, and so it can be used for Transaction instances. Args: ntuple: A tuple or namedtuple. predicate: A predicate function that returns true if an attribute is to be output. memo: An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. \"\"\" if memo is None : memo = set () id_ntuple = id ( ntuple ) if id_ntuple in memo : return memo . add ( id_ntuple ) if predicate ( ntuple ): yield for attribute in ntuple : if predicate ( attribute ): yield attribute if isinstance ( attribute , ( list , tuple )): for value in get_tuple_values ( attribute , predicate , memo ): yield value beancount . utils . misc_utils . groupby ( keyfun , elements ) \uf0c1 Group the elements as a dict of lists, where the key is computed using the function 'keyfun'. Parameters: keyfun \u2013 A callable, used to obtain the group key from each element. elements \u2013 An iterable of the elements to group. Returns: A dict of key to list of sequences. Source code in beancount/utils/misc_utils.py def groupby ( keyfun , elements ): \"\"\"Group the elements as a dict of lists, where the key is computed using the function 'keyfun'. Args: keyfun: A callable, used to obtain the group key from each element. elements: An iterable of the elements to group. Returns: A dict of key to list of sequences. \"\"\" # Note: We could allow a custom aggregation function. Another option is # provide another method to reduce the list values of a dict, but that can # be accomplished using a dict comprehension. grouped = defaultdict ( list ) for element in elements : grouped [ keyfun ( element )] . append ( element ) return grouped beancount . utils . misc_utils . idify ( string ) \uf0c1 Replace characters objectionable for a filename with underscores. Parameters: string \u2013 Any string. Returns: The input string, with offending characters replaced. Source code in beancount/utils/misc_utils.py def idify ( string ): \"\"\"Replace characters objectionable for a filename with underscores. Args: string: Any string. Returns: The input string, with offending characters replaced. \"\"\" for sfrom , sto in [( r \"[ \\(\\)]+\" , \"_\" ), ( r \"_*\\._*\" , \".\" )]: string = re . sub ( sfrom , sto , string ) string = string . strip ( \"_\" ) return string beancount . utils . misc_utils . import_curses () \uf0c1 Try to import the 'curses' module. (This is used here in order to override for tests.) Returns: The curses module, if it was possible to import it. Exceptions: ImportError \u2013 If the module could not be imported. Source code in beancount/utils/misc_utils.py def import_curses (): \"\"\"Try to import the 'curses' module. (This is used here in order to override for tests.) Returns: The curses module, if it was possible to import it. Raises: ImportError: If the module could not be imported. \"\"\" # Note: There's a recipe for getting terminal size on Windows here, without # curses, I should probably implement that at some point: # https://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window # Also, consider just using 'blessings' instead, which provides this across # multiple platforms. import curses return curses beancount . utils . misc_utils . is_sorted ( iterable , key =< function < lambda > at 0x78fcde6aac00 > , cmp =< function < lambda > at 0x78fcde6aaca0 > ) \uf0c1 Return true if the sequence is sorted. Parameters: iterable \u2013 An iterable sequence. key \u2013 A function to extract the quantity by which to sort. cmp \u2013 A function that compares two elements of a sequence. Returns: A boolean, true if the sequence is sorted. Source code in beancount/utils/misc_utils.py def is_sorted ( iterable , key = lambda x : x , cmp = lambda x , y : x <= y ): \"\"\"Return true if the sequence is sorted. Args: iterable: An iterable sequence. key: A function to extract the quantity by which to sort. cmp: A function that compares two elements of a sequence. Returns: A boolean, true if the sequence is sorted. \"\"\" iterator = map ( key , iterable ) prev = next ( iterator ) for element in iterator : if not cmp ( prev , element ): return False prev = element return True beancount . utils . misc_utils . log_time ( operation_name , log_timings , indent = 0 ) \uf0c1 A context manager that times the block and logs it to info level. Parameters: operation_name \u2013 A string, a label for the name of the operation. log_timings \u2013 A function to write log messages to. If left to None, no timings are written (this becomes a no-op). indent \u2013 An integer, the indentation level for the format of the timing line. This is useful if you're logging timing to a hierarchy of operations. Yields: The start time of the operation. Source code in beancount/utils/misc_utils.py @contextlib . contextmanager def log_time ( operation_name , log_timings , indent = 0 ): \"\"\"A context manager that times the block and logs it to info level. Args: operation_name: A string, a label for the name of the operation. log_timings: A function to write log messages to. If left to None, no timings are written (this becomes a no-op). indent: An integer, the indentation level for the format of the timing line. This is useful if you're logging timing to a hierarchy of operations. Yields: The start time of the operation. \"\"\" time1 = time () yield time1 time2 = time () if log_timings : log_timings ( \"Operation: {:48} Time: {}{:6.0f} ms\" . format ( \"' {} '\" . format ( operation_name ), \" \" * indent , ( time2 - time1 ) * 1000 ) ) beancount . utils . misc_utils . longest ( seq ) \uf0c1 Return the longest of the given subsequences. Parameters: seq \u2013 An iterable sequence of lists. Returns: The longest list from the sequence. Source code in beancount/utils/misc_utils.py def longest ( seq ): \"\"\"Return the longest of the given subsequences. Args: seq: An iterable sequence of lists. Returns: The longest list from the sequence. \"\"\" longest , length = None , - 1 for element in seq : len_element = len ( element ) if len_element > length : longest , length = element , len_element return longest beancount . utils . misc_utils . map_namedtuple_attributes ( attributes , mapper , object_ ) \uf0c1 Map the value of the named attributes of object by mapper. Parameters: attributes \u2013 A sequence of string, the attribute names to map. mapper \u2013 A callable that accepts the value of a field and returns the new value. object_ \u2013 Some namedtuple object with attributes on it. Returns: A new instance of the same namedtuple with the named fields mapped by mapper. Source code in beancount/utils/misc_utils.py def map_namedtuple_attributes ( attributes , mapper , object_ ): \"\"\"Map the value of the named attributes of object by mapper. Args: attributes: A sequence of string, the attribute names to map. mapper: A callable that accepts the value of a field and returns the new value. object_: Some namedtuple object with attributes on it. Returns: A new instance of the same namedtuple with the named fields mapped by mapper. \"\"\" return object_ . _replace ( ** { attribute : mapper ( getattr ( object_ , attribute )) for attribute in attributes } ) beancount . utils . misc_utils . replace_namedtuple_values ( ntuple , predicate , mapper , memo = None ) \uf0c1 Recurse through all the members of namedtuples and lists, and for members that match the given predicate, run them through the given mapper. Parameters: ntuple \u2013 A namedtuple instance. predicate \u2013 A predicate function that returns true if an attribute is to be output. mapper \u2013 A callable, that will accept a single argument and return its replacement value. memo \u2013 An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. Source code in beancount/utils/misc_utils.py def replace_namedtuple_values ( ntuple , predicate , mapper , memo = None ): \"\"\"Recurse through all the members of namedtuples and lists, and for members that match the given predicate, run them through the given mapper. Args: ntuple: A namedtuple instance. predicate: A predicate function that returns true if an attribute is to be output. mapper: A callable, that will accept a single argument and return its replacement value. memo: An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. \"\"\" if memo is None : memo = set () id_ntuple = id ( ntuple ) if id_ntuple in memo : return None memo . add ( id_ntuple ) if not ( type ( ntuple ) is not tuple and isinstance ( ntuple , tuple )): return ntuple replacements = {} for attribute_name , attribute in zip ( ntuple . _fields , ntuple ): if predicate ( attribute ): replacements [ attribute_name ] = mapper ( attribute ) elif type ( attribute ) is not tuple and isinstance ( attribute , tuple ): replacements [ attribute_name ] = replace_namedtuple_values ( attribute , predicate , mapper , memo ) elif type ( attribute ) in ( list , tuple ): replacements [ attribute_name ] = [ replace_namedtuple_values ( member , predicate , mapper , memo ) for member in attribute ] return ntuple . _replace ( ** replacements ) beancount . utils . misc_utils . skipiter ( iterable , num_skip ) \uf0c1 Skip some elements from an iterator. Parameters: iterable \u2013 An iterator. num_skip \u2013 The number of elements in the period. Yields: Elements from the iterable, with num_skip elements skipped. For example, skipiter(range(10), 3) yields [0, 3, 6, 9]. Source code in beancount/utils/misc_utils.py def skipiter ( iterable , num_skip ): \"\"\"Skip some elements from an iterator. Args: iterable: An iterator. num_skip: The number of elements in the period. Yields: Elements from the iterable, with num_skip elements skipped. For example, skipiter(range(10), 3) yields [0, 3, 6, 9]. \"\"\" assert num_skip > 0 sit = iter ( iterable ) while 1 : try : value = next ( sit ) except StopIteration : return yield value for _ in range ( num_skip - 1 ): try : next ( sit ) except StopIteration : return beancount . utils . misc_utils . sorted_uniquify ( iterable , keyfunc = None , last = False ) \uf0c1 Given a sequence of elements, sort and remove duplicates of the given key. Keep either the first or the last (by key) element of a sequence of key-identical elements. This does not maintain the ordering of the original elements, they are returned sorted (by key) instead. Parameters: iterable \u2013 An iterable sequence. keyfunc \u2013 A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last \u2013 A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. Source code in beancount/utils/misc_utils.py def sorted_uniquify ( iterable , keyfunc = None , last = False ): \"\"\"Given a sequence of elements, sort and remove duplicates of the given key. Keep either the first or the last (by key) element of a sequence of key-identical elements. This does _not_ maintain the ordering of the original elements, they are returned sorted (by key) instead. Args: iterable: An iterable sequence. keyfunc: A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last: A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. \"\"\" if keyfunc is None : keyfunc = lambda x : x if last : prev_obj = UNSET prev_key = UNSET for obj in sorted ( iterable , key = keyfunc ): key = keyfunc ( obj ) if key != prev_key and prev_obj is not UNSET : yield prev_obj prev_obj = obj prev_key = key if prev_obj is not UNSET : yield prev_obj else : prev_key = UNSET for obj in sorted ( iterable , key = keyfunc ): key = keyfunc ( obj ) if key != prev_key : yield obj prev_key = key beancount . utils . misc_utils . staticvar ( varname , initial_value ) \uf0c1 Returns a decorator that defines a Python function attribute. This is used to simulate a static function variable in Python. Parameters: varname \u2013 A string, the name of the variable to define. initial_value \u2013 The value to initialize the variable to. Returns: A function decorator. Source code in beancount/utils/misc_utils.py def staticvar ( varname , initial_value ): \"\"\"Returns a decorator that defines a Python function attribute. This is used to simulate a static function variable in Python. Args: varname: A string, the name of the variable to define. initial_value: The value to initialize the variable to. Returns: A function decorator. \"\"\" def deco ( fun ): setattr ( fun , varname , initial_value ) return fun return deco beancount . utils . misc_utils . swallow ( * exception_types ) \uf0c1 Catch and ignore certain exceptions. Parameters: exception_types \u2013 A tuple of exception classes to ignore. Yields: None. Source code in beancount/utils/misc_utils.py @contextlib . contextmanager def swallow ( * exception_types ): \"\"\"Catch and ignore certain exceptions. Args: exception_types: A tuple of exception classes to ignore. Yields: None. \"\"\" try : yield except Exception as exc : if not isinstance ( exc , exception_types ): raise beancount . utils . misc_utils . uniquify ( iterable , keyfunc = None , last = False ) \uf0c1 Given a sequence of elements, remove duplicates of the given key. Keep either the first or the last element of a sequence of key-identical elements. Order is maintained as much as possible. This does maintain the ordering of the original elements, they are returned in the same order as the original elements. Parameters: iterable \u2013 An iterable sequence. keyfunc \u2013 A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last \u2013 A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. Source code in beancount/utils/misc_utils.py def uniquify ( iterable , keyfunc = None , last = False ): \"\"\"Given a sequence of elements, remove duplicates of the given key. Keep either the first or the last element of a sequence of key-identical elements. Order is maintained as much as possible. This does maintain the ordering of the original elements, they are returned in the same order as the original elements. Args: iterable: An iterable sequence. keyfunc: A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last: A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. \"\"\" if keyfunc is None : keyfunc = lambda x : x seen = set () if last : unique_reversed_list = [] for obj in reversed ( iterable ): key = keyfunc ( obj ) if key not in seen : seen . add ( key ) unique_reversed_list . append ( obj ) yield from reversed ( unique_reversed_list ) else : for obj in iterable : key = keyfunc ( obj ) if key not in seen : seen . add ( key ) yield obj beancount.utils.pager \uf0c1 Code to write output to a pager. This module contains an object accumulates lines up to a minimum and then decides whether to flush them to the original output directly if under the threshold (no pager) or creates a pager and flushes the lines to it if above the threshold and then forwards all future lines to it. The purpose of this object is to pipe output to a pager only if the number of lines to be printed exceeds a minimum number of lines. The contextmanager is intended to be used to pipe output to a pager and wait on the pager to complete before continuing. Simply write to the file object and upon exit we close the file object. This also silences broken pipe errors triggered by the user exiting the sub-process, and recovers from a failing pager command by just using stdout. beancount.utils.pager.ConditionalPager \uf0c1 A proxy file for a pager that only creates a pager after a minimum number of lines has been printed to it. beancount . utils . pager . ConditionalPager . __enter__ ( self ) special \uf0c1 Initialize the context manager and return this instance as it. Source code in beancount/utils/pager.py def __enter__ ( self ): \"\"\"Initialize the context manager and return this instance as it.\"\"\" # The file and pipe object we're writing to. This gets set after the # number of accumulated lines reaches the threshold. if self . minlines : self . file = None self . pipe = None else : self . file , self . pipe = create_pager ( self . command , self . default_file ) # Lines accumulated before the threshold. self . accumulated_data = [] self . accumulated_lines = 0 # Return this object to be used as the context manager itself. return self beancount . utils . pager . ConditionalPager . __exit__ ( self , type , value , unused_traceback ) special \uf0c1 Context manager exit. This flushes the output to our output file. Parameters: type \u2013 Optional exception type, as per context managers. value \u2013 Optional exception value, as per context managers. unused_traceback \u2013 Optional trace. Source code in beancount/utils/pager.py def __exit__ ( self , type , value , unused_traceback ): \"\"\"Context manager exit. This flushes the output to our output file. Args: type: Optional exception type, as per context managers. value: Optional exception value, as per context managers. unused_traceback: Optional trace. \"\"\" try : if self . file : # Flush the output file and close it. self . file . flush () else : # Oops... we never reached the threshold. Flush the accumulated # output to the file. self . flush_accumulated ( self . default_file ) # Wait for the subprocess (if we have one). if self . pipe : self . file . close () self . pipe . wait () # Absorb broken pipes that may occur on flush or close above. except BrokenPipeError : return True # Absorb broken pipes. if isinstance ( value , BrokenPipeError ): return True elif value : raise beancount . utils . pager . ConditionalPager . __init__ ( self , command , minlines = None ) special \uf0c1 Create a conditional pager. Parameters: command \u2013 A string, the shell command to run as a pager. minlines \u2013 If set, the number of lines under which you should not bother starting a pager. This avoids kicking off a pager if the screen is high enough to render the contents. If the value is unset, always starts a pager (which is fine behavior too). Source code in beancount/utils/pager.py def __init__ ( self , command , minlines = None ): \"\"\"Create a conditional pager. Args: command: A string, the shell command to run as a pager. minlines: If set, the number of lines under which you should not bother starting a pager. This avoids kicking off a pager if the screen is high enough to render the contents. If the value is unset, always starts a pager (which is fine behavior too). \"\"\" self . command = command self . minlines = minlines self . default_file = ( codecs . getwriter ( \"utf-8\" )( sys . stdout . buffer ) if hasattr ( sys . stdout , \"buffer\" ) else sys . stdout ) beancount . utils . pager . ConditionalPager . flush_accumulated ( self , file ) \uf0c1 Flush the existing lines to the newly created pager. This also disabled the accumulator. Parameters: file \u2013 A file object to flush the accumulated data to. Source code in beancount/utils/pager.py def flush_accumulated ( self , file ): \"\"\"Flush the existing lines to the newly created pager. This also disabled the accumulator. Args: file: A file object to flush the accumulated data to. \"\"\" if self . accumulated_data : write = file . write for data in self . accumulated_data : write ( data ) self . accumulated_data = None self . accumulated_lines = None beancount . utils . pager . ConditionalPager . write ( self , data ) \uf0c1 Write the data out. Overridden from the file object interface. Parameters: data \u2013 A string, data to write to the output. Source code in beancount/utils/pager.py def write ( self , data ): \"\"\"Write the data out. Overridden from the file object interface. Args: data: A string, data to write to the output. \"\"\" if self . file is None : # Accumulate the new lines. self . accumulated_lines += data . count ( \" \\n \" ) self . accumulated_data . append ( data ) # If we've reached the threshold, create a file. if self . accumulated_lines > self . minlines : self . file , self . pipe = create_pager ( self . command , self . default_file ) self . flush_accumulated ( self . file ) else : # We've already created a pager subprocess... flush the lines to it. self . file . write ( data ) # try: # except BrokenPipeError: # # Make sure we don't barf on __exit__(). # self.file = self.pipe = None # raise beancount . utils . pager . create_pager ( command , file ) \uf0c1 Try to create and return a pager subprocess. Parameters: command \u2013 A string, the shell command to run as a pager. file \u2013 The file object for the pager write to. This is also used as a default if we failed to create the pager subprocess. Returns: A pair of (file, pipe), a file object and an optional subprocess.Popen instance to wait on. The pipe instance may be set to None if we failed to create a subprocess. Source code in beancount/utils/pager.py def create_pager ( command , file ): \"\"\"Try to create and return a pager subprocess. Args: command: A string, the shell command to run as a pager. file: The file object for the pager write to. This is also used as a default if we failed to create the pager subprocess. Returns: A pair of (file, pipe), a file object and an optional subprocess.Popen instance to wait on. The pipe instance may be set to None if we failed to create a subprocess. \"\"\" if command is None : command = os . environ . get ( \"PAGER\" , DEFAULT_PAGER ) if not command : command = DEFAULT_PAGER pipe = None # In case of using 'less', make sure the charset is set properly. In theory # you could override this by setting PAGER to \"LESSCHARSET=utf-8 less\" but # this shouldn't affect other programs and is unlikely to cause problems, so # we set it here to make default behavior work for most people (we always # write UTF-8). env = os . environ . copy () env [ \"LESSCHARSET\" ] = \"utf-8\" try : pipe = subprocess . Popen ( command , shell = True , stdin = subprocess . PIPE , stdout = file , env = env ) except OSError as exc : logging . error ( \"Invalid pager: {} \" . format ( exc )) else : stdin_wrapper = io . TextIOWrapper ( pipe . stdin , \"utf-8\" ) file = stdin_wrapper return file , pipe beancount . utils . pager . flush_only ( fileobj ) \uf0c1 A contextmanager around a file object that does not close the file. This is used to return a context manager on a file object but not close it. We flush it instead. This is useful in order to provide an alternative to a pager class as above. Parameters: fileobj \u2013 A file object, to remain open after running the context manager. Yields: A context manager that yields this object. Source code in beancount/utils/pager.py @contextlib . contextmanager def flush_only ( fileobj ): \"\"\"A contextmanager around a file object that does not close the file. This is used to return a context manager on a file object but not close it. We flush it instead. This is useful in order to provide an alternative to a pager class as above. Args: fileobj: A file object, to remain open after running the context manager. Yields: A context manager that yields this object. \"\"\" try : yield fileobj finally : fileobj . flush () beancount.utils.snoop \uf0c1 Text manipulation utilities. beancount.utils.snoop.Snoop \uf0c1 A snooper callable that just saves the returned values of a function. This is particularly useful for re.match and re.search in conditionals, e.g.:: snoop = Snoop() ... if snoop(re.match(r\"(\\d+)-(\\d+)-(\\d+)\", text)): year, month, date = snoop.value.group(1, 2, 3) Attributes: Name Type Description value The last value snooped from a function call. history If 'maxlen' was specified, the last few values that were snooped. beancount . utils . snoop . Snoop . __call__ ( self , value ) special \uf0c1 Save a value to the snooper. This is meant to wrap a function call. Parameters: value \u2013 The value to push/save. Returns: Value itself. Source code in beancount/utils/snoop.py def __call__ ( self , value ): \"\"\"Save a value to the snooper. This is meant to wrap a function call. Args: value: The value to push/save. Returns: Value itself. \"\"\" self . value = value if self . history is not None : self . history . append ( value ) return value beancount . utils . snoop . Snoop . __getattr__ ( self , attr ) special \uf0c1 Forward the attribute to the value. Parameters: attr \u2013 A string, the name of the attribute. Returns: The value of the attribute. Source code in beancount/utils/snoop.py def __getattr__ ( self , attr ): \"\"\"Forward the attribute to the value. Args: attr: A string, the name of the attribute. Returns: The value of the attribute. \"\"\" return getattr ( self . value , attr ) beancount . utils . snoop . Snoop . __init__ ( self , maxlen = None ) special \uf0c1 Create a new snooper. Parameters: maxlen \u2013 If specified, an integer, which enables the saving of that Source code in beancount/utils/snoop.py def __init__ ( self , maxlen = None ): \"\"\"Create a new snooper. Args: maxlen: If specified, an integer, which enables the saving of that number of last values in the history attribute. \"\"\" self . value = None self . history = collections . deque ( maxlen = maxlen ) if maxlen else None beancount . utils . snoop . snoopify ( function ) \uf0c1 Decorate a function as snoopable. This is meant to reassign existing functions to a snoopable version of them. For example, if you wanted 're.match' to be automatically snoopable, just decorate it like this: re.match = snoopify(re.match) and then you can just call 're.match' in a conditional and then access 're.match.value' to get to the last returned value. Source code in beancount/utils/snoop.py def snoopify ( function ): \"\"\"Decorate a function as snoopable. This is meant to reassign existing functions to a snoopable version of them. For example, if you wanted 're.match' to be automatically snoopable, just decorate it like this: re.match = snoopify(re.match) and then you can just call 're.match' in a conditional and then access 're.match.value' to get to the last returned value. \"\"\" @functools . wraps ( function ) def wrapper ( * args , ** kw ): value = function ( * args , ** kw ) wrapper . value = value return value wrapper . value = None return wrapper beancount.utils.table \uf0c1 Table rendering. beancount.utils.table.Table ( tuple ) \uf0c1 Table(columns, header, body) beancount . utils . table . Table . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/utils/table.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . utils . table . Table . __new__ ( _cls , columns , header , body ) special staticmethod \uf0c1 Create new instance of Table(columns, header, body) beancount . utils . table . Table . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/utils/table.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount . utils . table . attribute_to_title ( fieldname ) \uf0c1 Convert programming id into readable field name. Parameters: fieldname \u2013 A string, a programming ids, such as 'book_value'. Returns: A readable string, such as 'Book Value.' Source code in beancount/utils/table.py def attribute_to_title ( fieldname ): \"\"\"Convert programming id into readable field name. Args: fieldname: A string, a programming ids, such as 'book_value'. Returns: A readable string, such as 'Book Value.' \"\"\" return fieldname . replace ( \"_\" , \" \" ) . title () beancount . utils . table . compute_table_widths ( rows ) \uf0c1 Compute the max character widths of a list of rows. Parameters: rows \u2013 A list of rows, which are sequences of strings. Returns: A list of integers, the maximum widths required to render the columns of this table. Exceptions: IndexError \u2013 If the rows are of different lengths. Source code in beancount/utils/table.py def compute_table_widths ( rows ): \"\"\"Compute the max character widths of a list of rows. Args: rows: A list of rows, which are sequences of strings. Returns: A list of integers, the maximum widths required to render the columns of this table. Raises: IndexError: If the rows are of different lengths. \"\"\" row_iter = iter ( rows ) first_row = next ( row_iter ) num_columns = len ( first_row ) column_widths = [ len ( cell ) for cell in first_row ] for row in row_iter : for i , cell in enumerate ( row ): if not isinstance ( cell , str ): cell = str ( cell ) cell_len = len ( cell ) if cell_len > column_widths [ i ]: column_widths [ i ] = cell_len if i + 1 != num_columns : raise IndexError ( \"Invalid number of rows\" ) return column_widths beancount . utils . table . create_table ( rows , field_spec = None ) \uf0c1 Convert a list of tuples to an table report object. Parameters: rows \u2013 A list of tuples. field_spec \u2013 A list of strings, or a list of (FIELDNAME-OR-INDEX, HEADER, FORMATTER-FUNCTION) triplets, that selects a subset of the fields is to be rendered as well as their ordering. If this is a dict, the values are functions to call on the fields to render them. If a function is set to None, we will just call str() on the field. Returns: A Table instance. Source code in beancount/utils/table.py def create_table ( rows , field_spec = None ): \"\"\"Convert a list of tuples to an table report object. Args: rows: A list of tuples. field_spec: A list of strings, or a list of (FIELDNAME-OR-INDEX, HEADER, FORMATTER-FUNCTION) triplets, that selects a subset of the fields is to be rendered as well as their ordering. If this is a dict, the values are functions to call on the fields to render them. If a function is set to None, we will just call str() on the field. Returns: A Table instance. \"\"\" # Normalize field_spec to a dict. if field_spec is None : namedtuple_class = type ( rows [ 0 ]) field_spec = [( field , None , None ) for field in namedtuple_class . _fields ] elif isinstance ( field_spec , ( list , tuple )): new_field_spec = [] for field in field_spec : if isinstance ( field , tuple ): assert len ( field ) <= 3 , field if len ( field ) == 1 : field = field [ 0 ] new_field_spec . append (( field , None , None )) elif len ( field ) == 2 : field , header = field new_field_spec . append (( field , header , None )) elif len ( field ) == 3 : new_field_spec . append ( field ) else : if isinstance ( field , str ): title = attribute_to_title ( field ) elif isinstance ( field , int ): title = \"Field {} \" . format ( field ) else : raise ValueError ( \"Invalid type for column name\" ) new_field_spec . append (( field , title , None )) field_spec = new_field_spec # Ensure a nicely formatted header. field_spec = [ ( ( name , attribute_to_title ( name ), formatter ) if header_ is None else ( name , header_ , formatter ) ) for ( name , header_ , formatter ) in field_spec ] assert isinstance ( field_spec , list ), field_spec assert all ( len ( x ) == 3 for x in field_spec ), field_spec # Compute the column names. columns = [ name for ( name , _ , __ ) in field_spec ] # Compute the table header. header = [ header_column for ( _ , header_column , __ ) in field_spec ] # Compute the table body. body = [] for row in rows : body_row = [] for name , _ , formatter in field_spec : if isinstance ( name , str ): value = getattr ( row , name ) elif isinstance ( name , int ): value = row [ name ] else : raise ValueError ( \"Invalid type for column name\" ) if value is not None : if formatter is not None : value = formatter ( value ) else : value = str ( value ) else : value = \"\" body_row . append ( value ) body . append ( body_row ) return Table ( columns , header , body ) beancount . utils . table . render_table ( table_ , output , output_format , css_id = None , css_class = None ) \uf0c1 Render the given table to the output file object in the requested format. The table gets written out to the 'output' file. Parameters: table_ \u2013 An instance of Table. output \u2013 A file object you can write to. output_format \u2013 A string, the format to write the table to, either 'csv', 'txt' or 'html'. css_id \u2013 A string, an optional CSS id for the table object (only used for HTML). css_class \u2013 A string, an optional CSS class for the table object (only used for HTML). Source code in beancount/utils/table.py def render_table ( table_ , output , output_format , css_id = None , css_class = None ): \"\"\"Render the given table to the output file object in the requested format. The table gets written out to the 'output' file. Args: table_: An instance of Table. output: A file object you can write to. output_format: A string, the format to write the table to, either 'csv', 'txt' or 'html'. css_id: A string, an optional CSS id for the table object (only used for HTML). css_class: A string, an optional CSS class for the table object (only used for HTML). \"\"\" if output_format in ( \"txt\" , \"text\" ): text = table_to_text ( table_ , \" \" , formats = { \"*\" : \">\" , \"account\" : \"<\" }) output . write ( text ) elif output_format in ( \"csv\" ,): table_to_csv ( table_ , file = output ) elif output_format in ( \"htmldiv\" , \"html\" ): if output_format == \"html\" : output . write ( \" \\n \" ) output . write ( \" \\n \" ) output . write ( '
    \\n ' . format ( css_id ) if css_id else \"
    \\n \" ) classes = [ css_class ] if css_class else None table_to_html ( table_ , file = output , classes = classes ) output . write ( \"
    \\n \" ) if output_format == \"html\" : output . write ( \" \\n \" ) output . write ( \" \\n \" ) else : raise NotImplementedError ( \"Unsupported format: {} \" . format ( output_format )) beancount . utils . table . table_to_csv ( table , file = None , ** kwargs ) \uf0c1 Render a Table to a CSV file. Parameters: table \u2013 An instance of a Table. file \u2013 A file object to write to. If no object is provided, this function returns a string. **kwargs \u2013 Optional arguments forwarded to csv.writer(). Returns: A string, the rendered table, or None, if a file object is provided to write to. Source code in beancount/utils/table.py def table_to_csv ( table , file = None , ** kwargs ): \"\"\"Render a Table to a CSV file. Args: table: An instance of a Table. file: A file object to write to. If no object is provided, this function returns a string. **kwargs: Optional arguments forwarded to csv.writer(). Returns: A string, the rendered table, or None, if a file object is provided to write to. \"\"\" output_file = file or io . StringIO () writer = csv . writer ( output_file , ** kwargs ) if table . header : writer . writerow ( table . header ) writer . writerows ( table . body ) if not file : return output_file . getvalue () beancount . utils . table . table_to_html ( table , classes = None , file = None ) \uf0c1 Render a Table to HTML. Parameters: table \u2013 An instance of a Table. classes \u2013 A list of string, CSS classes to set on the table. file \u2013 A file object to write to. If no object is provided, this function returns a string. Returns: A string, the rendered table, or None, if a file object is provided to write to. Source code in beancount/utils/table.py def table_to_html ( table , classes = None , file = None ): \"\"\"Render a Table to HTML. Args: table: An instance of a Table. classes: A list of string, CSS classes to set on the table. file: A file object to write to. If no object is provided, this function returns a string. Returns: A string, the rendered table, or None, if a file object is provided to write to. \"\"\" # Initialize file. oss = io . StringIO () if file is None else file oss . write ( ' \\n ' . format ( \" \" . join ( classes or []))) # Render header. if table . header : oss . write ( \" \\n \" ) oss . write ( \" \\n \" ) for header in table . header : oss . write ( \" \\n \" . format ( header )) oss . write ( \" \\n \" ) oss . write ( \" \\n \" ) # Render body. oss . write ( \" \\n \" ) for row in table . body : oss . write ( \" \\n \" ) for cell in row : oss . write ( \" \\n \" . format ( cell )) oss . write ( \" \\n \" ) oss . write ( \" \\n \" ) # Render footer. oss . write ( \"
    {}
    {}
    \\n \" ) if file is None : return oss . getvalue () beancount . utils . table . table_to_text ( table , column_interspace = ' ' , formats = None ) \uf0c1 Render a Table to ASCII text. Parameters: table \u2013 An instance of a Table. column_interspace \u2013 A string to render between the columns as spacer. formats \u2013 An optional dict of column name to a format character that gets inserted in a format string specified, like this (where '' is): {:}. A key of ' ' will provide a default value, like this, for example: (... formats={' ': '>'}). Returns: A string, the rendered text table. Source code in beancount/utils/table.py def table_to_text ( table , column_interspace = \" \" , formats = None ): \"\"\"Render a Table to ASCII text. Args: table: An instance of a Table. column_interspace: A string to render between the columns as spacer. formats: An optional dict of column name to a format character that gets inserted in a format string specified, like this (where '' is): {:}. A key of '*' will provide a default value, like this, for example: (... formats={'*': '>'}). Returns: A string, the rendered text table. \"\"\" column_widths = compute_table_widths ( itertools . chain ([ table . header ], table . body )) # Insert column format chars and compute line formatting string. column_formats = [] if formats : default_format = formats . get ( \"*\" , None ) for column , width in zip ( table . columns , column_widths ): if column and formats : format_ = formats . get ( column , default_format ) if format_ : column_formats . append ( \"{{: {}{:d} }}\" . format ( format_ , width )) else : column_formats . append ( \"{{: {:d} }}\" . format ( width )) else : column_formats . append ( \"{{: {:d} }}\" . format ( width )) line_format = column_interspace . join ( column_formats ) + \" \\n \" separator = line_format . format ( * [( \"-\" * width ) for width in column_widths ]) # Render the header. oss = io . StringIO () if table . header : oss . write ( line_format . format ( * table . header )) # Render the body. oss . write ( separator ) for row in table . body : oss . write ( line_format . format ( * row )) oss . write ( separator ) return oss . getvalue () beancount.utils.test_utils \uf0c1 Support utilities for testing scripts. beancount.utils.test_utils.ClickTestCase ( TestCase ) \uf0c1 Base class for command-line program test cases. beancount.utils.test_utils.RCall ( tuple ) \uf0c1 RCall(args, kwargs, return_value) beancount . utils . test_utils . RCall . __getnewargs__ ( self ) special \uf0c1 Return self as a plain tuple. Used by copy and pickle. Source code in beancount/utils/test_utils.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self ) beancount . utils . test_utils . RCall . __new__ ( _cls , args , kwargs , return_value ) special staticmethod \uf0c1 Create new instance of RCall(args, kwargs, return_value) beancount . utils . test_utils . RCall . __repr__ ( self ) special \uf0c1 Return a nicely formatted representation string Source code in beancount/utils/test_utils.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self beancount.utils.test_utils.TestCase ( TestCase ) \uf0c1 beancount . utils . test_utils . TestCase . assertLines ( self , text1 , text2 , message = None ) \uf0c1 Compare the lines of text1 and text2, ignoring whitespace. Parameters: text1 \u2013 A string, the expected text. text2 \u2013 A string, the actual text. message \u2013 An optional string message in case the assertion fails. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/utils/test_utils.py def assertLines ( self , text1 , text2 , message = None ): \"\"\"Compare the lines of text1 and text2, ignoring whitespace. Args: text1: A string, the expected text. text2: A string, the actual text. message: An optional string message in case the assertion fails. Raises: AssertionError: If the exception fails. \"\"\" clean_text1 = textwrap . dedent ( text1 . strip ()) clean_text2 = textwrap . dedent ( text2 . strip ()) lines1 = [ line . strip () for line in clean_text1 . splitlines ()] lines2 = [ line . strip () for line in clean_text2 . splitlines ()] # Compress all space longer than 4 spaces to exactly 4. # This affords us to be even looser. lines1 = [ re . sub ( \" [ \\t ]*\" , \" \" , line ) for line in lines1 ] lines2 = [ re . sub ( \" [ \\t ]*\" , \" \" , line ) for line in lines2 ] self . assertEqual ( lines1 , lines2 , message ) beancount . utils . test_utils . TestCase . assertOutput ( self , expected_text ) \uf0c1 Expect text printed to stdout. Parameters: expected_text \u2013 A string, the text that should have been printed to stdout. Exceptions: AssertionError \u2013 If the text differs. Source code in beancount/utils/test_utils.py @contextlib . contextmanager def assertOutput ( self , expected_text ): \"\"\"Expect text printed to stdout. Args: expected_text: A string, the text that should have been printed to stdout. Raises: AssertionError: If the text differs. \"\"\" with capture () as oss : yield oss self . assertLines ( textwrap . dedent ( expected_text ), oss . getvalue ()) beancount.utils.test_utils.TmpFilesTestBase ( TestCase ) \uf0c1 A test utility base class that creates and cleans up a directory hierarchy. This convenience is useful for testing functions that work on files, such as the documents tests, or the accounts walk. beancount . utils . test_utils . TmpFilesTestBase . create_file_hierarchy ( test_files , subdir = 'root' ) staticmethod \uf0c1 A test utility that creates a hierarchy of files. Parameters: test_files \u2013 A list of strings, relative filenames to a temporary root directory. If the filename ends with a '/', we create a directory; otherwise, we create a regular file. subdir \u2013 A string, the subdirectory name under the temporary directory location, to create the hierarchy under. Returns: A pair of strings, the temporary directory, and the subdirectory under that which hosts the root of the tree. Source code in beancount/utils/test_utils.py @staticmethod def create_file_hierarchy ( test_files , subdir = \"root\" ): \"\"\"A test utility that creates a hierarchy of files. Args: test_files: A list of strings, relative filenames to a temporary root directory. If the filename ends with a '/', we create a directory; otherwise, we create a regular file. subdir: A string, the subdirectory name under the temporary directory location, to create the hierarchy under. Returns: A pair of strings, the temporary directory, and the subdirectory under that which hosts the root of the tree. \"\"\" tempdir = tempfile . mkdtemp ( prefix = \"beancount-test-tmpdir.\" ) root = path . join ( tempdir , subdir ) for filename in test_files : abs_filename = path . join ( tempdir , filename ) if filename . endswith ( \"/\" ): os . makedirs ( abs_filename ) else : parent_dir = path . dirname ( abs_filename ) if not path . exists ( parent_dir ): os . makedirs ( parent_dir ) with open ( abs_filename , \"w\" ): pass return tempdir , root beancount . utils . test_utils . TmpFilesTestBase . setUp ( self ) \uf0c1 Hook method for setting up the test fixture before exercising it. Source code in beancount/utils/test_utils.py def setUp ( self ): self . tempdir , self . root = self . create_file_hierarchy ( self . TEST_DOCUMENTS ) beancount . utils . test_utils . TmpFilesTestBase . tearDown ( self ) \uf0c1 Hook method for deconstructing the test fixture after testing it. Source code in beancount/utils/test_utils.py def tearDown ( self ): shutil . rmtree ( self . tempdir , ignore_errors = True ) beancount . utils . test_utils . capture ( * attributes ) \uf0c1 A context manager that captures what's printed to stdout. Parameters: *attributes \u2013 A tuple of strings, the name of the sys attributes to override with StringIO instances. Yields: A StringIO string accumulator. Source code in beancount/utils/test_utils.py def capture ( * attributes ): \"\"\"A context manager that captures what's printed to stdout. Args: *attributes: A tuple of strings, the name of the sys attributes to override with StringIO instances. Yields: A StringIO string accumulator. \"\"\" if not attributes : attributes = \"stdout\" elif len ( attributes ) == 1 : attributes = attributes [ 0 ] return patch ( sys , attributes , io . StringIO ) beancount . utils . test_utils . create_temporary_files ( root , contents_map ) \uf0c1 Create a number of temporary files under 'root'. This routine is used to initialize the contents of multiple files under a temporary directory. Parameters: root \u2013 A string, the name of the directory under which to create the files. contents_map \u2013 A dict of relative filenames to their contents. The content strings will be automatically dedented for convenience. In addition, the string 'ROOT' in the contents will be automatically replaced by the root directory name. Source code in beancount/utils/test_utils.py def create_temporary_files ( root , contents_map ): \"\"\"Create a number of temporary files under 'root'. This routine is used to initialize the contents of multiple files under a temporary directory. Args: root: A string, the name of the directory under which to create the files. contents_map: A dict of relative filenames to their contents. The content strings will be automatically dedented for convenience. In addition, the string 'ROOT' in the contents will be automatically replaced by the root directory name. \"\"\" os . makedirs ( root , exist_ok = True ) for relative_filename , contents in contents_map . items (): assert not path . isabs ( relative_filename ) filename = path . join ( root , relative_filename ) os . makedirs ( path . dirname ( filename ), exist_ok = True ) clean_contents = textwrap . dedent ( contents . replace ( \" {root} \" , root )) with open ( filename , \"w\" ) as f : f . write ( clean_contents ) beancount . utils . test_utils . docfile ( function , ** kwargs ) \uf0c1 A decorator that write the function's docstring to a temporary file and calls the decorated function with the temporary filename. This is useful for writing tests. Parameters: function \u2013 A function to decorate. Returns: The decorated function. Source code in beancount/utils/test_utils.py def docfile ( function , ** kwargs ): \"\"\"A decorator that write the function's docstring to a temporary file and calls the decorated function with the temporary filename. This is useful for writing tests. Args: function: A function to decorate. Returns: The decorated function. \"\"\" contents = kwargs . pop ( \"contents\" , None ) @functools . wraps ( function ) def new_function ( self ): allowed = ( \"buffering\" , \"encoding\" , \"newline\" , \"dir\" , \"prefix\" , \"suffix\" ) if any ( key not in allowed for key in kwargs ): raise ValueError ( \"Invalid kwarg to docfile_extra\" ) with tempfile . NamedTemporaryFile ( \"w\" , ** kwargs ) as file : text = contents or function . __doc__ file . write ( textwrap . dedent ( text )) file . flush () return function ( self , file . name ) new_function . __doc__ = None return new_function beancount . utils . test_utils . docfile_extra ( ** kwargs ) \uf0c1 A decorator identical to @docfile, but it also takes kwargs for the temporary file, Kwargs: e.g. buffering, encoding, newline, dir, prefix, and suffix. Returns: docfile Source code in beancount/utils/test_utils.py def docfile_extra ( ** kwargs ): \"\"\" A decorator identical to @docfile, but it also takes kwargs for the temporary file, Kwargs: e.g. buffering, encoding, newline, dir, prefix, and suffix. Returns: docfile \"\"\" return functools . partial ( docfile , ** kwargs ) beancount . utils . test_utils . environ ( varname , newvalue ) \uf0c1 A context manager which pushes varname's value and restores it later. Parameters: varname \u2013 A string, the environ variable name. newvalue \u2013 A string, the desired value. Source code in beancount/utils/test_utils.py @contextlib . contextmanager def environ ( varname , newvalue ): \"\"\"A context manager which pushes varname's value and restores it later. Args: varname: A string, the environ variable name. newvalue: A string, the desired value. \"\"\" oldvalue = os . environ . get ( varname , None ) os . environ [ varname ] = newvalue yield if oldvalue is not None : os . environ [ varname ] = oldvalue else : del os . environ [ varname ] beancount . utils . test_utils . find_python_lib () \uf0c1 Return the path to the root of the Python libraries. Returns: A string, the root directory. Source code in beancount/utils/test_utils.py def find_python_lib (): \"\"\"Return the path to the root of the Python libraries. Returns: A string, the root directory. \"\"\" return path . dirname ( path . dirname ( path . dirname ( __file__ ))) beancount . utils . test_utils . find_repository_root ( filename = None ) \uf0c1 Return the path to the repository root. Parameters: filename \u2013 A string, the name of a file within the repository. Returns: A string, the root directory. Source code in beancount/utils/test_utils.py def find_repository_root ( filename = None ): \"\"\"Return the path to the repository root. Args: filename: A string, the name of a file within the repository. Returns: A string, the root directory. \"\"\" if filename is None : filename = __file__ # Support root directory under Bazel. match = re . match ( r \"(.*\\.runfiles/beancount)/\" , filename ) if match : return match . group ( 1 ) while not path . exists ( path . join ( filename , \"pyproject.toml\" )): prev_filename = filename filename = path . dirname ( filename ) if prev_filename == filename : raise ValueError ( \"Failed to find the root directory.\" ) return filename beancount . utils . test_utils . make_failing_importer ( * removed_module_names ) \uf0c1 Make an importer that raise an ImportError for some modules. Use it like this: @mock.patch('builtins. import ', make_failing_importer('setuptools')) def test_... Parameters: removed_module_name \u2013 The name of the module import that should raise an exception. Returns: A decorated test decorator. Source code in beancount/utils/test_utils.py def make_failing_importer ( * removed_module_names ): \"\"\"Make an importer that raise an ImportError for some modules. Use it like this: @mock.patch('builtins.__import__', make_failing_importer('setuptools')) def test_... Args: removed_module_name: The name of the module import that should raise an exception. Returns: A decorated test decorator. \"\"\" def failing_import ( name , * args , ** kwargs ): if name in removed_module_names : raise ImportError ( \"Could not import {} \" . format ( name )) return builtins . __import__ ( name , * args , ** kwargs ) return failing_import beancount . utils . test_utils . nottest ( func ) \uf0c1 Make the given function not testable. Source code in beancount/utils/test_utils.py def nottest ( func ): \"Make the given function not testable.\" func . __test__ = False return func beancount . utils . test_utils . patch ( obj , attributes , replacement_type ) \uf0c1 A context manager that temporarily patches an object's attributes. All attributes in 'attributes' are saved and replaced by new instances of type 'replacement_type'. Parameters: obj \u2013 The object to patch up. attributes \u2013 A string or a sequence of strings, the names of attributes to replace. replacement_type \u2013 A callable to build replacement objects. Yields: An instance of a list of sequences of 'replacement_type'. Source code in beancount/utils/test_utils.py @contextlib . contextmanager def patch ( obj , attributes , replacement_type ): \"\"\"A context manager that temporarily patches an object's attributes. All attributes in 'attributes' are saved and replaced by new instances of type 'replacement_type'. Args: obj: The object to patch up. attributes: A string or a sequence of strings, the names of attributes to replace. replacement_type: A callable to build replacement objects. Yields: An instance of a list of sequences of 'replacement_type'. \"\"\" single = isinstance ( attributes , str ) if single : attributes = [ attributes ] saved = [] replacements = [] for attribute in attributes : replacement = replacement_type () replacements . append ( replacement ) saved . append ( getattr ( obj , attribute )) setattr ( obj , attribute , replacement ) yield replacements [ 0 ] if single else replacements for attribute , saved_attr in zip ( attributes , saved ): setattr ( obj , attribute , saved_attr ) beancount . utils . test_utils . record ( fun ) \uf0c1 Decorates the function to intercept and record all calls and return values. Parameters: fun \u2013 A callable to be decorated. Returns: A wrapper function with a .calls attribute, a list of RCall instances. Source code in beancount/utils/test_utils.py def record ( fun ): \"\"\"Decorates the function to intercept and record all calls and return values. Args: fun: A callable to be decorated. Returns: A wrapper function with a .calls attribute, a list of RCall instances. \"\"\" @functools . wraps ( fun ) def wrapped ( * args , ** kw ): return_value = fun ( * args , ** kw ) wrapped . calls . append ( RCall ( args , kw , return_value )) return return_value wrapped . calls = [] return wrapped beancount . utils . test_utils . search_words ( words , line ) \uf0c1 Search for a sequence of words in a line. Parameters: words \u2013 A list of strings, the words to look for, or a space-separated string. line \u2013 A string, the line to search into. Returns: A MatchObject, or None. Source code in beancount/utils/test_utils.py def search_words ( words , line ): \"\"\"Search for a sequence of words in a line. Args: words: A list of strings, the words to look for, or a space-separated string. line: A string, the line to search into. Returns: A MatchObject, or None. \"\"\" if isinstance ( words , str ): words = words . split () return re . search ( \".*\" . join ( r \"\\b {} \\b\" . format ( word ) for word in words ), line ) beancount . utils . test_utils . skipIfRaises ( * exc_types ) \uf0c1 A context manager (or decorator) that skips a test if an exception is raised. Yields: Nothing, for you to execute the function code. Exceptions: SkipTest \u2013 if the test raised the expected exception. Source code in beancount/utils/test_utils.py @contextlib . contextmanager def skipIfRaises ( * exc_types ): \"\"\"A context manager (or decorator) that skips a test if an exception is raised. Args: exc_type Yields: Nothing, for you to execute the function code. Raises: SkipTest: if the test raised the expected exception. \"\"\" try : yield except exc_types as exception : raise unittest . SkipTest ( exception ) beancount . utils . test_utils . subprocess_env () \uf0c1 Return a dict to use as environment for running subprocesses. Returns: A string, the root directory. Source code in beancount/utils/test_utils.py def subprocess_env (): \"\"\"Return a dict to use as environment for running subprocesses. Returns: A string, the root directory. \"\"\" # Ensure we have locations to invoke our Python executable and our # runnable binaries in the test environment to run subprocesses. binpath = \":\" . join ( [ path . dirname ( sys . executable ), path . join ( find_repository_root ( __file__ ), \"bin\" ), os . environ . get ( \"PATH\" , \"\" ) . strip ( \":\" ), ] ) . strip ( \":\" ) return { \"PATH\" : binpath , \"PYTHONPATH\" : find_python_lib ()} beancount . utils . test_utils . tempdir ( delete = True , ** kw ) \uf0c1 A context manager that creates a temporary directory and deletes its contents unconditionally once done. Parameters: delete \u2013 A boolean, true if we want to delete the directory after running. **kw \u2013 Keyword arguments for mkdtemp. Yields: A string, the name of the temporary directory created. Source code in beancount/utils/test_utils.py @contextlib . contextmanager def tempdir ( delete = True , ** kw ): \"\"\"A context manager that creates a temporary directory and deletes its contents unconditionally once done. Args: delete: A boolean, true if we want to delete the directory after running. **kw: Keyword arguments for mkdtemp. Yields: A string, the name of the temporary directory created. \"\"\" tempdir = tempfile . mkdtemp ( prefix = \"beancount-test-tmpdir.\" , ** kw ) try : yield tempdir finally : if delete : shutil . rmtree ( tempdir , ignore_errors = True )","title":"beancount.utils"},{"location":"api_reference/beancount.utils.html#beancountutils","text":"Generic utility packages and functions.","title":"beancount.utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.bisect_key","text":"A version of bisect that accepts a custom key function, like the sorting ones do.","title":"bisect_key"},{"location":"api_reference/beancount.utils.html#beancount.utils.bisect_key.bisect_left_with_key","text":"Find the last element before the given value in a sorted list. Parameters: sequence \u2013 A sorted sequence of elements. value \u2013 The value to search for. key \u2013 An optional function used to extract the value from the elements of sequence. Returns: Return the index. May return None. Source code in beancount/utils/bisect_key.py def bisect_left_with_key ( sequence , value , key = None ): \"\"\"Find the last element before the given value in a sorted list. Args: sequence: A sorted sequence of elements. value: The value to search for. key: An optional function used to extract the value from the elements of sequence. Returns: Return the index. May return None. \"\"\" if key is None : key = lambda x : x # Identity. lo = 0 hi = len ( sequence ) while lo < hi : mid = ( lo + hi ) // 2 if key ( sequence [ mid ]) < value : lo = mid + 1 else : hi = mid return lo","title":"bisect_left_with_key()"},{"location":"api_reference/beancount.utils.html#beancount.utils.bisect_key.bisect_right_with_key","text":"Like bisect.bisect_right, but with a key lookup parameter. Parameters: a \u2013 The list to search in. x \u2013 The element to search for. key \u2013 A function, to extract the value from the list. lo \u2013 The smallest index to search. hi \u2013 The largest index to search. Returns: As in bisect.bisect_right, an element from list 'a'. Source code in beancount/utils/bisect_key.py def bisect_right_with_key ( a , x , key , lo = 0 , hi = None ): \"\"\"Like bisect.bisect_right, but with a key lookup parameter. Args: a: The list to search in. x: The element to search for. key: A function, to extract the value from the list. lo: The smallest index to search. hi: The largest index to search. Returns: As in bisect.bisect_right, an element from list 'a'. \"\"\" if lo < 0 : raise ValueError ( \"lo must be non-negative\" ) if hi is None : hi = len ( a ) while lo < hi : mid = ( lo + hi ) // 2 if x < key ( a [ mid ]): hi = mid else : lo = mid + 1 return lo","title":"bisect_right_with_key()"},{"location":"api_reference/beancount.utils.html#beancount.utils.date_utils","text":"Parse the date from various formats.","title":"date_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.date_utils.intimezone","text":"Temporarily reset the value of TZ. This is used for testing. Parameters: tz_value ( str ) \u2013 The value of TZ to set for the duration of this context. Returns: A contextmanager in the given timezone locale. Source code in beancount/utils/date_utils.py @contextlib . contextmanager def intimezone ( tz_value : str ): \"\"\"Temporarily reset the value of TZ. This is used for testing. Args: tz_value: The value of TZ to set for the duration of this context. Returns: A contextmanager in the given timezone locale. \"\"\" tz_old = os . environ . get ( \"TZ\" , None ) os . environ [ \"TZ\" ] = tz_value time . tzset () try : yield finally : if tz_old is None : del os . environ [ \"TZ\" ] else : os . environ [ \"TZ\" ] = tz_old time . tzset ()","title":"intimezone()"},{"location":"api_reference/beancount.utils.html#beancount.utils.date_utils.iter_dates","text":"Yield all the dates between 'start_date' and 'end_date'. Parameters: start_date \u2013 An instance of datetime.date. end_date \u2013 An instance of datetime.date. Yields: Instances of datetime.date. Source code in beancount/utils/date_utils.py def iter_dates ( start_date , end_date ): \"\"\"Yield all the dates between 'start_date' and 'end_date'. Args: start_date: An instance of datetime.date. end_date: An instance of datetime.date. Yields: Instances of datetime.date. \"\"\" oneday = datetime . timedelta ( days = 1 ) date = start_date while date < end_date : yield date date += oneday","title":"iter_dates()"},{"location":"api_reference/beancount.utils.html#beancount.utils.date_utils.next_month","text":"Compute the date at the beginning of the following month from the given date. Parameters: date \u2013 A datetime.date instance. Returns: A datetime.date instance, the first day of the month following 'date'. Source code in beancount/utils/date_utils.py def next_month ( date ): \"\"\"Compute the date at the beginning of the following month from the given date. Args: date: A datetime.date instance. Returns: A datetime.date instance, the first day of the month following 'date'. \"\"\" # Compute the date at the beginning of the following month. year = date . year month = date . month + 1 if date . month == 12 : year += 1 month = 1 return datetime . date ( year , month , 1 )","title":"next_month()"},{"location":"api_reference/beancount.utils.html#beancount.utils.date_utils.render_ofx_date","text":"Render a datetime to the OFX format. Parameters: dtime \u2013 A datetime.datetime instance. Returns: A string, rendered to milliseconds. Source code in beancount/utils/date_utils.py def render_ofx_date ( dtime ): \"\"\"Render a datetime to the OFX format. Args: dtime: A datetime.datetime instance. Returns: A string, rendered to milliseconds. \"\"\" return \" {} . {:03d} \" . format ( dtime . strftime ( \"%Y%m %d %H%M%S\" ), int ( dtime . microsecond / 1000 ))","title":"render_ofx_date()"},{"location":"api_reference/beancount.utils.html#beancount.utils.defdict","text":"An instance of collections.defaultdict whose factory accepts a key. Note: This really ought to be an enhancement to Python itself. I should bother adding this in eventually.","title":"defdict"},{"location":"api_reference/beancount.utils.html#beancount.utils.defdict.DefaultDictWithKey","text":"A version of defaultdict whose factory accepts the key as an argument. Note: collections.defaultdict would be improved by supporting this directly, this is a common occurrence.","title":"DefaultDictWithKey"},{"location":"api_reference/beancount.utils.html#beancount.utils.defdict.ImmutableDictWithDefault","text":"An immutable dict which returns a default value for missing keys. This differs from a defaultdict in that it does not insert a missing default value when one is materialized (from a missing fetch), and furthermore, the set method is make unavailable to prevent mutation beyond construction.","title":"ImmutableDictWithDefault"},{"location":"api_reference/beancount.utils.html#beancount.utils.defdict.ImmutableDictWithDefault.__setitem__","text":"Disallow mutating the dict in the usual way. Source code in beancount/utils/defdict.py def __setitem__ ( self , key , value ): \"\"\"Disallow mutating the dict in the usual way.\"\"\" raise NotImplementedError","title":"__setitem__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.defdict.ImmutableDictWithDefault.get","text":"Return the value for key if key is in the dictionary, else default. Source code in beancount/utils/defdict.py def get ( self , key , _ = None ): return self . __getitem__ ( key )","title":"get()"},{"location":"api_reference/beancount.utils.html#beancount.utils.encryption","text":"Support for encrypted tests.","title":"encryption"},{"location":"api_reference/beancount.utils.html#beancount.utils.encryption.is_encrypted_file","text":"Return true if the given filename contains an encrypted file. Parameters: filename \u2013 A path string. Returns: A boolean, true if the file contains an encrypted file. Source code in beancount/utils/encryption.py def is_encrypted_file ( filename ): \"\"\"Return true if the given filename contains an encrypted file. Args: filename: A path string. Returns: A boolean, true if the file contains an encrypted file. \"\"\" _ , ext = path . splitext ( filename ) if ext == \".gpg\" : return True if ext == \".asc\" : with open ( filename ) as encfile : head = encfile . read ( 1024 ) if re . search ( \"--BEGIN PGP MESSAGE--\" , head ): return True return False","title":"is_encrypted_file()"},{"location":"api_reference/beancount.utils.html#beancount.utils.encryption.is_gpg_installed","text":"Return true if GPG 1.4.x or 2.x are installed, which is what we use and support. Source code in beancount/utils/encryption.py def is_gpg_installed (): \"\"\"Return true if GPG 1.4.x or 2.x are installed, which is what we use and support.\"\"\" try : pipe = subprocess . Popen ( [ \"gpg\" , \"--version\" ], shell = 0 , stdout = subprocess . PIPE , stderr = subprocess . PIPE ) out , err = pipe . communicate () version_text = out . decode ( \"utf8\" ) return pipe . returncode == 0 and re . match ( r \"gpg \\(GnuPG\\) (1\\.4|2)\\.\" , version_text ) except OSError : return False","title":"is_gpg_installed()"},{"location":"api_reference/beancount.utils.html#beancount.utils.encryption.read_encrypted_file","text":"Decrypt and read an encrypted file without temporary storage. Parameters: filename \u2013 A string, the path to the encrypted file. Returns: A string, the contents of the file. Exceptions: OSError \u2013 If we could not properly decrypt the file. Source code in beancount/utils/encryption.py def read_encrypted_file ( filename ): \"\"\"Decrypt and read an encrypted file without temporary storage. Args: filename: A string, the path to the encrypted file. Returns: A string, the contents of the file. Raises: OSError: If we could not properly decrypt the file. \"\"\" command = [ \"gpg\" , \"--batch\" , \"--decrypt\" , path . realpath ( filename )] pipe = subprocess . Popen ( command , shell = False , stdout = subprocess . PIPE , stderr = subprocess . PIPE ) contents , errors = pipe . communicate () if pipe . returncode != 0 : raise OSError ( \"Could not decrypt file ( {} ): {} \" . format ( pipe . returncode , errors . decode ( \"utf8\" )) ) return contents . decode ( \"utf-8\" )","title":"read_encrypted_file()"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_utils","text":"File utilities.","title":"file_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_utils.chdir","text":"Temporarily chdir to the given directory. Parameters: directory \u2013 The directory to switch do. Returns: A context manager which restores the cwd after running. Source code in beancount/utils/file_utils.py @contextlib . contextmanager def chdir ( directory ): \"\"\"Temporarily chdir to the given directory. Args: directory: The directory to switch do. Returns: A context manager which restores the cwd after running. \"\"\" cwd = os . getcwd () os . chdir ( directory ) try : yield cwd finally : os . chdir ( cwd )","title":"chdir()"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_utils.find_files","text":"Enumerate the files under the given directories, stably. Invalid file or directory names will be logged to the error log. Parameters: fords \u2013 A list of strings, file or directory names. ignore_dirs \u2013 A list of strings, filenames or directories to be ignored. Yields: Strings, full filenames from the given roots. Source code in beancount/utils/file_utils.py def find_files ( fords , ignore_dirs = ( \".hg\" , \".svn\" , \".git\" ), ignore_files = ( \".DS_Store\" ,)): \"\"\"Enumerate the files under the given directories, stably. Invalid file or directory names will be logged to the error log. Args: fords: A list of strings, file or directory names. ignore_dirs: A list of strings, filenames or directories to be ignored. Yields: Strings, full filenames from the given roots. \"\"\" if isinstance ( fords , str ): fords = [ fords ] assert isinstance ( fords , ( list , tuple )) for ford in fords : if path . isdir ( ford ): for root , dirs , filenames in os . walk ( ford ): dirs [:] = sorted ( dirname for dirname in dirs if dirname not in ignore_dirs ) for filename in sorted ( filenames ): if filename in ignore_files : continue yield path . join ( root , filename ) elif path . isfile ( ford ) or path . islink ( ford ): yield ford elif not path . exists ( ford ): logging . error ( \"File or directory ' {} ' does not exist.\" . format ( ford ))","title":"find_files()"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_utils.guess_file_format","text":"Guess the file format from the filename. Parameters: filename \u2013 A string, the name of the file. This can be None. Returns: A string, the extension of the format, without a leading period. Source code in beancount/utils/file_utils.py def guess_file_format ( filename , default = None ): \"\"\"Guess the file format from the filename. Args: filename: A string, the name of the file. This can be None. Returns: A string, the extension of the format, without a leading period. \"\"\" if filename : if filename . endswith ( \".txt\" ) or filename . endswith ( \".text\" ): format = \"text\" elif filename . endswith ( \".csv\" ): format = \"csv\" elif filename . endswith ( \".html\" ) or filename . endswith ( \".xhtml\" ): format = \"html\" else : format = default else : format = default return format","title":"guess_file_format()"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_utils.path_greedy_split","text":"Split a path, returning the longest possible extension. Parameters: filename \u2013 A string, the filename to split. Returns: A pair of basename, extension (which includes the leading period). Source code in beancount/utils/file_utils.py def path_greedy_split ( filename ): \"\"\"Split a path, returning the longest possible extension. Args: filename: A string, the filename to split. Returns: A pair of basename, extension (which includes the leading period). \"\"\" basename = path . basename ( filename ) index = basename . find ( \".\" ) if index == - 1 : extension = None else : extension = basename [ index :] basename = basename [: index ] return ( path . join ( path . dirname ( filename ), basename ), extension )","title":"path_greedy_split()"},{"location":"api_reference/beancount.utils.html#beancount.utils.file_utils.touch_file","text":"Touch a file and wait until its timestamp has been changed. Parameters: filename \u2013 A string path, the name of the file to touch. otherfiles \u2013 A list of other files to ensure the timestamp is beyond of. Source code in beancount/utils/file_utils.py def touch_file ( filename , * otherfiles ): \"\"\"Touch a file and wait until its timestamp has been changed. Args: filename: A string path, the name of the file to touch. otherfiles: A list of other files to ensure the timestamp is beyond of. \"\"\" # Note: You could set os.stat_float_times() but then the main function would # have to set that up as well. It doesn't help so much, however, since # filesystems tend to have low resolutions, e.g. one second. orig_mtime_ns = max ( os . stat ( minfile ) . st_mtime_ns for minfile in ( filename ,) + otherfiles ) delay_secs = 0.05 while True : with open ( filename , \"a\" ): os . utime ( filename ) time . sleep ( delay_secs ) new_stat = os . stat ( filename ) if new_stat . st_mtime_ns > orig_mtime_ns : break","title":"touch_file()"},{"location":"api_reference/beancount.utils.html#beancount.utils.import_utils","text":"Utilities for importing symbols programmatically.","title":"import_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.import_utils.import_symbol","text":"Import a symbol in an arbitrary module. Parameters: dotted_name \u2013 A dotted path to a symbol. Returns: The object referenced by the given name. Exceptions: ImportError \u2013 If the module not not be imported. AttributeError \u2013 If the symbol could not be found in the module. Source code in beancount/utils/import_utils.py def import_symbol ( dotted_name ): \"\"\"Import a symbol in an arbitrary module. Args: dotted_name: A dotted path to a symbol. Returns: The object referenced by the given name. Raises: ImportError: If the module not not be imported. AttributeError: If the symbol could not be found in the module. \"\"\" comps = dotted_name . split ( \".\" ) module_name = \".\" . join ( comps [: - 1 ]) symbol_name = comps [ - 1 ] module = importlib . import_module ( module_name ) return getattr ( module , symbol_name )","title":"import_symbol()"},{"location":"api_reference/beancount.utils.html#beancount.utils.invariants","text":"Functions to register auxiliary functions on a class' methods to check for invariants. This is intended to be used in a test, whereby your test will setup a class to automatically run invariant verification functions before and after each function call, to ensure some extra sanity checks that wouldn't be used in non-tests. Example: Instrument the Inventory class with the check_inventory_invariants() function. def setUp(module): instrument_invariants(Inventory, check_inventory_invariants, check_inventory_invariants) def tearDown(module): uninstrument_invariants(Inventory)","title":"invariants"},{"location":"api_reference/beancount.utils.html#beancount.utils.invariants.instrument_invariants","text":"Instrument the class 'klass' with pre/post invariant checker functions. Parameters: klass \u2013 A class object, whose methods to be instrumented. prefun \u2013 A function that checks invariants pre-call. postfun \u2013 A function that checks invariants pre-call. Source code in beancount/utils/invariants.py def instrument_invariants ( klass , prefun , postfun ): \"\"\"Instrument the class 'klass' with pre/post invariant checker functions. Args: klass: A class object, whose methods to be instrumented. prefun: A function that checks invariants pre-call. postfun: A function that checks invariants pre-call. \"\"\" instrumented = {} for attrname , object_ in klass . __dict__ . items (): if attrname . startswith ( \"_\" ): continue if not isinstance ( object_ , types . FunctionType ): continue instrumented [ attrname ] = object_ setattr ( klass , attrname , invariant_check ( object_ , prefun , postfun )) klass . __instrumented = instrumented","title":"instrument_invariants()"},{"location":"api_reference/beancount.utils.html#beancount.utils.invariants.invariant_check","text":"Decorate a method with the pre/post invariant checkers. Parameters: method \u2013 An unbound method to instrument. prefun \u2013 A function that checks invariants pre-call. postfun \u2013 A function that checks invariants post-call. Returns: An unbound method, decorated. Source code in beancount/utils/invariants.py def invariant_check ( method , prefun , postfun ): \"\"\"Decorate a method with the pre/post invariant checkers. Args: method: An unbound method to instrument. prefun: A function that checks invariants pre-call. postfun: A function that checks invariants post-call. Returns: An unbound method, decorated. \"\"\" reentrant = [] def new_method ( self , * args , ** kw ): reentrant . append ( None ) if len ( reentrant ) == 1 : prefun ( self ) result = method ( self , * args , ** kw ) if len ( reentrant ) == 1 : postfun ( self ) reentrant . pop () return result return new_method","title":"invariant_check()"},{"location":"api_reference/beancount.utils.html#beancount.utils.invariants.uninstrument_invariants","text":"Undo the instrumentation for invariants. Parameters: klass \u2013 A class object, whose methods to be uninstrumented. Source code in beancount/utils/invariants.py def uninstrument_invariants ( klass ): \"\"\"Undo the instrumentation for invariants. Args: klass: A class object, whose methods to be uninstrumented. \"\"\" instrumented = getattr ( klass , \"__instrumented\" , None ) if instrumented : for attrname , object_ in instrumented . items (): setattr ( klass , attrname , object_ ) del klass . __instrumented","title":"uninstrument_invariants()"},{"location":"api_reference/beancount.utils.html#beancount.utils.memo","text":"Memoization utilities.","title":"memo"},{"location":"api_reference/beancount.utils.html#beancount.utils.memo.memoize_recent_fileobj","text":"Memoize recent calls to the given function which returns a file object. The results of the cache expire after some time. Parameters: function \u2013 A callable object. cache_filename \u2013 A string, the path to the database file to cache to. expiration \u2013 The time during which the results will be kept valid. Use 'None' to never expire the cache (this is the default). Returns: A memoized version of the function. Source code in beancount/utils/memo.py def memoize_recent_fileobj ( function , cache_filename , expiration = None ): \"\"\"Memoize recent calls to the given function which returns a file object. The results of the cache expire after some time. Args: function: A callable object. cache_filename: A string, the path to the database file to cache to. expiration: The time during which the results will be kept valid. Use 'None' to never expire the cache (this is the default). Returns: A memoized version of the function. \"\"\" urlcache = shelve . open ( cache_filename , \"c\" ) urlcache . lock = threading . Lock () # Note: 'shelve' is not thread-safe. @functools . wraps ( function ) def memoized ( * args , ** kw ): # Encode the arguments, including a date string in order to invalidate # results over some time. md5 = hashlib . md5 () md5 . update ( str ( args ) . encode ( \"utf-8\" )) md5 . update ( str ( sorted ( kw . items ())) . encode ( \"utf-8\" )) hash_ = md5 . hexdigest () time_now = now () try : with urlcache . lock : time_orig , contents = urlcache [ hash_ ] if expiration is not None and ( time_now - time_orig ) > expiration : raise KeyError except KeyError : fileobj = function ( * args , ** kw ) if fileobj : contents = fileobj . read () with urlcache . lock : urlcache [ hash_ ] = ( time_now , contents ) else : contents = None return io . BytesIO ( contents ) if contents else None return memoized","title":"memoize_recent_fileobj()"},{"location":"api_reference/beancount.utils.html#beancount.utils.memo.now","text":"Indirection on datetime.datetime.now() for testing. Source code in beancount/utils/memo.py def now (): \"Indirection on datetime.datetime.now() for testing.\" return datetime . datetime . now ()","title":"now()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils","text":"Generic utility packages and functions.","title":"misc_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.LineFileProxy","text":"A file object that will delegate writing full lines to another logging function. This may be used for writing data to a logging level without having to worry about lines.","title":"LineFileProxy"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.LineFileProxy.__init__","text":"Construct a new line delegator file object proxy. Parameters: line_writer \u2013 A callable function, used to write to the delegated output. prefix \u2013 An optional string, the prefix to insert before every line. write_newlines \u2013 A boolean, true if we should output the newline characters. Source code in beancount/utils/misc_utils.py def __init__ ( self , line_writer , prefix = None , write_newlines = False ): \"\"\"Construct a new line delegator file object proxy. Args: line_writer: A callable function, used to write to the delegated output. prefix: An optional string, the prefix to insert before every line. write_newlines: A boolean, true if we should output the newline characters. \"\"\" self . line_writer = line_writer self . prefix = prefix self . write_newlines = write_newlines self . data = []","title":"__init__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.LineFileProxy.close","text":"Close the line delegator. Source code in beancount/utils/misc_utils.py def close ( self ): \"\"\"Close the line delegator.\"\"\" self . flush ()","title":"close()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.LineFileProxy.flush","text":"Flush the data to the line writer. Source code in beancount/utils/misc_utils.py def flush ( self ): \"\"\"Flush the data to the line writer.\"\"\" data = \"\" . join ( self . data ) if data : lines = data . splitlines () self . data = [ lines . pop ( - 1 )] if data [ - 1 ] != \" \\n \" else [] for line in lines : if self . prefix : line = self . prefix + line if self . write_newlines : line += \" \\n \" self . line_writer ( line )","title":"flush()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.LineFileProxy.write","text":"Write some string data to the output. Parameters: data \u2013 A string, with or without newlines. Source code in beancount/utils/misc_utils.py def write ( self , data ): \"\"\"Write some string data to the output. Args: data: A string, with or without newlines. \"\"\" if \" \\n \" in data : self . data . append ( data ) self . flush () else : self . data . append ( data )","title":"write()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.TypeComparable","text":"A base class whose equality comparison includes comparing the type of the instance itself.","title":"TypeComparable"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.box","text":"A context manager that prints out a box around a block. This is useful for printing out stuff from tests in a way that is readable. Parameters: name \u2013 A string, the name of the box to use. file \u2013 The file object to print to. Yields: None. Source code in beancount/utils/misc_utils.py @contextlib . contextmanager def box ( name = None , file = None ): \"\"\"A context manager that prints out a box around a block. This is useful for printing out stuff from tests in a way that is readable. Args: name: A string, the name of the box to use. file: The file object to print to. Yields: None. \"\"\" file = file or sys . stdout file . write ( \" \\n \" ) if name : header = \",--------( {} )-------- \\n \" . format ( name ) footer = \"` {} \\n \" . format ( \"-\" * ( len ( header ) - 2 )) else : header = \",---------------- \\n \" footer = \"`---------------- \\n \" file . write ( header ) yield file . write ( footer ) file . flush ()","title":"box()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.cmptuple","text":"Manufacture a comparable namedtuple class, similar to collections.namedtuple. A comparable named tuple is a tuple which compares to False if contents are equal but the data types are different. We define this to supplement collections.namedtuple because by default a namedtuple disregards the type and we want to make precise comparisons for tests. Parameters: name \u2013 The given name of the class. attributes \u2013 A string or tuple of strings, with the names of the attributes. Returns: A new namedtuple-derived type that compares False with other tuples with same contents. Source code in beancount/utils/misc_utils.py def cmptuple ( name , attributes ): \"\"\"Manufacture a comparable namedtuple class, similar to collections.namedtuple. A comparable named tuple is a tuple which compares to False if contents are equal but the data types are different. We define this to supplement collections.namedtuple because by default a namedtuple disregards the type and we want to make precise comparisons for tests. Args: name: The given name of the class. attributes: A string or tuple of strings, with the names of the attributes. Returns: A new namedtuple-derived type that compares False with other tuples with same contents. \"\"\" base = collections . namedtuple ( \"_ {} \" . format ( name ), attributes ) return type ( name , ( TypeComparable , base , ), {}, )","title":"cmptuple()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.compute_unique_clean_ids","text":"Given a sequence of strings, reduce them to corresponding ids without any funny characters and insure that the list of ids is unique. Yields pairs of (id, string) for the result. Parameters: strings \u2013 A list of strings. Returns: A list of (id, string) pairs. Source code in beancount/utils/misc_utils.py def compute_unique_clean_ids ( strings ): \"\"\"Given a sequence of strings, reduce them to corresponding ids without any funny characters and insure that the list of ids is unique. Yields pairs of (id, string) for the result. Args: strings: A list of strings. Returns: A list of (id, string) pairs. \"\"\" string_set = set ( strings ) # Try multiple methods until we get one that has no collisions. for regexp , replacement in [ ( r \"[^A-Za-z0-9.-]\" , \"_\" ), ( r \"[^A-Za-z0-9_]\" , \"\" ), ]: seen = set () idmap = {} mre = re . compile ( regexp ) for string in string_set : id_ = mre . sub ( replacement , string ) if id_ in seen : break # Collision. seen . add ( id_ ) idmap [ id_ ] = string else : break else : return None # Could not find a unique mapping. return idmap","title":"compute_unique_clean_ids()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.deprecated","text":"A decorator generator to mark functions as deprecated and log a warning. Source code in beancount/utils/misc_utils.py def deprecated ( message ): \"\"\"A decorator generator to mark functions as deprecated and log a warning.\"\"\" def decorator ( func ): @functools . wraps ( func ) def new_func ( * args , ** kwargs ): warnings . warn ( \"Call to deprecated function {} : {} \" . format ( func . __name__ , message ), category = DeprecationWarning , stacklevel = 2 , ) return func ( * args , ** kwargs ) return new_func return decorator","title":"deprecated()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.dictmap","text":"Map a dictionary's value. Parameters: mdict \u2013 A dict. key \u2013 A callable to apply to the keys. value \u2013 A callable to apply to the values. Source code in beancount/utils/misc_utils.py def dictmap ( mdict , keyfun = None , valfun = None ): \"\"\"Map a dictionary's value. Args: mdict: A dict. key: A callable to apply to the keys. value: A callable to apply to the values. \"\"\" if keyfun is None : keyfun = lambda x : x if valfun is None : valfun = lambda x : x return { keyfun ( key ): valfun ( val ) for key , val in mdict . items ()}","title":"dictmap()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.escape_string","text":"Escape quotes and backslashes in payee and narration. Parameters: string \u2013 Any string. Returns. The input string, with offending characters replaced. Source code in beancount/utils/misc_utils.py def escape_string ( string ): \"\"\"Escape quotes and backslashes in payee and narration. Args: string: Any string. Returns. The input string, with offending characters replaced. \"\"\" return string . replace ( \" \\\\ \" , r \" \\\\ \" ) . replace ( '\"' , r \" \\\" \" )","title":"escape_string()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.filter_type","text":"Filter the given list to yield only instances of the given types. Parameters: elist \u2013 A sequence of elements. types \u2013 A sequence of types to include in the output list. Yields: Each element, if it is an instance of 'types'. Source code in beancount/utils/misc_utils.py def filter_type ( elist , types ): \"\"\"Filter the given list to yield only instances of the given types. Args: elist: A sequence of elements. types: A sequence of types to include in the output list. Yields: Each element, if it is an instance of 'types'. \"\"\" for element in elist : if not isinstance ( element , types ): continue yield element","title":"filter_type()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.first_paragraph","text":"Return the first sentence of a docstring. The sentence has to be delimited by an empty line. Parameters: docstring \u2013 A doc string. Returns: A string with just the first sentence on a single line. Source code in beancount/utils/misc_utils.py def first_paragraph ( docstring ): \"\"\"Return the first sentence of a docstring. The sentence has to be delimited by an empty line. Args: docstring: A doc string. Returns: A string with just the first sentence on a single line. \"\"\" lines = [] for line in docstring . strip () . splitlines (): if not line : break lines . append ( line . rstrip ()) return \" \" . join ( lines )","title":"first_paragraph()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.get_screen_height","text":"Return the height of the terminal that runs this program. Returns: An integer, the number of characters the screen is high. Return 0 if the terminal cannot be initialized. Source code in beancount/utils/misc_utils.py def get_screen_height (): \"\"\"Return the height of the terminal that runs this program. Returns: An integer, the number of characters the screen is high. Return 0 if the terminal cannot be initialized. \"\"\" return _get_screen_value ( \"lines\" , 0 )","title":"get_screen_height()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.get_screen_width","text":"Return the width of the terminal that runs this program. Returns: An integer, the number of characters the screen is wide. Return 0 if the terminal cannot be initialized. Source code in beancount/utils/misc_utils.py def get_screen_width (): \"\"\"Return the width of the terminal that runs this program. Returns: An integer, the number of characters the screen is wide. Return 0 if the terminal cannot be initialized. \"\"\" return _get_screen_value ( \"cols\" , 0 )","title":"get_screen_width()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.get_tuple_values","text":"Return all members referred to by this namedtuple instance that satisfy the given predicate. This function also works recursively on its members which are lists or tuples, and so it can be used for Transaction instances. Parameters: ntuple \u2013 A tuple or namedtuple. predicate \u2013 A predicate function that returns true if an attribute is to be output. memo \u2013 An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. Source code in beancount/utils/misc_utils.py def get_tuple_values ( ntuple , predicate , memo = None ): \"\"\"Return all members referred to by this namedtuple instance that satisfy the given predicate. This function also works recursively on its members which are lists or tuples, and so it can be used for Transaction instances. Args: ntuple: A tuple or namedtuple. predicate: A predicate function that returns true if an attribute is to be output. memo: An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. \"\"\" if memo is None : memo = set () id_ntuple = id ( ntuple ) if id_ntuple in memo : return memo . add ( id_ntuple ) if predicate ( ntuple ): yield for attribute in ntuple : if predicate ( attribute ): yield attribute if isinstance ( attribute , ( list , tuple )): for value in get_tuple_values ( attribute , predicate , memo ): yield value","title":"get_tuple_values()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.groupby","text":"Group the elements as a dict of lists, where the key is computed using the function 'keyfun'. Parameters: keyfun \u2013 A callable, used to obtain the group key from each element. elements \u2013 An iterable of the elements to group. Returns: A dict of key to list of sequences. Source code in beancount/utils/misc_utils.py def groupby ( keyfun , elements ): \"\"\"Group the elements as a dict of lists, where the key is computed using the function 'keyfun'. Args: keyfun: A callable, used to obtain the group key from each element. elements: An iterable of the elements to group. Returns: A dict of key to list of sequences. \"\"\" # Note: We could allow a custom aggregation function. Another option is # provide another method to reduce the list values of a dict, but that can # be accomplished using a dict comprehension. grouped = defaultdict ( list ) for element in elements : grouped [ keyfun ( element )] . append ( element ) return grouped","title":"groupby()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.idify","text":"Replace characters objectionable for a filename with underscores. Parameters: string \u2013 Any string. Returns: The input string, with offending characters replaced. Source code in beancount/utils/misc_utils.py def idify ( string ): \"\"\"Replace characters objectionable for a filename with underscores. Args: string: Any string. Returns: The input string, with offending characters replaced. \"\"\" for sfrom , sto in [( r \"[ \\(\\)]+\" , \"_\" ), ( r \"_*\\._*\" , \".\" )]: string = re . sub ( sfrom , sto , string ) string = string . strip ( \"_\" ) return string","title":"idify()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.import_curses","text":"Try to import the 'curses' module. (This is used here in order to override for tests.) Returns: The curses module, if it was possible to import it. Exceptions: ImportError \u2013 If the module could not be imported. Source code in beancount/utils/misc_utils.py def import_curses (): \"\"\"Try to import the 'curses' module. (This is used here in order to override for tests.) Returns: The curses module, if it was possible to import it. Raises: ImportError: If the module could not be imported. \"\"\" # Note: There's a recipe for getting terminal size on Windows here, without # curses, I should probably implement that at some point: # https://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window # Also, consider just using 'blessings' instead, which provides this across # multiple platforms. import curses return curses","title":"import_curses()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.is_sorted","text":"Return true if the sequence is sorted. Parameters: iterable \u2013 An iterable sequence. key \u2013 A function to extract the quantity by which to sort. cmp \u2013 A function that compares two elements of a sequence. Returns: A boolean, true if the sequence is sorted. Source code in beancount/utils/misc_utils.py def is_sorted ( iterable , key = lambda x : x , cmp = lambda x , y : x <= y ): \"\"\"Return true if the sequence is sorted. Args: iterable: An iterable sequence. key: A function to extract the quantity by which to sort. cmp: A function that compares two elements of a sequence. Returns: A boolean, true if the sequence is sorted. \"\"\" iterator = map ( key , iterable ) prev = next ( iterator ) for element in iterator : if not cmp ( prev , element ): return False prev = element return True","title":"is_sorted()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.log_time","text":"A context manager that times the block and logs it to info level. Parameters: operation_name \u2013 A string, a label for the name of the operation. log_timings \u2013 A function to write log messages to. If left to None, no timings are written (this becomes a no-op). indent \u2013 An integer, the indentation level for the format of the timing line. This is useful if you're logging timing to a hierarchy of operations. Yields: The start time of the operation. Source code in beancount/utils/misc_utils.py @contextlib . contextmanager def log_time ( operation_name , log_timings , indent = 0 ): \"\"\"A context manager that times the block and logs it to info level. Args: operation_name: A string, a label for the name of the operation. log_timings: A function to write log messages to. If left to None, no timings are written (this becomes a no-op). indent: An integer, the indentation level for the format of the timing line. This is useful if you're logging timing to a hierarchy of operations. Yields: The start time of the operation. \"\"\" time1 = time () yield time1 time2 = time () if log_timings : log_timings ( \"Operation: {:48} Time: {}{:6.0f} ms\" . format ( \"' {} '\" . format ( operation_name ), \" \" * indent , ( time2 - time1 ) * 1000 ) )","title":"log_time()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.longest","text":"Return the longest of the given subsequences. Parameters: seq \u2013 An iterable sequence of lists. Returns: The longest list from the sequence. Source code in beancount/utils/misc_utils.py def longest ( seq ): \"\"\"Return the longest of the given subsequences. Args: seq: An iterable sequence of lists. Returns: The longest list from the sequence. \"\"\" longest , length = None , - 1 for element in seq : len_element = len ( element ) if len_element > length : longest , length = element , len_element return longest","title":"longest()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.map_namedtuple_attributes","text":"Map the value of the named attributes of object by mapper. Parameters: attributes \u2013 A sequence of string, the attribute names to map. mapper \u2013 A callable that accepts the value of a field and returns the new value. object_ \u2013 Some namedtuple object with attributes on it. Returns: A new instance of the same namedtuple with the named fields mapped by mapper. Source code in beancount/utils/misc_utils.py def map_namedtuple_attributes ( attributes , mapper , object_ ): \"\"\"Map the value of the named attributes of object by mapper. Args: attributes: A sequence of string, the attribute names to map. mapper: A callable that accepts the value of a field and returns the new value. object_: Some namedtuple object with attributes on it. Returns: A new instance of the same namedtuple with the named fields mapped by mapper. \"\"\" return object_ . _replace ( ** { attribute : mapper ( getattr ( object_ , attribute )) for attribute in attributes } )","title":"map_namedtuple_attributes()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.replace_namedtuple_values","text":"Recurse through all the members of namedtuples and lists, and for members that match the given predicate, run them through the given mapper. Parameters: ntuple \u2013 A namedtuple instance. predicate \u2013 A predicate function that returns true if an attribute is to be output. mapper \u2013 A callable, that will accept a single argument and return its replacement value. memo \u2013 An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. Source code in beancount/utils/misc_utils.py def replace_namedtuple_values ( ntuple , predicate , mapper , memo = None ): \"\"\"Recurse through all the members of namedtuples and lists, and for members that match the given predicate, run them through the given mapper. Args: ntuple: A namedtuple instance. predicate: A predicate function that returns true if an attribute is to be output. mapper: A callable, that will accept a single argument and return its replacement value. memo: An optional memoizing dictionary. If a tuple has already been seen, the recursion will be avoided. Yields: Attributes of the tuple and its sub-elements if the predicate is true. \"\"\" if memo is None : memo = set () id_ntuple = id ( ntuple ) if id_ntuple in memo : return None memo . add ( id_ntuple ) if not ( type ( ntuple ) is not tuple and isinstance ( ntuple , tuple )): return ntuple replacements = {} for attribute_name , attribute in zip ( ntuple . _fields , ntuple ): if predicate ( attribute ): replacements [ attribute_name ] = mapper ( attribute ) elif type ( attribute ) is not tuple and isinstance ( attribute , tuple ): replacements [ attribute_name ] = replace_namedtuple_values ( attribute , predicate , mapper , memo ) elif type ( attribute ) in ( list , tuple ): replacements [ attribute_name ] = [ replace_namedtuple_values ( member , predicate , mapper , memo ) for member in attribute ] return ntuple . _replace ( ** replacements )","title":"replace_namedtuple_values()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.skipiter","text":"Skip some elements from an iterator. Parameters: iterable \u2013 An iterator. num_skip \u2013 The number of elements in the period. Yields: Elements from the iterable, with num_skip elements skipped. For example, skipiter(range(10), 3) yields [0, 3, 6, 9]. Source code in beancount/utils/misc_utils.py def skipiter ( iterable , num_skip ): \"\"\"Skip some elements from an iterator. Args: iterable: An iterator. num_skip: The number of elements in the period. Yields: Elements from the iterable, with num_skip elements skipped. For example, skipiter(range(10), 3) yields [0, 3, 6, 9]. \"\"\" assert num_skip > 0 sit = iter ( iterable ) while 1 : try : value = next ( sit ) except StopIteration : return yield value for _ in range ( num_skip - 1 ): try : next ( sit ) except StopIteration : return","title":"skipiter()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.sorted_uniquify","text":"Given a sequence of elements, sort and remove duplicates of the given key. Keep either the first or the last (by key) element of a sequence of key-identical elements. This does not maintain the ordering of the original elements, they are returned sorted (by key) instead. Parameters: iterable \u2013 An iterable sequence. keyfunc \u2013 A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last \u2013 A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. Source code in beancount/utils/misc_utils.py def sorted_uniquify ( iterable , keyfunc = None , last = False ): \"\"\"Given a sequence of elements, sort and remove duplicates of the given key. Keep either the first or the last (by key) element of a sequence of key-identical elements. This does _not_ maintain the ordering of the original elements, they are returned sorted (by key) instead. Args: iterable: An iterable sequence. keyfunc: A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last: A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. \"\"\" if keyfunc is None : keyfunc = lambda x : x if last : prev_obj = UNSET prev_key = UNSET for obj in sorted ( iterable , key = keyfunc ): key = keyfunc ( obj ) if key != prev_key and prev_obj is not UNSET : yield prev_obj prev_obj = obj prev_key = key if prev_obj is not UNSET : yield prev_obj else : prev_key = UNSET for obj in sorted ( iterable , key = keyfunc ): key = keyfunc ( obj ) if key != prev_key : yield obj prev_key = key","title":"sorted_uniquify()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.staticvar","text":"Returns a decorator that defines a Python function attribute. This is used to simulate a static function variable in Python. Parameters: varname \u2013 A string, the name of the variable to define. initial_value \u2013 The value to initialize the variable to. Returns: A function decorator. Source code in beancount/utils/misc_utils.py def staticvar ( varname , initial_value ): \"\"\"Returns a decorator that defines a Python function attribute. This is used to simulate a static function variable in Python. Args: varname: A string, the name of the variable to define. initial_value: The value to initialize the variable to. Returns: A function decorator. \"\"\" def deco ( fun ): setattr ( fun , varname , initial_value ) return fun return deco","title":"staticvar()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.swallow","text":"Catch and ignore certain exceptions. Parameters: exception_types \u2013 A tuple of exception classes to ignore. Yields: None. Source code in beancount/utils/misc_utils.py @contextlib . contextmanager def swallow ( * exception_types ): \"\"\"Catch and ignore certain exceptions. Args: exception_types: A tuple of exception classes to ignore. Yields: None. \"\"\" try : yield except Exception as exc : if not isinstance ( exc , exception_types ): raise","title":"swallow()"},{"location":"api_reference/beancount.utils.html#beancount.utils.misc_utils.uniquify","text":"Given a sequence of elements, remove duplicates of the given key. Keep either the first or the last element of a sequence of key-identical elements. Order is maintained as much as possible. This does maintain the ordering of the original elements, they are returned in the same order as the original elements. Parameters: iterable \u2013 An iterable sequence. keyfunc \u2013 A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last \u2013 A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. Source code in beancount/utils/misc_utils.py def uniquify ( iterable , keyfunc = None , last = False ): \"\"\"Given a sequence of elements, remove duplicates of the given key. Keep either the first or the last element of a sequence of key-identical elements. Order is maintained as much as possible. This does maintain the ordering of the original elements, they are returned in the same order as the original elements. Args: iterable: An iterable sequence. keyfunc: A function that extracts from the elements the sort key to use and uniquify on. If left unspecified, the identify function is used and the uniquification occurs on the elements themselves. last: A boolean, True if we should keep the last item of the same keys. Otherwise keep the first. Yields: Elements from the iterable. \"\"\" if keyfunc is None : keyfunc = lambda x : x seen = set () if last : unique_reversed_list = [] for obj in reversed ( iterable ): key = keyfunc ( obj ) if key not in seen : seen . add ( key ) unique_reversed_list . append ( obj ) yield from reversed ( unique_reversed_list ) else : for obj in iterable : key = keyfunc ( obj ) if key not in seen : seen . add ( key ) yield obj","title":"uniquify()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager","text":"Code to write output to a pager. This module contains an object accumulates lines up to a minimum and then decides whether to flush them to the original output directly if under the threshold (no pager) or creates a pager and flushes the lines to it if above the threshold and then forwards all future lines to it. The purpose of this object is to pipe output to a pager only if the number of lines to be printed exceeds a minimum number of lines. The contextmanager is intended to be used to pipe output to a pager and wait on the pager to complete before continuing. Simply write to the file object and upon exit we close the file object. This also silences broken pipe errors triggered by the user exiting the sub-process, and recovers from a failing pager command by just using stdout.","title":"pager"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.ConditionalPager","text":"A proxy file for a pager that only creates a pager after a minimum number of lines has been printed to it.","title":"ConditionalPager"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.ConditionalPager.__enter__","text":"Initialize the context manager and return this instance as it. Source code in beancount/utils/pager.py def __enter__ ( self ): \"\"\"Initialize the context manager and return this instance as it.\"\"\" # The file and pipe object we're writing to. This gets set after the # number of accumulated lines reaches the threshold. if self . minlines : self . file = None self . pipe = None else : self . file , self . pipe = create_pager ( self . command , self . default_file ) # Lines accumulated before the threshold. self . accumulated_data = [] self . accumulated_lines = 0 # Return this object to be used as the context manager itself. return self","title":"__enter__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.ConditionalPager.__exit__","text":"Context manager exit. This flushes the output to our output file. Parameters: type \u2013 Optional exception type, as per context managers. value \u2013 Optional exception value, as per context managers. unused_traceback \u2013 Optional trace. Source code in beancount/utils/pager.py def __exit__ ( self , type , value , unused_traceback ): \"\"\"Context manager exit. This flushes the output to our output file. Args: type: Optional exception type, as per context managers. value: Optional exception value, as per context managers. unused_traceback: Optional trace. \"\"\" try : if self . file : # Flush the output file and close it. self . file . flush () else : # Oops... we never reached the threshold. Flush the accumulated # output to the file. self . flush_accumulated ( self . default_file ) # Wait for the subprocess (if we have one). if self . pipe : self . file . close () self . pipe . wait () # Absorb broken pipes that may occur on flush or close above. except BrokenPipeError : return True # Absorb broken pipes. if isinstance ( value , BrokenPipeError ): return True elif value : raise","title":"__exit__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.ConditionalPager.__init__","text":"Create a conditional pager. Parameters: command \u2013 A string, the shell command to run as a pager. minlines \u2013 If set, the number of lines under which you should not bother starting a pager. This avoids kicking off a pager if the screen is high enough to render the contents. If the value is unset, always starts a pager (which is fine behavior too). Source code in beancount/utils/pager.py def __init__ ( self , command , minlines = None ): \"\"\"Create a conditional pager. Args: command: A string, the shell command to run as a pager. minlines: If set, the number of lines under which you should not bother starting a pager. This avoids kicking off a pager if the screen is high enough to render the contents. If the value is unset, always starts a pager (which is fine behavior too). \"\"\" self . command = command self . minlines = minlines self . default_file = ( codecs . getwriter ( \"utf-8\" )( sys . stdout . buffer ) if hasattr ( sys . stdout , \"buffer\" ) else sys . stdout )","title":"__init__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.ConditionalPager.flush_accumulated","text":"Flush the existing lines to the newly created pager. This also disabled the accumulator. Parameters: file \u2013 A file object to flush the accumulated data to. Source code in beancount/utils/pager.py def flush_accumulated ( self , file ): \"\"\"Flush the existing lines to the newly created pager. This also disabled the accumulator. Args: file: A file object to flush the accumulated data to. \"\"\" if self . accumulated_data : write = file . write for data in self . accumulated_data : write ( data ) self . accumulated_data = None self . accumulated_lines = None","title":"flush_accumulated()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.ConditionalPager.write","text":"Write the data out. Overridden from the file object interface. Parameters: data \u2013 A string, data to write to the output. Source code in beancount/utils/pager.py def write ( self , data ): \"\"\"Write the data out. Overridden from the file object interface. Args: data: A string, data to write to the output. \"\"\" if self . file is None : # Accumulate the new lines. self . accumulated_lines += data . count ( \" \\n \" ) self . accumulated_data . append ( data ) # If we've reached the threshold, create a file. if self . accumulated_lines > self . minlines : self . file , self . pipe = create_pager ( self . command , self . default_file ) self . flush_accumulated ( self . file ) else : # We've already created a pager subprocess... flush the lines to it. self . file . write ( data ) # try: # except BrokenPipeError: # # Make sure we don't barf on __exit__(). # self.file = self.pipe = None # raise","title":"write()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.create_pager","text":"Try to create and return a pager subprocess. Parameters: command \u2013 A string, the shell command to run as a pager. file \u2013 The file object for the pager write to. This is also used as a default if we failed to create the pager subprocess. Returns: A pair of (file, pipe), a file object and an optional subprocess.Popen instance to wait on. The pipe instance may be set to None if we failed to create a subprocess. Source code in beancount/utils/pager.py def create_pager ( command , file ): \"\"\"Try to create and return a pager subprocess. Args: command: A string, the shell command to run as a pager. file: The file object for the pager write to. This is also used as a default if we failed to create the pager subprocess. Returns: A pair of (file, pipe), a file object and an optional subprocess.Popen instance to wait on. The pipe instance may be set to None if we failed to create a subprocess. \"\"\" if command is None : command = os . environ . get ( \"PAGER\" , DEFAULT_PAGER ) if not command : command = DEFAULT_PAGER pipe = None # In case of using 'less', make sure the charset is set properly. In theory # you could override this by setting PAGER to \"LESSCHARSET=utf-8 less\" but # this shouldn't affect other programs and is unlikely to cause problems, so # we set it here to make default behavior work for most people (we always # write UTF-8). env = os . environ . copy () env [ \"LESSCHARSET\" ] = \"utf-8\" try : pipe = subprocess . Popen ( command , shell = True , stdin = subprocess . PIPE , stdout = file , env = env ) except OSError as exc : logging . error ( \"Invalid pager: {} \" . format ( exc )) else : stdin_wrapper = io . TextIOWrapper ( pipe . stdin , \"utf-8\" ) file = stdin_wrapper return file , pipe","title":"create_pager()"},{"location":"api_reference/beancount.utils.html#beancount.utils.pager.flush_only","text":"A contextmanager around a file object that does not close the file. This is used to return a context manager on a file object but not close it. We flush it instead. This is useful in order to provide an alternative to a pager class as above. Parameters: fileobj \u2013 A file object, to remain open after running the context manager. Yields: A context manager that yields this object. Source code in beancount/utils/pager.py @contextlib . contextmanager def flush_only ( fileobj ): \"\"\"A contextmanager around a file object that does not close the file. This is used to return a context manager on a file object but not close it. We flush it instead. This is useful in order to provide an alternative to a pager class as above. Args: fileobj: A file object, to remain open after running the context manager. Yields: A context manager that yields this object. \"\"\" try : yield fileobj finally : fileobj . flush ()","title":"flush_only()"},{"location":"api_reference/beancount.utils.html#beancount.utils.snoop","text":"Text manipulation utilities.","title":"snoop"},{"location":"api_reference/beancount.utils.html#beancount.utils.snoop.Snoop","text":"A snooper callable that just saves the returned values of a function. This is particularly useful for re.match and re.search in conditionals, e.g.:: snoop = Snoop() ... if snoop(re.match(r\"(\\d+)-(\\d+)-(\\d+)\", text)): year, month, date = snoop.value.group(1, 2, 3) Attributes: Name Type Description value The last value snooped from a function call. history If 'maxlen' was specified, the last few values that were snooped.","title":"Snoop"},{"location":"api_reference/beancount.utils.html#beancount.utils.snoop.Snoop.__call__","text":"Save a value to the snooper. This is meant to wrap a function call. Parameters: value \u2013 The value to push/save. Returns: Value itself. Source code in beancount/utils/snoop.py def __call__ ( self , value ): \"\"\"Save a value to the snooper. This is meant to wrap a function call. Args: value: The value to push/save. Returns: Value itself. \"\"\" self . value = value if self . history is not None : self . history . append ( value ) return value","title":"__call__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.snoop.Snoop.__getattr__","text":"Forward the attribute to the value. Parameters: attr \u2013 A string, the name of the attribute. Returns: The value of the attribute. Source code in beancount/utils/snoop.py def __getattr__ ( self , attr ): \"\"\"Forward the attribute to the value. Args: attr: A string, the name of the attribute. Returns: The value of the attribute. \"\"\" return getattr ( self . value , attr )","title":"__getattr__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.snoop.Snoop.__init__","text":"Create a new snooper. Parameters: maxlen \u2013 If specified, an integer, which enables the saving of that Source code in beancount/utils/snoop.py def __init__ ( self , maxlen = None ): \"\"\"Create a new snooper. Args: maxlen: If specified, an integer, which enables the saving of that number of last values in the history attribute. \"\"\" self . value = None self . history = collections . deque ( maxlen = maxlen ) if maxlen else None","title":"__init__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.snoop.snoopify","text":"Decorate a function as snoopable. This is meant to reassign existing functions to a snoopable version of them. For example, if you wanted 're.match' to be automatically snoopable, just decorate it like this: re.match = snoopify(re.match) and then you can just call 're.match' in a conditional and then access 're.match.value' to get to the last returned value. Source code in beancount/utils/snoop.py def snoopify ( function ): \"\"\"Decorate a function as snoopable. This is meant to reassign existing functions to a snoopable version of them. For example, if you wanted 're.match' to be automatically snoopable, just decorate it like this: re.match = snoopify(re.match) and then you can just call 're.match' in a conditional and then access 're.match.value' to get to the last returned value. \"\"\" @functools . wraps ( function ) def wrapper ( * args , ** kw ): value = function ( * args , ** kw ) wrapper . value = value return value wrapper . value = None return wrapper","title":"snoopify()"},{"location":"api_reference/beancount.utils.html#beancount.utils.table","text":"Table rendering.","title":"table"},{"location":"api_reference/beancount.utils.html#beancount.utils.table.Table","text":"Table(columns, header, body)","title":"Table"},{"location":"api_reference/beancount.utils.html#beancount.utils.table.Table.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/utils/table.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.table.Table.__new__","text":"Create new instance of Table(columns, header, body)","title":"__new__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.table.Table.__repr__","text":"Return a nicely formatted representation string Source code in beancount/utils/table.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.table.attribute_to_title","text":"Convert programming id into readable field name. Parameters: fieldname \u2013 A string, a programming ids, such as 'book_value'. Returns: A readable string, such as 'Book Value.' Source code in beancount/utils/table.py def attribute_to_title ( fieldname ): \"\"\"Convert programming id into readable field name. Args: fieldname: A string, a programming ids, such as 'book_value'. Returns: A readable string, such as 'Book Value.' \"\"\" return fieldname . replace ( \"_\" , \" \" ) . title ()","title":"attribute_to_title()"},{"location":"api_reference/beancount.utils.html#beancount.utils.table.compute_table_widths","text":"Compute the max character widths of a list of rows. Parameters: rows \u2013 A list of rows, which are sequences of strings. Returns: A list of integers, the maximum widths required to render the columns of this table. Exceptions: IndexError \u2013 If the rows are of different lengths. Source code in beancount/utils/table.py def compute_table_widths ( rows ): \"\"\"Compute the max character widths of a list of rows. Args: rows: A list of rows, which are sequences of strings. Returns: A list of integers, the maximum widths required to render the columns of this table. Raises: IndexError: If the rows are of different lengths. \"\"\" row_iter = iter ( rows ) first_row = next ( row_iter ) num_columns = len ( first_row ) column_widths = [ len ( cell ) for cell in first_row ] for row in row_iter : for i , cell in enumerate ( row ): if not isinstance ( cell , str ): cell = str ( cell ) cell_len = len ( cell ) if cell_len > column_widths [ i ]: column_widths [ i ] = cell_len if i + 1 != num_columns : raise IndexError ( \"Invalid number of rows\" ) return column_widths","title":"compute_table_widths()"},{"location":"api_reference/beancount.utils.html#beancount.utils.table.create_table","text":"Convert a list of tuples to an table report object. Parameters: rows \u2013 A list of tuples. field_spec \u2013 A list of strings, or a list of (FIELDNAME-OR-INDEX, HEADER, FORMATTER-FUNCTION) triplets, that selects a subset of the fields is to be rendered as well as their ordering. If this is a dict, the values are functions to call on the fields to render them. If a function is set to None, we will just call str() on the field. Returns: A Table instance. Source code in beancount/utils/table.py def create_table ( rows , field_spec = None ): \"\"\"Convert a list of tuples to an table report object. Args: rows: A list of tuples. field_spec: A list of strings, or a list of (FIELDNAME-OR-INDEX, HEADER, FORMATTER-FUNCTION) triplets, that selects a subset of the fields is to be rendered as well as their ordering. If this is a dict, the values are functions to call on the fields to render them. If a function is set to None, we will just call str() on the field. Returns: A Table instance. \"\"\" # Normalize field_spec to a dict. if field_spec is None : namedtuple_class = type ( rows [ 0 ]) field_spec = [( field , None , None ) for field in namedtuple_class . _fields ] elif isinstance ( field_spec , ( list , tuple )): new_field_spec = [] for field in field_spec : if isinstance ( field , tuple ): assert len ( field ) <= 3 , field if len ( field ) == 1 : field = field [ 0 ] new_field_spec . append (( field , None , None )) elif len ( field ) == 2 : field , header = field new_field_spec . append (( field , header , None )) elif len ( field ) == 3 : new_field_spec . append ( field ) else : if isinstance ( field , str ): title = attribute_to_title ( field ) elif isinstance ( field , int ): title = \"Field {} \" . format ( field ) else : raise ValueError ( \"Invalid type for column name\" ) new_field_spec . append (( field , title , None )) field_spec = new_field_spec # Ensure a nicely formatted header. field_spec = [ ( ( name , attribute_to_title ( name ), formatter ) if header_ is None else ( name , header_ , formatter ) ) for ( name , header_ , formatter ) in field_spec ] assert isinstance ( field_spec , list ), field_spec assert all ( len ( x ) == 3 for x in field_spec ), field_spec # Compute the column names. columns = [ name for ( name , _ , __ ) in field_spec ] # Compute the table header. header = [ header_column for ( _ , header_column , __ ) in field_spec ] # Compute the table body. body = [] for row in rows : body_row = [] for name , _ , formatter in field_spec : if isinstance ( name , str ): value = getattr ( row , name ) elif isinstance ( name , int ): value = row [ name ] else : raise ValueError ( \"Invalid type for column name\" ) if value is not None : if formatter is not None : value = formatter ( value ) else : value = str ( value ) else : value = \"\" body_row . append ( value ) body . append ( body_row ) return Table ( columns , header , body )","title":"create_table()"},{"location":"api_reference/beancount.utils.html#beancount.utils.table.render_table","text":"Render the given table to the output file object in the requested format. The table gets written out to the 'output' file. Parameters: table_ \u2013 An instance of Table. output \u2013 A file object you can write to. output_format \u2013 A string, the format to write the table to, either 'csv', 'txt' or 'html'. css_id \u2013 A string, an optional CSS id for the table object (only used for HTML). css_class \u2013 A string, an optional CSS class for the table object (only used for HTML). Source code in beancount/utils/table.py def render_table ( table_ , output , output_format , css_id = None , css_class = None ): \"\"\"Render the given table to the output file object in the requested format. The table gets written out to the 'output' file. Args: table_: An instance of Table. output: A file object you can write to. output_format: A string, the format to write the table to, either 'csv', 'txt' or 'html'. css_id: A string, an optional CSS id for the table object (only used for HTML). css_class: A string, an optional CSS class for the table object (only used for HTML). \"\"\" if output_format in ( \"txt\" , \"text\" ): text = table_to_text ( table_ , \" \" , formats = { \"*\" : \">\" , \"account\" : \"<\" }) output . write ( text ) elif output_format in ( \"csv\" ,): table_to_csv ( table_ , file = output ) elif output_format in ( \"htmldiv\" , \"html\" ): if output_format == \"html\" : output . write ( \" \\n \" ) output . write ( \" \\n \" ) output . write ( '
    \\n ' . format ( css_id ) if css_id else \"
    \\n \" ) classes = [ css_class ] if css_class else None table_to_html ( table_ , file = output , classes = classes ) output . write ( \"
    \\n \" ) if output_format == \"html\" : output . write ( \" \\n \" ) output . write ( \" \\n \" ) else : raise NotImplementedError ( \"Unsupported format: {} \" . format ( output_format ))","title":"render_table()"},{"location":"api_reference/beancount.utils.html#beancount.utils.table.table_to_csv","text":"Render a Table to a CSV file. Parameters: table \u2013 An instance of a Table. file \u2013 A file object to write to. If no object is provided, this function returns a string. **kwargs \u2013 Optional arguments forwarded to csv.writer(). Returns: A string, the rendered table, or None, if a file object is provided to write to. Source code in beancount/utils/table.py def table_to_csv ( table , file = None , ** kwargs ): \"\"\"Render a Table to a CSV file. Args: table: An instance of a Table. file: A file object to write to. If no object is provided, this function returns a string. **kwargs: Optional arguments forwarded to csv.writer(). Returns: A string, the rendered table, or None, if a file object is provided to write to. \"\"\" output_file = file or io . StringIO () writer = csv . writer ( output_file , ** kwargs ) if table . header : writer . writerow ( table . header ) writer . writerows ( table . body ) if not file : return output_file . getvalue ()","title":"table_to_csv()"},{"location":"api_reference/beancount.utils.html#beancount.utils.table.table_to_html","text":"Render a Table to HTML. Parameters: table \u2013 An instance of a Table. classes \u2013 A list of string, CSS classes to set on the table. file \u2013 A file object to write to. If no object is provided, this function returns a string. Returns: A string, the rendered table, or None, if a file object is provided to write to. Source code in beancount/utils/table.py def table_to_html ( table , classes = None , file = None ): \"\"\"Render a Table to HTML. Args: table: An instance of a Table. classes: A list of string, CSS classes to set on the table. file: A file object to write to. If no object is provided, this function returns a string. Returns: A string, the rendered table, or None, if a file object is provided to write to. \"\"\" # Initialize file. oss = io . StringIO () if file is None else file oss . write ( ' \\n ' . format ( \" \" . join ( classes or []))) # Render header. if table . header : oss . write ( \" \\n \" ) oss . write ( \" \\n \" ) for header in table . header : oss . write ( \" \\n \" . format ( header )) oss . write ( \" \\n \" ) oss . write ( \" \\n \" ) # Render body. oss . write ( \" \\n \" ) for row in table . body : oss . write ( \" \\n \" ) for cell in row : oss . write ( \" \\n \" . format ( cell )) oss . write ( \" \\n \" ) oss . write ( \" \\n \" ) # Render footer. oss . write ( \"
    {}
    {}
    \\n \" ) if file is None : return oss . getvalue ()","title":"table_to_html()"},{"location":"api_reference/beancount.utils.html#beancount.utils.table.table_to_text","text":"Render a Table to ASCII text. Parameters: table \u2013 An instance of a Table. column_interspace \u2013 A string to render between the columns as spacer. formats \u2013 An optional dict of column name to a format character that gets inserted in a format string specified, like this (where '' is): {:}. A key of ' ' will provide a default value, like this, for example: (... formats={' ': '>'}). Returns: A string, the rendered text table. Source code in beancount/utils/table.py def table_to_text ( table , column_interspace = \" \" , formats = None ): \"\"\"Render a Table to ASCII text. Args: table: An instance of a Table. column_interspace: A string to render between the columns as spacer. formats: An optional dict of column name to a format character that gets inserted in a format string specified, like this (where '' is): {:}. A key of '*' will provide a default value, like this, for example: (... formats={'*': '>'}). Returns: A string, the rendered text table. \"\"\" column_widths = compute_table_widths ( itertools . chain ([ table . header ], table . body )) # Insert column format chars and compute line formatting string. column_formats = [] if formats : default_format = formats . get ( \"*\" , None ) for column , width in zip ( table . columns , column_widths ): if column and formats : format_ = formats . get ( column , default_format ) if format_ : column_formats . append ( \"{{: {}{:d} }}\" . format ( format_ , width )) else : column_formats . append ( \"{{: {:d} }}\" . format ( width )) else : column_formats . append ( \"{{: {:d} }}\" . format ( width )) line_format = column_interspace . join ( column_formats ) + \" \\n \" separator = line_format . format ( * [( \"-\" * width ) for width in column_widths ]) # Render the header. oss = io . StringIO () if table . header : oss . write ( line_format . format ( * table . header )) # Render the body. oss . write ( separator ) for row in table . body : oss . write ( line_format . format ( * row )) oss . write ( separator ) return oss . getvalue ()","title":"table_to_text()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils","text":"Support utilities for testing scripts.","title":"test_utils"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.ClickTestCase","text":"Base class for command-line program test cases.","title":"ClickTestCase"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.RCall","text":"RCall(args, kwargs, return_value)","title":"RCall"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.RCall.__getnewargs__","text":"Return self as a plain tuple. Used by copy and pickle. Source code in beancount/utils/test_utils.py def __getnewargs__ ( self ): 'Return self as a plain tuple. Used by copy and pickle.' return _tuple ( self )","title":"__getnewargs__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.RCall.__new__","text":"Create new instance of RCall(args, kwargs, return_value)","title":"__new__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.RCall.__repr__","text":"Return a nicely formatted representation string Source code in beancount/utils/test_utils.py def __repr__ ( self ): 'Return a nicely formatted representation string' return self . __class__ . __name__ + repr_fmt % self","title":"__repr__()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.TestCase","text":"","title":"TestCase"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.TestCase.assertLines","text":"Compare the lines of text1 and text2, ignoring whitespace. Parameters: text1 \u2013 A string, the expected text. text2 \u2013 A string, the actual text. message \u2013 An optional string message in case the assertion fails. Exceptions: AssertionError \u2013 If the exception fails. Source code in beancount/utils/test_utils.py def assertLines ( self , text1 , text2 , message = None ): \"\"\"Compare the lines of text1 and text2, ignoring whitespace. Args: text1: A string, the expected text. text2: A string, the actual text. message: An optional string message in case the assertion fails. Raises: AssertionError: If the exception fails. \"\"\" clean_text1 = textwrap . dedent ( text1 . strip ()) clean_text2 = textwrap . dedent ( text2 . strip ()) lines1 = [ line . strip () for line in clean_text1 . splitlines ()] lines2 = [ line . strip () for line in clean_text2 . splitlines ()] # Compress all space longer than 4 spaces to exactly 4. # This affords us to be even looser. lines1 = [ re . sub ( \" [ \\t ]*\" , \" \" , line ) for line in lines1 ] lines2 = [ re . sub ( \" [ \\t ]*\" , \" \" , line ) for line in lines2 ] self . assertEqual ( lines1 , lines2 , message )","title":"assertLines()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.TestCase.assertOutput","text":"Expect text printed to stdout. Parameters: expected_text \u2013 A string, the text that should have been printed to stdout. Exceptions: AssertionError \u2013 If the text differs. Source code in beancount/utils/test_utils.py @contextlib . contextmanager def assertOutput ( self , expected_text ): \"\"\"Expect text printed to stdout. Args: expected_text: A string, the text that should have been printed to stdout. Raises: AssertionError: If the text differs. \"\"\" with capture () as oss : yield oss self . assertLines ( textwrap . dedent ( expected_text ), oss . getvalue ())","title":"assertOutput()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.TmpFilesTestBase","text":"A test utility base class that creates and cleans up a directory hierarchy. This convenience is useful for testing functions that work on files, such as the documents tests, or the accounts walk.","title":"TmpFilesTestBase"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.TmpFilesTestBase.create_file_hierarchy","text":"A test utility that creates a hierarchy of files. Parameters: test_files \u2013 A list of strings, relative filenames to a temporary root directory. If the filename ends with a '/', we create a directory; otherwise, we create a regular file. subdir \u2013 A string, the subdirectory name under the temporary directory location, to create the hierarchy under. Returns: A pair of strings, the temporary directory, and the subdirectory under that which hosts the root of the tree. Source code in beancount/utils/test_utils.py @staticmethod def create_file_hierarchy ( test_files , subdir = \"root\" ): \"\"\"A test utility that creates a hierarchy of files. Args: test_files: A list of strings, relative filenames to a temporary root directory. If the filename ends with a '/', we create a directory; otherwise, we create a regular file. subdir: A string, the subdirectory name under the temporary directory location, to create the hierarchy under. Returns: A pair of strings, the temporary directory, and the subdirectory under that which hosts the root of the tree. \"\"\" tempdir = tempfile . mkdtemp ( prefix = \"beancount-test-tmpdir.\" ) root = path . join ( tempdir , subdir ) for filename in test_files : abs_filename = path . join ( tempdir , filename ) if filename . endswith ( \"/\" ): os . makedirs ( abs_filename ) else : parent_dir = path . dirname ( abs_filename ) if not path . exists ( parent_dir ): os . makedirs ( parent_dir ) with open ( abs_filename , \"w\" ): pass return tempdir , root","title":"create_file_hierarchy()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.TmpFilesTestBase.setUp","text":"Hook method for setting up the test fixture before exercising it. Source code in beancount/utils/test_utils.py def setUp ( self ): self . tempdir , self . root = self . create_file_hierarchy ( self . TEST_DOCUMENTS )","title":"setUp()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.TmpFilesTestBase.tearDown","text":"Hook method for deconstructing the test fixture after testing it. Source code in beancount/utils/test_utils.py def tearDown ( self ): shutil . rmtree ( self . tempdir , ignore_errors = True )","title":"tearDown()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.capture","text":"A context manager that captures what's printed to stdout. Parameters: *attributes \u2013 A tuple of strings, the name of the sys attributes to override with StringIO instances. Yields: A StringIO string accumulator. Source code in beancount/utils/test_utils.py def capture ( * attributes ): \"\"\"A context manager that captures what's printed to stdout. Args: *attributes: A tuple of strings, the name of the sys attributes to override with StringIO instances. Yields: A StringIO string accumulator. \"\"\" if not attributes : attributes = \"stdout\" elif len ( attributes ) == 1 : attributes = attributes [ 0 ] return patch ( sys , attributes , io . StringIO )","title":"capture()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.create_temporary_files","text":"Create a number of temporary files under 'root'. This routine is used to initialize the contents of multiple files under a temporary directory. Parameters: root \u2013 A string, the name of the directory under which to create the files. contents_map \u2013 A dict of relative filenames to their contents. The content strings will be automatically dedented for convenience. In addition, the string 'ROOT' in the contents will be automatically replaced by the root directory name. Source code in beancount/utils/test_utils.py def create_temporary_files ( root , contents_map ): \"\"\"Create a number of temporary files under 'root'. This routine is used to initialize the contents of multiple files under a temporary directory. Args: root: A string, the name of the directory under which to create the files. contents_map: A dict of relative filenames to their contents. The content strings will be automatically dedented for convenience. In addition, the string 'ROOT' in the contents will be automatically replaced by the root directory name. \"\"\" os . makedirs ( root , exist_ok = True ) for relative_filename , contents in contents_map . items (): assert not path . isabs ( relative_filename ) filename = path . join ( root , relative_filename ) os . makedirs ( path . dirname ( filename ), exist_ok = True ) clean_contents = textwrap . dedent ( contents . replace ( \" {root} \" , root )) with open ( filename , \"w\" ) as f : f . write ( clean_contents )","title":"create_temporary_files()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.docfile","text":"A decorator that write the function's docstring to a temporary file and calls the decorated function with the temporary filename. This is useful for writing tests. Parameters: function \u2013 A function to decorate. Returns: The decorated function. Source code in beancount/utils/test_utils.py def docfile ( function , ** kwargs ): \"\"\"A decorator that write the function's docstring to a temporary file and calls the decorated function with the temporary filename. This is useful for writing tests. Args: function: A function to decorate. Returns: The decorated function. \"\"\" contents = kwargs . pop ( \"contents\" , None ) @functools . wraps ( function ) def new_function ( self ): allowed = ( \"buffering\" , \"encoding\" , \"newline\" , \"dir\" , \"prefix\" , \"suffix\" ) if any ( key not in allowed for key in kwargs ): raise ValueError ( \"Invalid kwarg to docfile_extra\" ) with tempfile . NamedTemporaryFile ( \"w\" , ** kwargs ) as file : text = contents or function . __doc__ file . write ( textwrap . dedent ( text )) file . flush () return function ( self , file . name ) new_function . __doc__ = None return new_function","title":"docfile()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.docfile_extra","text":"A decorator identical to @docfile, but it also takes kwargs for the temporary file, Kwargs: e.g. buffering, encoding, newline, dir, prefix, and suffix. Returns: docfile Source code in beancount/utils/test_utils.py def docfile_extra ( ** kwargs ): \"\"\" A decorator identical to @docfile, but it also takes kwargs for the temporary file, Kwargs: e.g. buffering, encoding, newline, dir, prefix, and suffix. Returns: docfile \"\"\" return functools . partial ( docfile , ** kwargs )","title":"docfile_extra()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.environ","text":"A context manager which pushes varname's value and restores it later. Parameters: varname \u2013 A string, the environ variable name. newvalue \u2013 A string, the desired value. Source code in beancount/utils/test_utils.py @contextlib . contextmanager def environ ( varname , newvalue ): \"\"\"A context manager which pushes varname's value and restores it later. Args: varname: A string, the environ variable name. newvalue: A string, the desired value. \"\"\" oldvalue = os . environ . get ( varname , None ) os . environ [ varname ] = newvalue yield if oldvalue is not None : os . environ [ varname ] = oldvalue else : del os . environ [ varname ]","title":"environ()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.find_python_lib","text":"Return the path to the root of the Python libraries. Returns: A string, the root directory. Source code in beancount/utils/test_utils.py def find_python_lib (): \"\"\"Return the path to the root of the Python libraries. Returns: A string, the root directory. \"\"\" return path . dirname ( path . dirname ( path . dirname ( __file__ )))","title":"find_python_lib()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.find_repository_root","text":"Return the path to the repository root. Parameters: filename \u2013 A string, the name of a file within the repository. Returns: A string, the root directory. Source code in beancount/utils/test_utils.py def find_repository_root ( filename = None ): \"\"\"Return the path to the repository root. Args: filename: A string, the name of a file within the repository. Returns: A string, the root directory. \"\"\" if filename is None : filename = __file__ # Support root directory under Bazel. match = re . match ( r \"(.*\\.runfiles/beancount)/\" , filename ) if match : return match . group ( 1 ) while not path . exists ( path . join ( filename , \"pyproject.toml\" )): prev_filename = filename filename = path . dirname ( filename ) if prev_filename == filename : raise ValueError ( \"Failed to find the root directory.\" ) return filename","title":"find_repository_root()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.make_failing_importer","text":"Make an importer that raise an ImportError for some modules. Use it like this: @mock.patch('builtins. import ', make_failing_importer('setuptools')) def test_... Parameters: removed_module_name \u2013 The name of the module import that should raise an exception. Returns: A decorated test decorator. Source code in beancount/utils/test_utils.py def make_failing_importer ( * removed_module_names ): \"\"\"Make an importer that raise an ImportError for some modules. Use it like this: @mock.patch('builtins.__import__', make_failing_importer('setuptools')) def test_... Args: removed_module_name: The name of the module import that should raise an exception. Returns: A decorated test decorator. \"\"\" def failing_import ( name , * args , ** kwargs ): if name in removed_module_names : raise ImportError ( \"Could not import {} \" . format ( name )) return builtins . __import__ ( name , * args , ** kwargs ) return failing_import","title":"make_failing_importer()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.nottest","text":"Make the given function not testable. Source code in beancount/utils/test_utils.py def nottest ( func ): \"Make the given function not testable.\" func . __test__ = False return func","title":"nottest()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.patch","text":"A context manager that temporarily patches an object's attributes. All attributes in 'attributes' are saved and replaced by new instances of type 'replacement_type'. Parameters: obj \u2013 The object to patch up. attributes \u2013 A string or a sequence of strings, the names of attributes to replace. replacement_type \u2013 A callable to build replacement objects. Yields: An instance of a list of sequences of 'replacement_type'. Source code in beancount/utils/test_utils.py @contextlib . contextmanager def patch ( obj , attributes , replacement_type ): \"\"\"A context manager that temporarily patches an object's attributes. All attributes in 'attributes' are saved and replaced by new instances of type 'replacement_type'. Args: obj: The object to patch up. attributes: A string or a sequence of strings, the names of attributes to replace. replacement_type: A callable to build replacement objects. Yields: An instance of a list of sequences of 'replacement_type'. \"\"\" single = isinstance ( attributes , str ) if single : attributes = [ attributes ] saved = [] replacements = [] for attribute in attributes : replacement = replacement_type () replacements . append ( replacement ) saved . append ( getattr ( obj , attribute )) setattr ( obj , attribute , replacement ) yield replacements [ 0 ] if single else replacements for attribute , saved_attr in zip ( attributes , saved ): setattr ( obj , attribute , saved_attr )","title":"patch()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.record","text":"Decorates the function to intercept and record all calls and return values. Parameters: fun \u2013 A callable to be decorated. Returns: A wrapper function with a .calls attribute, a list of RCall instances. Source code in beancount/utils/test_utils.py def record ( fun ): \"\"\"Decorates the function to intercept and record all calls and return values. Args: fun: A callable to be decorated. Returns: A wrapper function with a .calls attribute, a list of RCall instances. \"\"\" @functools . wraps ( fun ) def wrapped ( * args , ** kw ): return_value = fun ( * args , ** kw ) wrapped . calls . append ( RCall ( args , kw , return_value )) return return_value wrapped . calls = [] return wrapped","title":"record()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.search_words","text":"Search for a sequence of words in a line. Parameters: words \u2013 A list of strings, the words to look for, or a space-separated string. line \u2013 A string, the line to search into. Returns: A MatchObject, or None. Source code in beancount/utils/test_utils.py def search_words ( words , line ): \"\"\"Search for a sequence of words in a line. Args: words: A list of strings, the words to look for, or a space-separated string. line: A string, the line to search into. Returns: A MatchObject, or None. \"\"\" if isinstance ( words , str ): words = words . split () return re . search ( \".*\" . join ( r \"\\b {} \\b\" . format ( word ) for word in words ), line )","title":"search_words()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.skipIfRaises","text":"A context manager (or decorator) that skips a test if an exception is raised. Yields: Nothing, for you to execute the function code. Exceptions: SkipTest \u2013 if the test raised the expected exception. Source code in beancount/utils/test_utils.py @contextlib . contextmanager def skipIfRaises ( * exc_types ): \"\"\"A context manager (or decorator) that skips a test if an exception is raised. Args: exc_type Yields: Nothing, for you to execute the function code. Raises: SkipTest: if the test raised the expected exception. \"\"\" try : yield except exc_types as exception : raise unittest . SkipTest ( exception )","title":"skipIfRaises()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.subprocess_env","text":"Return a dict to use as environment for running subprocesses. Returns: A string, the root directory. Source code in beancount/utils/test_utils.py def subprocess_env (): \"\"\"Return a dict to use as environment for running subprocesses. Returns: A string, the root directory. \"\"\" # Ensure we have locations to invoke our Python executable and our # runnable binaries in the test environment to run subprocesses. binpath = \":\" . join ( [ path . dirname ( sys . executable ), path . join ( find_repository_root ( __file__ ), \"bin\" ), os . environ . get ( \"PATH\" , \"\" ) . strip ( \":\" ), ] ) . strip ( \":\" ) return { \"PATH\" : binpath , \"PYTHONPATH\" : find_python_lib ()}","title":"subprocess_env()"},{"location":"api_reference/beancount.utils.html#beancount.utils.test_utils.tempdir","text":"A context manager that creates a temporary directory and deletes its contents unconditionally once done. Parameters: delete \u2013 A boolean, true if we want to delete the directory after running. **kw \u2013 Keyword arguments for mkdtemp. Yields: A string, the name of the temporary directory created. Source code in beancount/utils/test_utils.py @contextlib . contextmanager def tempdir ( delete = True , ** kw ): \"\"\"A context manager that creates a temporary directory and deletes its contents unconditionally once done. Args: delete: A boolean, true if we want to delete the directory after running. **kw: Keyword arguments for mkdtemp. Yields: A string, the name of the temporary directory created. \"\"\" tempdir = tempfile . mkdtemp ( prefix = \"beancount-test-tmpdir.\" , ** kw ) try : yield tempdir finally : if delete : shutil . rmtree ( tempdir , ignore_errors = True )","title":"tempdir()"}]} \ No newline at end of file diff --git a/settlement_dates_in_beancount.html b/settlement_dates_in_beancount.html index acf855ee..c86f3699 100644 --- a/settlement_dates_in_beancount.html +++ b/settlement_dates_in_beancount.html @@ -105,6 +105,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -182,8 +184,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -192,20 +192,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/sharing_expenses_with_beancount.html b/sharing_expenses_with_beancount.html index 10995599..e3572a77 100644 --- a/sharing_expenses_with_beancount.html +++ b/sharing_expenses_with_beancount.html @@ -153,6 +153,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -200,8 +202,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -210,20 +210,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • @@ -297,13 +289,13 @@

    A Note About Sharing<

    One of the attributes of the method we show here is that the decision of how we choose to split expenses can be made separately from the actual payment of costs. For example, we may decide in the end that I pay for ⅔rd of the trip, but that can be decided precisely rather than in an ad-hoc “oh, I think I remember I paid for this” manner. This is especially useful in a larger group of people because when expenses aren’t tracked accurately, usually everyone is left with a feeling that they paid more than the others. The sum of the perceived fractions of expenses paid in a group is always greater than 100%...

    Overview of the Method

    In this section we present a brief illustrated overview of the method. A set of common Assets accounts that belong to the project, and book all our individual expenses and transfer for the trip as coming from external Income accounts:

    -

    +

    During the trip, we use the common Assets to make expenses. Most of the expenses are attributed to both of us (and to be shared eventually), but some of the expenses are intended to be attributed to each of us individually:

    -

    +

    After the trip, remaining Assets (like cash we walked home with) gets distributed back to ourselves to zero out the balances of the Assets accounts and we record this through contra postings to Income accounts:

    -

    +

    Finally, the list of shared Expenses are split between each other—using a plugin that forks every posting that is intended to be a shared expense—and the final amount is used to make a final transfer between each other so that we’ve each paid for our respective expenses and we’re square:

    -

    +

    Note that the final balance of expenses for each participant may differ, and these are due to particular expenses that were attributed separately, or if we decide to split the total unevenly.

    How to Track Expenses

    In order to write down all expenses for this trip we will open a brand new Beancount input file. Despite the fact that the expenses will come from each person’s personal accounts, it is useful to think of the trip as a special project, as if it were a separate entity, e.g., a company that exists only for the duration of the trip. The example file we wrote for our trip can be found here and should help you follow along this text.

    diff --git a/sharing_expenses_with_beancount/media/132c6d11d6e252da29b6947bcd9d65f56d08370e.png b/sharing_expenses_with_beancount/media/132c6d11d6e252da29b6947bcd9d65f56d08370e.png new file mode 100644 index 0000000000000000000000000000000000000000..bac76125ec105b2f642b07377125dac13ce3971d GIT binary patch literal 8597 zcmcJVhf`BgyY5vKQK^Cg(gY-^)KEiLI?|hfR6&AB3(~s^h7KZ9Lr3XS1nB`GbV7j8 zK}w{D5+M{rJ)3j?fcxFK=ga_;VXxV<*IsXVe$SgoJsmZwoA+*Bx^#(3LtWY6(xuC+ zz~@{Fa^UZsiA(2~E^%6DC_gpwwc5(K(X2hj-YrTaNt6wv%%x4^61u5tkhiO>8yVxJ z^zd6p=__B|G%Jfp<@zvm6#5lE5#~aJ}jou<#*p-8KnWs0Uo{d<-nic>wpgF6J~|){u~pK(w>91tp$sHxceO zpB~J#X37U01sqR4S)2apl#m^CFx8jHs|zX$24^f_6S&ntt)vyiMxk!D{Dfe1e9Osh z|F@Z17X);5KJKJ^Yb!OaQ_nWa4x`UEFX6B&STGZ zd3pK1Pn@WaEkVa;w*Ti5vxAS;pSj|9x;f5g1#d~560^^L_aS}xe71iV3R<_ww#iG- z{-7!}sj%X+3f$=|BD+p~b#zH5Q+hw>Xm4GpXydePf7IiQo1cFZnTM>xURu%e0v(idnT(uYl%(}4Q6c!`-?alj_pxR_V({0v!t7w_=_Qx> zmY^52jb1*~%p$KMg90t~ev#3z3Yk_eL_W~?3<35k9YjDLT4e~oY!5uH4|`|>uXY?M zSP$ExWU!Evl?@C!+@3YDu7|zMJM{BdL6eG=!l^e*6_r&~w8cryLA?a*xBeFd;W$kG z9B?E9sxC9Nv%jt)-!AuE?eit`-WYeG(jf$85?asC9O9NO=0KqAMzt8Gst9{>NAk;u z3DREp{?ns<^OoS#Bj&B?pXS|9z^l%4&3@8uRCB7b${vFB=|>^|aN&Ldhuf#`4-joA zao`hDQALxbD~7pxsL7=kN68}Ld}E(B@>2mDDxAg|%~GWn&t6IqKT&2TI?@y;U|y@W z@w_NkB{}r@<(n&M&Ay_{S92_&f+^f0OIuQS%{jN z+HorHV2RU2xs%_4mTKalsUgY9w^n%-l$~;O|2EHGN*OQw-i!>PnYfXw6b>Q=j!nK8 zU?Q@YMu0I)1#B@&iGv>T5fP@gT^V&aqY@*>R;hBuk2m+$so+X8_3T@RR<~UyE1wPu zW$k0XX*!6OgMgT`j_uVR)z4Fop!m-DY-_laf{%5v#qB*s%%#a3CPA*%VR zu`fDUS0p|3<+`~?5&UL9oa6!yEc~{AOJ$Ri^sb>{sS#hJ@9=qx+4p@h(Q=S@J!I1) zwfZf^YT9*IKTX7*Izjh3pCl5xn|6|V#eP2ccuR5j%LY5Kyzb3d$uUz5$g)r;({e=` zpRPYQ(QtlttSjRCus=<7)p1vzw3KN4)lcv1*b@u4dG0@LMLMl`OOAcRuKN#8|DtHR zQ#A%IExmUb%(5(uUE|gIOms%+=Mvnp&;@tvW@Do!-BjWYw|F+0g6m+iNO=+ay<7AN zKY-+Q0S)d6xM(DBbtV~nd&X7}kK{GCNMfRN0K(W%y|jnuIbLQqxXa=v#Bz2@2sv*@ zrvLC<$67-C_tqX9^kBbgEUpX9{nj$YytMe#P3%t=KonYafxQr(Kt$=Lt#*4gcZWsY z6SBSXWvtv%CcU{Z%l~nD4VYDY8v~;r<^lC8OW1YE`UC_I>2z_}hhC?#dSiWsMyWRLm6gmp+}RUm2COzWz2 zg0ycFjFl|5E*;DYx?>;9A~~m;9w+J)h^2g3j(YVVRtcdVE#{KBU^IqN^d`vX%)ALX# zp@|M>bs~ZS1SIw9dPS98w_vK2$BM=Nu?W1qVI{cKs5meC)|Niq=PZjo4*k5K_^L-> zX6nSh%PH1#(y)bJ_E4;7ocGqW_*g2H-G_*e_P;u#8F^FXM2_G$io5!F@SImf;Gw=4 z$o%R52|B6Lg>MaHG$uDfWD zY@jO0u24;Iw&%t$3fOnSXoXC=?gc39Z%oWTWv8P*)KZv5!y;f(6;?=p84XeBBL|Cw z6??6FiLP_aB+wDOk;&nJs8Mvq6@qsONfpy%#LDtx%N}V+z8QUu@R8%s?Kz~D9AJsj zaHUj3x_fpb#7omXr+X-Z210E_49>H=Iqcg-chpCES#`fGApPEOeKdC|(m_bLug8L6UBtZ}1dz14dsM9hSH;s6LR#2`lsTa{-b;N_Gum*0 z6buSKz(R|kg=wX$M+!-vfTY2vgG>$~F?MEo!{< zTP$L)heYgknr<;X%&F+@hl|)jV%P}Bc4lfVhwPXE%J7H?F`=hTf?Q(6u~F>?C}gFd z!i^2vHQ){-_-lpjcF^l?iu?KPE=j4dW8Sb(cRiVAqIT7~SA`LyNf<)ZGF3^`5iX~- zy-GH%H9a)MEEH^k#CSzS;dWYgz(X;Q(vT1DOqGEwt86>TzAidJQo)X(JDR+~cEWRq z4tQ^+$T662*V4=Og7`55k*+58gQq=`i`V3iH>;19)xtIhVZyxFE*Khn6ZJ9@D_?nyjfT(3}y0o}3rckN4XacN!m6?J+3ONCX`zk!w2j#o#6nQ)p4RXnsd| z(Bzhvc}N;A6#*4MPcS7va1Ps{?R9uU`@OQQGskwT$6vAqZbzOKb(1=^juP>{V3x^( zw7X1`uA9)j46#x%UiB5-N#@*qu$%Wg^|C(oR`dYulUZ$5Oul?|$D852PEa4|%9C{3 z$>V$OK%%k-q0CcHnfpjhx2lWPQU`OEObHURaUa@XT1(x&=9M;WtVfy4+-@#eG@49Y zoLm;wJ|CDnCSbnXC!m{>i&3>{YmoycE0@kcA4srD*!wXLvrjL(Kx#rIu)$njMBB&# zm}yqRps%FgjyS7Cy5GyGws$N?{gmO<5-hb#!|SOOoDa#L$Oq6tEz8G0A*6&_;Gqd= zMIY_2w>D&fnYhUM&>(PuQLXcoQUNtLNQ|sEY@RX@vrMIO^c;>#pJa77h?oo$C944h zMnJEfU7BYgWw_XelZPdUtY4A*jy*IeZr!hL-J5|Dt~AY`omQVpJ{XN;tL6Y|#mBZE z@G3@Y6z2f@O~<25hi;F5f1-u8+4{Ewwh?dmb$IQ+> z3Gw;#e!>*O99)zhm^_u}6+N2lUG>GTuLERDBUNUah*m^f7)45h&T?PUp)QDOvckTRx0_yGM6Ob3QPnGSr*E5hT_ zqD(jN_wu`{zOPUcYDLz*`M%%EiDtT-8b;h2(X7HYH<(Eg<1H$$7RTBU!FVeq<{5K# zwbb}LhvKdSSF;#%zC{Z7S5=HxUu3O1{ew?k>KfG*%IV(}R2^G>HxyrGiCfzq&Jg_A&Mv(~@nPY;)XPD7&L4Aelb4^LshJ|U{v%&49`9d7*j?_QGxn?Z-N|!h>E4=c znh^9Bw5ZRS>G49&M7~ca*2hWmnUs%`y5l(xm)vDtl)3^#&QEvphdYS=!s_VW(Q&cRrhiA z`^nicA)pUXaf`18(v4U1Go(DEStMOY>1&LN4YE!~iw!sInQcT^9r_?~k!N!O1i#&w z@d|4K5KFTf2#N2!6#%wjaHw!Omv_a6h0*}Q?6M}x`n`4d_ZOG7J5}u<1NXm12{AXjl-V<2F;PU1)anDHwn;ud6o64D`G@qa)J?x_6RAJ zqMI9C=sfkKQC3#gA|-%;6JGBveettI6nF~6HYeSF;Qp8ihQ+NJ_dq+{C|hZ1s5K=uO|<>lphK2T|3yR64SO1-DaQ@t9BV@gCZ#OUt`AsUuE%BEg{vV-Wi(|%|pB-T&v ziN9GB8@@gK39OLpw0F5*66-FW0n$!i6Dx4O|KrL*k~7&Y?+%#710%}t-dtBTmdN)) z8p<^MlBR+{%5^`|%Xv3a>tc@7=-b(xs+2^L^Mg7K;DLEpla-+BzeJ+mViq^(E7g>k z>FD&|fte3v>s7aZH|+n6rDH;i|C``HriCjpBrAjjXF>DLwkxs+Ny69g4i)&V zy7_UEvvM^JiU62WzRBmOtrhYYKr6KV$vEAxRm}=fJI|w zsjB>IMIccYz?$iyVA3+9rE1M+y}RA1addBws)*NJAu9`Ay%s%yKl(=QqDqWQJ((w* zW*dJJk2WecByPX|Rc_g|XGIH&D24@1qu8msq5a9)*74t1R%v;5defzW0$V8Q16ClT zy9*5i|A@DbI|VlANg|_>%^6u4ynIi>xoAY;>|ef)-G@Ey6>S9@BaYyIxh>bX4P!YS zc9(jyHm=EBWY}Y%^SoCC_SgZt+QtZGWdxkJtJc67DO0M^2GXJ2>A z!IsoAThFGWI3_Pr^4YQwmcj*3Tm#I)Flt*!q?Ij1aqa&9;y((Q z_BCqKuv>-icM8Be2+aQOBlA)0-Wh1Q@RL{q3tf|b0Z_NCIQ7y#N(W&W?ycU?btE7 zI{NQL&=qfa`{si*rk?{9-RNFLaQ43FuyU0XQY6h4KX2=O4hsA0t{-9of*p{fU}`2a z@iS9huA#JC8BAcVe31|K!D!B*qd8`2v5!Hg=EH!YB-ZC^Wt6xoBkrejoMxJL?(`jB zO7kUZ@|209ojbqeu_$D@`~H<;zOC-<*Ke%NDZtb;?12?8@aRLk+n4pxpE&bR1@iwW z0sAp5)iBmwH~(fB4EjA=WZV}sZ`l9sgLg>7`zTL#;{sTFy#7_il$qGs;h~lS+Puw; z384WTrxJ7qN_UCMqtQqAjWxlWf`&%-+uYJ%N$){yx+m|cR)^lW&@)1f>ulq@S)q_? zIJ^A5haQkW-NLSFjC1!G_cMY7I6ETm3Ev+}h=_O;ti}B@DuQVNqI3IL`u*3Zq2!N} zNV4C>6tz1RgF&w!zAkQTtwTtJ6*E zwZFUQDje^N?-|NPZZ*!TYNHt*97UO|(^ZI_9RuzE?)k)B8lj4WP2+LJ#Z6EuLH|^tki9V^=mkNo} zRqG|#E8ip$;!epkE!iFF$5q4EzBq2~**$O{zCn^K~aEYXk1J$MhC8xwNut|H$^y{Q4yPEgzesf*_u* zN$_dC8goyGvtdJI`D5hE?CeRs1YPX-cXJcN#K2G3%L<@{T0y=6W6gr(61J(WgKONG zs#`tfTCb2Cb~}ZkZm~1^?d3|evaE3t|0d(xd^e(Ad_Y388LvLjr$2YMzjT^(Kmvz9 zS^peW9&9x6raai;Ckx$8&xqeCW@){T708JpvGY}gEq57?%E3EfEP4gl&iLV+el;#G zs~_<+szx0|@sl{v^ZjsU zm7-l+e`PlrD=t*sKr8C`TJE@B?+LRC&$kwVigQVg+O*$Vj9&`)kkd7eBQf5YK4h7y z-)|YN>{i=Ln4{Fxc={?_6uHz?L>tRSrlS3fB~*$v ze0_cWkAFAO{cL`guLWJnhE})C`ZT?r?5247#ME>WU1nB&=3i(Gv2K|q5tkx=5DDaW z1YUSL0ZpD}?7R7)G9VniSJ7!6nL#lRH!7xn*dGg60o!cBo{+0O7k; zv@(|iw(FVu9uF(vmF^ROP00W|JRGfhyZmewU@)vbpxV6wfy=FH z227zcjtQ;v>+`Lx^mp$T#21^_l}lWSh>7t8%-|9tP`Eali5QtiO0SLa0S}!7E|m;m z6L=3(^6^rWLHch=kB$4xe!fXsO^5wmcv=WFFAOl5*>E%=%pv8LN1sZOzA*Be4ov~G zqwRRWM_&tgprDz|-^gL}>)qw7TA;HJFw6Q@7Ru+*?-pvM<&C(tkP37R8B-79*yW~s zllW53w6ob1f`6yU1=J>Tt5FZ0=FoAgzD_K*LN@XehSfN{09S9R0cv~Erauh8q8Np? zJqdG`PCcJE8!1uh>0-rM6vUN*k?sqBs}apa?SHbNr`MO9WIWo_Gd~lYBvD08VC7 zGK)Zf6CPI`1z^DPdA=&us`dF%3bBVf9&&uJ)fAi`vfIrO0s**jd}X{qn}rtu3Z2gn zG%_wU?QY1)B1a?jwA$Ch-yVbOuU8l26~;*xgd`KI;X{wjNwCA_WL4Jj!oL`1QT$%m z4SLTYKvpt5GFS^1UJcplWW)fJm)Ux{1Jhl{--l~e>X}N(;3*GB2+Q+c@mH@ z2{xjvZb^cc(}tMy`l#m3sp<`lwi)&q9D~3Mqr%8bZ&%R8L_p6k2A+{tk9-!pVlzp^ zRT*Hufn5ix;KpJsPLD=*f7DP~F7O|06`1HvI~x>e74x!dK2QP>ZqHroHC~)PSu$`i zOKzXg%^skoi$**>4LI0z?~3OjFz`Ls%D#Y^*B5i#T#L;mLtJFc$ls{=XMUK=3dzQ?<-YcqE>Au({#U}0fH)Hkt1s2-H;>yhHbPBa!wS|3j zYdsw&*6OARX7bJ}E7m3pm>1+T+F29qWb&rwCGefqS&h$2_O1UT{mCb_PV z?omfV8pA`qC#x)rVyEo}l)$5MtHq730|xkd4Hy~%N1Yh~=^*P;Vpd%c|A_bbh1j2@ z?4Cn`g@+=YQ#1H^(JSRH5dKiKJUM+GFj_s5?7w99sz+<`!i&32j;*j(U|YX0;cMKA zU%bn2x?osw2ogM&faXcLLYBE8%Xf`zU0|1E)?;V39? z;%LN;U*y8}$~@4J4yHuFO?3e1d#~k&(0(cbDZF>)+0FwBImlK}q`F9*J0D4?9; z#kO9BxB%2~WPNnpdVX3lXPBQcaM*D{dsh*-Nn5UJ=Kd^l>-P~h>V^`2yRYJLm6!W( zhs~$DSrdJSgEn1n=%c>OqUcd)8Y!a2`2Cbg|g(V%eJG=Ww$G&B>>#hNR? zBs?uqKFEL2FZH^0K=VriT*463Vw4Ed-gC2q@beCkOa9@b>;GBpFo@^jnc&Wj=vEeB zx)h+ZL{y2Tf|3QPy;5gpX-#EqU3>#k7M-)6tG|orTJbLe<}{5#k!%2D+_;O8mg4A>lVO*ra;4CEkTufc{q33K=F8Al>ns(qv5O~Bo6ehp&3ibJuN93Po za7fiat3=?>e994>hfJJal~Ob-?699cH%tZfl*;sD@2!GB|aZ0AIJb Oq@kjtT&`&I;lBXBTbK#HJ%G=b226AZmcQ>3VL=@ME%dY2vu zgixd=NC};H`<(NBbN)Q@&6~+2nLTUod*5xX>v!$MJT%avrnpH#L_|cbqpkjkh=>>p z{DzZ}0G~eARv|=0ys*pEf=m(NcV?&P5#g$ds%j5cPn&) zKa!EN`1;`#ocnrR<<8R6?w7l&QI4E`a&H(agh=2qGdUcmU47ShD_pa%9Ni=_wkfWW zSQa=tsq+I0H_Uvm8>)?Y(&BFK1*XBV)V~JH&^>6g(wHA=v)llESZ4uRHw#TjpH@zM zdxBkJB8GS(Ms|bX>9TL=#(Brg!kj!|hf6$GojgSUt8G?ssh^`fwDEl0~O# zpBSF2KZ3E#PU_YwbXIw7!1%f;8FXG0aj7UvJryJ?3@zaZien*}9&wAqqTug2g53SZ zzHl#1_w5RIcR#W-R4~w+5jmC(+I(VR4l`3w&Xh+Fy5-s!cXz=#IauU(GQMeL95)jf zd)xb)@6wamT_wX&!xu>Hz_RFUZHi)Y@YkqBc)UW(%8?_8t^G=P@(rX_H#2-qFiE-V z*hOAB`E|E4lKm?-NLI}IY^16Ys{rrka4g)o{-W%voBHqXpB}mTzXoNd-Q@|N=bSQ} zA-$DE|5n;DK^uCxGVW%C>?e}ns@Uipj4Pt?p|Usbj%UgPS?T6j_@xV*NQ!Eqs9 z2-)iH9^vn0vZqN%Xa*h)-}~{&(CX@=tIR9QAOt2US^MP2xLZ(nQOq*#e*QsfOz2ly z-k=xA)1Vkz+XucSIX0=C-*5p0b@!*g#1pfB>zEOT6G$CXRLhb5=`O8kPq;^uJbb7-oA zr^CY*Lr@bw>0QQlgXzOVUl7|&a4R1ox|Les(^h*p3s2DzF}tN3CT9uuV1xCofh`Nv zy*7KeZ-58G^oMIrPR!L`DsdnTlMpRP6M(>)cQbNq${+p>>!a5tVb<+s=UfxKIFS#j z;H)eu+*%?7H=&$rDpC5&gI(`df+Ehegb>}gF8WYi5$POvvr7DT??LmtbM;|z@x$Xi zGm|>0IH~D-MKLZm63Vj zyUus*WD2)R7<+>d#3rFFnXhelJl&kEru1&9AnQ+UlQo6d8&ndH1D-y(;g9o^uhYLp z;3A`#wv?588pW!TC{NRV>ns9G8ne7SycVBcym7>lv}tTGLv&fKbML=RD*t8f`TqzD zAJhE#j>*rZdJxi~F3v7&Ts{ZhQ8220$v*gcuE83Cc#wMM>e5=QbvDzTC%>5pIqqSc zUelN;p?9+}){vc+)1AjlEs{-k{ST@?NIT*-rYQRzC%#E51-1xvt#=YmM_l@tZXvA2 z20(~J?D>nvaN~gp1~gR;=pAn?9h#~71EkDxg#jwg@&G3J_MK3qh$;+IA(RLz+Eu~(j+Y<7Ca)o4nY~bkIn*I{*%RTW}PrJ-!--8o`MUz)^*L(IUeyDL0VkONi(xqv;F33FS z{D7|t*SMDZV&|#4eU9hk5e${I#Av7}?l5KHox=^5DJRE7?EQF~$#Ge>tD!PC%gCn^#)pCv60w@xWS?k~o9aO>IFt60%G6Z1y*m7kLKLn2<|3JxP5 z-wb(c=J@~VRdM;=E6~p!c!X&u%zH14z?vos!)4TpZEeJmO}4#Bs(dNPjWjWf=LI7_ zQJvf7n{nr-1l&&c>+8eSQ&7LbmZPQEpy|NVZI*0TBzf|ZbL_hxbQ`I{#>_(fZ)x2y z5yH<(O$u|WDlbAa%ej+u?ACI;FWxWcHdiyHo4fbk!TIY`;7V5qjm5YW>I~0hJYEasR zH@(BgT=}Hh0~&BJaRITK@3$ELZr5KS)J?oO{#CM2^Zm^|6Xc^VrC)G&j}f1}RZsIu zs}RdWY7W^-RE%J*?5kh;G8P6+W%X!ZqmpN3X9pks;MaFTi>K~M}KmUNV6FLhdndb9U6+CJCkCC~8dR#cNWZg}e^)uWS<^2<4o z;Q$-Y0ugQD5iKB#j?nzDKc1P5F#n81iPX+Oyx&yGpq7xE=wG%csowXvjf{cDMp_>_59nmGvcN6%%hGF3Pi@v3Cfe#xcbae_dq0PjPq$(p8_3z^@2#aC zy%B;RxeR`gUK{fQ5}=s@MeoMkF4@_4XH;Etl|C$Ud6%t^P6_lV>u7rxSY0nF!f50j zx=7fa@PaGA)-^k^Sc;!pOEredz2j-GHPuPkJewmhLJkz!tp~)jRe}PecCp>wn4xo zLm~o(sPlYs19&1BN*kN)7&yD?+NcJ~JYrIh$ z67$rN>xWcn@t@)MyDeqztz&_rB8YTb*G#ltC-a9KOFlwerAf)PiM)|7<0A zl;&>U^+6R3pME?KCujU2yI_Wtrk4=#*jVu2)5q06EqQj3oS${A+44gkw`&`*`Yb|EGw<0og&jhGLRUAi^<`ou zu8{c&blq%^oSAFNQL{A2maUSUs=q+27iDQ$@$Hau`{Up4Fa=>7u{ekS-JANb7`jS& zoh|#kfJ1iZw;1A4W0)vNvM(?2WIbeBoq9T&qmCRrDsc#;_s#T|(J~Nv3dm5TGrb7h z;D*zbACT}!je@Z6W#C`T>UY#Zx3UxEGqy{vTw||79@Cl5dgIXZDx_DUo#7NQzPGEj zL&-Q$pd9lELWT`|S5>zr-Z{(W4MSFNz-C!J%^F)Ty^UGt!D1LK4KKy_cJ(Lqt{BzU zTXuK5@@UJc|JD#M(P!gNGVmaS>3!bA8T-8L>qBFAcFk{<5cN4rJ!{Pj=uZnXZ zA#Lqr)B=bC+MZr_3x#-(=mdQjxk2>e_AwcU5ba}XKu}JB=y>rwm%64=G@LEV9M3Gf zx%{?lQC3ARP6x&rxKgpd1(%<_n!KX~G#HwIG^5CrA?T*=ysV$e_uv&QdXKdwUZqEl_Mzm}FY+AMc z2dVX_!Iu<}HOVg?Oi&q~0OhiP)5GS zZdzu}wL{zK*%PuDeZ141$f+vrWF7LX!ukVtkS)?N>oE?o2PbsAp7?g$OQTYGcm^+9 zT&fA?F<^YSlm78a$SnX5rc8uWH8QGj)=;PhPi-T3Ds*e@NURnA|vJL4?OF}2x3qufgl9132g@eXb+ca2|f z>o4;hJH#@_$bZ!u&p6)RjB-LcE?I>kY`WpfKdgPC&b=ej`Gp`Q!FaaqJ>|48=ESwW zT7E>eS&d1R(`-##vbg}W7OZOBW{X+$dBGk&K?v@yD@w&FzyH`N`~%@R(5ScekfJ{+ zGiE4LvX~Dye@Lwd>ur86MeSo)+9nIZ)Yg!c-0Pg2eaQ%Y#FFyM@>!icQsLED!BB*= zZJN}-rZQF-F)e!+qS|eL+sOo@UH7CE+I*s@>kgTs;#1x6xC^dVc3u3ox>QMh~&3k^vV)KT&aPLt(W?jF{+yv-W%9XXm z$AaDO8meY=Y+cdhI5y<{pXD00ra6_tX?`m@JEAl?_H`*oi=B03a5={qLPz|4e)aD>`*NA8)z(?YBX@Z=ei40?J22J3oMpLS|S zG%%_=A!imarL-Ss$_E9~tUM_mmjI^q6h&=AP|a?%ROTqw?+70;b9UsZ*>LX93xmc6 zWWQe>MAOWat}rs7tEnbc-z_3t;@TWq#r9F1{`0KLoK|=jO$1BU*;a90;6~nUuH9K> zlI4D~*kGU^HRsfR@Ckb8seEYft-$l7qJK&eAGu<~`sKL_ASF#MunRSattR_=LP5|shyIm#r| z|3>cZyojDh7k~P}w3UwRGj|KJrO21MaDy4*9YHPX+*VPo^cT#-wt0gQw$+g}6vK8? zHTK-f{tZE0ylRn0r}y+Koj=}OR|EM9&7ASdvCo=4@-U$FF$k!jla-1SOs71UpmF#qktz*q=~D`3`lgZBeo` z*`ushWI5{f98xk>_=fy56Sq+dN%?<=EqW!^@eyndaejzoqGZ0&vmN{Y4nKgMznmTa zTB3fIFZ5U9SsyQbU_dXuDT-N%`Uj5Ez<}aEFVl)=IQ2eX`l0nVInejBu*9?N9UxzH zkT7*AY)oWrcGd*;%nW|8U}rJ=yr9?rhx>wF*0~k}Po4LB_~O-pY@=IhHxm+G z{}zvpSJ4C{g(*T7Jksc~_8X87v1uZv3>D99dMGgg01D&-3cU;ooGgldo71ts{)-;{EoJ14YWzK8m4*E+0^74E*|k4!d@?_EGC;1JktEL>p-1<0cy zpvn8M$ia+`bFCO)%-CIi7%Tx;7E2?$7z}eG*2{v3&V%D(=>X|m3cftqte{L`Vd2w) z@3(ipV-g;-V-kM0A9y~h#eH3LO5~3!%ur!l>T=P|AX9=LxU+Wdpfh^s=7Y&mKzU>H zJS&awyLK93XT0=G`v#b^dxxKe%-t z-lo^+H1qwfT=({D?VhBRCv)l_!rrh-eBkA|w@c8;da+UM>xr}Kzb1>(>EZTgmCLhV zQ`DUD0CyjK+U>I$mBVA1^0g8RgkN z8f1aOcyB_SOnz`Ik{*o#!+^t$;(;$M0J#a>WE|_ab2)R}`);GXiCgI5Q4e9{+Ej!& z4?|6i-I<3=Ty?z4g{+d-Jsi6E+@!0Ottn3Hr4s zd@jKLS8S?oB^V=7-=3CSS&?l61`fCRTak;aTch;%L-)_)6Q!n-&jA=10}Gl{s2nYZ zt-G55$oQ62^7KsuCKAn7nrk`(l83s(#OT)tG9{Zw%wd>_>l<*TLzAFZ#F-6TuIS!8 zxnTBUh4v5r))~)9s z+&ivh91Jn#b`owC4Eq4L2(w~=Fb4*_QZ3$_KVgZc|o}JVBRx!ESD_e#iDhonRrkE zL?F)C+%2Nw>f9zkP3c^*;}Ooho>qYGLZ~|yy-!zS6br{ZGjm=!Z7cxhrT;1)6&)|} zTN03Zcb4mJXb3qNd;+w!pU8En$?kk(FjKNSFZR8S_^G(Z3oKb1$Igfv?ZMQn$FNfK zIl#?FzKpkDs)1-;8n?L^!S^ok9kio>k+Qi>t8zB7mLR)o$6z-yc}-djL8cf+RkezC zQ>0@--wz`>-gABHF)J%e01!9&hc|AA4>T(|5=;(O+^Xz2yCxHOcJu|XM_Q;bTa#t% zLyOM&>Wa_JvV@#55v$k{0Ut(^X91`;O=Xt3 zsK^o^ZNjF?Ek1vS%lDxGQz*AWa4kLn*wa2$%Suj_?Ig+|DNNylOY6B84mEwmVzV3z zZoxxrf(fcpP6XRlRD+uhH>Z1RoDt|gqkbp{$g_)iB{MwzV+>jH$B4b?%KI;nv3YK- zdSlmariioQr@~q23*u5Y02W>BR$QC@HXfQV?}ZvHW?)q-P!b9&rP}pL(1BI$26cgP zSDJ3^?fQTbZ5)&ml$l2vc!sK*-=2J3>UR~&o|aEo>B;d%n=Wc(;m4Tgd^gL`tBO{M zcaf^$4m4`eRQG&nxnmd!yMrgj{4(&*kIw1v>)X$M@F46N8=4Q}FLI&9m*3hNmIayy$HavvfP90Nip#Z-k z%D?0ID_j(P`N~p&i5x(?z^gXxCo%)O%mRqCE}PJfRjvmVKKzK?Ct)FW z$)2o|$iOs7V zLV7aOxH7S6SFF7H?E+GXPcJ$mTd1!Nfb@IZ7s6$3y!jK6mRm)ZvceAOzyA<`koNZQ zYjxAslrDP)$iVN~U!)$+BC~E!5Sjd8%o&YzNaYL!9vbaBJ3R}r0{j7k*3-(o-mmry zXwmw)vGghm;%IJMwnQ|Tx4;XGk><4?>>NBr13NN6LEr#6X4QxcPj4d=U1S|?-uPC6 z7=Dq3|KmyfO10}yddfT*{(T$R*&T-5#>Oygpd~|(A4cN_?CU_?z&L`8k(&|PKT&zC z7R4tNJyBB8va#8bbLq-hDul6<8~PK&&ya_FsCCWp_Ekqx)`H-R@B7yR)RlBaccwT>?_#c}N!B28^q(ATP7&%f&eE)7 z6s(Mpen*%nKlb}F;pb0o_84RABD9)gwvN~G^2)0vAZ@|Nw}r{95{3GG!VWydkd7C9 zV_EKLli7v}J5!L3U<(!ua;nm~5C&Lmr}EFDoJd#>Qi*QYx+-SiC7?5|e6wAebUHz9 zPDsBV-Y8gBRX^)I;8y^50qd{|`!e@%Cql*m8ZElnoA;U6$JOok0;+0z^8sa{D8lUznv8*t4SieL7B?0?9-dCP@g{tx zFQn?l!SZG%ek-{wKRg}5kBi<9*RLt~M&!pkeZ<)^kWM0lhJCK+NH-m&%b68S1f2Bm ztwD_SdLd7uDa9Jv>Z^C@D8LQ-W4ZyZL`%qildhL@C^Xf80HW=LwyL%{+Fcm2T+}1n zr8Ab@iZJht7|y4A)kg29er-KSKk6d=`o6C)2*WY=Cc^cf|a1biRNW`m`>b?_R7%WcHT>Q`6=@zEpD z*_4!d)EP^^QtldH1?y{xiU_k2i5SP^MrZgipM*ZW+I-(pQ70 zP<1n6jxaVM{0DsqrwIRFU`!jO3qUMdJJrL=zckz|>;c!jyYqV8-6v(wJsp5YZX)FQ cjhD1?L@30Ecb6i-zcYw*Gz`?sRjoq*2d}pi?*IS* diff --git a/sharing_expenses_with_beancount/media/51e501649abf9acccf147ac47836b82a2d07b8f9.png b/sharing_expenses_with_beancount/media/51e501649abf9acccf147ac47836b82a2d07b8f9.png new file mode 100644 index 0000000000000000000000000000000000000000..1af48918b2d4a21608b93c216d7d45d4f49f1769 GIT binary patch literal 8645 zcmbW7cTiJZyY@jkNLLU6=}nM;NFY>c(nUcj(nSP?Py{5jgc1-0LX{#-5Nse-dT619 zjvyL3hESy>^bX(lIdjguXXg9knKP4QGAnzpz4pECb+7Ap?Zn)>tIJ4#m7a`@j8R`t z+mwuq+z$ATqNN5t10Fksk&y|+>1%5|fZ46*&}3Zd-0P&N5`20|PIR75ZI`PJ2|3o@ z$)xf*$nptP>4(Hpl3%`@Dy%_AFDNd~&(Hr3cqdco+#ESS`q|IxmNhW&l$chs3bU!z=j=oyii|I;52V3AU-LOv~xqF z+wg(DaL(BQOefTs2lV13k5GhKrSXg2F-M6h&#}8 z*mWM_UE$M@a6t()BhPBl1n;7yvL8C15E)Y_W0t_jm-~esY1;M^rQX3n;r?6k*CrHu z2c@p0#yQID=e}?dNpSjH`H@$w$UQAMy9N41S{nV8H_wQdh*un{$Z~SRqEHOH{supV z`cGz#W9EM^*k#ZnE6I`jcd*3pN=_b6ec{L-$n?>6aV(a1_Qi>OQk(-hMxWj>+KBG@12l*s@~o^JoUpuFJ{8^;3>EP9PlZDAl=0 zS(e@0EIkiY`DPURWNT}f)gLZBg&hX>+s|7IvTcwGHZR!p?emnbEsxzwI#Jd8)_n86 z%&x#Z$Ov{0N{Tbm_pJ6Q$+p~WBIf9oK35V+W>J&k#V}anWC}Jv{1vAY=_+r_!#stvmMJV!!NLoco*42YiZ|}%-=OQ{#OQ-2D5_g43 zQ2^nHF@$p?}dF1nOEk0UN73C}XK*qK@gnCrq=ed)^XJ-=<)Q{3fKlx89F zh?gT3XY_>A&Xl!^SAntD4#+8Pl0TAjv$jn*RW*Zjq2rRe13^+6ZN_V2$QvOYvxYG& zP20B0}=&7OkwDgC0Eelny|UnrGPC9(UY`ML;w^L_~Lq-b&w zGbPm;C_CqiR$QJQ?<)tkv(e26?H5~1+gM3czhI`G-CF%|vhyna+yAzx{L9+&e>e;| z(#2uzKJ3=GUc)W@dfc-9#X__9Y|LBvNiTCkR~ELf_`&B95WHEu%c|mW=QZzH3579l zEN@Ff!KWPYr-YdxqN@Cca+P&W$*p3mDyGtAO-s9`z}=P2Z5`RWV$HLWaI;c={AC|U z4tzWhA8nrq`muB>>FM&N`xmW{q|%I^-&Q`GO9KbwBNenhMnCQrL~@=fRasRviKmlZ z*Wbfr<(p~CK4HQz=nl9}HxwW(`Xkw9F6Icn7Vx=bH+B~zxu{9!8FiPnGqICpX8AKU zBRnAw^xE41Cg=NFJW2{hy6ZV2{7A*a=KMfjTek3@t1QHB2km5#++HzN(K`{N7}NWo z&!9cY!O#hH>4vWHua9onss-&~H)mOO-v4ugTe|uQNvjq%J~2O414YEgF3m1XfuW0K zHVxm*PHH=FHYNORwdFyqL$nzY~8XZfB7;Uh6q)GEwb@^FKM* zv?ch7^jLc~`E~3~dY)q$6{ck9kAYC~ZrJJg4nmfUqHAwRsBLzzNBGZ%>>qBvAiQO< zQ!I1CNo>wuXTqX*CmHRnzqzuWYkIsns>Ia$EWMnAbj5L1^=9ProShvLc?o`qS7slK zRw~DzX5A>Zte6{l_Tz&e3b)kDX$Qlv=DeKv+_Am)H;~EkiV4c{6JG6%=W)0VCGN`cbUG9)4J~oo4M(56gnq zSgH6d_CMBbilAZMzz4hRNPNuR`q9Phi9W%<;=A_Z{B)l=w$|GAZw45E7X{IVX;Kom zG$l$rRz`|+=xx!)Q-jrmieA54a|b`0lT?n^YiHLCrR(R9uRr`Er3ge_3_Hnj*-u~; z7^>90w3Taf7zT?H%L)T!C&yDNIa_t%l-|872Lhckz7FU?pDWR=+f ziPm!9xrix1Dp=26GPih;=lq<#*5|9-TM}j!I7?Yl{rqGbYaJkpztOw2#ubys>Su21 zsCsr~zbA2gBS+1@xbQ+VA^4>Ydisy)-|VlHO(7{&97S;LHy>|scWqINDXx}OjTpM{ zeA0-ZcCy;@k~U0N3ponbB6%SoNDkE*wD5gQPWI=7R4(_Ao+ z=ec(d-O8H4iE4NrPCu#o>kwTm;PgczQ_v9Gz|( zp<%!`v6~mPQD62Il}h!D>WKE(jAlY(UrODoG12V-EUO4Sxj1-Z@2Ry7Y&z(zX3G}S zve2I|7G-K;{Z3obC8efC28Pq7Ydp1A51s;fk>}hzR%}sb zVxqkMO@A=YVy-b?ajxa(mxoz~lg0|uz7PrFVjrx~W?v0-5{`mm`h*jP?nN!LCQWpB z;yh-XLuR#@AWyoER?E)C7J}9)bdPN35~h|OTG-0JK1fnmE{t-21H$WBa$ z;wFGfv1B?V%M=kIQ(iu#x4f&wDuNg$0(wJbM5el9S-Yzox zc-K~Bw)R6L=LhiQ>o?dqhO!vJ6@GK^XquWEVR?anK5H!U?Wjr!cSPtjAQqZ<;0O2? ze%LpyP{pCGwM3ti+*O%?$-<%Icc&dO?2SsZ));x#o1#8{fXzJ2n|D}*5$ToeftFx_xj^7(Zm7^|535lbTwC%fNLU)hFqD(T-% zfn_r9RaoT=tBaxs6Q14-U=?xNKHjl9KRa#ujmKu2%uW)wm@?cehJbY&GrB#P?RB?W z?7Q1{bZFG1A;mNxW1oy|vg>0Qg?=2N=Jw+uZxAmBk z&kA@(|6(X)5|~c%G4~G+K57w47I;63k)zRTY^v&8AM^hM8tWz06f7Ns}vk%)^| zQ|~rj>ck+^_fa8)cm)*ZsZM0#80FB1$1vX5TbZflR)N+n0-B=e8ptlNS;Evy9#wzI zTO(1p4H3uarSCG(&fOwkfYV)P(KFSiWQ&&f6Gc;sacE4^GqXtnLGXZ`&e4L@+@bvz z>Ic6m^4P4h{)(bhcd1fRCFSB3yg=Z^vd)_*B_7_%wAb4GW1W}n`eN8l1yEhygs#JA zjwJ@4qF*!GOvwo~u1TL#0=45aXPe>OqG(YjaJRDGpZWKaY5rpf3z${-<0^XEykRw> z6U3D@pwz}1qui(yd%5p}ai3z3^J8x$XO>!*$8dp;5;JqmoOtahrrfc;!AWQojhmw{ zYtnkLh*vJB^&iZJ@Jdi>5`{+2E_g41L@0%mad8vnYi9BGjp;LxK~SSKC{UwFB8@ zI5{j}H;P&V=0|%f1U=kenxbSH_vJ=)TGh%7ltQ;(OQLP$s_`{(515~Qk}R!z92es3 zMQWotrp@w?=cNb1^gJPPK&`2gKM>fpj;Gx*hL6HugmaHPr{pXaLKn7|w+~Fn z>-?7z{K;wuR%8+EBKZ4UPp-8&LMD-aXHSNL_!aI_)a>cQ6;HBpj1qBWqCFPmR z04(jZUmr~>TFj(NNn5#JTv_CuMZAwF(&1~>R7s+e)nKM(NDZfPBm&%;VweX#y z>XS`D{^v%U(YQTXe)~I?l-b6x{B_SUpAr;l0>^43y1ANuO9aiOB*H!ZNdv}0DJc|Q z*x#(&p;6wGXT@DWK9O|HqZ;TNhG=WQc-kvjcJbs-aaka8Vg64Y3JRC4a$@GXS*Q$V z6?&~GrmyYBZJ3yVkz1%@vFZgR-9p*g#2xTO2{GgFy3HWDr~RZn&@jyy-V@o< zDL3xoBQRbf@D<}TF26pm8+Qet=e_(r|4faA-JSc>oukC)Tzw>+loYdAnoZq@NPmx4 zOmaDoFCPuKIzFKrsH*bCegIVj5XTBF%2!rNDqDqU#IaPbUQqAhD+WghF>KY+BLfi} z3YFkKxyl{2Ezv{d=6fI}}^vUsFZK3$z%zu112dF}bXG@b;XE+tr zi{pa-X=bx)$=ur>D%L*vkG7dPj}I{mul}K%G*BrW{`9Lqk?q+3bgn`7AH?b7a(}KWo&t#dpT~Pul&Zp+o!gIQZG&b z16Pk}i3JUQfBIwo1Cjb4!d$?f@XWLQjsHQj%oGw33$_I|J6X>ECMi=`mI9Gm{`oiZ z|7`9TMayN$XQ?zjEbjeZ94CcoguVRn;T7_K9W997CH&Kn=7#tCt^Tba@$%lKDpvKL z%Ucq%oE+GX{|AKq|DPhjezseNh%dHn+eHhZBn??^7`GShBERi{pM881K zW+%W0AZ_nuz)DcNjgd^11bz<=MOuhk0KXrS0iMWT8KAQY6G;Rx6=n|>3EdJs|Pcr`AeLmfLeul z+r2YPmmc;RQsum1RVi#&-Id6R`HPD>@mJ*^MYf#oS)*5!UakE2sOu3IbYGIbFCCOH zQ3K_ccm2+PG=w-~Z`zx18y+b#Bn%i_6w3>)Y^epr2zCko$3RgCyo_OQWC72*x@sqc>d z?Mvd8+b^jJz<_vf>pv|&@g&8#0CBdR7pb+nb{7bX}82!jzrk9L>5kVW3G%opR8 z&MPlalhtlZF0Oy1t}hUQNVA;+Fg~*}(-ice`uW52wMl5L?P$K%t7QOqcKq6-7`OZ6 z+IMmG#L0DFU%L+o{r444*gFbSURpcT{vZDhuE_&=QCPdu0+69L0PCNKWh^^7 zfaDKRAGR-wAK{qU{bdbW|G{z}uj-=`xZ^szhqFiQg4GyFk?_-_fb#%=i<>|uZ*NJ| z1Ak zhv_?5nz`b_7qW9da4i&w(Nu!-NmfN4tdQ^Jm?I5>cVcq;`_QOW9?L@?P>Bq%W>1%U z#PXzMI1DcYh@b%qy?zfx!2@J;Gis24_qOSH%_O2=U|aqCX93_<6x!ddyYyGZk1Uh} zV7$WgbZ;8qAGDmMYE`9$hh;l==9M8pq zj+8pMj!UWr4H!-(%g}76C)-2K>KGqarxZOK7zm)h3pmx&ogYmhilZ=IxCK^wT0jv{8|vMmNITRtpKKV+d!fEhy0NgYUbl(|S~cKXD=BfDnI{3#|jaUw&; z4nV+WCa*FY93Me+_|9?1vnAa#HD0SJ30_^oXl+`<#(=V z7G6(GCj+%88odmx#~-?ZW85Bfj<;L}c@WZIROcGDKQdm3%%`rMS$)dh4e-8p_vD6D z2&=}08H_QC8MfptI*`HR)BRapoUMOO?PRSd(MOMG`)15dSGVx1oOYDC8c~)$M8#{s zGWgvVaT=i50DS@nVqVAl-J(!Fog>V zJj@6<=a4ezzO=K6F%_}j@N%oJVG~6q3c+h%zZ$33kTkcwT~p9>6vfSeYcvBIg0}xe zrqiLVYdwaCb@|rUw};ecaVgtaz^Z9%vfR4%%N`NJ$NH8uLIiT@)jrhyBvy6{K*PW+ z+14O|t2IHDv(Tx!iUg*5`!4aMcQK+GOi4;kv0FwM!|&Qi{qL9n?Xh<<_|Bm^d(meX z7<)4=9=Ks*X-&Hfb|aY8NKUyv?hl`9bYBZv!<*5?35;Az)i94bQ0W#%j~%EJ(*471 zlMfHjoOTWZdnG=?vQiD63D`%b-y*yJ zrurEG9!ECpw@v9nN8@d2SaKX^feolR&qNVV=_wQ}if$w}2OR}+e69=^R&)s0{ge?+ zCqtH`0KINrk}*T7{JC_nN%+x>QJn>L6}2V@2a)u?r{2LT86-PEU&50Y53i)eiJ4v< zjr>e~v;p{os6n;kwQr!5*Oa1Ym6osyhg0rF7z4YVAwjmy(T7!wwAmiKMgy@jo3i+78^$bkU&Zin z%ci40#m#C{hs+9Xui5O<2vT{Lf}0W-{j|%|ZkE5~N0*Asc?YK5Rs^988VDnXpj}ih zqfM-MxOlq*MZ_nh2K_R~(B!-BXw~Q(e4ve<-Od4`)i*h={jdcd;%VLCDUnu3!6AYM zs`M5(+=KS`^R2`(OdowhAp1CjbREB~i?e$u`L>xkORM1hv~B9uB^xB}nXcT#)G$yjlanfM(ty}4)UkTIrMd?41?R%8x8|&7-7DNRha)K+AO$xrikt{9f&9~MO*cT!m0gF z4^9q0E@rJm2MREpVO^oia@BuGzNcBfsT`aMV)STO5w(8>swxmrsx}b>z{?qNF6xna z;WMDNVELXd1c6I-UgOT`hrh`>BGW742I^{?xH61kVY_^f-GW#b%wavoNfvP_g%CtQ zeKbR!j=)H&fVL=l(Nz(5*}~>N|Epdx5c+mUgh@s8q7FC`CM?0jt_ z%_{F_Gi$D1s7&G%4d_S$$*eW89gwIf$PK_9Sc>NUka12f0ZaM&xDUqMo+-1=T8YPx zi=z0=JnaKzUY4)yvlwXyJvY8?RGDZMh43wh+35{;5A`vu# zW|ODv-d@FBTF<(AQQGl;xWf4n%qmc*Iqm&rWWha-$n;SP_%(Q?NGde-8U8YIu0V>u ztb79ikADLun)TuTns7L0p8x#92t4Ln^!-azzyqG7zsU5k*!)xpctV&)IXrbv!~JS< WU99*c7WjJxnZC|l?NUvL@c#!|d*yxr literal 0 HcmV?d00001 diff --git a/sharing_expenses_with_beancount/media/6497688e752bd90be8d7257afa678cc1fd1d8084.png b/sharing_expenses_with_beancount/media/6497688e752bd90be8d7257afa678cc1fd1d8084.png deleted file mode 100644 index 3bb105459f3b9aaaf61ae644e49eefa44a0fc19e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8596 zcmcJV2T)Vrx9`P*h@r_(se*ulC?cVR&{c}`&_s}4Bfa;gpfrI<5kd>noAf4K2oR(s zROx|82~~RNaF6%?@64Tf^XA=|ckc|7ft)jEpS}0mYkj`oMTCa>ODalc%1f6nQ7I|P zXQ!oqih%<7{{;&6^HCYxxDUA?ePf( zl7qwaOlzSU$`P-BYV_=}z2>Cjm$#O;MD%YHztu{jLT&LW$t;Va9KBfl85D~SN(zvtO%di1e zPf%Qd(MWFnAe+*2yo#A5ev(u0j%JKm&bx)CyQXXo&4NA%<)TdJaF%mX2EF{}VFV%k zcLHO~Lk-}1Coh;)^XvUku1Ia*#%D^v^?cyQ_i?~=b>PO!55Q?tUz3I4*oer;6b2R+ ziHNt%Fx@n$3#aD(C&JYvYIbSKmHjaj6H$lB zYB5j!7kjf8=U&^tN(}NRkLt4ISg>hgZh`g_m5ZV0XD3fJGHfhzCbNjJw--7h#gPV(o+w-%jr!{tC>UFqJWx%*Xu}NdV+QCzryzoBo zXy1R-Ghcj+nZoeM0TY#;1qQY9JTG}T7`nTSVK1A@WAj_DnBAx}9hSK`!abS(ROd>> z2J8&ws?e`nT;Z5<_T68tIsM3B9FA`d3VNpYXW?i3tW{6KN>c-#-4G^4lo7P-c6K@V zGb1yO!K(uAsat9&Ci{vw5ZK6v+*?dC;a`v*ct>_65$Vre`IB8H@YMgL>CpDibiIrg zMjIw|+hV8IVG2^?IMW~z3Sz44mX?-o0G0xAn@?}cyVCM!nqj?bdc(<|#;2-C0W(y9 zq4_@o=!x$x^TO?(t=Edj&=v8UUojrg2*7ac}p(#=xw62bQP1_FpL zDRg5@f2t^KW4eB~?5>38X@6tXd7d#BD=9tJ8zPzLOf1S`>5EbR*uM>kwm$F>%kUC!ee!B%D}04f2}R# zI?uNM==hsX#$C4Ja4?oJ?{1WAJkAXf6=i7Il~za4DblgY5G#H0k#cXvBUEX3f^jR(N7cy_;HJfSNa3Rg2~997Rd_l|ubU-*BhO-!5B2Rpo{{BX3K@iF>T} z<_WO~zb)@i+x`9NsiV+Qfoihg^`H46Us8=lj#L9x-&Z=d94#^IB&@ORm)6;i7Wpwn zaU12Ur>8H8uci#*$Lcp*PSgdwAKs*)k&(y0WDls06Q+r*W)5Fl_mh)k99?mDTSfKqV+%$x^*ttFTUT>K+7ZjsB;~tTz}5k} zf83og_*7M{Cq=}rHbQc^uKK~NBAq0aFONLJrAa9J?N?d?X~2%FbC+=9w1E(mK|3up z+aK-?!;)Oe^7w!tAR<`BcBK3)SB7$T_ZXZUx%r3!{N3|Mi`}tk?Wgm|1~qXfd=pRq z?z}kO;)Q%Pg8*5N?O5quS$sT9nn`w&F^1VIUo33-R=g!=D%b+mC?)fjXC6@r?CDeP zGT@Xp4(BLkZ*H&TXW&-?B!7Q2H_lq@`7&(rF6q_^5cO=zD9L&_3arZu=Y@%@Bm%>8 z9Diuh!0jmZB=DAP{jSb{ncsFsn{m>*RNIc)Ua=wZJS3K=qear z<-VM*c(=(28uPx(pr4UCKC-ean#JjX8YWpp+J^j}F*Cuqu1vKY`4otzBX`Bb(@d$H zAg%F(aPQR0X^zOF)#9={L%QVDEahF!v^4SWyIFLbf0E)cbMsH!H=7SuA5aZadURo& z6^q+y!tBf;IY(+&<3;R#x$TXOS6C2nNU%lBalj2(f}vY0umyv-kE8+$B^duP;^ANm z=6gZ{C`{>aNad*m6V!R`me2~*O9`((Y36;Q;O8Cff^+R3Z_@X%F$JJM=vXXr?$eSn zp`S~_Imvnyh3e~LdCR0S&jTF7I zWAIt3hb0SI@$ZJN(GoPeJ|zlTK*N(BTSPZ!9Ph6=@gTw@{PHp!EunJX(}45cd^d-w zDnY1JaaWK#T1nIi@6Qz=YZN7Pa&%vpOq6xMRz@x*_ofZo9h+YXIV>W!)uWAUi5d%j zVW~@0UH3&pv+>Y|`TmqwYT^!-A6k~43osR4;N}qmgkN?1a_61%7-uDe7(3iXWkCFf9 zGim3*(a>7X0lG$@rl47j* zn5qPD=_D#<;kOZj`9{|${}~utu#!y%vv$QCoK<||n)Rb0Dul|)$)zNHHDaJkIkH{N zGZbRowfay;Z8N_F{q7~j{le#T8*_kclJwb0yH5kJp}KBy&jEB@lsj4vo)V_GtY#3U zmpXOuuFq6Zh0oyI2`}7v*pohmKA8y(&WmJx+m+B~oDgZR8(XSZ@jGznh6rmPQaC;` zm%bIuxOpPv(U_TI3zf9k5ilFZ*zz*18PS~FTS(mwM)@o;o$YYFAi<$XdK(LLO8t*^uJ`3D;$2zGvWlvjlD&IrJ#YDFI`+ML7)I)}f_C#w>2+yl=$qHP9W>`! z^e4NgMzJ0ceaJ{nk7yx0={pB4p3Z!n7~$%JYq);%N_CWPGlg>RSTyx#$OJ*ousgAJ z5M0;;P%JH7>4ZcZ=oYXLUV-!ZNoD??@tag3l?GArwNM(sxiwd>Ed!|3Q!-dnaikxLVILOBD3&}M4(F+zTF z_U*}JZ%W?ONG()UsETuh%BJks)bq!`x8ewi6s(fQ>@`ifKc*x{b2ud8R1H-_h@JSD z2^xaoZo8>IdR(P|@Y$N?qwpEFYF^BLyOY5eHqd0K+OgsQ&G8}Oqzz%Psg zqx8Ep3`5azgX?18u5q+#0Rj0CwU!+1QkU=+-S++l*ThvWHTP$es+OQoVM4K3SolXZ z6V-?_5M-enGZ-Pscoj)Q7b*sC3%*uW7zXpnaj_U$BHLLR|CZZ7+i&{fL$6`J>tfGI z!IXdAg7zanPJEDkrw@DJAwfB2k(HF%XNiG=?@o4wm}$q_nL*C~42GZakTdLy*pEB2 zT$xQU(?wF4#4&F9Pv)>yW%dTr*|<@sHs=%$kF10dSLjn}$ zM@02AJm;hz!kciJ1RgE+FC@c%_J;hFZygeq^rAl+G?61)_37AwlXn?1`dObM!x?4o zIY`&jC&xidvcjzoFa6N7(yBT&b3+Llf0lGRf6$NOBBqq7;j^eWiRxDs8ksV(>hsH zFWrgDo&b^y10?@{Cv{fMwEr38|IY_aIs@b?AgZ0G`dE%Ls3%Qq*Z;q23@PiY&uRbn ze*RG2ylD^zl!AWF*yR)?JxP-8az_Y_wD$nr)wU~c?I@lX?`Y9=M((5K0tv7#%c(q%`|#R z{EU0xHc}$yv%BO`YTRtgeoMd_n^ssh-0bJo_%MeNV7nIF^APe0d5#Fim`@*42p~p* zXRsK6DprG%o|^p84m{qofuElqVS)OZUIoYb?mRVVS?4oq7)cg&Lb)AonUx8#5~u6k zuuof0fAbkP>9JqufuY=eN$Z7WgDnAmJlxz}_1@cA{1UDU;iX~6{6-B6Mqk1Jpj@tX znFg3~-JE!OwA&DEIM$fXSRjU2^R6=Qq)ZQ59|yzOFvAebzJr6=pfQ`tOO2izuGDNz z5YVr0`ty8x<zI)U432Wf+e#!}_e<7i7Rn-Vj0i{EpyB z(<#*Q!S?_hS(xy?-QPmf`0dn7t0@2YrrH9I1xS3Q8ayv-GX$0YbjJe~ zK+?xmAS8FfAq97yyz$DW!|o@E`))`egroJ`d1h0J@@@zC3>Yi#v`} zo>EGmDwYmo+6X3?#ma352*~}bU9_rl!bgW!SDl_vgveO3Q%_ct+or6L!MNKgKLpaVODm<0p1VBp7F#0 zGIJ*j0b&wdZqhAzBNlK%^59lQe&xyyzIJoRH|cB)CUPF3n09o3G-_OV&zYm)~;fu=~Tsub(;?3V5vQ zq$47nCENXb5}y?Ja&dCH*B`J^$D^8xUKNYa23?#{9efV=Z8_ahA=Zo;yT$RqN^u;n z%d@Sm=g|>9X!7qi+4>D++!)ktkMh&^uZ?}=*{6V=zsf{Cc0G$_2ua15O~RwwMt&9> zHP#AQbb(-j5C8S1`)gwds{Bs-E5k37cC8)pP4&jEu_if+bU6Gqh4=?5chWO>wYi&v zg4@PDI1*{*aL#(a2dJxDj0&|LWJob0C4DL({G71>==)5fjUywx0k&9jQ`gZ+zyfEj z33k5JpXwpK&(!BiBcu{eK(_qgQ@Q|xNqht6Jl%;GlS1)^yCJ0))S6bZ*?lZkuz|PczD{g2SYK!yU0-_JZ-amKz7E z6M#nW2n6EKJ>_l0+ioO#ugLMcMV6IAX7zY(HFsuh2(3QZiE{1xYZB&A{#%bi(F+=N z+0h+8lI~|+Ph{a^SRE;x|L38`51Xml68!x|Z<76Zs@au0@#eg;(wdihIjY=EndHTY z+xs`hvooW@!yo=Yg8bMFxxD^NA5V5sfR>m~?Gghh&>WP#88WTvRdCr49W`LE8{3I8 z^74Mw>;0uvr&WJQ)ZRNes zVs6&W>*8)pQT$w-rn`wsC=okdhqt|nPlm1JRH9i#f0@)EqwMc%!?%BT?q6NJIa;8Z zE$IOMW>~M4(EqcD-RI-8SHd^>h&E>4l8+`-R@CvPieYci>M_tKrd(1Fu$AUqr_vlk>|{WyQO9I@8LD5wurX znZ?~x;^H4tG3|UbGP!3nm?eLd_{2cm>pS=4+0JWvu(-H(qsvr!M^S)|`-4Vx0<}Dd z=cfwjCB@wi=eVX0=MMwV|2My2qmE(z@|+2+V?_%V9Y|h_x_^E=Kd`@Vq;0v71RQC~ zMyVdTN5x8eg+tuANN6<&(8#dEqj-r@@DN#DnJ{mAv$N8yJDCDH1;WN> z((~}Fh~K}&tpsOAT07Uz)j)LfSr?_MbA2A6gqYH5cHk7wkaNgErOi8}dMFYP!C}wV zB;d;fI;4Fkg)r*7L%j1o&(%;)qYGk-aPx@sGLfP(y!IpW1B-j%;gO|9eRDbNArv|M zb?lAJg#iv69Z+HKtr^=wAsj;Ks$7L^XcVa~N|^%4Lu$p`ucUIF64Z~AoxQSj9|;A% ze3C>tdA3OYXvjw{x!pxZfsVq+R@Bo^_9W#+cx9bQt9w`|DA_7i{*rLt3SO!{pxJiP?$dwJ`_Rt!>oqMEQCTjPKuP#7FDVAhmLHTfa zoPbgMojt$f1r5HQ_~McB5yFAng%=`N8E-VUq41hP0N?N}(X?!wc#Z7mNJo$=)3c!5=D3>)(ybhlk3i$IwfLyj_eu^L7iDTS&_ zUx0E5uPM77*>tytt4c7jbBw89WW!s^wl{zz7VIpWP~Vj4KH6)K#ZcdUgOld}M?*nV zsnDs{?m{ijfm?LQC)Y;#!gjKHManbX>vFn6N&nZrMR>1$tz1-$dYsABhx;*+)^AuF zYE9g9pJbz|du8d)*W7cBdlWqn!O3n#_LW-k;o;$`qG5mF*WTWTZ%!CB zNhWR!-P;B&?KB;si+%NY{+l@?9(Sg8-*AgB)NOTy7vS?H@pS2cNogHg#~Xiv`40N8 zm&ZCfy(Vnk$9SSafc&_b31k&@029`;zRCiW5!~;*L&R+gJ4}vb1Mbgc7(M~0Kr??i zKfNR(I@$xEd`q|=H0ZpkD(E6mVyfPEzbK~mbky%?$BGEhsb}pH042vMqUezT(z<@5 zqU;$!_mx#5ex&v7DemJr-HNh$#~JZ6|G#_?PxCT{b&&E7P_FKegNXG+)b7t#*TqO} zpdn!dabMO*f1Rhwlzt94q-ob~-tophHmLn}o(>dY_++&WN?iAw9zpVOE~J4E=;~mv zv-{8S_9i{ea09vmVrM4@UVvQuiYPRwD}`JRkBoG!gyTwghx1{ZP5ZwL8bBUvzxv(O zV>#v10Jn14O^^In@oOA??w3cedi3jEbBQU=b3e%PUlr?OXS`-NY+eH}A+qQRs14J}=`8vkKrwwAE^9q+P}gQa zl497TCVzf;oOG_10kjPIl11zivO6onfRJBpFo?GtL~W2E+7ufU{X*n>vm z7{x52oNrL)Sb`7@m6h58T7;{6fWS)!7SpEcuN*^p^TmQbQr;fIfHMonRQ2#%KznGU zl|5ijQwr>Kbsw1h6q8I~lQtj#1F&NzK_&e+c9no8iH8acCvyzo@`u8ttyRv;&Cu@R zf`VH4VCL{Lf^){!pT^DAJ)n`AH=H-yl%xY}|A+3VJbf>7{y_+gv;8mko#kBJM_E&Re(zpqarBe1U6RD|?#_b@-!X5j04bqlD3=lVcvy~(DU{iHd`U+;~tHXx(Yoj@T$ zN){Ab9rUo}>Z8?zvY<1Se83A|-}rnE#ANBPDXHF@{PrWAfyF&9t-nGEI56YumT}Br zvhhoKQ3W0A2u2G*K%LkRW_`qqa`dOp_!G=5l6J7^5)xOgQ;+z8mx>4a0oPrGB_DZ> z<(C|Eq;rgq#0QOWUA4T?zC%S$#hjDPd~U^yak1XSYyp{j#s5#Nntj!gJ*lJ*b;IwC zVa6iU#$TO-g-<9-k4==Dg%~R*p903dPvV!ZXMpU>8|ITT@g`hr=1~xD39a(pHc!t> zUoZRHrMplGx**D24Eot7R2`xIGqw#>Eg=>Y!*h>m=|DC;fB7EF3Nh6W-eRHGZ}zRp z|0CwMwER>vPn~3P*mqM@DD8G8qoeb!N)jX)gzL@$%_tW@X)lP; z?9SOCMAv5qta~PsY=?3p&K*Eui#Zc>3jPnw>phqHn3{7Id>81qkp#fijdG%rz_yI!t+6imIy4{f-ximY^*z-?|NaIR2#5MuWBV zgoKI^$pQt>#ko+CuNY|k8F+M%NgEGFcf|}-Nh5h8K6EedR5p|cfP!KnbGf1UK3%;g zruwJ80aFuprgcn&oY9mjMF0IGnv3GwkaHa=hm=!@mtb3};hYiQhHW5R!zP$lJM3Pj zx7GkVw00=Vg+NojZg|#F&@1DhvQ$?kMG;EvMB6si&8IpPxNL?6vkz;z3Tn1T@;KVw z)g8b0{H-M9_iMFQ8n;~(XCXzjkJ*4`>jeZl3b_XJ7u%CyZUy6OFYl(0_u9I1b|>jO ziKJ2St{XmJ?lQL>kb{P0q%BLxDThPA&bq0F1D$>j=nm21WX$0@^y2e14n7dltDE-W z@xpfOi<(SNPo5XI(Yk33Ge()l8Z}8%11l(P?&5d$%PV$HzCKe?)Uk8rLX2e<*SwA; z_9UC#(AZC{!KxaSYa6Mh<>;bIUqB z3WSV(p4a+&C)715=@&H5cM8tf>-<#mnqE(;H-}tG;(J(Rjh2#0UWT{DfKV^(Fb#BB z>U-A$T!UuDe}&P-j({rmll)7Nvm*rOwD`ubZ%S_;-_&{WzOEsRv(A2bL!}lP?U*ZR zp6RVU5S1b88(CR9rWc+5#dM&1i{p{mP+tvX%qsRBJZ*z4I_h%Lc0jlvqt>t7MZqss_sx8AsY6p2`PuG%&vu}T&JBSoW;U1&==3gL z-!*5BjxsTyP#>tvfVz2JiN0$m-pnc`Q??CEd^;zRk{DXt#O2TAa$oI@^ds1oBjX=* z?0F;JY|LSJ>>U3zql-o{y8tCQlucMKCaQ&P#H?4I@RL4AV$bU%mrz|-a%LCV1kW_$ zC2iOqsWE|>EYt61@R=}64H=p^&izp|Z>o3N#%p@=o=2lkd#$VS&Crh(D7bza=ZTY& z@bf^NvZ(9J^0o*pcKdJdlR?~Vj@4hwfk97HX1m^0lfpTfA!|aH!YuVYXTjd7B`h_l zO0O&ma;K3VA(jrJo>n{@-@wNv1PQS3-#sgf5k`1J)_hFAg>|lQWIPCzgctPL^eu=F znUCV@R>{}^^vhbP9^`lSt&SKAe(vn*4+-{S8*s562K3|WWF*UAAFWq$lB=~R&$$B~ z`LzonCYGrh@q^+A_K%BuWs>tiZVbNQp2T<1dt2rrgb`&a>0yh#F~m~zScdeF4?RK! zWpxs_wDv3|ldNu_vCpa#Xc$V=bGhTi=}TxbDq&}J`>K$0*~H?D9iM9(@@{|c3b;i| z-fk>?13dk--YknYRbTG`M4Z)D`g>bhmKX+Wj{|x7|7HBm#JTJOSI%-l)5$8^RsO1) zSw;7|!~Xlk@P9H!>c&XCSGVZj%dERp*L@9C+<%m&x%#$fd_;V(m2q5G@z0dlCT}QM zsdsXclR9`+&*;_%&^IY6uf~OCPx@^Qy-{O5H+-2TgaRzoL;@^pL?m$pw@Kn)ktoiH zmnhEU0)!Sv0pb<+|A%I!p)~Hng!=mW50!#dnS$vC#ZN8K`m|&eDQ#GBB)8Xc$CJI# z0p{81Yn(xH2wHtjC!>zx!_KW4#uzX|GM!3aN!dp>_IwW2GnzIrn@7$o zb^?RzDN5JvHX_SCttZ&H=Q)>$zpFlIii5))7RJ93pWtGGdZcKRQ{d>pIFvUVt)37G z$>0kE*m4x=KvaN5DH3%+9;*@kQAN;?eu)BYiacuGVh9f9a2!l#J$$y@Av0o& zJb5VNRXPVCx$!tD@p6msDC{dU8zA>IJoZ&il= zw(&z6L^8jAUXJ?;wzIE1KkT#NGKpcW^{0uG`*vI+BD=#ChQ-tK|C!}zDyQnCw!-CZ zQ}^70k%~nOjii6g&|B`fp7X6h^qgz|TYM1pS3XA6i{mNtb%QqVb&c-mRncclpQS#S zeRwoz8?;>-1+!iH>_-7C6DLjTa@iV?aLJkTF#G;UW;+dq;@PU-f6aEI&wXD}%6?uY z05>b>vv@o{J}$g_>Bp2U?Uk1=?X|45(Cpr%KO<^U|5!WX_8l?5tsnL&p~3v)Me2$8 zuY9+E^2(oYr1MJ*;6g3txF4fF1s<&FYb}iB%1N&eyk?&GJ(}Boukx1f@y^sXm_h97 zj`6GLXo3LTF=oaTH~dzZ!Ln^|wI^&WPeBfUC0FXnG!!`=i(Oxhr;k$DC0TyP|c6MN`>Xy}3BW*Qd7i$|8AC^{ZC* zbSqd^(P|ruoWE{q?HR5;0PD~01-wN`Rp@MQdVL{=hM8FobUD5)+!aSkoj+cf%C^3^ zx7h%!sJ~ll*O)hl{krq{ui>L)R=NGr_fi-#7IEV;U1_VfXPp$MLtS-p>dz_3H{3g? zh0SU#qA{L(s&pxMpIt`Y#~f~s**SD(oGG3JZw#f)35Z)ZIvoIU$bj)Dy?1n% zW#zNDwa*bc=7G6a>x17j3jA+q_0}^ymyTb*Z_zV?F@j)hVM-VEwKMBrzcbvHT)AHE zdl~=TS-S=bd5a&(KGO>hRms>QrGljEm;PU&M|Uyj7MEL^?4iw;I=dt%+4nu?qc4sz z?2TSnF9KcIMIUCWI>17oiZY6j>>~e~{AbhdT$A05r@j*I$a_*hSJqD**yXzI3y1y$ zo7qO^Zz0VO^WCpGe5Ae(N zgElmA-GY#LZHUqP%ah2j>2DFXFVJ^d9uAs)pmv55eaaOut;uTlQP$&*C0|Mjh0|7~ zOJBORcueW7`kd}B1+2x+)L0&7UCL3&BdWu1uC0!}>cQneFl`!6vrzY|YmiN+r<5IGGm z^O&mIdBp8B?XMONqJA@TLa~$q97SP9t-vj$km*iN?HG_ZkG0eJD&q(VId<4yY^3>2 z%w0%NiDvUk4B2ybt(B-%_Y3;AI=>_YJKifFqlHyRU@AN4%2e_c3m(_8LUyG zgm~mMl3_Dd_;BU)7CzzA)?|v1=_kR(nklwXvQDBwEb^y|KAOj^vV6K(^%vE3U8wLS z@gFk>OJTSRPt{MBO)d>VIrgL>H@MRsrqzfHPSx0jzw>$O5b6QfBgheN^8z-dn+k#6 z0vCcfHlq9&d2)D?=019_f6NfI+P^s%WH(>?`iA#GMmGmliFZ(egEG3W1wr|7n@-qF zzVzF#Z<$Il>+4M&spj4+^8G){^~0@3mFmdZR@)xLDsP6 zn4(Z7OOZFb6*!o}o*#n&eOkc&TH=x+21WP;@3qWKN)7n+q_eE`C}`&H1`o>l?KER@ z1C|ZnWHy>LrcHcZANunHl(uJYXaWXBT>hxHSCzu46=2t3Fv8)X`)M|-_AeEuftx|#=_?XRO{Vl93d1uPR|+B zorjS!Q8h$3Z5R3d60|M%DhfUw0SyxwhDf?}#6St6pimA>m?DrnA6AH4weGqj@1{06 ze=94)OGVP_#Z&W+7PI0rIg=uYh#5f61YL>nS7VGX^FvSO25N2E_thZ&gQo|5%KpCI zV{E)RBJU)tvCpiU1=61SK8C;8*&1J460=de@ZFv~8G<$=Bvv_PL-mX#gxXPF>JZj1 zLHl|!P5yh2TJ!}irz*S7))J3lP8MkXm5;x-zx&P+tnZgle$T zOBu!mCxA#4zkOeC^t&iNjS8zMud&EfUb@6~clQgi=$ij1uNNH-se=GOQz2nb=!@|7B)t+Zu;*Tp|%$XI=1!AY6M2H^QAmZ!E?}4T1+K>p21!dpRR%P z5XjjJ1>c&qt)p3OD_AVirfI2-^iiK^`I}Ox8cawruwuC;mfi2ea;9}Ms?G}N~<4kYtE`5KA(K|59rYf~f5?4>6YYuJu_9P55d+QF!6rGt`%6s^gX?GiJsf(RHXVQd<+{ zWByBaQIMM~L_Lv+`g0n;R8X0L#h>1CO;==z#P?Imq#%zw&b=@HHs)Q21Jo!tLEuFYoZEo&)#^8Bj5kQ?BwzF!H zdWVp>HtIT?R{=X9%P`zU1aj_}*MMNGvmDnStz8s9{6>~oFumd{o<L##v&iH4(N2>oER~_rL|*q#q*_W;JF#X8g?x!!$U%Rv1?C2;=8Q^3$?y$ zzlB#++|$LirqyOU4jDr<51KYut-%@4ZosAp7Mri9tpHoJ|31u#mLJz)j|%Ic6%=J1JV5E+!ZqZGw{ z3g_|)mp3^s3}X*18%{IcOt_7XQJ)E8uYz7vBBp9rCO4&Jk4>IWp0ZNV4w2)UY});U zy~6@43dp>W^&gHuu-pzM!>M+sLhe7ECG4m~wNg^HMi>|jfQgU|zhfHdsi&F7GbJwT z_vIMTZ3v^FEh(1pHQSp%;-&pKN#KGn<4FN?u|kdWzo>ix%jPYr%WD-GRB!J=w>W~JwIy5$S&SH8i(~(A*8E3$ z_Y!>k^2u%}pD98X`Zt3VZJxwzzS5=iMs}Ny_0plP0)}>7Y{jNT3(%4i6VQ@-6U37bKuEj5eS~5j{FePRO1T{2LAd#^Z6c{`oF1nlX|J`H<*PUnj0n`-{MK z60@`@F`NLpHm0dT-^!jHO}`W-Z&6}yas_YaxXmIdm`$sfTBnb{%YM=7N^LHhk&BOSX}iI(IGr+1i7MdP(U!`IW9lUQ zW$EEW!LM%<3n_3FJl@=)p$Z;?wFd_G`?lnyYEzNw*3`ETI33E3?{khS=aCb^A$Y<5p69k511v<3i8aWl$)n$=n2LwN5Xdp6kyxF0^ zpUi#ZQ@Sk(UE2z8b|vy52`>Qn!o4U|E1{$ggRnpJrk4WSI3vl}rM4V*xF=*?bW|z- z;_uU@wmVP>zTNd`f>%FFALP?UQ%W<^oOr9;214u=Kdl~RA%f^28b!=SOU{{R0E0)P zIQwWw_+J^oq61aX1XLt(l==cW94ctaHR2c=H31eC`9ee_(SLO&R!9G1OBPvQtsKSB z`DL{s8=zIIf_U@l{E$mEmQ*E=NjAAtrr_y(v-V__>4Seo{YhD>!}ib89eH2{?p`F? z!0^-_sCW0=-@JKveRV$74I*c$_u?AOQxF!j>v~#e<_8!ymX5<>iQD9HsA8(#DfB;uf^ZKeu2Ldr@Xh=6>60u|L&|6z<=Sdd!e^BUtt`b z&;FoygJuFekBW_U|g(FAD*G?Fte)#2EZCm?lDN_k8g z9KZVPhpDP$APj5w-&6bSyKRC43U$5=al6_se9p&mlf}3Z=@k=v(kO|5{lzLpV6t3X zTq?T@ZTh>*omS3f75e##oo>@L?_dG@IhWEe$(ZlI0&2LAk_{>`B=JB#|Fq!f=s0!P zo6T*oHI?{e;w)&=%)$r*XMnJRmnDuJtEY z2VLa>-4c{;t|sIoXvqP;s%XOik%SIlFFNOM8FZQ{Y^G6#LB~7${-j)!QeMl}N0=FX z`Qu4rq3UrD(Ms&R8iMe5&tpYDtIIC!5`0ay^g8}9u0QfwDNo`;1Rw2-+s#*Y0*IH(5Y5v{f zo@4TdNmYPgP~tXG_L}6SV=-Gd2-WYp%RUFHB_4i-nW?MZYY}KhHdp-J_*xuRN56E9 zyX^0ar%?w)nWY)f;OD@DB3akT2To$oTxOfd6Q6)i)J0EQ;33 zzW*WkBBESkKFv>A;Qhh+VD$xmUZ9GIWn-UBiU{bK@$8J$&}kU?RkVRkA*jM1gVcFY z2nZl+fXuibBuDuL&_*Vkbtw=c&-Jq;;GwbiIORtG(Z^Jwkqpzk4G4w^SDjQ5x5bYQ zLfqg(by)D}zUi+7I^n9nV@fwQ!=hHLCe9e-&OdJT!*<_v4YQ~c11B*04Pw5{tB&wi z8Ii?CA1&T5@AZL7f8G3}bjYoP$I1@#|wjYQr7Pi_Lv6tvv_p?_rC8B}=!=l~y2Z`JeB($SSVMf!KL zm5gJwF80h?pU#;;0Ja*~6f&&}3~~K6{I-e>qcPn=U4vQ8N#nKmyaN;~DxQ~L`SUCM zm_sx;BvF1pix5IAHOz%q*zlVL`3f)*DTJw3ZXP&&+v9mg|4BeZuKfvgRTGZ6@TgnS z5wC8=w4^O}1}B=^zEF3T|5qz7=;z5>>gFrjI7=04yf%6wUNQKdj8M1_fYN9zAjs%w zgC64*eXwY7@UQ{lGfhBhMmXe97Zme=K(l`RumFIGDj*^+`&|j5@!vU*<=MQo3p;m~ zQs}$pAgNg@V6)V_xyDVQpc`f729(@TAtzmnEA!olJXFua@4q``8%ld@A$81T{{v&{37;9Me9#Jz*LN(4f*Igsk` z_lS?UKX-lX5fGu6-qOH-`C16&O;X0~?d=_ZO#*=s>>Q)2S#dMp{OjwP+1arN3LX7l zy7rHx?=py4BV0MOBXC7F`sp>RZ9FX;uRRZP0bO-Id-B|)@Erdn{;)4woJ=k5un*-Y z00+|XBp-{)KH>j;+5C@gs{h@&KapX@uuS*e3WPKk^6w^mlXD(Ur1>rVx90g%;Hxd3 Nwz{5Lt;*xb{{=4DsLTKW diff --git a/sharing_expenses_with_beancount/media/98f0e5ed8b0be4c152e147c3f41ee833bb11cc5c.png b/sharing_expenses_with_beancount/media/98f0e5ed8b0be4c152e147c3f41ee833bb11cc5c.png new file mode 100644 index 0000000000000000000000000000000000000000..7abe617cfe648be50e6c348729903335b6082c62 GIT binary patch literal 11592 zcmd6NWmJ@3ysn6JOLxhTA`IQqphJTojSML%APvIMjVLMI9ZGi%3?bb}cc(Jsef|IM zy&ukpbIylz*Sc%Xntb0qd+%R8&+pmc>Z*!(*i_h$9zDWSR+7_v^azC#ct44W4m=kT z=^`FIdQPA$C#~&ae3*{mK_dUqhw(WM9ZNorN)|^4MK+#+mXwPvhvXrL0W(mE7{vsK zVfb+oiSkRLuQ`})n#NwF?BfR3Sgk_40&&O@_upHyX*7thhW$oydpvEnFGjgcm=vVL?EH@&3 zW>>u(#gC3nw4B;QXH+**!%>no`qpH8i6~(hJsy^ZSvMM5)<4u)vge1AUkTKFS$)ik zJ#48^lxk2EJ8GOwVR_JOP*iW>{R=vZ#j0m)F6k?4-KK7oT_1c@<4uc^{yc!PPd$Zl zOjqkQWxPn|W$&bQn!5Arr5PFfWZq~^5bUDw!1zhDUsi?KkX7ucaJH8WOVC=fnXrFh2W~vQ4Gu0^y9l7zxUuU+p6dYOH&f+ zBhuGZ{iSM2t_jFF?GPpJOyGJRy-3PZtJIDG-xroaMUlxw`KJ1)rr?Ix?AW}y=OjFv zs{&{0TVaSN2IHnlqd18(J95b~&+9ScabrUJTi6mhYk&QW>X*gy`;G|W?WPa}o7=i5 z?BKnogjm*W)^-skXxw4*L*btvoqn<&!CmjKAlIW9i0b?{_!-{YoQW_^gIboAvYk+Q zHh4FhgIQB}b(mT(EWoC`eA5P1ZypbUV7tHFWl$3N9Y9d9o%)xrv#*9DYtvUE|Dw`6)OfK&PDGg_^3&7tIJ_uamD|u_I z*J7Wg{hQ!Xm`W1b3<;-CPbLfK_zDCpPp$~GNPV7CXfbj9M3EB=o>Bmrf;?)T6pJM> zX%*6!U6wx7_`n86pTK2sod7 zLb>&ml8_<%v%T2hJT=nOYeceUwZ1mgP6{C$H*I=It!loh3^&|i1ygmgbD_JSpy#>@ z3X-)qC=xbxDqSWg3WrC9IEA^a_vudHg+zs@&+<_Kmzrt*ePI%SG3GLk_YlFVsZUSkCJG#5%KJFHLpuba6keywL>WmwMN_^x0dmouDE*#1)adC~g+$9gIW;0`= zR3Y28v$NANBYW7}T8-+r_96d+8xjQ?OrS$IpSn8zb$N|-4D!W=tJm9LNdY7A{!~pS z^aZuzDuhSDt*R&>Ee>QyVNoEQRU%kpp~LkJ(USaeCNYkyniWigDHrkBW7hObzjX^q zWaq);Fr#nngEyFpo(1;L{e%4!Fpc>KiS@N_*1dL{XVW~i4+ru^*us|#eF}5P@1HLvm?EH zEiY3O^&nDYT^J&?)HgA-=T6!#m%yrJAoeA5yabAFnw7ErfpG}JVrh_U16~SlJQWli zwD9j2>md0^vO7*N_=Cxt>0FC}Zxu%Q*zWRE9B*1mX0|yw)U!G?Tia(*7j|#1KSv~S z`8LS7K0k^W(Dh_eu6)6Xb>DRo=pre>$+^4iFZ64%J+nE-pf4tUbpM=`luOSk)Pe1H z$H(iow!GIuM_h9sHh{6kVvJg|XOg}?@n=j}g@XSOr$jM=DQDzC=0qt`zVQeOrUYjr zD3~lBF-!hG`jf>M1ealh)9J)jN0tjS*(e-nZB$6b1!Z?(p>oNtea*+U7sp!&78wdB709RaA{FKaMZ ztW+#dgor&RbRS~f4=0x4P``WRcKc}Ebh*Lt)N(gv`P=W7C_^j2$8mLsr4|upgE~9J znx@lY|Ha|Nc%#cUbU7?MT;%?2@C9qmgJGMGXF*#!=Z87ye4TwWHJ3 zn&b2}68THs;EP=9v0)Q`aLS_EQSwkvII-^K!JNxN?fV(aorR{{WxpGH`QHa~2`-xh zZaC=#mT$6Ug1=vXwAqzThP3%0D(ZenXMYG*IO}O!iuZ~& z^9R^Bq%KBfXDa-jrolj4@m&)+$VQ(&29c_AK20_OuTyTs~`+>ye4tetS0f>b&~y zd_?Me*=xVtXf39J0H(6J?n|iY08GbU>3FHt8($SPf2y1*srce}u6yQZ#;|!P-gm10 zw)Xoq*_+FC`&6OrJMYUiRzdrkM7*PkP_?VG*M8RLTUx^OFbsmFPhPQf64zFMFV#lE1h})_=VDATO*LKg(&QWcnE%u5 z;k5dm7J{RL9o8KJ^S+o`g&AS@o!rW{>pe*bnzm#G)=x1eXFFJ1;JN2UfBf6#kB6y| z><8~FTkCWgDkH%utIk>;gF<3w+^tfpk%9-`QwJ{O_pd&!u3y1X$?YKMdtxVp3q`2r zsPI_v?}!zNo6G%rByu}b>XY9O;Qty+aA(c|uV0RMKcD|PiT2&19l7~C+8nJ(*t(p5 zXB`kwvmt_Jx27#CZgzGD%V|Q688ZbcX%b+9>~Zfbx9gGF_P>BA}}dCkihMKu8* z1pI&>b61hhbK|q9y7kUz_P$08v0;&VHmBEII5F!z*PF8BVac3sW1Vbpmr(1{r%#`n zzjEk^=8iWw?LcS!2EMR!uEI+oi@hS;*aP#m@9S|qHwTlXLitsnjgO6u6&{o$>`Ypc zF|KY5VsT0cNqZ{~7gz_+gF?^y-}QRzK4r<%Xhm+NH9HecF=%kSt=VX|UD*d|wRoIH zDve}Fc(ae+-`~5-PdXE8-sH-bR8i0l8wwqX>bXm&SkJuOSe<8xj)^=2LY-Ez@FH>d zvjcdCKw4T_84a7HA;JgqFmN}EMu^FUKVs)_r4#m@2Tk5Y14-zhIg4?_lrH6e|GoiM z`oX!JJ9)%+uExT`BmlwhU!JaF(*NMZ*ygj(Xv0B3&cSppN%gxt8bR_xCnU+gY`|!8 ztl9mze5W)RhrBHZvUM=`{Q(aPtI@uKT%#~TJtrsUXRcaU|0e&y-c+eOztzug^+OA~ z<@%Q-b$B>934M48UD2v^xzmytQg+S$4}PVq?A>thFYH=dEy8VsBjV>njCV~9C9Xo0 z8Z+`kSVZLvM(uug%xsQ}4X4s>GERMevtJT3J$|Imq7Cm$Y5`kU3Y&x24;J97W<5e8 zBF15K5bO`TZpETVp48O}?0HzKTxd&ROs(tgxcrXNP8ARDq=4OQ)#dE(Ijc;GF#aPN zrFDGk5olKY-Oc%>%l4T$wV;x0=kLKnwfa=taWy%`Wf|9io%Q0vjL&_sP0rOi+l1~b z=^ko|5O~d949n|8t}fHh$BjP7oWq6s2Li3k9QjCN1=erC4nO@;902>scn5g$pxDB| zS5ESu$-L$wIg-wbi&;?vlon(O(o|#pCjNJ|>M3|k0&;zOG>r3eb@mGnPD`Vfym1y1 zVLZ;F2wzAuiE272%oQTS56B%RTMt(dNUhK8RwFjH2>kJrxSbY zuu#7I(uQN|{nWbOS_p}lTU|7wfjWIp2eQS}MJL|;-rsvv z@96WKhgnrpFUWJA%IR~+$hWHtLA^mRK?Z$=i^|{9a*-H#!sry=A{i)_c z+4E!qJw7aH_|4~XX#m<&-M2gfHDxO|@d{V`%Ep+>WGCe| zL_wV}*WhtVsZ&Vts3v`fUx|QT(ua0DS(f>*2vO9`e^jQFo4~*Ye~&9OB}tQT<*nb^ z-sroVJp={wP~^RzF7teBgNqHBIHeLIccBH6L29VaV((O~|54-i#--nJ>z_JKU~?5H zAy_s_@;*CaO6H@jH^zVDTDYl*5T(;)!<_OnaSLrH0}sktb3RvKlj=LA2!Rx{Tvn#> z5NXhgJ{}dnH7aDx4jk6STdVM+qQd{r(XaYkNJ^Av2?l#NPbFfOk5cZk#4v>`3fra( zwdv_EQO?*DTnMU-1$eI_usN)ejx@ZeP`lxLAd>wXwaqUmA)K)sqA<*C26lqBC|F06 z`miL2``yN+a+}4?^QAN07_^XC9|ZSSTcD_&x@~!cZ%nRfpV6O*YJ5mWsP+N$2IH;YMNF zDB+1in&T8)C-dLCQ>G|>_1BgZ(CiY8YTJ1)n9^Y5;!&Ggd%UI)nnqus=%Ho-Y%v8& z!OJeay5hQ33XKYgg8cofisYzd@au#Cl(hY`AK)SA-gLP)iOpg#;b*gIvg`_Q_}{c8 z+m`yq429rE1fV!NeZAeZsM-7jX5xD?rmg6-WbWX+>RD{ruKVMU2OeO*IWT2De}!(W zNm%UmaV_80iry8ei@0%{>n^_v`h@I;ne)>_q6yjCi=3!L^$Lg%Z6+TG767X=^Ob*Y zY;K#iL`_S4C@ZFu(L8Vfg0Y7r(+vs;Ln3(^PLe6KWCe-{QJAPSY!>^!38Z8esw_v% zMTfo9i!* z5>Nlzc^X=Evapd)HtiKCE36M;8F0=sK9zNBau&@H`d#CQ@f2Eo<2!aKxQbc;mmC>1%HnVlT%xk~i<3vI+ zy1LlNUOIPqU+&eCqQiW|KgcRdV{{l!wp4tI3G&j$fUz@_b5k%e;on~38*Q!~7sw{%6|anaEd6+V z@>MKm;37HG^32`qK6C`>tr($pyCyNVtV5;o{F~N;>{a{VZ7v}0m}a(HL5$UI*9SNM+M{;R2l8NgZsrz8|6jf^hJrP`LI@IU=3fB>I$9_h!eE}>L!S{{ja+jyYh4Tt>qpFT zoF?J8jpEQh`OB?60uXW=rUwe_@Cppx#4qX9I82qnZSnQY^Hc0EfAD^(7R#q>=D@wI z>5mz$AGGd=V%8%LKnZEpQxqOpQTDM`c75jXmGyV(s(cZx8DXaIHxcdPA=R&~S)bvO zKBz*o+ng1EZE8@l6igw6i$+TM2%CrY+?p|e>6tC?i=sWZI;fyGXO%-vT>BZzDU!DO zSlBWL3G$iGU)0s9g~p+h^6O;j9n5Jf2y+D85NUUUQJ+1 z4tc9@99uI3Qyu55@q$!0LjvO>=zA)}7(QKM^I7uhOGRW+0ke5w4OT9ZhDy$Cd6Tgk zC@(jxs{%_AGM-CrWZj(n>KECeL7whsjlZT%P*y4UK^Xf~D|`~7U_)o_jU8U3hzl2c z*FF;T_{TLEm1r$1v6?@J{1>z*ydlIl*;y4#1E%b<2NxqcSVZ47K^9Zxx1V)`f);;KQ>B)`(a>17hZCc3ut;ARiz`?1F$b@YP?K#< zo|;qJMf#ovg)Ym}dbLtMze^QkZmqZ=gu?{oFx|1RvGs?)^EjLLomFKq&~0t^teHWEN8udZKOO>=UKC7QQItA1utx#Mj znC=q!xu z1g0c-2}%%jW>!3_q7k8qvAt(ENK`~aJ3Z*K^J;&PZ1+4bFy8dd-r4Mw0j;C-Luq6c z$aPADX{HQ`7d}2$f7Gtk&8Z-N`rM#yM`zxYxOL-ay5(J*%7tbOyeU=5F2aKps+@@rIB2SC@MazVbvo|~j?_6ve3 zQqX=v;|3=)B%Ml{;O6lD^jEc@t{SUtSs7WRF;PCvP*Uu2kve0r4TEDeTesj%kmoHY z6O9Y9_QU2CirGsx`Uo{7lNZt0$jXT1x?}rYop@s6bAvO0%M6&$wfjpsF1NJ~IZhPd zEq}L)9Wxzt2FTQO)AMP^IO8VQVo106lJ6fQzmpZ|r6#xcZ~GdY*P(N_$a5rdVx@sL znU>^%J75naC4-x4hWC+(q{|j$|He}c2!M_x2zp3B06iroVy41~69WT1B*&oM0bKcw zqKBoY172q}lJOOl07*ndbgMxm5a!ZV@3_pZf(65|0J;XB<28;J8#iBSAzu}_xci9v z%TYWY=UqD%Hxmv$-&QjNi^<ANpr-%Ur>o(Fe1To7Sf?~`$l!-{JcRjL=DPW~EnycR z;pcX1zN6ezWO?ofvy6qxTY@q?RX}D#%*2KlmFZQ#Yl#y6U8-BDKk{8j76=U=1Qh54 zvWiJXv9Pm;Z$FLikR_0S`sQfq%2BgrihI`Z=jAHK4%~7ZHTB=BNh0MOyf^xZQly;M zmdGjmos3zTp4|djHPDxN{B8Y%-GKX?Y%%(gKL6x-UkER(TzU(?a-dn zPH2qeZ57u7OVj~#{7)8i(VV1J6CcL?zYR)@U+JQ5pZhA>ScCh~k${C0cL}6!)eC?X zGET%K&$Wfn{LK-iwJBxsYzn{3+%9*D#}=2Cd++dyW!^vp{;sU3rFuRjb_VQ zNXA$oKC?`-GvOr zdo*_QKa?Gg5C6$>yeTi*u4F!bto3_Ls8b}b0SV^`?s%Wip;OY|Yh)q(Dmj%#>?34) ztlrTi4eU=8KqKt*GSQ5o#HZ|zgh&IcO=0wnH7#F(bYaiMpdHvfNZF29<4Y>27XxD< zc}T@f(^i_oji0>BJPI=dR|>h@E~Ff*0bsg ztS>=D*R3*@PWw}8(%vRM)AqLzoscGk$Hpe1%BR2{yX$SNhZCYd^E`oMJTE#j6V4;dE zsMo|K;BRq?K!{@!j`j5P^f!A_6Cn(rYD74&H5np#=z^LSFweXEJk&^4h<0)QExdST zjw`H`XJN&7ZQNODff9{3wU6J1hWVX^?c$IXIpfM*|6A3+p-x0^4W|u;xUDoP@C{ux0C%zZI9vakNrT`}+2t1v{qJ^Zpjzb&GjeW+>9}1=sMdFWJNK#oekJoFP2sy$JaO*AEZqLoQFA1H{n@}8jU z+vuCMdmtJ=mDz!>6i!2ZubtWt<~Ap?E-eQC0^6V3B|T|xb70qQd(fTZLk_4mEOV1Y zOH?tJ>c{g}2e7&|>UnFi_@@9OB&4Ac2XNMw!W%811Uqcu;j5AreyYFtqqao#^n<&j zwRH&C)=p4ISJ;&5TPrh=p2f0u5gG!}OqR_G{!_U0D0(r6@fE?Hxrk zRhQnlZWQf!?5~W`Eho5xrPV>mZ2Na(Mrpz$B9o6i} z{YJQ{1|TCMaEWwJfuevlA76p89)E4V=UNqj(9=ZwZ}r<%wRxj)YS zrledDPfB|-Q8B=pu>w!H-tH1Pys_o{FU}zS=O>g1gIJRhQT~qo6q@fNFOE^hXw8qd_PsQK0~(#Y6?%3y{VM(SaBVgoH5Q0+sMd3a&H- z;JKJ!%0M0<#SlWlPt0C|)*1if04L}PZ>!&-k%y+r019Mz8+&}`GZvOp0I8m>c8BiE z8)E?faso;~0D?iKZe@lJpXJb~5DPdN5C8|>pAYek?EK$r0Fs(&++*U0w4ei!sXua) zJO$7i^C2&cTaqh{n%}hDufPaACJIy<+uPgA%(s80Pv6x${Nljl0^$2lO>Azel!`ntQiJnnCiBSY=P9x0bfdnl)t3oXco$!^Yesb-_teu zHnVPYI$GN2l`2k7Rf|V|mi@PZ5}PoFw?ts?aXu`BL?p0U_@+<-hFn$otGYRh^k)O5 zYOuy_e;R=j*$J3KFypy_ARRF=^I+l&6NhyDuk2bON!&*IZQQyQGyvn-QjDeB^gP>$ zhh1LIm~6+2pM}wIDaO#m!!Q?e9Fkun&o(WnP_KR;F8BZz07v^ax$bsFl5)bghf$h);HCa*|mfIvMP1+?Lms%Rfl!$$Aet+#l%av}Oz5lCx zSpeE*A{^Gj3K0HO^m$L9LttauANm#`L5XT9;NVO#_ZV^x?fxd;&Oo%yeQ6jv;`;hx zmw5se9XD>*c^!`6|B3NwuIT)em#mBo^g8lITO3F$TLvu!?wwoClM75odn?=5e*Q=N zmdo{pO|Eka#lKr*p14CeeTGjf+AoePoQxZV!IltE%%K00KMNfJtkCn}{%&(WR_b0a zSq*z&(3POpF8?${YKN^uF5of)oU^t={!Rp($BEgMyxT zDk|ooVv}=o#X7I`gx>=2dHQaM;i=5CU|%!zqDtGTlA43s8RLXq01XfD4?XQjjw3J! zZ!@P#`vLt8C>1D}KZMF~eS8_1dkXd+kWBm?RIbZDZisE9449*TIn|7j{1r4WLy#V9W)7c>Nm?{MYO z(Yoo7!>1AM07yZ4jwEIuK2NQNf^ODJlzsjY+<6@H9zk0JxpyyB)GUWnExr@XNBdCZ zQ6>|eNnP(Sr-UF)=fhe630}kwEE|Fwzf13TC0$@pYs)h68-flptbr=0WHKw_Q|_+% zSbPMsNsv%VRI32xCo$F>RET#KSpfUvQI#zK+)P_PdCzS7Xsr_b0&}A12Sg#jjb09` zKXljB2)&ZG6+9EZ`}MIF2Cgl{TfJS>_)}-{3lm8Z;cEJvXOn-I#FM8OMPc>}!}L1% z7EqbzH`aaQVv8P|+CQHBu1%Sr-Yyw;Eh2FBXYxr5F z-!-B@oDaZTj4cQ#o!1UyV^&d+YKz4nnMl6PY*n^lPtQj^>n8(H9-~fE0OY5*b*#n2 zpuI^ZX2OPLqD=5I$ZrBT>Rp3zgP_s<&m%;SmR0lRTvO0JJh{REW5h$im9r=6vK4h%`xu6?``v0pU!?$Wy<6a168M93 zUKT9)4a9{oy{HcnCaOM?EgM?4U`7=zUK3UzjbV>kG4!#8a7N*uxyMMciB)Z7Fb;cfhRew~|i6jr6+x;ie%p&p? zL&j*A%8tfB=k?nh?yMMs!xOT9yf-Ii6oWju?oX~HJsYA~dyU+Q{5wS2%&%D3e+0!~OjQkGYh JtB^4b{9lgS59a^? literal 0 HcmV?d00001 diff --git a/sharing_expenses_with_beancount/media/e897ff1fb4ea70efa1164dca215811e00b4a06d0.png b/sharing_expenses_with_beancount/media/e897ff1fb4ea70efa1164dca215811e00b4a06d0.png deleted file mode 100644 index 15ca5ff5d00cac9cf3eaf4adb6d350d4df7e46d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11583 zcmd5?S5#A7lm?N25K5$mUIYO_AoSislYk&1y@%d=kzOQ7Hy}g=q@yBD5TzF>p-Km7 zA|0gnKG*+WYaZrdW*+8Y*2)U3dvotSXWzZg{>q8f)>0)Qq9ek=!68vsgFnH+!G!|< zj}n4_?-GbU76*q9qz+ej>TkA{Mc_}VeA7knDmhvHmO?U}60shxQVNTjZfgYFqdk72 z2o{tuWVZ%qm>2a(Qy!9*N7(elEKv33A+?d>j%FYU^70 z;P6I4ZLO2!a~~=mM33JIw&01Q-rzc{`azT%`1-Oo`n6Hi1ko(g0_nn6?jd&m2I}|T zLD31LQ;S)RrY8JE=~$iy!H{M2!E>861ID6Oh_Y->5TJ=`A=^XNH4zP2|5)fmW9AQ@ z+yOyzlE?3;YLxd%nC}-TWJH9-VW3 z9+bA@#q*B4y!#s?9J9y}!8F$4IlD&`Q1G~w$|i=h!gC%CW!+G;g>z?&<79xhpS)D; zTa(5|1o$UNr%G7CB={UEG}e@RoxIr)xvveQ)IeQINV8Pzub-}VDoeSb<|(8i#u7}A z5v-A5KP)N)a|&VijTTVYk`R140c-UzfB9 z5_rf=y9SODlY_fJ&2mxy7i0?`u^s1bEA%Yo2fQY>e zN|f#!48%v9*Q^zfJA({{2Qt9aY3al+kT-4dYn$8}&=d*5OmLYKzAk18*<|(N&LvNE zMj<2iot&w)%IyLso@A{59e+pNbm$13kpE@Odm`C8@!SyVRUEd=Hb-^}0Saluh|w$! z2=~*DeY&F(GDl0P)c0!mE&hE)c-wyQ@D;oX8Zm4f{I3e5#d~V{!C0^@<+hW)(F#34TjNX zx#s!DN#x(JB*c83kAyrtXrfQ2zKS2X;ZsBjXSgW}`XB#J_MSifc6qYvsyH zNa*0-hD5mvJee)#)Vi0?%J6&UtH+pkBx^n^wOm zCY0uLc3tW;C3Y(WQcUaK!43Ue9TMHnC4ax4402fqrP(=ztHT@c8+iKm1EO;Mf z$g7|`BjK2tVRZ8yGQfkd`vozr1|qH|VvQkf_3F%#f}=i;ZArkL;y*8T zsphEjN9wTU??HJ%6y=%GQf&EHMgQ*>+wVVKheY`eZKi*}H9I_Ik=kuvg~KFD%K@X~ z@30b(^6^qL-?|Bp?^MOI{Bu1F)@hM?eA6N>c4IByHjTCT=jdJ7xCPkfiqAtDnn32^ zJM)#(>PNGU{$l5qa~BK4*fYbfVDF7|bHzhzRv5EsF=waumCPRB^9@;%+-6sDt+hO} z97CC6PU?}Q=v@V^uFfW>E@_4_hQG5W*UfbF?Q7S`8r@{CVnM>-j>%YZS0zrR0uTng zAo}ltuj;K4_!BBBD%-t1fz=4iNX^Hm3uPrn9WV8d&V!T}78V{HU0abk7w>Fp@R(H{ zE)WgXe0{)Uz8rh6v8nXlDRDdTz*%)^X+h}Cthd)q$o5NDMk!y#@jZtL9AV0MkVEXMMVX@ z+Rbh|*~dedS%~D%jyM`SFq5=@u)qJQ&r+6L=$r%lb8^m+Dx2QMHDdx&hK#~=87>JY zSY#9ZU}nQ{XoJR1h$!blFMo3>eA=kSu|_|X%&ywD|IL{|q;ajQy3)6RgOx+)sVa^* zjaCo#k=n-^s{>i+V0KQ?Lq)zy} zWit$;iKqrPrV9%REMdUy>mPntq8D|}`1tSO`KG;7gR%2+Vday)yG0+0AEogWC*S-~ zA~PMCZ->4QOk|e3PVZ4Nzh28v5}p6%)wQlxq#Q?m;knX>No7|}l={81)zZ>(?@%LH zNoo5MLr} zQZotIeny|@LWsc#+Mh z3xq6TG(kI4wQlqM{0+NpxQF?`!qBH<^EI0}Xt(v5$q0}!JLHSD^|Tb57(I_N-w~MNuFu;uBT7;)>Mj+P4^yxr{9&=qvs%~L{BNGi zbvAQNj$4U|9z#Xv@(~!=YR8^w$nlgN+4@9> zUST~JRrkZH1z?x zbzV6Fg(7qi6@WH82A(^>dbJS4?5Z$&PjZRMyOcxYH7{IMF(x`Tw=7pFrmXABpv=jP zLV?GsJe{BXb51;xor$FK9HAKI=?VopqA%v6E^o$=$suf#=hS(`aOTHySJl!XqX}`J zBxAnf?R?xQ}P)d!Xy`f9Hh=ikT6seuk$yZ zD$Y$kD>bOJ0II8({3urHH^}Dc0N3XX@Gwf{S((Wv75=6j;}UN9w9Mp8_@{Lu(*1LmklldQjJsfA z$MFW86?1_{|9!2uU+?PNVdJC9%`-2duaZG4(3A7ApX*<+^4A9TSuQ(E411u-P_Uq~JLa#MUqk)x(`e$NVq)XAwAML@ZZ%!%rk zGN0z=XPOSRZDCMa@UV!}xOBKM2)}6EKOr;jTcT#BXuq=T_V10KayOR;`+?0j*B8@i z*k3{U;G!tu0yf!0#sa~$x#!KsY-ThGs3kq8@s+eZr*0SXprf0D_s+6G82lq7n7+5M6w7Sx@Dw&!-uq|Ys{V+hRpKiIF;PC7rI99>_e2VU zdP;N<8ea?KgP!4^B3IbDzZ3*l52{3}%!XC>Ujp`J5&?u_C=rD?^>$wN4 z2tMF5N26$b=|8LSSPMNy8Ztj{)G$}fUI}sd5uLZe3^uvb>H+x9Me{^%JNC3t!zOjO z#MLx}RW`eMemo=6dO^Q$&;M+=a6}l2AWMnqP_I@+knN4c!Hg8Y!SsBB0NMm1{rTdE zs>VOzeR!aoH=>$s>uJC?&*w~V5v^4q@5Ca6Tv3H8MQTFgc4|cC{WC%vm zMaV3jM>m^)%2S>WI~ou=o$&~X9S-^#QNxX}{jqBxk)^xI0IK=1Sqb~pMNyT%f3viG0l=QvP0cxD7At-F1&=FaPd}&UNG#zoeeJvX^*NUtv62&9>}JsjcCs&-aE2YM z4w@H?hp^NTzI}}?snGzEFG~X@wIF5sFcVBIY00|*sSJ$zw4FzYmvVi+S-y;B zBqYu^!0EWNGxd4(87+8!ubYGspApAP=b-ME34WH@*$vGoczu13%DZfWZ@9JS{A%sVqwIgmtwWc~Yg`BC?dO#rUb{+qtG^*)a;bH` zcxYa^Sb|784*-SMc7>k)Y4$5P{mwr}ab;Qv8OtlT`Wbh0x1qT1eaYj!H_T2-8Tc&B z!QiB_6uTXW=y~o>0_%YwGY>G~U6V7D1R>8UWRT|ZIFcbvh`IAXRE7|ZjrTlm_qSc= zM32Eats$8B;1nz^8`{GZ38PNIPSRlFSz*u#<^Lb2I5btl%7giPdwA%WnrfdtrSwy# zilb_G`(98oq8in-KqxyK6QQUmM$Km)S)bQtledS4aBuV}dRCL2i56R1&&yEIbeU3S z!I_Jv-7Y@*T=1pTb6vVaQ^r`)tWP2lFWl*2P&ScnBw_9Umjioj=-6SQpbW&wGZ?hZ z1t5gCpqpIRyUy|b6rseM%d&9z!_H^1%_b#{W~VGHXx5odVgx4}fwkBpL0=hEWx=0# zQ}TfRql5;=p|V8Wyad+xS?>NxJTJ=laDFarEyW}R zVy21o{Gjva2)=}Ljy~GYRm5yo^$}Y&S3=;w(}#V;MhSg+ringm zE%r8ri!jnkrD@V8pd9aZxQ-nK!dMyMl)jqcRE*A%Bao^E`xpI2;r6(0e-+DwEa ztT5U?HRDl%E$opObXr#pE@LGy2e}A-NcnCo9f~3+s7Wn^G_G;7Yke2_(;n~jYT>V9 zfmYW9JsiRv;ds7Db?hBp$=45+df>av_GFUn`t`(SP?$G)%DA3t9u0?c3|A-2G|(dH zGZtAzsGy)gY;9%b>i?+5ajb6r!(rX9?$FSX81c$LaQedb&T4vtwD{@vTypYL5m7EK z329@|?fXv~{P*rP4#ZO?%1H~4zF+JTous~4A|bAilROL*GCCLWQwcO(XN@OtL6L8> zX>B-WN5|63yomwf?`g})F`aa?zg1N&{vedbvsVMC;{Pp zHQS!?#1V5Dj>?hnQYnYczFXt4WK$?VJGDtdE^YBSIlQO@SJm(^AH}Vg~$<>LLvdA74&#pa05T)BQUE|WrZ{Ad@ zzb*_5gp5@F2*Zg<#H~`{Z*8S){EGAMh~!`|$??)9Xq9I|E_}L zNp{+wDR#uOL#)hCMh%0K0fVE3)s;NySSVR}Gxi-r<{HQ2 zY~jrq=e+dd?7TG&#(yI)01B>Ck2 zy?h!EaIS>clfzeu{|pPdnxfNXog~39W>R8;sU;#ypPryx|Ni~^^CRi2YMsY(4G!aY zQ*izoC*8wW1Ai|u7s3GG5gQJCna+LG=Wi4@T_0DDIHdh=>s_4vZ<_qKr#UV!GY66*?MLHy<5W` zC`37F;-Ov-F#E5lFuIs%=yco~=cg^!%(Z3geIemMjBIdS&^#_4XiZrbkiiLdMbH5! zvwq9(v_o&sH~F2W0PV`ZIphkNI#Qy;etj?~cd)dCG!%6l<(jLoRGNFIfOmZ~Vcv@( zG^z7eSAkEij_I2(Pk(VipG6XotpKvw>z$zjb&9z^?~9ixa)11pI{TGO-lPxbCIB)@ zjOVGEC0)1iJuqnc55>w}Im*3nd7>w6k$`tuX_wm^&d+uZJv>{z%<`K{=^D;cCT{Xt z)e7LfIN4bRgbwuCC*#_c=f{5p>6|47L|uLw)OMVc3)y&=n759~t|ozdH5(;+^RSKDC|6N+m6HpG%a51t3MGmNQf67y<%^Kh)!o zez{pqww>FHf2Ub$T&v|9LaXuQ$=yLdw(uCH!0vPSDmTRCUC?lW`fze0lMK&+l;1Y* zf!x)h?zgQJc2(qc)Ex?>?DnArpjyDLwrb~lgyV@Ej{F06<{Dw93gE!AZ&amYgp0$P zpp+gLL3uQD=v(HyT?A-}^WF9;=X8OpBxT83+BKBfi{P`u(u-&svt%E?Ka)C4_2=1? zZ1hWh8>8w)a_4L87}vRmZwM(Xm|DBGwp*5@4>Iubbg!xbkRf0$*$-Q_^|HhV0UBHJ zIzS2uV`@o9sE|~2CeZgP=vLe7V9dTaV|a5UeRxhj!|A9VJge3!ygplHc1jHk9>w`H zTo~8a%by2wOj&1=3GDWqMGC0p1Ron>(seVyvS)D1bL1`(^3P71gW zR3wxDh2Sv-l$$r3jS~}5@!UHQbDgp8#~b^ghd%pkH~4xTcoDtc4iE^gs-{X-e(ijS zagk~w1NX==0;H<}m+CGzs^KxO^L}5`H_ZwxwoORb2IEx-4>!=+C#mo14~x2U_? z=(O{YRQhZQ4U_TPHo<81A}hW`0)$k+MF=RC*G}T%F$Tz!H2Z%N<|WqRlf1BM8JPuy zqOC=d3B=Vx`Kvf5bI=AP zv@cD?FWCV0%r&7hkR!*>es4*SI;96}TUwu?337TUkT%Q8dRd2ZzuVU4j8G|Djop9A zR;OT!dN}&4%ZNOYCT=8Sn*<7juPho+^P4$m-DUfL$KQDP3SoU7^(DqI+gZX976~GG zwSIoI+3WKG2<3rW)7gv1nWCE6sy?K^`f#uI#P^>CGovrvJcL`S-bfA?#WR}6RRqsa z{8jTLjYn_kcNvU<1EHD7D8;$CHd36ViD^d3lQM|2^a_yc$&QD-M3vk&XSZjsPK1uU z)3rLc5yeQg-TM;D^+*%_+iRs-5R=(fFd8ASVb2-0Bj39A$dD=R!O_;VehB{t#xA=O zTW(ta#fO!8^YMzcKJ&HLD6(Q83g1nS09v^l&DNVSuifpyna34q+n*|CwK`X zz~8Ru?xJ7?t6k3Zq9E)`*7`>s%B6)YMDnuZF4tEl=9!Z21W5@AX*P~J2I1?()HYXF zm;MDP+1D?*e|+m!3*lr_e7O6DMCDFg(}U;~Q_7-npNw7|9nqw^9ioHwr$jy$SGH*- z_lNi4cSaa#Z)fwq$9G|n8}>hp??&8g-1;dRcxZq2op6YHplffj%LXGUNAu7m*#RDP z9xzkumSYmr%6>Ca{J3|JY2k7q$dZpRuDR*f--bek4j^aAi;ay<@jDo2d-d&J9_6gI z_qTmZX+ob`>XqoL-E5vWY{NAFHfuVA`Ig79B`RI|AX9lU{O6u0PWV-%kgFxC5gK0Y zMOxHhG_ZChW5o%I==x+uyn~{5zz(L9f0vEiaz;@2@Pc?m>d3 zB7c_l?epunm-g?kicuGBPSl3nNp%N+IDmVsdv}^|1`ezsk#ZY_rODbKki7D;w5wCfP^ZBqw z_KjGLmWqhIQv9-?)von3d-fDbVA%~ryg1PN_-wCRG1_1Li*u00Lfd5`UR*teasfFf zC@6#W^wic@g(d$Dh$OzGGd$-!>IZc-H;(yp@|CDBcTfH{T%--URqUnQ#%$(=&EUfv z&?kOlQ2^Nb6g}aX8@7ac3+YO47kD$}v4e{qz@ z6PnKN-)FiVKRhYy>EC@Tf$re`#dmpyt_2ay&(hz0^`R4l&k+-6-}Sh}fA65ZK z+S*!XKzrNv?^El`%U9`A?w9t~UPI6Rpfw6SNDl))*9kCHvnrAgP_#@wzmA{n>7Crd z#&e)5kk*cMEt9WT;c$XF*@pKPM7srnL2cxDD$E%Rf(USx+DN z0^}%K*Q`UfPyGIEg#sG32tx`s1ba=Ze^#X?z^OR;I1J%Lu~xMkfijq6Qnul`6}6|4 zUsfqo-r6&3k`cDy!I#K~)cHInCJ30TUsRfM#MeuY^Q#cyJ6+h>vfY9jcMhW*9bw1i z0vWxNX60clC~uZYgScPq2*1mFkj6usKN%>{_RF>9D6UYI`N6nk9TGNCnGXt?7(vZI z)#(@()Zslc>yKmA+}V|kNl-sua1*ym=PeMT0lel>jm z>A&B5^go`>wjdR{#-)u-9gD7%FAx)P9R1pMC*l{-|ECI*$k+hn&q{~~l#T+Y5;+LS zY=DxofPpf-jR7mr0!c;)gGLAdl|qh$-L?c47FIxoXbVGhDghKU3KtU}^Ivb!Mvt%s z3{DdUN1|ZB1U)Z;gV3p|->0h`+5u6!h$es$cuHF)f)lXZ8eC`Vo@wUDn$D+oTaYqI zbCvgh9+ds~pT8*rAYULPbU97mABnto13+$@I>7dXHQEocHTiBCot*;!VM+6i@WS== zbyZ@Pl%LVbe4Won;nJ2Cd9!bxHnM;Fd+6D|%{RAVt*j-}bHFfJ3D{p+pt!G9y)6ac zc{2btF_DwE0PLY3kjdRJq02o~-^~5}zs~H)-JFp%KR+qsovySd2;Ilt4**cG&6Ro3 z9)7deYGRF6h2t2{^?on^K}CfC6D8$$06h%wo7N%u%s<;ENUtO(lb>Dd79mp3_V)HJ z&sK9*b8~a0L$5E#)!YE!c=>HLdlB$)escgmz?M#zkoTH)0K(+re+inWF%QytEpyN((i;GO zt-nM4NT<7(iXA<$(LowYfQ4UyC@|nKv)$N@RR6BF>Csn?HcJK|)IUdL6L2 zpS(c;&O4HBC*ez5yzIpvAxzA1jq9w@o5A&w;)G5ctA9fjZrNHvMcMFcwYEeQZj6HFEY-5LnQ5+Ek0 zmoo<+>PSplNL!q2&%&5(BP1#A-d$p3Od;hm`C1{}d53~?mtM@(;6x}=jWC7sI2!t{ z>4VgR^URl4JC%#nUGW{A5LE2pDh1mbCEs&obEkP7_q}5=yOcleENH7;#@bTxoygcL_ zV7`9Bdqx9#3xgt}DIpLDj7;U$Nb6J0Ej$Vd_g^?UB2LdjPDD-kq z72`TpRfw1tf@Zh_)~`)SB7+37Am$)#3Sa=_b4VDV_u_27MUe_@wHn(eM=zmxMbfZ6;RvhTj97J9UX@3lr}KA%iTysJ8uja z+BP4s%J|Ko(W3PhCJEoU8rMlYvCT881o zH{hZoHS2Jit~hR{c%lgcr>6#9if6#?oaJK-6j1$;1M#8ZrK_Wg;(2vJT{*Sz`F9Qr; zbc4Feetsx=3=A`o093y5BBk-_c)H4kryR(h{a9UF02(RzM`Bx~D^MRc0aJK^iH!|rbUln7F4Bf)+J?>wga>>KhjF{F%Dt}zaj`=g4S8%B+RHE&@H zv_9lEz{aJyl|7-Bi(iu!T!C^HPYnw2yONmGK{g;2l;mMh20J3)=K4zXjCRRBKj1@g z3!BhJ+eM4;=o#KdP@Y+sSG4dzSq&V+-wx-d!E>UQJ5y>QL~R+fiY32~CRZ3&P> zrPC_xud;-i@P8Dg+bOsHhPfALs006-hWWqlxKB$BJaT@h-{t=J0O%p1n~Uwnr=jK@ Xq^QWS!98GOG>*Em7Q9T+BH}*)P#(IF diff --git a/sharing_expenses_with_beancount/media/e9418073c42793ed5a2bdbc69f415ad2d0a1ff08.png b/sharing_expenses_with_beancount/media/e9418073c42793ed5a2bdbc69f415ad2d0a1ff08.png new file mode 100644 index 0000000000000000000000000000000000000000..9240217a52f46934640cccfe5c4c122eaf9de9cc GIT binary patch literal 7646 zcmcJUWl)=4_wJ#zXmJe$x8T~6LPK#aPH@)(MT-QtP_#I-eF#qR0>xWgiWf?84bm2O z3;w^Kcg~zo=ggToGw+vs?wNh>mDy|Wz1H=+605DLOiV~mh=qkktg51*i-m;^2HrFA zae?RKQi*LWEXGf&3bOhLi#-(42Mzdrf77D-S8*?ieVz+5HL55tb@Ds638GqU ze~A9FA*Tb!GL@LT*Wi;2ImA-GkAoS7^<%as;hm0lmt3ED$1(8vv(dqkj|Ut6AxU-9 z^Nt5~7Zy8pp)PwoJuFBvIegat<0DzSs3zgT`4HAGnLcs#uZ0QonWC&sJ!zu<%=4+v z7e*$?i{2%B>h$rmL)ZKb`k2)H;}b~v?I{8tr>5{1qCVd}1IhJKHU?NC43!+zpUqQq z5T7Ji#}zkJpn8PeIcl?IY-vddf>}@#qRVwtV<{k<&hqz8_O8BMD zt$H7#hiWv`KEc!VA|he5YRp`q^__D=QfoD=;ZX7O+r z+I(qhf737gC`_~Nr?5TFGZ_^}dwm~QciK_ib@#a^nVv(!9q5;R+@hl(hrUP25&grj zoIaeKJ_IFwomZKE6zo`#02gKQnv$inwSO&fN23YrL6I~FkAy^2s=Ux=O7S%6GwGok ziWFTI9%)~YS2CUPwWM>feE5m@jXxIY&{NU31I>c$i&L8WSU&cPz$&1t7Co(loaXlU z{gRGVLhG#wEdoB955>%Lv+=nrDiT7+W$P=GV~xdf-V=*OvNkpVo`vkxBs2YS^`_<`msXY5sON6&~(QNv|N9|PBE4q11*gXzT<{LMFQRxc+ zxjcSzeQoYaMxtx3!3=s~I+PcG)MV$zi7L!|isip5V4&IaT=4weXxyy*hdDCUg;%S2 zN!Fe3g-6~$>J&uXi;YNs85I_zzm*C1NIo+1h0`;4jR0Ww1i**bB|zGnUE!4=qo_z)>{*&TbT6F*t)@uNZu}zwA1X~rtdGO`Jwb}G9Ub8h6B+($ z^!<=zu?@E#Eg8>zg`g$bXPB``=o-kBW7vug;L+kdbM{f|HeC8=0z6vzDm(9wSd;3)>rpcI$tk@V4 zMe@^-{_FM=1j3VE^Z!yB{&&sbaD8(#EYuU43;k06!qa;ZdXvP3-a14(GxJ?Kbud-6yq-I7+!*yQ1dQGa;ikX|9ohdm*;^3g1{hPbVTL#d)*9L6C;2^B9WIOA|e$b zS6d~V=Z&XizN;L2^PCPrcVA=ZU?!C@acZm|WeB;SKfghNvus9D&-io%Em|@D=_Q)k z0jAeyG@FRo`@VM!vv%5U_(ACFJH?Dn=aOC5b_FA&+3#c0*RR{!?tHu5 zR}<);sAW&uCc)0L+5})oI8-E!EB3+VF##$mk()X2rP*^2mZ;YetSHCa;HOMJSx;S$ z)$y0tuFiJq=$|-y*TX9e@H|_KHL{qhEIaPp=|;|7-D&*fpji^`794)u`|a2JeCLhz zR8174n6kRXVCSJq&iyM@vA-=lg@uKN3l}@$jj!iwH-LyG#Le%Jn$Mqg~^k|?hA2{xibg)Ca3EskS3E&r~Gx_>x@y}v=w{M&91 ztoC5XA%zBs<8M-ZwmntxehoMp$s~45%@@93s!W^Q1`8D9Gfk^G+mmjYo_X0Z4Gq5+ zvTSsoD!2c}h(jFwxW8$iXa*dN7>vtpoaH<@$yqNWEYT>TT{`=`;HL_Q(&0gwLS9_e zRL!gt`Cx80rMR_nZbE()slKpfM%3gnZd5jX*Jng4hRT3$vG-ClHf^U*zMPL=EC;&B zr_U3$l~ubsIWzXFo*YB2Q0AREQtf^rSk#yB(VJL z$VV|lT7IL$+A1N_#@)HkrcHu!3#VHXGkH~>>wU^ru0f;eJbK4j?(JV2y#5TuUP)EA zozMFQqkvJ@fsbMvmfwiA5K9IvzI&9v7A0u3a;uOhBWYUYt^vOLD7G}&fjx!qwf0Nd zI9~J;0jD9mL5H8icUm_@&s*Z8Zyjj@3upQA(QXqRU4kg!x3Xo8 z{hmJ-UG(^P$*~!%U#`DZPcxjzC`GeIztu>=sfL;<=m}1lQD$+G19|ai=XKhvC|jht znAo)|n2?buMAj~b+AMwZnQBlPsN9RWBjMnv-edmuSvy~jRwViGXBn{Jsa0d|h0qz* z9d{-$abYYSKXmW~r}0Jnj$_*$F}=8l<1v|T3Y+5Hq$}KIn#RSK5ze-CJueK84*lFc zXnDw#!hNu0V{2A``P0dhBBf;kkA51AM*DAM3rrt{j|I58cOwaQw4*B(lOp?>LvIY* zJ}2_%72lk0mE>0SJD&Ok`3)qqF6g+S+k;U|j&wK^#Tt4uSfurp#&yfdGw0$%$->fw zV|haPOCg~*@4|?bE%9MktjzW6T&o<`BTrrDd;PX2o$i~^AuYICnPy+TZXd<6yqmzG z#v$acii+&is5Y+qt7=|l(RQT@X3qT+)rwqS;mPDw&t&kr^LOfvA-|8j9GCW7>-Bln zzaoE`X5EwSu}@5-cWnY<%$9o^!#JE7J4DSFM=t+1rVAGM4B02A8{A7R0!7xsi+Jmg@0IKeCgPI*-2{9Bm3* z=?s_h*z@=?{?;YN^`PTEWXk`rSE*mhcX!sz?8U%Rh>$zAlPVM+weV&PR#GKWg_HA} z3~&G0wjEjuI5pL?GnS#|sun{ggG!p*Nvu8c%nFUsiaG+|hx_uwnF3R%l;Q31>nn5+ z_5}ss@M!F-*)VJzU+;q{W#X%iz;nY0r}^4<^Np(UjxglJ=3wSFTEz0kFsj^TUhcNy z;CgesP-41NXZ<&Wd+QhC3qsf9<+7c!sK|B$pz*xfblKEuZmL3u&}<|aM@g)%v>w4! z80_(B_5%3V!;vpZY-`mrcw-0Re&*Wst!vV|Njwk)+c84c`wM`IcD@6V7lyFo)b2a} z9!Sa3v21Xj(u3HtYz(F;YhbhR2%wJt4Cl&9;ffE@t&mX&k__Po(MFQxieSCCIqcC0 zzCK@y65UNaSdR_g-CWFpsX@Pb{nbY=wR<}e&TV!^9FGuC_A&%t#qfU=tJq7if3<4W zDerxSDzeh|jY7wXN=BS5W)nKzUY$AT>eiO)1>YR?cdS4$3I0-}q~YPkcw96w<6Xj5 zpJy(8*#rpiB5^N3K1P!YjQc!H5DdRq64CTl(lY5)eon28`2!#KwzFx23J+m(+1&0% z$WB?nU@9kk8hL&4LjqlT7lWPX^1^w9`})n_ARmo!y3}b4ujR3iu)bj z!tj`!*HZ@R_(JTGzB_xY;D=XKeGiL8Udby(1PFi^U3TYRZTSj#+9whj@V9#y;$ZCul|$ehTM)I%M0QxD?Yw zfBr^(Wy=oMA){nMnYgUv5JsyV{e-kqYNc)z-+M)XcAJ&kyre`80X2?_d_q#c^F+u_ z93n3WvCRuP9arwy-21Ha%}g=L2YgLWBAS;d)W+}iXY_NiUNCM4d(PHRc*E9pA|9(K zH$R>gIG>o*vfgQxrKc$;SEzejoydq8NQ%}@`&}%iA34z$cmadVl*4zB7MqdA{YlIn zvP;5FrjzT``61n}7aCpjtGmY^On~0OJl@#zwu3LWOVndT+S7;#-mUPc@AMc;R-HrZ z3Sj)-Bm93ZY+F`%d^<%bju^Y;w8yXwi$ z-Z4lR=s6>+yStvyiNE?uvSb+vy8qx&gU_$%MeLJ|SFCMx*fYpRtOP5zl??tK#DG45 zLpQCORX6Q0u5`N}61U(8*oXf1+eQkKWyuVPiGFa0N4{Kv?9w;h_%4^@b<@Te?(G>v zR}8J|m5~HxV#xN2QSu{Pf~v^7EKb-#pCs*2J3gL$>%0e-u(L^Pe$+KD zikw}k^fCClrsExvH%0)AqH?PYVTjlw9h$E9F2LfDw@_hMFL0dX*EkgZk&ocOIN=qf{Xi_mc8d1Xxi+&rXJzaf2uA_ao7B4i} zErCl&BQ`GkB_`%}rx6etw-Mg}rC%Z86dohR3Pwqdf@U7r+qFa1SBsj$pKC|UzCB)U z3#487!Cl2i6Xl8aO!}$p(RSF`6Nx_|cuPZNT(JM*%E$kssQqX`<$ckEYhW{|?N42A zG$@(G2UHF-2}2k0Vq4GeUQI>6ZN13nRUR>8iOYa+UXl14W*kL_x!8v4*&xefqkUMFZJ0H{dNW6`-!*p^i+w=03$_r zdoodJ;u8J`oR$GN5`chT01))wB`gT|;6ErfC?mTH1u!j88`>U4p}E=}Wk@YxBL0+J zG17S|l|jE+@FtefehB;pKuh-L&%yUCILcP0*BA27)G~Pdnst9DBRj0omZf-Rwg+OM z2t9K08GHN=HqqLz!J!~ea2$JI965X6#Fs^-_w|>>^g3}YTsXe#(jlPU>xe4S={;OE zF2ZWB8)h5uJjA@We7g`zq>6rw@-MAV@5$FklGoGGQNL(1?7gp@2S$3Di^Qa;MIN$4 zvdI4CrjyvGCF=Z&O+aPDu{;0~II^*+%_V8b#T&N6=%iVASM$utuoDk>vb;5uKgrM8 zu|)7@Z(^RMYs9SlWAVL+Eny)RF~qel!oroGPNg76XuW6=q&$bDW8>mFDN{`=&?Aq2 zL7ppaEvI@dqSD9G`RmdRQw}%Y?Pp;POVw0R(C2+{#0g=rd7J~X)E#1zrVhuDdDih{ zk!?KvMEPFa-;3Z{TD9-DZ_$afcQ^kKSL90S=!ibjt1j=g3ZdDr*gB#=4zhE0GA!9? zDJf#QdBLd<2&BW7FWwkDD*4;b<7wdg~VR| zdAK(PJ6my!U0qTw>)kK+zwI{FO5YFs1|7KU(=>UEYF3i0t*sX#KpDfCFr9jjRii*y zjP1b&o! z3^$b~S-uDU5we?rjf#XdM;j`O>}KtyB;=0Kh9VJis}IQuqsg=&C>FU@vNeJzN`?P5 zSp`D40Z7EPpZW4@lVy76Tyk#zj>>639Y;~*0BTVmPyY1W{ef+N^7lu9PN9;`e7)0o zak9<#IO^@^9rt&EfqE)H_!Mo*Q9#Ikw$rPBrE_)=j)S+G6*Ezy6;DJfI9mZXx3MW` zGx5B;@%KF2h9`ZiZog7vw8b2*GM%klphIc@g-XM~Lm)!g>k$Tw^h4(jz?xX=jZJUb z1I&Ojw*w-mvKccUpiAHzmw*fpoQwqCKQ#+(F#>Bu{^K#<=*lN*Kg0n{E5RL9W)A-C zCKYni{OV_3c)7B;>%6{3oA6;s z5sm&C;DpeAQh!f#eZEK8;&)*1k$St%Zjf!h$z3QlHFdEA`}Jb8PW`L#?(EnMeq-HA z^Oor)CR*B(#Ml!Vsm7?6HMsy=#{jlN*%e@7K#t5>5jv;aQ|fkOIp$M_)fVQAI5H*} zSA2oDZ;2VokI(lO;9r4!764|4&19(#8<2Tt=2>P0pgR@YgD&-#U>A#p?S99+ zI${8nEDN~Fp|uRsxdAfqm!Rkku&)$q-Cw=8QU|i7no3O?$}wj%mQz-xmL2UajFI?0 zjFR4D@^2rLlfJmA8FKbEKpxjv_e_Spd-o3hQ2|ixlbr`kElpQUjSONA2_K%gWKvKL z(P7AFegg%E5`J+!EEL}!d>!ZT;~ySlqb{mXGT{%_qG&D1eq>1)F4!JEu+s(nHeDZp zpQy)R7brRr8>Dj}AOYdQQH0bAGganrEHJ|v21X_*ZF@f;?V+Kq1kjzLYqetjry@mo zsW$6bV8iD50a~b7Gn;oPooBsW8an;;&Bo-jLN`D%7XbFh_B@ghIOYVLt0TEDEQuow zEra11fE^*U|7}O`;uJ^Ax}JR}Oj|sSTg%`K@TKyM+JnBfFd36S+w0V)e6K0Pet~qY7pB#OwKb)hYIc8qKUb z%sQ!G@x#Fg08mRsYlSV5b$$mH@G9fFV&4119@6r~GCgj1qh=WsVg(z1F>UHzHX@Rq z=MkX-At(vF*mowgiVIWJe2J}{n2?~q_d$A#TkOGFS6FX-p?Qn%en8B|N>}9eR@F{r zK5d~MB?(Qck5U*9yd&gR!ON?*g;7R4pp!<(ViA@h6@;llTrBxbIoc5$6ciT1VZR;i z$8uohOdqrV?lqwedeqY zcnil}XaZL0Tv!Zf1WbNgILa#Jcm0DqEvlW=@Xz;n+KMs25Ynstm7lwbgZo%}@1XT0 zF_0O{MNUUYM~&;%%#JL5TVfH~?GFHZ8LZ&P&OucGLiGDhR~i>?e{r)j=j-}LG72)L zm^P5qj3v3*6mcHHI0$RhkMLZ9p9hn0|ZWM{#b5c79Un#n0{Jric63U;&s&P7S#!EEa?ARNnTZsU() zj)^8j-A@7A(AV)f8A+!k#j@Gf?HY!Rt5|EA#~OiX4iI~b9{=X!;s+D`0QexyNfIG9 zhzm(_B#zTso!;t-$*Uta8bPy7i(Mrv;;q{Y^h~t+1<)Qi^pWdSFO}EU*Y$3wMMd!^ zccKiXuBzYfSy@Z4vUaTpBH|;1mv-H4Msr|0qcXZU*NQDUT*h7Qs;;v13u{uqovxE8 zJDHL@!P}+Wh?TIUk0IHJjoQ(U09p`81BzTE{C_r||G&<~3ooy409=UI literal 0 HcmV?d00001 diff --git a/sitemap.xml b/sitemap.xml index ee6de9b1..16d5809d 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,267 +2,242 @@ https://beancount.github.io/docs/index.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/a_comparison_of_beancount_and_ledger_hledger.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/a_proposal_for_an_improvement_on_inventory_booking.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/balance_assertions_in_beancount.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/beancount_cheat_sheet.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/beancount_design_doc.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/beancount_history_and_credits.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/beancount_language_syntax.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/beancount_options_reference.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/beancount_query_language.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/beancount_scripting_plugins.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/beancount_v3.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/beancount_v3_dependencies.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/beangulp.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/calculating_portolio_returns.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/command_line_accounting_cookbook.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/command_line_accounting_in_context.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/exporting_your_portfolio.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/external_contributions.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/fetching_prices_in_beancount.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/fund_accounting_with_beancount.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/getting_started_with_beancount.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/health_care_expenses.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/how_inventories_work.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/how_we_share_expenses.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/importing_external_data.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/installing_beancount.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/installing_beancount_v3.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/ledgerhub_design_doc.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/precision_tolerances.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/rounding_precision_in_beancount.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/running_beancount_and_generating_reports.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/settlement_dates_in_beancount.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/sharing_expenses_with_beancount.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/stock_vesting_in_beancount.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/the_double_entry_counting_method.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/tracking_medical_claims.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/trading_with_beancount.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/tutorial_example.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/api_reference/index.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/api_reference/beancount.core.html - 2024-07-01 - daily - - - https://beancount.github.io/docs/api_reference/beancount.ingest.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/api_reference/beancount.loader.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/api_reference/beancount.ops.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/api_reference/beancount.parser.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/api_reference/beancount.plugins.html - 2024-07-01 - daily - - - https://beancount.github.io/docs/api_reference/beancount.prices.html - 2024-07-01 - daily - - - https://beancount.github.io/docs/api_reference/beancount.query.html - 2024-07-01 - daily - - - https://beancount.github.io/docs/api_reference/beancount.reports.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/api_reference/beancount.scripts.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/api_reference/beancount.tools.html - 2024-07-01 + 2024-07-28 daily https://beancount.github.io/docs/api_reference/beancount.utils.html - 2024-07-01 - daily - - - https://beancount.github.io/docs/api_reference/beancount.web.html - 2024-07-01 + 2024-07-28 daily \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 7a3ebed21e1a60d8ad36316fe3ed5efec65fbc00..84eb9aeb66f5640668e00eab5ca4030e758afb64 100644 GIT binary patch delta 751 zcmV z8hq4vjcz4|CmEs zbuRUJw{qB6G_i5P!fDl~!Ec)K!0q!orUC8tasBx9e*NwK@rk>tx-KYg z6@%QnI5~F-Tti@8ielK=(YvfWQ1Kj+K8fMRagIxZ7KjTEh#`N?z`k;^yBO?f$g#~s zr$(PU7v#A3;fo+t&VM7YdWj zMMsu8i3!bK9Uxw2JyO6^51bJ5&U3lv8N^v#rR@NzM_W5SX|rFSX{|G59^|zfV*S)s z-0UIcB4gRaaMHu*YhTaQqkCU6ma$Lxf%v_fK?0%Z9<{B1Q}Yve9HUV3QBxFab{U%v hpV7~oCsOGXqL<2^llx!n(}~6Q_75wI`~e&z001Fcc&-2d delta 777 zcmV+k1NQuZ2Ac*4ABzYG@OXle2Oxj_7{|Hg3EDG*VJ$I6tg?V*zkL@ecAS~!(nE*U zMKVJ2540cuh0XJ+_lqM?bQqqN53BWZ0l}bkp?zBZ{`GSIefhk-+hp<)(tCnQS;B{-Y z^XE--(=vlKSF(R%)XHE#XmSyQ!o9k7R61DY!L~rEo6pq`Wgr|dYOnT4%&%O}El22q zhr`2j;mB7=XSG8-u!U4Upq+pAdI;q$J$YHh+T^kg) zib3vNoQ%6Tt|724MKSDb=xx?5sCW)ZpTuzEF#9FJ9*7eUh#`N?z_xOkcQM$}kmETI ztr~o8U65nq$1j3VIsOg6@+BAt5~jG#eyAMNXm7IDv+W`!^bR?0q)dO+8!=17ev*SO zuo^K_pE3-?GO9=^q4+GO42A@!+GPoxioq1*GiI8sXJ5>xdh7%gSdN!jCb!E|uB;q- z56N_skCHPWN^$Qq8~KfWk9=DUd<$uUj!9DjD+`_r9M8B$6DW382(kLC+2MFCh&jir z5$PZRdE^qI6Ew}Pzl4A8Yc#Yv>Ds9SyP@|5TZ#+*4Y_{JJuj?ri<||Afxx)3+^Sf@ z0+ki2+MSTN(at&j!Y(MSQx4&ySr;+eLg2}tA`3x@F|~h$$O+!hYq1;wWhtwYa^@I5 zCmmVpBqlTmb%c1SdZd7-4mcp@o#%AV6~tLxrEL$XLt8sOX>)m)X{|G*4)WUdv3_bR zu6jth$XK3YIO%@ywXbLD!M!gT%h)IUK>Xg#Ac0VHHgXP$Lg77XTmO#iC-68#q2z<6 zD0bRq?3jE?Kdr|Jg`alYgtE&hFFvE6Hw>lHCqyrmJtrrYeT3crA)iiM8gKsqZ<$=m H$|e8+iwAz! diff --git a/stock_vesting_in_beancount.html b/stock_vesting_in_beancount.html index 638af07a..7cb1cff6 100644 --- a/stock_vesting_in_beancount.html +++ b/stock_vesting_in_beancount.html @@ -147,6 +147,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -194,8 +196,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -204,20 +204,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/the_double_entry_counting_method.html b/the_double_entry_counting_method.html index 579239c9..664efb00 100644 --- a/the_double_entry_counting_method.html +++ b/the_double_entry_counting_method.html @@ -153,6 +153,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -200,8 +202,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -210,20 +210,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -
  • diff --git a/tracking_medical_claims.html b/tracking_medical_claims.html index 3884040e..aa80ece1 100644 --- a/tracking_medical_claims.html +++ b/tracking_medical_claims.html @@ -9,7 +9,7 @@ - Tracking Out-of-Network Medical Claims in Beancount<a id="title"></a> - Beancount Documentation + Tracking Out-of-Network Medical Claims in Beancount - Beancount Documentation @@ -19,7 +19,7 @@ @@ -50,7 +50,7 @@

    Outline

    - diff --git a/tutorial_example.html b/tutorial_example.html index a548fda4..50df0ce4 100644 --- a/tutorial_example.html +++ b/tutorial_example.html @@ -145,6 +145,8 @@
  • Calculating Portfolio Returns
  • +
  • Tracking Out-of-Network Medical Claims in Beancount +
  • Documentation for Developers @@ -192,8 +194,6 @@
  • beancount.core
  • -
  • beancount.ingest -
  • beancount.loader
  • beancount.ops @@ -202,20 +202,12 @@
  • beancount.plugins
  • -
  • beancount.prices -
  • -
  • beancount.query -
  • -
  • beancount.reports -
  • beancount.scripts
  • beancount.tools
  • beancount.utils
  • -
  • beancount.web -